package protocol import ( "encoding/json" "strings" "testing" "time" "github.com/stretchr/testify/require" ) func TestPayloadValidationRejectsInvalidContracts(t *testing.T) { deadline := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) metadata := Metadata{ MessageID: messageID, ExpiresAt: &deadline, } descriptor := `{"manufacturer":"example","model":"relay","firmware":"1","capabilities":[{"id":"relay","type":"switch","reports_state":true,"accepts_commands":false}]}` testCases := []struct { name string kind Kind target string payload string }{ { name: "rejects unknown descriptor field", kind: Descriptor, payload: strings.Replace(descriptor, `"model":"relay"`, `"model":"relay","room":"kitchen"`, 1), }, { name: "rejects unknown capability field", kind: Descriptor, payload: strings.Replace(descriptor, `"type":"switch"`, `"type":"switch","extra":true`, 1), }, { name: "requires both access flags", kind: Descriptor, payload: strings.Replace(descriptor, `,"accepts_commands":false`, ``, 1), }, { name: "rejects null access flag", kind: Descriptor, payload: strings.Replace(descriptor, `"accepts_commands":false`, `"accepts_commands":null`, 1), }, { name: "requires active access mode", kind: Descriptor, payload: strings.Replace(descriptor, `"reports_state":true`, `"reports_state":false`, 1), }, { name: "requires device metadata", kind: Descriptor, payload: strings.Replace(descriptor, `"model":"relay"`, `"model":""`, 1), }, { name: "requires capabilities", kind: Descriptor, payload: `{"manufacturer":"example","model":"relay","firmware":"1","capabilities":[]}`, }, { name: "rejects invalid capability identifier", kind: Descriptor, payload: strings.Replace(descriptor, `"id":"relay"`, `"id":"relay/1"`, 1), }, { name: "rejects duplicate capability", kind: Descriptor, payload: strings.Replace(descriptor, `}]`, `},{"id":"relay","type":"switch","reports_state":true,"accepts_commands":false}]`, 1), }, { name: "rejects unknown availability status", kind: Availability, payload: `{"status":"ready"}`, }, { name: "rejects unknown availability field", kind: Availability, payload: `{"status":"online","extra":1}`, }, { name: "requires report value", kind: Reported, target: "relay", payload: `{}`, }, { name: "rejects unknown report field", kind: Reported, target: "relay", payload: `{"value":true,"extra":1}`, }, { name: "requires telemetry value", kind: Telemetry, target: "power", payload: `{}`, }, { name: "rejects unknown telemetry field", kind: Telemetry, target: "power", payload: `{"value":1,"extra":1}`, }, { name: "rejects unknown command field", kind: Command, target: "relay", payload: `{"value":true,"extra":1}`, }, { name: "requires command value", kind: Command, target: "relay", payload: `{}`, }, { name: "rejects array event", kind: Event, target: "press", payload: `[]`, }, { name: "rejects unknown result field", kind: Result, target: commandID, payload: `{"status":"completed","extra":1}`, }, { name: "rejects unknown hub field", kind: HubStatus, payload: `{"id":"` + deviceID + `","status":"ready","address":"example.com","extra":1}`, }, { name: "rejects unknown diagnostic field", kind: HubStatus, payload: `{"id":"` + deviceID + `","status":"ready","address":"example.com","diagnostics":{"observed_at":"2026-01-01T00:00:00Z","stale":false,"extra":1}}`, }, { name: "rejects unknown pairing field", kind: HubStatus, payload: `{"id":"` + deviceID + `","status":"ready","address":"example.com","pairing":{"extra":1}}`, }, { name: "rejects hub identity mismatch", kind: HubStatus, payload: `{"id":"` + commandID + `","status":"ready","address":"example.com"}`, }, { name: "rejects invalid hub status", kind: HubStatus, payload: `{"id":"` + deviceID + `","status":"online","address":"example.com"}`, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { data, err := EncodeEnvelope(metadata, json.RawMessage(tc.payload)) if tc.kind == Event { require.Error(t, err) return } require.NoError(t, err) topic, err := (Address{ Kind: tc.kind, ID: deviceID, Target: tc.target, }).Topic() require.NoError(t, err) _, err = Parse(topic, data, false) require.Error(t, err) }) } } func TestBuildersApplyCommandAndResultRules(t *testing.T) { metadata := Metadata{MessageID: messageID} _, err := BuildCommand(deviceID, "relay", metadata, ValuePayload{Value: json.RawMessage(`true`)}) require.Error(t, err) _, err = BuildHubStatus(deviceID, metadata, HubStatusPayload{ ID: deviceID, Address: "example.com", Status: "ready", }) require.Error(t, err) _, err = BuildDescriptor("bad", metadata, testDescriptor()) require.Error(t, err) _, err = BuildReported(deviceID, "relay", metadata, ValuePayload{Value: json.RawMessage(`bad`)}) require.Error(t, err) _, err = BuildAvailability(deviceID, metadata, AvailabilityPayload{Status: "bad"}) require.Error(t, err) for _, value := range []string{`null`, `true`, `12`, `"open"`, `{"mode":"off"}`, `[1,2]`} { var publication Publication publication, err = BuildReported(deviceID, "point", metadata, ValuePayload{Value: json.RawMessage(value)}) require.NoError(t, err) var message Message[ValuePayload] message, err = ParseReported(publication.Topic, publication.Data, false) require.NoError(t, err) require.JSONEq(t, value, string(message.Payload.Value)) } testCases := []struct { name string status string reason string valid bool }{ { name: "accepts intermediate result", status: "accepted", valid: true, }, { name: "accepts completed result", status: "completed", valid: true, }, { name: "accepts rejection reason", status: "rejected", reason: "unsupported", valid: true, }, { name: "accepts failure reason", status: "failed", reason: "readback", valid: true, }, { name: "requires rejection reason", status: "rejected", }, { name: "requires failure reason", status: "failed", }, { name: "rejects successful reason", status: "completed", reason: "extra", }, { name: "rejects unknown result status", status: "expired", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { correlated := Metadata{ MessageID: messageID, CorrelationID: commandID, } var publication Publication publication, err = BuildResult(deviceID, commandID, correlated, ResultPayload{ Status: tc.status, Reason: tc.reason, }) if !tc.valid { require.Error(t, err) return } require.NoError(t, err) var parsed Message[ResultPayload] parsed, err = ParseResult(publication.Topic, publication.Data, false) require.NoError(t, err) require.Equal(t, tc.status, parsed.Payload.Status) }) } _, err = BuildResult(deviceID, commandID, metadata, ResultPayload{Status: "completed"}) require.Error(t, err) descriptor := testDescriptor() descriptor.Attribution = strings.Repeat("x", 2001) _, err = BuildDescriptor(deviceID, metadata, descriptor) require.Error(t, err) }