package bridge import ( "context" "encoding/json" "testing" "time" "github.com/AFASystems/presence/internal/pkg/common/appcontext" "github.com/AFASystems/presence/internal/pkg/model" mqtt "github.com/eclipse/paho.mqtt.golang" ) // MockMQTTClient is a mock implementation of mqtt.Client for testing type MockMQTTClient struct { PublishedMessages map[string][]byte } func NewMockMQTTClient() *MockMQTTClient { return &MockMQTTClient{ PublishedMessages: make(map[string][]byte), } } func (m *MockMQTTClient) Publish(topic string, qos byte, retained bool, payload interface{}) mqtt.Token { // Convert payload to bytes var payloadBytes []byte if b, ok := payload.([]byte); ok { payloadBytes = b } else { payloadBytes, _ = json.Marshal(payload) } m.PublishedMessages[topic] = payloadBytes return &mockToken{} } func (m *MockMQTTClient) Subscribe(topic string, qos byte, handler mqtt.MessageHandler) mqtt.Token { return &mockToken{} } func (m *MockMQTTClient) Disconnect(quiesce uint) { // Mock implementation } type mockToken struct{} func (m *mockToken) Wait() bool { return true } func (m *mockToken) WaitTimeout(time.Duration) bool { return true } func (m *mockToken) Error() error { return nil } func (m *mockToken) Done() <-chan struct{} { ch := make(chan struct{}) close(ch) return ch } func TestEventLoop_ApiUpdate_POST(t *testing.T) { // Setup appState := appcontext.NewAppState() chApi := make(chan model.ApiUpdate, 10) ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Create a POST message msg := model.ApiUpdate{ Method: "POST", MAC: "AA:BB:CC:DD:EE:FF", ID: "beacon-123", } // Test channel send in a goroutine go func() { chApi <- msg time.Sleep(100 * time.Millisecond) cancel() }() // Simulate the event loop handling select { case <-ctx.Done(): // Context canceled case msg := <-chApi: if msg.Method == "POST" { appState.AddBeaconToLookup(msg.MAC, msg.ID) } } // Assert beaconID, exists := appState.BeaconExists("AA:BB:CC:DD:EE:FF") if !exists { t.Error("Expected beacon to exist in lookup") } if beaconID != "beacon-123" { t.Errorf("Expected beacon ID 'beacon-123', got '%s'", beaconID) } } func TestEventLoop_ApiUpdate_DELETE(t *testing.T) { // Setup appState := appcontext.NewAppState() appState.AddBeaconToLookup("AA:BB:CC:DD:EE:FF", "beacon-123") chApi := make(chan model.ApiUpdate, 10) // Create a DELETE message msg := model.ApiUpdate{ Method: "DELETE", MAC: "AA:BB:CC:DD:EE:FF", } // Simulate the event loop handling chApi <- msg select { case msg := <-chApi: if msg.Method == "DELETE" { appState.RemoveBeaconFromLookup(msg.MAC) } case <-time.After(1 * time.Second): t.Fatal("Timeout waiting for message") } // Assert _, exists := appState.BeaconExists("AA:BB:CC:DD:EE:FF") if exists { t.Error("Expected beacon to be removed from lookup") } } func TestEventLoop_ApiUpdate_DELETE_All(t *testing.T) { // Setup appState := appcontext.NewAppState() appState.AddBeaconToLookup("AA:BB:CC:DD:EE:FF", "beacon-1") appState.AddBeaconToLookup("11:22:33:44:55:66", "beacon-2") chApi := make(chan model.ApiUpdate, 10) // Create a DELETE all message msg := model.ApiUpdate{ Method: "DELETE", MAC: "all", } // Simulate the event loop handling chApi <- msg select { case msg := <-chApi: if msg.Method == "DELETE" && msg.MAC == "all" { appState.CleanLookup() } case <-time.After(1 * time.Second): t.Fatal("Timeout waiting for message") } // Assert _, exists1 := appState.BeaconExists("AA:BB:CC:DD:EE:FF") _, exists2 := appState.BeaconExists("11:22:33:44:55:66") if exists1 || exists2 { t.Error("Expected all beacons to be removed from lookup") } } func TestEventLoop_AlertMessage(t *testing.T) { // Setup mockClient := NewMockMQTTClient() chAlert := make(chan model.Alert, 10) // Create an alert message msg := model.Alert{ ID: "tracker-123", Type: "battery_low", Value: "15", } go func() { alert := <-chAlert p, _ := json.Marshal(alert) mockClient.Publish("/alerts", 0, true, p) }() chAlert <- msg time.Sleep(100 * time.Millisecond) // Assert if _, exists := mockClient.PublishedMessages["/alerts"]; !exists { t.Error("Expected message to be published to /alerts topic") } var publishedAlert model.Alert err := json.Unmarshal(mockClient.PublishedMessages["/alerts"], &publishedAlert) if err != nil { t.Fatalf("Failed to unmarshal published alert: %v", err) } if publishedAlert.ID != "tracker-123" { t.Errorf("Expected ID 'tracker-123', got '%s'", publishedAlert.ID) } if publishedAlert.Type != "battery_low" { t.Errorf("Expected Type 'battery_low', got '%s'", publishedAlert.Type) } } func TestEventLoop_TrackerMessage(t *testing.T) { // Setup mockClient := NewMockMQTTClient() chMqtt := make(chan []model.Tracker, 10) // Create tracker messages trackers := []model.Tracker{ { ID: "tracker-1", Name: "Tracker One", MAC: "AA:BB:CC:DD:EE:FF", Status: "active", X: 10.5, Y: 20.3, }, { ID: "tracker-2", Name: "Tracker Two", MAC: "11:22:33:44:55:66", Status: "inactive", X: 15.2, Y: 25.7, }, } go func() { trackerMsg := <-chMqtt p, _ := json.Marshal(trackerMsg) mockClient.Publish("/trackers", 0, true, p) }() chMqtt <- trackers time.Sleep(100 * time.Millisecond) // Assert if _, exists := mockClient.PublishedMessages["/trackers"]; !exists { t.Error("Expected message to be published to /trackers topic") } var publishedTrackers []model.Tracker err := json.Unmarshal(mockClient.PublishedMessages["/trackers"], &publishedTrackers) if err != nil { t.Fatalf("Failed to unmarshal published trackers: %v", err) } if len(publishedTrackers) != 2 { t.Errorf("Expected 2 trackers, got %d", len(publishedTrackers)) } if publishedTrackers[0].Name != "Tracker One" { t.Errorf("Expected tracker name 'Tracker One', got '%s'", publishedTrackers[0].Name) } } func TestEventLoop_ContextCancellation(t *testing.T) { // Setup ctx, cancel := context.WithCancel(context.Background()) defer cancel() chApi := make(chan model.ApiUpdate, 10) chAlert := make(chan model.Alert, 10) chMqtt := make(chan []model.Tracker, 10) // Cancel context immediately cancel() // Simulate event loop select { case <-ctx.Done(): // Expected - context was canceled return case msg := <-chApi: t.Errorf("Should not receive API messages after context cancellation, got: %+v", msg) case msg := <-chAlert: t.Errorf("Should not receive alert messages after context cancellation, got: %+v", msg) case msg := <-chMqtt: t.Errorf("Should not receive tracker messages after context cancellation, got: %+v", msg) case <-time.After(1 * time.Second): t.Error("Timeout - context cancellation should have been immediate") } }