main
py 189 lines 7.01 KB
Raw
1 """Tests for scripts/validate_content_images.py."""
2
3 from __future__ import annotations
4
5 import json
6 from pathlib import Path
7
8 import scripts.validate_content_images as validator
9
10
11 def _write_md(tmp_path: Path, filename: str, content: str) -> Path:
12 """Write a markdown file in a temp content dir."""
13 content_dir = tmp_path / "content"
14 content_dir.mkdir(exist_ok=True)
15 filepath = content_dir / filename
16 filepath.write_text(content, encoding="utf-8")
17 return content_dir
18
19
20 class TestFrontmatterExtraction:
21 def test_extracts_cover_image(self) -> None:
22 text = '---\ntitle: "Test"\ncover_image: "covers/test.webp"\n---\nBody'
23 fields = validator._extract_frontmatter(text)
24 assert fields["cover_image"] == "covers/test.webp"
25
26 def test_extracts_og_image(self) -> None:
27 text = '---\nog_image: "covers/og.png"\n---\n'
28 fields = validator._extract_frontmatter(text)
29 assert fields["og_image"] == "covers/og.png"
30
31 def test_ignores_non_image_fields(self) -> None:
32 text = '---\ntitle: "Hello"\nauthor: "Test"\n---\n'
33 fields = validator._extract_frontmatter(text)
34 assert fields == {}
35
36 def test_handles_no_frontmatter(self) -> None:
37 text = "Just some content"
38 fields = validator._extract_frontmatter(text)
39 assert fields == {}
40
41
42 class TestHotlinkDetection:
43 def test_detects_frontmatter_hotlink(self, tmp_path: Path) -> None:
44 content_dir = _write_md(
45 tmp_path,
46 "test.md",
47 '---\ncover_image: "https://evil.com/img.png"\n---\nBody',
48 )
49 violations = validator.validate_content(content_dir)
50 assert any("hotlinks external URL" in v for v in violations)
51
52 def test_detects_markdown_image_hotlink(self, tmp_path: Path) -> None:
53 content_dir = _write_md(
54 tmp_path,
55 "test.md",
56 "---\ntitle: test\n---\n![alt](https://example.com/photo.jpg)\n",
57 )
58 violations = validator.validate_content(content_dir)
59 assert any("Markdown image hotlinks" in v for v in violations)
60
61 def test_detects_html_img_hotlink(self, tmp_path: Path) -> None:
62 content_dir = _write_md(
63 tmp_path,
64 "test.md",
65 '---\ntitle: test\n---\n<img src="http://evil.com/x.png" alt="bad">\n',
66 )
67 violations = validator.validate_content(content_dir)
68 assert any("HTML img hotlinks" in v for v in violations)
69
70 def test_detects_protocol_relative_url(self, tmp_path: Path) -> None:
71 content_dir = _write_md(
72 tmp_path,
73 "test.md",
74 '---\nog_image: "//cdn.example.com/image.png"\n---\n',
75 )
76 violations = validator.validate_content(content_dir)
77 assert any("hotlinks external URL" in v for v in violations)
78
79 def test_allows_local_paths(self, tmp_path: Path) -> None:
80 content_dir = _write_md(
81 tmp_path,
82 "test.md",
83 '---\ncover_image: "covers/local.webp"\n---\n![alt](/images/chart.svg)\n',
84 )
85 violations = validator.validate_content(content_dir)
86 assert len(violations) == 0
87
88
89 class TestSecretDetection:
90 def test_detects_sas_token(self, tmp_path: Path) -> None:
91 content_dir = _write_md(
92 tmp_path,
93 "test.md",
94 "---\ntitle: test\n---\n![x](https://store.blob.core.windows.net/c/img.png?sv=2021&sig=abc)\n",
95 )
96 violations = validator.validate_content(content_dir)
97 assert any("suspicious parameters" in v for v in violations)
98
99 def test_detects_tracking_params(self, tmp_path: Path) -> None:
100 content_dir = _write_md(
101 tmp_path,
102 "test.md",
103 "---\ntitle: test\n---\n![x](https://example.com/img.png?utm_source=twitter&utm_medium=social)\n",
104 )
105 violations = validator.validate_content(content_dir)
106 assert any("suspicious parameters" in v for v in violations)
107
108 def test_detects_api_key_param(self, tmp_path: Path) -> None:
109 content_dir = _write_md(
110 tmp_path,
111 "test.md",
112 '---\ncover_image: "covers/x.webp?api_key=secret123"\n---\n',
113 )
114 violations = validator.validate_content(content_dir)
115 assert any("suspicious parameters" in v for v in violations)
116
117
118 class TestRegistryValidation:
119 def test_flags_unregistered_cover(self, tmp_path: Path) -> None:
120 content_dir = _write_md(
121 tmp_path,
122 "test.md",
123 '---\ncover_image: "covers/unregistered.webp"\n---\nBody',
124 )
125 reg_path = tmp_path / "registry.json"
126 reg_path.write_text(json.dumps({"images": []}), encoding="utf-8")
127 violations = validator.validate_registry_references(content_dir, reg_path)
128 assert any("unregistered image" in v for v in violations)
129
130 def test_passes_registered_cover(self, tmp_path: Path) -> None:
131 content_dir = _write_md(
132 tmp_path,
133 "test.md",
134 '---\ncover_image: "covers/registered.webp"\n---\nBody',
135 )
136 reg_path = tmp_path / "registry.json"
137 reg_path.write_text(
138 json.dumps(
139 {
140 "images": [
141 {"filename": "covers/registered.webp", "license": "CC0", "added_by": "test"}
142 ]
143 }
144 ),
145 encoding="utf-8",
146 )
147 violations = validator.validate_registry_references(content_dir, reg_path)
148 assert len(violations) == 0
149
150 def test_ignores_non_cover_local_paths(self, tmp_path: Path) -> None:
151 content_dir = _write_md(
152 tmp_path,
153 "test.md",
154 '---\ncover_image: "images/generated-chart.svg"\n---\nBody',
155 )
156 reg_path = tmp_path / "registry.json"
157 reg_path.write_text(json.dumps({"images": []}), encoding="utf-8")
158 violations = validator.validate_registry_references(content_dir, reg_path)
159 # Non-cover paths (not starting with covers/ or assets/covers/) are not flagged
160 assert len(violations) == 0
161
162 def test_handles_missing_registry(self, tmp_path: Path) -> None:
163 content_dir = _write_md(
164 tmp_path,
165 "test.md",
166 '---\ncover_image: "covers/x.webp"\n---\n',
167 )
168 violations = validator.validate_registry_references(
169 content_dir, tmp_path / "nonexistent.json"
170 )
171 assert len(violations) == 1
172 assert "not found" in violations[0]
173
174
175 class TestHelpers:
176 def test_is_remote_url_http(self) -> None:
177 assert validator._is_remote_url("http://example.com/img.png")
178
179 def test_is_remote_url_https(self) -> None:
180 assert validator._is_remote_url("https://example.com/img.png")
181
182 def test_is_remote_url_protocol_relative(self) -> None:
183 assert validator._is_remote_url("//cdn.example.com/img.png")
184
185 def test_is_not_remote_url_local(self) -> None:
186 assert not validator._is_remote_url("covers/local.webp")
187
188 def test_is_not_remote_url_relative(self) -> None:
189 assert not validator._is_remote_url("assets/images/chart.svg")