douyin-release-gate / verify (push) Failing after 4m3s
- 前端创建页折叠区块:平台/品牌/版本/语言/时区/硬件并发/关闭伪装维度,留空使用默认 - 后端 accountRequest 增加 fingerprint,校验前置 400(避免账号已建、绑定失败的中间态) - seed 服务端从账号派生、proxy 由网络出口管理,两者不接受表单值 - 补建端点支持携带指纹重试;start 自愈补建保持零值指纹 - PG 集成测试:指纹落库/非法值拒绝不建号/补建指纹重试
124 lines
5.7 KiB
Go
124 lines
5.7 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"testing"
|
|
|
|
accountdomain "git.ipao.vip/rogee/creator-hub/internal/account"
|
|
hub "git.ipao.vip/rogee/creator-hub/internal/environment"
|
|
"github.com/gofiber/fiber/v3"
|
|
)
|
|
|
|
// 创建社媒账号的指纹浏览器环境表单:自定义指纹随创建请求落库;
|
|
// seed 由服务端从账号派生、proxy 由出口体系管理(客户端值被忽略/清空)。
|
|
func TestAccountCreateAcceptsFingerprintForm(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)
|
|
app := fiber.New()
|
|
RegisterAccountRoutes(app, accountStore, hubStore, &testCredentialBridge{values: map[string]string{}})
|
|
if _, err := hubStore.CreateGateway(ctx, "gw-main", gatewayServer.URL, gateway.token); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
response := do(app, http.MethodPost, "/api/phase-a/accounts",
|
|
`{"name":"指纹账号","platform":"douyin","platform_account_key":"key-fp-1","cookies":"sessionid=1",
|
|
"fingerprint":{"seed":42,"platform":"windows","platform_version":"10.0.0","brand":"Chrome","brand_version":"132.0.6834.159",
|
|
"hardware_concurrency":8,"lang":"zh-CN","accept_lang":"zh-CN,en-US","timezone":"Asia/Shanghai",
|
|
"proxy_server":"socks5://proxy.example:1080","disable_spoofing":"canvas,gpu"}}`)
|
|
if response.Code != http.StatusCreated {
|
|
t.Fatalf("expected 201 create account with fingerprint, 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)
|
|
}
|
|
auditDB, err := sql.Open("pgx", databaseURL)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { _ = auditDB.Close() })
|
|
var accountRowID int64
|
|
if err := auditDB.QueryRowContext(ctx, `SELECT id FROM social_account WHERE account_id = $1`, created.ID).Scan(&accountRowID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
environment, err := hubStore.GetEnvironmentContext(ctx, created.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fingerprint := environment.Fingerprint
|
|
if fingerprint.Platform != "windows" || fingerprint.PlatformVersion != "10.0.0" ||
|
|
fingerprint.Brand != "Chrome" || fingerprint.BrandVersion != "132.0.6834.159" ||
|
|
fingerprint.HardwareConcurrency != 8 || fingerprint.Lang != "zh-CN" ||
|
|
fingerprint.AcceptLang != "zh-CN,en-US" || fingerprint.Timezone != "Asia/Shanghai" ||
|
|
fingerprint.DisableSpoofing != "canvas,gpu" {
|
|
t.Fatalf("fingerprint not persisted as submitted: %#v", fingerprint)
|
|
}
|
|
if fingerprint.Seed != accountRowID+1000 || fingerprint.ProxyServer != "" {
|
|
t.Fatalf("seed must be server-derived and proxy cleared: %#v (row id %d)", fingerprint, accountRowID)
|
|
}
|
|
|
|
// 非法指纹值 → 400,账号不落库(校验前置,无创建后绑定失败的中间态)。
|
|
if response := do(app, http.MethodPost, "/api/phase-a/accounts",
|
|
`{"name":"坏指纹","platform":"douyin","platform_account_key":"key-fp-2","fingerprint":{"platform":"android"}}`); response.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400 for invalid fingerprint, got %d: %s", response.Code, response.Body.String())
|
|
}
|
|
var invalidCount int
|
|
if err := auditDB.QueryRowContext(ctx, `SELECT count(*) FROM social_account WHERE platform_account_key = 'key-fp-2'`).Scan(&invalidCount); err != nil || invalidCount != 0 {
|
|
t.Fatalf("invalid fingerprint must not create account: rows=%d err=%v", invalidCount, err)
|
|
}
|
|
|
|
// 幂等补建端点可携带同一指纹表单重试(创建时绑定失败的场景)。
|
|
retry := do(app, http.MethodPost, "/api/phase-a/accounts",
|
|
`{"name":"补建账号","platform":"douyin","platform_account_key":"key-fp-3","fingerprint":{"timezone":"Asia/Shanghai"}}`)
|
|
if retry.Code != http.StatusCreated {
|
|
t.Fatalf("expected 201 create account for rebind retry, got %d: %s", retry.Code, retry.Body.String())
|
|
}
|
|
var rebindCreated struct {
|
|
ID string `json:"id"`
|
|
}
|
|
if err := json.Unmarshal(retry.Body.Bytes(), &rebindCreated); err != nil || rebindCreated.ID == "" {
|
|
t.Fatalf("rebind create payload: %s err=%v", retry.Body.String(), err)
|
|
}
|
|
// 幂等补建端点可携带创建时未落库的指纹重试:先删除环境模拟"创建时绑定失败",补建后指纹落库。
|
|
if err := hubStore.DeleteAccountEnvironment(ctx, rebindCreated.ID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rebind := do(app, http.MethodPost, "/api/phase-a/accounts/"+rebindCreated.ID+"/environment",
|
|
`{"fingerprint":{"platform":"linux","lang":"en-US"}}`)
|
|
if rebind.Code != http.StatusOK {
|
|
t.Fatalf("expected 200 environment rebind with fingerprint, got %d: %s", rebind.Code, rebind.Body.String())
|
|
}
|
|
reboundEnvironment, err := hubStore.GetEnvironmentContext(ctx, rebindCreated.ID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// 补建以传入指纹为准(时区为空 = 创建时的时区不保留)。
|
|
if reboundEnvironment.Fingerprint.Platform != "linux" || reboundEnvironment.Fingerprint.Lang != "en-US" || reboundEnvironment.Fingerprint.Timezone != "" {
|
|
t.Fatalf("rebind fingerprint mismatch: %#v", reboundEnvironment.Fingerprint)
|
|
}
|
|
}
|