krz/octosentry

macOS menu bar app to monitor GitHub security alerts github macos menubar security

Commit fb42c28038

fb42c28038b71ab57db588cd508c738b8b1a2e68

parent: 89bac7b5f8

Unsigned

cmc <hello@cleberg.net> · 2026-08-22 20:33 UTC
committer: <noreply@github.com>

Add tests for the deterministic core; keep unreadable state.json (#25)

PersistenceStore.load() returned .placeholder whenever decoding failed,
including when state.json existed but was corrupt or written by a newer
version. The next save then wrote the placeholder watch list over the
user's. It now distinguishes a missing file (first launch) from an
unreadable one, moves the unreadable file aside as
state-unreadable-<epoch>.json, and reports the failure through the
feed's existing error list.

Tests cover SeverityMapping, SecurityEventSeverity ordering and raw
values, PersistedState round-trip and legacy decode, PersistenceStore
load/save including the unreadable-file path, and GitHubAPIModels
decoding from recorded response bodies.

Replaces the empty Xcode stub test.

Layout: unified · split

octosentry/PersistenceStore.swift +35 −6
@@ -12,6 +12,11 @@ import Foundation
1212actor PersistenceStore {
1313 private let fileURL: URL
1414
15 /// Set when state.json existed but could not be decoded. The unreadable
16 /// file is moved aside rather than overwritten, so nothing is lost
17 /// silently; the message names where it went.
18 private(set) var loadFailureMessage: String?
19
1520 private static let decoder: JSONDecoder = {
1621 let decoder = JSONDecoder()
1722 decoder.dateDecodingStrategy = .iso8601
@@ -24,23 +29,47 @@ actor PersistenceStore {
2429 return encoder
2530 }()
2631
27 init() {
28 let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
29 let directory = appSupport.appendingPathComponent("octosentry", isDirectory: true)
32 init(directory: URL? = nil) {
33 let directory = directory ?? FileManager.default
34 .urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
35 .appendingPathComponent("octosentry", isDirectory: true)
3036 try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
3137 fileURL = directory.appendingPathComponent("state.json")
3238 }
3339
3440 func load() -> PersistedState {
35 guard let data = try? Data(contentsOf: fileURL),
36 let state = try? Self.decoder.decode(PersistedState.self, from: data) else {
41 // No file at all is first launch, not a failure.
42 guard FileManager.default.fileExists(atPath: fileURL.path) else {
43 return .placeholder
44 }
45
46 do {
47 let data = try Data(contentsOf: fileURL)
48 let state = try Self.decoder.decode(PersistedState.self, from: data)
49 loadFailureMessage = nil
50 return state
51 } catch {
52 // Falling back to .placeholder here means the next save writes a
53 // default watch list over the user's. Preserve the file first.
54 loadFailureMessage = preserveUnreadableState(error: error)
3755 return .placeholder
3856 }
39 return state
4057 }
4158
4259 func save(_ state: PersistedState) {
4360 guard let data = try? Self.encoder.encode(state) else { return }
4461 try? data.write(to: fileURL, options: .atomic)
4562 }
63
64 private func preserveUnreadableState(error: Error) -> String {
65 let backupURL = fileURL
66 .deletingLastPathComponent()
67 .appendingPathComponent("state-unreadable-\(Int(Date().timeIntervalSince1970)).json")
68
69 guard (try? FileManager.default.moveItem(at: fileURL, to: backupURL)) != nil else {
70 return "Saved settings couldn't be read (\(error.localizedDescription))."
71 }
72 return "Saved settings couldn't be read (\(error.localizedDescription)). "
73 + "The previous file was kept as \(backupURL.lastPathComponent); octosentry started from defaults."
74 }
4675}
octosentry/SecurityEventStore.swift +4 −2
@@ -43,18 +43,20 @@ final class SecurityEventStore {
4343 defer { isLoading = false }
4444
4545 var state = await persistenceStore.load()
46 let stateLoadFailure = await persistenceStore.loadFailureMessage
4647 minimumSeverity = state.minimumSeverity
4748 watchedRepos = state.watchedRepos
4849
4950 guard let token = KeychainTokenStore.load() else {
50 errorMessages = [GitHubAPIError.missingToken.errorDescription ?? "Not signed in."]
51 errorMessages = [stateLoadFailure, GitHubAPIError.missingToken.errorDescription ?? "Not signed in."]
52 .compactMap { $0 }
5153 return
5254 }
5355
5456 let client = GitHubSecurityAPIClient(token: token)
5557
5658 var fetchedEvents: [SecurityEvent] = []
57 var errors: [String] = []
59 var errors: [String] = [stateLoadFailure].compactMap { $0 }
5860 var notices: [String] = []
5961
6062 for repoFullName in state.watchedRepos {
octosentryTests/Fixtures.swift added +135
@@ -0,0 +1,135 @@
1//
2// Fixtures.swift
3// octosentryTests
4//
5// Response bodies recorded from the GitHub REST alert endpoints, trimmed
6// to a couple of entries each but otherwise left as the API returns them.
7//
8
9enum Fixtures {
10
11 static let dependabotAlerts = """
12 [
13 {
14 "number": 4,
15 "state": "open",
16 "html_url": "https://github.com/octocat/hello-world/security/dependabot/4",
17 "created_at": "2026-06-21T22:12:22Z",
18 "updated_at": "2026-06-22T13:10:00Z",
19 "dependency": {
20 "package": { "ecosystem": "npm", "name": "some-package" },
21 "manifest_path": "package-lock.json",
22 "scope": "runtime"
23 },
24 "security_advisory": {
25 "ghsa_id": "GHSA-rf4j-j272-fj86",
26 "cve_id": "CVE-2026-11111",
27 "summary": "Denial of service in some-package",
28 "severity": "high"
29 },
30 "security_vulnerability": {
31 "severity": "high",
32 "vulnerable_version_range": "< 1.2.3"
33 }
34 },
35 {
36 "number": 3,
37 "state": "open",
38 "html_url": "https://github.com/octocat/hello-world/security/dependabot/3",
39 "created_at": "2026-06-19T09:01:00Z",
40 "updated_at": "2026-06-19T09:01:00Z",
41 "dependency": {
42 "package": { "ecosystem": "npm", "name": "other-package" },
43 "manifest_path": "package-lock.json",
44 "scope": "development"
45 },
46 "security_advisory": {
47 "ghsa_id": "GHSA-aaaa-bbbb-cccc",
48 "cve_id": null,
49 "summary": "Prototype pollution in other-package",
50 "severity": "moderate"
51 },
52 "security_vulnerability": {
53 "severity": "moderate",
54 "vulnerable_version_range": "< 4.0.0"
55 }
56 }
57 ]
58 """
59
60 static let codeScanningAlerts = """
61 [
62 {
63 "number": 12,
64 "state": "open",
65 "html_url": "https://github.com/octocat/hello-world/security/code-scanning/12",
66 "created_at": "2026-06-20T08:00:00Z",
67 "updated_at": "2026-06-20T08:30:00Z",
68 "rule": {
69 "id": "js/sql-injection",
70 "name": "js/sql-injection",
71 "severity": "error",
72 "security_severity_level": "high",
73 "description": "Query built from user-controlled sources",
74 "tags": ["security", "external/cwe/cwe-089"]
75 },
76 "tool": { "name": "CodeQL", "version": "2.16.0" },
77 "most_recent_instance": {
78 "ref": "refs/heads/main",
79 "state": "open",
80 "message": { "text": "This query depends on a user-provided value." },
81 "location": { "path": "src/db.js", "start_line": 42 }
82 }
83 },
84 {
85 "number": 11,
86 "state": "open",
87 "html_url": "https://github.com/octocat/hello-world/security/code-scanning/11",
88 "created_at": "2026-06-18T11:00:00Z",
89 "updated_at": "2026-06-18T11:00:00Z",
90 "rule": {
91 "id": "js/unused-local-variable",
92 "name": "js/unused-local-variable",
93 "severity": "warning",
94 "security_severity_level": null,
95 "description": "Unused variable",
96 "tags": ["maintainability"]
97 },
98 "tool": { "name": "CodeQL", "version": "2.16.0" }
99 }
100 ]
101 """
102
103 static let secretScanningAlerts = """
104 [
105 {
106 "number": 2,
107 "state": "open",
108 "html_url": "https://github.com/octocat/hello-world/security/secret-scanning/2",
109 "created_at": "2026-06-22T17:45:00Z",
110 "updated_at": "2026-06-22T17:45:00Z",
111 "secret_type": "github_personal_access_token",
112 "secret_type_display_name": "GitHub Personal Access Token",
113 "validity": "active",
114 "push_protection_bypassed": false
115 },
116 {
117 "number": 1,
118 "state": "open",
119 "html_url": "https://github.com/octocat/hello-world/security/secret-scanning/1",
120 "created_at": "2026-06-15T10:00:00Z",
121 "updated_at": "2026-06-15T10:00:00Z",
122 "secret_type": "generic_api_key",
123 "secret_type_display_name": "Generic API Key",
124 "push_protection_bypassed": false
125 }
126 ]
127 """
128
129 static let userRepos = """
130 [
131 { "id": 1296269, "name": "hello-world", "full_name": "octocat/hello-world", "private": false },
132 { "id": 1296270, "name": "spoon-knife", "full_name": "octocat/spoon-knife", "private": true }
133 ]
134 """
135}
octosentryTests/GitHubAPIModelsTests.swift added +103
@@ -0,0 +1,103 @@
1//
2// GitHubAPIModelsTests.swift
3// octosentryTests
4//
5// Decoding against recorded response bodies from the three alert
6// endpoints. Fields octosentry doesn't read are left in the fixtures on
7// purpose — decoding must tolerate them.
8//
9
10import Foundation
11import Testing
12@testable import octosentry
13
14struct GitHubAPIModelsTests {
15
16 private static let decoder: JSONDecoder = {
17 let decoder = JSONDecoder()
18 decoder.dateDecodingStrategy = .iso8601
19 return decoder
20 }()
21
22 private static func iso8601(_ string: String) -> Date {
23 ISO8601DateFormatter().date(from: string)!
24 }
25
26 // MARK: - Dependabot
27
28 @Test func decodesDependabotAlerts() throws {
29 let alerts = try Self.decoder.decode([DependabotAlertDTO].self, from: Data(Fixtures.dependabotAlerts.utf8))
30
31 #expect(alerts.count == 2)
32
33 let first = try #require(alerts.first)
34 #expect(first.number == 4)
35 #expect(first.htmlUrl.absoluteString == "https://github.com/octocat/hello-world/security/dependabot/4")
36 #expect(first.securityAdvisory.summary == "Denial of service in some-package")
37 #expect(first.securityAdvisory.severity == "high")
38 #expect(first.createdAt == Self.iso8601("2026-06-21T22:12:22Z"))
39 #expect(first.updatedAt == Self.iso8601("2026-06-22T13:10:00Z"))
40
41 #expect(alerts[1].securityAdvisory.severity == "moderate")
42 }
43
44 // MARK: - Code scanning
45
46 @Test func decodesCodeScanningAlertWithSecuritySeverity() throws {
47 let alerts = try Self.decoder.decode([CodeScanningAlertDTO].self, from: Data(Fixtures.codeScanningAlerts.utf8))
48
49 let first = try #require(alerts.first)
50 #expect(first.number == 12)
51 #expect(first.rule.id == "js/sql-injection")
52 #expect(first.rule.severity == "error")
53 #expect(first.rule.securitySeverityLevel == "high")
54 #expect(first.mostRecentInstance?.message?.text == "This query depends on a user-provided value.")
55 }
56
57 // A rule with no security_severity_level and no instance message is the
58 // case that drives the summary and severity fallbacks in the client.
59 @Test func decodesCodeScanningAlertWithNullsAndNoInstance() throws {
60 let alerts = try Self.decoder.decode([CodeScanningAlertDTO].self, from: Data(Fixtures.codeScanningAlerts.utf8))
61
62 let second = try #require(alerts.dropFirst().first)
63 #expect(second.rule.securitySeverityLevel == nil)
64 #expect(second.rule.severity == "warning")
65 #expect(second.rule.description == "Unused variable")
66 #expect(second.mostRecentInstance == nil)
67 }
68
69 // MARK: - Secret scanning
70
71 @Test func decodesSecretScanningAlerts() throws {
72 let alerts = try Self.decoder.decode([SecretScanningAlertDTO].self, from: Data(Fixtures.secretScanningAlerts.utf8))
73
74 #expect(alerts.count == 2)
75
76 let first = try #require(alerts.first)
77 #expect(first.number == 2)
78 #expect(first.secretTypeDisplayName == "GitHub Personal Access Token")
79 #expect(first.validity == "active")
80
81 // validity is absent on providers GitHub can't check.
82 #expect(alerts[1].validity == nil)
83 #expect(alerts[1].secretTypeDisplayName == "Generic API Key")
84 }
85
86 // MARK: - Repos
87
88 @Test func decodesAccessibleRepos() throws {
89 let repos = try Self.decoder.decode([GitHubRepoDTO].self, from: Data(Fixtures.userRepos.utf8))
90
91 #expect(repos.map(\.fullName) == ["octocat/hello-world", "octocat/spoon-knife"])
92 }
93
94 // MARK: - Empty responses
95
96 @Test func decodesAnEmptyAlertList() throws {
97 let empty = Data("[]".utf8)
98
99 #expect(try Self.decoder.decode([DependabotAlertDTO].self, from: empty).isEmpty)
100 #expect(try Self.decoder.decode([CodeScanningAlertDTO].self, from: empty).isEmpty)
101 #expect(try Self.decoder.decode([SecretScanningAlertDTO].self, from: empty).isEmpty)
102 }
103}
octosentryTests/PersistedStateTests.swift added +95
@@ -0,0 +1,95 @@
1//
2// PersistedStateTests.swift
3// octosentryTests
4//
5// PersistedState is a file format: state.json written by one version has
6// to load in the next. These pin the current shape.
7//
8
9import Foundation
10import Testing
11@testable import octosentry
12
13struct PersistedStateTests {
14
15 private static let encoder: JSONEncoder = {
16 let encoder = JSONEncoder()
17 encoder.dateEncodingStrategy = .iso8601
18 return encoder
19 }()
20
21 private static let decoder: JSONDecoder = {
22 let decoder = JSONDecoder()
23 decoder.dateDecodingStrategy = .iso8601
24 return decoder
25 }()
26
27 @Test func roundTripsEveryField() throws {
28 let fetchedAt = Date(timeIntervalSince1970: 1_785_000_000)
29 let original = PersistedState(
30 watchedRepos: ["octocat/hello-world", "octocat/spoon-knife"],
31 seenEventIDs: ["dependabot-octocat/hello-world-1", "codeScanning-octocat/spoon-knife-7"],
32 lastFetchByRepo: ["octocat/hello-world": fetchedAt],
33 minimumSeverity: .high,
34 hasRepoScope: true
35 )
36
37 let decoded = try Self.decoder.decode(
38 PersistedState.self,
39 from: Self.encoder.encode(original)
40 )
41
42 #expect(decoded.watchedRepos == original.watchedRepos)
43 #expect(decoded.seenEventIDs == original.seenEventIDs)
44 #expect(decoded.lastFetchByRepo == original.lastFetchByRepo)
45 #expect(decoded.minimumSeverity == original.minimumSeverity)
46 #expect(decoded.hasRepoScope == original.hasRepoScope)
47 }
48
49 @Test func encodesTheKeysOnDiskReadersDependOn() throws {
50 let data = try Self.encoder.encode(PersistedState.placeholder)
51 let object = try #require(
52 try JSONSerialization.jsonObject(with: data) as? [String: Any]
53 )
54
55 #expect(Set(object.keys) == [
56 "watchedRepos", "seenEventIDs", "lastFetchByRepo", "minimumSeverity", "hasRepoScope",
57 ])
58 }
59
60 // A state.json written before hasRepoScope existed must still load.
61 @Test func decodesLegacyStateWithoutRepoScope() throws {
62 let legacy = """
63 {
64 "watchedRepos": ["octocat/hello-world"],
65 "seenEventIDs": ["dependabot-octocat/hello-world-1"],
66 "lastFetchByRepo": {"octocat/hello-world": "2026-08-01T12:00:00Z"},
67 "minimumSeverity": 1
68 }
69 """
70
71 let state = try Self.decoder.decode(PersistedState.self, from: Data(legacy.utf8))
72
73 #expect(state.watchedRepos == ["octocat/hello-world"])
74 #expect(state.seenEventIDs == ["dependabot-octocat/hello-world-1"])
75 #expect(state.minimumSeverity == .medium)
76 #expect(state.hasRepoScope == false)
77 }
78
79 @Test func rejectsStateMissingARequiredField() {
80 let missingWatchList = """
81 {"seenEventIDs": [], "lastFetchByRepo": {}, "minimumSeverity": 0}
82 """
83
84 #expect(throws: (any Error).self) {
85 try Self.decoder.decode(PersistedState.self, from: Data(missingWatchList.utf8))
86 }
87 }
88
89 @Test func placeholderStartsWithNoSeenStateAndNoRepoScope() {
90 #expect(PersistedState.placeholder.seenEventIDs.isEmpty)
91 #expect(PersistedState.placeholder.lastFetchByRepo.isEmpty)
92 #expect(PersistedState.placeholder.minimumSeverity == .low)
93 #expect(PersistedState.placeholder.hasRepoScope == false)
94 }
95}
octosentryTests/PersistenceStoreTests.swift added +100
@@ -0,0 +1,100 @@
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: ["octocat/hello-world"],
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}
octosentryTests/SecurityEventSeverityTests.swift added +53
@@ -0,0 +1,53 @@
1//
2// SecurityEventSeverityTests.swift
3// octosentryTests
4//
5
6import Foundation
7import Testing
8@testable import octosentry
9
10struct SecurityEventSeverityTests {
11
12 @Test func ordersLowToCritical() {
13 #expect(SecurityEventSeverity.low < .medium)
14 #expect(SecurityEventSeverity.medium < .high)
15 #expect(SecurityEventSeverity.high < .critical)
16 #expect(SecurityEventSeverity.critical > .low)
17 }
18
19 @Test func sortsAscendingByRawValue() {
20 let sorted = SecurityEventSeverity.allCases.shuffled().sorted()
21 #expect(sorted == [.low, .medium, .high, .critical])
22 }
23
24 // The minimum-severity filter is a `>=` comparison, so a threshold of
25 // .high must admit exactly high and critical.
26 @Test func thresholdComparisonAdmitsAtOrAboveOnly() {
27 let admitted = SecurityEventSeverity.allCases.filter { $0 >= .high }
28 #expect(admitted == [.high, .critical])
29 }
30
31 @Test func allCasesIsDeclarationOrder() {
32 #expect(SecurityEventSeverity.allCases == [.low, .medium, .high, .critical])
33 }
34
35 // Raw values are written into state.json, so they are a file format.
36 @Test func rawValuesAreStable() {
37 #expect(SecurityEventSeverity.low.rawValue == 0)
38 #expect(SecurityEventSeverity.medium.rawValue == 1)
39 #expect(SecurityEventSeverity.high.rawValue == 2)
40 #expect(SecurityEventSeverity.critical.rawValue == 3)
41 }
42
43 @Test func roundTripsThroughCodable() throws {
44 for severity in SecurityEventSeverity.allCases {
45 let data = try JSONEncoder().encode(severity)
46 #expect(try JSONDecoder().decode(SecurityEventSeverity.self, from: data) == severity)
47 }
48 }
49
50 @Test func hasADisplayNameForEveryCase() {
51 #expect(SecurityEventSeverity.allCases.allSatisfy { !$0.displayName.isEmpty })
52 }
53}
octosentryTests/SeverityMappingTests.swift added +62
@@ -0,0 +1,62 @@
1//
2// SeverityMappingTests.swift
3// octosentryTests
4//
5
6import Testing
7@testable import octosentry
8
9struct SeverityMappingTests {
10
11 @Test func dependabotMapsGitHubVocabulary() {
12 #expect(SeverityMapping.dependabot("critical") == .critical)
13 #expect(SeverityMapping.dependabot("high") == .high)
14 #expect(SeverityMapping.dependabot("moderate") == .medium)
15 #expect(SeverityMapping.dependabot("medium") == .medium)
16 #expect(SeverityMapping.dependabot("low") == .low)
17 }
18
19 @Test func dependabotIsCaseInsensitive() {
20 #expect(SeverityMapping.dependabot("CRITICAL") == .critical)
21 #expect(SeverityMapping.dependabot("Moderate") == .medium)
22 }
23
24 @Test func dependabotFallsBackToMediumOnUnknownSeverity() {
25 #expect(SeverityMapping.dependabot("") == .medium)
26 #expect(SeverityMapping.dependabot("catastrophic") == .medium)
27 }
28
29 @Test func codeScanningPrefersSecuritySeverityLevel() {
30 // security_severity_level wins even when rule.severity disagrees.
31 #expect(SeverityMapping.codeScanning(securitySeverityLevel: "critical", ruleSeverity: "note") == .critical)
32 #expect(SeverityMapping.codeScanning(securitySeverityLevel: "high", ruleSeverity: "note") == .high)
33 #expect(SeverityMapping.codeScanning(securitySeverityLevel: "medium", ruleSeverity: "error") == .medium)
34 #expect(SeverityMapping.codeScanning(securitySeverityLevel: "low", ruleSeverity: "error") == .low)
35 }
36
37 @Test func codeScanningFallsBackToRuleSeverity() {
38 #expect(SeverityMapping.codeScanning(securitySeverityLevel: nil, ruleSeverity: "error") == .high)
39 #expect(SeverityMapping.codeScanning(securitySeverityLevel: nil, ruleSeverity: "warning") == .medium)
40 #expect(SeverityMapping.codeScanning(securitySeverityLevel: nil, ruleSeverity: "note") == .low)
41 }
42
43 @Test func codeScanningFallsBackToRuleSeverityWhenLevelIsUnrecognized() {
44 #expect(SeverityMapping.codeScanning(securitySeverityLevel: "unknown", ruleSeverity: "error") == .high)
45 }
46
47 @Test func codeScanningDefaultsToMediumWithNothingUsable() {
48 #expect(SeverityMapping.codeScanning(securitySeverityLevel: nil, ruleSeverity: nil) == .medium)
49 #expect(SeverityMapping.codeScanning(securitySeverityLevel: "unknown", ruleSeverity: "unknown") == .medium)
50 }
51
52 @Test func secretScanningTreatsActiveSecretsAsCritical() {
53 #expect(SeverityMapping.secretScanning(validity: "active") == .critical)
54 #expect(SeverityMapping.secretScanning(validity: "ACTIVE") == .critical)
55 }
56
57 @Test func secretScanningTreatsEverythingElseAsHigh() {
58 #expect(SeverityMapping.secretScanning(validity: nil) == .high)
59 #expect(SeverityMapping.secretScanning(validity: "inactive") == .high)
60 #expect(SeverityMapping.secretScanning(validity: "unknown") == .high)
61 }
62}
octosentryTests/octosentryTests.swift deleted −19
@@ -1,19 +0,0 @@
1//
2// octosentryTests.swift
3// octosentryTests
4//
5// Created by cmc on 2026-07-17.
6//
7
8import Testing
9@testable import octosentry
10
11struct octosentryTests {
12
13 @Test func example() async throws {
14 // Write your test here and use APIs like `#expect(...)` to check expected conditions.
15 // Swift Testing Documentation
16 // https://developer.apple.com/documentation/testing
17 }
18
19}