gitbayTests/OrgTests.swift
149 lines · 5748 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 let orgShowJSON = """
21 {"protocol_version":1,"data":{"org":"krz","members":[\
22 {"user":"cmc","role":"admin"},{"user":"guest","role":"member"}]},"exit_code":0}
23 """
24private let teamsJSON = #"{"protocol_version":1,"data":["core"],"exit_code":0}"#
25private let okJSON = #"{"protocol_version":1,"data":{},"exit_code":0}"#
26
27@MainActor
28struct OrgViewModelTests {
29
30 private func loadedModel() async throws -> (OrgViewModel, StubProtocol.Box) {
31 let (client, stub) = try makeClient()
32 stub.enqueue(.init(status: 200, json: orgShowJSON, match: "argv=show"))
33 stub.enqueue(.init(status: 200, json: teamsJSON, match: "argv=team"))
34 let model = OrgViewModel(client: client, orgName: "krz")
35 await model.load()
36 return (model, stub)
37 }
38
39 @Test func loadsMembersAndBareTeamNames() async throws {
40 let (model, _) = try await loadedModel()
41
42 let loaded = try #require(model.state.value)
43 #expect(loaded.members.map(\.user) == ["cmc", "guest"])
44 #expect(loaded.teams == ["core"])
45 }
46
47 @Test func membershipWritesUseTheRightCommands() async throws {
48 let (model, stub) = try await loadedModel()
49 for _ in 0..<4 {
50 stub.enqueue(.init(status: 200, json: okJSON, match: "cmd"))
51 stub.enqueue(.init(status: 200, json: orgShowJSON, match: "argv=show"))
52 stub.enqueue(.init(status: 200, json: teamsJSON, match: "argv=team"))
53 }
54 let guest = try #require(model.state.value?.members.last)
55
56 await model.addMember("newbie", role: "member")
57 await model.setRole(guest, role: "admin")
58 await model.removeMember(guest)
59 await model.createTeam("infra")
60
61 let writes = try stub.seen.filter { $0.method == "POST" }.map(argvOf)
62 #expect(writes == [
63 ["org", "members", "add", "krz", "newbie", "--role", "member"],
64 ["org", "members", "add", "krz", "guest", "--role", "admin"],
65 ["org", "members", "remove", "krz", "guest"],
66 ["org", "team", "create", "krz", "infra"],
67 ])
68 }
69
70 @Test func anOutsiderGetsTheRefusalAsTheScreenState() async throws {
71 let (client, stub) = try makeClient()
72 stub.enqueue(.init(status: 403, json:
73 #"{"protocol_version":1,"error":"you are not a member of krz","exit_code":4}"#,
74 match: "argv=show"))
75 stub.enqueue(.init(status: 403, json:
76 #"{"protocol_version":1,"error":"you are not a member of krz","exit_code":4}"#,
77 match: "argv=team"))
78 let model = OrgViewModel(client: client, orgName: "krz")
79
80 await model.load()
81
82 guard case .failed(let message) = model.state else {
83 Issue.record("expected .failed, got \(model.state)")
84 return
85 }
86 #expect(message == "you are not a member of krz")
87 }
88}
89
90@MainActor
91struct TeamViewModelTests {
92
93 private let teamShowJSON = """
94 {"protocol_version":1,"data":{"team":"core","members":["cmc"],\
95 "grants":[{"repo":"krz/gitbay","role":"write"}]},"exit_code":0}
96 """
97
98 private func loadedModel() async throws -> (TeamViewModel, StubProtocol.Box) {
99 let (client, stub) = try makeClient()
100 stub.enqueue(.init(status: 200, json: teamShowJSON, match: "argv=show"))
101 let model = TeamViewModel(client: client, orgName: "krz", teamName: "core")
102 await model.load()
103 return (model, stub)
104 }
105
106 @Test func loadsMembersAndGrants() async throws {
107 let (model, _) = try await loadedModel()
108
109 let loaded = try #require(model.state.value)
110 #expect(loaded.members == ["cmc"])
111 #expect(loaded.grants?.first?.repo == "krz/gitbay")
112 }
113
114 @Test func teamWritesTargetOrgTeamAndSubject() async throws {
115 let (model, stub) = try await loadedModel()
116 for _ in 0..<4 {
117 stub.enqueue(.init(status: 200, json: okJSON, match: "cmd"))
118 stub.enqueue(.init(status: 200, json: teamShowJSON, match: "argv=show"))
119 }
120 let grant = try #require(model.state.value?.grants?.first)
121
122 await model.addMember("guest")
123 await model.removeMember("guest")
124 await model.grant(repo: "krz/orgo", role: "admin")
125 await model.revoke(grant)
126
127 let writes = try stub.seen.filter { $0.method == "POST" }.map(argvOf)
128 #expect(writes == [
129 ["org", "team", "add", "krz", "core", "guest"],
130 ["org", "team", "remove", "krz", "core", "guest"],
131 ["org", "team", "grant", "krz", "core", "krz/orgo", "admin"],
132 ["org", "team", "revoke", "krz", "core", "krz/gitbay"],
133 ])
134 }
135
136 @Test func aMissingTeamIsAnEmptyState() async throws {
137 let (client, stub) = try makeClient()
138 stub.enqueue(.init(status: 404, json:
139 #"{"protocol_version":1,"error":"no team \"core\" in krz","exit_code":3}"#))
140 let model = TeamViewModel(client: client, orgName: "krz", teamName: "core")
141
142 await model.load()
143
144 guard case .empty = model.state else {
145 Issue.record("expected .empty, got \(model.state)")
146 return
147 }
148 }
149}