AuthBackendRPCImplentationTests.swift 33 KB

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