import Foundation import Testing @testable import gitbay private func makeClient() throws -> (GitbayClient, StubProtocol.Box) { let box = StubProtocol.box() let client = GitbayClient( instance: try GitbayInstance(url: "https://gitbay.org"), token: "test-token", session: box.session() ) return (client, box) } private func argvOf(_ seen: StubProtocol.Seen) throws -> ([String], String?) { let body = try #require(try JSONSerialization.jsonObject(with: seen.body) as? [String: Any]) return (try #require(body["argv"] as? [String]), body["stdin"] as? String) } private let snippetRow = """ {"id":"196505b81127","url":"https://gitbay.org/cmc/-/snippets/196505b81127","owner":"cmc",\ "description":"bootstrap file for homebrew","visibility":"public",\ "created_at":"2026-09-13T18:27:00.989Z","updated_at":"2026-09-13T18:27:00.989Z",\ "files":[{"name":"brew.sh","size":812}]} """ private let bareRow = """ {"id":"aa11bb22cc33","url":"https://gitbay.org/cmc/-/snippets/aa11bb22cc33","owner":"cmc",\ "visibility":"unlisted","created_at":"2026-09-14T10:00:00Z","updated_at":"2026-09-14T10:00:00Z",\ "files":[{"name":"notes.txt","size":4},{"name":"more.txt","size":5}]} """ private let listPageJSON = """ {"protocol_version":1,"data":{"items":[\(snippetRow),\(bareRow)]},"exit_code":0} """ private let showJSON = """ {"protocol_version":1,"data":{"id":"196505b81127","url":"https://gitbay.org/cmc/-/snippets/196505b81127",\ "owner":"cmc","description":"bootstrap file for homebrew","visibility":"public",\ "created_at":"2026-09-13T18:27:00.989Z","updated_at":"2026-09-13T18:27:00.989Z",\ "files":[{"name":"brew.sh","size":812,"content":"#!/usr/bin/env bash\\necho hi\\n"}]},"exit_code":0} """ private let okJSON = #"{"protocol_version":1,"data":{},"exit_code":0}"# struct SnippetDecodingTests { @Test func aListRowCarriesFileNamesAndSizesButNoContent() throws { let snippet = try GitbayClient.decoder().decode(Snippet.self, from: Data(snippetRow.utf8)) #expect(snippet.id == "196505b81127") #expect(snippet.owner == "cmc") #expect(snippet.visibility == "public") #expect(snippet.files.map(\.name) == ["brew.sh"]) #expect(snippet.files[0].size == 812) #expect(snippet.files[0].content == nil) #expect(snippet.title == "bootstrap file for homebrew") } /// The web titles a snippet by its description and falls back to the /// id; the description key is omitempty. @Test func withoutADescriptionTheTitleIsTheId() throws { let snippet = try GitbayClient.decoder().decode(Snippet.self, from: Data(bareRow.utf8)) #expect(snippet.description == nil) #expect(snippet.title == "aa11bb22cc33") #expect(snippet.fileNames == "notes.txt, more.txt") } } @MainActor struct SnippetListViewModelTests { @Test func mineListsWithoutAnOwner() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: listPageJSON)) let model = SnippetListViewModel(client: client, owner: nil) await model.load() #expect(stub.seen.first?.url.query()?.hasPrefix("argv=snippet&argv=list&argv=--limit") == true) #expect(model.state.value?.map(\.id) == ["196505b81127", "aa11bb22cc33"]) } @Test func anOwnersListNamesTheOwner() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: listPageJSON)) let model = SnippetListViewModel(client: client, owner: "cmc") await model.load() #expect(stub.seen.first?.url.query()?.hasPrefix("argv=snippet&argv=list&argv=cmc&argv=--limit") == true) } /// `snippet create [--description] [--visibility] < file`: /// the file travels as bare stdin, an empty description sends no /// flag, and the created snippet comes back. @Test func createSendsTheFileOnStdinAndReturnsTheSnippet() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: listPageJSON)) let model = SnippetListViewModel(client: client, owner: nil) await model.load() stub.enqueue(.init(status: 200, json: showJSON, match: "cmd")) stub.enqueue(.init(status: 200, json: listPageJSON, match: "argv=list")) let created = await model.create( filename: "brew.sh", description: "", visibility: "public", content: "echo hi\n") #expect(created?.id == "196505b81127") let (argv, stdin) = try argvOf(try #require(stub.seen.first { $0.method == "POST" })) #expect(argv == ["snippet", "create", "brew.sh", "--visibility", "public"]) #expect(stdin == "echo hi\n") #expect(stub.seen.filter { $0.method == "GET" }.count == 2) } @Test func aRefusedCreateSurfacesTheRule() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: listPageJSON)) let model = SnippetListViewModel(client: client, owner: nil) await model.load() stub.enqueue(.init(status: 200, json: """ {"protocol_version":1,"error":"snippet limit reached (20); delete one first","exit_code":2} """, match: "cmd")) let created = await model.create( filename: "x.txt", description: "d", visibility: "unlisted", content: "x") #expect(created == nil) #expect(model.actionError == "snippet limit reached (20); delete one first") } } @MainActor struct SnippetDetailViewModelTests { private func loaded() async throws -> (SnippetDetailViewModel, StubProtocol.Box) { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: showJSON, match: "argv=show")) let model = SnippetDetailViewModel(client: client, id: "196505b81127") await model.load() return (model, stub) } @Test func showCarriesEveryFilesContent() async throws { let (model, stub) = try await loaded() #expect(stub.seen.first?.url.query() == "argv=snippet&argv=show&argv=196505b81127") let snippet = try #require(model.state.value) #expect(snippet.files[0].content == "#!/usr/bin/env bash\necho hi\n") } @Test func editSendsOnlyTheFieldsGiven() async throws { let (model, stub) = try await loaded() for _ in 0..<2 { stub.enqueue(.init(status: 200, json: okJSON, match: "cmd")) stub.enqueue(.init(status: 200, json: showJSON, match: "argv=show")) } await model.edit(description: "renamed", visibility: "private") await model.edit(description: nil, visibility: "unlisted") let writes = try stub.seen.filter { $0.method == "POST" }.map { try argvOf($0).0 } #expect(writes == [ ["snippet", "edit", "196505b81127", "--description", "renamed", "--visibility", "private"], ["snippet", "edit", "196505b81127", "--visibility", "unlisted"], ]) } @Test func fileSetTravelsAsStdinAndRemoveNamesTheFile() async throws { let (model, stub) = try await loaded() for _ in 0..<2 { stub.enqueue(.init(status: 200, json: okJSON, match: "cmd")) stub.enqueue(.init(status: 200, json: showJSON, match: "argv=show")) } await model.setFile("notes.txt", content: "hello\n") await model.removeFile("notes.txt") let writes = try stub.seen.filter { $0.method == "POST" }.map(argvOf) #expect(writes[0].0 == ["snippet", "file", "set", "196505b81127", "notes.txt"]) #expect(writes[0].1 == "hello\n") #expect(writes[1].0 == ["snippet", "file", "remove", "196505b81127", "notes.txt"]) } @Test func deleteReportsSuccessAndDoesNotReload() async throws { let (model, stub) = try await loaded() stub.enqueue(.init(status: 200, json: okJSON, match: "cmd")) let ok = await model.delete() #expect(ok) #expect(try argvOf(try #require(stub.seen.first { $0.method == "POST" })).0 == ["snippet", "delete", "196505b81127"]) #expect(stub.seen.filter { $0.method == "GET" }.count == 1) } }