import Foundation import Testing @testable import gitbay private func makeClient() throws -> (GitbayClient, StubProtocol.Box) { let box = StubProtocol.box() let client = GitbayClient( instance: try GitbayInstance(url: "https://gitbay.org"), token: "test-token", session: box.session() ) return (client, box) } private func argvOf(_ seen: StubProtocol.Seen) throws -> [String] { let body = try #require(try JSONSerialization.jsonObject(with: seen.body) as? [String: Any]) return try #require(body["argv"] as? [String]) } private func stdinOf(_ seen: StubProtocol.Seen) throws -> String? { let body = try #require(try JSONSerialization.jsonObject(with: seen.body) as? [String: Any]) return body["stdin"] as? String } private let okJSON = #"{"protocol_version":1,"data":{},"exit_code":0}"# private let hooksJSON = """ {"protocol_version":1,"data":[\ {"id":4,"url":"https://ci.example/hook","events":"*","active":true,"has_secret":true},\ {"id":9,"url":"https://chat.example/in","events":"push,issue.created","active":false,"has_secret":false}\ ],"exit_code":0} """ private let deliveriesJSON = """ {"protocol_version":1,"data":[\ {"id":71,"url":"https://ci.example/hook","event":"push","status":"delivered","attempts":1,"last_status":200},\ {"id":70,"url":"https://chat.example/in","event":"issue.created","status":"failed","attempts":3,\ "last_status":502,"last_error":"bad gateway"}\ ],"exit_code":0} """ @MainActor struct WebhookListViewModelTests { private func loaded() async throws -> (WebhookListViewModel, StubProtocol.Box) { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: hooksJSON, match: "argv=list")) let model = WebhookListViewModel(client: client, repoPath: "krz/gitbay") await model.load() return (model, stub) } @Test func listsHooksWithTheirEventsAndSecretFlag() async throws { let (model, stub) = try await loaded() let hooks = try #require(model.state.value) #expect(hooks.map(\.id) == [4, 9]) #expect(hooks[0].hasSecret && hooks[0].active) #expect(hooks[1].events == "push,issue.created") #expect(stub.seen.first?.url.query() == "argv=webhook&argv=list&argv=krz/gitbay") } @Test func noHooksIsAnEmptyState() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":[],"exit_code":0}"#)) let model = WebhookListViewModel(client: client, repoPath: "krz/gitbay") await model.load() guard case .empty = model.state else { Issue.record("expected .empty, got \(model.state)") return } } @Test func addSendsSecretAndEventsOnlyWhenGiven() async throws { let (model, stub) = try await loaded() for _ in 0..<2 { stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"id":10,"url":"u","events":"*"},"exit_code":0}"#, match: "cmd")) stub.enqueue(.init(status: 200, json: hooksJSON, match: "argv=list")) } let first = await model.add(url: " https://a.example/h ", secret: "s3", events: "push") let second = await model.add(url: "https://b.example/h", secret: "", events: "") #expect(first && second) let writes = try stub.seen.filter { $0.method == "POST" }.map(argvOf) #expect(writes == [ ["webhook", "add", "krz/gitbay", "https://a.example/h", "--secret", "-", "--events", "push"], ["webhook", "add", "krz/gitbay", "https://b.example/h"], ]) let posts = stub.seen.filter { $0.method == "POST" } #expect(try posts.map(stdinOf) == ["s3\n", nil]) } @Test func removeSendsTheIdThenReloads() async throws { let (model, stub) = try await loaded() stub.enqueue(.init(status: 200, json: okJSON, match: "cmd")) stub.enqueue(.init(status: 200, json: hooksJSON, match: "argv=list")) await model.remove(id: 9) let write = try #require(stub.seen.first { $0.method == "POST" }) #expect(try argvOf(write) == ["webhook", "remove", "krz/gitbay", "9"]) #expect(stub.seen.filter { $0.method == "GET" }.count == 2) } @Test func aRefusedAddSurfacesAndReturnsFalse() async throws { let (model, stub) = try await loaded() stub.enqueue(.init(status: 403, json: #"{"protocol_version":1,"error":"admin access required","exit_code":4}"#, match: "cmd")) let ok = await model.add(url: "https://a.example/h", secret: "", events: "") #expect(!ok) #expect(model.actionError == "admin access required") } } @MainActor struct WebhookDeliveriesViewModelTests { @Test func listsDeliveriesWithStatusAndLastError() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: deliveriesJSON)) let model = WebhookDeliveriesViewModel(client: client, repoPath: "krz/gitbay") await model.load() let rows = try #require(model.state.value) #expect(rows.map(\.id) == [71, 70]) #expect(rows[1].status == "failed") #expect(rows[1].lastStatus == 502) #expect(rows[1].lastError == "bad gateway") #expect(rows[0].lastError == nil) #expect(stub.seen.first?.url.query() == "argv=webhook&argv=deliveries&argv=krz/gitbay") } @Test func redeliverSendsTheDeliveryIdThenReloads() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: deliveriesJSON, match: "argv=deliveries")) let model = WebhookDeliveriesViewModel(client: client, repoPath: "krz/gitbay") await model.load() stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"requeued":70},"exit_code":0}"#, match: "cmd")) stub.enqueue(.init(status: 200, json: deliveriesJSON, match: "argv=deliveries")) await model.redeliver(id: 70) let write = try #require(stub.seen.first { $0.method == "POST" }) #expect(try argvOf(write) == ["webhook", "redeliver", "krz/gitbay", "70"]) #expect(stub.seen.filter { $0.method == "GET" }.count == 2) #expect(model.actionError == nil) } }