9 "github.com/stretchr/testify/require"
12 func TestPayloadValidationRejectsInvalidContracts(t *testing.T) {
13 deadline := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
18 descriptor := `{"manufacturer":"example","model":"relay","firmware":"1","capabilities":[{"id":"relay","type":"switch","reports_state":true,"accepts_commands":false}]}`
19 testCases := []struct {
26 name: "rejects unknown descriptor field",
28 payload: strings.Replace(descriptor, `"model":"relay"`, `"model":"relay","room":"kitchen"`, 1),
31 name: "rejects unknown capability field",
33 payload: strings.Replace(descriptor, `"type":"switch"`, `"type":"switch","extra":true`, 1),
36 name: "requires both access flags",
38 payload: strings.Replace(descriptor, `,"accepts_commands":false`, ``, 1),
41 name: "rejects null access flag",
43 payload: strings.Replace(descriptor, `"accepts_commands":false`, `"accepts_commands":null`, 1),
46 name: "requires active access mode",
48 payload: strings.Replace(descriptor, `"reports_state":true`, `"reports_state":false`, 1),
51 name: "requires device metadata",
53 payload: strings.Replace(descriptor, `"model":"relay"`, `"model":""`, 1),
56 name: "requires capabilities",
58 payload: `{"manufacturer":"example","model":"relay","firmware":"1","capabilities":[]}`,
61 name: "rejects invalid capability identifier",
63 payload: strings.Replace(descriptor, `"id":"relay"`, `"id":"relay/1"`, 1),
66 name: "rejects duplicate capability",
68 payload: strings.Replace(descriptor, `}]`, `},{"id":"relay","type":"switch","reports_state":true,"accepts_commands":false}]`, 1),
71 name: "rejects unknown availability status",
73 payload: `{"status":"ready"}`,
76 name: "rejects unknown availability field",
78 payload: `{"status":"online","extra":1}`,
81 name: "requires report value",
87 name: "rejects unknown report field",
90 payload: `{"value":true,"extra":1}`,
93 name: "requires telemetry value",
99 name: "rejects unknown telemetry field",
102 payload: `{"value":1,"extra":1}`,
105 name: "rejects unknown command field",
108 payload: `{"value":true,"extra":1}`,
111 name: "requires command value",
117 name: "rejects array event",
123 name: "rejects unknown result field",
126 payload: `{"status":"completed","extra":1}`,
129 name: "rejects unknown hub field",
131 payload: `{"id":"` + deviceID + `","status":"ready","address":"example.com","extra":1}`,
134 name: "rejects unknown diagnostic field",
136 payload: `{"id":"` + deviceID + `","status":"ready","address":"example.com","diagnostics":{"observed_at":"2026-01-01T00:00:00Z","stale":false,"extra":1}}`,
139 name: "rejects unknown pairing field",
141 payload: `{"id":"` + deviceID + `","status":"ready","address":"example.com","pairing":{"extra":1}}`,
144 name: "rejects hub identity mismatch",
146 payload: `{"id":"` + commandID + `","status":"ready","address":"example.com"}`,
149 name: "rejects invalid hub status",
151 payload: `{"id":"` + deviceID + `","status":"online","address":"example.com"}`,
155 for _, tc := range testCases {
156 t.Run(tc.name, func(t *testing.T) {
157 data, err := EncodeEnvelope(metadata, json.RawMessage(tc.payload))
158 if tc.kind == Event {
159 require.Error(t, err)
164 require.NoError(t, err)
166 topic, err := (Address{
171 require.NoError(t, err)
173 _, err = Parse(topic, data, false)
174 require.Error(t, err)
179 func TestBuildersApplyCommandAndResultRules(t *testing.T) {
180 metadata := Metadata{MessageID: messageID}
181 _, err := BuildCommand(deviceID, "relay", metadata, ValuePayload{Value: json.RawMessage(`true`)})
182 require.Error(t, err)
184 _, err = BuildHubStatus(deviceID, metadata, HubStatusPayload{
186 Address: "example.com",
189 require.Error(t, err)
191 _, err = BuildDescriptor("bad", metadata, testDescriptor())
192 require.Error(t, err)
194 _, err = BuildReported(deviceID, "relay", metadata, ValuePayload{Value: json.RawMessage(`bad`)})
195 require.Error(t, err)
197 _, err = BuildAvailability(deviceID, metadata, AvailabilityPayload{Status: "bad"})
198 require.Error(t, err)
200 for _, value := range []string{`null`, `true`, `12`, `"open"`, `{"mode":"off"}`, `[1,2]`} {
201 var publication Publication
203 publication, err = BuildReported(deviceID, "point", metadata, ValuePayload{Value: json.RawMessage(value)})
204 require.NoError(t, err)
206 var message Message[ValuePayload]
208 message, err = ParseReported(publication.Topic, publication.Data, false)
209 require.NoError(t, err)
210 require.JSONEq(t, value, string(message.Payload.Value))
213 testCases := []struct {
220 name: "accepts intermediate result",
225 name: "accepts completed result",
230 name: "accepts rejection reason",
232 reason: "unsupported",
236 name: "accepts failure reason",
242 name: "requires rejection reason",
246 name: "requires failure reason",
250 name: "rejects successful reason",
255 name: "rejects unknown result status",
260 for _, tc := range testCases {
261 t.Run(tc.name, func(t *testing.T) {
262 correlated := Metadata{
263 MessageID: messageID,
264 CorrelationID: commandID,
267 var publication Publication
269 publication, err = BuildResult(deviceID, commandID, correlated, ResultPayload{
274 require.Error(t, err)
279 require.NoError(t, err)
281 var parsed Message[ResultPayload]
283 parsed, err = ParseResult(publication.Topic, publication.Data, false)
284 require.NoError(t, err)
285 require.Equal(t, tc.status, parsed.Payload.Status)
289 _, err = BuildResult(deviceID, commandID, metadata, ResultPayload{Status: "completed"})
290 require.Error(t, err)
292 descriptor := testDescriptor()
293 descriptor.Attribution = strings.Repeat("x", 2001)
294 _, err = BuildDescriptor(deviceID, metadata, descriptor)
295 require.Error(t, err)