| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257 |
- """Tests for ops.rotate API-only single-pool logic."""
- from __future__ import annotations
- import json
- from pathlib import Path
- from datetime import datetime, timedelta
- import pytest
- from ops.rotate import RotateResult, rotate_once
- from ops.rotate_probe import classify_status_message
- class FakeClient:
- def __init__(self, files: list[dict[str, object]], *, healthy: bool = True):
- self.files = [dict(item) for item in files]
- self.healthy = healthy
- self.deleted: list[str] = []
- def health_check(self) -> bool:
- return self.healthy
- def list_auth_files(self) -> list[dict[str, object]]:
- return [dict(item) for item in self.files]
- def delete_auth_file(self, name: str) -> bool:
- self.deleted.append(name)
- self.files = [item for item in self.files if str(item.get("name")) != name]
- return True
- @pytest.fixture(autouse=True)
- def _stub_runtime_reconcile(monkeypatch):
- monkeypatch.setattr("ops.rotate._maybe_reconcile_cpa_runtime", lambda **kwargs: None)
- def test_classify_unauthorized_returns_401() -> None:
- assert classify_status_message("unauthorized") == 401
- assert classify_status_message("Token invalidated by provider") == 401
- def test_classify_quota_returns_429() -> None:
- msg = json.dumps({"error": {"type": "usage_limit_reached", "message": "weekly quota exceeded"}})
- assert classify_status_message(msg) == 429
- def test_classify_empty_returns_200() -> None:
- assert classify_status_message("") == 200
- assert classify_status_message(None) == 200 # type: ignore[arg-type]
- def test_rotate_deletes_401(tmp_path: Path) -> None:
- pool_dir = tmp_path / "pool"
- pool_dir.mkdir()
- target = pool_dir / "bad@example.com.json"
- target.write_text('{"email": "bad@example.com"}', encoding="utf-8")
- client = FakeClient([{"name": "bad@example.com.json", "status_message": "unauthorized", "status": "error"}])
- result = rotate_once(pool_dir=pool_dir, client=client)
- assert result.deleted_401 == 1
- assert result.deleted_429 == 0
- assert result.main_pool_before == 1
- assert result.main_pool_after == 0
- assert client.deleted == ["bad@example.com.json"]
- assert not target.exists()
- def test_rotate_keeps_429(tmp_path: Path) -> None:
- pool_dir = tmp_path / "pool"
- pool_dir.mkdir()
- target = pool_dir / "quota@example.com.json"
- target.write_text('{"email": "quota@example.com"}', encoding="utf-8")
- client = FakeClient(
- [
- {
- "name": "quota@example.com.json",
- "status_message": json.dumps({"error": {"type": "usage_limit_reached", "message": "quota"}}),
- "status": "error",
- }
- ]
- )
- result = rotate_once(pool_dir=pool_dir, client=client)
- assert result.deleted_401 == 0
- assert result.deleted_429 == 0
- assert client.deleted == []
- assert target.exists()
- def test_rotate_deletes_deactivated(tmp_path: Path) -> None:
- pool_dir = tmp_path / "pool"
- pool_dir.mkdir()
- target = pool_dir / "dead@example.com.json"
- target.write_text('{"email": "dead@example.com"}', encoding="utf-8")
- client = FakeClient(
- [
- {
- "name": "dead@example.com.json",
- "status_message": json.dumps({"error": {"type": "account_deactivated", "message": "has been deactivated"}}),
- "status": "error",
- }
- ]
- )
- result = rotate_once(pool_dir=pool_dir, client=client)
- assert result.deleted_401 == 1
- assert client.deleted == ["dead@example.com.json"]
- assert not target.exists()
- def test_rotate_keeps_transport_error(tmp_path: Path) -> None:
- pool_dir = tmp_path / "pool"
- pool_dir.mkdir()
- target = pool_dir / "retry@example.com.json"
- target.write_text('{"email": "retry@example.com"}', encoding="utf-8")
- client = FakeClient(
- [{"name": "retry@example.com.json", "status_message": 'Post "https://chatgpt.com/backend-api/codex/responses": EOF', "status": "error"}]
- )
- result = rotate_once(pool_dir=pool_dir, client=client)
- assert result.deleted_401 == 0
- assert result.deleted_429 == 0
- assert target.exists()
- def test_rotate_keeps_healthy(tmp_path: Path) -> None:
- pool_dir = tmp_path / "pool"
- pool_dir.mkdir()
- target = pool_dir / "ok@example.com.json"
- target.write_text('{"email": "ok@example.com"}', encoding="utf-8")
- client = FakeClient([{"name": "ok@example.com.json", "status_message": "", "status": "active"}])
- result = rotate_once(pool_dir=pool_dir, client=client)
- assert result.deleted_401 == 0
- assert result.deleted_429 == 0
- assert result.quota_probed == 0
- assert target.exists()
- def test_rotate_quota_probe_detects_401(monkeypatch, tmp_path: Path) -> None:
- pool_dir = tmp_path / "pool"
- pool_dir.mkdir()
- target = pool_dir / "probe401@example.com.json"
- target.write_text('{"email": "probe401@example.com"}', encoding="utf-8")
- class FakeCpaClient(FakeClient):
- def _resolve_key(self): # noqa: ANN202
- return "test-key"
- client = FakeCpaClient(
- [
- {
- "name": "probe401@example.com.json",
- "status_message": 'Post "https://chatgpt.com/backend-api/codex/responses": EOF',
- "status": "error",
- "provider": "codex",
- "auth_index": "auth-1",
- "id_token": {"chatgpt_account_id": "acct-1"},
- }
- ]
- )
- monkeypatch.setattr(
- "ops.rotate._collect_quota_probe_results",
- lambda entries, **kwargs: (
- {"probe401@example.com.json": (401, "invalidated", False)},
- {"probed": 1, "probe_401": 1, "probe_429": 0, "probe_skipped": 0},
- ),
- )
- result = rotate_once(pool_dir=pool_dir, client=client)
- assert result.quota_probed == 1
- assert result.deleted_401 == 1
- assert not target.exists()
- def test_rotate_cpa_unreachable_returns_empty(tmp_path: Path) -> None:
- client = FakeClient([], healthy=False)
- result = rotate_once(pool_dir=tmp_path / "pool", client=client)
- assert result == RotateResult()
- def test_rotate_result_fields_correct(tmp_path: Path) -> None:
- pool_dir = tmp_path / "pool"
- pool_dir.mkdir()
- for name in ["bad@example.com.json", "quota@example.com.json", "ok@example.com.json"]:
- (pool_dir / name).write_text("{}", encoding="utf-8")
- client = FakeClient(
- [
- {"name": "bad@example.com.json", "status_message": "unauthorized", "status": "error"},
- {"name": "quota@example.com.json", "status_message": json.dumps({"error": {"type": "usage_limit_reached", "message": "quota"}}), "status": "error"},
- {"name": "ok@example.com.json", "status_message": "", "status": "active"},
- ]
- )
- result = rotate_once(pool_dir=pool_dir, client=client)
- assert result.main_pool_before == 3
- assert result.deleted_401 == 1
- assert result.deleted_429 == 0
- assert result.main_pool_after == 2
- def test_rotate_skips_fresh_accounts_during_grace_period(monkeypatch, tmp_path: Path) -> None:
- pool_dir = tmp_path / "pool"
- pool_dir.mkdir()
- target = pool_dir / "fresh@example.com.json"
- target.write_text(
- json.dumps(
- {
- "email": "fresh@example.com",
- "created_at": datetime.now().astimezone().isoformat(timespec="seconds"),
- }
- ),
- encoding="utf-8",
- )
- class FakeCpaClient(FakeClient):
- def _resolve_key(self): # noqa: ANN202
- return "test-key"
- client = FakeCpaClient(
- [
- {
- "name": "fresh@example.com.json",
- "status_message": "",
- "status": "active",
- "provider": "codex",
- "auth_index": "auth-1",
- "id_token": {"chatgpt_account_id": "acct-1"},
- }
- ]
- )
- monkeypatch.setattr(
- "ops.rotate._collect_quota_probe_results",
- lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("fresh accounts should skip rotate probing during grace period")),
- )
- result = rotate_once(
- pool_dir=pool_dir,
- client=client,
- fresh_grace_seconds=600,
- )
- assert result.deleted_401 == 0
- assert result.quota_probed == 0
- assert result.quota_probe_skipped == 1
- assert target.exists()
|