PhoneAuthProviderTests.swift 46 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090
  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. #if os(iOS)
  15. import Foundation
  16. import XCTest
  17. @testable import FirebaseAuth
  18. import FirebaseCore
  19. import SafariServices
  20. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  21. class PhoneAuthProviderTests: RPCBaseTests {
  22. static let kFakeAuthorizedDomain = "test.firebaseapp.com"
  23. static let kFakeAPIKey = "asdfghjkl"
  24. static let kFakeEmulatorHost = "emulatorhost"
  25. static let kFakeEmulatorPort = 12345
  26. static let kFakeClientID = "123456.apps.googleusercontent.com"
  27. static let kFakeFirebaseAppID = "1:123456789:ios:123abc456def"
  28. static let kFakeEncodedFirebaseAppID = "app-1-123456789-ios-123abc456def"
  29. static let kFakeTenantID = "tenantID"
  30. static let kFakeReverseClientID = "com.googleusercontent.apps.123456"
  31. private let kTestVerificationID = "verificationID"
  32. private let kTestVerificationCode = "verificationCode"
  33. private let kTestReceipt = "receipt"
  34. private let kTestTimeout = "1"
  35. private let kTestSecret = "secret"
  36. private let kVerificationIDKey = "sessionInfo"
  37. private let kFakeEncodedFirebaseAppID = "app-1-123456789-ios-123abc456def"
  38. private let kFakeReCAPTCHAToken = "fakeReCAPTCHAToken"
  39. private let kCaptchaResponse: String = "captchaResponse"
  40. private let kRecaptchaVersion: String = "RECAPTCHA_ENTERPRISE"
  41. static var auth: Auth?
  42. /** @fn testCredentialWithVerificationID
  43. @brief Tests the @c credentialWithToken method to make sure that it returns a valid AuthCredential instance.
  44. */
  45. func testCredentialWithVerificationID() throws {
  46. initApp(#function)
  47. let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
  48. let provider = PhoneAuthProvider.provider(auth: auth)
  49. let credential = provider.credential(withVerificationID: kTestVerificationID,
  50. verificationCode: kTestVerificationCode)
  51. switch credential.credentialKind {
  52. case .phoneNumber: XCTFail("Should be verification case")
  53. case let .verification(id, code):
  54. XCTAssertEqual(id, kTestVerificationID)
  55. XCTAssertEqual(code, kTestVerificationCode)
  56. }
  57. }
  58. /** @fn testVerifyEmptyPhoneNumber
  59. @brief Tests a failed invocation verifyPhoneNumber because an empty phone
  60. number was provided.
  61. */
  62. func testVerifyEmptyPhoneNumber() async throws {
  63. initApp(#function)
  64. let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
  65. let provider = PhoneAuthProvider.provider(auth: auth)
  66. do {
  67. _ = try await provider.verifyPhoneNumber("")
  68. XCTFail("Expected an error, but verification succeeded.")
  69. } catch {
  70. XCTAssertEqual((error as NSError).code, AuthErrorCode.missingPhoneNumber.rawValue)
  71. }
  72. }
  73. /** @fn testVerifyInvalidPhoneNumber
  74. @brief Tests a failed invocation @c verifyPhoneNumber:completion: because an invalid phone
  75. number was provided.
  76. */
  77. func testVerifyInvalidPhoneNumber() async throws {
  78. try await internalTestVerify(errorString: "INVALID_PHONE_NUMBER",
  79. errorCode: AuthErrorCode.invalidPhoneNumber.rawValue,
  80. function: #function)
  81. }
  82. /** @fn testVerifyPhoneNumber
  83. @brief Tests a successful invocation of @c verifyPhoneNumber:completion:.
  84. */
  85. func testVerifyPhoneNumber() async throws {
  86. try await internalTestVerify(function: #function)
  87. }
  88. /**
  89. @fn testVerifyPhoneNumberWithRceEnforce
  90. @brief Tests a successful invocation of @c verifyPhoneNumber with recaptcha enterprise enforced
  91. */
  92. func testVerifyPhoneNumberWithRceEnforceSuccess() async throws {
  93. initApp(#function)
  94. let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
  95. // TODO: Figure out how to mock objective C's FIRRecaptchaGetToken response
  96. let provider = PhoneAuthProvider.provider(auth: auth)
  97. let mockVerifier = FakeAuthRecaptchaVerifier(captchaResponse: kCaptchaResponse)
  98. AuthRecaptchaVerifier.setShared(mockVerifier, auth: auth)
  99. rpcIssuer.rceMode = "ENFORCE"
  100. let requestExpectation = expectation(description: "verifyRequester")
  101. rpcIssuer?.verifyRequester = { request in
  102. XCTAssertEqual(request.phoneNumber, self.kTestPhoneNumber)
  103. XCTAssertEqual(request.captchaResponse, self.kCaptchaResponse)
  104. XCTAssertEqual(request.recaptchaVersion, "RECAPTCHA_ENTERPRISE")
  105. XCTAssertEqual(request.codeIdentity, CodeIdentity.empty)
  106. requestExpectation.fulfill()
  107. do {
  108. try self.rpcIssuer?
  109. .respond(withJSON: [self.kVerificationIDKey: self.kTestVerificationID])
  110. } catch {
  111. XCTFail("Failure sending response: \(error)")
  112. }
  113. }
  114. do {
  115. let result = try await provider.verifyClAndSendVerificationCodeWithRecaptcha(
  116. toPhoneNumber: kTestPhoneNumber,
  117. retryOnInvalidAppCredential: false,
  118. uiDelegate: nil,
  119. recaptchaVerifier: mockVerifier
  120. )
  121. XCTAssertEqual(result, kTestVerificationID)
  122. } catch {
  123. XCTFail("Unexpected error")
  124. }
  125. await fulfillment(of: [requestExpectation], timeout: 5.0)
  126. }
  127. /**
  128. @fn testVerifyPhoneNumberWithRceEnforceInvalidRecaptcha
  129. @brief Tests a successful invocation of @c verifyPhoneNumber with recaptcha enterprise enforced
  130. */
  131. func testVerifyPhoneNumberWithRceEnforceInvalidRecaptcha() async throws {
  132. initApp(#function)
  133. let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
  134. // TODO: Figure out how to mock objective C's FIRRecaptchaGetToken response
  135. let provider = PhoneAuthProvider.provider(auth: auth)
  136. let mockVerifier = FakeAuthRecaptchaVerifier()
  137. AuthRecaptchaVerifier.setShared(mockVerifier, auth: auth)
  138. rpcIssuer.rceMode = "ENFORCE"
  139. let requestExpectation = expectation(description: "verifyRequester")
  140. rpcIssuer?.verifyRequester = { request in
  141. XCTAssertEqual(request.phoneNumber, self.kTestPhoneNumber)
  142. XCTAssertEqual(request.captchaResponse, "NO_RECAPTCHA")
  143. XCTAssertEqual(request.recaptchaVersion, "RECAPTCHA_ENTERPRISE")
  144. XCTAssertEqual(request.codeIdentity, CodeIdentity.empty)
  145. requestExpectation.fulfill()
  146. do {
  147. try self.rpcIssuer?
  148. .respond(
  149. serverErrorMessage: "INVALID_RECAPTCHA_TOKEN",
  150. error: AuthErrorUtils.invalidRecaptchaTokenError() as NSError
  151. )
  152. } catch {
  153. XCTFail("Failure sending response: \(error)")
  154. }
  155. }
  156. do {
  157. _ = try await provider.verifyClAndSendVerificationCodeWithRecaptcha(
  158. toPhoneNumber: kTestPhoneNumber,
  159. retryOnInvalidAppCredential: false,
  160. uiDelegate: nil,
  161. recaptchaVerifier: mockVerifier
  162. )
  163. // XCTAssertEqual(result, kTestVerificationID)
  164. } catch {
  165. // Traverse the nested error to find the root cause
  166. let underlyingError = (error as NSError).userInfo[NSUnderlyingErrorKey] as? NSError
  167. let rootError = underlyingError?.userInfo[NSUnderlyingErrorKey] as? NSError
  168. // Compare the root error code to the expected error code
  169. XCTAssertEqual(rootError?.code, AuthErrorCode.invalidRecaptchaToken.code.rawValue)
  170. }
  171. await fulfillment(of: [requestExpectation], timeout: 5.0)
  172. }
  173. /**
  174. @fn testVerifyPhoneNumberWithRceEnforceSDKNotLinked
  175. @brief Tests a successful invocation of @c verifyPhoneNumber with recaptcha enterprise enforced
  176. */
  177. func testVerifyPhoneNumberWithRceEnforceRecaptchaSDKNotLinked() async throws {
  178. return try await testRecaptchaFlowError(
  179. function: #function,
  180. rceError: AuthErrorUtils.recaptchaSDKNotLinkedError()
  181. )
  182. }
  183. /**
  184. @fn testVerifyPhoneNumberWithRceEnforceSDKNotLinked
  185. @brief Tests a successful invocation of @c verifyPhoneNumber with recaptcha enterprise enforced
  186. */
  187. func testVerifyPhoneNumberWithRceEnforceRecaptchaActionCreationFailed() async throws {
  188. return try await testRecaptchaFlowError(
  189. function: #function,
  190. rceError: AuthErrorUtils.recaptchaActionCreationFailed()
  191. )
  192. }
  193. /// @fn testVerifyPhoneNumberWithRceAudit
  194. /// @brief Tests a successful invocation of @c verifyPhoneNumber with recaptcha enterprise in
  195. /// audit mode
  196. func testVerifyPhoneNumberWithRceAuditSuccess() async throws {
  197. initApp(#function)
  198. let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
  199. let provider = PhoneAuthProvider.provider(auth: auth)
  200. let mockVerifier = FakeAuthRecaptchaVerifier(captchaResponse: kCaptchaResponse)
  201. AuthRecaptchaVerifier.setShared(mockVerifier, auth: auth)
  202. rpcIssuer.rceMode = "AUDIT"
  203. let requestExpectation = expectation(description: "verifyRequester")
  204. rpcIssuer?.verifyRequester = { request in
  205. XCTAssertEqual(request.phoneNumber, self.kTestPhoneNumber)
  206. XCTAssertEqual(request.captchaResponse, self.kCaptchaResponse)
  207. XCTAssertEqual(request.recaptchaVersion, "RECAPTCHA_ENTERPRISE")
  208. XCTAssertEqual(request.codeIdentity, CodeIdentity.empty)
  209. requestExpectation.fulfill()
  210. do {
  211. try self.rpcIssuer?
  212. .respond(withJSON: [self.kVerificationIDKey: self.kTestVerificationID])
  213. } catch {
  214. XCTFail("Failure sending response: \(error)")
  215. }
  216. }
  217. do {
  218. let result = try await provider.verifyClAndSendVerificationCodeWithRecaptcha(
  219. toPhoneNumber: kTestPhoneNumber,
  220. retryOnInvalidAppCredential: false,
  221. uiDelegate: nil,
  222. recaptchaVerifier: mockVerifier
  223. )
  224. XCTAssertEqual(result, kTestVerificationID)
  225. } catch {
  226. XCTFail("Unexpected error")
  227. }
  228. await fulfillment(of: [requestExpectation], timeout: 5.0)
  229. }
  230. /// @fn testVerifyPhoneNumberWithRceAuditInvalidRecaptcha
  231. /// @brief Tests a successful invocation of @c verifyPhoneNumber with recaptcha enterprise in
  232. /// audit mode
  233. func testVerifyPhoneNumberWithRceAuditInvalidRecaptcha() async throws {
  234. initApp(#function)
  235. let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
  236. let provider = PhoneAuthProvider.provider(auth: auth)
  237. let mockVerifier = FakeAuthRecaptchaVerifier()
  238. AuthRecaptchaVerifier.setShared(mockVerifier, auth: auth)
  239. rpcIssuer.rceMode = "AUDIT"
  240. let requestExpectation = expectation(description: "verifyRequester")
  241. rpcIssuer?.verifyRequester = { request in
  242. XCTAssertEqual(request.phoneNumber, self.kTestPhoneNumber)
  243. XCTAssertEqual(request.captchaResponse, "NO_RECAPTCHA")
  244. XCTAssertEqual(request.recaptchaVersion, "RECAPTCHA_ENTERPRISE")
  245. XCTAssertEqual(request.codeIdentity, CodeIdentity.empty)
  246. requestExpectation.fulfill()
  247. do {
  248. try self.rpcIssuer?
  249. .respond(
  250. serverErrorMessage: "INVALID_RECAPTCHA_TOKEN",
  251. error: AuthErrorUtils.invalidRecaptchaTokenError() as NSError
  252. )
  253. } catch {
  254. XCTFail("Failure sending response: \(error)")
  255. }
  256. }
  257. do {
  258. _ = try await provider.verifyClAndSendVerificationCodeWithRecaptcha(
  259. toPhoneNumber: kTestPhoneNumber,
  260. retryOnInvalidAppCredential: false,
  261. uiDelegate: nil,
  262. recaptchaVerifier: mockVerifier
  263. )
  264. } catch {
  265. let underlyingError = (error as NSError).userInfo[NSUnderlyingErrorKey] as? NSError
  266. let rootError = underlyingError?.userInfo[NSUnderlyingErrorKey] as? NSError
  267. XCTAssertEqual(rootError?.code, AuthErrorCode.invalidRecaptchaToken.code.rawValue)
  268. }
  269. await fulfillment(of: [requestExpectation], timeout: 5.0)
  270. }
  271. /** @fn testVerifyPhoneNumberInTestMode
  272. @brief Tests a successful invocation of @c verifyPhoneNumber:completion: when app verification
  273. is disabled.
  274. */
  275. func testVerifyPhoneNumberInTestMode() async throws {
  276. try await internalTestVerify(function: #function, testMode: true)
  277. }
  278. /** @fn testVerifyPhoneNumberInTestModeFailure
  279. @brief Tests a failed invocation of @c verifyPhoneNumber:completion: when app verification
  280. is disabled.
  281. */
  282. func testVerifyPhoneNumberInTestModeFailure() async throws {
  283. try await internalTestVerify(errorString: "INVALID_PHONE_NUMBER",
  284. errorCode: AuthErrorCode.invalidPhoneNumber.rawValue,
  285. function: #function, testMode: true)
  286. }
  287. /** @fn testVerifyPhoneNumberUIDelegateFirebaseAppIdFlow
  288. @brief Tests a successful invocation of @c verifyPhoneNumber:UIDelegate:completion:.
  289. */
  290. func testVerifyPhoneNumberUIDelegateFirebaseAppIdFlow() async throws {
  291. try await internalTestVerify(function: #function, reCAPTCHAfallback: true)
  292. }
  293. /** @fn testVerifyPhoneNumberUIDelegateFirebaseAppIdWhileClientIdPresentFlow
  294. @brief Tests a successful invocation of @c verifyPhoneNumber:UIDelegate:completion: when the
  295. client ID is present in the plist file, but the encoded app ID is the registered custom URL scheme.
  296. */
  297. func testVerifyPhoneNumberUIDelegateFirebaseAppIdWhileClientIdPresentFlow() async throws {
  298. try await internalTestVerify(function: #function, useClientID: true,
  299. bothClientAndAppID: true, reCAPTCHAfallback: true)
  300. }
  301. /** @fn testVerifyPhoneNumberUIDelegateClientIdFlow
  302. @brief Tests a successful invocation of @c verifyPhoneNumber:UIDelegate:completion:.
  303. */
  304. func testVerifyPhoneNumberUIDelegateClientIdFlow() async throws {
  305. try await internalTestVerify(function: #function, useClientID: true, reCAPTCHAfallback: true)
  306. }
  307. /** @fn testVerifyPhoneNumberUIDelegateInvalidClientID
  308. @brief Tests a invocation of @c verifyPhoneNumber:UIDelegate:completion: which results in an
  309. invalid client ID error.
  310. */
  311. func testVerifyPhoneNumberUIDelegateInvalidClientID() async throws {
  312. try await internalTestVerify(
  313. errorURLString: PhoneAuthProviderTests.kFakeRedirectURLStringInvalidClientID,
  314. errorCode: AuthErrorCode.invalidClientID.rawValue,
  315. function: #function,
  316. useClientID: true,
  317. reCAPTCHAfallback: true
  318. )
  319. }
  320. /** @fn testVerifyPhoneNumberUIDelegateWebNetworkRequestFailed
  321. @brief Tests a invocation of @c verifyPhoneNumber:UIDelegate:completion: which results in a web
  322. network request failed error.
  323. */
  324. func testVerifyPhoneNumberUIDelegateWebNetworkRequestFailed() async throws {
  325. try await internalTestVerify(
  326. errorURLString: PhoneAuthProviderTests.kFakeRedirectURLStringWebNetworkRequestFailed,
  327. errorCode: AuthErrorCode.webNetworkRequestFailed.rawValue,
  328. function: #function,
  329. useClientID: true,
  330. reCAPTCHAfallback: true
  331. )
  332. }
  333. /** @fn testVerifyPhoneNumberUIDelegateWebInternalError
  334. @brief Tests a invocation of @c verifyPhoneNumber:UIDelegate:completion: which results in a web
  335. internal error.
  336. */
  337. func testVerifyPhoneNumberUIDelegateWebInternalError() async throws {
  338. try await internalTestVerify(
  339. errorURLString: PhoneAuthProviderTests.kFakeRedirectURLStringWebInternalError,
  340. errorCode: AuthErrorCode.webInternalError.rawValue,
  341. function: #function,
  342. useClientID: true,
  343. reCAPTCHAfallback: true
  344. )
  345. }
  346. /** @fn testVerifyPhoneNumberUIDelegateUnexpectedError
  347. @brief Tests a invocation of @c verifyPhoneNumber:UIDelegate:completion: which results in an
  348. invalid client ID.
  349. */
  350. func testVerifyPhoneNumberUIDelegateUnexpectedError() async throws {
  351. try await internalTestVerify(
  352. errorURLString: PhoneAuthProviderTests.kFakeRedirectURLStringUnknownError,
  353. errorCode: AuthErrorCode.webSignInUserInteractionFailure.rawValue,
  354. function: #function,
  355. useClientID: true,
  356. reCAPTCHAfallback: true
  357. )
  358. }
  359. /** @fn testVerifyPhoneNumberUIDelegateUnstructuredError
  360. @brief Tests a invocation of @c verifyPhoneNumber:UIDelegate:completion: which results in an
  361. error being surfaced with a default NSLocalizedFailureReasonErrorKey due to an unexpected
  362. structure of the error response.
  363. */
  364. func testVerifyPhoneNumberUIDelegateUnstructuredError() async throws {
  365. try await internalTestVerify(
  366. errorURLString: PhoneAuthProviderTests.kFakeRedirectURLStringUnstructuredError,
  367. errorCode: AuthErrorCode.appVerificationUserInteractionFailure.rawValue,
  368. function: #function,
  369. useClientID: true,
  370. reCAPTCHAfallback: true
  371. )
  372. }
  373. // TODO: This test is skipped. What was formerly an Objective-C exception is now a Swift fatal_error.
  374. // The test runs correctly, but it's not clear how to automate fatal_error testing. Switching to
  375. // Swift exceptions would break the API.
  376. /** @fn testVerifyPhoneNumberUIDelegateRaiseException
  377. @brief Tests a invocation of @c verifyPhoneNumber:UIDelegate:completion: which results in an
  378. exception.
  379. */
  380. func SKIPtestVerifyPhoneNumberUIDelegateRaiseException() async throws {
  381. initApp(#function)
  382. let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
  383. auth.mainBundleUrlTypes = [["CFBundleURLSchemes": ["fail"]]]
  384. let provider = PhoneAuthProvider.provider(auth: auth)
  385. provider.verifyPhoneNumber(kTestPhoneNumber, uiDelegate: nil) { verificationID, error in
  386. XCTFail("Should not call completion")
  387. }
  388. }
  389. /** @fn testNotForwardingNotification
  390. @brief Tests returning an error for the app failing to forward notification.
  391. */
  392. func testNotForwardingNotification() throws {
  393. func testVerifyPhoneNumberUIDelegateUnstructuredError() async throws {
  394. try await internalTestVerify(
  395. errorURLString: PhoneAuthProviderTests.kFakeRedirectURLStringUnstructuredError,
  396. errorCode: AuthErrorCode.appVerificationUserInteractionFailure.rawValue,
  397. function: #function,
  398. useClientID: true,
  399. reCAPTCHAfallback: true,
  400. forwardingNotification: false
  401. )
  402. }
  403. }
  404. /** @fn testMissingAPNSToken
  405. @brief Tests returning an error for the app failing to provide an APNS device token.
  406. */
  407. func testMissingAPNSToken() async throws {
  408. try await internalTestVerify(
  409. errorCode: AuthErrorCode.missingAppToken.rawValue,
  410. function: #function,
  411. useClientID: true,
  412. reCAPTCHAfallback: true,
  413. presenterError: NSError(
  414. domain: AuthErrors.domain,
  415. code: AuthErrorCode.missingAppToken.rawValue
  416. )
  417. )
  418. }
  419. /** @fn testVerifyPhoneNumberUIDelegateiOSSecretMissingFlow
  420. @brief Tests a successful invocation of @c verifyPhoneNumber:UIDelegate:completion: that falls
  421. back to the reCAPTCHA flow when the push notification is not received before the timeout.
  422. */
  423. func testVerifyPhoneNumberUIDelegateiOSSecretMissingFlow() throws {
  424. try internalFlow(function: #function, useClientID: false, reCAPTCHAfallback: true)
  425. }
  426. /** @fn testVerifyClient
  427. @brief Tests verifying client before sending verification code.
  428. */
  429. func testVerifyClient() throws {
  430. try internalFlow(function: #function, useClientID: true, reCAPTCHAfallback: false)
  431. }
  432. /** @fn testSendVerificationCodeFailedRetry
  433. @brief Tests failed retry after failing to send verification code.
  434. */
  435. func testSendVerificationCodeFailedRetry() throws {
  436. try internalFlowRetry(function: #function)
  437. }
  438. /** @fn testSendVerificationCodeSuccessfulRetry
  439. @brief Tests successful retry after failing to send verification code.
  440. */
  441. func testSendVerificationCodeSuccessfulRetry() throws {
  442. try internalFlowRetry(function: #function, goodRetry: true)
  443. }
  444. /** @fn testPhoneAuthCredentialCoding
  445. @brief Tests successful archiving and unarchiving of @c PhoneAuthCredential.
  446. */
  447. func testPhoneAuthCredentialCoding() throws {
  448. let kVerificationID = "My verificationID"
  449. let kVerificationCode = "1234"
  450. let credential = PhoneAuthCredential(withProviderID: PhoneAuthProvider.id,
  451. verificationID: kVerificationID,
  452. verificationCode: kVerificationCode)
  453. XCTAssertTrue(PhoneAuthCredential.supportsSecureCoding)
  454. let data = try NSKeyedArchiver.archivedData(
  455. withRootObject: credential,
  456. requiringSecureCoding: true
  457. )
  458. let unarchivedCredential = try XCTUnwrap(NSKeyedUnarchiver.unarchivedObject(
  459. ofClass: PhoneAuthCredential.self, from: data
  460. ))
  461. switch unarchivedCredential.credentialKind {
  462. case .phoneNumber: XCTFail("Should be verification case")
  463. case let .verification(id, code):
  464. XCTAssertEqual(id, kVerificationID)
  465. XCTAssertEqual(code, kVerificationCode)
  466. }
  467. XCTAssertEqual(unarchivedCredential.provider, PhoneAuthProvider.id)
  468. }
  469. /** @fn testPhoneAuthCredentialCodingPhone
  470. @brief Tests successful archiving and unarchiving of @c PhoneAuthCredential after other constructor.
  471. */
  472. func testPhoneAuthCredentialCodingPhone() throws {
  473. let kTemporaryProof = "Proof"
  474. let kPhoneNumber = "123457"
  475. let credential = PhoneAuthCredential(withTemporaryProof: kTemporaryProof,
  476. phoneNumber: kPhoneNumber,
  477. providerID: PhoneAuthProvider.id)
  478. XCTAssertTrue(PhoneAuthCredential.supportsSecureCoding)
  479. let data = try NSKeyedArchiver.archivedData(
  480. withRootObject: credential,
  481. requiringSecureCoding: true
  482. )
  483. let unarchivedCredential = try XCTUnwrap(NSKeyedUnarchiver.unarchivedObject(
  484. ofClasses: [PhoneAuthCredential.self, NSString.self], from: data
  485. ) as? PhoneAuthCredential)
  486. switch unarchivedCredential.credentialKind {
  487. case let .phoneNumber(phoneNumber, temporaryProof):
  488. XCTAssertEqual(temporaryProof, kTemporaryProof)
  489. XCTAssertEqual(phoneNumber, kPhoneNumber)
  490. case .verification: XCTFail("Should be phoneNumber case")
  491. }
  492. XCTAssertEqual(unarchivedCredential.provider, PhoneAuthProvider.id)
  493. }
  494. private func testRecaptchaFlowError(function: String, rceError: Error) async throws {
  495. initApp(function)
  496. let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
  497. // TODO: Figure out how to mock objective C's FIRRecaptchaGetToken response
  498. // Mocking the output of verify() method
  499. let provider = PhoneAuthProvider.provider(auth: auth)
  500. let mockVerifier = FakeAuthRecaptchaVerifier(error: rceError)
  501. AuthRecaptchaVerifier.setShared(mockVerifier, auth: auth)
  502. rpcIssuer.rceMode = "ENFORCE"
  503. do {
  504. let _ = try await provider.verifyClAndSendVerificationCodeWithRecaptcha(
  505. toPhoneNumber: kTestPhoneNumber,
  506. retryOnInvalidAppCredential: false,
  507. uiDelegate: nil,
  508. recaptchaVerifier: mockVerifier
  509. )
  510. } catch {
  511. XCTAssertEqual((error as NSError).code, (rceError as NSError).code)
  512. }
  513. }
  514. private func internalFlowRetry(function: String, goodRetry: Bool = false) throws {
  515. let function = function
  516. initApp(function, useClientID: true, fakeToken: true)
  517. let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
  518. let provider = PhoneAuthProvider.provider(auth: auth)
  519. let expectation = self.expectation(description: function)
  520. // Fake push notification.
  521. auth.appCredentialManager?.fakeCredential = AuthAppCredential(
  522. receipt: kTestReceipt,
  523. secret: kTestSecret
  524. )
  525. // 1. Intercept, handle, and test three RPC calls.
  526. let verifyClientRequestExpectation = self.expectation(description: "verifyClientRequest")
  527. verifyClientRequestExpectation.expectedFulfillmentCount = 2
  528. rpcIssuer?.verifyClientRequester = { request in
  529. XCTAssertEqual(request.appToken, "21402324255E")
  530. XCTAssertFalse(request.isSandbox)
  531. verifyClientRequestExpectation.fulfill()
  532. do {
  533. // Response for the underlying VerifyClientRequest RPC call.
  534. try self.rpcIssuer?.respond(withJSON: [
  535. "receipt": self.kTestReceipt,
  536. "suggestedTimeout": self.kTestTimeout,
  537. ])
  538. } catch {
  539. XCTFail("Failure sending response: \(error)")
  540. }
  541. }
  542. let verifyRequesterExpectation = self.expectation(description: "verifyRequester")
  543. verifyRequesterExpectation.expectedFulfillmentCount = 2
  544. var visited = false
  545. rpcIssuer?.verifyRequester = { request in
  546. XCTAssertEqual(request.phoneNumber, self.kTestPhoneNumber)
  547. switch request.codeIdentity {
  548. case let .credential(credential):
  549. XCTAssertEqual(credential.receipt, self.kTestReceipt)
  550. XCTAssertEqual(credential.secret, self.kTestSecret)
  551. default:
  552. XCTFail("Should be credential")
  553. }
  554. verifyRequesterExpectation.fulfill()
  555. do {
  556. if visited == false || goodRetry == false {
  557. // First Response for the underlying SendVerificationCode RPC call.
  558. try self.rpcIssuer?.respond(serverErrorMessage: "INVALID_APP_CREDENTIAL")
  559. visited = true
  560. } else {
  561. // Second Response for the underlying SendVerificationCode RPC call.
  562. try self.rpcIssuer?
  563. .respond(withJSON: [self.kVerificationIDKey: self.kTestVerificationID])
  564. }
  565. } catch {
  566. XCTFail("Failure sending response: \(error)")
  567. }
  568. }
  569. // Use fake authURLPresenter so we can test the parameters that get sent to it.
  570. PhoneAuthProviderTests.auth?.authURLPresenter =
  571. FakePresenter(
  572. urlString: PhoneAuthProviderTests.kFakeRedirectURLStringWithReCAPTCHAToken,
  573. clientID: PhoneAuthProviderTests.kFakeClientID,
  574. firebaseAppID: nil,
  575. errorTest: false,
  576. presenterError: nil
  577. )
  578. // 2. After setting up the fakes and parameters, call `verifyPhoneNumber`.
  579. provider
  580. .verifyPhoneNumber(kTestPhoneNumber, uiDelegate: nil) { verificationID, error in
  581. // 8. After the response triggers the callback in the FakePresenter, verify the callback.
  582. XCTAssertTrue(Thread.isMainThread)
  583. if goodRetry {
  584. XCTAssertNil(error)
  585. XCTAssertEqual(verificationID, self.kTestVerificationID)
  586. } else {
  587. XCTAssertNil(verificationID)
  588. XCTAssertEqual((error as? NSError)?.code, AuthErrorCode.internalError.rawValue)
  589. }
  590. expectation.fulfill()
  591. }
  592. waitForExpectations(timeout: 5)
  593. }
  594. private func internalFlow(function: String,
  595. useClientID: Bool = false,
  596. reCAPTCHAfallback: Bool = false) throws {
  597. let function = function
  598. initApp(function, useClientID: useClientID, fakeToken: true)
  599. let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
  600. let provider = PhoneAuthProvider.provider(auth: auth)
  601. let expectation = self.expectation(description: function)
  602. // Fake push notification.
  603. auth.appCredentialManager?.fakeCredential = AuthAppCredential(
  604. receipt: kTestReceipt,
  605. secret: reCAPTCHAfallback ? nil : kTestSecret
  606. )
  607. // 1. Intercept, handle, and test three RPC calls.
  608. let verifyClientRequestExpectation = self.expectation(description: "verifyClientRequest")
  609. rpcIssuer?.verifyClientRequester = { request in
  610. XCTAssertEqual(request.appToken, "21402324255E")
  611. XCTAssertFalse(request.isSandbox)
  612. verifyClientRequestExpectation.fulfill()
  613. do {
  614. // Response for the underlying VerifyClientRequest RPC call.
  615. try self.rpcIssuer?.respond(withJSON: [
  616. "receipt": self.kTestReceipt,
  617. "suggestedTimeout": self.kTestTimeout,
  618. ])
  619. } catch {
  620. XCTFail("Failure sending response: \(error)")
  621. }
  622. }
  623. if reCAPTCHAfallback {
  624. let projectConfigExpectation = self.expectation(description: "projectConfiguration")
  625. rpcIssuer?.projectConfigRequester = { request in
  626. XCTAssertEqual(request.apiKey, PhoneAuthProviderTests.kFakeAPIKey)
  627. projectConfigExpectation.fulfill()
  628. kAuthGlobalWorkQueue.async {
  629. do {
  630. // Response for the underlying VerifyClientRequest RPC call.
  631. try self.rpcIssuer?.respond(
  632. withJSON: ["projectId": "kFakeProjectID",
  633. "authorizedDomains": [PhoneAuthProviderTests.kFakeAuthorizedDomain]]
  634. )
  635. } catch {
  636. XCTFail("Failure sending response: \(error)")
  637. }
  638. }
  639. }
  640. }
  641. let verifyRequesterExpectation = self.expectation(description: "verifyRequester")
  642. rpcIssuer?.verifyRequester = { request in
  643. XCTAssertEqual(request.phoneNumber, self.kTestPhoneNumber)
  644. if reCAPTCHAfallback {
  645. switch request.codeIdentity {
  646. case let .recaptcha(token):
  647. XCTAssertEqual(token, self.kFakeReCAPTCHAToken)
  648. default:
  649. XCTFail("Should be recaptcha")
  650. }
  651. } else {
  652. switch request.codeIdentity {
  653. case let .credential(credential):
  654. XCTAssertEqual(credential.receipt, self.kTestReceipt)
  655. XCTAssertEqual(credential.secret, self.kTestSecret)
  656. default:
  657. XCTFail("Should be credential")
  658. }
  659. }
  660. verifyRequesterExpectation.fulfill()
  661. do {
  662. // Response for the underlying SendVerificationCode RPC call.
  663. try self.rpcIssuer?
  664. .respond(withJSON: [self.kVerificationIDKey: self.kTestVerificationID])
  665. } catch {
  666. XCTFail("Failure sending response: \(error)")
  667. }
  668. }
  669. // Use fake authURLPresenter so we can test the parameters that get sent to it.
  670. PhoneAuthProviderTests.auth?.authURLPresenter =
  671. FakePresenter(
  672. urlString: PhoneAuthProviderTests.kFakeRedirectURLStringWithReCAPTCHAToken,
  673. clientID: useClientID ? PhoneAuthProviderTests.kFakeClientID : nil,
  674. firebaseAppID: useClientID ? nil : PhoneAuthProviderTests.kFakeFirebaseAppID,
  675. errorTest: false,
  676. presenterError: nil
  677. )
  678. let uiDelegate = reCAPTCHAfallback ? FakeUIDelegate() : nil
  679. // 2. After setting up the fakes and parameters, call `verifyPhoneNumber`.
  680. provider
  681. .verifyPhoneNumber(kTestPhoneNumber, uiDelegate: uiDelegate) { verificationID, error in
  682. // 8. After the response triggers the callback in the FakePresenter, verify the callback.
  683. XCTAssertTrue(Thread.isMainThread)
  684. XCTAssertNil(error)
  685. XCTAssertEqual(verificationID, self.kTestVerificationID)
  686. expectation.fulfill()
  687. }
  688. waitForExpectations(timeout: 5)
  689. }
  690. /** @fn testVerifyClient
  691. @brief Tests verifying client before sending verification code.
  692. */
  693. private func internalTestVerify(errorString: String? = nil,
  694. errorURLString: String? = nil,
  695. errorCode: Int = 0,
  696. function: String,
  697. testMode: Bool = false,
  698. useClientID: Bool = false,
  699. bothClientAndAppID: Bool = false,
  700. reCAPTCHAfallback: Bool = false,
  701. forwardingNotification: Bool = true,
  702. presenterError: Error? = nil) async throws {
  703. initApp(function, useClientID: useClientID, bothClientAndAppID: bothClientAndAppID,
  704. testMode: testMode,
  705. forwardingNotification: forwardingNotification)
  706. let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
  707. let provider = PhoneAuthProvider.provider(auth: auth)
  708. var expectations: [XCTestExpectation] = []
  709. if !reCAPTCHAfallback {
  710. // Fake out appCredentialManager flow.
  711. auth.appCredentialManager?.credential = AuthAppCredential(receipt: kTestReceipt,
  712. secret: kTestSecret)
  713. } else {
  714. // 1. Intercept, handle, and test the projectConfiguration RPC calls.
  715. let projectConfigExpectation = expectation(description: "projectConfiguration")
  716. expectations.append(projectConfigExpectation)
  717. rpcIssuer?.projectConfigRequester = { request in
  718. XCTAssertEqual(request.apiKey, PhoneAuthProviderTests.kFakeAPIKey)
  719. projectConfigExpectation.fulfill()
  720. do {
  721. // Response for the underlying VerifyClientRequest RPC call.
  722. try self.rpcIssuer?.respond(
  723. withJSON: ["projectId": "kFakeProjectID",
  724. "authorizedDomains": [PhoneAuthProviderTests.kFakeAuthorizedDomain]]
  725. )
  726. } catch {
  727. XCTFail("Failure sending response: \(error)")
  728. }
  729. }
  730. }
  731. if reCAPTCHAfallback {
  732. // Use fake authURLPresenter so we can test the parameters that get sent to it.
  733. let urlString = errorURLString ??
  734. PhoneAuthProviderTests.kFakeRedirectURLStringWithReCAPTCHAToken
  735. let errorTest = errorURLString != nil
  736. PhoneAuthProviderTests.auth?.authURLPresenter =
  737. FakePresenter(
  738. urlString: urlString,
  739. clientID: useClientID ? PhoneAuthProviderTests.kFakeClientID : nil,
  740. firebaseAppID: useClientID ? nil : PhoneAuthProviderTests.kFakeFirebaseAppID,
  741. errorTest: errorTest,
  742. presenterError: presenterError
  743. )
  744. }
  745. if errorURLString == nil, presenterError == nil {
  746. let requestExpectation = expectation(description: "verifyRequester")
  747. expectations.append(requestExpectation)
  748. rpcIssuer?.verifyRequester = { request in
  749. XCTAssertEqual(request.phoneNumber, self.kTestPhoneNumber)
  750. switch request.codeIdentity {
  751. case let .credential(credential):
  752. XCTAssertFalse(reCAPTCHAfallback)
  753. XCTAssertEqual(credential.receipt, self.kTestReceipt)
  754. XCTAssertEqual(credential.secret, self.kTestSecret)
  755. case let .recaptcha(token):
  756. XCTAssertTrue(reCAPTCHAfallback)
  757. XCTAssertEqual(token, self.kFakeReCAPTCHAToken)
  758. case .empty:
  759. XCTAssertTrue(testMode)
  760. }
  761. requestExpectation.fulfill()
  762. do {
  763. // Response for the underlying SendVerificationCode RPC call.
  764. if let errorString {
  765. try self.rpcIssuer?.respond(serverErrorMessage: errorString)
  766. } else {
  767. try self.rpcIssuer?
  768. .respond(withJSON: [self.kVerificationIDKey: self.kTestVerificationID])
  769. }
  770. } catch {
  771. XCTFail("Failure sending response: \(error)")
  772. }
  773. }
  774. }
  775. let uiDelegate = reCAPTCHAfallback ? FakeUIDelegate() : nil
  776. // 2. After setting up the parameters, call `verifyPhoneNumber`.
  777. do {
  778. // Call the async function to verify the phone number
  779. let verificationID = try await provider.verifyPhoneNumber(
  780. kTestPhoneNumber,
  781. uiDelegate: uiDelegate
  782. )
  783. // Assert that the verificationID matches the expected value
  784. XCTAssertEqual(verificationID, kTestVerificationID)
  785. } catch {
  786. // If an error occurs, assert that verificationID is nil and the error code matches the
  787. // expected value
  788. XCTAssertEqual((error as NSError).code, errorCode)
  789. }
  790. await fulfillment(of: expectations, timeout: 5.0)
  791. }
  792. private func initApp(_ functionName: String,
  793. useClientID: Bool = false,
  794. bothClientAndAppID: Bool = false,
  795. testMode: Bool = false,
  796. forwardingNotification: Bool = true,
  797. fakeToken: Bool = false) {
  798. let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000",
  799. gcmSenderID: "00000000000000000-00000000000-000000000")
  800. options.apiKey = PhoneAuthProviderTests.kFakeAPIKey
  801. options.projectID = "myProjectID"
  802. if useClientID {
  803. options.clientID = PhoneAuthProviderTests.kFakeClientID
  804. }
  805. if !useClientID || bothClientAndAppID {
  806. // Use the appID.
  807. options.googleAppID = PhoneAuthProviderTests.kFakeFirebaseAppID
  808. }
  809. let scheme = useClientID ? PhoneAuthProviderTests.kFakeReverseClientID :
  810. PhoneAuthProviderTests.kFakeEncodedFirebaseAppID
  811. let strippedName = functionName.replacingOccurrences(of: "(", with: "")
  812. .replacingOccurrences(of: ")", with: "")
  813. FirebaseApp.configure(name: strippedName, options: options)
  814. let auth = Auth(app: FirebaseApp.app(name: strippedName)!, backend: authBackend)
  815. kAuthGlobalWorkQueue.sync {
  816. // Wait for Auth protectedDataInitialization to finish.
  817. PhoneAuthProviderTests.auth = auth
  818. if testMode {
  819. // Disable app verification.
  820. let settings = AuthSettings()
  821. settings.appVerificationDisabledForTesting = true
  822. auth.settings = settings
  823. }
  824. auth.notificationManager?.immediateCallbackForTestFaking = { forwardingNotification }
  825. auth.mainBundleUrlTypes = [["CFBundleURLSchemes": [scheme]]]
  826. if fakeToken {
  827. guard let data = "!@#$%^".data(using: .utf8) else {
  828. XCTFail("Failed to encode data for fake token")
  829. return
  830. }
  831. auth.tokenManager?.tokenStore = AuthAPNSToken(withData: data, type: .prod)
  832. } else {
  833. // Skip APNS token fetching.
  834. auth.tokenManager = FakeTokenManager(withApplication: UIApplication.shared)
  835. }
  836. }
  837. }
  838. class FakeAuthRecaptchaVerifier: AuthRecaptchaVerifier, @unchecked Sendable {
  839. var captchaResponse: String
  840. var error: Error?
  841. init(captchaResponse: String? = nil, error: Error? = nil) {
  842. self.captchaResponse = captchaResponse ?? "NO_RECAPTCHA"
  843. self.error = error
  844. super.init()
  845. }
  846. override func verify(forceRefresh: Bool, action: AuthRecaptchaAction) async throws -> String {
  847. if let error = error {
  848. throw error
  849. }
  850. return captchaResponse
  851. }
  852. }
  853. class FakeTokenManager: AuthAPNSTokenManager {
  854. override func getTokenInternal(callback: @escaping (Result<AuthAPNSToken, Error>) -> Void) {
  855. let error = NSError(domain: "dummy domain", code: AuthErrorCode.missingAppToken.rawValue)
  856. callback(.failure(error))
  857. }
  858. }
  859. class FakePresenter: NSObject, AuthWebViewControllerDelegate {
  860. func webViewController(_ webViewController: AuthWebViewController,
  861. canHandle URL: URL) -> Bool {
  862. XCTFail("Do not call")
  863. return false
  864. }
  865. func webViewControllerDidCancel(_ webViewController: AuthWebViewController) {
  866. XCTFail("Do not call")
  867. }
  868. func webViewController(_ webViewController: AuthWebViewController,
  869. didFailWithError error: Error) {
  870. XCTFail("Do not call")
  871. }
  872. func present(_ presentURL: URL,
  873. uiDelegate UIDelegate: AuthUIDelegate?,
  874. callbackMatcher: @escaping (URL?) -> Bool,
  875. completion: @escaping (URL?, Error?) -> Void) {
  876. // 5. Verify flow triggers present in the FakePresenter class with the right parameters.
  877. XCTAssertEqual(presentURL.scheme, "https")
  878. XCTAssertEqual(presentURL.host, kFakeAuthorizedDomain)
  879. XCTAssertEqual(presentURL.path, "/__/auth/handler")
  880. let actualURLComponents = URLComponents(url: presentURL, resolvingAgainstBaseURL: false)
  881. guard let _ = actualURLComponents?.queryItems else {
  882. XCTFail("Failed to get queryItems")
  883. return
  884. }
  885. let params = AuthWebUtils.dictionary(withHttpArgumentsString: presentURL.query)
  886. XCTAssertEqual(params["ibi"], Bundle.main.bundleIdentifier)
  887. XCTAssertEqual(params["apiKey"], PhoneAuthProviderTests.kFakeAPIKey)
  888. XCTAssertEqual(params["authType"], "verifyApp")
  889. XCTAssertNotNil(params["v"])
  890. if OAuthProviderTests.testTenantID {
  891. XCTAssertEqual(params["tid"], OAuthProviderTests.kFakeTenantID)
  892. } else {
  893. XCTAssertNil(params["tid"])
  894. }
  895. let appCheckToken = presentURL.fragment
  896. let verifyAppCheckToken = OAuthProviderTests.testAppCheck ? "fac=fakeAppCheckToken" : nil
  897. XCTAssertEqual(appCheckToken, verifyAppCheckToken)
  898. var redirectURL = ""
  899. if let clientID {
  900. XCTAssertEqual(params["clientId"], clientID)
  901. redirectURL = "\(kFakeReverseClientID)\(urlString)"
  902. }
  903. if let firebaseAppID {
  904. XCTAssertEqual(params["appId"], firebaseAppID)
  905. redirectURL = "\(kFakeEncodedFirebaseAppID)\(urlString)"
  906. }
  907. // 6. Test callbackMatcher
  908. // Verify that the URL is rejected by the callback matcher without the event ID.
  909. XCTAssertFalse(callbackMatcher(URL(string: "\(redirectURL)")))
  910. // Verify that the URL is accepted by the callback matcher with the matching event ID.
  911. guard let eventID = params["eventId"] else {
  912. XCTFail("Failed to get eventID")
  913. return
  914. }
  915. let redirectWithEventID = "\(redirectURL)%26eventId%3D\(eventID)"
  916. let originalComponents = URLComponents(string: redirectWithEventID)!
  917. XCTAssertEqual(callbackMatcher(originalComponents.url), !errorTest)
  918. var components = originalComponents
  919. components.query = "https"
  920. XCTAssertFalse(callbackMatcher(components.url))
  921. components = originalComponents
  922. components.host = "badhost"
  923. XCTAssertFalse(callbackMatcher(components.url))
  924. components = originalComponents
  925. components.path = "badpath"
  926. XCTAssertFalse(callbackMatcher(components.url))
  927. components = originalComponents
  928. components.query = "badquery"
  929. XCTAssertFalse(callbackMatcher(components.url))
  930. // 7. Do the callback to the original call.
  931. kAuthGlobalWorkQueue.async {
  932. if let presenterError = self.presenterError {
  933. completion(nil, presenterError)
  934. } else {
  935. completion(URL(string: "\(kFakeEncodedFirebaseAppID)\(self.urlString)") ?? nil, nil)
  936. }
  937. }
  938. }
  939. let urlString: String
  940. let clientID: String?
  941. let firebaseAppID: String?
  942. let errorTest: Bool
  943. let presenterError: Error?
  944. init(urlString: String, clientID: String?, firebaseAppID: String?, errorTest: Bool,
  945. presenterError: Error?) {
  946. self.urlString = urlString
  947. self.clientID = clientID
  948. self.firebaseAppID = firebaseAppID
  949. self.errorTest = errorTest
  950. self.presenterError = presenterError
  951. }
  952. }
  953. private class FakeUIDelegate: NSObject, AuthUIDelegate {
  954. func present(_ viewControllerToPresent: UIViewController, animated flag: Bool,
  955. completion: (() -> Void)? = nil) {
  956. guard let safariController = viewControllerToPresent as? SFSafariViewController,
  957. let delegate = safariController.delegate as? AuthURLPresenter,
  958. let uiDelegate = delegate.uiDelegate as? FakeUIDelegate else {
  959. XCTFail("Failed to get presentURL from controller")
  960. return
  961. }
  962. XCTAssertEqual(self, uiDelegate)
  963. }
  964. func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
  965. XCTFail("Implement me")
  966. }
  967. }
  968. private static let kFakeRedirectURLStringInvalidClientID =
  969. "//firebaseauth/" +
  970. "link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fcal" +
  971. "lback%3FfirebaseError%3D%257B%2522code%2522%253A%2522auth%252Finvalid-oauth-client-id%2522%252" +
  972. "C%2522message%2522%253A%2522The%2520OAuth%2520client%2520ID%2520provided%2520is%2520either%252" +
  973. "0invalid%2520or%2520does%2520not%2520match%2520the%2520specified%2520API%2520key.%2522%257D%26" +
  974. "authType%3DverifyApp"
  975. private static let kFakeRedirectURLStringWebNetworkRequestFailed =
  976. "//firebaseauth/link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fc" +
  977. "allback%3FfirebaseError%3D%257B%2522code%2522%253A%2522auth%252Fnetwork-request-failed%2522%" +
  978. "252C%2522message%2522%253A%2522The%2520network%2520request%2520failed%2520.%2522%257D%" +
  979. "26authType%3DverifyApp"
  980. private static let kFakeRedirectURLStringWebInternalError =
  981. "//firebaseauth/link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fcal" +
  982. "lback%3FfirebaseError%3D%257B%2522code%2522%253A%2522auth%252Finternal-error%2522%252C%" +
  983. "2522message%2522%253A%2522Internal%2520error%2520.%2522%257D%26authType%3DverifyApp"
  984. private static let kFakeRedirectURLStringUnknownError =
  985. "//firebaseauth/link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fcal" +
  986. "lback%3FfirebaseError%3D%257B%2522code%2522%253A%2522auth%252Funknown-error-id%2522%252" +
  987. "C%2522message%2522%253A%2522The%2520OAuth%2520client%2520ID%2520provided%2520is%2520either%252" +
  988. "0invalid%2520or%2520does%2520not%2520match%2520the%2520specified%2520API%2520key.%2522%257D%26" +
  989. "authType%3DverifyApp"
  990. private static let kFakeRedirectURLStringUnstructuredError =
  991. "//firebaseauth/link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fcal" +
  992. "lback%3FfirebaseError%3D%257B%2522unstructuredcode%2522%253A%2522auth%252Funknown-error-id%" +
  993. "2522%252" +
  994. "C%2522unstructuredmessage%2522%253A%2522The%2520OAuth%2520client%2520ID%2520provided%2520is%" +
  995. "2520either%252" +
  996. "0invalid%2520or%2520does%2520not%2520match%2520the%2520specified%2520API%2520key.%2522%257D%" +
  997. "26authType%3DverifyApp"
  998. private static let kFakeRedirectURLStringWithReCAPTCHAToken =
  999. "://firebaseauth/" +
  1000. "link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fcallback%3FauthType%" +
  1001. "3DverifyApp%26recaptchaToken%3DfakeReCAPTCHAToken"
  1002. }
  1003. #endif