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 mrShow(labels: String?) -> String { let field = labels.map { #","labels":\#($0)"# } ?? "" return """ {"protocol_version":1,"data":{"number":7,"title":"t","state":"open","author":"cmc",\ "source":"feature","target_ref":"main","head_sha":"aa",\ "created_at":"2026-09-22T04:40:16.000Z"\(field)},"exit_code":0} """ } private let okJSON = #"{"protocol_version":1,"exit_code":0}"# 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]) } @MainActor struct MRLabelTests { @Test func mrShowCarriesItsLabels() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: mrShow(labels: #"["bug","ios"]"#), match: "argv=show")) let model = MRDetailViewModel(client: client, repoPath: "krz/gitbay", number: 7) await model.load() #expect(try #require(model.state.value).labels == ["bug", "ios"]) } /// The server omits the key when there are none. @Test func noLabelsKeyMeansNoLabels() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: mrShow(labels: nil), match: "argv=show")) let model = MRDetailViewModel(client: client, repoPath: "krz/gitbay", number: 7) await model.load() #expect(try #require(model.state.value).labels.isEmpty) } @Test func addingAndRemovingSendMrLabel() async throws { let (client, stub) = try makeClient() let model = MRDetailViewModel(client: client, repoPath: "krz/gitbay", number: 7) stub.enqueue(.init(status: 200, json: okJSON, match: "cmd")) stub.enqueue(.init(status: 200, json: mrShow(labels: #"["bug"]"#), match: "argv=show")) await model.addLabel("bug") stub.enqueue(.init(status: 200, json: okJSON, match: "cmd")) stub.enqueue(.init(status: 200, json: mrShow(labels: nil), match: "argv=show")) await model.removeLabel("bug") let writes = try stub.seen.filter { $0.method == "POST" }.map(argvOf) #expect(writes == [ ["mr", "label", "krz/gitbay", "7", "--add", "bug"], ["mr", "label", "krz/gitbay", "7", "--remove", "bug"], ]) #expect(model.actionError == nil) } /// Colours come from the repository's label set, read once. @Test func labelColoursAreReadOnce() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: """ {"protocol_version":1,"data":[{"name":"bug","color":"#d73a4a","issues":2}],"exit_code":0} """, match: "argv=label&argv=list")) let model = MRDetailViewModel(client: client, repoPath: "krz/gitbay", number: 7) await model.loadLabels() await model.loadLabels() #expect(stub.seen.count == 1) #expect(stub.seen[0].url.query() == "argv=label&argv=list&argv=krz/gitbay") #expect(model.colors.hex("bug") == LabelPalette.hex(name: "bug", stored: "#d73a4a")) } } struct MRLabelFilterFlagTests { /// Same position as in IssueFilter: after --search, before --author. @Test func aLabelSendsTheLabelFlag() { var filter = MRFilter() filter.search = "gate" filter.label = "bug" filter.author = "cmc" #expect(filter.flags() == ["--state", "open", "--search", "gate", "--label", "bug", "--author", "cmc"]) #expect(filter.isNarrowed) } @Test func noLabelSendsNoFlag() { #expect(!MRFilter().flags().contains("--label")) } } @MainActor struct MRLabelFilterTests { @Test func pickingALabelReloadsWithIt() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"items":[]},"exit_code":0}"#)) let model = MRListViewModel(client: client, repoPath: "krz/gitbay") await model.load() stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"items":[]},"exit_code":0}"#)) model.filter.label = "bug" await until { guard stub.seen.count == 2 else { return false } if case .loading = model.state { return false } return true } let query = try #require(stub.seen.last?.url.query()) #expect(query.contains("argv=--label&argv=bug")) } @Test func thePickerOffersTheRepositorysLabels() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: """ {"protocol_version":1,"data":[{"name":"bug","color":"#d73a4a","issues":2},\ {"name":"ios","issues":0}],"exit_code":0} """, match: "argv=label&argv=list")) let model = MRListViewModel(client: client, repoPath: "krz/gitbay") await model.loadLabels() await model.loadLabels() #expect(model.labels.map(\.name) == ["bug", "ios"]) #expect(model.colors.hex("bug") == LabelPalette.hex(name: "bug", stored: "#d73a4a")) #expect(stub.seen.count == 1) } }