gitbayTests/PagedListTests.swift
125 lines · 4568 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 nonisolated struct Row: Decodable, Sendable, Equatable {
16 let number: Int
17}
18
19private func page(_ numbers: [Int], next: String? = nil) -> String {
20 let items = numbers.map { #"{"number":\#($0)}"# }.joined(separator: ",")
21 let nextField = next.map { #","next":"\#($0)""# } ?? ""
22 return #"{"protocol_version":1,"data":{"items":[\#(items)]\#(nextField)},"exit_code":0}"#
23}
24
25struct ClientPagingTests {
26
27 @Test func readPageSendsLimitAndCursorAndDecodesNext() async throws {
28 let (client, stub) = try makeClient()
29 stub.enqueue(.init(status: 200, json: page([1, 2], next: "abc")))
30 stub.enqueue(.init(status: 200, json: page([3])))
31
32 let first = try await client.readPage(["issue", "list", "krz/gitbay"], of: Row.self, limit: 2)
33 let second = try await client.readPage(
34 ["issue", "list", "krz/gitbay"], of: Row.self, limit: 2, cursor: first.next
35 )
36
37 #expect(first.items == [Row(number: 1), Row(number: 2)])
38 #expect(first.next == "abc")
39 #expect(second.items == [Row(number: 3)])
40 #expect(second.next == nil)
41 #expect(stub.seen[0].url.query() ==
42 "argv=issue&argv=list&argv=krz/gitbay&argv=--limit&argv=2")
43 #expect(stub.seen[1].url.query() ==
44 "argv=issue&argv=list&argv=krz/gitbay&argv=--limit&argv=2&argv=--cursor&argv=abc")
45 }
46}
47
48@MainActor
49struct PagedListModelTests {
50
51 private func makeList(pageSize: Int = 2) throws -> (PagedListModel<Row>, StubProtocol.Box) {
52 let (client, stub) = try makeClient()
53 let list = PagedListModel<Row>(
54 client: client,
55 argv: ["issue", "list", "krz/gitbay"],
56 emptyMessage: "nothing",
57 pageSize: pageSize
58 )
59 return (list, stub)
60 }
61
62 @Test func loadMoreAppendsUntilTheCursorRunsOut() async throws {
63 let (list, stub) = try makeList()
64 stub.enqueue(.init(status: 200, json: page([1, 2], next: "c1")))
65 stub.enqueue(.init(status: 200, json: page([3, 4], next: "c2")))
66 stub.enqueue(.init(status: 200, json: page([5])))
67
68 await list.reload()
69 #expect(list.state.value == [Row(number: 1), Row(number: 2)])
70 #expect(list.hasMore)
71
72 await list.loadMore()
73 await list.loadMore()
74 #expect(list.state.value?.map(\.number) == [1, 2, 3, 4, 5])
75 #expect(!list.hasMore)
76
77 // Further calls are no-ops, not requests.
78 await list.loadMore()
79 #expect(stub.seen.count == 3)
80 #expect(stub.seen[1].url.query()?.contains("argv=--cursor&argv=c1") == true)
81 #expect(stub.seen[2].url.query()?.contains("argv=--cursor&argv=c2") == true)
82 }
83
84 @Test func anEmptyFirstPageIsAnEmptyState() async throws {
85 let (list, stub) = try makeList()
86 stub.enqueue(.init(status: 200, json: page([])))
87
88 await list.reload()
89
90 guard case .empty(let message) = list.state else {
91 Issue.record("expected .empty, got \(list.state)")
92 return
93 }
94 #expect(message == "nothing")
95 }
96
97 @Test func aFailedLaterPageKeepsTheListAndTheCursor() async throws {
98 let (list, stub) = try makeList()
99 stub.enqueue(.init(status: 200, json: page([1, 2], next: "c1")))
100 stub.enqueue(.init(status: 500, json: #"{"protocol_version":1,"error":"boom","exit_code":1}"#))
101 stub.enqueue(.init(status: 500, json: #"{"protocol_version":1,"error":"boom","exit_code":1}"#))
102
103 await list.reload()
104 await list.loadMore()
105
106 #expect(list.state.value == [Row(number: 1), Row(number: 2)])
107 #expect(list.hasMore) // the cursor survives for a retry
108
109 stub.enqueue(.init(status: 200, json: page([3])))
110 await list.loadMore()
111 #expect(list.state.value?.map(\.number) == [1, 2, 3])
112 }
113
114 @Test func reloadDropsTheOldCursorAndItems() async throws {
115 let (list, stub) = try makeList()
116 stub.enqueue(.init(status: 200, json: page([1, 2], next: "c1")))
117 stub.enqueue(.init(status: 200, json: page([9])))
118
119 await list.reload()
120 await list.reload()
121
122 #expect(list.state.value == [Row(number: 9)])
123 #expect(!list.hasMore)
124 }
125}