OAuthProviderTests.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. // Copyright 2021 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 Combine
  16. import XCTest
  17. import FirebaseAuth
  18. class OAuthProviderTests: XCTestCase {
  19. override class func setUp() {
  20. FirebaseApp.configureForTests()
  21. Bundle.mock(with: MockBundle.self)
  22. }
  23. override class func tearDown() {
  24. FirebaseApp.app()?.delete { success in
  25. if success {
  26. print("Shut down app successfully.")
  27. } else {
  28. print("💥 There was a problem when shutting down the app..")
  29. }
  30. }
  31. }
  32. static let encodedFirebaseAppID = "app-1-1085102361755-ios-f790a919483d5bdf"
  33. static let reverseClientID = "com.googleusercontent.apps.123456"
  34. static let providerID = "fakeProviderID"
  35. static let authorizedDomain = "test.firebaseapp.com"
  36. static let oauthResponseURL = "fakeOAuthResponseURL"
  37. static let redirectURLResponseURL =
  38. "://firebaseauth/link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fcallback%3FauthType%3DsignInWithRedirect%26link%3D"
  39. static let redirectURLBaseErrorString =
  40. "com.googleusercontent.apps.123456://firebaseauth/link?deep_link_id=https%3A%2F%2Fexample.firebaseapp.com%2F__%2Fauth%2Fcallback%3f"
  41. static let networkRequestFailedErrorString =
  42. "firebaseError%3D%257B%2522code%2522%253A%2522auth%252Fnetwork-request-failed%2522%252C%2522message%2522%253A%2522The%2520network%2520request%2520failed%2520.%2522%257D%26authType%3DsignInWithRedirect"
  43. static let internalErrorString =
  44. "firebaseError%3D%257B%2522code%2522%253A%2522auth%252Finternal-error%2522%252C%2522message%2522%253A%2522Internal%2520error%2520.%2522%257D%26authType%3DsignInWithRedirect"
  45. static let invalidClientIDString =
  46. "firebaseError%3D%257B%2522code%2522%253A%2522auth%252Finvalid-oauth-client-id%2522%252C%2522message%2522%253A%2522The%2520OAuth%2520client%2520ID%2520provided%2520is%2520either%2520invalid%2520or%2520does%2520not%2520match%2520the%2520specified%2520API%2520key.%2522%257D%26authType%3DsignInWithRedirect"
  47. static let unknownErrorString =
  48. "firebaseError%3D%257B%2522code%2522%253A%2522auth%252Funknown-error-id%2522%252C%2522message%2522%253A%2522The%2520OAuth%2520client%2520ID%2520provided%2520is%2520either%2520invalid%2520or%2520does%2520not%2520match%2520the%2520specified%2520API%2520key.%2522%257D%26authType%3DsignInWithRedirect"
  49. class MockAuth: Auth {
  50. private var _authURLPresenter: FIRAuthURLPresenter!
  51. override class func auth() -> Auth {
  52. let auth = MockAuth(
  53. apiKey: Credentials.apiKey,
  54. appName: "app1",
  55. appID: Credentials.googleAppID
  56. )!
  57. auth._authURLPresenter = MockAuthURLPresenter()
  58. return auth
  59. }
  60. override var app: FirebaseApp? {
  61. FirebaseApp.appForAuthUnitTestsWithName(name: "app1")
  62. }
  63. override var authURLPresenter: FIRAuthURLPresenter { _authURLPresenter }
  64. override var requestConfiguration: FIRAuthRequestConfiguration {
  65. MockRequestConfiguration(apiKey: Credentials.apiKey, appID: Credentials.googleAppID)!
  66. }
  67. }
  68. class MockRequestConfiguration: FIRAuthRequestConfiguration {}
  69. class MockUIDelegate: NSObject, AuthUIDelegate {
  70. func present(_ viewControllerToPresent: UIViewController,
  71. animated flag: Bool, completion: (() -> Void)? = nil) {}
  72. func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {}
  73. }
  74. class MockAuthURLPresenter: FIRAuthURLPresenter {
  75. var authURLPresentationResult: Result<URL, Error>?
  76. var redirectURL = reverseClientID + redirectURLResponseURL + oauthResponseURL
  77. override func present(_ URL: URL, uiDelegate UIDelegate: AuthUIDelegate?,
  78. callbackMatcher: @escaping FIRAuthURLCallbackMatcher,
  79. completion: @escaping FIRAuthURLPresentationCompletion) {
  80. XCTAssertEqual(URL.scheme, "https")
  81. XCTAssertEqual(URL.host, OAuthProviderTests.authorizedDomain)
  82. XCTAssertEqual(URL.path, "/__/auth/handler")
  83. do {
  84. let query = try XCTUnwrap(URL.query)
  85. let params = FIRAuthWebUtils.dictionary(withHttpArgumentsString: query)
  86. let ibi = try XCTUnwrap(params["ibi"] as? String)
  87. XCTAssertEqual(ibi, Credentials.bundleID)
  88. let clientId = try XCTUnwrap(params["clientId"] as? String)
  89. XCTAssertEqual(clientId, Credentials.clientID)
  90. let apiKey = try XCTUnwrap(params["apiKey"] as? String)
  91. XCTAssertEqual(apiKey, Credentials.apiKey)
  92. let authType = try XCTUnwrap(params["authType"] as? String)
  93. XCTAssertEqual(authType, "signInWithRedirect")
  94. XCTAssertNotNil(params["v"])
  95. // Verify that the URL is rejected by the callback matcher without the event ID.
  96. XCTAssertFalse(callbackMatcher(Foundation.URL(string: redirectURL)))
  97. redirectURL.append("%26eventId%3D")
  98. let eventId = try XCTUnwrap(params["eventId"] as? String)
  99. redirectURL.append(eventId)
  100. let originalComponents = URLComponents(string: redirectURL)
  101. // Verify that the URL is accepted by the callback matcher with the matching event ID.
  102. XCTAssertTrue(callbackMatcher(originalComponents?.url))
  103. var components = originalComponents
  104. components?.query = "https"
  105. XCTAssertFalse(callbackMatcher(components?.url))
  106. components = originalComponents
  107. components?.host = "badhost"
  108. XCTAssertFalse(callbackMatcher(components?.url))
  109. components = originalComponents
  110. components?.path = "badpath"
  111. XCTAssertFalse(callbackMatcher(components?.url))
  112. components = originalComponents
  113. components?.query = "badquery"
  114. XCTAssertFalse(callbackMatcher(components?.url))
  115. FIRAuthGlobalWorkQueue().async { [weak self] in
  116. switch self?.authURLPresentationResult {
  117. case let .success(url):
  118. completion(url, nil)
  119. case let .failure(error):
  120. completion(nil, error)
  121. default:
  122. completion(originalComponents?.url, nil)
  123. }
  124. }
  125. } catch {
  126. XCTFail("💥 Expect non-nil: \(error)")
  127. }
  128. }
  129. }
  130. class MockGetProjectConfigResponse: FIRGetProjectConfigResponse {
  131. override var authorizedDomains: [Any]? { [authorizedDomain] }
  132. }
  133. class MockAuthBackend: AuthBackendImplementationMock {
  134. override func getProjectConfig(_ request: FIRGetProjectConfigRequest,
  135. callback: @escaping FIRGetProjectConfigResponseCallback) {
  136. XCTAssertNotNil(request)
  137. FIRAuthGlobalWorkQueue().async {
  138. callback(MockGetProjectConfigResponse(), nil)
  139. }
  140. }
  141. }
  142. class MockBundle: Bundle {
  143. override class var main: Bundle { MockBundle() }
  144. override var bundleIdentifier: String? { Credentials.bundleID }
  145. override func object(forInfoDictionaryKey key: String) -> Any? {
  146. switch key {
  147. case "CFBundleURLTypes":
  148. return [["CFBundleURLSchemes": [reverseClientID]]]
  149. default:
  150. return nil
  151. }
  152. }
  153. }
  154. func testGetCredentialWithUIDelegateWithClientID() {
  155. // given
  156. FIRAuthBackend.setBackendImplementation(MockAuthBackend())
  157. var cancellables = Set<AnyCancellable>()
  158. let getCredentialExpectation = expectation(description: "Get credential")
  159. let uiDelegate = MockUIDelegate()
  160. let auth = MockAuth.auth()
  161. let provider = OAuthProvider(providerID: Self.providerID, auth: auth)
  162. provider.getCredentialWith(uiDelegate)
  163. .sink { completion in
  164. switch completion {
  165. case .finished:
  166. print("Finished")
  167. case let .failure(error):
  168. XCTFail("💥 Something went wrong: \(error)")
  169. }
  170. } receiveValue: { credential in
  171. do {
  172. XCTAssertTrue(Thread.isMainThread)
  173. let oauthCredential = try XCTUnwrap(credential as? OAuthCredential)
  174. XCTAssertEqual(Self.oauthResponseURL, oauthCredential.oAuthResponseURLString)
  175. } catch {
  176. XCTFail("💥 Expect non-nil OAuth credential: \(error)")
  177. }
  178. getCredentialExpectation.fulfill()
  179. }
  180. .store(in: &cancellables)
  181. // then
  182. wait(for: [getCredentialExpectation], timeout: expectationTimeout)
  183. }
  184. func testGetCredentialWithUIDelegateUserCancellationWithClientID() {
  185. // given
  186. FIRAuthBackend.setBackendImplementation(MockAuthBackend())
  187. var cancellables = Set<AnyCancellable>()
  188. let getCredentialExpectation = expectation(description: "Get credential")
  189. let uiDelegate = MockUIDelegate()
  190. let auth = MockAuth.auth()
  191. let authURLPresenter = auth.authURLPresenter as? MockAuthURLPresenter
  192. let cancelError = FIRAuthErrorUtils.webContextCancelledError(withMessage: nil)
  193. authURLPresenter?.authURLPresentationResult = .failure(cancelError)
  194. let provider = OAuthProvider(providerID: Self.providerID, auth: auth)
  195. provider.getCredentialWith(uiDelegate)
  196. .sink { completion in
  197. if case let .failure(error as NSError) = completion {
  198. XCTAssertEqual(error.code, AuthErrorCode.webContextCancelled.rawValue)
  199. getCredentialExpectation.fulfill()
  200. }
  201. } receiveValue: { authDataResult in
  202. XCTFail("💥 result unexpected")
  203. }
  204. .store(in: &cancellables)
  205. // then
  206. wait(for: [getCredentialExpectation], timeout: expectationTimeout)
  207. }
  208. func testGetCredentialWithUIDelegateNetworkRequestFailedWithClientID() {
  209. // given
  210. FIRAuthBackend.setBackendImplementation(MockAuthBackend())
  211. var cancellables = Set<AnyCancellable>()
  212. let getCredentialExpectation = expectation(description: "Get credential")
  213. let uiDelegate = MockUIDelegate()
  214. let auth = MockAuth.auth()
  215. let authURLPresenter = auth.authURLPresenter as? MockAuthURLPresenter
  216. authURLPresenter?.redirectURL = Self.redirectURLBaseErrorString + Self
  217. .networkRequestFailedErrorString
  218. let provider = OAuthProvider(providerID: Self.providerID, auth: auth)
  219. provider.getCredentialWith(uiDelegate)
  220. .sink { completion in
  221. if case let .failure(error as NSError) = completion {
  222. XCTAssertEqual(error.code, AuthErrorCode.webNetworkRequestFailed.rawValue)
  223. getCredentialExpectation.fulfill()
  224. }
  225. } receiveValue: { authDataResult in
  226. XCTFail("💥 result unexpected")
  227. }
  228. .store(in: &cancellables)
  229. // then
  230. wait(for: [getCredentialExpectation], timeout: expectationTimeout)
  231. }
  232. func testGetCredentialWithUIDelegateInternalErrorWithClientID() {
  233. // given
  234. FIRAuthBackend.setBackendImplementation(MockAuthBackend())
  235. var cancellables = Set<AnyCancellable>()
  236. let getCredentialExpectation = expectation(description: "Get credential")
  237. let uiDelegate = MockUIDelegate()
  238. let auth = MockAuth.auth()
  239. let authURLPresenter = auth.authURLPresenter as? MockAuthURLPresenter
  240. authURLPresenter?.redirectURL = Self.redirectURLBaseErrorString + Self.internalErrorString
  241. let provider = OAuthProvider(providerID: Self.providerID, auth: auth)
  242. provider.getCredentialWith(uiDelegate)
  243. .sink { completion in
  244. if case let .failure(error as NSError) = completion {
  245. XCTAssertEqual(error.code, AuthErrorCode.webInternalError.rawValue)
  246. getCredentialExpectation.fulfill()
  247. }
  248. } receiveValue: { authDataResult in
  249. XCTFail("💥 result unexpected")
  250. }
  251. .store(in: &cancellables)
  252. // then
  253. wait(for: [getCredentialExpectation], timeout: expectationTimeout)
  254. }
  255. func testGetCredentialWithUIDelegateInvalidClientID() {
  256. // given
  257. FIRAuthBackend.setBackendImplementation(MockAuthBackend())
  258. var cancellables = Set<AnyCancellable>()
  259. let getCredentialExpectation = expectation(description: "Get credential")
  260. let uiDelegate = MockUIDelegate()
  261. let auth = MockAuth.auth()
  262. let authURLPresenter = auth.authURLPresenter as? MockAuthURLPresenter
  263. authURLPresenter?.redirectURL = Self.redirectURLBaseErrorString
  264. authURLPresenter?.redirectURL.append(Self.invalidClientIDString)
  265. let provider = OAuthProvider(providerID: Self.providerID, auth: auth)
  266. provider.getCredentialWith(uiDelegate)
  267. .sink { completion in
  268. if case let .failure(error as NSError) = completion {
  269. XCTAssertEqual(error.code, AuthErrorCode.invalidClientID.rawValue)
  270. getCredentialExpectation.fulfill()
  271. }
  272. } receiveValue: { authDataResult in
  273. XCTFail("💥 result unexpected")
  274. }
  275. .store(in: &cancellables)
  276. // then
  277. wait(for: [getCredentialExpectation], timeout: expectationTimeout)
  278. }
  279. func testGetCredentialWithUIDelegateUnknownErrorWithClientID() {
  280. // given
  281. FIRAuthBackend.setBackendImplementation(MockAuthBackend())
  282. var cancellables = Set<AnyCancellable>()
  283. let getCredentialExpectation = expectation(description: "Get credential")
  284. let uiDelegate = MockUIDelegate()
  285. let auth = MockAuth.auth()
  286. let authURLPresenter = auth.authURLPresenter as? MockAuthURLPresenter
  287. authURLPresenter?.redirectURL = Self.redirectURLBaseErrorString
  288. authURLPresenter?.redirectURL.append(Self.unknownErrorString)
  289. let provider = OAuthProvider(providerID: Self.providerID, auth: auth)
  290. provider.getCredentialWith(uiDelegate)
  291. .sink { completion in
  292. if case let .failure(error as NSError) = completion {
  293. XCTAssertEqual(
  294. error.code,
  295. AuthErrorCode.webSignInUserInteractionFailure.rawValue
  296. )
  297. getCredentialExpectation.fulfill()
  298. }
  299. } receiveValue: { authDataResult in
  300. XCTFail("💥 result unexpected")
  301. }
  302. .store(in: &cancellables)
  303. // then
  304. wait(for: [getCredentialExpectation], timeout: expectationTimeout)
  305. }
  306. }