240 lines
10 KiB
Go
240 lines
10 KiB
Go
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")
|
||
|
|
}
|
||
|
|
}
|