gitbayTests/AdminRepoTests.swift

v1.14.1
gitbay-ios/gitbayTests/AdminRepoTests.swift history · blame · raw

129 lines · 5667 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 argv(_ url: URL) -> [String] {
 16    URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems?.compactMap(\.value) ?? []
 17}
 18
 19private func posted(_ stub: StubProtocol.Box) throws -> [[String]] {
 20    try stub.seen.filter { $0.method == "POST" }.map { seen in
 21        let body = try #require(try JSONSerialization.jsonObject(with: seen.body) as? [String: Any])
 22        return try #require(body["argv"] as? [String])
 23    }
 24}
 25
 26private let listJSON = """
 27    {"protocol_version":1,"data":{"items":[\
 28    {"path":"sim/app","visibility":"public","created_at":"2026-10-14T04:19:38.964Z","last_push":"2026-10-14T06:04:58.461Z","bytes":38159},\
 29    {"path":"sim/old","visibility":"private","archived":true,"created_at":"2026-10-14T04:19:38.964Z","bytes":10}]}}
 30    """
 31
 32@MainActor
 33struct AdminRepoTests {
 34
 35    @Test func theListIsFilteredByOwnerAndVisibility() async throws {
 36        let (client, stub) = try makeClient()
 37        stub.enqueue(.init(status: 200, json: listJSON))
 38        let model = AdminReposViewModel(client: client)
 39        model.owner = " sim "
 40        model.visibility = "private"
 41
 42        await model.load()
 43
 44        #expect(argv(try #require(stub.seen.first).url)
 45                == ["admin", "repo", "list", "--owner", "sim", "--visibility", "private", "--limit", "50"])
 46        let repos = try #require(model.list.state.value)
 47        #expect(repos.map(\.isArchived) == [false, true])
 48        #expect(repos.last?.lastPush == nil)
 49    }
 50
 51    @Test func eachActionSendsItsCommand() {
 52        #expect(AdminRepoAction.archive.argv("a/b") == ["admin", "repo", "archive", "a/b"])
 53        #expect(AdminRepoAction.unarchive.argv("a/b") == ["admin", "repo", "unarchive", "a/b"])
 54        #expect(AdminRepoAction.makePrivate.argv("a/b") == ["admin", "repo", "visibility", "a/b", "private"])
 55        #expect(AdminRepoAction.reindex.argv("a/b") == ["admin", "symbols", "reindex", "a/b"])
 56        #expect(AdminRepoAction.delete.argv("a/b") == ["admin", "repo", "delete", "a/b", "--yes"])
 57    }
 58
 59    @Test func aReindexSaysItIsQueued() async throws {
 60        let (client, stub) = try makeClient()
 61        stub.enqueue(.init(status: 200, json: listJSON, match: "argv=list"))
 62        stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"repo":"sim/app","state":"queued"},"exit_code":0}"#, match: "cmd"))
 63        stub.enqueue(.init(status: 200, json: listJSON, match: "argv=list"))
 64        let model = AdminReposViewModel(client: client)
 65        await model.load()
 66        let repo = try #require(model.list.state.value?.first)
 67
 68        await model.perform(.reindex, on: repo)
 69
 70        #expect(try posted(stub) == [["admin", "symbols", "reindex", "sim/app"]])
 71        #expect(model.notice?.contains("queued") == true)
 72    }
 73}
 74
 75@MainActor
 76struct AdminRepoDeleteTests {
 77
 78    @Test func aDeleteWhoseRetryFindsNothingIsDoneAndAnnounced() async throws {
 79        let (client, stub) = try makeClient()
 80        stub.enqueue(.init(status: 200, json: listJSON, match: "argv=list"))
 81        stub.enqueue(.init(status: 500, json: #"{"protocol_version":1,"error":"cleanup failed","exit_code":1}"#, match: "cmd"))
 82        stub.enqueue(.init(status: 404, json: #"{"protocol_version":1,"error":"no repository sim/app","exit_code":3}"#, match: "cmd"))
 83        stub.enqueue(.init(status: 200, json: listJSON, match: "argv=list"))
 84        let model = AdminReposViewModel(client: client)
 85        await model.load()
 86        let repo = try #require(model.list.state.value?.first)
 87        let announced = NotificationCenter.default.notifications(named: .gitbayRepositoryChanged)
 88
 89        async let first = announced.first { _ in true }
 90        await model.perform(.delete, on: repo)
 91
 92        #expect(model.actionError == nil)
 93        #expect((await first)?.object as? String == "sim/app")
 94        // The list stayed on screen through the reload.
 95        #expect(model.list.state.value != nil)
 96    }
 97}
 98
 99@MainActor
100struct AdminOrgLimitsTests {
101
102    @Test func anOrgsLimitsLoadAndChange() async throws {
103        let (client, stub) = try makeClient()
104        let json = #"{"protocol_version":1,"data":{"org":"acme","repos":0,"bytes":0,"repos_owned":1,"bytes_owned":2,"override":false}}"#
105        stub.enqueue(.init(status: 200, json: json, match: "cmd"))
106        stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"org":"acme","repos":5,"bytes":0,"repos_owned":1,"bytes_owned":2,"override":true}}"#, match: "cmd"))
107        let model = AdminOrgLimitsViewModel(client: client)
108
109        await model.load(org: " acme ")
110        #expect(model.limits?.reposOwned == 1)
111        #expect(model.limits?.orgs == nil)
112
113        #expect(await model.set(repos: "5", megabytes: ""))
114
115        #expect(try posted(stub) == [["admin", "org", "limits", "acme"], ["admin", "org", "limits", "acme", "--repos", "5"]])
116        #expect(model.limits?.repos == 5)
117    }
118
119    @Test func anUnknownOrgSaysSo() async throws {
120        let (client, stub) = try makeClient()
121        stub.enqueue(.init(status: 404, json: #"{"protocol_version":1,"error":"no organization \"nope\"","exit_code":3}"#, match: "cmd"))
122        let model = AdminOrgLimitsViewModel(client: client)
123
124        await model.load(org: "nope")
125
126        #expect(model.limits == nil)
127        #expect(model.actionError == #"no organization "nope""#)
128    }
129}