gabriel / musehub public
test_musehub_collaborators.py python
267 lines 9.4 KB
Raw
sha256:a10adeeb7a0169cb9900f9806ed7a973047258abb6283724fe55e8eb68ff3f0a init: musehub initial commit Human 171 days ago
1 """Tests for MuseHub collaborators management endpoints.
2
3 Covers the acceptance criteria:
4 - GET /repos/{repo_id}/collaborators returns collaborator list
5 - POST /repos/{repo_id}/collaborators invites a collaborator (owner/admin+)
6 - PUT /repos/{repo_id}/collaborators/{handle}/permission updates permission
7 - DELETE /repos/{repo_id}/collaborators/{handle} removes collaborator
8 - GET /repos/{repo_id}/collaborators/{handle}/permission checks presence
9 - Owner cannot be removed as a collaborator
10 - Only admin+ (or owner) may mutate collaborators
11 - Duplicate invite returns 409
12 """
13 from __future__ import annotations
14
15 import pytest
16 from httpx import AsyncClient
17 from musehub.muse_contracts.json_types import JSONObject, StrDict
18
19 # ── Constants ─────────────────────────────────────────────────────────────────
20
21 _COLLABORATOR_HANDLE = "collabuser"
22
23
24 # ── Helpers ───────────────────────────────────────────────────────────────────
25
26
27 async def _create_repo(client: AsyncClient, auth_headers: StrDict, name: str = "collab-test-repo") -> str:
28 """Create a repo via the API and return its repo_id."""
29 response = await client.post(
30 "/api/repos",
31 json={"name": name, "owner": "testuser"},
32 headers=auth_headers,
33 )
34 assert response.status_code == 201, response.text
35 repo_id: str = response.json()["repoId"]
36 return repo_id
37
38
39 async def _invite_collaborator(
40 client: AsyncClient,
41 auth_headers: StrDict,
42 repo_id: str,
43 handle: str = _COLLABORATOR_HANDLE,
44 permission: str = "write",
45 ) -> JSONObject:
46 """Invite a collaborator via the API."""
47 response = await client.post(
48 f"/api/repos/{repo_id}/collaborators",
49 json={"handle": handle, "permission": permission},
50 headers=auth_headers,
51 )
52 assert response.status_code == 201, response.text
53 data = response.json()
54 return data
55
56
57 # ── POST /collaborators ───────────────────────────────────────────────────────
58
59
60 @pytest.mark.anyio
61 async def test_invite_collaborator_returns_201(
62 client: AsyncClient,
63 auth_headers: StrDict,
64 ) -> None:
65 """Owner can invite a collaborator; response contains all required fields."""
66 repo_id = await _create_repo(client, auth_headers, "invite-201-repo")
67 data = await _invite_collaborator(client, auth_headers, repo_id)
68
69 assert data["handle"] == _COLLABORATOR_HANDLE
70 assert data["repoId"] == repo_id
71 assert data["permission"] == "write"
72 assert "collaboratorId" in data
73
74
75 @pytest.mark.anyio
76 async def test_invite_collaborator_duplicate_returns_409(
77 client: AsyncClient,
78 auth_headers: StrDict,
79 ) -> None:
80 """Inviting the same user twice returns 409 Conflict."""
81 repo_id = await _create_repo(client, auth_headers, "invite-dup-repo")
82 await _invite_collaborator(client, auth_headers, repo_id)
83
84 response = await client.post(
85 f"/api/repos/{repo_id}/collaborators",
86 json={"handle": _COLLABORATOR_HANDLE, "permission": "read"},
87 headers=auth_headers,
88 )
89 assert response.status_code == 409
90
91
92 @pytest.mark.anyio
93 async def test_invite_collaborator_unknown_repo_returns_404(
94 client: AsyncClient,
95 auth_headers: StrDict,
96 ) -> None:
97 """Inviting a collaborator to a non-existent repo returns 404."""
98 response = await client.post(
99 "/api/repos/nonexistent-repo-id/collaborators",
100 json={"handle": _COLLABORATOR_HANDLE, "permission": "read"},
101 headers=auth_headers,
102 )
103 assert response.status_code == 404
104
105
106 @pytest.mark.anyio
107 async def test_invite_collaborator_requires_auth(
108 client: AsyncClient,
109 ) -> None:
110 """POST /collaborators returns 401 without a MSign Authorization header."""
111 response = await client.post(
112 "/api/repos/some-repo/collaborators",
113 json={"handle": _COLLABORATOR_HANDLE, "permission": "read"},
114 )
115 assert response.status_code == 401
116
117
118 # ── GET /collaborators ────────────────────────────────────────────────────────
119
120
121 @pytest.mark.anyio
122 async def test_list_collaborators_empty(
123 client: AsyncClient,
124 auth_headers: StrDict,
125 ) -> None:
126 """GET /collaborators returns empty list for a repo with no collaborators."""
127 repo_id = await _create_repo(client, auth_headers, "list-empty-repo")
128 response = await client.get(
129 f"/api/repos/{repo_id}/collaborators",
130 headers=auth_headers,
131 )
132 assert response.status_code == 200
133 body = response.json()
134 assert body["total"] == 0
135 assert body["collaborators"] == []
136
137
138 @pytest.mark.anyio
139 async def test_list_collaborators_after_invite(
140 client: AsyncClient,
141 auth_headers: StrDict,
142 ) -> None:
143 """GET /collaborators returns the invited collaborator after POST."""
144 repo_id = await _create_repo(client, auth_headers, "list-after-invite-repo")
145 await _invite_collaborator(client, auth_headers, repo_id)
146
147 response = await client.get(
148 f"/api/repos/{repo_id}/collaborators",
149 headers=auth_headers,
150 )
151 assert response.status_code == 200
152 body = response.json()
153 assert body["total"] == 1
154 assert body["collaborators"][0]["handle"] == _COLLABORATOR_HANDLE
155
156
157 # ── GET /collaborators/{user_id}/permission ───────────────────────────────────
158
159
160 @pytest.mark.anyio
161 async def test_check_permission_not_collaborator(
162 client: AsyncClient,
163 auth_headers: StrDict,
164 ) -> None:
165 """Permission check returns 404 for a non-member user (access-check semantics)."""
166 repo_id = await _create_repo(client, auth_headers, "perm-check-not-member-repo")
167 response = await client.get(
168 f"/api/repos/{repo_id}/collaborators/{_COLLABORATOR_HANDLE}/permission",
169 headers=auth_headers,
170 )
171 assert response.status_code == 404
172 assert _COLLABORATOR_HANDLE in response.json()["detail"]
173
174
175 @pytest.mark.anyio
176 async def test_check_permission_is_collaborator(
177 client: AsyncClient,
178 auth_headers: StrDict,
179 ) -> None:
180 """Permission check returns username and permission level after invite."""
181 repo_id = await _create_repo(client, auth_headers, "perm-check-member-repo")
182 await _invite_collaborator(client, auth_headers, repo_id, permission="admin")
183
184 response = await client.get(
185 f"/api/repos/{repo_id}/collaborators/{_COLLABORATOR_HANDLE}/permission",
186 headers=auth_headers,
187 )
188 assert response.status_code == 200
189 body = response.json()
190 assert body["username"] == _COLLABORATOR_HANDLE
191 assert body["permission"] == "admin"
192
193
194 # ── PUT /collaborators/{user_id}/permission ───────────────────────────────────
195
196
197 @pytest.mark.anyio
198 async def test_update_permission_success(
199 client: AsyncClient,
200 auth_headers: StrDict,
201 ) -> None:
202 """Owner can update a collaborator's permission level."""
203 repo_id = await _create_repo(client, auth_headers, "update-perm-repo")
204 await _invite_collaborator(client, auth_headers, repo_id, permission="read")
205
206 response = await client.put(
207 f"/api/repos/{repo_id}/collaborators/{_COLLABORATOR_HANDLE}/permission",
208 json={"permission": "admin"},
209 headers=auth_headers,
210 )
211 assert response.status_code == 200
212 body = response.json()
213 assert body["permission"] == "admin"
214
215
216 @pytest.mark.anyio
217 async def test_update_permission_not_found_returns_404(
218 client: AsyncClient,
219 auth_headers: StrDict,
220 ) -> None:
221 """Updating permission for a non-collaborator returns 404."""
222 repo_id = await _create_repo(client, auth_headers, "update-perm-404-repo")
223 response = await client.put(
224 f"/api/repos/{repo_id}/collaborators/{_COLLABORATOR_HANDLE}/permission",
225 json={"permission": "admin"},
226 headers=auth_headers,
227 )
228 assert response.status_code == 404
229
230
231 # ── DELETE /collaborators/{user_id} ──────────────────────────────────────────
232
233
234 @pytest.mark.anyio
235 async def test_remove_collaborator_success(
236 client: AsyncClient,
237 auth_headers: StrDict,
238 ) -> None:
239 """Owner can remove a collaborator; subsequent list shows 0 collaborators."""
240 repo_id = await _create_repo(client, auth_headers, "remove-collab-repo")
241 await _invite_collaborator(client, auth_headers, repo_id)
242
243 response = await client.delete(
244 f"/api/repos/{repo_id}/collaborators/{_COLLABORATOR_HANDLE}",
245 headers=auth_headers,
246 )
247 assert response.status_code == 204
248
249 list_response = await client.get(
250 f"/api/repos/{repo_id}/collaborators",
251 headers=auth_headers,
252 )
253 assert list_response.json()["total"] == 0
254
255
256 @pytest.mark.anyio
257 async def test_remove_collaborator_not_found_returns_404(
258 client: AsyncClient,
259 auth_headers: StrDict,
260 ) -> None:
261 """Removing a non-collaborator returns 404."""
262 repo_id = await _create_repo(client, auth_headers, "remove-404-repo")
263 response = await client.delete(
264 f"/api/repos/{repo_id}/collaborators/{_COLLABORATOR_HANDLE}",
265 headers=auth_headers,
266 )
267 assert response.status_code == 404
File History 1 commit
sha256:a10adeeb7a0169cb9900f9806ed7a973047258abb6283724fe55e8eb68ff3f0a init: musehub initial commit Human 171 days ago