Files
story/internal/saas/device_binding_service_integration_test.go
2026-08-29 21:56:51 +08:00

108 lines
5.4 KiB
Go

package saas
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"os"
"testing"
"data-terminal-cloud/internal/audit"
"data-terminal-cloud/internal/identity"
"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgxpool"
)
func TestDeviceBindingImpactTransferAndUnbind(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()
devicePublicID := uuid.NewString()
targetSubjectPublicID := uuid.NewString()
serialSum := sha256.Sum256([]byte(devicePublicID))
serialDigest := hex.EncodeToString(serialSum[:])
var deviceID, targetSubjectID string
if err := pool.QueryRow(ctx, `
INSERT INTO subject_profile (public_id,tenant_id,household_id,preferred_name,status,profile_version)
SELECT $1::uuid,t.id,h.id,'绑定转移测试长者','active',1
FROM tenant t JOIN household h ON h.tenant_id=t.id
WHERE t.public_id=$2::uuid AND h.public_id=$3::uuid
RETURNING id::text`, targetSubjectPublicID, testTenantPublicID, "10000000-0000-0000-0000-000000000211").Scan(&targetSubjectID); err != nil {
t.Fatalf("insert target subject: %v", err)
}
if err := pool.QueryRow(ctx, `
INSERT INTO device (public_id,tenant_id,serial_digest,model,hardware_revision,status,credential_ref,minimum_security_version)
SELECT $1::uuid,id,$2,'binding-test-terminal','test-rev','registered','db://binding-test','test-v1'
FROM tenant WHERE public_id=$3::uuid
RETURNING id::text`, devicePublicID, serialDigest, testTenantPublicID).Scan(&deviceID); err != nil {
t.Fatalf("insert device: %v", err)
}
if _, err := pool.Exec(ctx, `INSERT INTO device_twin (device_id,tenant_id) SELECT id,tenant_id FROM device WHERE id=$1::uuid`, deviceID); err != nil {
t.Fatalf("insert device twin: %v", err)
}
var sourceBindingID string
if err := pool.QueryRow(ctx, `
INSERT INTO device_binding (tenant_id,device_id,household_id,subject_id,bound_by,reason)
SELECT d.tenant_id,d.id,h.id,sp.id,ua.id,'integration-test'
FROM device d
JOIN household h ON h.tenant_id=d.tenant_id AND h.public_id=$2::uuid
JOIN subject_profile sp ON sp.household_id=h.id AND sp.public_id=$3::uuid
JOIN user_account ua ON ua.tenant_id=d.tenant_id AND ua.oidc_issuer=$4 AND ua.oidc_subject=$5
WHERE d.id=$1::uuid
RETURNING public_id::text`, deviceID, "10000000-0000-0000-0000-000000000211", "10000000-0000-0000-0000-000000000411", testIssuer, testOwnerSubject).Scan(&sourceBindingID); err != nil {
t.Fatalf("insert source binding: %v", err)
}
defer func() {
_, _ = pool.Exec(ctx, `DELETE FROM audit_event WHERE request_id LIKE 'device-binding-integration-%'`)
_, _ = pool.Exec(ctx, `DELETE FROM device_binding 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)
_, _ = pool.Exec(ctx, `DELETE FROM subject_profile WHERE id=$1::uuid`, targetSubjectID)
}()
service := NewService(pool, audit.NewRepository(), nil, nil)
owner := identity.Principal{Issuer: testIssuer, Subject: testOwnerSubject}
impact, err := service.GetDeviceBindingImpact(ctx, owner, devicePublicID, DeviceBindingActionTransfer, targetSubjectPublicID)
if err != nil || !impact.Allowed || impact.BindingID != sourceBindingID || impact.TargetSubjectID == nil || *impact.TargetSubjectID != targetSubjectPublicID {
t.Fatalf("unexpected transfer impact: %+v err=%v", impact, err)
}
if _, err := service.ChangeDeviceBinding(ctx, owner, devicePublicID, "device-binding-integration-stale", ChangeDeviceBindingInput{
Action: DeviceBindingActionTransfer, ExpectedBindingID: uuid.NewString(), TargetSubjectID: targetSubjectPublicID, Reason: "stale review", Acknowledged: true,
}); !errors.Is(err, ErrDeviceBindingStale) {
t.Fatalf("stale binding error=%v", err)
}
transferred, err := service.ChangeDeviceBinding(ctx, owner, devicePublicID, "device-binding-integration-transfer", ChangeDeviceBindingInput{
Action: DeviceBindingActionTransfer, ExpectedBindingID: impact.BindingID, TargetSubjectID: targetSubjectPublicID, Reason: "change assigned elder", Acknowledged: true,
})
if err != nil || transferred.BindingID == nil || transferred.SubjectID == nil || *transferred.SubjectID != targetSubjectPublicID {
t.Fatalf("transfer result=%+v err=%v", transferred, err)
}
device, err := service.GetDevice(ctx, owner, devicePublicID)
if err != nil || device.BindingID != *transferred.BindingID || device.SubjectID != targetSubjectPublicID {
t.Fatalf("transferred device=%+v err=%v", device, err)
}
unbindImpact, err := service.GetDeviceBindingImpact(ctx, owner, devicePublicID, DeviceBindingActionUnbind, "")
if err != nil || !unbindImpact.Allowed {
t.Fatalf("unbind impact=%+v err=%v", unbindImpact, err)
}
unbound, err := service.ChangeDeviceBinding(ctx, owner, devicePublicID, "device-binding-integration-unbind", ChangeDeviceBindingInput{
Action: DeviceBindingActionUnbind, ExpectedBindingID: unbindImpact.BindingID, Reason: "device removed from service", Acknowledged: true,
})
if err != nil || unbound.BindingID != nil {
t.Fatalf("unbind result=%+v err=%v", unbound, err)
}
if _, err := service.GetDevice(ctx, owner, devicePublicID); !errors.Is(err, ErrAccessDenied) {
t.Fatalf("unbound device should no longer be visible, error=%v", err)
}
}