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] { let body = try #require(try JSONSerialization.jsonObject(with: seen.body) as? [String: Any]) return try #require(body["argv"] as? [String]) } private let ok = """ {"protocol_version":1,"exit_code":0} """ @MainActor struct RepoActionsTests { @Test func bookmarkingAndUnbookmarkingAreDifferentCommands() async throws { let (client, stub) = try makeClient() let model = RepoActionsViewModel(client: client, repoPath: "krz/gitbay") stub.enqueue(.init(status: 200, json: ok)) await model.setBookmarked(false) #expect(stub.seen.contains { (try? argvOf($0)) == ["repo", "unbookmark", "krz/gitbay"] }) #expect(stub.seen.count == 1) let model2 = RepoActionsViewModel(client: client, repoPath: "krz/other") let before = stub.seen.count stub.enqueue(.init(status: 200, json: ok)) await model2.setBookmarked(true) #expect(stub.seen.contains { (try? argvOf($0)) == ["repo", "bookmark", "krz/other"] }) #expect(stub.seen.count == before + 1) } @Test func watchMuteAndUnwatchSendTheirOwnCommands() async throws { let (client, stub) = try makeClient() let model = RepoActionsViewModel(client: client, repoPath: "krz/gitbay") stub.enqueue(.init(status: 200, json: ok)) await model.watch() #expect(try argvOf(try #require(stub.seen.last)) == ["repo", "watch", "krz/gitbay"]) stub.enqueue(.init(status: 200, json: ok)) await model.mute() #expect(try argvOf(try #require(stub.seen.last)) == ["repo", "mute", "krz/gitbay"]) stub.enqueue(.init(status: 200, json: ok)) await model.unwatch() #expect(try argvOf(try #require(stub.seen.last)) == ["repo", "unwatch", "krz/gitbay"]) } @Test func forkingReturnsTheNewPath() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: """ {"protocol_version":1,"data":{"path":"cmc/gitbay","fork_of":"krz/gitbay"},\ "exit_code":0} """)) let model = RepoActionsViewModel(client: client, repoPath: "krz/gitbay") let result = await model.fork(named: nil) #expect(result?.path == "cmc/gitbay") #expect(result?.forkOf == "krz/gitbay") #expect(try argvOf(try #require(stub.seen.last)) == ["repo", "fork", "krz/gitbay"]) } @Test func forkingWithANameAppendsTheFlag() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: """ {"protocol_version":1,"data":{"path":"cmc/mine","fork_of":"krz/gitbay"},\ "exit_code":0} """)) let model = RepoActionsViewModel(client: client, repoPath: "krz/gitbay") _ = await model.fork(named: "mine") #expect(try argvOf(try #require(stub.seen.last)) == ["repo", "fork", "krz/gitbay", "--name", "mine"]) } /// Surrounding whitespace must not reach the server — the name is /// trimmed, not just tested for blankness. @Test func forkingWithSurroundingWhitespaceTrimsTheName() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: """ {"protocol_version":1,"data":{"path":"cmc/mine","fork_of":"krz/gitbay"},\ "exit_code":0} """)) let model = RepoActionsViewModel(client: client, repoPath: "krz/gitbay") _ = await model.fork(named: " mine ") #expect(try argvOf(try #require(stub.seen.last)) == ["repo", "fork", "krz/gitbay", "--name", "mine"]) } /// A blank name must not send an empty flag value. @Test func forkingWithABlankNameOmitsTheFlag() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: """ {"protocol_version":1,"data":{"path":"cmc/gitbay","fork_of":"krz/gitbay"},\ "exit_code":0} """)) let model = RepoActionsViewModel(client: client, repoPath: "krz/gitbay") _ = await model.fork(named: " ") #expect(try argvOf(try #require(stub.seen.last)) == ["repo", "fork", "krz/gitbay"]) } @Test func aRefusedForkReturnsNilAndSurfaces() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: """ {"protocol_version":1,"error":"a repository named gitbay already exists",\ "exit_code":1} """)) let model = RepoActionsViewModel(client: client, repoPath: "krz/gitbay") let result = await model.fork(named: nil) #expect(result == nil) #expect(model.actionError?.isEmpty == false) #expect(model.working == false) } /// A stale `notice` from an earlier success must not survive a later /// failure — the screen must never show a success banner and a /// failure banner at once. @Test func aFailedActionClearsAStaleNotice() async throws { let (client, stub) = try makeClient() let model = RepoActionsViewModel(client: client, repoPath: "krz/gitbay") stub.enqueue(.init(status: 200, json: ok)) await model.watch() #expect(model.notice != nil) stub.enqueue(.init(status: 200, json: """ {"protocol_version":1,"error":"a repository named gitbay already exists",\ "exit_code":1} """)) _ = await model.fork(named: nil) #expect(model.notice == nil) #expect(model.actionError?.isEmpty == false) } /// A 0-exit fork whose envelope carries no `data` must not fail /// silently — the caller needs to know nothing came back. @Test func aForkThatReportsNothingBackSetsAnError() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: """ {"protocol_version":1,"exit_code":0} """)) let model = RepoActionsViewModel(client: client, repoPath: "krz/gitbay") let result = await model.fork(named: nil) #expect(result == nil) #expect(model.actionError?.isEmpty == false) } }