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