gabriel / musehub public
test_musehub_ui_jsonld.py python
243 lines 8.7 KB
Raw
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor ⚠ breaking 155 days ago
1 """Unit tests for the MuseHub JSON-LD structured data helpers.
2
3 Covers — jsonld_repo and jsonld_release produce valid
4 schema.org/MusicComposition and schema.org/MusicRecording dicts.
5 render_jsonld_script produces a safe, well-formed <script> tag.
6
7 Tests:
8 - test_jsonld_repo_returns_music_composition_type
9 - test_jsonld_repo_includes_required_fields
10 - test_jsonld_repo_includes_genre_when_tags_present
11 - test_jsonld_repo_omits_genre_when_no_tags
12 - test_jsonld_repo_includes_key_signature_when_present
13 - test_jsonld_repo_includes_tempo_when_present
14 - test_jsonld_repo_omits_key_and_tempo_when_absent
15 - test_jsonld_release_returns_music_recording_type
16 - test_jsonld_release_includes_required_fields
17 - test_jsonld_release_includes_genre_from_repo_tags
18 - test_jsonld_release_falls_back_to_repo_owner_when_no_author
19 - test_jsonld_release_uses_tag_when_title_empty
20 - test_render_jsonld_script_wraps_in_script_tag
21 - test_render_jsonld_script_escapes_closing_tag_xss
22 - test_render_jsonld_script_preserves_unicode
23 """
24 from __future__ import annotations
25
26 import json
27 from datetime import datetime, timezone
28 from unittest.mock import MagicMock
29
30 import pytest
31
32 from musehub.api.routes.musehub.ui_jsonld import (
33 jsonld_release,
34 jsonld_repo,
35 render_jsonld_script,
36 )
37 from musehub.models.musehub import ReleaseDownloadUrls, ReleaseResponse, RepoResponse
38
39
40 # ---------------------------------------------------------------------------
41 # Fixtures — minimal valid model instances
42 # ---------------------------------------------------------------------------
43
44
45 def _make_repo(
46 *,
47 name: str = "Kind of Blue",
48 owner: str = "miles_davis",
49 description: str = "Landmark modal jazz album",
50 tags: list[str] | None = None,
51 ) -> RepoResponse:
52 """Build a minimal RepoResponse for testing."""
53 return RepoResponse(
54 repo_id="aabbccdd",
55 name=name,
56 owner=owner,
57 slug="kind-of-blue",
58 visibility="public",
59 owner_user_id="user-uuid-123",
60 clone_url="https://musehub.ai/api/repos/aabbccdd",
61 description=description,
62 tags=tags or [],
63 created_at=datetime(2024, 1, 15, 12, 0, 0, tzinfo=timezone.utc),
64 updated_at=datetime(2024, 1, 15, 12, 0, 0, tzinfo=timezone.utc),
65 )
66
67
68 def _make_release(
69 *,
70 tag: str = "v1.0",
71 title: str = "First Release",
72 body: str = "Initial recording session.",
73 author: str = "miles_davis",
74 ) -> ReleaseResponse:
75 """Build a minimal ReleaseResponse for testing."""
76 return ReleaseResponse(
77 release_id="release-uuid-456",
78 tag=tag,
79 title=title,
80 body=body,
81 commit_id=None,
82 download_urls=ReleaseDownloadUrls(),
83 author=author,
84 created_at=datetime(2024, 3, 1, 9, 0, 0, tzinfo=timezone.utc),
85 )
86
87
88 # ---------------------------------------------------------------------------
89 # jsonld_repo — MusicComposition
90 # ---------------------------------------------------------------------------
91
92
93 def test_jsonld_repo_returns_music_composition_type() -> None:
94 """JSON-LD for a repo always declares @type MusicComposition."""
95 repo = _make_repo()
96 data = jsonld_repo(repo, "https://example.com/miles/kind-of-blue")
97 assert data["@type"] == "MusicComposition"
98 assert data["@context"] == "https://schema.org"
99
100
101 def test_jsonld_repo_includes_required_fields() -> None:
102 """Repo JSON-LD includes name, description, url, dateCreated, creator."""
103 repo = _make_repo()
104 url = "https://example.com/miles/kind-of-blue"
105 data = jsonld_repo(repo, url)
106
107 assert data["name"] == "Kind of Blue"
108 assert data["description"] == "Landmark modal jazz album"
109 assert data["url"] == url
110 assert "2024-01-15" in str(data["dateCreated"])
111 creator = data["creator"]
112 assert isinstance(creator, dict)
113 assert creator["@type"] == "Person"
114 assert creator["name"] == "miles_davis"
115
116
117 def test_jsonld_repo_includes_genre_when_tags_present() -> None:
118 """Tags are mapped to the genre field when the repo has tags."""
119 repo = _make_repo(tags=["jazz", "modal", "F minor"])
120 data = jsonld_repo(repo, "https://example.com/repo")
121 assert data["genre"] == ["jazz", "modal", "F minor"]
122
123
124 def test_jsonld_repo_omits_genre_when_no_tags() -> None:
125 """The genre field is absent when the repo has no tags."""
126 repo = _make_repo(tags=[])
127 data = jsonld_repo(repo, "https://example.com/repo")
128 assert "genre" not in data
129
130
131
132 # ---------------------------------------------------------------------------
133 # jsonld_release — MusicRecording
134 # ---------------------------------------------------------------------------
135
136
137 def test_jsonld_release_returns_music_recording_type() -> None:
138 """JSON-LD for a release always declares @type MusicRecording."""
139 release = _make_release()
140 repo = _make_repo()
141 data = jsonld_release(release, repo, "https://example.com/release/v1.0")
142 assert data["@type"] == "MusicRecording"
143 assert data["@context"] == "https://schema.org"
144
145
146 def test_jsonld_release_includes_required_fields() -> None:
147 """Release JSON-LD includes name, description, url, datePublished, byArtist, inAlbum."""
148 release = _make_release()
149 repo = _make_repo()
150 url = "https://example.com/miles/kind-of-blue/releases/v1.0"
151 data = jsonld_release(release, repo, url)
152
153 assert data["name"] == "First Release"
154 assert data["description"] == "Initial recording session."
155 assert data["url"] == url
156 assert "2024-03-01" in str(data["datePublished"])
157
158 artist = data["byArtist"]
159 assert isinstance(artist, dict)
160 assert artist["@type"] == "Person"
161 assert artist["name"] == "miles_davis"
162
163 album = data["inAlbum"]
164 assert isinstance(album, dict)
165 assert album["@type"] == "MusicAlbum"
166 assert album["name"] == "Kind of Blue"
167
168
169 def test_jsonld_release_includes_genre_from_repo_tags() -> None:
170 """Release JSON-LD inherits genre from the parent repo's tags."""
171 release = _make_release()
172 repo = _make_repo(tags=["bebop", "quintet"])
173 data = jsonld_release(release, repo, "https://example.com/release")
174 assert data["genre"] == ["bebop", "quintet"]
175
176
177 def test_jsonld_release_falls_back_to_repo_owner_when_no_author() -> None:
178 """byArtist falls back to repo.owner when release.author is empty."""
179 release = _make_release(author="")
180 repo = _make_repo(owner="coltrane")
181 data = jsonld_release(release, repo, "https://example.com/release")
182 artist = data["byArtist"]
183 assert isinstance(artist, dict)
184 assert artist["name"] == "coltrane"
185
186
187 def test_jsonld_release_uses_tag_when_title_empty() -> None:
188 """name falls back to the release tag when title is an empty string."""
189 release = _make_release(title="", tag="v2.0-beta")
190 repo = _make_repo()
191 data = jsonld_release(release, repo, "https://example.com/release")
192 assert data["name"] == "v2.0-beta"
193
194
195 # ---------------------------------------------------------------------------
196 # render_jsonld_script
197 # ---------------------------------------------------------------------------
198
199
200 def test_render_jsonld_script_wraps_in_script_tag() -> None:
201 """Rendered output is a <script type="application/ld+json"> tag."""
202 data = {"@context": "https://schema.org", "@type": "MusicComposition", "name": "Test"}
203 script = render_jsonld_script(data)
204 assert script.startswith('<script type="application/ld+json">')
205 assert script.endswith("</script>")
206
207
208 def test_render_jsonld_script_contains_valid_json() -> None:
209 """The content inside the script tag is valid JSON matching the input dict."""
210 data = {
211 "@context": "https://schema.org",
212 "@type": "MusicComposition",
213 "name": "Blue in Green",
214 }
215 script = render_jsonld_script(data)
216 inner = script.removeprefix('<script type="application/ld+json">').removesuffix("</script>")
217 parsed = json.loads(inner)
218 assert parsed["name"] == "Blue in Green"
219 assert parsed["@type"] == "MusicComposition"
220
221
222 def test_render_jsonld_script_escapes_closing_tag_xss() -> None:
223 """</script> sequences inside JSON values are escaped to prevent XSS."""
224 data = {
225 "@context": "https://schema.org",
226 "name": "Attack</script><script>alert(1)//",
227 }
228 script = render_jsonld_script(data)
229 # Extract only the JSON content between the opening and closing script tags.
230 inner = script.removeprefix('<script type="application/ld+json">').removesuffix("</script>")
231 # The closing tag sequence must not appear verbatim inside the JSON payload.
232 assert "</script>" not in inner
233
234
235 def test_render_jsonld_script_preserves_unicode() -> None:
236 """Non-ASCII characters (e.g. accented, CJK) are preserved without escaping."""
237 data = {
238 "@context": "https://schema.org",
239 "name": "Caf\u00e9 M\u00fasica \u3084\u307e\u3068",
240 }
241 script = render_jsonld_script(data)
242 assert "Caf\u00e9" in script
243 assert "\u3084\u307e\u3068" in script
File History 1 commit
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor ⚠ 155 days ago