krz/octosentry

macOS menu bar app to monitor GitHub security alerts

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

main: 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}