Files
creator-hub/internal/account/store_test.go
T

232 lines
9.1 KiB
Go

package account
import (
"context"
"database/sql"
"errors"
"io"
"os"
"path/filepath"
"testing"
"github.com/jackc/pgx/v5"
)
type testCredentialBridge struct {
values map[string]string
storeErr error
}
func (bridge *testCredentialBridge) Store(ctx context.Context, _ CredentialReference, key, value string) error {
if err := ctx.Err(); err != nil {
return err
}
bridge.values[key] = value
return bridge.storeErr
}
func TestCreateAccountCompensatesPartialCredentialStore(t *testing.T) {
credentials := &testCredentialBridge{values: map[string]string{}, storeErr: errors.New("provider failed after write")}
account := Account{
ID: "account-partial-store", Name: "Partial Store", Platform: "douyin", PlatformAccountKey: "partial-store",
Tags: []string{}, Cookies: "sessionid=value",
CredentialReference: CredentialReference{ID: "account-partial-store-cookies", Provider: "os_keyring"},
CredentialKey: "creatorhub/account-partial-store/cookies",
}
if err := (&Store{}).CreateAccount(context.Background(), account, credentials); err == nil {
t.Fatal("partial credential store returned success")
}
if _, ok := credentials.values[account.CredentialKey]; ok {
t.Fatal("partial credential store left an orphan")
}
}
func (bridge *testCredentialBridge) Delete(ctx context.Context, _ CredentialReference, key string) error {
if err := ctx.Err(); err != nil {
return err
}
delete(bridge.values, key)
return nil
}
func TestNewAccountIDIsStoreValidAndUnique(t *testing.T) {
first, second := NewAccountID(), NewAccountID()
if first == second || !idPattern.MatchString(first) || !idPattern.MatchString(second) {
t.Fatalf("invalid generated account ids: %q %q", first, second)
}
}
func TestValidationRejectsInvalidInputsBeforePersistence(t *testing.T) {
store := &Store{}
valid := Account{
ID: "account-a", Name: "账号 A", Platform: "douyin", PlatformAccountKey: "platform-a",
Tags: []string{"主账号"}, Cookies: "sessionid=value; token=second",
CredentialReference: CredentialReference{ID: "account-a-cookies", Provider: "os_keyring"},
CredentialKey: "creatorhub/account-a/cookies",
}
credentials := &testCredentialBridge{values: map[string]string{}}
for _, platform := range []string{"douyin"} {
account := valid
account.Platform = platform
if !validAccount(account) {
t.Fatalf("supported platform rejected: %s", platform)
}
}
empty := valid
empty.Cookies = ""
if !validAccount(empty) {
t.Fatal("empty cookies must stay valid (scan-to-login account)")
}
for name, mutate := range map[string]func(*Account){
"id": func(account *Account) { account.ID = "INVALID" },
"name": func(account *Account) { account.Name = " " },
"platform": func(account *Account) { account.Platform = "mock" },
"platform account key": func(account *Account) { account.PlatformAccountKey = "secret value" },
"tag": func(account *Account) { account.Tags = []string{""} },
"cookie header": func(account *Account) { account.Cookies = "sessionid" },
} {
t.Run(name, func(t *testing.T) {
account := valid
mutate(&account)
if err := store.CreateAccount(context.Background(), account, credentials); !errors.Is(err, ErrInvalid) {
t.Fatalf("expected invalid account, got %v", err)
}
})
}
}
func applyHubMigrationsForPhaseATest(t *testing.T, store *Store) {
t.Helper()
for _, migrationFile := range []struct {
version int
name string
}{{2, "002_hub.sql"}, {3, "003_unified_accounts.sql"}, {4, "004_environment_actions.sql"}, {5, "005_sanitize_legacy_proxy.sql"},
{6, "006_runtime_cleanup.sql"}, {7, "007_runtime_binding_version.sql"}, {8, "008_runtime_cleanup_generation.sql"},
{9, "009_runtime_cleanup_compatibility.sql"}, {10, "010_runtime_network_generation.sql"}, {11, "011_task_recovery.sql"},
{12, "012_task_recovery_compatibility.sql"}, {13, "013_account_creation.sql"}, {14, "014_account_creation_compatibility.sql"},
{15, "015_gateway_rename_cascade.sql"}, {16, "016_network_exit_plain_credentials.sql"}, {17, "017_native_browser_versions.sql"},
{33, "033_unique_fingerprint_seed.sql"}, {34, "034_gateway_browser_default.sql"}} {
var applied bool
if err := store.db.QueryRow(`SELECT EXISTS (SELECT 1 FROM schema_migration WHERE version = $1)`, migrationFile.version).Scan(&applied); err != nil {
t.Fatal(err)
}
if applied {
continue
}
migration, err := os.ReadFile(filepath.Join("..", "environment", "migrations", migrationFile.name))
if err != nil {
t.Fatal(err)
}
if _, err := store.db.Exec(string(migration)); err != nil {
t.Fatalf("apply test migration %d: %v", migrationFile.version, err)
}
if _, err := store.db.Exec(`INSERT INTO schema_migration (version) VALUES ($1)`, migrationFile.version); err != nil {
t.Fatal(err)
}
}
}
func TestCreateAccountWithoutCookiesSkipsCredentialStore(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()
store, err := Open(ctx, databaseURL)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = store.Close() })
applyHubMigrationsForPhaseATest(t, store)
if _, err := store.db.ExecContext(ctx, `
TRUNCATE audit_event,
runtime_instance, environment_binding, network_exit, social_account, credential_reference,
browser_env, gateway RESTART IDENTITY CASCADE`); err != nil {
t.Fatal(err)
}
credentials := &testCredentialBridge{values: map[string]string{}}
account := Account{ID: "account-no-cookies", Name: "扫码账号", Platform: "douyin", PlatformAccountKey: "qr-login",
Tags: []string{}, Cookies: "",
CredentialReference: CredentialReference{ID: "account-no-cookies-cookies", Provider: "os_keyring"}, CredentialKey: "creatorhub/account-no-cookies/cookies"}
if err := store.CreateAccount(ctx, account, credentials); err != nil {
t.Fatalf("creating an account without cookies failed: %v", err)
}
if _, stored := credentials.values[account.CredentialKey]; stored {
t.Fatal("empty cookies must not be written to the credential provider")
}
assertCount(t, store, `SELECT count(*) FROM social_account WHERE id = $1`, 1, account.ID)
assertCount(t, store, `SELECT count(*) FROM credential_reference WHERE id = $1`, 1, account.CredentialReference.ID)
}
func TestAccountCredentialCommitResult(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()
store, err := Open(ctx, databaseURL)
if err != nil {
t.Fatal(err)
}
applyHubMigrationsForPhaseATest(t, store)
if _, err := store.db.ExecContext(ctx, `
TRUNCATE audit_event,
runtime_instance, environment_binding, network_exit, social_account, credential_reference,
browser_env, gateway RESTART IDENTITY CASCADE`); err != nil {
t.Fatal(err)
}
credentials := &testCredentialBridge{values: map[string]string{}}
account := func(id, platformKey string) Account {
return Account{ID: id, Name: id, Platform: "douyin", PlatformAccountKey: platformKey, Tags: []string{}, Cookies: "sessionid=" + id,
CredentialReference: CredentialReference{ID: id + "-cookies", Provider: "os_keyring"}, CredentialKey: "creatorhub/" + id + "/cookies"}
}
store.accountCommit = func(tx *sql.Tx) error {
if err := tx.Commit(); err != nil {
return err
}
return io.ErrUnexpectedEOF
}
committed := account("account-committed", "platform-committed")
if err := store.CreateAccount(ctx, committed, credentials); !errors.Is(err, ErrAccountCreationUnknown) {
t.Fatalf("ambiguous commit did not return unknown: %v", err)
}
if credentials.values[committed.CredentialKey] == "" {
t.Fatal("committed unknown result deleted its credential")
}
assertCount(t, store, `SELECT count(*) FROM social_account WHERE id = $1`, 1, committed.ID)
store.accountCommit = func(tx *sql.Tx) error {
_ = tx.Rollback()
return pgx.ErrTxCommitRollback
}
rolledBack := account("account-rolled-back", "platform-rolled-back")
if err := store.CreateAccount(ctx, rolledBack, credentials); err == nil || errors.Is(err, ErrAccountCreationUnknown) {
t.Fatalf("known rollback did not return a known failure: %v", err)
}
if _, ok := credentials.values[rolledBack.CredentialKey]; ok {
t.Fatal("known rollback retained its credential")
}
store.accountCommit = func(tx *sql.Tx) error {
_ = tx.Rollback()
return io.ErrUnexpectedEOF
}
ambiguousRollback := account("account-ambiguous", "platform-ambiguous")
if err := store.CreateAccount(ctx, ambiguousRollback, credentials); !errors.Is(err, ErrAccountCreationUnknown) {
t.Fatalf("transport error did not preserve an unknown result: %v", err)
}
if credentials.values[ambiguousRollback.CredentialKey] == "" {
t.Fatal("commit-unknown deleted the credential")
}
}
func assertCount(t *testing.T, store *Store, query string, expected int, args ...any) {
t.Helper()
var actual int
if err := store.db.QueryRowContext(context.Background(), query, args...).Scan(&actual); err != nil || actual != expected {
t.Fatalf("count mismatch: expected=%d actual=%d err=%v query=%s", expected, actual, err, query)
}
}