2026-03-12 01:11:35 +08:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import sys
|
|
|
|
|
import types
|
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
|
from types import SimpleNamespace
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
from src.services.provider import auth as module
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _FakeQuery:
|
|
|
|
|
def __init__(self, row: Any | None) -> None:
|
|
|
|
|
self._row = row
|
|
|
|
|
|
|
|
|
|
def filter(self, *_args: Any, **_kwargs: Any) -> "_FakeQuery":
|
|
|
|
|
return self
|
|
|
|
|
|
|
|
|
|
def first(self) -> Any | None:
|
|
|
|
|
return self._row
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _FakeDB:
|
|
|
|
|
def __init__(self, row: Any | None) -> None:
|
|
|
|
|
self.row = row
|
|
|
|
|
self.committed = False
|
|
|
|
|
|
|
|
|
|
def query(self, _model: Any) -> _FakeQuery:
|
|
|
|
|
return _FakeQuery(self.row)
|
|
|
|
|
|
|
|
|
|
def commit(self) -> None:
|
|
|
|
|
self.committed = True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _FakeSessionCtx:
|
|
|
|
|
def __init__(self, db: _FakeDB) -> None:
|
|
|
|
|
self.db = db
|
|
|
|
|
|
|
|
|
|
def __enter__(self) -> _FakeDB:
|
|
|
|
|
return self.db
|
|
|
|
|
|
|
|
|
|
def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> bool:
|
|
|
|
|
_ = exc_type, exc, tb
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _install_module(monkeypatch: pytest.MonkeyPatch, name: str, attrs: dict[str, Any]) -> None:
|
|
|
|
|
fake_module = types.ModuleType(name)
|
|
|
|
|
for key, value in attrs.items():
|
|
|
|
|
setattr(fake_module, key, value)
|
|
|
|
|
monkeypatch.setitem(sys.modules, name, fake_module)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_persist_refreshed_token_detached_key_does_not_raise(
|
|
|
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
|
|
|
) -> None:
|
|
|
|
|
key = SimpleNamespace(
|
|
|
|
|
id="key-1",
|
|
|
|
|
api_key="old-api",
|
|
|
|
|
auth_config="old-config",
|
|
|
|
|
oauth_invalid_at=datetime.now(timezone.utc),
|
|
|
|
|
oauth_invalid_reason="[REFRESH_FAILED] stale",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
module, "object_session", lambda _key: (_ for _ in ()).throw(RuntimeError())
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(module.crypto_service, "encrypt", lambda value: f"enc:{value}")
|
|
|
|
|
|
|
|
|
|
module._persist_refreshed_token(key, "new-token", {"refresh_token": "rt-2"})
|
|
|
|
|
|
|
|
|
|
assert key.api_key == "enc:new-token"
|
|
|
|
|
assert key.auth_config == 'enc:{"refresh_token": "rt-2"}'
|
|
|
|
|
assert key.oauth_invalid_at is None
|
|
|
|
|
assert key.oauth_invalid_reason is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_mark_refresh_token_invalid_persists_detached_key(
|
|
|
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
|
|
|
) -> None:
|
|
|
|
|
key = SimpleNamespace(id="key-1")
|
|
|
|
|
row = SimpleNamespace(id="key-1", oauth_invalid_at=None, oauth_invalid_reason=None)
|
|
|
|
|
fake_db = _FakeDB(row)
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
module, "object_session", lambda _key: (_ for _ in ()).throw(RuntimeError())
|
|
|
|
|
)
|
|
|
|
|
_install_module(
|
|
|
|
|
monkeypatch, "src.database", {"create_session": lambda: _FakeSessionCtx(fake_db)}
|
|
|
|
|
)
|
|
|
|
|
_install_module(
|
|
|
|
|
monkeypatch,
|
|
|
|
|
"src.models.database",
|
|
|
|
|
{"ProviderAPIKey": type("ProviderAPIKey", (), {"id": "id"})},
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
module._mark_refresh_token_invalid(
|
|
|
|
|
key,
|
|
|
|
|
401,
|
|
|
|
|
'{"error": {"code": "refresh_token_reused", "message": "used"}}',
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert fake_db.committed is True
|
|
|
|
|
assert key.oauth_invalid_at is not None
|
|
|
|
|
assert row.oauth_invalid_at is not None
|
|
|
|
|
assert str(key.oauth_invalid_reason).startswith("[REFRESH_FAILED] Token 续期失败 (401)")
|
|
|
|
|
assert "refresh_token_reused" in str(row.oauth_invalid_reason)
|
2026-03-15 16:56:58 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_account_block_token_invalidated_is_refresh_recoverable() -> None:
|
|
|
|
|
from src.services.provider.oauth_token import is_account_level_block
|
|
|
|
|
|
|
|
|
|
assert (
|
|
|
|
|
is_account_level_block(
|
|
|
|
|
"[ACCOUNT_BLOCK] Authentication token has been invalidated. Please sign in again."
|
|
|
|
|
)
|
|
|
|
|
is False
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_persist_refreshed_token_clears_legacy_token_invalidated_account_block(
|
|
|
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
|
|
|
) -> None:
|
|
|
|
|
key = SimpleNamespace(
|
|
|
|
|
id="key-1",
|
|
|
|
|
api_key="old-api",
|
|
|
|
|
auth_config="old-config",
|
|
|
|
|
oauth_invalid_at=datetime.now(timezone.utc),
|
|
|
|
|
oauth_invalid_reason=(
|
|
|
|
|
"[ACCOUNT_BLOCK] Authentication token has been invalidated. Please sign in again."
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
monkeypatch.setattr(
|
|
|
|
|
module, "object_session", lambda _key: (_ for _ in ()).throw(RuntimeError())
|
|
|
|
|
)
|
|
|
|
|
monkeypatch.setattr(module.crypto_service, "encrypt", lambda value: f"enc:{value}")
|
|
|
|
|
|
|
|
|
|
module._persist_refreshed_token(key, "new-token", {"refresh_token": "rt-2"})
|
|
|
|
|
|
|
|
|
|
assert key.api_key == "enc:new-token"
|
|
|
|
|
assert key.auth_config == 'enc:{"refresh_token": "rt-2"}'
|
|
|
|
|
assert key.oauth_invalid_at is None
|
|
|
|
|
assert key.oauth_invalid_reason is None
|