2026-08-29 21:56:51 +08:00
package opsdevice
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"os"
"testing"
"data-terminal-cloud/internal/audit"
"data-terminal-cloud/internal/deviceauth"
"data-terminal-cloud/internal/identity"
"github.com/jackc/pgx/v5/pgxpool"
)
func TestOperationsDeviceLifecycle ( 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 )
}
defer pool . Close ()
resolver , err := deviceauth . NewDatabaseSecretResolver ( pool , "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" )
if err != nil {
t . Fatal ( err )
}
service , err := NewService ( pool , resolver , audit . NewRepository ())
if err != nil {
t . Fatal ( err )
}
principal := integrationOperatorPrincipal ( t , ctx , pool )
const tenantID = "10000000-0000-0000-0000-000000000010"
created , err := service . Create ( ctx , principal , tenantID , "ops-device-integration-create" , CreateInput {
Model : "integration-terminal" , HardwareRevision : "rev-test" ,
HouseholdID : "10000000-0000-0000-0000-000000000211" , SubjectID : "10000000-0000-0000-0000-000000000411" ,
2026-09-06 17:04:11 +08:00
VoiceRuntime : "volc_hardware_agent" ,
2026-08-29 21:56:51 +08:00
})
if err != nil {
t . Fatal ( err )
}
deviceID := created . Device . ID
defer func () {
_ , _ = pool . Exec ( ctx , `DELETE FROM audit_event WHERE resource_id=$1::uuid` , deviceID )
2026-09-06 17:04:11 +08:00
_ , _ = pool . Exec ( ctx , `DELETE FROM device_activation_transition WHERE device_id=(SELECT id FROM device WHERE public_id=$1::uuid)` , deviceID )
_ , _ = pool . Exec ( ctx , `DELETE FROM hardware_agent_device_binding WHERE device_id=(SELECT id FROM device WHERE public_id=$1::uuid)` , deviceID )
2026-08-29 21:56:51 +08:00
_ , _ = pool . Exec ( ctx , `DELETE FROM device_binding WHERE device_id=(SELECT id FROM device WHERE public_id=$1::uuid)` , deviceID )
_ , _ = pool . Exec ( ctx , `DELETE FROM device_credential WHERE device_id=(SELECT id FROM device WHERE public_id=$1::uuid)` , deviceID )
_ , _ = pool . Exec ( ctx , `DELETE FROM device_twin WHERE device_id=(SELECT id FROM device WHERE public_id=$1::uuid)` , deviceID )
_ , _ = pool . Exec ( ctx , `DELETE FROM device WHERE public_id=$1::uuid` , deviceID )
}()
2026-09-06 17:04:11 +08:00
if len ( created . Credential . Secret ) != 64 || created . Credential . KeyVersion != 1 || created . Device . SubjectName == nil || * created . Device . SubjectName != "测试长者" ||
created . Device . ActivationState != "BOUND" ||
created . Device . VoiceRuntime != "volc_hardware_agent" || created . Device . HardwareAgentBotID != "" || created . Device . HardwareAgentTransport != "" || created . Device . HardwareAgentMinimumSDKVersion != "" || created . Device . HardwareAgentStatus != "" {
2026-08-29 21:56:51 +08:00
t . Fatalf ( "unexpected created result: %+v" , created )
}
if created . Claim != nil {
t . Fatalf ( "assigned device unexpectedly received claim: %+v" , created . Claim )
}
if _ , err := service . IssueDeviceClaim ( ctx , principal , tenantID , deviceID , "ops-device-integration-bound-claim" ); ! errors . Is ( err , ErrClaimUnavailable ) {
t . Fatalf ( "bound device claim error=%v" , err )
}
resolved , err := resolver . Resolve ( ctx , "device-secret://" + deviceID )
if err != nil || ! bytes . Equal ( resolved , [] byte ( created . Credential . Secret )) {
t . Fatalf ( "created credential did not resolve: err=%v" , err )
}
model := "integration-terminal-v2"
status := "quarantined"
updated , err := service . Update ( ctx , principal , tenantID , deviceID , "ops-device-integration-update" , created . Device . Version , UpdateInput { Model : & model , Status : & status })
if err != nil || updated . Model != model || updated . Status != status || updated . Version != created . Device . Version + 1 {
t . Fatalf ( "unexpected update: %+v err=%v" , updated , err )
}
rotated , err := service . RotateCredential ( ctx , principal , tenantID , deviceID , "ops-device-integration-rotate" )
if err != nil || rotated . KeyVersion != 2 || rotated . Secret == created . Credential . Secret {
t . Fatalf ( "unexpected credential rotation: %+v err=%v" , rotated , err )
}
resolved , err = resolver . Resolve ( ctx , "device-secret://" + deviceID )
if err != nil || ! bytes . Equal ( resolved , [] byte ( rotated . Secret )) {
t . Fatalf ( "rotated credential did not resolve: err=%v" , err )
}
retired , err := service . Delete ( ctx , principal , tenantID , deviceID , "ops-device-integration-delete" , updated . Version )
2026-09-06 17:04:11 +08:00
if err != nil || retired . Status != "revoked" || retired . ActivationState != "REVOKED" || retired . CredentialStatus != "revoked" || retired . HouseholdID != nil || retired . HardwareAgentStatus != "" {
2026-08-29 21:56:51 +08:00
t . Fatalf ( "unexpected delete result: %+v err=%v" , retired , err )
}
if _ , err := resolver . Resolve ( ctx , "device-secret://" + deviceID ); err == nil {
t . Fatal ( "retired device credential remained active" )
}
}
func TestOperationsDeviceClaimIssueAndRotation ( 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 )
}
defer pool . Close ()
resolver , err := deviceauth . NewDatabaseSecretResolver ( pool , "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" )
if err != nil {
t . Fatal ( err )
}
service , err := NewService ( pool , resolver , audit . NewRepository ())
if err != nil {
t . Fatal ( err )
}
principal := integrationOperatorPrincipal ( t , ctx , pool )
const tenantID = "10000000-0000-0000-0000-000000000010"
created , err := service . Create ( ctx , principal , tenantID , "ops-device-claim-integration-create" , CreateInput { Model : "unbound-integration-terminal" })
if err != nil {
t . Fatal ( err )
}
deviceID := created . Device . ID
defer func () {
_ , _ = pool . Exec ( ctx , `DELETE FROM audit_event WHERE resource_id=$1::uuid` , deviceID )
2026-09-06 17:04:11 +08:00
_ , _ = pool . Exec ( ctx , `DELETE FROM device_activation_transition WHERE device_id=(SELECT id FROM device WHERE public_id=$1::uuid)` , deviceID )
2026-08-29 21:56:51 +08:00
_ , _ = pool . Exec ( ctx , `DELETE FROM device_claim WHERE device_id=(SELECT id FROM device WHERE public_id=$1::uuid)` , deviceID )
_ , _ = pool . Exec ( ctx , `DELETE FROM device_credential WHERE device_id=(SELECT id FROM device WHERE public_id=$1::uuid)` , deviceID )
_ , _ = pool . Exec ( ctx , `DELETE FROM device_twin WHERE device_id=(SELECT id FROM device WHERE public_id=$1::uuid)` , deviceID )
_ , _ = pool . Exec ( ctx , `DELETE FROM device WHERE public_id=$1::uuid` , deviceID )
}()
2026-09-06 17:04:11 +08:00
if created . Device . ActivationState != "CLAIMABLE" || created . Claim == nil || len ( created . Claim . ClaimCode ) != 19 || ! created . Claim . ExpiresAt . After ( created . Claim . IssuedAt ) {
2026-08-29 21:56:51 +08:00
t . Fatalf ( "unexpected initial claim: %+v" , created . Claim )
}
firstDigest := sha256 . Sum256 ([] byte ( created . Claim . ClaimCode ))
var storedDigest string
if err := pool . QueryRow ( ctx , `SELECT token_digest FROM device_claim WHERE device_id=(SELECT id FROM device WHERE public_id=$1::uuid) AND revoked_at IS NULL` , deviceID ). Scan ( & storedDigest ); err != nil || storedDigest != hex . EncodeToString ( firstDigest [:]) {
t . Fatalf ( "stored initial claim digest=%s err=%v" , storedDigest , err )
}
rotated , err := service . IssueDeviceClaim ( ctx , principal , tenantID , deviceID , "ops-device-claim-integration-rotate" )
if err != nil || rotated . ClaimCode == created . Claim . ClaimCode {
t . Fatalf ( "rotated claim=%+v err=%v" , rotated , err )
}
var activeClaims , revokedClaims int
if err := pool . QueryRow ( ctx , `SELECT count(*) FILTER (WHERE revoked_at IS NULL),count(*) FILTER (WHERE revoked_at IS NOT NULL) FROM device_claim WHERE device_id=(SELECT id FROM device WHERE public_id=$1::uuid)` , deviceID ). Scan ( & activeClaims , & revokedClaims ); err != nil {
t . Fatal ( err )
}
if activeClaims != 1 || revokedClaims != 1 {
t . Fatalf ( "claim counts active=%d revoked=%d" , activeClaims , revokedClaims )
}
}
func integrationOperatorPrincipal ( t * testing . T , ctx context . Context , pool * pgxpool . Pool ) identity . Principal {
t . Helper ()
var principal identity . Principal
if err := pool . QueryRow ( ctx , `SELECT oidc_issuer,oidc_subject FROM user_account WHERE public_id='10000000-0000-0000-0000-000000000114'::uuid AND status='active' AND account_kind='operator'` ). Scan ( & principal . Issuer , & principal . Subject ); err != nil {
t . Fatalf ( "resolve integration operator: %v" , err )
}
return principal
}