krz/octosentry
macOS menu bar app to monitor GitHub security alerts
clone: git clone https://gitbay.org/krz/octosentry.git
main: 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}