Subscribes to devices/+/telemetry, validates payloads, buffers writes into ClickHouse (flush on size or interval), and emits a per-reading event on RabbitMQ for rule-engine-service to consume later. Adds the initial Postgres and ClickHouse schema migrations, a Dockerfile, and wires the service into docker-compose. Verified end-to-end via docker compose + mosquitto_pub: row lands in ClickHouse and the event reaches the telemetry.new_reading queue. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
75 lines
1.9 KiB
Go
75 lines
1.9 KiB
Go
package telemetry
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestParseReading_Valid(t *testing.T) {
|
|
receivedAt := time.Date(2026, 7, 26, 12, 0, 5, 0, time.UTC)
|
|
raw := []byte(`{
|
|
"device_id": "esp32-abc123",
|
|
"zone_id": "growbox-1",
|
|
"sensor_type": "temperature",
|
|
"value": 24.5,
|
|
"timestamp": "2026-07-26T12:00:00Z"
|
|
}`)
|
|
|
|
got, err := ParseReading(raw, receivedAt)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
want := Reading{
|
|
DeviceID: "esp32-abc123",
|
|
ZoneID: "growbox-1",
|
|
SensorType: "temperature",
|
|
Value: 24.5,
|
|
RecordedAt: time.Date(2026, 7, 26, 12, 0, 0, 0, time.UTC),
|
|
ReceivedAt: receivedAt,
|
|
}
|
|
if got != want {
|
|
t.Fatalf("got %+v, want %+v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestParseReading_Invalid(t *testing.T) {
|
|
receivedAt := time.Now()
|
|
|
|
cases := map[string]string{
|
|
"malformed json": `{not json`,
|
|
"missing device_id": `{
|
|
"zone_id": "growbox-1", "sensor_type": "temperature",
|
|
"value": 1, "timestamp": "2026-07-26T12:00:00Z"
|
|
}`,
|
|
"missing zone_id": `{
|
|
"device_id": "esp32-1", "sensor_type": "temperature",
|
|
"value": 1, "timestamp": "2026-07-26T12:00:00Z"
|
|
}`,
|
|
"missing sensor_type": `{
|
|
"device_id": "esp32-1", "zone_id": "growbox-1",
|
|
"value": 1, "timestamp": "2026-07-26T12:00:00Z"
|
|
}`,
|
|
"missing timestamp": `{
|
|
"device_id": "esp32-1", "zone_id": "growbox-1",
|
|
"sensor_type": "temperature", "value": 1
|
|
}`,
|
|
"invalid timestamp format": `{
|
|
"device_id": "esp32-1", "zone_id": "growbox-1",
|
|
"sensor_type": "temperature", "value": 1, "timestamp": "not-a-date"
|
|
}`,
|
|
"value wrong type": `{
|
|
"device_id": "esp32-1", "zone_id": "growbox-1",
|
|
"sensor_type": "temperature", "value": "not-a-number", "timestamp": "2026-07-26T12:00:00Z"
|
|
}`,
|
|
}
|
|
|
|
for name, raw := range cases {
|
|
t.Run(name, func(t *testing.T) {
|
|
if _, err := ParseReading([]byte(raw), receivedAt); err == nil {
|
|
t.Fatalf("expected error, got nil")
|
|
}
|
|
})
|
|
}
|
|
}
|