feat: expand companion platform, device controls, and memoir workflows
ci / verify (push) Canceled after 0s
ci / verify (push) Canceled after 0s
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.
This commit is contained in:
@@ -1,22 +1,23 @@
|
||||
package opsdevice
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"data-terminal-cloud/internal/audit"
|
||||
"data-terminal-cloud/internal/deviceauth"
|
||||
"data-terminal-cloud/internal/devicebinding"
|
||||
"data-terminal-cloud/internal/identity"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
func TestOperationsDeviceLifecycle(t *testing.T) {
|
||||
func TestOperationsDeviceCreatesPhysicalConfirmIdentity(t *testing.T) {
|
||||
databaseURL := os.Getenv("TEST_DATABASE_URL")
|
||||
if databaseURL == "" {
|
||||
t.Skip("TEST_DATABASE_URL is not set")
|
||||
@@ -31,90 +32,16 @@ func TestOperationsDeviceLifecycle(t *testing.T) {
|
||||
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",
|
||||
VoiceRuntime: "volc_hardware_agent",
|
||||
service, err := NewService(pool, resolver, audit.NewRepository(), Options{
|
||||
OnboardingMasterKey: strings.Repeat("15", 32),
|
||||
DeviceOrigin: "https://platform.example/onboarding/entry",
|
||||
})
|
||||
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)
|
||||
_, _ = 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)
|
||||
_, _ = 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)
|
||||
}()
|
||||
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 != "" {
|
||||
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)
|
||||
if err != nil || retired.Status != "revoked" || retired.ActivationState != "REVOKED" || retired.CredentialStatus != "revoked" || retired.HouseholdID != nil || retired.HardwareAgentStatus != "" {
|
||||
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"})
|
||||
created, err := service.Create(ctx, principal, tenantID, "ops-device-integration-create", CreateInput{Model: "ECHOEAR-S3"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -122,29 +49,66 @@ func TestOperationsDeviceClaimIssueAndRotation(t *testing.T) {
|
||||
defer func() {
|
||||
_, _ = pool.Exec(ctx, `DELETE FROM audit_event WHERE resource_id=$1::uuid`, deviceID)
|
||||
_, _ = 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 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_onboarding_key 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)
|
||||
}()
|
||||
if created.Device.ActivationState != "CLAIMABLE" || created.Claim == nil || len(created.Claim.ClaimCode) != 19 || !created.Claim.ExpiresAt.After(created.Claim.IssuedAt) {
|
||||
t.Fatalf("unexpected initial claim: %+v", created.Claim)
|
||||
|
||||
credential := created.Credential
|
||||
if !regexp.MustCompile(`^KP01-[0-9A-F]{12}$`).MatchString(credential.FactoryID) || credential.FactoryID != created.Device.FactoryID || credential.Model != "ECHOEAR-S3" || credential.Origin != "https://platform.example" || credential.KeyVersion != 1 {
|
||||
t.Fatalf("unexpected burn-in identity: %+v", credential)
|
||||
}
|
||||
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)
|
||||
key, err := hex.DecodeString(credential.DeviceKeyHex)
|
||||
if err != nil || len(key) != 32 {
|
||||
t.Fatalf("invalid device key: len=%d err=%v", len(key), 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)
|
||||
if created.Device.EnrollmentMode != "physical_confirm" || created.Device.ActivationState != "CLAIMABLE" || created.Device.Bound || created.Device.CredentialStatus != "active" || created.Device.KeyVersion != 1 {
|
||||
t.Fatalf("unexpected created device: %+v", created.Device)
|
||||
}
|
||||
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 {
|
||||
|
||||
var released bool
|
||||
var desiredConfig string
|
||||
var activeKeys, legacyCredentials, claims, bindings int
|
||||
if err := pool.QueryRow(ctx, `SELECT onboarding_released_at IS NOT NULL FROM device WHERE public_id=$1::uuid`, deviceID).Scan(&released); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if activeClaims != 1 || revokedClaims != 1 {
|
||||
t.Fatalf("claim counts active=%d revoked=%d", activeClaims, revokedClaims)
|
||||
if err := pool.QueryRow(ctx, `SELECT desired_config::text FROM device_twin WHERE device_id=(SELECT id FROM device WHERE public_id=$1::uuid)`, deviceID).Scan(&desiredConfig); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := pool.QueryRow(ctx, `SELECT
|
||||
(SELECT count(*) FROM device_onboarding_key WHERE device_id=d.id AND status='ACTIVE'),
|
||||
(SELECT count(*) FROM device_credential WHERE device_id=d.id),
|
||||
(SELECT count(*) FROM device_claim WHERE device_id=d.id),
|
||||
(SELECT count(*) FROM device_binding WHERE device_id=d.id AND unbound_at IS NULL)
|
||||
FROM device d WHERE d.public_id=$1::uuid`, deviceID).Scan(&activeKeys, &legacyCredentials, &claims, &bindings); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !released || desiredConfig != "{}" || activeKeys != 1 || legacyCredentials != 0 || claims != 0 || bindings != 0 {
|
||||
t.Fatalf("device was not created as direct BLE inventory: released=%v desired=%s active_keys=%d legacy_credentials=%d claims=%d bindings=%d", released, desiredConfig, activeKeys, legacyCredentials, claims, bindings)
|
||||
}
|
||||
if _, err := service.IssueDeviceClaim(ctx, principal, tenantID, deviceID, "ops-device-integration-legacy-claim"); !errors.Is(err, devicebinding.ErrConfirmationRequired) {
|
||||
t.Fatalf("physical-confirm device accepted a legacy claim: %v", err)
|
||||
}
|
||||
|
||||
rotated, err := service.RotateCredential(ctx, principal, tenantID, deviceID, "ops-device-integration-rotate")
|
||||
if err != nil || rotated.KeyVersion != 2 || rotated.DeviceKeyHex == credential.DeviceKeyHex || rotated.FactoryID != credential.FactoryID {
|
||||
t.Fatalf("unexpected credential rotation: %+v err=%v", rotated, err)
|
||||
}
|
||||
var activeVersion, revokedKeys int
|
||||
if err := pool.QueryRow(ctx, `SELECT max(key_version) FILTER (WHERE status='ACTIVE'),count(*) FILTER (WHERE status='REVOKED') FROM device_onboarding_key WHERE device_id=(SELECT id FROM device WHERE public_id=$1::uuid)`, deviceID).Scan(&activeVersion, &revokedKeys); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if activeVersion != 2 || revokedKeys != 1 {
|
||||
t.Fatalf("unexpected rotated key state: active=%d revoked=%d", activeVersion, revokedKeys)
|
||||
}
|
||||
|
||||
var version int64
|
||||
if err := pool.QueryRow(ctx, `SELECT version FROM device WHERE public_id=$1::uuid`, deviceID).Scan(&version); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
retired, err := service.Delete(ctx, principal, tenantID, deviceID, "ops-device-integration-delete", version)
|
||||
if err != nil || retired.Status != "revoked" || retired.ActivationState != "REVOKED" || retired.CredentialStatus != "missing" {
|
||||
t.Fatalf("unexpected delete result: %+v err=%v", retired, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user