AuthBackendTests.swift 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789
  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. import Foundation
  15. import XCTest
  16. @testable import FirebaseAuth
  17. import FirebaseCoreExtension
  18. #if COCOAPODS || SWIFT_PACKAGE
  19. // Heartbeats are not supported in the internal build system.
  20. import FirebaseCoreInternal
  21. #endif
  22. private let kFakeAPIKey = "kTestAPIKey"
  23. private let kFakeAppID = "kTestFirebaseAppID"
  24. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  25. class AuthBackendTests: RPCBaseTests {
  26. let kFakeErrorDomain = "fakeDomain"
  27. let kFakeErrorCode = -1
  28. /** @fn testRequestEncodingError
  29. @brief This test checks the behaviour of @c postWithRequest:response:callback: when the
  30. request passed returns an error during it's unencodedHTTPRequestBodyWithError: method.
  31. The error returned should be delivered to the caller without any change.
  32. */
  33. func testRequestEncodingError() async throws {
  34. let encodingError = NSError(domain: kFakeErrorDomain, code: kFakeErrorCode)
  35. let request = FakeRequest(withEncodingError: encodingError)
  36. do {
  37. let _ = try await authBackend.call(with: request)
  38. XCTFail("Expected to throw")
  39. } catch {
  40. let rpcError = error as NSError
  41. XCTAssertEqual(rpcError.domain, AuthErrors.domain)
  42. XCTAssertEqual(rpcError.code, AuthErrorCode.internalError.rawValue)
  43. let underlyingError = try XCTUnwrap(rpcError.userInfo[NSUnderlyingErrorKey] as? NSError)
  44. XCTAssertEqual(underlyingError.domain, AuthErrorUtils.internalErrorDomain)
  45. XCTAssertEqual(underlyingError.code, AuthInternalErrorCode.JSONSerializationError.rawValue)
  46. XCTAssertNil(underlyingError.userInfo[AuthErrorUtils.userInfoDeserializedResponseKey])
  47. XCTAssertNil(underlyingError.userInfo[AuthErrorUtils.userInfoDataKey])
  48. }
  49. }
  50. /** @fn testBodyDataSerializationError
  51. @brief This test checks the behaviour of @c postWithRequest:response:callback: when the
  52. request returns an object which isn't serializable by @c NSJSONSerialization.
  53. The error from @c NSJSONSerialization should be returned as the underlyingError for an
  54. @c NSError with the code @c FIRAuthErrorCodeJSONSerializationError.
  55. */
  56. func testBodyDataSerializationError() async throws {
  57. let request = FakeRequest(withRequestBody: ["unencodable": self])
  58. do {
  59. let _ = try await authBackend.call(with: request)
  60. XCTFail("Expected to throw")
  61. } catch {
  62. let rpcError = error as NSError
  63. XCTAssertEqual(rpcError.domain, AuthErrors.domain)
  64. XCTAssertEqual(rpcError.code, AuthErrorCode.internalError.rawValue)
  65. let underlyingError = try XCTUnwrap(rpcError.userInfo[NSUnderlyingErrorKey] as? NSError)
  66. XCTAssertEqual(underlyingError.code, AuthInternalErrorCode.JSONSerializationError.rawValue)
  67. XCTAssertEqual(underlyingError.domain, AuthErrorUtils.internalErrorDomain)
  68. XCTAssertNil(underlyingError.userInfo[NSUnderlyingErrorKey])
  69. XCTAssertNil(underlyingError.userInfo[AuthErrorUtils.userInfoDeserializedResponseKey])
  70. XCTAssertNil(underlyingError.userInfo[AuthErrorUtils.userInfoDataKey])
  71. }
  72. }
  73. /** @fn testNetworkError
  74. @brief This test checks to make sure a network error is properly wrapped and forwarded with the
  75. correct code (FIRAuthErrorCodeNetworkError).
  76. */
  77. func testNetworkError() async throws {
  78. let request = FakeRequest(withRequestBody: [:])
  79. rpcIssuer.respondBlock = {
  80. let responseError = NSError(domain: self.kFakeErrorDomain, code: self.kFakeErrorCode)
  81. return (nil, responseError)
  82. }
  83. do {
  84. let _ = try await authBackend.call(with: request)
  85. XCTFail("Expected to throw")
  86. } catch {
  87. let rpcError = error as NSError
  88. XCTAssertEqual(rpcError.domain, AuthErrors.domain)
  89. XCTAssertEqual(rpcError.code, AuthErrorCode.networkError.rawValue)
  90. let underlyingError = try XCTUnwrap(rpcError.userInfo[NSUnderlyingErrorKey] as? NSError)
  91. XCTAssertEqual(underlyingError.domain, kFakeErrorDomain)
  92. XCTAssertEqual(underlyingError.code, kFakeErrorCode)
  93. XCTAssertNil(underlyingError.userInfo[NSUnderlyingErrorKey])
  94. XCTAssertNil(underlyingError.userInfo[AuthErrorUtils.userInfoDeserializedResponseKey])
  95. XCTAssertNil(underlyingError.userInfo[AuthErrorUtils.userInfoDataKey])
  96. }
  97. }
  98. /** @fn testUnparsableErrorResponse
  99. @brief This test checks the behaviour of @c postWithRequest:response:callback: when the
  100. response isn't deserializable by @c NSJSONSerialization and an error
  101. condition (with an associated error response message) was expected. We are expecting to
  102. receive the original network error wrapped in an @c NSError with the code
  103. @c FIRAuthErrorCodeUnexpectedHTTPResponse.
  104. */
  105. func testUnparsableErrorResponse() async throws {
  106. let data = "<html><body>An error occurred.</body></html>".data(using: .utf8)
  107. let request = FakeRequest(withRequestBody: [:])
  108. rpcIssuer.respondBlock = {
  109. let responseError = NSError(domain: self.kFakeErrorDomain, code: self.kFakeErrorCode)
  110. return (data, error: responseError)
  111. }
  112. do {
  113. let _ = try await authBackend.call(with: request)
  114. XCTFail("Expected to throw")
  115. } catch {
  116. let rpcError = error as NSError
  117. XCTAssertEqual(rpcError.domain, AuthErrors.domain)
  118. XCTAssertEqual(rpcError.code, AuthErrorCode.internalError.rawValue)
  119. let underlyingError = try XCTUnwrap(rpcError.userInfo[NSUnderlyingErrorKey] as? NSError)
  120. XCTAssertEqual(underlyingError.domain, AuthErrorUtils.internalErrorDomain)
  121. XCTAssertEqual(underlyingError.code, AuthInternalErrorCode.unexpectedErrorResponse.rawValue)
  122. let underlyingUnderlying = try XCTUnwrap(underlyingError
  123. .userInfo[NSUnderlyingErrorKey] as? NSError)
  124. XCTAssertEqual(underlyingUnderlying.domain, kFakeErrorDomain)
  125. XCTAssertEqual(underlyingUnderlying.code, kFakeErrorCode)
  126. XCTAssertNil(underlyingError.userInfo[AuthErrorUtils.userInfoDeserializedResponseKey])
  127. XCTAssertEqual(data,
  128. try XCTUnwrap(underlyingError
  129. .userInfo[AuthErrorUtils.userInfoDataKey] as? Data))
  130. }
  131. }
  132. /** @fn testUnparsableSuccessResponse
  133. @brief This test checks the behaviour of @c postWithRequest:response:callback: when the
  134. response isn't deserializable by @c NSJSONSerialization and no error
  135. condition was indicated. We are expecting to
  136. receive the @c NSJSONSerialization error wrapped in an @c NSError with the code
  137. @c FIRAuthErrorCodeUnexpectedServerResponse.
  138. */
  139. func testUnparsableSuccessResponse() async throws {
  140. let data = "<xml>Some non-JSON value.</xml>".data(using: .utf8)
  141. let request = FakeRequest(withRequestBody: [:])
  142. rpcIssuer.respondBlock = {
  143. (data, nil)
  144. }
  145. do {
  146. let _ = try await authBackend.call(with: request)
  147. XCTFail("Expected to throw")
  148. } catch {
  149. let rpcError = error as NSError
  150. XCTAssertEqual(rpcError.domain, AuthErrors.domain)
  151. XCTAssertEqual(rpcError.code, AuthErrorCode.internalError.rawValue)
  152. let underlyingError = try XCTUnwrap(rpcError.userInfo[NSUnderlyingErrorKey] as? NSError)
  153. XCTAssertEqual(underlyingError.domain, AuthErrorUtils.internalErrorDomain)
  154. XCTAssertEqual(underlyingError.code, AuthInternalErrorCode.unexpectedResponse.rawValue)
  155. let underlyingUnderlying = try XCTUnwrap(underlyingError
  156. .userInfo[NSUnderlyingErrorKey] as? NSError)
  157. XCTAssertEqual(underlyingUnderlying.domain, NSCocoaErrorDomain)
  158. XCTAssertNil(underlyingError.userInfo[AuthErrorUtils.userInfoDeserializedResponseKey])
  159. XCTAssertEqual(data,
  160. try XCTUnwrap(underlyingError
  161. .userInfo[AuthErrorUtils.userInfoDataKey] as? Data))
  162. }
  163. }
  164. /** @fn testNonDictionaryErrorResponse
  165. @brief This test checks the behaviour of @c postWithRequest:response:callback: when the
  166. response deserialized by @c NSJSONSerialization is not a dictionary, and an error was
  167. expected. We are expecting to receive the original network error wrapped in an @c NSError
  168. with the code @c FIRAuthInternalErrorCodeUnexpectedErrorResponse with the decoded response
  169. in the @c NSError.userInfo dictionary associated with the key
  170. @c FIRAuthErrorUserInfoDeserializedResponseKey.
  171. */
  172. func testNonDictionaryErrorResponse() async throws {
  173. // We are responding with a JSON-encoded string value representing an array - which is
  174. // unexpected. It should normally be a dictionary, and we need to check for this sort
  175. // of thing. Because we can successfully decode this value, however, we do return it
  176. // in the error results. We check for this array later in the test.
  177. let data = "[]".data(using: .utf8)
  178. let responseError = NSError(domain: kFakeErrorDomain, code: kFakeErrorCode)
  179. let request = FakeRequest(withRequestBody: [:])
  180. rpcIssuer.respondBlock = {
  181. (data, responseError)
  182. }
  183. do {
  184. let _ = try await authBackend.call(with: request)
  185. XCTFail("Expected to throw")
  186. } catch {
  187. let rpcError = error as NSError
  188. XCTAssertEqual(rpcError.domain, AuthErrors.domain)
  189. XCTAssertEqual(rpcError.code, AuthErrorCode.internalError.rawValue)
  190. let underlyingError = try XCTUnwrap(rpcError.userInfo[NSUnderlyingErrorKey] as? NSError)
  191. XCTAssertEqual(underlyingError.domain, AuthErrorUtils.internalErrorDomain)
  192. XCTAssertEqual(underlyingError.code, AuthInternalErrorCode.unexpectedErrorResponse.rawValue)
  193. let underlyingUnderlying = try XCTUnwrap(underlyingError
  194. .userInfo[NSUnderlyingErrorKey] as? NSError)
  195. XCTAssertEqual(underlyingUnderlying.domain, kFakeErrorDomain)
  196. XCTAssertEqual(underlyingUnderlying.code, kFakeErrorCode)
  197. XCTAssertNotNil(try XCTUnwrap(
  198. underlyingError.userInfo[AuthErrorUtils.userInfoDeserializedResponseKey]
  199. ) as? [Int])
  200. XCTAssertNil(underlyingError.userInfo[AuthErrorUtils.userInfoDataKey])
  201. }
  202. }
  203. /** @fn testNonDictionarySuccessResponse
  204. @brief This test checks the behaviour of @c postWithRequest:response:callback: when the
  205. response deserialized by @c NSJSONSerialization is not a dictionary, and no error was
  206. expected. We are expecting to receive an @c NSError with the code
  207. @c FIRAuthErrorCodeUnexpectedServerResponse with the decoded response in the
  208. @c NSError.userInfo dictionary associated with the key
  209. `userInfoDeserializedResponseKey`.
  210. */
  211. func testNonDictionarySuccessResponse() async throws {
  212. // We are responding with a JSON-encoded string value representing an array - which is
  213. // unexpected. It should normally be a dictionary, and we need to check for this sort
  214. // of thing. Because we can successfully decode this value, however, we do return it
  215. // in the error results. We check for this array later in the test.
  216. let data = "[]".data(using: .utf8)
  217. let request = FakeRequest(withRequestBody: [:])
  218. rpcIssuer.respondBlock = {
  219. (data, nil)
  220. }
  221. do {
  222. let _ = try await authBackend.call(with: request)
  223. XCTFail("Expected to throw")
  224. } catch {
  225. let rpcError = error as NSError
  226. XCTAssertEqual(rpcError.domain, AuthErrors.domain)
  227. XCTAssertEqual(rpcError.code, AuthErrorCode.internalError.rawValue)
  228. let underlyingError = try XCTUnwrap(rpcError.userInfo[NSUnderlyingErrorKey] as? NSError)
  229. XCTAssertEqual(underlyingError.domain, AuthErrorUtils.internalErrorDomain)
  230. XCTAssertEqual(underlyingError.code, AuthInternalErrorCode.unexpectedResponse.rawValue)
  231. XCTAssertNil(underlyingError.userInfo[NSUnderlyingErrorKey])
  232. XCTAssertNotNil(try XCTUnwrap(
  233. underlyingError.userInfo[AuthErrorUtils.userInfoDeserializedResponseKey]
  234. ) as? [Int])
  235. XCTAssertNil(underlyingError.userInfo[AuthErrorUtils.userInfoDataKey])
  236. }
  237. }
  238. /** @fn testCaptchaRequiredResponse
  239. @brief This test checks the behaviour of @c postWithRequest:response:callback: when the
  240. we get an error message indicating captcha is required. The backend should not be returning
  241. this error to mobile clients. If it does, we should wrap it in an @c NSError with the code
  242. @c FIRAuthInternalErrorCodeUnexpectedErrorResponse with the decoded error message in the
  243. @c NSError.userInfo dictionary associated with the key
  244. @c FIRAuthErrorUserInfoDeserializedResponseKey.
  245. */
  246. func testCaptchaRequiredResponse() async throws {
  247. let kErrorMessageCaptchaRequired = "CAPTCHA_REQUIRED"
  248. let request = FakeRequest(withRequestBody: [:])
  249. rpcIssuer.respondBlock = {
  250. let responseError = NSError(domain: self.kFakeErrorDomain, code: self.kFakeErrorCode)
  251. return try self.rpcIssuer.respond(serverErrorMessage: kErrorMessageCaptchaRequired,
  252. error: responseError)
  253. }
  254. do {
  255. let _ = try await authBackend.call(with: request)
  256. XCTFail("Expected to throw")
  257. } catch {
  258. let rpcError = error as NSError
  259. XCTAssertEqual(rpcError.domain, AuthErrors.domain)
  260. XCTAssertEqual(rpcError.code, AuthErrorCode.internalError.rawValue)
  261. let underlyingError = try XCTUnwrap(rpcError.userInfo[NSUnderlyingErrorKey] as? NSError)
  262. XCTAssertEqual(underlyingError.domain, AuthErrorUtils.internalErrorDomain)
  263. XCTAssertEqual(underlyingError.code, AuthInternalErrorCode.unexpectedErrorResponse.rawValue)
  264. let underlyingUnderlying = try XCTUnwrap(underlyingError
  265. .userInfo[NSUnderlyingErrorKey] as? NSError)
  266. XCTAssertEqual(underlyingUnderlying.domain, kFakeErrorDomain)
  267. XCTAssertEqual(underlyingUnderlying.code, kFakeErrorCode)
  268. let dictionary = try XCTUnwrap(underlyingError
  269. .userInfo[AuthErrorUtils.userInfoDeserializedResponseKey] as? [String: AnyHashable])
  270. XCTAssertEqual(dictionary["message"], kErrorMessageCaptchaRequired)
  271. XCTAssertNil(underlyingError.userInfo[AuthErrorUtils.userInfoDataKey])
  272. }
  273. }
  274. /** @fn testCaptchaCheckFailedResponse
  275. @brief This test checks the behaviour of @c postWithRequest:response:callback: when the
  276. we get an error message indicating captcha check failed. The backend should not be returning
  277. this error to mobile clients. If it does, we should wrap it in an @c NSError with the code
  278. @c FIRAuthErrorCodeUnexpectedServerResponse with the decoded error message in the
  279. @c NSError.userInfo dictionary associated with the key
  280. @c FIRAuthErrorUserInfoDecodedErrorResponseKey.
  281. */
  282. func testCaptchaCheckFailedResponse() async throws {
  283. let kErrorMessageCaptchaCheckFailed = "CAPTCHA_CHECK_FAILED"
  284. let request = FakeRequest(withRequestBody: [:])
  285. rpcIssuer.respondBlock = {
  286. let responseError = NSError(domain: self.kFakeErrorDomain, code: self.kFakeErrorCode)
  287. return try self.rpcIssuer.respond(
  288. serverErrorMessage: kErrorMessageCaptchaCheckFailed,
  289. error: responseError
  290. )
  291. }
  292. do {
  293. let _ = try await authBackend.call(with: request)
  294. XCTFail("Expected to throw")
  295. } catch {
  296. let rpcError = error as NSError
  297. XCTAssertEqual(rpcError.domain, AuthErrors.domain)
  298. XCTAssertEqual(rpcError.code, AuthErrorCode.captchaCheckFailed.rawValue)
  299. }
  300. }
  301. /** @fn testCaptchaRequiredInvalidPasswordResponse
  302. @brief This test checks the behaviour of @c postWithRequest:response:callback: when the
  303. we get an error message indicating captcha is required and an invalid password was entered.
  304. The backend should not be returning this error to mobile clients. If it does, we should wrap
  305. it in an @c NSError with the code
  306. @c FIRAuthInternalErrorCodeUnexpectedErrorResponse with the decoded error message in the
  307. @c NSError.userInfo dictionary associated with the key
  308. @c FIRAuthErrorUserInfoDeserializedResponseKey.
  309. */
  310. func testCaptchaRequiredInvalidPasswordResponse() async throws {
  311. let kErrorMessageCaptchaRequiredInvalidPassword = "CAPTCHA_REQUIRED_INVALID_PASSWORD"
  312. let responseError = NSError(domain: kFakeErrorDomain, code: kFakeErrorCode)
  313. let request = FakeRequest(withRequestBody: [:])
  314. rpcIssuer.respondBlock = {
  315. try self.rpcIssuer.respond(serverErrorMessage: kErrorMessageCaptchaRequiredInvalidPassword,
  316. error: responseError)
  317. }
  318. do {
  319. let _ = try await authBackend.call(with: request)
  320. XCTFail("Expected to throw")
  321. } catch {
  322. let rpcError = error as NSError
  323. XCTAssertEqual(rpcError.domain, AuthErrors.domain)
  324. XCTAssertEqual(rpcError.code, AuthErrorCode.internalError.rawValue)
  325. let underlyingError = try XCTUnwrap(rpcError.userInfo[NSUnderlyingErrorKey] as? NSError)
  326. XCTAssertEqual(underlyingError.domain, AuthErrorUtils.internalErrorDomain)
  327. XCTAssertEqual(underlyingError.code, AuthInternalErrorCode.unexpectedErrorResponse.rawValue)
  328. let underlyingUnderlying = try XCTUnwrap(underlyingError
  329. .userInfo[NSUnderlyingErrorKey] as? NSError)
  330. XCTAssertEqual(underlyingUnderlying.domain, kFakeErrorDomain)
  331. XCTAssertEqual(underlyingUnderlying.code, kFakeErrorCode)
  332. let dictionary = try XCTUnwrap(underlyingError
  333. .userInfo[AuthErrorUtils.userInfoDeserializedResponseKey] as? [String: AnyHashable])
  334. XCTAssertEqual(dictionary["message"], kErrorMessageCaptchaRequiredInvalidPassword)
  335. XCTAssertNil(underlyingError.userInfo[AuthErrorUtils.userInfoDataKey])
  336. }
  337. }
  338. /// Test Blocking Function Error Response flow
  339. func testBlockingFunctionError() async throws {
  340. let kErrorMessageBlocking = "BLOCKING_FUNCTION_ERROR_RESPONSE"
  341. let responseError = NSError(domain: kFakeErrorDomain, code: kFakeErrorCode)
  342. let request = FakeRequest(withRequestBody: [:])
  343. rpcIssuer.respondBlock = {
  344. try self.rpcIssuer.respond(serverErrorMessage: kErrorMessageBlocking, error: responseError)
  345. }
  346. do {
  347. let _ = try await authBackend.call(with: request)
  348. XCTFail("Expected to throw")
  349. } catch {
  350. let rpcError = error as NSError
  351. XCTAssertEqual(rpcError.domain, AuthErrors.domain)
  352. XCTAssertEqual(rpcError.code, AuthErrorCode.blockingCloudFunctionError.rawValue)
  353. }
  354. }
  355. /// Test Blocking Function Error Response flow - including JSON parsing.
  356. /// Regression Test for #14052
  357. func testBlockingFunctionErrorWithJSON() async throws {
  358. let kErrorMessageBlocking = "BLOCKING_FUNCTION_ERROR_RESPONSE"
  359. let stringWithJSON = "BLOCKING_FUNCTION_ERROR_RESPONSE : ((HTTP request to" +
  360. "http://127.0.0.1:9999/project-id/us-central1/beforeUserCreated returned HTTP error 400:" +
  361. " {\"error\":{\"details\":{\"code\":\"invalid-email\"},\"message\":\"invalid " +
  362. "email\",\"status\":\"INVALID_ARGUMENT\"}}))"
  363. let responseError = NSError(domain: kFakeErrorDomain, code: kFakeErrorCode)
  364. let request = FakeRequest(withRequestBody: [:])
  365. rpcIssuer.respondBlock = {
  366. try self.rpcIssuer.respond(
  367. serverErrorMessage: kErrorMessageBlocking + " : " + stringWithJSON,
  368. error: responseError
  369. )
  370. }
  371. do {
  372. let _ = try await authBackend.call(with: request)
  373. XCTFail("Expected to throw")
  374. } catch {
  375. let rpcError = error as NSError
  376. XCTAssertEqual(rpcError.domain, AuthErrors.domain)
  377. XCTAssertEqual(rpcError.code, AuthErrorCode.blockingCloudFunctionError.rawValue)
  378. XCTAssertEqual(rpcError.localizedDescription, "invalid email")
  379. }
  380. }
  381. /** @fn testDecodableErrorResponseWithUnknownMessage
  382. @brief This test checks the behaviour of @c postWithRequest:response:callback: when the
  383. response deserialized by @c NSJSONSerialization represents a valid error response (and an
  384. error was indicated) but we didn't receive an error message we know about. We are expecting
  385. to receive the original network error wrapped in an @c NSError with the code
  386. @c FIRAuthInternalErrorCodeUnexpectedErrorResponse with the decoded
  387. error message in the @c NSError.userInfo dictionary associated with the key
  388. @c FIRAuthErrorUserInfoDeserializedResponseKey.
  389. */
  390. func testDecodableErrorResponseWithUnknownMessage() async throws {
  391. // We need to return a valid "error" response here, but we are going to intentionally use a
  392. // bogus error message.
  393. let kUnknownServerErrorMessage = "UNKNOWN_MESSAGE"
  394. let responseError = NSError(domain: kFakeErrorDomain, code: kFakeErrorCode)
  395. let request = FakeRequest(withRequestBody: [:])
  396. rpcIssuer.respondBlock = {
  397. try self.rpcIssuer.respond(serverErrorMessage: kUnknownServerErrorMessage,
  398. error: responseError)
  399. }
  400. do {
  401. let _ = try await authBackend.call(with: request)
  402. XCTFail("Expected to throw")
  403. } catch {
  404. let rpcError = error as NSError
  405. XCTAssertEqual(rpcError.domain, AuthErrors.domain)
  406. XCTAssertEqual(rpcError.code, AuthErrorCode.internalError.rawValue)
  407. let underlyingError = try XCTUnwrap(rpcError.userInfo[NSUnderlyingErrorKey] as? NSError)
  408. XCTAssertEqual(underlyingError.domain, AuthErrorUtils.internalErrorDomain)
  409. XCTAssertEqual(underlyingError.code, AuthInternalErrorCode.unexpectedErrorResponse.rawValue)
  410. let underlyingUnderlying = try XCTUnwrap(underlyingError
  411. .userInfo[NSUnderlyingErrorKey] as? NSError)
  412. XCTAssertEqual(underlyingUnderlying.domain, kFakeErrorDomain)
  413. XCTAssertEqual(underlyingUnderlying.code, kFakeErrorCode)
  414. let dictionary = try XCTUnwrap(underlyingError
  415. .userInfo[AuthErrorUtils.userInfoDeserializedResponseKey] as? [String: AnyHashable])
  416. XCTAssertEqual(dictionary["message"], kUnknownServerErrorMessage)
  417. XCTAssertNil(underlyingError.userInfo[AuthErrorUtils.userInfoDataKey])
  418. }
  419. }
  420. /** @fn testErrorResponseWithNoErrorMessage
  421. @brief This test checks the behaviour of @c postWithRequest:response:callback: when the
  422. response deserialized by @c NSJSONSerialization is a dictionary, and an error was indicated,
  423. but no error information was present in the decoded response. We are expecting to receive
  424. the original network error wrapped in an @c NSError with the code
  425. @c FIRAuthErrorCodeUnexpectedServerResponse with the decoded
  426. response message in the @c NSError.userInfo dictionary associated with the key
  427. @c FIRAuthErrorUserInfoDeserializedResponseKey.
  428. */
  429. func testErrorResponseWithNoErrorMessage() async throws {
  430. let request = FakeRequest(withRequestBody: [:])
  431. let responseError = NSError(domain: kFakeErrorDomain, code: kFakeErrorCode)
  432. rpcIssuer.respondBlock = {
  433. try self.rpcIssuer.respond(withJSON: [:], error: responseError)
  434. }
  435. do {
  436. let _ = try await authBackend.call(with: request)
  437. XCTFail("Expected to throw")
  438. } catch {
  439. let rpcError = error as NSError
  440. XCTAssertEqual(rpcError.domain, AuthErrors.domain)
  441. XCTAssertEqual(rpcError.code, AuthErrorCode.internalError.rawValue)
  442. let underlyingError = try XCTUnwrap(rpcError.userInfo[NSUnderlyingErrorKey] as? NSError)
  443. XCTAssertEqual(underlyingError.domain, AuthErrorUtils.internalErrorDomain)
  444. XCTAssertEqual(underlyingError.code, AuthInternalErrorCode.unexpectedErrorResponse.rawValue)
  445. let underlyingUnderlying = try XCTUnwrap(underlyingError
  446. .userInfo[NSUnderlyingErrorKey] as? NSError)
  447. XCTAssertEqual(underlyingUnderlying.domain, kFakeErrorDomain)
  448. XCTAssertEqual(underlyingUnderlying.code, kFakeErrorCode)
  449. let dictionary = try XCTUnwrap(underlyingError
  450. .userInfo[AuthErrorUtils.userInfoDeserializedResponseKey] as? [String: AnyHashable])
  451. XCTAssertEqual(dictionary, [:])
  452. XCTAssertNil(underlyingError.userInfo[AuthErrorUtils.userInfoDataKey])
  453. }
  454. }
  455. /** @fn testClientErrorResponse
  456. @brief This test checks the behaviour of @c postWithRequest:response:callback: when the
  457. response contains a client error specified by an error message sent from the backend.
  458. */
  459. func testClientErrorResponse() async throws {
  460. let responseError = NSError(domain: kFakeErrorDomain, code: kFakeErrorCode)
  461. let kUserDisabledErrorMessage = "USER_DISABLED"
  462. let kServerErrorDetailMarker = " : "
  463. let kFakeUserDisabledCustomErrorMessage = "The user has been disabled."
  464. let customErrorMessage = "\(kUserDisabledErrorMessage)" +
  465. "\(kServerErrorDetailMarker)\(kFakeUserDisabledCustomErrorMessage)"
  466. rpcIssuer.respondBlock = {
  467. try self.rpcIssuer.respond(serverErrorMessage: customErrorMessage, error: responseError)
  468. }
  469. do {
  470. let _ = try await authBackend.call(with: FakeRequest(withRequestBody: [:]))
  471. XCTFail("Expected to throw")
  472. } catch {
  473. let rpcError = error as NSError
  474. XCTAssertEqual(rpcError.domain, AuthErrors.domain)
  475. XCTAssertEqual(rpcError.code, AuthErrorCode.userDisabled.rawValue)
  476. let customMessage = try XCTUnwrap(rpcError.userInfo[NSLocalizedDescriptionKey] as? String)
  477. XCTAssertEqual(customMessage, kFakeUserDisabledCustomErrorMessage)
  478. }
  479. }
  480. /** @fn testUndecodableSuccessResponse
  481. @brief This test checks the behaviour of @c postWithRequest:response:callback: when the
  482. response isn't decodable by the response class but no error condition was expected. We are
  483. expecting to receive an @c NSError with the code
  484. @c FIRAuthErrorCodeUnexpectedServerResponse and the error from @c setWithDictionary:error:
  485. as the value of the underlyingError.
  486. */
  487. func testUndecodableSuccessResponse() async throws {
  488. rpcIssuer.respondBlock = {
  489. try self.rpcIssuer.respond(withJSON: [:])
  490. }
  491. do {
  492. let request = FakeDecodingErrorRequest(withRequestBody: [:])
  493. let _ = try await authBackend.call(with: request)
  494. XCTFail("Expected to throw")
  495. } catch {
  496. let rpcError = error as NSError
  497. XCTAssertEqual(rpcError.domain, AuthErrors.domain)
  498. XCTAssertEqual(rpcError.code, AuthErrorCode.internalError.rawValue)
  499. let underlyingError = try XCTUnwrap(rpcError.userInfo[NSUnderlyingErrorKey] as? NSError)
  500. XCTAssertEqual(underlyingError.domain, AuthErrorUtils.internalErrorDomain)
  501. XCTAssertEqual(underlyingError.code, AuthInternalErrorCode.RPCResponseDecodingError.rawValue)
  502. let dictionary = try XCTUnwrap(underlyingError
  503. .userInfo[AuthErrorUtils.userInfoDeserializedResponseKey] as? [String: AnyHashable])
  504. XCTAssertEqual(dictionary, [:])
  505. XCTAssertNil(underlyingError.userInfo[AuthErrorUtils.userInfoDataKey])
  506. }
  507. }
  508. /** @fn testSuccessfulResponse
  509. @brief Tests that a decoded dictionary is handed to the response instance.
  510. */
  511. func testSuccessfulResponse() async throws {
  512. let kTestKey = "TestKey"
  513. let kTestValue = "TestValue"
  514. rpcIssuer.respondBlock = {
  515. // It doesn't matter what we respond with here, as long as it's not an error response. The
  516. // fake response will deterministically simulate a decoding error regardless of the response
  517. // value it was given.
  518. try self.rpcIssuer.respond(withJSON: [kTestKey: kTestValue])
  519. }
  520. let rpcResponse = try await authBackend.call(with: FakeRequest(withRequestBody: [:]))
  521. XCTAssertEqual(try XCTUnwrap(rpcResponse.receivedValue), kTestValue)
  522. }
  523. #if COCOAPODS || SWIFT_PACKAGE
  524. private class FakeHeartbeatLogger: NSObject, FIRHeartbeatLoggerProtocol {
  525. func headerValue() -> String? {
  526. // `asyncHeaderValue` should be used instead.
  527. fatalError("FakeHeartbeatLogger headerValue should not be used in tests.")
  528. }
  529. func asyncHeaderValue() async -> String? {
  530. let payload = flushHeartbeatsIntoPayload()
  531. guard !payload.isEmpty else {
  532. return nil
  533. }
  534. return payload.headerValue()
  535. }
  536. var onFlushHeartbeatsIntoPayloadHandler: (() -> _ObjC_HeartbeatsPayload)?
  537. func log() {
  538. // This API should not be used by the below tests because the Auth
  539. // SDK does not log heartbeats in its networking context.
  540. fatalError("FakeHeartbeatLogger log should not be used in tests.")
  541. }
  542. func flushHeartbeatsIntoPayload() -> FirebaseCoreInternal._ObjC_HeartbeatsPayload {
  543. guard let handler = onFlushHeartbeatsIntoPayloadHandler else {
  544. fatalError("Missing Handler")
  545. }
  546. return handler()
  547. }
  548. func heartbeatCodeForToday() -> FIRDailyHeartbeatCode {
  549. // This API should not be used by the below tests because the Auth
  550. // SDK uses only the V2 heartbeat API (`flushHeartbeatsIntoPayload`) for
  551. // getting heartbeats.
  552. return FIRDailyHeartbeatCode.none
  553. }
  554. }
  555. /** @fn testRequest_IncludesHeartbeatPayload_WhenHeartbeatsNeedSending
  556. @brief This test checks the behavior of @c postWithRequest:response:callback:
  557. to verify that a heartbeats payload is attached as a header to an
  558. outgoing request when there are stored heartbeats that need sending.
  559. */
  560. func testRequest_IncludesHeartbeatPayload_WhenHeartbeatsNeedSending() async throws {
  561. // Given
  562. let fakeHeartbeatLogger = FakeHeartbeatLogger()
  563. let requestConfiguration = AuthRequestConfiguration(apiKey: kFakeAPIKey,
  564. appID: kFakeAppID,
  565. heartbeatLogger: fakeHeartbeatLogger)
  566. let request = FakeRequest(withRequestBody: [:], requestConfiguration: requestConfiguration)
  567. // When
  568. let nonEmptyHeartbeatsPayload = HeartbeatLoggingTestUtils.nonEmptyHeartbeatsPayload
  569. fakeHeartbeatLogger.onFlushHeartbeatsIntoPayloadHandler = {
  570. nonEmptyHeartbeatsPayload
  571. }
  572. rpcIssuer.respondBlock = {
  573. // Force return from async post
  574. try self.rpcIssuer.respond(withJSON: [:])
  575. }
  576. _ = try? await authBackend.call(with: request)
  577. // Then
  578. let expectedHeader = HeartbeatLoggingTestUtils.nonEmptyHeartbeatsPayload.headerValue()
  579. let completeRequest = await rpcIssuer.completeRequest.value
  580. let headerValue = completeRequest.value(forHTTPHeaderField: "X-Firebase-Client")
  581. XCTAssertEqual(headerValue, expectedHeader)
  582. }
  583. /** @fn testRequest_IncludesAppCheckHeader
  584. @brief This test checks the behavior of @c postWithRequest:response:callback:
  585. to verify that a appCheck token is attached as a header to an
  586. outgoing request.
  587. */
  588. func testRequest_IncludesAppCheckHeader() async throws {
  589. // Given
  590. let fakeAppCheck = FakeAppCheck()
  591. let requestConfiguration = AuthRequestConfiguration(apiKey: kFakeAPIKey,
  592. appID: kFakeAppID,
  593. appCheck: fakeAppCheck)
  594. let request = FakeRequest(withRequestBody: [:], requestConfiguration: requestConfiguration)
  595. rpcIssuer.respondBlock = {
  596. // Just force return from async call.
  597. try self.rpcIssuer.respond(withJSON: [:])
  598. }
  599. _ = try? await authBackend.call(with: request)
  600. let completeRequest = await rpcIssuer.completeRequest.value
  601. let headerValue = completeRequest.value(forHTTPHeaderField: "X-Firebase-AppCheck")
  602. XCTAssertEqual(headerValue, fakeAppCheck.fakeAppCheckToken)
  603. }
  604. /** @fn testRequest_DoesNotIncludeAHeartbeatPayload_WhenNoHeartbeatsNeedSending
  605. @brief This test checks the behavior of @c postWithRequest:response:callback:
  606. to verify that a request header does not contain heartbeat data in the
  607. case that there are no stored heartbeats that need sending.
  608. */
  609. func testRequest_DoesNotIncludeAHeartbeatPayload_WhenNoHeartbeatsNeedSending() async throws {
  610. // Given
  611. let fakeHeartbeatLogger = FakeHeartbeatLogger()
  612. let requestConfiguration = AuthRequestConfiguration(apiKey: kFakeAPIKey,
  613. appID: kFakeAppID,
  614. heartbeatLogger: fakeHeartbeatLogger)
  615. let request = FakeRequest(withRequestBody: [:], requestConfiguration: requestConfiguration)
  616. // When
  617. let emptyHeartbeatsPayload = HeartbeatLoggingTestUtils.emptyHeartbeatsPayload
  618. fakeHeartbeatLogger.onFlushHeartbeatsIntoPayloadHandler = {
  619. emptyHeartbeatsPayload
  620. }
  621. rpcIssuer.respondBlock = {
  622. // Force return from async post
  623. try self.rpcIssuer.respond(withJSON: [:])
  624. }
  625. _ = try? await authBackend.call(with: request)
  626. // Then
  627. let completeRequest = await rpcIssuer.completeRequest.value
  628. XCTAssertNil(completeRequest.value(forHTTPHeaderField: "X-Firebase-Client"))
  629. }
  630. #endif // COCOAPODS || SWIFT_PACKAGE
  631. private class FakeRequest: AuthRPCRequest {
  632. typealias Response = FakeResponse
  633. func requestConfiguration() -> AuthRequestConfiguration {
  634. return configuration
  635. }
  636. let kFakeRequestURL = "https://www.google.com/"
  637. func requestURL() -> URL {
  638. return try! XCTUnwrap(URL(string: kFakeRequestURL))
  639. }
  640. var unencodedHTTPRequestBody: [String: AnyHashable]? {
  641. if encodingError == nil {
  642. return requestBody
  643. }
  644. // Else, return an unencodable request body that will cause an error to be thrown.
  645. struct UnencodableObject: Hashable {
  646. static func == (lhs: UnencodableObject, rhs: UnencodableObject) -> Bool {
  647. true
  648. }
  649. }
  650. return ["foo": UnencodableObject()]
  651. }
  652. static func makeRequestConfiguration() -> AuthRequestConfiguration {
  653. return AuthRequestConfiguration(
  654. apiKey: kFakeAPIKey,
  655. appID: kFakeAppID
  656. )
  657. }
  658. private let configuration: AuthRequestConfiguration
  659. let encodingError: NSError?
  660. let requestBody: [String: AnyHashable]
  661. init(withEncodingError error: NSError) {
  662. encodingError = error
  663. requestBody = [:]
  664. configuration = FakeRequest.makeRequestConfiguration()
  665. }
  666. init(withDecodingError error: NSError) {
  667. encodingError = nil
  668. requestBody = [:]
  669. configuration = FakeRequest.makeRequestConfiguration()
  670. }
  671. init(withRequestBody body: [String: AnyHashable],
  672. requestConfiguration: AuthRequestConfiguration = FakeRequest.makeRequestConfiguration()) {
  673. encodingError = nil
  674. requestBody = body
  675. configuration = requestConfiguration
  676. }
  677. }
  678. private struct FakeResponse: AuthRPCResponse {
  679. var receivedValue: String?
  680. init(dictionary: [String: AnyHashable]) throws {
  681. receivedValue = dictionary["TestKey"] as? String
  682. }
  683. }
  684. private class FakeDecodingErrorRequest: AuthRPCRequest {
  685. typealias Response = FakeDecodingErrorResponse
  686. func requestURL() -> URL {
  687. return fakeRequest.requestURL()
  688. }
  689. var unencodedHTTPRequestBody: [String: AnyHashable]? {
  690. fakeRequest.unencodedHTTPRequestBody
  691. }
  692. func requestConfiguration() -> FirebaseAuth.AuthRequestConfiguration {
  693. return fakeRequest.requestConfiguration()
  694. }
  695. let fakeRequest: FakeRequest
  696. init(withRequestBody body: [String: AnyHashable]) {
  697. fakeRequest = FakeRequest(withRequestBody: body)
  698. }
  699. }
  700. private struct FakeDecodingErrorResponse: AuthRPCResponse {
  701. init(dictionary: [String: AnyHashable]) throws {
  702. throw NSError(domain: "dummy", code: -1)
  703. }
  704. }
  705. }