Skip to content

Commit

Permalink
fix: Add update or create release
Browse files Browse the repository at this point in the history
  • Loading branch information
lahirumaramba committed Jun 23, 2022
1 parent c13aab5 commit 263a88c
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 1 deletion.
25 changes: 25 additions & 0 deletions src/security-rules/security-rules-api-client-internal.ts
Expand Up @@ -163,6 +163,16 @@ export class SecurityRulesApiClient {
return this.getResource<Release>(`releases/${name}`);
}

public updateOrCreateRelease(name: string, rulesetName: string): Promise<Release> {
return this.updateRelease(name, rulesetName).catch((error) => {
// if ruleset update failed with a NOT_FOUND error, attempt to create instead.
if (error.code === `security-rules/${ERROR_CODE_MAPPING.NOT_FOUND}`) {
return this.createRelease(name, rulesetName);
}
throw error;
});
}

public updateRelease(name: string, rulesetName: string): Promise<Release> {
return this.getUrl()
.then((url) => {
Expand All @@ -178,6 +188,21 @@ export class SecurityRulesApiClient {
});
}

public createRelease(name: string, rulesetName: string): Promise<Release> {
return this.getUrl()
.then((url) => {
return this.getReleaseDescription(name, rulesetName)
.then((release) => {
const request: HttpRequestConfig = {
method: 'POST',
url: `${url}/releases`,
data: release,
};
return this.sendRequest<Release>(request);
});
});
}

private getUrl(): Promise<string> {
return this.getProjectIdPrefix()
.then((projectIdPrefix) => {
Expand Down
2 changes: 1 addition & 1 deletion src/security-rules/security-rules.ts
Expand Up @@ -378,7 +378,7 @@ export class SecurityRules {
}

const rulesetName = validator.isString(ruleset) ? ruleset : ruleset.name;
return this.client.updateRelease(releaseName, rulesetName)
return this.client.updateOrCreateRelease(releaseName, rulesetName)
.then(() => {
return;
});
Expand Down
97 changes: 97 additions & 0 deletions test/unit/security-rules/security-rules-api-client.spec.ts
Expand Up @@ -490,6 +490,35 @@ describe('SecurityRulesApiClient', () => {
});
});

describe('updateOrCreateRelease', () => {
it('should propagate API errors', () => {
const EXPECTED_ERROR = new FirebaseSecurityRulesError('internal-error', 'message');
const stub = sinon
.stub(SecurityRulesApiClient.prototype, 'updateRelease')
.rejects(EXPECTED_ERROR);
stubs.push(stub);
return apiClient.updateOrCreateRelease(RELEASE_NAME, RULESET_NAME)
.should.eventually.be.rejected.and.deep.include(EXPECTED_ERROR);
});

it('should create a new ruleset when update fails with a not-found error', () => {
const NOT_FOUND_ERROR = new FirebaseSecurityRulesError('not-found', 'message');
const updateRelease = sinon
.stub(SecurityRulesApiClient.prototype, 'updateRelease')
.rejects(NOT_FOUND_ERROR);
const createRelease = sinon
.stub(SecurityRulesApiClient.prototype, 'createRelease')
.resolves();
stubs.push(updateRelease, createRelease);

return apiClient.updateOrCreateRelease(RELEASE_NAME, RULESET_NAME)
.then(() => {
expect(updateRelease).to.have.been.calledOnce.and.calledWith(RELEASE_NAME, RULESET_NAME);
expect(createRelease).to.have.been.called.calledOnce.and.calledWith(RELEASE_NAME, RULESET_NAME);
});
});
});

describe('updateRelease', () => {
it('should reject when project id is not available', () => {
return clientWithoutProjectId.updateRelease(RELEASE_NAME, RULESET_NAME)
Expand Down Expand Up @@ -560,6 +589,74 @@ describe('SecurityRulesApiClient', () => {
});
});

describe('createRelease', () => {
it('should reject when project id is not available', () => {
return clientWithoutProjectId.createRelease(RELEASE_NAME, RULESET_NAME)
.should.eventually.be.rejectedWith(noProjectId);
});

it('should resolve with the created release on success', () => {
const stub = sinon
.stub(HttpClient.prototype, 'send')
.resolves(utils.responseFrom({ name: 'bar' }));
stubs.push(stub);
return apiClient.createRelease(RELEASE_NAME, RULESET_NAME)
.then((resp) => {
expect(resp.name).to.equal('bar');
expect(stub).to.have.been.calledOnce.and.calledWith({
method: 'POST',
url: 'https://firebaserules.googleapis.com/v1/projects/test-project/releases',
data: {
name: 'projects/test-project/releases/test.service',
rulesetName: 'projects/test-project/rulesets/ruleset-id',
},
headers: EXPECTED_HEADERS,
});
});
});

it('should throw when a full platform error response is received', () => {
const stub = sinon
.stub(HttpClient.prototype, 'send')
.rejects(utils.errorFrom(ERROR_RESPONSE, 404));
stubs.push(stub);
const expected = new FirebaseSecurityRulesError('not-found', 'Requested entity not found');
return apiClient.createRelease(RELEASE_NAME, RULESET_NAME)
.should.eventually.be.rejected.and.deep.include(expected);
});

it('should throw unknown-error when error code is not present', () => {
const stub = sinon
.stub(HttpClient.prototype, 'send')
.rejects(utils.errorFrom({}, 404));
stubs.push(stub);
const expected = new FirebaseSecurityRulesError('unknown-error', 'Unknown server error: {}');
return apiClient.createRelease(RELEASE_NAME, RULESET_NAME)
.should.eventually.be.rejected.and.deep.include(expected);
});

it('should throw unknown-error for non-json response', () => {
const stub = sinon
.stub(HttpClient.prototype, 'send')
.rejects(utils.errorFrom('not json', 404));
stubs.push(stub);
const expected = new FirebaseSecurityRulesError(
'unknown-error', 'Unexpected response with status: 404 and body: not json');
return apiClient.createRelease(RELEASE_NAME, RULESET_NAME)
.should.eventually.be.rejected.and.deep.include(expected);
});

it('should throw when rejected with a FirebaseAppError', () => {
const expected = new FirebaseAppError('network-error', 'socket hang up');
const stub = sinon
.stub(HttpClient.prototype, 'send')
.rejects(expected);
stubs.push(stub);
return apiClient.createRelease(RELEASE_NAME, RULESET_NAME)
.should.eventually.be.rejected.and.deep.include(expected);
});
});

describe('deleteRuleset', () => {
const INVALID_NAMES: any[] = [null, undefined, '', 1, true, {}, []];
INVALID_NAMES.forEach((invalidName) => {
Expand Down

0 comments on commit 263a88c

Please sign in to comment.