diff --git a/etc/firebase-admin.remote-config.api.md b/etc/firebase-admin.remote-config.api.md index dc48662e8c..e81cc7fc37 100644 --- a/etc/firebase-admin.remote-config.api.md +++ b/etc/firebase-admin.remote-config.api.md @@ -199,15 +199,23 @@ export class RemoteConfig { // (undocumented) readonly app: App; createTemplateFromJSON(json: string): RemoteConfigTemplate; + getServerConfigTemplate(): Promise; + getServerConfigTemplateAtVersion(versionNumber: number | string): Promise; getServerTemplate(options?: GetServerTemplateOptions): Promise; getTemplate(): Promise; getTemplateAtVersion(versionNumber: number | string): Promise; initServerTemplate(options?: InitServerTemplateOptions): ServerTemplate; + listServerConfigVersions(options?: ListVersionsOptions): Promise; listVersions(options?: ListVersionsOptions): Promise; + publishServerConfigTemplate(template: RemoteConfigTemplate, options?: { + force: boolean; + }): Promise; publishTemplate(template: RemoteConfigTemplate, options?: { force: boolean; }): Promise; rollback(versionNumber: number | string): Promise; + rollbackServerConfigTemplate(versionNumber: number | string): Promise; + validateServerConfigTemplate(template: RemoteConfigTemplate): Promise; validateTemplate(template: RemoteConfigTemplate): Promise; } diff --git a/src/remote-config/remote-config-api-client-internal.ts b/src/remote-config/remote-config-api-client-internal.ts index 6eb254f2dc..e65db48e23 100644 --- a/src/remote-config/remote-config-api-client-internal.ts +++ b/src/remote-config/remote-config-api-client-internal.ts @@ -68,15 +68,40 @@ export class RemoteConfigApiClient { } public getTemplate(): Promise { - return this.getUrl() - .then((url) => { - const request: HttpRequestConfig = { - method: 'GET', - url: `${url}/remoteConfig`, - headers: FIREBASE_REMOTE_CONFIG_HEADERS - }; - return this.httpClient.send(request); + return this.getTemplateInternal('remoteConfig'); + } + + public getTemplateAtVersion(versionNumber: number | string): Promise { + return this.getTemplateInternal('remoteConfig', versionNumber); + } + + public validateTemplate(template: RemoteConfigTemplate): Promise { + template = this.validateInputRemoteConfigTemplate(template); + return this.sendPutRequest(template, template.etag, true) + .then((resp) => { + // validating a template returns an etag with the suffix -0 means that your update + // was successfully validated. We set the etag back to the original etag of the template + // to allow future operations. + this.validateEtag(resp.headers['etag']); + return this.toRemoteConfigTemplate(resp, template.etag); }) + .catch((err) => { + throw this.toFirebaseError(err); + }); + } + + public publishTemplate( + template: RemoteConfigTemplate, + options?: { force: boolean; } + ): Promise { + template = this.validateInputRemoteConfigTemplate(template); + let ifMatch: string = template.etag; + if (options && options.force === true) { + // setting `If-Match: *` forces the Remote Config template to be updated + // and circumvent the ETag, and the protection from that it provides. + ifMatch = '*'; + } + return this.sendPutRequest(template, ifMatch) .then((resp) => { return this.toRemoteConfigTemplate(resp); }) @@ -85,29 +110,43 @@ export class RemoteConfigApiClient { }); } - public getTemplateAtVersion(versionNumber: number | string): Promise { - const data = { versionNumber: this.validateVersionNumber(versionNumber) }; + public rollback(versionNumber: number | string): Promise { + return this.rollbackInternal('remoteConfig:rollback', versionNumber); + } + + public listVersions(options?: ListVersionsOptions): Promise { + return this.listVersionsInternal('remoteConfig:listVersions', options); + } + + public getServerTemplate(): Promise { return this.getUrl() .then((url) => { const request: HttpRequestConfig = { method: 'GET', - url: `${url}/remoteConfig`, - headers: FIREBASE_REMOTE_CONFIG_HEADERS, - data + url: `${url}/namespaces/firebase-server/serverRemoteConfig`, + headers: FIREBASE_REMOTE_CONFIG_HEADERS }; return this.httpClient.send(request); }) .then((resp) => { - return this.toRemoteConfigTemplate(resp); + return this.toRemoteConfigServerTemplate(resp); }) .catch((err) => { throw this.toFirebaseError(err); }); } - public validateTemplate(template: RemoteConfigTemplate): Promise { + public getServerConfigTemplate(): Promise { + return this.getTemplateInternal('namespaces/firebase-server/remoteConfig'); + } + + public getServerConfigTemplateAtVersion(versionNumber: number | string): Promise { + return this.getTemplateInternal('namespaces/firebase-server/remoteConfig', versionNumber); + } + + public validateServerConfigTemplate(template: RemoteConfigTemplate): Promise { template = this.validateInputRemoteConfigTemplate(template); - return this.sendPutRequest(template, template.etag, true) + return this.sendServerPutRequest(template, template.etag, true) .then((resp) => { // validating a template returns an etag with the suffix -0 means that your update // was successfully validated. We set the etag back to the original etag of the template @@ -120,7 +159,10 @@ export class RemoteConfigApiClient { }); } - public publishTemplate(template: RemoteConfigTemplate, options?: { force: boolean; }): Promise { + public publishServerConfigTemplate( + template: RemoteConfigTemplate, + options?: { force: boolean; } + ): Promise { template = this.validateInputRemoteConfigTemplate(template); let ifMatch: string = template.etag; if (options && options.force === true) { @@ -128,7 +170,7 @@ export class RemoteConfigApiClient { // and circumvent the ETag, and the protection from that it provides. ifMatch = '*'; } - return this.sendPutRequest(template, ifMatch) + return this.sendServerPutRequest(template, ifMatch) .then((resp) => { return this.toRemoteConfigTemplate(resp); }) @@ -137,16 +179,31 @@ export class RemoteConfigApiClient { }); } - public rollback(versionNumber: number | string): Promise { - const data = { versionNumber: this.validateVersionNumber(versionNumber) }; + public rollbackServerConfigTemplate(versionNumber: number | string): Promise { + return this.rollbackInternal('namespaces/firebase-server/remoteConfig:rollback', versionNumber); + } + + public listServerConfigVersions(options?: ListVersionsOptions): Promise { + return this.listVersionsInternal('namespaces/firebase-server/remoteConfig:listVersions', options); + } + + private getTemplateInternal( + path: string, + versionNumber?: number | string + ): Promise { + const data = typeof versionNumber !== 'undefined' + ? { versionNumber: this.validateVersionNumber(versionNumber) } + : undefined; return this.getUrl() .then((url) => { const request: HttpRequestConfig = { - method: 'POST', - url: `${url}/remoteConfig:rollback`, - headers: FIREBASE_REMOTE_CONFIG_HEADERS, - data + method: 'GET', + url: `${url}/${path}`, + headers: FIREBASE_REMOTE_CONFIG_HEADERS }; + if (typeof data !== 'undefined') { + request.data = data; + } return this.httpClient.send(request); }) .then((resp) => { @@ -157,52 +214,60 @@ export class RemoteConfigApiClient { }); } - public listVersions(options?: ListVersionsOptions): Promise { - if (typeof options !== 'undefined') { - options = this.validateListVersionsOptions(options); - } + private rollbackInternal( + path: string, + versionNumber: number | string + ): Promise { + const data = { versionNumber: this.validateVersionNumber(versionNumber) }; return this.getUrl() .then((url) => { const request: HttpRequestConfig = { - method: 'GET', - url: `${url}/remoteConfig:listVersions`, + method: 'POST', + url: `${url}/${path}`, headers: FIREBASE_REMOTE_CONFIG_HEADERS, - data: options + data }; return this.httpClient.send(request); }) .then((resp) => { - return resp.data; + return this.toRemoteConfigTemplate(resp); }) .catch((err) => { throw this.toFirebaseError(err); }); } - public getServerTemplate(): Promise { + private listVersionsInternal( + path: string, + options?: ListVersionsOptions + ): Promise { + if (typeof options !== 'undefined') { + options = this.validateListVersionsOptions(options); + } return this.getUrl() .then((url) => { const request: HttpRequestConfig = { method: 'GET', - url: `${url}/namespaces/firebase-server/serverRemoteConfig`, - headers: FIREBASE_REMOTE_CONFIG_HEADERS + url: `${url}/${path}`, + headers: FIREBASE_REMOTE_CONFIG_HEADERS, + data: options }; return this.httpClient.send(request); }) .then((resp) => { - return this.toRemoteConfigServerTemplate(resp); + return resp.data; }) .catch((err) => { throw this.toFirebaseError(err); }); } - private sendPutRequest( + private sendPutRequestInternal( + path: string, template: RemoteConfigTemplate, etag: string, validateOnly?: boolean ): Promise { - let path = 'remoteConfig'; if (validateOnly) { path += '?validate_only=true'; } @@ -223,6 +288,22 @@ export class RemoteConfigApiClient { }); } + private sendServerPutRequest( + template: RemoteConfigTemplate, + etag: string, + validateOnly?: boolean + ): Promise { + return this.sendPutRequestInternal('namespaces/firebase-server/remoteConfig', template, etag, validateOnly); + } + + private sendPutRequest( + template: RemoteConfigTemplate, + etag: string, + validateOnly?: boolean + ): Promise { + return this.sendPutRequestInternal('remoteConfig', template, etag, validateOnly); + } + private getUrl(): Promise { return this.getProjectIdPrefix() .then((projectIdPrefix) => { diff --git a/src/remote-config/remote-config.ts b/src/remote-config/remote-config.ts index 7e74fc9381..90731fc8c5 100644 --- a/src/remote-config/remote-config.ts +++ b/src/remote-config/remote-config.ts @@ -213,6 +213,95 @@ export class RemoteConfig { return template; } + + /** + * Gets the current active version of the server-side {@link RemoteConfigTemplate} of the project. + * + * @returns A promise that fulfills with a `RemoteConfigTemplate`. + */ + public getServerConfigTemplate(): Promise { + return this.client.getServerConfigTemplate() + .then((templateResponse) => { + return new RemoteConfigTemplateImpl(templateResponse); + }); + } + + /** + * Gets the requested version of the server-side {@link RemoteConfigTemplate} of the project. + * + * @param versionNumber - Version number of the Remote Config template to look up. + * + * @returns A promise that fulfills with a `RemoteConfigTemplate`. + */ + public getServerConfigTemplateAtVersion(versionNumber: number | string): Promise { + return this.client.getServerConfigTemplateAtVersion(versionNumber) + .then((templateResponse) => { + return new RemoteConfigTemplateImpl(templateResponse); + }); + } + + /** + * Validates a server-side {@link RemoteConfigTemplate}. + * + * @param template - The Remote Config template to be validated. + * @returns A promise that fulfills with the validated `RemoteConfigTemplate`. + */ + public validateServerConfigTemplate(template: RemoteConfigTemplate): Promise { + return this.client.validateServerConfigTemplate(template) + .then((templateResponse) => { + return new RemoteConfigTemplateImpl(templateResponse); + }); + } + + /** + * Publishes a server-side Remote Config template. + * + * @param template - The Remote Config template to be published. + * @param options - Optional options object when publishing a Remote Config template: + * - `force`: Setting this to `true` forces the Remote Config template to + * be updated and circumvent the ETag. + * + * @returns A Promise that fulfills with the published `RemoteConfigTemplate`. + */ + public publishServerConfigTemplate( + template: RemoteConfigTemplate, + options?: { force: boolean } + ): Promise { + return this.client.publishServerConfigTemplate(template, options) + .then((templateResponse) => { + return new RemoteConfigTemplateImpl(templateResponse); + }); + } + + /** + * Rolls back a project's published server-side Remote Config template to the specified version. + * + * @param versionNumber - The version number of the Remote Config template to roll back to. + * @returns A promise that fulfills with the published `RemoteConfigTemplate`. + */ + public rollbackServerConfigTemplate(versionNumber: number | string): Promise { + return this.client.rollbackServerConfigTemplate(versionNumber) + .then((templateResponse) => { + return new RemoteConfigTemplateImpl(templateResponse); + }); + } + + /** + * Gets a list of server-side Remote Config template versions that have been published, sorted in reverse + * chronological order. + * + * @param options - Optional options object for getting a list of versions. + * @returns A promise that fulfills with a `ListVersionsResult`. + */ + public listServerConfigVersions(options?: ListVersionsOptions): Promise { + return this.client.listServerConfigVersions(options) + .then((listVersionsResponse) => { + return { + versions: listVersionsResponse.versions?.map(version => new VersionImpl(version)) ?? [], + nextPageToken: listVersionsResponse.nextPageToken, + }; + }); + } } /** 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 f515a76130..fe67982382 100644 --- a/test/unit/remote-config/remote-config-api-client.spec.ts +++ b/test/unit/remote-config/remote-config-api-client.spec.ts @@ -708,6 +708,257 @@ describe('RemoteConfigApiClient', () => { }); }); + describe('getServerConfigTemplate', () => { + it('should reject when project id is not available', () => { + return clientWithoutProjectId.getServerConfigTemplate() + .should.eventually.be.rejectedWith(noProjectId); + }); + + // tests for api response validations + runEtagHeaderTests(() => apiClient.getServerConfigTemplate()); + runErrorResponseTests(() => apiClient.getServerConfigTemplate()); + + 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.getServerConfigTemplate() + .then((resp) => { + expect(resp.conditions).to.deep.equal(TEST_RESPONSE.conditions); + expect(resp.parameters).to.deep.equal(TEST_RESPONSE.parameters); + expect(resp.parameterGroups).to.deep.equal(TEST_RESPONSE.parameterGroups); + 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/remoteConfig', + headers: EXPECTED_HEADERS, + }); + }); + }); + }); + + describe('getServerConfigTemplateAtVersion', () => { + it('should reject when project id is not available', () => { + return clientWithoutProjectId.getServerConfigTemplateAtVersion(65) + .should.eventually.be.rejectedWith(noProjectId); + }); + + // test for version number validations + runTemplateVersionNumberTests((v: string | number) => { apiClient.getServerConfigTemplateAtVersion(v); }); + + // tests for api response validations + runEtagHeaderTests(() => apiClient.getServerConfigTemplateAtVersion(65)); + runErrorResponseTests(() => apiClient.getServerConfigTemplateAtVersion(65)); + + it('should convert version number to string', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(TEST_RESPONSE, 200, { etag: 'etag-123456789012-60' })); + stubs.push(stub); + return apiClient.getServerConfigTemplateAtVersion(60) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'GET', + url: 'https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/namespaces/firebase-server/remoteConfig', + headers: EXPECTED_HEADERS, + data: { versionNumber: '60' }, + }); + }); + }); + + it('should resolve with the requested template version on success', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(TEST_RESPONSE, 200, { etag: 'etag-123456789012-60' })); + stubs.push(stub); + return apiClient.getServerConfigTemplateAtVersion('60') + .then((resp) => { + expect(resp.conditions).to.deep.equal(TEST_RESPONSE.conditions); + expect(resp.parameters).to.deep.equal(TEST_RESPONSE.parameters); + expect(resp.parameterGroups).to.deep.equal(TEST_RESPONSE.parameterGroups); + expect(resp.etag).to.equal('etag-123456789012-60'); + 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/remoteConfig', + headers: EXPECTED_HEADERS, + data: { versionNumber: '60' }, + }); + }); + }); + }); + + describe('validateServerConfigTemplate', () => { + it('should reject when project id is not available', () => { + return clientWithoutProjectId.validateServerConfigTemplate(REMOTE_CONFIG_TEMPLATE) + .should.eventually.be.rejectedWith(noProjectId); + }); + + // tests for input template validations + testInvalidInputTemplates((t: RemoteConfigTemplate) => apiClient.validateServerConfigTemplate(t)); + + // tests for api response validations + runEtagHeaderTests(() => apiClient.validateServerConfigTemplate(REMOTE_CONFIG_TEMPLATE)); + runErrorResponseTests(() => apiClient.validateServerConfigTemplate(REMOTE_CONFIG_TEMPLATE)); + + it('should exclude output only parameters from version metadata', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(TEST_RESPONSE, 200, { etag: 'etag-123456789012-0' })); + stubs.push(stub); + const templateCopy = deepCopy(REMOTE_CONFIG_TEMPLATE); + templateCopy.version = VERSION_INFO; + return apiClient.validateServerConfigTemplate(templateCopy) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'PUT', + url: 'https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/namespaces/firebase-server/remoteConfig?validate_only=true', + headers: { ...EXPECTED_HEADERS, 'If-Match': REMOTE_CONFIG_TEMPLATE.etag }, + data: { + conditions: REMOTE_CONFIG_TEMPLATE.conditions, + parameters: REMOTE_CONFIG_TEMPLATE.parameters, + parameterGroups: REMOTE_CONFIG_TEMPLATE.parameterGroups, + version: { description: VERSION_INFO.description }, + } + }); + }); + }); + }); + + describe('publishServerConfigTemplate', () => { + it('should reject when project id is not available', () => { + return clientWithoutProjectId.publishServerConfigTemplate(REMOTE_CONFIG_TEMPLATE) + .should.eventually.be.rejectedWith(noProjectId); + }); + + // tests for input template validations + testInvalidInputTemplates((t: RemoteConfigTemplate) => apiClient.publishServerConfigTemplate(t)); + + // tests for api response validations + runEtagHeaderTests(() => apiClient.publishServerConfigTemplate(REMOTE_CONFIG_TEMPLATE)); + runErrorResponseTests(() => apiClient.publishServerConfigTemplate(REMOTE_CONFIG_TEMPLATE)); + + it('should publish the requested template with ETag on success', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(TEST_RESPONSE, 200, { etag: 'etag-123456789012-10' })); + stubs.push(stub); + const templateCopy = deepCopy(REMOTE_CONFIG_TEMPLATE); + templateCopy.version = VERSION_INFO; + return apiClient.publishServerConfigTemplate(templateCopy) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'PUT', + url: 'https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/namespaces/firebase-server/remoteConfig', + headers: { ...EXPECTED_HEADERS, 'If-Match': REMOTE_CONFIG_TEMPLATE.etag }, + data: { + conditions: REMOTE_CONFIG_TEMPLATE.conditions, + parameters: REMOTE_CONFIG_TEMPLATE.parameters, + parameterGroups: REMOTE_CONFIG_TEMPLATE.parameterGroups, + version: { description: VERSION_INFO.description }, + } + }); + }); + }); + + it('should force publish the requested template when force option is true', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(TEST_RESPONSE, 200, { etag: 'etag-123456789012-10' })); + stubs.push(stub); + const templateCopy = deepCopy(REMOTE_CONFIG_TEMPLATE); + templateCopy.version = VERSION_INFO; + return apiClient.publishServerConfigTemplate(templateCopy, { force: true }) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'PUT', + url: 'https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/namespaces/firebase-server/remoteConfig', + headers: { ...EXPECTED_HEADERS, 'If-Match': '*' }, + data: { + conditions: REMOTE_CONFIG_TEMPLATE.conditions, + parameters: REMOTE_CONFIG_TEMPLATE.parameters, + parameterGroups: REMOTE_CONFIG_TEMPLATE.parameterGroups, + version: { description: VERSION_INFO.description }, + } + }); + }); + }); + }); + + describe('rollbackServerConfigTemplate', () => { + it('should reject when project id is not available', () => { + return clientWithoutProjectId.rollbackServerConfigTemplate(65) + .should.eventually.be.rejectedWith(noProjectId); + }); + + // test for version number validations + runTemplateVersionNumberTests((v: string | number) => { apiClient.rollbackServerConfigTemplate(v); }); + + // tests for api response validations + runEtagHeaderTests(() => apiClient.rollbackServerConfigTemplate(65)); + runErrorResponseTests(() => apiClient.rollbackServerConfigTemplate(65)); + + it('should convert version number to string', () => { + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(TEST_RESPONSE, 200, { etag: 'etag-123456789012-60' })); + stubs.push(stub); + return apiClient.rollbackServerConfigTemplate(60) + .then(() => { + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'POST', + url: 'https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/namespaces/firebase-server/remoteConfig:rollback', + headers: EXPECTED_HEADERS, + data: { versionNumber: '60' }, + }); + }); + }); + }); + + describe('listServerConfigVersions', () => { + it('should reject when project id is not available', () => { + return clientWithoutProjectId.listServerConfigVersions() + .should.eventually.be.rejectedWith(noProjectId); + }); + + // tests for api response validations + runErrorResponseTests(() => apiClient.listServerConfigVersions()); + + it('should resolve with a list of template versions on success', () => { + const startTime = new Date(2020, 4, 2); + const endTime = 'Thu, 07 May 2020 18:44:41 GMT'; + const stub = sinon + .stub(HttpClient.prototype, 'send') + .resolves(utils.responseFrom(TEST_VERSIONS_RESULT, 200)); + stubs.push(stub); + return apiClient.listServerConfigVersions({ + pageSize: 2, + pageToken: '70', + endVersionNumber: '78', + startTime: startTime, + endTime: endTime, + }) + .then((resp) => { + expect(resp.versions).to.deep.equal(TEST_VERSIONS_RESULT.versions); + expect(resp.nextPageToken).to.equal(TEST_VERSIONS_RESULT.nextPageToken); + expect(stub).to.have.been.calledOnce.and.calledWith({ + method: 'GET', + url: 'https://firebaseremoteconfig.googleapis.com/v1/projects/test-project/namespaces/firebase-server/remoteConfig:listVersions', + headers: EXPECTED_HEADERS, + data: { + pageSize: 2, + pageToken: '70', + endVersionNumber: '78', + startTime: startTime.toISOString(), + endTime: new Date(endTime).toISOString(), + } + }); + }); + }); + }); + function runTemplateVersionNumberTests(rcOperation: (v: string | number) => any): void { ['', null, NaN, true, [], {}].forEach((invalidVersion) => { it(`should reject if the versionNumber is: ${invalidVersion}`, () => { diff --git a/test/unit/remote-config/remote-config.spec.ts b/test/unit/remote-config/remote-config.spec.ts index ffd65f79de..4084310456 100644 --- a/test/unit/remote-config/remote-config.spec.ts +++ b/test/unit/remote-config/remote-config.spec.ts @@ -544,6 +544,109 @@ describe('RemoteConfig', () => { }); }); + describe('getServerConfigTemplate', () => { + runInvalidResponseTests(() => remoteConfig.getServerConfigTemplate(), + 'getServerConfigTemplate'); + runValidResponseTests(() => remoteConfig.getServerConfigTemplate(), + 'getServerConfigTemplate'); + }); + + describe('getServerConfigTemplateAtVersion', () => { + runInvalidResponseTests(() => remoteConfig.getServerConfigTemplateAtVersion(65), + 'getServerConfigTemplateAtVersion'); + runValidResponseTests(() => remoteConfig.getServerConfigTemplateAtVersion(65), + 'getServerConfigTemplateAtVersion'); + }); + + describe('validateServerConfigTemplate', () => { + runInvalidResponseTests(() => remoteConfig.validateServerConfigTemplate(REMOTE_CONFIG_TEMPLATE), + 'validateServerConfigTemplate'); + runValidResponseTests(() => remoteConfig.validateServerConfigTemplate(REMOTE_CONFIG_TEMPLATE), + 'validateServerConfigTemplate'); + }); + + describe('publishServerConfigTemplate', () => { + runInvalidResponseTests(() => remoteConfig.publishServerConfigTemplate(REMOTE_CONFIG_TEMPLATE), + 'publishServerConfigTemplate'); + runValidResponseTests(() => remoteConfig.publishServerConfigTemplate(REMOTE_CONFIG_TEMPLATE), + 'publishServerConfigTemplate'); + }); + + describe('rollbackServerConfigTemplate', () => { + runInvalidResponseTests(() => remoteConfig.rollbackServerConfigTemplate('5'), 'rollbackServerConfigTemplate'); + runValidResponseTests(() => remoteConfig.rollbackServerConfigTemplate('5'), 'rollbackServerConfigTemplate'); + }); + + describe('listServerConfigVersions', () => { + it('should propagate API errors', () => { + const stub = sinon + .stub(RemoteConfigApiClient.prototype, 'listServerConfigVersions') + .rejects(INTERNAL_ERROR); + stubs.push(stub); + return remoteConfig.listServerConfigVersions() + .should.eventually.be.rejected.and.deep.equal(INTERNAL_ERROR); + }); + + ['', null, NaN, true, [], {}].forEach((invalidVersion) => { + it(`should reject if the versionNumber is: ${invalidVersion}`, () => { + const response = deepCopy(REMOTE_CONFIG_LIST_VERSIONS_RESULT); + response.versions[0].versionNumber = invalidVersion as any; + const stub = sinon + .stub(RemoteConfigApiClient.prototype, 'listServerConfigVersions') + .resolves(response); + stubs.push(stub); + return remoteConfig.listServerConfigVersions() + .should.eventually.be.rejected + .and.to.match(/^Error: Version number must be a non-empty string in int64 format or a number$/); + }); + }); + + ['abc', 'a123b', 'a123', '123a', 1.2, '70.2'].forEach((invalidVersion) => { + it(`should reject if the versionNumber is: ${invalidVersion}`, () => { + const response = deepCopy(REMOTE_CONFIG_LIST_VERSIONS_RESULT); + response.versions[0].versionNumber = invalidVersion as any; + const stub = sinon + .stub(RemoteConfigApiClient.prototype, 'listServerConfigVersions') + .resolves(response); + stubs.push(stub); + return remoteConfig.listServerConfigVersions() + .should.eventually.be.rejected + .and.to.match(/^Error: Version number must be an integer or a string in int64 format$/); + }); + }); + + it('should resolve with an empty versions list if no results are available for requested list options', () => { + const stub = sinon + .stub(RemoteConfigApiClient.prototype, 'listServerConfigVersions') + .resolves({} as any); + stubs.push(stub); + return remoteConfig.listServerConfigVersions({ + pageSize: 2, + endVersionNumber: 10, + }) + .then((response) => { + expect(response.versions.length).to.equal(0); + expect(response.nextPageToken).to.be.undefined; + }); + }); + + it('should resolve with template versions list on success', () => { + const stub = sinon + .stub(RemoteConfigApiClient.prototype, 'listServerConfigVersions') + .resolves(REMOTE_CONFIG_LIST_VERSIONS_RESULT); + stubs.push(stub); + return remoteConfig.listServerConfigVersions({ + pageSize: 2 + }) + .then((response) => { + expect(response.versions.length).to.equal(2); + expect(response.versions[0].updateTime).equals('Thu, 07 May 2020 18:46:09 GMT'); + expect(response.versions[1].updateTime).equals('Thu, 07 May 2020 18:44:41 GMT'); + expect(response.nextPageToken).to.equal('76'); + }); + }); + }); + const INVALID_PARAMETERS: any[] = [null, '', 'abc', 1, true, []]; const INVALID_PARAMETER_GROUPS: any[] = [null, '', 'abc', 1, true, []]; const INVALID_CONDITIONS: any[] = [null, '', 'abc', 1, true, {}];