conftest.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. import asyncio
  2. from collections.abc import AsyncGenerator
  3. from datetime import datetime
  4. from uuid import uuid4
  5. import pytest
  6. import pytest_asyncio
  7. from httpx import ASGITransport, AsyncClient
  8. from sqlalchemy import delete, select
  9. from sqlalchemy.orm import Session
  10. from app.core import config, security
  11. from app.core.session import engine
  12. from app.core.session import session as session_maker
  13. from app.main import app
  14. from app.models import (
  15. Base,
  16. GlobalRole,
  17. Organization,
  18. OrgRole,
  19. Project,
  20. Slot,
  21. SlotTag,
  22. SlotTemplate,
  23. Sms,
  24. User,
  25. UserOrganization,
  26. Volunteer,
  27. )
  28. default_user_id = "b75365d9-7bf9-4f54-add5-aeab333a087b"
  29. default_user_email = "geralt@wiedzmin.pl"
  30. default_user_password = "geralt"
  31. default_user_password_hash = security.get_password_hash(default_user_password)
  32. default_user_access_token = security.create_jwt_token(
  33. str(default_user_id), 60 * 60 * 24, refresh=False
  34. )[0]
  35. default_project_id = "e233ac66-3a29-4ae5-991e-30ec7334c566"
  36. default_project_name = "Default project"
  37. default_volunteer_id = "5514f4ef-75ee-40b2-ad99-420927c5c9e5"
  38. default_slot_id = "def04027-0048-48e2-8f47-f955fe080b31"
  39. default_sms_id = "b05c2b38-edb1-4e7a-9689-a0cba904ef29"
  40. default_tag_id = "47709be8-7edb-4fb1-9de0-8a529a91856d"
  41. default_template_id = "dcfcf2e9-8b1d-4bbf-b44b-aa511a7fe274"
  42. default_organization_id = "a1f2e3d4-5b6c-47d8-9e0f-1a2b3c4d5e6f"
  43. default_organization_name = "Default Org"
  44. @pytest.fixture(scope="session")
  45. def event_loop():
  46. loop = asyncio.new_event_loop()
  47. asyncio.set_event_loop(loop)
  48. yield loop
  49. loop.close()
  50. @pytest_asyncio.fixture(scope="session")
  51. async def test_db_setup_sessionmaker():
  52. assert config.settings.ENVIRONMENT == "PYTEST"
  53. Base.metadata.drop_all(engine)
  54. Base.metadata.create_all(engine)
  55. @pytest_asyncio.fixture(autouse=True)
  56. async def session(test_db_setup_sessionmaker) -> AsyncGenerator[Session, None]:
  57. with session_maker() as db:
  58. yield db
  59. for name, table in Base.metadata.tables.items():
  60. db.execute(delete(table))
  61. db.commit()
  62. @pytest_asyncio.fixture(scope="session")
  63. async def client() -> AsyncGenerator[AsyncClient, None]:
  64. async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as client:
  65. yield client
  66. @pytest.fixture
  67. def default_user(test_db_setup_sessionmaker) -> User:
  68. with session_maker() as db:
  69. result = db.execute(select(User).where(User.email == default_user_email))
  70. user = result.scalars().first()
  71. if user is None:
  72. new_user = User(
  73. email=default_user_email,
  74. hashed_password=default_user_password_hash,
  75. name="Geralt",
  76. )
  77. new_user.id = default_user_id
  78. db.add(new_user)
  79. db.commit()
  80. db.refresh(new_user)
  81. return new_user
  82. return user
  83. @pytest.fixture
  84. def default_organization(test_db_setup_sessionmaker) -> Organization:
  85. """Uses session_maker() directly, like default_user/default_project, so
  86. it's safe to depend on from fixtures that also use session_maker()
  87. without cross-session visibility issues (same DB, autocommitted)."""
  88. with session_maker() as db:
  89. result = db.execute(select(Organization).where(Organization.id == default_organization_id))
  90. org = result.scalars().first()
  91. if org is None:
  92. new_org = Organization(id=default_organization_id, name=default_organization_name)
  93. db.add(new_org)
  94. db.commit()
  95. db.refresh(new_org)
  96. return new_org
  97. return org
  98. @pytest.fixture
  99. def other_org_user(session: Session):
  100. """A user who is org_admin of a DIFFERENT organization entirely --
  101. distinct from make_org_user(role=None), which has no membership at all.
  102. This confirms role checks are scoped per-organization, not just
  103. 'has some role somewhere'."""
  104. other_org = Organization(id=uuid4(), name="Other Org")
  105. session.add(other_org)
  106. session.commit()
  107. from app.core import security
  108. user_id = uuid4()
  109. user = User(
  110. id=user_id,
  111. email=f"{user_id}@test.com",
  112. hashed_password="x",
  113. name="Outsider",
  114. )
  115. session.add(user)
  116. session.commit()
  117. session.add(
  118. UserOrganization(user_id=user.id, organization_id=other_org.id, role=OrgRole.ORG_ADMIN)
  119. )
  120. session.commit()
  121. token = security.create_jwt_token(str(user_id), 60 * 60 * 24, refresh=False)[0]
  122. headers = {"Authorization": f"Bearer {token}"}
  123. return user, headers
  124. @pytest.fixture
  125. def default_user_org_membership(
  126. default_user: User, default_organization: Organization
  127. ) -> UserOrganization:
  128. """default_user is org_admin of default_organization."""
  129. with session_maker() as db:
  130. result = db.execute(
  131. select(UserOrganization).where(
  132. UserOrganization.user_id == default_user.id,
  133. UserOrganization.organization_id == default_organization.id,
  134. )
  135. )
  136. membership = result.scalars().first()
  137. if membership is None:
  138. new_membership = UserOrganization(
  139. user_id=default_user.id,
  140. organization_id=default_organization.id,
  141. role=OrgRole.ORG_ADMIN,
  142. )
  143. db.add(new_membership)
  144. db.commit()
  145. db.refresh(new_membership)
  146. return new_membership
  147. return membership
  148. @pytest.fixture
  149. def make_org_user(default_organization: Organization):
  150. """
  151. Factory fixture for parametrized role tests.
  152. make_org_user(role=OrgRole.RESPO_BENEVOLE) -> (user, headers), member of default_organization.
  153. make_org_user(role=None) -> authenticated user with NO membership (tests 403 path).
  154. make_org_user(global_role=GlobalRole.SUPER_ADMIN) -> bypasses org role checks entirely.
  155. Always creates a brand-new user (unique email) so parametrized cases don't collide.
  156. """
  157. def _factory(
  158. role: OrgRole | None = None,
  159. global_role: GlobalRole = GlobalRole.USER,
  160. ) -> tuple[User, dict]:
  161. with session_maker() as db:
  162. user_id = uuid4()
  163. user = User(
  164. id=user_id,
  165. email=f"{user_id}@test.com",
  166. hashed_password=default_user_password_hash,
  167. name="Test User",
  168. global_role=global_role,
  169. )
  170. db.add(user)
  171. db.commit()
  172. db.refresh(user)
  173. if role is not None:
  174. db.add(
  175. UserOrganization(
  176. user_id=user.id, organization_id=default_organization.id, role=role
  177. )
  178. )
  179. db.commit()
  180. db.refresh(user)
  181. token = security.create_jwt_token(str(user_id), 60 * 60 * 24, refresh=False)[0]
  182. headers = {"Authorization": f"Bearer {token}"}
  183. return user, headers
  184. return _factory
  185. @pytest.fixture
  186. def default_project(default_organization: Organization) -> Project:
  187. """An empty private project. Depends on default_organization so the FK
  188. is always satisfied regardless of which fixtures a test happens to request."""
  189. with session_maker() as db:
  190. result = db.execute(select(Project).where(Project.id == default_project_id))
  191. project = result.scalars().first()
  192. if project is None:
  193. new_project = Project(
  194. organization_id=default_organization.id,
  195. name=default_project_name,
  196. is_public=False,
  197. )
  198. new_project.id = default_project_id
  199. db.add(new_project)
  200. db.commit()
  201. db.refresh(new_project)
  202. return new_project
  203. return project
  204. @pytest.fixture
  205. def default_public_project(default_organization: Organization) -> Project:
  206. """A public project with 1 volunteer, 1 slot & 1 sms associated to."""
  207. with session_maker() as db:
  208. result = db.execute(select(Project).where(Project.id == default_project_id))
  209. project = result.scalars().first()
  210. if project is None:
  211. new_project = Project(
  212. organization_id=default_organization.id,
  213. name=default_project_name,
  214. is_public=True,
  215. )
  216. new_project.id = default_project_id
  217. db.add(new_project)
  218. volunteer = Volunteer(
  219. project_id=default_project_id,
  220. name="Arthur",
  221. surname="Pandragon",
  222. email="arthur.pandragon@kamelot.fr",
  223. phone_number="02 66 66 66 66 66",
  224. automatic_sms=True,
  225. )
  226. volunteer.id = default_volunteer_id
  227. db.add(volunteer)
  228. slot = Slot(
  229. project_id=default_project_id,
  230. title="être roi",
  231. starting_time=datetime(1600, 1, 1),
  232. ending_time=datetime(1900, 1, 1),
  233. )
  234. slot.id = default_slot_id
  235. slot.volunteers.append(volunteer)
  236. db.add(slot)
  237. tag = SlotTag(project_id=default_project_id, title="Royal")
  238. tag.id = default_tag_id
  239. db.add(tag)
  240. sms = Sms(
  241. project_id=default_project_id,
  242. content="Bonjour sir",
  243. phone_number="66 66 66 66 66",
  244. )
  245. sms.id = default_sms_id
  246. db.add(sms)
  247. tmp = SlotTemplate(project_id=default_project_id, title="basic template")
  248. tmp.id = default_template_id
  249. db.add(tmp)
  250. db.commit()
  251. db.refresh(new_project)
  252. return new_project
  253. return project
  254. @pytest.fixture
  255. def default_user_headers(default_user: User):
  256. return {"Authorization": f"Bearer {default_user_access_token}"}