gabriel / musehub public

test_musehub_discover.py file-level

at sha256:3 · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 💥 blast risk
sha256:0 fix: fall back to any indexed mpack in read_object_bytes when push mpac… · gabriel · Jun 17, 2026
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 from datetime import datetime, timezone
15
16 import pytest
17 from httpx import AsyncClient
18 from sqlalchemy.ext.asyncio import AsyncSession
19
20 from musehub.core.genesis import compute_identity_id, compute_repo_id
21 from musehub.db.musehub_repo_models import MusehubRepo
22
23
24 # ---------------------------------------------------------------------------
25 # Helpers
26 # ---------------------------------------------------------------------------
27
28
29 async def _make_public_repo(
30 db_session: AsyncSession,
31 *,
32 name: str = "test-jazz-repo",
33 tags: list[str] | None = None,
34 description: str = "",
35 ) -> str:
36 """Seed a public repo and return its repo_id."""
37 import re as _re
38 slug = _re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")[:64].strip("-") or "repo"
39 created_at = datetime.now(tz=timezone.utc)
40 owner_id = compute_identity_id(b"testuser")
41 repo = MusehubRepo(
42 repo_id=compute_repo_id(owner_id, slug, "code", created_at.isoformat()),
43 name=name,
44 owner="testuser",
45 slug=slug,
46 visibility="public",
47 owner_user_id=owner_id,
48 description=description,
49 tags=tags or [],
50 created_at=created_at,
51 updated_at=created_at,
52 )
53 db_session.add(repo)
54 await db_session.commit()
55 await db_session.refresh(repo)
56 return str(repo.repo_id)
57
58
59 async def _make_private_repo(db_session: AsyncSession, name: str = "private-beats") -> str:
60 """Seed a private repo and return its repo_id."""
61 import re as _re
62 slug = _re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-")[:64].strip("-") or "repo"
63 created_at = datetime.now(tz=timezone.utc)
64 owner_id = compute_identity_id(b"testuser")
65 repo = MusehubRepo(
66 repo_id=compute_repo_id(owner_id, slug, "code", created_at.isoformat()),
67 name=name,
68 owner="testuser",
69 slug=slug,
70 visibility="private",
71 owner_user_id=owner_id,
72 description="",
73 tags=[],
74 created_at=created_at,
75 updated_at=created_at,
76 )
77 db_session.add(repo)
78 await db_session.commit()
79 await db_session.refresh(repo)
80 return str(repo.repo_id)
81
82
83 # ---------------------------------------------------------------------------
84 # UI page tests (no auth required)
85 # ---------------------------------------------------------------------------
86
87
88 async def test_explore_page_renders(client: AsyncClient) -> None:
89 """GET /explore returns 200 HTML with filter controls."""
90 response = await client.get("/explore")
91 assert response.status_code == 200
92 assert "text/html" in response.headers["content-type"]
93 body = response.text
94 assert "MuseHub" in body
95 assert "Explore" in body
96 # Filter sidebar and sort controls rendered by the Jinja2 template
97 assert "filter-form" in body
98 assert 'name="sort"' in body
99 assert 'name="license"' in body
100 assert "/explore" in body
101
102
103
104 async def test_explore_page_no_auth_required(client: AsyncClient) -> None:
105 """GET /explore must not return 401 — it is a public page."""
106 response = await client.get("/explore")
107 assert response.status_code == 200
108
109
110 # ---------------------------------------------------------------------------
111 # JSON API tests — public browse endpoint (no auth)
112 # ---------------------------------------------------------------------------
113
114
115 async def test_list_public_repos_empty(client: AsyncClient, db_session: AsyncSession) -> None:
116 """GET /api/discover/repos returns empty list when no public repos exist."""
117 response = await client.get("/api/discover/repos")
118 assert response.status_code == 200
119 body = response.json()
120 assert body["repos"] == []
121 assert body["total"] == 0
122 assert body["nextCursor"] is None
123
124
125 async def test_explore_only_public_repos(
126 client: AsyncClient, db_session: AsyncSession
127 ) -> None:
128 """Private repos must not appear in discover results."""
129 await _make_public_repo(db_session, name="public-one")
130 await _make_private_repo(db_session, name="private-one")
131
132 response = await client.get("/api/discover/repos")
133 assert response.status_code == 200
134 body = response.json()
135 assert body["total"] == 1
136 names = [r["name"] for r in body["repos"]]
137 assert "public-one" in names
138 assert "private-one" not in names
139
140
141 async def test_explore_filters_by_genre(
142 client: AsyncClient, db_session: AsyncSession
143 ) -> None:
144 """genre= filter returns only repos whose tags contain the genre string."""
145 await _make_public_repo(db_session, name="jazz-project", tags=["jazz", "swing"])
146 await _make_public_repo(db_session, name="lofi-project", tags=["lo-fi", "chill"])
147
148 response = await client.get("/api/discover/repos?genre=jazz")
149 assert response.status_code == 200
150 body = response.json()
151 assert body["total"] == 1
152 assert body["repos"][0]["name"] == "jazz-project"
153
154
155 async def test_explore_filters_by_instrumentation(
156 client: AsyncClient, db_session: AsyncSession
157 ) -> None:
158 """instrumentation= filter matches repos whose tags include the instrument."""
159 await _make_public_repo(db_session, name="bass-heavy", tags=["jazz", "bass", "drums"])
160 await _make_public_repo(db_session, name="keys-only", tags=["ambient", "keys"])
161
162 response = await client.get("/api/discover/repos?instrumentation=bass")
163 assert response.status_code == 200
164 body = response.json()
165 assert body["total"] == 1
166 assert body["repos"][0]["name"] == "bass-heavy"
167
168
169 async def test_explore_sorts_by_created(
170 client: AsyncClient, db_session: AsyncSession
171 ) -> None:
172 """sort=created returns newest repos first (default sort)."""
173 await _make_public_repo(db_session, name="first-created")
174 await _make_public_repo(db_session, name="second-created")
175
176 response = await client.get("/api/discover/repos?sort=created")
177 assert response.status_code == 200
178 body = response.json()
179 # Newest first — second-created was inserted last
180 names = [r["name"] for r in body["repos"]]
181 assert names.index("second-created") < names.index("first-created")
182
183
184 async def test_explore_pagination(
185 client: AsyncClient, db_session: AsyncSession
186 ) -> None:
187 """Cursor pagination: second page returns a different set of repos than page 1."""
188 for i in range(5):
189 await _make_public_repo(db_session, name=f"repo-{i:02d}")
190
191 page1 = (await client.get("/api/discover/repos?limit=3")).json()
192 assert page1["total"] == 5
193 assert page1["nextCursor"] is not None
194
195 next_cursor = page1["nextCursor"]
196 page2 = (await client.get(f"/api/discover/repos?limit=3&cursor={next_cursor}")).json()
197
198 assert page2["total"] == 5
199 page1_ids = {r["repoId"] for r in page1["repos"]}
200 page2_ids = {r["repoId"] for r in page2["repos"]}
201 # Pages must not overlap
202 assert not page1_ids & page2_ids
203
204
205 async def test_explore_invalid_sort_returns_422(
206 client: AsyncClient, db_session: AsyncSession
207 ) -> None:
208 """sort= with an invalid value returns 422 Unprocessable Entity."""
209 response = await client.get("/api/discover/repos?sort=invalid")
210 assert response.status_code == 422
211
212