Fixing up tests

This commit is contained in:
Max Goedjen
2026-09-14 20:49:44 -07:00
parent c9ddb592b3
commit 0646fc4460
7 changed files with 41 additions and 120 deletions
@@ -8,7 +8,7 @@ public final class AuthenticationContext: AuthenticationContextProtocol {
/// The Secret to persist authentication for.
public let secret: AnySecret
/// The LAContext used to authorize the persistent context.
public let laContext: LAContext
public let laContext: LAContext?
enum Validity {
/// - Note - Monotonic time instead of Date() to prevent people setting the clock back.
@@ -55,12 +55,21 @@ public final class AuthenticationContext: AuthenticationContextProtocol {
}
}
public func evaluate() async throws -> Bool {
guard let laContext else { return false }
return try await laContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: laContext.localizedReason)
}
public func cancel() async {
laContext?.invalidate()
}
}
@MainActor public protocol AuthenticationHandlerProtocol: Observable {
var batchableRequests: [[SignatureRequest]] { get }
func setBatchAuthHandler(_ handler: @escaping () async throws -> Void)
func waitForAuthentication(for request: SignatureRequest) async throws -> any AuthenticationContextProtocol
var batchableRequests: [[SignatureRequest]] { get }
func persistAuthentication<SecretType: Secret>(secret: SecretType, forDuration duration: TimeInterval) async throws
func requestAuthentication(for requests: Set<SignatureRequest>) async throws
}
@@ -70,7 +79,7 @@ public final class AuthenticationContext: AuthenticationContextProtocol {
private var persistedContexts: [AnySecret: AuthenticationContext] = [:]
private var holdingRequests: Set<SignatureRequest> = []
private var activeTask: Task<Bool, any Error>?
private var activeContext: LAContext?
private var activeContext: (any AuthenticationContextProtocol)?
private var lastBatchAuthPresentation: Set<SignatureRequest>?
private var presentBatchAuth: (() async throws -> Void)?
@@ -102,7 +111,7 @@ public final class AuthenticationContext: AuthenticationContextProtocol {
lastBatchAuthPresentation = holdingRequests
logger.log("Requesting batch auth presentation")
try await presentBatchAuth?()
activeContext?.invalidate()
await activeContext?.cancel()
logger.log("Requested batch auth presentation")
}
if let preauthorized = existingAuthenticationContext(for: request) {
@@ -117,11 +126,11 @@ public final class AuthenticationContext: AuthenticationContextProtocol {
laContext.localizedReason = String(localized: .authContextRequestSignatureDescription(appName: request.provenance.origin.displayName, secretName: request.secret.name))
laContext.localizedCancelTitle = String(localized: .authContextRequestDenyButton)
let context = AuthenticationContext(secret: request.secret, context: laContext, requestID: request.id)
activeContext = laContext
activeContext = context
activeTask = Task {
logger.log("Beginning individual auth prompt")
let result = (try? await laContext.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: laContext.localizedReason)) ?? false
let result = (try? await context.evaluate()) ?? false
logger.log("Ended individual auth prompt")
return result
}
@@ -3,14 +3,11 @@ import LocalAuthentication
/// Protocol describing an authentication context. This is an authorization that can be reused for multiple access to a secret that requires authentication for a specific period of time.
public protocol AuthenticationContextProtocol: Sendable, Identifiable {
/// Whether the context remains valid.
var secret: AnySecret { get }
var laContext: LAContext { get }
func valid(for request: SignatureRequest) -> Bool
var laContext: LAContext? { get }
func evaluate() async throws -> Bool
func cancel() async
}
public struct SignatureRequest: Identifiable, Hashable, Sendable, Comparable {
@@ -11,7 +11,7 @@ import CertificateKit
// MARK: Identity Listing
@Test func emptyStores() async throws {
let agent = Agent(storeList: SecretStoreList(), certificateStore: CertificateStore())
let agent = Agent(storeList: SecretStoreList(), certificateStore: CertificateStore(), authenticationHandler: AuthenticationHandler())
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestIdentities)
let response = await agent.handle(request: request, provenance: .test, hosts: nil)
#expect(response == Constants.Responses.requestIdentitiesEmpty)
@@ -19,7 +19,7 @@ import CertificateKit
@Test func identitiesList() async throws {
let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
let agent = Agent(storeList: list, certificateStore: CertificateStore())
let agent = Agent(storeList: list, certificateStore: CertificateStore(), authenticationHandler: AuthenticationHandler())
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestIdentities)
let response = await agent.handle(request: request, provenance: .test, hosts: nil)
@@ -33,7 +33,7 @@ import CertificateKit
@Test func noMatchingIdentities() async throws {
let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
let agent = Agent(storeList: list, certificateStore: CertificateStore())
let agent = Agent(storeList: list, certificateStore: CertificateStore(), authenticationHandler: AuthenticationHandler())
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignatureWithNoneMatching)
let response = await agent.handle(request: request, provenance: .test, hosts: nil)
#expect(response == Constants.Responses.requestFailure)
@@ -43,7 +43,7 @@ import CertificateKit
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature)
guard case SSHAgent.Request.signRequest(let context) = request else { return }
let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
let agent = Agent(storeList: list, certificateStore: CertificateStore())
let agent = Agent(storeList: list, certificateStore: CertificateStore(), authenticationHandler: AuthenticationHandler())
let response = await agent.handle(request: request, provenance: .test, hosts: nil)
let responseReader = OpenSSHReader(data: response)
let length = try responseReader.readNextBytes(as: UInt32.self)
@@ -78,7 +78,7 @@ import CertificateKit
let witness = StubWitness(speakNow: { _,_ in
return true
}, witness: { _, _ in })
let agent = Agent(storeList: list, certificateStore: CertificateStore(), witness: witness)
let agent = Agent(storeList: list, certificateStore: CertificateStore(), authenticationHandler: AuthenticationHandler(), witness: witness)
let response = await agent.handle(request: .signRequest(.empty), provenance: .test, hosts: nil)
#expect(response == Constants.Responses.requestFailure)
}
@@ -91,7 +91,7 @@ import CertificateKit
}, witness: { _, trace in
witnessed = true
})
let agent = Agent(storeList: list, certificateStore: CertificateStore(), witness: witness)
let agent = Agent(storeList: list, certificateStore: CertificateStore(), authenticationHandler: AuthenticationHandler(), witness: witness)
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature)
_ = await agent.handle(request: request, provenance: .test, hosts: nil)
#expect(witnessed)
@@ -107,7 +107,7 @@ import CertificateKit
}, witness: { _, trace in
witnessTrace = trace
})
let agent = Agent(storeList: list, certificateStore: CertificateStore(), witness: witness)
let agent = Agent(storeList: list, certificateStore: CertificateStore(), authenticationHandler: AuthenticationHandler(), witness: witness)
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature)
_ = await agent.handle(request: request, provenance: .test, hosts: nil)
#expect(witnessTrace == speakNowTrace)
@@ -120,7 +120,7 @@ import CertificateKit
let list = await storeList(with: [Constants.Secrets.ecdsa256Secret, Constants.Secrets.ecdsa384Secret])
let store = list.stores.first?.base as! Stub.Store
store.shouldThrow = true
let agent = Agent(storeList: list, certificateStore: CertificateStore())
let agent = Agent(storeList: list, certificateStore: CertificateStore(), authenticationHandler: AuthenticationHandler())
let request = try SSHAgentInputParser().parse(data: Constants.Requests.requestSignature)
let response = await agent.handle(request: request, provenance: .test, hosts: nil)
#expect(response == Constants.Responses.requestFailure)
@@ -129,7 +129,7 @@ import CertificateKit
// MARK: Unsupported
@Test func unhandledAdd() async throws {
let agent = Agent(storeList: SecretStoreList(), certificateStore: CertificateStore())
let agent = Agent(storeList: SecretStoreList(), certificateStore: CertificateStore(), authenticationHandler: AuthenticationHandler())
let response = await agent.handle(request: .addIdentity, provenance: .test, hosts: nil)
#expect(response == Constants.Responses.requestFailure)
}
@@ -0,0 +1,13 @@
import Testing
import SecretAgentKit
@Suite @MainActor struct AuthenticationHandlerTests {
@Test func singleImmediatelyRequests() async throws {
}
@Test func authRequiredDoesntBlockNoAuthRequired() async throws {
}
}
@@ -17,7 +17,7 @@ extension StubWitness: SigningWitness {
}
}
func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, target: SigningRequestTarget?) throws {
func witness(accessTo secret: AnySecret, from store: AnySecretStore, by provenance: SigningRequestProvenance, target: SigningRequestTarget?, offerPersistence: Bool) async throws {
witness(secret, provenance)
}
@@ -78,7 +78,6 @@
50BDCB742E6436CA0072D2E7 /* ErrorStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */; };
50BDCB762E6450950072D2E7 /* ConfigurationItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */; };
50C385A52407A76D00AF2719 /* SecretDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50C385A42407A76D00AF2719 /* SecretDetailView.swift */; };
50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */; };
50E0145C2EDB9CDF00B121F1 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 50E0145B2EDB9CDF00B121F1 /* Common */; };
50E0145E2EDB9CE400B121F1 /* Common in Frameworks */ = {isa = PBXBuildFile; productRef = 50E0145D2EDB9CE400B121F1 /* Common */; };
50E204E92FA9D12700402380 /* CertificateDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50E204E82FA9D12700402380 /* CertificateDetailView.swift */; };
@@ -294,7 +293,6 @@
50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorStyle.swift; sourceTree = "<group>"; };
50BDCB752E6450950072D2E7 /* ConfigurationItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurationItemView.swift; sourceTree = "<group>"; };
50C385A42407A76D00AF2719 /* SecretDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecretDetailView.swift; sourceTree = "<group>"; };
50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButtonStyle.swift; sourceTree = "<group>"; };
50E204E82FA9D12700402380 /* CertificateDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateDetailView.swift; sourceTree = "<group>"; };
50E204EC2FAA997F00402380 /* CertificateListItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificateListItemView.swift; sourceTree = "<group>"; };
50E205142FAAB81C00402380 /* SecretiveCertificateParser.xpc */ = {isa = PBXFileReference; explicitFileType = "wrapper.xpc-service"; includeInIndex = 0; path = SecretiveCertificateParser.xpc; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -385,7 +383,6 @@
504788ED2E681EB200B4556F /* Modifiers */ = {
isa = PBXGroup;
children = (
50CF4ABB2E601B0F005588DC /* ActionButtonStyle.swift */,
50BDCB732E6436C60072D2E7 /* ErrorStyle.swift */,
504789222E697DD300B4556F /* BoxBackgroundStyle.swift */,
5065E312295517C500E16645 /* ToolbarButtonStyle.swift */,
@@ -912,7 +909,6 @@
5065E313295517C500E16645 /* ToolbarButtonStyle.swift in Sources */,
50617D8523FCE48E0099B055 /* ContentView.swift in Sources */,
504788F62E68206F00B4556F /* GettingStartedView.swift in Sources */,
50CF4ABC2E601B0F005588DC /* ActionButtonStyle.swift in Sources */,
50E204ED2FAA997F00402380 /* CertificateListItemView.swift in Sources */,
50571E0324393C2600F76F6C /* JustUpdatedChecker.swift in Sources */,
5079BA0F250F29BF00EA86F4 /* StoreListView.swift in Sources */,
@@ -1,94 +0,0 @@
import SwiftUI
struct PrimaryButtonModifier: ViewModifier {
@Environment(\.colorScheme) var colorScheme
@Environment(\.isEnabled) var isEnabled
func body(content: Content) -> some View {
// Tinted glass prominent is really hard to read on 26.0.
if #available(macOS 26.0, *), colorScheme == .dark, isEnabled {
content.buttonStyle(.glassProminent)
} else {
content.buttonStyle(.borderedProminent)
}
}
}
extension View {
func primaryButton() -> some View {
modifier(PrimaryButtonModifier())
}
}
struct ToolbarCircleButtonModifier: ViewModifier {
func body(content: Content) -> some View {
if #available(macOS 26.0, *) {
content
.glassEffect(.regular.tint(.white.opacity(0.1)), in: .circle)
} else {
content
.buttonStyle(.borderless)
}
}
}
extension View {
func toolbarCircleButton() -> some View {
modifier(ToolbarCircleButtonModifier())
}
}
struct NormalButtonModifier: ViewModifier {
func body(content: Content) -> some View {
if #available(macOS 26.0, *) {
content.buttonStyle(.glass)
} else {
content.buttonStyle(.bordered)
}
}
}
extension View {
func normalButton() -> some View {
modifier(NormalButtonModifier())
}
}
struct DangerButtonModifier: ViewModifier {
@Environment(\.colorScheme) var colorScheme
func body(content: Content) -> some View {
// Tinted glass prominent is really hard to read on 26.0.
if #available(macOS 26.0, *), colorScheme == .dark {
content.buttonStyle(.glassProminent)
.tint(.red)
.foregroundStyle(.white)
} else {
content.buttonStyle(.borderedProminent)
.tint(.red)
.foregroundStyle(.white)
}
}
}
extension View {
func danger() -> some View {
modifier(DangerButtonModifier())
}
}