gitbayTests/IssueBuildViewModelTests.swift
162 lines · 6440 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 issueListJSON = """
16 {"protocol_version":1,"data":{"items":[\
17 {"number":11,"title":"iOS app","state":"open","author":"krz",\
18 "labels":["app"],"assignees":["cmc"],"created_at":"2026-08-20T10:00:00.000Z"},\
19 {"number":40,"title":"pagination","state":"open","author":"krz",\
20 "created_at":"2026-08-21T10:00:00.000Z"}\
21 ]},"exit_code":0}
22 """
23
24private let issueShowJSON = """
25 {"protocol_version":1,"data":{"number":11,"title":"iOS app","state":"open",\
26 "author":"krz","labels":["app"],"assignees":["cmc"],"body":"Build it.",\
27 "created_at":"2026-08-20T10:00:00.000Z",\
28 "comments":[{"author":"cmc","body":"started","created_at":"2026-08-22T09:00:00.000Z"}]},\
29 "exit_code":0}
30 """
31
32private let buildListJSON = """
33 {"protocol_version":1,"data":[\
34 {"number":3,"job":"ci","status":"success","sha":"65ba14e0000000000000",\
35 "ref":"refs/heads/main","created_at":"2026-08-20T10:00:00.000Z",\
36 "finished_at":"2026-08-20T10:05:00.000Z"},\
37 {"number":4,"job":"ci","status":"running","sha":"7953e780000000000000",\
38 "ref":"refs/heads/main","created_at":"2026-08-20T11:00:00.000Z"}\
39 ],"exit_code":0}
40 """
41
42@MainActor
43struct IssueListViewModelTests {
44
45 @Test func listsInServerOrderWithLabelsAndAssignees() async throws {
46 let (client, stub) = try makeClient()
47 stub.enqueue(.init(status: 200, json: issueListJSON))
48 let model = IssueListViewModel(client: client, repoPath: "krz/gitbay")
49
50 await model.load()
51
52 let issues = try #require(model.state.value)
53 #expect(issues.map(\.number) == [11, 40])
54 #expect(issues[0].labels == ["app"])
55 #expect(issues[0].assignees == ["cmc"])
56 #expect(stub.seen.first?.url.query() ==
57 "argv=issue&argv=list&argv=krz/gitbay&argv=--state&argv=open&argv=--limit&argv=50")
58 }
59}
60
61@MainActor
62struct IssueDetailViewModelTests {
63
64 private func loadedModel() async throws -> (IssueDetailViewModel, StubProtocol.Box) {
65 let (client, stub) = try makeClient()
66 stub.enqueue(.init(status: 200, json: issueShowJSON))
67 let model = IssueDetailViewModel(client: client, repoPath: "krz/gitbay", number: 11)
68 await model.load()
69 return (model, stub)
70 }
71
72 @Test func loadsIssueWithComments() async throws {
73 let (model, _) = try await loadedModel()
74
75 let issue = try #require(model.state.value)
76 #expect(issue.title == "iOS app")
77 #expect(issue.comments?.count == 1)
78 }
79
80 @Test func commentTravelsInStdin() async throws {
81 let (model, stub) = try await loadedModel()
82 stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{},"exit_code":0}"#))
83 stub.enqueue(.init(status: 200, json: issueShowJSON))
84
85 await model.comment("triaged from the phone")
86
87 let write = stub.seen[1]
88 #expect(write.method == "POST")
89 let body = try #require(try JSONSerialization.jsonObject(with: write.body) as? [String: Any])
90 #expect(body["argv"] as? [String] == ["issue", "comment", "krz/gitbay", "11", "--file", "-"])
91 #expect(body["stdin"] as? String == "triaged from the phone")
92 }
93
94 @Test func labelAndAssigneeEditsUseAddRemoveFlags() async throws {
95 let (model, stub) = try await loadedModel()
96 for _ in 0..<4 {
97 stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{},"exit_code":0}"#))
98 stub.enqueue(.init(status: 200, json: issueShowJSON))
99 }
100
101 await model.addLabel("bug")
102 await model.removeLabel("app")
103 await model.addAssignee("krz")
104 await model.removeAssignee("cmc")
105
106 let writes = stub.seen.filter { $0.method == "POST" }
107 let argvs = try writes.map {
108 try #require(try JSONSerialization.jsonObject(with: $0.body) as? [String: Any])["argv"] as? [String]
109 }
110 #expect(argvs[0] == ["issue", "label", "krz/gitbay", "11", "--add", "bug"])
111 #expect(argvs[1] == ["issue", "label", "krz/gitbay", "11", "--remove", "app"])
112 #expect(argvs[2] == ["issue", "assign", "krz/gitbay", "11", "--add", "krz"])
113 #expect(argvs[3] == ["issue", "assign", "krz/gitbay", "11", "--remove", "cmc"])
114 }
115
116 @Test func closeAndReopenAreSeparateCommands() async throws {
117 let (model, stub) = try await loadedModel()
118 stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{},"exit_code":0}"#))
119 stub.enqueue(.init(status: 200, json: issueShowJSON))
120 stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{},"exit_code":0}"#))
121 stub.enqueue(.init(status: 200, json: issueShowJSON))
122
123 await model.close()
124 await model.reopen()
125
126 let writes = stub.seen.filter { $0.method == "POST" }
127 let argvs = try writes.map {
128 try #require(try JSONSerialization.jsonObject(with: $0.body) as? [String: Any])["argv"] as? [String]
129 }
130 #expect(argvs[0] == ["issue", "close", "krz/gitbay", "11"])
131 #expect(argvs[1] == ["issue", "reopen", "krz/gitbay", "11"])
132 }
133
134 @Test func aDeniedTriageActionSurfacesTheRule() async throws {
135 let (model, stub) = try await loadedModel()
136 stub.enqueue(.init(status: 403, json:
137 #"{"protocol_version":1,"error":"krz/gitbay is archived and read-only","exit_code":4}"#))
138
139 await model.close()
140
141 #expect(model.actionError == "krz/gitbay is archived and read-only")
142 #expect(model.state.value != nil)
143 }
144}
145
146@MainActor
147struct BuildListViewModelTests {
148
149 @Test func listsNewestFirst() async throws {
150 let (client, stub) = try makeClient()
151 stub.enqueue(.init(status: 200, json: buildListJSON))
152 let model = BuildListViewModel(client: client, repoPath: "krz/gitbay")
153
154 await model.load()
155
156 let builds = try #require(model.state.value)
157 #expect(builds.map(\.number) == [4, 3])
158 #expect(builds[0].status == "running")
159 #expect(builds[0].finishedAt == nil)
160 #expect(builds[1].finishedAt != nil)
161 }
162}