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