gitbayTests/DiscoveryTests.swift
201 lines · 7714 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
15struct FeedEventTests {
16
17 private func decode(_ json: String) throws -> FeedEvent {
18 let decoder = JSONDecoder()
19 decoder.dateDecodingStrategy = .iso8601
20 return try decoder.decode(FeedEvent.self, from: Data(json.utf8))
21 }
22
23 @Test func knownKindsGetPhrasesAndDestinations() throws {
24 let merged = try decode("""
25 {"id":777,"repo":"krz/gitbay-ios","actor":"cmc","kind":"mr.merged",\
26 "data":{"number":6,"sha":"b55fbb"},"created_at":"2026-08-27T15:31:10Z"}
27 """)
28 #expect(merged.phrase == "merged !6")
29 #expect(merged.destination == .mr(repo: "krz/gitbay-ios", number: 6))
30
31 let build = try decode("""
32 {"id":1,"repo":"krz/gitbay","actor":"","kind":"build.success",\
33 "data":{"number":56},"created_at":"2026-08-27T15:31:10Z"}
34 """)
35 #expect(build.phrase == "build #56 succeeded")
36 #expect(build.destination == .build(repo: "krz/gitbay", number: 56))
37
38 let release = try decode("""
39 {"id":2,"repo":"krz/orgo","actor":"krz","kind":"release.created",\
40 "data":{"tag":"v2.1.0"},"created_at":"2026-08-27T15:31:10Z"}
41 """)
42 #expect(release.phrase == "released v2.1.0")
43 #expect(release.destination == .release(repo: "krz/orgo", tag: "v2.1.0"))
44 }
45
46 @Test func unknownKindsRenderRawAndFallBackToTheRepo() throws {
47 let event = try decode("""
48 {"id":3,"repo":"krz/gitbay","kind":"wiki.edited",\
49 "data":{"page":"Parity"},"created_at":"2026-08-27T15:31:10Z"}
50 """)
51 #expect(event.phrase == "wiki.edited")
52 #expect(event.destination == .repo("krz/gitbay"))
53 }
54
55 @Test func missingDataDoesNotSinkTheEvent() throws {
56 let event = try decode("""
57 {"id":4,"repo":"krz/gitbay","actor":"cmc","kind":"push",\
58 "created_at":"2026-08-27T15:31:10Z"}
59 """)
60 #expect(event.phrase == "pushed")
61 #expect(event.destination == .repo("krz/gitbay"))
62 }
63}
64
65@MainActor
66struct GrepViewModelTests {
67
68 @Test func searchGroupsMatchesByFileInServerOrder() async throws {
69 let (client, stub) = try makeClient()
70 stub.enqueue(.init(status: 200, json: """
71 {"protocol_version":1,"data":[\
72 {"path":"internal/a.go","line":3,"text":"foo bar"},\
73 {"path":"internal/a.go","line":9,"text":"more foo"},\
74 {"path":"cmd/b.go","line":1,"text":"foo again"}\
75 ],"exit_code":0}
76 """))
77 let model = GrepViewModel(client: client, repoPath: "krz/gitbay")
78
79 await model.search(" foo ")
80
81 #expect(model.byFile.map(\.file) == ["internal/a.go", "cmd/b.go"])
82 #expect(model.byFile[0].matches.map(\.line) == [3, 9])
83 let seen = try #require(stub.seen.first)
84 #expect(seen.url.query() == "argv=repo&argv=grep&argv=krz/gitbay&argv=foo")
85 }
86
87 @Test func noMatchesIsAnEmptyStateNamingTheQuery() async throws {
88 let (client, stub) = try makeClient()
89 stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"exit_code":0}"#))
90 let model = GrepViewModel(client: client, repoPath: "krz/gitbay")
91
92 await model.search("nothing")
93
94 guard case .empty(let message)? = model.state else {
95 Issue.record("expected .empty, got \(String(describing: model.state))")
96 return
97 }
98 #expect(message.contains("nothing"))
99 }
100
101 @Test func aBlankQueryDoesNotSearch() async throws {
102 let (client, stub) = try makeClient()
103 _ = client
104 let model = GrepViewModel(client: client, repoPath: "krz/gitbay")
105
106 await model.search(" ")
107
108 #expect(model.state == nil)
109 #expect(stub.seen.isEmpty)
110 }
111}
112
113@MainActor
114struct ProfileViewModelTests {
115
116 @Test func loadsProfileAndPrefixFilteredRepos() async throws {
117 let (client, stub) = try makeClient()
118 stub.enqueue(.init(status: 200, json: """
119 {"protocol_version":1,"data":{"name":"krz","kind":"org",\
120 "description":"warez for the public","website":"https://krz.sh"},"exit_code":0}
121 """, match: "argv=profile"))
122 stub.enqueue(.init(status: 200, json: """
123 {"protocol_version":1,"data":[\
124 {"path":"krz/gitbay","visibility":"public","topics":["git"]},\
125 {"path":"cmc/krz-notes","visibility":"public"}\
126 ],"exit_code":0}
127 """, match: "argv=search"))
128 let model = ProfileViewModel(client: client, name: "krz")
129
130 await model.load()
131
132 let profile = try #require(model.state.value)
133 #expect(profile.kind == "org")
134 // Only true owner-prefix matches; "cmc/krz-notes" merely mentions it.
135 #expect(model.repos.map(\.path) == ["krz/gitbay"])
136 }
137
138 @Test func anUnknownNameIsAnEmptyState() async throws {
139 let (client, stub) = try makeClient()
140 stub.enqueue(.init(status: 404, json:
141 #"{"protocol_version":1,"error":"no such user or org \"nobody\"","exit_code":3}"#,
142 match: "argv=profile"))
143 let model = ProfileViewModel(client: client, name: "nobody")
144
145 await model.load()
146
147 guard case .empty = model.state else {
148 Issue.record("expected .empty, got \(model.state)")
149 return
150 }
151 }
152}
153
154@MainActor
155struct RepoSearchTests {
156
157 @Test func aTypedQuerySearchesServerSideAfterTheDebounce() async throws {
158 let (client, stub) = try makeClient()
159 stub.enqueue(.init(status: 200, json: """
160 {"protocol_version":1,"data":{"items":[\
161 {"path":"cmc/notes","visibility":"private"}]},"exit_code":0}
162 """, match: "argv=repo&argv=list"))
163 stub.enqueue(.init(status: 200, json: """
164 {"protocol_version":1,"data":[\
165 {"path":"krz/space-wiki","visibility":"public","description":"A fun wiki about space.",\
166 "topics":["wiki"]}],"exit_code":0}
167 """, match: "argv=search"))
168 let model = RepoListViewModel(client: client)
169 await model.load()
170
171 model.searchText = "space"
172 // Before the server answers, the client filter runs over loaded
173 // pages — no match here.
174 #expect(model.visibleRepos.isEmpty)
175 try await Task.sleep(for: .milliseconds(700))
176
177 // The server search found a public repo not in the account's list.
178 #expect(model.visibleRepos.map(\.path) == ["krz/space-wiki"])
179 #expect(stub.seen.contains {
180 $0.url.query() == "argv=repo&argv=search&argv=space"
181 })
182 }
183
184 @Test func clearingTheQueryRestoresThePagedList() async throws {
185 let (client, stub) = try makeClient()
186 stub.enqueue(.init(status: 200, json: """
187 {"protocol_version":1,"data":{"items":[\
188 {"path":"cmc/notes","visibility":"private"}]},"exit_code":0}
189 """, match: "argv=repo&argv=list"))
190 let model = RepoListViewModel(client: client)
191 await model.load()
192
193 model.searchText = "zzz"
194 model.searchText = ""
195 try await Task.sleep(for: .milliseconds(500))
196
197 #expect(model.visibleRepos.map(\.path) == ["cmc/notes"])
198 // The emptied query never reached the server.
199 #expect(!stub.seen.contains { ($0.url.query() ?? "").contains("argv=search") })
200 }
201}