diff --git a/src/adapters/inmemory/repositories/group.py b/src/adapters/inmemory/repositories/group.py index 76f4b82..efcad76 100644 --- a/src/adapters/inmemory/repositories/group.py +++ b/src/adapters/inmemory/repositories/group.py @@ -65,3 +65,10 @@ def find_by_owner_id(self, owner_id: UUID) -> Optional[List[DomainGroup]]: def find_all_groups(self) -> Optional[List[DomainGroup]]: return list(self._groups.values()) + + def find_groups_by_member_id(self, user_id: UUID) -> Optional[List[DomainGroup]]: + groups = [] + for group_id, member_ids in self._members.items(): + if user_id in member_ids: + groups.append(self._groups[group_id]) + return groups if groups else [] diff --git a/src/adapters/sqlalchemy/repositories/group.py b/src/adapters/sqlalchemy/repositories/group.py index 01a63d2..0553465 100644 --- a/src/adapters/sqlalchemy/repositories/group.py +++ b/src/adapters/sqlalchemy/repositories/group.py @@ -84,4 +84,8 @@ def find_by_owner_id(self, owner_id: UUID) -> Optional[List[DomainGroup]]: def find_all_groups(self) -> Optional[List[DomainGroup]]: orms = self._session.query(ORMGroup).all() + return [group_from_orm(orm) for orm in orms] if orms else [] + + def find_groups_by_member_id(self, user_id: UUID) -> Optional[List[DomainGroup]]: + orms = self._session.query(ORMGroup).join(group_users).filter(group_users.c.profile_id == user_id).all() return [group_from_orm(orm) for orm in orms] if orms else [] \ No newline at end of file diff --git a/src/domain/ports/group_repository.py b/src/domain/ports/group_repository.py index 93530f1..741c9fc 100644 --- a/src/domain/ports/group_repository.py +++ b/src/domain/ports/group_repository.py @@ -39,4 +39,8 @@ def list_members(self, group_id: UUID) -> Optional[list[UUID]]: @abstractmethod def find_all_groups(self) -> Optional[list[Group]]: + pass + + @abstractmethod + def find_groups_by_member_id(self, user_id: UUID) -> Optional[list[Group]]: pass \ No newline at end of file diff --git a/src/domain/services/group.py b/src/domain/services/group.py index 05bcb38..cdbc19b 100644 --- a/src/domain/services/group.py +++ b/src/domain/services/group.py @@ -80,4 +80,30 @@ def get_all_groups(self) -> List[DomainGroup]: if not groups: raise NotFoundError("No groups found") - return groups \ No newline at end of file + return groups + + def get_my_coaches(self, user_id: UUID) -> List[DomainProfile]: + groups = self._repo.find_groups_by_member_id(user_id) + if not groups: + raise NotFoundError(f"No groups found for user {user_id}") + + coach_ids = set() + for group in groups: + coach_ids.add(group.owner_id) + + from src.container import container + profile_service = container.get_profile_service() + + coaches = [] + for coach_id in coach_ids: + try: + coach = profile_service._repo.find_by_id(coach_id) + if coach and "coach" in coach.roles: + coaches.append(coach) + except NotFoundError: + continue + + if not coaches: + raise NotFoundError("No coaches found in your groups") + + return coaches \ No newline at end of file diff --git a/src/entrypoints/api/routers/group.py b/src/entrypoints/api/routers/group.py index cc8de3f..d183dc8 100644 --- a/src/entrypoints/api/routers/group.py +++ b/src/entrypoints/api/routers/group.py @@ -8,6 +8,7 @@ from src.entrypoints.api.deps.auth import require_group_owner_or_admin, get_current_user from src.entrypoints.api.deps.roles import require_roles from src.entrypoints.api.schemas.group import GroupCreate, GroupUpdate, GroupRead, GroupMember +from src.entrypoints.api.schemas.profile import CoachProfileRead from src.container import container @@ -103,4 +104,15 @@ def leave_group(group_id: UUID, user=Depends(get_current_user)): service.remove_member(group_id, UUID(user["sub"])) except NotFoundError as e: raise HTTPException(404, str(e)) + +@router.get("/coachs/mine", response_model=List[CoachProfileRead]) +def get_my_coaches(user=Depends(get_current_user)): + """Get all coaches from groups where the current user is a member""" + service = container.get_group_service() + try: + coaches = service.get_my_coaches(UUID(user["sub"])) + return [CoachProfileRead.model_validate(coach) for coach in coaches] + except NotFoundError as e: + raise HTTPException(404, str(e)) + diff --git a/src/entrypoints/api/tests/group.py b/src/entrypoints/api/tests/group.py index 7a0a2eb..76452b5 100644 --- a/src/entrypoints/api/tests/group.py +++ b/src/entrypoints/api/tests/group.py @@ -283,6 +283,147 @@ async def test_28_coach_delete_own_group(self, client, test_state): ) assert r.status_code == 204 + # 29 – Setup pour les tests de coaches: coach crée un nouveau groupe + async def test_29_setup_coach_create_new_group(self, client, test_state): + payload = {"name": "NewCoachGroup", "description": "Nouveau groupe pour tests coaches"} + r = await client.post( + "/groups", + json=payload, + headers={"Authorization": f"Bearer {test_state['coach_token']}"} + ) + assert r.status_code == 201 + grp = r.json() + test_state["new_coach_group_uuid"] = grp["id"] + + # 30 – Coach ajoute l'utilisateur au nouveau groupe + async def test_30_setup_coach_add_user_to_new_group(self, client, test_state): + r = await client.post( + f"/groups/{test_state['new_coach_group_uuid']}/members/{test_state['user_uuid']}", + headers={"Authorization": f"Bearer {test_state['coach_token']}"} + ) + assert r.status_code == 204 + + # 31 – User récupère ses coaches (il est membre du groupe du coach) → 200 + async def test_31_user_get_my_coaches_success(self, client, test_state): + r = await client.get( + "/groups/coachs/mine", + headers={"Authorization": f"Bearer {test_state['user_token']}"} + ) + assert r.status_code == 200 + coaches = r.json() + assert isinstance(coaches, list) + assert len(coaches) == 1 + assert coaches[0]["id"] == test_state["coach_uuid"] + assert coaches[0]["name"] == "Coach" + + # 32 – Admin récupère ses coaches (il n'est membre d'aucun groupe) → 404 + async def test_32_admin_get_my_coaches_not_found(self, client, test_state): + r = await client.get( + "/groups/coachs/mine", + headers={"Authorization": f"Bearer {test_state['admin_token']}"} + ) + assert r.status_code == 404 + + # 33 – User quitte le groupe puis essaie de récupérer ses coaches → 404 + async def test_33_user_leave_group_then_get_coaches_not_found(self, client, test_state): + r = await client.delete( + f"/groups/{test_state['new_coach_group_uuid']}/leave", + headers={"Authorization": f"Bearer {test_state['user_token']}"} + ) + assert r.status_code == 204 + + r = await client.get( + "/groups/coachs/mine", + headers={"Authorization": f"Bearer {test_state['user_token']}"} + ) + assert r.status_code == 404 + + # 34 – Création d'un second coach pour tester plusieurs coaches + async def test_34_create_second_coach(self, client, test_state): + payload = { + "email": "coach2@example.com", + "password": "Coach2Pass123!", + "confirm_password": "Coach2Pass123!", + "name": "Coach2", + "sex": "F", + "age": 30 + } + r = await client.post("/profiles", json=payload) + assert r.status_code == 201 + body = r.json() + test_state["coach2_uuid"] = body["profile"]["id"] + + update_data = {"roles": ["user", "coach"]} + r = await client.patch( + f"/profiles/{test_state['coach2_uuid']}/roles", + json=update_data, + headers={"Authorization": f"Bearer {test_state['admin_token']}"} + ) + assert r.status_code == 200 + + r = await client.post( + "/profiles/login", + json={"email": "coach2@example.com", "password": "Coach2Pass123!"} + ) + assert r.status_code == 200 + test_state["coach2_token"] = r.json()["access_token"] + + # 35 – Second coach crée un groupe + async def test_35_coach2_create_group(self, client, test_state): + payload = {"name": "Coach2Group", "description": "Groupe du second coach"} + r = await client.post( + "/groups", + json=payload, + headers={"Authorization": f"Bearer {test_state['coach2_token']}"} + ) + assert r.status_code == 201 + grp = r.json() + test_state["coach2_group_uuid"] = grp["id"] + + # 36 – Les deux coaches ajoutent l'utilisateur à leurs groupes + async def test_36_both_coaches_add_user(self, client, test_state): + # Coach 1 ajoute user + r = await client.post( + f"/groups/{test_state['new_coach_group_uuid']}/members/{test_state['user_uuid']}", + headers={"Authorization": f"Bearer {test_state['coach_token']}"} + ) + assert r.status_code == 204 + + r = await client.post( + f"/groups/{test_state['coach2_group_uuid']}/members/{test_state['user_uuid']}", + headers={"Authorization": f"Bearer {test_state['coach2_token']}"} + ) + assert r.status_code == 204 + + # 37 – User récupère ses coaches (2 coaches cette fois) → 200 + async def test_37_user_get_multiple_coaches(self, client, test_state): + r = await client.get( + "/groups/coachs/mine", + headers={"Authorization": f"Bearer {test_state['user_token']}"} + ) + assert r.status_code == 200 + coaches = r.json() + assert isinstance(coaches, list) + assert len(coaches) == 2 + + coach_ids = [coach["id"] for coach in coaches] + assert test_state["coach_uuid"] in coach_ids + assert test_state["coach2_uuid"] in coach_ids + +# 38 – User quitte les groupes de ses coaches → 204 + async def test_38_user_leave_coach_groups(self, client, test_state): + r = await client.delete( + f"/groups/{test_state['new_coach_group_uuid']}/leave", + headers={"Authorization": f"Bearer {test_state['user_token']}"} + ) + assert r.status_code == 204 + + r = await client.delete( + f"/groups/{test_state['coach2_group_uuid']}/leave", + headers={"Authorization": f"Bearer {test_state['user_token']}"} + ) + assert r.status_code == 204 + # créer un user/ créer un coach (user) # admin update role coach add coach