PhoneAuthProviderTests.swift 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849
  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. import FirebaseCore
  18. @testable import FirebaseAuth
  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. static var auth: Auth?
  40. /** @fn testCredentialWithVerificationID
  41. @brief Tests the @c credentialWithToken method to make sure that it returns a valid AuthCredential instance.
  42. */
  43. func testCredentialWithVerificationID() throws {
  44. initApp(#function)
  45. let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
  46. let provider = PhoneAuthProvider.provider(auth: auth)
  47. let credential = provider.credential(withVerificationID: kTestVerificationID,
  48. verificationCode: kTestVerificationCode)
  49. switch credential.credentialKind {
  50. case .phoneNumber: XCTFail("Should be verification case")
  51. case let .verification(id, code):
  52. XCTAssertEqual(id, kTestVerificationID)
  53. XCTAssertEqual(code, kTestVerificationCode)
  54. }
  55. }
  56. /** @fn testVerifyEmptyPhoneNumber
  57. @brief Tests a failed invocation @c verifyPhoneNumber:completion: because an empty phone
  58. number was provided.
  59. */
  60. func testVerifyEmptyPhoneNumber() throws {
  61. initApp(#function)
  62. let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
  63. let provider = PhoneAuthProvider.provider(auth: auth)
  64. let expectation = self.expectation(description: #function)
  65. // Empty phone number is checked on the client side so no backend RPC is faked.
  66. provider.verifyPhoneNumber("", uiDelegate: nil) { verificationID, error in
  67. XCTAssertNotNil(error)
  68. XCTAssertNil(verificationID)
  69. XCTAssertEqual((error as? NSError)?.code, AuthErrorCode.missingPhoneNumber.rawValue)
  70. expectation.fulfill()
  71. }
  72. waitForExpectations(timeout: 5)
  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() throws {
  79. try 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() throws {
  87. try internalTestVerify(function: #function)
  88. }
  89. /** @fn testVerifyPhoneNumberInTestMode
  90. @brief Tests a successful invocation of @c verifyPhoneNumber:completion: when app verification
  91. is disabled.
  92. */
  93. func testVerifyPhoneNumberInTestMode() throws {
  94. try internalTestVerify(function: #function, testMode: true)
  95. }
  96. /** @fn testVerifyPhoneNumberInTestModeFailure
  97. @brief Tests a failed invocation of @c verifyPhoneNumber:completion: when app verification
  98. is disabled.
  99. */
  100. func testVerifyPhoneNumberInTestModeFailure() throws {
  101. try internalTestVerify(errorString: "INVALID_PHONE_NUMBER",
  102. errorCode: AuthErrorCode.invalidPhoneNumber.rawValue,
  103. function: #function, testMode: true)
  104. }
  105. /** @fn testVerifyPhoneNumberUIDelegateFirebaseAppIdFlow
  106. @brief Tests a successful invocation of @c verifyPhoneNumber:UIDelegate:completion:.
  107. */
  108. func testVerifyPhoneNumberUIDelegateFirebaseAppIdFlow() throws {
  109. try internalTestVerify(function: #function, reCAPTCHAfallback: true)
  110. }
  111. /** @fn testVerifyPhoneNumberUIDelegateFirebaseAppIdWhileClientIdPresentFlow
  112. @brief Tests a successful invocation of @c verifyPhoneNumber:UIDelegate:completion: when the
  113. client ID is present in the plist file, but the encoded app ID is the registered custom URL scheme.
  114. */
  115. func testVerifyPhoneNumberUIDelegateFirebaseAppIdWhileClientIdPresentFlow() throws {
  116. try internalTestVerify(function: #function, useClientID: true,
  117. bothClientAndAppID: true, reCAPTCHAfallback: true)
  118. }
  119. /** @fn testVerifyPhoneNumberUIDelegateClientIdFlow
  120. @brief Tests a successful invocation of @c verifyPhoneNumber:UIDelegate:completion:.
  121. */
  122. func testVerifyPhoneNumberUIDelegateClientIdFlow() throws {
  123. try internalTestVerify(function: #function, useClientID: true, reCAPTCHAfallback: true)
  124. }
  125. /** @fn testVerifyPhoneNumberUIDelegateInvalidClientID
  126. @brief Tests a invocation of @c verifyPhoneNumber:UIDelegate:completion: which results in an
  127. invalid client ID error.
  128. */
  129. func testVerifyPhoneNumberUIDelegateInvalidClientID() throws {
  130. try internalTestVerify(
  131. errorURLString: PhoneAuthProviderTests.kFakeRedirectURLStringInvalidClientID,
  132. errorCode: AuthErrorCode.invalidClientID.rawValue,
  133. function: #function,
  134. useClientID: true,
  135. reCAPTCHAfallback: true
  136. )
  137. }
  138. /** @fn testVerifyPhoneNumberUIDelegateWebNetworkRequestFailed
  139. @brief Tests a invocation of @c verifyPhoneNumber:UIDelegate:completion: which results in a web
  140. network request failed error.
  141. */
  142. func testVerifyPhoneNumberUIDelegateWebNetworkRequestFailed() throws {
  143. try internalTestVerify(
  144. errorURLString: PhoneAuthProviderTests.kFakeRedirectURLStringWebNetworkRequestFailed,
  145. errorCode: AuthErrorCode.webNetworkRequestFailed.rawValue,
  146. function: #function,
  147. useClientID: true,
  148. reCAPTCHAfallback: true
  149. )
  150. }
  151. /** @fn testVerifyPhoneNumberUIDelegateWebInternalError
  152. @brief Tests a invocation of @c verifyPhoneNumber:UIDelegate:completion: which results in a web
  153. internal error.
  154. */
  155. func testVerifyPhoneNumberUIDelegateWebInternalError() throws {
  156. try internalTestVerify(
  157. errorURLString: PhoneAuthProviderTests.kFakeRedirectURLStringWebInternalError,
  158. errorCode: AuthErrorCode.webInternalError.rawValue,
  159. function: #function,
  160. useClientID: true,
  161. reCAPTCHAfallback: true
  162. )
  163. }
  164. /** @fn testVerifyPhoneNumberUIDelegateUnexpectedError
  165. @brief Tests a invocation of @c verifyPhoneNumber:UIDelegate:completion: which results in an
  166. invalid client ID.
  167. */
  168. func testVerifyPhoneNumberUIDelegateUnexpectedError() throws {
  169. try internalTestVerify(
  170. errorURLString: PhoneAuthProviderTests.kFakeRedirectURLStringUnknownError,
  171. errorCode: AuthErrorCode.webSignInUserInteractionFailure.rawValue,
  172. function: #function,
  173. useClientID: true,
  174. reCAPTCHAfallback: true
  175. )
  176. }
  177. /** @fn testVerifyPhoneNumberUIDelegateUnstructuredError
  178. @brief Tests a invocation of @c verifyPhoneNumber:UIDelegate:completion: which results in an
  179. error being surfaced with a default NSLocalizedFailureReasonErrorKey due to an unexpected
  180. structure of the error response.
  181. */
  182. func testVerifyPhoneNumberUIDelegateUnstructuredError() throws {
  183. try internalTestVerify(
  184. errorURLString: PhoneAuthProviderTests.kFakeRedirectURLStringUnstructuredError,
  185. errorCode: AuthErrorCode.appVerificationUserInteractionFailure.rawValue,
  186. function: #function,
  187. useClientID: true,
  188. reCAPTCHAfallback: true
  189. )
  190. }
  191. // TODO: This test is skipped. What was formerly an Objective C exception is now a Swift fatal_error.
  192. // The test runs correctly, but it's not clear how to automate fatal_error testing. Switching to
  193. // Swift exceptions would break the API.
  194. /** @fn testVerifyPhoneNumberUIDelegateRaiseException
  195. @brief Tests a invocation of @c verifyPhoneNumber:UIDelegate:completion: which results in an
  196. exception.
  197. */
  198. func SKIPtestVerifyPhoneNumberUIDelegateRaiseException() throws {
  199. initApp(#function)
  200. let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
  201. auth.mainBundleUrlTypes = [["CFBundleURLSchemes": ["fail"]]]
  202. let provider = PhoneAuthProvider.provider(auth: auth)
  203. provider.verifyPhoneNumber(kTestPhoneNumber, uiDelegate: nil) { verificationID, error in
  204. XCTFail("Should not call completion")
  205. }
  206. }
  207. /** @fn testNotForwardingNotification
  208. @brief Tests returning an error for the app failing to forward notification.
  209. */
  210. func testNotForwardingNotification() throws {
  211. func testVerifyPhoneNumberUIDelegateUnstructuredError() throws {
  212. try internalTestVerify(
  213. errorURLString: PhoneAuthProviderTests.kFakeRedirectURLStringUnstructuredError,
  214. errorCode: AuthErrorCode.appVerificationUserInteractionFailure.rawValue,
  215. function: #function,
  216. useClientID: true,
  217. reCAPTCHAfallback: true,
  218. forwardingNotification: false
  219. )
  220. }
  221. }
  222. /** @fn testMissingAPNSToken
  223. @brief Tests returning an error for the app failing to provide an APNS device token.
  224. */
  225. func testMissingAPNSToken() throws {
  226. try internalTestVerify(
  227. errorCode: AuthErrorCode.missingAppToken.rawValue,
  228. function: #function,
  229. useClientID: true,
  230. reCAPTCHAfallback: true,
  231. presenterError: NSError(
  232. domain: AuthErrors.domain,
  233. code: AuthErrorCode.missingAppToken.rawValue
  234. )
  235. )
  236. }
  237. /** @fn testVerifyPhoneNumberUIDelegateiOSSecretMissingFlow
  238. @brief Tests a successful invocation of @c verifyPhoneNumber:UIDelegate:completion: that falls
  239. back to the reCAPTCHA flow when the push notification is not received before the timeout.
  240. */
  241. func testVerifyPhoneNumberUIDelegateiOSSecretMissingFlow() throws {
  242. try internalFlow(function: #function, useClientID: false, reCAPTCHAfallback: true)
  243. }
  244. /** @fn testVerifyClient
  245. @brief Tests verifying client before sending verification code.
  246. */
  247. func testVerifyClient() throws {
  248. try internalFlow(function: #function, useClientID: true, reCAPTCHAfallback: false)
  249. }
  250. /** @fn testSendVerificationCodeFailedRetry
  251. @brief Tests failed retry after failing to send verification code.
  252. */
  253. func testSendVerificationCodeFailedRetry() throws {
  254. try internalFlowRetry(function: #function)
  255. }
  256. /** @fn testSendVerificationCodeSuccessfulRetry
  257. @brief Tests successful retry after failing to send verification code.
  258. */
  259. func testSendVerificationCodeSuccessfulRetry() throws {
  260. try internalFlowRetry(function: #function, goodRetry: true)
  261. }
  262. /** @fn testPhoneAuthCredentialCoding
  263. @brief Tests successful archiving and unarchiving of @c PhoneAuthCredential.
  264. */
  265. func testPhoneAuthCredentialCoding() throws {
  266. let kVerificationID = "My verificationID"
  267. let kVerificationCode = "1234"
  268. let credential = PhoneAuthCredential(withProviderID: PhoneAuthProvider.id,
  269. verificationID: kVerificationID,
  270. verificationCode: kVerificationCode)
  271. XCTAssertTrue(PhoneAuthCredential.supportsSecureCoding)
  272. let data = try NSKeyedArchiver.archivedData(
  273. withRootObject: credential,
  274. requiringSecureCoding: true
  275. )
  276. let unarchivedCredential = try XCTUnwrap(NSKeyedUnarchiver.unarchivedObject(
  277. ofClass: PhoneAuthCredential.self, from: data
  278. ))
  279. switch unarchivedCredential.credentialKind {
  280. case .phoneNumber: XCTFail("Should be verification case")
  281. case let .verification(id, code):
  282. XCTAssertEqual(id, kVerificationID)
  283. XCTAssertEqual(code, kVerificationCode)
  284. }
  285. XCTAssertEqual(unarchivedCredential.provider, PhoneAuthProvider.id)
  286. }
  287. /** @fn testPhoneAuthCredentialCodingPhone
  288. @brief Tests successful archiving and unarchiving of @c PhoneAuthCredential after other constructor.
  289. */
  290. func testPhoneAuthCredentialCodingPhone() throws {
  291. let kTemporaryProof = "Proof"
  292. let kPhoneNumber = "123457"
  293. let credential = PhoneAuthCredential(withTemporaryProof: kTemporaryProof,
  294. phoneNumber: kPhoneNumber,
  295. providerID: PhoneAuthProvider.id)
  296. XCTAssertTrue(PhoneAuthCredential.supportsSecureCoding)
  297. let data = try NSKeyedArchiver.archivedData(
  298. withRootObject: credential,
  299. requiringSecureCoding: true
  300. )
  301. let unarchivedCredential = try XCTUnwrap(NSKeyedUnarchiver.unarchivedObject(
  302. ofClasses: [PhoneAuthCredential.self, NSString.self], from: data
  303. ) as? PhoneAuthCredential)
  304. switch unarchivedCredential.credentialKind {
  305. case let .phoneNumber(phoneNumber, temporaryProof):
  306. XCTAssertEqual(temporaryProof, kTemporaryProof)
  307. XCTAssertEqual(phoneNumber, kPhoneNumber)
  308. case .verification: XCTFail("Should be phoneNumber case")
  309. }
  310. XCTAssertEqual(unarchivedCredential.provider, PhoneAuthProvider.id)
  311. }
  312. private func internalFlowRetry(function: String, goodRetry: Bool = false) throws {
  313. let function = function
  314. initApp(function, useClientID: true, fakeToken: true)
  315. let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
  316. let provider = PhoneAuthProvider.provider(auth: auth)
  317. let expectation = self.expectation(description: function)
  318. // Fake push notification.
  319. auth.appCredentialManager.fakeCredential = AuthAppCredential(
  320. receipt: kTestReceipt,
  321. secret: kTestSecret
  322. )
  323. // 1. Intercept, handle, and test three RPC calls.
  324. let verifyClientRequestExpectation = self.expectation(description: "verifyClientRequest")
  325. verifyClientRequestExpectation.expectedFulfillmentCount = 2
  326. rpcIssuer?.verifyClientRequester = { request in
  327. XCTAssertEqual(request.appToken, "21402324255E")
  328. XCTAssertFalse(request.isSandbox)
  329. verifyClientRequestExpectation.fulfill()
  330. do {
  331. // Response for the underlying VerifyClientRequest RPC call.
  332. try self.rpcIssuer?.respond(withJSON: [
  333. "receipt": self.kTestReceipt,
  334. "suggestedTimeout": self.kTestTimeout,
  335. ])
  336. } catch {
  337. XCTFail("Failure sending response: \(error)")
  338. }
  339. }
  340. let verifyRequesterExpectation = self.expectation(description: "verifyRequester")
  341. verifyRequesterExpectation.expectedFulfillmentCount = 2
  342. var visited = false
  343. rpcIssuer?.verifyRequester = { request in
  344. XCTAssertEqual(request.phoneNumber, self.kTestPhoneNumber)
  345. XCTAssertEqual(request.appCredential?.receipt, self.kTestReceipt)
  346. XCTAssertEqual(request.appCredential?.secret, self.kTestSecret)
  347. verifyRequesterExpectation.fulfill()
  348. do {
  349. if visited == false || goodRetry == false {
  350. // First Response for the underlying SendVerificationCode RPC call.
  351. try self.rpcIssuer?.respond(serverErrorMessage: "INVALID_APP_CREDENTIAL")
  352. visited = true
  353. } else {
  354. // Second Response for the underlying SendVerificationCode RPC call.
  355. try self.rpcIssuer?
  356. .respond(withJSON: [self.kVerificationIDKey: self.kTestVerificationID])
  357. }
  358. } catch {
  359. XCTFail("Failure sending response: \(error)")
  360. }
  361. }
  362. // Use fake authURLPresenter so we can test the parameters that get sent to it.
  363. PhoneAuthProviderTests.auth?.authURLPresenter =
  364. FakePresenter(
  365. urlString: PhoneAuthProviderTests.kFakeRedirectURLStringWithReCAPTCHAToken,
  366. clientID: PhoneAuthProviderTests.kFakeClientID,
  367. firebaseAppID: nil,
  368. errorTest: false,
  369. presenterError: nil
  370. )
  371. // 2. After setting up the fakes and parameters, call `verifyPhoneNumber`.
  372. provider
  373. .verifyPhoneNumber(kTestPhoneNumber, uiDelegate: nil) { verificationID, error in
  374. // 8. After the response triggers the callback in the FakePresenter, verify the callback.
  375. XCTAssertTrue(Thread.isMainThread)
  376. if goodRetry {
  377. XCTAssertNil(error)
  378. XCTAssertEqual(verificationID, self.kTestVerificationID)
  379. } else {
  380. XCTAssertNil(verificationID)
  381. XCTAssertEqual((error as? NSError)?.code, AuthErrorCode.internalError.rawValue)
  382. }
  383. expectation.fulfill()
  384. }
  385. waitForExpectations(timeout: 5)
  386. }
  387. private func internalFlow(function: String,
  388. useClientID: Bool = false,
  389. reCAPTCHAfallback: Bool = false) throws {
  390. let function = function
  391. initApp(function, useClientID: useClientID, fakeToken: true)
  392. let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
  393. let provider = PhoneAuthProvider.provider(auth: auth)
  394. let expectation = self.expectation(description: function)
  395. // Fake push notification.
  396. auth.appCredentialManager.fakeCredential = AuthAppCredential(
  397. receipt: kTestReceipt,
  398. secret: reCAPTCHAfallback ? nil : kTestSecret
  399. )
  400. // 1. Intercept, handle, and test three RPC calls.
  401. let verifyClientRequestExpectation = self.expectation(description: "verifyClientRequest")
  402. rpcIssuer?.verifyClientRequester = { request in
  403. XCTAssertEqual(request.appToken, "21402324255E")
  404. XCTAssertFalse(request.isSandbox)
  405. verifyClientRequestExpectation.fulfill()
  406. do {
  407. // Response for the underlying VerifyClientRequest RPC call.
  408. try self.rpcIssuer?.respond(withJSON: [
  409. "receipt": self.kTestReceipt,
  410. "suggestedTimeout": self.kTestTimeout,
  411. ])
  412. } catch {
  413. XCTFail("Failure sending response: \(error)")
  414. }
  415. }
  416. if reCAPTCHAfallback {
  417. let projectConfigExpectation = self.expectation(description: "projectConfiguration")
  418. rpcIssuer?.projectConfigRequester = { request in
  419. XCTAssertEqual(request.apiKey, PhoneAuthProviderTests.kFakeAPIKey)
  420. projectConfigExpectation.fulfill()
  421. kAuthGlobalWorkQueue.async {
  422. do {
  423. // Response for the underlying VerifyClientRequest RPC call.
  424. try self.rpcIssuer?.respond(
  425. withJSON: ["projectId": "kFakeProjectID",
  426. "authorizedDomains": [PhoneAuthProviderTests.kFakeAuthorizedDomain]]
  427. )
  428. } catch {
  429. XCTFail("Failure sending response: \(error)")
  430. }
  431. }
  432. }
  433. }
  434. let verifyRequesterExpectation = self.expectation(description: "verifyRequester")
  435. rpcIssuer?.verifyRequester = { request in
  436. XCTAssertEqual(request.phoneNumber, self.kTestPhoneNumber)
  437. if reCAPTCHAfallback {
  438. XCTAssertNil(request.appCredential)
  439. XCTAssertEqual(request.reCAPTCHAToken, self.kFakeReCAPTCHAToken)
  440. } else {
  441. XCTAssertEqual(request.appCredential?.receipt, self.kTestReceipt)
  442. XCTAssertEqual(request.appCredential?.secret, self.kTestSecret)
  443. }
  444. verifyRequesterExpectation.fulfill()
  445. do {
  446. // Response for the underlying SendVerificationCode RPC call.
  447. try self.rpcIssuer?
  448. .respond(withJSON: [self.kVerificationIDKey: self.kTestVerificationID])
  449. } catch {
  450. XCTFail("Failure sending response: \(error)")
  451. }
  452. }
  453. // Use fake authURLPresenter so we can test the parameters that get sent to it.
  454. PhoneAuthProviderTests.auth?.authURLPresenter =
  455. FakePresenter(
  456. urlString: PhoneAuthProviderTests.kFakeRedirectURLStringWithReCAPTCHAToken,
  457. clientID: useClientID ? PhoneAuthProviderTests.kFakeClientID : nil,
  458. firebaseAppID: useClientID ? nil : PhoneAuthProviderTests.kFakeFirebaseAppID,
  459. errorTest: false,
  460. presenterError: nil
  461. )
  462. let uiDelegate = reCAPTCHAfallback ? FakeUIDelegate() : nil
  463. // 2. After setting up the fakes and parameters, call `verifyPhoneNumber`.
  464. provider
  465. .verifyPhoneNumber(kTestPhoneNumber, uiDelegate: uiDelegate) { verificationID, error in
  466. // 8. After the response triggers the callback in the FakePresenter, verify the callback.
  467. XCTAssertTrue(Thread.isMainThread)
  468. XCTAssertNil(error)
  469. XCTAssertEqual(verificationID, self.kTestVerificationID)
  470. expectation.fulfill()
  471. }
  472. waitForExpectations(timeout: 5)
  473. }
  474. /** @fn testVerifyClient
  475. @brief Tests verifying client before sending verification code.
  476. */
  477. private func internalTestVerify(errorString: String? = nil,
  478. errorURLString: String? = nil,
  479. errorCode: Int = 0,
  480. function: String,
  481. testMode: Bool = false,
  482. useClientID: Bool = false,
  483. bothClientAndAppID: Bool = false,
  484. reCAPTCHAfallback: Bool = false,
  485. forwardingNotification: Bool = true,
  486. presenterError: Error? = nil) throws {
  487. initApp(function, useClientID: useClientID, bothClientAndAppID: bothClientAndAppID,
  488. testMode: testMode,
  489. forwardingNotification: forwardingNotification)
  490. let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
  491. let provider = PhoneAuthProvider.provider(auth: auth)
  492. let expectation = self.expectation(description: function)
  493. if !reCAPTCHAfallback {
  494. // Fake out appCredentialManager flow.
  495. auth.appCredentialManager.credential = AuthAppCredential(receipt: kTestReceipt,
  496. secret: kTestSecret)
  497. } else {
  498. // 1. Intercept, handle, and test the projectConfiguration RPC calls.
  499. let projectConfigExpectation = self.expectation(description: "projectConfiguration")
  500. rpcIssuer?.projectConfigRequester = { request in
  501. XCTAssertEqual(request.apiKey, PhoneAuthProviderTests.kFakeAPIKey)
  502. projectConfigExpectation.fulfill()
  503. do {
  504. // Response for the underlying VerifyClientRequest RPC call.
  505. try self.rpcIssuer?.respond(
  506. withJSON: ["projectId": "kFakeProjectID",
  507. "authorizedDomains": [PhoneAuthProviderTests.kFakeAuthorizedDomain]]
  508. )
  509. } catch {
  510. XCTFail("Failure sending response: \(error)")
  511. }
  512. }
  513. }
  514. if errorURLString == nil, presenterError == nil {
  515. let requestExpectation = self.expectation(description: "verifyRequester")
  516. rpcIssuer?.verifyRequester = { request in
  517. XCTAssertEqual(request.phoneNumber, self.kTestPhoneNumber)
  518. if reCAPTCHAfallback {
  519. XCTAssertNil(request.appCredential)
  520. XCTAssertEqual(request.reCAPTCHAToken, self.kFakeReCAPTCHAToken)
  521. } else if testMode {
  522. XCTAssertNil(request.appCredential)
  523. XCTAssertNil(request.reCAPTCHAToken)
  524. } else {
  525. XCTAssertEqual(request.appCredential?.receipt, self.kTestReceipt)
  526. XCTAssertEqual(request.appCredential?.secret, self.kTestSecret)
  527. }
  528. requestExpectation.fulfill()
  529. do {
  530. // Response for the underlying SendVerificationCode RPC call.
  531. if let errorString {
  532. try self.rpcIssuer?.respond(serverErrorMessage: errorString)
  533. } else {
  534. try self.rpcIssuer?
  535. .respond(withJSON: [self.kVerificationIDKey: self.kTestVerificationID])
  536. }
  537. } catch {
  538. XCTFail("Failure sending response: \(error)")
  539. }
  540. }
  541. }
  542. if reCAPTCHAfallback {
  543. // Use fake authURLPresenter so we can test the parameters that get sent to it.
  544. let urlString = errorURLString ??
  545. PhoneAuthProviderTests.kFakeRedirectURLStringWithReCAPTCHAToken
  546. let errorTest = errorURLString != nil
  547. PhoneAuthProviderTests.auth?.authURLPresenter =
  548. FakePresenter(
  549. urlString: urlString,
  550. clientID: useClientID ? PhoneAuthProviderTests.kFakeClientID : nil,
  551. firebaseAppID: useClientID ? nil : PhoneAuthProviderTests.kFakeFirebaseAppID,
  552. errorTest: errorTest,
  553. presenterError: presenterError
  554. )
  555. }
  556. let uiDelegate = reCAPTCHAfallback ? FakeUIDelegate() : nil
  557. // 2. After setting up the parameters, call `verifyPhoneNumber`.
  558. provider
  559. .verifyPhoneNumber(kTestPhoneNumber, uiDelegate: uiDelegate) { verificationID, error in
  560. // 8. After the response triggers the callback in the FakePresenter, verify the callback.
  561. XCTAssertTrue(Thread.isMainThread)
  562. if errorCode != 0 {
  563. XCTAssertNil(verificationID)
  564. XCTAssertEqual((error as? NSError)?.code, errorCode)
  565. } else {
  566. XCTAssertNil(error)
  567. XCTAssertEqual(verificationID, self.kTestVerificationID)
  568. }
  569. expectation.fulfill()
  570. }
  571. waitForExpectations(timeout: 5)
  572. }
  573. private func initApp(_ functionName: String,
  574. useClientID: Bool = false,
  575. bothClientAndAppID: Bool = false,
  576. testMode: Bool = false,
  577. forwardingNotification: Bool = true,
  578. fakeToken: Bool = false) {
  579. let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000",
  580. gcmSenderID: "00000000000000000-00000000000-000000000")
  581. options.apiKey = PhoneAuthProviderTests.kFakeAPIKey
  582. options.projectID = "myProjectID"
  583. if useClientID {
  584. options.clientID = PhoneAuthProviderTests.kFakeClientID
  585. }
  586. if !useClientID || bothClientAndAppID {
  587. // Use the appID.
  588. options.googleAppID = PhoneAuthProviderTests.kFakeFirebaseAppID
  589. }
  590. let scheme = useClientID ? PhoneAuthProviderTests.kFakeReverseClientID :
  591. PhoneAuthProviderTests.kFakeEncodedFirebaseAppID
  592. let strippedName = functionName.replacingOccurrences(of: "(", with: "")
  593. .replacingOccurrences(of: ")", with: "")
  594. FirebaseApp.configure(name: strippedName, options: options)
  595. let auth = Auth.auth(app: FirebaseApp.app(name: strippedName)!)
  596. kAuthGlobalWorkQueue.sync {
  597. // Wait for Auth protectedDataInitialization to finish.
  598. PhoneAuthProviderTests.auth = auth
  599. if testMode {
  600. // Disable app verification.
  601. let settings = AuthSettings()
  602. settings.appVerificationDisabledForTesting = true
  603. auth.settings = settings
  604. }
  605. auth.notificationManager.immediateCallbackForTestFaking = { forwardingNotification }
  606. auth.mainBundleUrlTypes = [["CFBundleURLSchemes": [scheme]]]
  607. if fakeToken {
  608. guard let data = "!@#$%^".data(using: .utf8) else {
  609. XCTFail("Failed to encode data for fake token")
  610. return
  611. }
  612. auth.tokenManager.tokenStore = AuthAPNSToken(withData: data, type: .prod)
  613. } else {
  614. // Skip APNS token fetching.
  615. auth.tokenManager.failFastForTesting = true
  616. }
  617. }
  618. }
  619. private class FakeApplication: Application {
  620. var delegate: UIApplicationDelegate?
  621. }
  622. class FakePresenter: NSObject, AuthWebViewControllerDelegate {
  623. func webViewController(_ webViewController: AuthWebViewController,
  624. canHandle URL: URL) -> Bool {
  625. XCTFail("Do not call")
  626. return false
  627. }
  628. func webViewControllerDidCancel(_ webViewController: AuthWebViewController) {
  629. XCTFail("Do not call")
  630. }
  631. func webViewController(_ webViewController: AuthWebViewController,
  632. didFailWithError error: Error) {
  633. XCTFail("Do not call")
  634. }
  635. func present(_ presentURL: URL,
  636. uiDelegate UIDelegate: AuthUIDelegate?,
  637. callbackMatcher: @escaping (URL?) -> Bool,
  638. completion: @escaping (URL?, Error?) -> Void) {
  639. // 5. Verify flow triggers present in the FakePresenter class with the right parameters.
  640. XCTAssertEqual(presentURL.scheme, "https")
  641. XCTAssertEqual(presentURL.host, kFakeAuthorizedDomain)
  642. XCTAssertEqual(presentURL.path, "/__/auth/handler")
  643. let actualURLComponents = URLComponents(url: presentURL, resolvingAgainstBaseURL: false)
  644. guard let _ = actualURLComponents?.queryItems else {
  645. XCTFail("Failed to get queryItems")
  646. return
  647. }
  648. let params = AuthWebUtils.dictionary(withHttpArgumentsString: presentURL.query)
  649. XCTAssertEqual(params["ibi"], Bundle.main.bundleIdentifier)
  650. XCTAssertEqual(params["apiKey"], PhoneAuthProviderTests.kFakeAPIKey)
  651. XCTAssertEqual(params["authType"], "verifyApp")
  652. XCTAssertNotNil(params["v"])
  653. if OAuthProviderTests.testTenantID {
  654. XCTAssertEqual(params["tid"], OAuthProviderTests.kFakeTenantID)
  655. } else {
  656. XCTAssertNil(params["tid"])
  657. }
  658. let appCheckToken = presentURL.fragment
  659. let verifyAppCheckToken = OAuthProviderTests.testAppCheck ? "fac=fakeAppCheckToken" : nil
  660. XCTAssertEqual(appCheckToken, verifyAppCheckToken)
  661. var redirectURL = ""
  662. if let clientID {
  663. XCTAssertEqual(params["clientId"], clientID)
  664. redirectURL = "\(kFakeReverseClientID)\(urlString)"
  665. }
  666. if let firebaseAppID {
  667. XCTAssertEqual(params["appId"], firebaseAppID)
  668. redirectURL = "\(kFakeEncodedFirebaseAppID)\(urlString)"
  669. }
  670. // 6. Test callbackMatcher
  671. // Verify that the URL is rejected by the callback matcher without the event ID.
  672. XCTAssertFalse(callbackMatcher(URL(string: "\(redirectURL)")))
  673. // Verify that the URL is accepted by the callback matcher with the matching event ID.
  674. guard let eventID = params["eventId"] else {
  675. XCTFail("Failed to get eventID")
  676. return
  677. }
  678. let redirectWithEventID = "\(redirectURL)%26eventId%3D\(eventID)"
  679. let originalComponents = URLComponents(string: redirectWithEventID)!
  680. XCTAssertEqual(callbackMatcher(originalComponents.url), !errorTest)
  681. var components = originalComponents
  682. components.query = "https"
  683. XCTAssertFalse(callbackMatcher(components.url))
  684. components = originalComponents
  685. components.host = "badhost"
  686. XCTAssertFalse(callbackMatcher(components.url))
  687. components = originalComponents
  688. components.path = "badpath"
  689. XCTAssertFalse(callbackMatcher(components.url))
  690. components = originalComponents
  691. components.query = "badquery"
  692. XCTAssertFalse(callbackMatcher(components.url))
  693. // 7. Do the callback to the original call.
  694. kAuthGlobalWorkQueue.async {
  695. if let presenterError = self.presenterError {
  696. completion(nil, presenterError)
  697. } else {
  698. completion(URL(string: "\(kFakeEncodedFirebaseAppID)\(self.urlString)") ?? nil, nil)
  699. }
  700. }
  701. }
  702. let urlString: String
  703. let clientID: String?
  704. let firebaseAppID: String?
  705. let errorTest: Bool
  706. let presenterError: Error?
  707. init(urlString: String, clientID: String?, firebaseAppID: String?, errorTest: Bool,
  708. presenterError: Error?) {
  709. self.urlString = urlString
  710. self.clientID = clientID
  711. self.firebaseAppID = firebaseAppID
  712. self.errorTest = errorTest
  713. self.presenterError = presenterError
  714. }
  715. }
  716. private class FakeUIDelegate: NSObject, AuthUIDelegate {
  717. func present(_ viewControllerToPresent: UIViewController, animated flag: Bool,
  718. completion: (() -> Void)? = nil) {
  719. guard let safariController = viewControllerToPresent as? SFSafariViewController,
  720. let delegate = safariController.delegate as? AuthURLPresenter,
  721. let uiDelegate = delegate.uiDelegate as? FakeUIDelegate else {
  722. XCTFail("Failed to get presentURL from controller")
  723. return
  724. }
  725. XCTAssertEqual(self, uiDelegate)
  726. }
  727. func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
  728. XCTFail("Implement me")
  729. }
  730. }
  731. private static let kFakeRedirectURLStringInvalidClientID =
  732. "//firebaseauth/" +
  733. "link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fcal" +
  734. "lback%3FfirebaseError%3D%257B%2522code%2522%253A%2522auth%252Finvalid-oauth-client-id%2522%252" +
  735. "C%2522message%2522%253A%2522The%2520OAuth%2520client%2520ID%2520provided%2520is%2520either%252" +
  736. "0invalid%2520or%2520does%2520not%2520match%2520the%2520specified%2520API%2520key.%2522%257D%26" +
  737. "authType%3DverifyApp"
  738. private static let kFakeRedirectURLStringWebNetworkRequestFailed =
  739. "//firebaseauth/link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fc" +
  740. "allback%3FfirebaseError%3D%257B%2522code%2522%253A%2522auth%252Fnetwork-request-failed%2522%" +
  741. "252C%2522message%2522%253A%2522The%2520network%2520request%2520failed%2520.%2522%257D%" +
  742. "26authType%3DverifyApp"
  743. private static let kFakeRedirectURLStringWebInternalError =
  744. "//firebaseauth/link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fcal" +
  745. "lback%3FfirebaseError%3D%257B%2522code%2522%253A%2522auth%252Finternal-error%2522%252C%" +
  746. "2522message%2522%253A%2522Internal%2520error%2520.%2522%257D%26authType%3DverifyApp"
  747. private static let kFakeRedirectURLStringUnknownError =
  748. "//firebaseauth/link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fcal" +
  749. "lback%3FfirebaseError%3D%257B%2522code%2522%253A%2522auth%252Funknown-error-id%2522%252" +
  750. "C%2522message%2522%253A%2522The%2520OAuth%2520client%2520ID%2520provided%2520is%2520either%252" +
  751. "0invalid%2520or%2520does%2520not%2520match%2520the%2520specified%2520API%2520key.%2522%257D%26" +
  752. "authType%3DverifyApp"
  753. private static let kFakeRedirectURLStringUnstructuredError =
  754. "//firebaseauth/link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fcal" +
  755. "lback%3FfirebaseError%3D%257B%2522unstructuredcode%2522%253A%2522auth%252Funknown-error-id%" +
  756. "2522%252" +
  757. "C%2522unstructuredmessage%2522%253A%2522The%2520OAuth%2520client%2520ID%2520provided%2520is%" +
  758. "2520either%252" +
  759. "0invalid%2520or%2520does%2520not%2520match%2520the%2520specified%2520API%2520key.%2522%257D%" +
  760. "26authType%3DverifyApp"
  761. private static let kFakeRedirectURLStringWithReCAPTCHAToken =
  762. "://firebaseauth/" +
  763. "link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fcallback%3FauthType%" +
  764. "3DverifyApp%26recaptchaToken%3DfakeReCAPTCHAToken"
  765. }
  766. #endif