OAuthProviderTests.swift 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. // Copyright 2023 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License")
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. import Foundation
  15. import XCTest
  16. @testable import FirebaseAuth
  17. import FirebaseCore
  18. #if os(iOS)
  19. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  20. class OAuthProviderTests: RPCBaseTests {
  21. static let kFakeAuthorizedDomain = "test.firebaseapp.com"
  22. static let kFakeAuthorizedWebDomain = "test.web.app"
  23. private let kFakeAccessToken = "fakeAccessToken"
  24. private let kFakeIDToken = "fakeIDToken"
  25. private let kFakeProviderID = "fakeProviderID"
  26. static let kFakeAPIKey = "asdfghjkl"
  27. static let kFakeEmulatorHost = "emulatorhost"
  28. static let kFakeEmulatorPort = 12345
  29. static let kFakeClientID = "123456.apps.googleusercontent.com"
  30. static let kFakeOAuthResponseURL = "fakeOAuthResponseURL"
  31. static let kFakeFirebaseAppID = "1:123456789:ios:123abc456def"
  32. static let kFakeEncodedFirebaseAppID = "app-1-123456789-ios-123abc456def"
  33. static let kFakeTenantID = "tenantID"
  34. static let kFakeReverseClientID = "com.googleusercontent.apps.123456"
  35. // Switches for testing different OAuth test flows
  36. // TODO: Consider using an enum for these instead.
  37. static var testTenantID = false
  38. static var testCancel = false
  39. static var testErrorString = false
  40. static var testInternalError = false
  41. static var testInvalidClientID = false
  42. static var testUnknownError = false
  43. static var testAppID = false
  44. static var testEmulator = false
  45. static var testAppCheck = false
  46. static var auth: Auth?
  47. /** @fn testObtainingOAuthCredentialNoIDToken
  48. @brief Tests the correct creation of an OAuthCredential without an IDToken.
  49. */
  50. func testObtainingOAuthCredentialNoIDToken() throws {
  51. initApp(#function)
  52. let credential = OAuthProvider.credential(providerID: .apple,
  53. accessToken: kFakeAccessToken)
  54. XCTAssertEqual(credential.accessToken, kFakeAccessToken)
  55. XCTAssertEqual(credential.provider, AuthProviderID.apple.rawValue)
  56. XCTAssertNil(credential.idToken)
  57. }
  58. /** @fn testObtainingOAuthCredentialWithFullName
  59. @brief Tests the correct creation of an OAuthCredential with a fullName.
  60. */
  61. func testObtainingOAuthCredentialWithFullName() throws {
  62. let kFakeGivenName = "Paul"
  63. let kFakeFamilyName = "B"
  64. var fullName = PersonNameComponents()
  65. fullName.givenName = kFakeGivenName
  66. fullName.familyName = kFakeFamilyName
  67. initApp(#function)
  68. let credential = OAuthProvider.appleCredential(withIDToken: kFakeIDToken, rawNonce: nil,
  69. fullName: fullName)
  70. XCTAssertEqual(credential.fullName, fullName)
  71. XCTAssertEqual(credential.provider, "apple.com")
  72. XCTAssertEqual(credential.idToken, kFakeIDToken)
  73. XCTAssertNil(credential.accessToken)
  74. }
  75. /** @fn testObtainingOAuthCredentialWithIDToken
  76. @brief Tests the correct creation of an OAuthCredential with an IDToken
  77. */
  78. func testObtainingOAuthCredentialWithIDToken() throws {
  79. initApp(#function)
  80. let credential = OAuthProvider.credential(providerID: .email,
  81. idToken: kFakeIDToken,
  82. accessToken: kFakeAccessToken)
  83. XCTAssertEqual(credential.accessToken, kFakeAccessToken)
  84. XCTAssertEqual(credential.provider, AuthProviderID.email.rawValue)
  85. XCTAssertEqual(credential.idToken, kFakeIDToken)
  86. }
  87. /** @fn testObtainingOAuthProvider
  88. @brief Tests the correct creation of an FIROAuthProvider instance.
  89. */
  90. func testObtainingOAuthProvider() throws {
  91. initApp(#function)
  92. let provider = OAuthProvider(providerID: kFakeProviderID, auth: OAuthProviderTests.auth!)
  93. XCTAssertEqual(provider.providerID, kFakeProviderID)
  94. }
  95. /** @fn testGetCredentialWithUIDelegateWithClientID
  96. @brief Tests a successful invocation of @c getCredentialWithUIDelegate
  97. */
  98. func testGetCredentialWithUIDelegateWithClientID() throws {
  99. initApp(#function)
  100. try testOAuthFlow(description: #function)
  101. }
  102. /** @fn testGetCredentialWithUIDelegateWithTenantID
  103. @brief Tests a successful invocation of @c getCredentialWithUIDelegate:completion:
  104. */
  105. func testGetCredentialWithUIDelegateWithTenantID() throws {
  106. initApp(#function)
  107. // Update tenantID on workqueue to enable _protectedDataDidBecomeAvailableObserver to finish
  108. // init.
  109. kAuthGlobalWorkQueue.sync {
  110. OAuthProviderTests.auth?.tenantID = OAuthProviderTests.kFakeTenantID
  111. }
  112. OAuthProviderTests.testTenantID = true
  113. try testOAuthFlow(description: #function)
  114. OAuthProviderTests.auth?.tenantID = nil
  115. OAuthProviderTests.testTenantID = false
  116. }
  117. /** @fn testGetCredentialWithUIDelegateUserCancellationWithClientID
  118. @brief Tests an unsuccessful invocation of @c testGetCredentialWithUIDelegateUserCancellationWithClientID due to user
  119. cancelation.
  120. */
  121. func testGetCredentialWithUIDelegateUserCancellationWithClientID() throws {
  122. initApp(#function)
  123. OAuthProviderTests.testCancel = true
  124. try testOAuthFlow(description: #function)
  125. OAuthProviderTests.testCancel = false
  126. }
  127. /** @fn testGetCredentialWithUIDelegateNetworkRequestFailedWithClientID
  128. @brief Tests an unsuccessful invocation of @c getCredentialWithUIDelegate due to a
  129. failed network request within the web context.
  130. */
  131. func testGetCredentialWithUIDelegateNetworkRequestFailedWithClientID() throws {
  132. initApp(#function)
  133. OAuthProviderTests.testErrorString = true
  134. try testOAuthFlow(description: #function)
  135. OAuthProviderTests.testErrorString = false
  136. }
  137. /** @fn testGetCredentialWithUIDelegateInternalErrorWithClientID
  138. @brief Tests an unsuccessful invocation of @c getCredentialWithUIDelegate due to an
  139. internal error within the web context.
  140. */
  141. func testGetCredentialWithUIDelegateInternalErrorWithClientID() throws {
  142. initApp(#function)
  143. OAuthProviderTests.testInternalError = true
  144. try testOAuthFlow(description: #function)
  145. OAuthProviderTests.testInternalError = false
  146. }
  147. /** @fn testGetCredentialWithUIDelegateInvalidClientID
  148. @brief Tests an unsuccessful invocation of @c getCredentialWithUIDelegate due to an
  149. use of an invalid client ID.
  150. */
  151. func testGetCredentialWithUIDelegateInvalidClientID() throws {
  152. initApp(#function)
  153. OAuthProviderTests.testInvalidClientID = true
  154. try testOAuthFlow(description: #function)
  155. OAuthProviderTests.testInvalidClientID = false
  156. }
  157. /** @fn testGetCredentialWithUIDelegateUnknownErrorWithClientID
  158. @brief Tests an unsuccessful invocation of @c getCredentialWithUIDelegate due to an
  159. unknown error.
  160. */
  161. func testGetCredentialWithUIDelegateUnknownErrorWithClientID() throws {
  162. initApp(#function)
  163. OAuthProviderTests.testUnknownError = true
  164. try testOAuthFlow(description: #function)
  165. OAuthProviderTests.testUnknownError = false
  166. }
  167. /** @fn testGetCredentialWithUIDelegateWithFirebaseAppID
  168. @brief Tests a successful invocation of @c getCredentialWithUIDelegate
  169. */
  170. func testGetCredentialWithUIDelegateWithFirebaseAppID() throws {
  171. initApp(#function, useAppID: true, omitClientID: true,
  172. scheme: OAuthProviderTests.kFakeEncodedFirebaseAppID)
  173. OAuthProviderTests.testAppID = true
  174. try testOAuthFlow(description: #function)
  175. OAuthProviderTests.testAppID = false
  176. }
  177. /** @fn testGetCredentialWithUIDelegateWithFirebaseAppIDWhileClientIdPresent
  178. @brief Tests a successful invocation of @c getCredentialWithUIDelegate when the
  179. client ID is present in the plist file, but the encoded app ID is the registered custom URL
  180. scheme.
  181. */
  182. func testGetCredentialWithUIDelegateWithFirebaseAppIDWhileClientIdPresent() throws {
  183. initApp(#function, useAppID: true, scheme: OAuthProviderTests.kFakeEncodedFirebaseAppID)
  184. OAuthProviderTests.testAppID = true
  185. try testOAuthFlow(description: #function)
  186. OAuthProviderTests.testAppID = false
  187. }
  188. /** @fn testGetCredentialWithUIDelegateUseEmulator
  189. @brief Tests a successful invocation of @c getCredentialWithUIDelegate when using the emulator.
  190. */
  191. func testGetCredentialWithUIDelegateUseEmulator() throws {
  192. initApp(#function, useAppID: true)
  193. OAuthProviderTests.auth?.requestConfiguration.emulatorHostAndPort =
  194. "\(OAuthProviderTests.kFakeEmulatorHost):\(OAuthProviderTests.kFakeEmulatorPort)"
  195. OAuthProviderTests.testEmulator = true
  196. try testOAuthFlow(description: #function)
  197. OAuthProviderTests.testEmulator = false
  198. }
  199. /** @fn testGetCredentialWithUIDelegateWithAppCheckToken
  200. @brief Tests a successful invocation of @c getCredentialWithUIDelegate
  201. */
  202. func testGetCredentialWithUIDelegateWithAppCheckToken() throws {
  203. let fakeAppCheck = FakeAppCheck()
  204. initApp(#function, useAppID: true)
  205. OAuthProviderTests.auth?.requestConfiguration.appCheck = fakeAppCheck
  206. OAuthProviderTests.testAppCheck = true
  207. try testOAuthFlow(description: #function)
  208. OAuthProviderTests.testAppCheck = false
  209. }
  210. /** @fn testOAuthCredentialCoding
  211. @brief Tests successful archiving and unarchiving of @c GoogleAuthCredential.
  212. */
  213. func testOAuthCredentialCoding() throws {
  214. let kAccessToken = "accessToken"
  215. let kIDToken = "idToken"
  216. let kRawNonce = "nonce"
  217. let kSecret = "sEcret"
  218. let kFullName = PersonNameComponents()
  219. let kPendingToken = "pendingToken"
  220. let credential = OAuthCredential(withProviderID: "dummyProvider",
  221. idToken: kIDToken,
  222. rawNonce: kRawNonce,
  223. accessToken: kAccessToken,
  224. secret: kSecret,
  225. fullName: kFullName,
  226. pendingToken: kPendingToken)
  227. XCTAssertTrue(OAuthCredential.supportsSecureCoding)
  228. let data = try NSKeyedArchiver.archivedData(
  229. withRootObject: credential,
  230. requiringSecureCoding: true
  231. )
  232. let unarchivedCredential = try XCTUnwrap(NSKeyedUnarchiver.unarchivedObject(
  233. ofClasses: [OAuthCredential.self, NSPersonNameComponents.self], from: data
  234. ) as? OAuthCredential)
  235. XCTAssertEqual(unarchivedCredential.idToken, kIDToken)
  236. XCTAssertEqual(unarchivedCredential.rawNonce, kRawNonce)
  237. XCTAssertEqual(unarchivedCredential.accessToken, kAccessToken)
  238. XCTAssertEqual(unarchivedCredential.secret, kSecret)
  239. XCTAssertEqual(unarchivedCredential.fullName, kFullName)
  240. XCTAssertEqual(unarchivedCredential.pendingToken, kPendingToken)
  241. XCTAssertEqual(unarchivedCredential.provider, OAuthProvider.id)
  242. }
  243. private func initApp(_ functionName: String, useAppID: Bool = false, omitClientID: Bool = false,
  244. scheme: String = OAuthProviderTests.kFakeReverseClientID) {
  245. let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000",
  246. gcmSenderID: "00000000000000000-00000000000-000000000")
  247. options.apiKey = OAuthProviderTests.kFakeAPIKey
  248. options.projectID = "myProjectID"
  249. if useAppID {
  250. options.googleAppID = OAuthProviderTests.kFakeFirebaseAppID
  251. }
  252. if !omitClientID {
  253. options.clientID = OAuthProviderTests.kFakeClientID
  254. }
  255. let strippedName = functionName.replacingOccurrences(of: "(", with: "")
  256. .replacingOccurrences(of: ")", with: "")
  257. FirebaseApp.configure(name: strippedName, options: options)
  258. OAuthProviderTests.auth = Auth.auth(app: FirebaseApp.app(name: strippedName)!)
  259. OAuthProviderTests.auth?.mainBundleUrlTypes =
  260. [["CFBundleURLSchemes": [scheme]]]
  261. }
  262. private func testOAuthFlow(description: String,
  263. with fakeAppCheck: FakeAppCheck? = nil) throws {
  264. let expectation = self.expectation(description: description)
  265. let provider = OAuthProvider(providerID: kFakeProviderID, auth: OAuthProviderTests.auth!)
  266. // Use fake authURLPresenter so we can test the parameters that get sent to it.
  267. OAuthProviderTests.auth?.authURLPresenter = FakePresenter()
  268. // 1. Setup fakes and parameters for getCredential.
  269. if !OAuthProviderTests.testEmulator {
  270. let projectConfigExpectation = self.expectation(description: "projectConfiguration")
  271. rpcIssuer?.projectConfigRequester = { request in
  272. // 3. Validate the created Request instance.
  273. XCTAssertEqual(request.apiKey, OAuthProviderTests.kFakeAPIKey)
  274. XCTAssertEqual(request.endpoint, "getProjectConfig")
  275. // 4. Fulfill the expectation.
  276. projectConfigExpectation.fulfill()
  277. kAuthGlobalWorkQueue.async {
  278. do {
  279. // 5. Send the response from the fake backend.
  280. try self.rpcIssuer?
  281. .respond(withJSON: ["authorizedDomains": [
  282. OAuthProviderTests.kFakeAuthorizedWebDomain,
  283. OAuthProviderTests.kFakeAuthorizedDomain]])
  284. } catch {
  285. XCTFail("Failure sending response: \(error)")
  286. }
  287. }
  288. }
  289. }
  290. class FakePresenter: NSObject, AuthWebViewControllerDelegate {
  291. func webViewController(_ webViewController: AuthWebViewController,
  292. canHandle URL: URL) -> Bool {
  293. XCTFail("Do not call")
  294. return false
  295. }
  296. func webViewControllerDidCancel(_ webViewController: AuthWebViewController) {
  297. XCTFail("Do not call")
  298. }
  299. func webViewController(_ webViewController: AuthWebViewController,
  300. didFailWithError error: Error) {
  301. XCTFail("Do not call")
  302. }
  303. func present(_ presentURL: URL, uiDelegate UIDelegate: AuthUIDelegate?,
  304. callbackMatcher: @escaping (URL?) -> Bool,
  305. completion: @escaping (URL?, Error?) -> Void) {
  306. // 6. Verify flow triggers present in the FakePresenter class with the right parameters.
  307. if OAuthProviderTests.testEmulator {
  308. XCTAssertEqual(presentURL.scheme, "http")
  309. XCTAssertEqual(presentURL.host, OAuthProviderTests.kFakeEmulatorHost)
  310. XCTAssertEqual(presentURL.port, OAuthProviderTests.kFakeEmulatorPort)
  311. XCTAssertEqual(presentURL.path, "/emulator/auth/handler")
  312. } else {
  313. XCTAssertEqual(presentURL.scheme, "https")
  314. XCTAssertEqual(presentURL.host, OAuthProviderTests.kFakeAuthorizedDomain)
  315. XCTAssertEqual(presentURL.path, "/__/auth/handler")
  316. }
  317. let params = AuthWebUtils.dictionary(withHttpArgumentsString: presentURL.query)
  318. XCTAssertEqual(params["ibi"], Bundle.main.bundleIdentifier)
  319. if OAuthProviderTests.testAppID {
  320. XCTAssertEqual(params["appId"], OAuthProviderTests.kFakeFirebaseAppID)
  321. } else {
  322. XCTAssertEqual(params["clientId"], OAuthProviderTests.kFakeClientID)
  323. }
  324. XCTAssertEqual(params["apiKey"], OAuthProviderTests.kFakeAPIKey)
  325. XCTAssertEqual(params["authType"], "signInWithRedirect")
  326. XCTAssertNotNil(params["v"])
  327. if OAuthProviderTests.testTenantID {
  328. XCTAssertEqual(params["tid"], OAuthProviderTests.kFakeTenantID)
  329. } else {
  330. XCTAssertNil(params["tid"])
  331. }
  332. let appCheckToken = presentURL.fragment
  333. let verifyAppCheckToken = OAuthProviderTests.testAppCheck ? "fac=fakeAppCheckToken" : nil
  334. XCTAssertEqual(appCheckToken, verifyAppCheckToken)
  335. // 7. Test callbackMatcher
  336. let kFakeRedirectStart = OAuthProviderTests
  337. .testAppID ? "app-1-123456789-ios-123abc456def" :
  338. OAuthProviderTests.kFakeReverseClientID
  339. let kFakeRedirectURLBase = kFakeRedirectStart + "://firebaseauth/" +
  340. "link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fcallback%3F"
  341. var kFakeRedirectURLRest = "authType%3DsignInWithRedirect%26link%3D"
  342. if OAuthProviderTests.testInternalError {
  343. kFakeRedirectURLRest = "firebaseError%3D%257B%2522code%2522%253" +
  344. "A%2522auth%252Finternal-error%2522%252C%2522message%2522%253A%2522Internal%2520" +
  345. "error%2520.%2522%257D%26authType%3DsignInWithRedirect"
  346. } else if OAuthProviderTests.testInvalidClientID {
  347. kFakeRedirectURLRest = "firebaseError%3D%257B%2522code%2522%253A%2522auth" +
  348. "%252Finvalid-oauth-client-id%2522%252C%2522message%2522%253A%2522The%2520OAuth%2520client%" +
  349. "2520ID%2520provided%2520is%2520either%2520invalid%2520or%2520does%2520not%2520match%2520the%" +
  350. "2520specified%2520API%2520key.%2522%257D%26authType%3DsignInWithRedirect"
  351. } else if OAuthProviderTests.testErrorString {
  352. kFakeRedirectURLRest = "firebaseError%3D%257B%2522code%2" +
  353. "522%253A%2522auth%252Fnetwork-request-failed%2522%252C%2522message%2522%253A%2522The%" +
  354. "2520network%2520request%2520failed%2520.%2522%257D%26authType%3DsignInWithRedirect"
  355. } else if OAuthProviderTests.testUnknownError {
  356. kFakeRedirectURLRest = "firebaseError%3D%257B%2522code%2522%253A%2522auth%2" +
  357. "52Funknown-error-id%2522%252C%2522message%2522%253A%2522The%2520OAuth%2520client%2520ID%" +
  358. "2520provided%2520is%2520either%2520invalid%2520or%2520does%2520not%2520match%2520the%2520" +
  359. "specified%2520API%2520key.%2522%257D%26authType%3DsignInWithRedirect"
  360. }
  361. var redirectURL = "\(kFakeRedirectURLBase)\(kFakeRedirectURLRest)"
  362. // Add fake OAuthResponse to callback.
  363. if !OAuthProviderTests.testErrorString, !OAuthProviderTests.testInternalError,
  364. !OAuthProviderTests.testInvalidClientID, !OAuthProviderTests.testUnknownError {
  365. redirectURL += OAuthProviderTests.kFakeOAuthResponseURL
  366. }
  367. // Verify that the URL is rejected by the callback matcher without the event ID.
  368. XCTAssertFalse(callbackMatcher(URL(string: "\(redirectURL)")))
  369. // Verify that the URL is accepted by the callback matcher with the matching event ID.
  370. let redirectWithEventID =
  371. "\(redirectURL)%26eventId%3D\(params["eventId"] ?? "missingEventID")"
  372. let originalComponents = URLComponents(string: redirectWithEventID)!
  373. XCTAssertTrue(callbackMatcher(originalComponents.url))
  374. var components = originalComponents
  375. components.query = "https"
  376. XCTAssertFalse(callbackMatcher(components.url))
  377. components = originalComponents
  378. components.host = "badhost"
  379. XCTAssertFalse(callbackMatcher(components.url))
  380. components = originalComponents
  381. components.path = "badpath"
  382. XCTAssertFalse(callbackMatcher(components.url))
  383. components = originalComponents
  384. components.query = "badquery"
  385. XCTAssertFalse(callbackMatcher(components.url))
  386. // 8. Do the callback to the original call.
  387. kAuthGlobalWorkQueue.async {
  388. if OAuthProviderTests.testCancel {
  389. completion(nil, AuthErrorUtils.webContextCancelledError(message: nil))
  390. } else {
  391. completion(originalComponents.url, nil)
  392. }
  393. }
  394. }
  395. }
  396. // 2. Request the credential.
  397. provider.getCredentialWith(nil) { credential, error in
  398. // 9. After the response triggers the callback, verify the values in the callback credential
  399. XCTAssertTrue(Thread.isMainThread)
  400. if OAuthProviderTests.testCancel {
  401. XCTAssertNil(credential)
  402. XCTAssertEqual((error as? NSError)?.code, AuthErrorCode.webContextCancelled.rawValue)
  403. } else if OAuthProviderTests.testErrorString {
  404. XCTAssertNil(credential)
  405. XCTAssertEqual((error as? NSError)?.code, AuthErrorCode.webNetworkRequestFailed.rawValue)
  406. } else if OAuthProviderTests.testInternalError {
  407. XCTAssertNil(credential)
  408. XCTAssertEqual((error as? NSError)?.code, AuthErrorCode.webInternalError.rawValue)
  409. } else if OAuthProviderTests.testInvalidClientID {
  410. XCTAssertNil(credential)
  411. XCTAssertEqual((error as? NSError)?.code, AuthErrorCode.invalidClientID.rawValue)
  412. } else if OAuthProviderTests.testUnknownError {
  413. XCTAssertNil(credential)
  414. XCTAssertEqual(
  415. (error as? NSError)?.code,
  416. AuthErrorCode.webSignInUserInteractionFailure.rawValue
  417. )
  418. } else {
  419. XCTAssertNil(error)
  420. let oAuthCredential = credential as? OAuthCredential
  421. XCTAssertEqual(
  422. oAuthCredential?.OAuthResponseURLString,
  423. OAuthProviderTests.kFakeOAuthResponseURL
  424. )
  425. }
  426. expectation.fulfill()
  427. }
  428. waitForExpectations(timeout: 5)
  429. }
  430. }
  431. #endif