Coverage for src/cstlcore/memberships/router.py: 35%
62 statements
« prev ^ index » next coverage.py v7.9.1, created at 2026-02-19 12:46 +0000
« prev ^ index » next coverage.py v7.9.1, created at 2026-02-19 12:46 +0000
1from fastapi import APIRouter, Body, Depends, HTTPException
2from sqlmodel import Session, select
4from cstlcore.auth.dependencies import get_current_user
5from cstlcore.constellations.dependencies import get_existing_constellation
6from cstlcore.constellations.models import Constellation
7from cstlcore.database.dependencies import get_session
8from cstlcore.memberships.dependencies import (
9 require_admin_access,
10 require_strictly_higher_access,
11)
12from cstlcore.memberships.models import (
13 AccessEnum,
14 ConstellationMembership,
15 ConstellationMembershipCreate,
16 ConstellationMembershipPublic,
17 MemberPublic,
18)
19from cstlcore.memberships.services import as_member
20from cstlcore.users.dependencies import get_user_by_id
21from cstlcore.users.models import User
23router = APIRouter()
26@router.get("/memberships", response_model=list[ConstellationMembershipPublic])
27async def get_memberships(session: Session = Depends(get_session)):
28 db_memberships = session.exec(select(ConstellationMembership)).all()
29 return db_memberships
32@router.post("/memberships", response_model=MemberPublic)
33async def create_membership(
34 membership: ConstellationMembershipCreate, session: Session = Depends(get_session)
35):
36 # Validate that exactly one of user_id or user_email is provided
37 if not membership.user_id and not membership.user_email:
38 raise HTTPException(
39 status_code=400, detail="Either user_id or user_email must be provided"
40 )
42 if membership.user_id and membership.user_email:
43 raise HTTPException(
44 status_code=400, detail="Cannot provide both user_id and user_email"
45 )
47 # Determine the user_id to use
48 user_id = membership.user_id
49 if membership.user_email:
50 # Look up user by email
51 user = session.exec(
52 select(User).where(User.email == membership.user_email)
53 ).first()
54 if not user:
55 raise HTTPException(
56 status_code=404,
57 detail=f"User with email {membership.user_email} not found",
58 )
59 user_id = user.id
60 # At this point, user_id is guaranteed to be not None due to validation above
61 assert user_id is not None
63 # Check if membership already exists
64 existing_membership = session.exec(
65 select(ConstellationMembership).where(
66 ConstellationMembership.constellation_id == membership.constellation_id,
67 ConstellationMembership.user_id == user_id,
68 )
69 ).first()
71 if existing_membership:
72 raise HTTPException(
73 status_code=409,
74 detail="Membership already exists for this user and constellation",
75 )
77 # Create the membership with the resolved user_id
78 db_membership = ConstellationMembership(
79 constellation_id=membership.constellation_id,
80 user_id=user_id,
81 access=membership.access,
82 )
83 session.add(db_membership)
84 session.commit()
85 session.refresh(db_membership)
86 return as_member(db_membership)
89@router.get(
90 "/constellations/{constellation_id}/members", response_model=list[MemberPublic]
91)
92async def get_constellation_members(
93 constellation: Constellation = Depends(get_existing_constellation),
94 session: Session = Depends(get_session),
95):
96 memberships = session.exec(
97 select(ConstellationMembership).where(
98 ConstellationMembership.constellation_id == constellation.id
99 )
100 ).all()
102 return [as_member(membership) for membership in memberships]
105@router.patch(
106 "/constellations/{constellation_id}/members/{user_id}/access",
107 response_model=MemberPublic,
108 dependencies=[Depends(require_strictly_higher_access)],
109)
110async def update_member_access(
111 access: AccessEnum = Body(..., embed=True),
112 constellation: Constellation = Depends(require_admin_access),
113 user: User = Depends(get_user_by_id),
114 current_user: User = Depends(get_current_user),
115 session: Session = Depends(get_session),
116):
117 if current_user.id == user.id:
118 raise HTTPException(
119 status_code=400,
120 detail="You cannot change your own access level",
121 )
123 membership = session.exec(
124 select(ConstellationMembership).where(
125 ConstellationMembership.constellation_id == constellation.id,
126 ConstellationMembership.user_id == user.id,
127 )
128 ).first()
130 if not membership:
131 raise HTTPException(
132 status_code=404,
133 detail="Membership not found for this user in constellation",
134 )
136 membership.access = access
137 session.add(membership)
138 session.commit()
139 session.refresh(membership)
141 return as_member(membership)
144@router.delete(
145 "/constellations/{constellation_id}/members/{user_id}",
146 dependencies=[Depends(require_strictly_higher_access)],
147 status_code=204,
148)
149async def delete_member(
150 constellation: Constellation = Depends(require_admin_access),
151 user: User = Depends(get_user_by_id),
152 current_user: User = Depends(get_current_user),
153 session: Session = Depends(get_session),
154):
155 if current_user.id == user.id:
156 raise HTTPException(
157 status_code=400,
158 detail="You cannot remove yourself from the constellation",
159 )
161 membership = session.exec(
162 select(ConstellationMembership).where(
163 ConstellationMembership.constellation_id == constellation.id,
164 ConstellationMembership.user_id == user.id,
165 )
166 ).first()
168 if not membership:
169 raise HTTPException(
170 status_code=404,
171 detail="Membership not found for this user in constellation",
172 )
174 session.delete(membership)
175 session.commit()