Skip to main content

max / pter

3.3 KB · 117 lines History Blame Raw
1 use proptest::prelude::*;
2
3 // Strategy: generate arbitrary HTML-like strings
4 fn html_fragment() -> impl Strategy<Value = String> {
5 let tags = prop::sample::select(vec![
6 "p",
7 "div",
8 "span",
9 "strong",
10 "em",
11 "a",
12 "h1",
13 "h2",
14 "h3",
15 "ul",
16 "ol",
17 "li",
18 "blockquote",
19 "pre",
20 "code",
21 "br",
22 "hr",
23 "img",
24 "table",
25 "tr",
26 "td",
27 "th",
28 "b",
29 "i",
30 "del",
31 "sup",
32 "sub",
33 ]);
34
35 let text = "[a-zA-Z0-9 .,!?]{0,100}";
36
37 prop::collection::vec(
38 prop_oneof![
39 // Plain text
40 text.prop_map(|s| s),
41 // Opening + closing tag with text
42 (tags.clone(), text).prop_map(|(tag, content)| { format!("<{tag}>{content}</{tag}>") }),
43 // Self-closing tag
44 tags.clone().prop_map(|tag| format!("<{tag}/>")),
45 // Nested tags
46 (tags.clone(), tags.clone(), text).prop_map(|(outer, inner, content)| {
47 format!("<{outer}><{inner}>{content}</{inner}></{outer}>")
48 }),
49 ],
50 1..10,
51 )
52 .prop_map(|parts| parts.join(""))
53 }
54
55 proptest! {
56 #[test]
57 fn never_panics(html in html_fragment()) {
58 let _ = pter::convert(&html);
59 }
60
61 #[test]
62 fn never_panics_on_arbitrary_bytes(s in "\\PC{0,500}") {
63 let _ = pter::convert(&s);
64 }
65
66 #[test]
67 fn output_contains_no_html_tags(html in html_fragment()) {
68 let md = pter::convert(&html);
69 // Output should never contain raw HTML tags
70 // (except inside code blocks, which we skip checking)
71 let without_code_blocks: String = md
72 .split("```")
73 .enumerate()
74 .filter(|(i, _)| i % 2 == 0) // only outside code blocks
75 .map(|(_, s)| s)
76 .collect();
77
78 // No <script>, <style>, <div>, etc. should leak through
79 assert!(!without_code_blocks.contains("<script"), "leaked <script> in: {md}");
80 assert!(!without_code_blocks.contains("<style"), "leaked <style> in: {md}");
81 assert!(!without_code_blocks.contains("<head"), "leaked <head> in: {md}");
82 }
83
84 #[test]
85 fn output_is_valid_utf8(html in html_fragment()) {
86 let md = pter::convert(&html);
87 // String type guarantees UTF-8, but verify no replacement chars snuck in
88 // from bad entity decoding
89 assert!(!md.contains('\u{FFFD}'), "replacement char in: {md}");
90 }
91
92 #[test]
93 fn no_excessive_blank_lines(html in html_fragment()) {
94 let md = pter::convert(&html);
95 assert!(!md.contains("\n\n\n"), "triple newline in output: {md}");
96 }
97
98 #[test]
99 fn no_trailing_whitespace_on_lines(html in html_fragment()) {
100 let md = pter::convert(&html);
101 for (i, line) in md.lines().enumerate() {
102 assert!(
103 line == line.trim_end(),
104 "trailing whitespace on line {i}: '{line}'"
105 );
106 }
107 }
108
109 #[test]
110 fn empty_input_returns_empty(s in "\\s{0,20}") {
111 let html = format!("<html><body>{s}</body></html>");
112 let md = pter::convert(&html);
113 // Whitespace-only input should produce empty or whitespace-only output
114 assert!(md.trim().is_empty() || !s.trim().is_empty());
115 }
116 }
117