feat: expand MQTT contract and reliability lessons

This commit is contained in:
2026-10-13 18:08:17 +03:30
parent 2d639d78eb
commit f9ab91ed60
17 changed files with 2083 additions and 91 deletions
+76
View File
@@ -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")
}
}