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}