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 e73b14e..0000000 Binary files a/presence.db and /dev/null differ diff --git a/presense.container b/presense.container deleted file mode 100644 index 7deb716..0000000 --- a/presense.container +++ /dev/null @@ -1,28 +0,0 @@ -[Unit] -Description=Presense -PartOf=podman.service -Wants=network-online.target podman-conf-login.service -After=podman.service network-online.target podman-conf-login.service -StartLimitIntervalSec=0 - -[Container] -Image=presense-go:latest -ContainerName=presense -PodmanArgs=-a stdout -a stderr -Network=sandbox.network -PublishPort=127.0.0.1:1902:8080 -Environment=HTTP_HOST_PATH=0.0.0.0:8080 -Environment=HTTPWS_HOST_PATH=0.0.0.0:8088 -Environment=MQTT_HOST=emqx:1883 -Environment=MQTT_USERNAME=sandbox -Environment=MQTT_PASSWORD=sandbox2025 -Environment=MQTT_CLIENT_ID=presence-detector -Environment=DB_PATH=.presence.db - -[Service] -Restart=always -TimeoutStartSec=infinity -RestartSec=5 - -[Install] -WantedBy=multi-user.target podman.service \ No newline at end of file diff --git a/test/README.md b/test/README.md deleted file mode 100644 index 10bf773..0000000 --- a/test/README.md +++ /dev/null @@ -1,130 +0,0 @@ -# Unit Tests Documentation - -This directory contains comprehensive unit tests for the high-priority internal packages of the AFASystems presence detection system. - -## Test Coverage - -The following files have been thoroughly tested: - -1. **`distance_test.go`** - Tests for distance calculation utilities - - `CalculateDistance()` - Distance calculation from RSSI and TX power - - `twosComp()` - Two's complement hex conversion - - `ValidateRSSI()` - RSSI value validation - - `ValidateTXPower()` - TX power validation - - Edge cases and real-world scenarios - -2. **`beacons_test.go`** - Tests for beacon parsing utilities - - `ParseADFast()` - Advertising Data structure parsing - - `RemoveFlagBytes()` - Bluetooth flag bytes removal - - `LoopADStructures()` - Beacon type detection and parsing - - `isValidADStructure()` - AD structure validation - - Beacon format support: Ingics, Eddystone TLM, Minew B7 - -3. **`typeMethods_test.go`** - Tests for model type methods - - `Hash()` - Beacon event hash generation with battery rounding - - `ToJSON()` - JSON marshaling for beacon events - - `convertStructToMap()` - Generic struct-to-map conversion - - `RedisHashable()` - Redis hash map conversion for HTTPLocation and BeaconEvent - - JSON roundtrip integrity tests - -4. **`mqtthandler_test.go`** - Tests for MQTT message processing - - `MqttHandler()` - Main MQTT message processing with JSON/CSV input - - `parseButtonState()` - Button counter parsing for different beacon formats - - Kafka writer integration (with mock) - - Hostname extraction from MQTT topics - - Error handling and edge cases - -## Running Tests - -### Run All Tests -```bash -go test ./test/... -v -``` - -### Run Specific Test File -```bash -go test ./test/distance_test.go -v -go test ./test/beacons_test.go -v -go test ./test/typeMethods_test.go -v -go test ./test/mqtthandler_test.go -v -``` - -### Run Tests for Specific Function -```bash -go test ./test/distance_test.go -run TestCalculateDistance -v -go test ./test/beacons_test.go -run TestParseADFast -v -go test ./test/typeMethods_test.go -run TestHash -v -go test ./test/mqtthandler_test.go -run TestMqttHandlerJSONArrayInput -v -``` - -### Run Benchmarks -```bash -# Run all benchmarks -go test ./test/... -bench=. - -# Run specific benchmarks -go test ./test/distance_test.go -bench=BenchmarkCalculateDistance -v -go test ./test/beacons_test.go -bench=BenchmarkParseADFast -v -go test ./test/typeMethods_test.go -bench=BenchmarkHash -v -go test ./test/mqtthandler_test.go -bench=BenchmarkMqttHandlerJSON -v -``` - -### Run Tests with Coverage Report -```bash -go test ./test/... -cover -go test ./test/... -coverprofile=coverage.out -go tool cover -html=coverage.out -o coverage.html -``` - -### Run Tests with Race Detection -```bash -go test ./test/... -race -v -``` - -## Test Organization - -Each test file follows Go testing conventions with: -- **Function tests**: Individual function behavior testing -- **Edge case tests**: Boundary conditions and error scenarios -- **Integration tests**: Multi-function workflow testing -- **Benchmark tests**: Performance measurement -- **Table-driven tests**: Multiple test cases with expected results - -## Mock Objects - -The mqtthandler tests use a `MockKafkaWriter` to simulate Kafka operations without requiring a running Kafka instance. This allows for: - -- Deterministic test results -- Failure scenario simulation -- Message content verification -- Performance benchmarking - -## Known Limitations - -- **CSV Processing**: The original CSV handler in `mqtthandler.go` contains `os.Exit(2)` calls which make it untestable. The test demonstrates the intended structure but cannot fully validate CSV processing due to this design choice. -- **External Dependencies**: Tests use mocks for external systems (Kafka) to ensure tests remain fast and reliable. - -## Best Practices Demonstrated - -These tests demonstrate several Go testing best practices: - -1. **Table-driven tests** for multiple scenarios -2. **Subtests** for logical test grouping -3. **Benchmark tests** for performance measurement -4. **Mock objects** for dependency isolation -5. **Error case testing** for robustness validation -6. **Deterministic testing** with consistent setup and teardown - -## Running Tests in CI/CD - -For automated testing environments: - -```bash -# Standard CI test run -go test ./test/... -race -cover -timeout=30s - -# Performance regression testing -go test ./test/... -bench=. -benchmem -``` - -This comprehensive test suite ensures the reliability and correctness of the core business logic in the AFASystems presence detection system. \ No newline at end of file diff --git a/test/beacons_test.go b/test/beacons_test.go deleted file mode 100644 index 4eb9848..0000000 --- a/test/beacons_test.go +++ /dev/null @@ -1,560 +0,0 @@ -package utils - -import ( - "testing" - - "github.com/AFASystems/presence/internal/pkg/model" -) - -func TestParseADFast(t *testing.T) { - tests := []struct { - name string - input []byte - expected [][2]int - }{ - { - name: "Empty input", - input: []byte{}, - expected: [][2]int{}, - }, - { - name: "Single AD structure", - input: []byte{0x02, 0x01, 0x06}, - expected: [][2]int{{0, 2}}, - }, - { - name: "Multiple AD structures", - input: []byte{0x02, 0x01, 0x06, 0x03, 0x02, 0x01, 0x02}, - expected: [][2]int{{0, 2}, {3, 6}}, - }, - { - name: "Complex AD structures", - input: []byte{0x02, 0x01, 0x06, 0x1A, 0xFF, 0x4C, 0x00, 0x02, 0x15, 0xE2, 0xC5, 0x6D, 0xB5, 0xDF, 0xFB, 0x48, 0xD2, 0xB0, 0x60, 0xD0, 0xF5, 0xA7, 0x10, 0x96, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xC5}, - expected: [][2]int{{0, 2}, {2, 28}}, - }, - { - name: "Zero length AD structure", - input: []byte{0x00, 0x01, 0x06, 0x03, 0x02, 0x01, 0x02}, - expected: [][2]int{{2, 5}}, - }, - { - name: "AD structure exceeding bounds", - input: []byte{0x05, 0x01, 0x06}, - expected: [][2]int{}, - }, - { - name: "Incomplete AD structure", - input: []byte{0x03, 0x01}, - expected: [][2]int{}, - }, - { - name: "Valid then invalid structure", - input: []byte{0x02, 0x01, 0x06, 0xFF, 0x01, 0x06}, - expected: [][2]int{{0, 2}}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := ParseADFast(tt.input) - if len(result) != len(tt.expected) { - t.Errorf("ParseADFast() length = %v, expected %v", len(result), len(tt.expected)) - return - } - - for i, r := range result { - if r[0] != tt.expected[i][0] || r[1] != tt.expected[i][1] { - t.Errorf("ParseADFast()[%d] = %v, expected %v", i, r, tt.expected[i]) - } - } - }) - } -} - -func TestRemoveFlagBytes(t *testing.T) { - tests := []struct { - name string - input []byte - expected []byte - }{ - { - name: "Empty input", - input: []byte{}, - expected: []byte{}, - }, - { - name: "Single byte input", - input: []byte{0x01}, - expected: []byte{0x01}, - }, - { - name: "No flag bytes", - input: []byte{0x02, 0x01, 0x06, 0x03, 0x02, 0x01}, - expected: []byte{0x02, 0x01, 0x06, 0x03, 0x02, 0x01}, - }, - { - name: "With flag bytes", - input: []byte{0x02, 0x01, 0x06, 0x1A, 0xFF, 0x4C, 0x00, 0x02}, - expected: []byte{0x1A, 0xFF, 0x4C, 0x00, 0x02}, - }, - { - name: "Flag type is 0x01", - input: []byte{0x02, 0x01, 0x06, 0x05, 0x01, 0x02, 0x03, 0x04}, - expected: []byte{0x05, 0x01, 0x02, 0x03, 0x04}, - }, - { - name: "Flag type is not 0x01", - input: []byte{0x02, 0x02, 0x06, 0x05, 0x01, 0x02, 0x03, 0x04}, - expected: []byte{0x02, 0x02, 0x06, 0x05, 0x01, 0x02, 0x03, 0x04}, - }, - { - name: "Length exceeds bounds", - input: []byte{0xFF, 0x01, 0x06}, - expected: []byte{0xFF, 0x01, 0x06}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := RemoveFlagBytes(tt.input) - if len(result) != len(tt.expected) { - t.Errorf("RemoveFlagBytes() length = %v, expected %v", len(result), len(tt.expected)) - return - } - - for i, b := range result { - if b != tt.expected[i] { - t.Errorf("RemoveFlagBytes()[%d] = %v, expected %v", i, b, tt.expected[i]) - } - } - }) - } -} - -func TestIsValidADStructure(t *testing.T) { - tests := []struct { - name string - data []byte - expected bool - }{ - { - name: "Empty data", - data: []byte{}, - expected: false, - }, - { - name: "Single byte", - data: []byte{0x01}, - expected: false, - }, - { - name: "Valid minimal structure", - data: []byte{0x01, 0x01}, - expected: true, - }, - { - name: "Valid structure", - data: []byte{0x02, 0x01, 0x06}, - expected: true, - }, - { - name: "Zero length", - data: []byte{0x00, 0x01, 0x06}, - expected: false, - }, - { - name: "Length exceeds data", - data: []byte{0x05, 0x01, 0x06}, - expected: false, - }, - { - name: "Length exactly matches", - data: []byte{0x02, 0x01, 0x06}, - expected: true, - }, - { - name: "Large valid structure", - data: []byte{0x1F, 0xFF, 0x4C, 0x00, 0x02, 0x15, 0xE2, 0xC5, 0x6D, 0xB5, 0xDF, 0xFB, 0x48, 0xD2, 0xB0, 0x60, 0xD0, 0xF5, 0xA7, 0x10, 0x96, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xC5, 0x01, 0x02, 0x03, 0x04, 0x05}, - expected: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := isValidADStructure(tt.data) - if result != tt.expected { - t.Errorf("isValidADStructure() = %v, expected %v", result, tt.expected) - } - }) - } -} - -func TestCheckIngics(t *testing.T) { - tests := []struct { - name string - ad []byte - expected bool - }{ - { - name: "Valid Ingics beacon", - ad: []byte{0x08, 0xFF, 0x59, 0x00, 0x80, 0xBC, 0x12, 0x34, 0x01}, - expected: true, - }, - { - name: "Invalid - too short", - ad: []byte{0x05, 0xFF, 0x59, 0x00}, - expected: false, - }, - { - name: "Invalid - wrong manufacturer ID", - ad: []byte{0x08, 0xFF, 0x59, 0x01, 0x80, 0xBC, 0x12, 0x34, 0x01}, - expected: false, - }, - { - name: "Invalid - wrong type", - ad: []byte{0x08, 0xFE, 0x59, 0x00, 0x80, 0xBC, 0x12, 0x34, 0x01}, - expected: false, - }, - { - name: "Valid with minimum length", - ad: []byte{0x06, 0xFF, 0x59, 0x00, 0x80, 0xBC}, - expected: true, - }, - { - name: "Empty data", - ad: []byte{}, - expected: false, - }, - { - name: "Partial match only", - ad: []byte{0x06, 0xFF, 0x59, 0x00, 0x80}, - expected: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := checkIngics(tt.ad) - if result != tt.expected { - t.Errorf("checkIngics() = %v, expected %v", result, tt.expected) - } - }) - } -} - -func TestParseIngicsState(t *testing.T) { - tests := []struct { - name string - ad []byte - expected model.BeaconEvent - }{ - { - name: "Valid Ingics data", - ad: []byte{0x08, 0xFF, 0x59, 0x00, 0x80, 0xBC, 0x34, 0x12, 0x05}, - expected: model.BeaconEvent{ - Battery: 0x1234, // 4660 in little endian - Event: 0x05, - Type: "Ingics", - }, - }, - { - name: "Zero battery", - ad: []byte{0x08, 0xFF, 0x59, 0x00, 0x80, 0xBC, 0x00, 0x00, 0x00}, - expected: model.BeaconEvent{ - Battery: 0, - Event: 0, - Type: "Ingics", - }, - }, - { - name: "Max battery value", - ad: []byte{0x08, 0xFF, 0x59, 0x00, 0x80, 0xBC, 0xFF, 0xFF, 0xFF}, - expected: model.BeaconEvent{ - Battery: 0xFFFF, - Event: 0xFF, - Type: "Ingics", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := parseIngicsState(tt.ad) - if result.Battery != tt.expected.Battery { - t.Errorf("parseIngicsState() Battery = %v, expected %v", result.Battery, tt.expected.Battery) - } - if result.Event != tt.expected.Event { - t.Errorf("parseIngicsState() Event = %v, expected %v", result.Event, tt.expected.Event) - } - if result.Type != tt.expected.Type { - t.Errorf("parseIngicsState() Type = %v, expected %v", result.Type, tt.expected.Type) - } - }) - } -} - -func TestCheckEddystoneTLM(t *testing.T) { - tests := []struct { - name string - ad []byte - expected bool - }{ - { - name: "Valid Eddystone TLM", - ad: []byte{0x12, 0x16, 0xAA, 0xFE, 0x20, 0x00, 0x01, 0x02, 0x03, 0x04}, - expected: true, - }, - { - name: "Invalid - too short", - ad: []byte{0x03, 0x16, 0xAA}, - expected: false, - }, - { - name: "Invalid - wrong type", - ad: []byte{0x12, 0x15, 0xAA, 0xFE, 0x20, 0x00, 0x01, 0x02, 0x03, 0x04}, - expected: false, - }, - { - name: "Invalid - wrong company ID", - ad: []byte{0x12, 0x16, 0xAA, 0xFF, 0x20, 0x00, 0x01, 0x02, 0x03, 0x04}, - expected: false, - }, - { - name: "Invalid - wrong TLM type", - ad: []byte{0x12, 0x16, 0xAA, 0xFE, 0x21, 0x00, 0x01, 0x02, 0x03, 0x04}, - expected: false, - }, - { - name: "Valid with minimum length", - ad: []byte{0x04, 0x16, 0xAA, 0xFE, 0x20}, - expected: true, - }, - { - name: "Empty data", - ad: []byte{}, - expected: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := checkEddystoneTLM(tt.ad) - if result != tt.expected { - t.Errorf("checkEddystoneTLM() = %v, expected %v", result, tt.expected) - } - }) - } -} - -func TestParseEddystoneState(t *testing.T) { - tests := []struct { - name string - ad []byte - expected model.BeaconEvent - }{ - { - name: "Valid Eddystone TLM data", - ad: []byte{0x12, 0x16, 0xAA, 0xFE, 0x20, 0x34, 0x12, 0x78, 0x56, 0x00}, - expected: model.BeaconEvent{ - Battery: 0x1234, // 4660 in big endian (note: different from Ingics) - Type: "Eddystone", - }, - }, - { - name: "Zero battery", - ad: []byte{0x12, 0x16, 0xAA, 0xFE, 0x20, 0x00, 0x00, 0x78, 0x56, 0x00}, - expected: model.BeaconEvent{ - Battery: 0, - Type: "Eddystone", - }, - }, - { - name: "Max battery value", - ad: []byte{0x12, 0x16, 0xAA, 0xFE, 0x20, 0xFF, 0xFF, 0x78, 0x56, 0x00}, - expected: model.BeaconEvent{ - Battery: 0xFFFF, - Type: "Eddystone", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := parseEddystoneState(tt.ad) - if result.Battery != tt.expected.Battery { - t.Errorf("parseEddystoneState() Battery = %v, expected %v", result.Battery, tt.expected.Battery) - } - if result.Type != tt.expected.Type { - t.Errorf("parseEddystoneState() Type = %v, expected %v", result.Type, tt.expected.Type) - } - }) - } -} - -func TestCheckMinewB7(t *testing.T) { - tests := []struct { - name string - ad []byte - expected bool - }{ - { - name: "Valid Minew B7", - ad: []byte{0x08, 0x16, 0xE1, 0xFF, 0x01, 0x02, 0x03, 0x04}, - expected: true, - }, - { - name: "Invalid - too short", - ad: []byte{0x03, 0x16, 0xE1}, - expected: false, - }, - { - name: "Invalid - wrong type", - ad: []byte{0x08, 0x15, 0xE1, 0xFF, 0x01, 0x02, 0x03, 0x04}, - expected: false, - }, - { - name: "Invalid - wrong company ID", - ad: []byte{0x08, 0x16, 0xE1, 0xFE, 0x01, 0x02, 0x03, 0x04}, - expected: false, - }, - { - name: "Valid with minimum length", - ad: []byte{0x04, 0x16, 0xE1, 0xFF}, - expected: true, - }, - { - name: "Empty data", - ad: []byte{}, - expected: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := checkMinewB7(tt.ad) - if result != tt.expected { - t.Errorf("checkMinewB7() = %v, expected %v", result, tt.expected) - } - }) - } -} - -func TestLoopADStructures(t *testing.T) { - tests := []struct { - name string - data []byte - ranges [][2]int - id string - expected model.BeaconEvent - }{ - { - name: "Ingics beacon found", - data: []byte{0x08, 0xFF, 0x59, 0x00, 0x80, 0xBC, 0x34, 0x12, 0x05, 0x02, 0x01, 0x06}, - ranges: [][2]int{{0, 8}, {8, 11}}, - id: "test-beacon", - expected: model.BeaconEvent{ - ID: "test-beacon", - Name: "test-beacon", - Battery: 0x1234, - Event: 0x05, - Type: "Ingics", - }, - }, - { - name: "Eddystone beacon found", - data: []byte{0x02, 0x01, 0x06, 0x12, 0x16, 0xAA, 0xFE, 0x20, 0x34, 0x12, 0x78, 0x56}, - ranges: [][2]int{{0, 2}, {2, 14}}, - id: "eddystone-test", - expected: model.BeaconEvent{ - ID: "eddystone-test", - Name: "eddystone-test", - Battery: 0x1234, - Type: "Eddystone", - }, - }, - { - name: "Minew B7 beacon found", - data: []byte{0x08, 0x16, 0xE1, 0xFF, 0x01, 0x02, 0x03, 0x04, 0x02, 0x01, 0x06}, - ranges: [][2]int{{0, 8}, {8, 11}}, - id: "minew-test", - expected: model.BeaconEvent{ - ID: "minew-test", - Name: "minew-test", - Type: "", // Minew B7 returns empty BeaconEvent - }, - }, - { - name: "No matching beacon type", - data: []byte{0x02, 0x01, 0x06, 0x03, 0x02, 0x01, 0x02}, - ranges: [][2]int{{0, 2}, {2, 5}}, - id: "unknown-test", - expected: model.BeaconEvent{}, - }, - { - name: "Invalid AD structure", - data: []byte{0x02, 0x01, 0x06, 0xFF, 0x01, 0x06}, - ranges: [][2]int{{0, 2}, {2, 4}}, - id: "invalid-test", - expected: model.BeaconEvent{}, - }, - { - name: "Empty data", - data: []byte{}, - ranges: [][2]int{}, - id: "empty-test", - expected: model.BeaconEvent{ - ID: "empty-test", - Name: "empty-test", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := LoopADStructures(tt.data, tt.ranges, tt.id) - - if result.ID != tt.expected.ID { - t.Errorf("LoopADStructures() ID = %v, expected %v", result.ID, tt.expected.ID) - } - if result.Name != tt.expected.Name { - t.Errorf("LoopADStructures() Name = %v, expected %v", result.Name, tt.expected.Name) - } - if result.Type != tt.expected.Type { - t.Errorf("LoopADStructures() Type = %v, expected %v", result.Type, tt.expected.Type) - } - if result.Battery != tt.expected.Battery { - t.Errorf("LoopADStructures() Battery = %v, expected %v", result.Battery, tt.expected.Battery) - } - }) - } -} - -func TestLoopADStructuresPriority(t *testing.T) { - // Test that Ingics is checked first - data := []byte{0x08, 0xFF, 0x59, 0x00, 0x80, 0xBC, 0x34, 0x12, 0x05, 0x12, 0x16, 0xAA, 0xFE, 0x20, 0x78, 0x56} - ranges := [][2]int{{0, 8}, {8, 15}} - - result := LoopADStructures(data, ranges, "priority-test") - - // Should detect Ingics first, not Eddystone - if result.Type != "Ingics" { - t.Errorf("LoopADStructures() Type = %v, expected Ingics (priority test)", result.Type) - } -} - -// Benchmark tests -func BenchmarkParseADFast(b *testing.B) { - data := []byte{0x02, 0x01, 0x06, 0x1A, 0xFF, 0x4C, 0x00, 0x02, 0x15, 0xE2, 0xC5, 0x6D, 0xB5, 0xDF, 0xFB, 0x48, 0xD2, 0xB0, 0x60, 0xD0, 0xF5, 0xA7, 0x10, 0x96, 0xE0, 0x00, 0x00, 0x00, 0x00, 0xC5} - - for i := 0; i < b.N; i++ { - ParseADFast(data) - } -} - -func BenchmarkRemoveFlagBytes(b *testing.B) { - data := []byte{0x02, 0x01, 0x06, 0x1A, 0xFF, 0x4C, 0x00, 0x02, 0x15, 0xE2, 0xC5} - - for i := 0; i < b.N; i++ { - RemoveFlagBytes(data) - } -} \ No newline at end of file diff --git a/test/distance_test.go b/test/distance_test.go deleted file mode 100644 index 3430429..0000000 --- a/test/distance_test.go +++ /dev/null @@ -1,294 +0,0 @@ -package test - -import ( - "testing" - - "github.com/AFASystems/presence/internal/pkg/common/utils" - "github.com/AFASystems/presence/internal/pkg/model" -) - -func TestCalculateDistance(t *testing.T) { - tests := []struct { - name string - adv model.BeaconAdvertisement - expected float64 - }{ - { - name: "Strong signal - close distance", - adv: model.BeaconAdvertisement{ - RSSI: -30, - TXPower: "59", // 89 in decimal - }, - expected: 0.89976, // Close to minimum - }, - { - name: "Medium signal", - adv: model.BeaconAdvertisement{ - RSSI: -65, - TXPower: "59", - }, - expected: 1.5, // Medium distance - }, - { - name: "Weak signal - far distance", - adv: model.BeaconAdvertisement{ - RSSI: -95, - TXPower: "59", - }, - expected: 8.0, // Far distance - }, - { - name: "Equal RSSI and TX power", - adv: model.BeaconAdvertisement{ - RSSI: -59, - TXPower: "59", - }, - expected: 1.0, // Ratio = 1.0 - }, - { - name: "Very strong signal", - adv: model.BeaconAdvertisement{ - RSSI: -10, - TXPower: "59", - }, - expected: 0.89976, // Minimum distance - }, - { - name: "Negative TX power (two's complement)", - adv: model.BeaconAdvertisement{ - RSSI: -70, - TXPower: "C6", // -58 in decimal - }, - expected: 1.2, // Medium distance - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := utils.CalculateDistance(tt.adv) - // Allow for small floating point differences - if result < tt.expected*0.9 || result > 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