test_sub2api_client.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. import json
  2. from urllib.error import HTTPError
  3. import pytest
  4. from ops.sub2api_client import Sub2ApiClient
  5. class FakeResponse:
  6. def __init__(self, payload: dict, status: int = 200):
  7. self.payload = payload
  8. self.status = status
  9. def read(self) -> bytes:
  10. return json.dumps(self.payload).encode("utf-8")
  11. def __enter__(self):
  12. return self
  13. def __exit__(self, exc_type, exc, tb):
  14. return False
  15. class FakeHttpError(HTTPError):
  16. def __init__(self, url: str, code: int, payload: dict):
  17. super().__init__(url, code, "error", hdrs=None, fp=None)
  18. self._payload = payload
  19. def read(self) -> bytes:
  20. return json.dumps(self._payload).encode("utf-8")
  21. def _decode_body(request) -> dict:
  22. data = request.data
  23. assert data is not None
  24. return json.loads(data.decode("utf-8"))
  25. def test_login_caches_jwt(monkeypatch):
  26. calls: list[tuple[str, str, dict | None]] = []
  27. def fake_urlopen(request, timeout=0):
  28. body = _decode_body(request) if request.data else None
  29. calls.append((request.get_method(), request.full_url, body))
  30. if request.full_url.endswith("/api/v1/auth/login"):
  31. return FakeResponse({"code": 0, "data": {"access_token": "jwt-1"}})
  32. return FakeResponse({"code": 0, "data": {"items": [], "total": 0, "page": 1, "page_size": 100, "pages": 1}})
  33. monkeypatch.setattr("ops.sub2api_client.urlopen", fake_urlopen)
  34. client = Sub2ApiClient("http://example.test", "admin@example.com", "secret")
  35. first = client._ensure_jwt()
  36. second = client._ensure_jwt()
  37. assert first == "jwt-1"
  38. assert second == "jwt-1"
  39. assert calls == [
  40. (
  41. "POST",
  42. "http://example.test/api/v1/auth/login",
  43. {"email": "admin@example.com", "password": "secret"},
  44. )
  45. ]
  46. def test_create_account_sends_correct_payload(monkeypatch):
  47. seen: dict[str, object] = {}
  48. def fake_urlopen(request, timeout=0):
  49. if request.full_url.endswith("/api/v1/auth/login"):
  50. return FakeResponse({"code": 0, "data": {"access_token": "jwt-1"}})
  51. seen["headers"] = dict(request.header_items())
  52. seen["body"] = _decode_body(request)
  53. return FakeResponse({"code": 0, "data": {"id": 9, "name": "demo"}})
  54. monkeypatch.setattr("ops.sub2api_client.urlopen", fake_urlopen)
  55. client = Sub2ApiClient("http://example.test", "admin@example.com", "secret")
  56. payload = client.create_account(
  57. name="demo",
  58. credentials={"refresh_token": "rt", "access_token": "at", "email": "demo@example.com"},
  59. platform="openai",
  60. type="oauth",
  61. concurrency=2,
  62. priority=5,
  63. )
  64. assert payload == {"id": 9, "name": "demo"}
  65. assert seen["body"] == {
  66. "name": "demo",
  67. "platform": "openai",
  68. "type": "oauth",
  69. "credentials": {"refresh_token": "rt", "access_token": "at", "email": "demo@example.com"},
  70. "concurrency": 2,
  71. "priority": 5,
  72. }
  73. assert seen["headers"]["Authorization"] == "Bearer jwt-1"
  74. def test_batch_create(monkeypatch):
  75. def fake_urlopen(request, timeout=0):
  76. if request.full_url.endswith("/api/v1/auth/login"):
  77. return FakeResponse({"code": 0, "data": {"access_token": "jwt-1"}})
  78. assert request.full_url.endswith("/api/v1/admin/accounts/batch")
  79. assert _decode_body(request) == {"accounts": [{"name": "a"}, {"name": "b"}]}
  80. return FakeResponse({"code": 0, "data": {"success": 2, "failed": 0, "results": []}})
  81. monkeypatch.setattr("ops.sub2api_client.urlopen", fake_urlopen)
  82. client = Sub2ApiClient("http://example.test", "admin@example.com", "secret")
  83. assert client.batch_create_accounts([{"name": "a"}, {"name": "b"}]) == {"success": 2, "failed": 0, "results": []}
  84. def test_list_accounts_with_pagination(monkeypatch):
  85. def fake_urlopen(request, timeout=0):
  86. if request.full_url.endswith("/api/v1/auth/login"):
  87. return FakeResponse({"code": 0, "data": {"access_token": "jwt-1"}})
  88. assert request.full_url == (
  89. "http://example.test/api/v1/admin/accounts?platform=openai&page=2&page_size=50"
  90. )
  91. return FakeResponse(
  92. {"code": 0, "data": {"items": [{"id": 1}], "total": 1, "page": 2, "page_size": 50, "pages": 1}}
  93. )
  94. monkeypatch.setattr("ops.sub2api_client.urlopen", fake_urlopen)
  95. client = Sub2ApiClient("http://example.test", "admin@example.com", "secret")
  96. payload = client.list_accounts(page=2, page_size=50)
  97. assert payload["items"] == [{"id": 1}]
  98. assert payload["page"] == 2
  99. def test_delete_account(monkeypatch):
  100. def fake_urlopen(request, timeout=0):
  101. if request.full_url.endswith("/api/v1/auth/login"):
  102. return FakeResponse({"code": 0, "data": {"access_token": "jwt-1"}})
  103. assert request.get_method() == "DELETE"
  104. assert request.full_url.endswith("/api/v1/admin/accounts/7")
  105. return FakeResponse({"code": 0, "data": {"message": "Account deleted successfully"}})
  106. monkeypatch.setattr("ops.sub2api_client.urlopen", fake_urlopen)
  107. client = Sub2ApiClient("http://example.test", "admin@example.com", "secret")
  108. assert client.delete_account(7) is True
  109. def test_refresh_account(monkeypatch):
  110. def fake_urlopen(request, timeout=0):
  111. if request.full_url.endswith("/api/v1/auth/login"):
  112. return FakeResponse({"code": 0, "data": {"access_token": "jwt-1"}})
  113. assert request.full_url.endswith("/api/v1/admin/accounts/3/refresh")
  114. return FakeResponse({"code": 0, "data": {"message": "refresh queued"}})
  115. monkeypatch.setattr("ops.sub2api_client.urlopen", fake_urlopen)
  116. client = Sub2ApiClient("http://example.test", "admin@example.com", "secret")
  117. assert client.refresh_account(3) == {"message": "refresh queued"}
  118. def test_health_check(monkeypatch):
  119. def fake_urlopen(request, timeout=0):
  120. assert request.full_url == "http://example.test/health"
  121. return FakeResponse({"status": "ok"})
  122. monkeypatch.setattr("ops.sub2api_client.urlopen", fake_urlopen)
  123. client = Sub2ApiClient("http://example.test", "admin@example.com", "secret")
  124. assert client.health_check() is True
  125. def test_401_retry_refreshes_jwt(monkeypatch):
  126. calls: list[str] = []
  127. def fake_urlopen(request, timeout=0):
  128. calls.append(request.full_url)
  129. if request.full_url.endswith("/api/v1/auth/login"):
  130. token = "jwt-1" if calls.count(request.full_url) == 1 else "jwt-2"
  131. return FakeResponse({"code": 0, "data": {"access_token": token}})
  132. auth_header = dict(request.header_items()).get("Authorization")
  133. if auth_header == "Bearer jwt-1":
  134. raise FakeHttpError(request.full_url, 401, {"code": 401, "message": "unauthorized"})
  135. return FakeResponse({"code": 0, "data": {"id": 42}})
  136. monkeypatch.setattr("ops.sub2api_client.urlopen", fake_urlopen)
  137. client = Sub2ApiClient("http://example.test", "admin@example.com", "secret")
  138. assert client.get_account(42) == {"id": 42}
  139. assert calls == [
  140. "http://example.test/api/v1/auth/login",
  141. "http://example.test/api/v1/admin/accounts/42",
  142. "http://example.test/api/v1/auth/login",
  143. "http://example.test/api/v1/admin/accounts/42",
  144. ]
  145. def test_api_key_mode_skips_login(monkeypatch):
  146. seen_headers: list[dict[str, str]] = []
  147. def fake_urlopen(request, timeout=0):
  148. seen_headers.append(dict(request.header_items()))
  149. return FakeResponse({"code": 0, "data": {"items": [], "total": 0, "page": 1, "page_size": 100, "pages": 1}})
  150. monkeypatch.setattr("ops.sub2api_client.urlopen", fake_urlopen)
  151. client = Sub2ApiClient("http://example.test", "", "", api_key="key-123")
  152. client.list_accounts()
  153. assert len(seen_headers) == 1
  154. headers = {key.lower(): value for key, value in seen_headers[0].items()}
  155. assert headers["x-api-key"] == "key-123"
  156. assert "authorization" not in headers