diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 29dfa686..288087c1 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.23.0" + ".": "2.24.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5544c29c..5251d752 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [2.24.0](https://github.com/supabase/supabase-swift/compare/v2.23.0...v2.24.0) (2024-12-05) + + +### Features + +* **realtime:** pull access token mechanism ([#615](https://github.com/supabase/supabase-swift/issues/615)) ([c88dd36](https://github.com/supabase/supabase-swift/commit/c88dd3675b8bc7da93c71847ff9ba9862323ff8d)) + + +### Bug Fixes + +* **realtime:** prevent sending expired tokens ([#618](https://github.com/supabase/supabase-swift/issues/618)) ([595277b](https://github.com/supabase/supabase-swift/commit/595277b5eb35b8b76bbb000d44fc221c4d3298f1)) + ## [2.23.0](https://github.com/supabase/supabase-swift/compare/v2.22.1...v2.23.0) (2024-11-22) diff --git a/Examples/Examples.xcodeproj/project.pbxproj b/Examples/Examples.xcodeproj/project.pbxproj index 4a19d2bf..f4f32b61 100644 --- a/Examples/Examples.xcodeproj/project.pbxproj +++ b/Examples/Examples.xcodeproj/project.pbxproj @@ -12,7 +12,7 @@ 793895CC2954ABFF0044F2B8 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793895CB2954ABFF0044F2B8 /* RootView.swift */; }; 793895CE2954AC000044F2B8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 793895CD2954AC000044F2B8 /* Assets.xcassets */; }; 793895D22954AC000044F2B8 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 793895D12954AC000044F2B8 /* Preview Assets.xcassets */; }; - 793E03092B2CED5D00AC7DED /* Contants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793E03082B2CED5D00AC7DED /* Contants.swift */; }; + 793E03092B2CED5D00AC7DED /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793E03082B2CED5D00AC7DED /* Constants.swift */; }; 793E030B2B2CEDDA00AC7DED /* ActionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793E030A2B2CEDDA00AC7DED /* ActionState.swift */; }; 793E030D2B2DAB5700AC7DED /* SignInWithApple.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793E030C2B2DAB5700AC7DED /* SignInWithApple.swift */; }; 79401F332BC6FEAE004C9C0F /* SignInWithOAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = 79401F322BC6FEAE004C9C0F /* SignInWithOAuth.swift */; }; @@ -86,7 +86,7 @@ 793895CD2954AC000044F2B8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 793895CF2954AC000044F2B8 /* Examples.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Examples.entitlements; sourceTree = ""; }; 793895D12954AC000044F2B8 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - 793E03082B2CED5D00AC7DED /* Contants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contants.swift; sourceTree = ""; }; + 793E03082B2CED5D00AC7DED /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 793E030A2B2CEDDA00AC7DED /* ActionState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionState.swift; sourceTree = ""; }; 793E030C2B2DAB5700AC7DED /* SignInWithApple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInWithApple.swift; sourceTree = ""; }; 79401F322BC6FEAE004C9C0F /* SignInWithOAuth.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInWithOAuth.swift; sourceTree = ""; }; @@ -226,7 +226,7 @@ 794EF1232955F3DE008C9526 /* TodoListRow.swift */, 796298982AEBBA77000AA957 /* MFAFlow.swift */, 79AF04852B2CE586008761AD /* Debug.swift */, - 793E03082B2CED5D00AC7DED /* Contants.swift */, + 793E03082B2CED5D00AC7DED /* Constants.swift */, 793E030A2B2CEDDA00AC7DED /* ActionState.swift */, 79E2B55B2B97A2310042CD21 /* UIApplicationExtensions.swift */, 797EFB672BABD90500098D6B /* Stringfy.swift */, @@ -507,7 +507,7 @@ 79401F332BC6FEAE004C9C0F /* SignInWithOAuth.swift in Sources */, 79B1C80E2BAC017C00D991AA /* AnyJSONView.swift in Sources */, 79E2B5552B9788BF0042CD21 /* GoogleSignInSDKFlow.swift in Sources */, - 793E03092B2CED5D00AC7DED /* Contants.swift in Sources */, + 793E03092B2CED5D00AC7DED /* Constants.swift in Sources */, 794C61D62BAD1E12000E6B0F /* UserIdentityList.swift in Sources */, 793895CC2954ABFF0044F2B8 /* RootView.swift in Sources */, 7956406A2955AFBD0088A06F /* ErrorText.swift in Sources */, diff --git a/Examples/Examples/Contants.swift b/Examples/Examples/Constants.swift similarity index 95% rename from Examples/Examples/Contants.swift rename to Examples/Examples/Constants.swift index 3dcfdfb1..dd72e28d 100644 --- a/Examples/Examples/Contants.swift +++ b/Examples/Examples/Constants.swift @@ -1,5 +1,5 @@ // -// Contants.swift +// Constants.swift // Examples // // Created by Guilherme Souza on 15/12/23. diff --git a/Package.resolved b/Package.resolved index 7b09421a..2a6d5e7d 100644 --- a/Package.resolved +++ b/Package.resolved @@ -23,8 +23,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-crypto.git", "state" : { - "revision" : "06dc63c6d8da54ee11ceb268cde1fa68161afc96", - "version" : "3.9.1" + "revision" : "ff0f781cf7c6a22d52957e50b104f5768b50c779", + "version" : "3.10.0" } }, { diff --git a/README.md b/README.md index bdad8697..5792f3a8 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ let package = Package( dependencies: [ ... .package( - url: "https://github.com/supabase-community/supabase-swift.git", + url: "https://github.com/supabase/supabase-swift.git", from: "2.0.0" ), ], diff --git a/Sources/Auth/AuthClient.swift b/Sources/Auth/AuthClient.swift index ea192f2c..db25af39 100644 --- a/Sources/Auth/AuthClient.swift +++ b/Sources/Auth/AuthClient.swift @@ -877,7 +877,7 @@ public final class AuthClient: Sendable { var hasExpired = true var session: Session - let jwt = try decode(jwt: accessToken) + let jwt = JWT.decodePayload(accessToken) if let exp = jwt?["exp"] as? TimeInterval { expiresAt = Date(timeIntervalSince1970: exp) hasExpired = expiresAt <= now diff --git a/Sources/Auth/AuthMFA.swift b/Sources/Auth/AuthMFA.swift index fb6ac547..698a54aa 100644 --- a/Sources/Auth/AuthMFA.swift +++ b/Sources/Auth/AuthMFA.swift @@ -134,7 +134,7 @@ public struct AuthMFA: Sendable { { do { let session = try await sessionManager.session() - let payload = try decode(jwt: session.accessToken) + let payload = JWT.decodePayload(session.accessToken) var currentLevel: AuthenticatorAssuranceLevels? diff --git a/Sources/Auth/Internal/Contants.swift b/Sources/Auth/Internal/Constants.swift similarity index 81% rename from Sources/Auth/Internal/Contants.swift rename to Sources/Auth/Internal/Constants.swift index 7b99330f..d37f4955 100644 --- a/Sources/Auth/Internal/Contants.swift +++ b/Sources/Auth/Internal/Constants.swift @@ -1,5 +1,5 @@ // -// Contants.swift +// Constants.swift // // // Created by Guilherme Souza on 22/05/24. @@ -33,9 +33,9 @@ struct APIVersion { } static func date(for name: Name) -> Date { - let formattar = ISO8601DateFormatter() - formattar.formatOptions = [.withInternetDateTime, .withFractionalSeconds] - return formattar.date(from: "\(name.rawValue)T00:00:00.0Z")! + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + return formatter.date(from: "\(name.rawValue)T00:00:00.0Z")! } } diff --git a/Sources/Auth/Internal/Helpers.swift b/Sources/Auth/Internal/Helpers.swift index 07d2a02a..de321a30 100644 --- a/Sources/Auth/Internal/Helpers.swift +++ b/Sources/Auth/Internal/Helpers.swift @@ -39,33 +39,3 @@ private func extractParams(from fragment: String) -> [URLQueryItem] { : nil } } - -func decode(jwt: String) throws -> [String: Any]? { - let parts = jwt.split(separator: ".") - guard parts.count == 3 else { - return nil - } - - let payload = String(parts[1]) - guard let data = base64URLDecode(payload) else { - return nil - } - let json = try JSONSerialization.jsonObject(with: data, options: []) - guard let decodedPayload = json as? [String: Any] else { - return nil - } - return decodedPayload -} - -private func base64URLDecode(_ value: String) -> Data? { - var base64 = value.replacingOccurrences(of: "-", with: "+") - .replacingOccurrences(of: "_", with: "/") - let length = Double(base64.lengthOfBytes(using: .utf8)) - let requiredLength = 4 * ceil(length / 4.0) - let paddingLength = requiredLength - length - if paddingLength > 0 { - let padding = "".padding(toLength: Int(paddingLength), withPad: "=", startingAt: 0) - base64 = base64 + padding - } - return Data(base64Encoded: base64, options: .ignoreUnknownCharacters) -} diff --git a/Sources/Helpers/JWT.swift b/Sources/Helpers/JWT.swift new file mode 100644 index 00000000..86dfb5d0 --- /dev/null +++ b/Sources/Helpers/JWT.swift @@ -0,0 +1,40 @@ +// +// JWT.swift +// Supabase +// +// Created by Guilherme Souza on 28/11/24. +// + +import Foundation + +package enum JWT { + package static func decodePayload(_ jwt: String) -> [String: Any]? { + let parts = jwt.split(separator: ".") + guard parts.count == 3 else { + return nil + } + + let payload = String(parts[1]) + guard let data = base64URLDecode(payload) else { + return nil + } + let json = try? JSONSerialization.jsonObject(with: data, options: []) + guard let decodedPayload = json as? [String: Any] else { + return nil + } + return decodedPayload + } + + private static func base64URLDecode(_ value: String) -> Data? { + var base64 = value.replacingOccurrences(of: "-", with: "+") + .replacingOccurrences(of: "_", with: "/") + let length = Double(base64.lengthOfBytes(using: .utf8)) + let requiredLength = 4 * ceil(length / 4.0) + let paddingLength = requiredLength - length + if paddingLength > 0 { + let padding = "".padding(toLength: Int(paddingLength), withPad: "=", startingAt: 0) + base64 = base64 + padding + } + return Data(base64Encoded: base64, options: .ignoreUnknownCharacters) + } +} diff --git a/Sources/Helpers/Version.swift b/Sources/Helpers/Version.swift index 4c9f63df..d0964296 100644 --- a/Sources/Helpers/Version.swift +++ b/Sources/Helpers/Version.swift @@ -1 +1 @@ -package let version = "2.23.0" // {x-release-please-version} +package let version = "2.24.0" // {x-release-please-version} diff --git a/Sources/Realtime/V2/RealtimeChannelV2.swift b/Sources/Realtime/V2/RealtimeChannelV2.swift index e9c74017..0e5d99c7 100644 --- a/Sources/Realtime/V2/RealtimeChannelV2.swift +++ b/Sources/Realtime/V2/RealtimeChannelV2.swift @@ -29,7 +29,7 @@ struct Socket: Sendable { var broadcastURL: @Sendable () -> URL var status: @Sendable () -> RealtimeClientStatus var options: @Sendable () -> RealtimeClientOptions - var accessToken: @Sendable () -> String? + var accessToken: @Sendable () async -> String? var apiKey: @Sendable () -> String? var makeRef: @Sendable () -> Int @@ -50,7 +50,12 @@ extension Socket { broadcastURL: { [weak client] in client?.broadcastURL ?? URL(string: "http://localhost")! }, status: { [weak client] in client?.status ?? .disconnected }, options: { [weak client] in client?.options ?? .init() }, - accessToken: { [weak client] in client?.mutableState.accessToken }, + accessToken: { [weak client] in + if let accessToken = try? await client?.options.accessToken?() { + return accessToken + } + return client?.mutableState.accessToken + }, apiKey: { [weak client] in client?.apikey }, makeRef: { [weak client] in client?.makeRef() ?? 0 }, connect: { [weak client] in await client?.connect() }, @@ -143,7 +148,7 @@ public final class RealtimeChannelV2: Sendable { let payload = RealtimeJoinPayload( config: joinConfig, - accessToken: socket.accessToken() + accessToken: await socket.accessToken() ) let joinRef = socket.makeRef().description @@ -217,7 +222,7 @@ public final class RealtimeChannelV2: Sendable { if let apiKey = socket.apiKey() { headers[.apiKey] = apiKey } - if let accessToken = socket.accessToken() { + if let accessToken = await socket.accessToken() { headers[.authorization] = "Bearer \(accessToken)" } diff --git a/Sources/Realtime/V2/RealtimeClientV2.swift b/Sources/Realtime/V2/RealtimeClientV2.swift index 656b3ff5..cc2c1499 100644 --- a/Sources/Realtime/V2/RealtimeClientV2.swift +++ b/Sources/Realtime/V2/RealtimeClientV2.swift @@ -115,7 +115,11 @@ public final class RealtimeClientV2: Sendable { apikey = options.apikey mutableState.withValue { - $0.accessToken = options.accessToken ?? options.apikey + if let accessToken = options.headers[.authorization]?.split(separator: " ").last { + $0.accessToken = String(accessToken) + } else { + $0.accessToken = options.apikey + } } } @@ -369,9 +373,31 @@ public final class RealtimeClientV2: Sendable { } /// Sets the JWT access token used for channel subscription authorization and Realtime RLS. - /// - Parameter token: A JWT string. - public func setAuth(_ token: String?) async { - mutableState.withValue { + /// + /// If `token` is nil it will use the ``RealtimeClientOptions/accessToken`` callback function or the token set on the client. + /// + /// On callback used, it will set the value of the token internal to the client. + /// - Parameter token: A JWT string to override the token set on the client. + public func setAuth(_ token: String? = nil) async { + var token = token + + if token == nil { + token = try? await options.accessToken?() + } + + if token == nil { + token = mutableState.accessToken + } + + if let token, let payload = JWT.decodePayload(token), + let exp = payload["exp"] as? TimeInterval, exp < Date().timeIntervalSince1970 + { + options.logger?.warning( + "InvalidJWTToken: Invalid value for JWT claim \"exp\" with value \(exp)") + return + } + + mutableState.withValue { [token] in $0.accessToken = token } diff --git a/Sources/Realtime/V2/Types.swift b/Sources/Realtime/V2/Types.swift index 116ce8c2..3df4579c 100644 --- a/Sources/Realtime/V2/Types.swift +++ b/Sources/Realtime/V2/Types.swift @@ -28,6 +28,7 @@ public struct RealtimeClientOptions: Sendable { _ bodyData: Data? ) async throws -> (Data, HTTPResponse) )? + package var accessToken: (@Sendable () async throws -> String)? package var logger: (any SupabaseLogger)? public static let defaultHeartbeatInterval: TimeInterval = 15 @@ -49,6 +50,7 @@ public struct RealtimeClientOptions: Sendable { _ bodyData: Data? ) async throws -> (Data, HTTPResponse) )? = nil, + accessToken: (@Sendable () async throws -> String)? = nil, logger: (any SupabaseLogger)? = nil ) { self.headers = headers @@ -58,19 +60,13 @@ public struct RealtimeClientOptions: Sendable { self.disconnectOnSessionLoss = disconnectOnSessionLoss self.connectOnSubscribe = connectOnSubscribe self.fetch = fetch + self.accessToken = accessToken self.logger = logger } var apikey: String? { headers[.apiKey] } - - var accessToken: String? { - guard let accessToken = headers[.authorization]?.split(separator: " ").last else { - return nil - } - return String(accessToken) - } } public typealias RealtimeSubscription = ObservationToken diff --git a/Sources/Supabase/SupabaseClient.swift b/Sources/Supabase/SupabaseClient.swift index 93abdff5..71fd4456 100644 --- a/Sources/Supabase/SupabaseClient.swift +++ b/Sources/Supabase/SupabaseClient.swift @@ -33,10 +33,11 @@ public final class SupabaseClient: Sendable { /// Supabase Auth allows you to create and manage user sessions for access to data that is secured by access policies. public var auth: AuthClient { if options.auth.accessToken != nil { - reportIssue(""" - Supabase Client is configured with the auth.accessToken option, - accessing supabase.auth is not possible. - """) + reportIssue( + """ + Supabase Client is configured with the auth.accessToken option, + accessing supabase.auth is not possible. + """) } return _auth } @@ -92,7 +93,14 @@ public final class SupabaseClient: Sendable { let _realtime: UncheckedSendable /// Realtime client for Supabase - public let realtimeV2: RealtimeClientV2 + public var realtimeV2: RealtimeClientV2 { + mutableState.withValue { + if $0.realtime == nil { + $0.realtime = _initRealtimeClient() + } + return $0.realtime! + } + } /// Supabase Functions allows you to deploy and invoke edge functions. public var functions: FunctionsClient { @@ -127,6 +135,7 @@ public final class SupabaseClient: Sendable { var storage: SupabaseStorageClient? var rest: PostgrestClient? var functions: FunctionsClient? + var realtime: RealtimeClientV2? var changedAccessToken: String? } @@ -208,16 +217,6 @@ public final class SupabaseClient: Sendable { ) ) - var realtimeOptions = options.realtime - realtimeOptions.headers.merge(self.headers) { $1 } - if realtimeOptions.logger == nil { - realtimeOptions.logger = options.global.logger - } - - realtimeV2 = RealtimeClientV2( - url: supabaseURL.appendingPathComponent("/realtime/v1"), - options: realtimeOptions - ) if options.auth.accessToken == nil { listenForAuthEvents() @@ -369,12 +368,7 @@ public final class SupabaseClient: Sendable { } private func adapt(request: HTTPRequest) async -> HTTPRequest { - let token: String? = - if let accessToken = options.auth.accessToken { - try? await accessToken() - } else { - try? await auth.session.accessToken - } + let token = try? await _getAccessToken() var request = request if let token { @@ -383,6 +377,14 @@ public final class SupabaseClient: Sendable { return request } + private func _getAccessToken() async throws -> String { + if let accessToken = options.auth.accessToken { + try await accessToken() + } else { + try await auth.session.accessToken + } + } + private func listenForAuthEvents() { let task = Task { for await (event, session) in auth.authStateChanges { @@ -396,7 +398,9 @@ public final class SupabaseClient: Sendable { private func handleTokenChanged(event: AuthChangeEvent, session: Session?) async { let accessToken: String? = mutableState.withValue { - if [.initialSession, .signedIn, .tokenRefreshed].contains(event), $0.changedAccessToken != session?.accessToken { + if [.initialSession, .signedIn, .tokenRefreshed].contains(event), + $0.changedAccessToken != session?.accessToken + { $0.changedAccessToken = session?.accessToken return session?.accessToken ?? supabaseKey } @@ -412,4 +416,33 @@ public final class SupabaseClient: Sendable { realtime.setAuth(accessToken) await realtimeV2.setAuth(accessToken) } + + private func _initRealtimeClient() -> RealtimeClientV2 { + var realtimeOptions = options.realtime + realtimeOptions.headers.merge(self.headers) { $1 } + + if realtimeOptions.logger == nil { + realtimeOptions.logger = options.global.logger + } + + if realtimeOptions.accessToken == nil { + realtimeOptions.accessToken = { [weak self] in + try await self?._getAccessToken() ?? "" + } + } else { + reportIssue( + """ + You assigned a custom `accessToken` closure to the RealtimeClientV2. This might not work as you expect + as SupabaseClient uses Auth for pulling an access token to send on the realtime channels. + + Please make sure you know what you're doing. + """ + ) + } + + return RealtimeClientV2( + url: supabaseURL.appendingPathComponent("/realtime/v1"), + options: realtimeOptions + ) + } } diff --git a/Supabase.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Supabase.xcworkspace/xcshareddata/swiftpm/Package.resolved index 21ad5848..673d45b6 100644 --- a/Supabase.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Supabase.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -5,8 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/openid/AppAuth-iOS.git", "state" : { - "revision" : "c89ed571ae140f8eb1142735e6e23d7bb8c34cb2", - "version" : "1.7.5" + "revision" : "2781038865a80e2c425a1da12cc1327bcd56501f", + "version" : "1.7.6" } }, { @@ -45,13 +45,22 @@ "version" : "1.0.6" } }, + { + "identity" : "swift-asn1", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-asn1.git", + "state" : { + "revision" : "7faebca1ea4f9aaf0cda1cef7c43aecd2311ddf6", + "version" : "1.3.0" + } + }, { "identity" : "swift-case-paths", "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-case-paths", "state" : { - "revision" : "642e6aab8e03e5f992d9c83e38c5be98cfad5078", - "version" : "1.5.5" + "revision" : "bc92c4b27f9a84bfb498cdbfdf35d5a357e9161f", + "version" : "1.5.6" } }, { @@ -59,8 +68,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections", "state" : { - "revision" : "9bf03ff58ce34478e66aaee630e491823326fd06", - "version" : "1.1.3" + "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", + "version" : "1.1.4" } }, { @@ -68,8 +77,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-concurrency-extras", "state" : { - "revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71", - "version" : "1.1.0" + "revision" : "163409ef7dae9d960b87f34b51587b6609a76c1f", + "version" : "1.3.0" } }, { @@ -77,8 +86,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-crypto.git", "state" : { - "revision" : "9f95b4d033a4edd3814b48608db3f2ca90c7218b", - "version" : "3.7.0" + "revision" : "ff0f781cf7c6a22d52957e50b104f5768b50c779", + "version" : "3.10.0" } }, { @@ -93,10 +102,10 @@ { "identity" : "swift-http-types", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-http-types", + "location" : "https://github.com/apple/swift-http-types.git", "state" : { - "revision" : "ae67c8178eb46944fd85e4dc6dd970e1f3ed6ccd", - "version" : "1.3.0" + "revision" : "ef18d829e8b92d731ad27bb81583edd2094d1ce3", + "version" : "1.3.1" } }, { @@ -113,8 +122,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/swift-snapshot-testing", "state" : { - "revision" : "6d932a79e7173b275b96c600c86c603cf84f153c", - "version" : "1.17.4" + "revision" : "42a086182681cf661f5c47c9b7dc3931de18c6d7", + "version" : "1.17.6" } }, { @@ -122,8 +131,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/swiftlang/swift-syntax", "state" : { - "revision" : "515f79b522918f83483068d99c68daeb5116342d", - "version" : "600.0.0-prerelease-2024-08-20" + "revision" : "0687f71944021d616d34d922343dcef086855920", + "version" : "600.0.1" } }, { @@ -140,8 +149,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", "state" : { - "revision" : "96beb108a57f24c8476ae1f309239270772b2940", - "version" : "1.2.5" + "revision" : "a3f634d1a409c7979cabc0a71b3f26ffa9fc8af1", + "version" : "1.4.3" } } ], diff --git a/Tests/AuthTests/JWTTests.swift b/Tests/HelpersTests/JWTTests.swift similarity index 84% rename from Tests/AuthTests/JWTTests.swift rename to Tests/HelpersTests/JWTTests.swift index f505a05d..9e8161b9 100644 --- a/Tests/AuthTests/JWTTests.swift +++ b/Tests/HelpersTests/JWTTests.swift @@ -1,13 +1,13 @@ import XCTest -@testable import Auth +@testable import Helpers final class JWTTests: XCTestCase { func testDecodeJWT() throws { let token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNjQ4NjQwMDIxLCJzdWIiOiJmMzNkM2VjOS1hMmVlLTQ3YzQtODBlMS01YmQ5MTlmM2Q4YjgiLCJlbWFpbCI6ImhpQGJpbmFyeXNjcmFwaW5nLmNvIiwicGhvbmUiOiIiLCJhcHBfbWV0YWRhdGEiOnsicHJvdmlkZXIiOiJlbWFpbCIsInByb3ZpZGVycyI6WyJlbWFpbCJdfSwidXNlcl9tZXRhZGF0YSI6e30sInJvbGUiOiJhdXRoZW50aWNhdGVkIn0.CGr5zNE5Yltlbn_3Ms2cjSLs_AW9RKM3lxh7cTQrg0w" - let jwt = try decode(jwt: token) + let jwt = JWT.decodePayload(token) let exp = try XCTUnwrap(jwt?["exp"] as? TimeInterval) - XCTAssertEqual(exp, 1648640021) + XCTAssertEqual(exp, 1_648_640_021) } } diff --git a/Tests/IntegrationTests/Potsgrest/PostgresTransformsTests.swift b/Tests/IntegrationTests/Postgrest/PostgresTransformsTests.swift similarity index 100% rename from Tests/IntegrationTests/Potsgrest/PostgresTransformsTests.swift rename to Tests/IntegrationTests/Postgrest/PostgresTransformsTests.swift diff --git a/Tests/IntegrationTests/Potsgrest/PostgrestBasicTests.swift b/Tests/IntegrationTests/Postgrest/PostgrestBasicTests.swift similarity index 100% rename from Tests/IntegrationTests/Potsgrest/PostgrestBasicTests.swift rename to Tests/IntegrationTests/Postgrest/PostgrestBasicTests.swift diff --git a/Tests/IntegrationTests/Potsgrest/PostgrestFilterTests.swift b/Tests/IntegrationTests/Postgrest/PostgrestFilterTests.swift similarity index 81% rename from Tests/IntegrationTests/Potsgrest/PostgrestFilterTests.swift rename to Tests/IntegrationTests/Postgrest/PostgrestFilterTests.swift index b6fa13be..1ce7cdd4 100644 --- a/Tests/IntegrationTests/Potsgrest/PostgrestFilterTests.swift +++ b/Tests/IntegrationTests/Postgrest/PostgrestFilterTests.swift @@ -21,7 +21,8 @@ final class PostgrestFilterTests: XCTestCase { ) func testNot() async throws { - let res = try await client.from("users") + let res = + try await client.from("users") .select("status") .not("status", operator: .eq, value: "OFFLINE") .execute() @@ -44,7 +45,8 @@ final class PostgrestFilterTests: XCTestCase { } func testOr() async throws { - let res = try await client.from("users") + let res = + try await client.from("users") .select("status,username") .or("status.eq.OFFLINE,username.eq.supabot") .execute() @@ -66,7 +68,8 @@ final class PostgrestFilterTests: XCTestCase { } func testEq() async throws { - let res = try await client.from("users") + let res = + try await client.from("users") .select("username") .eq("username", value: "supabot") .execute() @@ -83,7 +86,8 @@ final class PostgrestFilterTests: XCTestCase { } func testNeq() async throws { - let res = try await client.from("users") + let res = + try await client.from("users") .select("username") .neq("username", value: "supabot") .execute() @@ -106,7 +110,8 @@ final class PostgrestFilterTests: XCTestCase { } func testGt() async throws { - let res = try await client.from("messages") + let res = + try await client.from("messages") .select("id") .gt("id", value: 1) .execute() @@ -123,7 +128,8 @@ final class PostgrestFilterTests: XCTestCase { } func testGte() async throws { - let res = try await client.from("messages") + let res = + try await client.from("messages") .select("id") .gte("id", value: 1) .execute() @@ -143,7 +149,8 @@ final class PostgrestFilterTests: XCTestCase { } func testLe() async throws { - let res = try await client.from("messages") + let res = + try await client.from("messages") .select("id") .lt("id", value: 2) .execute() @@ -160,7 +167,8 @@ final class PostgrestFilterTests: XCTestCase { } func testLte() async throws { - let res = try await client.from("messages") + let res = + try await client.from("messages") .select("id") .lte("id", value: 2) .execute() @@ -180,7 +188,8 @@ final class PostgrestFilterTests: XCTestCase { } func testLike() async throws { - let res = try await client.from("users") + let res = + try await client.from("users") .select("username") .like("username", pattern: "%supa%") .execute() @@ -197,7 +206,8 @@ final class PostgrestFilterTests: XCTestCase { } func testLikeAllOf() async throws { - let res = try await client.from("users") + let res = + try await client.from("users") .select("username") .likeAllOf("username", patterns: ["%supa%", "%bot%"]) .execute() @@ -214,7 +224,8 @@ final class PostgrestFilterTests: XCTestCase { } func testLikeAnyOf() async throws { - let res = try await client.from("users") + let res = + try await client.from("users") .select("username") .likeAnyOf("username", patterns: ["%supa%", "%kiwi%"]) .execute() @@ -234,7 +245,8 @@ final class PostgrestFilterTests: XCTestCase { } func testIlike() async throws { - let res = try await client.from("users") + let res = + try await client.from("users") .select("username") .ilike("username", pattern: "%SUPA%") .execute() @@ -251,7 +263,8 @@ final class PostgrestFilterTests: XCTestCase { } func testIlikeAllOf() async throws { - let res = try await client.from("users") + let res = + try await client.from("users") .select("username") .iLikeAllOf("username", patterns: ["%SUPA%", "%bot%"]) .execute() @@ -268,7 +281,8 @@ final class PostgrestFilterTests: XCTestCase { } func testIlikeAnyOf() async throws { - let res = try await client.from("users") + let res = + try await client.from("users") .select("username") .iLikeAnyOf("username", patterns: ["%supa%", "%KIWI%"]) .execute() @@ -288,7 +302,8 @@ final class PostgrestFilterTests: XCTestCase { } func testIs() async throws { - let res = try await client.from("users").select("data").is("data", value: nil) + let res = + try await client.from("users").select("data").is("data", value: nil) .execute() .value as AnyJSON @@ -314,7 +329,8 @@ final class PostgrestFilterTests: XCTestCase { func testIn() async throws { let statuses = ["ONLINE", "OFFLINE"] - let res = try await client.from("users").select("status").in("status", values: statuses) + let res = + try await client.from("users").select("status").in("status", values: statuses) .execute() .value as AnyJSON @@ -339,7 +355,8 @@ final class PostgrestFilterTests: XCTestCase { } func testContains() async throws { - let res = try await client.from("users").select("age_range").contains("age_range", value: "[1,2)") + let res = + try await client.from("users").select("age_range").contains("age_range", value: "[1,2)") .execute() .value as AnyJSON @@ -355,7 +372,8 @@ final class PostgrestFilterTests: XCTestCase { } func testContainedBy() async throws { - let res = try await client.from("users").select("age_range").containedBy("age_range", value: "[1,2)") + let res = + try await client.from("users").select("age_range").containedBy("age_range", value: "[1,2)") .execute() .value as AnyJSON @@ -371,7 +389,8 @@ final class PostgrestFilterTests: XCTestCase { } func testRangeLt() async throws { - let res = try await client.from("users").select("age_range").rangeLt("age_range", range: "[2,25)") + let res = + try await client.from("users").select("age_range").rangeLt("age_range", range: "[2,25)") .execute() .value as AnyJSON @@ -387,7 +406,8 @@ final class PostgrestFilterTests: XCTestCase { } func testRangeGt() async throws { - let res = try await client.from("users").select("age_range").rangeGt("age_range", range: "[2,25)") + let res = + try await client.from("users").select("age_range").rangeGt("age_range", range: "[2,25)") .execute() .value as AnyJSON @@ -406,7 +426,8 @@ final class PostgrestFilterTests: XCTestCase { } func testRangeLte() async throws { - let res = try await client.from("users").select("age_range").rangeLte("age_range", range: "[2,25)") + let res = + try await client.from("users").select("age_range").rangeLte("age_range", range: "[2,25)") .execute() .value as AnyJSON @@ -422,7 +443,8 @@ final class PostgrestFilterTests: XCTestCase { } func testRangeGte() async throws { - let res = try await client.from("users").select("age_range").rangeGte("age_range", range: "[2,25)") + let res = + try await client.from("users").select("age_range").rangeGte("age_range", range: "[2,25)") .execute() .value as AnyJSON @@ -444,7 +466,8 @@ final class PostgrestFilterTests: XCTestCase { } func testRangeAdjacent() async throws { - let res = try await client.from("users").select("age_range").rangeAdjacent("age_range", range: "[2,25)") + let res = + try await client.from("users").select("age_range").rangeAdjacent("age_range", range: "[2,25)") .execute() .value as AnyJSON @@ -466,7 +489,8 @@ final class PostgrestFilterTests: XCTestCase { } func testOverlaps() async throws { - let res = try await client.from("users").select("age_range").overlaps("age_range", value: "[2,25)") + let res = + try await client.from("users").select("age_range").overlaps("age_range", value: "[2,25)") .execute() .value as AnyJSON @@ -482,7 +506,8 @@ final class PostgrestFilterTests: XCTestCase { } func testTextSearch() async throws { - let res = try await client.from("users").select("catchphrase") + let res = + try await client.from("users").select("catchphrase") .textSearch("catchphrase", query: "'fat' & 'cat'", config: "english") .execute() .value as AnyJSON @@ -499,7 +524,8 @@ final class PostgrestFilterTests: XCTestCase { } func testTextSearchWithPlain() async throws { - let res = try await client.from("users").select("catchphrase") + let res = + try await client.from("users").select("catchphrase") .textSearch("catchphrase", query: "'fat' & 'cat'", config: "english", type: .plain) .execute() .value as AnyJSON @@ -516,7 +542,8 @@ final class PostgrestFilterTests: XCTestCase { } func testTextSearchWithPhrase() async throws { - let res = try await client.from("users").select("catchphrase") + let res = + try await client.from("users").select("catchphrase") .textSearch("catchphrase", query: "cat", config: "english", type: .phrase) .execute() .value as AnyJSON @@ -536,7 +563,8 @@ final class PostgrestFilterTests: XCTestCase { } func testTextSearchWithWebsearch() async throws { - let res = try await client.from("users").select("catchphrase") + let res = + try await client.from("users").select("catchphrase") .textSearch("catchphrase", query: "'fat' & 'cat'", config: "english", type: .websearch) .execute() .value as AnyJSON @@ -553,7 +581,8 @@ final class PostgrestFilterTests: XCTestCase { } func testMultipleFilters() async throws { - let res = try await client.from("users") + let res = + try await client.from("users") .select() .eq("username", value: "supabot") .is("data", value: nil) @@ -579,7 +608,8 @@ final class PostgrestFilterTests: XCTestCase { } func testFilter() async throws { - let res = try await client.from("users") + let res = + try await client.from("users") .select("username") .filter("username", operator: "eq", value: "supabot") .execute() @@ -597,7 +627,8 @@ final class PostgrestFilterTests: XCTestCase { } func testMatch() async throws { - let res = try await client.from("users") + let res = + try await client.from("users") .select("username,status") .match(["username": "supabot", "status": "ONLINE"]) .execute() @@ -616,7 +647,8 @@ final class PostgrestFilterTests: XCTestCase { } func testFilterOnRpc() async throws { - let res = try await client.rpc("get_username_and_status", params: ["name_param": "supabot"]) + let res = + try await client.rpc("get_username_and_status", params: ["name_param": "supabot"]) .neq("status", value: "ONLINE") .execute() .value as AnyJSON diff --git a/Tests/IntegrationTests/Potsgrest/PostgrestResourceEmbeddingTests.swift b/Tests/IntegrationTests/Postgrest/PostgrestResourceEmbeddingTests.swift similarity index 100% rename from Tests/IntegrationTests/Potsgrest/PostgrestResourceEmbeddingTests.swift rename to Tests/IntegrationTests/Postgrest/PostgrestResourceEmbeddingTests.swift diff --git a/Tests/RealtimeTests/RealtimeTests.swift b/Tests/RealtimeTests/RealtimeTests.swift index b7956208..73e3d6f2 100644 --- a/Tests/RealtimeTests/RealtimeTests.swift +++ b/Tests/RealtimeTests/RealtimeTests.swift @@ -37,7 +37,10 @@ final class RealtimeTests: XCTestCase { headers: [.apiKey: apiKey], heartbeatInterval: 1, reconnectDelay: 1, - timeoutInterval: 2 + timeoutInterval: 2, + accessToken: { + "custom.access.token" + } ), ws: ws, http: http @@ -101,7 +104,7 @@ final class RealtimeTests: XCTestCase { "event" : "phx_join", "join_ref" : "1", "payload" : { - "access_token" : "anon.api.key", + "access_token" : "custom.access.token", "config" : { "broadcast" : { "ack" : false, @@ -180,7 +183,7 @@ final class RealtimeTests: XCTestCase { "event" : "phx_join", "join_ref" : "1", "payload" : { - "access_token" : "anon.api.key", + "access_token" : "custom.access.token", "config" : { "broadcast" : { "ack" : false, @@ -202,7 +205,7 @@ final class RealtimeTests: XCTestCase { "event" : "phx_join", "join_ref" : "2", "payload" : { - "access_token" : "anon.api.key", + "access_token" : "custom.access.token", "config" : { "broadcast" : { "ack" : false, @@ -321,7 +324,7 @@ final class RealtimeTests: XCTestCase { assertInlineSnapshot(of: urlRequest as? URLRequest, as: .raw(pretty: true)) { """ POST https://localhost:54321/realtime/v1/api/broadcast - Authorization: Bearer anon.api.key + Authorization: Bearer custom.access.token Content-Type: application/json apiKey: anon.api.key @@ -341,6 +344,27 @@ final class RealtimeTests: XCTestCase { } } + func testSetAuth() async { + let validToken = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjY0MDkyMjExMjAwfQ.GfiEKLl36X8YWcatHg31jRbilovlGecfUKnOyXMSX9c" + await sut.setAuth(validToken) + + XCTAssertEqual(sut.mutableState.accessToken, validToken) + } + + func testSetAuthWithExpiredToken() async throws { + let expiredToken = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOi02NDA5MjIxMTIwMH0.tnbZRC8vEyK3zaxPxfOjNgvpnuum18dxYlXeHJ4r7u8" + await sut.setAuth(expiredToken) + + XCTAssertNotEqual(sut.mutableState.accessToken, expiredToken) + } + + func testSetAuthWithNonJWT() async throws { + let token = "sb-token" + await sut.setAuth(token) + } + private func connectSocketAndWait() async { ws.mockConnect(.connected) await sut.connect()