FunctionsTests.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. // Copyright 2022 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 FirebaseCore
  16. @testable import FirebaseFunctions
  17. #if COCOAPODS
  18. import GTMSessionFetcher
  19. #else
  20. import GTMSessionFetcherCore
  21. #endif
  22. import SharedTestUtilities
  23. import XCTest
  24. class FunctionsTests: XCTestCase {
  25. var functions: Functions?
  26. var functionsCustomDomain: Functions?
  27. let fetcherService = GTMSessionFetcherService()
  28. let appCheckFake = FIRAppCheckFake()
  29. override func setUp() {
  30. super.setUp()
  31. functions = Functions(
  32. projectID: "my-project",
  33. region: "my-region",
  34. customDomain: nil,
  35. auth: nil,
  36. messaging: nil,
  37. appCheck: appCheckFake,
  38. fetcherService: fetcherService
  39. )
  40. functionsCustomDomain = Functions(projectID: "my-project", region: "my-region",
  41. customDomain: "https://mydomain.com", auth: nil,
  42. messaging: nil, appCheck: nil,
  43. fetcherService: fetcherService)
  44. }
  45. override func tearDown() {
  46. functions = nil
  47. functionsCustomDomain = nil
  48. super.tearDown()
  49. }
  50. func testFunctionsInstanceIsStablePerApp() throws {
  51. let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000",
  52. gcmSenderID: "00000000000000000-00000000000-000000000")
  53. options.projectID = "myProjectID"
  54. FirebaseApp.configure(options: options)
  55. var functions1 = Functions.functions()
  56. var functions2 = Functions.functions(app: FirebaseApp.app()!)
  57. XCTAssertEqual(functions1, functions2)
  58. FirebaseApp.configure(name: "test", options: options)
  59. let app2 = try XCTUnwrap(FirebaseApp.app(name: "test"))
  60. functions2 = Functions.functions(app: app2, region: "us-central2")
  61. XCTAssertNotEqual(functions1, functions2)
  62. functions1 = Functions.functions(app: app2, region: "us-central2")
  63. XCTAssertEqual(functions1, functions2)
  64. functions1 = Functions.functions(customDomain: "test_domain")
  65. functions2 = Functions.functions(region: "us-central1")
  66. XCTAssertNotEqual(functions1, functions2)
  67. functions2 = Functions.functions(app: FirebaseApp.app()!, customDomain: "test_domain")
  68. XCTAssertEqual(functions1, functions2)
  69. }
  70. func testFunctionURLForName() throws {
  71. XCTAssertEqual(
  72. functions?.functionURL(for: "my-endpoint")?.absoluteString,
  73. "https://my-region-my-project.cloudfunctions.net/my-endpoint"
  74. )
  75. }
  76. func testFunctionURLForNameEmulator() throws {
  77. functionsCustomDomain?.useEmulator(withHost: "localhost", port: 5005)
  78. XCTAssertEqual(
  79. functionsCustomDomain?.functionURL(for: "my-endpoint")?.absoluteString,
  80. "http://localhost:5005/my-project/my-region/my-endpoint"
  81. )
  82. }
  83. func testFunctionURLForNameRegionWithEmulatorWithScheme() throws {
  84. functionsCustomDomain?.useEmulator(withHost: "http://localhost", port: 5005)
  85. XCTAssertEqual(
  86. functionsCustomDomain?.functionURL(for: "my-endpoint")?.absoluteString,
  87. "http://localhost:5005/my-project/my-region/my-endpoint"
  88. )
  89. }
  90. func testFunctionURLForNameCustomDomain() throws {
  91. XCTAssertEqual(
  92. functionsCustomDomain?.functionURL(for: "my-endpoint")?.absoluteString,
  93. "https://mydomain.com/my-endpoint"
  94. )
  95. }
  96. func testSetEmulatorSettings() throws {
  97. functions?.useEmulator(withHost: "localhost", port: 1000)
  98. XCTAssertEqual("http://localhost:1000", functions?.emulatorOrigin)
  99. }
  100. /// Test that Functions instances get deallocated.
  101. func testFunctionsLifecycle() throws {
  102. weak var weakApp: FirebaseApp?
  103. weak var weakFunctions: Functions?
  104. try autoreleasepool {
  105. let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000",
  106. gcmSenderID: "00000000000000000-00000000000-000000000")
  107. options.projectID = "myProjectID"
  108. let app1 = FirebaseApp(instanceWithName: "transitory app", options: options)
  109. weakApp = try XCTUnwrap(app1)
  110. let functions = Functions(app: app1, region: "transitory-region", customDomain: nil)
  111. weakFunctions = functions
  112. XCTAssertNotNil(weakFunctions)
  113. }
  114. XCTAssertNil(weakApp)
  115. XCTAssertNil(weakFunctions)
  116. }
  117. // MARK: - App Check Integration
  118. func testCallFunctionWhenUsingLimitedUseAppCheckTokenThenTokenSuccess() {
  119. // Given
  120. // Stub returns of two different kinds of App Check tokens. Only the
  121. // limited use token should be present in Functions's request header.
  122. appCheckFake.tokenResult = FIRAppCheckTokenResultFake(token: "shared_valid_token", error: nil)
  123. appCheckFake.limitedUseTokenResult = FIRAppCheckTokenResultFake(
  124. token: "limited_use_valid_token",
  125. error: nil
  126. )
  127. let httpRequestExpectation = expectation(description: "HTTPRequestExpectation")
  128. fetcherService.testBlock = { fetcherToTest, testResponse in
  129. let appCheckTokenHeader = fetcherToTest.request?
  130. .value(forHTTPHeaderField: "X-Firebase-AppCheck")
  131. // Assert that header contains limited use token.
  132. XCTAssertEqual(appCheckTokenHeader, "limited_use_valid_token")
  133. testResponse(nil, "{\"data\":\"May the force be with you!\"}".data(using: .utf8), nil)
  134. httpRequestExpectation.fulfill()
  135. }
  136. // When
  137. let options = HTTPSCallableOptions(requireLimitedUseAppCheckTokens: true)
  138. // Then
  139. let completionExpectation = expectation(description: "completionExpectation")
  140. functions?
  141. .httpsCallable("fake_func", options: options)
  142. .call { result, error in
  143. guard let result = result else {
  144. return XCTFail("Unexpected error: \(error!).")
  145. }
  146. XCTAssertEqual(result.data as! String, "May the force be with you!")
  147. completionExpectation.fulfill()
  148. }
  149. waitForExpectations(timeout: 1.5)
  150. }
  151. func testCallFunctionWhenLimitedUseAppCheckTokenDisabledThenCallWithoutToken() {
  152. // Given
  153. let limitedUseDummyToken = "limited use dummy token"
  154. appCheckFake.limitedUseTokenResult = FIRAppCheckTokenResultFake(
  155. token: limitedUseDummyToken,
  156. error: NSError(domain: #function, code: -1)
  157. )
  158. let httpRequestExpectation = expectation(description: "HTTPRequestExpectation")
  159. fetcherService.testBlock = { fetcherToTest, testResponse in
  160. // Assert that header does not contain an AppCheck token.
  161. fetcherToTest.request?.allHTTPHeaderFields?.forEach { key, value in
  162. if key == "X-Firebase-AppCheck" {
  163. XCTAssertNotEqual(value, limitedUseDummyToken)
  164. }
  165. }
  166. testResponse(nil, "{\"data\":\"May the force be with you!\"}".data(using: .utf8), nil)
  167. httpRequestExpectation.fulfill()
  168. }
  169. // When
  170. let options = HTTPSCallableOptions(requireLimitedUseAppCheckTokens: false)
  171. // Then
  172. let completionExpectation = expectation(description: "completionExpectation")
  173. functions?
  174. .httpsCallable("fake_func", options: options)
  175. .call { result, error in
  176. guard let result = result else {
  177. return XCTFail("Unexpected error: \(error!).")
  178. }
  179. XCTAssertEqual(result.data as! String, "May the force be with you!")
  180. completionExpectation.fulfill()
  181. }
  182. waitForExpectations(timeout: 1.5)
  183. }
  184. func testCallFunctionWhenLimitedUseAppCheckTokenCannotBeGeneratedThenCallWithoutToken() {
  185. // Given
  186. appCheckFake.limitedUseTokenResult = FIRAppCheckTokenResultFake(
  187. token: "dummy token",
  188. error: NSError(domain: #function, code: -1)
  189. )
  190. let httpRequestExpectation = expectation(description: "HTTPRequestExpectation")
  191. fetcherService.testBlock = { fetcherToTest, testResponse in
  192. // Assert that header does not contain an AppCheck token.
  193. fetcherToTest.request?.allHTTPHeaderFields?.forEach { key, _ in
  194. XCTAssertNotEqual(key, "X-Firebase-AppCheck")
  195. }
  196. testResponse(nil, "{\"data\":\"May the force be with you!\"}".data(using: .utf8), nil)
  197. httpRequestExpectation.fulfill()
  198. }
  199. // When
  200. let options = HTTPSCallableOptions(requireLimitedUseAppCheckTokens: true)
  201. // Then
  202. let completionExpectation = expectation(description: "completionExpectation")
  203. functions?
  204. .httpsCallable("fake_func", options: options)
  205. .call { result, error in
  206. guard let result = result else {
  207. return XCTFail("Unexpected error: \(error!).")
  208. }
  209. XCTAssertEqual(result.data as! String, "May the force be with you!")
  210. completionExpectation.fulfill()
  211. }
  212. waitForExpectations(timeout: 1.5)
  213. }
  214. func testCallFunctionWhenAppCheckIsInstalledAndFACTokenSuccess() {
  215. // Stub returns of two different kinds of App Check tokens. Only the
  216. // shared use token should be present in Functions's request header.
  217. appCheckFake.tokenResult = FIRAppCheckTokenResultFake(token: "shared_valid_token", error: nil)
  218. appCheckFake.limitedUseTokenResult = FIRAppCheckTokenResultFake(
  219. token: "limited_use_valid_token",
  220. error: nil
  221. )
  222. let networkError = NSError(
  223. domain: "testCallFunctionWhenAppCheckIsInstalled",
  224. code: -1,
  225. userInfo: nil
  226. )
  227. let httpRequestExpectation = expectation(description: "HTTPRequestExpectation")
  228. fetcherService.testBlock = { fetcherToTest, testResponse in
  229. let appCheckTokenHeader = fetcherToTest.request?
  230. .value(forHTTPHeaderField: "X-Firebase-AppCheck")
  231. XCTAssertEqual(appCheckTokenHeader, "shared_valid_token")
  232. testResponse(nil, nil, networkError)
  233. httpRequestExpectation.fulfill()
  234. }
  235. let completionExpectation = expectation(description: "completionExpectation")
  236. functions?
  237. .httpsCallable("fake_func")
  238. .call { result, error in
  239. guard let error = error else {
  240. return XCTFail("Unexpected success: \(result!).")
  241. }
  242. XCTAssertEqual(error as NSError, networkError)
  243. completionExpectation.fulfill()
  244. }
  245. waitForExpectations(timeout: 1.5)
  246. }
  247. func testAsyncCallFunctionWhenAppCheckIsNotInstalled() async {
  248. let networkError = NSError(
  249. domain: "testCallFunctionWhenAppCheckIsInstalled",
  250. code: -1,
  251. userInfo: nil
  252. )
  253. let httpRequestExpectation = expectation(description: "HTTPRequestExpectation")
  254. fetcherService.testBlock = { fetcherToTest, testResponse in
  255. let appCheckTokenHeader = fetcherToTest.request?
  256. .value(forHTTPHeaderField: "X-Firebase-AppCheck")
  257. XCTAssertNil(appCheckTokenHeader)
  258. testResponse(nil, nil, networkError)
  259. httpRequestExpectation.fulfill()
  260. }
  261. do {
  262. _ = try await functionsCustomDomain?
  263. .callFunction(
  264. at: URL(string: "https://example.com/fake_func")!,
  265. withObject: nil,
  266. options: nil,
  267. timeout: 10
  268. )
  269. XCTFail("Expected an error")
  270. } catch {
  271. XCTAssertEqual(error as NSError, networkError)
  272. }
  273. await fulfillment(of: [httpRequestExpectation], timeout: 1.5)
  274. }
  275. func testCallFunctionWhenAppCheckIsNotInstalled() {
  276. let networkError = NSError(
  277. domain: "testCallFunctionWhenAppCheckIsInstalled",
  278. code: -1,
  279. userInfo: nil
  280. )
  281. let httpRequestExpectation = expectation(description: "HTTPRequestExpectation")
  282. fetcherService.testBlock = { fetcherToTest, testResponse in
  283. let appCheckTokenHeader = fetcherToTest.request?
  284. .value(forHTTPHeaderField: "X-Firebase-AppCheck")
  285. XCTAssertNil(appCheckTokenHeader)
  286. testResponse(nil, nil, networkError)
  287. httpRequestExpectation.fulfill()
  288. }
  289. let completionExpectation = expectation(description: "completionExpectation")
  290. functionsCustomDomain?.callFunction(
  291. at: URL(string: "https://example.com/fake_func")!,
  292. withObject: nil,
  293. options: nil,
  294. timeout: 10
  295. ) { result in
  296. switch result {
  297. case .success:
  298. XCTFail("Unexpected success from functions?.callFunction")
  299. case let .failure(error as NSError):
  300. XCTAssertEqual(error, networkError)
  301. }
  302. completionExpectation.fulfill()
  303. }
  304. waitForExpectations(timeout: 1.5)
  305. }
  306. }