| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406 |
- import uuid
- import pytest
- from httpx import AsyncClient
- from sqlalchemy import select
- from sqlalchemy.orm import Session
- from app.main import app
- from app.models import OrgRole, Project, Slot, Sms, Volunteer
- from app.tests.conftest import default_project_id, default_slot_id, default_volunteer_id
- from app.tests.shared_access import SharedProjectAccessTests
- pytestmark = pytest.mark.asyncio
- WRITE_FORBIDDEN_ROLES = [OrgRole.RESPO_COMMISSION, None]
- route_kwargs = {"project_id": default_project_id}
- route_kwargs_2 = {**route_kwargs, "volunteer_id": default_volunteer_id}
- VOLUNTEER_ROUTES = [
- ("GET", "list_project_volunteers", route_kwargs, None),
- ("POST", "create_volunteer", route_kwargs, {"name": "Test", "email": "a@free.fr"}),
- ("POST", "update_volunteer", route_kwargs_2, {"name": "Updated"}),
- ("DELETE", "delete_volunteer", route_kwargs_2, None),
- ]
- class TestVolunteerCrossCuttingAccess(SharedProjectAccessTests):
- @pytest.fixture(params=VOLUNTEER_ROUTES, ids=lambda x: f"{x[0]}-{x[1]}")
- def resolved_route(self, request, default_project):
- return request.param
- class TestListVolunteer:
- @pytest.mark.parametrize(
- "role", [OrgRole.ORG_ADMIN, OrgRole.RESPO_BENEVOLE, OrgRole.RESPO_COMMISSION]
- )
- async def test_readable_by_all_org_roles(
- self,
- client: AsyncClient,
- default_public_project: Project,
- make_org_user,
- role,
- ):
- """respo_commission can read volunteers -- needs visibility into who
- they'll be working with -- even though they can't manage them."""
- _, headers = make_org_user(role=role)
- response = await client.get(
- app.url_path_for("list_project_volunteers", project_id=default_project_id),
- headers=headers,
- )
- assert response.status_code == 200
- data = response.json()
- assert len(data) == 1
- assert data[0]["name"] == "Arthur"
- assert data[0]["id"] == default_volunteer_id
- assert "created_at" in data[0]
- async def test_no_membership_forbidden(
- self,
- client: AsyncClient,
- default_public_project: Project,
- make_org_user,
- ):
- _, headers = make_org_user(role=None)
- response = await client.get(
- app.url_path_for("list_project_volunteers", project_id=default_project_id),
- headers=headers,
- )
- assert response.status_code == 403
- class TestCreateVolunteer:
- async def test_invalid_payload(
- self,
- client: AsyncClient,
- default_public_project: Project,
- make_org_user,
- ):
- _, headers = make_org_user(role=OrgRole.ORG_ADMIN)
- payload = {"email": "lancelot@dulac.fr", "phone_number": "03 14 15 92 65"}
- response = await client.post(
- app.url_path_for("create_volunteer", project_id=default_project_id),
- json=payload,
- headers=headers,
- )
- assert response.status_code == 422
- @pytest.mark.parametrize("role", [OrgRole.ORG_ADMIN, OrgRole.RESPO_BENEVOLE])
- async def test_write_roles_create(
- self,
- client: AsyncClient,
- default_public_project: Project,
- make_org_user,
- session: Session,
- role,
- ):
- _, headers = make_org_user(role=role)
- payload = {
- "name": "Lancelot",
- "email": "lancelot@dulac.fr",
- "phone_number": "03 14 15 92 65",
- }
- response = await client.post(
- app.url_path_for("create_volunteer", project_id=default_project_id),
- json=payload,
- headers=headers,
- )
- assert response.status_code == 200
- assert response.json()["id"] != default_project_id
- assert response.json()["name"] == "Lancelot"
- assert response.json()["comment"] == ""
- volunteers = (
- session.execute(select(Volunteer).where(Volunteer.project_id == default_project_id))
- .scalars()
- .all()
- )
- assert len(volunteers) > 1
- @pytest.mark.parametrize("role", WRITE_FORBIDDEN_ROLES)
- async def test_read_only_roles_forbidden(
- self,
- client: AsyncClient,
- default_public_project: Project,
- make_org_user,
- role,
- ):
- _, headers = make_org_user(role=role)
- payload = {
- "name": "Lancelot",
- "email": "lancelot@dulac.fr",
- "phone_number": "03 14 15 92 65",
- }
- response = await client.post(
- app.url_path_for("create_volunteer", project_id=default_project_id),
- json=payload,
- headers=headers,
- )
- assert response.status_code == 403
- async def test_create_with_slots(
- self,
- client: AsyncClient,
- default_public_project: Project,
- make_org_user,
- session: Session,
- ):
- _, headers = make_org_user(role=OrgRole.ORG_ADMIN)
- payload = {
- "name": "Lancelot",
- "email": "lancelot@dulac.fr",
- "phone_number": "03 14 15 92 65",
- "slots": [default_slot_id],
- }
- response = await client.post(
- app.url_path_for("create_volunteer", project_id=default_project_id),
- json=payload,
- headers=headers,
- )
- assert response.status_code == 200
- volunteers = (
- session.execute(select(Volunteer).where(Volunteer.project_id == default_project_id))
- .scalars()
- .all()
- )
- new_volunteer = [v for v in volunteers if v.id != default_volunteer_id][0]
- assert len(new_volunteer.slots) == 1
- async def test_create_with_comment(
- self,
- client: AsyncClient,
- default_public_project: Project,
- make_org_user,
- session: Session,
- ):
- _, headers = make_org_user(role=OrgRole.ORG_ADMIN)
- payload = {
- "name": "Lancelot",
- "email": "lancelot@dulac.fr",
- "phone_number": "03 14 15 92 65",
- "comment": "it's a knight",
- }
- response = await client.post(
- app.url_path_for("create_volunteer", project_id=default_project_id),
- json=payload,
- headers=headers,
- )
- assert response.status_code == 200
- assert response.json()["comment"] != ""
- volunteer = session.get(Volunteer, response.json()["id"])
- assert volunteer.comment == "it's a knight"
- class TestUpdateVolunteer:
- async def test_invalid_volunteer_id(
- self,
- client: AsyncClient,
- default_public_project: Project,
- make_org_user,
- ):
- _, headers = make_org_user(role=OrgRole.ORG_ADMIN)
- payload = {"name": "Lancelot", "email": "l@dulac.fr", "phone_number": "0314159265"}
- response = await client.post(
- app.url_path_for(
- "update_volunteer", project_id=default_project_id, volunteer_id=uuid.uuid4()
- ),
- json=payload,
- headers=headers,
- )
- assert response.status_code == 404
- @pytest.mark.parametrize("role", [OrgRole.ORG_ADMIN, OrgRole.RESPO_BENEVOLE])
- async def test_write_roles_update_each_field(
- self,
- client: AsyncClient,
- default_public_project: Project,
- make_org_user,
- role,
- ):
- _, headers = make_org_user(role=role)
- payload = {
- "name": "Lancelot",
- "email": "lancelot@dulac.fr",
- "phone_number": "03 14 15 92 65",
- "automatic_sms": False,
- "comment": "new comment",
- }
- for k, v in payload.items():
- response = await client.post(
- app.url_path_for(
- "update_volunteer",
- project_id=default_project_id,
- volunteer_id=default_volunteer_id,
- ),
- json={k: v},
- headers=headers,
- )
- assert response.status_code == 200
- assert response.json()["id"] == default_volunteer_id
- assert response.json()[k] == v
- @pytest.mark.parametrize("role", WRITE_FORBIDDEN_ROLES)
- async def test_read_only_roles_forbidden(
- self,
- client: AsyncClient,
- default_public_project: Project,
- make_org_user,
- role,
- ):
- _, headers = make_org_user(role=role)
- response = await client.post(
- app.url_path_for(
- "update_volunteer", project_id=default_project_id, volunteer_id=default_volunteer_id
- ),
- json={"comment": "hijacked"},
- headers=headers,
- )
- assert response.status_code == 403
- async def test_update_slots(
- self,
- client: AsyncClient,
- default_public_project: Project,
- make_org_user,
- session: Session,
- ):
- _, headers = make_org_user(role=OrgRole.ORG_ADMIN)
- url = app.url_path_for(
- "update_volunteer", project_id=default_project_id, volunteer_id=default_volunteer_id
- )
- response = await client.post(url, json={"slots": []}, headers=headers)
- assert response.status_code == 200
- slot = session.execute(select(Slot).where(Slot.id == default_slot_id)).scalars().first()
- assert slot.volunteers_id == []
- response = await client.post(url, json={"slots": [default_slot_id]}, headers=headers)
- assert response.status_code == 200
- session.refresh(slot)
- assert slot.volunteers_id == [default_volunteer_id]
- response = await client.post(url, json={"slots": [str(uuid.uuid4())]}, headers=headers)
- assert response.status_code == 400
- response = await client.post(url, json={"slots": ["not uuid str"]}, headers=headers)
- assert response.status_code == 422
- class TestDeleteVolunteer:
- @pytest.mark.parametrize("role", WRITE_FORBIDDEN_ROLES)
- async def test_read_only_roles_forbidden(
- self,
- client: AsyncClient,
- default_public_project: Project,
- make_org_user,
- session: Session,
- role,
- ):
- _, headers = make_org_user(role=role)
- response = await client.delete(
- app.url_path_for(
- "delete_volunteer", project_id=default_project_id, volunteer_id=default_volunteer_id
- ),
- headers=headers,
- )
- assert response.status_code == 403
- assert session.get(Volunteer, default_volunteer_id) is not None
- async def test_org_admin_deletes(
- self,
- client: AsyncClient,
- default_public_project: Project,
- make_org_user,
- session: Session,
- ):
- _, headers = make_org_user(role=OrgRole.ORG_ADMIN)
- response = await client.delete(
- app.url_path_for(
- "delete_volunteer", project_id=default_project_id, volunteer_id=default_volunteer_id
- ),
- headers=headers,
- )
- assert response.status_code == 200
- assert session.get(Volunteer, default_volunteer_id) is None
- slot = session.execute(select(Slot).where(Slot.id == default_slot_id)).scalars().first()
- assert default_volunteer_id not in slot.volunteers_id
- async def test_idempotent_delete(
- self,
- client: AsyncClient,
- default_public_project: Project,
- make_org_user,
- ):
- _, headers = make_org_user(role=OrgRole.ORG_ADMIN)
- url = app.url_path_for(
- "delete_volunteer", project_id=default_project_id, volunteer_id=default_volunteer_id
- )
- response = await client.delete(url, headers=headers)
- assert response.status_code == 200
- response = await client.delete(url, headers=headers)
- assert response.status_code == 200
- async def test_delete_random_uuid(
- self,
- client: AsyncClient,
- default_public_project: Project,
- make_org_user,
- ):
- """Unlike delete_slot/delete_template, this still bulk-DELETEs
- without an existence check via a Depends() resolver -- so a
- nonexistent volunteer_id stays a silent 200, matching the original
- behavior. If you want 404-on-missing consistency across all three
- routers, this needs the same get()-then-check pattern slots.py uses."""
- _, headers = make_org_user(role=OrgRole.ORG_ADMIN)
- response = await client.delete(
- app.url_path_for(
- "delete_volunteer", project_id=default_project_id, volunteer_id=uuid.uuid4()
- ),
- headers=headers,
- )
- assert response.status_code == 200
- async def test_invalid_uuid_format(
- self,
- client: AsyncClient,
- default_public_project: Project,
- make_org_user,
- ):
- _, headers = make_org_user(role=OrgRole.ORG_ADMIN)
- response = await client.delete(
- app.url_path_for(
- "delete_volunteer", project_id=default_project_id, volunteer_id="not uidstr"
- ),
- headers=headers,
- )
- assert response.status_code == 422
- async def test_delete_cascades_sms(
- self,
- client: AsyncClient,
- default_public_project: Project,
- make_org_user,
- session: Session,
- ):
- _, headers = make_org_user(role=OrgRole.ORG_ADMIN)
- sms = Sms(
- project_id=default_project_id,
- content="coucou",
- phone_number="02 66 66 66 66 66",
- volunteer_id=default_volunteer_id,
- )
- session.add(sms)
- session.commit()
- sms_id = sms.id
- response = await client.delete(
- app.url_path_for(
- "delete_volunteer", project_id=default_project_id, volunteer_id=default_volunteer_id
- ),
- headers=headers,
- )
- assert response.status_code == 200
- assert session.get(Volunteer, default_volunteer_id) is None
- assert session.execute(select(Sms).where(Sms.id == sms_id)).scalar_one_or_none() is None
|