krz/hutch
an ios client for sourcehut
clone: git clone https://gitbay.org/krz/hutch.git
main: HutchTests/HomeViewModelTests.swift · raw
1import Foundation
2import Testing
3@testable import Hutch
4
5@MainActor
6struct HomeViewModelTests {
7
8 @Test
9 func failedBuildsKeepsOnlyFailedAndTimedOutJobs() {
10 let jobs = [
11 makeJob(id: 1, status: .success, created: Date(timeIntervalSince1970: 10)),
12 makeJob(id: 2, status: .failed, created: Date(timeIntervalSince1970: 20)),
13 makeJob(id: 3, status: .timeout, created: Date(timeIntervalSince1970: 30)),
14 makeJob(id: 4, status: .running, created: Date(timeIntervalSince1970: 40))
15 ]
16
17 let failedBuilds = HomeViewModel.failedBuilds(from: jobs)
18
19 #expect(failedBuilds.map(\.job.id) == [3, 2])
20 }
21
22 @Test
23 func failedBuildsWithinLookbackExcludeOlderFailures() {
24 let now = Date(timeIntervalSince1970: 60 * 60 * 24 * 20)
25 let recentFailure = HomeBuildItem(
26 job: JobSummary(
27 id: 1,
28 created: now.addingTimeInterval(-(60 * 60 * 24)),
29 updated: now.addingTimeInterval(-(60 * 60 * 24)),
30 status: .failed,
31 note: nil,
32 tags: [],
33 visibility: nil,
34 image: nil,
35 tasks: []
36 ),
37 repositoryName: nil,
38 repositoryOwner: nil
39 )
40 let oldFailure = HomeBuildItem(
41 job: JobSummary(
42 id: 2,
43 created: now.addingTimeInterval(-(60 * 60 * 24 * 10)),
44 updated: now.addingTimeInterval(-(60 * 60 * 24 * 10)),
45 status: .timeout,
46 note: nil,
47 tags: [],
48 visibility: nil,
49 image: nil,
50 tasks: []
51 ),
52 repositoryName: nil,
53 repositoryOwner: nil
54 )
55 let recentSuccess = HomeBuildItem(
56 job: JobSummary(
57 id: 3,
58 created: now.addingTimeInterval(-(60 * 60 * 24)),
59 updated: now.addingTimeInterval(-(60 * 60 * 24)),
60 status: .success,
61 note: nil,
62 tags: [],
63 visibility: nil,
64 image: nil,
65 tasks: []
66 ),
67 repositoryName: nil,
68 repositoryOwner: nil
69 )
70
71 let filtered = HomeViewModel.failedBuilds(
72 in: [recentFailure, oldFailure, recentSuccess],
73 lookbackDays: 7,
74 now: now,
75 calendar: Calendar(identifier: .gregorian)
76 )
77
78 #expect(filtered.map(\.job.id) == [1])
79 }
80
81 @Test
82 func failedBuildLookbackDaysFallsBackToDefaultWhenUnsetOrInvalid() {
83 let defaults = UserDefaults(suiteName: #function)!
84 defaults.removePersistentDomain(forName: #function)
85
86 #expect(HomeViewModel.failedBuildLookbackDays(defaults: defaults) == HomeViewModel.defaultFailedBuildLookbackDays)
87
88 defaults.set(99, forKey: AppStorageKeys.homeFailedBuildLookbackDays)
89
90 #expect(HomeViewModel.failedBuildLookbackDays(defaults: defaults) == HomeViewModel.defaultFailedBuildLookbackDays)
91 }
92
93 @Test
94 func deduplicateInboxThreadsCollapsesMessagesIntoOneThreadSummary() {
95 let list = InboxMailingListReference(
96 id: 1,
97 rid: "list",
98 name: "hutch-devel",
99 owner: Entity(canonicalName: "~owner")
100 )
101 let first = InboxThreadSummary(
102 rootEmailID: 10,
103 rootMessageID: "message-1",
104 threadRootEmailIDs: [10],
105 threadRootMessageIDs: ["message-1"],
106 listID: list.id,
107 listRID: list.rid,
108 listName: list.name,
109 listOwner: list.owner,
110 subject: "Re: [PATCH] add search",
111 latestSender: Entity(canonicalName: "~alice"),
112 lastActivityAt: Date(timeIntervalSince1970: 100),
113 messageCount: 1,
114 repo: "hutch",
115 containsPatch: true,
116 isUnread: true
117 )
118 let second = InboxThreadSummary(
119 rootEmailID: 11,
120 rootMessageID: "message-2",
121 threadRootEmailIDs: [11],
122 threadRootMessageIDs: ["message-2"],
123 listID: list.id,
124 listRID: list.rid,
125 listName: list.name,
126 listOwner: list.owner,
127 subject: "[PATCH] add search",
128 latestSender: Entity(canonicalName: "~bob"),
129 lastActivityAt: Date(timeIntervalSince1970: 200),
130 messageCount: 2,
131 repo: "hutch",
132 containsPatch: true,
133 isUnread: true
134 )
135
136 let deduplicated = HomeViewModel.deduplicateInboxThreads([first, second])
137
138 #expect(deduplicated.count == 1)
139 #expect(deduplicated[0].rootEmailID == 11)
140 #expect(deduplicated[0].threadRootEmailIDs == [10, 11])
141 #expect(deduplicated[0].threadRootMessageIDs == ["message-1", "message-2"])
142 #expect(deduplicated[0].messageCount == 2)
143 }
144
145 @Test
146 func matchesCurrentUserAssigneeNormalizesCanonicalNameAndUsername() {
147 let currentUser = User(
148 id: 42,
149 created: nil,
150 updated: nil,
151 username: "owner",
152 canonicalName: "~owner",
153 email: "owner@example.com",
154 url: nil,
155 location: nil,
156 bio: nil,
157 avatar: nil,
158 pronouns: nil,
159 userType: nil,
160 receivesPaidServices: nil,
161 suspensionNotice: nil
162 )
163
164 #expect(HomeViewModel.matchesCurrentUserAssignee(Entity(canonicalName: "~owner"), currentUser: currentUser))
165 #expect(HomeViewModel.matchesCurrentUserAssignee(Entity(canonicalName: "owner"), currentUser: currentUser))
166 #expect(HomeViewModel.matchesCurrentUserAssignee(Entity(canonicalName: currentUser.username), currentUser: currentUser))
167 #expect(HomeViewModel.matchesCurrentUserAssignee(Entity(canonicalName: "~someone-else"), currentUser: currentUser) == false)
168 }
169
170 @Test
171 func primaryRepositoryReferenceParsesManifestSourceURL() {
172 let manifest = """
173 image: alpine/latest
174 sources:
175 - https://git.sr.ht/~owner/hutch
176 tasks:
177 - true
178 """
179
180 let repository = HomeViewModel.primaryRepositoryReference(in: manifest)
181
182 #expect(repository?.ownerCanonicalName == "~owner")
183 #expect(repository?.name == "hutch")
184 }
185
186 @Test
187 func buildItemsAreSortedForTriage() {
188 let jobs = [
189 makeJob(id: 1, status: .success, created: Date(timeIntervalSince1970: 10)),
190 makeJob(id: 2, status: .running, created: Date(timeIntervalSince1970: 20)),
191 makeJob(id: 3, status: .failed, created: Date(timeIntervalSince1970: 30))
192 ]
193
194 let sorted = HomeViewModel.buildItems(from: jobs)
195
196 #expect(sorted.map(\.job.id) == [3, 2, 1])
197 }
198
199 private func makeJob(id: Int, status: JobStatus, created: Date) -> HomeJobPayload {
200 HomeJobPayload(
201 id: id,
202 created: created,
203 updated: created,
204 status: status,
205 note: nil,
206 tags: [],
207 visibility: nil,
208 image: nil,
209 tasks: [],
210 manifest: nil
211 )
212 }
213}