gabriel / musehub public
test_musehub_ui_jsonld.py python
247 lines 9.1 KB
Raw
sha256:d035733f21ccff27735fddebfbbe0ed24565a32a22db8de5885402262671ecd2 chore: bump version to 0.2.0rc15 for musehub#113 fix release Sonnet 4.6 patch 14 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 11 commits
sha256:d035733f21ccff27735fddebfbbe0ed24565a32a22db8de5885402262671ecd2 chore: bump version to 0.2.0rc15 for musehub#113 fix release Sonnet 4.6 patch 14 days ago
sha256:0032d6cfa33bc3c8367436ad768e7dd0e339b4332153160247da8266cb5fa352 Merge branch 'task/version-tags-phase3-server' into dev Human 17 days ago
sha256:4669620efda9ff41c55bdefd1f7bfe1c239d468428744c84ead9957e5a003a53 merge: rescue snapshot-recovery hardening (c00aa21d) into d… Opus 4.8 minor 29 days ago
sha256:a59da49c4611b970fc4b6ae48678ce4943261c213a07ddbd73ce9201df869b4a fix: remove false-positive proposal_comments index drop fro… Sonnet 4.6 patch 34 days ago
sha256:0a240d6dbff234f07d98a28a4a9a68db702f3f9ff9260196f24219bdb1c0b6f3 feat: render markdown mists as HTML with heading anchor links Sonnet 4.6 patch 34 days ago
sha256:24a7d47486ebc4ebd1832830580e177ec6f877b48dced8c000e198cdec4ce9d6 Merge 'task/bump-version-rc12' into 'dev' — proposal: Bump … Human 35 days ago
sha256:b9ff931d147e0114a1f17060f415b89ed551c170a91ff226c70437aa5c85f9ee Merge 'task/bump-version-rc12' into 'dev' — proposal: Bump … Human 35 days ago
sha256:d1122d21e73471879b460037b22c0b50fded7c423444a176f248428f75dac39c Merge 'task/fix-issue-pagination-cursor' into 'dev' — propo… Human 35 days ago
sha256:01e18975e73d2b3cd5b6db7929c895bef9aa6e0d4391dc5b2adfc548b41318dd Merge 'feat/adding-debug-logs-to-staging' into 'dev' — prop… Human 36 days ago
sha256:6b1949fc2797ca4c1936a637a4cbfec828ef56cf52398a2e74ca3c4f494e728f fix: use wire_bytes not mpack_bytes_raw in compute_object_b… Sonnet 4.6 patch 48 days ago
sha256:b99f2455dc346966d040133f5203297e6e3ef5803a93728a2c30568d0a0f7583 rename: delta_add → delta_upsert across wire format, models… Sonnet 4.6 patch 50 days ago