krz/hutch
an ios client for sourcehut
clone: git clone https://gitbay.org/krz/hutch.git
v2.6.1: HutchTests/InboxViewModelTests.swift · raw
1import Foundation
2import Testing
3@testable import Hutch
4
5struct InboxViewModelTests {
6
7 @Test
8 func derivesRepositoryNameFromCommonPatchListSuffixes() {
9 #expect(InboxViewModel.deriveRepositoryName(from: "hut-devel") == "hut")
10 #expect(InboxViewModel.deriveRepositoryName(from: "git.patches") == "git")
11 #expect(InboxViewModel.deriveRepositoryName(from: "discuss") == nil)
12 }
13
14 @Test
15 func parsesMailtoDraft() {
16 let draft = ThreadViewModel.mailComposeDraft(
17 from: "mailto:list@example.com?cc=author@example.com&subject=Re:%20PATCH&body=LGTM"
18 )
19
20 #expect(draft?.recipients == ["list@example.com"])
21 #expect(draft?.ccRecipients == ["author@example.com"])
22 #expect(draft?.subject == "Re: PATCH")
23 #expect(draft?.body == "LGTM")
24 }
25
26 @Test
27 func computesLocalUnreadStateFromLastViewedMarker() {
28 let suiteName = "InboxViewModelTests-\(UUID().uuidString)"
29 let defaults = UserDefaults(suiteName: suiteName)!
30 defer { defaults.removePersistentDomain(forName: suiteName) }
31
32 let threadID = "list#message"
33 let lastActivity = Date(timeIntervalSince1970: 2_000)
34
35 #expect(InboxReadStateStore.isUnread(threadID: threadID, lastActivityAt: lastActivity, defaults: defaults))
36
37 InboxReadStateStore.markViewed(Date(timeIntervalSince1970: 1_000), for: threadID, defaults: defaults)
38 #expect(InboxReadStateStore.isUnread(threadID: threadID, lastActivityAt: lastActivity, defaults: defaults))
39
40 InboxReadStateStore.markViewed(Date(timeIntervalSince1970: 2_500), for: threadID, defaults: defaults)
41 #expect(!InboxReadStateStore.isUnread(threadID: threadID, lastActivityAt: lastActivity, defaults: defaults))
42 }
43
44 @Test
45 func normalizesThreadSubjectsForDisplay() {
46 let summary = InboxThreadSummary(
47 rootEmailID: 1,
48 rootMessageID: "message",
49 threadRootEmailIDs: [1],
50 threadRootMessageIDs: ["message"],
51 listID: 2,
52 listRID: "list",
53 listName: "hut-devel",
54 listOwner: Entity(canonicalName: "~owner"),
55 subject: "Re: Fwd: [PATCH] test: add parser ",
56 latestSender: Entity(canonicalName: "~sender"),
57 lastActivityAt: Date(timeIntervalSince1970: 2_000),
58 messageCount: 3,
59 repo: "hut",
60 containsPatch: true,
61 isUnread: true
62 )
63
64 #expect(summary.displaySubject == "[PATCH] test: add parser")
65 #expect(summary.metadataLine.contains("~sender"))
66 #expect(summary.metadataLine.contains("2 replies"))
67 }
68
69 @Test
70 func keepsDistinctThreadsDistinctByRootMessageID() {
71 let baseList = InboxMailingListReference(
72 id: 1,
73 rid: "list",
74 name: "hut-devel",
75 owner: Entity(canonicalName: "~owner")
76 )
77
78 let first = InboxThreadSummary(
79 rootEmailID: 10,
80 rootMessageID: "message-1",
81 threadRootEmailIDs: [10],
82 threadRootMessageIDs: ["message-1"],
83 listID: baseList.id,
84 listRID: baseList.rid,
85 listName: baseList.name,
86 listOwner: baseList.owner,
87 subject: "[PATCH] test",
88 latestSender: Entity(canonicalName: "~a"),
89 lastActivityAt: Date(timeIntervalSince1970: 100),
90 messageCount: 1,
91 repo: "hut",
92 containsPatch: true,
93 isUnread: true
94 )
95 let second = InboxThreadSummary(
96 rootEmailID: 11,
97 rootMessageID: "message-2",
98 threadRootEmailIDs: [11],
99 threadRootMessageIDs: ["message-2"],
100 listID: baseList.id,
101 listRID: baseList.rid,
102 listName: baseList.name,
103 listOwner: baseList.owner,
104 subject: "[PATCH] test",
105 latestSender: Entity(canonicalName: "~b"),
106 lastActivityAt: Date(timeIntervalSince1970: 200),
107 messageCount: 2,
108 repo: "hut",
109 containsPatch: true,
110 isUnread: true
111 )
112
113 #expect(first.id != second.id)
114 }
115
116 @Test
117 func mailingListThreadFilterMatchesSubjectAndSender() {
118 let baseList = InboxMailingListReference(
119 id: 1,
120 rid: "list",
121 name: "hutch-devel",
122 owner: Entity(canonicalName: "~owner")
123 )
124 let threads = [
125 InboxThreadSummary(
126 rootEmailID: 10,
127 rootMessageID: "message-1",
128 threadRootEmailIDs: [10],
129 threadRootMessageIDs: ["message-1"],
130 listID: baseList.id,
131 listRID: baseList.rid,
132 listName: baseList.name,
133 listOwner: baseList.owner,
134 subject: "Re: [PATCH] add search",
135 latestSender: Entity(canonicalName: "~alice"),
136 lastActivityAt: Date(timeIntervalSince1970: 100),
137 messageCount: 1,
138 repo: "hutch",
139 containsPatch: true,
140 isUnread: true
141 ),
142 InboxThreadSummary(
143 rootEmailID: 11,
144 rootMessageID: "message-2",
145 threadRootEmailIDs: [11],
146 threadRootMessageIDs: ["message-2"],
147 listID: baseList.id,
148 listRID: baseList.rid,
149 listName: baseList.name,
150 listOwner: baseList.owner,
151 subject: "Release planning",
152 latestSender: Entity(canonicalName: "~bob"),
153 lastActivityAt: Date(timeIntervalSince1970: 200),
154 messageCount: 2,
155 repo: "hutch",
156 containsPatch: false,
157 isUnread: true
158 )
159 ]
160
161 let subjectMatches = MailingListDetailViewModel.filterThreads(threads, matching: "search")
162 let senderMatches = MailingListDetailViewModel.filterThreads(threads, matching: "~bob")
163
164 #expect(subjectMatches.map(\.rootEmailID) == [10])
165 #expect(senderMatches.map(\.rootEmailID) == [11])
166 }
167
168 @Test
169 func segmentsPatchBodyAndTreatsSignatureAsPlainText() {
170 let body = """
171 From: Christian Cleberg <hello@cleberg.net>
172
173 ---
174 test-patch.txt | 1 +
175 1 file changed, 1 insertion(+)
176 create mode 100644 test-patch.txt
177
178 diff --git a/test-patch.txt b/test-patch.txt
179 new file mode 100644
180 index 0000000..c7b6eed
181 --- /dev/null
182 +++ b/test-patch.txt
183 @@ -0,0 +1 @@
184 +test Wed Mar 18 23:19:03 CDT 2026
185 --
186 2.50.1 (Apple Git-155)
187 """
188
189 let segments = ThreadViewModel.segmentMessageBodyForTesting(body, isPatch: true)
190
191 #expect(segments.count == 3)
192
193 guard case let .plainText(leadingPlainText) = segments[0] else {
194 Issue.record("Expected first segment to be plain text")
195 return
196 }
197 #expect(leadingPlainText.contains("From: Christian Cleberg <hello@cleberg.net>"))
198 #expect(leadingPlainText.contains("---"))
199 #expect(leadingPlainText.contains(" test-patch.txt | 1 +"))
200 #expect(leadingPlainText.contains(" 1 file changed, 1 insertion(+)"))
201 #expect(leadingPlainText.contains(" create mode 100644 test-patch.txt"))
202
203 guard case let .diff(diff) = segments[1] else {
204 Issue.record("Expected second segment to be diff")
205 return
206 }
207 #expect(diff.contains("diff --git a/test-patch.txt b/test-patch.txt"))
208 #expect(diff.contains("--- /dev/null"))
209 #expect(diff.contains("+++ b/test-patch.txt"))
210 #expect(diff.contains("+test Wed Mar 18 23:19:03 CDT 2026"))
211 #expect(!diff.contains("-- \n2.50.1 (Apple Git-155)"))
212
213 guard case let .plainText(trailingPlainText) = segments[2] else {
214 Issue.record("Expected third segment to be plain text")
215 return
216 }
217 #expect(trailingPlainText.contains("--"))
218 #expect(trailingPlainText.contains("2.50.1 (Apple Git-155)"))
219 }
220}