gitbayTests/OrgDeleteMRPruneTests.swift
101 lines · 4670 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 writes(_ stub: StubProtocol.Box) throws -> [[String]] {
16 try stub.seen.filter { $0.url.path().hasSuffix("/cmd") }.map {
17 let body = try #require(try JSONSerialization.jsonObject(with: $0.body) as? [String: Any])
18 return try #require(body["argv"] as? [String])
19 }
20}
21
22private let mergedJSON = """
23 {"protocol_version":1,"data":{"number":7,"title":"client: envelope decoding",\
24 "state":"merged","author":"cmc","source":"client-envelope","target_ref":"main",\
25 "head_sha":"aabbcc","created_at":"2026-08-20T10:00:00.000Z",\
26 "merged_at":"2026-08-21T09:15:00.000Z","merged_by":"cmc"},"exit_code":0}
27 """
28
29@MainActor
30struct OrgDeleteTests {
31
32 @Test func deleteConfirmsAndTellsOtherScreens() async throws {
33 let (client, stub) = try makeClient()
34 stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"deleted":"acme"},"exit_code":0}"#, match: "cmd"))
35 let model = OrgViewModel(client: client, orgName: "acme")
36 var noticed: String?
37 let token = NotificationCenter.default.addObserver(forName: .gitbayOrganizationsChanged, object: nil, queue: nil) {
38 noticed = $0.object as? String
39 }
40 defer { NotificationCenter.default.removeObserver(token) }
41
42 #expect(await model.delete())
43
44 #expect(try writes(stub) == [["org", "delete", "acme", "--yes"]])
45 #expect(noticed == "acme")
46 }
47
48 @Test func aNonEmptyOrganizationIsRefusedWithTheReason() async throws {
49 let (client, stub) = try makeClient()
50 let refusal = #"{"protocol_version":1,"error":"the organization still owns 2 repositories; delete or transfer them first","exit_code":1}"#
51 stub.enqueue(.init(status: 500, json: refusal, match: "cmd"))
52 stub.enqueue(.init(status: 500, json: refusal, match: "cmd"))
53 let model = OrgViewModel(client: client, orgName: "acme")
54
55 #expect(await model.delete() == false)
56 #expect(model.actionError == "the organization still owns 2 repositories; delete or transfer them first")
57 }
58
59 @Test func aRetryThatFindsItGoneIsDone() async throws {
60 let (client, stub) = try makeClient()
61 stub.enqueue(.init(status: 500, json: #"{"protocol_version":1,"error":"database is locked","exit_code":1}"#, match: "cmd"))
62 stub.enqueue(.init(status: 404, json: #"{"protocol_version":1,"error":"no organization acme","exit_code":3}"#, match: "cmd"))
63 let model = OrgViewModel(client: client, orgName: "acme")
64
65 #expect(await model.delete())
66 }
67}
68
69@MainActor
70struct MRPruneTests {
71
72 @Test func pruneNamesTheMergeRequestAndConfirms() async throws {
73 let (client, stub) = try makeClient()
74 stub.enqueue(.init(status: 200, json: mergedJSON, match: "argv=show"))
75 stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"user":"cmc","admin":true},"exit_code":0}"#, match: "argv=whoami"))
76 stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":[{"number":7,"head":"abc123"}],"exit_code":0}"#, match: "cmd"))
77 stub.enqueue(.init(status: 200, json: mergedJSON, match: "argv=show"))
78 let model = MRDetailViewModel(client: client, repoPath: "krz/gitbay", number: 7)
79 await model.load()
80 await model.checkCanPruneHead()
81 #expect(model.canPruneHead, "an admin on a merged merge request is offered the prune")
82
83 await model.pruneHead()
84
85 #expect(try writes(stub) == [["admin", "mr", "prune", "krz/gitbay", "7", "--yes"]])
86 #expect(model.actionError == nil)
87 #expect(model.canPruneHead == false, "a pruned head is not offered again")
88 }
89
90 @Test func aStillMergeableHeadIsRefusedWithTheReason() async throws {
91 let (client, stub) = try makeClient()
92 let refusal = #"{"protocol_version":1,"error":"!7 is still mergeable and its head is what makes it so; merge or close it first","exit_code":1}"#
93 stub.enqueue(.init(status: 500, json: refusal, match: "cmd"))
94 stub.enqueue(.init(status: 500, json: refusal, match: "cmd"))
95 let model = MRDetailViewModel(client: client, repoPath: "krz/gitbay", number: 7)
96
97 await model.pruneHead()
98
99 #expect(model.actionError == "!7 is still mergeable and its head is what makes it so; merge or close it first")
100 }
101}