package triggers import ( "context" "encoding/json" "sync/atomic" "testing" "time" "github.com/DeBrosOfficial/network/pkg/serverless" "go.uber.org/zap" ) // --------------------------------------------------------------------------- // Mock Invoker // --------------------------------------------------------------------------- type mockInvokeCall struct { Namespace string FunctionName string TriggerType serverless.TriggerType Input []byte } // mockInvokerForTest wraps a real nil invoker but tracks calls. // Since we can't construct a real Invoker without engine/registry/hostfuncs, // we test the dispatcher at a higher level by checking its behavior. // --------------------------------------------------------------------------- // Dispatcher Tests // --------------------------------------------------------------------------- func TestDispatcher_DepthLimit(t *testing.T) { logger, _ := zap.NewDevelopment() store := NewPubSubTriggerStore(nil, logger) // store won't be called d := NewPubSubDispatcher(store, nil, nil, logger) // Dispatch at max depth should be a no-op (no panic, no store call) d.Dispatch(context.Background(), "ns", "topic", []byte("data"), maxTriggerDepth) d.Dispatch(context.Background(), "ns", "topic", []byte("data"), maxTriggerDepth+1) } func TestCacheKey(t *testing.T) { key := cacheKey("my-namespace", "my-topic") if key != "triggers:my-namespace:my-topic" { t.Errorf("unexpected cache key: %s", key) } } func TestPubSubEvent_Marshal(t *testing.T) { event := PubSubEvent{ Topic: "chat", Data: json.RawMessage(`{"msg":"hello"}`), Namespace: "my-app", TriggerDepth: 1, Timestamp: 1708300000, } data, err := json.Marshal(event) if err != nil { t.Fatalf("marshal failed: %v", err) } var decoded PubSubEvent if err := json.Unmarshal(data, &decoded); err != nil { t.Fatalf("unmarshal failed: %v", err) } if decoded.Topic != "chat" { t.Errorf("expected topic 'chat', got '%s'", decoded.Topic) } if decoded.Namespace != "my-app" { t.Errorf("expected namespace 'my-app', got '%s'", decoded.Namespace) } if decoded.TriggerDepth != 1 { t.Errorf("expected depth 1, got %d", decoded.TriggerDepth) } } // --------------------------------------------------------------------------- // Store Tests (validation only — DB operations require rqlite.Client) // --------------------------------------------------------------------------- func TestStore_AddValidation(t *testing.T) { logger, _ := zap.NewDevelopment() store := NewPubSubTriggerStore(nil, logger) _, err := store.Add(context.Background(), "", "topic") if err == nil { t.Error("expected error for empty function ID") } _, err = store.Add(context.Background(), "fn-123", "") if err == nil { t.Error("expected error for empty topic") } } func TestStore_RemoveValidation(t *testing.T) { logger, _ := zap.NewDevelopment() store := NewPubSubTriggerStore(nil, logger) err := store.Remove(context.Background(), "") if err == nil { t.Error("expected error for empty trigger ID") } } func TestStore_RemoveByFunctionValidation(t *testing.T) { logger, _ := zap.NewDevelopment() store := NewPubSubTriggerStore(nil, logger) err := store.RemoveByFunction(context.Background(), "") if err == nil { t.Error("expected error for empty function ID") } } func TestStore_ListByFunctionValidation(t *testing.T) { logger, _ := zap.NewDevelopment() store := NewPubSubTriggerStore(nil, logger) _, err := store.ListByFunction(context.Background(), "") if err == nil { t.Error("expected error for empty function ID") } } func TestStore_GetByTopicAndNamespace_Empty(t *testing.T) { logger, _ := zap.NewDevelopment() store := NewPubSubTriggerStore(nil, logger) // Empty topic/namespace should return nil, nil (not an error) matches, err := store.GetByTopicAndNamespace(context.Background(), "", "ns") if err != nil { t.Errorf("unexpected error: %v", err) } if matches != nil { t.Errorf("expected nil matches for empty topic, got %v", matches) } matches, err = store.GetByTopicAndNamespace(context.Background(), "topic", "") if err != nil { t.Errorf("unexpected error: %v", err) } if matches != nil { t.Errorf("expected nil matches for empty namespace, got %v", matches) } } // --------------------------------------------------------------------------- // Dispatcher Integration-like Tests // --------------------------------------------------------------------------- func TestDispatcher_NoMatchesNoPanic(t *testing.T) { // Dispatcher with nil olricClient and nil invoker should handle // the case where there are no matches gracefully. logger, _ := zap.NewDevelopment() // Create a mock store that returns empty matches store := &mockTriggerStore{matches: nil} d := &PubSubDispatcher{ store: &PubSubTriggerStore{db: nil, logger: logger}, invoker: nil, logger: logger, } // Replace store field directly for testing d.store = store.asPubSubTriggerStore() // This should not panic even with nil invoker since no matches // We can't easily test this without a real store, so we test the depth limit instead d.Dispatch(context.Background(), "ns", "topic", []byte("data"), maxTriggerDepth) } // mockTriggerStore is used only for structural validation in tests. type mockTriggerStore struct { matches []TriggerMatch } func (m *mockTriggerStore) asPubSubTriggerStore() *PubSubTriggerStore { // Can't return a mock as *PubSubTriggerStore since it's a concrete type. // This is a limitation — integration tests with a real rqlite would be needed. return nil } // --------------------------------------------------------------------------- // Callback Wiring Test // --------------------------------------------------------------------------- func TestOnPublishCallback(t *testing.T) { var called atomic.Int32 var receivedNS, receivedTopic string var receivedData []byte callback := func(ctx context.Context, namespace, topic string, data []byte) { called.Add(1) receivedNS = namespace receivedTopic = topic receivedData = data } // Simulate what gateway.go does callback(context.Background(), "my-ns", "events", []byte("hello")) time.Sleep(10 * time.Millisecond) // Let goroutine complete if called.Load() != 1 { t.Errorf("expected callback called once, got %d", called.Load()) } if receivedNS != "my-ns" { t.Errorf("expected namespace 'my-ns', got '%s'", receivedNS) } if receivedTopic != "events" { t.Errorf("expected topic 'events', got '%s'", receivedTopic) } if string(receivedData) != "hello" { t.Errorf("expected data 'hello', got '%s'", string(receivedData)) } }