Files
story/internal/devicereset/service_integration_test.go
T
Codex Local 0c2151ace9
ci / verify (push) Canceled after 0s
chore: checkpoint 第一版
2026-09-06 17:04:11 +08:00

141 lines
7.2 KiB
Go

package devicereset
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"os"
"testing"
"time"
"data-terminal-cloud/internal/audit"
"data-terminal-cloud/internal/deviceauth"
"data-terminal-cloud/internal/idempotency"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
)
func TestFactoryResetUnbindsAndSuspendsProviderCredential(t *testing.T) {
databaseURL := os.Getenv("TEST_DATABASE_URL")
if databaseURL == "" {
t.Skip("TEST_DATABASE_URL is not set")
}
ctx := context.Background()
pool, err := pgxpool.New(ctx, databaseURL)
if err != nil {
t.Fatal(err)
}
t.Cleanup(pool.Close)
deviceID := uuid.NewString()
devicePublicID := uuid.NewString()
serial := sha256.Sum256([]byte(devicePublicID))
const tenantID = "10000000-0000-0000-0000-000000000001"
if _, err := pool.Exec(ctx, `
INSERT INTO device(id,public_id,tenant_id,serial_digest,model,status,credential_ref,activation_state,activation_reason)
VALUES($1::uuid,$2::uuid,$3::uuid,$4,'factory-reset-test','registered',$5,'ACTIVE','integration-test')`,
deviceID, devicePublicID, tenantID, hex.EncodeToString(serial[:]), "test://"+devicePublicID); err != nil {
t.Fatal(err)
}
if _, err := pool.Exec(ctx, `INSERT INTO device_twin(tenant_id,device_id,desired_revision,desired_config) VALUES($1::uuid,$2::uuid,2,'{"voice_runtime":"volc_hardware_agent"}'::jsonb)`, tenantID, deviceID); err != nil {
t.Fatal(err)
}
if _, err := pool.Exec(ctx, `
INSERT INTO device_binding(tenant_id,device_id,household_id,subject_id,reason)
SELECT $1::uuid,$2::uuid,h.id,sp.id,'factory-reset-integration'
FROM household h JOIN subject_profile sp ON sp.household_id=h.id AND sp.tenant_id=h.tenant_id
WHERE h.public_id='10000000-0000-0000-0000-000000000211'::uuid
AND sp.public_id='10000000-0000-0000-0000-000000000411'::uuid`, tenantID, deviceID); err != nil {
t.Fatal(err)
}
if _, err := pool.Exec(ctx, `
INSERT INTO device_presence_state(tenant_id,device_id,household_id,subject_id,online,last_seen_at,changed_at)
SELECT $1::uuid,$2::uuid,h.id,sp.id,true,now(),now()
FROM household h JOIN subject_profile sp ON sp.household_id=h.id AND sp.tenant_id=h.tenant_id
WHERE h.public_id='10000000-0000-0000-0000-000000000211'::uuid
AND sp.public_id='10000000-0000-0000-0000-000000000411'::uuid`, tenantID, deviceID); err != nil {
t.Fatal(err)
}
if _, err := pool.Exec(ctx, `
INSERT INTO hardware_agent_device_binding(tenant_id,device_id,product_key,provider_device_ref,credential_ref,bot_id,transport,status)
VALUES($1::uuid,$2::uuid,'product-test','device-test',$3,'bot-test','low_load','ACTIVE')`, tenantID, deviceID, "volc-device-secret://"+devicePublicID); err != nil {
t.Fatal(err)
}
if _, err := pool.Exec(ctx, `
INSERT INTO hardware_agent_provider_credential(tenant_id,device_id,credential_ref,instance_id,product_key,provider_device_name,hardware_id,encrypted_secret,nonce,status)
VALUES($1::uuid,$2::uuid,$3,'instance-test','product-test','device-test','hardware-test',$4,$5,'ACTIVE')`,
tenantID, deviceID, "volc-device-secret://"+devicePublicID, []byte("encrypted"), []byte("nonce")); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
_, _ = pool.Exec(ctx, `DELETE FROM idempotency_record WHERE actor_key=$1`, "device:"+devicePublicID)
_, _ = pool.Exec(ctx, `DELETE FROM audit_event WHERE resource_id=$1::uuid`, devicePublicID)
_, _ = pool.Exec(ctx, `DELETE FROM web_event WHERE resource_public_id=$1::uuid`, devicePublicID)
_, _ = pool.Exec(ctx, `DELETE FROM device_presence_state WHERE device_id=$1::uuid`, deviceID)
_, _ = pool.Exec(ctx, `DELETE FROM device_claim WHERE device_id=$1::uuid`, deviceID)
_, _ = pool.Exec(ctx, `DELETE FROM hardware_agent_provider_credential WHERE device_id=$1::uuid`, deviceID)
_, _ = pool.Exec(ctx, `DELETE FROM hardware_agent_device_binding WHERE device_id=$1::uuid`, deviceID)
_, _ = pool.Exec(ctx, `DELETE FROM device_binding WHERE device_id=$1::uuid`, deviceID)
_, _ = pool.Exec(ctx, `DELETE FROM device_activation_transition WHERE device_id=$1::uuid`, deviceID)
_, _ = pool.Exec(ctx, `DELETE FROM device_twin WHERE device_id=$1::uuid`, deviceID)
_, _ = pool.Exec(ctx, `DELETE FROM device WHERE id=$1::uuid`, deviceID)
})
service, err := NewService(pool, idempotency.NewExecutor(pool), audit.NewRepository())
if err != nil {
t.Fatal(err)
}
input := Input{
ResetID: uuid.NewString(), ResetCounter: 1, OccurredAt: time.Now().UTC(),
Trigger: "physical_long_press", FirmwareVersion: "1.1.0", PendingTurnCount: 0,
}
raw, _ := json.Marshal(input)
identity := deviceauth.Identity{ID: deviceID, PublicID: devicePublicID, TenantID: tenantID, Status: "registered", ActivationState: "ACTIVE"}
first, replayed, err := service.Reset(ctx, identity, "factory-reset-integration-key", "factory-reset-integration-request", raw, input)
if err != nil || replayed {
t.Fatalf("first reset response=%+v replayed=%v err=%v", first, replayed, err)
}
second, replayed, err := service.Reset(ctx, identity, "factory-reset-integration-key", "factory-reset-integration-request-replay", raw, input)
if err != nil || !replayed || second.Claim.Code != first.Claim.Code {
t.Fatalf("replay response=%+v replayed=%v err=%v", second, replayed, err)
}
var state, providerStatus, bindingStatus string
var resetCounter int64
var activeBindings, bindingResetEvents int
var online bool
if err := pool.QueryRow(ctx, `SELECT activation_state,factory_reset_counter FROM device WHERE id=$1::uuid`, deviceID).Scan(&state, &resetCounter); err != nil {
t.Fatal(err)
}
if err := pool.QueryRow(ctx, `SELECT count(*) FROM device_binding WHERE device_id=$1::uuid AND unbound_at IS NULL`, deviceID).Scan(&activeBindings); err != nil {
t.Fatal(err)
}
if err := pool.QueryRow(ctx, `SELECT status FROM hardware_agent_provider_credential WHERE device_id=$1::uuid`, deviceID).Scan(&providerStatus); err != nil {
t.Fatal(err)
}
if err := pool.QueryRow(ctx, `SELECT status FROM hardware_agent_device_binding WHERE device_id=$1::uuid`, deviceID).Scan(&bindingStatus); err != nil {
t.Fatal(err)
}
if err := pool.QueryRow(ctx, `SELECT online FROM device_presence_state WHERE device_id=$1::uuid`, deviceID).Scan(&online); err != nil {
t.Fatal(err)
}
if err := pool.QueryRow(ctx, `SELECT count(*) FROM web_event WHERE resource_public_id=$1::uuid AND event_type='device.binding.reset'`, devicePublicID).Scan(&bindingResetEvents); err != nil {
t.Fatal(err)
}
if state != "CLAIMABLE" || resetCounter != 1 || activeBindings != 0 || providerStatus != "SUSPENDED" || bindingStatus != "DISABLED" ||
online || bindingResetEvents != 1 || !first.BindingRemoved || first.LicenseAction != "RETAINED" || first.Claim.Code == "" {
t.Fatalf("reset state=%s counter=%d bindings=%d provider=%s binding=%s online=%v reset_events=%d response=%+v", state, resetCounter, activeBindings, providerStatus, bindingStatus, online, bindingResetEvents, first)
}
}
func TestFactoryResetRejectsPendingTranscriptsBeforeDatabaseWork(t *testing.T) {
service := &Service{}
input := Input{ResetID: uuid.NewString(), ResetCounter: 1, OccurredAt: time.Now().UTC(), Trigger: "device_menu", FirmwareVersion: "1.0.0", PendingTurnCount: 1}
_, _, err := service.Reset(context.Background(), deviceauth.Identity{}, "factory-reset-pending-turns", "request", []byte(`{}`), input)
if !errors.Is(err, ErrPendingTranscripts) {
t.Fatalf("error=%v", err)
}
}