package protocol import ( "encoding/json" "strings" "testing" "time" "github.com/stretchr/testify/require" ) func TestTypedPublicationsRoundTrip(t *testing.T) { now := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) deadline := now.Add(time.Minute) metadata := Metadata{ MessageID: messageID, SentAt: &now, } commandMetadata := Metadata{ MessageID: messageID, ExpiresAt: &deadline, } resultMetadata := Metadata{ MessageID: messageID, CorrelationID: commandID, } testCases := []struct { name string kind Kind build func() (Publication, error) payload any retain bool qos byte }{ { name: "preserves metadata contract", kind: Descriptor, retain: true, qos: 1, build: func() (Publication, error) { return BuildDescriptor(deviceID, metadata, testDescriptor()) }, payload: testDescriptor(), }, { name: "preserves online reachability", kind: Availability, retain: true, qos: 1, build: func() (Publication, error) { return BuildAvailability(deviceID, metadata, AvailabilityPayload{Status: "online"}) }, payload: AvailabilityPayload{Status: "online"}, }, { name: "preserves retained object value", kind: Reported, retain: true, qos: 1, build: func() (Publication, error) { return BuildReported(deviceID, "weather", metadata, ValuePayload{Value: json.RawMessage(`{"temperature":20,"humidity":50}`)}) }, payload: ValuePayload{Value: json.RawMessage(`{"temperature":20,"humidity":50}`)}, }, { name: "preserves unconfirmed sample", kind: Telemetry, qos: 0, build: func() (Publication, error) { return BuildTelemetry(deviceID, "power", metadata, ValuePayload{Value: json.RawMessage(`12.5`)}) }, payload: ValuePayload{Value: json.RawMessage(`12.5`)}, }, { name: "preserves open event data", kind: Event, qos: 1, build: func() (Publication, error) { return BuildEvent(deviceID, "motion", metadata, EventPayload{ "action": json.RawMessage(`"motion"`), "custom": json.RawMessage(`7`), }) }, payload: EventPayload{ "action": json.RawMessage(`"motion"`), "custom": json.RawMessage(`7`), }, }, { name: "preserves command deadline", kind: Command, qos: 1, build: func() (Publication, error) { return BuildCommand(deviceID, "relay", commandMetadata, ValuePayload{Value: json.RawMessage(`true`)}) }, payload: ValuePayload{Value: json.RawMessage(`true`)}, }, { name: "preserves ordered member requests", kind: CommandBatch, qos: 1, build: func() (Publication, error) { return BuildCommandBatch(deviceID, metadata, testBatch(deadline)) }, payload: testBatch(deadline), }, { name: "preserves correlated result", kind: Result, qos: 1, build: func() (Publication, error) { return BuildResult(deviceID, commandID, resultMetadata, ResultPayload{Status: "completed"}) }, payload: ResultPayload{Status: "completed"}, }, { name: "preserves expiring hub snapshot", kind: HubStatus, retain: true, qos: 1, build: func() (Publication, error) { return BuildHubStatus(deviceID, commandMetadata, HubStatusPayload{ ID: deviceID, Address: "example.com", Status: "ready", }) }, payload: HubStatusPayload{ ID: deviceID, Address: "example.com", Status: "ready", }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { publication, err := tc.build() require.NoError(t, err) require.Equal(t, tc.retain, publication.Retain) require.Equal(t, tc.qos, publication.QoS) parsed, err := Parse(publication.Topic, publication.Data, publication.Retain) require.NoError(t, err) require.Equal(t, tc.kind, parsed.Address.Kind) require.Equal(t, deviceID, parsed.Address.ID) require.Equal(t, tc.payload, parsed.Payload) require.Equal(t, messageID, parsed.Metadata.MessageID) live, err := Parse(publication.Topic, publication.Data, false) require.NoError(t, err) require.False(t, live.Retained) if !tc.retain { _, err = Parse(publication.Topic, publication.Data, true) require.Error(t, err) } malformed := strings.Replace(publication.Topic, Root, "wrong/v1", 1) _, err = Parse(malformed, publication.Data, false) require.Error(t, err) unknown := strings.Replace(string(publication.Data), `"schema":1`, `"schema":1,"unknown":true`, 1) _, err = Parse(publication.Topic, []byte(unknown), false) require.Error(t, err) wrongSchema := strings.Replace(string(publication.Data), `"schema":1`, `"schema":2`, 1) _, err = Parse(publication.Topic, []byte(wrongSchema), false) require.Error(t, err) if tc.kind == Command || tc.kind == CommandBatch || tc.kind == HubStatus { require.Equal(t, &deadline, publication.ExpiresAt) } if tc.kind == Command { response, err := ParseTopic(publication.ResponseTopic) require.NoError(t, err) require.Equal(t, Result, response.Kind) require.Equal(t, messageID, response.Target) require.Equal(t, messageID, publication.CorrelationData) } else { require.Empty(t, publication.ResponseTopic) require.Empty(t, publication.CorrelationData) } }) } } func TestTypedParsersRejectWrongOperations(t *testing.T) { parsers := []func(string, []byte, bool) error{ func(topic string, data []byte, retained bool) error { _, err := ParseDescriptor(topic, data, retained) return err }, func(topic string, data []byte, retained bool) error { _, err := ParseAvailability(topic, data, retained) return err }, func(topic string, data []byte, retained bool) error { _, err := ParseReported(topic, data, retained) return err }, func(topic string, data []byte, retained bool) error { _, err := ParseTelemetry(topic, data, retained) return err }, func(topic string, data []byte, retained bool) error { _, err := ParseEvent(topic, data, retained) return err }, func(topic string, data []byte, retained bool) error { _, err := ParseCommand(topic, data, retained) return err }, func(topic string, data []byte, retained bool) error { _, err := ParseCommandBatch(topic, data, retained) return err }, func(topic string, data []byte, retained bool) error { _, err := ParseResult(topic, data, retained) return err }, func(topic string, data []byte, retained bool) error { _, err := ParseHubStatus(topic, data, retained) return err }, } for _, parse := range parsers { require.Error(t, parse(Root+"/devices/"+deviceID+"/unknown", []byte(`{}`), false)) } }