krz/octosentry
macOS menu bar app to monitor GitHub security alerts
clone: git clone https://gitbay.org/krz/octosentry.git
1.1.1: octosentryTests/PersistenceStoreTests.swift · raw
1//
2// PersistenceStoreTests.swift
3// octosentryTests
4//
5
6import Foundation
7import Testing
8@testable import octosentry
9
10struct PersistenceStoreTests {
11
12 /// Each test gets its own directory so none of them touch the real
13 /// Application Support container.
14 private func makeTemporaryDirectory() throws -> URL {
15 let directory = URL(fileURLWithPath: NSTemporaryDirectory())
16 .appendingPathComponent("octosentry-tests-\(UUID().uuidString)", isDirectory: true)
17 try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
18 return directory
19 }
20
21 @Test func loadReturnsPlaceholderOnFirstLaunch() async throws {
22 let directory = try makeTemporaryDirectory()
23 defer { try? FileManager.default.removeItem(at: directory) }
24
25 let store = PersistenceStore(directory: directory)
26 let state = await store.load()
27
28 #expect(state.watchedRepos == PersistedState.placeholder.watchedRepos)
29 #expect(await store.loadFailureMessage == nil)
30 }
31
32 @Test func savedStateSurvivesAReload() async throws {
33 let directory = try makeTemporaryDirectory()
34 defer { try? FileManager.default.removeItem(at: directory) }
35
36 let saved = PersistedState(
37 watchedRepos: [WatchedRepo(fullName: "octocat/hello-world", accountID: 42)],
38 seenEventIDs: ["dependabot-octocat/hello-world-1"],
39 lastFetchByRepo: ["octocat/hello-world": Date(timeIntervalSince1970: 1_785_000_000)],
40 minimumSeverity: .high,
41 hasRepoScope: true
42 )
43 await PersistenceStore(directory: directory).save(saved)
44
45 let reloaded = await PersistenceStore(directory: directory).load()
46
47 #expect(reloaded.watchedRepos == saved.watchedRepos)
48 #expect(reloaded.seenEventIDs == saved.seenEventIDs)
49 #expect(reloaded.lastFetchByRepo == saved.lastFetchByRepo)
50 #expect(reloaded.minimumSeverity == saved.minimumSeverity)
51 #expect(reloaded.hasRepoScope == saved.hasRepoScope)
52 }
53
54 // The failure that matters: state.json exists but won't decode. Falling
55 // back to .placeholder is fine, overwriting the user's watch list on the
56 // next save is not.
57 @Test func unreadableStateIsKeptAndReported() async throws {
58 let directory = try makeTemporaryDirectory()
59 defer { try? FileManager.default.removeItem(at: directory) }
60
61 let stateURL = directory.appendingPathComponent("state.json")
62 let original = Data(#"{"watchedRepos":["octocat/hello-world"],"#.utf8)
63 try original.write(to: stateURL)
64
65 let store = PersistenceStore(directory: directory)
66 let state = await store.load()
67 #expect(state.watchedRepos == PersistedState.placeholder.watchedRepos)
68
69 let message = await store.loadFailureMessage
70 #expect(message != nil)
71
72 // The original bytes are still on disk under a different name...
73 let preserved = try FileManager.default
74 .contentsOfDirectory(atPath: directory.path)
75 .filter { $0.hasPrefix("state-unreadable-") }
76 #expect(preserved.count == 1)
77 let preservedName = try #require(preserved.first)
78 #expect(message?.contains(preservedName) == true)
79 #expect(try Data(contentsOf: directory.appendingPathComponent(preservedName)) == original)
80
81 // ...and a later save doesn't clobber the preserved copy.
82 await store.save(state)
83 #expect(try Data(contentsOf: directory.appendingPathComponent(preservedName)) == original)
84 }
85
86 @Test func loadFailureIsClearedByASubsequentGoodLoad() async throws {
87 let directory = try makeTemporaryDirectory()
88 defer { try? FileManager.default.removeItem(at: directory) }
89
90 try Data("not json".utf8).write(to: directory.appendingPathComponent("state.json"))
91
92 let store = PersistenceStore(directory: directory)
93 _ = await store.load()
94 #expect(await store.loadFailureMessage != nil)
95
96 await store.save(PersistedState.placeholder)
97 _ = await store.load()
98 #expect(await store.loadFailureMessage == nil)
99 }
100}