"""Section 28 — Labels: 7-layer test suite. Covers: musehub/api/routes/musehub/labels.py, musehub/db/musehub_label_models.py Layers: 1. Unit — pure-function tests, no DB, no HTTP 2. Integration — real DB session, service-level calls 3. End-to-End — full HTTP stack via AsyncClient 4. Stress — concurrency, bulk operations 5. Data Integrity— constraint enforcement, rollback 6. Security — auth bypass, privilege escalation 7. Performance — latency budgets, query efficiency """ from __future__ import annotations import time import uuid from typing import AsyncGenerator import pytest from httpx import AsyncClient from sqlalchemy import text from sqlalchemy.ext.asyncio import AsyncSession from musehub.muse_contracts.json_types import StrDict from musehub.api.routes.musehub.labels import ( DEFAULT_LABELS, LabelCreate, LabelListResponse, LabelResponse, LabelUpdate, AssignLabelsRequest, _get_label_or_404, seed_default_labels, ) from musehub.db.musehub_label_models import MusehubIssueLabel, MusehubLabel, MusehubProposalLabel from musehub.db.musehub_models import MusehubIssue, MusehubProposal, MusehubRepo # ── helpers ─────────────────────────────────────────────────────────────────── def _uid() -> str: return str(uuid.uuid4()) async def _db_repo(session: AsyncSession, *, visibility: str = "public") -> MusehubRepo: slug = f"label-repo-{_uid()[:8]}" repo = MusehubRepo( repo_id=_uid(), name=slug, slug=slug, owner="testuser", owner_user_id="testuser", visibility=visibility, ) session.add(repo) await session.flush() return repo async def _db_label( session: AsyncSession, repo_id: str, *, name: str | None = None, color: str = "#aabbcc", ) -> MusehubLabel: label = MusehubLabel( id=_uid(), repo_id=repo_id, name=name or f"label-{_uid()[:8]}", color=color, description="test label", ) session.add(label) await session.flush() return label async def _db_issue(session: AsyncSession, repo_id: str, *, number: int = 1) -> MusehubIssue: issue = MusehubIssue( issue_id=_uid(), repo_id=repo_id, number=number, title="Test issue", body="body", state="open", labels=[], author="testuser", ) session.add(issue) await session.flush() return issue async def _db_proposal( session: AsyncSession, repo_id: str, *, proposal_number: int = 1 ) -> MusehubProposal: proposal = MusehubProposal( proposal_id=_uid(), repo_id=repo_id, proposal_number=proposal_number, title="Test Proposal", body="", state="open", from_branch="feature", to_branch="dev", author="testuser", ) session.add(proposal) await session.flush() return proposal # ═══════════════════════════════════════════════════════════════════════════════ # Layer 1 — Unit # ═══════════════════════════════════════════════════════════════════════════════ class TestUnitLabelModels: """Pure model/schema validation — no DB, no HTTP.""" def test_default_labels_not_empty(self) -> None: assert len(DEFAULT_LABELS) > 0 def test_default_labels_have_required_fields(self) -> None: for entry in DEFAULT_LABELS: assert "name" in entry assert "color" in entry assert entry["color"].startswith("#") assert len(entry["color"]) == 7 # #rrggbb def test_default_labels_names_unique(self) -> None: names = [e["name"] for e in DEFAULT_LABELS] assert len(names) == len(set(names)) def test_label_create_valid(self) -> None: lc = LabelCreate(name="bug", color="#d73a4a") assert lc.name == "bug" assert lc.color == "#d73a4a" assert lc.description is None def test_label_create_with_description(self) -> None: lc = LabelCreate(name="bug", color="#d73a4a", description="It breaks") assert lc.description == "It breaks" def test_label_update_all_optional(self) -> None: lu = LabelUpdate() assert lu.name is None assert lu.color is None assert lu.description is None def test_label_update_partial(self) -> None: lu = LabelUpdate(color="#ffffff") assert lu.color == "#ffffff" assert lu.name is None def test_label_response_round_trip(self) -> None: lr = LabelResponse( label_id="abc", repo_id="repo1", name="bug", color="#d73a4a", description=None, ) assert lr.label_id == "abc" assert lr.repo_id == "repo1" assert lr.description is None def test_label_list_response(self) -> None: items = [ LabelResponse(label_id=_uid(), repo_id="r", name="a", color="#111111"), LabelResponse(label_id=_uid(), repo_id="r", name="b", color="#222222"), ] llr = LabelListResponse(items=items, total=2) assert llr.total == 2 assert len(llr.items) == 2 def test_assign_labels_request_requires_min_one(self) -> None: with pytest.raises(Exception): AssignLabelsRequest(label_ids=[]) def test_assign_labels_request_valid(self) -> None: req = AssignLabelsRequest(label_ids=["abc", "def"]) assert req.label_ids == ["abc", "def"] # ═══════════════════════════════════════════════════════════════════════════════ # Layer 2 — Integration # ═══════════════════════════════════════════════════════════════════════════════ class TestIntegrationLabelDB: """Real DB session, service-layer functions.""" @pytest.mark.anyio async def test_seed_default_labels_inserts_all(self, db_session: AsyncSession) -> None: repo = await _db_repo(db_session) await db_session.commit() await seed_default_labels(db_session, repo.repo_id) await db_session.commit() result = await db_session.execute( text("SELECT COUNT(*) FROM musehub_labels WHERE repo_id = :rid"), {"rid": repo.repo_id}, ) count = result.scalar_one() assert count == len(DEFAULT_LABELS) @pytest.mark.anyio async def test_seed_default_labels_idempotent(self, db_session: AsyncSession) -> None: repo = await _db_repo(db_session) await db_session.commit() await seed_default_labels(db_session, repo.repo_id) await db_session.commit() await seed_default_labels(db_session, repo.repo_id) await db_session.commit() result = await db_session.execute( text("SELECT COUNT(*) FROM musehub_labels WHERE repo_id = :rid"), {"rid": repo.repo_id}, ) assert result.scalar_one() == len(DEFAULT_LABELS) @pytest.mark.anyio async def test_get_label_or_404_found(self, db_session: AsyncSession) -> None: repo = await _db_repo(db_session) label = await _db_label(db_session, repo.repo_id, name="found-label") await db_session.commit() result = await _get_label_or_404(db_session, repo.repo_id, label.id) assert result.name == "found-label" @pytest.mark.anyio async def test_get_label_or_404_missing(self, db_session: AsyncSession) -> None: from fastapi import HTTPException repo = await _db_repo(db_session) await db_session.commit() with pytest.raises(HTTPException) as exc_info: await _get_label_or_404(db_session, repo.repo_id, "nonexistent-id") assert exc_info.value.status_code == 404 @pytest.mark.anyio async def test_label_unique_constraint_within_repo(self, db_session: AsyncSession) -> None: from sqlalchemy.exc import IntegrityError repo = await _db_repo(db_session) await _db_label(db_session, repo.repo_id, name="duplicate") await db_session.flush() label2 = MusehubLabel( id=_uid(), repo_id=repo.repo_id, name="duplicate", color="#000000" ) db_session.add(label2) with pytest.raises(IntegrityError): await db_session.flush() @pytest.mark.anyio async def test_same_name_different_repos_allowed(self, db_session: AsyncSession) -> None: repo1 = await _db_repo(db_session) repo2 = await _db_repo(db_session) await _db_label(db_session, repo1.repo_id, name="shared-name") await _db_label(db_session, repo2.repo_id, name="shared-name") await db_session.flush() # no error expected @pytest.mark.anyio async def test_issue_label_assignment_db(self, db_session: AsyncSession) -> None: repo = await _db_repo(db_session) label = await _db_label(db_session, repo.repo_id) issue = await _db_issue(db_session, repo.repo_id) await db_session.commit() il = MusehubIssueLabel(issue_id=issue.issue_id, label_id=label.id) db_session.add(il) await db_session.flush() result = await db_session.execute( text("SELECT COUNT(*) FROM musehub_issue_labels WHERE issue_id = :iid"), {"iid": issue.issue_id}, ) assert result.scalar_one() == 1 @pytest.mark.anyio async def test_proposal_label_assignment_db(self, db_session: AsyncSession) -> None: repo = await _db_repo(db_session) label = await _db_label(db_session, repo.repo_id) proposal = await _db_proposal(db_session, repo.repo_id) await db_session.commit() prl = MusehubProposalLabel(proposal_id=proposal.proposal_id, label_id=label.id) db_session.add(prl) await db_session.flush() result = await db_session.execute( text("SELECT COUNT(*) FROM musehub_proposal_labels WHERE proposal_id = :pid"), {"pid": proposal.proposal_id}, ) assert result.scalar_one() == 1 # ═══════════════════════════════════════════════════════════════════════════════ # Layer 3 — End-to-End # ═══════════════════════════════════════════════════════════════════════════════ class TestE2ELabels: """Full HTTP stack via AsyncClient.""" @pytest.mark.anyio async def test_list_labels_empty( self, client: AsyncClient, db_session: AsyncSession ) -> None: repo = await _db_repo(db_session) await db_session.commit() resp = await client.get(f"/api/repos/{repo.repo_id}/labels") assert resp.status_code == 200 data = resp.json() assert data["total"] == 0 assert data["items"] == [] @pytest.mark.anyio async def test_list_labels_with_data( self, client: AsyncClient, db_session: AsyncSession ) -> None: repo = await _db_repo(db_session) await _db_label(db_session, repo.repo_id, name="alpha") await _db_label(db_session, repo.repo_id, name="beta") await db_session.commit() resp = await client.get(f"/api/repos/{repo.repo_id}/labels") assert resp.status_code == 200 data = resp.json() assert data["total"] == 2 names = [i["name"] for i in data["items"]] assert "alpha" in names assert "beta" in names @pytest.mark.anyio async def test_list_labels_sorted_alphabetically( self, client: AsyncClient, db_session: AsyncSession ) -> None: repo = await _db_repo(db_session) await _db_label(db_session, repo.repo_id, name="zzz") await _db_label(db_session, repo.repo_id, name="aaa") await db_session.commit() resp = await client.get(f"/api/repos/{repo.repo_id}/labels") names = [i["name"] for i in resp.json()["items"]] assert names == sorted(names) @pytest.mark.anyio async def test_list_labels_repo_not_found(self, client: AsyncClient) -> None: resp = await client.get("/api/repos/nonexistent-repo/labels") assert resp.status_code == 404 @pytest.mark.anyio async def test_create_label_success( self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict, ) -> None: repo = await _db_repo(db_session) await db_session.commit() resp = await client.post( f"/api/repos/{repo.repo_id}/labels", json={"name": "enhancement", "color": "#a2eeef", "description": "New feature"}, headers=auth_headers, ) assert resp.status_code == 201 data = resp.json() assert data["name"] == "enhancement" assert data["color"] == "#a2eeef" assert data["label_id"] is not None @pytest.mark.anyio async def test_create_label_duplicate_name_409( self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict, ) -> None: repo = await _db_repo(db_session) await _db_label(db_session, repo.repo_id, name="existing") await db_session.commit() resp = await client.post( f"/api/repos/{repo.repo_id}/labels", json={"name": "existing", "color": "#ffffff"}, headers=auth_headers, ) assert resp.status_code == 409 @pytest.mark.anyio async def test_update_label_name( self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict, ) -> None: repo = await _db_repo(db_session) label = await _db_label(db_session, repo.repo_id, name="old-name") await db_session.commit() resp = await client.patch( f"/api/repos/{repo.repo_id}/labels/{label.id}", json={"name": "new-name"}, headers=auth_headers, ) assert resp.status_code == 200 assert resp.json()["name"] == "new-name" @pytest.mark.anyio async def test_update_label_color( self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict, ) -> None: repo = await _db_repo(db_session) label = await _db_label(db_session, repo.repo_id, name="colored", color="#000000") await db_session.commit() resp = await client.patch( f"/api/repos/{repo.repo_id}/labels/{label.id}", json={"color": "#ffffff"}, headers=auth_headers, ) assert resp.status_code == 200 assert resp.json()["color"] == "#ffffff" @pytest.mark.anyio async def test_update_label_name_conflict_409( self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict, ) -> None: repo = await _db_repo(db_session) await _db_label(db_session, repo.repo_id, name="taken") label = await _db_label(db_session, repo.repo_id, name="mine") await db_session.commit() resp = await client.patch( f"/api/repos/{repo.repo_id}/labels/{label.id}", json={"name": "taken"}, headers=auth_headers, ) assert resp.status_code == 409 @pytest.mark.anyio async def test_delete_label_success( self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict, ) -> None: repo = await _db_repo(db_session) label = await _db_label(db_session, repo.repo_id, name="to-delete") await db_session.commit() resp = await client.delete( f"/api/repos/{repo.repo_id}/labels/{label.id}", headers=auth_headers, ) assert resp.status_code == 204 @pytest.mark.anyio async def test_delete_label_not_found( self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict, ) -> None: repo = await _db_repo(db_session) await db_session.commit() resp = await client.delete( f"/api/repos/{repo.repo_id}/labels/nonexistent-id", headers=auth_headers, ) assert resp.status_code == 404 @pytest.mark.anyio async def test_list_labels_after_create( self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict, ) -> None: # Verify that a label created via POST is immediately visible via GET repo = await _db_repo(db_session) await db_session.commit() await client.post( f"/api/repos/{repo.repo_id}/labels", json={"name": "freshly-created", "color": "#aabbcc"}, headers=auth_headers, ) resp = await client.get(f"/api/repos/{repo.repo_id}/labels") assert resp.status_code == 200 names = [i["name"] for i in resp.json()["items"]] assert "freshly-created" in names @pytest.mark.anyio async def test_update_then_list_reflects_change( self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict, ) -> None: repo = await _db_repo(db_session) label = await _db_label(db_session, repo.repo_id, name="original") await db_session.commit() await client.patch( f"/api/repos/{repo.repo_id}/labels/{label.id}", json={"name": "renamed"}, headers=auth_headers, ) resp = await client.get(f"/api/repos/{repo.repo_id}/labels") names = [i["name"] for i in resp.json()["items"]] assert "renamed" in names assert "original" not in names @pytest.mark.anyio async def test_delete_then_list_removes_label( self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict, ) -> None: repo = await _db_repo(db_session) label = await _db_label(db_session, repo.repo_id, name="gone") await db_session.commit() await client.delete( f"/api/repos/{repo.repo_id}/labels/{label.id}", headers=auth_headers, ) resp = await client.get(f"/api/repos/{repo.repo_id}/labels") names = [i["name"] for i in resp.json()["items"]] assert "gone" not in names @pytest.mark.anyio async def test_assign_issue_labels_via_issues_route( self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict, ) -> None: # Note: POST /repos/{id}/issues/{n}/labels is handled by issues.py (registered # alphabetically before labels.py). That route takes {"labels": [name, ...]} — free-form # string labels stored on the issue's JSON field. repo = await _db_repo(db_session) issue = await _db_issue(db_session, repo.repo_id, number=1) await db_session.commit() resp = await client.post( f"/api/repos/{repo.repo_id}/issues/{issue.number}/labels", json={"labels": ["bug", "needs-review"]}, headers=auth_headers, ) assert resp.status_code == 200 data = resp.json() assert "bug" in data.get("labels", []) @pytest.mark.anyio async def test_assign_labels_to_proposal( self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict, ) -> None: repo = await _db_repo(db_session) label = await _db_label(db_session, repo.repo_id, name="proposal-label") proposal = await _db_proposal(db_session, repo.repo_id, proposal_number=1) await db_session.commit() resp = await client.post( f"/api/repos/{repo.repo_id}/proposals/{proposal.proposal_id}/labels", json={"label_ids": [label.id]}, headers=auth_headers, ) assert resp.status_code == 200 assigned = resp.json() assert len(assigned) == 1 @pytest.mark.anyio async def test_assign_labels_to_proposal_not_found( self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict, ) -> None: repo = await _db_repo(db_session) label = await _db_label(db_session, repo.repo_id, name="proposal-label") await db_session.commit() resp = await client.post( f"/api/repos/{repo.repo_id}/proposals/nonexistent-proposal/labels", json={"label_ids": [label.id]}, headers=auth_headers, ) assert resp.status_code == 404 @pytest.mark.anyio async def test_remove_label_from_proposal( self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict, ) -> None: repo = await _db_repo(db_session) label = await _db_label(db_session, repo.repo_id, name="proposal-removable") proposal = await _db_proposal(db_session, repo.repo_id, proposal_number=1) prl = MusehubProposalLabel(proposal_id=proposal.proposal_id, label_id=label.id) db_session.add(prl) await db_session.commit() resp = await client.delete( f"/api/repos/{repo.repo_id}/proposals/{proposal.proposal_id}/labels/{label.id}", headers=auth_headers, ) assert resp.status_code == 204 @pytest.mark.anyio async def test_create_label_returns_label_fields( self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict, ) -> None: repo = await _db_repo(db_session) await db_session.commit() resp = await client.post( f"/api/repos/{repo.repo_id}/labels", json={"name": "full-check", "color": "#123456", "description": "desc"}, headers=auth_headers, ) assert resp.status_code == 201 data = resp.json() assert data["name"] == "full-check" assert data["color"] == "#123456" assert data["description"] == "desc" assert data["repo_id"] == repo.repo_id # ═══════════════════════════════════════════════════════════════════════════════ # Layer 4 — Stress # ═══════════════════════════════════════════════════════════════════════════════ class TestStressLabels: @pytest.mark.anyio async def test_bulk_create_labels( self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict, ) -> None: repo = await _db_repo(db_session) await db_session.commit() n = 20 for i in range(n): resp = await client.post( f"/api/repos/{repo.repo_id}/labels", json={"name": f"label-{i}", "color": "#aabbcc"}, headers=auth_headers, ) assert resp.status_code == 201 resp = await client.get(f"/api/repos/{repo.repo_id}/labels") assert resp.json()["total"] == n @pytest.mark.anyio async def test_sequential_label_creates( self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict, ) -> None: # Sequential creates validate that the endpoint handles repeated calls correctly. repo = await _db_repo(db_session) await db_session.commit() for i in range(10): resp = await client.post( f"/api/repos/{repo.repo_id}/labels", json={"name": f"sequential-{i}", "color": "#aabbcc"}, headers=auth_headers, ) assert resp.status_code == 201 resp = await client.get(f"/api/repos/{repo.repo_id}/labels") assert resp.json()["total"] == 10 @pytest.mark.anyio async def test_assign_many_labels_to_proposal( self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict, ) -> None: repo = await _db_repo(db_session) labels = [await _db_label(db_session, repo.repo_id, name=f"lbl-{i}") for i in range(5)] proposal = await _db_proposal(db_session, repo.repo_id, proposal_number=1) await db_session.commit() resp = await client.post( f"/api/repos/{repo.repo_id}/proposals/{proposal.proposal_id}/labels", json={"label_ids": [l.id for l in labels]}, headers=auth_headers, ) assert resp.status_code == 200 assert len(resp.json()) == 5 # ═══════════════════════════════════════════════════════════════════════════════ # Layer 5 — Data Integrity # ═══════════════════════════════════════════════════════════════════════════════ class TestDataIntegrityLabels: @pytest.mark.anyio async def test_label_unique_constraint_db(self, db_session: AsyncSession) -> None: from sqlalchemy.exc import IntegrityError repo = await _db_repo(db_session) await _db_label(db_session, repo.repo_id, name="unique-check") await db_session.flush() dupe = MusehubLabel( id=_uid(), repo_id=repo.repo_id, name="unique-check", color="#ffffff", ) db_session.add(dupe) with pytest.raises(IntegrityError): await db_session.flush() @pytest.mark.anyio async def test_issue_label_composite_pk(self, db_session: AsyncSession) -> None: from sqlalchemy import insert from sqlalchemy.exc import IntegrityError repo = await _db_repo(db_session) label = await _db_label(db_session, repo.repo_id) issue = await _db_issue(db_session, repo.repo_id) await db_session.commit() il1 = MusehubIssueLabel(issue_id=issue.issue_id, label_id=label.id) db_session.add(il1) await db_session.flush() # Use a raw INSERT to bypass SQLAlchemy's identity map (which already # holds il1 under this PK) and hit the DB-level PK constraint directly. with pytest.raises(IntegrityError): await db_session.execute( insert(MusehubIssueLabel).values( issue_id=issue.issue_id, label_id=label.id ) ) await db_session.flush() @pytest.mark.anyio async def test_delete_label_removes_issue_associations( self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict, ) -> None: repo = await _db_repo(db_session) label = await _db_label(db_session, repo.repo_id, name="to-remove") issue = await _db_issue(db_session, repo.repo_id, number=1) # assign via DB (labels.py issue-label route is shadowed by issues.py) il = MusehubIssueLabel(issue_id=issue.issue_id, label_id=label.id) db_session.add(il) await db_session.commit() # delete label resp = await client.delete( f"/api/repos/{repo.repo_id}/labels/{label.id}", headers=auth_headers, ) assert resp.status_code == 204 # verify issue_label row gone result = await db_session.execute( text("SELECT COUNT(*) FROM musehub_issue_labels WHERE label_id = :lid"), {"lid": label.id}, ) assert result.scalar_one() == 0 @pytest.mark.anyio async def test_remove_label_from_issue_idempotent( self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict, ) -> None: repo = await _db_repo(db_session) label = await _db_label(db_session, repo.repo_id, name="idem-remove") proposal = await _db_proposal(db_session, repo.repo_id, proposal_number=1) await db_session.commit() # First delete (not assigned) should still return 204 (idempotent) resp = await client.delete( f"/api/repos/{repo.repo_id}/proposals/{proposal.proposal_id}/labels/{label.id}", headers=auth_headers, ) assert resp.status_code == 204 @pytest.mark.anyio async def test_label_color_stored_correctly(self, db_session: AsyncSession) -> None: repo = await _db_repo(db_session) label = await _db_label(db_session, repo.repo_id, name="colorcheck", color="#112233") await db_session.commit() result = await db_session.execute( text("SELECT color FROM musehub_labels WHERE id = :lid"), {"lid": label.id} ) assert result.scalar_one() == "#112233" # ═══════════════════════════════════════════════════════════════════════════════ # Layer 6 — Security # ═══════════════════════════════════════════════════════════════════════════════ class TestSecurityLabels: @pytest.mark.anyio async def test_create_label_requires_auth( self, client: AsyncClient, db_session: AsyncSession ) -> None: repo = await _db_repo(db_session) await db_session.commit() resp = await client.post( f"/api/repos/{repo.repo_id}/labels", json={"name": "unauth", "color": "#aaaaaa"}, ) assert resp.status_code == 401 @pytest.mark.anyio async def test_update_label_requires_auth( self, client: AsyncClient, db_session: AsyncSession ) -> None: repo = await _db_repo(db_session) label = await _db_label(db_session, repo.repo_id, name="secure") await db_session.commit() resp = await client.patch( f"/api/repos/{repo.repo_id}/labels/{label.id}", json={"name": "hacked"}, ) assert resp.status_code == 401 @pytest.mark.anyio async def test_delete_label_requires_auth( self, client: AsyncClient, db_session: AsyncSession ) -> None: repo = await _db_repo(db_session) label = await _db_label(db_session, repo.repo_id, name="secure") await db_session.commit() resp = await client.delete( f"/api/repos/{repo.repo_id}/labels/{label.id}" ) assert resp.status_code == 401 @pytest.mark.anyio async def test_assign_labels_to_proposal_requires_auth( self, client: AsyncClient, db_session: AsyncSession ) -> None: repo = await _db_repo(db_session) label = await _db_label(db_session, repo.repo_id, name="bug") proposal = await _db_proposal(db_session, repo.repo_id, proposal_number=1) await db_session.commit() resp = await client.post( f"/api/repos/{repo.repo_id}/proposals/{proposal.proposal_id}/labels", json={"label_ids": [label.id]}, ) assert resp.status_code == 401 @pytest.mark.anyio async def test_list_labels_public_no_auth( self, client: AsyncClient, db_session: AsyncSession ) -> None: repo = await _db_repo(db_session, visibility="public") await _db_label(db_session, repo.repo_id, name="open") await db_session.commit() resp = await client.get(f"/api/repos/{repo.repo_id}/labels") assert resp.status_code == 200 @pytest.mark.anyio async def test_create_label_wrong_repo_404( self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict, ) -> None: resp = await client.post( "/api/repos/nonexistent-repo/labels", json={"name": "bug", "color": "#ff0000"}, headers=auth_headers, ) assert resp.status_code == 404 @pytest.mark.anyio async def test_assign_label_from_different_repo_to_proposal_404( self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict, ) -> None: """Label belonging to repo2 cannot be assigned on repo1's proposal.""" repo1 = await _db_repo(db_session) repo2 = await _db_repo(db_session) label_in_repo2 = await _db_label(db_session, repo2.repo_id, name="foreign-label") proposal = await _db_proposal(db_session, repo1.repo_id, proposal_number=1) await db_session.commit() resp = await client.post( f"/api/repos/{repo1.repo_id}/proposals/{proposal.proposal_id}/labels", json={"label_ids": [label_in_repo2.id]}, headers=auth_headers, ) # _get_label_or_404 checks (repo_id, label_id) pair — should 404 assert resp.status_code == 404 @pytest.mark.anyio async def test_remove_label_from_proposal_requires_auth( self, client: AsyncClient, db_session: AsyncSession ) -> None: repo = await _db_repo(db_session) label = await _db_label(db_session, repo.repo_id, name="secure-proposal") proposal = await _db_proposal(db_session, repo.repo_id, proposal_number=1) await db_session.commit() resp = await client.delete( f"/api/repos/{repo.repo_id}/proposals/{proposal.proposal_id}/labels/{label.id}" ) assert resp.status_code == 401 # ═══════════════════════════════════════════════════════════════════════════════ # Layer 7 — Performance # ═══════════════════════════════════════════════════════════════════════════════ class TestPerformanceLabels: @pytest.mark.anyio async def test_list_labels_latency( self, client: AsyncClient, db_session: AsyncSession ) -> None: repo = await _db_repo(db_session) for i in range(15): await _db_label(db_session, repo.repo_id, name=f"perf-{i}") await db_session.commit() start = time.perf_counter() resp = await client.get(f"/api/repos/{repo.repo_id}/labels") elapsed = time.perf_counter() - start assert resp.status_code == 200 assert elapsed < 0.5 @pytest.mark.anyio async def test_create_label_latency( self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict, ) -> None: repo = await _db_repo(db_session) await db_session.commit() start = time.perf_counter() resp = await client.post( f"/api/repos/{repo.repo_id}/labels", json={"name": "perf-label", "color": "#112233"}, headers=auth_headers, ) elapsed = time.perf_counter() - start assert resp.status_code == 201 assert elapsed < 0.5 @pytest.mark.anyio async def test_delete_label_latency( self, client: AsyncClient, db_session: AsyncSession, auth_headers: StrDict, ) -> None: repo = await _db_repo(db_session) label = await _db_label(db_session, repo.repo_id, name="perf-delete") await db_session.commit() start = time.perf_counter() resp = await client.delete( f"/api/repos/{repo.repo_id}/labels/{label.id}", headers=auth_headers, ) elapsed = time.perf_counter() - start assert resp.status_code == 204 assert elapsed < 0.5