PhoneAuthProviderTests.swift 46 KB

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