gitbayTests/BodyFormatTests.swift

v1.13.1
gitbay-ios/gitbayTests/BodyFormatTests.swift history · blame · raw

317 lines · 13155 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 func stdinOf(_ seen: StubProtocol.Seen) throws -> String? {
 21    let body = try #require(try JSONSerialization.jsonObject(with: seen.body) as? [String: Any])
 22    return body["stdin"] as? String
 23}
 24
 25struct BodyFormatDecodingTests {
 26
 27    private func decodeIssue(_ json: String) throws -> IssueDetail {
 28        let decoder = GitbayClient.decoder()
 29        return try decoder.decode(IssueDetail.self, from: Data(json.utf8))
 30    }
 31
 32    private func decodeMR(_ json: String) throws -> MRDetail {
 33        let decoder = GitbayClient.decoder()
 34        return try decoder.decode(MRDetail.self, from: Data(json.utf8))
 35    }
 36
 37    /// body_format is omitempty; an ordinary body carries no key.
 38    @Test func anIssueWithoutAStoredFormatDecodes() throws {
 39        let issue = try decodeIssue("""
 40            {"number":7,"title":"t","state":"open","author":"cmc",\
 41            "body":"x","created_at":"2026-09-01T00:00:00Z"}
 42            """)
 43        #expect(issue.bodyFormat == nil)
 44    }
 45
 46    @Test func anIssueStoredAsOrgSaysSo() throws {
 47        let issue = try decodeIssue("""
 48            {"number":7,"title":"t","state":"open","author":"cmc",\
 49            "body":"x","body_format":"org","created_at":"2026-09-01T00:00:00Z"}
 50            """)
 51        #expect(issue.bodyFormat == "org")
 52    }
 53
 54    @Test func aMergeRequestWithoutAStoredFormatDecodes() throws {
 55        let mr = try decodeMR("""
 56            {"number":7,"title":"t","state":"open","author":"cmc","source":"f",\
 57            "target_ref":"main","head_sha":"a","created_at":"2026-09-01T00:00:00Z"}
 58            """)
 59        #expect(mr.bodyFormat == nil)
 60    }
 61
 62    /// MRDetail's hand-written initialiser must still decode everything
 63    /// it did before the new field was added.
 64    @Test func addingTheFormatDidNotDropAnyMergeRequestField() throws {
 65        let mr = try decodeMR("""
 66            {"number":7,"title":"t","state":"open","draft":true,"author":"cmc","source":"f",\
 67            "target_ref":"main","head_sha":"abc","body":"b","body_format":"org",\
 68            "milestone":"v1","review_requests":["rae"],\
 69            "stacked_on":{"number":6,"title":"below"},\
 70            "stacked":[{"number":8,"title":"above"}],\
 71            "created_at":"2026-09-01T00:00:00Z","merged_at":"2026-09-02T00:00:00Z",\
 72            "merged_by":"cmc","checks_combined":"success","unresolved_threads":2}
 73            """)
 74        #expect(mr.bodyFormat == "org")
 75        #expect(mr.draft)
 76        #expect(mr.milestone == "v1")
 77        #expect(mr.reviewRequests == ["rae"])
 78        #expect(mr.stackedOn?.number == 6)
 79        #expect(mr.stacked.map(\.number) == [8])
 80        #expect(mr.mergedBy == "cmc")
 81        #expect(mr.checksCombined == "success")
 82        #expect(mr.unresolvedThreads == 2)
 83    }
 84
 85    /// body_format is omitempty on a comment too; an ordinary one carries no key.
 86    @Test func anIssueCommentWithoutAStoredFormatDecodesNil() throws {
 87        let issue = try decodeIssue("""
 88            {"number":7,"title":"t","state":"open","author":"cmc",\
 89            "created_at":"2026-09-01T00:00:00Z","comments":[{"author":"rae",\
 90            "body":"hi","created_at":"2026-09-01T00:00:00Z"}]}
 91            """)
 92        #expect(issue.comments?.first?.bodyFormat == nil)
 93    }
 94
 95    @Test func anIssueCommentStoredAsOrgSaysSo() throws {
 96        let issue = try decodeIssue("""
 97            {"number":7,"title":"t","state":"open","author":"cmc",\
 98            "created_at":"2026-09-01T00:00:00Z","comments":[{"author":"rae",\
 99            "body":"hi","body_format":"org","created_at":"2026-09-01T00:00:00Z"}]}
100            """)
101        #expect(issue.comments?.first?.bodyFormat == "org")
102    }
103
104    @Test func aMergeRequestCommentWithoutAStoredFormatDecodesNil() throws {
105        let mr = try decodeMR("""
106            {"number":7,"title":"t","state":"open","author":"cmc","source":"f",\
107            "target_ref":"main","head_sha":"a","created_at":"2026-09-01T00:00:00Z",\
108            "comments":[{"author":"rae","body":"hi",\
109            "created_at":"2026-09-01T00:00:00Z"}]}
110            """)
111        #expect(mr.comments?.first?.bodyFormat == nil)
112    }
113
114    @Test func aMergeRequestCommentStoredAsOrgSaysSo() throws {
115        let mr = try decodeMR("""
116            {"number":7,"title":"t","state":"open","author":"cmc","source":"f",\
117            "target_ref":"main","head_sha":"a","created_at":"2026-09-01T00:00:00Z",\
118            "comments":[{"author":"rae","body":"hi","body_format":"org",\
119            "created_at":"2026-09-01T00:00:00Z"}]}
120            """)
121        #expect(mr.comments?.first?.bodyFormat == "org")
122    }
123}
124
125@MainActor
126struct BodyFormatWriteTests {
127
128    private let issueJSON = """
129        {"protocol_version":1,"data":{"number":7,"title":"t","state":"open","author":"cmc",\
130        "body":"x","body_format":"org","created_at":"2026-09-01T00:00:00Z"},"exit_code":0}
131        """
132
133    @Test func anIssueCommentCarriesItsFormat() async throws {
134        let (client, stub) = try makeClient()
135        stub.enqueue(.init(status: 200, json: issueJSON))
136        let model = IssueDetailViewModel(client: client, repoPath: "krz/gitbay", number: 7)
137        await model.load()
138
139        stub.enqueue(.init(status: 200, json: """
140            {"protocol_version":1,"exit_code":0}
141            """))
142        stub.enqueue(.init(status: 200, json: issueJSON))
143        await model.comment("hello", format: "org")
144
145        let write = try #require(stub.seen.first { $0.method == "POST" })
146        #expect(try argvOf(write)
147            == ["issue", "comment", "krz/gitbay", "7", "--file", "-", "--format", "org"])
148        #expect(try stdinOf(write) == "hello")
149    }
150
151    /// No format given means no flag — the server then defaults. Sending
152    /// `--format ""` would be a usage error.
153    @Test func noFormatMeansNoFlag() async throws {
154        let (client, stub) = try makeClient()
155        stub.enqueue(.init(status: 200, json: issueJSON))
156        let model = IssueDetailViewModel(client: client, repoPath: "krz/gitbay", number: 7)
157        await model.load()
158
159        stub.enqueue(.init(status: 200, json: """
160            {"protocol_version":1,"exit_code":0}
161            """))
162        stub.enqueue(.init(status: 200, json: issueJSON))
163        await model.comment("hello", format: nil)
164
165        let write = try #require(stub.seen.first { $0.method == "POST" })
166        #expect(try argvOf(write).contains("--format") == false)
167    }
168
169    @Test func anIssueEditCarriesItsFormat() async throws {
170        let (client, stub) = try makeClient()
171        stub.enqueue(.init(status: 200, json: issueJSON))
172        let model = IssueDetailViewModel(client: client, repoPath: "krz/gitbay", number: 7)
173        await model.load()
174
175        stub.enqueue(.init(status: 200, json: """
176            {"protocol_version":1,"exit_code":0}
177            """))
178        stub.enqueue(.init(status: 200, json: issueJSON))
179        await model.edit(title: "t2", body: "b2", format: "md")
180
181        let write = try #require(stub.seen.first { $0.method == "POST" })
182        #expect(try argvOf(write)
183            == ["issue", "edit", "krz/gitbay", "7", "--title", "t2", "--file", "-", "--format", "md"])
184        #expect(try stdinOf(write) == "b2")
185    }
186}
187
188@MainActor
189struct BodyFormatMRWriteTests {
190
191    private let plainDetail = """
192        {"number":7,"title":"a change","state":"open","author":"cmc",\
193        "source":"feat","target_ref":"main","head_sha":"abc123",\
194        "created_at":"2026-09-01T00:00:00Z"}
195        """
196
197    private func loaded() throws -> (MRDetailViewModel, GitbayClient, StubProtocol.Box) {
198        let (client, box) = try makeClient()
199        box.enqueue(.init(status: 200, json: """
200            {"protocol_version":1,"data":\(plainDetail),"exit_code":0}
201            """))
202        let model = MRDetailViewModel(client: client, repoPath: "krz/gitbay", number: 7)
203        return (model, client, box)
204    }
205
206    /// `mr show` load also fires `mr diff` and `mr threads`; leave both
207    /// unstubbed so they fail harmlessly, same as `MRLifecycleTests`.
208    @Test func aMergeRequestCommentCarriesItsFormat() async throws {
209        let (model, _, box) = try loaded()
210        await model.load()
211
212        box.enqueue(.init(status: 200, json: """
213            {"protocol_version":1,"exit_code":0}
214            """))
215        box.enqueue(.init(status: 200, json: """
216            {"protocol_version":1,"data":\(plainDetail),"exit_code":0}
217            """))
218        await model.comment("hello", format: "org")
219
220        let write = try #require(box.seen.first { $0.method == "POST" })
221        #expect(try argvOf(write)
222            == ["mr", "comment", "krz/gitbay", "7", "--file", "-", "--format", "org"])
223        #expect(try stdinOf(write) == "hello")
224    }
225
226    @Test func aMergeRequestCommentWithNoFormatOmitsTheFlag() async throws {
227        let (model, _, box) = try loaded()
228        await model.load()
229
230        box.enqueue(.init(status: 200, json: """
231            {"protocol_version":1,"exit_code":0}
232            """))
233        box.enqueue(.init(status: 200, json: """
234            {"protocol_version":1,"data":\(plainDetail),"exit_code":0}
235            """))
236        await model.comment("hello", format: nil)
237
238        let write = try #require(box.seen.first { $0.method == "POST" })
239        #expect(try argvOf(write).contains("--format") == false)
240    }
241
242    @Test func aMergeRequestEditCarriesItsFormat() async throws {
243        let (model, _, box) = try loaded()
244        await model.load()
245
246        box.enqueue(.init(status: 200, json: """
247            {"protocol_version":1,"exit_code":0}
248            """))
249        box.enqueue(.init(status: 200, json: """
250            {"protocol_version":1,"data":\(plainDetail),"exit_code":0}
251            """))
252        await model.edit(title: "t2", body: "b2", format: "md")
253
254        let write = try #require(box.seen.first { $0.method == "POST" })
255        #expect(try argvOf(write)
256            == ["mr", "edit", "krz/gitbay", "7", "--title", "t2", "--file", "-", "--format", "md"])
257        #expect(try stdinOf(write) == "b2")
258    }
259}
260
261@MainActor
262struct BodyFormatCreateTests {
263
264    @Test func creatingAnIssueWithAFormatAppendsTheFlag() async throws {
265        let (client, box) = try makeClient()
266        box.enqueue(.init(status: 200, json: """
267            {"protocol_version":1,"data":{"number":7},"exit_code":0}
268            """))
269        let model = IssueCreateViewModel(client: client, repoPath: "krz/gitbay")
270        model.format = "org"
271        _ = await model.create(title: "a title", body: "a body")
272
273        let write = try #require(box.seen.first { $0.method == "POST" })
274        #expect(try argvOf(write)
275            == ["issue", "create", "krz/gitbay", "--title", "a title",
276                "--file", "-", "--format", "org"])
277    }
278
279    @Test func creatingAnIssueWithoutAFormatOmitsTheFlag() async throws {
280        let (client, box) = try makeClient()
281        box.enqueue(.init(status: 200, json: """
282            {"protocol_version":1,"data":{"number":7},"exit_code":0}
283            """))
284        let model = IssueCreateViewModel(client: client, repoPath: "krz/gitbay")
285        _ = await model.create(title: "a title", body: "a body")
286
287        let write = try #require(box.seen.first { $0.method == "POST" })
288        #expect(try argvOf(write).contains("--format") == false)
289    }
290
291    @Test func creatingAMergeRequestWithAFormatAppendsTheFlag() async throws {
292        let (client, box) = try makeClient()
293        box.enqueue(.init(status: 200, json: """
294            {"protocol_version":1,"data":{"number":7},"exit_code":0}
295            """))
296        let model = MRCreateViewModel(client: client, repoPath: "krz/gitbay")
297        model.format = "org"
298        _ = await model.create(source: "feat", target: "main", title: "a change", body: "a body")
299
300        let write = try #require(box.seen.first { $0.method == "POST" })
301        #expect(try argvOf(write)
302            == ["mr", "create", "krz/gitbay", "--source", "feat", "--target", "main",
303                "--title", "a change", "--file", "-", "--format", "org"])
304    }
305
306    @Test func creatingAMergeRequestWithoutAFormatOmitsTheFlag() async throws {
307        let (client, box) = try makeClient()
308        box.enqueue(.init(status: 200, json: """
309            {"protocol_version":1,"data":{"number":7},"exit_code":0}
310            """))
311        let model = MRCreateViewModel(client: client, repoPath: "krz/gitbay")
312        _ = await model.create(source: "feat", target: "main", title: "a change", body: "a body")
313
314        let write = try #require(box.seen.first { $0.method == "POST" })
315        #expect(try argvOf(write).contains("--format") == false)
316    }
317}