Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Async FunctionsContextProvider.context(options:) #13900

Merged
merged 1 commit into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 47 additions & 6 deletions FirebaseFunctions/Sources/Internal/FunctionsContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
}
ncooke3 marked this conversation as resolved.
Show resolved Hide resolved

@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<String?, Never>) 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)) {
Expand Down
103 changes: 103 additions & 0 deletions FirebaseFunctions/Tests/Unit/ContextProviderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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.")
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down
Loading