From 86069ec16e12b6b65da7b2f290f0f915668dcb09 Mon Sep 17 00:00:00 2001 From: Martin Kellner Date: Wed, 2 Sep 2026 18:52:42 +0200 Subject: [PATCH 1/4] Add errorDescription to AuthorizationError to surface type and originalError --- Sources/SimpleAuthenticationServices.swift | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Sources/SimpleAuthenticationServices.swift b/Sources/SimpleAuthenticationServices.swift index 2bfec50..405c2aa 100644 --- a/Sources/SimpleAuthenticationServices.swift +++ b/Sources/SimpleAuthenticationServices.swift @@ -119,9 +119,17 @@ public enum AuthorizationErrorType: String, Equatable, Sendable, Codable { public struct AuthorizationError: Error, LocalizedError, Sendable { public let type: AuthorizationErrorType public let originalError: Error? - + public init(type: AuthorizationErrorType, originalError: Error? = nil) { self.type = type self.originalError = originalError } + + public var errorDescription: String? { + guard let originalError else { + return "AuthorizationError(type: \(type.rawValue))" + } + + return "AuthorizationError(type: \(type.rawValue), originalError: \(String(describing: originalError)))" + } } From 2019f8789b2fd8fe515d5b9343de79577d366955 Mon Sep 17 00:00:00 2001 From: Martin Kellner Date: Wed, 2 Sep 2026 19:08:05 +0200 Subject: [PATCH 2/4] Guard ASCredentialUpdater for macOS 26 to fix the macOS build --- Sources/Real/RealAuthorizationController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Real/RealAuthorizationController.swift b/Sources/Real/RealAuthorizationController.swift index 545cfa6..40ae4f9 100644 --- a/Sources/Real/RealAuthorizationController.swift +++ b/Sources/Real/RealAuthorizationController.swift @@ -75,7 +75,7 @@ final public class RealAuthorizationController: AuthorizationControllerProtocol, @MainActor public func signalAllAcceptedCredentials(rpID: String, userHandle: Data, acceptedCredentialIDs: [Data]) async throws(AuthorizationError) { - if #available(iOS 26.0, *) { + if #available(iOS 26.0, macOS 26.0, *) { let credentialUpdater = ASCredentialUpdater() do { try await credentialUpdater.reportAllAcceptedPublicKeyCredentials( From e4ebf8e9ccc0bb733246746d8d3c8113c30bfdc5 Mon Sep 17 00:00:00 2001 From: Martin Kellner Date: Mon, 7 Sep 2026 14:11:19 +0200 Subject: [PATCH 3/4] Fix CI SDK selection and cover authorization error diagnostics --- .github/workflows/ci.yml | 20 ++++------ .../AuthorizationErrorTests.swift | 40 +++++++++++++++++++ 2 files changed, 48 insertions(+), 12 deletions(-) create mode 100644 Tests/SimpleAuthenticationServicesTests/AuthorizationErrorTests.swift diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dce7bf1..9594df9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,26 +8,22 @@ on: jobs: build_and_test: - runs-on: macOS-15 - env: - SWIFT_VERSION: 6.0 + runs-on: macos-15 name: Build and Test steps: - name: Checkout repository uses: actions/checkout@v4 - - name: Select Xcode 16.0 + # ASCredentialUpdater requires the SDK shipped with Xcode 26 or newer. + - name: Select Xcode 26.0.1 uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: '16.0' + xcode-version: '26.0.1' - - name: Install Swift - uses: swift-actions/setup-swift@v2 - with: - swift-version: ${{ env.SWIFT_VERSION }} - - - name: Get swift version - run: swift --version + - name: Show Xcode and Swift versions + run: | + xcodebuild -version + xcrun swift --version - name: Run Swift Package Tests run: xcodebuild test -scheme SimpleAuthenticationServices-Package -destination 'platform=macOS' diff --git a/Tests/SimpleAuthenticationServicesTests/AuthorizationErrorTests.swift b/Tests/SimpleAuthenticationServicesTests/AuthorizationErrorTests.swift new file mode 100644 index 0000000..98a8ee0 --- /dev/null +++ b/Tests/SimpleAuthenticationServicesTests/AuthorizationErrorTests.swift @@ -0,0 +1,40 @@ +import AuthenticationServices +import Foundation +import SimpleAuthenticationServices +import Testing + +@Test func authorizationErrorDescribesTypeWithoutUnderlyingError() { + let error: any Error = AuthorizationError(type: .noPresentationAnchor) + + #expect(error.localizedDescription == "AuthorizationError(type: noPresentationAnchor)") +} + +@Test func authorizationErrorPreservesNativeDiagnostics() { + let native = ASAuthorizationError(.failed, userInfo: [ + NSLocalizedDescriptionKey: "Authorization failed", + NSDebugDescriptionErrorKey: "Diagnostic detail for the failed operation" + ]) + let error: any Error = AuthorizationError(type: .unknown, originalError: native) + let description = error.localizedDescription + + #expect(description.contains("type: unknown")) + #expect(description.contains(ASAuthorizationError.errorDomain)) + #expect(description.contains("\(ASAuthorizationError.Code.failed.rawValue)")) + #expect(description.contains("Authorization failed")) + #expect(description.contains("Diagnostic detail for the failed operation")) +} + +@Test func authorizationErrorPreservesNestedError() { + let underlying = NSError(domain: "TestCredentialProvider", code: 42, userInfo: [ + NSLocalizedDescriptionKey: "Credential provider rejected the update" + ]) + let native = ASAuthorizationError(.failed, userInfo: [ + NSUnderlyingErrorKey: underlying + ]) + let error: any Error = AuthorizationError(type: .unknown, originalError: native) + let description = error.localizedDescription + + #expect(description.contains("TestCredentialProvider")) + #expect(description.contains("42")) + #expect(description.contains("Credential provider rejected the update")) +} From 55b5f050a773dafe1de9a3950d87ccb993fd625b Mon Sep 17 00:00:00 2001 From: Martin Kellner Date: Mon, 7 Sep 2026 14:15:09 +0200 Subject: [PATCH 4/4] Wait for shared test server readiness across parallel tests --- .../Utils/RelyingPartyServer.swift | 1 + .../VirtualAuthorization.swift | 44 ++++++++++++++++--- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/Tests/SimpleAuthenticationServicesTests/Utils/RelyingPartyServer.swift b/Tests/SimpleAuthenticationServicesTests/Utils/RelyingPartyServer.swift index 829c005..27b1fec 100644 --- a/Tests/SimpleAuthenticationServicesTests/Utils/RelyingPartyServer.swift +++ b/Tests/SimpleAuthenticationServicesTests/Utils/RelyingPartyServer.swift @@ -236,6 +236,7 @@ class RelyingPartyServer { let url = baseURL.appendingPathComponent("health") var request = URLRequest(url: url) request.httpMethod = "GET" + request.timeoutInterval = 1 do { let (data, response) = try await session.data(for: request) diff --git a/Tests/SimpleAuthenticationServicesTests/VirtualAuthorization.swift b/Tests/SimpleAuthenticationServicesTests/VirtualAuthorization.swift index afc9a8a..407d0f3 100644 --- a/Tests/SimpleAuthenticationServicesTests/VirtualAuthorization.swift +++ b/Tests/SimpleAuthenticationServicesTests/VirtualAuthorization.swift @@ -10,13 +10,23 @@ import SimpleAuthenticationServices final class GoServerManager { static let shared = GoServerManager() private var goServerProcess: Process? - private var isServerReady = false + private var startup: Task? private init() {} func startServerIfNeeded() async throws { - guard goServerProcess == nil else { return } - + if let startup { + return try await startup.value + } + + // All parallel tests must await readiness, including callers that arrive + // after the process launches but before it starts accepting requests. + let startup = Task { try await self.startServer() } + self.startup = startup + try await startup.value + } + + private func startServer() async throws { print("Starting relying party server...") let process = Process() guard let executableURL = Bundle.module.url(forResource: "relying-pary-server-arm64-darwin", withExtension: nil, subdirectory: "TestBinaries") else { @@ -26,13 +36,29 @@ final class GoServerManager { try process.run() goServerProcess = process - // server takes a bit of time to start - try await Task.sleep(for: .milliseconds(250)) - print("Started relying party server.") + let server = try RelyingPartyServer(baseURLString: "http://localhost:8080") + let clock = ContinuousClock() + let deadline = clock.now.advanced(by: .seconds(10)) + while clock.now < deadline { + try Task.checkCancellation() + guard process.isRunning else { + throw ServerStartupError.exited(status: process.terminationStatus) + } + if await server.checkHealth() { + print("Started relying party server.") + return + } + try await Task.sleep(for: .milliseconds(50)) + } + + process.terminate() + throw ServerStartupError.readinessTimedOut } func stopServer() { + startup?.cancel() + startup = nil guard let process = goServerProcess, process.isRunning else { return } print("Stopping relying party server...") process.terminate() @@ -41,6 +67,11 @@ final class GoServerManager { } } +private enum ServerStartupError: Error { + case exited(status: Int32) + case readinessTimedOut +} + let rpID = "test.corbado.io" @MainActor @@ -250,4 +281,3 @@ func assertThrows(throws: T.Type, _ block: @Sendable @escaping () async throw return nil } -