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) } struct FeedEventTests { private func decode(_ json: String) throws -> FeedEvent { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 return try decoder.decode(FeedEvent.self, from: Data(json.utf8)) } @Test func knownKindsGetPhrasesAndDestinations() throws { let merged = try decode(""" {"id":777,"repo":"krz/gitbay-ios","actor":"cmc","kind":"mr.merged",\ "data":{"number":6,"sha":"b55fbb"},"created_at":"2026-08-27T15:31:10Z"} """) #expect(merged.phrase == "merged !6") #expect(merged.destination == .mr(repo: "krz/gitbay-ios", number: 6)) let build = try decode(""" {"id":1,"repo":"krz/gitbay","actor":"","kind":"build.success",\ "data":{"number":56,"job":"ci"},"created_at":"2026-08-27T15:31:10Z"} """) #expect(build.phrase == "build success ci") #expect(build.displayActor == "gitbay") #expect(build.destination == .build(repo: "krz/gitbay", number: 56)) let release = try decode(""" {"id":2,"repo":"krz/orgo","actor":"krz","kind":"release.created",\ "data":{"tag":"v2.1.0"},"created_at":"2026-08-27T15:31:10Z"} """) #expect(release.phrase == "released v2.1.0") #expect(release.destination == .release(repo: "krz/orgo", tag: "v2.1.0")) } @Test func unknownKindsRenderRawAndFallBackToTheRepo() throws { let event = try decode(""" {"id":3,"repo":"krz/gitbay","kind":"wiki.edited",\ "data":{"page":"Parity"},"created_at":"2026-08-27T15:31:10Z"} """) #expect(event.phrase == "wiki.edited") #expect(event.destination == .repo("krz/gitbay")) } @Test func missingDataDoesNotSinkTheEvent() throws { let event = try decode(""" {"id":4,"repo":"krz/gitbay","actor":"cmc","kind":"push",\ "created_at":"2026-08-27T15:31:10Z"} """) #expect(event.phrase == "pushed") #expect(event.destination == .repo("krz/gitbay")) } } @MainActor struct GrepViewModelTests { @Test func searchGroupsMatchesByFileInServerOrder() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: """ {"protocol_version":1,"data":[\ {"path":"internal/a.go","line":3,"text":"foo bar"},\ {"path":"internal/a.go","line":9,"text":"more foo"},\ {"path":"cmd/b.go","line":1,"text":"foo again"}\ ],"exit_code":0} """)) let model = GrepViewModel(client: client, repoPath: "krz/gitbay") await model.search(" foo ") #expect(model.byFile.map(\.file) == ["internal/a.go", "cmd/b.go"]) #expect(model.byFile[0].matches.map(\.line) == [3, 9]) let seen = try #require(stub.seen.first) #expect(seen.url.query() == "argv=repo&argv=grep&argv=krz/gitbay&argv=foo") } @Test func noMatchesIsAnEmptyStateNamingTheQuery() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"exit_code":0}"#)) let model = GrepViewModel(client: client, repoPath: "krz/gitbay") await model.search("nothing") guard case .empty(let message)? = model.state else { Issue.record("expected .empty, got \(String(describing: model.state))") return } #expect(message.contains("nothing")) } @Test func aBlankQueryDoesNotSearch() async throws { let (client, stub) = try makeClient() _ = client let model = GrepViewModel(client: client, repoPath: "krz/gitbay") await model.search(" ") #expect(model.state == nil) #expect(stub.seen.isEmpty) } } @MainActor struct RepoSearchTests { @Test func aTypedQuerySearchesServerSideAfterTheDebounce() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: """ {"protocol_version":1,"data":{"items":[\ {"path":"cmc/notes","visibility":"private"}]},"exit_code":0} """, match: "argv=repo&argv=list")) stub.enqueue(.init(status: 200, json: """ {"protocol_version":1,"data":[\ {"path":"krz/space-wiki","visibility":"public","description":"A fun wiki about space.",\ "topics":["wiki"]}],"exit_code":0} """, match: "argv=search")) let model = RepoListViewModel(client: client) await model.load() model.searchText = "space" // Before the server answers, the client filter runs over loaded // pages — no match here. #expect(model.visibleRepos.isEmpty) try await Task.sleep(for: .milliseconds(700)) // The server search found a public repo not in the account's list. #expect(model.visibleRepos.map(\.path) == ["krz/space-wiki"]) #expect(stub.seen.contains { $0.url.query() == "argv=repo&argv=search&argv=space" }) } @Test func clearingTheQueryRestoresThePagedList() async throws { let (client, stub) = try makeClient() stub.enqueue(.init(status: 200, json: """ {"protocol_version":1,"data":{"items":[\ {"path":"cmc/notes","visibility":"private"}]},"exit_code":0} """, match: "argv=repo&argv=list")) let model = RepoListViewModel(client: client) await model.load() model.searchText = "zzz" model.searchText = "" try await Task.sleep(for: .milliseconds(500)) #expect(model.visibleRepos.map(\.path) == ["cmc/notes"]) // The emptied query never reached the server. #expect(!stub.seen.contains { ($0.url.query() ?? "").contains("argv=search") }) } }