a native ios client for gitbay

client ios swift

https://gitbay.org

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}