gitbayTests/ProfileCommitTests.swift
149 lines · 6195 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 userProfileJSON = """
16 {"protocol_version":1,"data":{"name":"cmc","kind":"user",\
17 "description":"Org-Mode · Self-Hosting · Privacy","website":"https://cleberg.net",\
18 "about":"* Christian\\n\\nOrg-Mode.","about_format":"org",\
19 "links":[{"label":"Tor","url":"http://example.onion/"},{"url":"https://krz.sh"}],\
20 "orgs":[{"name":"krz","role":"admin"},{"name":"audit-labs","role":"admin"}],\
21 "repos":[{"path":"cmc/notes","visibility":"private","description":"notes",\
22 "topics":["org","writing"],"license":"MIT","default_branch":"main","updated":"2026-08-28"},\
23 {"path":"cmc/cv","visibility":"public"}],\
24 "activity":[{"date":"2026-08-01","count":3},{"date":"2026-08-02","count":12}],\
25 "activity_total":15},"exit_code":0}
26 """
27
28private let orgProfileJSON = """
29 {"protocol_version":1,"data":{"name":"krz","kind":"org","description":"warez",\
30 "members":[{"name":"cmc","role":"admin"}],"repos":[],"activity_total":0},"exit_code":0}
31 """
32
33@MainActor
34struct ProfileAggregateTests {
35
36 @Test func aUserProfileCarriesOrgsReposAndActivity() async throws {
37 let (client, stub) = try makeClient()
38 stub.enqueue(.init(status: 200, json: userProfileJSON))
39 let model = ProfileViewModel(client: client, name: "cmc")
40
41 await model.load()
42
43 let profile = try #require(model.state.value)
44 #expect(!profile.isOrg)
45 #expect(profile.orgs?.map(\.name) == ["krz", "audit-labs"])
46 #expect(profile.repos.map(\.path) == ["cmc/notes", "cmc/cv"])
47 #expect(profile.repos[0].isPrivate)
48 // Repo rows carry the listing metadata, so a profile lists
49 // repositories the way explore does.
50 #expect(profile.repos[0].topics == ["org", "writing"])
51 #expect(profile.repos[0].meta == "main · MIT · updated 2026-08-28")
52 // A row the server reported without any of it still renders.
53 #expect(profile.repos[1].topics == nil)
54 #expect(profile.repos[1].meta.isEmpty)
55 #expect(profile.about == "* Christian\n\nOrg-Mode.")
56 // The format picks the filename the about text renders under.
57 #expect(profile.aboutFile == "about.org")
58 #expect(profile.links?.map(\.title) == ["Tor", "https://krz.sh"])
59 #expect(profile.activityTotal == 15)
60 // One read builds the whole page.
61 #expect(stub.seen.count == 1)
62 #expect(stub.seen[0].url.query() == "argv=profile&argv=show&argv=cmc")
63 }
64
65 @Test func anOrgProfileCarriesMembersInsteadOfOrgs() async throws {
66 let (client, stub) = try makeClient()
67 stub.enqueue(.init(status: 200, json: orgProfileJSON))
68 let model = ProfileViewModel(client: client, name: "krz")
69
70 await model.load()
71
72 let profile = try #require(model.state.value)
73 #expect(profile.isOrg)
74 #expect(profile.members?.map(\.name) == ["cmc"])
75 #expect(profile.orgs == nil)
76 #expect(profile.repos.isEmpty)
77 // A profile with no about text and no links renders neither.
78 #expect(profile.about == nil)
79 #expect(profile.links == nil)
80 // Markdown is the default for anything but org.
81 #expect(profile.aboutFile == "about.md")
82 }
83
84 @Test func anUnknownNameIsAnEmptyState() async throws {
85 let (client, stub) = try makeClient()
86 stub.enqueue(.init(status: 404, json:
87 #"{"protocol_version":1,"error":"no user or organization \"nobody\"","exit_code":3}"#))
88 let model = ProfileViewModel(client: client, name: "nobody")
89
90 await model.load()
91
92 guard case .empty = model.state else {
93 Issue.record("expected .empty, got \(model.state)")
94 return
95 }
96 }
97}
98
99@MainActor
100struct CommitDetailTests {
101
102 private let commitJSON = """
103 {"protocol_version":1,"data":{"path":"krz/gitbay",\
104 "sha":"59ae14400000000000000000000000000000abcd",\
105 "subject":"control: profile aggregate","message":"Body text.",\
106 "author_name":"Christian Cleberg","author_email":"hello@cleberg.net",\
107 "date":"2026-08-28T02:00:00Z",\
108 "signature":{"state":"verified","signer":"cmc"},\
109 "checks":[{"context":"ci","state":"success"}],\
110 "diff":"diff --git a/main.go b/main.go\\n--- a/main.go\\n+++ b/main.go\\n@@ -1,2 +1,3 @@\\n package main\\n+// added\\n"},\
111 "exit_code":0}
112 """
113
114 @Test func loadsTheCommitAndParsesItsPatch() async throws {
115 let (client, stub) = try makeClient()
116 stub.enqueue(.init(status: 200, json: commitJSON))
117 let model = CommitDetailViewModel(
118 client: client, repoPath: "krz/gitbay", sha: "59ae144")
119
120 await model.load()
121
122 let commit = try #require(model.state.value)
123 #expect(commit.subject == "control: profile aggregate")
124 #expect(commit.signature.state == .verified)
125 #expect(commit.checks?.first?.state == "success")
126 #expect(commit.shortSHA == "59ae144000")
127 // The patch is parsed by the same parser mr diff uses.
128 let diff = try #require(model.diff)
129 #expect(diff.files.map(\.displayPath) == ["main.go"])
130 #expect(diff.additions == 1)
131 #expect(stub.seen.first?.url.query() ==
132 "argv=repo&argv=commit&argv=krz/gitbay&argv=59ae144")
133 }
134
135 @Test func anUnknownShaIsAnEmptyState() async throws {
136 let (client, stub) = try makeClient()
137 stub.enqueue(.init(status: 404, json:
138 #"{"protocol_version":1,"error":"no commit \"deadbeef\" in krz/gitbay","exit_code":3}"#))
139 let model = CommitDetailViewModel(
140 client: client, repoPath: "krz/gitbay", sha: "deadbeef")
141
142 await model.load()
143
144 guard case .empty = model.state else {
145 Issue.record("expected .empty, got \(model.state)")
146 return
147 }
148 }
149}