test: add comprehensive tests for all Epic 1 stories
Story 1.2: Database Layer - Test database client creation, connection, ping, and close - Test connection pooling configuration - Tests skip if database is not available (short mode) Story 1.3: Health Monitoring and Metrics - Test health registry registration and checking - Test database health checker - Test liveness and readiness checks - Test metrics creation, middleware, and handler - Test Prometheus metrics endpoint Story 1.4: Error Handling and Error Bus - Test channel-based error bus creation - Test error publishing with context - Test nil error handling - Test channel full scenario - Test graceful shutdown - Fix Close() method to handle multiple calls safely Story 1.5: HTTP Server and Middleware - Test server creation with all middleware - Test request ID middleware - Test logging middleware - Test panic recovery middleware - Test CORS middleware - Test timeout middleware - Test health and metrics endpoints - Test server shutdown Story 1.6: OpenTelemetry Tracing - Test tracer initialization (enabled/disabled) - Test development and production modes - Test OTLP exporter configuration - Test graceful shutdown - Test no-op tracer provider All tests follow Go testing best practices: - Table-driven tests where appropriate - Parallel execution - Proper mocking of interfaces - Skip tests requiring external dependencies in short mode
This commit is contained in:
@@ -12,11 +12,12 @@ import (
|
||||
|
||||
// ChannelBus implements a channel-based error bus.
|
||||
type ChannelBus struct {
|
||||
errors chan errorWithContext
|
||||
logger logger.Logger
|
||||
done chan struct{}
|
||||
wg sync.WaitGroup
|
||||
once sync.Once
|
||||
errors chan errorWithContext
|
||||
logger logger.Logger
|
||||
done chan struct{}
|
||||
wg sync.WaitGroup
|
||||
once sync.Once
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
type errorWithContext struct {
|
||||
@@ -157,7 +158,9 @@ func (b *ChannelBus) Close() error {
|
||||
close(b.done)
|
||||
})
|
||||
b.wg.Wait()
|
||||
close(b.errors)
|
||||
b.closeOnce.Do(func() {
|
||||
close(b.errors)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
199
internal/errorbus/channel_bus_test.go
Normal file
199
internal/errorbus/channel_bus_test.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package errorbus
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.dcentral.systems/toolz/goplt/pkg/logger"
|
||||
)
|
||||
|
||||
func TestNewChannelBus(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mockLogger := &mockLogger{}
|
||||
bus := NewChannelBus(mockLogger, 100)
|
||||
|
||||
if bus == nil {
|
||||
t.Fatal("Expected bus, got nil")
|
||||
}
|
||||
|
||||
if bus.errors == nil {
|
||||
t.Error("Expected errors channel, got nil")
|
||||
}
|
||||
|
||||
if bus.logger == nil {
|
||||
t.Error("Expected logger, got nil")
|
||||
}
|
||||
|
||||
// Clean up
|
||||
_ = bus.Close()
|
||||
}
|
||||
|
||||
func TestNewChannelBus_DefaultBufferSize(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mockLogger := &mockLogger{}
|
||||
bus := NewChannelBus(mockLogger, 0)
|
||||
|
||||
if bus == nil {
|
||||
t.Fatal("Expected bus, got nil")
|
||||
}
|
||||
|
||||
// Clean up
|
||||
_ = bus.Close()
|
||||
}
|
||||
|
||||
func TestChannelBus_Publish(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mockLogger := &mockLogger{}
|
||||
bus := NewChannelBus(mockLogger, 10)
|
||||
|
||||
testErr := errors.New("test error")
|
||||
ctx := context.Background()
|
||||
|
||||
// Publish error
|
||||
bus.Publish(ctx, testErr)
|
||||
|
||||
// Wait a bit for the error to be processed
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Verify error was logged
|
||||
if len(mockLogger.errors) == 0 {
|
||||
t.Error("Expected error to be logged")
|
||||
}
|
||||
|
||||
// Clean up
|
||||
_ = bus.Close()
|
||||
}
|
||||
|
||||
func TestChannelBus_Publish_NilError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mockLogger := &mockLogger{}
|
||||
bus := NewChannelBus(mockLogger, 10)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Publish nil error (should be ignored)
|
||||
bus.Publish(ctx, nil)
|
||||
|
||||
// Wait a bit
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
// Verify nil error was not logged
|
||||
if len(mockLogger.errors) > 0 {
|
||||
t.Error("Expected nil error to be ignored")
|
||||
}
|
||||
|
||||
// Clean up
|
||||
_ = bus.Close()
|
||||
}
|
||||
|
||||
func TestChannelBus_Publish_WithContext(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mockLogger := &mockLogger{}
|
||||
bus := NewChannelBus(mockLogger, 10)
|
||||
|
||||
testErr := errors.New("test error")
|
||||
ctx := context.WithValue(context.Background(), "request_id", "test-request-id")
|
||||
|
||||
bus.Publish(ctx, testErr)
|
||||
|
||||
// Wait for processing
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Verify error was logged with context
|
||||
if len(mockLogger.errors) == 0 {
|
||||
t.Error("Expected error to be logged")
|
||||
}
|
||||
|
||||
// Clean up
|
||||
_ = bus.Close()
|
||||
}
|
||||
|
||||
func TestChannelBus_Close(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mockLogger := &mockLogger{}
|
||||
bus := NewChannelBus(mockLogger, 10)
|
||||
|
||||
// Publish some errors
|
||||
for i := 0; i < 5; i++ {
|
||||
bus.Publish(context.Background(), errors.New("test error"))
|
||||
}
|
||||
|
||||
// Close and wait
|
||||
if err := bus.Close(); err != nil {
|
||||
t.Errorf("Close failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify channel is closed
|
||||
select {
|
||||
case <-bus.errors:
|
||||
// Channel is closed, this is expected
|
||||
default:
|
||||
t.Error("Expected errors channel to be closed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelBus_Close_MultipleTimes(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mockLogger := &mockLogger{}
|
||||
bus := NewChannelBus(mockLogger, 10)
|
||||
|
||||
// Close first time
|
||||
if err := bus.Close(); err != nil {
|
||||
t.Errorf("First Close failed: %v", err)
|
||||
}
|
||||
|
||||
// Close second time should be safe (uses sync.Once)
|
||||
// The channel is already closed, but Close() should handle this gracefully
|
||||
if err := bus.Close(); err != nil {
|
||||
t.Errorf("Second Close failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelBus_ChannelFull(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
mockLogger := &mockLogger{}
|
||||
// Use small buffer to test channel full scenario
|
||||
bus := NewChannelBus(mockLogger, 1)
|
||||
|
||||
// Fill the channel
|
||||
bus.Publish(context.Background(), errors.New("error1"))
|
||||
|
||||
// This should not block (channel is full, should log directly)
|
||||
bus.Publish(context.Background(), errors.New("error2"))
|
||||
|
||||
// Wait a bit
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
// Clean up
|
||||
_ = bus.Close()
|
||||
}
|
||||
|
||||
// mockLogger implements logger.Logger for testing.
|
||||
type mockLogger struct {
|
||||
errors []string
|
||||
}
|
||||
|
||||
func (m *mockLogger) Debug(msg string, fields ...logger.Field) {}
|
||||
func (m *mockLogger) Info(msg string, fields ...logger.Field) {}
|
||||
func (m *mockLogger) Warn(msg string, fields ...logger.Field) {}
|
||||
func (m *mockLogger) Error(msg string, fields ...logger.Field) {
|
||||
m.errors = append(m.errors, msg)
|
||||
}
|
||||
|
||||
func (m *mockLogger) With(fields ...logger.Field) logger.Logger {
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *mockLogger) WithContext(ctx context.Context) logger.Logger {
|
||||
return m
|
||||
}
|
||||
Reference in New Issue
Block a user