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