PhoneAuthProviderTests.swift 46 KB

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