Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@
"coverageDirectory": "var/coverage/test",
"coverageThreshold": {
"global": {
"statements": 95,
"branches": 79,
"statements": 100,
"branches": 100,
"functions": 100,
"lines": 95
"lines": 100
}
},
"transform": {
Expand All @@ -70,28 +70,28 @@
}
},
"dependencies": {
"@aws-sdk/client-cloudformation": "^3.879.0",
"@aws-sdk/client-cloudformation": "^3.972.0",
"chalk": "^3.0.0",
"minimist": "^1.2.8"
},
"devDependencies": {
"@types/jest": "^29.5.14",
"@typescript-eslint/eslint-plugin": "^8.32.0",
"@typescript-eslint/parser": "^8.32.0",
"@types/jest": "^30.0.0",
"@typescript-eslint/eslint-plugin": "^8.53.1",
"@typescript-eslint/parser": "^8.53.1",
"eslint": "^8.57.1",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^8.10.0",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "^28.11.0",
"eslint-plugin-prettier": "^5.4.0",
"eslint-config-prettier": "^8.10.2",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jest": "^28.14.0",
"eslint-plugin-prettier": "^5.5.5",
"git-describe": "^4.1.1",
"jest": "^29.7.0",
"jest": "^30.2.0",
"license-checker": "^25.0.1",
"npm-run-all": "^4.1.5",
"prettier": "^3.5.3",
"prettier": "^3.8.0",
"shx": "^0.4.0",
"ts-jest": "^29.4.1",
"typescript": "^5.8.3"
"ts-jest": "^29.4.6",
"typescript": "^5.9.3"
},
"resolutions": {
"set-value": "^4.0.1"
Expand Down
183 changes: 183 additions & 0 deletions test/unit/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,186 @@ describe("API has unexpected error", () => {
return expect(cfnWaitReady(cloudFormation, params)).rejects.toBe(error);
});
});

describe("Stack describeStacks returns no stacks", () => {
let logSpy: jest.SpyInstance;

beforeEach(() => {
logSpy = jest.spyOn(console, "log").mockImplementation(() => undefined);
});

afterEach(() => {
logSpy.mockRestore();
jest.restoreAllMocks();
});

it("handles Stacks: [] (empty array) and returns without describing events", async () => {
const describeStacks = jest.fn().mockResolvedValue({ Stacks: [] });
const describeStackEvents = jest.fn();

const cloudFormation = { describeStacks, describeStackEvents } as unknown as CloudFormation;
const params = { StackName: "stack-name" };

await cfnWaitReady(cloudFormation, params);

expect(describeStackEvents).not.toHaveBeenCalled();
expect(logSpy).toHaveBeenCalled(); // keeps the test resilient to chalk formatting
});

it("handles Stacks: undefined (missing field) and returns without describing events", async () => {
// Simulate AWS returning no Stacks field at all
const describeStacks = jest.fn().mockResolvedValue({});
const describeStackEvents = jest.fn();

const cloudFormation = { describeStacks, describeStackEvents } as unknown as CloudFormation;
const params = { StackName: "stack-name" };

await cfnWaitReady(cloudFormation, params);

expect(describeStackEvents).not.toHaveBeenCalled();
expect(logSpy).toHaveBeenCalled();
});

it("handles Stacks: null and returns without describing events", async () => {
const describeStacks = jest.fn().mockResolvedValue({ Stacks: null });
const describeStackEvents = jest.fn();

const cloudFormation = { describeStacks, describeStackEvents } as unknown as CloudFormation;
const params = { StackName: "stack-name" };

await cfnWaitReady(cloudFormation, params);

expect(describeStackEvents).not.toHaveBeenCalled();
expect(logSpy).toHaveBeenCalled();
});
});

