carlos / todomvc-bakeoff Public

Clone
git clone https://amadan.net/carlos/todomvc-bakeoff

Plain git — no account needed to clone.

Download

Download this file

1## Unit tests: the store on its own, then the routes with fake requests.
2## Run with `make test`.
3
4import std/[unittest, os, tempfiles, strutils, uri]
5include "../src/app.nim"
6
7let dir = createTempDir("todo-nim-test", "")
8var dbCount = 0
9
10proc freshStore(): Store =
11 inc dbCount
12 store.open(dir / ("t" & $dbCount & ".db"))
13
14suite "store":
15 test "keeps each list to itself":
16 let s = freshStore()
17 let a = s.createList()
18 let b = s.createList()
19 s.add(a, "mine")
20 let id = s.todos(a)[0].id
21 check s.todos(b).len == 0
22 check not s.toggle(b, id)
23 check not s.rename(b, id, "theirs")
24 check not s.destroy(b, id)
25 check s.todos(a) == @[Todo(id: id, title: "mine", completed: false)]
26
27 test "filters, toggles all and clears completed":
28 let s = freshStore()
29 let l = s.createList()
30 for t in ["a", "b", "c"]: s.add(l, t)
31 check s.toggle(l, s.todos(l)[1].id)
32 check s.todos(l, fActive).len == 2
33 check s.todos(l, fCompleted).len == 1
34 s.toggleAll(l)
35 check s.counts(l) == (total: 3, completed: 3)
36 s.toggleAll(l)
37 check s.counts(l) == (total: 3, completed: 0)
38 discard s.toggle(l, s.todos(l)[0].id)
39 s.clearCompleted(l)
40 check s.todos(l).len == 2
41
42 test "keeps any Unicode and long titles exactly":
43 let s = freshStore()
44 let l = s.createList()
45 let long = "x".repeat(10_000)
46 s.add(l, "naïve 🚀 日本語 <b>")
47 s.add(l, long)
48 check s.todos(l)[0].title == "naïve 🚀 日本語 <b>"
49 check s.todos(l)[1].title == long
50
51 test "reopening runs no migration twice and keeps data":
52 let path = dir / "reopen.db"
53 var s = store.open(path)
54 let l = s.createList()
55 s.add(l, "kept")
56 s.close()
57 s = store.open(path)
58 check s.todos(l)[0].title == "kept"
59 check s.schemaVersion == 1
60 check fileExists(path & "-wal")
61 s.close()
62
63 test "list ids are 128 random bits":
64 let a = newListId()
65 check a.len == 32 and isWellFormedListId(a)
66 check a != newListId()
67 check not isWellFormedListId("%%%")
68
69# Routes
70
71let app = App(store: freshStore(), origin: "http://127.0.0.1:9", secure: false)
72
73proc request(meth: HttpMethod, target: string, body = "",
74 headers: openArray[(string, string)] = []): Request =
75 Request(reqMethod: meth, url: parseUri(target), body: body,
76 headers: newHttpHeaders(headers))
77
78proc headerOf(r: Reply, name: string): string =
79 for (k, v) in r.headers:
80 if k.cmpIgnoreCase(name) == 0: return v
81
82proc cookieOf(r: Reply): string =
83 r.headerOf("Set-Cookie").split(';')[0]
84
85proc post(target, body: string, cookie: string,
86 extra: openArray[(string, string)] = []): Reply =
87 app.route(request(HttpPost, target, body, @{"Cookie": cookie} & @extra))
88
89suite "routes":
90 test "the first visit gets a cookie and an empty page":
91 let r = app.route(request(HttpGet, "/"))
92 check r.code == Http200
93 let c = r.headerOf("Set-Cookie")
94 check c.startsWith("todo_list=") and "HttpOnly" in c and "SameSite=Lax" in c
95 check "Secure" notin c
96 check "<section class=\"main\">" notin r.body
97 check "Created by <a href=\"https://bakeoff.bfj.oncarlos.com\">the CARLOS bakeoff</a> · Nim" in r.body
98
99 test "a garbled or unknown cookie gets a fresh list":
100 for bad in ["%%%", "0123456789abcdef0123456789abcdef"]:
101 let r = app.route(request(HttpGet, "/", headers = {"Cookie": "todo_list=" & bad}))
102 check r.code == Http200
103 check r.cookieOf() != "todo_list=" & bad
104
105 test "actions answer 303 back to the filter":
106 let cookie = app.route(request(HttpGet, "/")).cookieOf()
107 for (filter, path) in [("all", "/"), ("active", "/active"),
108 ("completed", "/completed"), ("bogus", "/"), ("", "/")]:
109 let r = post("/todos", "title=x&filter=" & filter, cookie)
110 check r.code == Http303
111 check r.headerOf("Location") == path
112
113 test "titles are trimmed, blank ones ignored, and shown as text":
114 let cookie = app.route(request(HttpGet, "/")).cookieOf()
115 discard post("/todos", "title=" & encodeUrl(" ") & "&filter=all", cookie)
116 discard post("/todos", "title=" & encodeUrl(" <img src=x> & \"q\" "), cookie)
117 let page = app.route(request(HttpGet, "/", headers = {"Cookie": cookie})).body
118 check page.count("<li data-id=") == 1
119 check "<label>&lt;img src=x&gt; &amp; &quot;q&quot;</label>" in page
120
121 test "titles are trimmed of Unicode whitespace too":
122 let cookie = app.route(request(HttpGet, "/")).cookieOf()
123 let list = cookie.split('=')[1]
124 let spaces = "\u00A0\u3000\u2003\uFEFF"
125 discard post("/todos", "title=" & encodeUrl(spaces), cookie)
126 check app.store.todos(list).len == 0
127 discard post("/todos", "title=" & encodeUrl(spaces & "naïve 🚀" & spaces), cookie)
128 check app.store.todos(list)[0].title == "naïve 🚀"
129 let id = $app.store.todos(list)[0].id
130 discard post("/todos/" & id & "/edit", "title=" & encodeUrl("\u00A0\u00A0"), cookie)
131 check app.store.todos(list).len == 0
132
133 test "a title that isn't valid UTF-8 answers 400 and adds nothing":
134 let cookie = app.route(request(HttpGet, "/")).cookieOf()
135 let list = cookie.split('=')[1]
136 for bad in ["%80", "%C3", "%E2%82", "%F0%9F%9A", "a%FFb"]:
137 check post("/todos", "title=" & bad, cookie).code == Http400
138 check app.store.todos(list).len == 0
139 discard post("/todos", "title=ok", cookie)
140 let id = $app.store.todos(list)[0].id
141 check post("/todos/" & id & "/edit", "title=%80", cookie).code == Http400
142 check app.store.todos(list)[0].title == "ok"
143
144 test "another list's id answers 404 and changes nothing":
145 let mine = app.route(request(HttpGet, "/")).cookieOf()
146 let theirs = app.route(request(HttpGet, "/")).cookieOf()
147 discard post("/todos", "title=mine", mine)
148 let list = mine.split('=')[1]
149 let id = app.store.todos(list)[0].id
150 for verb in ["toggle", "destroy", "edit"]:
151 check post("/todos/" & $id & "/" & verb, "title=x", theirs).code == Http404
152 check post("/todos/nope/toggle", "", mine).code == Http404
153 check app.store.todos(list)[0] == Todo(id: id, title: "mine", completed: false)
154
155 test "a foreign Origin is refused; our own and none are allowed":
156 let cookie = app.route(request(HttpGet, "/")).cookieOf()
157 check post("/todos", "title=a", cookie, {"Origin": "https://evil.example"}).code == Http403
158 check post("/todos", "title=a", cookie, {"Origin": "http://127.0.0.1:9"}).code == Http303
159 check post("/todos", "title=a", cookie).code == Http303
160
161 test "editing: the edit view, renaming, and blank deletes":
162 let cookie = app.route(request(HttpGet, "/")).cookieOf()
163 discard post("/todos", "title=old", cookie)
164 let list = cookie.split('=')[1]
165 let id = $app.store.todos(list)[0].id
166 let page = app.route(request(HttpGet, "/active?edit=" & id,
167 headers = {"Cookie": cookie})).body
168 check "<li class=\"editing\" data-id=\"" & id & "\">" in page
169 check "action=\"/todos/" & id & "/edit\"" in page
170 discard post("/todos/" & id & "/edit", "title=" & encodeUrl(" new "), cookie)
171 check app.store.todos(list)[0].title == "new"
172 discard post("/todos/" & id & "/edit", "title=+++", cookie)
173 check app.store.todos(list).len == 0
174
175 test "the page's JavaScript gets a fragment instead of a redirect":
176 let cookie = app.route(request(HttpGet, "/")).cookieOf()
177 let r = post("/todos", "title=one&filter=active", cookie, {"X-Fragment": "1"})
178 check r.code == Http200
179 check r.body.strip.startsWith("<section class=\"main\">")
180 check "<strong>1</strong> item left" in r.body
181 check "<a class=\"selected\" href=\"/active\">" in r.body
182
183 test "counter, toggle-all state and clear-completed":
184 let cookie = app.route(request(HttpGet, "/")).cookieOf()
185 discard post("/todos", "title=a", cookie)
186 discard post("/todos", "title=b", cookie)
187 var page = app.route(request(HttpGet, "/", headers = {"Cookie": cookie})).body
188 check "<strong>2</strong> items left" in page
189 check "clear-completed" notin page.split("<footer class=\"footer\">")[1].split("</footer>")[0]
190 discard post("/todos/toggle-all", "", cookie)
191 page = app.route(request(HttpGet, "/", headers = {"Cookie": cookie})).body
192 check "<strong>0</strong> items left" in page
193 check "class=\"toggle-all\" type=\"checkbox\" checked" in page
194 check "<button class=\"clear-completed\">Clear completed</button>" in page
195
196 test "static files, health and version":
197 check app.route(request(HttpGet, "/static/base.css")).body == readFile("../../shared/base.css")
198 check app.route(request(HttpGet, "/static/index.css")).body == readFile("../../shared/index.css")
199 check app.route(request(HttpGet, "/healthz")).code == Http200
200 check app.route(request(HttpGet, "/api/version")).body == appVersion
201 check app.route(request(HttpGet, "/nope")).code == Http404
202
203app.store.close()
204removeDir(dir)
205