fuzzyworld-tools / files / tests / test_blog_comments.py
test_blog_comments.py
e9fdc5db 2026-11-01 Pavel Safronov | 1 | """Tests for reader comments in server/blog.py: email link, optional form, data-src. Run: python3 -m unittest discover tests""" |
| 2 | import json | |
| 3 | import os | |
| 4 | import sys | |
| 5 | import unittest | |
| 6 | ||
| 7 | sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) | |
| 8 | from test_blog import POST, Tree, blog, needs_markdown # noqa: E402 | |
| 9 | ||
| 10 | ON = {"comments_site": "writing", "comments_mailbox": "comments@example.org"} | |
| 11 | FIRST = "2018/03/02/first-post/index.html" | |
| 12 | ||
| 13 | ||
| 14 | def make(test, **config): | |
| 15 | t = Tree(test, config) | |
| 16 | t.file("2018-03-02-first-post.md", "---\npublished: true\n---\n" + POST) | |
| 17 | t.file("2016-05-06-old-post.md", "---\npublished: true\n---\n# Old\n\nSome old text.\n") | |
| 18 | return t | |
| 19 | ||
| 20 | ||
| 21 | @needs_markdown | |
| 22 | class CommentsOff(unittest.TestCase): | |
| 23 | def test_off_by_default_nothing_changes(self): | |
| 24 | t = make(self) | |
| 25 | _, out = t.build() | |
| 26 | everything = "\n".join(t.read(out, r) for r in (FIRST, "blog/index.html", "feed/index.xml")) | |
| 27 | for word in ("comments-section", "data-src", "mailto:", "comment-form"): | |
| 28 | self.assertNotIn(word, everything) | |
| 29 | self.assertFalse(os.path.exists(os.path.join(out, "_comments"))) | |
| 30 | ||
| 31 | def test_imported_comments_markup_is_the_same_off_and_on(self): | |
| 32 | data = {"/2018/03/02/first-post/": [ | |
| 33 | {"id": 1, "parent": 0, "author": "Eve", "date": "2018-03-03T10:00:00", "text": "Hi"}, | |
| 34 | {"id": 2, "parent": 1, "author": "Bob", "date": "2018-03-04T10:00:00", "text": "reply"}]} | |
| 35 | pages = [] | |
| 36 | for extra in ({}, ON): | |
| 37 | t = make(self, comments="comments.json", **extra) | |
| 38 | t.file("comments.json", json.dumps(data), base=t.dir) | |
| 39 | _, out = t.build() | |
| 40 | pages.append(t.read(out, FIRST)) | |
| 41 | off, on = pages | |
| 42 | tree = off[off.index('<ul class="comments">'):off.index('<p class="meta">Comments ©')] | |
| 43 | self.assertIn(tree, on) | |
| 44 | self.assertNotIn("data-src", off) | |
| 45 | self.assertIn('data-src="/_comments/2018/03/02/first-post.json"', on) | |
| 46 | ||
| 47 | ||
| 48 | @needs_markdown | |
| 49 | class CommentsOn(unittest.TestCase): | |
| 50 | def test_section_for_every_post_with_data_src_and_mailto(self): | |
| 51 | t = make(self, **ON) | |
| 52 | _, out = t.build() | |
| 53 | page = t.read(out, FIRST) | |
| 54 | self.assertIn('<section class="comments-section" data-src="/_comments/2018/03/02/first-post.json">', page) | |
| 55 | self.assertIn('<div class="comments-live"></div>', page) | |
| 56 | self.assertIn('href="mailto:comments@example.org?subject=Comment%20%5Bwriting%3A%2F2018%2F03%2F02%2Ffirst-post%2F%5D"', page) | |
| 57 | self.assertIn("Comments are moderated, and your address is never shown.", page) | |
| 58 | self.assertIn('data-src="/_comments/2016/05/06/old-post.json"', t.read(out, "2016/05/06/old-post/index.html")) | |
| 59 | self.assertNotIn("<form", page) | |
| 60 | self.assertNotIn('name="website"', page) | |
| 61 | self.assertFalse(os.path.exists(os.path.join(out, "_comments"))) | |
| 62 | blog.audit_output(out) | |
| 63 | ||
| 64 | def test_undated_url_scheme(self): | |
| 65 | t = make(self, url="/notes/{slug}/", **ON) | |
| 66 | _, out = t.build() | |
| 67 | page = t.read(out, "notes/first-post/index.html") | |
| 68 | self.assertIn('data-src="/_comments/notes/first-post.json"', page) | |
| 69 | self.assertIn("writing%3A%2Fnotes%2Ffirst-post%2F%5D", page) | |
| 70 | ||
| 71 | def test_hostile_title_does_not_reach_the_widget_and_values_are_escaped(self): | |
| 72 | t = Tree(self, {"comments_site": "writing", "comments_mailbox": "a+b&c@example.org", | |
| 73 | "comments_form": {"action": "https://c.example.org/post.php?a=1&b=2"}}) | |
| 74 | t.file("2018-03-02-x.md", '---\npublished: true\ntitle: "<script>alert(1)</script>"\n---\n# T\n\n## Writing\n\ntext\n') | |
| 75 | _, out = t.build() | |
| 76 | page = t.read(out, "2018/03/02/x/index.html") | |
| 77 | self.assertNotIn("<script>alert", page) | |
| 78 | self.assertIn("mailto:a%2Bb%26c@example.org?", page) | |
| 79 | self.assertIn('action="https://c.example.org/post.php?a=1&b=2"', page) | |
| 80 | ||
| 81 | def test_form_only_with_comments_form(self): | |
| 82 | t = make(self, comments_form={"action": "https://comments.example.org/post.php"}, **ON) | |
| 83 | _, out = t.build() | |
| 84 | page = t.read(out, FIRST) | |
| 85 | self.assertIn('<form class="comment-form" method="post" action="https://comments.example.org/post.php">', page) | |
| 86 | self.assertIn('<input type="hidden" name="site" value="writing">', page) | |
| 87 | self.assertIn('<input type="hidden" name="path" value="/2018/03/02/first-post/">', page) | |
| 88 | self.assertIn('<input type="hidden" name="elapsed" value="0">', page) | |
| 89 | self.assertIn('name="website" tabindex="-1" autocomplete="off"', page) | |
| 90 | self.assertIn('name="name" maxlength="60" required', page) | |
| 91 | self.assertIn('name="text" rows="5" maxlength="4000" required', page) | |
| 92 | self.assertIn('type="submit"', page) | |
| 93 | ||
| 94 | ||
| 95 | class CommentsConfig(unittest.TestCase): | |
| 96 | def bad(self, **config): | |
| 97 | with self.assertRaises(blog.BlogError): | |
| 98 | Tree(self, config).cfg() | |
| 99 | ||
| 100 | def test_validation(self): | |
| 101 | self.bad(comments_site="writing") # no mailbox | |
| 102 | self.bad(comments_site="writing", comments_mailbox="not-an-address") | |
| 103 | self.bad(comments_site="writing", comments_mailbox="a b@example.org") | |
| 104 | self.bad(comments_site="bad site", comments_mailbox="a@example.org") | |
| 105 | self.bad(comments_site=5, comments_mailbox="a@example.org") | |
| 106 | self.bad(comments_mailbox="a@example.org") # mailbox without a site | |
| 107 | self.bad(comments_form={"action": "https://x.org/p"}) # form without a site | |
| 108 | for form in ("https://x.org/p", {"action": "http://x.org/p"}, {"action": "https://"}, {"action": 3}, | |
| 109 | {"action": "https://x.org/p q"}, {"action": 'https://x.org/"'}, {"url": "https://x.org/p"}, | |
| 110 | {"action": "https://x.org/p", "extra": 1}, {}): | |
| 111 | self.bad(comments_form=form, **ON) | |
| 112 | ||
| 113 | def test_valid_form_is_accepted(self): | |
| 114 | cfg = Tree(self, {**ON, "comments_form": {"action": "https://x.org/p"}}).cfg() | |
| 115 | self.assertEqual(cfg["comments_form"], {"action": "https://x.org/p"}) | |
| 116 | ||
| 117 | def test_defaults(self): | |
| 118 | cfg = Tree(self).cfg() | |
| 119 | self.assertEqual((cfg["comments_site"], cfg["comments_mailbox"], cfg["comments_form"]), (None, None, None)) | |
| 120 | self.assertEqual(Tree(self, {**ON, "comments_form": None}).cfg()["comments_form"], None) | |
| 121 | ||
| 122 | ||
| 123 | if __name__ == "__main__": | |
| 124 | unittest.main() |