gabriel / musehub public
test_musehub_auth.py python
188 lines 6.9 KB
cd448303 Initial extraction of MuseHub from maestro monorepo. Gabriel Cardona <gabriel@tellurstori.com> 7d ago
1 """Auth guard tests for Muse Hub routes.
2
3 Auth model (updated in Phase 0–4 UX overhaul):
4 - GET endpoints use ``optional_token`` — public repos are accessible
5 unauthenticated; private repos return 401.
6 - POST / DELETE / write endpoints always use ``require_valid_token``.
7 - Non-existent repos return 404 regardless of auth status (no auth
8 pre-filter that exposes 401 before a DB lookup for GET routes).
9
10 Covers:
11 - Write endpoints (POST/DELETE) always return 401 without a token.
12 - GET endpoints return 404 (not 401) for non-existent repos without a token,
13 because the auth check is deferred to the visibility guard.
14 - GET endpoints return 401 for real private repos without a token.
15 - Valid tokens are accepted on write endpoints.
16 """
17 from __future__ import annotations
18
19 import pytest
20 from httpx import AsyncClient
21
22
23 # ---------------------------------------------------------------------------
24 # Write endpoints — always require auth (401 without token)
25 # ---------------------------------------------------------------------------
26
27
28 @pytest.mark.anyio
29 async def test_hub_routes_require_auth_create_repo(client: AsyncClient) -> None:
30 """POST /musehub/repos returns 401 without a Bearer token."""
31 response = await client.post(
32 "/api/v1/musehub/repos",
33 json={"name": "beats", "owner": "testuser"},
34 )
35 assert response.status_code == 401
36
37
38 @pytest.mark.anyio
39 async def test_hub_routes_require_auth_create_issue(client: AsyncClient) -> None:
40 """POST /musehub/repos/{id}/issues returns 401 without a Bearer token."""
41 response = await client.post(
42 "/api/v1/musehub/repos/any-repo-id/issues",
43 json={"title": "Bug report"},
44 )
45 assert response.status_code == 401
46
47
48 @pytest.mark.anyio
49 async def test_hub_routes_require_auth_close_issue(client: AsyncClient) -> None:
50 """POST /musehub/repos/{id}/issues/{n}/close returns 401 without a Bearer token."""
51 response = await client.post("/api/v1/musehub/repos/any-repo-id/issues/1/close")
52 assert response.status_code == 401
53
54
55 # ---------------------------------------------------------------------------
56 # GET endpoints — non-existent repos return 404 (not 401) without token
57 #
58 # Rationale: optional_token + visibility guard — unauthenticated requests
59 # reach the DB; a non-existent repo returns 404 before the auth check fires.
60 # The old test expectation of 401 was incorrect for the new auth model.
61 # ---------------------------------------------------------------------------
62
63
64 @pytest.mark.anyio
65 async def test_hub_get_nonexistent_repo_returns_404_without_auth(
66 client: AsyncClient,
67 ) -> None:
68 """GET /musehub/repos/{id} returns 404 for a non-existent repo without auth.
69
70 The auth check is now visibility-based: the DB lookup happens first,
71 so a missing repo returns 404 regardless of auth status.
72 """
73 response = await client.get("/api/v1/musehub/repos/non-existent-repo-id")
74 assert response.status_code == 404
75
76
77 @pytest.mark.anyio
78 async def test_hub_get_nonexistent_branches_returns_404_without_auth(
79 client: AsyncClient,
80 ) -> None:
81 """GET /musehub/repos/{id}/branches returns 404 for a non-existent repo without auth."""
82 response = await client.get("/api/v1/musehub/repos/non-existent-repo-id/branches")
83 assert response.status_code == 404
84
85
86 @pytest.mark.anyio
87 async def test_hub_get_nonexistent_commits_returns_404_without_auth(
88 client: AsyncClient,
89 ) -> None:
90 """GET /musehub/repos/{id}/commits returns 404 for a non-existent repo without auth."""
91 response = await client.get("/api/v1/musehub/repos/non-existent-repo-id/commits")
92 assert response.status_code == 404
93
94
95 @pytest.mark.anyio
96 async def test_hub_get_nonexistent_issues_returns_404_without_auth(
97 client: AsyncClient,
98 ) -> None:
99 """GET /musehub/repos/{id}/issues returns 404 for a non-existent repo without auth."""
100 response = await client.get("/api/v1/musehub/repos/non-existent-repo-id/issues")
101 assert response.status_code == 404
102
103
104 @pytest.mark.anyio
105 async def test_hub_get_nonexistent_issue_returns_404_without_auth(
106 client: AsyncClient,
107 ) -> None:
108 """GET /musehub/repos/{id}/issues/1 returns 404 for a non-existent repo without auth."""
109 response = await client.get("/api/v1/musehub/repos/non-existent-repo-id/issues/1")
110 assert response.status_code == 404
111
112
113 # ---------------------------------------------------------------------------
114 # Private repo visibility — GET returns 401 for private repos without token
115 # ---------------------------------------------------------------------------
116
117
118 @pytest.mark.anyio
119 async def test_private_repo_returns_401_without_auth(
120 client: AsyncClient,
121 auth_headers: dict[str, str],
122 ) -> None:
123 """GET /musehub/repos/{id} returns 401 for a private repo without a token.
124
125 Creates a private repo, then verifies unauthenticated access returns 401.
126 """
127 # Create a private repo
128 create_resp = await client.post(
129 "/api/v1/musehub/repos",
130 json={"name": "private-auth-test", "owner": "authtest", "visibility": "private"},
131 headers=auth_headers,
132 )
133 assert create_resp.status_code == 201
134 repo_id = create_resp.json()["repoId"]
135
136 # Unauthenticated access should return 401
137 unauth_resp = await client.get(f"/api/v1/musehub/repos/{repo_id}")
138 assert unauth_resp.status_code == 401, (
139 f"Expected 401 for private repo, got {unauth_resp.status_code}"
140 )
141
142
143 @pytest.mark.anyio
144 async def test_public_repo_accessible_without_auth(
145 client: AsyncClient,
146 auth_headers: dict[str, str],
147 ) -> None:
148 """GET /musehub/repos/{id} returns 200 for a public repo without a token.
149
150 Verifies the new auth model: public repos are browseable anonymously.
151 """
152 # Create a public repo
153 create_resp = await client.post(
154 "/api/v1/musehub/repos",
155 json={"name": "public-auth-test", "owner": "authtest", "visibility": "public"},
156 headers=auth_headers,
157 )
158 assert create_resp.status_code == 201
159 repo_id = create_resp.json()["repoId"]
160
161 # Unauthenticated access should return 200
162 unauth_resp = await client.get(f"/api/v1/musehub/repos/{repo_id}")
163 assert unauth_resp.status_code == 200, (
164 f"Expected 200 for public repo, got {unauth_resp.status_code}: {unauth_resp.text}"
165 )
166
167
168 # ---------------------------------------------------------------------------
169 # Sanity check — authenticated requests are NOT blocked
170 # ---------------------------------------------------------------------------
171
172
173 @pytest.mark.anyio
174 async def test_hub_routes_accept_valid_token(
175 client: AsyncClient,
176 auth_headers: dict[str, str],
177 ) -> None:
178 """POST /musehub/repos succeeds (201) with a valid Bearer token.
179
180 Ensures the auth dependency passes through valid tokens — guards against
181 accidentally blocking all traffic.
182 """
183 response = await client.post(
184 "/api/v1/musehub/repos",
185 json={"name": "auth-sanity-repo", "owner": "testuser"},
186 headers=auth_headers,
187 )
188 assert response.status_code == 201