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 dashboardJSON = """ {"protocol_version":1,"data":{\ "review_queue":[{"repo":"krz/gitbay-ios","number":3,"title":"adopt aggregate","author":"cmc",\ "state":"open","updated_at":"2026-08-27T10:00:00.000Z"}],\ "pinned":[{"path":"krz/gitbay","visibility":"public","description":"a CLI-first git forge"}],\ "open_mrs":[{"repo":"krz/gitbay-ios","number":3,"title":"adopt aggregate","author":"cmc",\ "state":"open","updated_at":"2026-08-27T10:00:00.000Z"}],\ "assigned_issues":[{"repo":"krz/gitbay","number":11,"title":"iOS app","author":"krz",\ "state":"open","updated_at":"2026-08-26T10:00:00.000Z"}],\ "open_issues":[{"repo":"krz/gitbay","number":11,"title":"iOS app","author":"krz",\ "state":"open","updated_at":"2026-08-26T10:00:00.000Z"}],\ "recent_activity":[{"id":7,"repo":"krz/gitbay-ios","actor":"cmc","kind":"mr.merged",\ "data":{"number":2},"created_at":"2026-08-27T10:30:00.000Z"}],\ "builds":[{"repo":"krz/gitbay","number":9,"job":"ci","status":"success",\ "sha":"65ba14e0000000000000","ref":"refs/heads/main",\ "created_at":"2026-08-27T09:00:00.000Z","finished_at":"2026-08-27T09:05:00.000Z"}]},\ "exit_code":0} """ private let emptyDashboardJSON = """ {"protocol_version":1,"data":{"pinned":[],"open_mrs":[],"assigned_issues":[],"builds":[]},\ "exit_code":0} """ @MainActor struct DashboardViewModelTests { @Test func oneReadFillsEverySection() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: dashboardJSON)) let model = DashboardViewModel(client: client) await model.load() let data = try #require(model.state.value) #expect(data.pinned.map(\.path) == ["krz/gitbay"]) #expect(data.reviewQueue.map(\.id) == ["krz/gitbay-ios#3"]) #expect(data.openMRs.map(\.id) == ["krz/gitbay-ios#3"]) #expect(data.assignedIssues.first?.number == 11) #expect(data.openIssues.first?.number == 11) #expect(data.recentActivity.first?.phrase == "merged !2") #expect(data.builds.first?.status == "success") // The whole screen cost exactly one request. #expect(stub.seen.count == 1) #expect(stub.seen.first?.url.query() == "argv=dashboard") } @Test func emptyAggregateStillLoads() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: emptyDashboardJSON)) let model = DashboardViewModel(client: client) await model.load() let data = try #require(model.state.value) #expect(data.pinned.isEmpty) #expect(data.openMRs.isEmpty) #expect(data.reviewQueue.isEmpty) #expect(data.openIssues.isEmpty) #expect(data.recentActivity.isEmpty) } @Test func aFailureIsTheScreensState() async throws { let (client, stub) = try makeClient() 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}"#)) let model = DashboardViewModel(client: client) await model.load() guard case .failed = model.state else { Issue.record("expected .failed, got \(model.state)") return } } }