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) } }