AuthBackendRPCImplentationTests.swift 34 KB

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