krz/hutch

an ios client for sourcehut

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

v2.4.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}