Add comprehensive test suite for current implementation
Some checks failed
CI / Test (pull_request) Successful in 1m43s
CI / Lint (pull_request) Failing after 27s
CI / Build (pull_request) Failing after 13s
CI / Format Check (pull_request) Successful in 2s

- Add tests for internal/config package (90.9% coverage)
  - Test all viperConfig getter methods
  - Test LoadConfig with default and environment-specific configs
  - Test error handling for missing config files

- Add tests for internal/di package (88.1% coverage)
  - Test Container lifecycle (NewContainer, Start, Stop)
  - Test providers (ProvideConfig, ProvideLogger, CoreModule)
  - Test lifecycle hooks registration
  - Include mock implementations for testing

- Add tests for internal/logger package (96.5% coverage)
  - Test zapLogger with JSON and console formats
  - Test all logging levels and methods
  - Test middleware (RequestIDMiddleware, LoggingMiddleware)
  - Test context helper functions
  - Include benchmark tests

- Update CI workflow to skip tests when no test files exist
  - Add conditional test execution based on test file presence
  - Add timeout for test execution
  - Verify build when no tests are present

All tests follow Go best practices with table-driven patterns,
parallel execution where safe, and comprehensive coverage.
This commit is contained in:
2025-11-05 12:45:08 +01:00
parent a1fc6e69a7
commit 0bfdb2c2d7
12 changed files with 1814 additions and 11 deletions

View File

