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