a native ios client for gitbay

client ios swift

https://gitbay.org

gitbayTests/RefsMilestoneTests.swift

main
gitbay-ios/gitbayTests/RefsMilestoneTests.swift history · blame · raw

163 lines · 6443 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] {
 16    let body = try #require(try JSONSerialization.jsonObject(with: seen.body) as? [String: Any])
 17    return try #require(body["argv"] as? [String])
 18}
 19
 20private let refsJSON = """
 21    {"protocol_version":1,"data":{"branches":[\
 22    {"name":"main","sha":"aaaaaaaaaabbbbbbbbbb"},{"name":"feature","sha":"cccccccccc"}],\
 23    "tags":[{"name":"v1.0.0","sha":"dddddddddd"}]},"exit_code":0}
 24    """
 25private let repoShowJSON = """
 26    {"protocol_version":1,"data":{"path":"krz/gitbay","visibility":"public",\
 27    "default_branch":"main"},"exit_code":0}
 28    """
 29private let milestonesJSON = """
 30    {"protocol_version":1,"data":[\
 31    {"title":"v1.0.0","description":"launch","state":"open","open":3,"closed":9},\
 32    {"title":"v0.5.0","state":"closed","open":0,"closed":4}],"exit_code":0}
 33    """
 34
 35@MainActor
 36struct RefsViewModelTests {
 37
 38    private func loadedModel() async throws -> (RefsViewModel, StubProtocol.Box) {
 39        let (client, stub) = try makeClient()
 40        stub.enqueue(.init(status: 200, json: refsJSON, match: "argv=refs"))
 41        stub.enqueue(.init(status: 200, json: repoShowJSON, match: "argv=show"))
 42        let model = RefsViewModel(client: client, repoPath: "krz/gitbay")
 43        await model.load()
 44        return (model, stub)
 45    }
 46
 47    @Test func loadsBranchesTagsAndTheDefault() async throws {
 48        let (model, stub) = try await loadedModel()
 49
 50        #expect(model.branches.map(\.name) == ["main", "feature"])
 51        #expect(model.tags.map(\.name) == ["v1.0.0"])
 52        #expect(model.defaultBranch == "main")
 53        #expect(stub.seen.contains { $0.url.query() == "argv=repo&argv=refs&argv=krz/gitbay" })
 54    }
 55
 56    @Test func theFilterMatchesBothBranchesAndTags() async throws {
 57        let (model, _) = try await loadedModel()
 58
 59        model.searchText = "v1"
 60        #expect(model.branches.isEmpty)
 61        #expect(model.tags.map(\.name) == ["v1.0.0"])
 62
 63        model.searchText = "feat"
 64        #expect(model.branches.map(\.name) == ["feature"])
 65        #expect(model.tags.isEmpty)
 66    }
 67
 68    @Test func aRepoWithoutRefsIsAnEmptyState() async throws {
 69        let (client, stub) = try makeClient()
 70        stub.enqueue(.init(status: 200, json:
 71            #"{"protocol_version":1,"data":{"branches":[],"tags":[]},"exit_code":0}"#,
 72            match: "argv=refs"))
 73        stub.enqueue(.init(status: 200, json: repoShowJSON, match: "argv=show"))
 74        let model = RefsViewModel(client: client, repoPath: "krz/empty")
 75
 76        await model.load()
 77
 78        guard case .empty = model.state else {
 79            Issue.record("expected .empty, got \(model.state)")
 80            return
 81        }
 82    }
 83}
 84
 85@MainActor
 86struct MilestoneListViewModelTests {
 87
 88    @Test func loadsMilestonesWithProgress() async throws {
 89        let (client, stub) = try makeClient()
 90        stub.enqueue(.init(status: 200, json: milestonesJSON))
 91        let model = MilestoneListViewModel(client: client, repoPath: "krz/gitbay")
 92
 93        await model.load()
 94
 95        let milestones = try #require(model.state.value)
 96        #expect(milestones.map(\.title) == ["v1.0.0", "v0.5.0"])
 97        #expect(milestones[0].open == 3 && milestones[0].closed == 9)
 98        #expect(stub.seen.first?.url.query() ==
 99            "argv=milestone&argv=list&argv=krz/gitbay&argv=--state&argv=open")
100    }
101
102    @Test func changingTheFilterReloadsWithThatState() async throws {
103        let (client, stub) = try makeClient()
104        stub.enqueue(.init(status: 200, json: milestonesJSON))
105        stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"exit_code":0}"#))
106        let model = MilestoneListViewModel(client: client, repoPath: "krz/gitbay")
107        await model.load()
108
109        model.filter = .closed
110        try await Task.sleep(for: .milliseconds(300))
111
112        #expect(stub.seen.count == 2)
113        #expect(stub.seen[1].url.query()?.contains("argv=closed") == true)
114    }
115}
116
117@MainActor
118struct MRMilestoneTests {
119
120    private let mrShow = """
121        {"protocol_version":1,"data":{"number":7,"title":"t","state":"open","author":"cmc",\
122        "source":"b","target_ref":"main","head_sha":"aa","milestone":"v1.0.0",\
123        "created_at":"2026-08-20T10:00:00.000Z"},"exit_code":0}
124        """
125
126    private func loadedModel() async throws -> (MRDetailViewModel, StubProtocol.Box) {
127        let (client, stub) = try makeClient()
128        stub.enqueue(.init(status: 200, json: mrShow, match: "argv=show"))
129        stub.enqueue(.init(status: 200, json:
130            #"{"protocol_version":1,"output":"","exit_code":0}"#, match: "argv=diff"))
131        stub.enqueue(.init(status: 200, json:
132            #"{"protocol_version":1,"exit_code":0}"#, match: "argv=threads"))
133        let model = MRDetailViewModel(client: client, repoPath: "krz/gitbay", number: 7)
134        await model.load()
135        return (model, stub)
136    }
137
138    @Test func mrShowCarriesItsMilestone() async throws {
139        let (model, _) = try await loadedModel()
140        // Previously a blind write: you could set it and nothing read back.
141        #expect(model.state.value?.milestone == "v1.0.0")
142    }
143
144    @Test func settingAndClearingUseTheCommandsSpelling() async throws {
145        let (model, stub) = try await loadedModel()
146        for _ in 0..<2 {
147            stub.enqueue(.init(status: 200, json:
148                #"{"protocol_version":1,"data":{},"exit_code":0}"#, match: "cmd"))
149            stub.enqueue(.init(status: 200, json: mrShow, match: "argv=show"))
150            stub.enqueue(.init(status: 200, json:
151                #"{"protocol_version":1,"output":"","exit_code":0}"#, match: "argv=diff"))
152            stub.enqueue(.init(status: 200, json:
153                #"{"protocol_version":1,"exit_code":0}"#, match: "argv=threads"))
154        }
155
156        await model.setMilestone("v1.1.0")
157        await model.setMilestone(nil)
158
159        let writes = try stub.seen.filter { $0.method == "POST" }.map(argvOf)
160        #expect(writes[0] == ["mr", "milestone", "krz/gitbay", "7", "v1.1.0"])
161        #expect(writes[1] == ["mr", "milestone", "krz/gitbay", "7", "none"])
162    }
163}