Files
creator-hub/internal/environment/fingerprint_update_test.go
T
rogee 2c4fe307f7 feat(api): 账号环境指纹更新端点——PUT /creator/accounts/:id/fingerprint 单独保存并按需重启
- Store.UpdateAccountFingerprint:更新账号绑定环境指纹,seed 保持账号派生值,携带代理直接拒绝(门禁对齐 CreateBoundEnv)
- 运行中的环境先停后启以应用新指纹,未运行仅落库下次启动生效;账号资源锁防并发冲突
- accountEnvironmentView 内联 fingerprint 回显,编辑页预填数据源
- 覆盖:store 层 seed 保留/校验拒绝/JSON 往返、路由畸形入参、PG 端到端(保存/回显/持久化/400/404)
2026-09-30 17:12:56 +08:00

120 lines
5.1 KiB
Go

package environment
import (
"context"
"encoding/json"
"errors"
"os"
"strings"
"testing"
)
// seedFingerprintAccount 造一条可直接建绑定的账号 + 网关,返回 store。
func seedFingerprintAccount(t *testing.T, ctx context.Context, accountID string) *Store {
t.Helper()
databaseURL := os.Getenv("CREATORHUB_POSTGRES_TEST_URL")
if databaseURL == "" {
t.Skip("set CREATORHUB_POSTGRES_TEST_URL to run PostgreSQL integration coverage")
}
databaseURL = isolatedDatabaseURL(t, databaseURL)
store := openFullyMigratedHub(t, ctx, databaseURL)
t.Cleanup(func() { _ = store.Close() })
if _, err := store.CreateGateway(ctx, "gw-fp", "http://127.0.0.1:8081", ""); err != nil {
t.Fatal(err)
}
if _, err := store.db.ExecContext(ctx, `
INSERT INTO social_account (account_id, credential_provider, credential_key, platform, platform_account_key, status)
VALUES ($1, 'os_keyring', $2, 'mock', $1, 'paused')`, accountID, "creatorhub/"+accountID); err != nil {
t.Fatal(err)
}
return store
}
func TestUpdateAccountFingerprintPreservesDerivedSeed(t *testing.T) {
ctx := context.Background()
store := seedFingerprintAccount(t, ctx, "fp-owner")
if _, created, err := store.CreateBoundEnv(ctx, Env{Alias: "fp-owner", Name: "fp-owner", Gateway: "gw-fp", Fingerprint: Fingerprint{Timezone: "Asia/Shanghai"}}, "fp-owner", ""); err != nil || !created {
t.Fatalf("create bound env: created=%v err=%v", created, err)
}
updated, err := store.UpdateAccountFingerprint(ctx, "fp-owner", Fingerprint{
Platform: "linux", Timezone: "Asia/Tokyo", Lang: "ja-JP",
})
if err != nil {
t.Fatalf("update account fingerprint: %v", err)
}
if updated.Fingerprint.Platform != "linux" || updated.Fingerprint.Timezone != "Asia/Tokyo" || updated.Fingerprint.Lang != "ja-JP" {
t.Fatalf("fingerprint fields not updated: %+v", updated.Fingerprint)
}
if updated.Fingerprint.Seed < 1 {
t.Fatalf("derived seed must be preserved, got %d", updated.Fingerprint.Seed)
}
// 直连环境不可携带代理:更新入口必须清洗代理字段。
if updated.Fingerprint.ProxyServer != "" || updated.Fingerprint.DisableNonProxiedUDP {
t.Fatalf("proxy fields must stay empty: %+v", updated.Fingerprint)
}
// 更新对后续读取可见(落库持久化)。
reloaded, err := store.GetEnvironmentContextForAccount(ctx, "fp-owner")
if err != nil || reloaded.Alias != "fp-owner" {
t.Fatalf("reload environment: %v", err)
}
if reloaded.Fingerprint.Timezone != "Asia/Tokyo" || reloaded.Fingerprint.Platform != "linux" {
t.Fatalf("update not persisted: %+v", reloaded.Fingerprint)
}
}
func TestUpdateAccountFingerprintRejectsInvalidInput(t *testing.T) {
ctx := context.Background()
store := seedFingerprintAccount(t, ctx, "fp-owner")
if _, _, err := store.CreateBoundEnv(ctx, Env{Alias: "fp-owner", Name: "fp-owner", Gateway: "gw-fp"}, "fp-owner", ""); err != nil {
t.Fatal(err)
}
if _, err := store.UpdateAccountFingerprint(ctx, "fp-owner", Fingerprint{Platform: "android"}); !errors.Is(err, ErrInvalid) {
t.Fatalf("expected ErrInvalid for bad platform, got %v", err)
}
if _, err := store.UpdateAccountFingerprint(ctx, "fp-owner", Fingerprint{ProxyServer: "socks5://proxy.example:1080", Timezone: "Asia/Shanghai"}); !errors.Is(err, ErrInvalid) {
t.Fatalf("stored fingerprint proxy must be rejected, got %v", err)
}
if _, err := store.UpdateAccountFingerprint(ctx, "fp-owner", Fingerprint{HardwareConcurrency: -1}); !errors.Is(err, ErrInvalid) {
t.Fatalf("expected ErrInvalid for bad concurrency, got %v", err)
}
if _, err := store.UpdateAccountFingerprint(ctx, "missing-owner", Fingerprint{Timezone: "Asia/Shanghai"}); !errors.Is(err, ErrNotFound) {
t.Fatalf("expected ErrNotFound for unbound account, got %v", err)
}
// 失败更新不得污染落库值。
current, err := store.GetEnvironmentContextForAccount(ctx, "fp-owner")
if err != nil {
t.Fatal(err)
}
if current.Fingerprint.Platform != "" || current.Fingerprint.Timezone != "" {
t.Fatalf("failed update must not persist: %+v", current.Fingerprint)
}
}
func TestUpdateAccountFingerprintJSONRoundTrip(t *testing.T) {
ctx := context.Background()
store := seedFingerprintAccount(t, ctx, "fp-owner")
if _, _, err := store.CreateBoundEnv(ctx, Env{Alias: "fp-owner", Name: "fp-owner", Gateway: "gw-fp"}, "fp-owner", ""); err != nil {
t.Fatal(err)
}
if _, err := store.UpdateAccountFingerprint(ctx, "fp-owner", Fingerprint{
Brand: "Edge", AcceptLang: "zh-CN,en-US", DisableSpoofing: "font,gpu", HardwareConcurrency: 8,
}); err != nil {
t.Fatal(err)
}
updated, err := store.GetEnvironmentContextForAccount(ctx, "fp-owner")
if err != nil {
t.Fatal(err)
}
if updated.Fingerprint.Brand != "Edge" || updated.Fingerprint.AcceptLang != "zh-CN,en-US" ||
updated.Fingerprint.DisableSpoofing != "font,gpu" || updated.Fingerprint.HardwareConcurrency != 8 {
t.Fatalf("complex fingerprint not persisted: %+v", updated.Fingerprint)
}
// 落库 JSON 必须可再次反序列化(browser_env.fingerprint jsonb 契约)。
encoded, err := json.Marshal(updated.Fingerprint)
if err != nil || !strings.Contains(string(encoded), `"brand":"Edge"`) {
t.Fatalf("fingerprint json roundtrip: %s err=%v", encoded, err)
}
}