a native ios client for gitbay

client ios swift

https://gitbay.org

gitbayTests/RepoViewModelTests.swift

ui-smoke
gitbay-ios/gitbayTests/RepoViewModelTests.swift history · blame · raw

273 lines · 10606 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 let repoListJSON = """
 16    {"protocol_version":1,"data":{"items":[\
 17    {"path":"krz/gitbay","visibility":"public","description":"a CLI-first git forge"},\
 18    {"path":"krz/hutch","visibility":"public","description":"SourceHut iOS client"},\
 19    {"path":"krz/secrets","visibility":"private","archived":true}\
 20    ]},"exit_code":0}
 21    """
 22
 23private let emptyDashboardJSON = """
 24    {"protocol_version":1,"data":{"pinned":[],"open_mrs":[],"assigned_issues":[],"builds":[]},\
 25    "exit_code":0}
 26    """
 27
 28private let repoShowJSON = """
 29    {"protocol_version":1,"data":{"path":"krz/gitbay","description":"a CLI-first git forge",\
 30    "visibility":"public","default_branch":"main","topics":["git","forge"],\
 31    "protected_branches":["main"]},"exit_code":0}
 32    """
 33
 34private let rootTreeJSON = """
 35    {"protocol_version":1,"data":{"path":"krz/gitbay","ref":"main","dir":"","entries":[\
 36    {"name":"internal","type":"tree","mode":"040000","sha":"aaa1"},\
 37    {"name":"README.md","type":"blob","mode":"100644","sha":"bbb2","size":1200},\
 38    {"name":"main.go","type":"blob","mode":"100644","sha":"ccc3","size":300}\
 39    ]},"exit_code":0}
 40    """
 41
 42private let readmeJSON = """
 43    {"protocol_version":1,"data":{"path":"krz/gitbay","ref":"main","file":"README.md",\
 44    "size":18,"binary":false,"content":"# gitbay\\n\\na forge"},"exit_code":0}
 45    """
 46
 47private let binaryFileJSON = """
 48    {"protocol_version":1,"data":{"path":"krz/gitbay","ref":"main","file":"logo.png",\
 49    "size":4,"binary":true,"base64":"iVBORw=="},"exit_code":0}
 50    """
 51
 52private let logJSON = """
 53    {"protocol_version":1,"data":[\
 54    {"sha":"65ba14e0000000000000000000000000000000ab","subject":"httpd: GET /api/v1/read",\
 55    "author_name":"krz","author_email":"krz@gitbay.org","date":"2026-08-20T10:00:00Z",\
 56    "signature":{"state":"verified","signer":"krz","key_fingerprint":"SHA256:abc"}},\
 57    {"sha":"7953e780000000000000000000000000000000cd","subject":"httpd: rate limit",\
 58    "author_name":"krz","author_email":"krz@gitbay.org","date":"2026-08-19T10:00:00Z",\
 59    "signature":{"state":"unsigned"}},\
 60    {"sha":"40dab3e0000000000000000000000000000000ef","subject":"control: repo tree and cat",\
 61    "author_name":"krz","author_email":"krz@gitbay.org","date":"2026-08-18T10:00:00Z",\
 62    "signature":{"state":"one_day_a_new_state"}}\
 63    ],"exit_code":0}
 64    """
 65
 66@MainActor
 67struct RepoListViewModelTests {
 68
 69    @Test func loadsOnePageInServerOrder() async throws {
 70        let (client, stub) = try makeClient()
 71        stub.enqueue(.init(status: 200, json: repoListJSON))
 72        let model = RepoListViewModel(client: client)
 73
 74        await model.load()
 75
 76        #expect(model.visibleRepos.map(\.path) == ["krz/gitbay", "krz/hutch", "krz/secrets"])
 77        #expect(model.visibleRepos[2].isPrivate)
 78        #expect(model.visibleRepos[2].isArchived)
 79        let seen = try #require(stub.seen.first)
 80        #expect(seen.url.query() == "argv=repo&argv=list&argv=--limit&argv=100")
 81    }
 82
 83    @Test func filterMatchesPathAndDescription() async throws {
 84        let (client, stub) = try makeClient()
 85        stub.enqueue(.init(status: 200, json: repoListJSON))
 86        let model = RepoListViewModel(client: client)
 87        await model.load()
 88
 89        model.searchText = "SourceHut"
 90        #expect(model.visibleRepos.map(\.path) == ["krz/hutch"])
 91
 92        model.searchText = "gitb"
 93        #expect(model.visibleRepos.map(\.path) == ["krz/gitbay"])
 94    }
 95
 96    @Test func anEmptyListIsAnEmptyStateNotAnError() async throws {
 97        let (client, stub) = try makeClient()
 98        stub.enqueue(.init(status: 200, json: #"{"protocol_version":1,"data":{"items":[]},"exit_code":0}"#))
 99        let model = RepoListViewModel(client: client)
100
101        await model.load()
102
103        guard case .empty = model.state else {
104            Issue.record("expected .empty, got \(model.state)")
105            return
106        }
107    }
108}
109
110@MainActor
111struct RepoDetailViewModelTests {
112
113    @Test func loadsHeaderThenFindsAndFetchesReadme() async throws {
114        let (client, stub) = try makeClient()
115        stub.enqueue(.init(status: 200, json: repoShowJSON, match: "argv=show"))
116        stub.enqueue(.init(status: 200, json: emptyDashboardJSON, match: "argv=dashboard"))
117        stub.enqueue(.init(status: 200, json: rootTreeJSON, match: "argv=tree"))
118        stub.enqueue(.init(status: 200, json: readmeJSON, match: "argv=cat"))
119        let model = RepoDetailViewModel(client: client, path: "krz/gitbay")
120
121        await model.load()
122
123        let detail = try #require(model.state.value)
124        #expect(detail.defaultBranch == "main")
125        #expect(detail.topics == ["git", "forge"])
126        #expect(model.readme == "# gitbay\n\na forge")
127        #expect(model.isPinned == false)
128        // The README was fetched by name from the tree listing.
129        let catRequest = try #require(stub.seen.last)
130        #expect(catRequest.url.query()?.contains("argv=README.md") == true)
131    }
132
133    @Test func aRepoWithoutAReadmeIsFine() async throws {
134        let (client, stub) = try makeClient()
135        stub.enqueue(.init(status: 200, json: repoShowJSON, match: "argv=show"))
136        stub.enqueue(.init(status: 200, json: emptyDashboardJSON, match: "argv=dashboard"))
137        stub.enqueue(.init(status: 200, json: """
138            {"protocol_version":1,"data":{"path":"krz/gitbay","ref":"main","dir":"",\
139            "entries":[{"name":"main.go","type":"blob","mode":"100644","sha":"ccc3","size":300}]},\
140            "exit_code":0}
141            """, match: "argv=tree"))
142        let model = RepoDetailViewModel(client: client, path: "krz/gitbay")
143
144        await model.load()
145
146        #expect(model.state.value != nil)
147        #expect(model.readme == nil)
148        #expect(!stub.seen.contains { ($0.url.query() ?? "").contains("argv=cat") })
149    }
150
151    @Test func aMissingRepoIsAnEmptyState() async throws {
152        let (client, stub) = try makeClient()
153        stub.enqueue(.init(status: 404, json:
154            #"{"protocol_version":1,"error":"no such repository krz/nope","exit_code":3}"#))
155        let model = RepoDetailViewModel(client: client, path: "krz/nope")
156
157        await model.load()
158
159        guard case .empty(let message) = model.state else {
160            Issue.record("expected .empty, got \(model.state)")
161            return
162        }
163        #expect(message == "no such repository krz/nope")
164    }
165}
166
167@MainActor
168struct TreeViewModelTests {
169
170    @Test func directoriesSortFirstThenAlphabetical() async throws {
171        let (client, stub) = try makeClient()
172        stub.enqueue(.init(status: 200, json: rootTreeJSON))
173        let model = TreeViewModel(client: client, repoPath: "krz/gitbay")
174
175        await model.load()
176
177        #expect(model.entries.map(\.name) == ["internal", "main.go", "README.md"])
178    }
179
180    @Test func childPathsNestCorrectly() async throws {
181        let (client, stub) = try makeClient()
182        stub.enqueue(.init(status: 200, json: rootTreeJSON))
183        let root = TreeViewModel(client: client, repoPath: "krz/gitbay")
184        await root.load()
185        let dir = try #require(root.entries.first { $0.isDirectory })
186
187        #expect(root.childDirectory(dir) == "internal")
188
189        let nested = TreeViewModel(client: client, repoPath: "krz/gitbay", directory: "internal")
190        #expect(nested.childDirectory(dir) == "internal/internal")
191    }
192
193    @Test func refIsForwardedWhenSet() async throws {
194        let (client, stub) = try makeClient()
195        stub.enqueue(.init(status: 200, json: rootTreeJSON))
196        let model = TreeViewModel(
197            client: client, repoPath: "krz/gitbay", directory: "internal", ref: "dev"
198        )
199
200        await model.load()
201
202        let seen = try #require(stub.seen.first)
203        #expect(seen.url.query() ==
204            "argv=repo&argv=tree&argv=krz/gitbay&argv=internal&argv=--ref&argv=dev")
205    }
206}
207
208@MainActor
209struct FileViewModelTests {
210
211    @Test func textFileLoads() async throws {
212        let (client, stub) = try makeClient()
213        stub.enqueue(.init(status: 200, json: readmeJSON))
214        let model = FileViewModel(client: client, repoPath: "krz/gitbay", filePath: "README.md")
215
216        await model.load()
217
218        let file = try #require(model.state.value)
219        #expect(file.content?.hasPrefix("# gitbay") == true)
220        #expect(!file.binary)
221        #expect(model.fileName == "README.md")
222    }
223
224    @Test func binaryFileCarriesBase64NotText() async throws {
225        let (client, stub) = try makeClient()
226        stub.enqueue(.init(status: 200, json: binaryFileJSON))
227        let model = FileViewModel(client: client, repoPath: "krz/gitbay", filePath: "logo.png")
228
229        await model.load()
230
231        let file = try #require(model.state.value)
232        #expect(file.binary)
233        #expect(file.content == nil)
234        #expect(file.data != nil)
235    }
236}
237
238@MainActor
239struct LogViewModelTests {
240
241    @Test func commitsDecodeWithSignatureStates() async throws {
242        let (client, stub) = try makeClient()
243        stub.enqueue(.init(status: 200, json: logJSON))
244        let model = LogViewModel(client: client, repoPath: "krz/gitbay")
245
246        await model.load()
247
248        let commits = try #require(model.state.value)
249        #expect(commits.count == 3)
250        #expect(commits[0].signature.state == .verified)
251        #expect(commits[0].signature.signer == "krz")
252        #expect(commits[1].signature.state == .unsigned)
253        // A state this build has never heard of must not sink the log.
254        #expect(commits[2].signature.state == .unrecognized("one_day_a_new_state"))
255        #expect(commits[0].shortSHA == "65ba14e000")
256    }
257}
258
259struct SyntaxHighlighterLanguageTests {
260
261    @Test func commonExtensionsResolve() {
262        #expect(SyntaxHighlighter.language(for: "GitbayClient.swift") == "swift")
263        #expect(SyntaxHighlighter.language(for: "read.go") == "go")
264        #expect(SyntaxHighlighter.language(for: "Makefile") == "makefile")
265        #expect(SyntaxHighlighter.language(for: "index.test.ts") == "typescript")
266    }
267
268    @Test func unknownExtensionsFallBackToPlain() {
269        #expect(SyntaxHighlighter.language(for: "notes.txt") == nil)
270        #expect(SyntaxHighlighter.language(for: "LICENSE") == nil)
271        #expect(SyntaxHighlighter.language(for: "weird.xyz123") == nil)
272    }
273}