Files
story/internal/onboarding/device_control_test.go
T

240 lines
10 KiB
Go
Raw Normal View History

package onboarding
import (
"data-terminal-cloud/internal/device"
"github.com/google/uuid"
"strings"
"sync"
"testing"
)
func TestControlInputValidation(t *testing.T) {
valid := DeviceControlReport{Firmware: "1.7.0", Revision: 1, Speaker: 70, RSSI: -55}
if err := validateControlReport(valid); err != nil {
t.Fatal(err)
}
for _, mutate := range []func(*DeviceControlReport){
func(v *DeviceControlReport) { v.Speaker = -1 }, func(v *DeviceControlReport) { v.Speaker = 101 },
func(v *DeviceControlReport) { v.Revision = -1 }, func(v *DeviceControlReport) { v.Firmware = "" },
func(v *DeviceControlReport) { v.RSSI = -128 },
func(v *DeviceControlReport) { s := "名称\n"; v.DisplayName = &s },
func(v *DeviceControlReport) { v.Power = &DevicePowerReport{Mode: "standby", MicrophonePaused: false} },
func(v *DeviceControlReport) {
v.Power = &DevicePowerReport{Mode: "standby", MicrophonePaused: true, BacklightPercent: 80}
},
func(v *DeviceControlReport) { v.Power = &DevicePowerReport{Mode: "off"} },
func(v *DeviceControlReport) { v.Power = &DevicePowerReport{Mode: "active", BacklightPercent: 101} },
} {
invalid := valid
mutate(&invalid)
if validateControlReport(invalid) == nil {
t.Fatalf("accepted %+v", invalid)
}
}
ack := DeviceControlAck{ID: uuid.NewString(), Status: "SUCCEEDED"}
if err := validateControlAck(ack); err != nil {
t.Fatal(err)
}
ack.Status = "QUEUED"
if validateControlAck(ack) == nil {
t.Fatal("accepted state regression")
}
ack.Status = "FAILED"
ack.Result = map[string]any{"body": strings.Repeat("x", 4097)}
if validateControlAck(ack) == nil {
t.Fatal("accepted oversized result")
}
}
func TestScreenWakeValidation(t *testing.T) {
in := DeviceControlReport{Firmware: "1.7.0-controls.32", Revision: 4, Speaker: 60, RSSI: -50,
Power: &DevicePowerReport{Mode: "active", BacklightPercent: 80},
ScreenWake: &DeviceScreenWake{ID: uuid.NewString(), Revision: 4}}
must(t, validateControlReport(in))
for _, mutation := range []func(*DeviceControlReport){
func(r *DeviceControlReport) { r.ScreenWake = &DeviceScreenWake{ID: "bad", Revision: 4} },
func(r *DeviceControlReport) { r.ScreenWake = &DeviceScreenWake{ID: uuid.NewString()} },
func(r *DeviceControlReport) { r.Power = nil },
func(r *DeviceControlReport) { r.Power = &DevicePowerReport{Mode: "standby", MicrophonePaused: true} },
} {
invalid := in
mutation(&invalid)
if validateControlReport(invalid) == nil {
t.Fatal("invalid screen wake accepted")
}
}
}
func TestScreenWakeReconcilesAndFencesNewerConfiguration(t *testing.T) {
f := newFixture(t)
_, err := f.s.pool.Exec(f.ctx, `UPDATE device_twin SET desired_revision=4,reported_revision=4,
desired_config='{"volume":{"speaker":31},"identity":{"display_name":"保留名称"},"power":{"mode":"standby"}}',
reported_config='{}' WHERE device_id=$1`, f.d.ID)
must(t, err)
in := DeviceControlReport{Firmware: "1.7.0-controls.32", Revision: 4, Speaker: 31, RSSI: -50,
Power: &DevicePowerReport{Mode: "active", BacklightPercent: 80},
ScreenWake: &DeviceScreenWake{ID: uuid.NewString(), Revision: 4}}
check := func(wantRevision int64, wantMode, wantResult string) {
t.Helper()
out, err := f.s.DeviceControlSync(f.ctx, f.d, in)
must(t, err)
if receipt := out["screen_wake"].(*DeviceScreenWakeReceipt); receipt.ID != in.ScreenWake.ID || receipt.Result != wantResult {
t.Fatal(receipt)
}
var revision, reported int64
var mode, name string
var speaker int
must(t, f.s.pool.QueryRow(f.ctx, `SELECT desired_revision,reported_revision,desired_config->'power'->>'mode',
desired_config->'identity'->>'display_name',(desired_config->'volume'->>'speaker')::int
FROM device_twin WHERE device_id=$1`, f.d.ID).Scan(&revision, &reported, &mode, &name, &speaker))
if revision != wantRevision || reported != in.Revision || mode != wantMode || name != "保留名称" || speaker != 31 {
t.Fatalf("revision=%d reported=%d mode=%s name=%s speaker=%d", revision, reported, mode, name, speaker)
}
}
check(5, "active", "applied")
check(5, "active", "applied") // Retry after a lost reply must not add a revision.
_, err = f.s.pool.Exec(f.ctx, `UPDATE device_twin SET desired_revision=6,
desired_config=jsonb_set(desired_config,'{power}','{"mode":"standby"}') WHERE device_id=$1`, f.d.ID)
must(t, err)
check(6, "standby", "applied") // The old successful event cannot undo a later standby.
in.ScreenWake = &DeviceScreenWake{ID: uuid.NewString(), Revision: 4}
check(6, "standby", "superseded") // An offline stale tap respects the newer config.
in.ScreenWake = &DeviceScreenWake{ID: uuid.NewString(), Revision: 6}
in.Revision = 6
var wg sync.WaitGroup
errs := make(chan error, 2)
for i := 0; i < 2; i++ {
wg.Add(1)
go func() { defer wg.Done(); _, err := f.s.DeviceControlSync(f.ctx, f.d, in); errs <- err }()
}
wg.Wait()
close(errs)
for err := range errs {
must(t, err)
}
check(7, "active", "applied")
in.ScreenWake = &DeviceScreenWake{ID: in.ScreenWake.ID, Revision: 7}
if _, err := f.s.DeviceControlSync(f.ctx, f.d, in); err == nil {
t.Fatal("receipt ID reused with a changed revision")
}
foreign := f.d
foreign.ID = uuid.NewString()
if _, err := f.s.DeviceControlSync(f.ctx, foreign, in); err == nil {
t.Fatal("foreign device altered power")
}
}
func TestControlNameReportsAreActualReadings(t *testing.T) {
f := newFixture(t)
_, err := f.s.pool.Exec(f.ctx, `UPDATE device_twin SET desired_revision=2,desired_config='{"identity":{"display_name":"云端名称"}}',reported_config='{}',reported_revision=NULL WHERE device_id=$1`, f.d.ID)
must(t, err)
in := DeviceControlReport{Firmware: "1.7.0", Speaker: 60, RSSI: -50}
_, err = f.s.DeviceControlSync(f.ctx, f.d, in)
must(t, err)
var name *string
must(t, f.s.pool.QueryRow(f.ctx, `SELECT reported_config->'identity'->>'display_name' FROM device_twin WHERE device_id=$1`, f.d.ID).Scan(&name))
if name != nil {
t.Fatal("old firmware falsely reported desired name")
}
actual := "设备实际名称"
in.DisplayName = &actual
_, err = f.s.DeviceControlSync(f.ctx, f.d, in)
must(t, err)
must(t, f.s.pool.QueryRow(f.ctx, `SELECT reported_config->'identity'->>'display_name' FROM device_twin WHERE device_id=$1`, f.d.ID).Scan(&name))
if name == nil || *name != actual {
t.Fatal("actual name lost or replaced by desired")
}
actual = ""
_, err = f.s.DeviceControlSync(f.ctx, f.d, in)
must(t, err)
must(t, f.s.pool.QueryRow(f.ctx, `SELECT reported_config->'identity'->>'display_name' FROM device_twin WHERE device_id=$1`, f.d.ID).Scan(&name))
if name == nil || *name != "" {
t.Fatal("explicit default name not reported")
}
}
func TestControlPowerActualReadings(t *testing.T) {
f := newFixture(t)
_, err := f.s.pool.Exec(f.ctx, `UPDATE device_twin SET desired_config=desired_config || '{"power":{"mode":"standby"}}'::jsonb WHERE device_id=$1`, f.d.ID)
must(t, err)
in := DeviceControlReport{Firmware: "1.7.0", Speaker: 60, RSSI: -50}
_, err = f.s.DeviceControlSync(f.ctx, f.d, in)
must(t, err)
var mode *string
must(t, f.s.pool.QueryRow(f.ctx, `SELECT reported_config->'power'->>'mode' FROM device_twin WHERE device_id=$1`, f.d.ID).Scan(&mode))
if mode != nil {
t.Fatal("old firmware falsely claimed standby")
}
for _, p := range []DevicePowerReport{{Mode: "active", BacklightPercent: 80, MicrophoneReads: 12}, {Mode: "standby", MicrophonePaused: true, BacklightPercent: 0, MicrophoneReads: 12}} {
in.Power = &p
_, err = f.s.DeviceControlSync(f.ctx, f.d, in)
must(t, err)
must(t, f.s.pool.QueryRow(f.ctx, `SELECT reported_config->'power'->>'mode' FROM device_twin WHERE device_id=$1`, f.d.ID).Scan(&mode))
if mode == nil || *mode != p.Mode {
t.Fatal("power report substituted by desired mode")
}
}
}
func TestDeviceControlReportsAndCommandIsolation(t *testing.T) {
f := newFixture(t)
_, err := f.s.pool.Exec(f.ctx, `UPDATE device_twin SET desired_revision=2,desired_config='{"volume":{"speaker":40}}',reported_revision=NULL WHERE device_id=$1`, f.d.ID)
must(t, err)
commandID := uuid.NewString()
_, err = f.s.pool.Exec(f.ctx, `INSERT INTO device_command(public_id,tenant_id,device_id,command_type,requested_by,idempotency_key,request_digest,expires_at)
VALUES($1,$2,$3,'diagnostics',$4,$5,repeat('d',64),now()+interval '5 minutes')`, commandID, f.tenant, f.d.ID, f.operatorID, uuid.NewString())
must(t, err)
in := DeviceControlReport{Firmware: "1.7.0", Speaker: 70, RSSI: -50, Busy: true}
out, err := f.s.DeviceControlSync(f.ctx, f.d, in)
must(t, err)
if len(out["commands"].([]device.Command)) != 0 {
t.Fatal("delivered during a call")
}
var actual int
var revision *int64
must(t, f.s.pool.QueryRow(f.ctx, `SELECT (reported_config->'volume'->>'speaker')::int,reported_revision FROM device_twin WHERE device_id=$1`, f.d.ID).Scan(&actual, &revision))
if actual != 70 || revision != nil {
t.Fatal("cloud desired value falsely reported as applied")
}
in.Speaker = 40
in.Revision = 2
in.Busy = false
out, err = f.s.DeviceControlSync(f.ctx, f.d, in)
must(t, err)
items := out["commands"].([]device.Command)
if len(items) != 1 || items[0].ID != commandID || items[0].Status != "DELIVERED" {
t.Fatal(items)
}
in.Revision = 3
if _, err = f.s.DeviceControlSync(f.ctx, f.d, in); err == nil {
t.Fatal("accepted future revision")
}
in.Revision = 1
if _, err = f.s.DeviceControlSync(f.ctx, f.d, in); err == nil {
t.Fatal("accepted stale revision")
}
ack := DeviceControlAck{ID: commandID, Status: "ACKNOWLEDGED"}
foreign := f.d
foreign.ID = uuid.NewString()
if _, err = f.s.DeviceControlAcknowledge(f.ctx, foreign, ack); err == nil {
t.Fatal("foreign device acknowledged command")
}
_, err = f.s.DeviceControlAcknowledge(f.ctx, f.d, ack)
must(t, err)
ack.Status = "SUCCEEDED"
ack.Result = map[string]any{"speaker": 40}
_, err = f.s.DeviceControlAcknowledge(f.ctx, f.d, ack)
must(t, err)
_, err = f.s.DeviceControlAcknowledge(f.ctx, f.d, ack)
must(t, err)
ack.Status = "FAILED"
if _, err = f.s.DeviceControlAcknowledge(f.ctx, f.d, ack); err == nil {
t.Fatal("changed terminal result")
}
blocked := f.d
blocked.Status = "revoked"
if _, err = f.s.DeviceControlSync(f.ctx, blocked, in); err == nil {
t.Fatal("revoked device synced")
}
}