package protocol import ( "encoding/json" "fmt" "strings" "testing" "time" "github.com/stretchr/testify/require" ) func TestBatchKeepsMemberMetadataAndEarliestDeadline(t *testing.T) { deadline := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) later := deadline.Add(time.Minute) payload := testBatch(deadline) payload.Commands[0].ExpiresAt = &later payload.Commands[0].CorrelationID = deviceID payload.Commands[1].CausationID = messageID publication, err := BuildCommandBatch(deviceID, Metadata{MessageID: messageID}, payload) require.NoError(t, err) require.Equal(t, &deadline, publication.ExpiresAt) parsed, err := ParseCommandBatch(publication.Topic, publication.Data, false) require.NoError(t, err) require.Equal(t, payload, parsed.Payload) for index := 2; index < MaximumCommandBatchSize; index++ { metadata := Metadata{ MessageID: fmt.Sprintf("%026d", index+20), ExpiresAt: &deadline, } payload.Commands = append(payload.Commands, BatchCommand{ Metadata: metadata, Capability: fmt.Sprintf("relay.%d", index+1), Payload: ValuePayload{Value: json.RawMessage(`false`)}, }) } _, err = BuildCommandBatch(deviceID, Metadata{MessageID: messageID}, payload) require.NoError(t, err) payload.Commands = append(payload.Commands, payload.Commands[0]) _, err = BuildCommandBatch(deviceID, Metadata{MessageID: messageID}, payload) require.Error(t, err) } func TestBatchRejectsMalformedMembersBeforeReturningCommands(t *testing.T) { deadline := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) metadata := Metadata{MessageID: messageID} publication, err := BuildCommandBatch(deviceID, metadata, testBatch(deadline)) require.NoError(t, err) testCases := []struct { name string old string replacement string }{ { name: "rejects member schema", old: `"schema":1`, replacement: `"schema":2`, }, { name: "rejects unknown batch field", old: `"commands":[`, replacement: `"extra":1,"commands":[`, }, { name: "rejects unknown member field", old: `"capability":"relay.1"`, replacement: `"capability":"relay.1","extra":1`, }, { name: "rejects unknown member value field", old: `"value":true`, replacement: `"value":true,"extra":1`, }, { name: "rejects duplicate member ID", old: commandID, replacement: messageID, }, { name: "rejects duplicate target", old: `"relay.2"`, replacement: `"relay.1"`, }, { name: "rejects invalid target", old: `"relay.2"`, replacement: `"relay/+"`, }, { name: "requires member expiry", old: `"expires_at":"2026-01-01T00:00:00Z",`, replacement: ``, }, { name: "requires member value", old: `"value":true`, replacement: ``, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { data := strings.ReplaceAll(string(publication.Data), tc.old, tc.replacement) var parsed Message[BatchPayload] parsed, err = ParseCommandBatch(publication.Topic, []byte(data), false) require.Error(t, err) require.Empty(t, parsed.Payload.Commands) }) } payload := testBatch(deadline) payload.Commands = payload.Commands[:1] _, err = BuildCommandBatch(deviceID, metadata, payload) require.Error(t, err) _, err = BuildCommandBatch(deviceID, Metadata{MessageID: commandID}, testBatch(deadline)) require.Error(t, err) data := strings.Replace(string(publication.Data), `"capability":"relay.1"`, `"capability":"relay.1","schema":2`, 1) _, err = ParseCommandBatch(publication.Topic, []byte(data), false) require.Error(t, err) }