"""HTTP-level tests for the /projects surface.""" from __future__ import annotations import pytest from tests.conftest import sample_project_payload def test_register_project_returns_owner(client): tc, ctx = client r = tc.post( "/projects", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, json=sample_project_payload(name="proj-alpha"), ) assert r.status_code == 200, r.text body = r.json() assert body["ok"] is True assert body["project"]["name"] == "proj-alpha" def test_register_requires_auth(client): tc, _ = client r = tc.post("/projects", json=sample_project_payload()) assert r.status_code == 401 def test_register_rejects_unknown_token(client): tc, _ = client r = tc.post( "/projects", headers={"Authorization": "Bearer ct_definitely_not_real"}, json=sample_project_payload(), ) assert r.status_code == 403 def test_list_projects_filters_by_token(client): tc, ctx = client tc.post( "/projects", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, json=sample_project_payload(name="alpha-1"), ) tc.post( "/projects", headers={"Authorization": f"Bearer {ctx['bravo_bearer']}"}, json=sample_project_payload(name="bravo-1"), ) r_a = tc.get("/projects", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}) assert r_a.status_code == 200 names = {p["name"] for p in r_a.json()["projects"]} assert names == {"alpha-1"} # Admin sees both r_admin = tc.get("/projects", headers={"Authorization": f"Bearer {ctx['admin_bearer']}"}) admin_names = {p["name"] for p in r_admin.json()["projects"]} assert admin_names == {"alpha-1", "bravo-1"} def test_get_project_404_for_other_token(client): """Existence-leak guard: bravo querying alpha's project gets 404 (not 403).""" tc, ctx = client tc.post( "/projects", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, json=sample_project_payload(name="secret-proj"), ) r = tc.get( "/projects/secret-proj", headers={"Authorization": f"Bearer {ctx['bravo_bearer']}"}, ) assert r.status_code == 404 def test_get_project_own(client): tc, ctx = client tc.post( "/projects", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, json=sample_project_payload(name="own-proj"), ) r = tc.get( "/projects/own-proj", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, ) assert r.status_code == 200 assert r.json()["project"]["name"] == "own-proj" def test_update_project_owner_can(client): tc, ctx = client tc.post( "/projects", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, json=sample_project_payload(name="upd-proj"), ) payload = sample_project_payload(name="upd-proj") payload["git_url"] = "https://changed.example/repo.git" r = tc.put( "/projects/upd-proj", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, json=payload, ) assert r.status_code == 200 assert r.json()["project"]["git_url"] == "https://changed.example/repo.git" def test_update_project_other_token_404(client): tc, ctx = client tc.post( "/projects", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, json=sample_project_payload(name="no-touch"), ) payload = sample_project_payload(name="no-touch") payload["git_url"] = "https://hijack.example/x.git" r = tc.put( "/projects/no-touch", headers={"Authorization": f"Bearer {ctx['bravo_bearer']}"}, json=payload, ) assert r.status_code == 404 def test_delete_project_owner_can(client): tc, ctx = client tc.post( "/projects", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, json=sample_project_payload(name="del-proj"), ) r = tc.delete( "/projects/del-proj", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, ) assert r.status_code == 200 # Confirm gone r2 = tc.get( "/projects/del-proj", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, ) assert r2.status_code == 404 def test_delete_project_other_token_404(client): tc, ctx = client tc.post( "/projects", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, json=sample_project_payload(name="hands-off"), ) r = tc.delete( "/projects/hands-off", headers={"Authorization": f"Bearer {ctx['bravo_bearer']}"}, ) assert r.status_code == 404 def test_admin_can_modify_any_project(client): tc, ctx = client tc.post( "/projects", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, json=sample_project_payload(name="adminable"), ) payload = sample_project_payload(name="adminable") payload["git_url"] = "https://admin-edit.example/x.git" r = tc.put( "/projects/adminable", headers={"Authorization": f"Bearer {ctx['admin_bearer']}"}, json=payload, ) assert r.status_code == 200 assert r.json()["project"]["git_url"] == "https://admin-edit.example/x.git" def test_register_duplicate_409_for_owner(client): tc, ctx = client tc.post( "/projects", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, json=sample_project_payload(name="dup"), ) r = tc.post( "/projects", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, json=sample_project_payload(name="dup"), ) assert r.status_code == 409 def test_register_duplicate_404_for_other(client): """Other-token re-registering an existing name gets 404 (existence leak guard).""" tc, ctx = client tc.post( "/projects", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}, json=sample_project_payload(name="hidden"), ) r = tc.post( "/projects", headers={"Authorization": f"Bearer {ctx['bravo_bearer']}"}, json=sample_project_payload(name="hidden"), ) assert r.status_code == 404 def test_admin_token_endpoint_admin_only(client): tc, ctx = client r = tc.get("/admin/tokens", headers={"Authorization": f"Bearer {ctx['alpha_bearer']}"}) assert r.status_code == 403 r2 = tc.get("/admin/tokens", headers={"Authorization": f"Bearer {ctx['admin_bearer']}"}) assert r2.status_code == 200 def test_admin_revoke_token(client): tc, ctx = client r = tc.delete( f"/admin/tokens/{ctx['bravo_name']}", headers={"Authorization": f"Bearer {ctx['admin_bearer']}"}, ) assert r.status_code == 200 # Bravo's token should now fail r2 = tc.get("/projects", headers={"Authorization": f"Bearer {ctx['bravo_bearer']}"}) assert r2.status_code == 403 def test_admin_cannot_revoke_admin(client): tc, ctx = client r = tc.delete( "/admin/tokens/admin", headers={"Authorization": f"Bearer {ctx['admin_bearer']}"}, ) assert r.status_code == 400