chore: checkpoint 第一版
ci / verify (push) Canceled after 0s

This commit is contained in:
Codex Local
2026-09-06 17:04:11 +08:00
parent a8cdec98d1
commit 0c2151ace9
659 changed files with 185855 additions and 554 deletions
+385
View File
@@ -0,0 +1,385 @@
package devicereset
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/base32"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"time"
"data-terminal-cloud/internal/audit"
"data-terminal-cloud/internal/deviceactivation"
"data-terminal-cloud/internal/deviceauth"
"data-terminal-cloud/internal/idempotency"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
var (
ErrInvalid = errors.New("device factory reset input is invalid")
ErrStateConflict = errors.New("device state does not allow factory reset")
ErrResetCounterConflict = errors.New("device factory reset counter is stale")
ErrFirmwareInProgress = errors.New("device firmware update is in progress")
ErrPendingTranscripts = errors.New("device has pending transcripts")
)
type Input struct {
ResetID string `json:"reset_id"`
ResetCounter int64 `json:"reset_counter"`
OccurredAt time.Time `json:"occurred_at"`
Trigger string `json:"trigger"`
FirmwareVersion string `json:"firmware_version"`
PendingTurnCount int `json:"pending_turn_count"`
ActiveConversationID string `json:"active_conversation_id,omitempty"`
}
type Claim struct {
Code string `json:"code"`
IssuedAt time.Time `json:"issued_at"`
ExpiresAt time.Time `json:"expires_at"`
}
type Response struct {
ResetID string `json:"reset_id"`
DeviceID string `json:"device_id"`
ActivationState string `json:"activation_state"`
ResetCounter int64 `json:"reset_counter"`
BindingRemoved bool `json:"binding_removed"`
PreviousBindingID string `json:"previous_binding_id,omitempty"`
ProviderCredentialStatus string `json:"provider_credential_status"`
LicenseAction string `json:"license_action"`
EndedConversationCount int `json:"ended_conversation_count"`
CancelledCommandCount int64 `json:"cancelled_command_count"`
ConfigRevision int64 `json:"config_revision"`
Claim Claim `json:"claim"`
LocalActions []string `json:"local_actions"`
ResetAt time.Time `json:"reset_at"`
}
type Service struct {
pool *pgxpool.Pool
executor *idempotency.Executor
audit *audit.Repository
}
func NewService(pool *pgxpool.Pool, executor *idempotency.Executor, auditRepository *audit.Repository) (*Service, error) {
if pool == nil || executor == nil || auditRepository == nil {
return nil, errors.New("device factory reset dependencies are required")
}
return &Service{pool: pool, executor: executor, audit: auditRepository}, nil
}
func Validate(input Input) error {
if _, err := uuid.Parse(strings.TrimSpace(input.ResetID)); err != nil || input.ResetCounter < 1 || input.OccurredAt.IsZero() ||
input.OccurredAt.After(time.Now().UTC().Add(5*time.Minute)) || len(strings.TrimSpace(input.FirmwareVersion)) < 1 ||
len(strings.TrimSpace(input.FirmwareVersion)) > 80 || input.PendingTurnCount < 0 {
return ErrInvalid
}
if input.Trigger != "physical_long_press" && input.Trigger != "device_menu" && input.Trigger != "service_tool" {
return ErrInvalid
}
if input.ActiveConversationID != "" {
if _, err := uuid.Parse(input.ActiveConversationID); err != nil {
return ErrInvalid
}
}
return nil
}
func (s *Service) Reset(ctx context.Context, identity deviceauth.Identity, idempotencyKey, requestID string, rawRequest []byte, input Input) (Response, bool, error) {
if err := Validate(input); err != nil {
return Response{}, false, err
}
if input.PendingTurnCount > 0 {
return Response{}, false, ErrPendingTranscripts
}
tenantID := identity.TenantID
result, err := s.executor.Execute(ctx, idempotency.Request{
TenantID: &tenantID, ActorKey: "device:" + identity.PublicID,
Scope: "POST /device/v3/factory-reset", Key: idempotencyKey,
RequestHash: idempotency.Hash(rawRequest), ExpiresAfter: 7 * 24 * time.Hour,
}, func(operationContext context.Context, tx pgx.Tx) (idempotency.Result, error) {
response, resetErr := s.reset(operationContext, tx, identity, requestID, input)
if resetErr != nil {
return idempotency.Result{}, resetErr
}
body, marshalErr := json.Marshal(response)
if marshalErr != nil {
return idempotency.Result{}, marshalErr
}
resourceID := identity.PublicID
return idempotency.Result{StatusCode: http.StatusOK, Body: body, ResourceID: &resourceID}, nil
})
if err != nil {
return Response{}, false, err
}
var response Response
if err := json.Unmarshal(result.Body, &response); err != nil {
return Response{}, result.Replayed, fmt.Errorf("decode device factory reset response: %w", err)
}
return response, result.Replayed, nil
}
func (s *Service) reset(ctx context.Context, tx pgx.Tx, identity deviceauth.Identity, requestID string, input Input) (Response, error) {
var activationState string
var currentCounter int64
if err := tx.QueryRow(ctx, `
SELECT activation_state,factory_reset_counter
FROM device
WHERE id=$1::uuid AND tenant_id=$2::uuid AND status='registered'
FOR UPDATE`, identity.ID, identity.TenantID).Scan(&activationState, &currentCounter); err != nil {
return Response{}, fmt.Errorf("lock device for factory reset: %w", err)
}
if activationState != string(deviceactivation.Claimable) && activationState != string(deviceactivation.Bound) &&
activationState != string(deviceactivation.Provisioning) && activationState != string(deviceactivation.Active) {
return Response{}, ErrStateConflict
}
if input.ResetCounter <= currentCounter {
return Response{}, ErrResetCounterConflict
}
var activeFirmware int
if err := tx.QueryRow(ctx, `
SELECT count(*) FROM firmware_assignment
WHERE device_id=$1::uuid AND tenant_id=$2::uuid AND status IN ('ASSIGNED','DOWNLOADING','INSTALLING')`,
identity.ID, identity.TenantID).Scan(&activeFirmware); err != nil {
return Response{}, fmt.Errorf("check active firmware update: %w", err)
}
if activeFirmware > 0 {
return Response{}, ErrFirmwareInProgress
}
now := time.Now().UTC()
endedConversations, err := endActiveConversations(ctx, tx, identity, requestID, now)
if err != nil {
return Response{}, err
}
var previousBindingID, previousHouseholdID, previousSubjectID string
err = tx.QueryRow(ctx, `
UPDATE device_binding
SET unbound_at=$3,unbound_by=NULL,reason='device_factory_reset'
WHERE device_id=$1::uuid AND tenant_id=$2::uuid AND unbound_at IS NULL
RETURNING public_id::text,household_id::text,subject_id::text`, identity.ID, identity.TenantID, now).
Scan(&previousBindingID, &previousHouseholdID, &previousSubjectID)
bindingRemoved := err == nil
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return Response{}, fmt.Errorf("close device binding for factory reset: %w", err)
}
if bindingRemoved {
if err := notifyPreviousOwner(ctx, tx, identity, previousHouseholdID, previousSubjectID, now); err != nil {
return Response{}, err
}
}
if _, err := tx.Exec(ctx, `
UPDATE device_claim SET revoked_at=$3
WHERE device_id=$1::uuid AND tenant_id=$2::uuid AND used_at IS NULL AND revoked_at IS NULL`,
identity.ID, identity.TenantID, now); err != nil {
return Response{}, fmt.Errorf("revoke old device claims: %w", err)
}
providerStatus := "NOT_PROVISIONED"
if tag, err := tx.Exec(ctx, `
UPDATE hardware_agent_provider_credential
SET status='SUSPENDED',updated_at=$3
WHERE device_id=$1::uuid AND tenant_id=$2::uuid AND status='ACTIVE'`, identity.ID, identity.TenantID, now); err != nil {
return Response{}, fmt.Errorf("suspend provider credential: %w", err)
} else if tag.RowsAffected() > 0 {
providerStatus = "SUSPENDED"
} else if err := tx.QueryRow(ctx, `SELECT status FROM hardware_agent_provider_credential WHERE device_id=$1::uuid`, identity.ID).Scan(&providerStatus); err != nil && !errors.Is(err, pgx.ErrNoRows) {
return Response{}, fmt.Errorf("read provider credential status: %w", err)
}
if _, err := tx.Exec(ctx, `UPDATE hardware_agent_device_binding SET status='DISABLED',updated_at=$2 WHERE device_id=$1::uuid AND status='ACTIVE'`, identity.ID, now); err != nil {
return Response{}, fmt.Errorf("disable hardware agent binding: %w", err)
}
var cancelledCommands int64
if tag, err := tx.Exec(ctx, `
UPDATE device_command
SET status='CANCELLED',result_summary='{"error_code":"DEVICE_FACTORY_RESET"}'::jsonb,
completed_at=$3,version=version+1,updated_at=$3
WHERE device_id=$1::uuid AND tenant_id=$2::uuid AND status IN ('QUEUED','DELIVERED','ACKNOWLEDGED')`,
identity.ID, identity.TenantID, now); err != nil {
return Response{}, fmt.Errorf("cancel device commands: %w", err)
} else {
cancelledCommands = tag.RowsAffected()
}
if err := transitionToClaimable(ctx, tx, identity, requestID); err != nil {
return Response{}, err
}
var configRevision int64
if err := tx.QueryRow(ctx, `
UPDATE device_twin
SET desired_revision=desired_revision+1,reported_revision=NULL,reported_config='{}'::jsonb,
capabilities='{}'::jsonb,updated_at=$3
WHERE device_id=$1::uuid AND tenant_id=$2::uuid
RETURNING desired_revision`, identity.ID, identity.TenantID, now).Scan(&configRevision); err != nil {
return Response{}, fmt.Errorf("reset device twin: %w", err)
}
if _, err := tx.Exec(ctx, `
UPDATE device SET factory_reset_counter=$3,version=version+1,updated_at=$4
WHERE id=$1::uuid AND tenant_id=$2::uuid`, identity.ID, identity.TenantID, input.ResetCounter, now); err != nil {
return Response{}, fmt.Errorf("record factory reset counter: %w", err)
}
claim, err := issueClaim(ctx, tx, identity, now)
if err != nil {
return Response{}, err
}
response := Response{
ResetID: input.ResetID, DeviceID: identity.PublicID, ActivationState: string(deviceactivation.Claimable),
ResetCounter: input.ResetCounter, BindingRemoved: bindingRemoved, PreviousBindingID: previousBindingID,
ProviderCredentialStatus: providerStatus, LicenseAction: "RETAINED",
EndedConversationCount: endedConversations, CancelledCommandCount: cancelledCommands,
ConfigRevision: configRevision, Claim: claim, ResetAt: now,
LocalActions: []string{"clear_volc_credential", "clear_user_context", "clear_session_state", "clear_network_credentials", "reboot_to_claimable"},
}
actorID := identity.PublicID
if err := s.audit.Record(ctx, tx, audit.Event{
TenantID: &identity.TenantID, ActorType: "device", ActorPublicID: &actorID,
Action: "device.factory_reset", ResourceType: "device", ResourceID: &actorID,
RequestID: requestID, Result: "success", Detail: map[string]any{
"reset_id": input.ResetID, "reset_counter": input.ResetCounter, "trigger": input.Trigger,
"firmware_version": input.FirmwareVersion, "previous_binding_id": previousBindingID,
"device_reported_conversation_id": input.ActiveConversationID,
"ended_conversation_count": endedConversations, "cancelled_command_count": cancelledCommands,
"provider_credential_status": providerStatus, "license_action": "RETAINED",
},
}); err != nil {
return Response{}, err
}
return response, nil
}
func notifyPreviousOwner(ctx context.Context, tx pgx.Tx, identity deviceauth.Identity, householdID, subjectID string, now time.Time) error {
var wasOnline bool
err := tx.QueryRow(ctx, `
UPDATE device_presence_state SET online=false,changed_at=$4
WHERE tenant_id=$1::uuid AND device_id=$2::uuid AND household_id=$3::uuid AND online
RETURNING true`, identity.TenantID, identity.ID, householdID, now).Scan(&wasOnline)
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
return fmt.Errorf("mark factory-reset device offline: %w", err)
}
if wasOnline {
if _, err := tx.Exec(ctx, `SELECT append_web_event($1::uuid,$2::uuid,$3::uuid,'device.presence.changed','device',$4::uuid,'offline')`,
identity.TenantID, householdID, subjectID, identity.PublicID); err != nil {
return fmt.Errorf("publish factory-reset presence event: %w", err)
}
}
if _, err := tx.Exec(ctx, `SELECT append_web_event($1::uuid,$2::uuid,$3::uuid,'device.binding.reset','device',$4::uuid,'CLAIMABLE')`,
identity.TenantID, householdID, subjectID, identity.PublicID); err != nil {
return fmt.Errorf("publish factory-reset binding event: %w", err)
}
return nil
}
func endActiveConversations(ctx context.Context, tx pgx.Tx, identity deviceauth.Identity, requestID string, now time.Time) (int, error) {
rows, err := tx.Query(ctx, `
SELECT id::text,status FROM conversation
WHERE device_id=$1::uuid AND tenant_id=$2::uuid AND status NOT IN ('COMPLETED','FAILED','EXPIRED')
FOR UPDATE`, identity.ID, identity.TenantID)
if err != nil {
return 0, fmt.Errorf("lock active conversations for factory reset: %w", err)
}
type activeConversation struct{ id, status string }
items := make([]activeConversation, 0)
for rows.Next() {
var item activeConversation
if err := rows.Scan(&item.id, &item.status); err != nil {
rows.Close()
return 0, err
}
items = append(items, item)
}
rows.Close()
if err := rows.Err(); err != nil {
return 0, err
}
for _, item := range items {
if _, err := tx.Exec(ctx, `
UPDATE conversation SET status='FAILED',ended_at=$2,stop_reason='device_factory_reset',
failure_code='DEVICE_FACTORY_RESET',version=version+1,updated_at=$2
WHERE id=$1::uuid`, item.id, now); err != nil {
return 0, fmt.Errorf("fail conversation during factory reset: %w", err)
}
if _, err := tx.Exec(ctx, `
INSERT INTO conversation_state_history(conversation_id,from_status,to_status,reason,source,request_id,occurred_at)
VALUES($1::uuid,$2,'FAILED','device factory reset','device',NULLIF($3,''),$4)`, item.id, item.status, requestID, now); err != nil {
return 0, fmt.Errorf("record factory reset conversation transition: %w", err)
}
if _, err := tx.Exec(ctx, `
UPDATE hardware_agent_session SET status='FAILED',provider_ended_at=COALESCE(provider_ended_at,$2),updated_at=$2
WHERE conversation_id=$1::uuid AND status NOT IN ('COMPLETED','FAILED','EXPIRED')`, item.id, now); err != nil {
return 0, fmt.Errorf("fail hardware agent session during factory reset: %w", err)
}
if _, err := tx.Exec(ctx, `
UPDATE rtc_task SET status='FAILED',updated_at=$2
WHERE conversation_id=$1::uuid AND status NOT IN ('COMPLETED','FAILED')`, item.id, now); err != nil {
return 0, fmt.Errorf("fail rtc task during factory reset: %w", err)
}
}
return len(items), nil
}
func transitionToClaimable(ctx context.Context, tx pgx.Tx, identity deviceauth.Identity, requestID string) error {
var stateText string
if err := tx.QueryRow(ctx, `SELECT activation_state FROM device WHERE id=$1::uuid AND tenant_id=$2::uuid FOR UPDATE`, identity.ID, identity.TenantID).Scan(&stateText); err != nil {
return fmt.Errorf("read device activation state after reset cleanup: %w", err)
}
state, err := deviceactivation.Parse(stateText)
if err != nil {
return ErrStateConflict
}
actorID := identity.PublicID
apply := func(target deviceactivation.State, reason string) error {
_, applyErr := deviceactivation.Apply(ctx, tx, deviceactivation.Input{
DeviceID: identity.ID, TenantID: identity.TenantID, Target: target,
Reason: reason, Source: "device-factory-reset", ActorType: "device",
ActorPublicID: &actorID, RequestID: requestID,
})
return applyErr
}
if state == deviceactivation.Provisioning {
if err := apply(deviceactivation.Bound, "设备恢复出厂,结束预配"); err != nil {
return fmt.Errorf("return provisioning device to bound state: %w", err)
}
state = deviceactivation.Bound
}
if state != deviceactivation.Claimable {
if state != deviceactivation.Bound && state != deviceactivation.Active && state != deviceactivation.Lost {
return ErrStateConflict
}
if err := apply(deviceactivation.Claimable, "设备恢复出厂并解除用户绑定"); err != nil {
return fmt.Errorf("make factory-reset device claimable: %w", err)
}
}
return nil
}
func issueClaim(ctx context.Context, tx pgx.Tx, identity deviceauth.Identity, issuedAt time.Time) (Claim, error) {
randomBytes := make([]byte, 10)
if _, err := rand.Read(randomBytes); err != nil {
return Claim{}, fmt.Errorf("generate factory reset claim: %w", err)
}
rawCode := base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(randomBytes)
code := strings.Join([]string{rawCode[0:4], rawCode[4:8], rawCode[8:12], rawCode[12:16]}, "-")
digest := sha256.Sum256([]byte(code))
expiresAt := issuedAt.Add(24 * time.Hour)
if _, err := tx.Exec(ctx, `
INSERT INTO device_claim(tenant_id,device_id,token_digest,expires_at,created_at)
VALUES($1::uuid,$2::uuid,$3,$4,$5)`, identity.TenantID, identity.ID, hex.EncodeToString(digest[:]), expiresAt, issuedAt); err != nil {
return Claim{}, fmt.Errorf("insert factory reset claim: %w", err)
}
return Claim{Code: code, IssuedAt: issuedAt, ExpiresAt: expiresAt}, nil
}
@@ -0,0 +1,140 @@
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)
}
}