From 7e75e8309f0217b8d5aebc1b69a74e2c648b1786 Mon Sep 17 00:00:00 2001 From: blazSmehov Date: Fri, 19 Dec 2025 13:48:16 +0100 Subject: [PATCH] chore: remove unused files, add dev docker compose and dockerfile for live rebuilds and testing --- ResLevis-Diagram-2.0.drawio | 320 --------- ResLevis-Diagram.drawio | 354 ---------- beacon.csv | 44 -- ...-compose.yaml => docker-compose.prod.yaml} | 0 build/docker-compose.yml | 158 +++++ build/package/Dockerfile.dev | 7 + cmd/bridge/main.go | 1 - gateway.csv | 17 - presence.db | Bin 32768 -> 0 bytes presense.container | 28 - test/README.md | 130 ---- test/beacons_test.go | 560 --------------- test/distance_test.go | 294 -------- test/mqtthandler_test.go | 568 --------------- test/typeMethods_test.go | 644 ------------------ 15 files changed, 165 insertions(+), 2960 deletions(-) delete mode 100644 ResLevis-Diagram-2.0.drawio delete mode 100644 ResLevis-Diagram.drawio delete mode 100644 beacon.csv rename build/{docker-compose.yaml => docker-compose.prod.yaml} (100%) create mode 100644 build/docker-compose.yml create mode 100644 build/package/Dockerfile.dev delete mode 100644 gateway.csv delete mode 100644 presence.db delete mode 100644 presense.container delete mode 100644 test/README.md delete mode 100644 test/beacons_test.go delete mode 100644 test/distance_test.go delete mode 100644 test/mqtthandler_test.go delete mode 100644 test/typeMethods_test.go diff --git a/ResLevis-Diagram-2.0.drawio b/ResLevis-Diagram-2.0.drawio deleted file mode 100644 index fa1650e..0000000 --- a/ResLevis-Diagram-2.0.drawio +++ /dev/null @@ -1,320 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ResLevis-Diagram.drawio b/ResLevis-Diagram.drawio deleted file mode 100644 index 9166bf6..0000000 --- a/ResLevis-Diagram.drawio +++ /dev/null @@ -1,354 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/beacon.csv b/beacon.csv deleted file mode 100644 index 042505c..0000000 --- a/beacon.csv +++ /dev/null @@ -1,44 +0,0 @@ -Position;Floor;RoomName;X;Y;Z;BeaconName;MAC -A01;0;PT-MAGA;200;50;107;BC-01;C3:00:00:57:B9:ED -A02;0;PT-MAGA;600;200;107;BC-02;C3:00:00:57:B9:E9 -A03;0;PT-MAGA;175;1535;107;BC-03;C3:00:00:57:B9:F0 -A04;0;PT-MAGA;620;1535;107;BC-04;C3:00:00:57:B9:E4 -A05;0;PT-MAGA;580;710;72;BC-05;C3:00:00:57:B9:FA -A06;0;PT-MENS;800;1065;142;BC-06;C3:00:00:57:B9:F9 -A07;0;PT-MENS;2165;315;107;BC-07;C3:00:00:57:B9:F8 -A08;0;PT-MENS;2195;50;142;BC-08;C3:00:00:57:B9:F7 -A09;0;PT-MENS;2895;50;142;BC-09;C3:00:00:57:B9:EA -A10;0;PT-MENS;2710;250;78;BC-10;C3:00:00:57:B9:EB -A11;0;PT-AMMI;1585;870;141;BC-11;C3:00:00:57:B9:EE -A12;0;PT-AMMI;1585;1540;141;BC-12;C3:00:00:57:B9:E2 -A13;0;PT-AMMI;2130;875;107;BC-13;C3:00:00:57:B9:E5 -A14;0;PT-AMMI;2095;1540;141;BC-14;C3:00:00:57:B9:D5 -A15;0;PT-AMMI;1875;1200;73;BC-15;C3:00:00:57:B9:EC -A16;0;PT-PROD;2180;875;107;BC-16;C3:00:00:57:B9:D8 -A17;0;PT-PROD;2180;1540;141;BC-17;C3:00:00:57:B9:E1 -A18;0;PT-PROD;2930;880;141;BC-18;C3:00:00:57:B9:F3 -A19;0;PT-PROD;2895;1530;141;BC-19;C3:00:00:57:B9:E0 -A20;0;PT-PROD;2650;1180;107;BC-20;C3:00:00:57:B9:EF -A21;1;P1-NETW;800;1050;107;BC-21;C3:00:00:57:B9:E6 -A22;1;P1-NETW;850;1545;107;BC-22;C3:00:00:57:B9:D4 -A23;1;P1-NETW;1425;1050;107;BC-23;C3:00:00:57:B9:E8 -A24;1;P1-NETW;1400;1530;107;BC-24;C3:00:00:57:B9:F1 -A25;1;P1-NETW;1195;1315;72;BC-25;C3:00:00:57:B9:E7 -A26;1;P1-RIUNI;2190;50;107;BC-26;C3:00:00:57:B9:D6 -A27;1;P1-RIUNI;2180;465;107;BC-27;C3:00:00:57:B9:D7 -A28;1;P1-RIUNI;2890;50;107;BC-28;C3:00:00:57:B9:F6 -A29;1;P1-RIUNI;2525;465;76;BC-29;C3:00:00:57:B9:F2 -A30;1;P1-RIUNI;2540;280;69;BC-30;C3:00:00:57:B9:D3 -A31;1;P1-SOFT;1895;865;107;BC-31;C3:00:00:57:B9:F4 -A32;1;P1-SOFT;1900;1535;107;BC-32;C3:00:00:57:B9:D9 -A33;1;P1-SOFT;2320;870;72;BC-33;C3:00:00:57:B9:F5 -A34;1;P1-SOFT;2330;1530;107;BC-34;C3:00:00:57:B9:DA -A35;1;P1-SOFT;2065;1190;20;BC-35;C3:00:00:57:B9:DB -A36;1;P1-CUCO;2370;865;107;BC-36;C3:00:00:57:B9:DC -A37;1;P1-CUCO;2380;1535;93;BC-37;C3:00:00:57:B9:DD -A38;1;P1-CUCO;2940;870;93;BC-38;C3:00:00:57:B9:E3 -A39;1;P1-CUCO;2905;1540;93;BC-39;C3:00:00:57:B9:DF -A40;1;P1-CUCO;2550;1360;72;BC-40;C3:00:00:57:B9:DE -A41;1;P1-AMOR;830;50;100;BC-41;C3:00:00:39:47:DF -A42;1;P1-DINO;1788;50;117;BC-42;C3:00:00:39:47:E2 -A43;1;TESTER;1026;1050;122;BC-43;C3:00:00:39:47:C4 diff --git a/build/docker-compose.yaml b/build/docker-compose.prod.yaml similarity index 100% rename from build/docker-compose.yaml rename to build/docker-compose.prod.yaml diff --git a/build/docker-compose.yml b/build/docker-compose.yml new file mode 100644 index 0000000..c683b11 --- /dev/null +++ b/build/docker-compose.yml @@ -0,0 +1,158 @@ +version: "2" +services: + db: + image: postgres + container_name: db + restart: always + ports: + - "127.0.0.1:5432:5432" + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + start_period: 30s + + kafdrop: + image: obsidiandynamics/kafdrop + restart: "no" + ports: + - "127.0.0.1:9000:9000" + environment: + KAFKA_BROKERCONNECT: "kafka:29092" + depends_on: + - "kafka" + kafka: + image: apache/kafka:3.9.0 + restart: "no" + ports: + # - "127.0.0.1:2181:2181" + - "127.0.0.1:9092:9092" + - "127.0.0.1:9093:9093" + healthcheck: # <-- ADD THIS BLOCK + test: ["CMD-SHELL", "/opt/kafka/bin/kafka-topics.sh --bootstrap-server 127.0.0.1:9092 --list"] + interval: 10s + timeout: 5s + retries: 10 + start_period: 20s + environment: + KAFKA_NODE_ID: 1 + KAFKA_PROCESS_ROLES: broker,controller + KAFKA_LISTENERS: INTERNAL://:29092,EXTERNAL://:9092,CONTROLLER://127.0.0.1:9093 + KAFKA_ADVERTISED_LISTENERS: INTERNAL://kafka:29092,EXTERNAL://localhost:9092 + KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER + KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT + KAFKA_CONTROLLER_QUORUM_VOTERS: 1@127.0.0.1:9093 + KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 + KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 + KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 + KAFKA_NUM_PARTITIONS: 3 + + kafka-init: + image: apache/kafka:3.9.0 + command: [ "sh", "-c", "ls -l /tmp/create_topic.sh && /tmp/create_topic.sh" ] + depends_on: + kafka: + condition: service_healthy + volumes: + - ./init-scripts/create_topic.sh:/tmp/create_topic.sh + environment: + - TOPIC_NAMES=topic1,topic2,topic3 + + valkey: + image: valkey/valkey:9.0.0 + container_name: valkey + ports: + - "127.0.0.1:6379:6379" + + presense-decoder: + build: + context: ../ + dockerfile: build/package/Dockerfile.dev + image: presense-decoder + container_name: presense-decoder + environment: + - KAFKA_URL=kafka:29092 + depends_on: + kafka-init: + condition: service_completed_successfully + db: + condition: service_healthy + restart: always + volumes: + - ../:/app + command: air --build.cmd "go build -buildvcs=false -o /tmp/decoder ./cmd/decoder" --build.bin "/tmp/decoder" + + presense-server: + build: + context: ../ + dockerfile: build/package/Dockerfile.dev + image: presense-server + container_name: presense-server + environment: + - VALKEY_URL=valkey:6379 + - KAFKA_URL=kafka:29092 + - DBHost=db + - DBUser=postgres + - DBPass=postgres + - DBName=postgres + ports: + - "127.0.0.1:1902:1902" + depends_on: + valkey: + condition: service_started + kafka-init: + condition: service_completed_successfully + db: + condition: service_healthy + restart: always + volumes: + - ../:/app + command: air --build.cmd "go build -buildvcs=false -o /tmp/server ./cmd/server" --build.bin "/tmp/server" + + presense-bridge: + build: + context: ../ + dockerfile: build/package/Dockerfile.dev + image: presense-bridge + container_name: presense-bridge + environment: + - KAFKA_URL=kafka:29092 + - MQTT_HOST=192.168.1.101 + - MQTT_USERNAME=user + - MQTT_PASSWORD=pass + depends_on: + kafka-init: + condition: service_completed_successfully + db: + condition: service_healthy + restart: always + volumes: + - ../:/app + command: air --build.cmd "go build -buildvcs=false -o /tmp/bridge ./cmd/bridge" --build.bin "/tmp/bridge" + + presense-location: + build: + context: ../ + dockerfile: build/package/Dockerfile.dev + image: presense-location + container_name: presense-location + environment: + - KAFKA_URL=kafka:29092 + depends_on: + kafka-init: + condition: service_completed_successfully + db: + condition: service_healthy + restart: always + volumes: + - ../:/app + command: air --build.cmd "go build -buildvcs=false -o /tmp/location ./cmd/location" --build.bin "/tmp/location" + + + diff --git a/build/package/Dockerfile.dev b/build/package/Dockerfile.dev new file mode 100644 index 0000000..9654ace --- /dev/null +++ b/build/package/Dockerfile.dev @@ -0,0 +1,7 @@ +FROM golang:1.25.0 +WORKDIR /app +RUN go install github.com/air-verse/air@latest +# Pre-download dependencies for the whole workspace +COPY go.mod go.sum ./ +RUN go mod download +CMD ["air"] \ No newline at end of file diff --git a/cmd/bridge/main.go b/cmd/bridge/main.go index 56d3bc3..c21a974 100644 --- a/cmd/bridge/main.go +++ b/cmd/bridge/main.go @@ -42,7 +42,6 @@ func mqtthandler(writer *kafka.Writer, topic string, message []byte, appState *a } // fmt.Printf("reading: %+v\n", reading) if !appState.BeaconExists(reading.MAC) { - fmt.Printf("Not tracking beacon: %s\n", reading.MAC) continue } diff --git a/gateway.csv b/gateway.csv deleted file mode 100644 index 0842802..0000000 --- a/gateway.csv +++ /dev/null @@ -1,17 +0,0 @@ -Position;Floor;RoomName;X;Y;Z;GatewayName;MAC -C01;0;PT-MAGA;220;250;13;GU-01;ac:23:3f:c1:dd:3c -C02;0;PT-FORM;825;745;13;GU-02;ac:23:3f:c1:dd:49 -C03;0;PT-LVNS;825;1435;13;GU-03;ac:23:3f:c1:dc:ee -C04;0;PT-RECE;2010;620;13;GU-04;ac:23:3f:c1:dd:40 -C05;0;PT-AMMI;1785;1260;13;GU-05;ac:23:3f:c1:dd:51 -C06;0;PT-PROD;2720;1220;13;GU-06;ac:23:3f:c1:dd:48 -C07;0;PT-BATH;2800;655;13;GU-07;ac:23:3f:c1:dd:50 -C08;0;PT-MENS;2580;490;13;GU-08;ac:23:3f:c1:dc:d3 -C09;1;P1-AMOR;900;50;13;GU-09;ac:23:3f:c1:dd:55 -C10;1;P1-NETW;1310;1440;13;GU-10;ac:23:3f:c1:dc:d1 -C11;1;P1-DINO;1662;480;13;GU-11;ac:23:3f:c1:dc:cb -C12;1;P1-COMM;1575;1455;13;GU-12;ac:23:3f:c1:dc:d2 -C13;1;P1-SOFT;2290;965;13;GU-13;ac:23:3f:c1:dd:31 -C14;1;P1-CUCO;2860;1120;13;GU-14;ac:23:3f:c1:dd:4b -C15;1;P1-BATH;2740;710;13;GU-15;ac:23:3f:c1:dd:4e -C16;1;P1-RIUN;2180;355;13;GU-16;ac:23:3f:c1:dc:cd diff --git a/presence.db b/presence.db deleted file mode 100644 index e73b14ea20122b8bb54ee31bc6cd2b7bb152b533..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 zcmeI)F>b;z6aY{cDC$s&%yeO5Vyk+G4lJCZ%2&U#I8X8RzzQY|~%Azb*FP=TnT+5+Fc;009C72oNAZ zfB*pk tt.expected*1.1 { - t.Errorf("CalculateDistance() = %v, expected around %v", result, tt.expected) - } - }) - } -} - -func TestCalculateDistanceEdgeCases(t *testing.T) { - tests := []struct { - name string - adv model.BeaconAdvertisement - expected float64 - }{ - { - name: "Zero RSSI", - adv: model.BeaconAdvertisement{ - RSSI: 0, - TXPower: "59", - }, - expected: 0.0, - }, - { - name: "Invalid TX power", - adv: model.BeaconAdvertisement{ - RSSI: -50, - TXPower: "XYZ", - }, - expected: 0.0, // twosComp returns 0 for invalid input - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := utils.CalculateDistance(tt.adv) - if result != tt.expected { - t.Errorf("CalculateDistance() = %v, expected %v", result, tt.expected) - } - }) - } -} - -func TestValidateRSSI(t *testing.T) { - tests := []struct { - name string - rssi int64 - expected bool - }{ - { - name: "Valid RSSI - strong signal", - rssi: -30, - expected: true, - }, - { - name: "Valid RSSI - weak signal", - rssi: -100, - expected: true, - }, - { - name: "Valid RSSI - boundary low", - rssi: -120, - expected: true, - }, - { - name: "Valid RSSI - boundary high", - rssi: 0, - expected: true, - }, - { - name: "Invalid RSSI - too strong", - rssi: 10, - expected: false, - }, - { - name: "Invalid RSSI - too weak", - rssi: -130, - expected: false, - }, - { - name: "Invalid RSSI - just below boundary", - rssi: -121, - expected: false, - }, - { - name: "Invalid RSSI - just above boundary", - rssi: 1, - expected: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := utils.ValidateRSSI(tt.rssi) - if result != tt.expected { - t.Errorf("ValidateRSSI() = %v, expected %v", result, tt.expected) - } - }) - } -} - -func TestValidateTXPower(t *testing.T) { - tests := []struct { - name string - txPower string - expected bool - }{ - { - name: "Valid TX power - positive", - txPower: "59", - expected: true, - }, - { - name: "Valid TX power - negative", - txPower: "C6", - expected: true, - }, - { - name: "Valid TX power - zero", - txPower: "00", - expected: true, - }, - { - name: "Valid TX power - max positive", - txPower: "7F", - expected: true, - }, - { - name: "Valid TX power - max negative", - txPower: "80", - expected: true, - }, - { - name: "Valid TX power - boundary negative", - txPower: "81", // -127 - expected: true, - }, - { - name: "Invalid TX power string", - txPower: "XYZ", - expected: true, // twosComp returns 0, which is valid - }, - { - name: "Empty TX power", - txPower: "", - expected: true, // twosComp returns 0, which is valid - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := utils.ValidateTXPower(tt.txPower) - if result != tt.expected { - t.Errorf("ValidateTXPower() = %v, expected %v", result, tt.expected) - } - }) - } -} - -func TestCalculateDistanceConsistency(t *testing.T) { - // Test that the function is deterministic - adv := model.BeaconAdvertisement{ - RSSI: -65, - TXPower: "59", - } - - result1 := utils.CalculateDistance(adv) - result2 := utils.CalculateDistance(adv) - - if result1 != result2 { - t.Errorf("CalculateDistance() is not deterministic: %v != %v", result1, result2) - } -} - -func TestCalculateDistanceRealWorldScenarios(t *testing.T) { - scenarios := []struct { - name string - rssi int64 - txPower string - expectedRange [2]float64 // min, max expected range - }{ - { - name: "Beacon very close (1m)", - rssi: -45, - txPower: "59", // 89 decimal - expectedRange: [2]float64{0.5, 1.5}, - }, - { - name: "Beacon at medium distance (5m)", - rssi: -75, - txPower: "59", - expectedRange: [2]float64{3.0, 8.0}, - }, - { - name: "Beacon far away (15m)", - rssi: -95, - txPower: "59", - expectedRange: [2]float64{10.0, 25.0}, - }, - } - - for _, scenario := range scenarios { - t.Run(scenario.name, func(t *testing.T) { - adv := model.BeaconAdvertisement{ - RSSI: scenario.rssi, - TXPower: scenario.txPower, - } - result := utils.CalculateDistance(adv) - - if result < scenario.expectedRange[0] || result > scenario.expectedRange[1] { - t.Errorf("CalculateDistance() = %v, expected range %v", result, scenario.expectedRange) - } - }) - } -} - -// Benchmark tests -func BenchmarkCalculateDistance(b *testing.B) { - adv := model.BeaconAdvertisement{ - RSSI: -65, - TXPower: "59", - } - - for i := 0; i < b.N; i++ { - utils.CalculateDistance(adv) - } -} diff --git a/test/mqtthandler_test.go b/test/mqtthandler_test.go deleted file mode 100644 index 24cc7cc..0000000 --- a/test/mqtthandler_test.go +++ /dev/null @@ -1,568 +0,0 @@ -package mqtthandler - -import ( - "context" - "encoding/json" - "testing" - - "github.com/AFASystems/presence/internal/pkg/model" - "github.com/segmentio/kafka-go" -) - -// MockKafkaWriter implements a mock for kafka.Writer interface -type MockKafkaWriter struct { - Messages []kafka.Message - ShouldFail bool - WriteCount int -} - -func (m *MockKafkaWriter) WriteMessages(ctx context.Context, msgs ...kafka.Message) error { - m.WriteCount++ - if m.ShouldFail { - return &kafka.Error{ - Err: &ErrMockWrite{}, - Cause: nil, - Context: msgs[0], - } - } - m.Messages = append(m.Messages, msgs...) - return nil -} - -// ErrMockWrite is a mock error for testing -type ErrMockWrite struct{} - -func (e *ErrMockWrite) Error() string { - return "mock write error" -} - -// Mock Kafka Close method (required for Writer interface) -func (m *MockKafkaWriter) Close() error { - return nil -} - -func TestMqttHandlerJSONArrayInput(t *testing.T) { - tests := []struct { - name string - topicName []byte - message []byte - expectedMsgs int - shouldFail bool - }{ - { - name: "Valid JSON array with multiple readings", - topicName: []byte("presence/gateway-001"), - message: []byte(`[{"timestamp":"2023-01-01T00:00:00Z","type":"Beacon","mac":"AA:BB:CC:DD:EE:FF","rssi":-65,"rawData":"0201060303E1FF1200001234"}]`), - expectedMsgs: 1, - shouldFail: false, - }, - { - name: "JSON array with multiple beacons", - topicName: []byte("presence/gateway-002"), - message: []byte(`[{"timestamp":"2023-01-01T00:00:00Z","type":"Beacon","mac":"AA:BB:CC:DD:EE:FF","rssi":-65,"rawData":"0201060303E1FF1200001234"},{"timestamp":"2023-01-01T00:00:01Z","type":"Beacon","mac":"11:22:33:44:55:66","rssi":-75,"rawData":"0201060303E1FF1200005678"}]`), - expectedMsgs: 2, - shouldFail: false, - }, - { - name: "JSON array with gateway reading (should be skipped)", - topicName: []byte("presence/gateway-003"), - message: []byte(`[{"timestamp":"2023-01-01T00:00:00Z","type":"Gateway","mac":"GG:AA:TT:EE:WA:Y","rssi":-20,"rawData":"gateway-data"},{"timestamp":"2023-01-01T00:00:01Z","type":"Beacon","mac":"AA:BB:CC:DD:EE:FF","rssi":-65,"rawData":"0201060303E1FF1200001234"}]`), - expectedMsgs: 1, // Only beacon should be processed - shouldFail: false, - }, - { - name: "JSON array with only gateways (should be skipped)", - topicName: []byte("presence/gateway-004"), - message: []byte(`[{"timestamp":"2023-01-01T00:00:00Z","type":"Gateway","mac":"GG:AA:TT:EE:WA:Y","rssi":-20,"rawData":"gateway-data"}]`), - expectedMsgs: 0, // All gateways should be skipped - shouldFail: false, - }, - { - name: "Invalid JSON array", - topicName: []byte("presence/gateway-005"), - message: []byte(`[{"timestamp":"2023-01-01T00:00:00Z","type":"Beacon","mac":"AA:BB:CC:DD:EE:FF","rssi":-65,"rawData":"0201060303E1FF1200001234"`), - expectedMsgs: 0, - shouldFail: false, // Should not panic, just log error - }, - { - name: "Empty JSON array", - topicName: []byte("presence/gateway-006"), - message: []byte(`[]`), - expectedMsgs: 0, - shouldFail: false, - }, - { - name: "JSON array with null readings", - topicName: []byte("presence/gateway-007"), - message: []byte(`[null]`), - expectedMsgs: 0, - shouldFail: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockWriter := &MockKafkaWriter{ - Messages: make([]kafka.Message, 0), - ShouldFail: tt.shouldFail, - } - - // Capture log output (you might want to use a test logger here) - MqttHandler(mockWriter, tt.topicName, tt.message) - - if len(mockWriter.Messages) != tt.expectedMsgs { - t.Errorf("MqttHandler() wrote %d messages, expected %d", len(mockWriter.Messages), tt.expectedMsgs) - } - - // Verify message content if we expected messages - if tt.expectedMsgs > 0 && len(mockWriter.Messages) > 0 { - for i, msg := range mockWriter.Messages { - var adv model.BeaconAdvertisement - err := json.Unmarshal(msg.Value, &adv) - if err != nil { - t.Errorf("MqttHandler() message %d is not valid BeaconAdvertisement JSON: %v", i, err) - } - - // Verify hostname extraction - expectedHostname := "gateway-007" // Extracted from topicName - if adv.Hostname != expectedHostname { - t.Errorf("MqttHandler() hostname = %v, expected %v", adv.Hostname, expectedHostname) - } - } - } - }) - } -} - -func TestMqttHandlerCSVInput(t *testing.T) { - tests := []struct { - name string - topicName []byte - message []byte - shouldProcess bool - }{ - { - name: "Valid CSV format", - topicName: []byte("presence/gateway-001"), - message: []byte("timestamp,AA:BB:CC:DD:EE:FF,-65,0201060303E1FF1200001234,1001,field6\n"), - shouldProcess: true, - }, - { - name: "CSV with button data", - topicName: []byte("presence/gateway-002"), - message: []byte("timestamp,AA:BB:CC:DD:EE:FF,-65,02010612FF5901C0012345678,1234,field6\n"), - shouldProcess: true, - }, - { - name: "CSV with insufficient fields", - topicName: []byte("presence/gateway-003"), - message: []byte("timestamp,AA:BB:CC:DD:EE:FF,-65,0201060303E1FF1200001234\n"), - shouldProcess: false, // Should log error and return early - }, - { - name: "Empty CSV", - topicName: []byte("presence/gateway-004"), - message: []byte(""), - shouldProcess: false, - }, - { - name: "CSV with wrong field count", - topicName: []byte("presence/gateway-005"), - message: []byte("field1,field2,field3\n"), - shouldProcess: false, - }, - { - name: "CSV with non-numeric RSSI", - topicName: []byte("presence/gateway-006"), - message: []byte("timestamp,AA:BB:CC:DD:EE:FF,invalid,0201060303E1FF1200001234,1001,field6\n"), - shouldProcess: false, // Should fail on ParseInt - }, - { - name: "CSV with non-numeric field6", - topicName: []byte("presence/gateway-007"), - message: []byte("timestamp,AA:BB:CC:DD:EE:FF,-65,0201060303E1FF1200001234,1001,invalid\n"), - shouldProcess: false, // Should fail on Atoi - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockWriter := &MockKafkaWriter{ - Messages: make([]kafka.Message, 0), - ShouldFail: false, - } - - // Note: The CSV handler in the original code has an os.Exit(2) which makes it untestable - // This test will fail due to os.Exit, but demonstrates the intended test structure - // In a real scenario, you'd want to refactor the code to avoid os.Exit - defer func() { - if r := recover(); r != nil { - // Expected due to os.Exit in original code - if tt.shouldProcess { - t.Errorf("MqttHandler() should not panic for valid CSV input: %v", r) - } - } - }() - - // This will panic due to os.Exit(2) in the original code when field6 is invalid - // In a real refactor, you'd replace os.Exit with error return - if !tt.shouldProcess && string(tt.message) == "timestamp,AA:BB:CC:DD:EE:FF,-65,0201060303E1FF1200001234,1001,invalid\n" { - // Skip the case that will definitely panic - return - } - - MqttHandler(mockWriter, tt.topicName, tt.message) - - // CSV processing doesn't write to Kafka in the current implementation - if len(mockWriter.Messages) != 0 { - t.Errorf("MqttHandler() CSV processing should not write to Kafka, but wrote %d messages", len(mockWriter.Messages)) - } - }) - } -} - -func TestParseButtonState(t *testing.T) { - tests := []struct { - name string - raw string - expected int64 - }{ - { - name: "Ingics button format - minimal length", - raw: "0201060303E1FF12", - expected: 0, // Too short for button field - }, - { - name: "Ingics button format - exact length", - raw: "0201060303E1FF123456", - expected: 0x3456, // 13398 in decimal - }, - { - name: "Ingics button format - longer", - raw: "0201060303E1FF12000012345678AB", - expected: 0x78AB, // 30891 in decimal - }, - { - name: "Ingics button format - zero button", - raw: "0201060303E1FF1200000000", - expected: 0, - }, - { - name: "Ingics button format - max button", - raw: "0201060303E1FF12FFFFFFFF", - expected: 0xFFFF, // 65535 in decimal - }, - { - name: "Minew button format - minimal length", - raw: "02010612FF590", - expected: 0, // Too short for counter field - }, - { - name: "Minew button format - exact length", - raw: "02010612FF590112", - expected: 0x12, // 18 in decimal - }, - { - name: "Minew button format - longer", - raw: "02010612FF5901C0012345678", - expected: 0x78, // 120 in decimal - }, - { - name: "Minew button format - zero counter", - raw: "02010612FF5901C000", - expected: 0, - }, - { - name: "Minew button format - max counter", - raw: "02010612FF5901C0FF", - expected: 0xFF, // 255 in decimal - }, - { - name: "Invalid prefix", - raw: "0201060303E1FE120000123456", - expected: 0, - }, - { - name: "Invalid hex characters", - raw: "0201060303E1FF12ZZZZ", - expected: 0, - }, - { - name: "Empty string", - raw: "", - expected: 0, - }, - { - name: "Single character", - raw: "0", - expected: 0, - }, - { - name: "Non-hex characters mixed", - raw: "0201060303E1FF12GHIJ", - expected: 0, - }, - { - name: "Lowercase hex", - raw: "0201060303e1ff120000123456", - expected: 0, // Should be converted to uppercase - }, - { - name: "Mixed case hex", - raw: "0201060303e1FF120000123456", - expected: 0x3456, // Should work after case conversion - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := parseButtonState(tt.raw) - if result != tt.expected { - t.Errorf("parseButtonState() = %v, expected %v", result, tt.expected) - } - }) - } -} - -func TestParseButtonStateEdgeCases(t *testing.T) { - // Test that Ingics format is checked before Minew format - ingicsRaw := "0201060303E1FF123456" - minewRaw := "02010612FF590112" - - ingicsResult := parseButtonState(ingicsRaw) - minewResult := parseButtonState(minewRaw) - - // Both should work, but Ingics should use bytes 34:38, Minew should use bytes 22:24 - if ingicsResult != 0x3456 { - t.Errorf("parseButtonState() Ingics format failed: got %v, want %v", ingicsResult, 0x3456) - } - - if minewResult != 0x12 { - t.Errorf("parseButtonState() Minew format failed: got %v, want %v", minewResult, 0x12) - } - - // Test with overlapping patterns (unlikely but good to test) - overlapRaw := "0201060303E1FF122FF590112" - overlapResult := parseButtonState(overlapRaw) - // Should match Ingics pattern and use bytes 34:38 - expectedOverlap := int64(0) // There are no bytes 34:38 in this string - if overlapResult != expectedOverlap { - t.Errorf("parseButtonState() overlap case: got %v, want %v", overlapResult, expectedOverlap) - } -} - -func TestHostnameExtraction(t *testing.T) { - tests := []struct { - name string - topicName []byte - expectedHost string - }{ - { - name: "Simple topic", - topicName: []byte("presence/gateway-001"), - expectedHost: "gateway-001", - }, - { - name: "Topic with multiple segments", - topicName: []byte("home/office/floor3/gateway-A123"), - expectedHost: "home", - }, - { - name: "Topic with numbers only", - topicName: []byte("12345"), - expectedHost: "12345", - }, - { - name: "Single segment topic", - topicName: []byte("singlegateway"), - expectedHost: "singlegateway", - }, - { - name: "Topic with empty segments", - topicName: []byte("//gateway//001//"), - expectedHost: "", // First non-empty segment after split - }, - { - name: "Empty topic", - topicName: []byte(""), - expectedHost: "", - }, - { - name: "Topic with special characters", - topicName: []byte("presence/gateway-with-dashes_and_underscores"), - expectedHost: "presence", - }, - { - name: "Topic starting with slash", - topicName: []byte("/presence/gateway-001"), - expectedHost: "", // First segment is empty - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockWriter := &MockKafkaWriter{ - Messages: make([]kafka.Message, 0), - } - - // Create a simple JSON message that will be processed - message := []byte(`[{"timestamp":"2023-01-01T00:00:00Z","type":"Beacon","mac":"AA:BB:CC:DD:EE:FF","rssi":-65,"rawData":"0201060303E1FF1200001234"}]`) - - MqttHandler(mockWriter, tt.topicName, message) - - if len(mockWriter.Messages) > 0 { - var adv model.BeaconAdvertisement - err := json.Unmarshal(mockWriter.Messages[0].Value, &adv) - if err != nil { - t.Errorf("Failed to unmarshal Kafka message: %v", err) - return - } - - if adv.Hostname != tt.expectedHost { - t.Errorf("Hostname extraction = %v, expected %v", adv.Hostname, tt.expectedHost) - } - } - }) - } -} - -func TestKafkaWriteFailure(t *testing.T) { - mockWriter := &MockKafkaWriter{ - Messages: make([]kafka.Message, 0), - ShouldFail: true, - } - - topicName := []byte("presence/test-gateway") - message := []byte(`[{"timestamp":"2023-01-01T00:00:00Z","type":"Beacon","mac":"AA:BB:CC:DD:EE:FF","rssi":-65,"rawData":"0201060303E1FF1200001234"}]`) - - // This should handle the write error gracefully (it sleeps for 1 second) - MqttHandler(mockWriter, topicName, message) - - // No messages should have been written successfully - if len(mockWriter.Messages) != 0 { - t.Errorf("Expected 0 messages on write failure, got %d", len(mockWriter.Messages)) - } - - // Should have attempted to write - if mockWriter.WriteCount != 1 { - t.Errorf("Expected 1 write attempt, got %d", mockWriter.WriteCount) - } -} - -func TestMessageMarshaling(t *testing.T) { - tests := []struct { - name string - reading model.RawReading - }{ - { - name: "Standard beacon reading", - reading: model.RawReading{ - Timestamp: "2023-01-01T00:00:00Z", - Type: "Beacon", - MAC: "AA:BB:CC:DD:EE:FF", - RSSI: -65, - RawData: "0201060303E1FF1200001234", - }, - }, - { - name: "Beacon with special characters in MAC", - reading: model.RawReading{ - Timestamp: "2023-01-01T00:00:00Z", - Type: "Beacon", - MAC: "AA:BB:CC:DD:EE:FF", - RSSI: -75, - RawData: "02010612FF5901C0012345678", - }, - }, - { - name: "Beacon with extreme RSSI values", - reading: model.RawReading{ - Timestamp: "2023-01-01T00:00:00Z", - Type: "Beacon", - MAC: "11:22:33:44:55:66", - RSSI: -120, - RawData: "0201060303E1FF120000ABCD", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockWriter := &MockKafkaWriter{ - Messages: make([]kafka.Message, 0), - } - - // Create JSON array with our test reading - readings := []model.RawReading{tt.reading} - message, err := json.Marshal(readings) - if err != nil { - t.Fatalf("Failed to marshal test reading: %v", err) - } - - topicName := []byte("presence/test-gateway") - MqttHandler(mockWriter, topicName, message) - - if len(mockWriter.Messages) != 1 { - t.Errorf("Expected 1 message, got %d", len(mockWriter.Messages)) - return - } - - // Verify the message can be unmarshaled back to BeaconAdvertisement - var adv model.BeaconAdvertisement - err = json.Unmarshal(mockWriter.Messages[0].Value, &adv) - if err != nil { - t.Errorf("Failed to unmarshal Kafka message: %v", err) - return - } - - // Verify fields match the original reading - if adv.MAC != tt.reading.MAC { - t.Errorf("MAC mismatch: got %v, want %v", adv.MAC, tt.reading.MAC) - } - if adv.RSSI != int64(tt.reading.RSSI) { - t.Errorf("RSSI mismatch: got %v, want %v", adv.RSSI, tt.reading.RSSI) - } - if adv.Data != tt.reading.RawData { - t.Errorf("Data mismatch: got %v, want %v", adv.Data, tt.reading.RawData) - } - }) - } -} - -// Benchmark tests -func BenchmarkParseButtonState(b *testing.B) { - raw := "0201060303E1FF12000012345678AB" - for i := 0; i < b.N; i++ { - parseButtonState(raw) - } -} - -func BenchmarkMqttHandlerJSON(b *testing.B) { - mockWriter := &MockKafkaWriter{ - Messages: make([]kafka.Message, 0), - } - - topicName := []byte("presence/benchmark-gateway") - message := []byte(`[{"timestamp":"2023-01-01T00:00:00Z","type":"Beacon","mac":"AA:BB:CC:DD:EE:FF","rssi":-65,"rawData":"0201060303E1FF1200001234"}]`) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - MqttHandler(mockWriter, topicName, message) - mockWriter.Messages = mockWriter.Messages[:0] // Reset messages - } -} - -func BenchmarkMqttHandlerMultipleBeacons(b *testing.B) { - mockWriter := &MockKafkaWriter{ - Messages: make([]kafka.Message, 0), - } - - topicName := []byte("presence/benchmark-gateway") - message := []byte(`[{"timestamp":"2023-01-01T00:00:00Z","type":"Beacon","mac":"AA:BB:CC:DD:EE:FF","rssi":-65,"rawData":"0201060303E1FF1200001234"},{"timestamp":"2023-01-01T00:00:01Z","type":"Beacon","mac":"11:22:33:44:55:66","rssi":-75,"rawData":"02010612FF5901C0012345678"}]`) - - b.ResetTimer() - for i := 0; i < b.N; i++ { - MqttHandler(mockWriter, topicName, message) - mockWriter.Messages = mockWriter.Messages[:0] // Reset messages - } -} \ No newline at end of file diff --git a/test/typeMethods_test.go b/test/typeMethods_test.go deleted file mode 100644 index 35ca1a2..0000000 --- a/test/typeMethods_test.go +++ /dev/null @@ -1,644 +0,0 @@ -package model - -import ( - "testing" -) - -func TestBeaconEventHash(t *testing.T) { - tests := []struct { - name string - be BeaconEvent - expected []byte - }{ - { - name: "Basic beacon event", - be: BeaconEvent{ - ID: "beacon-1", - Name: "Test Beacon", - Type: "Ingics", - Battery: 1000, - Event: 1, - }, - expected: nil, // We'll test that it produces a consistent hash - }, - { - name: "Same beacon with different battery should produce same hash", - be: BeaconEvent{ - ID: "beacon-1", - Name: "Test Beacon", - Type: "Ingics", - Battery: 1009, // 1000 + 9, should round to 1000 - Event: 1, - }, - expected: nil, - }, - { - name: "Different ID should produce different hash", - be: BeaconEvent{ - ID: "beacon-2", - Name: "Test Beacon", - Type: "Ingics", - Battery: 1000, - Event: 1, - }, - expected: nil, - }, - { - name: "Different event should produce different hash", - be: BeaconEvent{ - ID: "beacon-1", - Name: "Test Beacon", - Type: "Ingics", - Battery: 1000, - Event: 2, - }, - expected: nil, - }, - { - name: "Zero values", - be: BeaconEvent{ - ID: "", - Name: "", - Type: "", - Battery: 0, - Event: 0, - }, - expected: nil, - }, - { - name: "Special characters", - be: BeaconEvent{ - ID: "beacon!@#$%^&*()", - Name: "Test\nBeacon\tWith\tTabs", - Type: "Special-Type_123", - Battery: 1000, - Event: 1, - }, - expected: nil, - }, - } - - // Test that Hash produces consistent results - hashes := make([][]byte, len(tests)) - - for i, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - hashes[i] = tt.be.Hash() - - // Hash should always be 32 bytes for SHA256 - if len(hashes[i]) != 32 { - t.Errorf("Hash() length = %v, expected 32", len(hashes[i])) - } - - // Hash should not be empty unless all fields are empty - if len(hashes[i]) == 0 && (tt.be.ID != "" || tt.be.Name != "" || tt.be.Type != "") { - t.Errorf("Hash() should not be empty for non-empty beacon event") - } - }) - } - - // Test that same input produces same hash - be1 := BeaconEvent{ - ID: "test-beacon", - Name: "Test", - Type: "Ingics", - Battery: 1000, - Event: 1, - } - - hash1 := be1.Hash() - hash2 := be1.Hash() - - if string(hash1) != string(hash2) { - t.Errorf("Hash() should be deterministic: %v != %v", hash1, hash2) - } - - // Test battery rounding - beBattery1 := BeaconEvent{ - ID: "test-beacon", - Name: "Test", - Type: "Ingics", - Battery: 1005, // Should round to 1000 - Event: 1, - } - - beBattery2 := BeaconEvent{ - ID: "test-beacon", - Name: "Test", - Type: "Ingics", - Battery: 1000, - Event: 1, - } - - hashBattery1 := beBattery1.Hash() - hashBattery2 := beBattery2.Hash() - - if string(hashBattery1) != string(hashBattery2) { - t.Errorf("Hash() with battery rounding should be same: %v != %v", hashBattery1, hashBattery2) - } -} - -func TestBeaconEventToJSON(t *testing.T) { - tests := []struct { - name string - be BeaconEvent - expectedError bool - }{ - { - name: "Valid beacon event", - be: BeaconEvent{ - ID: "beacon-1", - Name: "Test Beacon", - Type: "Ingics", - Battery: 1000, - Event: 1, - }, - expectedError: false, - }, - { - name: "Empty beacon event", - be: BeaconEvent{}, - expectedError: false, - }, - { - name: "Beacon with special characters", - be: BeaconEvent{ - ID: "beacon-with-special-chars!@#$%", - Name: "Name with unicode: 测试", - Type: "Type-With-Dashes_and_underscores", - Battery: 12345, - Event: 255, - }, - expectedError: false, - }, - { - name: "Beacon with maximum values", - be: BeaconEvent{ - ID: "max-beacon", - Name: "Maximum Values Test", - Type: "MaxType", - Battery: 0xFFFFFFFF, // Max uint32 - Event: 2147483647, // Max int32 - }, - expectedError: false, - }, - { - name: "Zero values", - be: BeaconEvent{ - ID: "", - Name: "", - Type: "", - Battery: 0, - Event: 0, - }, - expectedError: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := tt.be.ToJSON() - - if (err != nil) != tt.expectedError { - t.Errorf("ToJSON() error = %v, expectedError %v", err, tt.expectedError) - return - } - - if !tt.expectedError { - // Result should not be nil - if result == nil { - t.Error("ToJSON() result should not be nil") - return - } - - // Result should not be empty JSON - if string(result) == "null" { - t.Error("ToJSON() result should not be 'null'") - } - - // Basic JSON validation - should start and end with braces for object - if len(result) > 0 && result[0] != '{' && result[0] != '[' { - t.Errorf("ToJSON() result should be valid JSON, got: %s", string(result)) - } - } - }) - } -} - -func TestConvertStructToMap(t *testing.T) { - tests := []struct { - name string - input any - expectedError bool - }{ - { - name: "Valid BeaconEvent", - input: BeaconEvent{ID: "test", Type: "Ingics"}, - expectedError: false, - }, - { - name: "Valid HTTPLocation", - input: HTTPLocation{Method: "POST", ID: "test"}, - expectedError: false, - }, - { - name: "Valid struct", - input: struct{ Name string }{Name: "test"}, - expectedError: false, - }, - { - name: "Nil input", - input: nil, - expectedError: false, - }, - { - name: "String input", - input: "test string", - expectedError: false, - }, - { - name: "Map input", - input: map[string]any{"test": "value"}, - expectedError: false, - }, - { - name: "Slice input", - input: []string{"test1", "test2"}, - expectedError: false, - }, - { - name: "Complex struct with nested structures", - input: struct { - SimpleField string - Nested struct { - InnerField int - } - SliceField []string - MapField map[string]any - }{ - SimpleField: "test", - Nested: struct{ InnerField int }{InnerField: 123}, - SliceField: []string{"a", "b", "c"}, - MapField: map[string]any{"key": "value"}, - }, - expectedError: false, - }, - { - name: "Struct with channel field", - input: struct{ Ch chan int }{Ch: make(chan int)}, - expectedError: true, // Channels cannot be marshaled to JSON - }, - { - name: "Struct with function field", - input: struct{ Func func() }{Func: func() {}}, - expectedError: true, // Functions cannot be marshaled to JSON - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := convertStructToMap(tt.input) - - if (err != nil) != tt.expectedError { - t.Errorf("convertStructToMap() error = %v, expectedError %v", err, tt.expectedError) - return - } - - if !tt.expectedError { - // Result should be a map - if result == nil && tt.input != nil { - t.Error("convertStructToMap() result should not be nil for non-nil input") - } - - // For valid inputs, result should be a map - if tt.input != nil { - if _, ok := result.(map[string]any); !ok && result != nil { - t.Errorf("convertStructToMap() result should be a map[string]any, got %T", result) - } - } - } - }) - } -} - -func TestHTTPLocationRedisHashable(t *testing.T) { - tests := []struct { - name string - location HTTPLocation - expectedError bool - }{ - { - name: "Valid location", - location: HTTPLocation{ - Method: "POST", - PreviousConfidentLocation: "room1", - Distance: 5.5, - ID: "beacon-123", - Location: "room2", - LastSeen: 1634567890, - }, - expectedError: false, - }, - { - name: "Minimal location", - location: HTTPLocation{ - Method: "GET", - ID: "beacon-1", - }, - expectedError: false, - }, - { - name: "Zero values", - location: HTTPLocation{}, - expectedError: false, - }, - { - name: "Location with special characters", - location: HTTPLocation{ - Method: "CUSTOM", - ID: "beacon-with-special-chars!@#$%", - Location: "Room-with-unicode: 测试", - Distance: -123.456, // Negative distance - }, - expectedError: false, - }, - { - name: "Maximum values", - location: HTTPLocation{ - Method: "MAX", - PreviousConfidentLocation: "max-room", - Distance: 9223372036854775807, // Max int64 as float64 - ID: "max-beacon-id-12345678901234567890", - Location: "max-location-name", - LastSeen: 9223372036854775807, // Max int64 - }, - expectedError: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := tt.location.RedisHashable() - - if (err != nil) != tt.expectedError { - t.Errorf("HTTPLocation.RedisHashable() error = %v, expectedError %v", err, tt.expectedError) - return - } - - if !tt.expectedError { - // Result should be a map - if result == nil { - t.Error("HTTPLocation.RedisHashable() result should not be nil") - return - } - - resultMap, ok := result.(map[string]any) - if !ok { - t.Errorf("HTTPLocation.RedisHashable() result should be a map[string]any, got %T", result) - return - } - - // Check that expected fields are present - expectedFields := []string{"method", "previous_confident_location", "distance", "id", "location", "last_seen"} - for _, field := range expectedFields { - if _, exists := resultMap[field]; !exists { - t.Errorf("HTTPLocation.RedisHashable() missing expected field: %s", field) - } - } - - // Check JSON tags are respected - if _, exists := resultMap["Method"]; exists { - t.Error("HTTPLocation.RedisHashable() should use JSON field names, not struct field names") - } - - if _, exists := resultMap["method"]; !exists { - t.Error("HTTPLocation.RedisHashable() should contain 'method' field (JSON tag)") - } - } - }) - } -} - -func TestBeaconEventRedisHashable(t *testing.T) { - tests := []struct { - name string - be BeaconEvent - expectedError bool - }{ - { - name: "Valid beacon event", - be: BeaconEvent{ - ID: "beacon-123", - Name: "Test Beacon", - Type: "Ingics", - Battery: 1000, - Event: 1, - }, - expectedError: false, - }, - { - name: "Minimal beacon event", - be: BeaconEvent{ - ID: "test", - }, - expectedError: false, - }, - { - name: "Zero values", - be: BeaconEvent{}, - expectedError: false, - }, - { - name: "Beacon event with special characters", - be: BeaconEvent{ - ID: "beacon-!@#$%^&*()", - Name: "Name with unicode: 测试", - Type: "Special-Type_123", - Battery: 12345, - Event: 255, - }, - expectedError: false, - }, - { - name: "Maximum values", - be: BeaconEvent{ - ID: "max-beacon-id", - Name: "Maximum Values Test", - Type: "MaxType", - Battery: 0xFFFFFFFF, // Max uint32 - Event: 2147483647, // Max int32 - }, - expectedError: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := tt.be.RedisHashable() - - if (err != nil) != tt.expectedError { - t.Errorf("BeaconEvent.RedisHashable() error = %v, expectedError %v", err, tt.expectedError) - return - } - - if !tt.expectedError { - // Result should be a map - if result == nil { - t.Error("BeaconEvent.RedisHashable() result should not be nil") - return - } - - resultMap, ok := result.(map[string]any) - if !ok { - t.Errorf("BeaconEvent.RedisHashable() result should be a map[string]any, got %T", result) - return - } - - // Check that expected fields are present - expectedFields := []string{"name", "id", "type", "battery", "event"} - for _, field := range expectedFields { - if _, exists := resultMap[field]; !exists { - t.Errorf("BeaconEvent.RedisHashable() missing expected field: %s", field) - } - } - - // Check JSON tags are respected (BeaconEvent fields are not tagged with JSON, so field names should be lowercase) - if _, exists := resultMap["Name"]; exists { - t.Error("BeaconEvent.RedisHashable() should use lowercase field names") - } - - if _, exists := resultMap["name"]; !exists { - t.Error("BeaconEvent.RedisHashable() should contain 'name' field") - } - } - }) - } -} - -func TestHashConsistencyWithBatteryRounding(t *testing.T) { - // Test that Hash() is consistent with battery rounding - testCases := []struct { - battery1 uint32 - battery2 uint32 - shouldMatch bool - }{ - {1000, 1009, true}, // Same rounding range - {1000, 1010, false}, // Different rounding range - {0, 9, true}, // Zero range - {100, 104, true}, // Same range (100-109 rounds to 100) - {100, 110, false}, // Different ranges - {4294967295, 4294967289, true}, // Max value range - } - - for i, tc := range testCases { - t.Run(fmt.Sprintf("BatteryRoundCase_%d", i), func(t *testing.T) { - be1 := BeaconEvent{ - ID: "test-beacon", - Name: "Test", - Type: "Ingics", - Battery: tc.battery1, - Event: 1, - } - - be2 := BeaconEvent{ - ID: "test-beacon", - Name: "Test", - Type: "Ingics", - Battery: tc.battery2, - Event: 1, - } - - hash1 := be1.Hash() - hash2 := be2.Hash() - - hashesMatch := string(hash1) == string(hash2) - - if hashesMatch != tc.shouldMatch { - t.Errorf("Hash consistency mismatch: battery1=%d, battery2=%d, hashesMatch=%v, shouldMatch=%v", - tc.battery1, tc.battery2, hashesMatch, tc.shouldMatch) - } - }) - } -} - -func TestJSONMarshalUnmarshalRoundtrip(t *testing.T) { - original := BeaconEvent{ - ID: "roundtrip-test", - Name: "Roundtrip Test", - Type: "TestType", - Battery: 12345, - Event: 42, - } - - // Test that ToJSON produces valid JSON that can be unmarshaled back - jsonData, err := original.ToJSON() - if err != nil { - t.Fatalf("ToJSON() error: %v", err) - } - - var unmarshaled BeaconEvent - err = json.Unmarshal(jsonData, &unmarshaled) - if err != nil { - t.Fatalf("json.Unmarshal() error: %v", err) - } - - // Verify roundtrip integrity - if unmarshaled.ID != original.ID { - t.Errorf("Roundtrip ID mismatch: got %v, want %v", unmarshaled.ID, original.ID) - } - if unmarshaled.Name != original.Name { - t.Errorf("Roundtrip Name mismatch: got %v, want %v", unmarshaled.Name, original.Name) - } - if unmarshaled.Type != original.Type { - t.Errorf("Roundtrip Type mismatch: got %v, want %v", unmarshaled.Type, original.Type) - } - if unmarshaled.Battery != original.Battery { - t.Errorf("Roundtrip Battery mismatch: got %v, want %v", unmarshaled.Battery, original.Battery) - } - if unmarshaled.Event != original.Event { - t.Errorf("Roundtrip Event mismatch: got %v, want %v", unmarshaled.Event, original.Event) - } -} - -// Benchmark tests -func BenchmarkHash(b *testing.B) { - be := BeaconEvent{ - ID: "benchmark-beacon", - Name: "Benchmark Test", - Type: "Ingics", - Battery: 1000, - Event: 1, - } - - for i := 0; i < b.N; i++ { - be.Hash() - } -} - -func BenchmarkToJSON(b *testing.B) { - be := BeaconEvent{ - ID: "benchmark-beacon", - Name: "Benchmark Test", - Type: "Ingics", - Battery: 1000, - Event: 1, - } - - for i := 0; i < b.N; i++ { - be.ToJSON() - } -} - -func BenchmarkConvertStructToMap(b *testing.B) { - be := BeaconEvent{ - ID: "benchmark-beacon", - Name: "Benchmark Test", - Type: "Ingics", - Battery: 1000, - Event: 1, - } - - for i := 0; i < b.N; i++ { - convertStructToMap(be) - } -} \ No newline at end of file