gitbayTests/ComposeViewModelTests.swift
182 lines · 8033 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 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 MRCreateViewModelTests {
66
67 @Test func targetPrefillsFromRepoShow() async throws {
68 let (client, stub) = try makeClient()
69 stub.enqueue(.init(status: 200, json: """
70 {"protocol_version":1,"data":{"path":"krz/gitbay","visibility":"public",\
71 "default_branch":"main"},"exit_code":0}
72 """))
73 let model = MRCreateViewModel(client: client, repoPath: "krz/gitbay")
74
75 await model.loadDefaultBranch()
76
77 #expect(model.defaultBranch == "main")
78 }
79
80 @Test func createSendsBranchesTitleAndBody() async throws {
81 let (client, stub) = try makeClient()
82 stub.enqueue(.init(status: 200, json:
83 #"{"protocol_version":1,"data":{"number":4,"head_sha":"aa"},"exit_code":0}"#))
84 let model = MRCreateViewModel(client: client, repoPath: "krz/gitbay")
85
86 let number = await model.create(
87 source: "fix-thing", target: "main", title: "fix the thing", body: "details"
88 )
89
90 #expect(number == 4)
91 let (argv, stdin) = try argvOf(try #require(stub.seen.first))
92 #expect(argv == ["mr", "create", "krz/gitbay",
93 "--source", "fix-thing", "--target", "main",
94 "--title", "fix the thing", "--file", "-"])
95 #expect(stdin == "details")
96 }
97}
98
99@MainActor
100struct EditAndMilestoneTests {
101
102 private let issueShow = """
103 {"protocol_version":1,"data":{"number":11,"title":"iOS app","state":"open",\
104 "author":"krz","body":"Build it.","created_at":"2026-08-20T10:00:00.000Z"},"exit_code":0}
105 """
106
107 @Test func issueEditSendsTitleAndBodyAlways() async throws {
108 let (client, stub) = try makeClient()
109 stub.enqueue(.init(status: 200, json: issueShow, match: "argv=show"))
110 let model = IssueDetailViewModel(client: client, repoPath: "krz/gitbay", number: 11)
111 await model.load()
112 stub.enqueue(.init(status: 200, json: okJSON, match: "cmd"))
113 stub.enqueue(.init(status: 200, json: issueShow, match: "argv=show"))
114
115 await model.edit(title: "iOS app (v1)", body: "")
116
117 let write = try #require(stub.seen.first { $0.method == "POST" })
118 let (argv, stdin) = try argvOf(write)
119 #expect(argv == ["issue", "edit", "krz/gitbay", "11", "--title", "iOS app (v1)", "--file", "-"])
120 #expect(stdin == "") // empty body clears, matching the CLI
121 }
122
123 @Test func milestoneSetAndClearSpellNoneCorrectly() async throws {
124 let (client, stub) = try makeClient()
125 stub.enqueue(.init(status: 200, json: issueShow, match: "argv=show"))
126 let model = IssueDetailViewModel(client: client, repoPath: "krz/gitbay", number: 11)
127 await model.load()
128 for _ in 0..<2 {
129 stub.enqueue(.init(status: 200, json: okJSON, match: "cmd"))
130 stub.enqueue(.init(status: 200, json: issueShow, match: "argv=show"))
131 }
132
133 await model.setMilestone("v1.0.0")
134 await model.setMilestone(nil)
135
136 let writes = try stub.seen.filter { $0.method == "POST" }.map { try argvOf($0).0 }
137 #expect(writes[0] == ["issue", "milestone", "krz/gitbay", "11", "v1.0.0"])
138 #expect(writes[1] == ["issue", "milestone", "krz/gitbay", "11", "none"])
139 }
140
141 @Test func milestonesLoadOnceForThePicker() async throws {
142 let (client, stub) = try makeClient()
143 stub.enqueue(.init(status: 200, json: issueShow, match: "argv=show"))
144 let model = IssueDetailViewModel(client: client, repoPath: "krz/gitbay", number: 11)
145 await model.load()
146 stub.enqueue(.init(status: 200, json: """
147 {"protocol_version":1,"data":[{"title":"v1.0.0","state":"open","open":3,"closed":5}],\
148 "exit_code":0}
149 """, match: "argv=milestone"))
150
151 await model.loadMilestones()
152 await model.loadMilestones() // second call must not refetch
153
154 #expect(model.availableMilestones?.first?.title == "v1.0.0")
155 #expect(stub.seen.count { ($0.url.query() ?? "").contains("argv=milestone") } == 1)
156 }
157
158 @Test func mrEditSendsTitleAndBody() async throws {
159 let (client, stub) = try makeClient()
160 let mrShow = """
161 {"protocol_version":1,"data":{"number":7,"title":"old","state":"open","author":"cmc",\
162 "source":"b","target_ref":"main","head_sha":"aa","created_at":"2026-08-20T10:00:00.000Z"},\
163 "exit_code":0}
164 """
165 stub.enqueue(.init(status: 200, json: mrShow, match: "argv=show"))
166 stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"output":"","exit_code":0}"#, match: "argv=diff"))
167 stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"exit_code":0}"#, match: "argv=threads"))
168 let model = MRDetailViewModel(client: client, repoPath: "krz/gitbay", number: 7)
169 await model.load()
170 stub.enqueue(.init(status: 200, json: okJSON, match: "cmd"))
171 stub.enqueue(.init(status: 200, json: mrShow, match: "argv=show"))
172 stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"output":"","exit_code":0}"#, match: "argv=diff"))
173 stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"exit_code":0}"#, match: "argv=threads"))
174
175 await model.edit(title: "new title", body: "new body")
176
177 let write = try #require(stub.seen.first { $0.method == "POST" })
178 let (argv, stdin) = try argvOf(write)
179 #expect(argv == ["mr", "edit", "krz/gitbay", "7", "--title", "new title", "--file", "-"])
180 #expect(stdin == "new body")
181 }
182}