108 lines
5.4 KiB
Go
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)
|
|
}
|
|
}
|