describe("Coverage for nullish/optional branches", () => {
const flushPromises = () =>
new Promise<void>((resolve) => {
setImmediate(resolve);
});

afterEach(() => {
jest.useRealTimers();
jest.restoreAllMocks();
});

it("covers stackStatus ?? null when StackStatus is undefined", async () => {
const describeStacks = jest.fn().mockResolvedValue({
Stacks: [{}],
});

const completeEvent1 = {
EventId: "1",
LogicalResourceId: "stack-name",
ResourceStatus: "UPDATE_COMPLETE",
Timestamp: new Date(),
ResourceStatusReason: null,
};

const describeStackEvents = jest.fn().mockResolvedValue({
StackEvents: [completeEvent1],
});

jest.spyOn(console, "log").mockImplementation(() => undefined);

const cloudFormation = { describeStacks, describeStackEvents } as unknown as CloudFormation;
await expect(cfnWaitReady(cloudFormation, { StackName: "stack-name" })).resolves.toBeUndefined();

// Since StackStatus was undefined, it should not early-return and should poll events.
expect(describeStackEvents).toHaveBeenCalled();
});

it("covers StackEvents undefined => events?.reverse() ?? []", async () => {
const describeStacks = jest.fn().mockResolvedValue({
Stacks: [{ StackStatus: "UPDATE_IN_PROGRESS" }],
});

const completeStackEvent = {
EventId: "2",
LogicalResourceId: "stack-name",
ResourceStatus: "UPDATE_COMPLETE",
Timestamp: new Date(),
ResourceStatusReason: null,
};

const describeStackEvents = jest
.fn()
.mockResolvedValueOnce({ StackEvents: undefined })
.mockResolvedValueOnce({ StackEvents: [completeStackEvent] });

jest.spyOn(console, "log").mockImplementation(() => undefined);

jest.useFakeTimers({ advanceTimers: true });

const cloudFormation = { describeStacks, describeStackEvents } as unknown as CloudFormation;

const p = cfnWaitReady(cloudFormation, { StackName: "stack-name" });

// allow describeStacks + first describeStackEvents to resolve
await flushPromises();
await flushPromises();

// release the internal sleep(10000)
jest.advanceTimersByTime(10_000);

// allow second poll to run and resolve
await flushPromises();
await flushPromises();

await expect(p).resolves.toBeUndefined();
expect(describeStackEvents).toHaveBeenCalledTimes(2);
});

it("covers ResourceStatus ?? null and EventId ?? null", async () => {
const describeStacks = jest.fn().mockResolvedValue({
Stacks: [{ StackStatus: "UPDATE_IN_PROGRESS" }],
});

// First poll: stack event missing EventId + ResourceStatus -> triggers both ?? null assignments
const missingFieldsStackEvent = {
LogicalResourceId: "stack-name",
Timestamp: new Date(),
ResourceStatusReason: null,
// EventId: undefined,
// ResourceStatus: undefined,
};

// Second poll: completing stack event to exit
const completeStackEvent = {
EventId: "99",
LogicalResourceId: "stack-name",
ResourceStatus: "UPDATE_COMPLETE",
Timestamp: new Date(),
ResourceStatusReason: null,
};

const describeStackEvents = jest
.fn()
.mockResolvedValueOnce({ StackEvents: [missingFieldsStackEvent] })
.mockResolvedValueOnce({ StackEvents: [completeStackEvent, missingFieldsStackEvent] });

jest.spyOn(console, "log").mockImplementation(() => undefined);

jest.useFakeTimers({ advanceTimers: true });

const cloudFormation = { describeStacks, describeStackEvents } as unknown as CloudFormation;

const p = cfnWaitReady(cloudFormation, { StackName: "stack-name" });

// let first poll resolve
await flushPromises();
await flushPromises();

// release internal sleep(10000)
jest.advanceTimersByTime(10_000);

// let second poll resolve and finish
await flushPromises();
await flushPromises();

await expect(p).resolves.toBeUndefined();
expect(describeStackEvents).toHaveBeenCalledTimes(2);
});
});
Loading
Loading