@@ -3,9 +3,9 @@ package logger
import (
"context"
"git.dcentral.systems/toolz/goplt/pkg/logger"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"git.dcentral.systems/toolz/goplt/pkg/logger"
)
const (

View File

@@ -0,0 +1,362 @@
package logger
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"git.dcentral.systems/toolz/goplt/pkg/logger"
"github.com/gin-gonic/gin"
)
func TestRequestIDMiddleware_GenerateNewID(t *testing.T) {
t.Parallel()
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(RequestIDMiddleware())
router.GET("/test", func(c *gin.Context) {
requestID := RequestIDFromContext(c.Request.Context())
if requestID == "" {
t.Error("Request ID should be generated")
}
c.JSON(http.StatusOK, gin.H{"request_id": requestID})
})
req := httptest.NewRequest(http.MethodGet, "/test", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", w.Code)
}
// Check that request ID is in response header
requestID := w.Header().Get(RequestIDHeader)
if requestID == "" {
t.Error("Request ID should be in response header")
}
}
func TestRequestIDMiddleware_UseExistingID(t *testing.T) {
t.Parallel()
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(RequestIDMiddleware())
router.GET("/test", func(c *gin.Context) {
requestID := RequestIDFromContext(c.Request.Context())
if requestID != "existing-id" {
t.Errorf("Expected request ID 'existing-id', got %q", requestID)
}
c.JSON(http.StatusOK, gin.H{"request_id": requestID})
})
req := httptest.NewRequest(http.MethodGet, "/test", nil)
req.Header.Set(RequestIDHeader, "existing-id")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", w.Code)
}
// Check that the same request ID is in response header
requestID := w.Header().Get(RequestIDHeader)
if requestID != "existing-id" {
t.Errorf("Expected request ID 'existing-id' in header, got %q", requestID)
}
}
func TestRequestIDFromContext(t *testing.T) {
t.Parallel()
tests := []struct {
name string
ctx context.Context
want string
wantEmpty bool
}{
{
name: "with request ID",
ctx: context.WithValue(context.Background(), RequestIDKey(), "test-id"),
want: "test-id",
wantEmpty: false,
},
{
name: "without request ID",
ctx: context.Background(),
want: "",
wantEmpty: true,
},
{
name: "with wrong type",
ctx: context.WithValue(context.Background(), RequestIDKey(), 123),
want: "",
wantEmpty: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := RequestIDFromContext(tt.ctx)
if tt.wantEmpty && got != "" {
t.Errorf("RequestIDFromContext() = %q, want empty string", got)
}
if !tt.wantEmpty && got != tt.want {
t.Errorf("RequestIDFromContext() = %q, want %q", got, tt.want)
}
})
}
}
func TestSetRequestID(t *testing.T) {
t.Parallel()
ctx := context.Background()
newCtx := SetRequestID(ctx, "test-id")
requestID := RequestIDFromContext(newCtx)
if requestID != "test-id" {
t.Errorf("SetRequestID failed, got %q, want %q", requestID, "test-id")
}
// Original context should not have the ID
originalID := RequestIDFromContext(ctx)
if originalID != "" {
t.Error("Original context should not have request ID")
}
}
func TestSetUserID(t *testing.T) {
t.Parallel()
ctx := context.Background()
newCtx := SetUserID(ctx, "user-123")
userID := UserIDFromContext(newCtx)
if userID != "user-123" {
t.Errorf("SetUserID failed, got %q, want %q", userID, "user-123")
}
// Original context should not have the ID
originalID := UserIDFromContext(ctx)
if originalID != "" {
t.Error("Original context should not have user ID")
}
}
func TestUserIDFromContext(t *testing.T) {
t.Parallel()
tests := []struct {
name string
ctx context.Context
want string
wantEmpty bool
}{
{
name: "with user ID",
ctx: context.WithValue(context.Background(), UserIDKey(), "user-123"),
want: "user-123",
wantEmpty: false,
},
{
name: "without user ID",
ctx: context.Background(),
want: "",
wantEmpty: true,
},
{
name: "with wrong type",
ctx: context.WithValue(context.Background(), UserIDKey(), 123),
want: "",
wantEmpty: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := UserIDFromContext(tt.ctx)
if tt.wantEmpty && got != "" {
t.Errorf("UserIDFromContext() = %q, want empty string", got)
}
if !tt.wantEmpty && got != tt.want {
t.Errorf("UserIDFromContext() = %q, want %q", got, tt.want)
}
})
}
}
func TestLoggingMiddleware(t *testing.T) {
t.Parallel()
// Create a mock logger that records log calls
mockLog := &mockLoggerForMiddleware{}
mockLog.logs = make([]logEntry, 0)
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(RequestIDMiddleware())
router.Use(LoggingMiddleware(mockLog))
router.GET("/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "test"})
})
req := httptest.NewRequest(http.MethodGet, "/test", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", w.Code)
}
// Verify that logs were recorded
if len(mockLog.logs) < 2 {
t.Fatalf("Expected at least 2 log entries (request + response), got %d", len(mockLog.logs))
}
// Check request log
requestLog := mockLog.logs[0]
if requestLog.message != "HTTP request" {
t.Errorf("Expected 'HTTP request' log, got %q", requestLog.message)
}
// Check response log
responseLog := mockLog.logs[1]
if responseLog.message != "HTTP response" {
t.Errorf("Expected 'HTTP response' log, got %q", responseLog.message)
}
}
func TestLoggingMiddleware_WithRequestID(t *testing.T) {
t.Parallel()
// Create a mock logger
mockLog := &mockLoggerForMiddleware{}
mockLog.logs = make([]logEntry, 0)
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(RequestIDMiddleware())
router.Use(LoggingMiddleware(mockLog))
router.GET("/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "test"})
})
req := httptest.NewRequest(http.MethodGet, "/test", nil)
req.Header.Set(RequestIDHeader, "custom-request-id")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("Expected status 200, got %d", w.Code)
}
// Verify that request ID is in the logs
if len(mockLog.logs) < 2 {
t.Fatalf("Expected at least 2 log entries, got %d", len(mockLog.logs))
}
// The logger should have received context with request ID
// (We can't easily verify this without exposing internal state, but we can check the logs were made)
}
func TestRequestIDMiddleware_MultipleRequests(t *testing.T) {
t.Parallel()
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(RequestIDMiddleware())
router.GET("/test", func(c *gin.Context) {
requestID := RequestIDFromContext(c.Request.Context())
c.JSON(http.StatusOK, gin.H{"request_id": requestID})
})
// Make multiple requests
requestIDs := make(map[string]bool)
for i := 0; i < 10; i++ {
req := httptest.NewRequest(http.MethodGet, "/test", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
requestID := w.Header().Get(RequestIDHeader)
if requestID == "" {
t.Errorf("Request %d: Request ID should be generated", i)
continue
}
if requestIDs[requestID] {
t.Errorf("Request ID %q was duplicated", requestID)
}
requestIDs[requestID] = true
}
}
func TestSetRequestID_Overwrite(t *testing.T) {
t.Parallel()
ctx := context.WithValue(context.Background(), RequestIDKey(), "old-id")
newCtx := SetRequestID(ctx, "new-id")
requestID := RequestIDFromContext(newCtx)
if requestID != "new-id" {
t.Errorf("SetRequestID failed to overwrite, got %q, want %q", requestID, "new-id")
}
}
func TestSetUserID_Overwrite(t *testing.T) {
t.Parallel()
ctx := context.WithValue(context.Background(), UserIDKey(), "old-user")
newCtx := SetUserID(ctx, "new-user")
userID := UserIDFromContext(newCtx)
if userID != "new-user" {
t.Errorf("SetUserID failed to overwrite, got %q, want %q", userID, "new-user")
}
}
// mockLoggerForMiddleware is a mock logger that records log calls for testing
type mockLoggerForMiddleware struct {
logs []logEntry
}
type logEntry struct {
message string
fields []logger.Field
}
func (m *mockLoggerForMiddleware) Debug(msg string, fields ...logger.Field) {
m.logs = append(m.logs, logEntry{message: msg, fields: fields})
}
func (m *mockLoggerForMiddleware) Info(msg string, fields ...logger.Field) {
m.logs = append(m.logs, logEntry{message: msg, fields: fields})
}
func (m *mockLoggerForMiddleware) Warn(msg string, fields ...logger.Field) {
m.logs = append(m.logs, logEntry{message: msg, fields: fields})
}
func (m *mockLoggerForMiddleware) Error(msg string, fields ...logger.Field) {
m.logs = append(m.logs, logEntry{message: msg, fields: fields})
}
func (m *mockLoggerForMiddleware) With(fields ...logger.Field) logger.Logger {
return m
}
func (m *mockLoggerForMiddleware) WithContext(ctx context.Context) logger.Logger {
return m
}

View File

@@ -3,9 +3,9 @@ package logger
import (
"context"
"git.dcentral.systems/toolz/goplt/pkg/logger"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"git.dcentral.systems/toolz/goplt/pkg/logger"
)
const (

View File

@@ -0,0 +1,368 @@
package logger
import (
"context"
"testing"
"git.dcentral.systems/toolz/goplt/pkg/logger"
"go.uber.org/zap"
)
func TestNewZapLogger_JSONFormat(t *testing.T) {
t.Parallel()
log, err := NewZapLogger("info", "json")
if err != nil {
t.Fatalf("NewZapLogger failed: %v", err)
}
if log == nil {
t.Fatal("NewZapLogger returned nil")
}
// Verify it implements the interface
var _ logger.Logger = log
// Test that it can log
log.Info("test message")
}
func TestNewZapLogger_ConsoleFormat(t *testing.T) {
t.Parallel()
log, err := NewZapLogger("info", "console")
if err != nil {
t.Fatalf("NewZapLogger failed: %v", err)
}
if log == nil {
t.Fatal("NewZapLogger returned nil")
}
// Test that it can log
log.Info("test message")
}
func TestNewZapLogger_InvalidLevel(t *testing.T) {
t.Parallel()
log, err := NewZapLogger("invalid", "json")
if err != nil {
t.Fatalf("NewZapLogger failed: %v", err)
}
if log == nil {
t.Fatal("NewZapLogger returned nil")
}
// Should default to info level
log.Info("test message")
}
func TestNewZapLogger_AllLevels(t *testing.T) {
t.Parallel()
levels := []string{"debug", "info", "warn", "error"}
for _, level := range levels {
t.Run(level, func(t *testing.T) {
t.Parallel()
log, err := NewZapLogger(level, "json")
if err != nil {
t.Fatalf("NewZapLogger(%q) failed: %v", level, err)
}
if log == nil {
t.Fatalf("NewZapLogger(%q) returned nil", level)
}
// Test logging at each level
log.Debug("debug message")
log.Info("info message")
log.Warn("warn message")
log.Error("error message")
})
}
}
func TestZapLogger_With(t *testing.T) {
t.Parallel()
log, err := NewZapLogger("info", "json")
if err != nil {
t.Fatalf("NewZapLogger failed: %v", err)
}
childLog := log.With(
logger.String("key", "value"),
logger.Int("number", 42),
)
if childLog == nil {
t.Fatal("With returned nil")
}
// Verify it's a different logger instance
if childLog == log {
t.Error("With should return a new logger instance")
}
// Test that child logger can log
childLog.Info("test message with fields")
}
func TestZapLogger_WithContext_RequestID(t *testing.T) {
t.Parallel()
log, err := NewZapLogger("info", "json")
if err != nil {
t.Fatalf("NewZapLogger failed: %v", err)
}
ctx := context.WithValue(context.Background(), requestIDKey, "test-request-id")
contextLog := log.WithContext(ctx)
if contextLog == nil {
t.Fatal("WithContext returned nil")
}
// Test that context logger can log
contextLog.Info("test message with request ID")
}
func TestZapLogger_WithContext_UserID(t *testing.T) {
t.Parallel()
log, err := NewZapLogger("info", "json")
if err != nil {
t.Fatalf("NewZapLogger failed: %v", err)
}
ctx := context.WithValue(context.Background(), userIDKey, "test-user-id")
contextLog := log.WithContext(ctx)
if contextLog == nil {
t.Fatal("WithContext returned nil")
}
// Test that context logger can log
contextLog.Info("test message with user ID")
}
func TestZapLogger_WithContext_Both(t *testing.T) {
t.Parallel()
log, err := NewZapLogger("info", "json")
if err != nil {
t.Fatalf("NewZapLogger failed: %v", err)
}
ctx := context.WithValue(context.Background(), requestIDKey, "test-request-id")
ctx = context.WithValue(ctx, userIDKey, "test-user-id")
contextLog := log.WithContext(ctx)
if contextLog == nil {
t.Fatal("WithContext returned nil")
}
// Test that context logger can log
contextLog.Info("test message with both IDs")
}
func TestZapLogger_WithContext_EmptyContext(t *testing.T) {
t.Parallel()
log, err := NewZapLogger("info", "json")
if err != nil {
t.Fatalf("NewZapLogger failed: %v", err)
}
ctx := context.Background()
contextLog := log.WithContext(ctx)
if contextLog == nil {
t.Fatal("WithContext returned nil")
}
// With empty context, should return the same logger
if contextLog != log {
t.Error("WithContext with empty context should return the same logger")
}
}
func TestZapLogger_LoggingMethods(t *testing.T) {
t.Parallel()
log, err := NewZapLogger("debug", "json")
if err != nil {
t.Fatalf("NewZapLogger failed: %v", err)
}
fields := []logger.Field{
logger.String("key", "value"),
logger.Int("number", 42),
logger.Bool("flag", true),
}
// Test all logging methods
log.Debug("debug message", fields...)
log.Info("info message", fields...)
log.Warn("warn message", fields...)
log.Error("error message", fields...)
}
func TestConvertFields(t *testing.T) {
t.Parallel()
tests := []struct {
name string
fields []logger.Field
}{
{
name: "empty fields",
fields: []logger.Field{},
},
{
name: "valid zap fields",
fields: []logger.Field{
zap.String("key", "value"),
zap.Int("number", 42),
},
},
{
name: "mixed fields",
fields: []logger.Field{
logger.String("key", "value"),
zap.Int("number", 42),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
log, err := NewZapLogger("info", "json")
if err != nil {
t.Fatalf("NewZapLogger failed: %v", err)
}
// Test that convertFields works by logging
log.Info("test message", tt.fields...)
})
}
}
func TestRequestIDKey(t *testing.T) {
t.Parallel()
key := RequestIDKey()
if key == "" {
t.Error("RequestIDKey returned empty string")
}
if key != requestIDKey {
t.Errorf("RequestIDKey() = %q, want %q", key, requestIDKey)
}
}
func TestUserIDKey(t *testing.T) {
t.Parallel()
key := UserIDKey()
if key == "" {
t.Error("UserIDKey returned empty string")
}
if key != userIDKey {
t.Errorf("UserIDKey() = %q, want %q", key, userIDKey)
}
}
func TestZapLogger_ChainedWith(t *testing.T) {
t.Parallel()
log, err := NewZapLogger("info", "json")
if err != nil {
t.Fatalf("NewZapLogger failed: %v", err)
}
// Chain multiple With calls
childLog := log.With(
logger.String("parent", "value1"),
).With(
logger.String("child", "value2"),
)
if childLog == nil {
t.Fatal("Chained With returned nil")
}
childLog.Info("test message with chained fields")
}
func TestZapLogger_WithContext_ChainedWith(t *testing.T) {
t.Parallel()
log, err := NewZapLogger("info", "json")
if err != nil {
t.Fatalf("NewZapLogger failed: %v", err)
}
ctx := context.WithValue(context.Background(), requestIDKey, "test-id")
contextLog := log.WithContext(ctx).With(
logger.String("additional", "field"),
)
if contextLog == nil {
t.Fatal("Chained WithContext and With returned nil")
}
contextLog.Info("test message with context and additional fields")
}
// Benchmark tests
func BenchmarkZapLogger_Info(b *testing.B) {
log, err := NewZapLogger("info", "json")
if err != nil {
b.Fatalf("NewZapLogger failed: %v", err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
log.Info("benchmark message")
}
}
func BenchmarkZapLogger_InfoWithFields(b *testing.B) {
log, err := NewZapLogger("info", "json")
if err != nil {
b.Fatalf("NewZapLogger failed: %v", err)
}
fields := []logger.Field{
logger.String("key1", "value1"),
logger.Int("key2", 42),
logger.Bool("key3", true),
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
log.Info("benchmark message", fields...)
}
}
func BenchmarkZapLogger_WithContext(b *testing.B) {
log, err := NewZapLogger("info", "json")
if err != nil {
b.Fatalf("NewZapLogger failed: %v", err)
}
ctx := context.WithValue(context.Background(), requestIDKey, "test-id")
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = log.WithContext(ctx)
}
}