| @@ -3,6 +3,7 @@ package server | |||
| import ( | |||
| "context" | |||
| "encoding/json" | |||
| "fmt" | |||
| "log/slog" | |||
| "time" | |||
| @@ -55,6 +56,10 @@ func RunEventLoop(ctx context.Context, a *ServerApp) { | |||
| slog.Error("saving decoder event for beacon", "id", id, "err", err) | |||
| continue | |||
| } | |||
| if msg.Type == "Eddystone" && msg.Battery < 3000 { | |||
| fmt.Printf("Sending alert for battery low: %+v\n", msg) | |||
| service.SendAlert(id, "BatteryLow", a.KafkaManager.GetWriter("alert"), ctx, a.DB) | |||
| } | |||
| case <-beaconTicker.C: | |||
| var list []model.Tracker | |||
| a.DB.Find(&list) | |||
| @@ -88,7 +88,7 @@ func LocationToBeaconService(msg model.HTTPLocation, db *gorm.DB, writer *kafka. | |||
| return | |||
| } | |||
| sendAlert(gw.ID, msg.ID, writer, ctx, allowedZones, db) | |||
| sendRestrictedZoneAlert(gw.ID, msg.ID, writer, ctx, allowedZones, db) | |||
| } | |||
| func LocationToBeaconServiceAI(msg model.HTTPLocation, db *gorm.DB, writer *kafka.Writer, ctx context.Context) { | |||
| @@ -126,48 +126,52 @@ func LocationToBeaconServiceAI(msg model.HTTPLocation, db *gorm.DB, writer *kafk | |||
| return | |||
| } | |||
| sendAlert(gw.ID, tracker.ID, writer, ctx, allowedZones, db) | |||
| sendRestrictedZoneAlert(gw.ID, tracker.ID, writer, ctx, allowedZones, db) | |||
| } | |||
| func sendAlert(gwId, trackerId string, writer *kafka.Writer, ctx context.Context, allowedZones []string, db *gorm.DB) { | |||
| if len(allowedZones) != 0 && !slices.Contains(allowedZones, gwId) { | |||
| var existingAlert model.Alert | |||
| result := db.Select("status").Where("tracker_id = ? AND type = ?", trackerId, "Restricted zone").Order("timestamp DESC").First(&existingAlert) | |||
| if result.Error == gorm.ErrRecordNotFound || existingAlert.Status == "resolved" { | |||
| alert := model.Alert{ | |||
| ID: uuid.New().String(), | |||
| TrackerID: trackerId, | |||
| Type: "Restricted zone", | |||
| Status: "new", | |||
| Timestamp: time.Now(), | |||
| } | |||
| func SendAlert(trackerId, alertType string, writer *kafka.Writer, ctx context.Context, db *gorm.DB) { | |||
| var existingAlert model.Alert | |||
| result := db.Select("status").Where("tracker_id = ? AND type = ?", trackerId, alertType).Order("timestamp DESC").First(&existingAlert) | |||
| if result.Error == gorm.ErrRecordNotFound || existingAlert.Status == "resolved" { | |||
| alert := model.Alert{ | |||
| ID: uuid.New().String(), | |||
| TrackerID: trackerId, | |||
| Type: alertType, | |||
| Status: "new", | |||
| Timestamp: time.Now(), | |||
| } | |||
| if err := InsertAlert(alert, db, ctx); err != nil { | |||
| msg := fmt.Sprintf("Error in inserting alert: %v", err) | |||
| slog.Error(msg) | |||
| return | |||
| } | |||
| if err := InsertAlert(alert, db, ctx); err != nil { | |||
| msg := fmt.Sprintf("Error in inserting alert: %v", err) | |||
| slog.Error(msg) | |||
| return | |||
| } | |||
| eMsg, err := json.Marshal(alert) | |||
| if err != nil { | |||
| msg := "Error in marshaling" | |||
| eMsg, err := json.Marshal(alert) | |||
| if err != nil { | |||
| msg := "Error in marshaling" | |||
| slog.Error(msg) | |||
| return | |||
| } else { | |||
| msg := kafka.Message{ | |||
| Value: eMsg, | |||
| } | |||
| if err := kafkaclient.Write(ctx, writer, msg); err != nil { | |||
| msg := fmt.Sprintf("Error in writing message: %v", err) | |||
| slog.Error(msg) | |||
| return | |||
| } else { | |||
| msg := kafka.Message{ | |||
| Value: eMsg, | |||
| } | |||
| if err := kafkaclient.Write(ctx, writer, msg); err != nil { | |||
| msg := fmt.Sprintf("Error in writing message: %v", err) | |||
| slog.Error(msg) | |||
| return | |||
| } | |||
| } | |||
| return | |||
| } else { | |||
| return | |||
| } | |||
| return | |||
| } else { | |||
| return | |||
| } | |||
| } | |||
| func sendRestrictedZoneAlert(gwId, trackerId string, writer *kafka.Writer, ctx context.Context, allowedZones []string, db *gorm.DB) { | |||
| if len(allowedZones) != 0 && !slices.Contains(allowedZones, gwId) { | |||
| SendAlert(trackerId, "Restricted zone", writer, ctx, db) | |||
| } | |||
| } | |||
| @@ -5,7 +5,6 @@ import ( | |||
| "testing" | |||
| "github.com/AFASystems/presence/internal/pkg/common/appcontext" | |||
| "github.com/AFASystems/presence/internal/pkg/model" | |||
| ) | |||
| func TestNewAppState(t *testing.T) { | |||
| @@ -64,7 +63,7 @@ func TestBeaconLookup_CleanLookup(t *testing.T) { | |||
| func TestBeacon_GetAndUpdate(t *testing.T) { | |||
| state := appcontext.NewAppState() | |||
| beacon := model.Beacon{ | |||
| beacon := appcontext.Beacon{ | |||
| ID: "test-beacon", | |||
| Name: "Test", | |||
| } | |||
| @@ -81,7 +80,7 @@ func TestBeacon_GetAndUpdate(t *testing.T) { | |||
| func TestBeaconEvent_GetAndUpdate(t *testing.T) { | |||
| state := appcontext.NewAppState() | |||
| event := model.BeaconEvent{ | |||
| event := appcontext.BeaconEvent{ | |||
| ID: "beacon-1", | |||
| Type: "iBeacon", | |||
| Battery: 85, | |||
| @@ -99,8 +98,8 @@ func TestBeaconEvent_GetAndUpdate(t *testing.T) { | |||
| func TestGetAllBeacons(t *testing.T) { | |||
| state := appcontext.NewAppState() | |||
| state.UpdateBeacon("b1", model.Beacon{ID: "b1"}) | |||
| state.UpdateBeacon("b2", model.Beacon{ID: "b2"}) | |||
| state.UpdateBeacon("b1", appcontext.Beacon{ID: "b1"}) | |||
| state.UpdateBeacon("b2", appcontext.Beacon{ID: "b2"}) | |||
| all := state.GetAllBeacons() | |||
| if len(all) != 2 { | |||
| @@ -111,7 +110,7 @@ func TestGetAllBeacons(t *testing.T) { | |||
| func TestUpdateSettings(t *testing.T) { | |||
| state := appcontext.NewAppState() | |||
| state.UpdateSettings(map[string]any{ | |||
| "current_algorithm": "ai", | |||
| "current_algorithm": "ai", | |||
| "location_confidence": int64(5), | |||
| }) | |||
| @@ -9,8 +9,8 @@ import ( | |||
| "github.com/AFASystems/presence/internal/pkg/common/appcontext" | |||
| "github.com/AFASystems/presence/internal/pkg/model" | |||
| "github.com/segmentio/kafka-go" | |||
| mqtt "github.com/eclipse/paho.mqtt.golang" | |||
| "github.com/segmentio/kafka-go" | |||
| ) | |||
| // mqtthandler is extracted from main.go for testing purposes | |||
| @@ -36,7 +36,7 @@ func mqtthandler(writer kafkaWriter, topic string, message []byte, appState *app | |||
| continue | |||
| } | |||
| adv := model.BeaconAdvertisement{ | |||
| adv := appcontext.BeaconAdvertisement{ | |||
| ID: val, | |||
| Hostname: hostname, | |||
| MAC: reading.MAC, | |||
| @@ -180,9 +180,11 @@ func TestEventLoop_AlertMessage(t *testing.T) { | |||
| // Create an alert message | |||
| msg := model.Alert{ | |||
| ID: "tracker-123", | |||
| Type: "battery_low", | |||
| Value: "15", | |||
| ID: "tracker-123", | |||
| Type: "battery_low", | |||
| Status: "new", | |||
| Timestamp: time.Now(), | |||
| TrackerID: "tracker-123", | |||
| } | |||
| go func() { | |||
| @@ -75,7 +75,7 @@ func TestIntegration_EndToEnd(t *testing.T) { | |||
| } | |||
| // Verify | |||
| var adv model.BeaconAdvertisement | |||
| var adv appcontext.BeaconAdvertisement | |||
| err = json.Unmarshal(msg.Value, &adv) | |||
| if err != nil { | |||
| t.Fatalf("Failed to unmarshal beacon advertisement: %v", err) | |||
| @@ -155,7 +155,7 @@ func TestIntegration_MultipleMessages(t *testing.T) { | |||
| t.Fatalf("Failed to read message %d from Kafka: %v", i+1, err) | |||
| } | |||
| var adv model.BeaconAdvertisement | |||
| var adv appcontext.BeaconAdvertisement | |||
| err = json.Unmarshal(msg.Value, &adv) | |||
| if err != nil { | |||
| t.Fatalf("Failed to unmarshal beacon advertisement %d: %v", i+1, err) | |||
| @@ -35,7 +35,7 @@ func TestMQTTHandler_SingleReading(t *testing.T) { | |||
| t.Errorf("Expected 1 message, got %d", len(mockWriter.Messages)) | |||
| } | |||
| var adv model.BeaconAdvertisement | |||
| var adv appcontext.BeaconAdvertisement | |||
| err := json.Unmarshal(mockWriter.Messages[0].Value, &adv) | |||
| if err != nil { | |||
| t.Fatalf("Failed to unmarshal beacon advertisement: %v", err) | |||
| @@ -175,8 +175,8 @@ func TestMQTTHandler_HostnameExtraction(t *testing.T) { | |||
| // Test various topic formats | |||
| testCases := []struct { | |||
| topic string | |||
| expectedHost string | |||
| topic string | |||
| expectedHost string | |||
| }{ | |||
| {"publish_out/gateway-1", "gateway-1"}, | |||
| {"publish_out/gateway-prod-02", "gateway-prod-02"}, | |||
| @@ -202,7 +202,7 @@ func TestMQTTHandler_HostnameExtraction(t *testing.T) { | |||
| t.Fatalf("Expected 1 message, got %d", len(mockWriter.Messages)) | |||
| } | |||
| var adv model.BeaconAdvertisement | |||
| var adv appcontext.BeaconAdvertisement | |||
| err := json.Unmarshal(mockWriter.Messages[0].Value, &adv) | |||
| if err != nil { | |||
| t.Fatalf("Failed to unmarshal beacon advertisement: %v", err) | |||
| @@ -242,7 +242,7 @@ func TestMQTTHandler_PreservesRawData(t *testing.T) { | |||
| t.Fatalf("Expected 1 message, got %d", len(mockWriter.Messages)) | |||
| } | |||
| var adv model.BeaconAdvertisement | |||
| var adv appcontext.BeaconAdvertisement | |||
| err := json.Unmarshal(mockWriter.Messages[0].Value, &adv) | |||
| if err != nil { | |||
| t.Fatalf("Failed to unmarshal beacon advertisement: %v", err) | |||
| @@ -94,7 +94,7 @@ func (th *TestHelper) CreateMQTTMessage(topic string, readings []model.RawReadin | |||
| } | |||
| // AssertBeaconAdvertisement asserts that a beacon advertisement matches expected values | |||
| func (th *TestHelper) AssertBeaconAdvertisement(adv model.BeaconAdvertisement, expectedID, expectedHostname, expectedMAC string, expectedRSSI int64) { | |||
| func (th *TestHelper) AssertBeaconAdvertisement(adv appcontext.BeaconAdvertisement, expectedID, expectedHostname, expectedMAC string, expectedRSSI int64) { | |||
| if adv.ID != expectedID { | |||
| th.t.Errorf("Expected ID '%s', got '%s'", expectedID, adv.ID) | |||
| } | |||
| @@ -2,11 +2,13 @@ package controller | |||
| import ( | |||
| "bytes" | |||
| "context" | |||
| "encoding/json" | |||
| "net/http" | |||
| "net/http/httptest" | |||
| "testing" | |||
| "github.com/AFASystems/presence/internal/pkg/common/appcontext" | |||
| "github.com/AFASystems/presence/internal/pkg/controller" | |||
| "github.com/AFASystems/presence/internal/pkg/model" | |||
| "github.com/gorilla/mux" | |||
| @@ -19,7 +21,7 @@ func setupTestDB(t *testing.T) *gorm.DB { | |||
| if err != nil { | |||
| t.Fatalf("Failed to open test DB: %v", err) | |||
| } | |||
| if err := db.AutoMigrate(&model.Gateway{}, &model.Zone{}, &model.Tracker{}, &model.TrackerZones{}, &model.Config{}, &model.Settings{}, &model.Tracks{}); err != nil { | |||
| if err := db.AutoMigrate(&model.Gateway{}, &model.Zone{}, &model.Tracker{}, &model.TrackerZones{}, &model.Config{}, appcontext.Settings{}, &model.Tracks{}); err != nil { | |||
| t.Fatalf("Failed to migrate: %v", err) | |||
| } | |||
| return db | |||
| @@ -27,7 +29,8 @@ func setupTestDB(t *testing.T) *gorm.DB { | |||
| func TestGatewayListController_Empty(t *testing.T) { | |||
| db := setupTestDB(t) | |||
| handler := controller.GatewayListController(db) | |||
| context := context.Background() | |||
| handler := controller.GatewayListController(db, context) | |||
| req := httptest.NewRequest(http.MethodGet, "/gateways", nil) | |||
| rec := httptest.NewRecorder() | |||
| @@ -47,7 +50,8 @@ func TestGatewayListController_Empty(t *testing.T) { | |||
| func TestGatewayAddController(t *testing.T) { | |||
| db := setupTestDB(t) | |||
| handler := controller.GatewayAddController(db) | |||
| context := context.Background() | |||
| handler := controller.GatewayAddController(db, context) | |||
| gateway := model.Gateway{ | |||
| ID: "gw-1", | |||
| @@ -77,11 +81,11 @@ func TestGatewayAddController(t *testing.T) { | |||
| func TestGatewayDeleteController(t *testing.T) { | |||
| db := setupTestDB(t) | |||
| db.Create(&model.Gateway{ID: "gw-1", Name: "G1", MAC: "AA:BB:CC:DD:EE:FF"}) | |||
| context := context.Background() | |||
| req := httptest.NewRequest(http.MethodDelete, "/gateways/gw-1", nil) | |||
| req = mux.SetURLVars(req, map[string]string{"id": "gw-1"}) | |||
| rec := httptest.NewRecorder() | |||
| controller.GatewayDeleteController(db).ServeHTTP(rec, req) | |||
| controller.GatewayDeleteController(db, context).ServeHTTP(rec, req) | |||
| if rec.Code != http.StatusOK { | |||
| t.Errorf("Expected 200, got %d", rec.Code) | |||
| @@ -96,7 +100,8 @@ func TestGatewayDeleteController(t *testing.T) { | |||
| func TestTrackerListController_Empty(t *testing.T) { | |||
| db := setupTestDB(t) | |||
| handler := controller.TrackerList(db) | |||
| context := context.Background() | |||
| handler := controller.TrackerList(db, context) | |||
| req := httptest.NewRequest(http.MethodGet, "/trackers", nil) | |||
| rec := httptest.NewRecorder() | |||
| @@ -116,7 +121,8 @@ func TestTrackerListController_Empty(t *testing.T) { | |||
| func TestZoneListController_Empty(t *testing.T) { | |||
| db := setupTestDB(t) | |||
| handler := controller.ZoneListController(db) | |||
| context := context.Background() | |||
| handler := controller.ZoneListController(db, context) | |||
| req := httptest.NewRequest(http.MethodGet, "/zones", nil) | |||
| rec := httptest.NewRecorder() | |||
| @@ -129,7 +135,8 @@ func TestZoneListController_Empty(t *testing.T) { | |||
| func TestSettingsListController(t *testing.T) { | |||
| db := setupTestDB(t) | |||
| handler := controller.SettingsListController(db) | |||
| context := context.Background() | |||
| handler := controller.SettingsListController(db, context) | |||
| req := httptest.NewRequest(http.MethodGet, "/settings", nil) | |||
| rec := httptest.NewRecorder() | |||
| @@ -15,7 +15,7 @@ func TestDecodeBeacon_EmptyData(t *testing.T) { | |||
| mockWriter := &MockKafkaWriter{Messages: []kafka.Message{}} | |||
| parserRegistry := &model.ParserRegistry{ParserList: make(map[string]model.BeaconParser)} | |||
| adv := model.BeaconAdvertisement{ | |||
| adv := appcontext.BeaconAdvertisement{ | |||
| ID: "test-beacon", | |||
| Data: "", // Empty data | |||
| } | |||
| @@ -39,7 +39,7 @@ func TestDecodeBeacon_WhitespaceOnly(t *testing.T) { | |||
| mockWriter := &MockKafkaWriter{Messages: []kafka.Message{}} | |||
| parserRegistry := &model.ParserRegistry{ParserList: make(map[string]model.BeaconParser)} | |||
| adv := model.BeaconAdvertisement{ | |||
| adv := appcontext.BeaconAdvertisement{ | |||
| ID: "test-beacon", | |||
| Data: " ", // Whitespace only | |||
| } | |||
| @@ -63,7 +63,7 @@ func TestDecodeBeacon_InvalidHex(t *testing.T) { | |||
| mockWriter := &MockKafkaWriter{Messages: []kafka.Message{}} | |||
| parserRegistry := &model.ParserRegistry{ParserList: make(map[string]model.BeaconParser)} | |||
| adv := model.BeaconAdvertisement{ | |||
| adv := appcontext.BeaconAdvertisement{ | |||
| ID: "test-beacon", | |||
| Data: "INVALID_HEX_DATA!!!", | |||
| } | |||
| @@ -88,7 +88,7 @@ func TestDecodeBeacon_ValidHexNoParser(t *testing.T) { | |||
| parserRegistry := &model.ParserRegistry{ParserList: make(map[string]model.BeaconParser)} // No parsers registered | |||
| // Valid hex but no matching parser (03 02 FF 06 - type 0x02, no parser registered for it) | |||
| adv := model.BeaconAdvertisement{ | |||
| adv := appcontext.BeaconAdvertisement{ | |||
| ID: "test-beacon", | |||
| Data: "0302FF06", // Valid AD structure | |||
| } | |||
| @@ -126,7 +126,7 @@ func TestDecodeBeacon_Deduplication(t *testing.T) { | |||
| parserRegistry.Register("test-parser", config) | |||
| // Create an event that will be parsed (03 02 FF 06 - not removed by RemoveFlagBytes) | |||
| adv := model.BeaconAdvertisement{ | |||
| adv := appcontext.BeaconAdvertisement{ | |||
| ID: "test-beacon", | |||
| Data: "0302FF06", | |||
| } | |||
| @@ -171,7 +171,7 @@ func TestDecodeBeacon_DifferentDataPublishes(t *testing.T) { | |||
| parserRegistry.Register("test-parser", config) | |||
| // First processing | |||
| adv1 := model.BeaconAdvertisement{ | |||
| adv1 := appcontext.BeaconAdvertisement{ | |||
| ID: "test-beacon", | |||
| Data: "020106", | |||
| } | |||
| @@ -184,7 +184,7 @@ func TestDecodeBeacon_DifferentDataPublishes(t *testing.T) { | |||
| firstMessageCount := len(mockWriter.Messages) | |||
| // Second processing with different data - should publish again | |||
| adv2 := model.BeaconAdvertisement{ | |||
| adv2 := appcontext.BeaconAdvertisement{ | |||
| ID: "test-beacon", | |||
| Data: "0302FF07", // Different data | |||
| } | |||
| @@ -220,7 +220,7 @@ func TestDecodeBeacon_WithFlagBytes(t *testing.T) { | |||
| parserRegistry.Register("test-parser", config) | |||
| // Data with flag bytes first (02 01 06), then our structure (03 02 FF 08) | |||
| adv := model.BeaconAdvertisement{ | |||
| adv := appcontext.BeaconAdvertisement{ | |||
| ID: "test-beacon", | |||
| Data: "0201060302FF08", // Flags removed, then 03 02 FF 08 remains | |||
| } | |||
| @@ -254,7 +254,7 @@ func TestDecodeBeacon_MultipleBeacons(t *testing.T) { | |||
| parserRegistry.Register("test-parser", config) | |||
| // Process multiple different beacons (03 02 FF xx - not removed by RemoveFlagBytes) | |||
| beacons := []model.BeaconAdvertisement{ | |||
| beacons := []appcontext.BeaconAdvertisement{ | |||
| {ID: "beacon-1", Data: "0302FF06"}, | |||
| {ID: "beacon-2", Data: "0302FF07"}, | |||
| {ID: "beacon-3", Data: "0302FF08"}, | |||
| @@ -280,7 +280,7 @@ func TestProcessIncoming_ErrorHandling(t *testing.T) { | |||
| parserRegistry := &model.ParserRegistry{ParserList: make(map[string]model.BeaconParser)} | |||
| // Invalid data that will cause an error | |||
| adv := model.BeaconAdvertisement{ | |||
| adv := appcontext.BeaconAdvertisement{ | |||
| ID: "test-beacon", | |||
| Data: "INVALID_HEX", | |||
| } | |||
| @@ -313,7 +313,7 @@ func TestDecodeBeacon_EventHashing(t *testing.T) { | |||
| } | |||
| parserRegistry.Register("test-parser", config) | |||
| adv := model.BeaconAdvertisement{ | |||
| adv := appcontext.BeaconAdvertisement{ | |||
| ID: "test-beacon", | |||
| Data: "0302FF06", | |||
| } | |||
| @@ -369,7 +369,7 @@ func TestDecodeBeacon_VariousHexFormats(t *testing.T) { | |||
| for _, tc := range testCases { | |||
| t.Run(tc.name, func(t *testing.T) { | |||
| adv := model.BeaconAdvertisement{ | |||
| adv := appcontext.BeaconAdvertisement{ | |||
| ID: "test-beacon", | |||
| Data: tc.hexData, | |||
| } | |||
| @@ -14,7 +14,7 @@ import ( | |||
| ) | |||
| // processIncoming processes incoming beacon advertisements | |||
| func processIncoming(adv model.BeaconAdvertisement, appState *appcontext.AppState, writer kafkaWriter, parserRegistry *model.ParserRegistry) { | |||
| func processIncoming(adv appcontext.BeaconAdvertisement, appState *appcontext.AppState, writer kafkaWriter, parserRegistry *model.ParserRegistry) { | |||
| err := decodeBeacon(adv, appState, writer, parserRegistry) | |||
| if err != nil { | |||
| eMsg := fmt.Sprintf("Error in decoding: %v", err) | |||
| @@ -24,7 +24,7 @@ func processIncoming(adv model.BeaconAdvertisement, appState *appcontext.AppStat | |||
| } | |||
| // decodeBeacon decodes beacon data and publishes events | |||
| func decodeBeacon(adv model.BeaconAdvertisement, appState *appcontext.AppState, writer kafkaWriter, parserRegistry *model.ParserRegistry) error { | |||
| func decodeBeacon(adv appcontext.BeaconAdvertisement, appState *appcontext.AppState, writer kafkaWriter, parserRegistry *model.ParserRegistry) error { | |||
| beacon := strings.TrimSpace(adv.Data) | |||
| id := adv.ID | |||
| if beacon == "" { | |||
| @@ -39,7 +39,7 @@ func decodeBeacon(adv model.BeaconAdvertisement, appState *appcontext.AppState, | |||
| b = utils.RemoveFlagBytes(b) | |||
| indeces := utils.ParseADFast(b) | |||
| event := utils.LoopADStructures(b, indeces, id, parserRegistry) | |||
| event := utils.LoopADStructures(b, indeces, id, parserRegistry, "") | |||
| if event.ID == "" { | |||
| return nil | |||
| @@ -16,12 +16,12 @@ func TestEventLoop_RawMessageProcessing(t *testing.T) { | |||
| mockWriter := &MockKafkaWriter{Messages: []kafka.Message{}} | |||
| parserRegistry := &model.ParserRegistry{ParserList: make(map[string]model.BeaconParser)} | |||
| chRaw := make(chan model.BeaconAdvertisement, 10) | |||
| chRaw := make(chan appcontext.BeaconAdvertisement, 10) | |||
| ctx, cancel := context.WithCancel(context.Background()) | |||
| defer cancel() | |||
| // Create a test message | |||
| msg := model.BeaconAdvertisement{ | |||
| msg := appcontext.BeaconAdvertisement{ | |||
| ID: "test-beacon", | |||
| Data: "020106", | |||
| } | |||
| @@ -242,7 +242,7 @@ func TestEventLoop_ContextCancellation(t *testing.T) { | |||
| ctx, cancel := context.WithCancel(context.Background()) | |||
| defer cancel() | |||
| chRaw := make(chan model.BeaconAdvertisement, 10) | |||
| chRaw := make(chan appcontext.BeaconAdvertisement, 10) | |||
| chParser := make(chan model.KafkaParser, 10) | |||
| // Cancel immediately | |||
| @@ -264,7 +264,7 @@ func TestEventLoop_ContextCancellation(t *testing.T) { | |||
| func TestEventLoop_ChannelBuffering(t *testing.T) { | |||
| // Setup - create buffered channels (like in main) | |||
| chRaw := make(chan model.BeaconAdvertisement, 2000) | |||
| chRaw := make(chan appcontext.BeaconAdvertisement, 2000) | |||
| chParser := make(chan model.KafkaParser, 200) | |||
| _, cancel := context.WithCancel(context.Background()) | |||
| @@ -272,7 +272,7 @@ func TestEventLoop_ChannelBuffering(t *testing.T) { | |||
| // Send multiple messages without blocking | |||
| for i := 0; i < 100; i++ { | |||
| msg := model.BeaconAdvertisement{ | |||
| msg := appcontext.BeaconAdvertisement{ | |||
| ID: "test-beacon", | |||
| Data: "020106", | |||
| } | |||
| @@ -310,14 +310,14 @@ func TestEventLoop_ChannelBuffering(t *testing.T) { | |||
| func TestEventLoop_ParserAndRawChannels(t *testing.T) { | |||
| // Setup | |||
| chRaw := make(chan model.BeaconAdvertisement, 10) | |||
| chRaw := make(chan appcontext.BeaconAdvertisement, 10) | |||
| chParser := make(chan model.KafkaParser, 10) | |||
| _, cancel := context.WithCancel(context.Background()) | |||
| defer cancel() | |||
| // Send both raw and parser messages | |||
| rawMsg := model.BeaconAdvertisement{ | |||
| rawMsg := appcontext.BeaconAdvertisement{ | |||
| ID: "test-beacon", | |||
| Data: "020106", | |||
| } | |||
| @@ -56,7 +56,7 @@ func TestIntegration_DecoderEndToEnd(t *testing.T) { | |||
| defer reader.Close() | |||
| // Create a test beacon advertisement | |||
| adv := model.BeaconAdvertisement{ | |||
| adv := appcontext.BeaconAdvertisement{ | |||
| ID: "integration-test-beacon", | |||
| Data: "020106", // Valid hex data | |||
| } | |||
| @@ -104,7 +104,7 @@ func TestIntegration_ParserRegistryOperations(t *testing.T) { | |||
| ID: "add", | |||
| Name: "kafka-test-parser", | |||
| Config: model.Config{ | |||
| Name: "kafka-test-parser", | |||
| Name: "kafka-test-parser", | |||
| Min: 2, | |||
| Max: 20, | |||
| Pattern: []string{"0x02", "0x01"}, | |||
| @@ -177,7 +177,7 @@ func TestIntegration_MultipleBeaconsSequential(t *testing.T) { | |||
| defer reader.Close() | |||
| // Process multiple beacons | |||
| beacons := []model.BeaconAdvertisement{ | |||
| beacons := []appcontext.BeaconAdvertisement{ | |||
| {ID: "beacon-1", Data: "020106"}, | |||
| {ID: "beacon-2", Data: "020107"}, | |||
| {ID: "beacon-3", Data: "020108"}, | |||
| @@ -243,7 +243,7 @@ func TestIntegration_EventDeduplication(t *testing.T) { | |||
| defer reader.Close() | |||
| // Create identical beacon advertisement | |||
| adv := model.BeaconAdvertisement{ | |||
| adv := appcontext.BeaconAdvertisement{ | |||
| ID: "dedup-test-beacon", | |||
| Data: "020106", | |||
| } | |||
| @@ -319,7 +319,7 @@ func TestIntegration_AppStatePersistence(t *testing.T) { | |||
| defer writer.Close() | |||
| // Process beacon | |||
| adv := model.BeaconAdvertisement{ | |||
| adv := appcontext.BeaconAdvertisement{ | |||
| ID: "persist-test-beacon", | |||
| Data: "020106", | |||
| } | |||
| @@ -381,7 +381,7 @@ func TestIntegration_ParserUpdateFlow(t *testing.T) { | |||
| parserRegistry.Register("update-test-parser", config1) | |||
| // Process with initial config | |||
| adv := model.BeaconAdvertisement{ | |||
| adv := appcontext.BeaconAdvertisement{ | |||
| ID: "update-test-beacon", | |||
| Data: "020106", | |||
| } | |||
| @@ -391,7 +391,7 @@ func TestIntegration_ParserUpdateFlow(t *testing.T) { | |||
| // Update parser config | |||
| config2 := model.Config{ | |||
| Name: "update-test-parser", | |||
| Name: "update-test-parser", | |||
| Min: 3, | |||
| Max: 25, | |||
| Pattern: []string{"0x03"}, | |||
| @@ -400,7 +400,7 @@ func TestIntegration_ParserUpdateFlow(t *testing.T) { | |||
| parserRegistry.Register("update-test-parser", config2) | |||
| // Process again with updated config | |||
| adv2 := model.BeaconAdvertisement{ | |||
| adv2 := appcontext.BeaconAdvertisement{ | |||
| ID: "update-test-beacon-2", | |||
| Data: "030107", | |||
| } | |||
| @@ -60,32 +60,32 @@ func (th *TestHelper) RegisterTestParser(name string) { | |||
| } | |||
| // CreateBeaconAdvertisement creates a test beacon advertisement | |||
| func (th *TestHelper) CreateBeaconAdvertisement(id, data string) model.BeaconAdvertisement { | |||
| return model.BeaconAdvertisement{ | |||
| func (th *TestHelper) CreateBeaconAdvertisement(id, data string) appcontext.BeaconAdvertisement { | |||
| return appcontext.BeaconAdvertisement{ | |||
| ID: id, | |||
| Data: data, | |||
| } | |||
| } | |||
| // CreateValidHexAdvertisement creates a beacon with valid hex data | |||
| func (th *TestHelper) CreateValidHexAdvertisement(id string) model.BeaconAdvertisement { | |||
| return model.BeaconAdvertisement{ | |||
| func (th *TestHelper) CreateValidHexAdvertisement(id string) appcontext.BeaconAdvertisement { | |||
| return appcontext.BeaconAdvertisement{ | |||
| ID: id, | |||
| Data: "020106", | |||
| } | |||
| } | |||
| // CreateInvalidHexAdvertisement creates a beacon with invalid hex data | |||
| func (th *TestHelper) CreateInvalidHexAdvertisement(id string) model.BeaconAdvertisement { | |||
| return model.BeaconAdvertisement{ | |||
| func (th *TestHelper) CreateInvalidHexAdvertisement(id string) appcontext.BeaconAdvertisement { | |||
| return appcontext.BeaconAdvertisement{ | |||
| ID: id, | |||
| Data: "INVALID_HEX", | |||
| } | |||
| } | |||
| // CreateEmptyAdvertisement creates a beacon with empty data | |||
| func (th *TestHelper) CreateEmptyAdvertisement(id string) model.BeaconAdvertisement { | |||
| return model.BeaconAdvertisement{ | |||
| func (th *TestHelper) CreateEmptyAdvertisement(id string) appcontext.BeaconAdvertisement { | |||
| return appcontext.BeaconAdvertisement{ | |||
| ID: id, | |||
| Data: "", | |||
| } | |||
| @@ -106,11 +106,11 @@ func (th *TestHelper) AssertParserNotExists(name string) { | |||
| } | |||
| // AssertEventExists asserts that an event exists in appState | |||
| func (th *TestHelper) AssertEventExists(id string) model.BeaconEvent { | |||
| func (th *TestHelper) AssertEventExists(id string) appcontext.BeaconEvent { | |||
| event, exists := th.appState.GetBeaconEvent(id) | |||
| if !exists { | |||
| th.t.Errorf("Event for beacon '%s' should exist in appState", id) | |||
| return model.BeaconEvent{} | |||
| return appcontext.BeaconEvent{} | |||
| } | |||
| return event | |||
| } | |||
| @@ -172,10 +172,10 @@ func AssertError(t *testing.T, err error, msg string) { | |||
| // Valid hex strings for testing | |||
| var ValidHexStrings = []string{ | |||
| "020106", // Simple AD structure | |||
| "0201060302A0", // AD structure with flags | |||
| "1AFF0C01", // iBeacon-like data | |||
| "0201061AFF0C01", // Multiple AD structures | |||
| "020106", // Simple AD structure | |||
| "0201060302A0", // AD structure with flags | |||
| "1AFF0C01", // iBeacon-like data | |||
| "0201061AFF0C01", // Multiple AD structures | |||
| } | |||
| // Invalid hex strings for testing | |||
| @@ -201,7 +201,7 @@ func CreateMockWriter() *MockKafkaWriter { | |||
| // Beacon event test helpers | |||
| // AssertEventFields asserts that event fields match expected values | |||
| func AssertEventFields(t *testing.T, event model.BeaconEvent, expectedID, expectedType string) { | |||
| func AssertEventFields(t *testing.T, event appcontext.BeaconEvent, expectedID, expectedType string) { | |||
| if event.ID != expectedID { | |||
| t.Errorf("Expected event ID '%s', got '%s'", expectedID, event.ID) | |||
| } | |||
| @@ -232,8 +232,8 @@ func CleanupTestParsers(registry *model.ParserRegistry) { | |||
| } | |||
| // CreateTestBeaconEvent creates a test beacon event | |||
| func CreateTestBeaconEvent(id, eventType string) model.BeaconEvent { | |||
| return model.BeaconEvent{ | |||
| func CreateTestBeaconEvent(id, eventType string) appcontext.BeaconEvent { | |||
| return appcontext.BeaconEvent{ | |||
| ID: id, | |||
| Type: eventType, | |||
| Battery: 100, | |||
| @@ -3,8 +3,8 @@ package location | |||
| import ( | |||
| "testing" | |||
| "github.com/AFASystems/presence/internal/pkg/common/appcontext" | |||
| "github.com/AFASystems/presence/internal/pkg/common/utils" | |||
| "github.com/AFASystems/presence/internal/pkg/model" | |||
| ) | |||
| // Test location algorithm scoring formula: seenW + (rssiW * (1.0 - (rssi / -100.0))) | |||
| @@ -30,7 +30,7 @@ func TestLocationScoringFormula(t *testing.T) { | |||
| } | |||
| func TestCalculateDistance_ForLocation(t *testing.T) { | |||
| adv := model.BeaconAdvertisement{ | |||
| adv := appcontext.BeaconAdvertisement{ | |||
| RSSI: -65, | |||
| TXPower: "C5", | |||
| } | |||
| @@ -4,11 +4,12 @@ import ( | |||
| "encoding/json" | |||
| "testing" | |||
| "github.com/AFASystems/presence/internal/pkg/common/appcontext" | |||
| "github.com/AFASystems/presence/internal/pkg/model" | |||
| ) | |||
| func TestBeaconEvent_Hash(t *testing.T) { | |||
| e := model.BeaconEvent{ | |||
| e := appcontext.BeaconEvent{ | |||
| ID: "beacon-1", | |||
| Name: "beacon-1", | |||
| Type: "iBeacon", | |||
| @@ -28,8 +29,8 @@ func TestBeaconEvent_Hash(t *testing.T) { | |||
| } | |||
| func TestBeaconEvent_Hash_BatteryRounded(t *testing.T) { | |||
| e1 := model.BeaconEvent{ID: "1", Battery: 84, Event: 1} | |||
| e2 := model.BeaconEvent{ID: "1", Battery: 89, Event: 1} | |||
| e1 := appcontext.BeaconEvent{ID: "1", Battery: 84, Event: 1} | |||
| e2 := appcontext.BeaconEvent{ID: "1", Battery: 89, Event: 1} | |||
| hash1 := e1.Hash() | |||
| hash2 := e2.Hash() | |||
| // Battery is rounded to nearest 10, so 84 and 89 should produce same hash | |||
| @@ -39,7 +40,7 @@ func TestBeaconEvent_Hash_BatteryRounded(t *testing.T) { | |||
| } | |||
| func TestBeaconEvent_ToJSON(t *testing.T) { | |||
| e := model.BeaconEvent{ | |||
| e := appcontext.BeaconEvent{ | |||
| ID: "beacon-1", | |||
| Name: "Test", | |||
| Type: "iBeacon", | |||
| @@ -49,7 +50,7 @@ func TestBeaconEvent_ToJSON(t *testing.T) { | |||
| if err != nil { | |||
| t.Fatalf("ToJSON failed: %v", err) | |||
| } | |||
| var decoded model.BeaconEvent | |||
| var decoded appcontext.BeaconEvent | |||
| if err := json.Unmarshal(data, &decoded); err != nil { | |||
| t.Fatalf("Failed to unmarshal: %v", err) | |||
| } | |||
| @@ -3,6 +3,7 @@ package utils | |||
| import ( | |||
| "testing" | |||
| "github.com/AFASystems/presence/internal/pkg/common/appcontext" | |||
| "github.com/AFASystems/presence/internal/pkg/common/utils" | |||
| "github.com/AFASystems/presence/internal/pkg/model" | |||
| ) | |||
| @@ -80,17 +81,17 @@ func TestRemoveFlagBytes_TooShort(t *testing.T) { | |||
| func TestCalculateDistance(t *testing.T) { | |||
| tests := []struct { | |||
| name string | |||
| rssi int64 | |||
| name string | |||
| rssi int64 | |||
| txPower string | |||
| }{ | |||
| {"typical beacon", -65, "C5"}, // -59 in two's complement | |||
| {"typical beacon", -65, "C5"}, // -59 in two's complement | |||
| {"weak signal", -90, "C5"}, | |||
| {"strong signal", -40, "C5"}, | |||
| } | |||
| for _, tt := range tests { | |||
| t.Run(tt.name, func(t *testing.T) { | |||
| adv := model.BeaconAdvertisement{RSSI: tt.rssi, TXPower: tt.txPower} | |||
| adv := appcontext.BeaconAdvertisement{RSSI: tt.rssi, TXPower: tt.txPower} | |||
| d := utils.CalculateDistance(adv) | |||
| if d < 0 { | |||
| t.Errorf("Distance should be non-negative, got %f", d) | |||
| @@ -103,7 +104,7 @@ func TestLoopADStructures_NoParsers(t *testing.T) { | |||
| registry := &model.ParserRegistry{ParserList: make(map[string]model.BeaconParser)} | |||
| data := []byte{0x02, 0x01, 0x06} | |||
| indices := utils.ParseADFast(data) | |||
| event := utils.LoopADStructures(data, indices, "beacon-1", registry) | |||
| event := utils.LoopADStructures(data, indices, "beacon-1", registry, "") | |||
| if event.ID != "" { | |||
| t.Errorf("Expected empty event with no parsers, got ID %s", event.ID) | |||
| } | |||