gabriel / musehub public
test_auth_authorization.py python
144 lines 5.9 KB
Raw
sha256:7d6dd8f4a89e2d1fef2d84f6e65feaff51385d382f466766b7f690a22ec18e32 fix: fall back to DB ancestry check when mpack-only fast-fo… Sonnet 4.6 patch 7 days ago
1 """Tests for checklist section 1.2 — Authorization guards.
2
3 Covers the three guard helpers that enforce repo-level access control:
4
5 _guard_visibility — 404 on missing repo; 401 on private + unauthenticated
6 _guard_write_access — 403 when non-owner tries to write to a private repo
7 _guard_repo_owner — 403 when non-owner attempts an owner-only action
8
9 All tests are synchronous unit tests — no DB or HTTP fixtures needed.
10 """
11 from __future__ import annotations
12
13 import pytest
14 from fastapi import HTTPException
15 from unittest.mock import AsyncMock, MagicMock
16
17
18 # ---------------------------------------------------------------------------
19 # Helpers — build lightweight repo/claims stand-ins without importing ORM
20 # ---------------------------------------------------------------------------
21
22 def _repo(*, visibility: str = "public", owner: str = "alice") -> MagicMock:
23 r = MagicMock()
24 r.visibility = visibility
25 r.owner = owner
26 return r
27
28
29 def _claims(*, handle: str = "alice") -> MagicMock:
30 c = MagicMock()
31 c.handle = handle
32 return c
33
34
35 def _db_no_collab() -> AsyncMock:
36 """Async DB session stub that returns no collaborator row."""
37 result = MagicMock()
38 result.scalar_one_or_none.return_value = None
39 db = AsyncMock()
40 db.execute = AsyncMock(return_value=result)
41 return db
42
43
44 # ---------------------------------------------------------------------------
45 # _guard_visibility
46 # ---------------------------------------------------------------------------
47
48 class TestGuardVisibility:
49 def setup_method(self) -> None:
50 from musehub.api.routes.musehub.repos import _guard_visibility
51 self.guard = _guard_visibility
52
53 def test_none_repo_raises_404(self) -> None:
54 with pytest.raises(HTTPException) as exc_info:
55 self.guard(None, _claims())
56 assert exc_info.value.status_code == 404
57
58 def test_none_repo_none_claims_raises_404(self) -> None:
59 """404 takes priority over 401 — missing repo is never exposed as 401."""
60 with pytest.raises(HTTPException) as exc_info:
61 self.guard(None, None)
62 assert exc_info.value.status_code == 404
63
64 def test_public_repo_no_claims_allowed(self) -> None:
65 """Public repo with no auth must not raise."""
66 self.guard(_repo(visibility="public"), None)
67
68 def test_public_repo_with_claims_allowed(self) -> None:
69 self.guard(_repo(visibility="public"), _claims())
70
71 def test_private_repo_with_claims_allowed(self) -> None:
72 """Authenticated user may access a private repo."""
73 self.guard(_repo(visibility="private"), _claims())
74
75 def test_private_repo_no_claims_raises_401(self) -> None:
76 with pytest.raises(HTTPException) as exc_info:
77 self.guard(_repo(visibility="private"), None)
78 assert exc_info.value.status_code == 401
79
80 def test_private_repo_401_includes_www_authenticate(self) -> None:
81 with pytest.raises(HTTPException) as exc_info:
82 self.guard(_repo(visibility="private"), None)
83 assert "WWW-Authenticate" in exc_info.value.headers
84 assert "MSign" in exc_info.value.headers["WWW-Authenticate"]
85
86
87 # ---------------------------------------------------------------------------
88 # _guard_write_access
89 # ---------------------------------------------------------------------------
90
91 class TestGuardWriteAccess:
92 def setup_method(self) -> None:
93 from musehub.api.routes.musehub.issues import _guard_write_access
94 self.guard = _guard_write_access
95
96 def test_public_repo_any_user_may_write(self) -> None:
97 """Any authenticated user can write to a public repo."""
98 self.guard(_repo(visibility="public", owner="alice"), "bob")
99
100 def test_public_repo_owner_may_write(self) -> None:
101 self.guard(_repo(visibility="public", owner="alice"), "alice")
102
103 def test_private_repo_owner_may_write(self) -> None:
104 self.guard(_repo(visibility="private", owner="alice"), "alice")
105
106 def test_private_repo_non_owner_raises_403(self) -> None:
107 with pytest.raises(HTTPException) as exc_info:
108 self.guard(_repo(visibility="private", owner="alice"), "bob")
109 assert exc_info.value.status_code == 403
110
111 def test_private_repo_403_detail_message(self) -> None:
112 with pytest.raises(HTTPException) as exc_info:
113 self.guard(_repo(visibility="private", owner="alice"), "bob")
114 assert "private" in exc_info.value.detail.lower()
115
116
117 # ---------------------------------------------------------------------------
118 # _guard_repo_owner
119 # ---------------------------------------------------------------------------
120
121 class TestGuardRepoOwner:
122 async def test_owner_allowed(self) -> None:
123 from musehub.api.routes.musehub.issues import _guard_repo_owner
124 # Owner check short-circuits before any DB call.
125 await _guard_repo_owner(_repo(owner="alice"), "alice", _db_no_collab())
126
127 async def test_non_owner_raises_403(self) -> None:
128 from musehub.api.routes.musehub.issues import _guard_repo_owner
129 with pytest.raises(HTTPException) as exc_info:
130 await _guard_repo_owner(_repo(owner="alice"), "bob", _db_no_collab())
131 assert exc_info.value.status_code == 403
132
133 async def test_non_owner_public_repo_still_raises_403(self) -> None:
134 """Even public repos: only owner may perform owner-only actions."""
135 from musehub.api.routes.musehub.issues import _guard_repo_owner
136 with pytest.raises(HTTPException) as exc_info:
137 await _guard_repo_owner(_repo(visibility="public", owner="alice"), "bob", _db_no_collab())
138 assert exc_info.value.status_code == 403
139
140 async def test_403_detail_message(self) -> None:
141 from musehub.api.routes.musehub.issues import _guard_repo_owner
142 with pytest.raises(HTTPException) as exc_info:
143 await _guard_repo_owner(_repo(owner="alice"), "bob", _db_no_collab())
144 assert "owner" in exc_info.value.detail.lower()
File History 1 commit
sha256:7d6dd8f4a89e2d1fef2d84f6e65feaff51385d382f466766b7f690a22ec18e32 fix: fall back to DB ancestry check when mpack-only fast-fo… Sonnet 4.6 patch 7 days ago