PhoneAuthProviderTests.swift 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865
  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. 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. switch request.codeIdentity {
  346. case let .credential(credential):
  347. XCTAssertEqual(credential.receipt, self.kTestReceipt)
  348. XCTAssertEqual(credential.secret, self.kTestSecret)
  349. default:
  350. XCTFail("Should be credential")
  351. }
  352. verifyRequesterExpectation.fulfill()
  353. do {
  354. if visited == false || goodRetry == false {
  355. // First Response for the underlying SendVerificationCode RPC call.
  356. try self.rpcIssuer?.respond(serverErrorMessage: "INVALID_APP_CREDENTIAL")
  357. visited = true
  358. } else {
  359. // Second Response for the underlying SendVerificationCode RPC call.
  360. try self.rpcIssuer?
  361. .respond(withJSON: [self.kVerificationIDKey: self.kTestVerificationID])
  362. }
  363. } catch {
  364. XCTFail("Failure sending response: \(error)")
  365. }
  366. }
  367. // Use fake authURLPresenter so we can test the parameters that get sent to it.
  368. PhoneAuthProviderTests.auth?.authURLPresenter =
  369. FakePresenter(
  370. urlString: PhoneAuthProviderTests.kFakeRedirectURLStringWithReCAPTCHAToken,
  371. clientID: PhoneAuthProviderTests.kFakeClientID,
  372. firebaseAppID: nil,
  373. errorTest: false,
  374. presenterError: nil
  375. )
  376. // 2. After setting up the fakes and parameters, call `verifyPhoneNumber`.
  377. provider
  378. .verifyPhoneNumber(kTestPhoneNumber, uiDelegate: nil) { verificationID, error in
  379. // 8. After the response triggers the callback in the FakePresenter, verify the callback.
  380. XCTAssertTrue(Thread.isMainThread)
  381. if goodRetry {
  382. XCTAssertNil(error)
  383. XCTAssertEqual(verificationID, self.kTestVerificationID)
  384. } else {
  385. XCTAssertNil(verificationID)
  386. XCTAssertEqual((error as? NSError)?.code, AuthErrorCode.internalError.rawValue)
  387. }
  388. expectation.fulfill()
  389. }
  390. waitForExpectations(timeout: 5)
  391. }
  392. private func internalFlow(function: String,
  393. useClientID: Bool = false,
  394. reCAPTCHAfallback: Bool = false) throws {
  395. let function = function
  396. initApp(function, useClientID: useClientID, fakeToken: true)
  397. let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
  398. let provider = PhoneAuthProvider.provider(auth: auth)
  399. let expectation = self.expectation(description: function)
  400. // Fake push notification.
  401. auth.appCredentialManager?.fakeCredential = AuthAppCredential(
  402. receipt: kTestReceipt,
  403. secret: reCAPTCHAfallback ? nil : kTestSecret
  404. )
  405. // 1. Intercept, handle, and test three RPC calls.
  406. let verifyClientRequestExpectation = self.expectation(description: "verifyClientRequest")
  407. rpcIssuer?.verifyClientRequester = { request in
  408. XCTAssertEqual(request.appToken, "21402324255E")
  409. XCTAssertFalse(request.isSandbox)
  410. verifyClientRequestExpectation.fulfill()
  411. do {
  412. // Response for the underlying VerifyClientRequest RPC call.
  413. try self.rpcIssuer?.respond(withJSON: [
  414. "receipt": self.kTestReceipt,
  415. "suggestedTimeout": self.kTestTimeout,
  416. ])
  417. } catch {
  418. XCTFail("Failure sending response: \(error)")
  419. }
  420. }
  421. if reCAPTCHAfallback {
  422. let projectConfigExpectation = self.expectation(description: "projectConfiguration")
  423. rpcIssuer?.projectConfigRequester = { request in
  424. XCTAssertEqual(request.apiKey, PhoneAuthProviderTests.kFakeAPIKey)
  425. projectConfigExpectation.fulfill()
  426. kAuthGlobalWorkQueue.async {
  427. do {
  428. // Response for the underlying VerifyClientRequest RPC call.
  429. try self.rpcIssuer?.respond(
  430. withJSON: ["projectId": "kFakeProjectID",
  431. "authorizedDomains": [PhoneAuthProviderTests.kFakeAuthorizedDomain]]
  432. )
  433. } catch {
  434. XCTFail("Failure sending response: \(error)")
  435. }
  436. }
  437. }
  438. }
  439. let verifyRequesterExpectation = self.expectation(description: "verifyRequester")
  440. rpcIssuer?.verifyRequester = { request in
  441. XCTAssertEqual(request.phoneNumber, self.kTestPhoneNumber)
  442. if reCAPTCHAfallback {
  443. switch request.codeIdentity {
  444. case let .recaptcha(token):
  445. XCTAssertEqual(token, self.kFakeReCAPTCHAToken)
  446. default:
  447. XCTFail("Should be recaptcha")
  448. }
  449. } else {
  450. switch request.codeIdentity {
  451. case let .credential(credential):
  452. XCTAssertEqual(credential.receipt, self.kTestReceipt)
  453. XCTAssertEqual(credential.secret, self.kTestSecret)
  454. default:
  455. XCTFail("Should be credential")
  456. }
  457. }
  458. verifyRequesterExpectation.fulfill()
  459. do {
  460. // Response for the underlying SendVerificationCode RPC call.
  461. try self.rpcIssuer?
  462. .respond(withJSON: [self.kVerificationIDKey: self.kTestVerificationID])
  463. } catch {
  464. XCTFail("Failure sending response: \(error)")
  465. }
  466. }
  467. // Use fake authURLPresenter so we can test the parameters that get sent to it.
  468. PhoneAuthProviderTests.auth?.authURLPresenter =
  469. FakePresenter(
  470. urlString: PhoneAuthProviderTests.kFakeRedirectURLStringWithReCAPTCHAToken,
  471. clientID: useClientID ? PhoneAuthProviderTests.kFakeClientID : nil,
  472. firebaseAppID: useClientID ? nil : PhoneAuthProviderTests.kFakeFirebaseAppID,
  473. errorTest: false,
  474. presenterError: nil
  475. )
  476. let uiDelegate = reCAPTCHAfallback ? FakeUIDelegate() : nil
  477. // 2. After setting up the fakes and parameters, call `verifyPhoneNumber`.
  478. provider
  479. .verifyPhoneNumber(kTestPhoneNumber, uiDelegate: uiDelegate) { verificationID, error in
  480. // 8. After the response triggers the callback in the FakePresenter, verify the callback.
  481. XCTAssertTrue(Thread.isMainThread)
  482. XCTAssertNil(error)
  483. XCTAssertEqual(verificationID, self.kTestVerificationID)
  484. expectation.fulfill()
  485. }
  486. waitForExpectations(timeout: 5)
  487. }
  488. /** @fn testVerifyClient
  489. @brief Tests verifying client before sending verification code.
  490. */
  491. private func internalTestVerify(errorString: String? = nil,
  492. errorURLString: String? = nil,
  493. errorCode: Int = 0,
  494. function: String,
  495. testMode: Bool = false,
  496. useClientID: Bool = false,
  497. bothClientAndAppID: Bool = false,
  498. reCAPTCHAfallback: Bool = false,
  499. forwardingNotification: Bool = true,
  500. presenterError: Error? = nil) throws {
  501. initApp(function, useClientID: useClientID, bothClientAndAppID: bothClientAndAppID,
  502. testMode: testMode,
  503. forwardingNotification: forwardingNotification)
  504. let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
  505. let provider = PhoneAuthProvider.provider(auth: auth)
  506. let expectation = self.expectation(description: function)
  507. if !reCAPTCHAfallback {
  508. // Fake out appCredentialManager flow.
  509. auth.appCredentialManager?.credential = AuthAppCredential(receipt: kTestReceipt,
  510. secret: kTestSecret)
  511. } else {
  512. // 1. Intercept, handle, and test the projectConfiguration RPC calls.
  513. let projectConfigExpectation = self.expectation(description: "projectConfiguration")
  514. rpcIssuer?.projectConfigRequester = { request in
  515. XCTAssertEqual(request.apiKey, PhoneAuthProviderTests.kFakeAPIKey)
  516. projectConfigExpectation.fulfill()
  517. do {
  518. // Response for the underlying VerifyClientRequest RPC call.
  519. try self.rpcIssuer?.respond(
  520. withJSON: ["projectId": "kFakeProjectID",
  521. "authorizedDomains": [PhoneAuthProviderTests.kFakeAuthorizedDomain]]
  522. )
  523. } catch {
  524. XCTFail("Failure sending response: \(error)")
  525. }
  526. }
  527. }
  528. if errorURLString == nil, presenterError == nil {
  529. let requestExpectation = self.expectation(description: "verifyRequester")
  530. rpcIssuer?.verifyRequester = { request in
  531. XCTAssertEqual(request.phoneNumber, self.kTestPhoneNumber)
  532. switch request.codeIdentity {
  533. case let .credential(credential):
  534. XCTAssertFalse(reCAPTCHAfallback)
  535. XCTAssertEqual(credential.receipt, self.kTestReceipt)
  536. XCTAssertEqual(credential.secret, self.kTestSecret)
  537. case let .recaptcha(token):
  538. XCTAssertTrue(reCAPTCHAfallback)
  539. XCTAssertEqual(token, self.kFakeReCAPTCHAToken)
  540. case .empty:
  541. XCTAssertTrue(testMode)
  542. }
  543. requestExpectation.fulfill()
  544. do {
  545. // Response for the underlying SendVerificationCode RPC call.
  546. if let errorString {
  547. try self.rpcIssuer?.respond(serverErrorMessage: errorString)
  548. } else {
  549. try self.rpcIssuer?
  550. .respond(withJSON: [self.kVerificationIDKey: self.kTestVerificationID])
  551. }
  552. } catch {
  553. XCTFail("Failure sending response: \(error)")
  554. }
  555. }
  556. }
  557. if reCAPTCHAfallback {
  558. // Use fake authURLPresenter so we can test the parameters that get sent to it.
  559. let urlString = errorURLString ??
  560. PhoneAuthProviderTests.kFakeRedirectURLStringWithReCAPTCHAToken
  561. let errorTest = errorURLString != nil
  562. PhoneAuthProviderTests.auth?.authURLPresenter =
  563. FakePresenter(
  564. urlString: urlString,
  565. clientID: useClientID ? PhoneAuthProviderTests.kFakeClientID : nil,
  566. firebaseAppID: useClientID ? nil : PhoneAuthProviderTests.kFakeFirebaseAppID,
  567. errorTest: errorTest,
  568. presenterError: presenterError
  569. )
  570. }
  571. let uiDelegate = reCAPTCHAfallback ? FakeUIDelegate() : nil
  572. // 2. After setting up the parameters, call `verifyPhoneNumber`.
  573. provider
  574. .verifyPhoneNumber(kTestPhoneNumber, uiDelegate: uiDelegate) { verificationID, error in
  575. // 8. After the response triggers the callback in the FakePresenter, verify the callback.
  576. XCTAssertTrue(Thread.isMainThread)
  577. if errorCode != 0 {
  578. XCTAssertNil(verificationID)
  579. XCTAssertEqual((error as? NSError)?.code, errorCode)
  580. } else {
  581. XCTAssertNil(error)
  582. XCTAssertEqual(verificationID, self.kTestVerificationID)
  583. }
  584. expectation.fulfill()
  585. }
  586. waitForExpectations(timeout: 5)
  587. }
  588. private func initApp(_ functionName: String,
  589. useClientID: Bool = false,
  590. bothClientAndAppID: Bool = false,
  591. testMode: Bool = false,
  592. forwardingNotification: Bool = true,
  593. fakeToken: Bool = false) {
  594. let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000",
  595. gcmSenderID: "00000000000000000-00000000000-000000000")
  596. options.apiKey = PhoneAuthProviderTests.kFakeAPIKey
  597. options.projectID = "myProjectID"
  598. if useClientID {
  599. options.clientID = PhoneAuthProviderTests.kFakeClientID
  600. }
  601. if !useClientID || bothClientAndAppID {
  602. // Use the appID.
  603. options.googleAppID = PhoneAuthProviderTests.kFakeFirebaseAppID
  604. }
  605. let scheme = useClientID ? PhoneAuthProviderTests.kFakeReverseClientID :
  606. PhoneAuthProviderTests.kFakeEncodedFirebaseAppID
  607. let strippedName = functionName.replacingOccurrences(of: "(", with: "")
  608. .replacingOccurrences(of: ")", with: "")
  609. FirebaseApp.configure(name: strippedName, options: options)
  610. let auth = Auth.auth(app: FirebaseApp.app(name: strippedName)!)
  611. kAuthGlobalWorkQueue.sync {
  612. // Wait for Auth protectedDataInitialization to finish.
  613. PhoneAuthProviderTests.auth = auth
  614. if testMode {
  615. // Disable app verification.
  616. let settings = AuthSettings()
  617. settings.appVerificationDisabledForTesting = true
  618. auth.settings = settings
  619. }
  620. auth.notificationManager?.immediateCallbackForTestFaking = { forwardingNotification }
  621. auth.mainBundleUrlTypes = [["CFBundleURLSchemes": [scheme]]]
  622. if fakeToken {
  623. guard let data = "!@#$%^".data(using: .utf8) else {
  624. XCTFail("Failed to encode data for fake token")
  625. return
  626. }
  627. auth.tokenManager?.tokenStore = AuthAPNSToken(withData: data, type: .prod)
  628. } else {
  629. // Skip APNS token fetching.
  630. auth.tokenManager = FakeTokenManager(withApplication: UIApplication.shared)
  631. }
  632. }
  633. }
  634. class FakeTokenManager: AuthAPNSTokenManager {
  635. override func getTokenInternal(callback: @escaping (AuthAPNSToken?, Error?) -> Void) {
  636. let error = NSError(domain: "dummy domain", code: AuthErrorCode.missingAppToken.rawValue)
  637. callback(nil, error)
  638. }
  639. }
  640. class FakePresenter: NSObject, AuthWebViewControllerDelegate {
  641. func webViewController(_ webViewController: AuthWebViewController,
  642. canHandle URL: URL) -> Bool {
  643. XCTFail("Do not call")
  644. return false
  645. }
  646. func webViewControllerDidCancel(_ webViewController: AuthWebViewController) {
  647. XCTFail("Do not call")
  648. }
  649. func webViewController(_ webViewController: AuthWebViewController,
  650. didFailWithError error: Error) {
  651. XCTFail("Do not call")
  652. }
  653. func present(_ presentURL: URL,
  654. uiDelegate UIDelegate: AuthUIDelegate?,
  655. callbackMatcher: @escaping (URL?) -> Bool,
  656. completion: @escaping (URL?, Error?) -> Void) {
  657. // 5. Verify flow triggers present in the FakePresenter class with the right parameters.
  658. XCTAssertEqual(presentURL.scheme, "https")
  659. XCTAssertEqual(presentURL.host, kFakeAuthorizedDomain)
  660. XCTAssertEqual(presentURL.path, "/__/auth/handler")
  661. let actualURLComponents = URLComponents(url: presentURL, resolvingAgainstBaseURL: false)
  662. guard let _ = actualURLComponents?.queryItems else {
  663. XCTFail("Failed to get queryItems")
  664. return
  665. }
  666. let params = AuthWebUtils.dictionary(withHttpArgumentsString: presentURL.query)
  667. XCTAssertEqual(params["ibi"], Bundle.main.bundleIdentifier)
  668. XCTAssertEqual(params["apiKey"], PhoneAuthProviderTests.kFakeAPIKey)
  669. XCTAssertEqual(params["authType"], "verifyApp")
  670. XCTAssertNotNil(params["v"])
  671. if OAuthProviderTests.testTenantID {
  672. XCTAssertEqual(params["tid"], OAuthProviderTests.kFakeTenantID)
  673. } else {
  674. XCTAssertNil(params["tid"])
  675. }
  676. let appCheckToken = presentURL.fragment
  677. let verifyAppCheckToken = OAuthProviderTests.testAppCheck ? "fac=fakeAppCheckToken" : nil
  678. XCTAssertEqual(appCheckToken, verifyAppCheckToken)
  679. var redirectURL = ""
  680. if let clientID {
  681. XCTAssertEqual(params["clientId"], clientID)
  682. redirectURL = "\(kFakeReverseClientID)\(urlString)"
  683. }
  684. if let firebaseAppID {
  685. XCTAssertEqual(params["appId"], firebaseAppID)
  686. redirectURL = "\(kFakeEncodedFirebaseAppID)\(urlString)"
  687. }
  688. // 6. Test callbackMatcher
  689. // Verify that the URL is rejected by the callback matcher without the event ID.
  690. XCTAssertFalse(callbackMatcher(URL(string: "\(redirectURL)")))
  691. // Verify that the URL is accepted by the callback matcher with the matching event ID.
  692. guard let eventID = params["eventId"] else {
  693. XCTFail("Failed to get eventID")
  694. return
  695. }
  696. let redirectWithEventID = "\(redirectURL)%26eventId%3D\(eventID)"
  697. let originalComponents = URLComponents(string: redirectWithEventID)!
  698. XCTAssertEqual(callbackMatcher(originalComponents.url), !errorTest)
  699. var components = originalComponents
  700. components.query = "https"
  701. XCTAssertFalse(callbackMatcher(components.url))
  702. components = originalComponents
  703. components.host = "badhost"
  704. XCTAssertFalse(callbackMatcher(components.url))
  705. components = originalComponents
  706. components.path = "badpath"
  707. XCTAssertFalse(callbackMatcher(components.url))
  708. components = originalComponents
  709. components.query = "badquery"
  710. XCTAssertFalse(callbackMatcher(components.url))
  711. // 7. Do the callback to the original call.
  712. kAuthGlobalWorkQueue.async {
  713. if let presenterError = self.presenterError {
  714. completion(nil, presenterError)
  715. } else {
  716. completion(URL(string: "\(kFakeEncodedFirebaseAppID)\(self.urlString)") ?? nil, nil)
  717. }
  718. }
  719. }
  720. let urlString: String
  721. let clientID: String?
  722. let firebaseAppID: String?
  723. let errorTest: Bool
  724. let presenterError: Error?
  725. init(urlString: String, clientID: String?, firebaseAppID: String?, errorTest: Bool,
  726. presenterError: Error?) {
  727. self.urlString = urlString
  728. self.clientID = clientID
  729. self.firebaseAppID = firebaseAppID
  730. self.errorTest = errorTest
  731. self.presenterError = presenterError
  732. }
  733. }
  734. private class FakeUIDelegate: NSObject, AuthUIDelegate {
  735. func present(_ viewControllerToPresent: UIViewController, animated flag: Bool,
  736. completion: (() -> Void)? = nil) {
  737. guard let safariController = viewControllerToPresent as? SFSafariViewController,
  738. let delegate = safariController.delegate as? AuthURLPresenter,
  739. let uiDelegate = delegate.uiDelegate as? FakeUIDelegate else {
  740. XCTFail("Failed to get presentURL from controller")
  741. return
  742. }
  743. XCTAssertEqual(self, uiDelegate)
  744. }
  745. func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
  746. XCTFail("Implement me")
  747. }
  748. }
  749. private static let kFakeRedirectURLStringInvalidClientID =
  750. "//firebaseauth/" +
  751. "link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fcal" +
  752. "lback%3FfirebaseError%3D%257B%2522code%2522%253A%2522auth%252Finvalid-oauth-client-id%2522%252" +
  753. "C%2522message%2522%253A%2522The%2520OAuth%2520client%2520ID%2520provided%2520is%2520either%252" +
  754. "0invalid%2520or%2520does%2520not%2520match%2520the%2520specified%2520API%2520key.%2522%257D%26" +
  755. "authType%3DverifyApp"
  756. private static let kFakeRedirectURLStringWebNetworkRequestFailed =
  757. "//firebaseauth/link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fc" +
  758. "allback%3FfirebaseError%3D%257B%2522code%2522%253A%2522auth%252Fnetwork-request-failed%2522%" +
  759. "252C%2522message%2522%253A%2522The%2520network%2520request%2520failed%2520.%2522%257D%" +
  760. "26authType%3DverifyApp"
  761. private static let kFakeRedirectURLStringWebInternalError =
  762. "//firebaseauth/link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fcal" +
  763. "lback%3FfirebaseError%3D%257B%2522code%2522%253A%2522auth%252Finternal-error%2522%252C%" +
  764. "2522message%2522%253A%2522Internal%2520error%2520.%2522%257D%26authType%3DverifyApp"
  765. private static let kFakeRedirectURLStringUnknownError =
  766. "//firebaseauth/link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fcal" +
  767. "lback%3FfirebaseError%3D%257B%2522code%2522%253A%2522auth%252Funknown-error-id%2522%252" +
  768. "C%2522message%2522%253A%2522The%2520OAuth%2520client%2520ID%2520provided%2520is%2520either%252" +
  769. "0invalid%2520or%2520does%2520not%2520match%2520the%2520specified%2520API%2520key.%2522%257D%26" +
  770. "authType%3DverifyApp"
  771. private static let kFakeRedirectURLStringUnstructuredError =
  772. "//firebaseauth/link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fcal" +
  773. "lback%3FfirebaseError%3D%257B%2522unstructuredcode%2522%253A%2522auth%252Funknown-error-id%" +
  774. "2522%252" +
  775. "C%2522unstructuredmessage%2522%253A%2522The%2520OAuth%2520client%2520ID%2520provided%2520is%" +
  776. "2520either%252" +
  777. "0invalid%2520or%2520does%2520not%2520match%2520the%2520specified%2520API%2520key.%2522%257D%" +
  778. "26authType%3DverifyApp"
  779. private static let kFakeRedirectURLStringWithReCAPTCHAToken =
  780. "://firebaseauth/" +
  781. "link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fcallback%3FauthType%" +
  782. "3DverifyApp%26recaptchaToken%3DfakeReCAPTCHAToken"
  783. }
  784. #endif