package hmmm_adapter import ( "context" "errors" "fmt" "strings" "sync" "testing" "time" ) func TestAdapter_Publish_OK(t *testing.T) { var joined, published bool a := NewAdapter( func(topic string) error { joined = (topic == "CHORUS/meta/issue/42"); return nil }, func(topic string, payload []byte) error { published = (topic == "CHORUS/meta/issue/42" && len(payload) > 0); return nil }, ) if err := a.Publish(context.Background(), "CHORUS/meta/issue/42", []byte(`{"ok":true}`)); err != nil { t.Fatalf("unexpected error: %v", err) } if !joined || !published { t.Fatalf("expected join and publish to be called") } // Verify metrics metrics := a.GetMetrics() if metrics.PublishCount != 1 { t.Fatalf("expected publish count 1, got %d", metrics.PublishCount) } if metrics.JoinCount != 1 { t.Fatalf("expected join count 1, got %d", metrics.JoinCount) } if metrics.ErrorCount != 0 { t.Fatalf("expected error count 0, got %d", metrics.ErrorCount) } } func TestAdapter_Publish_JoinError(t *testing.T) { a := NewAdapter( func(topic string) error { return errors.New("join failed") }, func(topic string, payload []byte) error { return nil }, ) if err := a.Publish(context.Background(), "t", []byte("{}")); err == nil { t.Fatalf("expected join error") } // Verify error was tracked metrics := a.GetMetrics() if metrics.ErrorCount != 1 { t.Fatalf("expected error count 1, got %d", metrics.ErrorCount) } } func TestAdapter_Publish_PublishError(t *testing.T) { a := NewAdapter( func(topic string) error { return nil }, func(topic string, payload []byte) error { return errors.New("publish failed") }, ) if err := a.Publish(context.Background(), "test-topic", []byte(`{"test":true}`)); err == nil { t.Fatalf("expected publish error") } // Verify error was tracked metrics := a.GetMetrics() if metrics.ErrorCount != 1 { t.Fatalf("expected error count 1, got %d", metrics.ErrorCount) } } func TestAdapter_Publish_EmptyTopic(t *testing.T) { a := NewAdapter( func(topic string) error { return nil }, func(topic string, payload []byte) error { return nil }, ) err := a.Publish(context.Background(), "", []byte(`{"test":true}`)) if err == nil { t.Fatalf("expected error for empty topic") } if !strings.Contains(err.Error(), "topic cannot be empty") { t.Fatalf("expected empty topic error, got: %v", err) } metrics := a.GetMetrics() if metrics.ErrorCount != 1 { t.Fatalf("expected error count 1, got %d", metrics.ErrorCount) } } func TestAdapter_Publish_EmptyPayload(t *testing.T) { a := NewAdapter( func(topic string) error { return nil }, func(topic string, payload []byte) error { return nil }, ) err := a.Publish(context.Background(), "test-topic", []byte{}) if err == nil { t.Fatalf("expected error for empty payload") } if !strings.Contains(err.Error(), "payload cannot be empty") { t.Fatalf("expected empty payload error, got: %v", err) } } func TestAdapter_Publish_PayloadTooLarge(t *testing.T) { config := DefaultAdapterConfig() config.MaxPayloadSize = 10 // Very small limit for testing a := NewAdapterWithConfig( func(topic string) error { return nil }, func(topic string, payload []byte) error { return nil }, config, ) largePayload := make([]byte, 20) // Larger than limit err := a.Publish(context.Background(), "test-topic", largePayload) if err == nil { t.Fatalf("expected error for payload too large") } if !strings.Contains(err.Error(), "exceeds maximum") { t.Fatalf("expected payload size error, got: %v", err) } } func TestAdapter_Publish_TopicCaching(t *testing.T) { joinCallCount := 0 a := NewAdapter( func(topic string) error { joinCallCount++; return nil }, func(topic string, payload []byte) error { return nil }, ) topic := "CHORUS/meta/issue/123" // First publish should join err := a.Publish(context.Background(), topic, []byte(`{"msg1":true}`)) if err != nil { t.Fatalf("unexpected error: %v", err) } if joinCallCount != 1 { t.Fatalf("expected 1 join call, got %d", joinCallCount) } // Second publish to same topic should not join again err = a.Publish(context.Background(), topic, []byte(`{"msg2":true}`)) if err != nil { t.Fatalf("unexpected error: %v", err) } if joinCallCount != 1 { t.Fatalf("expected 1 join call total, got %d", joinCallCount) } // Verify metrics metrics := a.GetMetrics() if metrics.JoinCount != 1 { t.Fatalf("expected join count 1, got %d", metrics.JoinCount) } if metrics.PublishCount != 2 { t.Fatalf("expected publish count 2, got %d", metrics.PublishCount) } // Verify topic is cached joinedTopics := a.GetJoinedTopics() if len(joinedTopics) != 1 || joinedTopics[0] != topic { t.Fatalf("expected topic to be cached: %v", joinedTopics) } } func TestAdapter_Publish_Timeout(t *testing.T) { config := DefaultAdapterConfig() config.PublishTimeout = 10 * time.Millisecond // Very short timeout a := NewAdapterWithConfig( func(topic string) error { return nil }, func(topic string, payload []byte) error { time.Sleep(50 * time.Millisecond) // Longer than timeout return nil }, config, ) err := a.Publish(context.Background(), "test-topic", []byte(`{"test":true}`)) if err == nil { t.Fatalf("expected timeout error") } if !strings.Contains(err.Error(), "timed out") { t.Fatalf("expected timeout error, got: %v", err) } } func TestAdapter_Publish_JoinTimeout(t *testing.T) { config := DefaultAdapterConfig() config.JoinTimeout = 10 * time.Millisecond // Very short timeout a := NewAdapterWithConfig( func(topic string) error { time.Sleep(50 * time.Millisecond) // Longer than timeout return nil }, func(topic string, payload []byte) error { return nil }, config, ) err := a.Publish(context.Background(), "test-topic", []byte(`{"test":true}`)) if err == nil { t.Fatalf("expected join timeout error") } if !strings.Contains(err.Error(), "failed to join topic") { t.Fatalf("expected join timeout error, got: %v", err) } } func TestAdapter_ConcurrentPublish(t *testing.T) { joinCalls := make(map[string]int) var joinMutex sync.Mutex a := NewAdapter( func(topic string) error { joinMutex.Lock() joinCalls[topic]++ joinMutex.Unlock() return nil }, func(topic string, payload []byte) error { return nil }, ) const numGoroutines = 10 const numTopics = 3 var wg sync.WaitGroup wg.Add(numGoroutines) for i := 0; i < numGoroutines; i++ { go func(id int) { defer wg.Done() topic := fmt.Sprintf("CHORUS/meta/issue/%d", id%numTopics) payload := fmt.Sprintf(`{"id":%d}`, id) err := a.Publish(context.Background(), topic, []byte(payload)) if err != nil { t.Errorf("unexpected error from goroutine %d: %v", id, err) } }(i) } wg.Wait() // Verify each topic was joined exactly once joinMutex.Lock() for topic, count := range joinCalls { if count != 1 { t.Errorf("topic %s was joined %d times, expected 1", topic, count) } } joinMutex.Unlock() // Verify metrics metrics := a.GetMetrics() if metrics.JoinCount != numTopics { t.Fatalf("expected join count %d, got %d", numTopics, metrics.JoinCount) } if metrics.PublishCount != numGoroutines { t.Fatalf("expected publish count %d, got %d", numGoroutines, metrics.PublishCount) } } func TestAdapter_ResetMetrics(t *testing.T) { a := NewAdapter( func(topic string) error { return nil }, func(topic string, payload []byte) error { return nil }, ) // Generate some metrics a.Publish(context.Background(), "topic1", []byte(`{"test":true}`)) a.Publish(context.Background(), "topic2", []byte(`{"test":true}`)) metrics := a.GetMetrics() if metrics.PublishCount == 0 { t.Fatalf("expected non-zero publish count") } // Reset metrics a.ResetMetrics() metrics = a.GetMetrics() if metrics.PublishCount != 0 { t.Fatalf("expected publish count to be reset to 0, got %d", metrics.PublishCount) } if metrics.JoinCount != 0 { t.Fatalf("expected join count to be reset to 0, got %d", metrics.JoinCount) } if metrics.ErrorCount != 0 { t.Fatalf("expected error count to be reset to 0, got %d", metrics.ErrorCount) } } func TestAdapter_ClearTopicCache(t *testing.T) { a := NewAdapter( func(topic string) error { return nil }, func(topic string, payload []byte) error { return nil }, ) // Publish to create cached topics a.Publish(context.Background(), "topic1", []byte(`{"test":true}`)) a.Publish(context.Background(), "topic2", []byte(`{"test":true}`)) joinedTopics := a.GetJoinedTopics() if len(joinedTopics) != 2 { t.Fatalf("expected 2 joined topics, got %d", len(joinedTopics)) } // Clear cache a.ClearTopicCache() joinedTopics = a.GetJoinedTopics() if len(joinedTopics) != 0 { t.Fatalf("expected 0 joined topics after cache clear, got %d", len(joinedTopics)) } } func TestAdapter_DefaultConfig(t *testing.T) { config := DefaultAdapterConfig() if config.MaxPayloadSize <= 0 { t.Fatalf("expected positive max payload size, got %d", config.MaxPayloadSize) } if config.JoinTimeout <= 0 { t.Fatalf("expected positive join timeout, got %v", config.JoinTimeout) } if config.PublishTimeout <= 0 { t.Fatalf("expected positive publish timeout, got %v", config.PublishTimeout) } } func TestAdapter_CustomConfig(t *testing.T) { config := AdapterConfig{ MaxPayloadSize: 1000, JoinTimeout: 5 * time.Second, PublishTimeout: 2 * time.Second, } a := NewAdapterWithConfig( func(topic string) error { return nil }, func(topic string, payload []byte) error { return nil }, config, ) if a.maxPayloadSize != 1000 { t.Fatalf("expected max payload size 1000, got %d", a.maxPayloadSize) } if a.joinTimeout != 5*time.Second { t.Fatalf("expected join timeout 5s, got %v", a.joinTimeout) } if a.publishTimeout != 2*time.Second { t.Fatalf("expected publish timeout 2s, got %v", a.publishTimeout) } }