Skip to content

Commit 865535f

Browse files
authored
Async FunctionsContextProvider.context(options:) (firebase#13900)
1 parent f061a83 commit 865535f

File tree

2 files changed

+150
-6
lines changed

2 files changed

+150
-6
lines changed

FirebaseFunctions/Sources/Internal/FunctionsContext.swift

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,53 @@ struct FunctionsContextProvider {
3636
self.appCheck = appCheck
3737
}
3838

39-
// TODO: Implement async await version
40-
// @available(macOS 10.15.0, *)
41-
// internal func getContext() async throws -> FunctionsContext {
42-
// return FunctionsContext(authToken: nil, fcmToken: nil, appCheckToken: nil)
43-
//
44-
// }
39+
@available(iOS 13, macCatalyst 13, macOS 10.15, tvOS 13, watchOS 7, *)
40+
func context(options: HTTPSCallableOptions?) async throws -> FunctionsContext {
41+
async let authToken = auth?.getToken(forcingRefresh: false)
42+
async let appCheckToken = getAppCheckToken(options: options)
43+
async let limitedUseAppCheckToken = getLimitedUseAppCheckToken(options: options)
44+
45+
// Only `authToken` is throwing, but the formatter script removes the `try`
46+
// from `try authToken` and puts it in front of the initializer call.
47+
return try await FunctionsContext(
48+
authToken: authToken,
49+
fcmToken: messaging?.fcmToken,
50+
appCheckToken: appCheckToken,
51+
limitedUseAppCheckToken: limitedUseAppCheckToken
52+
)
53+
}
54+
55+
@available(iOS 13, macCatalyst 13, macOS 10.15, tvOS 13, watchOS 7, *)
56+
private func getAppCheckToken(options: HTTPSCallableOptions?) async -> String? {
57+
guard
58+
options?.requireLimitedUseAppCheckTokens != true,
59+
let tokenResult = await appCheck?.getToken(forcingRefresh: false),
60+
tokenResult.error == nil
61+
else { return nil }
62+
return tokenResult.token
63+
}
64+
65+
@available(iOS 13, macCatalyst 13, macOS 10.15, tvOS 13, watchOS 7, *)
66+
private func getLimitedUseAppCheckToken(options: HTTPSCallableOptions?) async -> String? {
67+
// At the moment, `await` doesn’t get along with Objective-C’s optional protocol methods.
68+
await withCheckedContinuation { (continuation: CheckedContinuation<String?, Never>) in
69+
guard
70+
options?.requireLimitedUseAppCheckTokens == true,
71+
let appCheck,
72+
// `getLimitedUseToken(completion:)` is an optional protocol method. Optional binding
73+
// is performed to make sure `continuation` is called even if the method’s not implemented.
74+
let limitedUseTokenClosure = appCheck.getLimitedUseToken
75+
else {
76+
return continuation.resume(returning: nil)
77+
}
78+
79+
limitedUseTokenClosure { tokenResult in
80+
// Make sure there’s no error and the token is valid:
81+
guard tokenResult.error == nil else { return continuation.resume(returning: nil) }
82+
continuation.resume(returning: tokenResult.token)
83+
}
84+
}
85+
}
4586

4687
func getContext(options: HTTPSCallableOptions? = nil,
4788
_ completion: @escaping ((FunctionsContext, Error?) -> Void)) {

FirebaseFunctions/Tests/Unit/ContextProviderTests.swift

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ class ContextProviderTests: XCTestCase {
3535
let appCheckTokenSuccess = FIRAppCheckTokenResultFake(token: "valid_token", error: nil)
3636
let messagingFake = FIRMessagingInteropFake()
3737

38+
func testAsyncContextWithAuth() async throws {
39+
let auth = FIRAuthInteropFake(token: "token", userID: "userID", error: nil)
40+
let provider = FunctionsContextProvider(auth: auth, messaging: messagingFake, appCheck: nil)
41+
42+
let context = try await provider.context(options: nil)
43+
44+
XCTAssertNotNil(context)
45+
XCTAssertEqual(context.authToken, "token")
46+
XCTAssertEqual(context.fcmToken, messagingFake.fcmToken)
47+
}
48+
3849
func testContextWithAuth() {
3950
let auth = FIRAuthInteropFake(token: "token", userID: "userID", error: nil)
4051
let provider = FunctionsContextProvider(auth: auth, messaging: messagingFake, appCheck: nil)
@@ -49,6 +60,19 @@ class ContextProviderTests: XCTestCase {
4960
waitForExpectations(timeout: 0.1)
5061
}
5162

63+
func testAsyncContextWithAuthError() async {
64+
let authError = NSError(domain: "com.functions.tests", code: 4, userInfo: nil)
65+
let auth = FIRAuthInteropFake(token: nil, userID: "userID", error: authError)
66+
let provider = FunctionsContextProvider(auth: auth, messaging: messagingFake, appCheck: nil)
67+
68+
do {
69+
_ = try await provider.context(options: nil)
70+
XCTFail("Expected an error")
71+
} catch {
72+
XCTAssertEqual(error as NSError, authError)
73+
}
74+
}
75+
5276
func testContextWithAuthError() {
5377
let authError = NSError(domain: "com.functions.tests", code: 4, userInfo: nil)
5478
let auth = FIRAuthInteropFake(token: nil, userID: "userID", error: authError)
@@ -63,6 +87,15 @@ class ContextProviderTests: XCTestCase {
6387
waitForExpectations(timeout: 0.1)
6488
}
6589

90+
func testAsyncContextWithoutAuth() async throws {
91+
let provider = FunctionsContextProvider(auth: nil, messaging: nil, appCheck: nil)
92+
93+
let context = try await provider.context(options: nil)
94+
95+
XCTAssertNil(context.authToken)
96+
XCTAssertNil(context.fcmToken)
97+
}
98+
6699
func testContextWithoutAuth() {
67100
let provider = FunctionsContextProvider(auth: nil, messaging: nil, appCheck: nil)
68101
let expectation = expectation(description: "Completion handler should succeed without Auth.")
@@ -76,6 +109,17 @@ class ContextProviderTests: XCTestCase {
76109
waitForExpectations(timeout: 0.1)
77110
}
78111

112+
func testAsyncContextWithAppCheckOnlySuccess() async throws {
113+
appCheckFake.tokenResult = appCheckTokenSuccess
114+
let provider = FunctionsContextProvider(auth: nil, messaging: nil, appCheck: appCheckFake)
115+
116+
let context = try await provider.context(options: nil)
117+
118+
XCTAssertNil(context.authToken)
119+
XCTAssertNil(context.fcmToken)
120+
XCTAssertEqual(context.appCheckToken, appCheckTokenSuccess.token)
121+
}
122+
79123
func testContextWithAppCheckOnlySuccess() {
80124
appCheckFake.tokenResult = appCheckTokenSuccess
81125
let provider = FunctionsContextProvider(auth: nil, messaging: nil, appCheck: appCheckFake)
@@ -91,6 +135,18 @@ class ContextProviderTests: XCTestCase {
91135
waitForExpectations(timeout: 0.1)
92136
}
93137

138+
func testAsyncContextWithAppCheckOnlyError() async throws {
139+
appCheckFake.tokenResult = appCheckTokenError
140+
let provider = FunctionsContextProvider(auth: nil, messaging: nil, appCheck: appCheckFake)
141+
142+
let context = try await provider.context(options: nil)
143+
144+
XCTAssertNil(context.authToken)
145+
XCTAssertNil(context.fcmToken)
146+
// Don't expect any token in the case of App Check error.
147+
XCTAssertNil(context.appCheckToken)
148+
}
149+
94150
func testContextWithAppCheckOnlyError() {
95151
appCheckFake.tokenResult = appCheckTokenError
96152
let provider = FunctionsContextProvider(auth: nil, messaging: nil, appCheck: appCheckFake)
@@ -107,6 +163,19 @@ class ContextProviderTests: XCTestCase {
107163
waitForExpectations(timeout: 0.1)
108164
}
109165

166+
func testAsyncContextWithAppCheckWithoutOptionalMethods() async throws {
167+
let appCheck = AppCheckFakeWithoutOptionalMethods(tokenResult: appCheckTokenSuccess)
168+
let provider = FunctionsContextProvider(auth: nil, messaging: nil, appCheck: appCheck)
169+
170+
let context = try await provider.context(options: .init(requireLimitedUseAppCheckTokens: true))
171+
172+
XCTAssertNil(context.authToken)
173+
XCTAssertNil(context.fcmToken)
174+
XCTAssertNil(context.appCheckToken)
175+
// If the method for limited-use tokens is not implemented, the value should be `nil`:
176+
XCTAssertNil(context.limitedUseAppCheckToken)
177+
}
178+
110179
func testContextWithAppCheckWithoutOptionalMethods() {
111180
let appCheck = AppCheckFakeWithoutOptionalMethods(tokenResult: appCheckTokenSuccess)
112181
let provider = FunctionsContextProvider(auth: nil, messaging: nil, appCheck: appCheck)
@@ -126,6 +195,22 @@ class ContextProviderTests: XCTestCase {
126195
waitForExpectations(timeout: 0.1)
127196
}
128197

198+
func testAsyncAllContextsAvailableSuccess() async throws {
199+
appCheckFake.tokenResult = appCheckTokenSuccess
200+
let auth = FIRAuthInteropFake(token: "token", userID: "userID", error: nil)
201+
let provider = FunctionsContextProvider(
202+
auth: auth,
203+
messaging: messagingFake,
204+
appCheck: appCheckFake
205+
)
206+
207+
let context = try await provider.context(options: nil)
208+
209+
XCTAssertEqual(context.authToken, "token")
210+
XCTAssertEqual(context.fcmToken, messagingFake.fcmToken)
211+
XCTAssertEqual(context.appCheckToken, appCheckTokenSuccess.token)
212+
}
213+
129214
func testAllContextsAvailableSuccess() {
130215
appCheckFake.tokenResult = appCheckTokenSuccess
131216
let auth = FIRAuthInteropFake(token: "token", userID: "userID", error: nil)
@@ -146,6 +231,24 @@ class ContextProviderTests: XCTestCase {
146231
waitForExpectations(timeout: 0.1)
147232
}
148233

234+
func testAsyncAllContextsAuthAndAppCheckError() async {
235+
appCheckFake.tokenResult = appCheckTokenError
236+
let authError = NSError(domain: "com.functions.tests", code: 4, userInfo: nil)
237+
let auth = FIRAuthInteropFake(token: nil, userID: "userID", error: authError)
238+
let provider = FunctionsContextProvider(
239+
auth: auth,
240+
messaging: messagingFake,
241+
appCheck: appCheckFake
242+
)
243+
244+
do {
245+
_ = try await provider.context(options: nil)
246+
XCTFail("Expected an error")
247+
} catch {
248+
XCTAssertEqual(error as NSError, authError)
249+
}
250+
}
251+
149252
func testAllContextsAuthAndAppCheckError() {
150253
appCheckFake.tokenResult = appCheckTokenError
151254
let authError = NSError(domain: "com.functions.tests", code: 4, userInfo: nil)

0 commit comments

Comments
 (0)