diff --git a/src/remote-config/remote-config-api-client-internal.ts b/src/remote-config/remote-config-api-client-internal.ts index b8cfe22fc4..6331eaa1b1 100644 --- a/src/remote-config/remote-config-api-client-internal.ts +++ b/src/remote-config/remote-config-api-client-internal.ts @@ -21,10 +21,19 @@ import { PrefixedFirebaseError } from '../utils/error'; import * as utils from '../utils/index'; import * as validator from '../utils/validator'; import { deepCopy } from '../utils/deep-copy'; -import { ListVersionsOptions, ListVersionsResult, RemoteConfigTemplate } from './remote-config-api'; +import { + ListVersionsOptions, + ListVersionsResult, + RemoteConfigTemplate, + RemoteConfigServerTemplateData +} from './remote-config-api'; // Remote Config backend constants -const FIREBASE_REMOTE_CONFIG_V1_API = 'https://firebaseremoteconfig.googleapis.com/v1'; +/** + * Allows the `FIREBASE_REMOTE_CONFIG_URL_BASE` environment + * variable to override the default API endpoint URL. + */ +const FIREBASE_REMOTE_CONFIG_URL_BASE = process.env.FIREBASE_REMOTE_CONFIG_URL_BASE || 'https://firebaseremoteconfig.googleapis.com'; const FIREBASE_REMOTE_CONFIG_HEADERS = { 'X-Firebase-Client': `fire-admin-node/${utils.getSdkVersion()}`, // There is a known issue in which the ETag is not properly returned in cases where the request @@ -166,6 +175,24 @@ export class RemoteConfigApiClient { }); } + public getServerTemplate(): Promise { + return this.getUrl() + .then((url) => { + const request: HttpRequestConfig = { + method: 'GET', + url: `${url}/namespaces/firebase-server/serverRemoteConfig`, + headers: FIREBASE_REMOTE_CONFIG_HEADERS + }; + return this.httpClient.send(request); + }) + .then((resp) => { + return this.toRemoteConfigServerTemplate(resp); + }) + .catch((err) => { + throw this.toFirebaseError(err); + }); + } + private sendPutRequest(template: RemoteConfigTemplate, etag: string, validateOnly?: boolean): Promise { let path = 'remoteConfig'; if (validateOnly) { @@ -191,7 +218,7 @@ export class RemoteConfigApiClient { private getUrl(): Promise { return this.getProjectIdPrefix() .then((projectIdPrefix) => { - return `${FIREBASE_REMOTE_CONFIG_V1_API}/${projectIdPrefix}`; + return `${FIREBASE_REMOTE_CONFIG_URL_BASE}/v1/${projectIdPrefix}`; }); } @@ -255,6 +282,24 @@ export class RemoteConfigApiClient { }; } + /** + * Creates a RemoteConfigServerTemplate from the API response. + * If provided, customEtag is used instead of the etag returned in the API response. + * + * @param {HttpResponse} resp API response object. + * @param {string} customEtag A custom etag to replace the etag fom the API response (Optional). + */ + private toRemoteConfigServerTemplate(resp: HttpResponse, customEtag?: string): RemoteConfigServerTemplateData { + const etag = (typeof customEtag === 'undefined') ? resp.headers['etag'] : customEtag; + this.validateEtag(etag); + return { + conditions: resp.data.conditions, + parameters: resp.data.parameters, + etag, + version: resp.data.version, + }; + } + /** * Checks if the given RemoteConfigTemplate object is valid. * The object must have valid parameters, parameter groups, conditions, and an etag. diff --git a/src/remote-config/remote-config-api.ts b/src/remote-config/remote-config-api.ts index dd9f641034..a27e29c81a 100644 --- a/src/remote-config/remote-config-api.ts +++ b/src/remote-config/remote-config-api.ts @@ -55,7 +55,7 @@ export interface RemoteConfigCondition { } /** - * Interface representing a Remote Config condition in the data-plane. + * Represents a Remote Config condition in the dataplane. * A condition targets a specific group of users. A list of these conditions make up * part of a Remote Config template. */ @@ -156,7 +156,7 @@ export interface RemoteConfigParameterGroup { } /** - * Interface representing a Remote Config client template. + * Represents a Remote Config client template. */ export interface RemoteConfigTemplate { /** @@ -189,7 +189,7 @@ export interface RemoteConfigTemplate { } /** - * Interface representing the data in a Remote Config server template. + * Represents the data in a Remote Config server template. */ export interface RemoteConfigServerTemplateData { /** @@ -203,7 +203,7 @@ export interface RemoteConfigServerTemplateData { parameters: { [key: string]: RemoteConfigParameter }; /** - * ETag of the current Remote Config template (readonly). + * Current Remote Config template ETag (read-only). */ readonly etag: string; @@ -214,28 +214,28 @@ export interface RemoteConfigServerTemplateData { } /** - * Interface representing a stateful abstraction for a Remote Config server template. + * Represents a stateful abstraction for a Remote Config server template. */ export interface RemoteConfigServerTemplate { /** - * Cached {@link RemoteConfigServerTemplateData} + * Cached {@link RemoteConfigServerTemplateData}. */ cache: RemoteConfigServerTemplateData; /** - * A {@link RemoteConfigServerConfig} containing default values for Config + * A {@link RemoteConfigServerConfig} that contains default Config values. */ defaultConfig: RemoteConfigServerConfig; /** - * Evaluates the current template to produce a {@link RemoteConfigServerConfig} + * Evaluates the current template to produce a {@link RemoteConfigServerConfig}. */ evaluate(): RemoteConfigServerConfig; /** * Fetches and caches the current active version of the - * {@link RemoteConfigServerTemplate} of the project. + * project's {@link RemoteConfigServerTemplate}. */ load(): Promise; } @@ -364,6 +364,6 @@ export interface ListVersionsOptions { } /** - * Type representing the configuration produced by evaluating a server template. + * Represents the configuration produced by evaluating a server template. */ export type RemoteConfigServerConfig = { [key: string]: string | boolean | number } diff --git a/test/unit/remote-config/remote-config-api-client.spec.ts b/test/unit/remote-config/remote-config-api-client.spec.ts index 9c66f78a41..da2c87c639 100644 --- a/test/unit/remote-config/remote-config-api-client.spec.ts +++ b/test/unit/remote-config/remote-config-api-client.spec.ts @@ -33,6 +33,7 @@ import { getSdkVersion } from '../../../src/utils/index'; import { RemoteConfigTemplate, Version, ListVersionsResult, } from '../../../src/remote-config/index'; +import { RemoteConfigServerTemplateData } from '../../../src/remote-config/remote-config-api'; const expect = chai.expect; @@ -661,6 +662,36 @@ describe('RemoteConfigApiClient', () => { }); }); + describe('getServerTemplate', () => { + it('should reject when project id is not available', () => { + return clientWithoutProjectId.getServerTemplate() + .should.eventually.be.rejectedWith(noProjectId); + }); + + // tests for api response validations + runEtagHeaderTests(() => apiClient.getServerTemplate()); + runErrorResponseTests(() => apiClient.getServerTemplate()); + + it('should resolve with the latest template on success', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(TEST_RESPONSE, 200, { etag: 'etag-123456789012-1' })); + stubs.push(stub); + return apiClient.getServerTemplate() + .then((resp) => { + expect(resp.conditions).to.deep.equal(TEST_RESPONSE.conditions); + expect(resp.parameters).to.deep.equal(TEST_RESPONSE.parameters); + expect(resp.etag).to.equal('etag-123456789012-1'); + expect(resp.version).to.deep.equal(TEST_RESPONSE.version); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'GET', + url: 'https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/namespaces/firebase-server/serverRemoteConfig', + headers: EXPECTED_HEADERS, + }); + }); + }); + }); + function runTemplateVersionNumberTests(rcOperation: (v: string | number) => any): void { ['', null, NaN, true, [], {}].forEach((invalidVersion) => { it(`should reject if the versionNumber is: ${invalidVersion}`, () => { @@ -677,7 +708,7 @@ describe('RemoteConfigApiClient', () => { }); } - function runEtagHeaderTests(rcOperation: () => Promise): void { + function runEtagHeaderTests(rcOperation: () => Promise): void { it('should reject when the etag is not present in the response', () => { const stub = sinon .stub(HttpClient.prototype, 'send') @@ -690,7 +721,8 @@ describe('RemoteConfigApiClient', () => { }); } - function runErrorResponseTests(rcOperation: () => Promise): void { + function runErrorResponseTests( + rcOperation: () => Promise): void { it('should reject when a full platform error response is received', () => { const stub = sinon .stub(HttpClient.prototype, 'send')