krz/hutch

an ios client for sourcehut

clone: git clone https://gitbay.org/krz/hutch.git

v3.0.0: 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 inboxFilterSeparatesPatchThreadsFromDiscussionThreads() {
170        let baseList = InboxMailingListReference(
171            id: 1,
172            rid: "list",
173            name: "hutch-devel",
174            owner: Entity(canonicalName: "~owner")
175        )
176        let patchThread = InboxThreadSummary(
177            rootEmailID: 10,
178            rootMessageID: "message-1",
179            threadRootEmailIDs: [10],
180            threadRootMessageIDs: ["message-1"],
181            listID: baseList.id,
182            listRID: baseList.rid,
183            listName: baseList.name,
184            listOwner: baseList.owner,
185            subject: "[PATCH] add search",
186            latestSender: Entity(canonicalName: "~alice"),
187            lastActivityAt: Date(timeIntervalSince1970: 100),
188            messageCount: 1,
189            repo: "hutch",
190            containsPatch: true,
191            isUnread: true
192        )
193        let discussionThread = InboxThreadSummary(
194            rootEmailID: 11,
195            rootMessageID: "message-2",
196            threadRootEmailIDs: [11],
197            threadRootMessageIDs: ["message-2"],
198            listID: baseList.id,
199            listRID: baseList.rid,
200            listName: baseList.name,
201            listOwner: baseList.owner,
202            subject: "release planning",
203            latestSender: Entity(canonicalName: "~bob"),
204            lastActivityAt: Date(timeIntervalSince1970: 200),
205            messageCount: 2,
206            repo: "hutch",
207            containsPatch: false,
208            isUnread: true
209        )
210
211        let threads = [patchThread, discussionThread]
212
213        #expect(InboxViewModel.filterThreads(threads, filter: .patches).map(\.rootEmailID) == [10])
214        #expect(InboxViewModel.filterThreads(threads, filter: .discussions).map(\.rootEmailID) == [11])
215    }
216
217    @Test
218    func segmentsPatchBodyAndTreatsSignatureAsPlainText() {
219        let body = """
220        From: Christian Cleberg <hello@cleberg.net>
221
222        ---
223         test-patch.txt | 1 +
224         1 file changed, 1 insertion(+)
225         create mode 100644 test-patch.txt
226
227        diff --git a/test-patch.txt b/test-patch.txt
228        new file mode 100644
229        index 0000000..c7b6eed
230        --- /dev/null
231        +++ b/test-patch.txt
232        @@ -0,0 +1 @@
233        +test Wed Mar 18 23:19:03 CDT 2026
234        -- 
235        2.50.1 (Apple Git-155)
236        """
237
238        let segments = ThreadViewModel.segmentMessageBodyForTesting(body, isPatch: true)
239
240        #expect(segments.count == 3)
241
242        guard case let .plainText(leadingPlainText) = segments[0] else {
243            Issue.record("Expected first segment to be plain text")
244            return
245        }
246        #expect(leadingPlainText.contains("From: Christian Cleberg <hello@cleberg.net>"))
247        #expect(leadingPlainText.contains("---"))
248        #expect(leadingPlainText.contains(" test-patch.txt | 1 +"))
249        #expect(leadingPlainText.contains(" 1 file changed, 1 insertion(+)"))
250        #expect(leadingPlainText.contains(" create mode 100644 test-patch.txt"))
251
252        guard case let .diff(diff) = segments[1] else {
253            Issue.record("Expected second segment to be diff")
254            return
255        }
256        #expect(diff.contains("diff --git a/test-patch.txt b/test-patch.txt"))
257        #expect(diff.contains("--- /dev/null"))
258        #expect(diff.contains("+++ b/test-patch.txt"))
259        #expect(diff.contains("+test Wed Mar 18 23:19:03 CDT 2026"))
260        #expect(!diff.contains("-- \n2.50.1 (Apple Git-155)"))
261
262        guard case let .plainText(trailingPlainText) = segments[2] else {
263            Issue.record("Expected third segment to be plain text")
264            return
265        }
266        #expect(trailingPlainText.contains("--"))
267        #expect(trailingPlainText.contains("2.50.1 (Apple Git-155)"))
268    }
269}