diff --git a/pkg/log/log.go b/pkg/log/log.go index d382199..dcf8d21 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -64,7 +64,7 @@ var ( ErrMissingFilePath = errors.New("file path missing in destination config for file logging") ) -func (config *Config) validate() error { +func (config *Config) Validate() error { if _, exists := logLevels[config.level]; !exists { return ErrInvalidLogLevel } @@ -171,7 +171,7 @@ func getLogger(config Config) (zerolog.Logger, error) { func InitLogger(c Config) error { var initErr error once.Do(func() { - if err := c.validate(); err != nil { + if err := c.Validate(); err != nil { return } diff --git a/pkg/log/log_tes1.go b/pkg/log/log_tes1.go deleted file mode 100644 index 078bbaf..0000000 --- a/pkg/log/log_tes1.go +++ /dev/null @@ -1,301 +0,0 @@ -package log - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "net/http" - "os" - "strings" - "testing" - "time" - - "github.com/rs/zerolog" -) - -func TestLogFunctions(t *testing.T) { - testConfig := Config{ - level: DebugLevel, - destinations: []Destination{ - {Type: Stdout}, - }, - contextKeys: []any{"userID", "requestID"}, - } - err := InitLogger(testConfig) - if err != nil { - t.Fatalf("Failed to initialize logger: %v", err) - } - - tests := []struct { - name string - logFunc func(ctx context.Context) - expectedOutput string - }{ - { - name: "Debug log with context", - logFunc: func(ctx context.Context) { - type ctxKey any - var requestID ctxKey = "requestID" - - ctx = context.WithValue(ctx, requestID, "12345") - Debug(ctx, "debug message") - }, - expectedOutput: `{"level":"debug","requestID":"12345","message":"debug message"}`, - }, - { - name: "Debugf with context", - logFunc: func(ctx context.Context) { - type ctxKey any - var requestID ctxKey = "requestID" - - ctx = context.WithValue(ctx, requestID, "12345") - Debugf(ctx, "formatted %s", "debug message") - }, - expectedOutput: `{"level":"debug","requestID":"12345","message":"formatted debug message"}`, - }, - { - name: "Info log with message", - logFunc: func(ctx context.Context) { - type ctxKey any - var requestID ctxKey = "requestID" - - ctx = context.WithValue(ctx, requestID, "12345") - Info(ctx, "info message") - }, - expectedOutput: `{"level":"info","requestID":"12345","message":"info message"}`, - }, - - { - name: "Info log with formatted message", - logFunc: func(ctx context.Context) { - Infof(ctx, "formatted %s", "info message") - }, - expectedOutput: `{"level":"info","message":"formatted info message"}`, - }, - { - name: "Warn log with context", - logFunc: func(ctx context.Context) { - type ctxKey any - var requestID ctxKey = "requestID" - - ctx = context.WithValue(ctx, requestID, "12345") - Warn(ctx, "warning message") - }, - expectedOutput: `{"level":"warn","requestID":"12345","message":"warning message"}`, - }, - { - name: "Warnf with context", - logFunc: func(ctx context.Context) { - type ctxKey any - var requestID ctxKey = "requestID" - - ctx = context.WithValue(ctx, requestID, "12345") - Warnf(ctx, "formatted %s", "warning message") - }, - expectedOutput: `{"level":"warn","requestID":"12345","message":"formatted warning message"}`, - }, - { - name: "Error log with error and context", - logFunc: func(ctx context.Context) { - type ctxKey any - var userID ctxKey = "userID" - - ctx = context.WithValue(ctx, userID, "67890") - Error(ctx, errors.New("something went wrong"), "error message") - }, - expectedOutput: `{"level":"error","userID":"67890","error":"something went wrong","message":"error message"}`, - }, - { - name: "Errorf with error and context", - logFunc: func(ctx context.Context) { - type ctxKey any - var userID ctxKey = "userID" - - ctx = context.WithValue(ctx, userID, "67890") - Errorf(ctx, errors.New("something went wrong"), "formatted %s", "error message") - }, - expectedOutput: `{"level":"error","userID":"67890","error":"something went wrong","message":"formatted error message"}`, - }, - { - name: "Fatal log with error and context", - logFunc: func(ctx context.Context) { - type ctxKey any - var requestID ctxKey = "requestID" - - ctx = context.WithValue(ctx, requestID, "12345") - Fatal(ctx, errors.New("fatal error"), "fatal message") - }, - expectedOutput: `{"level":"fatal","requestID":"12345","error":"fatal error","message":"fatal message"}`, - }, - { - name: "Fatalf with error and context", - logFunc: func(ctx context.Context) { - type ctxKey any - var requestID ctxKey = "requestID" - - ctx = context.WithValue(ctx, requestID, "12345") - Fatalf(ctx, errors.New("fatal error"), "formatted %s", "fatal message") - }, - expectedOutput: `{"level":"fatal","requestID":"12345","error":"fatal error","message":"formatted fatal message"}`, - }, - { - name: "Panic log with error and context", - logFunc: func(ctx context.Context) { - type ctxKey any - var userID ctxKey = "userID" - - ctx = context.WithValue(ctx, userID, "67890") - Panic(ctx, errors.New("panic error"), "panic message") - }, - expectedOutput: `{"level":"panic","userID":"67890","error":"panic error","message":"panic message"}`, - }, - { - name: "Panicf with error and context", - logFunc: func(ctx context.Context) { - type ctxKey any - var userID ctxKey = "userID" - - ctx = context.WithValue(ctx, userID, "67890") - Panicf(ctx, errors.New("panic error"), "formatted %s", "panic message") - }, - expectedOutput: `{"level":"panic","userID":"67890","error":"panic error","message":"formatted panic message"}`, - }, - { - name: "Request log", - logFunc: func(ctx context.Context) { - req, _ := http.NewRequest("GET", "http://example.com", nil) - req.RemoteAddr = "127.0.0.1:8080" - Request(ctx, req, []byte("request body")) - }, - expectedOutput: `{"level":"info","method":"GET","url":"http://example.com","body":"request body","remoteAddr":"127.0.0.1:8080","message":"HTTP Request"}`, - }, - { - name: "Response log", - logFunc: func(ctx context.Context) { - req, _ := http.NewRequest("GET", "http://example.com", nil) - Response(ctx, req, 200, 100*time.Millisecond) - }, - expectedOutput: `{"level":"info","method":"GET","url":"http://example.com","statusCode":200,"responseTime":100,"message":"HTTP Response"}`, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var buf bytes.Buffer - logger = zerolog.New(&buf).With().Timestamp().Logger() - tt.logFunc(context.Background()) - output := buf.String() - t.Logf("Log output: %s", output) - lines := strings.Split(strings.TrimSpace(output), "\n") - if len(lines) == 0 { - t.Fatal("No log output found") - } - lastLine := lines[len(lines)-1] - var logOutput map[string]interface{} - if err := json.Unmarshal([]byte(lastLine), &logOutput); err != nil { - t.Fatalf("Failed to unmarshal log output: %v", err) - } - delete(logOutput, "time") - delete(logOutput, "caller") - var expectedOutput map[string]interface{} - if err := json.Unmarshal([]byte(tt.expectedOutput), &expectedOutput); err != nil { - t.Fatalf("Failed to unmarshal expected output: %v", err) - } - for key, expectedValue := range expectedOutput { - actualValue, ok := logOutput[key] - if !ok { - t.Errorf("Expected key %q not found in log output", key) - continue - } - if actualValue != expectedValue { - t.Errorf("Mismatch for key %q: expected %v, got %v", key, expectedValue, actualValue) - } - } - }) - } -} -func TestDestinationValidation(t *testing.T) { - tests := []struct { - name string - config Config - expectedError error - }{ - // Missing `path` for File destination - { - name: "Missing file path", - config: Config{ - level: InfoLevel, - destinations: []Destination{ - { - Type: File, - Config: map[string]string{ - "maxSize": "500", - "maxBackups": "15", - "maxAge": "30", - }, - }, - }, - }, - expectedError: ErrMissingFilePath, - }, - { - name: "Invalid maxAge", - config: Config{ - level: InfoLevel, - destinations: []Destination{ - { - Type: File, - Config: map[string]string{ - "path": "log/app.txt", - "maxSize": "500", - "maxBackups": "15", - "maxAge": "invalid", - }, - }, - }, - }, - expectedError: errors.New("invalid maxAge"), - }, - { - name: "Valid file destination", - config: Config{ - level: InfoLevel, - destinations: []Destination{ - { - Type: File, - Config: map[string]string{ - "path": "log/app.txt", - "maxSize": "500", - "maxBackups": "15", - "maxAge": "30", - }, - }, - }, - }, - expectedError: nil, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - fmt.Print(tt.config) - err := InitLogger(tt.config) - if (err == nil && tt.expectedError != nil) || (err != nil && tt.expectedError == nil) { - t.Errorf("Expected error: %v, got: %v", tt.expectedError, err) - } else if err != nil && tt.expectedError != nil && !strings.Contains(err.Error(), tt.expectedError.Error()) { - t.Errorf("Expected error to contain: %v, got: %v", tt.expectedError, err) - } - - // ✅ Send a test log to verify file write - logger.Info().Msg("This is a test log message") - - // ✅ Optional: Check if the file was created - filePath := tt.config.destinations[0].Config["path"] - if _, err := os.Stat(filePath); os.IsNotExist(err) { - t.Errorf("Log file was not created at %s", filePath) - } else { - fmt.Printf("Log file created at: %s\n", filePath) - } - }) - } -} diff --git a/pkg/log/log_test.go b/pkg/log/log_test.go index 9a90b29..417f46f 100644 --- a/pkg/log/log_test.go +++ b/pkg/log/log_test.go @@ -3,20 +3,16 @@ package log import ( "bufio" "bytes" - "fmt" - "net/http" - "time" - - // "bytes" - // "fmt" - // "net/http" - // "time" "context" "encoding/json" + "errors" + "fmt" + "net/http" "os" "path/filepath" "strings" "testing" + "time" ) const testLogFilePath = "./test_logs/test.log" @@ -49,12 +45,10 @@ func setupLogger(t *testing.T, l Level) string { }, contextKeys: []any{"userID", "requestID"}, } - err = InitLogger(config) if err != nil { t.Fatalf("failed to initialize logger: %v", err) } - return testLogFilePath } @@ -64,13 +58,11 @@ func readLogFile(t *testing.T, logPath string) []string { t.Fatalf("failed to open log file: %v", err) } defer file.Close() - var lines []string scanner := bufio.NewScanner(file) for scanner.Scan() { lines = append(lines, scanner.Text()) } - if err := scanner.Err(); err != nil { t.Fatalf("failed to read log file: %v", err) } @@ -92,7 +84,6 @@ func TestDebug(t *testing.T) { ctx := context.WithValue(context.Background(), userID, "12345") Debug(ctx, "Debug message") lines := readLogFile(t, logPath) - if len(lines) == 0 { t.Fatal("No logs were written.") } @@ -114,7 +105,6 @@ func TestInfo(t *testing.T) { ctx := context.WithValue(context.Background(), userID, "12345") Info(ctx, "Info message") lines := readLogFile(t, logPath) - if len(lines) == 0 { t.Fatal("No logs were written.") } @@ -137,7 +127,6 @@ func TestWarn(t *testing.T) { ctx := context.WithValue(context.Background(), userID, "12345") Warn(ctx, "Warning message") lines := readLogFile(t, logPath) - if len(lines) == 0 { t.Fatal("No logs were written.") } @@ -149,7 +138,6 @@ func TestWarn(t *testing.T) { break } } - if !found { t.Errorf("expected Warning message, but it was not found in logs") } @@ -160,7 +148,6 @@ func TestError(t *testing.T) { ctx := context.WithValue(context.Background(), userID, "12345") Error(ctx, fmt.Errorf("test error"), "Error message") lines := readLogFile(t, logPath) - if len(lines) == 0 { t.Fatal("No logs were written.") } @@ -172,7 +159,6 @@ func TestError(t *testing.T) { break } } - if !found { t.Errorf("expected Error message, but it was not found in logs") } @@ -185,7 +171,6 @@ func TestRequest(t *testing.T) { req.RemoteAddr = "127.0.0.1:8080" Request(ctx, req, []byte(`{"key":"value"}`)) lines := readLogFile(t, logPath) - if len(lines) == 0 { t.Fatal("No logs were written.") } @@ -200,7 +185,6 @@ func TestRequest(t *testing.T) { if !found { t.Errorf("expected formatted debug message, but it was not found in logs") } - } func TestResponse(t *testing.T) { @@ -209,7 +193,6 @@ func TestResponse(t *testing.T) { req, _ := http.NewRequest("GET", "/api/test", nil) Response(ctx, req, 200, time.Millisecond*123) lines := readLogFile(t, logPath) - if len(lines) == 0 { t.Fatal("No logs were written.") } @@ -217,18 +200,15 @@ func TestResponse(t *testing.T) { for _, line := range lines { logEntry := parseLogLine(t, line) if logEntry["message"] == "HTTP Response" { - if logEntry["message"] == "HTTP Response" { value, ok := logEntry["statusCode"] if !ok { t.Fatalf("Expected key 'statusCode' not found in log entry") } - statusCode, ok := value.(float64) if !ok { t.Fatalf("Value for 'statusCode' is not a float64, found: %T", value) } - if statusCode == 200 { found = true break @@ -239,7 +219,6 @@ func TestResponse(t *testing.T) { if !found { t.Errorf("expected message, but it was not found in logs") } - } func TestFatal(t *testing.T) { @@ -247,7 +226,6 @@ func TestFatal(t *testing.T) { ctx := context.WithValue(context.Background(), userID, "12345") Fatal(ctx, fmt.Errorf("fatal error"), "Fatal message") lines := readLogFile(t, logPath) - if len(lines) == 0 { t.Fatal("No logs were written.") } @@ -259,7 +237,6 @@ func TestFatal(t *testing.T) { break } } - if !found { t.Errorf("expected Fatal message, but it was not found in logs") } @@ -270,7 +247,6 @@ func TestPanic(t *testing.T) { ctx := context.WithValue(context.Background(), userID, "12345") Panic(ctx, fmt.Errorf("panic error"), "Panic message") lines := readLogFile(t, logPath) - if len(lines) == 0 { t.Fatal("No logs were written.") } @@ -282,7 +258,6 @@ func TestPanic(t *testing.T) { break } } - if !found { t.Errorf("expected Panic message, but it was not found in logs") } @@ -293,7 +268,6 @@ func TestDebugf(t *testing.T) { ctx := context.WithValue(context.Background(), userID, "12345") Debugf(ctx, "Debugf message: %s", "test") lines := readLogFile(t, logPath) - if len(lines) == 0 { t.Fatal("No logs were written.") } @@ -305,7 +279,6 @@ func TestDebugf(t *testing.T) { break } } - if !found { t.Errorf("expected formatted debug message, but it was not found in logs") } @@ -316,7 +289,6 @@ func TestInfof(t *testing.T) { ctx := context.WithValue(context.Background(), userID, "12345") Infof(ctx, "Infof message: %s", "test") lines := readLogFile(t, logPath) - if len(lines) == 0 { t.Fatal("No logs were written.") } @@ -328,7 +300,6 @@ func TestInfof(t *testing.T) { break } } - if !found { t.Errorf("expected Infof message, but it was not found in logs") } @@ -339,7 +310,6 @@ func TestWarnf(t *testing.T) { ctx := context.WithValue(context.Background(), userID, "12345") Warnf(ctx, "Warnf message: %s", "test") lines := readLogFile(t, logPath) - if len(lines) == 0 { t.Fatal("No logs were written.") } @@ -351,7 +321,6 @@ func TestWarnf(t *testing.T) { break } } - if !found { t.Errorf("expected Warnf message, but it was not found in logs") } @@ -363,7 +332,6 @@ func TestErrorf(t *testing.T) { err := fmt.Errorf("error message") Errorf(ctx, err, "Errorf message: %s", "test") lines := readLogFile(t, logPath) - if len(lines) == 0 { t.Fatal("No logs were written.") } @@ -375,7 +343,6 @@ func TestErrorf(t *testing.T) { break } } - if !found { t.Errorf("expected Errorf message, but it was not found in logs") } @@ -387,7 +354,6 @@ func TestFatalf(t *testing.T) { err := fmt.Errorf("fatal error") Fatalf(ctx, err, "Fatalf message: %s", "test") lines := readLogFile(t, logPath) - if len(lines) == 0 { t.Fatal("No logs were written.") } @@ -399,7 +365,6 @@ func TestFatalf(t *testing.T) { break } } - if !found { t.Errorf("expected Fatalf message, but it was not found in logs") } @@ -428,3 +393,143 @@ func TestPanicf(t *testing.T) { t.Errorf("expected Panicf message, but it was not found in logs") } } + +func TestValidateConfig(t *testing.T) { + tests := []struct { + name string + config Config + wantErr error + }{ + { + name: "Valid config with Stdout", + config: Config{ + level: InfoLevel, + destinations: []Destination{ + {Type: Stdout}, + }, + }, + wantErr: nil, + }, + { + name: "Valid config with File destination and valid path", + config: Config{ + level: InfoLevel, + destinations: []Destination{ + { + Type: File, + Config: map[string]string{ + "path": "./logs/app.log", + "maxSize": "10", + "maxBackups": "5", + "maxAge": "7", + }, + }, + }, + }, + wantErr: nil, + }, + { + name: "Error: Invalid log level", + config: Config{ + level: "invalid", + destinations: []Destination{ + {Type: Stdout}, + }, + }, + wantErr: ErrInvalidLogLevel, + }, + { + name: "Error: No destinations provided", + config: Config{ + level: InfoLevel, + destinations: []Destination{}, + }, + wantErr: ErrLogDestinationNil, + }, + { + name: "Error: Invalid destination type", + config: Config{ + level: InfoLevel, + destinations: []Destination{ + {Type: "unknown"}, + }, + }, + wantErr: fmt.Errorf("invalid destination type 'unknown'"), + }, + { + name: "Error: Missing file path for file destination", + config: Config{ + level: InfoLevel, + destinations: []Destination{ + { + Type: File, + Config: map[string]string{ + "maxSize": "10", + }, + }, + }, + }, + wantErr: ErrMissingFilePath, + }, + { + name: "Error: Invalid maxSize value in file destination", + config: Config{ + level: InfoLevel, + destinations: []Destination{ + { + Type: File, + Config: map[string]string{ + "path": "./logs/app.log", + "maxSize": "invalid", + }, + }, + }, + }, + wantErr: errors.New(`invalid maxSize: strconv.Atoi: parsing "invalid": invalid syntax`), + }, + { + name: "Error: Invalid maxBackups value in file destination", + config: Config{ + level: InfoLevel, + destinations: []Destination{ + { + Type: File, + Config: map[string]string{ + "path": "./logs/app.log", + "maxBackups": "invalid", + }, + }, + }, + }, + wantErr: errors.New(`invalid maxBackups: strconv.Atoi: parsing "invalid": invalid syntax`), + }, + { + name: "Error: Invalid maxAge value in file destination", + config: Config{ + level: InfoLevel, + destinations: []Destination{ + { + Type: File, + Config: map[string]string{ + "path": "./logs/app.log", + "maxAge": "invalid", + }, + }, + }, + }, + wantErr: errors.New(`invalid maxAge: strconv.Atoi: parsing "invalid": invalid syntax`), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.config.Validate() + if (err == nil) != (tt.wantErr == nil) { + t.Errorf("validate() error = %v, wantErr %v", err, tt.wantErr) + } + if err != nil && tt.wantErr != nil && err.Error() != tt.wantErr.Error() { + t.Errorf("validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +}