feat: expand MQTT contract and reliability lessons
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user