OAuthProviderTests.swift 14 KB

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