summaryrefslogtreecommitdiff
path: root/HutchTests/MailingListActivityTests.swift
blob: 46208cb43124ca190a391f415c7e6c28799840c8 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import Foundation
import Testing
@testable import Hutch

struct MailingListActivityTests {

    @Test
    func usesTheNewestArrivalOverTheRootTimestamp() {
        // The case that started this: sr.ht reports thread.updated seven seconds
        // after the root email on a thread carrying four replies, so the fallback
        // must lose to real arrival data.
        let rootInsert = Date(timeIntervalSince1970: 1_000)
        let newestReply = Date(timeIntervalSince1970: 5_000)
        let activity = MailingListActivity(newestByRootEmailID: [42: newestReply])

        #expect(activity.lastActivity(rootEmailID: 42, fallback: rootInsert) == newestReply)
    }

    @Test
    func fallsBackForThreadsOutsideTheScannedWindow() {
        // Threads with nothing new are absent from the feed scan; they keep the
        // root timestamp, which is older than any cutoff and so reads as read.
        let rootInsert = Date(timeIntervalSince1970: 1_000)
        let activity = MailingListActivity(newestByRootEmailID: [:])

        #expect(activity.lastActivity(rootEmailID: 42, fallback: rootInsert) == rootInsert)
    }

    @Test
    func neverGoesBackwardsFromTheFallback() {
        // A root inserted after the newest scanned reply must not age the thread
        // backwards.
        let rootInsert = Date(timeIntervalSince1970: 9_000)
        let staleReply = Date(timeIntervalSince1970: 5_000)
        let activity = MailingListActivity(newestByRootEmailID: [42: staleReply])

        #expect(activity.lastActivity(rootEmailID: 42, fallback: rootInsert) == rootInsert)
    }

    @Test
    func tracksThreadsIndependently() {
        let activity = MailingListActivity(newestByRootEmailID: [
            1: Date(timeIntervalSince1970: 5_000),
            2: Date(timeIntervalSince1970: 7_000)
        ])
        let fallback = Date(timeIntervalSince1970: 1_000)

        #expect(activity.lastActivity(rootEmailID: 1, fallback: fallback) == Date(timeIntervalSince1970: 5_000))
        #expect(activity.lastActivity(rootEmailID: 2, fallback: fallback) == Date(timeIntervalSince1970: 7_000))
        #expect(activity.lastActivity(rootEmailID: 3, fallback: fallback) == fallback)
    }

    @Test
    func newMailInAnOldThreadReadsAsUnread() {
        let suiteName = "MailingListActivityTests-\(UUID().uuidString)"
        let defaults = UserDefaults(suiteName: suiteName)!
        defer { defaults.removePersistentDomain(forName: suiteName) }

        let signIn = Date(timeIntervalSince1970: 5_000)
        InboxReadStateStore.establishBaselineIfNeeded(now: signIn, defaults: defaults)

        // A thread rooted long before sign-in, with a reply after it. Keyed on
        // thread.updated this reads as read, which was the bug.
        let rootInsert = Date(timeIntervalSince1970: 1_000)
        let replyAfterSignIn = Date(timeIntervalSince1970: 6_000)
        let activity = MailingListActivity(newestByRootEmailID: [42: replyAfterSignIn])
        let lastActivityAt = activity.lastActivity(rootEmailID: 42, fallback: rootInsert)

        #expect(
            InboxReadStateStore.isUnread(
                threadID: "list#old thread",
                lastActivityAt: lastActivityAt,
                defaults: defaults
            )
        )
    }
}