95 lines
3.9 KiB
Go
95 lines
3.9 KiB
Go
package contracts
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
const commandJSON = `{"schema_version":1,"device_id":"sim-001","request_id":"req-001","issued_at":"2026-10-06T10:00:00Z","expires_at":"2026-10-06T10:00:15Z","command":"ping"}`
|
|
|
|
func TestDecodeAndDeadline(t *testing.T) {
|
|
got, err := Decode("commands", []byte(commandJSON), "sim-001")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cmd := got.(*Command)
|
|
issued, _ := time.Parse(time.RFC3339, cmd.IssuedAt)
|
|
for _, tc := range []struct {
|
|
delta time.Duration
|
|
valid bool
|
|
}{{-time.Second, false}, {0, true}, {14 * time.Second, true}, {15 * time.Second, false}} {
|
|
err = cmd.CheckDeadline(issued.Add(tc.delta))
|
|
if (err == nil) != tc.valid {
|
|
t.Fatalf("deadline %s: %v", tc.delta, err)
|
|
}
|
|
}
|
|
}
|
|
func TestValidContracts(t *testing.T) {
|
|
cases := map[string]string{
|
|
"telemetry": `{"schema_version":1,"device_id":"sim-001","message_id":"evt-1","boot_id":"boot-1","sequence":0,"timestamp":"2026-10-06T10:00:00Z","readings":{"uptime_s":12}}`,
|
|
"status": `{"schema_version":1,"device_id":"sim-001","boot_id":"boot-1","state":"offline","will_configured_at":"2026-10-06T10:00:00Z"}`,
|
|
"acks": `{"schema_version":1,"device_id":"sim-001","request_id":"req-1","timestamp":"2026-10-06T10:00:00Z","stage":"completed","ok":true,"result":{"pong":true}}`,
|
|
}
|
|
for kind, payload := range cases {
|
|
t.Run(kind, func(t *testing.T) {
|
|
if _, err := Decode(kind, []byte(payload), "sim-001"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
func TestRejectUnsafePayloads(t *testing.T) {
|
|
for _, payload := range []string{
|
|
`null`, `[]`, commandJSON + `{}`, strings.Repeat("x", MaxPayloadBytes+1),
|
|
strings.Replace(commandJSON, `"ping"`, `"vend"`, 1),
|
|
strings.Replace(commandJSON, `"request_id":"req-001",`, "", 1),
|
|
strings.Replace(commandJSON, `"schema_version":1`, `"schema_version":2`, 1),
|
|
strings.Replace(commandJSON, `"command":"ping"`, `"command":"ping","password":"SECRET"`, 1),
|
|
strings.Replace(commandJSON, `"command":"ping"`, `"command":"ping","command":"get_version"`, 1),
|
|
strings.Replace(commandJSON, `"command":"ping"`, `"command":"ping","COMMAND":"get_version"`, 1),
|
|
strings.Replace(commandJSON, `10:00:15Z`, `10:00:00Z`, 1),
|
|
strings.Replace(commandJSON, `10:00:15Z`, `10:02:00Z`, 1),
|
|
strings.Replace(commandJSON, `10:00:15Z`, `10:00:15+00:00`, 1),
|
|
string([]byte{255, 0, 1}),
|
|
} {
|
|
if _, err := Decode("commands", []byte(payload), "sim-001"); err == nil {
|
|
t.Fatal("invalid payload accepted")
|
|
} else if strings.Contains(err.Error(), "SECRET") {
|
|
t.Fatal("raw payload leaked")
|
|
}
|
|
}
|
|
if _, err := Decode("commands", []byte(commandJSON), "other-device"); err == nil {
|
|
t.Fatal("wrong device accepted")
|
|
}
|
|
}
|
|
|
|
func TestNullReadingRejected(t *testing.T) {
|
|
payload := `{"schema_version":1,"device_id":"sim-001","message_id":"evt-1","boot_id":"boot-1","sequence":0,"timestamp":"2026-10-06T10:00:00Z","readings":{"uptime_s":null}}`
|
|
if _, err := Decode("telemetry", []byte(payload), "sim-001"); err == nil {
|
|
t.Fatal("null must not become a numeric zero")
|
|
}
|
|
}
|
|
func TestAckStages(t *testing.T) {
|
|
base := `{"schema_version":1,"device_id":"sim-001","request_id":"req-1","timestamp":"2026-10-06T10:00:00Z",`
|
|
for _, tc := range []struct {
|
|
body string
|
|
valid bool
|
|
}{
|
|
{`"stage":"accepted","ok":true}`, true},
|
|
{`"stage":"accepted"}`, false},
|
|
{`"stage":"accepted","ok":false}`, false},
|
|
{`"stage":"sent","ok":true}`, false},
|
|
{`"stage":"completed","ok":true}`, false},
|
|
{`"stage":"completed","ok":true,"result":{"version":"v1.0.0"}}`, true},
|
|
{`"stage":"completed","ok":true,"result":{"pong":true,"version":"v1"}}`, false},
|
|
{`"stage":"failed","ok":false,"error":{"code":"expired","message":"too late"}}`, true},
|
|
{`"stage":"failed","ok":true,"error":{"code":"expired","message":"too late"}}`, false},
|
|
} {
|
|
_, err := Decode("acks", []byte(base+tc.body), "sim-001")
|
|
if (err == nil) != tc.valid {
|
|
t.Fatalf("stage case %s: %v", tc.body, err)
|
|
}
|
|
}
|
|
}
|