gitbayTests/BodyFormatTests.swift

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

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