## Unit tests: the store on its own, then the routes with fake requests. ## Run with `make test`. import std/[unittest, os, tempfiles, strutils, uri] include "../src/app.nim" let dir = createTempDir("todo-nim-test", "") var dbCount = 0 proc freshStore(): Store = inc dbCount store.open(dir / ("t" & $dbCount & ".db")) suite "store": test "keeps each list to itself": let s = freshStore() let a = s.createList() let b = s.createList() s.add(a, "mine") let id = s.todos(a)[0].id check s.todos(b).len == 0 check not s.toggle(b, id) check not s.rename(b, id, "theirs") check not s.destroy(b, id) check s.todos(a) == @[Todo(id: id, title: "mine", completed: false)] test "filters, toggles all and clears completed": let s = freshStore() let l = s.createList() for t in ["a", "b", "c"]: s.add(l, t) check s.toggle(l, s.todos(l)[1].id) check s.todos(l, fActive).len == 2 check s.todos(l, fCompleted).len == 1 s.toggleAll(l) check s.counts(l) == (total: 3, completed: 3) s.toggleAll(l) check s.counts(l) == (total: 3, completed: 0) discard s.toggle(l, s.todos(l)[0].id) s.clearCompleted(l) check s.todos(l).len == 2 test "keeps any Unicode and long titles exactly": let s = freshStore() let l = s.createList() let long = "x".repeat(10_000) s.add(l, "naïve 🚀 日本語 ") s.add(l, long) check s.todos(l)[0].title == "naïve 🚀 日本語 " check s.todos(l)[1].title == long test "reopening runs no migration twice and keeps data": let path = dir / "reopen.db" var s = store.open(path) let l = s.createList() s.add(l, "kept") s.close() s = store.open(path) check s.todos(l)[0].title == "kept" check s.schemaVersion == 1 check fileExists(path & "-wal") s.close() test "list ids are 128 random bits": let a = newListId() check a.len == 32 and isWellFormedListId(a) check a != newListId() check not isWellFormedListId("%%%") # Routes let app = App(store: freshStore(), origin: "http://127.0.0.1:9", secure: false) proc request(meth: HttpMethod, target: string, body = "", headers: openArray[(string, string)] = []): Request = Request(reqMethod: meth, url: parseUri(target), body: body, headers: newHttpHeaders(headers)) proc headerOf(r: Reply, name: string): string = for (k, v) in r.headers: if k.cmpIgnoreCase(name) == 0: return v proc cookieOf(r: Reply): string = r.headerOf("Set-Cookie").split(';')[0] proc post(target, body: string, cookie: string, extra: openArray[(string, string)] = []): Reply = app.route(request(HttpPost, target, body, @{"Cookie": cookie} & @extra)) suite "routes": test "the first visit gets a cookie and an empty page": let r = app.route(request(HttpGet, "/")) check r.code == Http200 let c = r.headerOf("Set-Cookie") check c.startsWith("todo_list=") and "HttpOnly" in c and "SameSite=Lax" in c check "Secure" notin c check "
" notin r.body check "Created by the CARLOS bakeoff · Nim" in r.body test "a garbled or unknown cookie gets a fresh list": for bad in ["%%%", "0123456789abcdef0123456789abcdef"]: let r = app.route(request(HttpGet, "/", headers = {"Cookie": "todo_list=" & bad})) check r.code == Http200 check r.cookieOf() != "todo_list=" & bad test "actions answer 303 back to the filter": let cookie = app.route(request(HttpGet, "/")).cookieOf() for (filter, path) in [("all", "/"), ("active", "/active"), ("completed", "/completed"), ("bogus", "/"), ("", "/")]: let r = post("/todos", "title=x&filter=" & filter, cookie) check r.code == Http303 check r.headerOf("Location") == path test "titles are trimmed, blank ones ignored, and shown as text": let cookie = app.route(request(HttpGet, "/")).cookieOf() discard post("/todos", "title=" & encodeUrl(" ") & "&filter=all", cookie) discard post("/todos", "title=" & encodeUrl(" & \"q\" "), cookie) let page = app.route(request(HttpGet, "/", headers = {"Cookie": cookie})).body check page.count("
  • <img src=x> & "q"" in page test "titles are trimmed of Unicode whitespace too": let cookie = app.route(request(HttpGet, "/")).cookieOf() let list = cookie.split('=')[1] let spaces = "\u00A0\u3000\u2003\uFEFF" discard post("/todos", "title=" & encodeUrl(spaces), cookie) check app.store.todos(list).len == 0 discard post("/todos", "title=" & encodeUrl(spaces & "naïve 🚀" & spaces), cookie) check app.store.todos(list)[0].title == "naïve 🚀" let id = $app.store.todos(list)[0].id discard post("/todos/" & id & "/edit", "title=" & encodeUrl("\u00A0\u00A0"), cookie) check app.store.todos(list).len == 0 test "a title that isn't valid UTF-8 answers 400 and adds nothing": let cookie = app.route(request(HttpGet, "/")).cookieOf() let list = cookie.split('=')[1] for bad in ["%80", "%C3", "%E2%82", "%F0%9F%9A", "a%FFb"]: check post("/todos", "title=" & bad, cookie).code == Http400 check app.store.todos(list).len == 0 discard post("/todos", "title=ok", cookie) let id = $app.store.todos(list)[0].id check post("/todos/" & id & "/edit", "title=%80", cookie).code == Http400 check app.store.todos(list)[0].title == "ok" test "another list's id answers 404 and changes nothing": let mine = app.route(request(HttpGet, "/")).cookieOf() let theirs = app.route(request(HttpGet, "/")).cookieOf() discard post("/todos", "title=mine", mine) let list = mine.split('=')[1] let id = app.store.todos(list)[0].id for verb in ["toggle", "destroy", "edit"]: check post("/todos/" & $id & "/" & verb, "title=x", theirs).code == Http404 check post("/todos/nope/toggle", "", mine).code == Http404 check app.store.todos(list)[0] == Todo(id: id, title: "mine", completed: false) test "a foreign Origin is refused; our own and none are allowed": let cookie = app.route(request(HttpGet, "/")).cookieOf() check post("/todos", "title=a", cookie, {"Origin": "https://evil.example"}).code == Http403 check post("/todos", "title=a", cookie, {"Origin": "http://127.0.0.1:9"}).code == Http303 check post("/todos", "title=a", cookie).code == Http303 test "editing: the edit view, renaming, and blank deletes": let cookie = app.route(request(HttpGet, "/")).cookieOf() discard post("/todos", "title=old", cookie) let list = cookie.split('=')[1] let id = $app.store.todos(list)[0].id let page = app.route(request(HttpGet, "/active?edit=" & id, headers = {"Cookie": cookie})).body check "
  • " in page check "action=\"/todos/" & id & "/edit\"" in page discard post("/todos/" & id & "/edit", "title=" & encodeUrl(" new "), cookie) check app.store.todos(list)[0].title == "new" discard post("/todos/" & id & "/edit", "title=+++", cookie) check app.store.todos(list).len == 0 test "the page's JavaScript gets a fragment instead of a redirect": let cookie = app.route(request(HttpGet, "/")).cookieOf() let r = post("/todos", "title=one&filter=active", cookie, {"X-Fragment": "1"}) check r.code == Http200 check r.body.strip.startsWith("
    ") check "1 item left" in r.body check "" in r.body test "counter, toggle-all state and clear-completed": let cookie = app.route(request(HttpGet, "/")).cookieOf() discard post("/todos", "title=a", cookie) discard post("/todos", "title=b", cookie) var page = app.route(request(HttpGet, "/", headers = {"Cookie": cookie})).body check "2 items left" in page check "clear-completed" notin page.split("
    ")[1].split("
    ")[0] discard post("/todos/toggle-all", "", cookie) page = app.route(request(HttpGet, "/", headers = {"Cookie": cookie})).body check "0 items left" in page check "class=\"toggle-all\" type=\"checkbox\" checked" in page check "" in page test "static files, health and version": check app.route(request(HttpGet, "/static/base.css")).body == readFile("../../shared/base.css") check app.route(request(HttpGet, "/static/index.css")).body == readFile("../../shared/index.css") check app.route(request(HttpGet, "/healthz")).code == Http200 check app.route(request(HttpGet, "/api/version")).body == appVersion check app.route(request(HttpGet, "/nope")).code == Http404 app.store.close() removeDir(dir)