krz/octosentry

macOS menu bar app to monitor GitHub security alerts

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

1.1: octosentryTests/AlertTriageTests.swift · raw

  1//
  2//  AlertTriageTests.swift
  3//  octosentryTests
  4//
  5
  6import Foundation
  7import Testing
  8@testable import octosentry
  9
 10struct AlertTriageTests {
 11
 12    private let now = Date(timeIntervalSince1970: 1_785_000_000)
 13    private var later: Date { now.addingTimeInterval(3600) }
 14    private var earlier: Date { now.addingTimeInterval(-3600) }
 15
 16    // MARK: - Dismiss
 17
 18    @Test func dismissHidesAnAlert() {
 19        var triage = AlertTriage()
 20        triage.dismiss("a")
 21
 22        #expect(triage.isHidden("a", now: now))
 23        #expect(triage.isDismissed("a"))
 24        #expect(!triage.isHidden("b", now: now))
 25    }
 26
 27    @Test func dismissedAlertsStayHiddenIndefinitely() {
 28        var triage = AlertTriage()
 29        triage.dismiss("a")
 30
 31        #expect(triage.isHidden("a", now: now.addingTimeInterval(60 * 60 * 24 * 365)))
 32    }
 33
 34    // MARK: - Snooze
 35
 36    @Test func snoozeHidesUntilItsDeadline() {
 37        var triage = AlertTriage()
 38        triage.snooze("a", until: later)
 39
 40        #expect(triage.isHidden("a", now: now))
 41        #expect(triage.snoozedUntil("a", now: now) == later)
 42    }
 43
 44    // The resurfacing behaviour: a later poll sees the deadline has passed.
 45    @Test func snoozeStopsHidingOnceItsDeadlinePasses() {
 46        var triage = AlertTriage()
 47        triage.snooze("a", until: later)
 48
 49        #expect(!triage.isHidden("a", now: later.addingTimeInterval(1)))
 50        #expect(triage.snoozedUntil("a", now: later.addingTimeInterval(1)) == nil)
 51    }
 52
 53    @Test func snoozeExactlyAtItsDeadlineIsNoLongerHidden() {
 54        var triage = AlertTriage()
 55        triage.snooze("a", until: later)
 56
 57        #expect(!triage.isHidden("a", now: later))
 58    }
 59
 60    // MARK: - The two are exclusive
 61
 62    @Test func snoozingADismissedAlertReplacesTheDismissal() {
 63        var triage = AlertTriage()
 64        triage.dismiss("a")
 65        triage.snooze("a", until: later)
 66
 67        #expect(!triage.isDismissed("a"))
 68        #expect(triage.isHidden("a", now: now))
 69        #expect(!triage.isHidden("a", now: later))
 70    }
 71
 72    @Test func dismissingASnoozedAlertReplacesTheSnooze() {
 73        var triage = AlertTriage()
 74        triage.snooze("a", until: later)
 75        triage.dismiss("a")
 76
 77        #expect(triage.isDismissed("a"))
 78        #expect(triage.snoozedUntil("a", now: now) == nil)
 79        #expect(triage.isHidden("a", now: later.addingTimeInterval(1)))
 80    }
 81
 82    @Test func restoreClearsBothStates() {
 83        var triage = AlertTriage()
 84        triage.dismiss("a")
 85        triage.snooze("b", until: later)
 86
 87        triage.restore("a")
 88        triage.restore("b")
 89
 90        #expect(!triage.isHidden("a", now: now))
 91        #expect(!triage.isHidden("b", now: now))
 92    }
 93
 94    // MARK: - Pruning
 95
 96    // The case the issue calls out: an alert resolved on GitHub must not leave
 97    // local state behind forever.
 98    @Test func pruningDropsStateForAlertsGoneUpstream() {
 99        var triage = AlertTriage()
100        triage.dismiss("resolved")
101        triage.dismiss("still-open")
102        triage.snooze("also-resolved", until: later)
103
104        let pruned = triage.pruned(presentEventIDs: ["still-open"], now: now)
105
106        #expect(pruned.dismissedEventIDs == ["still-open"])
107        #expect(pruned.snoozedUntilByEventID.isEmpty)
108    }
109
110    @Test func pruningDropsElapsedSnoozes() {
111        var triage = AlertTriage()
112        triage.snooze("expired", until: earlier)
113        triage.snooze("active", until: later)
114
115        let pruned = triage.pruned(presentEventIDs: ["expired", "active"], now: now)
116
117        #expect(Set(pruned.snoozedUntilByEventID.keys) == ["active"])
118    }
119
120    @Test func pruningKeepsStateForAlertsStillPresent() {
121        var triage = AlertTriage()
122        triage.dismiss("a")
123        triage.snooze("b", until: later)
124
125        let pruned = triage.pruned(presentEventIDs: ["a", "b"], now: now)
126
127        #expect(pruned == triage)
128    }
129
130    @Test func pruningAnEmptyTriageIsEmpty() {
131        let pruned = AlertTriage().pruned(presentEventIDs: ["a"], now: now)
132
133        #expect(pruned == AlertTriage())
134    }
135
136    // MARK: - Persistence
137
138    @Test func roundTripsThroughCodable() throws {
139        var triage = AlertTriage()
140        triage.dismiss("a")
141        triage.snooze("b", until: later)
142
143        let encoder = JSONEncoder()
144        encoder.dateEncodingStrategy = .iso8601
145        let decoder = JSONDecoder()
146        decoder.dateDecodingStrategy = .iso8601
147
148        let decoded = try decoder.decode(AlertTriage.self, from: try encoder.encode(triage))
149
150        #expect(decoded == triage)
151    }
152}
153
154struct SnoozeDurationTests {
155
156    private let calendar: Calendar = {
157        var calendar = Calendar(identifier: .gregorian)
158        calendar.timeZone = TimeZone(identifier: "UTC")!
159        return calendar
160    }()
161
162    // 2026-07-25T17:20:00Z
163    private let now = Date(timeIntervalSince1970: 1_785_000_000)
164
165    @Test func anHourIsAnHourLater() {
166        #expect(SnoozeDuration.anHour.date(from: now, calendar: calendar) == now.addingTimeInterval(3600))
167    }
168
169    // Snoozing late at night shouldn't resurface the alert minutes later at
170    // midnight, so "tomorrow" is the start of the next day.
171    @Test func tomorrowIsTheStartOfTheNextDay() {
172        let date = SnoozeDuration.tomorrow.date(from: now, calendar: calendar)
173        let components = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: date)
174
175        #expect(components.day == 26)
176        #expect(components.hour == 0)
177        #expect(components.minute == 0)
178        #expect(date > now)
179    }
180
181    @Test func nextWeekIsTheStartOfTheDaySevenDaysOn() {
182        let date = SnoozeDuration.nextWeek.date(from: now, calendar: calendar)
183        let components = calendar.dateComponents([.month, .day, .hour], from: date)
184
185        #expect(components.month == 8)
186        #expect(components.day == 1)
187        #expect(components.hour == 0)
188    }
189
190    @Test func everyDurationMovesForward() {
191        for duration in SnoozeDuration.allCases {
192            #expect(duration.date(from: now, calendar: calendar) > now)
193            #expect(!duration.displayName.isEmpty)
194        }
195    }
196}