refactor(accounts): 移除「授权状态」概念——账号收敛为启用/暂停单一状态机
douyin-release-gate / verify (push) Failing after 3m43s
douyin-release-gate / verify (push) Failing after 3m43s
产品已收敛为自有账号管理,撤销授权在 UI 无入口、状态恒为 authorized,属废弃语义: - migration 1045:social_account DROP authorization_status/revoked_at/authorization_kind - 删除 revoke API 路由与 RevokeAccount/disableAccount 状态机分支(PauseAccount 独立) - accountRunnable/就绪判定/采集过滤/登录校验删除 authorization_status 检查 - EnvironmentContext/AccountProfile 契约删字段;前端删「已授权/已撤销」展示与 readiness 分支 - 测试同步:revoke 流程/409 用例删除,seed 语句去列;dev 库测试遗留 revoked 账号待 UI 删除
This commit is contained in:
+15
-38
@@ -45,7 +45,6 @@ type Account struct {
|
||||
Cookies string `json:"-"`
|
||||
CredentialReference CredentialReference `json:"-"`
|
||||
CredentialKey string `json:"-"`
|
||||
AuthorizationStatus string `json:"authorization_status"`
|
||||
RuntimeStatus string `json:"runtime_status"`
|
||||
Version int64 `json:"version"`
|
||||
}
|
||||
@@ -154,8 +153,8 @@ func (s *Store) CreateAccount(ctx context.Context, account Account, credentials
|
||||
defer tx.Rollback()
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO social_account
|
||||
(account_id, credential_provider, credential_key, name, platform, platform_account_key, tags, authorization_kind, authorization_status, status)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, 'owned', 'authorized', 'paused')`, account.ID,
|
||||
(account_id, credential_provider, credential_key, name, platform, platform_account_key, tags, status)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, 'paused')`, account.ID,
|
||||
account.CredentialReference.Provider, account.CredentialKey,
|
||||
account.Name, account.Platform, account.PlatformAccountKey, account.Tags); err != nil {
|
||||
return publicDatabaseError(err)
|
||||
@@ -189,7 +188,7 @@ func commitKnownRolledBack(err error) bool {
|
||||
func (s *Store) ListAccounts(ctx context.Context) ([]Account, error) {
|
||||
rows, err := s.db.QueryContext(ctx, `
|
||||
SELECT account.account_id, account.name, account.platform, account.platform_account_key, account.tags,
|
||||
account.authorization_status, account.status, account.version
|
||||
account.status, account.version
|
||||
FROM social_account account
|
||||
ORDER BY account.created_at, account.account_id`)
|
||||
if err != nil {
|
||||
@@ -213,7 +212,7 @@ func (s *Store) GetAccount(ctx context.Context, id string) (Account, error) {
|
||||
}
|
||||
return scanAccount(s.db.QueryRowContext(ctx, `
|
||||
SELECT account.account_id, account.name, account.platform, account.platform_account_key, account.tags,
|
||||
account.authorization_status, account.status, account.version
|
||||
account.status, account.version
|
||||
FROM social_account account
|
||||
WHERE account.account_id = $1`, id))
|
||||
}
|
||||
@@ -246,7 +245,7 @@ func scanAccount(row accountScanner) (Account, error) {
|
||||
var account Account
|
||||
var tags pgtype.FlatArray[string]
|
||||
if err := row.Scan(&account.ID, &account.Name, &account.Platform, &account.PlatformAccountKey, pgtype.NewMap().SQLScanner(&tags),
|
||||
&account.AuthorizationStatus, &account.RuntimeStatus, &account.Version); err != nil {
|
||||
&account.RuntimeStatus, &account.Version); err != nil {
|
||||
return Account{}, rowError(err)
|
||||
}
|
||||
account.Tags = []string(tags)
|
||||
@@ -282,14 +281,6 @@ func validAccount(account Account) bool {
|
||||
}
|
||||
|
||||
func (s *Store) PauseAccount(ctx context.Context, accountID string) error {
|
||||
return s.disableAccount(ctx, accountID, false)
|
||||
}
|
||||
|
||||
func (s *Store) RevokeAccount(ctx context.Context, accountID string) error {
|
||||
return s.disableAccount(ctx, accountID, true)
|
||||
}
|
||||
|
||||
func (s *Store) disableAccount(ctx context.Context, accountID string, revoke bool) error {
|
||||
if !idPattern.MatchString(accountID) {
|
||||
return ErrInvalid
|
||||
}
|
||||
@@ -299,32 +290,21 @@ func (s *Store) disableAccount(ctx context.Context, accountID string, revoke boo
|
||||
}
|
||||
defer tx.Rollback()
|
||||
var version int64
|
||||
var authorizationStatus, runtimeStatus string
|
||||
if err := tx.QueryRowContext(ctx, `SELECT version, authorization_status, status FROM social_account WHERE account_id = $1 FOR UPDATE`, accountID).
|
||||
Scan(&version, &authorizationStatus, &runtimeStatus); err != nil {
|
||||
var runtimeStatus string
|
||||
if err := tx.QueryRowContext(ctx, `SELECT version, status FROM social_account WHERE account_id = $1 FOR UPDATE`, accountID).
|
||||
Scan(&version, &runtimeStatus); err != nil {
|
||||
return rowError(err)
|
||||
}
|
||||
unchanged := (revoke && authorizationStatus == "revoked") || (!revoke && runtimeStatus == "paused")
|
||||
unchanged := runtimeStatus == "paused"
|
||||
if !unchanged {
|
||||
if err := tx.QueryRowContext(ctx, `
|
||||
UPDATE social_account
|
||||
SET authorization_status = CASE WHEN $2 THEN 'revoked' ELSE authorization_status END,
|
||||
status = 'paused', paused_at = now(), revoked_at = CASE WHEN $2 THEN now() ELSE revoked_at END,
|
||||
SET status = 'paused', paused_at = now(),
|
||||
version = version + 1, updated_at = now()
|
||||
WHERE account_id = $1 RETURNING version`, accountID, revoke).Scan(&version); err != nil {
|
||||
WHERE account_id = $1 RETURNING version`, accountID).Scan(&version); err != nil {
|
||||
return errors.New("change account state")
|
||||
}
|
||||
}
|
||||
reason := "account_paused"
|
||||
if revoke {
|
||||
reason = "account_revoked"
|
||||
}
|
||||
if !unchanged {
|
||||
eventType := "account_paused"
|
||||
if revoke {
|
||||
eventType = "account_revoked"
|
||||
}
|
||||
if err := appendAudit(ctx, tx, eventType, reason, accountID, map[string]any{
|
||||
if err := appendAudit(ctx, tx, "account_paused", "account_paused", accountID, map[string]any{
|
||||
"account_version": version,
|
||||
}); err != nil {
|
||||
return err
|
||||
@@ -342,14 +322,11 @@ func (s *Store) ResumeAccount(ctx context.Context, accountID string) error {
|
||||
return errors.New("begin resume transaction")
|
||||
}
|
||||
defer tx.Rollback()
|
||||
var authorizationStatus, runtimeStatus string
|
||||
if err := tx.QueryRowContext(ctx, `SELECT authorization_status, status FROM social_account WHERE account_id = $1 FOR UPDATE`, accountID).
|
||||
Scan(&authorizationStatus, &runtimeStatus); err != nil {
|
||||
var runtimeStatus string
|
||||
if err := tx.QueryRowContext(ctx, `SELECT status FROM social_account WHERE account_id = $1 FOR UPDATE`, accountID).
|
||||
Scan(&runtimeStatus); err != nil {
|
||||
return rowError(err)
|
||||
}
|
||||
if authorizationStatus == "revoked" {
|
||||
return ErrConflict
|
||||
}
|
||||
var ready bool
|
||||
if err := tx.QueryRowContext(ctx, `
|
||||
SELECT EXISTS (
|
||||
|
||||
@@ -263,7 +263,7 @@ func TestAccountStoreLifecycleAgainstPostgres(t *testing.T) {
|
||||
t.Fatalf("list accounts: %d err=%v", len(accounts), err)
|
||||
}
|
||||
account, err := store.GetAccount(ctx, "account-lifecycle-a")
|
||||
if err != nil || account.RuntimeStatus != "paused" || account.AuthorizationStatus != "authorized" || len(account.Tags) != 1 {
|
||||
if err != nil || account.RuntimeStatus != "paused" || len(account.Tags) != 1 {
|
||||
t.Fatalf("get account: %#v err=%v", account, err)
|
||||
}
|
||||
if _, err := store.GetAccount(ctx, "account-invalid"); !errors.Is(err, ErrInvalid) && !errors.Is(err, ErrNotFound) {
|
||||
@@ -312,15 +312,6 @@ func TestAccountStoreLifecycleAgainstPostgres(t *testing.T) {
|
||||
if account, err := store.GetAccount(ctx, "account-lifecycle-a"); err != nil || account.RuntimeStatus != "paused" {
|
||||
t.Fatalf("paused account: %#v err=%v", account, err)
|
||||
}
|
||||
if err := store.RevokeAccount(ctx, "account-lifecycle-b"); err != nil {
|
||||
t.Fatalf("revoke: %v", err)
|
||||
}
|
||||
if err := store.ResumeAccount(ctx, "account-lifecycle-b"); !errors.Is(err, ErrConflict) {
|
||||
t.Fatalf("resume revoked account must conflict: %v", err)
|
||||
}
|
||||
if err := store.RevokeAccount(ctx, "account-lifecycle-b"); err != nil {
|
||||
t.Fatalf("revoke must stay idempotent: %v", err)
|
||||
}
|
||||
|
||||
// 审计:分页 + 过滤 + 非法过滤参数
|
||||
page, err := store.ListAudit(ctx, AuditFilter{AccountID: "account-lifecycle-a", Page: 1, PageSize: 10})
|
||||
|
||||
@@ -110,18 +110,6 @@ func TestAccountEnvironmentAutoBindingAndStart(t *testing.T) {
|
||||
t.Fatalf("start audit pair missing: rows=%d err=%v", startAudits, err)
|
||||
}
|
||||
|
||||
// start 冲突分支:吊销账号后 start → 409 readiness blocked
|
||||
if response := do(app, http.MethodPost, "/api/phase-a/accounts/"+accountID+"/revoke", ""); response.Code != http.StatusNoContent {
|
||||
t.Fatalf("revoke failed: %d: %s", response.Code, response.Body.String())
|
||||
}
|
||||
response = do(app, http.MethodPost, "/api/phase-a/accounts/"+accountID+"/start", "")
|
||||
if response.Code != http.StatusConflict {
|
||||
t.Fatalf("expected 409 start on revoked account, got %d: %s", response.Code, response.Body.String())
|
||||
}
|
||||
var conflict map[string]string
|
||||
if err := json.Unmarshal(response.Body.Bytes(), &conflict); err != nil || conflict["reason_code"] != "account_revoked" || conflict["readiness"] != "blocked" {
|
||||
t.Fatalf("start conflict payload: %s err=%v", response.Body.String(), err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountEnvironmentDeletionLifecycle(t *testing.T) {
|
||||
@@ -234,8 +222,8 @@ func TestAccountAuditEndpointFilters(t *testing.T) {
|
||||
t.Cleanup(func() { _ = auditDB.Close() })
|
||||
if _, err := auditDB.ExecContext(ctx, `
|
||||
INSERT INTO gateway (name, endpoint, token) VALUES ('gw-1', 'http://gw-1:8081', 'unit-test-gateway-token');
|
||||
INSERT INTO social_account (account_id, credential_provider, credential_key, name, platform, platform_account_key, authorization_kind, authorization_status, status)
|
||||
VALUES ('account-audit-0000000000000000', 'os_keyring', 'creatorhub/account-audit-0000000000000000', '审计账号', 'douyin', 'key-audit', 'owned', 'authorized', 'paused');
|
||||
INSERT INTO social_account (account_id, credential_provider, credential_key, name, platform, platform_account_key, status)
|
||||
VALUES ('account-audit-0000000000000000', 'os_keyring', 'creatorhub/account-audit-0000000000000000', '审计账号', 'douyin', 'key-audit', 'paused');
|
||||
INSERT INTO audit_event (event_type, account_id, actor, reason_code, details)
|
||||
SELECT 'account_created', account.id, 'local-user', 'account_created', '{"platform":"douyin"}'
|
||||
FROM social_account account WHERE account.account_id = 'account-audit-0000000000000000'`); err != nil {
|
||||
|
||||
@@ -49,7 +49,7 @@ func RegisterAccountRoutes(app *fiber.App, store *accountdomain.Store, runtimeSt
|
||||
PlatformAccountKey: strings.TrimSpace(input.PlatformAccountKey), Tags: tags, Cookies: strings.TrimSpace(input.Cookies),
|
||||
CredentialReference: accountdomain.CredentialReference{ID: accountID + "-cookies", Provider: "os_keyring"},
|
||||
CredentialKey: "creatorhub/" + accountID + "/cookies",
|
||||
AuthorizationStatus: "authorized", RuntimeStatus: "paused", Version: 1,
|
||||
RuntimeStatus: "paused", Version: 1,
|
||||
}
|
||||
if err := store.CreateAccount(c.Context(), account, credentials); err != nil {
|
||||
if errors.Is(err, accountdomain.ErrAccountCreationUnknown) {
|
||||
@@ -165,23 +165,6 @@ func RegisterAccountRoutes(app *fiber.App, store *accountdomain.Store, runtimeSt
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
})
|
||||
|
||||
app.Post("/api/phase-a/accounts/:id/revoke", func(c fiber.Ctx) error {
|
||||
unlock, err := lockAccountResources(c.Context(), runtimeStore, c.Params("id"))
|
||||
if err != nil {
|
||||
return hubError(c, err)
|
||||
}
|
||||
defer unlock()
|
||||
if err := store.RevokeAccount(c.Context(), c.Params("id")); err != nil {
|
||||
return phaseAError(c, err)
|
||||
}
|
||||
if runtimeStore != nil {
|
||||
if err := stopAccountRuntime(c.Context(), runtimeStore, c.Params("id")); err != nil {
|
||||
return hubError(c, err)
|
||||
}
|
||||
}
|
||||
return c.SendStatus(fiber.StatusNoContent)
|
||||
})
|
||||
|
||||
app.Get("/api/phase-a/audit", func(c fiber.Ctx) error {
|
||||
filter, err := auditFilter(c)
|
||||
if err != nil {
|
||||
@@ -234,8 +217,6 @@ func auditFilter(c fiber.Ctx) (accountdomain.AuditFilter, error) {
|
||||
|
||||
func resumeBlockReason(account accountdomain.Account, environment hub.EnvironmentContext, bindingFound bool) string {
|
||||
switch {
|
||||
case account.AuthorizationStatus != "authorized":
|
||||
return "account_revoked"
|
||||
case !bindingFound:
|
||||
return "binding_missing"
|
||||
case environment.Exit.ID != "" && environment.Exit.HealthStatus != "healthy":
|
||||
@@ -328,8 +309,8 @@ func accountDetailPayload(store *accountdomain.Store, runtimeStore HubStore) fib
|
||||
payload := map[string]any{
|
||||
"id": account.ID, "name": account.Name, "platform": account.Platform,
|
||||
"platform_account_key": account.PlatformAccountKey, "tags": account.Tags,
|
||||
"authorization_status": account.AuthorizationStatus, "runtime_status": account.RuntimeStatus,
|
||||
"version": account.Version,
|
||||
"runtime_status": account.RuntimeStatus,
|
||||
"version": account.Version,
|
||||
}
|
||||
if runtimeStore != nil {
|
||||
if environment, envErr := runtimeStore.GetEnvironmentContextForAccount(c.Context(), account.ID); envErr == nil {
|
||||
|
||||
@@ -735,8 +735,7 @@ func creatorLoginQRCode(ctx context.Context, store *creator.Store, phaseAStore *
|
||||
return nil, err
|
||||
}
|
||||
if account.Platform != creator.PlatformDouyin || profile.Platform != creator.PlatformDouyin ||
|
||||
account.AuthorizationStatus != "authorized" || profile.PlatformAccountKey == "" ||
|
||||
account.PlatformAccountKey != profile.PlatformAccountKey {
|
||||
profile.PlatformAccountKey == "" || account.PlatformAccountKey != profile.PlatformAccountKey {
|
||||
return nil, creator.ErrConflict
|
||||
}
|
||||
unlock, lockErr := lockAccountResources(ctx, hubStore, accountID)
|
||||
@@ -784,7 +783,7 @@ func verifyCreatorAccount(ctx context.Context, store *creator.Store, phaseAStore
|
||||
if err != nil {
|
||||
return creator.LoginResult{}, err
|
||||
}
|
||||
if account.Platform != profile.Platform || account.Platform != creator.PlatformDouyin || account.AuthorizationStatus != "authorized" || profile.PlatformAccountKey == "" || account.PlatformAccountKey != profile.PlatformAccountKey {
|
||||
if account.Platform != profile.Platform || account.Platform != creator.PlatformDouyin || profile.PlatformAccountKey == "" || account.PlatformAccountKey != profile.PlatformAccountKey {
|
||||
return creator.LoginResult{}, creator.ErrConflict
|
||||
}
|
||||
environment, err := hubStore.GetEnvironmentContextForAccount(ctx, accountID)
|
||||
@@ -1221,7 +1220,7 @@ func newDouyinAccountBrowser(ctx context.Context, store *creator.Store, phaseASt
|
||||
if err != nil {
|
||||
return creatorGatewayBrowser{}, err
|
||||
}
|
||||
if account.Platform != creator.PlatformDouyin || account.AuthorizationStatus != "authorized" {
|
||||
if account.Platform != creator.PlatformDouyin {
|
||||
return creatorGatewayBrowser{}, creator.ErrConflict
|
||||
}
|
||||
profile, err := store.GetAccountProfile(ctx, accountID)
|
||||
@@ -1536,7 +1535,7 @@ func refreshCreatorMetricWork(ctx context.Context, store *creator.Store, phaseAS
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if account.Platform != creator.PlatformDouyin || account.AuthorizationStatus != "authorized" || (profile.BusinessStatus != "normal" && profile.BusinessStatus != "muted") || profile.LoginStatus != "logged_in" {
|
||||
if account.Platform != creator.PlatformDouyin || (profile.BusinessStatus != "normal" && profile.BusinessStatus != "muted") || profile.LoginStatus != "logged_in" {
|
||||
return creator.ErrConflict
|
||||
}
|
||||
environment, err := hubStore.GetEnvironmentContextForAccount(ctx, accountID)
|
||||
@@ -1600,7 +1599,7 @@ func syncCreatorOwned(ctx context.Context, store *creator.Store, phaseAStore *ac
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if account.Platform != creator.PlatformDouyin || account.AuthorizationStatus != "authorized" {
|
||||
if account.Platform != creator.PlatformDouyin {
|
||||
return creator.ErrConflict
|
||||
}
|
||||
profile, err := store.GetAccountProfile(ctx, accountID)
|
||||
@@ -1676,7 +1675,7 @@ func creatorCollectionAccount(ctx context.Context, store *creator.Store, phaseAS
|
||||
return "", err
|
||||
}
|
||||
for _, account := range accounts {
|
||||
if account.Platform != platform || account.AuthorizationStatus != "authorized" {
|
||||
if account.Platform != platform {
|
||||
continue
|
||||
}
|
||||
profile, err := store.GetAccountProfile(ctx, account.ID)
|
||||
|
||||
@@ -108,7 +108,7 @@ func TestDouyinGatewayBrowserDoesNotEchoCredentialOnFailure(t *testing.T) {
|
||||
|
||||
func readyDouyinEnvironment() hub.EnvironmentContext {
|
||||
return hub.EnvironmentContext{Env: hub.Env{Alias: "account-a", Gateway: "gateway-a"}, AccountID: "account-a",
|
||||
AccountStatus: "active", AuthorizationStatus: "authorized", BindingVersion: 2,
|
||||
AccountStatus: "active", BindingVersion: 2,
|
||||
Exit: hub.NetworkExit{ID: "exit-a", HealthStatus: "healthy"},
|
||||
RuntimeID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", RuntimeNetworkID: "native-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}
|
||||
}
|
||||
|
||||
@@ -333,7 +333,6 @@ type envView struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
AccountID string `json:"account_id"`
|
||||
AccountStatus string `json:"account_status"`
|
||||
AuthorizationStatus string `json:"authorization_status"`
|
||||
NetworkExitID string `json:"network_exit_id"`
|
||||
NetworkExitHealth string `json:"network_exit_health"`
|
||||
BindingVersion int64 `json:"binding_version"`
|
||||
@@ -350,8 +349,6 @@ type envView struct {
|
||||
|
||||
func environmentScheduleReadiness(environment hub.EnvironmentContext) (string, string) {
|
||||
switch {
|
||||
case environment.AuthorizationStatus != "authorized":
|
||||
return "blocked", "account_revoked"
|
||||
case environment.AccountStatus != "active":
|
||||
return "blocked", "account_paused"
|
||||
case environment.Exit.ID != "" && environment.Exit.HealthStatus != "healthy":
|
||||
@@ -392,7 +389,7 @@ func validCreatedRuntime(created runtimeStatus, environment hub.EnvironmentConte
|
||||
}
|
||||
|
||||
func accountRunnable(environment hub.EnvironmentContext) bool {
|
||||
return environment.AccountStatus == "active" && environment.AuthorizationStatus == "authorized"
|
||||
return environment.AccountStatus == "active"
|
||||
}
|
||||
|
||||
func releaseRuntime(ctx context.Context, store RuntimeStopStore, environment hub.EnvironmentContext) error {
|
||||
@@ -525,7 +522,7 @@ func getBrowser(store HubStore) fiber.Handler {
|
||||
view := envView{
|
||||
Env: environment.Env, State: "recorded", Status: "已记录运行实例",
|
||||
AccountID: environment.AccountID, AccountStatus: environment.AccountStatus,
|
||||
AuthorizationStatus: environment.AuthorizationStatus, NetworkExitID: environment.Exit.ID,
|
||||
NetworkExitID: environment.Exit.ID,
|
||||
NetworkExitHealth: environment.Exit.HealthStatus, BindingVersion: environment.BindingVersion,
|
||||
RuntimeID: environment.RuntimeID, RuntimeNodeID: environment.RuntimeNodeID,
|
||||
ScheduleStatus: "blocked", ScheduleBlockReason: "binding_missing",
|
||||
@@ -885,7 +882,7 @@ func listBrowsers(store HubStore) fiber.Handler {
|
||||
RecoveryRequired: true, GatewayReachable: false,
|
||||
}
|
||||
if environment, contextErr := store.GetEnvironmentContext(c.Context(), env.Alias); contextErr == nil {
|
||||
view.AccountID, view.AccountStatus, view.AuthorizationStatus = environment.AccountID, environment.AccountStatus, environment.AuthorizationStatus
|
||||
view.AccountID, view.AccountStatus = environment.AccountID, environment.AccountStatus
|
||||
view.NetworkExitID, view.NetworkExitHealth, view.BindingVersion = environment.Exit.ID, environment.Exit.HealthStatus, environment.BindingVersion
|
||||
view.RuntimeID = environment.RuntimeID
|
||||
view.RuntimeID = environment.RuntimeID
|
||||
|
||||
@@ -14,14 +14,13 @@ import (
|
||||
|
||||
func testRunnableEnvironment() hub.EnvironmentContext {
|
||||
return hub.EnvironmentContext{
|
||||
Env: hub.Env{Alias: "account-1"},
|
||||
AccountID: "account-1",
|
||||
AccountStatus: "active",
|
||||
AuthorizationStatus: "authorized",
|
||||
RuntimeID: "runtime-1",
|
||||
RuntimeNetworkID: "network-1",
|
||||
Exit: hub.NetworkExit{ID: "exit-1", HealthStatus: "healthy"},
|
||||
BindingVersion: 1,
|
||||
Env: hub.Env{Alias: "account-1"},
|
||||
AccountID: "account-1",
|
||||
AccountStatus: "active",
|
||||
RuntimeID: "runtime-1",
|
||||
RuntimeNetworkID: "network-1",
|
||||
Exit: hub.NetworkExit{ID: "exit-1", HealthStatus: "healthy"},
|
||||
BindingVersion: 1,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ func newMemoryStore() *memoryStore {
|
||||
}
|
||||
|
||||
func TestResumeBlockReasonIsStable(t *testing.T) {
|
||||
account := accountdomain.Account{AuthorizationStatus: "authorized"}
|
||||
account := accountdomain.Account{}
|
||||
healthy := hub.EnvironmentContext{Exit: hub.NetworkExit{ID: "exit-a", HealthStatus: "healthy"}}
|
||||
for name, test := range map[string]struct {
|
||||
account accountdomain.Account
|
||||
@@ -75,7 +75,6 @@ func TestResumeBlockReasonIsStable(t *testing.T) {
|
||||
found bool
|
||||
want string
|
||||
}{
|
||||
"revoked": {accountdomain.Account{AuthorizationStatus: "revoked"}, healthy, true, "account_revoked"},
|
||||
"missing binding": {account, hub.EnvironmentContext{}, false, "binding_missing"},
|
||||
"direct exit": {account, hub.EnvironmentContext{}, true, "account_conflict"},
|
||||
"unhealthy exit": {account, hub.EnvironmentContext{Exit: hub.NetworkExit{ID: "exit-a", HealthStatus: "unhealthy"}}, true, "network_exit_unhealthy"},
|
||||
@@ -281,7 +280,7 @@ func (s *memoryStore) CreateBoundEnv(ctx context.Context, env hub.Env, accountID
|
||||
s.mu.Lock()
|
||||
if existing, ok := s.bindings[env.Alias]; ok {
|
||||
if existing.AccountStatus == "" {
|
||||
existing.AccountStatus, existing.AuthorizationStatus = "active", "authorized"
|
||||
existing.AccountStatus = "active"
|
||||
}
|
||||
s.mu.Unlock()
|
||||
return existing, false, nil
|
||||
@@ -292,7 +291,7 @@ func (s *memoryStore) CreateBoundEnv(ctx context.Context, env hub.Env, accountID
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
bound := hub.EnvironmentContext{Env: env, AccountID: accountID, AccountStatus: "active", AuthorizationStatus: "authorized",
|
||||
bound := hub.EnvironmentContext{Env: env, AccountID: accountID, AccountStatus: "active",
|
||||
BindingVersion: 1, Exit: s.exits[exitID]}
|
||||
s.bindings[env.Alias] = bound
|
||||
return bound, true, nil
|
||||
@@ -302,7 +301,7 @@ func (s *memoryStore) GetEnvironmentContext(_ context.Context, alias string) (hu
|
||||
defer s.mu.Unlock()
|
||||
if bound, ok := s.bindings[alias]; ok {
|
||||
if bound.AccountStatus == "" {
|
||||
bound.AccountStatus, bound.AuthorizationStatus = "active", "authorized"
|
||||
bound.AccountStatus = "active"
|
||||
}
|
||||
return bound, nil
|
||||
}
|
||||
@@ -310,7 +309,7 @@ func (s *memoryStore) GetEnvironmentContext(_ context.Context, alias string) (hu
|
||||
if !ok {
|
||||
return hub.EnvironmentContext{}, hub.ErrNotFound
|
||||
}
|
||||
return hub.EnvironmentContext{Env: env, AccountID: alias, AccountStatus: "active", AuthorizationStatus: "authorized",
|
||||
return hub.EnvironmentContext{Env: env, AccountID: alias, AccountStatus: "active",
|
||||
BindingVersion: 1, Exit: s.exits["exit-1"]}, nil
|
||||
}
|
||||
func (s *memoryStore) GetEnvironmentContextForAccount(ctx context.Context, accountID string) (hub.EnvironmentContext, error) {
|
||||
@@ -368,9 +367,9 @@ func (s *memoryStore) ActivateRuntime(_ context.Context, alias, runtimeID string
|
||||
bound = hub.EnvironmentContext{Env: s.envs[alias], AccountID: alias, BindingVersion: 1, Exit: s.exits["exit-1"]}
|
||||
}
|
||||
if bound.AccountStatus == "" {
|
||||
bound.AccountStatus, bound.AuthorizationStatus = "active", "authorized"
|
||||
bound.AccountStatus = "active"
|
||||
}
|
||||
if bound.AccountStatus != "active" || bound.AuthorizationStatus != "authorized" ||
|
||||
if bound.AccountStatus != "active" ||
|
||||
bound.BindingVersion != bindingVersion || bound.Exit.ID != exitID || bound.RuntimeCleanupPending {
|
||||
return hub.EnvironmentContext{}, hub.ErrConflict
|
||||
}
|
||||
@@ -1031,8 +1030,7 @@ func TestCreateBrowserDeterministicRejectionDoesNotWedgeBinding(t *testing.T) {
|
||||
func TestReconcileGatewayCreateDoesNotReuseOldNetworkGeneration(t *testing.T) {
|
||||
store := newMemoryStore()
|
||||
store.envs["account-a"] = hub.Env{Alias: "account-a", Gateway: "gw-1"}
|
||||
environment := hub.EnvironmentContext{Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "active",
|
||||
AuthorizationStatus: "authorized", BindingVersion: 1, Exit: store.exits["exit-1"],
|
||||
environment := hub.EnvironmentContext{Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "active", BindingVersion: 1, Exit: store.exits["exit-1"],
|
||||
RuntimeID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", RuntimeNetworkID: "native-99999999999999999999999999999999"}
|
||||
store.bindings[environment.Alias] = environment
|
||||
gateway := &fakeGateway{token: "unit-test-gateway-token", runtimes: []runtimeStatus{{
|
||||
@@ -1106,14 +1104,13 @@ func TestActivationConflictKeepsTheGenerationWonByTheHeartbeat(t *testing.T) {
|
||||
candidateID := "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
|
||||
candidateNetwork := "native-88888888888888888888888888888888"
|
||||
current := hub.EnvironmentContext{
|
||||
Env: store.envs["account-a"],
|
||||
AccountID: "account-a",
|
||||
AccountStatus: "active",
|
||||
AuthorizationStatus: "authorized",
|
||||
BindingVersion: 1,
|
||||
RuntimeID: candidateID,
|
||||
RuntimeNetworkID: candidateNetwork,
|
||||
Exit: store.exits["exit-1"],
|
||||
Env: store.envs["account-a"],
|
||||
AccountID: "account-a",
|
||||
AccountStatus: "active",
|
||||
BindingVersion: 1,
|
||||
RuntimeID: candidateID,
|
||||
RuntimeNetworkID: candidateNetwork,
|
||||
Exit: store.exits["exit-1"],
|
||||
}
|
||||
store.bindings["account-a"] = current
|
||||
conflict := &activationConflictStore{memoryStore: store, current: current}
|
||||
@@ -1130,8 +1127,7 @@ func TestActivationConflictKeepsTheGenerationWonByTheHeartbeat(t *testing.T) {
|
||||
func TestLegacyActiveRuntimeWithoutNetworkGenerationDoesNotTouchSuccessor(t *testing.T) {
|
||||
store := newMemoryStore()
|
||||
store.envs["account-a"] = hub.Env{Alias: "account-a", Gateway: "gw-1"}
|
||||
environment := hub.EnvironmentContext{Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "active",
|
||||
AuthorizationStatus: "authorized", BindingVersion: 1, Exit: store.exits["exit-1"],
|
||||
environment := hub.EnvironmentContext{Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "active", BindingVersion: 1, Exit: store.exits["exit-1"],
|
||||
RuntimeID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}
|
||||
store.bindings[environment.Alias] = environment
|
||||
gateway := &fakeGateway{token: "unit-test-gateway-token", runtimes: []runtimeStatus{{
|
||||
@@ -1567,8 +1563,8 @@ func TestPauseAndRevokeStopContainerWithoutRuntimeLease(t *testing.T) {
|
||||
fixture.gateway.mu.Lock()
|
||||
fixture.gateway.runtimes[0].State = "running"
|
||||
fixture.gateway.mu.Unlock()
|
||||
if response := do(app, http.MethodPost, "/api/phase-a/accounts/account-a/revoke", ""); response.Code != http.StatusNoContent {
|
||||
t.Fatalf("revoke running orphan without lease: %d %s", response.Code, response.Body.String())
|
||||
if response := do(app, http.MethodPost, "/api/phase-a/accounts/account-a/pause", ""); response.Code != http.StatusNoContent {
|
||||
t.Fatalf("pause running orphan without lease: %d %s", response.Code, response.Body.String())
|
||||
}
|
||||
|
||||
after, err := fixture.store.GetEnvironmentContext(ctx, fixture.bound.Alias)
|
||||
@@ -1576,8 +1572,8 @@ func TestPauseAndRevokeStopContainerWithoutRuntimeLease(t *testing.T) {
|
||||
t.Fatalf("lease-free stop did not converge: %#v err=%v", after, err)
|
||||
}
|
||||
account, err := accountStore.GetAccount(ctx, fixture.bound.AccountID)
|
||||
if err != nil || account.AuthorizationStatus != "revoked" || account.RuntimeStatus != "paused" {
|
||||
t.Fatalf("revoke gate did not remain closed: %#v err=%v", account, err)
|
||||
if err != nil || account.RuntimeStatus != "paused" {
|
||||
t.Fatalf("pause gate did not apply: %#v err=%v", account, err)
|
||||
}
|
||||
fixture.gateway.mu.Lock()
|
||||
container := fixture.gateway.runtimes[0]
|
||||
@@ -1596,7 +1592,7 @@ func TestPauseAndRevokeStopContainerWithoutRuntimeLease(t *testing.T) {
|
||||
}
|
||||
}
|
||||
if stopCalls != 2 {
|
||||
t.Fatalf("pause/revoke did not reconcile both lease-free runtimes: %#v", requests)
|
||||
t.Fatalf("pause did not reconcile both lease-free runtimes: %#v", requests)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1874,8 +1870,7 @@ func TestRemoveGatewayRuntimePreservesKnownNetworkGeneration(t *testing.T) {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
store := newMemoryStore()
|
||||
store.envs["account-a"] = hub.Env{Alias: "account-a", Gateway: "gw-1"}
|
||||
environment := hub.EnvironmentContext{Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "active",
|
||||
AuthorizationStatus: "authorized", BindingVersion: 1, Exit: store.exits["exit-1"],
|
||||
environment := hub.EnvironmentContext{Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "active", BindingVersion: 1, Exit: store.exits["exit-1"],
|
||||
RuntimeID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", RuntimeNetworkID: "native-99999999999999999999999999999999"}
|
||||
store.bindings["account-a"] = environment
|
||||
gateway := &fakeGateway{token: "unit-test-gateway-token", runtimes: []runtimeStatus{{ID: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", Alias: "account-a",
|
||||
@@ -2670,70 +2665,68 @@ func TestPostgresNonRunnableReconcileAuditsRuntimeRelease(t *testing.T) {
|
||||
t.Skip("set CREATORHUB_POSTGRES_TEST_URL to run PostgreSQL integration coverage")
|
||||
}
|
||||
ctx := context.Background()
|
||||
for _, authorization := range []string{"authorized", "revoked"} {
|
||||
for _, observation := range []string{"stopped", "missing"} {
|
||||
for _, releaseFailure := range []bool{false, true} {
|
||||
name := authorization + "/" + observation + "/success"
|
||||
if releaseFailure {
|
||||
name = authorization + "/" + observation + "/release failure"
|
||||
for _, observation := range []string{"stopped", "missing"} {
|
||||
for _, releaseFailure := range []bool{false, true} {
|
||||
name := observation + "/success"
|
||||
if releaseFailure {
|
||||
name = observation + "/release failure"
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
fixture := newPostgresRebindFixture(t, databaseURL)
|
||||
setFixtureAccountStatus(t, fixture.databaseURL, "active")
|
||||
var err error
|
||||
fixture.bound, err = fixture.store.ActivateRuntime(ctx, "account-a", "stopped-container",
|
||||
fixture.bound.BindingVersion, fixture.bound.Exit.ID, "stopped-network")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := fixture.db.ExecContext(ctx, `
|
||||
UPDATE social_account SET status = 'paused' WHERE account_id = 'account-a'`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if observation == "stopped" {
|
||||
fixture.gateway.runtimes = []runtimeStatus{{
|
||||
ID: "stopped-container", Alias: "account-a", State: "exited",
|
||||
BindingVersion: fixture.bound.BindingVersion, NetworkExitID: fixture.bound.Exit.ID,
|
||||
}}
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
fixture := newPostgresRebindFixture(t, databaseURL)
|
||||
setFixtureAccountStatus(t, fixture.databaseURL, "active")
|
||||
var err error
|
||||
fixture.bound, err = fixture.store.ActivateRuntime(ctx, "account-a", "stopped-container",
|
||||
fixture.bound.BindingVersion, fixture.bound.Exit.ID, "stopped-network")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := fixture.db.ExecContext(ctx, `
|
||||
UPDATE social_account SET status = 'paused', authorization_status = $1 WHERE account_id = 'account-a'`, authorization); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if observation == "stopped" {
|
||||
fixture.gateway.runtimes = []runtimeStatus{{
|
||||
ID: "stopped-container", Alias: "account-a", State: "exited",
|
||||
BindingVersion: fixture.bound.BindingVersion, NetworkExitID: fixture.bound.Exit.ID,
|
||||
}}
|
||||
}
|
||||
|
||||
var store HubStore = fixture.store
|
||||
if releaseFailure {
|
||||
store = failingRuntimeReleaseStore{Store: fixture.store, err: errors.New("release unavailable")}
|
||||
}
|
||||
err = reconcileRuntimeLeases(ctx, store, fakeExitProbe{}, func(hub.NetworkExitAccess) (string, error) { return "", nil })
|
||||
wantOutcome, wantReason := "succeeded", "runtime_released"
|
||||
if releaseFailure {
|
||||
wantOutcome, wantReason = "failed", "runtime_release_failed"
|
||||
}
|
||||
if (err != nil) != releaseFailure {
|
||||
t.Fatalf("reconcile error=%v, releaseFailure=%v", err, releaseFailure)
|
||||
}
|
||||
after, err := fixture.store.GetEnvironmentContext(ctx, "account-a")
|
||||
if err != nil || (after.RuntimeID != "") == !releaseFailure {
|
||||
t.Fatalf("runtime lease after reconcile: %#v err=%v", after, err)
|
||||
}
|
||||
var store HubStore = fixture.store
|
||||
if releaseFailure {
|
||||
store = failingRuntimeReleaseStore{Store: fixture.store, err: errors.New("release unavailable")}
|
||||
}
|
||||
err = reconcileRuntimeLeases(ctx, store, fakeExitProbe{}, func(hub.NetworkExitAccess) (string, error) { return "", nil })
|
||||
wantOutcome, wantReason := "succeeded", "runtime_released"
|
||||
if releaseFailure {
|
||||
wantOutcome, wantReason = "failed", "runtime_release_failed"
|
||||
}
|
||||
if (err != nil) != releaseFailure {
|
||||
t.Fatalf("reconcile error=%v, releaseFailure=%v", err, releaseFailure)
|
||||
}
|
||||
after, err := fixture.store.GetEnvironmentContext(ctx, "account-a")
|
||||
if err != nil || (after.RuntimeID != "") == !releaseFailure {
|
||||
t.Fatalf("runtime lease after reconcile: %#v err=%v", after, err)
|
||||
}
|
||||
|
||||
var events, operations, missingOperations int
|
||||
var eventTypes, outcome, reason string
|
||||
if err := fixture.db.QueryRowContext(ctx, `
|
||||
var events, operations, missingOperations int
|
||||
var eventTypes, outcome, reason string
|
||||
if err := fixture.db.QueryRowContext(ctx, `
|
||||
SELECT count(*), count(DISTINCT operation_id),
|
||||
count(*) FILTER (WHERE coalesce(operation_id, '') = ''),
|
||||
string_agg(event_type, ',' ORDER BY id),
|
||||
coalesce(max(outcome) FILTER (WHERE event_type = 'environment_action_finished'), ''),
|
||||
coalesce(max(reason_code) FILTER (WHERE event_type = 'environment_action_finished'), '')
|
||||
FROM audit_event WHERE action = 'reconcile'`).Scan(
|
||||
&events, &operations, &missingOperations, &eventTypes, &outcome, &reason); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if events != 2 || operations != 1 || missingOperations != 0 ||
|
||||
eventTypes != "environment_action_requested,environment_action_finished" ||
|
||||
outcome != wantOutcome || reason != wantReason {
|
||||
t.Fatalf("reconcile audit mismatch: events=%d operations=%d missing=%d types=%q outcome=%q reason=%q",
|
||||
events, operations, missingOperations, eventTypes, outcome, reason)
|
||||
}
|
||||
})
|
||||
}
|
||||
&events, &operations, &missingOperations, &eventTypes, &outcome, &reason); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if events != 2 || operations != 1 || missingOperations != 0 ||
|
||||
eventTypes != "environment_action_requested,environment_action_finished" ||
|
||||
outcome != wantOutcome || reason != wantReason {
|
||||
t.Fatalf("reconcile audit mismatch: events=%d operations=%d missing=%d types=%q outcome=%q reason=%q",
|
||||
events, operations, missingOperations, eventTypes, outcome, reason)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2761,17 +2754,16 @@ func TestBrowserActionRoutesStartStopAndRejectsUnknown(t *testing.T) {
|
||||
|
||||
func TestStartRejectsPausedOrRevokedAccountBeforeGatewayCall(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name, status, authorization string
|
||||
name, status string
|
||||
}{
|
||||
{name: "paused", status: "paused", authorization: "authorized"},
|
||||
{name: "revoked", status: "paused", authorization: "revoked"},
|
||||
{name: "paused", status: "paused"},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
store := newMemoryStore()
|
||||
store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}}
|
||||
store.bindings["account-a"] = hub.EnvironmentContext{
|
||||
Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: test.status,
|
||||
AuthorizationStatus: test.authorization, BindingVersion: 1, Exit: store.exits["exit-1"],
|
||||
BindingVersion: 1, Exit: store.exits["exit-1"],
|
||||
}
|
||||
gateway := &fakeGateway{token: "unit-test-gateway-token"}
|
||||
app := newTestApp(t, store, gateway)
|
||||
@@ -2788,12 +2780,12 @@ func TestStartRejectsPausedOrRevokedAccountBeforeGatewayCall(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestReconcileStopsRuntimeForPausedOrRevokedAccount(t *testing.T) {
|
||||
for _, authorization := range []string{"authorized", "revoked"} {
|
||||
t.Run(authorization, func(t *testing.T) {
|
||||
{
|
||||
t.Run("paused", func(t *testing.T) {
|
||||
store := newMemoryStore()
|
||||
store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}}
|
||||
store.bindings["account-a"] = hub.EnvironmentContext{
|
||||
Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "paused", AuthorizationStatus: authorization,
|
||||
Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "paused",
|
||||
BindingVersion: 1, Exit: store.exits["exit-1"],
|
||||
RuntimeID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
|
||||
}
|
||||
@@ -2830,7 +2822,7 @@ func TestActivationConflictCleanupIsGenerationSafe(t *testing.T) {
|
||||
store := newMemoryStore()
|
||||
store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}}
|
||||
environment := hub.EnvironmentContext{
|
||||
Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "paused", AuthorizationStatus: "authorized",
|
||||
Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "paused",
|
||||
BindingVersion: 2, Exit: store.exits["exit-1"],
|
||||
}
|
||||
store.bindings[environment.Alias] = environment
|
||||
@@ -2865,7 +2857,7 @@ func TestExplicitStopRejectsStaleBindingBeforeGatewaySideEffect(t *testing.T) {
|
||||
store := newMemoryStore()
|
||||
store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}}
|
||||
store.bindings["account-a"] = hub.EnvironmentContext{
|
||||
Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "active", AuthorizationStatus: "authorized",
|
||||
Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "active",
|
||||
BindingVersion: 2, Exit: store.exits["exit-1"], RuntimeID: "new-container",
|
||||
}
|
||||
gateway := &fakeGateway{token: "unit-test-gateway-token", runtimes: []runtimeStatus{{
|
||||
@@ -2890,7 +2882,7 @@ func TestReconcileFinishedAuditUsesRebuiltRuntime(t *testing.T) {
|
||||
store.exits["exit-2"] = hub.NetworkExit{ID: "exit-2", Protocol: "http", Host: "proxy-2.example", Port: 8080, HealthStatus: "healthy", Version: 1}
|
||||
store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}}
|
||||
store.bindings["account-a"] = hub.EnvironmentContext{
|
||||
Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "active", AuthorizationStatus: "authorized",
|
||||
Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "active",
|
||||
BindingVersion: 2, Exit: store.exits["exit-2"], RuntimeID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", RuntimeNetworkID: "native-66666666666666666666666666666666",
|
||||
}
|
||||
gateway := &fakeGateway{token: "unit-test-gateway-token", runtimes: []runtimeStatus{{
|
||||
@@ -2913,7 +2905,7 @@ func TestReconcileContextRefreshFailureClearsAllAuditCorrelation(t *testing.T) {
|
||||
store := newMemoryStore()
|
||||
store.envs["account-a"] = hub.Env{Alias: "account-a", Name: "甲", Gateway: "gw-1", Fingerprint: hub.Fingerprint{Seed: 1}}
|
||||
store.bindings["account-a"] = hub.EnvironmentContext{
|
||||
Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "active", AuthorizationStatus: "authorized",
|
||||
Env: store.envs["account-a"], AccountID: "account-a", AccountStatus: "active",
|
||||
BindingVersion: 2, Exit: store.exits["exit-1"], RuntimeID: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", RuntimeNetworkID: "native-66666666666666666666666666666666",
|
||||
}
|
||||
gateway := &fakeGateway{token: "unit-test-gateway-token", runtimes: []runtimeStatus{{
|
||||
|
||||
@@ -366,7 +366,6 @@ func controlPlaneRouteMatrix() []controlPlaneRouteCase {
|
||||
{http.MethodPost, "/api/phase-a/accounts/:id/environment", "/api/phase-a/accounts/missing/environment", "", http.StatusNotFound},
|
||||
{http.MethodPost, "/api/phase-a/accounts/:id/start", "/api/phase-a/accounts/missing/start", "", http.StatusNotFound},
|
||||
{http.MethodPost, "/api/phase-a/accounts/:id/resume", "/api/phase-a/accounts/missing/resume", "", http.StatusNotFound},
|
||||
{http.MethodPost, "/api/phase-a/accounts/:id/revoke", "/api/phase-a/accounts/missing/revoke", "", http.StatusNotFound},
|
||||
|
||||
{http.MethodGet, "/api/phase-a/audit", "/api/phase-a/audit", "", http.StatusOK},
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ func (s *Store) EnsureAccountProfile(ctx context.Context, accountID string) erro
|
||||
func accountProfileQuery() string {
|
||||
return `
|
||||
SELECT a.account_id, a.name, a.platform, a.platform_account_key,
|
||||
a.authorization_kind, a.authorization_status, a.status,
|
||||
a.status,
|
||||
p.login_username, p.password_configured, p.real_name_status,
|
||||
p.real_name, p.identity_number, p.note, p.business_status,
|
||||
p.big_account, p.reply_requirements, p.login_status, p.login_reason,
|
||||
@@ -39,7 +39,7 @@ func scanAccountProfile(scanner interface{ Scan(...any) error }) (AccountProfile
|
||||
var checkedAt sql.NullTime
|
||||
if err := scanner.Scan(
|
||||
&result.ID, &result.Name, &result.Platform, &result.PlatformAccountKey,
|
||||
&result.AuthorizationKind, &result.AuthorizationStatus, &result.RuntimeStatus,
|
||||
&result.RuntimeStatus,
|
||||
&result.LoginUsername, &result.PasswordConfigured, &result.RealNameStatus,
|
||||
&result.RealName, &result.IdentityNumber, &result.Note, &result.BusinessStatus,
|
||||
&result.BigAccount, &result.ReplyRequirements, &result.LoginStatus, &result.LoginReason,
|
||||
|
||||
@@ -340,7 +340,7 @@ func (s *Store) ListDueOwnedAccounts(ctx context.Context, now time.Time, interva
|
||||
ON works_checkpoint.source_type='owned' AND works_checkpoint.source_id=account.account_id AND works_checkpoint.collection_kind='works'
|
||||
LEFT JOIN creator_collection_checkpoint comments_checkpoint
|
||||
ON comments_checkpoint.source_type='owned' AND comments_checkpoint.source_id=account.account_id AND comments_checkpoint.collection_kind='comments'
|
||||
WHERE account.platform = 'douyin' AND account.authorization_status='authorized'
|
||||
WHERE account.platform = 'douyin'
|
||||
AND profile.login_status='logged_in'
|
||||
AND COALESCE(works_checkpoint.status, '') <> 'blocked'
|
||||
AND COALESCE(comments_checkpoint.status, '') <> 'blocked'
|
||||
|
||||
@@ -222,7 +222,7 @@ func CanWrite(profile AccountProfile, automatic bool, action string) error {
|
||||
return ErrConflict
|
||||
}
|
||||
}
|
||||
if profile.LoginStatus != "logged_in" || profile.AuthorizationStatus != "authorized" {
|
||||
if profile.LoginStatus != "logged_in" {
|
||||
return ErrConflict
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -69,7 +69,7 @@ func TestActionTargetAndBusinessStatusGates(t *testing.T) {
|
||||
if ActionTargetValid(ActionReplyComment, "uid-1", "", "", "comment") {
|
||||
t.Fatal("reply without a comment target must be blocked")
|
||||
}
|
||||
profile := AccountProfile{AuthorizationStatus: "authorized", LoginStatus: "logged_in", BusinessStatus: "muted"}
|
||||
profile := AccountProfile{LoginStatus: "logged_in", BusinessStatus: "muted"}
|
||||
if err := CanWrite(profile, false, ActionDM); err == nil {
|
||||
t.Fatal("muted accounts cannot manually send DMs")
|
||||
}
|
||||
|
||||
+19
-21
@@ -52,27 +52,25 @@ type SettingsUpdate struct {
|
||||
}
|
||||
|
||||
type AccountProfile struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Platform string `json:"platform"`
|
||||
PlatformAccountKey string `json:"platform_account_key"`
|
||||
AuthorizationKind string `json:"authorization_kind"`
|
||||
AuthorizationStatus string `json:"authorization_status"`
|
||||
RuntimeStatus string `json:"runtime_status"`
|
||||
LoginUsername string `json:"login_username"`
|
||||
PasswordConfigured bool `json:"password_configured"`
|
||||
RealNameStatus string `json:"real_name_status"`
|
||||
RealName string `json:"real_name"`
|
||||
IdentityNumber string `json:"identity_number"`
|
||||
Note string `json:"note"`
|
||||
BusinessStatus string `json:"business_status"`
|
||||
BigAccount bool `json:"big_account"`
|
||||
ReplyRequirements string `json:"reply_requirements"`
|
||||
LoginStatus string `json:"login_status"`
|
||||
LoginReason string `json:"login_reason"`
|
||||
LoginCheckedAt *time.Time `json:"login_checked_at,omitempty"`
|
||||
CooldownSeconds int64 `json:"cooldown_seconds"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Platform string `json:"platform"`
|
||||
PlatformAccountKey string `json:"platform_account_key"`
|
||||
RuntimeStatus string `json:"runtime_status"`
|
||||
LoginUsername string `json:"login_username"`
|
||||
PasswordConfigured bool `json:"password_configured"`
|
||||
RealNameStatus string `json:"real_name_status"`
|
||||
RealName string `json:"real_name"`
|
||||
IdentityNumber string `json:"identity_number"`
|
||||
Note string `json:"note"`
|
||||
BusinessStatus string `json:"business_status"`
|
||||
BigAccount bool `json:"big_account"`
|
||||
ReplyRequirements string `json:"reply_requirements"`
|
||||
LoginStatus string `json:"login_status"`
|
||||
LoginReason string `json:"login_reason"`
|
||||
LoginCheckedAt *time.Time `json:"login_checked_at,omitempty"`
|
||||
CooldownSeconds int64 `json:"cooldown_seconds"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type AccountProfileUpdate struct {
|
||||
|
||||
@@ -49,7 +49,6 @@ type EnvironmentContext struct {
|
||||
Env
|
||||
AccountID string `json:"account_id"`
|
||||
AccountStatus string `json:"account_status"`
|
||||
AuthorizationStatus string `json:"authorization_status"`
|
||||
BindingVersion int64 `json:"binding_version"`
|
||||
RuntimeCleanupPending bool `json:"runtime_cleanup_pending,omitempty"`
|
||||
RuntimeCleanupBindingVersion int64 `json:"runtime_cleanup_binding_version,omitempty"`
|
||||
@@ -432,7 +431,6 @@ func (s *Store) CreateBoundEnv(ctx context.Context, env Env, accountID, exitID s
|
||||
JOIN gateway ON gateway.name = $3
|
||||
LEFT JOIN network_exit network ON network.exit_id = NULLIF($6, '')
|
||||
WHERE account.account_id = $5 AND account.status = 'paused'
|
||||
AND account.authorization_status = 'authorized'
|
||||
AND ($6 = '' OR network.health_status = 'healthy')
|
||||
RETURNING alias`, env.Alias, env.Name, env.Gateway, encoded, accountID, exitID).Scan(&created); err != nil {
|
||||
return EnvironmentContext{}, false, rowError(err)
|
||||
@@ -464,7 +462,7 @@ func (s *Store) GetEnvironmentContext(ctx context.Context, alias string) (Enviro
|
||||
var cleanupBindingVersion sql.NullInt64
|
||||
err = tx.QueryRowContext(ctx, `
|
||||
SELECT environment.alias, environment.name, gateway.name,
|
||||
environment.fingerprint, environment.created_at, account.account_id, account.status, account.authorization_status,
|
||||
environment.fingerprint, environment.created_at, account.account_id, account.status,
|
||||
environment.version,
|
||||
environment.runtime_cleanup_pending, environment.runtime_cleanup_binding_version,
|
||||
environment.runtime_cleanup_runtime_id, environment.runtime_cleanup_network_id,
|
||||
@@ -481,7 +479,7 @@ func (s *Store) GetEnvironmentContext(ctx context.Context, alias string) (Enviro
|
||||
LEFT JOIN network_exit network ON network.id = environment.exit_id
|
||||
WHERE environment.alias = $1`, alias).
|
||||
Scan(&result.Alias, &result.Name, &result.Gateway, &encoded, &result.CreatedAt,
|
||||
&result.AccountID, &result.AccountStatus, &result.AuthorizationStatus,
|
||||
&result.AccountID, &result.AccountStatus,
|
||||
&result.BindingVersion, &result.RuntimeCleanupPending, &cleanupBindingVersion,
|
||||
&cleanupRuntimeID, &cleanupNetworkID,
|
||||
&result.Exit.ID, &result.Exit.Protocol, &result.Exit.Host, &result.Exit.Port,
|
||||
@@ -584,7 +582,6 @@ func validateEnvironmentRebind(ctx context.Context, tx *sql.Tx, alias, exitID st
|
||||
FROM browser_env environment
|
||||
JOIN social_account account ON account.id = environment.account_id
|
||||
WHERE environment.alias = $1 AND account.status = 'paused'
|
||||
AND account.authorization_status = 'authorized'
|
||||
AND NOT environment.runtime_cleanup_pending
|
||||
FOR UPDATE OF environment, account`, alias).Scan(&accountID, &bindingVersion)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
@@ -688,21 +685,21 @@ func (s *Store) ActivateRuntime(ctx context.Context, alias, runtimeID string, bi
|
||||
return EnvironmentContext{}, errors.New("begin runtime activation")
|
||||
}
|
||||
defer tx.Rollback()
|
||||
var accountID, currentExitID, accountStatus, authorizationStatus string
|
||||
var accountID, currentExitID, accountStatus string
|
||||
var currentBindingVersion int64
|
||||
var cleanupPending bool
|
||||
err = tx.QueryRowContext(ctx, `
|
||||
SELECT account.account_id, environment.version, COALESCE(network.exit_id, ''), environment.runtime_cleanup_pending,
|
||||
account.status, account.authorization_status
|
||||
account.status
|
||||
FROM browser_env environment
|
||||
JOIN social_account account ON account.id = environment.account_id
|
||||
LEFT JOIN network_exit network ON network.id = environment.exit_id
|
||||
WHERE environment.alias = $1 FOR UPDATE OF environment, account`, alias).
|
||||
Scan(&accountID, ¤tBindingVersion, ¤tExitID, &cleanupPending, &accountStatus, &authorizationStatus)
|
||||
Scan(&accountID, ¤tBindingVersion, ¤tExitID, &cleanupPending, &accountStatus)
|
||||
if err != nil {
|
||||
return EnvironmentContext{}, rowError(err)
|
||||
}
|
||||
if cleanupPending || accountStatus != "active" || authorizationStatus != "authorized" ||
|
||||
if cleanupPending || accountStatus != "active" ||
|
||||
currentBindingVersion != bindingVersion || currentExitID != exitID {
|
||||
return EnvironmentContext{}, ErrConflict
|
||||
}
|
||||
|
||||
@@ -70,6 +70,6 @@ func TestMigration043ConsolidatedSchemaShape(t *testing.T) {
|
||||
// audit_event 任务列已删
|
||||
assertDatabaseCount(t, db, `SELECT count(*) FROM information_schema.columns WHERE table_schema = current_schema()
|
||||
AND table_name = 'audit_event' AND column_name IN ('confirmation_id','confirmation_version','attempt_id','task_id','runtime_instance_id')`, 0)
|
||||
// 统一登记表:1-38(除 36)、1017-1044、43 全部登记
|
||||
assertDatabaseCount(t, db, `SELECT count(*) FROM schema_migration`, 51)
|
||||
// 统一登记表:1-38(除 36)、1017-1045、43 全部登记
|
||||
assertDatabaseCount(t, db, `SELECT count(*) FROM schema_migration`, 52)
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ func TestUnifiedAccountMigration(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
assertDatabaseCount(t, db, `SELECT count(*) FROM schema_migration`, 51)
|
||||
assertDatabaseCount(t, db, `SELECT count(*) FROM schema_migration`, 52)
|
||||
assertDatabaseCount(t, db, `SELECT count(*) FROM information_schema.tables WHERE table_schema = current_schema()
|
||||
AND table_name IN ('social_account', 'browser_env', 'network_exit', 'environment_binding')`, 3)
|
||||
assertDatabaseCount(t, db, `SELECT count(*) FROM information_schema.tables WHERE table_schema = current_schema() AND table_name = 'browser_image'`, 0)
|
||||
@@ -42,7 +42,7 @@ func TestUnifiedAccountMigration(t *testing.T) {
|
||||
|
||||
store = openFullyMigratedHub(t, ctx, testURL)
|
||||
store.Close()
|
||||
assertDatabaseCount(t, db, `SELECT count(*) FROM schema_migration`, 51)
|
||||
assertDatabaseCount(t, db, `SELECT count(*) FROM schema_migration`, 52)
|
||||
})
|
||||
|
||||
t.Run("legacy migration 013 without account secrets is repaired forward", func(t *testing.T) {
|
||||
@@ -129,13 +129,13 @@ func TestUnifiedAccountMigration(t *testing.T) {
|
||||
assertDatabaseCount(t, db, `SELECT count(*) FROM audit_event`, 0)
|
||||
assertDatabaseCount(t, db, `SELECT count(*) FROM schema_migration WHERE version IN (3, 4, 5, 6, 7, 8, 9)`, 7)
|
||||
if _, err := db.Exec(`
|
||||
INSERT INTO social_account (account_id, credential_provider, credential_key, platform, platform_account_key, authorization_kind, authorization_status)
|
||||
VALUES ('mapped', 'os_keyring', 'creatorhub/mapped', 'mock', 'mapped', 'owned', 'authorized')`); err != nil {
|
||||
INSERT INTO social_account (account_id, credential_provider, credential_key, platform, platform_account_key)
|
||||
VALUES ('mapped', 'os_keyring', 'creatorhub/mapped', 'mock', 'mapped')`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := db.Exec(`
|
||||
INSERT INTO social_account (account_id, credential_provider, credential_key, platform, platform_account_key, authorization_kind, authorization_status)
|
||||
VALUES ('duplicate', 'os_keyring', 'creatorhub/duplicate', 'mock', 'mapped', 'owned', 'authorized')`); err == nil {
|
||||
INSERT INTO social_account (account_id, credential_provider, credential_key, platform, platform_account_key)
|
||||
VALUES ('duplicate', 'os_keyring', 'creatorhub/duplicate', 'mock', 'mapped')`); err == nil {
|
||||
t.Fatal("duplicate platform account must fail")
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
-- 移除「授权状态/授权种类」概念:产品已收敛为自有账号管理,前端无撤销入口,
|
||||
-- authorization_status 恒为 'authorized'、authorization_kind 恒为 'owned',列与判定全链路删除;
|
||||
-- 账号有效性由 status(active/paused)单一状态机表达。
|
||||
ALTER TABLE social_account DROP COLUMN IF EXISTS authorization_status;
|
||||
ALTER TABLE social_account DROP COLUMN IF EXISTS revoked_at;
|
||||
ALTER TABLE social_account DROP COLUMN IF EXISTS authorization_kind;
|
||||
@@ -173,6 +173,9 @@ var migration1043 string
|
||||
//go:embed migrations/1044_creator_account_metric_friend_count.sql
|
||||
var migration1044 string
|
||||
|
||||
//go:embed migrations/1045_drop_authorization_status.sql
|
||||
var migration1045 string
|
||||
|
||||
var (
|
||||
ErrConflict = errors.New("resource conflicts with existing state")
|
||||
ErrInvalid = errors.New("invalid environment input")
|
||||
@@ -312,7 +315,7 @@ func (s *Store) migrate(ctx context.Context) error {
|
||||
{1023, migration1023}, {1024, migration1024}, {1025, migration1025}, {1026, migration1026}, {1027, migration1027}, {1028, migration1028},
|
||||
{1029, migration1029}, {1030, migration1030}, {1031, migration1031}, {1032, migration1032}, {1033, migration1033}, {1034, migration1034},
|
||||
{1035, migration1035}, {1036, migration1036}, {1037, migration1037}, {1038, migration1038}, {1039, migration1039}, {1040, migration1040},
|
||||
{1041, migration1041}, {1042, migration1042}, {1043, migration1043}, {1044, migration1044},
|
||||
{1041, migration1041}, {1042, migration1042}, {1043, migration1043}, {1044, migration1044}, {1045, migration1045},
|
||||
{43, migration043}, {44, migration044}} {
|
||||
var applied bool
|
||||
if err := tx.QueryRowContext(ctx, `SELECT EXISTS (SELECT 1 FROM schema_migration WHERE version = $1)`, migration.version).Scan(&applied); err != nil {
|
||||
|
||||
@@ -293,9 +293,9 @@ func TestFingerprintSeedIsGloballyUnique(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := store.db.ExecContext(ctx, `
|
||||
INSERT INTO social_account (account_id, credential_provider, credential_key, platform, platform_account_key, authorization_kind, authorization_status)
|
||||
VALUES ('seed-account-a', 'os_keyring', 'creatorhub/seed-a', 'mock', 'seed-account-a', 'owned', 'authorized'),
|
||||
('seed-account-b', 'os_keyring', 'creatorhub/seed-b', 'mock', 'seed-account-b', 'owned', 'authorized')`); err != nil {
|
||||
INSERT INTO social_account (account_id, credential_provider, credential_key, platform, platform_account_key)
|
||||
VALUES ('seed-account-a', 'os_keyring', 'creatorhub/seed-a', 'mock', 'seed-account-a'),
|
||||
('seed-account-b', 'os_keyring', 'creatorhub/seed-b', 'mock', 'seed-account-b')`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
env := Env{Alias: "seed-environment-a", Name: "Seed A", Gateway: "gw-seed"}
|
||||
@@ -355,8 +355,8 @@ func TestHubWorkflow(t *testing.T) {
|
||||
}
|
||||
|
||||
if _, err := store.db.ExecContext(ctx, `
|
||||
INSERT INTO social_account (account_id, credential_provider, credential_key, platform, platform_account_key, authorization_kind, authorization_status)
|
||||
VALUES ('shop-owner', 'os_keyring', 'creatorhub/shop-owner', 'mock', 'shop-owner', 'owned', 'authorized')`); err != nil {
|
||||
INSERT INTO social_account (account_id, credential_provider, credential_key, platform, platform_account_key)
|
||||
VALUES ('shop-owner', 'os_keyring', 'creatorhub/shop-owner', 'mock', 'shop-owner')`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
env := Env{
|
||||
@@ -370,8 +370,8 @@ func TestHubWorkflow(t *testing.T) {
|
||||
t.Fatalf("expected idempotent environment reuse, created=%v err=%v", created, err)
|
||||
}
|
||||
if _, err := store.db.ExecContext(ctx, `
|
||||
INSERT INTO social_account (account_id, credential_provider, credential_key, platform, platform_account_key, authorization_kind, authorization_status)
|
||||
VALUES ('shop-owner-2', 'os_keyring', 'creatorhub/shop-owner-2', 'mock', 'shop-owner-2', 'owned', 'authorized')`); err != nil {
|
||||
INSERT INTO social_account (account_id, credential_provider, credential_key, platform, platform_account_key)
|
||||
VALUES ('shop-owner-2', 'os_keyring', 'creatorhub/shop-owner-2', 'mock', 'shop-owner-2')`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, err := store.CreateBoundEnv(ctx, Env{Alias: "shop-02", Name: "店铺二号", Gateway: "missing", Fingerprint: Fingerprint{Seed: 2}}, "shop-owner-2", ""); !errors.Is(err, ErrNotFound) {
|
||||
@@ -437,8 +437,8 @@ func TestNetworkExitBindingRuntimeAndAuditWorkflow(t *testing.T) {
|
||||
}
|
||||
if _, err := store.db.ExecContext(ctx, `
|
||||
INSERT INTO social_account
|
||||
(account_id, credential_provider, credential_key, platform, platform_account_key, authorization_kind, authorization_status)
|
||||
VALUES ('account-a', 'os_keyring', 'creatorhub/account-a', 'mock', 'account-a', 'owned', 'authorized')`); err != nil {
|
||||
(account_id, credential_provider, credential_key, platform, platform_account_key)
|
||||
VALUES ('account-a', 'os_keyring', 'creatorhub/account-a', 'mock', 'account-a')`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := store.CreateGateway(ctx, "gw-main", "http://127.0.0.1:8081", "unit-test-gateway-token"); err != nil {
|
||||
@@ -502,8 +502,8 @@ func TestNetworkExitBindingRuntimeAndAuditWorkflow(t *testing.T) {
|
||||
}
|
||||
if _, err := store.db.ExecContext(ctx, `
|
||||
INSERT INTO social_account
|
||||
(account_id, credential_provider, credential_key, platform, platform_account_key, authorization_kind, authorization_status)
|
||||
VALUES ('account-b', 'os_keyring', 'creatorhub/account-b', 'mock', 'account-b', 'owned', 'authorized');
|
||||
(account_id, credential_provider, credential_key, platform, platform_account_key)
|
||||
VALUES ('account-b', 'os_keyring', 'creatorhub/account-b', 'mock', 'account-b');
|
||||
INSERT INTO browser_env (alias, name, gateway_id, fingerprint, account_id)
|
||||
VALUES ('environment-b', '环境 B', (SELECT g.id FROM gateway g WHERE g.name = 'gw-main'), '{"seed":2}',
|
||||
(SELECT a.id FROM social_account a WHERE a.account_id = 'account-b'))`); err != nil {
|
||||
@@ -692,8 +692,8 @@ func TestRuntimeCleanupFenceAndBoundCreateReadiness(t *testing.T) {
|
||||
store := openFullyMigratedHub(t, ctx, isolatedDatabaseURL(t, databaseURL))
|
||||
t.Cleanup(func() { _ = store.Close() })
|
||||
if _, err := store.db.ExecContext(ctx, `
|
||||
INSERT INTO social_account (account_id, credential_provider, credential_key, platform, platform_account_key, authorization_kind, authorization_status)
|
||||
VALUES ('fence-a', 'os_keyring', 'creatorhub/fence-a', 'mock', 'fence-a', 'owned', 'authorized')`); err != nil {
|
||||
INSERT INTO social_account (account_id, credential_provider, credential_key, platform, platform_account_key)
|
||||
VALUES ('fence-a', 'os_keyring', 'creatorhub/fence-a', 'mock', 'fence-a')`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := store.CreateGateway(ctx, "gw-fence", "http://127.0.0.1:8081", "unit-test-gateway-token"); err != nil {
|
||||
|
||||
@@ -19,7 +19,6 @@ interface Row {
|
||||
platform_account_key: string;
|
||||
tags?: string[];
|
||||
runtime_status?: string;
|
||||
authorization_status?: string;
|
||||
work_count?: number;
|
||||
latest_published_at?: string | null;
|
||||
follower_count?: number | null;
|
||||
@@ -127,7 +126,7 @@ export default function AccountManagementList() {
|
||||
}
|
||||
}
|
||||
|
||||
// 扫码后核验:成功视为已登录,刷新列表拿到最新 authorization_status。
|
||||
// 扫码后核验:成功视为已登录,刷新列表同步最新账号状态。
|
||||
async function verifyLogin() {
|
||||
if (!loginTarget) return;
|
||||
setLoginBusy(true);
|
||||
@@ -212,12 +211,9 @@ export default function AccountManagementList() {
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
render: (_, account) => (
|
||||
<Flex>
|
||||
<Tag color={account.runtime_status === 'active' ? 'success' : 'default'}>
|
||||
{account.runtime_status === 'active' ? '启用' : '暂停'}
|
||||
</Tag>
|
||||
<Typography.Text type="secondary">{account.authorization_status === 'authorized' ? '已授权' : '已撤销'}</Typography.Text>
|
||||
</Flex>
|
||||
<Tag color={account.runtime_status === 'active' ? 'success' : 'default'}>
|
||||
{account.runtime_status === 'active' ? '启用' : '暂停'}
|
||||
</Tag>
|
||||
) },
|
||||
{
|
||||
title: '操作',
|
||||
|
||||
@@ -209,7 +209,6 @@ export default function Page() {
|
||||
<Card title="账号状态" extra={pauseResume}>
|
||||
<Flex vertical gap={16}>
|
||||
<Descriptions column={1} size="small">
|
||||
<Descriptions.Item label="授权">{account.authorization_status === 'authorized' ? '已授权' : '已撤销'}</Descriptions.Item>
|
||||
<Descriptions.Item label="运行">{`${account.runtime_status === 'active' ? '启用' : '暂停'} · 版本 ${account.version}`}</Descriptions.Item>
|
||||
<Descriptions.Item label="标签">{account.tags?.join('、') || '无'}</Descriptions.Item>
|
||||
<Descriptions.Item label="作品采集">
|
||||
|
||||
@@ -58,9 +58,8 @@ export function extractShareURL(value?: string): string {
|
||||
return candidates?.find((candidate) => platformForShareURL(candidate)) || candidates?.[0] || '';
|
||||
}
|
||||
|
||||
// 账号可恢复性判定(对齐归档版 accountReadiness):授权、绑定、出口健康、账号暂停四层。
|
||||
// 账号可恢复性判定(对齐归档版 accountReadiness):绑定、出口健康、账号暂停三层。
|
||||
export const readinessReasonText: Record<string, string> = {
|
||||
account_revoked: '授权已撤销',
|
||||
account_paused: '账号已暂停',
|
||||
binding_missing: '未绑定运行环境',
|
||||
network_exit_missing: '未绑定固定出口',
|
||||
@@ -80,8 +79,6 @@ export interface AccountReadiness {
|
||||
}
|
||||
|
||||
export function accountReadiness(account: any, binding: any, bindingError: any = null): AccountReadiness {
|
||||
if (account.authorization_status !== 'authorized')
|
||||
return { label: '授权已撤销', reason: 'account_revoked', canResume: false, ready: false };
|
||||
if (bindingError) return { label: '环境状态未知', reason: 'environment_unavailable', canResume: false, ready: false };
|
||||
if (!binding) return { label: '未绑定运行环境', reason: 'binding_missing', canResume: false, ready: false };
|
||||
let blocked = '';
|
||||
@@ -105,9 +102,5 @@ export function accountReadiness(account: any, binding: any, bindingError: any =
|
||||
}
|
||||
|
||||
export function isCollectionAccount(account: any): boolean {
|
||||
return (
|
||||
account.authorization_status === 'authorized' &&
|
||||
['normal', 'muted'].includes(account.business_status) &&
|
||||
account.login_status === 'logged_in'
|
||||
);
|
||||
return ['normal', 'muted'].includes(account.business_status) && account.login_status === 'logged_in';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user