Skip to content

Commit 12108a9

Browse files
fix: Add update or create release (#1779)
If updating the release fails with a NOT_FOUND error, create a new release instead. Fixes: #1198
1 parent cfea847 commit 12108a9

File tree

3 files changed

+123
-1
lines changed

3 files changed

+123
-1
lines changed

src/security-rules/security-rules-api-client-internal.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,16 @@ export class SecurityRulesApiClient {
163163
return this.getResource<Release>(`releases/${name}`);
164164
}
165165

166+
public updateOrCreateRelease(name: string, rulesetName: string): Promise<Release> {
167+
return this.updateRelease(name, rulesetName).catch((error) => {
168+
// if ruleset update failed with a NOT_FOUND error, attempt to create instead.
169+
if (error.code === `security-rules/${ERROR_CODE_MAPPING.NOT_FOUND}`) {
170+
return this.createRelease(name, rulesetName);
171+
}
172+
throw error;
173+
});
174+
}
175+
166176
public updateRelease(name: string, rulesetName: string): Promise<Release> {
167177
return this.getUrl()
168178
.then((url) => {
@@ -178,6 +188,21 @@ export class SecurityRulesApiClient {
178188
});
179189
}
180190

191+
public createRelease(name: string, rulesetName: string): Promise<Release> {
192+
return this.getUrl()
193+
.then((url) => {
194+
return this.getReleaseDescription(name, rulesetName)
195+
.then((release) => {
196+
const request: HttpRequestConfig = {
197+
method: 'POST',
198+
url: `${url}/releases`,
199+
data: release,
200+
};
201+
return this.sendRequest<Release>(request);
202+
});
203+
});
204+
}
205+
181206
private getUrl(): Promise<string> {
182207
return this.getProjectIdPrefix()
183208
.then((projectIdPrefix) => {

src/security-rules/security-rules.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ export class SecurityRules {
378378
}
379379

380380
const rulesetName = validator.isString(ruleset) ? ruleset : ruleset.name;
381-
return this.client.updateRelease(releaseName, rulesetName)
381+
return this.client.updateOrCreateRelease(releaseName, rulesetName)
382382
.then(() => {
383383
return;
384384
});

test/unit/security-rules/security-rules-api-client.spec.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -490,6 +490,35 @@ describe('SecurityRulesApiClient', () => {
490490
});
491491
});
492492

493+
describe('updateOrCreateRelease', () => {
494+
it('should propagate API errors', () => {
495+
const EXPECTED_ERROR = new FirebaseSecurityRulesError('internal-error', 'message');
496+
const stub = sinon
497+
.stub(SecurityRulesApiClient.prototype, 'updateRelease')
498+
.rejects(EXPECTED_ERROR);
499+
stubs.push(stub);
500+
return apiClient.updateOrCreateRelease(RELEASE_NAME, RULESET_NAME)
501+
.should.eventually.be.rejected.and.deep.include(EXPECTED_ERROR);
502+
});
503+
504+
it('should create a new ruleset when update fails with a not-found error', () => {
505+
const NOT_FOUND_ERROR = new FirebaseSecurityRulesError('not-found', 'message');
506+
const updateRelease = sinon
507+
.stub(SecurityRulesApiClient.prototype, 'updateRelease')
508+
.rejects(NOT_FOUND_ERROR);
509+
const createRelease = sinon
510+
.stub(SecurityRulesApiClient.prototype, 'createRelease')
511+
.resolves();
512+
stubs.push(updateRelease, createRelease);
513+
514+
return apiClient.updateOrCreateRelease(RELEASE_NAME, RULESET_NAME)
515+
.then(() => {
516+
expect(updateRelease).to.have.been.calledOnce.and.calledWith(RELEASE_NAME, RULESET_NAME);
517+
expect(createRelease).to.have.been.called.calledOnce.and.calledWith(RELEASE_NAME, RULESET_NAME);
518+
});
519+
});
520+
});
521+
493522
describe('updateRelease', () => {
494523
it('should reject when project id is not available', () => {
495524
return clientWithoutProjectId.updateRelease(RELEASE_NAME, RULESET_NAME)
@@ -560,6 +589,74 @@ describe('SecurityRulesApiClient', () => {
560589
});
561590
});
562591

592+
describe('createRelease', () => {
593+
it('should reject when project id is not available', () => {
594+
return clientWithoutProjectId.createRelease(RELEASE_NAME, RULESET_NAME)
595+
.should.eventually.be.rejectedWith(noProjectId);
596+
});
597+
598+
it('should resolve with the created release on success', () => {
599+
const stub = sinon
600+
.stub(HttpClient.prototype, 'send')
601+
.resolves(utils.responseFrom({ name: 'bar' }));
602+
stubs.push(stub);
603+
return apiClient.createRelease(RELEASE_NAME, RULESET_NAME)
604+
.then((resp) => {
605+
expect(resp.name).to.equal('bar');
606+
expect(stub).to.have.been.calledOnce.and.calledWith({
607+
method: 'POST',
608+
url: 'https://firebaserules.googleapis.com/v1/projects/test-project/releases',
609+
data: {
610+
name: 'projects/test-project/releases/test.service',
611+
rulesetName: 'projects/test-project/rulesets/ruleset-id',
612+
},
613+
headers: EXPECTED_HEADERS,
614+
});
615+
});
616+
});
617+
618+
it('should throw when a full platform error response is received', () => {
619+
const stub = sinon
620+
.stub(HttpClient.prototype, 'send')
621+
.rejects(utils.errorFrom(ERROR_RESPONSE, 404));
622+
stubs.push(stub);
623+
const expected = new FirebaseSecurityRulesError('not-found', 'Requested entity not found');
624+
return apiClient.createRelease(RELEASE_NAME, RULESET_NAME)
625+
.should.eventually.be.rejected.and.deep.include(expected);
626+
});
627+
628+
it('should throw unknown-error when error code is not present', () => {
629+
const stub = sinon
630+
.stub(HttpClient.prototype, 'send')
631+
.rejects(utils.errorFrom({}, 404));
632+
stubs.push(stub);
633+
const expected = new FirebaseSecurityRulesError('unknown-error', 'Unknown server error: {}');
634+
return apiClient.createRelease(RELEASE_NAME, RULESET_NAME)
635+
.should.eventually.be.rejected.and.deep.include(expected);
636+
});
637+
638+
it('should throw unknown-error for non-json response', () => {
639+
const stub = sinon
640+
.stub(HttpClient.prototype, 'send')
641+
.rejects(utils.errorFrom('not json', 404));
642+
stubs.push(stub);
643+
const expected = new FirebaseSecurityRulesError(
644+
'unknown-error', 'Unexpected response with status: 404 and body: not json');
645+
return apiClient.createRelease(RELEASE_NAME, RULESET_NAME)
646+
.should.eventually.be.rejected.and.deep.include(expected);
647+
});
648+
649+
it('should throw when rejected with a FirebaseAppError', () => {
650+
const expected = new FirebaseAppError('network-error', 'socket hang up');
651+
const stub = sinon
652+
.stub(HttpClient.prototype, 'send')
653+
.rejects(expected);
654+
stubs.push(stub);
655+
return apiClient.createRelease(RELEASE_NAME, RULESET_NAME)
656+
.should.eventually.be.rejected.and.deep.include(expected);
657+
});
658+
});
659+
563660
describe('deleteRuleset', () => {
564661
const INVALID_NAMES: any[] = [null, undefined, '', 1, true, {}, []];
565662
INVALID_NAMES.forEach((invalidName) => {

0 commit comments

Comments
 (0)