diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..5229f9df --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +/Tests/**/__Snapshots__/**/*.txt eol=lf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 46bb3530..06bf5835 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,9 +14,9 @@ concurrency: cancel-in-progress: true jobs: - library: + library-darwin: runs-on: macos-13 - name: Test Library + name: Test Library (Darwin) steps: - uses: actions/checkout@v3 - name: Select Xcode 15.0.1 @@ -25,7 +25,7 @@ jobs: run: make test-library library-evolution: - name: Library (evolution) + name: Library (evolution, Darwin) runs-on: macos-13 steps: - uses: actions/checkout@v4 @@ -34,6 +34,33 @@ jobs: - name: Build for library evolution run: make build-for-library-evolution + library-linux: + runs-on: ubuntu-latest + name: Test Library (Linux) + steps: + - uses: swift-actions/setup-swift@v1 + with: + swift-version: "5.9" + - uses: actions/checkout@v3 + - name: Run Tests + run: swift test + + library-windows: + runs-on: windows-latest + name: Test Library (Windows) + steps: + # We use BCNY's repo since they have newer builds of Swift + # which have fixed libcurl in Foundation. + - uses: compnerd/gha-setup-swift@main + with: + release-tag-name: "20231203.0" + github-repo: "thebrowsercompany/swift-build" + release-asset-name: installer-amd64.exe + github-token: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/checkout@v3 + - name: Run Tests + run: swift test + examples: runs-on: macos-13 name: Build Examples @@ -45,4 +72,3 @@ jobs: run: cp Examples/Examples/_Secrets.swift Examples/Examples/Secrets.swift - name: Build examples run: make build-examples - diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..12c67bcf --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,12 @@ +{ + "cSpell.words": [ + "apikey", + "HTTPURL", + "pkce", + "postgrest", + "preconcurrency", + "Supabase", + "whitespaces", + "xctest" + ] +} diff --git a/Examples/Examples/ExamplesApp.swift b/Examples/Examples/ExamplesApp.swift index 0ed1caad..4db4ef5f 100644 --- a/Examples/Examples/ExamplesApp.swift +++ b/Examples/Examples/ExamplesApp.swift @@ -20,5 +20,6 @@ struct ExamplesApp: App { let supabase = SupabaseClient( supabaseURL: Secrets.supabaseURL, - supabaseKey: Secrets.supabaseAnonKey + supabaseKey: Secrets.supabaseAnonKey, + options: .init(auth: .init(storage: KeychainLocalStorage(service: "supabase.gotrue.swift", accessGroup: nil))) ) diff --git a/Examples/UserManagement/Supabase.swift b/Examples/UserManagement/Supabase.swift index 266901cf..96b915b1 100644 --- a/Examples/UserManagement/Supabase.swift +++ b/Examples/UserManagement/Supabase.swift @@ -9,6 +9,7 @@ import Foundation import Supabase let supabase = SupabaseClient( - supabaseURL: "https://PROJECT_ID.supabase.co", - supabaseKey: "YOUR_SUPABASE_ANON_KEY" + supabaseURL: URL(string: "https://PROJECT_ID.supabase.co")!, + supabaseKey: "YOUR_SUPABASE_ANON_KEY", + options: .init(auth: .init(storage: KeychainLocalStorage(service: "supabase.gotrue.swift", accessGroup: nil))) ) diff --git a/Package.resolved b/Package.resolved index a6dbd1af..5c8656bb 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,21 +1,21 @@ { "pins" : [ { - "identity" : "keychainaccess", + "identity" : "swift-concurrency-extras", "kind" : "remoteSourceControl", - "location" : "https://github.com/kishikawakatsumi/KeychainAccess", + "location" : "https://github.com/pointfreeco/swift-concurrency-extras", "state" : { - "revision" : "84e546727d66f1adc5439debad16270d0fdd04e7", - "version" : "4.2.2" + "revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71", + "version" : "1.1.0" } }, { - "identity" : "swift-concurrency-extras", + "identity" : "swift-crypto", "kind" : "remoteSourceControl", - "location" : "https://github.com/pointfreeco/swift-concurrency-extras", + "location" : "https://github.com/apple/swift-crypto.git", "state" : { - "revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71", - "version" : "1.1.0" + "revision" : "60f13f60c4d093691934dc6cfdf5f508ada1f894", + "version" : "2.6.0" } }, { @@ -47,4 +47,4 @@ } ], "version" : 2 -} +} \ No newline at end of file diff --git a/Package.swift b/Package.swift index 3869c1b8..8ac5efbf 100644 --- a/Package.swift +++ b/Package.swift @@ -4,6 +4,28 @@ import Foundation import PackageDescription +var dependencies: [Package.Dependency] = [ + .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.8.1"), + .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.0.0"), + .package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.0.0"), + .package(url: "https://github.com/apple/swift-crypto.git", "1.0.0" ..< "3.0.0"), +] + +var goTrueDependencies: [Target.Dependency] = [ + "_Helpers", + .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), + .product(name: "Crypto", package: "swift-crypto"), +] + +#if !os(Windows) && !os(Linux) +dependencies += [ + .package(url: "https://github.com/kishikawakatsumi/KeychainAccess", from: "4.2.2"), +] +goTrueDependencies += [ + .product(name: "KeychainAccess", package: "KeychainAccess"), +] +#endif + let package = Package( name: "Supabase", platforms: [ @@ -24,12 +46,7 @@ let package = Package( targets: ["Supabase", "Functions", "PostgREST", "Auth", "Realtime", "Storage"] ), ], - dependencies: [ - .package(url: "https://github.com/kishikawakatsumi/KeychainAccess", from: "4.2.2"), - .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.8.1"), - .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.0.0"), - .package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.0.0"), - ], + dependencies: dependencies, targets: [ .target( name: "_Helpers", @@ -47,16 +64,13 @@ let package = Package( ), .target( name: "Auth", - dependencies: [ - "_Helpers", - .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), - .product(name: "KeychainAccess", package: "KeychainAccess"), - ] + dependencies: goTrueDependencies ), .testTarget( name: "AuthTests", dependencies: [ "Auth", + "_Helpers", .product(name: "SnapshotTesting", package: "swift-snapshot-testing"), .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), ], diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift index cdadd3f1..0e351cfc 100644 --- a/Sources/Auth/AuthClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -34,7 +34,7 @@ public actor AuthClient { url: URL, headers: [String: String] = [:], flowType: AuthFlowType = Configuration.defaultFlowType, - localStorage: AuthLocalStorage = Configuration.defaultLocalStorage, + localStorage: AuthLocalStorage, encoder: JSONEncoder = AuthClient.Configuration.jsonEncoder, decoder: JSONDecoder = AuthClient.Configuration.jsonDecoder, fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) } @@ -101,7 +101,7 @@ public actor AuthClient { url: URL, headers: [String: String] = [:], flowType: AuthFlowType = AuthClient.Configuration.defaultFlowType, - localStorage: AuthLocalStorage = AuthClient.Configuration.defaultLocalStorage, + localStorage: AuthLocalStorage, encoder: JSONEncoder = AuthClient.Configuration.jsonEncoder, decoder: JSONDecoder = AuthClient.Configuration.jsonDecoder, fetch: @escaping FetchHandler = { try await URLSession.shared.data(for: $0) } diff --git a/Sources/Auth/AuthLocalStorage.swift b/Sources/Auth/AuthLocalStorage.swift deleted file mode 100644 index 2b385c3d..00000000 --- a/Sources/Auth/AuthLocalStorage.swift +++ /dev/null @@ -1,32 +0,0 @@ -import Foundation -@preconcurrency import KeychainAccess - -public protocol AuthLocalStorage: Sendable { - func store(key: String, value: Data) throws - func retrieve(key: String) throws -> Data? - func remove(key: String) throws -} - -struct KeychainLocalStorage: AuthLocalStorage { - private let keychain: Keychain - - init(service: String, accessGroup: String?) { - if let accessGroup { - keychain = Keychain(service: service, accessGroup: accessGroup) - } else { - keychain = Keychain(service: service) - } - } - - func store(key: String, value: Data) throws { - try keychain.set(value, key: key) - } - - func retrieve(key: String) throws -> Data? { - try keychain.getData(key) - } - - func remove(key: String) throws { - try keychain.remove(key) - } -} diff --git a/Sources/Auth/Defaults.swift b/Sources/Auth/Defaults.swift index 479f9924..54192cb5 100644 --- a/Sources/Auth/Defaults.swift +++ b/Sources/Auth/Defaults.swift @@ -62,10 +62,4 @@ extension AuthClient.Configuration { /// The default ``AuthFlowType`` used when initializing a ``AuthClient`` instance. public static let defaultFlowType: AuthFlowType = .pkce - - /// The default ``AuthLocalStorage`` instance used by the ``AuthClient``. - public static let defaultLocalStorage: AuthLocalStorage = KeychainLocalStorage( - service: "supabase.gotrue.swift", - accessGroup: nil - ) } diff --git a/Sources/Auth/Internal/FixedWidthInteger+Random.swift b/Sources/Auth/Internal/FixedWidthInteger+Random.swift new file mode 100644 index 00000000..0b14688b --- /dev/null +++ b/Sources/Auth/Internal/FixedWidthInteger+Random.swift @@ -0,0 +1,30 @@ +import Foundation + +// Borrowed from the Vapor project, https://github.com/vapor/vapor/blob/main/Sources/Vapor/Utilities/Array%2BRandom.swift#L14 +extension FixedWidthInteger { + internal static func random() -> Self { + return Self.random(in: .min ... .max) + } + + internal static func random(using generator: inout T) -> Self + where T : RandomNumberGenerator + { + return Self.random(in: .min ... .max, using: &generator) + } +} + +extension Array where Element: FixedWidthInteger { + internal static func random(count: Int) -> [Element] { + var array: [Element] = .init(repeating: 0, count: count) + (0..(count: Int, using generator: inout T) -> [Element] + where T: RandomNumberGenerator + { + var array: [Element] = .init(repeating: 0, count: count) + (0.. String { - var buffer = [UInt8](repeating: 0, count: 64) - _ = SecRandomCopyBytes(kSecRandomDefault, buffer.count, &buffer) + let buffer = [UInt8].random(count: 64) return Data(buffer).pkceBase64EncodedString() } @@ -12,8 +11,10 @@ enum PKCE { guard let data = string.data(using: .utf8) else { preconditionFailure("provided string should be utf8 encoded.") } - let hashed = SHA256.hash(data: data) + var hasher = SHA256() + hasher.update(data: data) + let hashed = hasher.finalize() return Data(hashed).pkceBase64EncodedString() } } diff --git a/Sources/Auth/Internal/SessionManager.swift b/Sources/Auth/Internal/SessionManager.swift index 6e217fef..c8855e54 100644 --- a/Sources/Auth/Internal/SessionManager.swift +++ b/Sources/Auth/Internal/SessionManager.swift @@ -1,5 +1,4 @@ import Foundation -import KeychainAccess @_spi(Internal) import _Helpers struct SessionRefresher: Sendable { diff --git a/Sources/Auth/Storage/AuthLocalStorage.swift b/Sources/Auth/Storage/AuthLocalStorage.swift new file mode 100644 index 00000000..40e4930c --- /dev/null +++ b/Sources/Auth/Storage/AuthLocalStorage.swift @@ -0,0 +1,7 @@ +import Foundation + +public protocol AuthLocalStorage: Sendable { + func store(key: String, value: Data) throws + func retrieve(key: String) throws -> Data? + func remove(key: String) throws +} diff --git a/Sources/Auth/Storage/KeychainLocalStorage.swift b/Sources/Auth/Storage/KeychainLocalStorage.swift new file mode 100644 index 00000000..08c8b554 --- /dev/null +++ b/Sources/Auth/Storage/KeychainLocalStorage.swift @@ -0,0 +1,28 @@ +#if !os(Windows) && !os(Linux) +import Foundation +@preconcurrency import KeychainAccess + +public struct KeychainLocalStorage: AuthLocalStorage { + private let keychain: Keychain + + public init(service: String, accessGroup: String?) { + if let accessGroup { + keychain = Keychain(service: service, accessGroup: accessGroup) + } else { + keychain = Keychain(service: service) + } + } + + public func store(key: String, value: Data) throws { + try keychain.set(value, key: key) + } + + public func retrieve(key: String) throws -> Data? { + try keychain.getData(key) + } + + public func remove(key: String) throws { + try keychain.remove(key) + } +} +#endif diff --git a/Sources/Auth/Storage/WinCredLocalStorage.swift b/Sources/Auth/Storage/WinCredLocalStorage.swift new file mode 100644 index 00000000..c797aac2 --- /dev/null +++ b/Sources/Auth/Storage/WinCredLocalStorage.swift @@ -0,0 +1,82 @@ +#if os(Windows) +import Foundation +import WinSDK + +enum WinCredLocalStorageError: Error { + case windows(UInt32) + case other(Int) +} + +public struct WinCredLocalStorage: AuthLocalStorage { + private let service: String + + private let credentialType: DWORD + private let credentialPersistence: DWORD + + public init(service: String) { + self.service = service + credentialType = DWORD(CRED_TYPE_GENERIC) + credentialPersistence = DWORD(CRED_PERSIST_LOCAL_MACHINE) + } + + public func store(key: String, value: Data) throws { + var valueData = value + + var credential: CREDENTIALW = .init() + + credential.Type = credentialType + credential.Persist = credentialPersistence + "\(service)\\\(key)".withCString(encodedAs: UTF16.self, { keyName in + credential.TargetName = UnsafeMutablePointer(mutating: keyName) + }) + + withUnsafeMutableBytes(of: &valueData, { data in + credential.CredentialBlobSize = DWORD(data.count) + credential.CredentialBlob = data.baseAddress!.assumingMemoryBound(to: UInt8.self) + }) + + if !CredWriteW(&credential, 0) { + let lastError = GetLastError() + debugPrint("Unable to save password to credential vault, got error code \(lastError)") + + throw WinCredLocalStorageError.windows(lastError) + } + } + + public func retrieve(key: String) throws -> Data? { + var credential: PCREDENTIALW? + + let targetName = "\(service)\\\(key))".withCString(encodedAs: UTF16.self, { $0 }) + + if !CredReadW(targetName, credentialType, 0, &credential) { + let lastError = GetLastError() + debugPrint("Unable to find entry for key in credential vault, got error code \(lastError)") + + throw WinCredLocalStorageError.windows(lastError) + } + + guard let foundCredential = credential, let blob = foundCredential.pointee.CredentialBlob else { + throw WinCredLocalStorageError.other(-1) + } + + let blobSize = Int(foundCredential.pointee.CredentialBlobSize) + let pointer = blob.withMemoryRebound(to: UInt8.self, capacity: blobSize, { $0 }) + let data = Data(bytes: pointer, count: blobSize) + + CredFree(foundCredential) + + return data + } + + public func remove(key: String) throws { + let targetName = "\(service)\\\(key))".withCString(encodedAs: UTF16.self, { $0 }) + + if !CredDeleteW(targetName, credentialType, 0) { + let lastError = GetLastError() + debugPrint("Unable to remove key from credential vault, got error code \(lastError)") + + throw WinCredLocalStorageError.windows(lastError) + } + } +} +#endif diff --git a/Sources/Functions/FunctionsClient.swift b/Sources/Functions/FunctionsClient.swift index 2321723d..01e3865f 100644 --- a/Sources/Functions/FunctionsClient.swift +++ b/Sources/Functions/FunctionsClient.swift @@ -1,6 +1,10 @@ import Foundation @_spi(Internal) import _Helpers +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + let version = _Helpers.version /// An actor representing a client for invoking functions. diff --git a/Sources/PostgREST/PostgrestClient.swift b/Sources/PostgREST/PostgrestClient.swift index 4e5d9931..8f4104c0 100644 --- a/Sources/PostgREST/PostgrestClient.swift +++ b/Sources/PostgREST/PostgrestClient.swift @@ -1,6 +1,10 @@ import Foundation @_spi(Internal) import _Helpers +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + /// PostgREST client. public actor PostgrestClient { public typealias FetchHandler = @Sendable (_ request: URLRequest) async throws -> ( diff --git a/Sources/PostgREST/Types.swift b/Sources/PostgREST/Types.swift index c2e8ee24..63b86ddd 100644 --- a/Sources/PostgREST/Types.swift +++ b/Sources/PostgREST/Types.swift @@ -1,5 +1,9 @@ import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + public struct PostgrestError: Error, Codable, Sendable { public let details: String? public let hint: String? diff --git a/Sources/Realtime/PhoenixTransport.swift b/Sources/Realtime/PhoenixTransport.swift index 916be2e7..cf0ac2c9 100644 --- a/Sources/Realtime/PhoenixTransport.swift +++ b/Sources/Realtime/PhoenixTransport.swift @@ -20,6 +20,10 @@ import Foundation +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + // ---------------------------------------------------------------------- // MARK: - Transport Protocol @@ -224,8 +228,8 @@ open class URLSessionTransport: NSObject, PhoenixTransport, URLSessionWebSocketD } open func send(data: Data) { - task?.send(.string(String(data: data, encoding: .utf8)!)) { _ in - // TODO: What is the behavior when an error occurs? + Task { + try? await task?.send(.string(String(data: data, encoding: .utf8)!)) } } @@ -272,25 +276,25 @@ open class URLSessionTransport: NSObject, PhoenixTransport, URLSessionWebSocketD // MARK: - Private private func receive() { - task?.receive { [weak self] result in - switch result { - case let .success(message): - switch message { + Task { + do { + let result = try await task?.receive() + switch result { case .data: print("Data received. This method is unsupported by the Client") case let .string(text): - self?.delegate?.onMessage(message: text) + self.delegate?.onMessage(message: text) default: - fatalError("Unknown result was received. [\(result)]") + fatalError("Unknown result was received. [\(String(describing: result))]") } // Since `.receive()` is only good for a single message, it must // be called again after a message is received in order to // received the next message. - self?.receive() - case let .failure(error): + self.receive() + } catch { print("Error when receiving \(error)") - self?.abnormalErrorReceived(error, response: nil) + self.abnormalErrorReceived(error, response: nil) } } } diff --git a/Sources/Realtime/RealtimeClient.swift b/Sources/Realtime/RealtimeClient.swift index 31ab4608..debd7a50 100644 --- a/Sources/Realtime/RealtimeClient.swift +++ b/Sources/Realtime/RealtimeClient.swift @@ -22,6 +22,10 @@ import Foundation @_spi(Internal) import _Helpers import ConcurrencyExtras +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + public enum SocketError: Error { case abnormalClosureError } @@ -122,7 +126,7 @@ public class RealtimeClient: PhoenixTransportDelegate { /// must be set before calling `socket.connect()` in order to be applied public var disableSSLCertValidation: Bool = false - #if os(Linux) + #if os(Linux) || os(Windows) #else /// Configure custom SSL validation logic, eg. SSL pinning. This /// must be set before calling `socket.connect()` in order to apply. @@ -924,7 +928,7 @@ public class RealtimeClient: PhoenixTransportDelegate { } /// Sends a heartbeat payload to the phoenix servers - @objc func sendHeartbeat() { + func sendHeartbeat() { // Do not send if the connection is closed guard isConnected else { return } diff --git a/Sources/Supabase/SupabaseClient.swift b/Sources/Supabase/SupabaseClient.swift index d994a5e7..2cc773fb 100644 --- a/Sources/Supabase/SupabaseClient.swift +++ b/Sources/Supabase/SupabaseClient.swift @@ -7,6 +7,10 @@ import Foundation @_exported import Realtime @_exported import Storage +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + let version = _Helpers.version /// Supabase Client. @@ -68,7 +72,7 @@ public final class SupabaseClient: @unchecked Sendable { public init( supabaseURL: URL, supabaseKey: String, - options: SupabaseClientOptions = .init() + options: SupabaseClientOptions ) { self.supabaseURL = supabaseURL self.supabaseKey = supabaseKey @@ -81,7 +85,7 @@ public final class SupabaseClient: @unchecked Sendable { defaultHeaders = [ "X-Client-Info": "supabase-swift/\(version)", "Authorization": "Bearer \(supabaseKey)", - "apikey": supabaseKey, + "Apikey": supabaseKey, ].merging(options.global.headers) { _, new in new } auth = AuthClient( @@ -106,25 +110,6 @@ public final class SupabaseClient: @unchecked Sendable { listenForAuthEvents() } - /// Create a new client. - /// - Parameters: - /// - supabaseURL: The unique Supabase URL which is supplied when you create a new project in - /// your project dashboard. - /// - supabaseKey: The unique Supabase Key which is supplied when you create a new project in - /// your project dashboard. - /// - options: Custom options to configure client's behavior. - public convenience init( - supabaseURL: String, - supabaseKey: String, - options: SupabaseClientOptions = .init() - ) { - guard let supabaseURL = URL(string: supabaseURL) else { - fatalError("Invalid supabaseURL: \(supabaseURL)") - } - - self.init(supabaseURL: supabaseURL, supabaseKey: supabaseKey, options: options) - } - deinit { listenForAuthEventsTask.value?.cancel() } diff --git a/Sources/Supabase/Types.swift b/Sources/Supabase/Types.swift index e163b235..f09e46b5 100644 --- a/Sources/Supabase/Types.swift +++ b/Sources/Supabase/Types.swift @@ -2,6 +2,10 @@ import Auth import Foundation import PostgREST +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + public struct SupabaseClientOptions: Sendable { public let db: DatabaseOptions public let auth: AuthOptions @@ -44,7 +48,7 @@ public struct SupabaseClientOptions: Sendable { public let decoder: JSONDecoder public init( - storage: AuthLocalStorage = AuthClient.Configuration.defaultLocalStorage, + storage: AuthLocalStorage, flowType: AuthFlowType = AuthClient.Configuration.defaultFlowType, encoder: JSONEncoder = AuthClient.Configuration.jsonEncoder, decoder: JSONDecoder = AuthClient.Configuration.jsonDecoder @@ -71,7 +75,7 @@ public struct SupabaseClientOptions: Sendable { public init( db: DatabaseOptions = .init(), - auth: AuthOptions = .init(), + auth: AuthOptions, global: GlobalOptions = .init() ) { self.db = db diff --git a/Sources/_Helpers/Request.swift b/Sources/_Helpers/Request.swift index 13e2b286..95d87333 100644 --- a/Sources/_Helpers/Request.swift +++ b/Sources/_Helpers/Request.swift @@ -1,5 +1,9 @@ import Foundation +#if os(Linux) || os(Windows) +import FoundationNetworking +#endif + @_spi(Internal) public struct HTTPClient: Sendable { public typealias FetchHandler = @Sendable (URLRequest) async throws -> (Data, URLResponse) diff --git a/Sources/_Helpers/URLSession+AsyncAwait.swift b/Sources/_Helpers/URLSession+AsyncAwait.swift new file mode 100644 index 00000000..6e5b14d5 --- /dev/null +++ b/Sources/_Helpers/URLSession+AsyncAwait.swift @@ -0,0 +1,123 @@ +#if canImport(FoundationNetworking) +import Foundation +import FoundationNetworking + +/// A set of errors that can be returned from the +/// polyfilled extensions on ``URLSession`` +public enum URLSessionPolyfillError: Error { + /// Returned when no data and no error are provided. + case noDataNoErrorReturned +} + +/// A private helper which let's us manage the asynchronous cancellation +/// of the returned URLSessionTasks from our polyfill implementation. +/// +/// This is a lightly modified version of https://github.com/swift-server/async-http-client/blob/16aed40d3e30e8453e226828d59ad2e2c5fd6355/Sources/AsyncHTTPClient/AsyncAwait/HTTPClient%2Bexecute.swift#L152-L156 +/// we use this for the same reasons as listed in the linked code in that there +/// really isn't a good way to deal with cancellation in the 'with*Continuation' functions. +private actor URLSessionTaskCancellationHelper { + enum State { + case initialized + case registered(URLSessionTask) + case cancelled + } + + var state: State = .initialized + + init() {} + + nonisolated func register(_ task: URLSessionTask) { + Task { + await actuallyRegister(task) + } + } + + nonisolated func cancel() { + Task { + await actuallyCancel() + } + } + + private func actuallyRegister(_ task: URLSessionTask) { + switch state { + case .registered: + preconditionFailure("Attempting to register another task while the current helper already has a registered task!") + case .cancelled: + // Run through any cancellation logic which should be a noop as we're already cancelled. + actuallyCancel() + // Cancel the passed in task since we're already in a cancelled state. + task.cancel() + case .initialized: + state = .registered(task) + + } + } + + private func actuallyCancel() { + // Handle whatever needs to be done based on the current state + switch state { + case let .registered(task): + task.cancel() + case .cancelled: + break + case .initialized: + break + } + + // Set state into cancelled to short circuit subsequent cancellations or registrations. + state = .cancelled + } +} + +extension URLSession { + public func data(for request: URLRequest, delegate: (URLSessionTaskDelegate)? = nil) async throws -> (Data, URLResponse) { + let helper = URLSessionTaskCancellationHelper() + + return try await withTaskCancellationHandler(operation: { + return try await withCheckedThrowingContinuation({ continuation in + let task = dataTask(with: request, completionHandler: { data, response, error in + if let error { + continuation.resume(throwing: error) + } else if let data, let response { + continuation.resume(returning: (data, response)) + } else { + continuation.resume(throwing: URLSessionPolyfillError.noDataNoErrorReturned) + } + }) + + helper.register(task) + + task.resume() + }) + }, onCancel: { + helper.cancel() + }) + + } + + public func upload(for request: URLRequest, from bodyData: Data, delegate: (URLSessionTaskDelegate)? = nil) async throws -> (Data, URLResponse) { + let helper = URLSessionTaskCancellationHelper() + + return try await withTaskCancellationHandler(operation: { + return try await withCheckedThrowingContinuation({ continuation in + let task = uploadTask(with: request, from: bodyData, completionHandler: { data, response, error in + if let error { + continuation.resume(throwing: error) + } else if let data, let response { + continuation.resume(returning: (data, response)) + } else { + continuation.resume(throwing: URLSessionPolyfillError.noDataNoErrorReturned) + } + }) + + helper.register(task) + + task.resume() + }) + }, onCancel: { + helper.cancel() + }) + } +} + +#endif diff --git a/Tests/AuthTests/GoTrueClientTests.swift b/Tests/AuthTests/GoTrueClientTests.swift index 67830703..ab224a0d 100644 --- a/Tests/AuthTests/GoTrueClientTests.swift +++ b/Tests/AuthTests/GoTrueClientTests.swift @@ -11,13 +11,21 @@ import ConcurrencyExtras @testable import Auth +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + final class AuthClientTests: XCTestCase { + fileprivate var api: APIClient! + func testAuthStateChanges() async throws { let session = Session.validSession let sut = makeSUT() let events = ActorIsolated([AuthChangeEvent]()) - let expectation = expectation(description: "onAuthStateChangeEnd") + + // We use a semaphore here instead of the nicer XCTestExpectation as that isn't fully available on Linux. + let semaphore = DispatchSemaphore(value: 0) await withDependencies { $0.eventEmitter = .live @@ -31,11 +39,11 @@ final class AuthClientTests: XCTestCase { $0.append(event) } - expectation.fulfill() + semaphore.signal() } } - await fulfillment(of: [expectation]) + _ = semaphore.wait(timeout: .now() + 2.0) let events = await events.value XCTAssertEqual(events, [.initialSession]) @@ -149,7 +157,8 @@ final class AuthClientTests: XCTestCase { private func makeSUT() -> AuthClient { let configuration = AuthClient.Configuration( url: clientURL, - headers: ["apikey": "dummy.api.key"] + headers: ["Apikey": "dummy.api.key"], + localStorage: Dependencies.localStorage ) let sut = AuthClient( diff --git a/Tests/AuthTests/Mocks/Mocks.swift b/Tests/AuthTests/Mocks/Mocks.swift index 2851505e..e41c7ce4 100644 --- a/Tests/AuthTests/Mocks/Mocks.swift +++ b/Tests/AuthTests/Mocks/Mocks.swift @@ -68,9 +68,47 @@ extension APIClient { static let mock = APIClient(execute: unimplemented("APIClient.execute")) } +struct InsecureMockLocalStorage: AuthLocalStorage { + private let defaults: UserDefaults + + init(service: String, accessGroup: String?) { + guard let defaults = UserDefaults(suiteName: service) else { + fatalError("Unable to create defautls for service: \(service)") + } + + self.defaults = defaults + } + + func store(key: String, value: Data) throws { + print("[WARN] YOU ARE YOU WRITING TO INSECURE LOCAL STORAGE") + defaults.set(value, forKey: key) + } + + func retrieve(key: String) throws -> Data? { + print("[WARN] YOU ARE READING FROM INSECURE LOCAL STORAGE") + return defaults.data(forKey: key) + } + + func remove(key: String) throws { + print("[WARN] YOU ARE REMOVING A KEY FROM INSECURE LOCAL STORAGE") + defaults.removeObject(forKey: key) + } +} + extension Dependencies { + static let localStorage: some AuthLocalStorage = { + #if os(iOS) || os(macOS) || os(watchOS) || os(tvOS) + KeychainLocalStorage(service: "supabase.gotrue.swift", accessGroup: nil) + #elseif os(Windows) + WinCredLocalStorage(service: "supabase.gotrue.swift") + #else + // Only use an insecure mock when needed for testing + InsecureMockLocalStorage(service: "supabase.gotrue.swift", accessGroup: nil) + #endif + }() + static let mock = Dependencies( - configuration: AuthClient.Configuration(url: clientURL), + configuration: AuthClient.Configuration(url: clientURL, localStorage: Self.localStorage), sessionManager: .mock, api: .mock, eventEmitter: .mock, diff --git a/Tests/AuthTests/RequestsTests.swift b/Tests/AuthTests/RequestsTests.swift index 32725fd8..0f48b3da 100644 --- a/Tests/AuthTests/RequestsTests.swift +++ b/Tests/AuthTests/RequestsTests.swift @@ -11,6 +11,10 @@ import XCTest @testable import Auth +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + struct UnimplementedError: Error {} final class RequestsTests: XCTestCase { @@ -159,6 +163,9 @@ final class RequestsTests: XCTestCase { } } + #if !os(Windows) && !os(Linux) + // For some reason this crashes the testing bundle + // on Linux and Windows, skipping it. func testSessionFromURL() async throws { let sut = makeSUT(fetch: { request in let authorizationHeader = request.allHTTPHeaderFields?["Authorization"] @@ -192,6 +199,7 @@ final class RequestsTests: XCTestCase { XCTAssertEqual(session, expectedSession) } } + #endif func testSessionFromURLWithMissingComponent() async { let sut = makeSUT() @@ -395,7 +403,7 @@ final class RequestsTests: XCTestCase { let configuration = AuthClient.Configuration( url: clientURL, - headers: ["apikey": "dummy.api.key", "X-Client-Info": "gotrue-swift/x.y.z"], + headers: ["Apikey": "dummy.api.key", "X-Client-Info": "gotrue-swift/x.y.z"], flowType: flowType, localStorage: InMemoryLocalStorage(), encoder: encoder, diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testRefreshSession.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testRefreshSession.1.txt index eca3b057..75958f98 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testRefreshSession.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testRefreshSession.1.txt @@ -1,7 +1,7 @@ curl \ --request POST \ + --header "Apikey: dummy.api.key" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ - --header "apikey: dummy.api.key" \ --data "{\"refresh_token\":\"refresh-token\"}" \ "http://localhost:54321/auth/v1/token?grant_type=refresh_token" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testResendEmail.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testResendEmail.1.txt index 5277401b..4809ee9b 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testResendEmail.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testResendEmail.1.txt @@ -1,7 +1,7 @@ curl \ --request POST \ + --header "Apikey: dummy.api.key" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ - --header "apikey: dummy.api.key" \ --data "{\"email\":\"example@mail.com\",\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"},\"type\":\"email_change\"}" \ "http://localhost:54321/auth/v1/resend?redirect_to=https://supabase.com" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testResendPhone.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testResendPhone.1.txt index 033adeed..7972ac67 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testResendPhone.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testResendPhone.1.txt @@ -1,7 +1,7 @@ curl \ --request POST \ + --header "Apikey: dummy.api.key" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ - --header "apikey: dummy.api.key" \ --data "{\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"},\"phone\":\"+1 202-918-2132\",\"type\":\"phone_change\"}" \ "http://localhost:54321/auth/v1/resend" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testResetPasswordForEmail.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testResetPasswordForEmail.1.txt index 9b817521..f5eca058 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testResetPasswordForEmail.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testResetPasswordForEmail.1.txt @@ -1,7 +1,7 @@ curl \ --request POST \ + --header "Apikey: dummy.api.key" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ - --header "apikey: dummy.api.key" \ --data "{\"email\":\"example@mail.com\",\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"}}" \ "http://localhost:54321/auth/v1/recover?redirect_to=https://supabase.com" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testSessionFromURL.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSessionFromURL.1.txt index 52360aee..390c2455 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testSessionFromURL.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testSessionFromURL.1.txt @@ -1,5 +1,5 @@ curl \ + --header "Apikey: dummy.api.key" \ --header "Authorization: bearer accesstoken" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ - --header "apikey: dummy.api.key" \ "http://localhost:54321/auth/v1/user" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testSetSessionWithAExpiredToken.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSetSessionWithAExpiredToken.1.txt index a768588c..0e4fd094 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testSetSessionWithAExpiredToken.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testSetSessionWithAExpiredToken.1.txt @@ -1,7 +1,7 @@ curl \ --request POST \ + --header "Apikey: dummy.api.key" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ - --header "apikey: dummy.api.key" \ --data "{\"refresh_token\":\"dummy-refresh-token\"}" \ "http://localhost:54321/auth/v1/token?grant_type=refresh_token" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testSetSessionWithAFutureExpirationDate.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSetSessionWithAFutureExpirationDate.1.txt index ecd2e964..053a9f94 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testSetSessionWithAFutureExpirationDate.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testSetSessionWithAFutureExpirationDate.1.txt @@ -1,5 +1,5 @@ curl \ + --header "Apikey: dummy.api.key" \ --header "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjo0ODUyMTYzNTkzLCJzdWIiOiJmMzNkM2VjOS1hMmVlLTQ3YzQtODBlMS01YmQ5MTlmM2Q4YjgiLCJlbWFpbCI6ImhpQGJpbmFyeXNjcmFwaW5nLmNvIiwicGhvbmUiOiIiLCJhcHBfbWV0YWRhdGEiOnsicHJvdmlkZXIiOiJlbWFpbCIsInByb3ZpZGVycyI6WyJlbWFpbCJdfSwidXNlcl9tZXRhZGF0YSI6e30sInJvbGUiOiJhdXRoZW50aWNhdGVkIn0.UiEhoahP9GNrBKw_OHBWyqYudtoIlZGkrjs7Qa8hU7I" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ - --header "apikey: dummy.api.key" \ "http://localhost:54321/auth/v1/user" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithEmailAndPassword.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithEmailAndPassword.1.txt index c8400002..8aa2cb83 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithEmailAndPassword.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithEmailAndPassword.1.txt @@ -1,7 +1,7 @@ curl \ --request POST \ + --header "Apikey: dummy.api.key" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ - --header "apikey: dummy.api.key" \ --data "{\"email\":\"example@mail.com\",\"password\":\"the.pass\"}" \ "http://localhost:54321/auth/v1/token?grant_type=password" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithIdToken.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithIdToken.1.txt index 76c6c8e1..a2b652d9 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithIdToken.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithIdToken.1.txt @@ -1,7 +1,7 @@ curl \ --request POST \ + --header "Apikey: dummy.api.key" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ - --header "apikey: dummy.api.key" \ --data "{\"access_token\":\"access-token\",\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"},\"id_token\":\"id-token\",\"nonce\":\"nonce\",\"provider\":\"apple\"}" \ "http://localhost:54321/auth/v1/token?grant_type=id_token" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithOTPUsingEmail.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithOTPUsingEmail.1.txt index 158e10fa..1e737984 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithOTPUsingEmail.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithOTPUsingEmail.1.txt @@ -1,7 +1,7 @@ curl \ --request POST \ + --header "Apikey: dummy.api.key" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ - --header "apikey: dummy.api.key" \ --data "{\"create_user\":true,\"data\":{\"custom_key\":\"custom_value\"},\"email\":\"example@mail.com\",\"gotrue_meta_security\":{\"captcha_token\":\"dummy-captcha\"}}" \ "http://localhost:54321/auth/v1/otp?redirect_to=https://supabase.com" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithOTPUsingPhone.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithOTPUsingPhone.1.txt index 321a05af..eca5f22c 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithOTPUsingPhone.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithOTPUsingPhone.1.txt @@ -1,7 +1,7 @@ curl \ --request POST \ + --header "Apikey: dummy.api.key" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ - --header "apikey: dummy.api.key" \ --data "{\"create_user\":true,\"data\":{\"custom_key\":\"custom_value\"},\"gotrue_meta_security\":{\"captcha_token\":\"dummy-captcha\"},\"phone\":\"+1 202-918-2132\"}" \ "http://localhost:54321/auth/v1/otp" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithPhoneAndPassword.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithPhoneAndPassword.1.txt index 4e258238..32961127 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithPhoneAndPassword.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignInWithPhoneAndPassword.1.txt @@ -1,7 +1,7 @@ curl \ --request POST \ + --header "Apikey: dummy.api.key" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ - --header "apikey: dummy.api.key" \ --data "{\"password\":\"the.pass\",\"phone\":\"+1 202-918-2132\"}" \ "http://localhost:54321/auth/v1/token?grant_type=password" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignOut.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignOut.1.txt index 53581e45..af98f7e3 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignOut.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignOut.1.txt @@ -1,6 +1,6 @@ curl \ --request POST \ + --header "Apikey: dummy.api.key" \ --header "Authorization: Bearer accesstoken" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ - --header "apikey: dummy.api.key" \ "http://localhost:54321/auth/v1/logout?scope=global" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignOutWithLocalScope.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignOutWithLocalScope.1.txt index 86a4dd75..87aca3fc 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignOutWithLocalScope.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignOutWithLocalScope.1.txt @@ -1,6 +1,6 @@ curl \ --request POST \ + --header "Apikey: dummy.api.key" \ --header "Authorization: Bearer accesstoken" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ - --header "apikey: dummy.api.key" \ "http://localhost:54321/auth/v1/logout?scope=local" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignOutWithOthersScope.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignOutWithOthersScope.1.txt index 8ae5959e..8f912abe 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignOutWithOthersScope.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignOutWithOthersScope.1.txt @@ -1,6 +1,6 @@ curl \ --request POST \ + --header "Apikey: dummy.api.key" \ --header "Authorization: Bearer accesstoken" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ - --header "apikey: dummy.api.key" \ "http://localhost:54321/auth/v1/logout?scope=others" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignUpWithEmailAndPassword.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignUpWithEmailAndPassword.1.txt index 3e9aba91..1d615682 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignUpWithEmailAndPassword.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignUpWithEmailAndPassword.1.txt @@ -1,7 +1,7 @@ curl \ --request POST \ + --header "Apikey: dummy.api.key" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ - --header "apikey: dummy.api.key" \ --data "{\"data\":{\"custom_key\":\"custom_value\"},\"email\":\"example@mail.com\",\"gotrue_meta_security\":{\"captcha_token\":\"dummy-captcha\"},\"password\":\"the.pass\"}" \ "http://localhost:54321/auth/v1/signup?redirect_to=https://supabase.com" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignUpWithPhoneAndPassword.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignUpWithPhoneAndPassword.1.txt index c3a74957..aaaf01d8 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testSignUpWithPhoneAndPassword.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testSignUpWithPhoneAndPassword.1.txt @@ -1,7 +1,7 @@ curl \ --request POST \ + --header "Apikey: dummy.api.key" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ - --header "apikey: dummy.api.key" \ --data "{\"data\":{\"custom_key\":\"custom_value\"},\"gotrue_meta_security\":{\"captcha_token\":\"dummy-captcha\"},\"password\":\"the.pass\",\"phone\":\"+1 202-918-2132\"}" \ "http://localhost:54321/auth/v1/signup" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testUpdateUser.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testUpdateUser.1.txt index 7124704a..ab59fdc7 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testUpdateUser.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testUpdateUser.1.txt @@ -1,8 +1,8 @@ curl \ --request PUT \ + --header "Apikey: dummy.api.key" \ --header "Authorization: Bearer accesstoken" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ - --header "apikey: dummy.api.key" \ --data "{\"data\":{\"custom_key\":\"custom_value\"},\"email\":\"example@mail.com\",\"email_change_token\":\"123456\",\"password\":\"another.pass\",\"phone\":\"+1 202-918-2132\"}" \ "http://localhost:54321/auth/v1/user" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testVerifyOTPUsingEmail.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testVerifyOTPUsingEmail.1.txt index fa3582c3..487034c9 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testVerifyOTPUsingEmail.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testVerifyOTPUsingEmail.1.txt @@ -1,7 +1,7 @@ curl \ --request POST \ + --header "Apikey: dummy.api.key" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ - --header "apikey: dummy.api.key" \ --data "{\"email\":\"example@mail.com\",\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"},\"token\":\"123456\",\"type\":\"magiclink\"}" \ "http://localhost:54321/auth/v1/verify?redirect_to=https://supabase.com" \ No newline at end of file diff --git a/Tests/AuthTests/__Snapshots__/RequestsTests/testVerifyOTPUsingPhone.1.txt b/Tests/AuthTests/__Snapshots__/RequestsTests/testVerifyOTPUsingPhone.1.txt index b193713b..bb069709 100644 --- a/Tests/AuthTests/__Snapshots__/RequestsTests/testVerifyOTPUsingPhone.1.txt +++ b/Tests/AuthTests/__Snapshots__/RequestsTests/testVerifyOTPUsingPhone.1.txt @@ -1,7 +1,7 @@ curl \ --request POST \ + --header "Apikey: dummy.api.key" \ --header "Content-Type: application/json" \ --header "X-Client-Info: gotrue-swift/x.y.z" \ - --header "apikey: dummy.api.key" \ --data "{\"gotrue_meta_security\":{\"captcha_token\":\"captcha-token\"},\"phone\":\"+1 202-918-2132\",\"token\":\"123456\",\"type\":\"sms\"}" \ "http://localhost:54321/auth/v1/verify" \ No newline at end of file diff --git a/Tests/FunctionsTests/FunctionsClientTests.swift b/Tests/FunctionsTests/FunctionsClientTests.swift index f105982d..07727548 100644 --- a/Tests/FunctionsTests/FunctionsClientTests.swift +++ b/Tests/FunctionsTests/FunctionsClientTests.swift @@ -3,17 +3,21 @@ import XCTest @_spi(Internal) import _Helpers @testable import Functions +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + final class FunctionsClientTests: XCTestCase { let url = URL(string: "http://localhost:5432/functions/v1")! let apiKey = "supabase.anon.key" - lazy var sut = FunctionsClient(url: url, headers: ["apikey": apiKey]) + lazy var sut = FunctionsClient(url: url, headers: ["Apikey": apiKey]) func testInvoke() async throws { let url = URL(string: "http://localhost:5432/functions/v1/hello_world")! let _request = ActorIsolated(URLRequest?.none) - let sut = FunctionsClient(url: self.url, headers: ["apikey": apiKey]) { request in + let sut = FunctionsClient(url: self.url, headers: ["Apikey": apiKey]) { request in await _request.setValue(request) return ( Data(), HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! @@ -31,7 +35,7 @@ final class FunctionsClientTests: XCTestCase { XCTAssertEqual(request?.url, url) XCTAssertEqual(request?.httpMethod, "POST") - XCTAssertEqual(request?.value(forHTTPHeaderField: "apikey"), apiKey) + XCTAssertEqual(request?.value(forHTTPHeaderField: "Apikey"), apiKey) XCTAssertEqual(request?.value(forHTTPHeaderField: "X-Custom-Key"), "value") XCTAssertEqual( request?.value(forHTTPHeaderField: "X-Client-Info"), @@ -40,7 +44,7 @@ final class FunctionsClientTests: XCTestCase { } func testInvoke_shouldThrow_URLError_badServerResponse() async { - let sut = FunctionsClient(url: url, headers: ["apikey": apiKey]) { _ in + let sut = FunctionsClient(url: url, headers: ["Apikey": apiKey]) { _ in throw URLError(.badServerResponse) } @@ -56,7 +60,7 @@ final class FunctionsClientTests: XCTestCase { func testInvoke_shouldThrow_FunctionsError_httpError() async { let url = URL(string: "http://localhost:5432/functions/v1/hello_world")! - let sut = FunctionsClient(url: self.url, headers: ["apikey": apiKey]) { _ in + let sut = FunctionsClient(url: self.url, headers: ["Apikey": apiKey]) { _ in ( "error".data(using: .utf8)!, HTTPURLResponse(url: url, statusCode: 300, httpVersion: nil, headerFields: nil)! @@ -77,7 +81,7 @@ final class FunctionsClientTests: XCTestCase { func testInvoke_shouldThrow_FunctionsError_relayError() async { let url = URL(string: "http://localhost:5432/functions/v1/hello_world")! - let sut = FunctionsClient(url: self.url, headers: ["apikey": apiKey]) { _ in + let sut = FunctionsClient(url: self.url, headers: ["Apikey": apiKey]) { _ in ( Data(), HTTPURLResponse( diff --git a/Tests/PostgRESTIntegrationTests/IntegrationTests.swift b/Tests/PostgRESTIntegrationTests/IntegrationTests.swift index 08513c34..d09a3dd9 100644 --- a/Tests/PostgRESTIntegrationTests/IntegrationTests.swift +++ b/Tests/PostgRESTIntegrationTests/IntegrationTests.swift @@ -38,7 +38,7 @@ final class IntegrationTests: XCTestCase { let client = PostgrestClient( url: URL(string: "http://localhost:54321/rest/v1")!, headers: [ - "apikey": + "Apikey": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24iLCJleHAiOjE5ODM4MTI5OTZ9.CRXP1A7WOeoJeXxjNni43kdQwgnWNReilDMblYTn_I0", ] ) diff --git a/Tests/PostgRESTTests/BuildURLRequestTests.swift b/Tests/PostgRESTTests/BuildURLRequestTests.swift index 7eb36bc8..5708b897 100644 --- a/Tests/PostgRESTTests/BuildURLRequestTests.swift +++ b/Tests/PostgRESTTests/BuildURLRequestTests.swift @@ -54,12 +54,12 @@ final class BuildURLRequestTests: XCTestCase { fetch: { request in guard let runningTestCase = await runningTestCase.value else { XCTFail("execute called without a runningTestCase set.") - return (Data(), URLResponse()) + return (Data(), URLResponse.empty()) } await MainActor.run { [runningTestCase] in assertSnapshot( - matching: request, + of: request, as: .curl, named: runningTestCase.name, record: runningTestCase.record, @@ -69,7 +69,7 @@ final class BuildURLRequestTests: XCTestCase { ) } - return (Data(), URLResponse()) + return (Data(), URLResponse.empty()) }, encoder: encoder ) @@ -171,3 +171,16 @@ final class BuildURLRequestTests: XCTestCase { XCTAssertNotNil(clientInfoHeader) } } + +extension URLResponse { + // Windows and Linux don't have the ability to empty initialize a URLResponse like `URLResponse()` so + // We provide a function that can give us the right value on an platform. + // See https://github.com/apple/swift-corelibs-foundation/pull/4778 + fileprivate static func empty() -> URLResponse { + #if os(Windows) || os(Linux) + URLResponse(url: .init(string: "https://supabase.com")!, mimeType: nil, expectedContentLength: 0, textEncodingName: nil) + #else + URLResponse() + #endif + } +} diff --git a/Tests/PostgRESTTests/PostgrestResponseTests.swift b/Tests/PostgRESTTests/PostgrestResponseTests.swift index 83717e0f..f39ddcf0 100644 --- a/Tests/PostgRESTTests/PostgrestResponseTests.swift +++ b/Tests/PostgRESTTests/PostgrestResponseTests.swift @@ -2,6 +2,10 @@ import XCTest @testable import PostgREST +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + class PostgrestResponseTests: XCTestCase { func testInit() { // Prepare data and response diff --git a/Tests/RealtimeTests/RealtimeTests.swift b/Tests/RealtimeTests/RealtimeTests.swift index 78bbb70e..47099691 100644 --- a/Tests/RealtimeTests/RealtimeTests.swift +++ b/Tests/RealtimeTests/RealtimeTests.swift @@ -27,7 +27,7 @@ final class RealtimeTests: XCTestCase { // ) // // let socket = RealtimeClient( -// "\(supabaseUrl)/realtime/v1", params: ["apikey": supabaseKey] +// "\(supabaseUrl)/realtime/v1", params: ["Apikey": supabaseKey] // ) // // let e = expectation(description: "testConnection") @@ -63,7 +63,7 @@ final class RealtimeTests: XCTestCase { // ) // // let client = RealtimeClient( -// "\(supabaseUrl)/realtime/v1", params: ["apikey": supabaseKey] +// "\(supabaseUrl)/realtime/v1", params: ["Apikey": supabaseKey] // ) // let allChanges = client.channel(.all) // allChanges.on(.all) { message in diff --git a/Tests/StorageTests/SupabaseStorageClient+Test.swift b/Tests/StorageTests/SupabaseStorageClient+Test.swift index 0beffb77..e69cf520 100644 --- a/Tests/StorageTests/SupabaseStorageClient+Test.swift +++ b/Tests/StorageTests/SupabaseStorageClient+Test.swift @@ -15,7 +15,7 @@ extension SupabaseStorageClient { url: URL(string: supabaseURL)!, headers: [ "Authorization": "Bearer \(apiKey)", - "apikey": apiKey, + "Apikey": apiKey, ] ) ) diff --git a/Tests/SupabaseTests/SupabaseClientTests.swift b/Tests/SupabaseTests/SupabaseClientTests.swift index 597066ab..a4209150 100644 --- a/Tests/SupabaseTests/SupabaseClientTests.swift +++ b/Tests/SupabaseTests/SupabaseClientTests.swift @@ -20,7 +20,7 @@ final class SupabaseClientTests: XCTestCase { let customHeaders = ["header_field": "header_value"] let client = SupabaseClient( - supabaseURL: "https://project-ref.supabase.co", + supabaseURL: URL(string: "https://project-ref.supabase.co")!, supabaseKey: "ANON_KEY", options: SupabaseClientOptions( db: SupabaseClientOptions.DatabaseOptions(schema: customSchema), @@ -45,7 +45,7 @@ final class SupabaseClientTests: XCTestCase { client.defaultHeaders, [ "X-Client-Info": "supabase-swift/\(Supabase.version)", - "apikey": "ANON_KEY", + "Apikey": "ANON_KEY", "header_field": "header_value", "Authorization": "Bearer ANON_KEY", ] diff --git a/supabase-swift.xcworkspace/xcshareddata/swiftpm/Package.resolved b/supabase-swift.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5c238176..a5416571 100644 --- a/supabase-swift.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/supabase-swift.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -45,6 +45,15 @@ "version" : "1.1.0" } }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "60f13f60c4d093691934dc6cfdf5f508ada1f894", + "version" : "2.6.0" + } + }, { "identity" : "swift-custom-dump", "kind" : "remoteSourceControl",