krz/hutch
an ios client for sourcehut
clone: git clone https://gitbay.org/krz/hutch.git
main: HutchTests/MailingListActivityTests.swift · raw
1import Foundation
2import Testing
3@testable import Hutch
4
5@MainActor
6struct MailingListActivityTests {
7
8 @Test
9 func usesTheNewestArrivalOverTheRootTimestamp() {
10 // The case that started this: sr.ht reports thread.updated seven seconds
11 // after the root email on a thread carrying four replies, so the fallback
12 // must lose to real arrival data.
13 let rootInsert = Date(timeIntervalSince1970: 1_000)
14 let newestReply = Date(timeIntervalSince1970: 5_000)
15 let activity = MailingListActivity(newestByRootEmailID: [42: newestReply])
16
17 #expect(activity.lastActivity(rootEmailID: 42, fallback: rootInsert) == newestReply)
18 }
19
20 @Test
21 func fallsBackForThreadsOutsideTheScannedWindow() {
22 // Threads with nothing new are absent from the feed scan; they keep the
23 // root timestamp, which is older than any cutoff and so reads as read.
24 let rootInsert = Date(timeIntervalSince1970: 1_000)
25 let activity = MailingListActivity(newestByRootEmailID: [:])
26
27 #expect(activity.lastActivity(rootEmailID: 42, fallback: rootInsert) == rootInsert)
28 }
29
30 @Test
31 func neverGoesBackwardsFromTheFallback() {
32 // A root inserted after the newest scanned reply must not age the thread
33 // backwards.
34 let rootInsert = Date(timeIntervalSince1970: 9_000)
35 let staleReply = Date(timeIntervalSince1970: 5_000)
36 let activity = MailingListActivity(newestByRootEmailID: [42: staleReply])
37
38 #expect(activity.lastActivity(rootEmailID: 42, fallback: rootInsert) == rootInsert)
39 }
40
41 @Test
42 func tracksThreadsIndependently() {
43 let activity = MailingListActivity(newestByRootEmailID: [
44 1: Date(timeIntervalSince1970: 5_000),
45 2: Date(timeIntervalSince1970: 7_000)
46 ])
47 let fallback = Date(timeIntervalSince1970: 1_000)
48
49 #expect(activity.lastActivity(rootEmailID: 1, fallback: fallback) == Date(timeIntervalSince1970: 5_000))
50 #expect(activity.lastActivity(rootEmailID: 2, fallback: fallback) == Date(timeIntervalSince1970: 7_000))
51 #expect(activity.lastActivity(rootEmailID: 3, fallback: fallback) == fallback)
52 }
53
54 @Test
55 func newMailInAnOldThreadReadsAsUnread() {
56 let suiteName = "MailingListActivityTests-\(UUID().uuidString)"
57 let defaults = UserDefaults(suiteName: suiteName)!
58 defer { defaults.removePersistentDomain(forName: suiteName) }
59
60 let signIn = Date(timeIntervalSince1970: 5_000)
61 InboxReadStateStore.establishBaselineIfNeeded(now: signIn, defaults: defaults)
62
63 // A thread rooted long before sign-in, with a reply after it. Keyed on
64 // thread.updated this reads as read, which was the bug.
65 let rootInsert = Date(timeIntervalSince1970: 1_000)
66 let replyAfterSignIn = Date(timeIntervalSince1970: 6_000)
67 let activity = MailingListActivity(newestByRootEmailID: [42: replyAfterSignIn])
68 let lastActivityAt = activity.lastActivity(rootEmailID: 42, fallback: rootInsert)
69
70 #expect(
71 InboxReadStateStore.isUnread(
72 threadID: "list#old thread",
73 lastActivityAt: lastActivityAt,
74 defaults: defaults
75 )
76 )
77 }
78}