gitbayTests/RefsMilestoneTests.swift
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}