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 let emptyPage = #"{"protocol_version":1,"data":{"items":[]},"exit_code":0}"# /// A refusal, not a 5xx: the client retries a 5xx once, which would /// take the next stub and hide the failure. private let refusal = #"{"protocol_version":1,"error":"denied","exit_code":4}"# private func reads(_ stub: StubProtocol.Box, _ command: String) -> Int { stub.seen.count { $0.url.query()?.contains(command) == true } } /// Returning to a list from a pushed detail screen runs its `.task` /// again. `loadIfNeeded()` reads the list only when nothing is shown yet, /// so the pages and scroll position the user left are still there. @MainActor struct ListReturnTests { @Test func theIssueListLoadsOnce() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: emptyPage, match: "argv=issue&argv=list")) let model = IssueListViewModel(client: client, repoPath: "krz/gitbay") await model.loadIfNeeded() await model.loadIfNeeded() #expect(reads(stub, "argv=issue&argv=list") == 1) } @Test func theMRListLoadsOnce() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: emptyPage, match: "argv=mr&argv=list")) let model = MRListViewModel(client: client, repoPath: "krz/gitbay") await model.loadIfNeeded() await model.loadIfNeeded() #expect(reads(stub, "argv=mr&argv=list") == 1) } @Test func theBuildListLoadsOnce() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: emptyPage, match: "argv=build&argv=list")) let model = BuildListViewModel(client: client, repoPath: "krz/gitbay") await model.loadIfNeeded() await model.loadIfNeeded() #expect(reads(stub, "argv=build&argv=list") == 1) } @Test func aFailedLoadIsTriedAgainOnReturn() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 403, json: refusal, match: "argv=mr&argv=list")) stub.enqueue(.init(status: 200, json: emptyPage, match: "argv=mr&argv=list")) let model = MRListViewModel(client: client, repoPath: "krz/gitbay") await model.loadIfNeeded() await model.loadIfNeeded() #expect(reads(stub, "argv=mr&argv=list") == 2) guard case .empty = model.state else { Issue.record("expected .empty after the retry, got \(model.state)") return } } /// Pull-to-refresh still calls `load()`, which always reads. @Test func loadStillReloads() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: emptyPage, match: "argv=issue&argv=list")) stub.enqueue(.init(status: 200, json: emptyPage, match: "argv=issue&argv=list")) let model = IssueListViewModel(client: client, repoPath: "krz/gitbay") await model.loadIfNeeded() await model.load() #expect(reads(stub, "argv=issue&argv=list") == 2) } }