2026-03-04 22:06:19 +08:00
|
|
|
"""Tests for pool account-state resolution helpers."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-03-20 16:50:59 +08:00
|
|
|
from src.services.provider.pool.account_state import (
|
2026-03-20 19:16:52 +08:00
|
|
|
build_provider_key_status_snapshot,
|
2026-03-20 16:50:59 +08:00
|
|
|
resolve_pool_account_state,
|
|
|
|
|
should_auto_remove_account_state,
|
|
|
|
|
)
|
2026-03-04 22:06:19 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_resolve_from_kiro_banned_metadata() -> None:
|
|
|
|
|
state = resolve_pool_account_state(
|
|
|
|
|
provider_type="kiro",
|
|
|
|
|
upstream_metadata={"kiro": {"is_banned": True, "ban_reason": "account suspended"}},
|
|
|
|
|
oauth_invalid_reason=None,
|
|
|
|
|
)
|
|
|
|
|
assert state.blocked is True
|
|
|
|
|
assert state.code == "account_banned"
|
|
|
|
|
assert state.label == "账号封禁"
|
|
|
|
|
assert state.reason == "account suspended"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_resolve_from_antigravity_forbidden_metadata() -> None:
|
|
|
|
|
state = resolve_pool_account_state(
|
|
|
|
|
provider_type="antigravity",
|
|
|
|
|
upstream_metadata={"antigravity": {"is_forbidden": True, "forbidden_reason": "403"}},
|
|
|
|
|
oauth_invalid_reason=None,
|
|
|
|
|
)
|
|
|
|
|
assert state.blocked is True
|
|
|
|
|
assert state.code == "account_forbidden"
|
|
|
|
|
assert state.label == "访问受限"
|
|
|
|
|
assert state.reason == "403"
|
|
|
|
|
|
|
|
|
|
|
2026-03-05 17:32:21 +08:00
|
|
|
def test_resolve_from_structured_oauth_reason_verification() -> None:
|
2026-03-04 22:06:19 +08:00
|
|
|
state = resolve_pool_account_state(
|
|
|
|
|
provider_type="codex",
|
|
|
|
|
upstream_metadata=None,
|
|
|
|
|
oauth_invalid_reason="[ACCOUNT_BLOCK] Google requires verification",
|
|
|
|
|
)
|
|
|
|
|
assert state.blocked is True
|
|
|
|
|
assert state.code == "account_blocked"
|
|
|
|
|
assert state.label == "账号异常"
|
|
|
|
|
assert state.reason == "Google requires verification"
|
|
|
|
|
|
|
|
|
|
|
2026-03-20 16:50:59 +08:00
|
|
|
def test_resolve_from_structured_oauth_reason_verification_chinese() -> None:
|
|
|
|
|
state = resolve_pool_account_state(
|
|
|
|
|
provider_type="codex",
|
|
|
|
|
upstream_metadata=None,
|
|
|
|
|
oauth_invalid_reason="[ACCOUNT_BLOCK] Google 要求验证账号",
|
|
|
|
|
)
|
|
|
|
|
assert state.blocked is True
|
|
|
|
|
assert state.code == "account_verification"
|
|
|
|
|
assert state.label == "需要验证"
|
|
|
|
|
assert state.reason == "Google 要求验证账号"
|
|
|
|
|
|
|
|
|
|
|
2026-03-05 17:32:21 +08:00
|
|
|
def test_resolve_from_structured_oauth_reason_suspended() -> None:
|
|
|
|
|
state = resolve_pool_account_state(
|
|
|
|
|
provider_type="codex",
|
|
|
|
|
upstream_metadata=None,
|
|
|
|
|
oauth_invalid_reason="[ACCOUNT_BLOCK] account suspended by admin",
|
|
|
|
|
)
|
|
|
|
|
assert state.blocked is True
|
|
|
|
|
assert state.code == "account_suspended"
|
|
|
|
|
assert state.label == "账号封禁"
|
|
|
|
|
assert state.reason == "account suspended by admin"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_resolve_from_keyword_oauth_reason_disabled() -> None:
|
2026-03-04 22:06:19 +08:00
|
|
|
state = resolve_pool_account_state(
|
|
|
|
|
provider_type=None,
|
|
|
|
|
upstream_metadata={},
|
|
|
|
|
oauth_invalid_reason="organization has been disabled by admin",
|
|
|
|
|
)
|
|
|
|
|
assert state.blocked is True
|
2026-03-05 17:32:21 +08:00
|
|
|
assert state.code == "account_disabled"
|
|
|
|
|
assert state.label == "账号停用"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_resolve_from_keyword_oauth_reason_verification() -> None:
|
|
|
|
|
state = resolve_pool_account_state(
|
|
|
|
|
provider_type=None,
|
|
|
|
|
upstream_metadata={},
|
|
|
|
|
oauth_invalid_reason="validation_required: please verify your identity",
|
|
|
|
|
)
|
|
|
|
|
assert state.blocked is True
|
|
|
|
|
assert state.code == "account_verification"
|
|
|
|
|
assert state.label == "需要验证"
|
2026-03-04 22:06:19 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_resolve_healthy_state() -> None:
|
|
|
|
|
state = resolve_pool_account_state(
|
|
|
|
|
provider_type="codex",
|
|
|
|
|
upstream_metadata={"codex": {"primary_used_percent": 30}},
|
|
|
|
|
oauth_invalid_reason="Token expired",
|
|
|
|
|
)
|
|
|
|
|
assert state.blocked is False
|
|
|
|
|
assert state.code is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_bare_forbidden_not_treated_as_account_block() -> None:
|
|
|
|
|
"""HTTP 403 'Forbidden' from token refresh should not be misclassified."""
|
|
|
|
|
state = resolve_pool_account_state(
|
|
|
|
|
provider_type="codex",
|
|
|
|
|
upstream_metadata={},
|
|
|
|
|
oauth_invalid_reason="Forbidden",
|
|
|
|
|
)
|
|
|
|
|
assert state.blocked is False
|
|
|
|
|
|
|
|
|
|
|
2026-03-05 17:32:21 +08:00
|
|
|
def test_kiro_oauth_reason_text_detected_as_suspended() -> None:
|
2026-03-04 22:06:19 +08:00
|
|
|
state = resolve_pool_account_state(
|
|
|
|
|
provider_type="kiro",
|
|
|
|
|
upstream_metadata={},
|
|
|
|
|
oauth_invalid_reason="账户已封禁: Terms of Service violation",
|
|
|
|
|
)
|
|
|
|
|
assert state.blocked is True
|
2026-03-05 17:32:21 +08:00
|
|
|
assert state.code == "account_suspended"
|
|
|
|
|
assert state.label == "账号封禁"
|
2026-03-04 22:06:19 +08:00
|
|
|
|
|
|
|
|
|
2026-03-05 17:32:21 +08:00
|
|
|
def test_antigravity_oauth_reason_text_detected_as_disabled() -> None:
|
2026-03-04 22:06:19 +08:00
|
|
|
state = resolve_pool_account_state(
|
|
|
|
|
provider_type="antigravity",
|
|
|
|
|
upstream_metadata={},
|
|
|
|
|
oauth_invalid_reason="账户访问被禁止: 403 Forbidden",
|
|
|
|
|
)
|
|
|
|
|
assert state.blocked is True
|
2026-03-05 17:32:21 +08:00
|
|
|
assert state.code == "account_disabled"
|
|
|
|
|
assert state.label == "账号停用"
|
2026-03-06 13:13:01 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_resolve_from_structured_oauth_reason_workspace_deactivated() -> None:
|
|
|
|
|
state = resolve_pool_account_state(
|
|
|
|
|
provider_type="codex",
|
|
|
|
|
upstream_metadata=None,
|
|
|
|
|
oauth_invalid_reason="[ACCOUNT_BLOCK] 工作区已停用 (deactivated_workspace)",
|
|
|
|
|
)
|
|
|
|
|
assert state.blocked is True
|
|
|
|
|
assert state.code == "workspace_deactivated"
|
|
|
|
|
assert state.label == "工作区停用"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_resolve_from_structured_oauth_reason_token_invalidated() -> None:
|
|
|
|
|
state = resolve_pool_account_state(
|
|
|
|
|
provider_type="codex",
|
|
|
|
|
upstream_metadata=None,
|
|
|
|
|
oauth_invalid_reason="[OAUTH_EXPIRED] Your authentication token has been invalidated. Please try signing in again.",
|
|
|
|
|
)
|
|
|
|
|
assert state.blocked is True
|
|
|
|
|
assert state.code == "oauth_expired"
|
|
|
|
|
assert state.label == "Token 失效"
|
2026-03-16 01:12:35 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_refresh_failed_prefix_does_not_block_even_with_scary_keywords() -> None:
|
|
|
|
|
state = resolve_pool_account_state(
|
|
|
|
|
provider_type="codex",
|
|
|
|
|
upstream_metadata=None,
|
|
|
|
|
oauth_invalid_reason="[REFRESH_FAILED] Token 续期失败 (401): account_deactivated",
|
|
|
|
|
)
|
|
|
|
|
assert state.blocked is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_request_failed_prefix_does_not_block() -> None:
|
|
|
|
|
state = resolve_pool_account_state(
|
|
|
|
|
provider_type="codex",
|
|
|
|
|
upstream_metadata=None,
|
|
|
|
|
oauth_invalid_reason="[REQUEST_FAILED] Codex 账户访问受限 (403)",
|
|
|
|
|
)
|
|
|
|
|
assert state.blocked is False
|
2026-03-20 16:50:59 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_auto_remove_state_excludes_token_expired_and_verification() -> None:
|
|
|
|
|
expired = resolve_pool_account_state(
|
|
|
|
|
provider_type="codex",
|
|
|
|
|
upstream_metadata=None,
|
|
|
|
|
oauth_invalid_reason="[OAUTH_EXPIRED] token invalidated",
|
|
|
|
|
)
|
|
|
|
|
verification = resolve_pool_account_state(
|
|
|
|
|
provider_type="codex",
|
|
|
|
|
upstream_metadata=None,
|
|
|
|
|
oauth_invalid_reason="[ACCOUNT_BLOCK] Google 要求验证账号",
|
|
|
|
|
)
|
|
|
|
|
disabled = resolve_pool_account_state(
|
|
|
|
|
provider_type="codex",
|
|
|
|
|
upstream_metadata=None,
|
|
|
|
|
oauth_invalid_reason="[ACCOUNT_BLOCK] account has been deactivated",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert should_auto_remove_account_state(expired) is False
|
|
|
|
|
assert should_auto_remove_account_state(verification) is False
|
|
|
|
|
assert should_auto_remove_account_state(disabled) is True
|
2026-03-20 19:16:52 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_provider_key_status_snapshot_separates_account_block_from_oauth_state() -> None:
|
|
|
|
|
snapshot = build_provider_key_status_snapshot(
|
|
|
|
|
auth_type="oauth",
|
|
|
|
|
oauth_expires_at=2_000_000_000,
|
|
|
|
|
oauth_invalid_at=1_900_000_000,
|
|
|
|
|
oauth_invalid_reason="[ACCOUNT_BLOCK] 工作区已停用 (deactivated_workspace)",
|
|
|
|
|
provider_type="codex",
|
|
|
|
|
upstream_metadata=None,
|
|
|
|
|
now_ts=1_800_000_000,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert snapshot.account.blocked is True
|
|
|
|
|
assert snapshot.account.code == "workspace_deactivated"
|
|
|
|
|
assert snapshot.account.label == "工作区停用"
|
|
|
|
|
assert snapshot.oauth.code == "valid"
|
|
|
|
|
assert snapshot.oauth.requires_reauth is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_provider_key_status_snapshot_keeps_refresh_failure_visible_beside_account_block() -> (
|
|
|
|
|
None
|
|
|
|
|
):
|
|
|
|
|
snapshot = build_provider_key_status_snapshot(
|
|
|
|
|
auth_type="oauth",
|
|
|
|
|
oauth_expires_at=2_000_000_000,
|
|
|
|
|
oauth_invalid_at=1_900_000_000,
|
|
|
|
|
oauth_invalid_reason=(
|
|
|
|
|
"[ACCOUNT_BLOCK] 工作区已停用 (deactivated_workspace)\n"
|
|
|
|
|
"[REFRESH_FAILED] Token 续期失败 (400): refresh_token_reused"
|
|
|
|
|
),
|
|
|
|
|
provider_type="codex",
|
|
|
|
|
upstream_metadata=None,
|
|
|
|
|
now_ts=1_800_000_000,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert snapshot.account.blocked is True
|
|
|
|
|
assert snapshot.account.code == "workspace_deactivated"
|
|
|
|
|
assert snapshot.oauth.code == "invalid"
|
|
|
|
|
assert snapshot.oauth.label == "已失效"
|
|
|
|
|
assert snapshot.oauth.reason == "Token 续期失败 (400): refresh_token_reused"
|
|
|
|
|
assert snapshot.oauth.requires_reauth is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_provider_key_status_snapshot_marks_quota_exhausted() -> None:
|
|
|
|
|
snapshot = build_provider_key_status_snapshot(
|
|
|
|
|
auth_type="oauth",
|
|
|
|
|
oauth_expires_at=2_000_000_000,
|
|
|
|
|
oauth_invalid_at=None,
|
|
|
|
|
oauth_invalid_reason=None,
|
|
|
|
|
provider_type="codex",
|
|
|
|
|
upstream_metadata={
|
|
|
|
|
"codex": {
|
|
|
|
|
"primary_used_percent": 100.0,
|
|
|
|
|
"secondary_used_percent": 20.0,
|
|
|
|
|
"updated_at": 1_800_000_000,
|
|
|
|
|
"plan_type": "team",
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
now_ts=1_800_000_000,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert snapshot.quota.code == "exhausted"
|
|
|
|
|
assert snapshot.quota.exhausted is True
|
|
|
|
|
assert snapshot.quota.reason == "Codex 周限额剩余 0%"
|