Files
mqtt-5-test/internal/contracts/contracts_test.go
T
Ryan 71ef622b4f
Test and deploy MQTT backend / test (push) Successful in 2m8s
Test and deploy MQTT backend / deploy (push) Successful in 18s
Deploy MQTT test backend with private viewer and Gitea CI/CD
2026-10-08 14:48:53 +03:30

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)
}
}
}