krz/octosentry

macOS menu bar app to monitor GitHub security alerts

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

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