code.pablomurad.com

mdssg

Single-file static site generator for a folder of linked markdown notes

tests/test_cgi.py raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
import io
import json
import os
import unittest
from unittest import mock

from mdssg import cgiapp, config, render

from tests.test_build import SiteCase


class CgiCase(SiteCase):
    def index(self):
        return render.write_index(config.load(self.root))

    def request(self, path):
        stream = io.BytesIO()
        with mock.patch.dict(os.environ, {"PATH_INFO": path}, clear=False):
            code = cgiapp.main(self.root, stream)
        raw = stream.getvalue()
        head, _, body = raw.partition(b"\r\n\r\n")
        return code, head.decode("ascii"), body.decode("utf-8")


class IndexTests(CgiCase):
    def test_writes_routes_and_contexts(self):
        self.write("content/index.md", "---\ntitle: Home\n---\n\nSee [[Kant]].")
        self.write("content/notes/kant.md", "---\ntitle: Kant\n---\n\nbody")
        self.index()

        routes = json.loads((self.root / ".mdssg" / "routes.json").read_text(encoding="utf-8"))
        self.assertIn("/", routes["routes"])
        self.assertIn("/kant/", routes["routes"])
        self.assertTrue((self.root / ".mdssg" / "ctx" / "kant.json").is_file())

    def test_index_only_renders_no_pages(self):
        self.write("content/index.md", "---\ntitle: Home\n---\n")
        self.index()
        self.assertFalse((self.root / "public" / "index.html").exists())

    def test_seeds_are_routed(self):
        self.write("content/a.md", "---\ntitle: A\n---\n\nAbout [[Ghost]].")
        self.index()
        routes = json.loads((self.root / ".mdssg" / "routes.json").read_text(encoding="utf-8"))
        self.assertEqual(routes["routes"]["/ghost/"]["kind"], "seed")


class RequestTests(CgiCase):
    def setUp(self):
        super().setUp()
        self.write("content/index.md", "---\ntitle: Home\n---\n\nStart at [[Kant]].")
        self.write(
            "content/notes/kant.md",
            "---\ntitle: Kant\n---\n\n## Duty\n\nSee [[Hegel]] and [[Ghost]].",
        )
        self.write("content/notes/hegel.md", "---\ntitle: Hegel\n---\n\nAgainst [[Kant#Duty]].")
        self.index()

    def test_renders_a_note(self):
        code, head, body = self.request("/kant/")
        self.assertEqual(code, 0)
        self.assertIn("200 OK", head)
        self.assertIn("<h1 class=\"note-title\">Kant</h1>", body)
        self.assertIn("See", body)

    def test_serves_the_home_page(self):
        _, head, body = self.request("/")
        self.assertIn("200 OK", head)
        self.assertIn("Start at", body)

    def test_links_resolve_without_the_graph(self):
        _, _, body = self.request("/kant/")
        self.assertIn('href="/hegel/"', body)
        self.assertIn('href="/ghost/"', body)

    def test_anchors_resolve(self):
        _, _, body = self.request("/hegel/")
        self.assertIn('href="/kant/#duty"', body)

    def test_backlinks_are_served(self):
        _, _, body = self.request("/kant/")
        self.assertIn("Mentioned in", body)
        self.assertIn("<mark>", body)

    def test_seed_page_is_served(self):
        _, head, body = self.request("/ghost/")
        self.assertIn("200 OK", head)
        self.assertIn("seed-notice", body)
        self.assertIn("<mark>Ghost</mark>", body)

    def test_embeds_render_on_demand(self):
        self.write(
            "content/notes/embedder.md",
            "---\ntitle: Embedder\n---\n\n![[Kant#Duty]]",
        )
        self.index()
        _, _, body = self.request("/embedder/")
        self.assertIn('<figure class="embed">', body)
        self.assertIn("See", body)

    def test_unknown_path_is_404(self):
        code, head, body = self.request("/nope/")
        self.assertEqual(code, 0)
        self.assertIn("404", head)
        self.assertIn("Nothing is published", body)

    def test_trailing_slash_is_optional(self):
        _, head, _ = self.request("/kant")
        self.assertIn("200 OK", head)

    def test_query_string_is_ignored(self):
        _, head, _ = self.request("/kant/?utm_source=x")
        self.assertIn("200 OK", head)

    def test_headers_use_crlf_and_declare_length(self):
        _, head, body = self.request("/kant/")
        self.assertTrue(head.startswith("Status:"))
        self.assertIn("\r\n", head)
        self.assertNotIn("\r\r", head)
        length = int([l for l in head.split("\r\n") if l.startswith("Content-Length")][0].split(": ")[1])
        self.assertEqual(length, len(body.encode("utf-8")))

    def test_site_url_path_prefixes_cgi_links(self):
        self.write(
            "mdssg.toml",
            '[site]\ntitle = "Test"\nurl = "https://example.com/garden"\n',
        )
        self.index()
        _, _, body = self.request("/kant/")
        self.assertIn('href="/garden/hegel/"', body)
        self.assertIn('href="/garden/assets/style.css"', body)


