package contracts import ( "strings" "testing" "time" ) var testTopic = Topic{DeviceID: "device-001", Kind: KindTelemetry} var testNow = time.Date(2026, 10, 13, 12, 0, 0, 0, time.UTC) // Topic tests reject malformed routes and unsupported message kinds. func TestParseTopic(t *testing.T) { topic, err := ParseTopic("lab/v1/devices/device-001/sales") if err != nil { t.Fatal(err) } if topic.DeviceID != "device-001" || topic.Kind != KindSales { t.Fatalf("unexpected topic: %#v", topic) } for _, invalid := range []string{"lab/v1/devices/+/sales", "lab/v1/devices/device-001/unknown", "lab/v1/devices/device-001/sales/extra"} { if _, err := ParseTopic(invalid); err == nil { t.Errorf("ParseTopic(%q) succeeded", invalid) } } } // These cases cover strict JSON shape and single-value decoding. func TestDecodeRejectsUnknownAndTrailingValues(t *testing.T) { if _, err := Decode[Heartbeat]([]byte(`{"schema_version":1,"unknown":true}`), testTopic, testNow); err == nil { t.Fatal("unknown field was accepted") } if _, err := Decode[Heartbeat]([]byte(`{"schema_version":1,"message_id":"m-1","device_id":"device-001","occurred_at":"2026-10-13T12:00:00Z"} {"message_id":"m-2"}`), testTopic, testNow); err == nil { t.Fatal("trailing JSON value was accepted") } } // This case ensures the application limit is applied before decoding. func TestDecodeRejectsOversizedPayload(t *testing.T) { payload := []byte(`"` + strings.Repeat("x", MaxPayloadBytes) + `"`) if _, err := Decode[Heartbeat](payload, testTopic, testNow); err == nil { t.Fatal("oversized payload was accepted") } } // Decode validates the message against both its parsed route and supplied clock. func TestDecodeValidatesContract(t *testing.T) { payload := []byte(`{"schema_version":1,"message_id":"m-1","device_id":"device-002","occurred_at":"2026-10-13T12:00:00Z"}`) if _, err := Decode[Heartbeat](payload, testTopic, testNow); err == nil { t.Fatal("topic/body identity mismatch was accepted") } } // The type parameter must implement MQTTMessage; successful decoding also validates it. func TestDecodeAcceptsOnlyValidatedMQTTMessages(t *testing.T) { payload := []byte(`{"schema_version":1,"message_id":"m-1","device_id":"device-001","occurred_at":"2026-10-13T12:00:00Z"}`) message, err := Decode[Heartbeat](payload, testTopic, testNow) if err != nil { t.Fatal(err) } if message.DeviceID != testTopic.DeviceID { t.Fatalf("device_id = %q, want %q", message.DeviceID, testTopic.DeviceID) } } // A body cannot override the device selected by the topic. func TestCommandRejectsTopicIdentityMismatch(t *testing.T) { topic, err := ParseTopic("lab/v1/devices/device-001/commands") if err != nil { t.Fatal(err) } command := Command{SchemaVersion: SchemaVersion, MessageID: "m-1", DeviceID: "device-002", OccurredAt: time.Now().UTC().Format(time.RFC3339), CommandID: "c-1", Command: "ping"} if err := command.Validate(topic, time.Now().UTC()); err == nil { t.Fatal("topic/body identity mismatch was accepted") } }