gitbayTests/SnippetTests.swift

v1.12.0
gitbay-ios/gitbayTests/SnippetTests.swift history · blame · raw

189 lines · 8246 bytes

  1import Foundation
  2import Testing
  3@testable import gitbay
  4
  5private func makeClient() throws -> (GitbayClient, StubProtocol.Box) {
  6    let box = StubProtocol.box()
  7    let client = GitbayClient(
  8        instance: try GitbayInstance(url: "https://gitbay.org"),
  9        token: "test-token",
 10        session: box.session()
 11    )
 12    return (client, box)
 13}
 14
 15private func argvOf(_ seen: StubProtocol.Seen) throws -> ([String], String?) {
 16    let body = try #require(try JSONSerialization.jsonObject(with: seen.body) as? [String: Any])
 17    return (try #require(body["argv"] as? [String]), body["stdin"] as? String)
 18}
 19
 20private let snippetRow = """
 21    {"id":"196505b81127","url":"https://gitbay.org/cmc/-/snippets/196505b81127","owner":"cmc",\
 22    "description":"bootstrap file for homebrew","visibility":"public",\
 23    "created_at":"2026-09-13T18:27:00.989Z","updated_at":"2026-09-13T18:27:00.989Z",\
 24    "files":[{"name":"brew.sh","size":812}]}
 25    """
 26private let bareRow = """
 27    {"id":"aa11bb22cc33","url":"https://gitbay.org/cmc/-/snippets/aa11bb22cc33","owner":"cmc",\
 28    "visibility":"unlisted","created_at":"2026-09-14T10:00:00Z","updated_at":"2026-09-14T10:00:00Z",\
 29    "files":[{"name":"notes.txt","size":4},{"name":"more.txt","size":5}]}
 30    """
 31private let listPageJSON = """
 32    {"protocol_version":1,"data":{"items":[\(snippetRow),\(bareRow)]},"exit_code":0}
 33    """
 34private let showJSON = """
 35    {"protocol_version":1,"data":{"id":"196505b81127","url":"https://gitbay.org/cmc/-/snippets/196505b81127",\
 36    "owner":"cmc","description":"bootstrap file for homebrew","visibility":"public",\
 37    "created_at":"2026-09-13T18:27:00.989Z","updated_at":"2026-09-13T18:27:00.989Z",\
 38    "files":[{"name":"brew.sh","size":812,"content":"#!/usr/bin/env bash\\necho hi\\n"}]},"exit_code":0}
 39    """
 40private let okJSON = #"{"protocol_version":1,"data":{},"exit_code":0}"#
 41
 42struct SnippetDecodingTests {
 43
 44    @Test func aListRowCarriesFileNamesAndSizesButNoContent() throws {
 45        let snippet = try GitbayClient.decoder().decode(Snippet.self, from: Data(snippetRow.utf8))
 46        #expect(snippet.id == "196505b81127")
 47        #expect(snippet.owner == "cmc")
 48        #expect(snippet.visibility == "public")
 49        #expect(snippet.files.map(\.name) == ["brew.sh"])
 50        #expect(snippet.files[0].size == 812)
 51        #expect(snippet.files[0].content == nil)
 52        #expect(snippet.title == "bootstrap file for homebrew")
 53    }
 54
 55    /// The web titles a snippet by its description and falls back to the
 56    /// id; the description key is omitempty.
 57    @Test func withoutADescriptionTheTitleIsTheId() throws {
 58        let snippet = try GitbayClient.decoder().decode(Snippet.self, from: Data(bareRow.utf8))
 59        #expect(snippet.description == nil)
 60        #expect(snippet.title == "aa11bb22cc33")
 61        #expect(snippet.fileNames == "notes.txt, more.txt")
 62    }
 63}
 64
 65@MainActor
 66struct SnippetListViewModelTests {
 67
 68    @Test func mineListsWithoutAnOwner() async throws {
 69        let (client, stub) = try makeClient()
 70        stub.enqueue(.init(status: 200, json: listPageJSON))
 71        let model = SnippetListViewModel(client: client, owner: nil)
 72
 73        await model.load()
 74
 75        #expect(stub.seen.first?.url.query()?.hasPrefix("argv=snippet&argv=list&argv=--limit") == true)
 76        #expect(model.state.value?.map(\.id) == ["196505b81127", "aa11bb22cc33"])
 77    }
 78
 79    @Test func anOwnersListNamesTheOwner() async throws {
 80        let (client, stub) = try makeClient()
 81        stub.enqueue(.init(status: 200, json: listPageJSON))
 82        let model = SnippetListViewModel(client: client, owner: "cmc")
 83
 84        await model.load()
 85
 86        #expect(stub.seen.first?.url.query()?.hasPrefix("argv=snippet&argv=list&argv=cmc&argv=--limit") == true)
 87    }
 88
 89    /// `snippet create <filename> [--description] [--visibility] < file`:
 90    /// the file travels as bare stdin, an empty description sends no
 91    /// flag, and the created snippet comes back.
 92    @Test func createSendsTheFileOnStdinAndReturnsTheSnippet() async throws {
 93        let (client, stub) = try makeClient()
 94        stub.enqueue(.init(status: 200, json: listPageJSON))
 95        let model = SnippetListViewModel(client: client, owner: nil)
 96        await model.load()
 97        stub.enqueue(.init(status: 200, json: showJSON, match: "cmd"))
 98        stub.enqueue(.init(status: 200, json: listPageJSON, match: "argv=list"))
 99
100        let created = await model.create(
101            filename: "brew.sh", description: "", visibility: "public", content: "echo hi\n")
102
103        #expect(created?.id == "196505b81127")
104        let (argv, stdin) = try argvOf(try #require(stub.seen.first { $0.method == "POST" }))
105        #expect(argv == ["snippet", "create", "brew.sh", "--visibility", "public"])
106        #expect(stdin == "echo hi\n")
107        #expect(stub.seen.filter { $0.method == "GET" }.count == 2)
108    }
109
110    @Test func aRefusedCreateSurfacesTheRule() async throws {
111        let (client, stub) = try makeClient()
112        stub.enqueue(.init(status: 200, json: listPageJSON))
113        let model = SnippetListViewModel(client: client, owner: nil)
114        await model.load()
115        stub.enqueue(.init(status: 200, json: """
116            {"protocol_version":1,"error":"snippet limit reached (20); delete one first","exit_code":2}
117            """, match: "cmd"))
118
119        let created = await model.create(
120            filename: "x.txt", description: "d", visibility: "unlisted", content: "x")
121
122        #expect(created == nil)
123        #expect(model.actionError == "snippet limit reached (20); delete one first")
124    }
125}
126
127@MainActor
128struct SnippetDetailViewModelTests {
129
130    private func loaded() async throws -> (SnippetDetailViewModel, StubProtocol.Box) {
131        let (client, stub) = try makeClient()
132        stub.enqueue(.init(status: 200, json: showJSON, match: "argv=show"))
133        let model = SnippetDetailViewModel(client: client, id: "196505b81127")
134        await model.load()
135        return (model, stub)
136    }
137
138    @Test func showCarriesEveryFilesContent() async throws {
139        let (model, stub) = try await loaded()
140        #expect(stub.seen.first?.url.query() == "argv=snippet&argv=show&argv=196505b81127")
141        let snippet = try #require(model.state.value)
142        #expect(snippet.files[0].content == "#!/usr/bin/env bash\necho hi\n")
143    }
144
145    @Test func editSendsOnlyTheFieldsGiven() async throws {
146        let (model, stub) = try await loaded()
147        for _ in 0..<2 {
148            stub.enqueue(.init(status: 200, json: okJSON, match: "cmd"))
149            stub.enqueue(.init(status: 200, json: showJSON, match: "argv=show"))
150        }
151
152        await model.edit(description: "renamed", visibility: "private")
153        await model.edit(description: nil, visibility: "unlisted")
154
155        let writes = try stub.seen.filter { $0.method == "POST" }.map { try argvOf($0).0 }
156        #expect(writes == [
157            ["snippet", "edit", "196505b81127", "--description", "renamed", "--visibility", "private"],
158            ["snippet", "edit", "196505b81127", "--visibility", "unlisted"],
159        ])
160    }
161
162    @Test func fileSetTravelsAsStdinAndRemoveNamesTheFile() async throws {
163        let (model, stub) = try await loaded()
164        for _ in 0..<2 {
165            stub.enqueue(.init(status: 200, json: okJSON, match: "cmd"))
166            stub.enqueue(.init(status: 200, json: showJSON, match: "argv=show"))
167        }
168
169        await model.setFile("notes.txt", content: "hello\n")
170        await model.removeFile("notes.txt")
171
172        let writes = try stub.seen.filter { $0.method == "POST" }.map(argvOf)
173        #expect(writes[0].0 == ["snippet", "file", "set", "196505b81127", "notes.txt"])
174        #expect(writes[0].1 == "hello\n")
175        #expect(writes[1].0 == ["snippet", "file", "remove", "196505b81127", "notes.txt"])
176    }
177
178    @Test func deleteReportsSuccessAndDoesNotReload() async throws {
179        let (model, stub) = try await loaded()
180        stub.enqueue(.init(status: 200, json: okJSON, match: "cmd"))
181
182        let ok = await model.delete()
183
184        #expect(ok)
185        #expect(try argvOf(try #require(stub.seen.first { $0.method == "POST" })).0
186            == ["snippet", "delete", "196505b81127"])
187        #expect(stub.seen.filter { $0.method == "GET" }.count == 1)
188    }
189}