krz/octosentry
macOS menu bar app to monitor GitHub security alerts
clone: git clone https://gitbay.org/krz/octosentry.git
1.1: octosentryTests/AccountTests.swift · raw
1//
2// AccountTests.swift
3// octosentryTests
4//
5
6import Foundation
7import Testing
8@testable import octosentry
9
10struct AccountTests {
11
12 // An install that already has a token must keep using the Keychain item
13 // it's in — rewriting it at upgrade time risks stranding the token.
14 @Test func theLegacyAccountPointsAtTheExistingKeychainItem() {
15 let account = Account.legacy()
16
17 #expect(account.keychainAccount == KeychainTokenStore.legacyAccount)
18 #expect(account.id == 0)
19 #expect(account.login.isEmpty)
20 }
21
22 @Test func newAccountsGetTheirOwnKeychainItem() {
23 let first = Account.new(id: 1, login: "octocat", hasRepoScope: false)
24 let second = Account.new(id: 2, login: "hubot", hasRepoScope: true)
25
26 #expect(first.keychainAccount != second.keychainAccount)
27 #expect(first.keychainAccount != KeychainTokenStore.legacyAccount)
28 #expect(second.hasRepoScope)
29 }
30
31 @Test func displayNameFallsBackBeforeTheLoginIsKnown() {
32 #expect(Account.legacy().displayName == "GitHub account")
33 #expect(Account.new(id: 1, login: "octocat", hasRepoScope: false).displayName == "octocat")
34 }
35
36 @Test func roundTripsThroughCodable() throws {
37 let account = Account.new(id: 7, login: "octocat", hasRepoScope: true)
38 let decoded = try JSONDecoder().decode(Account.self, from: try JSONEncoder().encode(account))
39
40 #expect(decoded == account)
41 }
42
43 @Test func watchedRepoRoundTripsThroughCodable() throws {
44 let repo = WatchedRepo(fullName: "octocat/hello-world", accountID: 7)
45 let decoded = try JSONDecoder().decode(WatchedRepo.self, from: try JSONEncoder().encode(repo))
46
47 #expect(decoded == repo)
48 }
49}
50
51struct MultiAccountFeedTests {
52
53 private func event(_ id: String, repo: String = "octocat/hello-world", logins: [String]) -> SecurityEvent {
54 var event = TestEvents.event(id: id, repo: repo)
55 event.accountLogins = logins
56 return event
57 }
58
59 // MARK: - Merging
60
61 // The same alert reached through two identities is one alert.
62 @Test func duplicateAlertsCollapseAndUnionTheirAttributions() {
63 let merged = SecurityEventStore.merged([
64 event("a", logins: ["octocat"]),
65 event("a", logins: ["hubot"]),
66 ])
67
68 #expect(merged.count == 1)
69 #expect(merged[0].accountLogins.sorted() == ["hubot", "octocat"])
70 }
71
72 @Test func mergingLeavesDistinctAlertsAlone() {
73 let merged = SecurityEventStore.merged([
74 event("a", logins: ["octocat"]),
75 event("b", logins: ["octocat"]),
76 ])
77
78 #expect(merged.map(\.id) == ["a", "b"])
79 }
80
81 @Test func mergingPreservesInputOrder() {
82 let merged = SecurityEventStore.merged([
83 event("b", logins: ["octocat"]),
84 event("a", logins: ["hubot"]),
85 event("b", logins: ["hubot"]),
86 ])
87
88 #expect(merged.map(\.id) == ["b", "a"])
89 }
90
91 @Test func mergingDoesNotRepeatTheSameLogin() {
92 let merged = SecurityEventStore.merged([
93 event("a", logins: ["octocat"]),
94 event("a", logins: ["octocat"]),
95 ])
96
97 #expect(merged[0].accountLogins == ["octocat"])
98 }
99
100 @Test func mergingAnEmptyFeedIsEmpty() {
101 #expect(SecurityEventStore.merged([]).isEmpty)
102 }
103
104 // MARK: - Attribution
105
106 // Attribution is noise unless a repo is genuinely reachable two ways.
107 @Test func onlyReposWatchedUnderSeveralAccountsAreAttributed() {
108 let watched = [
109 WatchedRepo(fullName: "octocat/shared", accountID: 1),
110 WatchedRepo(fullName: "octocat/shared", accountID: 2),
111 WatchedRepo(fullName: "octocat/solo", accountID: 1),
112 ]
113
114 let attributed = SecurityEventStore.reposWatchedBySeveralAccounts(in: watched)
115
116 #expect(attributed == ["octocat/shared"])
117 }
118
119 @Test func aRepoListedTwiceUnderOneAccountIsNotAttributed() {
120 let watched = [
121 WatchedRepo(fullName: "octocat/solo", accountID: 1),
122 WatchedRepo(fullName: "octocat/solo", accountID: 1),
123 ]
124
125 #expect(SecurityEventStore.reposWatchedBySeveralAccounts(in: watched).isEmpty)
126 }
127
128 @Test func anEmptyWatchListAttributesNothing() {
129 #expect(SecurityEventStore.reposWatchedBySeveralAccounts(in: []).isEmpty)
130 }
131}