gitbayTests/ReleaseTests.swift

v1.11.0
gitbay-ios/gitbayTests/ReleaseTests.swift history · blame · raw

211 lines · 9671 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 releaseShowJSON = """
 21    {"protocol_version":1,"data":{"tag":"v1.0.0","title":"v1.0.0 — the CLI is the interface",\
 22    "notes":"Replace the binary and restart.","author":"cmc",\
 23    "created_at":"2026-08-26T05:27:36.148Z",\
 24    "assets":[{"name":"SHA256SUMS","size":557,\
 25    "sha256":"1646f3ec57f91bb2c346078735d47e6f853e4ee0ee5e5176123337663cf8c7f4"}]},"exit_code":0}
 26    """
 27
 28@MainActor
 29struct ReleaseListViewModelTests {
 30
 31    @Test func listsReleases() async throws {
 32        let (client, stub) = try makeClient()
 33        stub.enqueue(.init(status: 200, json: """
 34            {"protocol_version":1,"data":[\
 35            {"tag":"v1.0.0","title":"one","created_at":"2026-08-26T05:27:36.148Z"},\
 36            {"tag":"v0.5.0","title":"half","created_at":"2026-08-20T05:27:36.148Z"}\
 37            ],"exit_code":0}
 38            """))
 39        let model = ReleaseListViewModel(client: client, repoPath: "krz/gitbay")
 40
 41        await model.load()
 42
 43        #expect(model.state.value?.map(\.tag) == ["v1.0.0", "v0.5.0"])
 44        #expect(stub.seen.first?.url.query() == "argv=release&argv=list&argv=krz/gitbay")
 45    }
 46
 47    @Test func createSendsTagTitleAndNotesOverStdin() async throws {
 48        let (client, stub) = try makeClient()
 49        stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{},"exit_code":0}"#, match: "cmd"))
 50        stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"exit_code":0}"#, match: "argv=list"))
 51        let model = ReleaseListViewModel(client: client, repoPath: "krz/gitbay")
 52
 53        let created = await model.create(tag: "v1.1.0", title: "next", notes: "notes body")
 54
 55        #expect(created)
 56        let (argv, stdin) = try argvOf(try #require(stub.seen.first { $0.method == "POST" }))
 57        #expect(argv == ["release", "create", "krz/gitbay", "v1.1.0",
 58                         "--title", "next", "--file", "-"])
 59        #expect(stdin == "notes body")
 60    }
 61
 62    @Test func aMissingTagRefusalSurfacesVerbatim() async throws {
 63        let (client, stub) = try makeClient()
 64        stub.enqueue(.init(status: 404, json:
 65            #"{"protocol_version":1,"error":"no tag \"v9\" in krz/gitbay — push the tag first","exit_code":3}"#))
 66        let model = ReleaseListViewModel(client: client, repoPath: "krz/gitbay")
 67
 68        let created = await model.create(tag: "v9", title: "", notes: "")
 69
 70        #expect(!created)
 71        #expect(model.createError == #"no tag "v9" in krz/gitbay — push the tag first"#)
 72    }
 73
 74    @Test func emptyTitleAndNotesAreOmittedFromArgv() async throws {
 75        let (client, stub) = try makeClient()
 76        stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{},"exit_code":0}"#, match: "cmd"))
 77        stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"exit_code":0}"#, match: "argv=list"))
 78        let model = ReleaseListViewModel(client: client, repoPath: "krz/gitbay")
 79
 80        _ = await model.create(tag: "v1.1.0", title: "", notes: "")
 81
 82        let (argv, stdin) = try argvOf(try #require(stub.seen.first { $0.method == "POST" }))
 83        #expect(argv == ["release", "create", "krz/gitbay", "v1.1.0"])
 84        #expect(stdin == nil)
 85    }
 86}
 87
 88@MainActor
 89struct ReleaseDetailViewModelTests {
 90
 91    @Test func loadsShowWithAssets() async throws {
 92        let (client, stub) = try makeClient()
 93        stub.enqueue(.init(status: 200, json: releaseShowJSON))
 94        let model = ReleaseDetailViewModel(client: client, repoPath: "krz/gitbay", tag: "v1.0.0")
 95
 96        await model.load()
 97
 98        let release = try #require(model.state.value)
 99        #expect(release.assets?.count == 1)
100        #expect(stub.seen.first?.url.query() ==
101            "argv=release&argv=show&argv=krz/gitbay&argv=v1.0.0")
102    }
103
104    @Test func editSendsTitleAndNotes() async throws {
105        let (client, stub) = try makeClient()
106        stub.enqueue(.init(status: 200, json: releaseShowJSON, match: "argv=show"))
107        let model = ReleaseDetailViewModel(client: client, repoPath: "krz/gitbay", tag: "v1.0.0")
108        await model.load()
109        stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{},"exit_code":0}"#, match: "cmd"))
110        stub.enqueue(.init(status: 200, json: releaseShowJSON, match: "argv=show"))
111
112        await model.edit(title: "new title", notes: "new notes")
113
114        let (argv, stdin) = try argvOf(try #require(stub.seen.first { $0.method == "POST" }))
115        #expect(argv == ["release", "edit", "krz/gitbay", "v1.0.0",
116                         "--title", "new title", "--file", "-"])
117        #expect(stdin == "new notes")
118    }
119
120    @Test func removeAssetSendsTheCommandAndReloads() async throws {
121        let (client, stub) = try makeClient()
122        stub.enqueue(.init(status: 200, json: releaseShowJSON, match: "argv=show"))
123        let model = ReleaseDetailViewModel(client: client, repoPath: "krz/gitbay", tag: "v1.0.0")
124        await model.load()
125        stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{},"exit_code":0}"#, match: "cmd"))
126        stub.enqueue(.init(status: 200, json: releaseShowJSON, match: "argv=show"))
127
128        await model.removeAsset(named: "SHA256SUMS")
129
130        let (argv, _) = try argvOf(try #require(stub.seen.first { $0.method == "POST" }))
131        #expect(argv == ["release", "asset", "remove", "krz/gitbay", "v1.0.0", "SHA256SUMS"])
132        #expect(stub.seen.filter { $0.method == "GET" }.count == 2)
133        #expect(model.actionError == nil)
134    }
135
136    @Test func aRefusedAssetRemovalSurfaces() async throws {
137        let (client, stub) = try makeClient()
138        stub.enqueue(.init(status: 200, json: releaseShowJSON, match: "argv=show"))
139        let model = ReleaseDetailViewModel(client: client, repoPath: "krz/gitbay", tag: "v1.0.0")
140        await model.load()
141        stub.enqueue(.init(status: 403, json: #"{"protocol_version":1,"error":"write access required","exit_code":4}"#, match: "cmd"))
142
143        await model.removeAsset(named: "SHA256SUMS")
144
145        #expect(model.actionError == "write access required")
146        #expect(model.state.value?.assets?.count == 1)
147    }
148
149    @Test func downloadURLsUseTheWebRouteOnTheInstance() async throws {
150        let (client, _) = try makeClient()
151        let model = ReleaseDetailViewModel(client: client, repoPath: "krz/gitbay", tag: "v1.0.0")
152        let asset = Release.Asset(name: "gitbay-v1.0.0-linux-amd64", size: 1, sha256: "aa")
153
154        let url = model.downloadURL(for: asset)
155
156        #expect(url.absoluteString ==
157            "https://gitbay.org/krz/gitbay/releases/download/v1.0.0/gitbay-v1.0.0-linux-amd64")
158    }
159}
160
161struct FeedReleaseDestinationTests {
162
163    @Test func releaseEventsNavigateToTheRelease() throws {
164        let decoder = GitbayClient.decoder()
165        let event = try decoder.decode(FeedEvent.self, from: Data("""
166            {"id":2,"repo":"krz/gitbay","actor":"cmc","kind":"release.created",\
167            "data":{"tag":"v1.0.0"},"created_at":"2026-08-26T05:27:36Z"}
168            """.utf8))
169        #expect(event.destination == .release(repo: "krz/gitbay", tag: "v1.0.0"))
170    }
171}
172
173@MainActor
174struct ReleaseSummaryTests {
175
176    @Test func theSummaryIsOneLinePerMergeRequestAppendedAfterWhatWasTyped() async throws {
177        let (client, stub) = try makeClient()
178        stub.enqueue(.init(status: 200, json: """
179            {"protocol_version":1,"data":{"since":"v1.0.0","items":[\
180            {"number":12,"title":"Add a thing","author":"cmc","merged_at":"2026-08-20T10:00:00.000Z"},\
181            {"number":14,"title":"Fix it","author":"dee","merged_at":"2026-08-21T10:00:00.000Z"}]},"exit_code":0}
182            """))
183        let model = ReleaseListViewModel(client: client, repoPath: "krz/gitbay")
184
185        let lines = try #require(await model.summary(tag: "v1.1.0"))
186
187        #expect(ReleaseListViewModel.appending(lines, to: "Second cut.\n")
188                == "Second cut.\n\n- Add a thing (!12) by @cmc\n- Fix it (!14) by @dee\n")
189        #expect(ReleaseListViewModel.appending(lines, to: "  ") == lines)
190        #expect(stub.seen.first?.url.query() == "argv=release&argv=notes&argv=krz/gitbay&argv=v1.1.0")
191        #expect(model.summaryNote == nil)
192    }
193
194    @Test func anEmptyRangeGivesNoLinesAndSaysWhy() async throws {
195        let (client, stub) = try makeClient()
196        stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"since":"v1.0.0","items":[]},"exit_code":0}"#))
197        let model = ReleaseListViewModel(client: client, repoPath: "krz/gitbay")
198
199        #expect(await model.summary(tag: "v1.1.0") == nil)
200        #expect(model.summaryNote == "No merge requests landed since v1.0.0.")
201    }
202
203    @Test func aRefusalGivesNoLinesAndShowsIt() async throws {
204        let (client, stub) = try makeClient()
205        stub.enqueue(.init(status: 404, json: #"{"protocol_version":1,"error":"no ref \"v9\" in krz/gitbay","exit_code":3}"#))
206        let model = ReleaseListViewModel(client: client, repoPath: "krz/gitbay")
207
208        #expect(await model.summary(tag: "v9") == nil)
209        #expect(model.summaryNote == #"no ref "v9" in krz/gitbay"#)
210    }
211}