conftest.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  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 AsyncClient, ASGITransport
  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. OrgRole,
  18. Organization,
  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(
  65. transport=ASGITransport(app=app), base_url="http://test"
  66. ) as client:
  67. yield client
  68. @pytest.fixture
  69. def default_user(test_db_setup_sessionmaker) -> User:
  70. with session_maker() as db:
  71. result = db.execute(select(User).where(User.email == default_user_email))
  72. user = result.scalars().first()
  73. if user is None:
  74. new_user = User(
  75. email=default_user_email,
  76. hashed_password=default_user_password_hash,
  77. name="Geralt",
  78. )
  79. new_user.id = default_user_id
  80. db.add(new_user)
  81. db.commit()
  82. db.refresh(new_user)
  83. return new_user
  84. return user
  85. @pytest.fixture
  86. def default_organization(test_db_setup_sessionmaker) -> Organization:
  87. """Uses session_maker() directly, like default_user/default_project, so
  88. it's safe to depend on from fixtures that also use session_maker()
  89. without cross-session visibility issues (same DB, autocommitted)."""
  90. with session_maker() as db:
  91. result = db.execute(select(Organization).where(Organization.id == default_organization_id))
  92. org = result.scalars().first()
  93. if org is None:
  94. new_org = Organization(id=default_organization_id, name=default_organization_name)
  95. db.add(new_org)
  96. db.commit()
  97. db.refresh(new_org)
  98. return new_org
  99. return org
  100. @pytest.fixture
  101. def default_user_org_membership(
  102. default_user: User, default_organization: Organization
  103. ) -> UserOrganization:
  104. """default_user is org_admin of default_organization."""
  105. with session_maker() as db:
  106. result = db.execute(
  107. select(UserOrganization).where(
  108. UserOrganization.user_id == default_user.id,
  109. UserOrganization.organization_id == default_organization.id,
  110. )
  111. )
  112. membership = result.scalars().first()
  113. if membership is None:
  114. new_membership = UserOrganization(
  115. user_id=default_user.id, organization_id=default_organization.id, role=OrgRole.ORG_ADMIN
  116. )
  117. db.add(new_membership)
  118. db.commit()
  119. db.refresh(new_membership)
  120. return new_membership
  121. return membership
  122. @pytest.fixture
  123. def make_org_user(default_organization: Organization):
  124. """
  125. Factory fixture for parametrized role tests.
  126. make_org_user(role=OrgRole.RESPO_BENEVOLE) -> (user, headers), member of default_organization.
  127. make_org_user(role=None) -> authenticated user with NO membership (tests 403 path).
  128. make_org_user(global_role=GlobalRole.SUPER_ADMIN) -> bypasses org role checks entirely.
  129. Always creates a brand-new user (unique email) so parametrized cases don't collide.
  130. """
  131. def _factory(
  132. role: OrgRole | None = None,
  133. global_role: GlobalRole = GlobalRole.USER,
  134. ) -> tuple[User, dict]:
  135. with session_maker() as db:
  136. user_id = uuid4()
  137. user = User(
  138. id=user_id,
  139. email=f"{user_id}@test.com",
  140. hashed_password=default_user_password_hash,
  141. name="Test User",
  142. global_role=global_role,
  143. )
  144. db.add(user)
  145. db.commit()
  146. db.refresh(user)
  147. if role is not None:
  148. db.add(
  149. UserOrganization(
  150. user_id=user.id, organization_id=default_organization.id, role=role
  151. )
  152. )
  153. db.commit()
  154. db.refresh(user)
  155. token = security.create_jwt_token(str(user_id), 60 * 60 * 24, refresh=False)[0]
  156. headers = {"Authorization": f"Bearer {token}"}
  157. return user, headers
  158. return _factory
  159. @pytest.fixture
  160. def default_project(default_organization: Organization) -> Project:
  161. """An empty private project. Depends on default_organization so the FK
  162. is always satisfied regardless of which fixtures a test happens to request."""
  163. with session_maker() as db:
  164. result = db.execute(select(Project).where(Project.id == default_project_id))
  165. project = result.scalars().first()
  166. if project is None:
  167. new_project = Project(
  168. organization_id=default_organization.id,
  169. name=default_project_name,
  170. is_public=False,
  171. )
  172. new_project.id = default_project_id
  173. db.add(new_project)
  174. db.commit()
  175. db.refresh(new_project)
  176. return new_project
  177. return project
  178. @pytest.fixture
  179. def default_public_project(default_organization: Organization) -> Project:
  180. """A public project with 1 volunteer, 1 slot & 1 sms associated to."""
  181. with session_maker() as db:
  182. result = db.execute(select(Project).where(Project.id == default_project_id))
  183. project = result.scalars().first()
  184. if project is None:
  185. new_project = Project(
  186. organization_id=default_organization.id,
  187. name=default_project_name,
  188. is_public=True,
  189. )
  190. new_project.id = default_project_id
  191. db.add(new_project)
  192. volunteer = Volunteer(
  193. project_id=default_project_id,
  194. name="Arthur",
  195. surname="Pandragon",
  196. email="arthur.pandragon@kamelot.fr",
  197. phone_number="02 66 66 66 66 66",
  198. automatic_sms=True,
  199. )
  200. volunteer.id = default_volunteer_id
  201. db.add(volunteer)
  202. slot = Slot(
  203. project_id=default_project_id,
  204. title="être roi",
  205. starting_time=datetime(1600, 1, 1),
  206. ending_time=datetime(1900, 1, 1),
  207. )
  208. slot.id = default_slot_id
  209. slot.volunteers.append(volunteer)
  210. db.add(slot)
  211. tag = SlotTag(project_id=default_project_id, title="Royal")
  212. tag.id = default_tag_id
  213. db.add(tag)
  214. sms = Sms(
  215. project_id=default_project_id,
  216. content="Bonjour sir",
  217. phone_number="66 66 66 66 66",
  218. )
  219. sms.id = default_sms_id
  220. db.add(sms)
  221. tmp = SlotTemplate(project_id=default_project_id, title="basic template")
  222. tmp.id = default_template_id
  223. db.add(tmp)
  224. db.commit()
  225. db.refresh(new_project)
  226. return new_project
  227. return project
  228. @pytest.fixture
  229. def default_user_headers(default_user: User):
  230. return {"Authorization": f"Bearer {default_user_access_token}"}