Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 8 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
2 changes: 1 addition & 1 deletion Sources/Real/RealAuthorizationController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
10 changes: 9 additions & 1 deletion Sources/SimpleAuthenticationServices.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)))"
}
}
Original file line number Diff line number Diff line change
@@ -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"))
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
44 changes: 37 additions & 7 deletions Tests/SimpleAuthenticationServicesTests/VirtualAuthorization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<Void, Error>?

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 {
Expand All @@ -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()
Expand All @@ -41,6 +67,11 @@ final class GoServerManager {
}
}

private enum ServerStartupError: Error {
case exited(status: Int32)
case readinessTimedOut
}

let rpID = "test.corbado.io"

@MainActor
Expand Down Expand Up @@ -250,4 +281,3 @@ func assertThrows<T>(throws: T.Type, _ block: @Sendable @escaping () async throw

return nil
}

Loading