diff --git a/adapters/noop_logger_adapter.go b/adapters/noop_logger_adapter.go index cb51e55..bdcb296 100644 --- a/adapters/noop_logger_adapter.go +++ b/adapters/noop_logger_adapter.go @@ -8,7 +8,22 @@ func NewNoOpLoggerAdapter() *NoOpLoggerAdapter { return &NoOpLoggerAdapter{} } -func (n *NoOpLoggerAdapter) Debug(message string, args ...any) {} -func (n *NoOpLoggerAdapter) Info(message string, args ...any) {} -func (n *NoOpLoggerAdapter) Warn(message string, args ...any) {} -func (n *NoOpLoggerAdapter) Error(message string, args ...any) {} +func (n *NoOpLoggerAdapter) Debug(message string, args ...any) { + // Intentionally empty - no-op implementation + _ = message +} + +func (n *NoOpLoggerAdapter) Info(message string, args ...any) { + // Intentionally empty - no-op implementation + _ = message +} + +func (n *NoOpLoggerAdapter) Warn(message string, args ...any) { + // Intentionally empty - no-op implementation + _ = message +} + +func (n *NoOpLoggerAdapter) Error(message string, args ...any) { + // Intentionally empty - no-op implementation + _ = message +} diff --git a/adapters/noop_logger_adapter_test.go b/adapters/noop_logger_adapter_test.go new file mode 100644 index 0000000..64c0219 --- /dev/null +++ b/adapters/noop_logger_adapter_test.go @@ -0,0 +1,70 @@ +package adapters + +import ( + "testing" +) + +func TestNoOpLoggerAdapter(t *testing.T) { + logger := NewNoOpLoggerAdapter() + + // Test all methods - they should not panic and do nothing + logger.Debug("debug message", "arg1", "arg2") + logger.Info("info message", "arg1", "arg2") + logger.Warn("warn message", "arg1", "arg2") + logger.Error("error message", "arg1", "arg2") + + // If we reach here without panic, the test passes +} + +func TestNoOpLoggerAdapter_AllMethods(t *testing.T) { + logger := NewNoOpLoggerAdapter() + + // Call each method individually to ensure coverage + t.Run("Debug", func(t *testing.T) { + logger.Debug("test") + }) + + t.Run("Info", func(t *testing.T) { + logger.Info("test") + }) + + t.Run("Warn", func(t *testing.T) { + logger.Warn("test") + }) + + t.Run("Error", func(t *testing.T) { + logger.Error("test") + }) +} + +func TestNoOpLoggerAdapter_WithVariousArgs(t *testing.T) { + logger := NewNoOpLoggerAdapter() + + // Test with no arguments + logger.Debug("message") + logger.Info("message") + logger.Warn("message") + logger.Error("message") + + // Test with multiple arguments + logger.Debug("message %s %d", "test", 123) + logger.Info("message %s %d", "test", 123) + logger.Warn("message %s %d", "test", 123) + logger.Error("message %s %d", "test", 123) + + // Test with nil arguments + logger.Debug("message", nil) + logger.Info("message", nil) + logger.Warn("message", nil) + logger.Error("message", nil) +} + +func TestNoOpLoggerAdapter_Constructor(t *testing.T) { + logger := NewNoOpLoggerAdapter() + if logger == nil { + t.Fatal("expected non-nil logger") + } + + // Verify it implements LoggerAdapter interface + var _ LoggerAdapter = logger +} diff --git a/adapters/print_logger_adapter_test.go b/adapters/print_logger_adapter_test.go new file mode 100644 index 0000000..4f54ac7 --- /dev/null +++ b/adapters/print_logger_adapter_test.go @@ -0,0 +1,59 @@ +package adapters + +import ( + "testing" +) + +func TestPrintLoggerAdapter(t *testing.T) { + t.Run("should create logger with debug level", func(t *testing.T) { + logger := NewPrintLoggerAdapter(LogLevelDebug) + if logger.level != LogLevelDebug { + t.Errorf("expected debug level, got %s", logger.level) + } + }) + + t.Run("should log debug messages when level is debug", func(t *testing.T) { + logger := NewPrintLoggerAdapter(LogLevelDebug) + logger.Debug("debug message %s", "test") + // If we reach here without panic, the test passes + }) + + t.Run("should log info messages when level is debug", func(t *testing.T) { + logger := NewPrintLoggerAdapter(LogLevelDebug) + logger.Info("info message %s", "test") + // If we reach here without panic, the test passes + }) + + t.Run("should log warn messages when level is debug", func(t *testing.T) { + logger := NewPrintLoggerAdapter(LogLevelDebug) + logger.Warn("warn message %s", "test") + // If we reach here without panic, the test passes + }) + + t.Run("should log error messages when level is debug", func(t *testing.T) { + logger := NewPrintLoggerAdapter(LogLevelDebug) + logger.Error("error message %s", "test") + // If we reach here without panic, the test passes + }) + + t.Run("should respect log levels", func(t *testing.T) { + // Test that higher level loggers don't log lower level messages + logger := NewPrintLoggerAdapter(LogLevelError) + + // These should not cause any issues (they just won't output) + logger.Debug("debug message") + logger.Info("info message") + logger.Warn("warn message") + logger.Error("error message") + }) + + t.Run("should handle none level", func(t *testing.T) { + logger := NewPrintLoggerAdapter(LogLevelNone) + + // None of these should output anything + logger.Debug("debug message") + logger.Info("info message") + logger.Warn("warn message") + logger.Error("error message") + }) +} diff --git a/ripple_client_test.go b/ripple_client_test.go index 139ecb0..032f489 100644 --- a/ripple_client_test.go +++ b/ripple_client_test.go @@ -1,8 +1,12 @@ package ripple import ( + "errors" + "os" "testing" "time" + + "github.com/Tap30/ripple-go/adapters" ) func stringPtr(s string) *string { @@ -245,6 +249,42 @@ func TestClient_SetGetMetadata(t *testing.T) { } } +func TestClient_MetadataManager_IsEmpty(t *testing.T) { + client := createTestClient() + + // Test IsEmpty when no metadata is set + if !client.metadataManager.IsEmpty() { + t.Fatal("expected metadata manager to be empty") + } + + // Set metadata and test IsEmpty returns false + client.SetMetadata("key", "value") + if client.metadataManager.IsEmpty() { + t.Fatal("expected metadata manager to not be empty") + } +} + +func TestClient_MetadataManager_Clear(t *testing.T) { + client := createTestClient() + + // Set some metadata + client.SetMetadata("key1", "value1") + client.SetMetadata("key2", "value2") + + // Clear metadata + client.metadataManager.Clear() + + // Verify metadata is cleared + if !client.metadataManager.IsEmpty() { + t.Fatal("expected metadata manager to be empty after clear") + } + + metadata := client.GetAllMetadata() + if metadata != nil { + t.Fatal("expected nil metadata after clear") + } +} + func TestClient_Track(t *testing.T) { client := createTestClient() @@ -347,3 +387,265 @@ func TestClient_SetCustomAdapters(t *testing.T) { t.Fatal("expected custom HTTP adapter to be used") } } +func TestClient_NewClient_EdgeCases(t *testing.T) { + t.Run("should handle negative MaxBatchSize", func(t *testing.T) { + config := createTestConfig() + config.MaxBatchSize = -5 + + client, err := NewClient(config) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if client.config.MaxBatchSize != 10 { + t.Fatalf("expected MaxBatchSize to be set to default 10, got %d", client.config.MaxBatchSize) + } + }) + + t.Run("should handle zero MaxRetries", func(t *testing.T) { + config := createTestConfig() + config.MaxRetries = 0 + + client, err := NewClient(config) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if client.config.MaxRetries != 3 { + t.Fatalf("expected MaxRetries to be set to default 3, got %d", client.config.MaxRetries) + } + }) + + t.Run("should use provided LoggerAdapter", func(t *testing.T) { + config := createTestConfig() + customLogger := adapters.NewNoOpLoggerAdapter() + config.LoggerAdapter = customLogger + + client, err := NewClient(config) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if client.loggerAdapter != customLogger { + t.Fatal("expected custom logger to be used") + } + }) +} + +func TestClient_DisposeWithoutFlush_EdgeCases(t *testing.T) { + t.Run("should work when not initialized", func(t *testing.T) { + client := createTestClient() + + err := client.DisposeWithoutFlush() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + }) +} + +func TestClient_Init_EdgeCases(t *testing.T) { + t.Run("should handle init when already initialized", func(t *testing.T) { + client := createTestClient() + + mockHTTP := &mockHTTPAdapter{} + mockStorage := &mockStorageAdapter{} + client.httpAdapter = mockHTTP + client.storageAdapter = mockStorage + + // Initialize first time + err := client.Init() + if err != nil { + t.Fatalf("unexpected error on first init: %v", err) + } + + // Initialize second time - should not error + err = client.Init() + if err != nil { + t.Fatalf("unexpected error on second init: %v", err) + } + }) + + t.Run("should handle dispatcher start error", func(t *testing.T) { + client := createTestClient() + + // Use a mock storage that will cause an error during start + mockHTTP := &mockHTTPAdapter{} + mockStorage := &mockStorageAdapterWithError{} + client.httpAdapter = mockHTTP + client.storageAdapter = mockStorage + + err := client.Init() + if err == nil { + t.Fatal("expected error from dispatcher start") + } + }) + + t.Run("should use NoOpLoggerAdapter when none provided", func(t *testing.T) { + config := createTestConfig() + config.LoggerAdapter = nil + + client, err := NewClient(config) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Initialize to trigger logger usage + mockHTTP := &mockHTTPAdapter{} + mockStorage := &mockStorageAdapter{} + client.httpAdapter = mockHTTP + client.storageAdapter = mockStorage + + err = client.Init() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + defer client.Dispose() + + // Track an event to trigger more logger usage + err = client.Track("test", nil, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Flush to trigger dispatcher logging + client.Flush() + }) + + t.Run("should use explicit NoOpLoggerAdapter", func(t *testing.T) { + config := createTestConfig() + noopLogger := adapters.NewNoOpLoggerAdapter() + config.LoggerAdapter = noopLogger + + client, err := NewClient(config) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Verify the logger is set + if client.loggerAdapter != noopLogger { + t.Fatal("expected NoOpLoggerAdapter to be used") + } + + // Initialize and use the client to trigger all logger methods + mockHTTP := &mockHTTPAdapter{} + mockStorage := &mockStorageAdapter{} + client.httpAdapter = mockHTTP + client.storageAdapter = mockStorage + + err = client.Init() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + defer client.Dispose() + + // Track events to trigger logger calls + for i := 0; i < 5; i++ { + err = client.Track("test_event", map[string]any{"index": i}, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + } + + // Flush to trigger more logging + client.Flush() + + // Test all logger methods directly + noopLogger.Debug("test debug") + noopLogger.Info("test info") + noopLogger.Warn("test warn") + noopLogger.Error("test error") + }) +} + +func TestDispatcher_EdgeCases(t *testing.T) { + t.Run("should handle StopWithoutFlush when not started", func(t *testing.T) { + config := DispatcherConfig{ + Endpoint: "http://test.com", + FlushInterval: time.Second, + MaxBatchSize: 10, + MaxRetries: 3, + } + + dispatcher := NewDispatcher(config, &mockHTTPAdapter{}, &mockStorageAdapter{}, map[string]string{}) + + err := dispatcher.StopWithoutFlush() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run("should handle timer start race condition", func(t *testing.T) { + config := DispatcherConfig{ + Endpoint: "http://test.com", + FlushInterval: time.Millisecond * 10, + MaxBatchSize: 10, + MaxRetries: 3, + } + + dispatcher := NewDispatcher(config, &mockHTTPAdapter{}, &mockStorageAdapter{}, map[string]string{}) + dispatcher.Start() + defer dispatcher.Stop() + + // Try to start timer multiple times concurrently + for i := 0; i < 5; i++ { + go func() { + event := Event{Name: "test", IssuedAt: time.Now().UnixMilli()} + dispatcher.Enqueue(event) + }() + } + + time.Sleep(time.Millisecond * 50) + }) +} + +func TestFileStorageAdapter_EdgeCases(t *testing.T) { + t.Run("should handle load when file does not exist", func(t *testing.T) { + adapter := adapters.NewFileStorageAdapter("nonexistent_file.json") + + events, err := adapter.Load() + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if len(events) != 0 { + t.Fatalf("expected empty events, got %d", len(events)) + } + }) + + t.Run("should handle load with invalid JSON", func(t *testing.T) { + // Create a file with invalid JSON + filename := "invalid.json" + adapter := adapters.NewFileStorageAdapter(filename) + + // Write invalid JSON + err := os.WriteFile(filename, []byte("invalid json"), 0644) + if err != nil { + t.Fatalf("failed to write test file: %v", err) + } + defer os.Remove(filename) + + events, err := adapter.Load() + if err == nil { + t.Fatal("expected error for invalid JSON") + } + + if events != nil { + t.Fatal("expected nil events on error") + } + }) +} + +// Mock storage adapter that returns error on Load +type mockStorageAdapterWithError struct{} + +func (m *mockStorageAdapterWithError) Save(events []Event) error { + return nil +} + +func (m *mockStorageAdapterWithError) Load() ([]Event, error) { + return nil, errors.New("mock load error") +} + +func (m *mockStorageAdapterWithError) Clear() error { + return nil +}