Skip to content

Commit

Permalink
RulesDeploy.spec.ts tests migrated to esmock
Browse files Browse the repository at this point in the history
  • Loading branch information
joehan committed Dec 20, 2024
1 parent 69ee4f6 commit 555713b
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 132 deletions.
271 changes: 142 additions & 129 deletions src/rulesDeploy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,9 @@ import sinon from "sinon";
import esmock from "esmock";

import { FirebaseError } from "./error.js";
import * as prompt from "./prompt.js";
import * as resourceManager from "./gcp/resourceManager.js";
import * as projectNumber from "./getProjectNumber.js";
import { readFileSync } from "fs";
import { RulesetFile } from "./gcp/rules.js";
import { Config } from "./config.js";
import * as gcp from "./gcp/index.js";

import { RulesetServiceType } from "./rulesDeploy.js";
import { FIXTURE_DIR, FIXTURE_FIRESTORE_RULES_PATH } from "./test/fixtures/rulesDeploy/index.js";
Expand Down Expand Up @@ -53,7 +49,7 @@ describe("RulesDeploy", () => {
"./gcp/index.js": {
rules : {
"testRuleset": testRulesetStub,
}
},
}
});
rd = new RulesDeploy(BASE_OPTIONS, RulesetServiceType.CLOUD_FIRESTORE);
Expand Down Expand Up @@ -197,27 +193,67 @@ describe("RulesDeploy", () => {

describe("createRulesets", () => {
let rd: any;
let rdCrossService: any;
let sandbox = sinon.createSandbox();

let getProjectNumberStub = sandbox.stub();
let getLatestRulesetNameStub = sandbox.stub();
let getRulesetContentStub = sandbox.stub();
let createRulesetStub = sandbox.stub();
let listAllRulesetsStub = sandbox.stub();
let listAllReleasesStub = sandbox.stub();
let promptOnceStub = sandbox.stub();
let serviceAccountHasRolesStub = sandbox.stub();
let addServiceAccountToRolesStub = sandbox.stub();
let getRulesetIdStub = sandbox.stub();
let deleteRulesetStub = sandbox.stub();

const CROSS_SERVICE_OPTIONS: { cwd: string; project: string; config: any } = {
cwd: CROSS_SERVICE_FIXTURE_DIR,
project: "test-project",
config: null,
};
CROSS_SERVICE_OPTIONS.config = Config.load(CROSS_SERVICE_OPTIONS, false);

beforeEach(async () => {
const { RulesDeploy } = await esmock("./rulesDeploy.js", {}, {
"./gcp/index.js": {
rules : {
"getLatestRulesetName": getLatestRulesetNameStub,
"getRulesetContent": getRulesetContentStub,
"createRuleset": createRulesetStub,
}
}
"listAllRulesets": listAllRulesetsStub,
"listAllReleases": listAllReleasesStub,
"getRulesetId": getRulesetIdStub,
"deleteRuleset": deleteRulesetStub,
},
},
"./gcp/resourceManager.js": {
"serviceAccountHasRoles": serviceAccountHasRolesStub,
"addServiceAccountToRoles": addServiceAccountToRolesStub,
},
"./getProjectNumber.js": {
"getProjectNumber": getProjectNumberStub,
},
"./prompt.js" : {
"promptOnce": promptOnceStub,
},
});

getProjectNumberStub.resolves("12345");
getLatestRulesetNameStub.rejects(new Error("getLatestRulesetName behavior unspecified"));;
getRulesetContentStub.rejects(new Error("getRulesetContent behavior unspecified"));
createRulesetStub.rejects(new Error("createRuleset behavior unspecified"));
listAllRulesetsStub.rejects(new Error("listAllRulesets behavior unspecified"));
listAllReleasesStub.rejects(new Error("listAllReleases behavior unspecified"));
promptOnceStub.rejects(new Error("promptOnce behavior unspecified"));
serviceAccountHasRolesStub.rejects(new Error("serviceAccountHasRoles behavior unspecified"));
addServiceAccountToRolesStub.rejects(new Error("addServiceAccountToRoles behavior unspecified"));
getRulesetIdStub.rejects(new Error("getRulesetId behavior unspecified"));
deleteRulesetStub.rejects(new Error("deleteRulesetStub behavior unspecified"));

rd = new RulesDeploy(BASE_OPTIONS, RulesetServiceType.CLOUD_FIRESTORE);
getLatestRulesetNameStub
.rejects(new Error("getLatestRulesetName behavior unspecified"));
getRulesetContentStub
.rejects(new Error("getRulesetContent behavior unspecified"));
createRulesetStub
.rejects(new Error("createRuleset behavior unspecified"));
rdCrossService = new RulesDeploy(CROSS_SERVICE_OPTIONS, RulesetServiceType.FIREBASE_STORAGE);
});

afterEach(() => {
Expand Down Expand Up @@ -358,47 +394,37 @@ describe("RulesDeploy", () => {
});

describe("with cross-service rules", () => {
const CROSS_SERVICE_OPTIONS: { cwd: string; project: string; config: any } = {
cwd: CROSS_SERVICE_FIXTURE_DIR,
project: "test-project",
config: null,
};
CROSS_SERVICE_OPTIONS.config = Config.load(CROSS_SERVICE_OPTIONS, false);

beforeEach(() => {
getLatestRulesetNameStub.resolves(null);
createRulesetStub.onFirstCall().resolves("compiled");
sinon.stub(projectNumber, "getProjectNumber").resolves("12345");
rd = rdCrossService;
rd.addFile("storage.rules");
});

afterEach(() => {
sandbox.restore();
});

it("should deploy even with IAM failure", async () => {
sinon.stub(resourceManager, "serviceAccountHasRoles").rejects();
serviceAccountHasRolesStub.rejects();
const result = rd.createRulesets(RulesetServiceType.FIREBASE_STORAGE);
await expect(result).to.eventually.deep.equal(["compiled"]);

expect(gcp.rules.createRuleset).calledOnceWithExactly(BASE_OPTIONS.project, [
expect(createRulesetStub).calledOnceWithExactly(BASE_OPTIONS.project, [
{ name: "storage.rules", content: sinon.match.string },
]);
expect(resourceManager.serviceAccountHasRoles).calledOnce;
expect(serviceAccountHasRolesStub).calledOnce;
});

it("should update permissions if prompted", async () => {
sinon.stub(resourceManager, "serviceAccountHasRoles").resolves(false);
sinon.stub(resourceManager, "addServiceAccountToRoles").resolves();
sinon.stub(prompt, "promptOnce").onFirstCall().resolves(true);
serviceAccountHasRolesStub.resolves(false);
addServiceAccountToRolesStub.resolves();
promptOnceStub.onFirstCall().resolves(true);

const result = rd.createRulesets(RulesetServiceType.FIREBASE_STORAGE);
await expect(result).to.eventually.deep.equal(["compiled"]);

expect(gcp.rules.createRuleset).calledOnceWithExactly(BASE_OPTIONS.project, [
expect(createRulesetStub).calledOnceWithExactly(BASE_OPTIONS.project, [
{ name: "storage.rules", content: sinon.match.string },
]);
expect(resourceManager.addServiceAccountToRoles).calledOnceWithExactly(
expect(addServiceAccountToRolesStub).to.have.been.calledOnce;
expect(addServiceAccountToRolesStub).calledOnceWithExactly(
"12345",
"service-12345@gcp-sa-firebasestorage.iam.gserviceaccount.com",
["roles/firebaserules.firestoreServiceAgent"],
Expand All @@ -407,32 +433,31 @@ describe("RulesDeploy", () => {
});

it("should not update permissions if declined", async () => {
sinon.stub(resourceManager, "serviceAccountHasRoles").resolves(false);
sinon.stub(resourceManager, "addServiceAccountToRoles").resolves();
sinon.stub(prompt, "promptOnce").onFirstCall().resolves(false);
serviceAccountHasRolesStub.resolves(false);
addServiceAccountToRolesStub.resolves();
promptOnceStub.onFirstCall().resolves(false);

const result = rd.createRulesets(RulesetServiceType.FIREBASE_STORAGE);
await expect(result).to.eventually.deep.equal(["compiled"]);

expect(gcp.rules.createRuleset).calledOnceWithExactly(BASE_OPTIONS.project, [
expect(createRulesetStub).calledOnceWithExactly(BASE_OPTIONS.project, [
{ name: "storage.rules", content: sinon.match.string },
]);
expect(resourceManager.addServiceAccountToRoles).not.called;
expect(addServiceAccountToRolesStub).not.called;
});

it("should not prompt if role already granted", async () => {
sinon.stub(resourceManager, "serviceAccountHasRoles").resolves(true);
sinon.stub(resourceManager, "addServiceAccountToRoles").resolves();
const promptSpy = sinon.spy(prompt, "promptOnce");
serviceAccountHasRolesStub.resolves(true);
addServiceAccountToRolesStub.resolves();

const result = rd.createRulesets(RulesetServiceType.FIREBASE_STORAGE);
await expect(result).to.eventually.deep.equal(["compiled"]);

expect(gcp.rules.createRuleset).calledOnceWithExactly(BASE_OPTIONS.project, [
expect(createRulesetStub).calledOnceWithExactly(BASE_OPTIONS.project, [
{ name: "storage.rules", content: sinon.match.string },
]);
expect(resourceManager.addServiceAccountToRoles).not.called;
expect(promptSpy).not.called;
expect(addServiceAccountToRolesStub).not.called;
expect(promptOnceStub).not.called;
});
});

Expand All @@ -441,15 +466,9 @@ describe("RulesDeploy", () => {
(QUOTA_ERROR as any).status = 429;

beforeEach(() => {
(gcp.rules.getLatestRulesetName as sinon.SinonStub).resolves("deadbeef");
(gcp.rules.getRulesetContent as sinon.SinonStub).resolves([]);
(gcp.rules.createRuleset as sinon.SinonStub).rejects(new Error("failing"));

sinon.stub(gcp.rules, "listAllRulesets").rejects(new Error("listAllRulesets failing"));
});

afterEach(() => {
sinon.restore();
getLatestRulesetNameStub.resolves("deadbeef");
getRulesetContentStub.resolves([]);
createRulesetStub.rejects(new Error("failing"));
});

it("should throw if it return not a quota issue", async () => {
Expand All @@ -460,108 +479,102 @@ describe("RulesDeploy", () => {
});

it("should do nothing if there are not a lot of previous rulesets", async () => {
(gcp.rules.createRuleset as sinon.SinonStub).onFirstCall().rejects(QUOTA_ERROR);
(gcp.rules.listAllRulesets as sinon.SinonStub).resolves(Array(1));
createRulesetStub.onFirstCall().rejects(QUOTA_ERROR);
listAllRulesetsStub.resolves(Array(1));
rd.addFile("firestore.rules");

const result = rd.createRulesets(RulesetServiceType.CLOUD_FIRESTORE);
await expect(result).to.eventually.be.fulfilled;
});

describe("and a prompt is made", () => {
beforeEach(() => {
sinon.stub(prompt, "promptOnce").rejects(new Error("behavior unspecified"));
sinon.stub(gcp.rules, "listAllReleases").rejects(new Error("listAllReleases failing"));
sinon.stub(gcp.rules, "deleteRuleset").rejects(new Error("deleteRuleset failing"));
sinon.stub(gcp.rules, "getRulesetId").throws(new Error("getRulesetId failing"));
});

afterEach(() => {
sinon.restore();
});

it("should prompt for a choice (no)", async () => {
(gcp.rules.createRuleset as sinon.SinonStub).onFirstCall().rejects(QUOTA_ERROR);
(gcp.rules.listAllRulesets as sinon.SinonStub).resolves(Array(1001));
(prompt.promptOnce as sinon.SinonStub).onFirstCall().resolves(false);
createRulesetStub.onFirstCall().rejects(QUOTA_ERROR);
listAllRulesetsStub.resolves(Array(1001));
promptOnceStub.onFirstCall().resolves(false);
rd.addFile("firestore.rules");

const result = rd.createRulesets(RulesetServiceType.CLOUD_FIRESTORE);
await expect(result).to.eventually.deep.equal([]);
expect(gcp.rules.createRuleset).to.be.calledOnce;
expect(prompt.promptOnce).to.be.calledOnce;
expect(createRulesetStub).to.be.calledOnce;
expect(promptOnceStub).to.be.calledOnce;
});

it("should prompt for a choice (yes) and delete and retry creation", async () => {
(gcp.rules.createRuleset as sinon.SinonStub).onFirstCall().rejects(QUOTA_ERROR);
(gcp.rules.listAllRulesets as sinon.SinonStub).resolves(
createRulesetStub.onFirstCall().rejects(QUOTA_ERROR);
listAllRulesetsStub.resolves(
new Array(1001).fill(0).map(() => ({ name: "foo" })),
);
(prompt.promptOnce as sinon.SinonStub).onFirstCall().resolves(true);
(gcp.rules.listAllReleases as sinon.SinonStub).resolves([
promptOnceStub.onFirstCall().resolves(true);
listAllReleasesStub.resolves([
{ rulesetName: "name", name: "bar" },
]);
(gcp.rules.getRulesetId as sinon.SinonStub).returns("");
(gcp.rules.deleteRuleset as sinon.SinonStub).resolves();
(gcp.rules.createRuleset as sinon.SinonStub).onSecondCall().resolves("created");
getRulesetIdStub.returns("");
deleteRulesetStub.resolves();
createRulesetStub.onSecondCall().resolves("created");
rd.addFile("firestore.rules");

const result = rd.createRulesets(RulesetServiceType.CLOUD_FIRESTORE);
await expect(result).to.eventually.deep.equal(["created"]);
expect(gcp.rules.createRuleset).to.be.calledTwice;
expect(createRulesetStub).to.be.calledTwice;
});
});
});
});

// describe("release", () => {
// let rd = new RulesDeploy(BASE_OPTIONS, RulesetServiceType.CLOUD_FIRESTORE);

// beforeEach(() => {
// rd = new RulesDeploy(BASE_OPTIONS, RulesetServiceType.CLOUD_FIRESTORE);
// sinon
// .stub(gcp.rules, "updateOrCreateRelease")
// .rejects(new Error("updateOrCreateRelease behavior unspecified"));
// });

// afterEach(() => {
// sinon.restore();
// });

// it("should release the rules", async () => {
// (gcp.rules.updateOrCreateRelease as sinon.SinonStub).resolves();

// const result = rd.release("firestore.rules", RulesetServiceType.CLOUD_FIRESTORE);
// await expect(result).to.eventually.be.fulfilled;

// expect(gcp.rules.updateOrCreateRelease).calledOnceWithExactly(
// BASE_OPTIONS.project,
// undefined, // Because we didn't compile anything.
// RulesetServiceType.CLOUD_FIRESTORE,
// );
// });

// it("should enforce a subresource for storage", async () => {
// const result = rd.release("firestore.rules", RulesetServiceType.FIREBASE_STORAGE);
// await expect(result).to.eventually.be.rejectedWith(
// FirebaseError,
// /Cannot release resource type "firebase.storage"/,
// );

// expect(gcp.rules.updateOrCreateRelease).not.called;
// });

// it("should append a subresource for storage", async () => {
// (gcp.rules.updateOrCreateRelease as sinon.SinonStub).resolves();

// const result = rd.release("firestore.rules", RulesetServiceType.FIREBASE_STORAGE, "bar");
// await expect(result).to.eventually.be.fulfilled;

// expect(gcp.rules.updateOrCreateRelease).calledOnceWithExactly(
// BASE_OPTIONS.project,
// undefined, // Because we didn't compile anything.
// `${RulesetServiceType.FIREBASE_STORAGE}/bar`,
// );
// });
// });
describe("release", () => {
let rd: any;
let updateOrCreateReleaseStub = sinon.stub();
beforeEach(async () => {
const { RulesDeploy } = await esmock("./rulesDeploy.js", {}, {
"./gcp/index.js": {
rules : {
"updateOrCreateRelease": updateOrCreateReleaseStub,
},
}
});
rd = new RulesDeploy(BASE_OPTIONS, RulesetServiceType.CLOUD_FIRESTORE);
updateOrCreateReleaseStub
.rejects(new Error("updateOrCreateRelease behavior unspecified"));
});

afterEach(() => {
updateOrCreateReleaseStub.reset();
});

it("should release the rules", async () => {
updateOrCreateReleaseStub.resolves();

const result = rd.release("firestore.rules", RulesetServiceType.CLOUD_FIRESTORE);
await expect(result).to.eventually.be.fulfilled;

expect(updateOrCreateReleaseStub).calledOnceWithExactly(
BASE_OPTIONS.project,
undefined, // Because we didn't compile anything.
RulesetServiceType.CLOUD_FIRESTORE,
);
});

it("should enforce a subresource for storage", async () => {
const result = rd.release("firestore.rules", RulesetServiceType.FIREBASE_STORAGE);
await expect(result).to.eventually.be.rejectedWith(
'Cannot release resource type "firebase.storage"',
);

expect(updateOrCreateReleaseStub).not.called;
});

it("should append a subresource for storage", async () => {
updateOrCreateReleaseStub.resolves();

const result = rd.release("firestore.rules", RulesetServiceType.FIREBASE_STORAGE, "bar");
await expect(result).to.eventually.be.fulfilled;

expect(updateOrCreateReleaseStub).calledOnceWithExactly(
BASE_OPTIONS.project,
undefined, // Because we didn't compile anything.
`${RulesetServiceType.FIREBASE_STORAGE}/bar`,
);
});
});
});
Loading

0 comments on commit 555713b

Please sign in to comment.