class SecurityTests(CgiCase):
    def setUp(self):
        super().setUp()
        self.write("content/index.md", "---\ntitle: Home\n---\n")
        self.write("secret.txt", "do not serve me")
        self.index()

    def test_path_traversal_is_not_routable(self):
        for attack in (
            "/../secret.txt",
            "/../../etc/passwd",
            "/..%2f..%2fsecret.txt",
            "/content/notes/kant.md",
            "//etc/passwd",
        ):
            with self.subTest(attack=attack):
                _, head, _ = self.request(attack)
                self.assertIn("404", head)

    def test_a_failure_returns_500_without_a_traceback(self):
        (self.root / ".mdssg" / "ctx" / "home.json").write_text("{bad json", encoding="utf-8")
        code, head, body = self.request("/")
        self.assertEqual(code, 1)
        self.assertIn("500", head)
        self.assertNotIn("Traceback", body)
        self.assertNotIn(str(self.root), body)

    def test_the_failure_is_logged(self):
        (self.root / ".mdssg" / "ctx" / "home.json").write_text("{bad json", encoding="utf-8")
        self.request("/")
        log = (self.root / ".mdssg" / "error.log").read_text(encoding="utf-8")
        self.assertIn("Traceback", log)


class StaleTests(CgiCase):
    def test_edits_after_indexing_are_detected(self):
        self.write("content/index.md", "---\ntitle: Home\n---\n")
        self.index()
        application = cgiapp.Application(self.root)
        self.assertFalse(application.stale())

        path = self.root / "content" / "index.md"
        stat = path.stat()
        os.utime(path, ns=(stat.st_atime_ns, stat.st_mtime_ns + 10_000_000_000))
        application = cgiapp.Application(self.root)
        self.assertTrue(application.stale())
        _, body = application.handle("/")
        self.assertIn(b"CGI index was built", body)

    def test_deletions_after_indexing_are_detected(self):
        path = self.write("content/index.md", "---\ntitle: Home\n---\n")
        self.index()
        path.unlink()
        self.assertTrue(cgiapp.Application(self.root).stale())

    def test_config_changes_after_indexing_are_detected(self):
        self.write("content/index.md", "---\ntitle: Home\n---\n")
        self.index()
        self.write("mdssg.toml", '[site]\ntitle = "Changed"\n')
        self.assertTrue(cgiapp.Application(self.root).stale())

    def test_reindex_removes_obsolete_contexts(self):
        path = self.write("content/old.md", "---\ntitle: Old\n---\n")
        self.index()
        context = self.root / ".mdssg" / "ctx" / "old.json"
        self.assertTrue(context.exists())
        path.unlink()
        self.write("content/index.md", "---\ntitle: Home\n---\n")
        self.index()
        self.assertFalse(context.exists())


if __name__ == "__main__":
    unittest.main()