gabriel / musehub public
test_musehub_ui_search_ssr.py python
201 lines 6.0 KB
9236d457 fix: remove all inline scripts, restore strict script-src CSP (#51) Gabriel Cardona <cgcardona@gmail.com> 23h ago
1 """SSR tests for MuseHub global search page.
2
3 Verifies that global_search_page() renders results server-side in Jinja2
4 templates without requiring JavaScript execution.
5 Tests assert on HTML content directly returned by the server.
6
7 Covers GET /search (global search):
8 - test_global_search_renders_results_server_side
9 - test_global_search_no_results_shows_empty_state
10 - test_global_search_short_query_shows_prompt
11 - test_global_search_htmx_fragment_path
12 - test_global_search_empty_query_shows_prompt
13 """
14
15 from __future__ import annotations
16
17 from datetime import datetime, timezone
18
19 import pytest
20 from httpx import AsyncClient
21 from sqlalchemy.ext.asyncio import AsyncSession
22
23 from musehub.db.musehub_models import MusehubBranch, MusehubCommit, MusehubRepo
24 from musehub.muse_cli import models as _cli_models # noqa: F401 — register tables
25 from musehub.muse_cli.db import insert_commit, upsert_snapshot
26 from musehub.muse_cli.models import MuseCliCommit
27 from musehub.muse_cli.snapshot import compute_commit_id, compute_snapshot_id
28
29
30 # ---------------------------------------------------------------------------
31 # Helpers
32 # ---------------------------------------------------------------------------
33
34
35 async def _make_repo(
36 db: AsyncSession,
37 owner: str = "search_ssr_artist",
38 slug: str = "search-ssr-album",
39 visibility: str = "public",
40 ) -> str:
41 """Seed a public repo and return its repo_id string."""
42 repo = MusehubRepo(
43 name=slug,
44 owner=owner,
45 slug=slug,
46 visibility=visibility,
47 owner_user_id=f"uid-{owner}",
48 )
49 db.add(repo)
50 await db.commit()
51 await db.refresh(repo)
52 return str(repo.repo_id)
53
54
55 async def _make_musehub_commit(
56 db: AsyncSession,
57 repo_id: str,
58 *,
59 commit_id: str,
60 message: str,
61 branch: str = "main",
62 author: str = "musician",
63 ) -> None:
64 """Seed a MusehubCommit (used by global_search via musehub_commits table)."""
65 commit = MusehubCommit(
66 commit_id=commit_id,
67 repo_id=repo_id,
68 branch=branch,
69 parent_ids=[],
70 message=message,
71 author=author,
72 timestamp=datetime.now(tz=timezone.utc),
73 )
74 db_branch = MusehubBranch(
75 repo_id=repo_id,
76 name=branch,
77 head_commit_id=commit_id,
78 )
79 db.add(commit)
80 db.add(db_branch)
81 await db.commit()
82
83
84 async def _make_cli_commit(
85 db: AsyncSession,
86 repo_id: str,
87 *,
88 message: str,
89 branch: str = "main",
90 author: str = "musician",
91 ) -> str:
92 """Seed a MuseCliCommit (used by in-repo search via muse_commits table).
93
94 Returns the generated commit_id.
95 """
96 manifest: dict[str, str] = {"track.mid": "deadbeef"}
97 snap_id = compute_snapshot_id(manifest)
98 await upsert_snapshot(db, manifest=manifest, snapshot_id=snap_id)
99 committed_at = datetime.now(tz=timezone.utc)
100 commit_id = compute_commit_id(
101 parent_ids=[],
102 snapshot_id=snap_id,
103 message=message,
104 committed_at_iso=committed_at.isoformat(),
105 )
106 commit = MuseCliCommit(
107 commit_id=commit_id,
108 repo_id=repo_id,
109 branch=branch,
110 parent_commit_id=None,
111 snapshot_id=snap_id,
112 message=message,
113 author=author,
114 committed_at=committed_at,
115 )
116 await insert_commit(db, commit)
117 await db.flush()
118 return commit_id
119
120
121 # ---------------------------------------------------------------------------
122 # Global search — GET /search
123 # ---------------------------------------------------------------------------
124
125
126 @pytest.mark.anyio
127 async def test_global_search_renders_results_server_side(
128 client: AsyncClient,
129 db_session: AsyncSession,
130 ) -> None:
131 """Commit message appears in the HTML returned by the server (not injected by JS)."""
132 repo_id = await _make_repo(db_session)
133 await _make_musehub_commit(
134 db_session,
135 repo_id,
136 commit_id="aabbccddeeff00112233445566778899aabbccd1",
137 message="beatbox groove pattern in D minor",
138 )
139 response = await client.get("/search?q=beatbox")
140 assert response.status_code == 200
141 assert "text/html" in response.headers["content-type"]
142 assert "beatbox" in response.text
143
144
145 @pytest.mark.anyio
146 async def test_global_search_no_results_shows_empty_state(
147 client: AsyncClient,
148 db_session: AsyncSession,
149 ) -> None:
150 """A query with no matches renders the empty-state block."""
151 await _make_repo(db_session, owner="search_noresult", slug="noresult-album")
152 response = await client.get("/search?q=zzznomatch")
153 assert response.status_code == 200
154 assert "text/html" in response.headers["content-type"]
155 assert "No results" in response.text
156
157
158 @pytest.mark.anyio
159 async def test_global_search_short_query_shows_prompt(
160 client: AsyncClient,
161 db_session: AsyncSession,
162 ) -> None:
163 """A single-character query renders the tips/idle state (no results run)."""
164 response = await client.get("/search?q=a")
165 assert response.status_code == 200
166 assert "Global Search" in response.text
167
168
169 @pytest.mark.anyio
170 async def test_global_search_empty_query_shows_prompt(
171 client: AsyncClient,
172 db_session: AsyncSession,
173 ) -> None:
174 """An empty query renders the search page without running any DB search."""
175 response = await client.get("/search")
176 assert response.status_code == 200
177 assert "Global Search" in response.text
178
179
180 @pytest.mark.anyio
181 async def test_global_search_htmx_fragment_path(
182 client: AsyncClient,
183 db_session: AsyncSession,
184 ) -> None:
185 """HX-Request: true causes the handler to return only the fragment - no <html> shell."""
186 repo_id = await _make_repo(db_session, owner="htmx_search_artist", slug="htmx-search-album")
187 await _make_musehub_commit(
188 db_session,
189 repo_id,
190 commit_id="aabbccddeeff00112233445566778899aabbccd2",
191 message="funky bassline in Eb",
192 )
193 response = await client.get(
194 "/search?q=funky",
195 headers={"HX-Request": "true"},
196 )
197 assert response.status_code == 200
198 assert "<html" not in response.text
199 assert "funky" in response.text
200
201