summaryrefslogtreecommitdiff
path: root/HutchTests/MailingListActivityTests.swift
diff options
context:
space:
mode:
Diffstat (limited to 'HutchTests/MailingListActivityTests.swift')
-rw-r--r--HutchTests/MailingListActivityTests.swift77
1 files changed, 77 insertions, 0 deletions
diff --git a/HutchTests/MailingListActivityTests.swift b/HutchTests/MailingListActivityTests.swift
new file mode 100644
index 0000000..46208cb
--- /dev/null
+++ b/HutchTests/MailingListActivityTests.swift
@@ -0,0 +1,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
+ )
+ )
+ }
+}