package creator import ( "errors" "fmt" "os" "strings" "sync" "testing" "git.ipao.vip/rogee/creator-hub/internal/account" ) func TestCreatorFirstLoginBindsUIDAndNeverReplacesIt(t *testing.T) { store, accounts, ctx := openCreatorIntegrationStore(t) create := func(id string) { t.Helper() err := accounts.CreateAccount(ctx, account.Account{ID: id, Name: id, Platform: "douyin", CredentialReference: account.CredentialReference{ID: id, Provider: "secret_manager"}, CredentialKey: "creatorhub/" + id}, integrationCredentialBridge{}) if err != nil { t.Fatal(err) } if err := store.EnsureAccountProfile(ctx, id); err != nil { t.Fatal(err) } } create("uid-first") create("uid-second") before, err := store.GetAccountProfile(ctx, "uid-first") if err != nil || before.PlatformAccountKey != "" { t.Fatalf("new account must be unbound: %#v err=%v", before, err) } for i := 0; i < 2; i++ { result, err := store.RecordVerifiedLoginResult(ctx, "uid-first", "99491952055") if err != nil || result.Status != "logged_in" || result.ActualKey != "99491952055" { t.Fatalf("verify %d: %#v err=%v", i, result, err) } } bound, err := accounts.GetAccount(ctx, "uid-first") if err != nil || bound.PlatformAccountKey != "99491952055" { t.Fatalf("verified UID was not persisted: %#v err=%v", bound, err) } if _, err := store.RecordVerifiedLoginResult(ctx, "uid-first", "123456789"); !errors.Is(err, ErrConflict) || !strings.Contains(err.Error(), "99491952055") || !strings.Contains(err.Error(), "123456789") { t.Fatalf("mismatch must explain both UIDs: %v", err) } if _, err := store.RecordVerifiedLoginResult(ctx, "uid-second", "99491952055"); !errors.Is(err, ErrConflict) || !strings.Contains(err.Error(), "已绑定") { t.Fatalf("duplicate binding should fail visibly: %v", err) } second, err := store.GetAccountProfile(ctx, "uid-second") if err != nil || second.PlatformAccountKey != "" || second.LoginStatus == "logged_in" { t.Fatalf("failed bind changed account: %#v err=%v", second, err) } } func TestCreatorConcurrentFirstLoginBindsExactlyOneUID(t *testing.T) { store, accounts, ctx := openCreatorIntegrationStore(t) id := "uid-concurrent" if err := accounts.CreateAccount(ctx, account.Account{ID: id, Name: id, Platform: "douyin", CredentialReference: account.CredentialReference{ID: id, Provider: "secret_manager"}, CredentialKey: "creatorhub/" + id}, integrationCredentialBridge{}); err != nil { t.Fatal(err) } if err := store.EnsureAccountProfile(ctx, id); err != nil { t.Fatal(err) } var wg sync.WaitGroup outcomes := make(chan error, 2) for _, uid := range []string{"111111111", "222222222"} { wg.Add(1) go func(uid string) { defer wg.Done() _, err := store.RecordVerifiedLoginResult(ctx, id, uid) outcomes <- err }(uid) } wg.Wait() close(outcomes) succeeded, conflicts := 0, 0 for err := range outcomes { if err == nil { succeeded++ } else if errors.Is(err, ErrConflict) { conflicts++ } else { t.Fatal(err) } } if succeeded != 1 || conflicts != 1 { t.Fatalf("competing logins changed UID more than once: success=%d conflict=%d", succeeded, conflicts) } } func TestUIDMigrationClearsUnverifiedKeysAndPreservesVerifiedUID(t *testing.T) { store, accounts, ctx := openCreatorIntegrationStore(t) for i, status := range []string{"logged_in", "needs_login", "manual_required"} { id := fmt.Sprintf("uid-migration-%d", i) if err := accounts.CreateAccount(ctx, account.Account{ID: id, Name: id, Platform: "douyin", PlatformAccountKey: fmt.Sprintf("1000000%d", i), CredentialReference: account.CredentialReference{ID: id, Provider: "secret_manager"}, CredentialKey: "creatorhub/" + id}, integrationCredentialBridge{}); err != nil { t.Fatal(err) } if err := store.EnsureAccountProfile(ctx, id); err != nil { t.Fatal(err) } if _, err := store.db.ExecContext(ctx, `UPDATE creator_account_profile SET login_status=$2 WHERE account_id=(SELECT id FROM social_account WHERE account_id=$1)`, id, status); err != nil { t.Fatal(err) } } migration, err := os.ReadFile("../environment/migrations/1047_account_uid_first_login.sql") if err != nil { t.Fatal(err) } if _, err := store.db.ExecContext(ctx, string(migration)); err != nil { t.Fatal(err) } for i := 0; i < 3; i++ { profile, err := store.GetAccountProfile(ctx, fmt.Sprintf("uid-migration-%d", i)) want := "" if i == 0 { want = "10000000" } if err != nil || profile.PlatformAccountKey != want { t.Fatalf("migration %d: %#v err=%v", i, profile, err) } } }