test_auth_identity_service.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. from pathlib import Path
  2. from tests.conftest import (
  3. build_fastapi_test_client,
  4. build_postgres_database_url,
  5. build_postgres_engine,
  6. prepare_known_service_import,
  7. )
  8. class FakeRedis:
  9. def __init__(self) -> None:
  10. self.values: dict[str, object] = {}
  11. def set(self, key: str, value: object, ex: int | None = None) -> bool:
  12. self.values[key] = value
  13. return True
  14. def get(self, key: str) -> object | None:
  15. return self.values.get(key)
  16. def exists(self, key: str) -> int:
  17. return 1 if key in self.values else 0
  18. def test_identity_bootstraps_demo_user_for_frontend_login(
  19. tmp_path: Path,
  20. monkeypatch,
  21. ) -> None:
  22. prepare_known_service_import("auth-service")
  23. from app.bootstrap.app import create_app
  24. from app.db.models import Base
  25. database_url = build_postgres_database_url(tmp_path, "demo-auth")
  26. monkeypatch.setenv("AGENT_PLATFORM_DATABASE_URL", database_url)
  27. engine = build_postgres_engine(database_url)
  28. Base.metadata.create_all(engine)
  29. app = create_app()
  30. client = build_fastapi_test_client(app)
  31. response = client.post(
  32. "/identity/auth/login",
  33. json={"username": "demo-user", "password": "demo-password"},
  34. )
  35. assert response.status_code == 200
  36. payload = response.json()
  37. assert payload["data"]["accessToken"]
  38. assert payload["data"]["user"]["username"] == "demo-user"
  39. def test_identity_post_contract_supports_login_permissions_and_api_keys(
  40. tmp_path: Path,
  41. monkeypatch,
  42. ) -> None:
  43. prepare_known_service_import("auth-service")
  44. from app.bootstrap.app import create_app
  45. from app.db.models import Base
  46. from app.domain.repositories import RoleAssignmentRepository, RoleRepository, UserRepository
  47. from app.infrastructure.passwords import hash_password
  48. from core_db import create_session_factory
  49. database_url = build_postgres_database_url(tmp_path, "auth")
  50. monkeypatch.setenv("AGENT_PLATFORM_DATABASE_URL", database_url)
  51. engine = build_postgres_engine(database_url)
  52. Base.metadata.create_all(engine)
  53. app = create_app()
  54. app.state.session_factory = create_session_factory(engine)
  55. client = build_fastapi_test_client(app)
  56. session_factory = create_session_factory(engine)
  57. with session_factory() as db:
  58. user = UserRepository(db).create(
  59. username="demo",
  60. password_hash=hash_password("super-secret"),
  61. display_name="Demo User",
  62. email="demo@example.com",
  63. metadata_json={"source": "test"},
  64. )
  65. role = RoleRepository(db).create(
  66. code="admin",
  67. name="Admin",
  68. description="Platform admin",
  69. permissions_json=[],
  70. )
  71. RoleAssignmentRepository(db).create(
  72. user_id=user.id,
  73. role_id=role.id,
  74. scope_type=None,
  75. scope_id=None,
  76. expires_time=None,
  77. )
  78. user_id = user.id
  79. role_id = role.id
  80. binding_response = client.post(
  81. "/identity/rolePermissionBindings/add",
  82. json={
  83. "roleId": role_id,
  84. "permission": "agents:*",
  85. },
  86. )
  87. assert binding_response.status_code == 200
  88. binding_payload = binding_response.json()
  89. assert binding_payload["success"] is True
  90. assert binding_payload["data"]["roleId"] == role_id
  91. login_response = client.post(
  92. "/identity/auth/login",
  93. json={"username": "demo", "password": "super-secret"},
  94. )
  95. assert login_response.status_code == 200
  96. login_payload = login_response.json()
  97. assert login_payload["data"]["accessToken"]
  98. assert login_payload["data"]["user"]["displayName"] == "Demo User"
  99. assert "createdTime" in login_payload["data"]["user"]
  100. token = login_payload["data"]["accessToken"]
  101. me_response = client.post(
  102. "/identity/auth/me",
  103. headers={"Authorization": f"Bearer {token}"},
  104. json={},
  105. )
  106. assert me_response.status_code == 200
  107. me_payload = me_response.json()
  108. assert me_payload["data"]["user"]["id"] == user_id
  109. assert me_payload["data"]["permissions"] == ["agents:*"]
  110. permission_response = client.post(
  111. "/identity/permissions/check",
  112. json={
  113. "userId": user_id,
  114. "permission": "agents:create",
  115. },
  116. )
  117. assert permission_response.status_code == 200
  118. assert permission_response.json()["data"]["allowed"] is True
  119. api_key_response = client.post(
  120. "/identity/apiKeys/create",
  121. json={"name": "Gateway Test Key", "scopes": "agents:read"},
  122. )
  123. assert api_key_response.status_code == 200
  124. api_key_payload = api_key_response.json()["data"]
  125. assert api_key_payload["secret"].startswith("agp_")
  126. assert api_key_payload["apiKey"]["keyPrefix"]
  127. api_keys_response = client.post("/identity/apiKeys/list", json={"page": 1, "pageSize": 20})
  128. assert api_keys_response.status_code == 200
  129. assert api_keys_response.json()["data"]["total"] == 1
  130. def test_identity_logout_revokes_token_with_redis_blacklist(
  131. tmp_path: Path,
  132. monkeypatch,
  133. ) -> None:
  134. prepare_known_service_import("auth-service")
  135. from app.api import identity_routes
  136. from app.bootstrap.app import create_app
  137. from app.db.models import Base
  138. from app.domain.repositories import UserRepository
  139. from app.infrastructure.passwords import hash_password
  140. from core_db import create_session_factory
  141. database_url = build_postgres_database_url(tmp_path, "auth-redis")
  142. monkeypatch.setenv("AGENT_PLATFORM_DATABASE_URL", database_url)
  143. engine = build_postgres_engine(database_url)
  144. Base.metadata.create_all(engine)
  145. fake_redis = FakeRedis()
  146. monkeypatch.setattr(identity_routes, "try_build_redis_client", lambda redis_url: fake_redis)
  147. app = create_app()
  148. app.state.session_factory = create_session_factory(engine)
  149. client = build_fastapi_test_client(app)
  150. session_factory = create_session_factory(engine)
  151. with session_factory() as db:
  152. UserRepository(db).create(
  153. username="redis-user",
  154. password_hash=hash_password("super-secret"),
  155. display_name="Redis User",
  156. email="redis@example.com",
  157. metadata_json={},
  158. )
  159. login_response = client.post(
  160. "/identity/auth/login",
  161. json={"username": "redis-user", "password": "super-secret"},
  162. )
  163. assert login_response.status_code == 200
  164. token = login_response.json()["data"]["accessToken"]
  165. verify_before_response = client.post(
  166. "/identity/auth/tokens/verify",
  167. json={"accessToken": token},
  168. )
  169. assert verify_before_response.status_code == 200
  170. assert verify_before_response.json()["data"]["active"] is True
  171. logout_response = client.post(
  172. "/identity/auth/logout",
  173. headers={"Authorization": f"Bearer {token}"},
  174. json={},
  175. )
  176. assert logout_response.status_code == 200
  177. assert logout_response.json()["data"]["ok"] is True
  178. verify_after_response = client.post(
  179. "/identity/auth/tokens/verify",
  180. json={"accessToken": token},
  181. )
  182. assert verify_after_response.status_code == 200
  183. assert verify_after_response.json()["data"]["active"] is False
  184. assert verify_after_response.json()["data"]["reason"] == "token_revoked"