@@ -10,31 +10,11 @@ import (
"testing"
accountdomain "git.ipao.vip/rogee/creator-hub/internal/account"
"git.ipao.vip/rogee/creator-hub/internal/creator"
hub "git.ipao.vip/rogee/creator-hub/internal/environment"
"github.com/gofiber/fiber/v3"
)
func TestDeriveSeed ( t * testing . T ) {
// 确定性:同 ID 派生同 seed
if deriveSeed ( "account-0123456789abcdef01234567" ) != deriveSeed ( "account-0123456789abcdef01234567" ) {
t . Fatal ( "deriveSeed must be deterministic" )
}
// 值域:1001..2147483647( seed 上限约束,偏移 1000 对齐未来 bigint id + 1000)
for _ , accountID := range [ ] string { "account-a" , "account-000000000000000000000000" , "account-zzzzzzzzzzzzzzzzzzzzzzzz" , "" } {
seed := deriveSeed ( accountID )
if seed < 1001 || seed > 2147483647 {
t . Fatalf ( "seed out of range for %q: %d" , accountID , seed )
}
}
// 不同 ID 派生不同 seed(固定向量,防回归)
if deriveSeed ( "account-a" ) == deriveSeed ( "account-b" ) {
t . Fatal ( "distinct accounts must derive distinct seeds" )
}
if got := deriveSeed ( "account-a" ) ; got != 1816671480 {
t . Fatalf ( "deriveSeed vector drifted: %d" , got )
}
}
func TestAccountEnvironmentAutoBindingAndStart ( t * testing . T ) {
databaseURL := os . Getenv ( "CREATORHUB_POSTGRES_TEST_URL" )
if databaseURL == "" {
@@ -76,6 +56,12 @@ func TestAccountEnvironmentAutoBindingAndStart(t *testing.T) {
t . Fatalf ( "expected 404 environment rebind without gateway, got %d: %s" , response . Code , response . Body . String ( ) )
}
auditDB , err := sql . Open ( "pgx" , databaseURL )
if err != nil {
t . Fatal ( err )
}
t . Cleanup ( func ( ) { _ = auditDB . Close ( ) } )
// 注册唯一网关后:补建成功,alias=账号 ID、出口直连、seed 派生
if _ , err := hubStore . CreateGateway ( ctx , "gw-main" , gatewayServer . URL , gateway . token ) ; err != nil {
t . Fatal ( err )
@@ -92,9 +78,13 @@ func TestAccountEnvironmentAutoBindingAndStart(t *testing.T) {
if err := json . Unmarshal ( response . Body . Bytes ( ) , & bound ) ; err != nil || bound . Alias != accountID || bound . Gateway != "gw-main" || ! bound . Created {
t . Fatalf ( "rebind payload: %s err=%v" , response . Body . String ( ) , err )
}
var accountRowID int64
if err := auditDB . QueryRowContext ( ctx , ` SELECT id FROM social_account WHERE account_id = $1 ` , accountID ) . Scan ( & accountRowID ) ; err != nil {
t . Fatal ( err )
}
environment , err := hubStore . GetEnvironmentContext ( ctx , accountID )
if err != nil || environment . Fingerprint . Seed != deriveSeed ( accountID ) || environment . Exit . ID != "" {
t . Fatalf ( "auto-bound environment: %#v err=%v" , environment , err )
if err != nil || environment . Fingerprint . Seed != accountRowID + 1000 || environment . Exit . ID != "" {
t . Fatalf ( "auto-bound environment: %#v err=%v (account row id %d) " , environment , err , accountRowID )
}
// 幂等:重复补建返回既有环境
response = do ( app , http . MethodPost , "/api/phase-a/accounts/" + accountID + "/environment" , "" )
@@ -114,14 +104,9 @@ func TestAccountEnvironmentAutoBindingAndStart(t *testing.T) {
if err != nil || environment . RuntimeID == "" {
t . Fatalf ( "start must activate the environment runtime: %#v err=%v" , environment , err )
}
auditDB , err := sql . Open ( "pgx" , databaseURL )
if err != nil {
t . Fatal ( err )
}
t . Cleanup ( func ( ) { _ = auditDB . Close ( ) } )
var startAudits int
if err := auditDB . QueryRowContext ( ctx ,
` SELECT count(*) FROM audit_event WHERE account_id = $1 AND action = 'start' AND reason_code IN ('action_requested','environment_started') ` , accountID ) . Scan ( & startAudits ) ; err != nil || startAudits != 2 {
` SELECT count(*) FROM audit_event a JOIN social_account sa ON sa.id = a.account_id WHERE sa. account_id = $1 AND action = 'start' AND reason_code IN ('action_requested','environment_started') ` , accountID ) . Scan ( & startAudits ) ; err != nil || startAudits != 2 {
t . Fatalf ( "start audit pair missing: rows=%d err=%v" , startAudits , err )
}
@@ -138,3 +123,84 @@ func TestAccountEnvironmentAutoBindingAndStart(t *testing.T) {
t . Fatalf ( "start conflict payload: %s err=%v" , response . Body . String ( ) , err )
}
}
func TestAccountEnvironmentDeletionLifecycle ( t * testing . T ) {
databaseURL := os . Getenv ( "CREATORHUB_POSTGRES_TEST_URL" )
if databaseURL == "" {
t . Skip ( "set CREATORHUB_POSTGRES_TEST_URL to run PostgreSQL integration coverage" )
}
ctx := context . Background ( )
databaseURL = isolatedControlPlaneDatabaseURL ( t , databaseURL )
accountStore , err := accountdomain . Open ( ctx , databaseURL )
if err != nil {
t . Fatal ( err )
}
t . Cleanup ( func ( ) { _ = accountStore . Close ( ) } )
hubStore , err := hub . Open ( ctx , databaseURL )
if err != nil {
t . Fatal ( err )
}
t . Cleanup ( func ( ) { _ = hubStore . Close ( ) } )
gateway := & fakeGateway { token : "unit-test-gateway-token" }
gatewayServer := httptest . NewServer ( gateway . handler ( t ) )
t . Cleanup ( gatewayServer . Close )
creatorStore , err := creator . Open ( ctx , databaseURL )
if err != nil {
t . Fatal ( err )
}
t . Cleanup ( func ( ) { _ = creatorStore . Close ( ) } )
app := fiber . New ( )
RegisterAccountRoutes ( app , accountStore , hubStore , & testCredentialBridge { values : map [ string ] string { } } )
RegisterAccountDeletion ( app , accountStore , hubStore , creatorStore , & testCredentialBridge { values : map [ string ] string { } } )
if _ , err := hubStore . CreateGateway ( ctx , "gw-main" , gatewayServer . URL , gateway . token ) ; err != nil {
t . Fatal ( err )
}
// 删除不存在的账号 → 404
if response := do ( app , http . MethodDelete , "/api/phase-a/accounts/account-missing0000000000000" , "" ) ; response . Code != http . StatusNotFound {
t . Fatalf ( "expected 404 deleting missing account, got %d: %s" , response . Code , response . Body . String ( ) )
}
// 创建即绑定 → 审计与浏览器环境随账号生成
response := do ( app , http . MethodPost , "/api/phase-a/accounts" ,
` { "name":"待删账号","platform":"douyin","platform_account_key":"key-delete-1","cookies":"sessionid=1"} ` )
if response . Code != http . StatusCreated {
t . Fatalf ( "expected 201 create account, got %d: %s" , response . Code , response . Body . String ( ) )
}
var created struct {
ID string ` json:"id" `
}
if err := json . Unmarshal ( response . Body . Bytes ( ) , & created ) ; err != nil || created . ID == "" {
t . Fatalf ( "create payload: %s err=%v" , response . Body . String ( ) , err )
}
accountID := created . ID
if _ , err := hubStore . GetEnvironmentContext ( ctx , accountID ) ; err != nil {
t . Fatalf ( "auto-bound environment missing: %v" , err )
}
// 删除账号 → 204;账号、环境与审计全部清除
if response := do ( app , http . MethodDelete , "/api/phase-a/accounts/" + accountID , "" ) ; response . Code != http . StatusNoContent {
t . Fatalf ( "expected 204 delete account, got %d: %s" , response . Code , response . Body . String ( ) )
}
if _ , err := accountStore . GetAccount ( ctx , accountID ) ; err == nil {
t . Fatal ( "account row must be gone after delete" )
}
if _ , err := hubStore . GetEnvironmentContext ( ctx , accountID ) ; err == nil {
t . Fatal ( "environment must be gone after delete" )
}
auditDB , err := sql . Open ( "pgx" , databaseURL )
if err != nil {
t . Fatal ( err )
}
t . Cleanup ( func ( ) { _ = auditDB . Close ( ) } )
var auditCount int
if err := auditDB . QueryRowContext ( ctx , `
SELECT count(*) FROM audit_event audit
JOIN social_account account ON account.id = audit.account_id
WHERE account.account_id = $1 ` , accountID ) . Scan ( & auditCount ) ; err != nil {
t . Fatal ( err )
}
if auditCount != 0 {
t . Fatalf ( "account audit events must be purged with the account: rows=%d" , auditCount )
}
}