gitbayTests/ComposeViewModelTests.swift
213 lines · 9214 bytes
17 symbols in this file
makeClientargvOfIssueCreateViewModelTestscreateSendsTitleInArgvAndBodyOverStdinanEmptyBodyOmitsStdinAndTheFileFlagaRefusalSurfacesAndReturnsNilIssueTemplateTestsloadTemplatesReadsTheRepoTemplatesaTemplateReadFailureLeavesNoTemplatesAndNoErrorMRCreateViewModelTeststargetPrefillsFromRepoShowcreateSendsBranchesTitleAndBodyEditAndMilestoneTestsissueEditSendsTitleAndBodyAlwaysmilestoneSetAndClearSpellNoneCorrectlymilestonesLoadOnceForThePickermrEditSendsTitleAndBody
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 let createdJSON = #"{"protocol_version":1,"data":{"number":7},"exit_code":0}"#
16private let okJSON = #"{"protocol_version":1,"data":{},"exit_code":0}"#
17
18private func argvOf(_ seen: StubProtocol.Seen) throws -> ([String], String?) {
19 let body = try #require(try JSONSerialization.jsonObject(with: seen.body) as? [String: Any])
20 return (try #require(body["argv"] as? [String]), body["stdin"] as? String)
21}
22
23@MainActor
24struct IssueCreateViewModelTests {
25
26 @Test func createSendsTitleInArgvAndBodyOverStdin() async throws {
27 let (client, stub) = try makeClient()
28 stub.enqueue(.init(status: 200, json: createdJSON))
29 let model = IssueCreateViewModel(client: client, repoPath: "krz/gitbay")
30
31 let number = await model.create(title: "org READMEs", body: "render them properly")
32
33 #expect(number == 7)
34 let (argv, stdin) = try argvOf(try #require(stub.seen.first))
35 #expect(argv == ["issue", "create", "krz/gitbay", "--title", "org READMEs", "--file", "-"])
36 #expect(stdin == "render them properly")
37 }
38
39 @Test func anEmptyBodyOmitsStdinAndTheFileFlag() async throws {
40 let (client, stub) = try makeClient()
41 stub.enqueue(.init(status: 200, json: createdJSON))
42 let model = IssueCreateViewModel(client: client, repoPath: "krz/gitbay")
43
44 _ = await model.create(title: "just a title", body: "")
45
46 let (argv, stdin) = try argvOf(try #require(stub.seen.first))
47 #expect(argv == ["issue", "create", "krz/gitbay", "--title", "just a title"])
48 #expect(stdin == nil)
49 }
50
51 @Test func aRefusalSurfacesAndReturnsNil() async throws {
52 let (client, stub) = try makeClient()
53 stub.enqueue(.init(status: 403, json:
54 #"{"protocol_version":1,"error":"krz/gitbay is archived and read-only","exit_code":4}"#))
55 let model = IssueCreateViewModel(client: client, repoPath: "krz/gitbay")
56
57 let number = await model.create(title: "t", body: "b")
58
59 #expect(number == nil)
60 #expect(model.errorMessage == "krz/gitbay is archived and read-only")
61 }
62}
63
64@MainActor
65struct IssueTemplateTests {
66
67 @Test func loadTemplatesReadsTheRepoTemplates() async throws {
68 let (client, stub) = try makeClient()
69 stub.enqueue(.init(status: 200, json: """
70 {"protocol_version":1,"data":[\
71 {"name":"bug","body":"## Steps\\n\\n1. "},\
72 {"name":"feature","body":"## Why\\n"}],"exit_code":0}
73 """))
74 let model = IssueCreateViewModel(client: client, repoPath: "krz/gitbay")
75
76 await model.loadTemplates()
77
78 #expect(model.templates.map(\.name) == ["bug", "feature"])
79 #expect(model.templates.first?.body == "## Steps\n\n1. ")
80 #expect(stub.seen.first?.url.query() == "argv=issue&argv=templates&argv=krz/gitbay")
81 }
82
83 @Test func aTemplateReadFailureLeavesNoTemplatesAndNoError() async throws {
84 let (client, stub) = try makeClient()
85 stub.enqueue(.init(status: 500, json: #"{"protocol_version":1,"error":"boom","exit_code":1}"#))
86 let model = IssueCreateViewModel(client: client, repoPath: "krz/gitbay")
87
88 await model.loadTemplates()
89
90 #expect(model.templates.isEmpty)
91 #expect(model.errorMessage == nil)
92 }
93}
94
95@MainActor
96struct MRCreateViewModelTests {
97
98 @Test func targetPrefillsFromRepoShow() async throws {
99 let (client, stub) = try makeClient()
100 stub.enqueue(.init(status: 200, json: """
101 {"protocol_version":1,"data":{"path":"krz/gitbay","visibility":"public",\
102 "default_branch":"main"},"exit_code":0}
103 """))
104 let model = MRCreateViewModel(client: client, repoPath: "krz/gitbay")
105
106 await model.loadDefaultBranch()
107
108 #expect(model.defaultBranch == "main")
109 }
110
111 @Test func createSendsBranchesTitleAndBody() async throws {
112 let (client, stub) = try makeClient()
113 stub.enqueue(.init(status: 200, json:
114 #"{"protocol_version":1,"data":{"number":4,"head_sha":"aa"},"exit_code":0}"#))
115 let model = MRCreateViewModel(client: client, repoPath: "krz/gitbay")
116
117 let number = await model.create(
118 source: "fix-thing", target: "main", title: "fix the thing", body: "details"
119 )
120
121 #expect(number == 4)
122 let (argv, stdin) = try argvOf(try #require(stub.seen.first))
123 #expect(argv == ["mr", "create", "krz/gitbay",
124 "--source", "fix-thing", "--target", "main",
125 "--title", "fix the thing", "--file", "-"])
126 #expect(stdin == "details")
127 }
128}
129
130@MainActor
131struct EditAndMilestoneTests {
132
133 private let issueShow = """
134 {"protocol_version":1,"data":{"number":11,"title":"iOS app","state":"open",\
135 "author":"krz","body":"Build it.","created_at":"2026-08-20T10:00:00.000Z"},"exit_code":0}
136 """
137
138 @Test func issueEditSendsTitleAndBodyAlways() async throws {
139 let (client, stub) = try makeClient()
140 stub.enqueue(.init(status: 200, json: issueShow, match: "argv=show"))
141 let model = IssueDetailViewModel(client: client, repoPath: "krz/gitbay", number: 11)
142 await model.load()
143 stub.enqueue(.init(status: 200, json: okJSON, match: "cmd"))
144 stub.enqueue(.init(status: 200, json: issueShow, match: "argv=show"))
145
146 await model.edit(title: "iOS app (v1)", body: "")
147
148 let write = try #require(stub.seen.first { $0.method == "POST" })
149 let (argv, stdin) = try argvOf(write)
150 #expect(argv == ["issue", "edit", "krz/gitbay", "11", "--title", "iOS app (v1)", "--file", "-"])
151 #expect(stdin == "") // empty body clears, matching the CLI
152 }
153
154 @Test func milestoneSetAndClearSpellNoneCorrectly() async throws {
155 let (client, stub) = try makeClient()
156 stub.enqueue(.init(status: 200, json: issueShow, match: "argv=show"))
157 let model = IssueDetailViewModel(client: client, repoPath: "krz/gitbay", number: 11)
158 await model.load()
159 for _ in 0..<2 {
160 stub.enqueue(.init(status: 200, json: okJSON, match: "cmd"))
161 stub.enqueue(.init(status: 200, json: issueShow, match: "argv=show"))
162 }
163
164 await model.setMilestone("v1.0.0")
165 await model.setMilestone(nil)
166
167 let writes = try stub.seen.filter { $0.method == "POST" }.map { try argvOf($0).0 }
168 #expect(writes[0] == ["issue", "milestone", "krz/gitbay", "11", "v1.0.0"])
169 #expect(writes[1] == ["issue", "milestone", "krz/gitbay", "11", "none"])
170 }
171
172 @Test func milestonesLoadOnceForThePicker() async throws {
173 let (client, stub) = try makeClient()
174 stub.enqueue(.init(status: 200, json: issueShow, match: "argv=show"))
175 let model = IssueDetailViewModel(client: client, repoPath: "krz/gitbay", number: 11)
176 await model.load()
177 stub.enqueue(.init(status: 200, json: """
178 {"protocol_version":1,"data":[{"title":"v1.0.0","state":"open","open":3,"closed":5}],\
179 "exit_code":0}
180 """, match: "argv=milestone"))
181
182 await model.loadMilestones()
183 await model.loadMilestones() // second call must not refetch
184
185 #expect(model.availableMilestones?.first?.title == "v1.0.0")
186 #expect(stub.seen.count { ($0.url.query() ?? "").contains("argv=milestone") } == 1)
187 }
188
189 @Test func mrEditSendsTitleAndBody() async throws {
190 let (client, stub) = try makeClient()
191 let mrShow = """
192 {"protocol_version":1,"data":{"number":7,"title":"old","state":"open","author":"cmc",\
193 "source":"b","target_ref":"main","head_sha":"aa","created_at":"2026-08-20T10:00:00.000Z"},\
194 "exit_code":0}
195 """
196 stub.enqueue(.init(status: 200, json: mrShow, match: "argv=show"))
197 stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"output":"","exit_code":0}"#, match: "argv=diff"))
198 stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"exit_code":0}"#, match: "argv=threads"))
199 let model = MRDetailViewModel(client: client, repoPath: "krz/gitbay", number: 7)
200 await model.load()
201 stub.enqueue(.init(status: 200, json: okJSON, match: "cmd"))
202 stub.enqueue(.init(status: 200, json: mrShow, match: "argv=show"))
203 stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"output":"","exit_code":0}"#, match: "argv=diff"))
204 stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"exit_code":0}"#, match: "argv=threads"))
205
206 await model.edit(title: "new title", body: "new body")
207
208 let write = try #require(stub.seen.first { $0.method == "POST" })
209 let (argv, stdin) = try argvOf(write)
210 #expect(argv == ["mr", "edit", "krz/gitbay", "7", "--title", "new title", "--file", "-"])
211 #expect(stdin == "new body")
212 }
213}