gabriel / musehub public
test_musehub_discover.py python
199 lines 7.0 KB
Raw
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor ⚠ breaking 156 days ago
1 """Tests for the MuseHub explore/discover API endpoints.
2
3 Covers acceptance criteria:
4 - test_explore_page_renders — GET /explore returns 200 HTML
5 - test_list_public_repos_empty — no public repos → empty list
6 - test_explore_only_public_repos — private repos are excluded from results
7 - test_explore_filters_by_genre — genre tag filter works
8 - test_explore_filters_by_instrumentation — instrumentation tag filter works
9 - test_explore_sorts_by_created — created sort returns newest first
10 - test_explore_pagination — page 2 returns different repos
11 """
12 from __future__ import annotations
13
14 import pytest
15 from httpx import AsyncClient
16 from sqlalchemy.ext.asyncio import AsyncSession
17
18 from musehub.db.musehub_models import MusehubRepo
19
20
21 # ---------------------------------------------------------------------------
22 # Helpers
23 # ---------------------------------------------------------------------------
24
25
26 async def _make_public_repo(
27 db_session: AsyncSession,
28 *,
29 name: str = "test-jazz-repo",
30 tags: list[str] | None = None,
31 description: str = "",
32 ) -> str:
33 """Seed a public repo and return its repo_id."""
34 import re as _re
35 slug = _re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")[:64].strip("-") or "repo"
36 repo = MusehubRepo(
37 name=name,
38 owner="testuser",
39 slug=slug,
40 visibility="public",
41 owner_user_id="test-owner",
42 description=description,
43 tags=tags or [],
44 )
45 db_session.add(repo)
46 await db_session.commit()
47 await db_session.refresh(repo)
48 return str(repo.repo_id)
49
50
51 async def _make_private_repo(db_session: AsyncSession, name: str = "private-beats") -> str:
52 """Seed a private repo and return its repo_id."""
53 import re as _re
54 slug = _re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")[:64].strip("-") or "repo"
55 repo = MusehubRepo(
56 name=name,
57 owner="testuser",
58 slug=slug,
59 visibility="private",
60 owner_user_id="test-owner",
61 description="",
62 tags=[],
63 )
64 db_session.add(repo)
65 await db_session.commit()
66 await db_session.refresh(repo)
67 return str(repo.repo_id)
68
69
70 # ---------------------------------------------------------------------------
71 # UI page tests (no auth required)
72 # ---------------------------------------------------------------------------
73
74
75 async def test_explore_page_renders(client: AsyncClient) -> None:
76 """GET /explore returns 200 HTML with filter controls."""
77 response = await client.get("/explore")
78 assert response.status_code == 200
79 assert "text/html" in response.headers["content-type"]
80 body = response.text
81 assert "MuseHub" in body
82 assert "Explore" in body
83 # Filter sidebar and sort controls rendered by the Jinja2 template
84 assert "filter-form" in body
85 assert 'name="sort"' in body
86 assert 'name="license"' in body
87 assert "/explore" in body
88
89
90
91 async def test_explore_page_no_auth_required(client: AsyncClient) -> None:
92 """GET /explore must not return 401 — it is a public page."""
93 response = await client.get("/explore")
94 assert response.status_code == 200
95
96
97 # ---------------------------------------------------------------------------
98 # JSON API tests — public browse endpoint (no auth)
99 # ---------------------------------------------------------------------------
100
101
102 async def test_list_public_repos_empty(client: AsyncClient, db_session: AsyncSession) -> None:
103 """GET /api/discover/repos returns empty list when no public repos exist."""
104 response = await client.get("/api/discover/repos")
105 assert response.status_code == 200
106 body = response.json()
107 assert body["repos"] == []
108 assert body["total"] == 0
109 assert body["nextCursor"] is None
110
111
112 async def test_explore_only_public_repos(
113 client: AsyncClient, db_session: AsyncSession
114 ) -> None:
115 """Private repos must not appear in discover results."""
116 await _make_public_repo(db_session, name="public-one")
117 await _make_private_repo(db_session, name="private-one")
118
119 response = await client.get("/api/discover/repos")
120 assert response.status_code == 200
121 body = response.json()
122 assert body["total"] == 1
123 names = [r["name"] for r in body["repos"]]
124 assert "public-one" in names
125 assert "private-one" not in names
126
127
128 async def test_explore_filters_by_genre(
129 client: AsyncClient, db_session: AsyncSession
130 ) -> None:
131 """genre= filter returns only repos whose tags contain the genre string."""
132 await _make_public_repo(db_session, name="jazz-project", tags=["jazz", "swing"])
133 await _make_public_repo(db_session, name="lofi-project", tags=["lo-fi", "chill"])
134
135 response = await client.get("/api/discover/repos?genre=jazz")
136 assert response.status_code == 200
137 body = response.json()
138 assert body["total"] == 1
139 assert body["repos"][0]["name"] == "jazz-project"
140
141
142 async def test_explore_filters_by_instrumentation(
143 client: AsyncClient, db_session: AsyncSession
144 ) -> None:
145 """instrumentation= filter matches repos whose tags include the instrument."""
146 await _make_public_repo(db_session, name="bass-heavy", tags=["jazz", "bass", "drums"])
147 await _make_public_repo(db_session, name="keys-only", tags=["ambient", "keys"])
148
149 response = await client.get("/api/discover/repos?instrumentation=bass")
150 assert response.status_code == 200
151 body = response.json()
152 assert body["total"] == 1
153 assert body["repos"][0]["name"] == "bass-heavy"
154
155
156 async def test_explore_sorts_by_created(
157 client: AsyncClient, db_session: AsyncSession
158 ) -> None:
159 """sort=created returns newest repos first (default sort)."""
160 await _make_public_repo(db_session, name="first-created")
161 await _make_public_repo(db_session, name="second-created")
162
163 response = await client.get("/api/discover/repos?sort=created")
164 assert response.status_code == 200
165 body = response.json()
166 # Newest first — second-created was inserted last
167 names = [r["name"] for r in body["repos"]]
168 assert names.index("second-created") < names.index("first-created")
169
170
171 async def test_explore_pagination(
172 client: AsyncClient, db_session: AsyncSession
173 ) -> None:
174 """Cursor pagination: second page returns a different set of repos than page 1."""
175 for i in range(5):
176 await _make_public_repo(db_session, name=f"repo-{i:02d}")
177
178 page1 = (await client.get("/api/discover/repos?limit=3")).json()
179 assert page1["total"] == 5
180 assert page1["nextCursor"] is not None
181
182 next_cursor = page1["nextCursor"]
183 page2 = (await client.get(f"/api/discover/repos?limit=3&cursor={next_cursor}")).json()
184
185 assert page2["total"] == 5
186 page1_ids = {r["repoId"] for r in page1["repos"]}
187 page2_ids = {r["repoId"] for r in page2["repos"]}
188 # Pages must not overlap
189 assert not page1_ids & page2_ids
190
191
192 async def test_explore_invalid_sort_returns_422(
193 client: AsyncClient, db_session: AsyncSession
194 ) -> None:
195 """sort= with an invalid value returns 422 Unprocessable Entity."""
196 response = await client.get("/api/discover/repos?sort=invalid")
197 assert response.status_code == 422
198
199
File History 1 commit
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor ⚠ 156 days ago