v2 / vlib / x / templating / dtm2 / dynamic_template_manager2_test.v
429 lines · 395 sloc · 15.48 KB · 3eff1b83cf719199d0ff6f63524da63c9294ffd5
Raw
1module dtm2
2
3import os
4import time
5
6const test_root_name = 'dtm2_test'
7
8fn dtm2_test_root() string {
9 return os.join_path(os.vtmp_dir(), '${test_root_name}_${os.getpid()}')
10}
11
12fn dtm2_outside_template_path() string {
13 return os.join_path(os.vtmp_dir(), '${test_root_name}_${os.getpid()}_outside.html')
14}
15
16fn testsuite_begin() {
17 root := dtm2_test_root()
18 os.rmdir_all(root) or {}
19 os.mkdir_all(os.join_path(root, 'partials'))!
20 outside_path := dtm2_outside_template_path()
21 os.rm(outside_path) or {}
22 os.write_file(outside_path, '<p>@title outside</p>')!
23 os.write_file(os.join_path(root, 'page.html'), '<main>@title @body @missing</main>')!
24 os.write_file(os.join_path(root, 'invalid.css'), 'body { color: red; }')!
25 os.write_file(os.join_path(root, 'raw.txt'), 'Raw: @body')!
26 os.write_file(os.join_path(root, 'with_include.html'),
27 '<header>@include "partials/nav"</header><main>@title</main>')!
28 os.write_file(os.join_path(root, 'partials', 'nav.html'), '<nav>@title</nav>')!
29 os.write_file(os.join_path(root, 'include_outside_relative.html'),
30 '@include "../${os.base(outside_path)}"')!
31 os.write_file(os.join_path(root, 'include_outside_absolute.html'), '@include "${outside_path}"')!
32 os.write_file(os.join_path(root, 'reload.html'), '<p>@title</p>')!
33 os.write_file(os.join_path(root, 'pinned.html'), '<em>@title</em>')!
34 os.write_file(os.join_path(root, 'path_alias.html'), '<section>@title</section>')!
35 os.write_file(os.join_path(root, 'prefix.html'), '<p>@unknown</p>')!
36 os.write_file(os.join_path(root, 'recursive.html'), '@include "recursive"')!
37 os.write_file(os.join_path(root, 'compress.html'), '<div>\n <span>@title</span>\n</div>')!
38 os.write_file(os.join_path(root, 'empty.html'), '')!
39 os.write_file(os.join_path(root, 'feed.xml'), '<feed><title>@title</title></feed>')!
40 os.write_file(os.join_path(root, 'custom.page'), '<article>@title</article>')!
41 os.write_file(os.join_path(root, 'custom.note'), 'Note: @body')!
42 os.write_file(os.join_path(root, 'override.html'), '<div>\n @title\n</div>')!
43 os.write_file(os.join_path(root, 'custom.view'), '<section>@title</section>')!
44 os.write_file(os.join_path(root, 'custom.mail'), 'Subject: @title')!
45 os.write_file(os.join_path(root, 'auto.fragment'), '<aside>@title</aside>')!
46 os.write_file(os.join_path(root, 'auto.message'), 'Auto: @title')!
47 os.write_file(os.join_path(root, 'bad.path'), '<p>@title</p>')!
48 os.write_file(os.join_path(root, 'extensions.json'), '{"html":[".view"],"text":["mail"]}')!
49 os.write_file(os.join_path(root, default_extension_config_filename),
50 '{"html":[".fragment"],"text":[".message"]}')!
51 os.write_file(os.join_path(root, 'invalid_extensions.json'),
52 '{"html":["../bad"],"text":["bad/value",".message"]}')!
53}
54
55fn testsuite_end() {
56 os.rmdir_all(dtm2_test_root()) or {}
57 os.rm(dtm2_outside_template_path()) or {}
58}
59
60fn new_test_manager(compress_html bool) &Manager {
61 return initialize(
62 template_dir: dtm2_test_root()
63 compress_html: compress_html
64 )
65}
66
67fn test_render_placeholders_escape_and_preserve_missing() {
68 mut manager := new_test_manager(false)
69 placeholders := {
70 'title': 'Hello'
71 'body': '<script>alert(1)</script>'
72 }
73 rendered := manager.expand('page.html', placeholders: &placeholders)
74 assert rendered == '<main>Hello <script>alert(1)</script> @missing</main>\n'
75 assert manager.compiled_template_count() == 1
76}
77
78fn test_include_and_compiled_template_registry() {
79 mut manager := new_test_manager(true)
80 placeholders := {
81 'title': 'Home'
82 }
83 rendered := manager.expand('with_include.html', placeholders: &placeholders)
84 assert rendered == '<header><nav>Home</nav></header><main>Home</main>'
85 assert manager.compiled_template_count() == 1
86 _ = manager.expand('with_include.html', placeholders: &placeholders)
87 assert manager.compiled_template_count() == 1
88}
89
90fn test_include_html_suffix_keeps_only_allowed_tags() {
91 mut manager := new_test_manager(false)
92 placeholders := {
93 'body_#includehtml': '<thead><tr><td>safe</td></tr></thead><script>bad</script>'
94 'title': 'HTML'
95 }
96 rendered := manager.expand('page.html', placeholders: &placeholders)
97 assert rendered.contains('<thead><tr><td>safe</td></tr></thead>')
98 assert rendered.contains('<script>bad</script>')
99}
100
101fn test_text_mode_always_escapes_html() {
102 mut manager := new_test_manager(false)
103 placeholders := {
104 'body_#includehtml': '<span>text</span>'
105 }
106 rendered := manager.expand('raw.txt', placeholders: &placeholders)
107 assert rendered == 'Raw: <span>text</span>\n'
108}
109
110fn test_reload_modified_template_without_render_cache() {
111 mut manager := new_test_manager(false)
112 placeholders := {
113 'title': 'One'
114 }
115 first := manager.expand('reload.html', placeholders: &placeholders)
116 assert first == '<p>One</p>\n'
117 os.write_file(os.join_path(dtm2_test_root(), 'reload.html'), '<strong>@title</strong>')!
118 second := manager.expand('reload.html', placeholders: &placeholders)
119 assert second == '<strong>One</strong>\n'
120 assert manager.compiled_template_count() == 1
121}
122
123fn test_reload_same_second_same_size_template_content_change() {
124 path := os.join_path(dtm2_test_root(), 'same_size.html')
125 fixed_time := i64(2_306_102_495)
126 os.write_file(path, '<p>@title A</p>')!
127 os.utime(path, fixed_time, fixed_time)!
128 mut manager := new_test_manager(false)
129 placeholders := {
130 'title': 'One'
131 }
132 first := manager.expand('same_size.html', placeholders: &placeholders)
133 assert first == '<p>One A</p>\n'
134 os.write_file(path, '<b>@title B</b>')!
135 os.utime(path, fixed_time, fixed_time)!
136 second := manager.expand('same_size.html', placeholders: &placeholders)
137 assert second == '<b>One B</b>\n'
138 assert manager.compiled_template_count() == 1
139}
140
141fn test_reload_same_second_same_size_include_content_change() {
142 parent_path := os.join_path(dtm2_test_root(), 'same_size_include_parent.html')
143 include_path := os.join_path(dtm2_test_root(), 'partials', 'same_size_include.html')
144 fixed_time := i64(2_306_102_496)
145 os.write_file(parent_path, '<main>@include "partials/same_size_include"</main>')!
146 os.write_file(include_path, '<p>@title A</p>')!
147 os.utime(include_path, fixed_time, fixed_time)!
148 mut manager := new_test_manager(false)
149 placeholders := {
150 'title': 'One'
151 }
152 first := manager.expand('same_size_include_parent.html', placeholders: &placeholders)
153 assert first == '<main><p>One A</p></main>\n'
154 os.write_file(include_path, '<b>@title B</b>')!
155 os.utime(include_path, fixed_time, fixed_time)!
156 second := manager.expand('same_size_include_parent.html', placeholders: &placeholders)
157 assert second == '<main><b>One B</b></main>\n'
158 assert manager.compiled_template_count() == 1
159}
160
161fn test_reload_modified_include_without_render_cache() {
162 mut manager := new_test_manager(true)
163 placeholders := {
164 'title': 'Home'
165 }
166 first := manager.expand('with_include.html', placeholders: &placeholders)
167 assert first == '<header><nav>Home</nav></header><main>Home</main>'
168 time.sleep(1100 * time.millisecond)
169 os.write_file(os.join_path(dtm2_test_root(), 'partials', 'nav.html'),
170 '<nav class="updated">@title</nav>')!
171 second := manager.expand('with_include.html', placeholders: &placeholders)
172 assert second == '<header><nav class="updated">Home</nav></header><main>Home</main>'
173 assert manager.compiled_template_count() == 1
174}
175
176fn test_reload_disabled_keeps_cached_template_tree() {
177 mut manager := initialize(
178 template_dir: dtm2_test_root()
179 compress_html: false
180 reload_modified_templates: false
181 )
182 placeholders := {
183 'title': 'Pinned'
184 }
185 first := manager.expand('pinned.html', placeholders: &placeholders)
186 assert first == '<em>Pinned</em>\n'
187 os.write_file(os.join_path(dtm2_test_root(), 'pinned.html'), '<strong>@title</strong>')!
188 second := manager.expand('pinned.html', placeholders: &placeholders)
189 assert second == '<em>Pinned</em>\n'
190 assert manager.compiled_template_count() == 1
191}
192
193fn test_relative_and_absolute_paths_share_compiled_template() {
194 mut manager := new_test_manager(false)
195 placeholders := {
196 'title': 'Alias'
197 }
198 relative := manager.expand('path_alias.html', placeholders: &placeholders)
199 absolute := manager.expand(os.join_path(dtm2_test_root(), 'path_alias.html'),
200 placeholders: &placeholders
201 )
202 assert relative == '<section>Alias</section>\n'
203 assert absolute == relative
204 assert manager.compiled_template_count() == 1
205}
206
207fn test_template_path_cannot_escape_template_dir() {
208 mut manager := new_test_manager(false)
209 placeholders := {
210 'title': 'Escape'
211 }
212 relative_escape := manager.expand('../${os.base(dtm2_outside_template_path())}',
213 placeholders: &placeholders
214 )
215 absolute_escape := manager.expand(dtm2_outside_template_path(), placeholders: &placeholders)
216 assert relative_escape == internal_server_error
217 assert absolute_escape == internal_server_error
218 assert manager.compiled_template_count() == 0
219}
220
221fn test_cached_template_path_revalidates_symlink_escape() {
222 $if windows {
223 return
224 }
225 path := os.join_path(dtm2_test_root(), 'symlink_swap.html')
226 os.write_file(path, '<p>@title safe</p>')!
227 mut manager := new_test_manager(false)
228 placeholders := {
229 'title': 'Link'
230 }
231 first := manager.expand('symlink_swap.html', placeholders: &placeholders)
232 assert first == '<p>Link safe</p>\n'
233 os.rm(path)!
234 os.symlink(dtm2_outside_template_path(), path) or { return }
235 escaped := manager.expand('symlink_swap.html', placeholders: &placeholders)
236 assert escaped == internal_server_error
237}
238
239fn test_cached_include_dependency_revalidates_symlink_escape() {
240 $if windows {
241 return
242 }
243 parent_path := os.join_path(dtm2_test_root(), 'include_symlink_swap_parent.html')
244 include_path := os.join_path(dtm2_test_root(), 'partials', 'symlink_swap.html')
245 os.write_file(parent_path, '<main>@include "partials/symlink_swap"</main>')!
246 os.write_file(include_path, '<p>@title safe</p>')!
247 mut manager := new_test_manager(false)
248 placeholders := {
249 'title': 'Link'
250 }
251 first := manager.expand('include_symlink_swap_parent.html', placeholders: &placeholders)
252 assert first == '<main><p>Link safe</p></main>\n'
253 os.rm(include_path)!
254 os.symlink(dtm2_outside_template_path(), include_path) or { return }
255 escaped := manager.expand('include_symlink_swap_parent.html', placeholders: &placeholders)
256 assert escaped == internal_server_error
257}
258
259fn test_reload_disabled_does_not_reopen_cached_symlink_swap() {
260 $if windows {
261 return
262 }
263 path := os.join_path(dtm2_test_root(), 'pinned_symlink_swap.html')
264 os.write_file(path, '<p>@title safe</p>')!
265 mut manager := initialize(
266 template_dir: dtm2_test_root()
267 compress_html: false
268 reload_modified_templates: false
269 )
270 placeholders := {
271 'title': 'Pinned'
272 }
273 first := manager.expand('pinned_symlink_swap.html', placeholders: &placeholders)
274 assert first == '<p>Pinned safe</p>\n'
275 os.rm(path)!
276 os.symlink(dtm2_outside_template_path(), path) or { return }
277 second := manager.expand('pinned_symlink_swap.html', placeholders: &placeholders)
278 assert second == first
279 assert manager.compiled_template_count() == 1
280}
281
282fn test_include_path_cannot_escape_template_dir() {
283 mut manager := new_test_manager(false)
284 placeholders := {
285 'title': 'Escape'
286 }
287 relative_include := manager.expand('include_outside_relative.html', placeholders: &placeholders)
288 absolute_include := manager.expand('include_outside_absolute.html', placeholders: &placeholders)
289 assert relative_include == internal_server_error
290 assert absolute_include == internal_server_error
291 assert manager.compiled_template_count() == 0
292}
293
294fn test_custom_missing_placeholder_prefix() {
295 mut manager := new_test_manager(false)
296 placeholders := map[string]string{}
297 rendered := manager.expand('prefix.html',
298 placeholders: &placeholders
299 missing_placeholder_prefix: '?'
300 )
301 assert rendered == '<p>?unknown</p>\n'
302}
303
304fn test_recursive_include_returns_internal_server_error() {
305 mut manager := new_test_manager(false)
306 placeholders := map[string]string{}
307 rendered := manager.expand('recursive.html', placeholders: &placeholders)
308 assert rendered == internal_server_error
309 assert manager.compiled_template_count() == 0
310}
311
312fn test_html_compression_is_deterministic() {
313 mut manager := new_test_manager(true)
314 placeholders := {
315 'title': 'Compact'
316 }
317 rendered := manager.expand('compress.html', placeholders: &placeholders)
318 assert rendered == '<div><span>Compact</span></div>'
319}
320
321fn test_empty_template_renders_empty() {
322 mut manager := new_test_manager(false)
323 placeholders := map[string]string{}
324 rendered := manager.expand('empty.html', placeholders: &placeholders)
325 assert rendered == ''
326}
327
328fn test_xml_template_uses_html_rendering_mode() {
329 mut manager := new_test_manager(false)
330 placeholders := {
331 'title': '<escaped>'
332 }
333 rendered := manager.expand('feed.xml', placeholders: &placeholders)
334 assert rendered == '<feed><title><escaped></title></feed>\n'
335}
336
337fn test_custom_template_extension_map() {
338 mut manager := initialize(
339 template_dir: dtm2_test_root()
340 template_extensions: {
341 '.page': TemplateType.html
342 'note': TemplateType.text
343 }
344 )
345 html_placeholders := {
346 'title_#includehtml': '<span>custom</span><script>blocked</script>'
347 }
348 text_placeholders := {
349 'body_#includehtml': '<span>custom</span>'
350 }
351 html := manager.expand('custom.page', placeholders: &html_placeholders)
352 text := manager.expand('custom.note', placeholders: &text_placeholders)
353 assert html.contains('<span>custom</span><script>blocked</script>')
354 assert text == 'Note: <span>custom</span>\n'
355}
356
357fn test_custom_template_extension_map_can_override_builtin_mapping() {
358 mut manager := initialize(
359 template_dir: dtm2_test_root()
360 compress_html: true
361 template_extensions: {
362 '.html': TemplateType.text
363 }
364 )
365 placeholders := {
366 'title': 'Text'
367 }
368 rendered := manager.expand('override.html', placeholders: &placeholders)
369 assert rendered == '<div>\n Text\n</div>\n'
370}
371
372fn test_json_extension_config_file() {
373 mut manager := initialize(
374 template_dir: dtm2_test_root()
375 extension_config_file: os.join_path(dtm2_test_root(), 'extensions.json')
376 )
377 placeholders := {
378 'title': '<Config>'
379 }
380 html := manager.expand('custom.view', placeholders: &placeholders)
381 text := manager.expand('custom.mail', placeholders: &placeholders)
382 assert html == '<section><Config></section>'
383 assert text == 'Subject: <Config>\n'
384}
385
386fn test_default_json_extension_config_file_is_loaded_from_template_dir() {
387 mut manager := initialize(
388 template_dir: dtm2_test_root()
389 compress_html: false
390 )
391 placeholders := {
392 'title': '<Auto>'
393 }
394 html := manager.expand('auto.fragment', placeholders: &placeholders)
395 text := manager.expand('auto.message', placeholders: &placeholders)
396 assert html == '<aside><Auto></aside>\n'
397 assert text == 'Auto: <Auto>\n'
398}
399
400fn test_invalid_json_extension_entries_are_ignored() {
401 mut manager := initialize(
402 template_dir: dtm2_test_root()
403 extension_config_file: os.join_path(dtm2_test_root(), 'invalid_extensions.json')
404 compress_html: false
405 )
406 placeholders := {
407 'title': '<Safe>'
408 }
409 ignored := manager.expand('bad.path', placeholders: &placeholders)
410 loaded := manager.expand('auto.message', placeholders: &placeholders)
411 assert ignored == internal_server_error
412 assert loaded == 'Auto: <Safe>\n'
413}
414
415fn test_missing_template_returns_internal_server_error() {
416 mut manager := new_test_manager(false)
417 placeholders := map[string]string{}
418 rendered := manager.expand('missing.html', placeholders: &placeholders)
419 assert rendered == internal_server_error
420 assert manager.compiled_template_count() == 0
421}
422
423fn test_invalid_template_extension_returns_internal_server_error() {
424 mut manager := new_test_manager(false)
425 placeholders := map[string]string{}
426 rendered := manager.expand('invalid.css', placeholders: &placeholders)
427 assert rendered == internal_server_error
428 assert manager.compiled_template_count() == 0
429}
430