diff --git a/.github/workflows/remoteconfig.yml b/.github/workflows/remoteconfig.yml index dd8b5a879e..c696cdbee9 100644 --- a/.github/workflows/remoteconfig.yml +++ b/.github/workflows/remoteconfig.yml @@ -30,6 +30,10 @@ jobs: - name: Install Secret GoogleService-Info.plist run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/RemoteConfigSwiftAPI/GoogleService-Info.plist.gpg \ FirebaseRemoteConfig/Tests/SwiftAPI/GoogleService-Info.plist "$plist_secret" + - name: Generate Access Token for RemoteConfigConsoleAPI in IntegrationTests + if: matrix.target == 'iOS' + run: scripts/generate_access_token.sh "$plist_secret" scripts/gha-encrypted/RemoteConfigSwiftAPI/ServiceAccount.json.gpg + FirebaseRemoteConfig/Tests/SwiftAPI/AccessToken.json - name: BuildAndUnitTest # can be replaced with pod lib lint with CocoaPods 1.10 run: scripts/third_party/travis/retry.sh scripts/build.sh RemoteConfig ${{ matrix.target }} unit - name: Fake Console API Tests diff --git a/FirebaseRemoteConfig.podspec b/FirebaseRemoteConfig.podspec index 89e4e6aa30..669271a16c 100644 --- a/FirebaseRemoteConfig.podspec +++ b/FirebaseRemoteConfig.podspec @@ -87,7 +87,8 @@ app update. swift_api.pod_target_xcconfig = { 'SWIFT_OBJC_BRIDGING_HEADER' => '$(PODS_TARGET_SRCROOT)/FirebaseRemoteConfig/Tests/FakeUtils/Bridging-Header.h' } - swift_api.resources = 'FirebaseRemoteConfig/Tests/SwiftAPI/GoogleService-Info.plist' + swift_api.resources = 'FirebaseRemoteConfig/Tests/SwiftAPI/GoogleService-Info.plist', + 'FirebaseRemoteConfig/Tests/SwiftAPI/AccessToken.json' swift_api.dependency 'OCMock' end diff --git a/FirebaseRemoteConfig/Tests/SwiftAPI/APITests.swift b/FirebaseRemoteConfig/Tests/SwiftAPI/APITests.swift index 015ac4a1c6..7ce3aa3ef2 100644 --- a/FirebaseRemoteConfig/Tests/SwiftAPI/APITests.swift +++ b/FirebaseRemoteConfig/Tests/SwiftAPI/APITests.swift @@ -17,11 +17,37 @@ import FirebaseCore import XCTest +/// String constants used for testing. +private enum Constants { + static let key1 = "Key1" + static let jedi = "Jedi" + static let sith = "Sith_Lord" + static let value1 = "Value1" + static let obiwan = "Obi-Wan" + static let yoda = "Yoda" + static let darthSidious = "Darth Sidious" +} + class APITests: APITestBase { + var console: RemoteConfigConsole! + override func setUp() { super.setUp() if APITests.useFakeConfig { - fakeConsole.config = ["Key1": "Value1"] + fakeConsole.config = [Constants.key1: Constants.value1] + } else { + console = RemoteConfigConsole() + console.updateRemoteConfigValue(Constants.obiwan, forKey: Constants.jedi) + } + } + + override func tearDown() { + super.tearDown() + + // If using RemoteConfigConsole, reset remote config values. + if !APITests.useFakeConfig { + console.removeRemoteConfigValue(forKey: Constants.sith) + console.removeRemoteConfigValue(forKey: Constants.jedi) } } @@ -34,7 +60,7 @@ class APITests: APITestBase { XCTAssertEqual(status, RemoteConfigFetchStatus.success) self.config.activate { _, error in XCTAssertNil(error) - XCTAssertEqual(self.config["Key1"].stringValue, "Value1") + XCTAssertEqual(self.config[Constants.key1].stringValue, Constants.value1) expectation.fulfill() } } @@ -50,7 +76,7 @@ class APITests: APITestBase { XCTAssertEqual(status, RemoteConfigFetchStatus.success) self.config.activate { _, error in XCTAssertNil(error) - XCTAssertEqual(self.config["Key1"].stringValue, "Value1") + XCTAssertEqual(self.config[Constants.key1].stringValue, Constants.value1) expectation.fulfill() } } @@ -63,7 +89,7 @@ class APITests: APITestBase { if let error = error { XCTFail("Fetch and Activate Error \(error)") } - XCTAssertEqual(self.config["Key1"].stringValue, "Value1") + XCTAssertEqual(self.config[Constants.key1].stringValue, Constants.value1) expectation.fulfill() } waitForExpectations() @@ -82,7 +108,7 @@ class APITests: APITestBase { if let error = error { print("Activate Error \(error)") } - XCTAssertEqual(self.config["Key1"].stringValue, "Value1") + XCTAssertEqual(self.config[Constants.key1].stringValue, Constants.value1) expectation.fulfill() } } @@ -98,7 +124,7 @@ class APITests: APITestBase { if let error = error { XCTAssertEqual((error as NSError).code, RemoteConfigError.internalError.rawValue) } - XCTAssertEqual(self.config["Key1"].stringValue, "Value1") + XCTAssertEqual(self.config[Constants.key1].stringValue, Constants.value1) expectation2.fulfill() } } @@ -117,7 +143,7 @@ class APITests: APITestBase { self.config.activate { changed, error in XCTAssertTrue(!APITests.useFakeConfig || changed) XCTAssertNil(error) - XCTAssertEqual(self.config["Key1"].stringValue, "Value1") + XCTAssertEqual(self.config[Constants.key1].stringValue, Constants.value1) expectation.fulfill() } } @@ -131,13 +157,161 @@ class APITests: APITestBase { self.config.activate { changed, error in XCTAssertFalse(changed) XCTAssertNil(error) - XCTAssertEqual(self.config["Key1"].stringValue, "Value1") + XCTAssertEqual(self.config[Constants.key1].stringValue, Constants.value1) expectation2.fulfill() } } waitForExpectations() } + func testFetchAndActivateUnchangedConfig() throws { + guard APITests.useFakeConfig == false else { return } + + let expectation = self.expectation(description: #function) + + XCTAssertEqual(config.settings.minimumFetchInterval, 0) + + let serialQueue = DispatchQueue(label: "\(#function)Queue") + let group = DispatchGroup() + group.enter() + serialQueue.async { + // Represents pre-fetch occuring sometime in past. + self.config.fetch { status, error in + XCTAssertNil(error, "Fetch Error \(error!)") + XCTAssertEqual(status, .success) + group.leave() + } + } + + serialQueue.async { + group.wait() + group.enter() + // Represents a `fetchAndActivate` being made to pull latest changes from Remote Config. + self.config.fetchAndActivate { status, error in + XCTAssertNil(error, "Fetch & Activate Error \(error!)") + // Since no updates to remote config have occurred we use the `.successUsingPreFetchedData`. + XCTAssertEqual(status, .successUsingPreFetchedData) + // The `lastETagUpdateTime` should either be older or the same time as `lastFetchTime`. + if let lastFetchTime = try? XCTUnwrap(self.config.lastFetchTime) { + XCTAssertLessThanOrEqual(Double(self.config.settings.lastETagUpdateTime), + Double(lastFetchTime.timeIntervalSince1970)) + } else { + XCTFail("Could not unwrap lastFetchTime.") + } + + expectation.fulfill() + } + } + + waitForExpectations() + } + + // MARK: - RemoteConfigConsole Tests + + func testFetchConfigThenUpdateConsoleThenFetchAgain() { + guard APITests.useFakeConfig == false else { return } + + let expectation = self.expectation(description: #function) + + config.fetchAndActivate { status, error in + XCTAssertNil(error, "Fetch & Activate Error \(error!)") + + if let configValue = self.config.configValue(forKey: Constants.jedi).stringValue { + XCTAssertEqual(configValue, Constants.obiwan) + } else { + XCTFail("Could not unwrap config value for key: \(Constants.jedi)") + } + expectation.fulfill() + } + waitForExpectations() + + // Synchronously update the console. + console.updateRemoteConfigValue(Constants.yoda, forKey: Constants.jedi) + + let expectation2 = self.expectation(description: #function + "2") + config.fetchAndActivate { status, error in + XCTAssertNil(error, "Fetch & Activate Error \(error!)") + + if let configValue = self.config.configValue(forKey: Constants.jedi).stringValue { + XCTAssertEqual(configValue, Constants.yoda) + } else { + XCTFail("Could not unwrap config value for key: \(Constants.jedi)") + } + + expectation2.fulfill() + } + waitForExpectations() + } + + func testFetchConfigThenAddValueOnConsoleThenFetchAgain() { + guard APITests.useFakeConfig == false else { return } + + // Ensure no Sith Lord has been written to Remote Config yet. + let expectation = self.expectation(description: #function) + + config.fetchAndActivate { status, error in + XCTAssertNil(error, "Fetch & Activate Error \(error!)") + + XCTAssertTrue(self.config.configValue(forKey: Constants.sith).dataValue.isEmpty) + + expectation.fulfill() + } + waitForExpectations() + + // Synchronously update the console + console.updateRemoteConfigValue(Constants.darthSidious, forKey: Constants.sith) + + // Verify the Sith Lord can now be fetched from Remote Config. + let expectation2 = self.expectation(description: #function + "2") + + config.fetchAndActivate { status, error in + XCTAssertNil(error, "Fetch & Activate Error \(error!)") + + if let configValue = self.config.configValue(forKey: Constants.sith).stringValue { + XCTAssertEqual(configValue, Constants.darthSidious) + } else { + XCTFail("Could not unwrap config value for key: \(Constants.sith)") + } + + expectation2.fulfill() + } + waitForExpectations() + } + + func testFetchConfigThenDeleteValueOnConsoleThenFetchAgain() { + guard APITests.useFakeConfig == false else { return } + + let expectation = self.expectation(description: #function) + + config.fetchAndActivate { status, error in + XCTAssertNil(error, "Fetch & Activate Error \(error!)") + + if let configValue = self.config.configValue(forKey: Constants.jedi).stringValue { + XCTAssertEqual(configValue, Constants.obiwan) + } else { + XCTFail("Could not unwrap config value for key: \(Constants.jedi)") + } + expectation.fulfill() + } + waitForExpectations() + + // Synchronously delete value on the console. + console.removeRemoteConfigValue(forKey: Constants.jedi) + + let expectation2 = self.expectation(description: #function + "2") + config.fetchAndActivate { status, error in + XCTAssertNil(error, "Fetch & Activate Error \(error!)") + + XCTAssertTrue(self.config.configValue(forKey: Constants.jedi).dataValue.isEmpty, + "Remote config should have been deleted.") + + expectation2.fulfill() + } + waitForExpectations() + } + + // MARK: - Private Helpers + private func waitForExpectations() { let kFIRStorageIntegrationTestTimeout = 10.0 waitForExpectations(timeout: kFIRStorageIntegrationTestTimeout, diff --git a/FirebaseRemoteConfig/Tests/SwiftAPI/AccessToken.json b/FirebaseRemoteConfig/Tests/SwiftAPI/AccessToken.json new file mode 100644 index 0000000000..c798f43e92 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/SwiftAPI/AccessToken.json @@ -0,0 +1,5 @@ +{ + "token_type":"Bearer", + "expires_in":3599, + "access_token":"Generated by ./scripts/generate_access_token.sh" +} diff --git a/FirebaseRemoteConfig/Tests/SwiftAPI/RemoteConfigConsole.swift b/FirebaseRemoteConfig/Tests/SwiftAPI/RemoteConfigConsole.swift new file mode 100644 index 0000000000..2a4043a1ed --- /dev/null +++ b/FirebaseRemoteConfig/Tests/SwiftAPI/RemoteConfigConsole.swift @@ -0,0 +1,239 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +class RemoteConfigConsole { + private let projectID: String + private var latestConfig: [String: Any]! + + public var requestTimeout: TimeInterval = 10 + + private var consoleURL: URL { + let api = "https://firebaseremoteconfig.googleapis.com" + let endpoint = "/v1/projects/\(projectID)/remoteConfig" + return URL(string: api + endpoint)! + } + + private lazy var accessToken: String = { + guard let fileURL = Bundle(for: type(of: self)) + .url(forResource: "AccessToken", withExtension: "json") else { + fatalError("Could not find AccessToken.json in bundle.") + } + guard let data = try? Data(contentsOf: fileURL), + let json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments), + let jsonDict = json as? [String: Any], + let accessToken = jsonDict["access_token"] as? String else { + fatalError("Could not retrieve access token.") + } + return accessToken + }() + + /// Synchronously fetches and returns currently active Remote Config, if it exists. + public var activeRemoteConfig: [String: Any]? { + var config: [String: Any]? + perform(configRequest: .get) { latestConfigJSON in + config = latestConfigJSON + } + if let config = config { + saveConfig(config) + } + return config + } + + /// Exposing this initializer allows us to create`RemoteConfigConsole` instances without + /// depending on a `GoogleService-Info.plist`. + init(projectID: String) { + self.projectID = projectID + syncWithConsole() + } + + /// This initializer will attempt to read from a `GoogleService-Info.plist` to set `projectID`. + convenience init() { + let currentBundle = Bundle(for: type(of: self)) + let projectID = currentBundle.plistValue( + forKey: "PROJECT_ID", + fromPlist: "GoogleService-Info.plist" + ) + self.init(projectID: projectID! as! String) + } + + // MARK: - Public API + + /// Update Remote Config with multiple String key value pairs. + /// - Parameter parameters: Dictionary representation of config key value pairs. + public func updateRemoteConfig(with parameters: [String: CustomStringConvertible]) { + var updatedConfig: [String: Any] = latestConfig + + let latestParameters = latestConfig["parameters"] as? [String: Any] + var updatedParameters = latestParameters ?? [String: Any]() + for (key, value) in parameters { + updatedParameters.updateValue(["defaultValue": ["value": value.description]], forKey: key) + } + updatedConfig.updateValue(updatedParameters, forKey: "parameters") + + publish(config: updatedConfig) + } + + /// Updates a Remote Config value for a given key. + /// - Parameters: + /// - value: Use strings, numbers, and booleans to represent Remote Config values. + /// - key: The corresponding string key that maps to the given value. + public func updateRemoteConfigValue(_ value: CustomStringConvertible, forKey key: String) { + var updatedConfig: [String: Any] = latestConfig + + let latestParameters = latestConfig["parameters"] as? [String: Any] + if var parameters = latestParameters { + parameters.updateValue(["defaultValue": ["value": value.description]], forKey: key) + updatedConfig.updateValue(parameters, forKey: "parameters") + + } else { + updatedConfig.updateValue( + [key: ["defaultValue": ["value": value.description]]], + forKey: "parameters" + ) + } + + publish(config: updatedConfig) + } + + public func removeRemoteConfigValue(forKey key: String) { + var updatedConfig: [String: Any] = latestConfig + + let latestParameters = latestConfig["parameters"] as? [String: Any] + if var parameters = latestParameters { + parameters.removeValue(forKey: key) + updatedConfig.updateValue(parameters, forKey: "parameters") + } + + publish(config: updatedConfig) + } + + public func clearRemoteConfig() { + var updatedConfig: [String: Any]! = latestConfig + updatedConfig.removeValue(forKey: "parameters") + publish(config: updatedConfig) + } + + // MARK: - Networking + + private enum ConfigRequest { + case get, put(_ data: Data) + + var httpMethod: String { + switch self { + case .get: return "GET" + case .put(data: _): return "PUT" + } + } + + var httpBody: Data? { + switch self { + case .get: return nil + case let .put(data: data): return data + } + } + + var httpHeaderFields: [String: String]? { + switch self { + case .get: return nil + case .put(data: _): + return ["Content-Type": "application/json; UTF8", "If-Match": "*"] + } + } + + func secureRequest(url: URL, with token: String, _ timeout: TimeInterval = 10) -> URLRequest { + var request = URLRequest(url: url, timeoutInterval: timeout) + request.httpMethod = self.httpMethod + request.allHTTPHeaderFields = self.httpHeaderFields + request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + request.httpBody = self.httpBody + return request + } + } + + /// Performs a given `ConfigRequest` synchronously. + private func perform(configRequest: ConfigRequest, + _ completion: (([String: Any]?) -> Void)? = nil) { + let request = configRequest.secureRequest(url: consoleURL, with: accessToken, requestTimeout) + + let semaphore = DispatchSemaphore(value: 0) + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + // Signal the semaphore when this scope is escaped. + defer { semaphore.signal() } + + guard let data = data else { + print(String(describing: error)) + return + } + + let json = try? JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed) + + if let response = response as? HTTPURLResponse, + let json = json as? [String: Any] { + if response.statusCode >= 400 { + print("RemoteConfigConsole Error: \(String(describing: json["error"]!))") + } + } + + completion?(json as? [String: Any]) + } + + task.resume() + semaphore.wait() + } + + /// Publishes a config object to the live console and updates `latestConfig`. + private func publish(config: [String: Any]) { + let configData = data(withConfig: config) + perform(configRequest: .put(configData)) + saveConfig(config) + } + + // MARK: - Private Helpers + + /// Creates an optional Data object given a config object. + /// Used for serializing config objects before posting them to live console. + private func data(withConfig config: [String: Any]) -> Data { + let dictionary = NSDictionary(dictionary: config, copyItems: true) + let data = try! JSONSerialization.data(withJSONObject: dictionary, options: .fragmentsAllowed) + return data + } + + /// Perform a synchronous sync with remote config console. + private func syncWithConsole() { + if let consoleConfig = activeRemoteConfig { + latestConfig = consoleConfig + } else { + fatalError("Could not sync with console.") + } + } + + /// A more intuitively named setter for `latestConfig`. + private func saveConfig(_ config: [String: Any]) { + latestConfig = config + } +} + +// MARK: - Extensions + +extension Bundle { + func plistValue(forKey key: String, fromPlist plist: String) -> Any? { + guard let plistURL = url(forResource: plist, withExtension: "") else { + print("Could not find plist file \(plist) in bundle.") + return nil + } + let plistDictionary = NSDictionary(contentsOf: plistURL) + return plistDictionary?.object(forKey: key) + } +} diff --git a/FirebaseRemoteConfig/Tests/SwiftAPI/Resources/README.md b/FirebaseRemoteConfig/Tests/SwiftAPI/Resources/README.md new file mode 100644 index 0000000000..ad61b30ff3 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/SwiftAPI/Resources/README.md @@ -0,0 +1,54 @@ +# Remote Config Console API + +[`RemoteConfigConsole.swift`](https://github.com/firebase/firebase-ios-sdk/blob/master/FirebaseRemoteConfig/Tests/SwiftAPI/RemoteConfigConsole.swift) +provides a simple API for interacting with an app's Remote Config on the +Firebase console. + +## Local Development Setup +1. Create a Firebase project on the Firebase Console and download +the `GoogleService-Info.plist`. +2. Navigate to your project's settings. Click on the **Service accounts** tab and +then download a private key by clicking the blue button that says "Generate new private key". +Rename it `ServiceAccount.json`. +3. Within the `firebase-ios-sdk`, run: +```bash +./scripts/generate_access_token.sh local_dev PATH/TO/ServiceAccount.json FirebaseRemoteConfig/Tests/SwiftAPI/AccessToken.json +``` +4. Generate the `FirebaseRemoteConfig` project: +```bash +pod gen FirebaseRemoteConfig.podspec --local-sources=./ --auto-open --platforms=ios +``` +5. Copy the `GoogleService-Info.plist` you downloaded earlier into the generated +Xcode project. + +🚀 Everything is ready to go! Run the tests in the `swift-api-tests` target. + + +## How it works + +While the `RemoteConfigConsole` API basically just makes simple network calls, +we need to include an `access token` so our requests do the proper "handshake" with the Firebase console. + +### Firebase Service Account Private Key +This private key is needed to create an access token with the valid parameters +that authorizes our requests to programmatically make changes to remote config on the Firebase console. + +The private key can be located on the Firebase console and navigate to your project's settings. To download, +click on the **Service accounts** tab and and then generate the private key by clicking +the blue button that says "Generate new private key". + +### Create the Access Token +We use Google's [Auth Library for Swift](https://github.com/googleapis/google-auth-library-swift) +to generate the access token. There are a few example use cases provided. We use the +[`TokenSource`](https://github.com/googleapis/google-auth-library-swift/blob/master/Sources/Examples/TokenSource/main.swift) +example. + +For the access token to be generated, the `GOOGLE_APPLICATION_CREDENTIALS` env var should be set to point to where the +Firebase project's service account key is stored. This is set in the +[`generate_access_token.sh`](https://github.com/firebase/firebase-ios-sdk/blob/master/scripts/generate_access_token.sh) +script. + +### Remote Config API Tests +There is a [section](https://github.com/firebase/firebase-ios-sdk/blob/master/FirebaseRemoteConfig/Tests/SwiftAPI/APITests.swift#L210) +of tests in [`APITests.swift`](https://github.com/firebase/firebase-ios-sdk/blob/master/FirebaseRemoteConfig/Tests/SwiftAPI/APITests.swift) +showcasing the `RemoteConfigConsole` in action. diff --git a/scripts/generate_access_token.sh b/scripts/generate_access_token.sh new file mode 100755 index 0000000000..6bc791b43c --- /dev/null +++ b/scripts/generate_access_token.sh @@ -0,0 +1,94 @@ +#!/bin/bash + +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This script generates access tokens that are needed to make admin API +# calls to the Firebase Console. +# +# The script takes three arguments: +# - GHA_SECRET: The password for decrypting GitHub secrets. +# - SERVICE_ACCOUNT: The path to the encrypted service account secret. +# - OUTPUT: The path to file where generated access token will be stored. +# +# This script uses Google's Swift Auth Client Library. +# - https://github.com/googleapis/google-auth-library-swift +# +# Generated tokens are `JSON` in the form: +# { +# "token_type":"Bearer", +# "expires_in":3599, +# "access_token":"1234567890ABCDEFG" +# } + +GHA_SECRET="$1" # Pass in `local_dev` if the SERVICE_ACCOUNT does not to be decrypted. +SERVICE_ACCOUNT="$2" +OUTPUT="$3" + +echo "GHA_SECRET: ***" +echo "SERVICE_ACCOUNT: ${SERVICE_ACCOUNT}" +echo "OUTPUT: ${OUTPUT}" + +if [[ ! -f $SERVICE_ACCOUNT ]]; then + echo ERROR: Cannot find encrypted secret at $SERVICE_ACCOUNT, aborting. + exit 1 +fi + +if [[ ! -f $OUTPUT ]]; then + echo ERROR: Cannot find $OUTPUT, aborting. + exit 1 +fi + +# The access token is generated using a downloaded Service Account JSON file from a +# Firebase Project. This can be downloaded from Firebase console under 'Project Settings'. +# +# The following stores the decrypted service account JSON file in `$HOME/.credentials/` and points +# the GOOGLE_APPLICATION_CREDENTIALS env var to it. +SERVICE_ACCOUNT_FILE=$(basename $SERVICE_ACCOUNT .gpg) + +echo "Creating ~/.credentials/ directory" +mkdir -p ~/.credentials/ + +if [[ $GHA_SECRET == "local_dev" ]]; then + echo "Local Development Mode" + echo "Copying ${SERVICE_ACCOUNT_FILE} to ~/.credentials/" + cp $SERVICE_ACCOUNT ~/.credentials/ +else + echo "Decrypting ${SERVICE_ACCOUNT_FILE}.gpg" + scripts/decrypt_GHA_SECRET.sh $SERVICE_ACCOUNT ~/.credentials/$SERVICE_ACCOUNT_FILE "$plist_secret" +fi + +echo "::set-env name=GOOGLE_APPLICATION_CREDENTIALS::${HOME}/.credentials/${SERVICE_ACCOUNT_FILE}" +export GOOGLE_APPLICATION_CREDENTIALS="${HOME}/.credentials/${SERVICE_ACCOUNT_FILE}" + +# Clone Google's Swift Auth Client Library and use it to generate a token. +# The generated token is piped to the specified OUTPUT file. +git clone https://github.com/googleapis/google-auth-library-swift.git +cd google-auth-library-swift +git checkout --quiet 7b1c9cd4ffd8cb784bcd8b7fd599794b69a810cf # Working main branch as of 7/9/20. +make -f Makefile + +# Prepend OUTPUT path with ../ since we cd'd into `google-auth-library-swift`. +swift run TokenSource > ../$OUTPUT + +# Remove cloned Swift Auth Client Library. +cd .. +rm -rf google-auth-library-swift + +if grep -q "access_token" $OUTPUT; then + echo "Token successfully generated and placed at ${OUTPUT}" +else + echo "ERROR: $(cat $OUTPUT)" + exit 1 +fi diff --git a/scripts/gha-encrypted/RemoteConfigSwiftAPI/ServiceAccount.json.gpg b/scripts/gha-encrypted/RemoteConfigSwiftAPI/ServiceAccount.json.gpg new file mode 100644 index 0000000000..19b54e4f47 Binary files /dev/null and b/scripts/gha-encrypted/RemoteConfigSwiftAPI/ServiceAccount.json.gpg differ