diff --git a/FirebaseFunctions/Sources/Internal/FunctionsContext.swift b/FirebaseFunctions/Sources/Internal/FunctionsContext.swift index 7daaa6032d1..5a2f24ec41b 100644 --- a/FirebaseFunctions/Sources/Internal/FunctionsContext.swift +++ b/FirebaseFunctions/Sources/Internal/FunctionsContext.swift @@ -36,12 +36,53 @@ struct FunctionsContextProvider { self.appCheck = appCheck } - // TODO: Implement async await version -// @available(macOS 10.15.0, *) -// internal func getContext() async throws -> FunctionsContext { -// return FunctionsContext(authToken: nil, fcmToken: nil, appCheckToken: nil) -// -// } + @available(iOS 13, macCatalyst 13, macOS 10.15, tvOS 13, watchOS 7, *) + func context(options: HTTPSCallableOptions?) async throws -> FunctionsContext { + async let authToken = auth?.getToken(forcingRefresh: false) + async let appCheckToken = getAppCheckToken(options: options) + async let limitedUseAppCheckToken = getLimitedUseAppCheckToken(options: options) + + // Only `authToken` is throwing, but the formatter script removes the `try` + // from `try authToken` and puts it in front of the initializer call. + return try await FunctionsContext( + authToken: authToken, + fcmToken: messaging?.fcmToken, + appCheckToken: appCheckToken, + limitedUseAppCheckToken: limitedUseAppCheckToken + ) + } + + @available(iOS 13, macCatalyst 13, macOS 10.15, tvOS 13, watchOS 7, *) + private func getAppCheckToken(options: HTTPSCallableOptions?) async -> String? { + guard + options?.requireLimitedUseAppCheckTokens != true, + let tokenResult = await appCheck?.getToken(forcingRefresh: false), + tokenResult.error == nil + else { return nil } + return tokenResult.token + } + + @available(iOS 13, macCatalyst 13, macOS 10.15, tvOS 13, watchOS 7, *) + private func getLimitedUseAppCheckToken(options: HTTPSCallableOptions?) async -> String? { + // At the moment, `await` doesn’t get along with Objective-C’s optional protocol methods. + await withCheckedContinuation { (continuation: CheckedContinuation) in + guard + options?.requireLimitedUseAppCheckTokens == true, + let appCheck, + // `getLimitedUseToken(completion:)` is an optional protocol method. Optional binding + // is performed to make sure `continuation` is called even if the method’s not implemented. + let limitedUseTokenClosure = appCheck.getLimitedUseToken + else { + return continuation.resume(returning: nil) + } + + limitedUseTokenClosure { tokenResult in + // Make sure there’s no error and the token is valid: + guard tokenResult.error == nil else { return continuation.resume(returning: nil) } + continuation.resume(returning: tokenResult.token) + } + } + } func getContext(options: HTTPSCallableOptions? = nil, _ completion: @escaping ((FunctionsContext, Error?) -> Void)) { diff --git a/FirebaseFunctions/Tests/Unit/ContextProviderTests.swift b/FirebaseFunctions/Tests/Unit/ContextProviderTests.swift index db565bcdb01..089c2d9f9ef 100644 --- a/FirebaseFunctions/Tests/Unit/ContextProviderTests.swift +++ b/FirebaseFunctions/Tests/Unit/ContextProviderTests.swift @@ -35,6 +35,17 @@ class ContextProviderTests: XCTestCase { let appCheckTokenSuccess = FIRAppCheckTokenResultFake(token: "valid_token", error: nil) let messagingFake = FIRMessagingInteropFake() + func testAsyncContextWithAuth() async throws { + let auth = FIRAuthInteropFake(token: "token", userID: "userID", error: nil) + let provider = FunctionsContextProvider(auth: auth, messaging: messagingFake, appCheck: nil) + + let context = try await provider.context(options: nil) + + XCTAssertNotNil(context) + XCTAssertEqual(context.authToken, "token") + XCTAssertEqual(context.fcmToken, messagingFake.fcmToken) + } + func testContextWithAuth() { let auth = FIRAuthInteropFake(token: "token", userID: "userID", error: nil) let provider = FunctionsContextProvider(auth: auth, messaging: messagingFake, appCheck: nil) @@ -49,6 +60,19 @@ class ContextProviderTests: XCTestCase { waitForExpectations(timeout: 0.1) } + func testAsyncContextWithAuthError() async { + let authError = NSError(domain: "com.functions.tests", code: 4, userInfo: nil) + let auth = FIRAuthInteropFake(token: nil, userID: "userID", error: authError) + let provider = FunctionsContextProvider(auth: auth, messaging: messagingFake, appCheck: nil) + + do { + _ = try await provider.context(options: nil) + XCTFail("Expected an error") + } catch { + XCTAssertEqual(error as NSError, authError) + } + } + func testContextWithAuthError() { let authError = NSError(domain: "com.functions.tests", code: 4, userInfo: nil) let auth = FIRAuthInteropFake(token: nil, userID: "userID", error: authError) @@ -63,6 +87,15 @@ class ContextProviderTests: XCTestCase { waitForExpectations(timeout: 0.1) } + func testAsyncContextWithoutAuth() async throws { + let provider = FunctionsContextProvider(auth: nil, messaging: nil, appCheck: nil) + + let context = try await provider.context(options: nil) + + XCTAssertNil(context.authToken) + XCTAssertNil(context.fcmToken) + } + func testContextWithoutAuth() { let provider = FunctionsContextProvider(auth: nil, messaging: nil, appCheck: nil) let expectation = expectation(description: "Completion handler should succeed without Auth.") @@ -76,6 +109,17 @@ class ContextProviderTests: XCTestCase { waitForExpectations(timeout: 0.1) } + func testAsyncContextWithAppCheckOnlySuccess() async throws { + appCheckFake.tokenResult = appCheckTokenSuccess + let provider = FunctionsContextProvider(auth: nil, messaging: nil, appCheck: appCheckFake) + + let context = try await provider.context(options: nil) + + XCTAssertNil(context.authToken) + XCTAssertNil(context.fcmToken) + XCTAssertEqual(context.appCheckToken, appCheckTokenSuccess.token) + } + func testContextWithAppCheckOnlySuccess() { appCheckFake.tokenResult = appCheckTokenSuccess let provider = FunctionsContextProvider(auth: nil, messaging: nil, appCheck: appCheckFake) @@ -91,6 +135,18 @@ class ContextProviderTests: XCTestCase { waitForExpectations(timeout: 0.1) } + func testAsyncContextWithAppCheckOnlyError() async throws { + appCheckFake.tokenResult = appCheckTokenError + let provider = FunctionsContextProvider(auth: nil, messaging: nil, appCheck: appCheckFake) + + let context = try await provider.context(options: nil) + + XCTAssertNil(context.authToken) + XCTAssertNil(context.fcmToken) + // Don't expect any token in the case of App Check error. + XCTAssertNil(context.appCheckToken) + } + func testContextWithAppCheckOnlyError() { appCheckFake.tokenResult = appCheckTokenError let provider = FunctionsContextProvider(auth: nil, messaging: nil, appCheck: appCheckFake) @@ -107,6 +163,19 @@ class ContextProviderTests: XCTestCase { waitForExpectations(timeout: 0.1) } + func testAsyncContextWithAppCheckWithoutOptionalMethods() async throws { + let appCheck = AppCheckFakeWithoutOptionalMethods(tokenResult: appCheckTokenSuccess) + let provider = FunctionsContextProvider(auth: nil, messaging: nil, appCheck: appCheck) + + let context = try await provider.context(options: .init(requireLimitedUseAppCheckTokens: true)) + + XCTAssertNil(context.authToken) + XCTAssertNil(context.fcmToken) + XCTAssertNil(context.appCheckToken) + // If the method for limited-use tokens is not implemented, the value should be `nil`: + XCTAssertNil(context.limitedUseAppCheckToken) + } + func testContextWithAppCheckWithoutOptionalMethods() { let appCheck = AppCheckFakeWithoutOptionalMethods(tokenResult: appCheckTokenSuccess) let provider = FunctionsContextProvider(auth: nil, messaging: nil, appCheck: appCheck) @@ -126,6 +195,22 @@ class ContextProviderTests: XCTestCase { waitForExpectations(timeout: 0.1) } + func testAsyncAllContextsAvailableSuccess() async throws { + appCheckFake.tokenResult = appCheckTokenSuccess + let auth = FIRAuthInteropFake(token: "token", userID: "userID", error: nil) + let provider = FunctionsContextProvider( + auth: auth, + messaging: messagingFake, + appCheck: appCheckFake + ) + + let context = try await provider.context(options: nil) + + XCTAssertEqual(context.authToken, "token") + XCTAssertEqual(context.fcmToken, messagingFake.fcmToken) + XCTAssertEqual(context.appCheckToken, appCheckTokenSuccess.token) + } + func testAllContextsAvailableSuccess() { appCheckFake.tokenResult = appCheckTokenSuccess let auth = FIRAuthInteropFake(token: "token", userID: "userID", error: nil) @@ -146,6 +231,24 @@ class ContextProviderTests: XCTestCase { waitForExpectations(timeout: 0.1) } + func testAsyncAllContextsAuthAndAppCheckError() async { + appCheckFake.tokenResult = appCheckTokenError + let authError = NSError(domain: "com.functions.tests", code: 4, userInfo: nil) + let auth = FIRAuthInteropFake(token: nil, userID: "userID", error: authError) + let provider = FunctionsContextProvider( + auth: auth, + messaging: messagingFake, + appCheck: appCheckFake + ) + + do { + _ = try await provider.context(options: nil) + XCTFail("Expected an error") + } catch { + XCTAssertEqual(error as NSError, authError) + } + } + func testAllContextsAuthAndAppCheckError() { appCheckFake.tokenResult = appCheckTokenError let authError = NSError(domain: "com.functions.tests", code: 4, userInfo: nil)