Files
story/internal/onboarding/device_reset_test.go
T
Codex Local bf1233d5df
ci / verify (push) Canceled after 0s
feat: expand companion platform, device controls, and memoir workflows
Add customer and operator login, BLE provisioning, device fleet controls, call memory, memoir generation, and refreshed family and maintenance portals. Include API contracts, migrations, firmware sources, tests, assets, and deployment documentation.
2026-10-08 10:06:44 +08:00

175 lines
7.1 KiB
Go

package onboarding
import (
"bytes"
"data-terminal-cloud/internal/devicereset"
"encoding/json"
"github.com/google/uuid"
"net/http"
"net/http/httptest"
"testing"
"time"
)
func resetInput() devicereset.Input {
return devicereset.Input{ResetID: uuid.NewString(), ResetCounter: 1, OccurredAt: time.Now().UTC(), Trigger: "device_menu", FirmwareVersion: "1.7.0"}
}
func TestFactoryResetDEVREQ1ClearsSettingsAndReplays(t *testing.T) {
f := newFixture(t)
_, err := f.s.pool.Exec(f.ctx, `UPDATE device_twin SET desired_config='{"identity":{"display_name":"旧用户"},"volume":{"speaker":12},"power":{"mode":"standby"}}',reported_config='{"volume":{"speaker":12}}',reported_revision=desired_revision WHERE device_id=$1`, f.d.ID)
must(t, err)
status, body := f.deviceCall("factory-reset/state", map[string]any{}, "", true)
if status != 200 || body["data"].(map[string]any)["reset_counter"] != float64(0) {
t.Fatalf("state %d %v", status, body)
}
in := resetInput()
status, body = f.deviceCall("factory-reset", in, "", true)
if status != 200 {
t.Fatalf("reset %d %v", status, body)
}
first, _ := json.Marshal(body["data"])
result := body["data"].(map[string]any)
if result["claim"] != nil || result["activation_state"] != "CLAIMABLE" || result["reset_counter"] != float64(1) {
t.Fatalf("result %v", result)
}
var desired, reported []byte
var revision int64
must(t, f.s.pool.QueryRow(f.ctx, `SELECT desired_config,reported_config,desired_revision FROM device_twin WHERE device_id=$1`, f.d.ID).Scan(&desired, &reported, &revision))
if string(desired) != "{}" || string(reported) != "{}" {
t.Fatal("old settings survived reset")
}
// Same durable body, fresh signed HTTP nonce/request ID after lost response.
status, body = f.deviceCall("factory-reset", in, "", true)
second, _ := json.Marshal(body["data"])
if status != 200 || !bytes.Equal(first, second) {
t.Fatalf("replay %d %v", status, body)
}
var after int64
must(t, f.s.pool.QueryRow(f.ctx, `SELECT desired_revision FROM device_twin WHERE device_id=$1`, f.d.ID).Scan(&after))
if after != revision {
t.Fatal("replay reset twice")
}
in.FirmwareVersion = "different"
status, body = f.deviceCall("factory-reset", in, "", true)
if status != 409 || body["code"] != "IDEMPOTENCY_KEY_CONFLICT" {
t.Fatalf("mutated replay %d %v", status, body)
}
}
func TestFactoryResetRejectsBothOwnershipKindsAndAnonymous(t *testing.T) {
for _, kind := range []string{"family", "account"} {
t.Run(kind, func(t *testing.T) {
f := newFixture(t)
if kind == "family" {
f.bound()
} else {
p := accountOnlyUser(t, f)
token := accountToken(t, f, p)
_, err := f.s.BindBLE(f.ctx, f.d, uuid.NewString(), BLEBindInput{Token: token})
must(t, err)
}
for _, path := range []string{"factory-reset/state", "factory-reset"} {
var in any = map[string]any{}
if path == "factory-reset" {
in = resetInput()
}
status, body := f.deviceCall(path, in, "", true)
if status != 409 || body["code"] != "DEVICE_RESET_AUTHORIZATION_REQUIRED" {
t.Fatalf("bound %s %d %v", path, status, body)
}
mux := http.NewServeMux()
f.s.RegisterRoutes(mux)
r := httptest.NewRequest("POST", "https://platform.example/api/v1/device/"+path, bytes.NewReader(requestJSON(in)))
r.Header.Set("X-Request-ID", uuid.NewString())
r.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
mux.ServeHTTP(w, r)
if w.Code != 401 {
t.Fatalf("anonymous request returned %d", w.Code)
}
}
var counter int64
must(t, f.s.pool.QueryRow(f.ctx, `SELECT factory_reset_counter FROM device WHERE id=$1`, f.d.ID).Scan(&counter))
if counter != 0 {
t.Fatal("refused reset changed device")
}
})
}
}
func TestFactoryResetRejectsFleetOTAAndDoesNotClearSettings(t *testing.T) {
f := newFixture(t)
_, err := f.s.pool.Exec(f.ctx, `UPDATE device_twin SET desired_config='{"identity":{"display_name":"保留"}}' WHERE device_id=$1`, f.d.ID)
must(t, err)
var release string
must(t, f.s.pool.QueryRow(f.ctx, `INSERT INTO fleet_firmware(tenant_id,model,version,size,sha256,object_ref,status) VALUES($1,$2,'reset-guard',32,$3,$4,'READY') RETURNING id::text`, f.tenant, f.d.Model, digest([]byte("test")), "test://"+uuid.NewString()).Scan(&release))
_, err = f.s.pool.Exec(f.ctx, `INSERT INTO fleet_ota(device_id,release_id,request_id) VALUES($1,$2,$3)`, f.d.ID, release, uuid.NewString())
must(t, err)
for _, phase := range []string{"pending", "accepted"} {
_, err = f.s.pool.Exec(f.ctx, `UPDATE fleet_ota SET status=$2 WHERE device_id=$1`, f.d.ID, phase)
must(t, err)
status, body := f.deviceCall("factory-reset", resetInput(), "", true)
if status != 409 || body["code"] != "DEVICE_RESET_FIRMWARE_IN_PROGRESS" {
t.Fatalf("OTA %s: %d %v", phase, status, body)
}
}
var counter int64
must(t, f.s.pool.QueryRow(f.ctx, `SELECT factory_reset_counter FROM device WHERE id=$1`, f.d.ID).Scan(&counter))
if counter != 0 {
t.Fatal("OTA denial incremented reset counter")
}
var name string
must(t, f.s.pool.QueryRow(f.ctx, `SELECT desired_config->'identity'->>'display_name' FROM device_twin WHERE device_id=$1`, f.d.ID).Scan(&name))
if name != "保留" {
t.Fatal("refused reset changed settings")
}
}
func TestFactoryResetRejectsActiveDirectCalls(t *testing.T) {
f := newFixture(t)
_, err := f.s.pool.Exec(f.ctx, `INSERT INTO call_session(device_id,tenant_id,boot_id,hook_event_id,call_generation,binding_generation) VALUES($1,$2,$3,'reset-guard',1,0)`, f.d.ID, f.tenant, uuid.NewString())
must(t, err)
for _, phase := range []string{"PREPARING", "STARTING", "TALKING", "STOPPING"} {
_, err = f.s.pool.Exec(f.ctx, `UPDATE call_session SET call_state=$2 WHERE device_id=$1`, f.d.ID, phase)
must(t, err)
status, body := f.deviceCall("factory-reset", resetInput(), "", true)
if status != 409 || body["code"] != "DEVICE_RESET_CALL_IN_PROGRESS" {
t.Fatalf("call %s: %d %v", phase, status, body)
}
}
_, err = f.s.pool.Exec(f.ctx, `UPDATE call_session SET call_state='ENDED' WHERE device_id=$1`, f.d.ID)
must(t, err)
status, body := f.deviceCall("factory-reset", resetInput(), "", true)
if status != 200 {
t.Fatalf("idle reset %d %v", status, body)
}
}
func TestFactoryResetOldReceiptCannotClearNewOwner(t *testing.T) {
f := newFixture(t)
in := resetInput()
status, body := f.deviceCall("factory-reset", in, "", true)
if status != 200 {
t.Fatalf("reset %d %v", status, body)
}
// Simulate the next boot's fresh authenticated device snapshot, then use
// the real account-first binding flow to establish a new owner.
d, err := f.s.findFactoryDevice(f.ctx, f.d.FactoryID, 1)
must(t, err)
f.d = d
p := accountOnlyUser(t, f)
token := accountToken(t, f, p)
_, err = f.s.BindBLE(f.ctx, f.d, uuid.NewString(), BLEBindInput{Token: token})
must(t, err)
status, body = f.deviceCall("factory-reset", in, "", true)
if status != 409 || body["code"] != "DEVICE_RESET_AUTHORIZATION_REQUIRED" {
t.Fatalf("old receipt %d %v", status, body)
}
var count int
must(t, f.s.pool.QueryRow(f.ctx, `SELECT count(*) FROM device_account_binding WHERE device_id=$1 AND released_at IS NULL`, f.d.ID).Scan(&count))
if count != 1 {
t.Fatal("cached reset removed new owner")
}
}