for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
- data, err := EncodeEnvelope(metadata, json.RawMessage(tc.payload))
+ data, err := encodeEnvelope(metadata, json.RawMessage(tc.payload))
if tc.kind == Event {
require.Error(t, err)
Kind: tc.kind,
ID: deviceID,
Target: tc.target,
- }).Topic()
+ }).topic()
require.NoError(t, err)
_, err = Parse(topic, data, false)
_, err = BuildDescriptor(deviceID, metadata, descriptor)
require.Error(t, err)
}
+
+func TestBuildersKeepNullValuesWithoutChangingInputs(t *testing.T) {
+ metadata := Metadata{MessageID: messageID}
+ payload := ValuePayload{}
+ publication, err := BuildReported(deviceID, "relay", metadata, payload)
+ require.NoError(t, err)
+ require.Nil(t, payload.Value)
+
+ parsed, err := ParseReported(publication.Topic, publication.Data, false)
+ require.NoError(t, err)
+ require.JSONEq(t, `null`, string(parsed.Payload.Value))
+
+ deadline := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
+ batch := testBatch(deadline)
+ batch.Commands[0].Payload.Value = nil
+ publication, err = BuildCommandBatch(deviceID, metadata, batch)
+ require.NoError(t, err)
+ require.Nil(t, batch.Commands[0].Payload.Value)
+
+ commands, err := ParseCommandBatch(publication.Topic, publication.Data, false)
+ require.NoError(t, err)
+ require.JSONEq(t, `null`, string(commands.Payload.Commands[0].Payload.Value))
+}
+
+func TestBuildEventRejectsNonObjectAndInvalidValues(t *testing.T) {
+ testCases := []struct {
+ name string
+ payload EventPayload
+ valid bool
+ }{
+ {
+ name: "rejects null instead of an object",
+ },
+ {
+ name: "rejects malformed event data",
+ payload: EventPayload{"action": json.RawMessage(`bad`)},
+ },
+ {
+ name: "rejects empty nonnull event data",
+ payload: EventPayload{"action": json.RawMessage{}},
+ },
+ {
+ name: "keeps null event members",
+ payload: EventPayload{"action": nil},
+ valid: true,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ publication, err := BuildEvent(deviceID, "press", Metadata{MessageID: messageID}, tc.payload)
+ if !tc.valid {
+ require.Error(t, err)
+
+ return
+ }
+
+ require.NoError(t, err)
+
+ parsed, err := ParseEvent(publication.Topic, publication.Data, false)
+ require.NoError(t, err)
+ require.JSONEq(t, `null`, string(parsed.Payload["action"]))
+ })
+ }
+}
+
+func TestBuildRejectsInvalidMetadataBeforeReturningPublications(t *testing.T) {
+ deadline := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
+ metadata := Metadata{MessageID: messageID}
+ testCases := []struct {
+ name string
+ mutate func(*Metadata)
+ }{
+ {
+ name: "rejects malformed message identity",
+ mutate: func(metadata *Metadata) {
+ metadata.MessageID = "bad"
+ },
+ },
+ {
+ name: "rejects malformed correlation identity",
+ mutate: func(metadata *Metadata) {
+ metadata.CorrelationID = "bad"
+ },
+ },
+ {
+ name: "rejects malformed causation identity",
+ mutate: func(metadata *Metadata) {
+ metadata.CausationID = "bad"
+ },
+ },
+ {
+ name: "rejects non UTC expiry",
+ mutate: func(metadata *Metadata) {
+ expiry := deadline.In(time.FixedZone("test", 3600))
+ metadata.ExpiresAt = &expiry
+ },
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ invalid := metadata
+ tc.mutate(&invalid)
+
+ _, err := BuildReported(deviceID, "relay", invalid, ValuePayload{Value: json.RawMessage(`true`)})
+ require.Error(t, err)
+
+ batch := testBatch(deadline)
+ tc.mutate(&batch.Commands[1].Metadata)
+
+ _, err = BuildCommandBatch(deviceID, metadata, batch)
+ require.Error(t, err)
+ })
+ }
+}
+
+func TestDescriptorAttributionLimitUsesEncodedText(t *testing.T) {
+ payload := testDescriptor()
+ payload.Attribution = strings.Repeat("\xff", 667)
+ _, err := BuildDescriptor(deviceID, Metadata{MessageID: messageID}, payload)
+ require.Error(t, err)
+
+ payload.Attribution = strings.Repeat("\xff", 666)
+ publication, err := BuildDescriptor(deviceID, Metadata{MessageID: messageID}, payload)
+ require.NoError(t, err)
+
+ parsed, err := ParseDescriptor(publication.Topic, publication.Data, true)
+ require.NoError(t, err)
+ require.Len(t, parsed.Payload.Attribution, 1998)
+}