diff --git a/FirebaseFunctions/Sources/Internal/FunctionsContext.swift b/FirebaseFunctions/Sources/Internal/FunctionsContext.swift index 5a2f24ec41b..9225ca6e2f5 100644 --- a/FirebaseFunctions/Sources/Internal/FunctionsContext.swift +++ b/FirebaseFunctions/Sources/Internal/FunctionsContext.swift @@ -56,9 +56,9 @@ struct FunctionsContextProvider { private func getAppCheckToken(options: HTTPSCallableOptions?) async -> String? { guard options?.requireLimitedUseAppCheckTokens != true, - let tokenResult = await appCheck?.getToken(forcingRefresh: false), - tokenResult.error == nil + let tokenResult = await appCheck?.getToken(forcingRefresh: false) else { return nil } + // The placeholder token should be used in the case of App Check error. return tokenResult.token } @@ -77,8 +77,7 @@ struct FunctionsContextProvider { } limitedUseTokenClosure { tokenResult in - // Make sure there’s no error and the token is valid: - guard tokenResult.error == nil else { return continuation.resume(returning: nil) } + // The placeholder token should be used in the case of App Check error. continuation.resume(returning: tokenResult.token) } } @@ -111,10 +110,8 @@ struct FunctionsContextProvider { // If it’s not implemented, we still need to leave the dispatch group. if let limitedUseTokenClosure = appCheck.getLimitedUseToken { limitedUseTokenClosure { tokenResult in - // Send only valid token to functions. - if tokenResult.error == nil { - limitedUseAppCheckToken = tokenResult.token - } + // In the case of an error, the token will be the placeholder token. + limitedUseAppCheckToken = tokenResult.token dispatchGroup.leave() } } else { @@ -122,10 +119,8 @@ struct FunctionsContextProvider { } } else { appCheck.getToken(forcingRefresh: false) { tokenResult in - // Send only valid token to functions. - if tokenResult.error == nil { - appCheckToken = tokenResult.token - } + // In the case of an error, the token will be the placeholder token. + appCheckToken = tokenResult.token dispatchGroup.leave() } } diff --git a/FirebaseFunctions/Tests/Unit/ContextProviderTests.swift b/FirebaseFunctions/Tests/Unit/ContextProviderTests.swift index 089c2d9f9ef..b528f45ce90 100644 --- a/FirebaseFunctions/Tests/Unit/ContextProviderTests.swift +++ b/FirebaseFunctions/Tests/Unit/ContextProviderTests.swift @@ -32,6 +32,12 @@ class ContextProviderTests: XCTestCase { code: -1, userInfo: nil )) + let appCheckLimitedUseTokenError = FIRAppCheckTokenResultFake(token: "limited use token", + error: NSError( + domain: "testAppCheckError", + code: -1, + userInfo: nil + )) let appCheckTokenSuccess = FIRAppCheckTokenResultFake(token: "valid_token", error: nil) let messagingFake = FIRMessagingInteropFake() @@ -143,8 +149,20 @@ class ContextProviderTests: XCTestCase { XCTAssertNil(context.authToken) XCTAssertNil(context.fcmToken) - // Don't expect any token in the case of App Check error. - XCTAssertNil(context.appCheckToken) + // Expect placeholder token in the case of App Check error. + XCTAssertEqual(context.appCheckToken, appCheckFake.tokenResult.token) + } + + func testAsyncContextWithAppCheckOnlyError_LimitedUseToken() async throws { + appCheckFake.limitedUseTokenResult = appCheckLimitedUseTokenError + let provider = FunctionsContextProvider(auth: nil, messaging: nil, appCheck: appCheckFake) + + let context = try await provider.context(options: .init(requireLimitedUseAppCheckTokens: true)) + + XCTAssertNil(context.authToken) + XCTAssertNil(context.fcmToken) + // Expect placeholder token in the case of App Check error. + XCTAssertEqual(context.limitedUseAppCheckToken, appCheckFake.limitedUseTokenResult.token) } func testContextWithAppCheckOnlyError() { @@ -156,8 +174,24 @@ class ContextProviderTests: XCTestCase { XCTAssertNil(error) XCTAssertNil(context.authToken) XCTAssertNil(context.fcmToken) - // Don't expect any token in the case of App Check error. - XCTAssertNil(context.appCheckToken) + // Expect placeholder token in the case of App Check error. + XCTAssertEqual(context.appCheckToken, self.appCheckFake.tokenResult.token) + expectation.fulfill() + } + waitForExpectations(timeout: 0.1) + } + + func testContextWithAppCheckOnlyError_LimitedUseToken() { + appCheckFake.limitedUseTokenResult = appCheckLimitedUseTokenError + let provider = FunctionsContextProvider(auth: nil, messaging: nil, appCheck: appCheckFake) + let expectation = expectation(description: "Verify bad app check token") + provider.getContext(options: .init(requireLimitedUseAppCheckTokens: true)) { context, error in + XCTAssertNotNil(context) + XCTAssertNil(error) + XCTAssertNil(context.authToken) + XCTAssertNil(context.fcmToken) + // Expect placeholder token in the case of App Check error. + XCTAssertEqual(context.limitedUseAppCheckToken, self.appCheckFake.limitedUseTokenResult.token) expectation.fulfill() } waitForExpectations(timeout: 0.1) @@ -264,8 +298,30 @@ class ContextProviderTests: XCTestCase { XCTAssertEqual(error as NSError?, authError) XCTAssertNil(context.authToken) XCTAssertEqual(context.fcmToken, self.messagingFake.fcmToken) - // Don't expect any token in the case of App Check error. - XCTAssertNil(context.appCheckToken) + // Expect placeholder token in the case of App Check error. + XCTAssertEqual(context.appCheckToken, self.appCheckFake.tokenResult.token) + expectation.fulfill() + } + waitForExpectations(timeout: 0.1) + } + + func testAllContextsAuthAndAppCheckError_LimitedUseToken() { + appCheckFake.limitedUseTokenResult = appCheckLimitedUseTokenError + 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 + ) + let expectation = expectation(description: "All contexts with errors") + provider.getContext(options: .init(requireLimitedUseAppCheckTokens: true)) { context, error in + XCTAssertNotNil(context) + XCTAssertEqual(error as NSError?, authError) + XCTAssertNil(context.authToken) + XCTAssertEqual(context.fcmToken, self.messagingFake.fcmToken) + // Expect placeholder token in the case of App Check error. + XCTAssertEqual(context.limitedUseAppCheckToken, self.appCheckFake.limitedUseTokenResult.token) expectation.fulfill() } waitForExpectations(timeout: 0.1) diff --git a/FirebaseFunctions/Tests/Unit/FunctionsTests.swift b/FirebaseFunctions/Tests/Unit/FunctionsTests.swift index 42e684cdf1a..476fe116853 100644 --- a/FirebaseFunctions/Tests/Unit/FunctionsTests.swift +++ b/FirebaseFunctions/Tests/Unit/FunctionsTests.swift @@ -215,18 +215,22 @@ class FunctionsTests: XCTestCase { waitForExpectations(timeout: 1.5) } - func testCallFunctionWhenLimitedUseAppCheckTokenCannotBeGeneratedThenCallWithoutToken() { + func testCallFunctionWhenLimitedUseAppCheckTokenCannotBeGeneratedThenCallWithPlaceholderToken() { // Given appCheckFake.limitedUseTokenResult = FIRAppCheckTokenResultFake( - token: "dummy token", + token: "limited use token", error: NSError(domain: #function, code: -1) ) let httpRequestExpectation = expectation(description: "HTTPRequestExpectation") fetcherService.testBlock = { fetcherToTest, testResponse in // Assert that header does not contain an AppCheck token. - fetcherToTest.request?.allHTTPHeaderFields?.forEach { key, _ in - XCTAssertNotEqual(key, "X-Firebase-AppCheck") + do { + let appCheckHeader = try XCTUnwrap(fetcherToTest.request? + .allHTTPHeaderFields?["X-Firebase-AppCheck"]) + XCTAssertEqual(appCheckHeader, self.appCheckFake.limitedUseTokenResult.token) + } catch { + XCTFail("Unexpected failure: \(error)") } testResponse(nil, "{\"data\":\"May the force be with you!\"}".data(using: .utf8), nil)