| 1 | import Foundation |
| 2 | import Testing |
| 3 | @testable import TodoApp |
| 4 | |
| 5 | /// A store on a fresh database file, removed afterwards. |
| 6 | func withStore<T>(_ body: (Store, String) async throws -> T) async throws -> T { |
| 7 | let dir = FileManager.default.temporaryDirectory.appendingPathComponent("todo-swift-test-\(UUID().uuidString)") |
| 8 | try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) |
| 9 | defer { try? FileManager.default.removeItem(at: dir) } |
| 10 | let path = dir.appendingPathComponent("app.db").path |
| 11 | let store = try await Store.open(path: path) |
| 12 | do { |
| 13 | let result = try await body(store, path) |
| 14 | try await store.close() |
| 15 | return result |
| 16 | } catch { |
| 17 | try? await store.close() |
| 18 | throw error |
| 19 | } |
| 20 | } |
| 21 | |
| 22 | @Suite struct StoreTests { |
| 23 | @Test func usesWALAndRecordsTheSchemaVersion() async throws { |
| 24 | try await withStore { store, path in |
| 25 | #expect(try await store.db.query("PRAGMA journal_mode").first?.column("journal_mode")?.string == "wal") |
| 26 | let version = try await store.db.query("PRAGMA user_version").first?.column("user_version")?.integer |
| 27 | #expect(version == Store.migrations.count) |
| 28 | // Opening again runs nothing twice and keeps the data. |
| 29 | let list = try await store.createList(token: Visitors.newToken()) |
| 30 | try await store.add(list: list, title: "kept") |
| 31 | let again = try await Store.open(path: path) |
| 32 | #expect(try await again.todos(list: list).map(\.title) == ["kept"]) |
| 33 | try await again.close() |
| 34 | } |
| 35 | } |
| 36 | |
| 37 | @Test func toggleAllCompletesEverythingThenReopensEverything() async throws { |
| 38 | try await withStore { store, _ in |
| 39 | let list = try await store.createList(token: Visitors.newToken()) |
| 40 | for title in ["a", "b", "c"] { try await store.add(list: list, title: title) } |
| 41 | let first = try await store.todos(list: list)[0].id |
| 42 | #expect(try await store.toggle(list: list, id: first)) |
| 43 | |
| 44 | try await store.toggleAll(list: list) |
| 45 | #expect(try await store.todos(list: list).allSatisfy(\.completed)) |
| 46 | try await store.toggleAll(list: list) |
| 47 | #expect(try await store.todos(list: list).allSatisfy { !$0.completed }) |
| 48 | } |
| 49 | } |
| 50 | |
| 51 | @Test func listsAreSeparate() async throws { |
| 52 | try await withStore { store, _ in |
| 53 | let mine = try await store.createList(token: Visitors.newToken()) |
| 54 | let theirs = try await store.createList(token: Visitors.newToken()) |
| 55 | try await store.add(list: mine, title: "mine") |
| 56 | let id = try await store.todos(list: mine)[0].id |
| 57 | |
| 58 | #expect(try await store.toggle(list: theirs, id: id) == false) |
| 59 | #expect(try await store.rename(list: theirs, id: id, title: "x") == false) |
| 60 | #expect(try await store.destroy(list: theirs, id: id) == false) |
| 61 | try await store.toggleAll(list: theirs) |
| 62 | try await store.clearCompleted(list: theirs) |
| 63 | #expect(try await store.todos(list: mine) == [Todo(id: id, title: "mine", completed: false)]) |
| 64 | #expect(try await store.todos(list: theirs).isEmpty) |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | @Test func clearCompletedKeepsActiveItems() async throws { |
| 69 | try await withStore { store, _ in |
| 70 | let list = try await store.createList(token: Visitors.newToken()) |
| 71 | for title in ["a", "b", "c"] { try await store.add(list: list, title: title) } |
| 72 | let b = try await store.todos(list: list)[1].id |
| 73 | _ = try await store.toggle(list: list, id: b) |
| 74 | try await store.clearCompleted(list: list) |
| 75 | #expect(try await store.todos(list: list).map(\.title) == ["a", "c"]) |
| 76 | } |
| 77 | } |
| 78 | } |
| 79 | |
| 80 | @Suite struct OptionsTests { |
| 81 | @Test func socketMode() throws { |
| 82 | let o = try Options(arguments: ["--socket", "/run/a.sock", "--db", "/d/app.db"], environment: ["CARLOS_HOST": "todo-swift.bfj.oncarlos.com"]) |
| 83 | #expect(o.address == .socket("/run/a.sock")) |
| 84 | #expect(o.origin == "https://todo-swift.bfj.oncarlos.com") |
| 85 | #expect(o.servedOverHTTPS) |
| 86 | } |
| 87 | |
| 88 | @Test func listenMode() throws { |
| 89 | let o = try Options(arguments: ["--listen", "127.0.0.1:8080", "--db", "x.db"], environment: [:]) |
| 90 | #expect(o.address == .listen(host: "127.0.0.1", port: 8080)) |
| 91 | #expect(o.origin == "http://127.0.0.1:8080") |
| 92 | #expect(!o.servedOverHTTPS) |
| 93 | } |
| 94 | |
| 95 | @Test(arguments: [ |
| 96 | ["--listen", "0.0.0.0:8080", "--db", "x.db"], |
| 97 | ["--listen", ":8080", "--db", "x.db"], |
| 98 | ["--listen", "127.0.0.1:8080"], |
| 99 | ["--socket", "a.sock", "--listen", "127.0.0.1:1", "--db", "x.db"], |
| 100 | ["--db"], |
| 101 | ]) |
| 102 | func refusesBadArguments(_ arguments: [String]) { |
| 103 | #expect(throws: Options.UsageError.self) { try Options(arguments: arguments, environment: [:]) } |
| 104 | } |
| 105 | } |
| 106 | |
| 107 | @Suite struct AssetTests { |
| 108 | static let appDir = URL(fileURLWithPath: #filePath).deletingLastPathComponent().deletingLastPathComponent().deletingLastPathComponent() |
| 109 | |
| 110 | @Test(arguments: [ |
| 111 | ("../../shared/base.css", Assets.baseCSS), |
| 112 | ("../../shared/index.css", Assets.indexCSS), |
| 113 | ("static/app.css", Assets.appCSS), |
| 114 | ("static/app.js", Assets.appJS), |
| 115 | ]) |
| 116 | func embeddedFilesMatchTheirSources(_ path: String, _ embedded: String) throws { |
| 117 | let source = try Data(contentsOf: Self.appDir.appendingPathComponent(path)) |
| 118 | #expect(Data(embedded.utf8) == source, "run tools/embed.sh") |
| 119 | } |
| 120 | } |
| 121 | |