gabriel / musehub public
test_musehub_ui_jsonld.py python
242 lines 8.7 KB
Raw
sha256:a10adeeb7a0169cb9900f9806ed7a973047258abb6283724fe55e8eb68ff3f0a init: musehub initial commit Human 171 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 )
65
66
67 def _make_release(
68 *,
69 tag: str = "v1.0",
70 title: str = "First Release",
71 body: str = "Initial recording session.",
72 author: str = "miles_davis",
73 ) -> ReleaseResponse:
74 """Build a minimal ReleaseResponse for testing."""
75 return ReleaseResponse(
76 release_id="release-uuid-456",
77 tag=tag,
78 title=title,
79 body=body,
80 commit_id=None,
81 download_urls=ReleaseDownloadUrls(),
82 author=author,
83 created_at=datetime(2024, 3, 1, 9, 0, 0, tzinfo=timezone.utc),
84 )
85
86
87 # ---------------------------------------------------------------------------
88 # jsonld_repo — MusicComposition
89 # ---------------------------------------------------------------------------
90
91
92 def test_jsonld_repo_returns_music_composition_type() -> None:
93 """JSON-LD for a repo always declares @type MusicComposition."""
94 repo = _make_repo()
95 data = jsonld_repo(repo, "https://example.com/miles/kind-of-blue")
96 assert data["@type"] == "MusicComposition"
97 assert data["@context"] == "https://schema.org"
98
99
100 def test_jsonld_repo_includes_required_fields() -> None:
101 """Repo JSON-LD includes name, description, url, dateCreated, creator."""
102 repo = _make_repo()
103 url = "https://example.com/miles/kind-of-blue"
104 data = jsonld_repo(repo, url)
105
106 assert data["name"] == "Kind of Blue"
107 assert data["description"] == "Landmark modal jazz album"
108 assert data["url"] == url
109 assert "2024-01-15" in str(data["dateCreated"])
110 creator = data["creator"]
111 assert isinstance(creator, dict)
112 assert creator["@type"] == "Person"
113 assert creator["name"] == "miles_davis"
114
115
116 def test_jsonld_repo_includes_genre_when_tags_present() -> None:
117 """Tags are mapped to the genre field when the repo has tags."""
118 repo = _make_repo(tags=["jazz", "modal", "F minor"])
119 data = jsonld_repo(repo, "https://example.com/repo")
120 assert data["genre"] == ["jazz", "modal", "F minor"]
121
122
123 def test_jsonld_repo_omits_genre_when_no_tags() -> None:
124 """The genre field is absent when the repo has no tags."""
125 repo = _make_repo(tags=[])
126 data = jsonld_repo(repo, "https://example.com/repo")
127 assert "genre" not in data
128
129
130
131 # ---------------------------------------------------------------------------
132 # jsonld_release — MusicRecording
133 # ---------------------------------------------------------------------------
134
135
136 def test_jsonld_release_returns_music_recording_type() -> None:
137 """JSON-LD for a release always declares @type MusicRecording."""
138 release = _make_release()
139 repo = _make_repo()
140 data = jsonld_release(release, repo, "https://example.com/release/v1.0")
141 assert data["@type"] == "MusicRecording"
142 assert data["@context"] == "https://schema.org"
143
144
145 def test_jsonld_release_includes_required_fields() -> None:
146 """Release JSON-LD includes name, description, url, datePublished, byArtist, inAlbum."""
147 release = _make_release()
148 repo = _make_repo()
149 url = "https://example.com/miles/kind-of-blue/releases/v1.0"
150 data = jsonld_release(release, repo, url)
151
152 assert data["name"] == "First Release"
153 assert data["description"] == "Initial recording session."
154 assert data["url"] == url
155 assert "2024-03-01" in str(data["datePublished"])
156
157 artist = data["byArtist"]
158 assert isinstance(artist, dict)
159 assert artist["@type"] == "Person"
160 assert artist["name"] == "miles_davis"
161
162 album = data["inAlbum"]
163 assert isinstance(album, dict)
164 assert album["@type"] == "MusicAlbum"
165 assert album["name"] == "Kind of Blue"
166
167
168 def test_jsonld_release_includes_genre_from_repo_tags() -> None:
169 """Release JSON-LD inherits genre from the parent repo's tags."""
170 release = _make_release()
171 repo = _make_repo(tags=["bebop", "quintet"])
172 data = jsonld_release(release, repo, "https://example.com/release")
173 assert data["genre"] == ["bebop", "quintet"]
174
175
176 def test_jsonld_release_falls_back_to_repo_owner_when_no_author() -> None:
177 """byArtist falls back to repo.owner when release.author is empty."""
178 release = _make_release(author="")
179 repo = _make_repo(owner="coltrane")
180 data = jsonld_release(release, repo, "https://example.com/release")
181 artist = data["byArtist"]
182 assert isinstance(artist, dict)
183 assert artist["name"] == "coltrane"
184
185
186 def test_jsonld_release_uses_tag_when_title_empty() -> None:
187 """name falls back to the release tag when title is an empty string."""
188 release = _make_release(title="", tag="v2.0-beta")
189 repo = _make_repo()
190 data = jsonld_release(release, repo, "https://example.com/release")
191 assert data["name"] == "v2.0-beta"
192
193
194 # ---------------------------------------------------------------------------
195 # render_jsonld_script
196 # ---------------------------------------------------------------------------
197
198
199 def test_render_jsonld_script_wraps_in_script_tag() -> None:
200 """Rendered output is a <script type="application/ld+json"> tag."""
201 data = {"@context": "https://schema.org", "@type": "MusicComposition", "name": "Test"}
202 script = render_jsonld_script(data)
203 assert script.startswith('<script type="application/ld+json">')
204 assert script.endswith("</script>")
205
206
207 def test_render_jsonld_script_contains_valid_json() -> None:
208 """The content inside the script tag is valid JSON matching the input dict."""
209 data = {
210 "@context": "https://schema.org",
211 "@type": "MusicComposition",
212 "name": "Blue in Green",
213 }
214 script = render_jsonld_script(data)
215 inner = script.removeprefix('<script type="application/ld+json">').removesuffix("</script>")
216 parsed = json.loads(inner)
217 assert parsed["name"] == "Blue in Green"
218 assert parsed["@type"] == "MusicComposition"
219
220
221 def test_render_jsonld_script_escapes_closing_tag_xss() -> None:
222 """</script> sequences inside JSON values are escaped to prevent XSS."""
223 data = {
224 "@context": "https://schema.org",
225 "name": "Attack</script><script>alert(1)//",
226 }
227 script = render_jsonld_script(data)
228 # Extract only the JSON content between the opening and closing script tags.
229 inner = script.removeprefix('<script type="application/ld+json">').removesuffix("</script>")
230 # The closing tag sequence must not appear verbatim inside the JSON payload.
231 assert "</script>" not in inner
232
233
234 def test_render_jsonld_script_preserves_unicode() -> None:
235 """Non-ASCII characters (e.g. accented, CJK) are preserved without escaping."""
236 data = {
237 "@context": "https://schema.org",
238 "name": "Caf\u00e9 M\u00fasica \u3084\u307e\u3068",
239 }
240 script = render_jsonld_script(data)
241 assert "Caf\u00e9" in script
242 assert "\u3084\u307e\u3068" in script
File History 1 commit
sha256:a10adeeb7a0169cb9900f9806ed7a973047258abb6283724fe55e8eb68ff3f0a init: musehub initial commit Human 171 days ago