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 nonisolated struct Row: Decodable, Sendable, Equatable { let number: Int } private func page(_ numbers: [Int], next: String? = nil) -> String { let items = numbers.map { #"{"number":\#($0)}"# }.joined(separator: ",") let nextField = next.map { #","next":"\#($0)""# } ?? "" return #"{"protocol_version":1,"data":{"items":[\#(items)]\#(nextField)},"exit_code":0}"# } struct ClientPagingTests { @Test func readPageSendsLimitAndCursorAndDecodesNext() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: page([1, 2], next: "abc"))) stub.enqueue(.init(status: 200, json: page([3]))) let first = try await client.readPage(["issue", "list", "krz/gitbay"], of: Row.self, limit: 2) let second = try await client.readPage( ["issue", "list", "krz/gitbay"], of: Row.self, limit: 2, cursor: first.next ) #expect(first.items == [Row(number: 1), Row(number: 2)]) #expect(first.next == "abc") #expect(second.items == [Row(number: 3)]) #expect(second.next == nil) #expect(stub.seen[0].url.query() == "argv=issue&argv=list&argv=krz/gitbay&argv=--limit&argv=2") #expect(stub.seen[1].url.query() == "argv=issue&argv=list&argv=krz/gitbay&argv=--limit&argv=2&argv=--cursor&argv=abc") } } @MainActor struct PagedListModelTests { private func makeList(pageSize: Int = 2) throws -> (PagedListModel, StubProtocol.Box) { let (client, stub) = try makeClient() let list = PagedListModel( client: client, argv: ["issue", "list", "krz/gitbay"], emptyMessage: "nothing", pageSize: pageSize ) return (list, stub) } @Test func loadMoreAppendsUntilTheCursorRunsOut() async throws { let (list, stub) = try makeList() stub.enqueue(.init(status: 200, json: page([1, 2], next: "c1"))) stub.enqueue(.init(status: 200, json: page([3, 4], next: "c2"))) stub.enqueue(.init(status: 200, json: page([5]))) await list.reload() #expect(list.state.value == [Row(number: 1), Row(number: 2)]) #expect(list.hasMore) await list.loadMore() await list.loadMore() #expect(list.state.value?.map(\.number) == [1, 2, 3, 4, 5]) #expect(!list.hasMore) // Further calls are no-ops, not requests. await list.loadMore() #expect(stub.seen.count == 3) #expect(stub.seen[1].url.query()?.contains("argv=--cursor&argv=c1") == true) #expect(stub.seen[2].url.query()?.contains("argv=--cursor&argv=c2") == true) } @Test func anEmptyFirstPageIsAnEmptyState() async throws { let (list, stub) = try makeList() stub.enqueue(.init(status: 200, json: page([]))) await list.reload() guard case .empty(let message) = list.state else { Issue.record("expected .empty, got \(list.state)") return } #expect(message == "nothing") } @Test func aFailedLaterPageKeepsTheListAndTheCursor() async throws { let (list, stub) = try makeList() stub.enqueue(.init(status: 200, json: page([1, 2], next: "c1"))) stub.enqueue(.init(status: 500, json: #"{"protocol_version":1,"error":"boom","exit_code":1}"#)) stub.enqueue(.init(status: 500, json: #"{"protocol_version":1,"error":"boom","exit_code":1}"#)) await list.reload() await list.loadMore() #expect(list.state.value == [Row(number: 1), Row(number: 2)]) #expect(list.hasMore) // the cursor survives for a retry stub.enqueue(.init(status: 200, json: page([3]))) await list.loadMore() #expect(list.state.value?.map(\.number) == [1, 2, 3]) } @Test func reloadDropsTheOldCursorAndItems() async throws { let (list, stub) = try makeList() stub.enqueue(.init(status: 200, json: page([1, 2], next: "c1"))) stub.enqueue(.init(status: 200, json: page([9]))) await list.reload() await list.reload() #expect(list.state.value == [Row(number: 9)]) #expect(!list.hasMore) } }