- Store.UpdateAccountFingerprint:更新账号绑定环境指纹,seed 保持账号派生值,携带代理直接拒绝(门禁对齐 CreateBoundEnv) - 运行中的环境先停后启以应用新指纹,未运行仅落库下次启动生效;账号资源锁防并发冲突 - accountEnvironmentView 内联 fingerprint 回显,编辑页预填数据源 - 覆盖:store 层 seed 保留/校验拒绝/JSON 往返、路由畸形入参、PG 端到端(保存/回显/持久化/400/404)
120 lines
5.1 KiB
Go
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)
|
|
}
|
|
}
|