Skip to content

Commit 09271ea

Browse files
committed
Update tests for new version of code
1 parent a92e3f7 commit 09271ea

File tree

8 files changed

+620
-152
lines changed

8 files changed

+620
-152
lines changed

__tests__/deploy.test.ts

Lines changed: 366 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,366 @@
1+
import {
2+
CloudFormationClient,
3+
DescribeStacksCommand,
4+
DescribeChangeSetCommand,
5+
DescribeEventsCommand,
6+
CreateChangeSetCommand,
7+
StackStatus,
8+
ChangeSetStatus
9+
} from '@aws-sdk/client-cloudformation'
10+
import { mockClient } from 'aws-sdk-client-mock'
11+
import { waitUntilStackOperationComplete, updateStack } from '../src/deploy'
12+
import * as core from '@actions/core'
13+
14+
jest.mock('@actions/core')
15+
16+
const mockCfnClient = mockClient(CloudFormationClient)
17+
const cfn = new CloudFormationClient({ region: 'us-east-1' })
18+
19+
describe('Deploy error scenarios', () => {
20+
beforeEach(() => {
21+
jest.clearAllMocks()
22+
mockCfnClient.reset()
23+
jest.useFakeTimers()
24+
})
25+
26+
afterEach(() => {
27+
jest.useRealTimers()
28+
})
29+
30+
describe('waitUntilStackOperationComplete', () => {
31+
it('throws error on CREATE_FAILED status', async () => {
32+
mockCfnClient.on(DescribeStacksCommand).resolves({
33+
Stacks: [
34+
{
35+
StackName: 'TestStack',
36+
StackStatus: StackStatus.CREATE_FAILED,
37+
CreationTime: new Date()
38+
}
39+
]
40+
})
41+
42+
await expect(
43+
waitUntilStackOperationComplete(
44+
{ client: cfn, maxWaitTime: 60, minDelay: 1 },
45+
{ StackName: 'TestStack' }
46+
)
47+
).rejects.toThrow('Stack operation failed with status: CREATE_FAILED')
48+
})
49+
50+
it('throws error on UPDATE_ROLLBACK_COMPLETE status', async () => {
51+
mockCfnClient.on(DescribeStacksCommand).resolves({
52+
Stacks: [
53+
{
54+
StackName: 'TestStack',
55+
StackStatus: StackStatus.UPDATE_ROLLBACK_COMPLETE,
56+
CreationTime: new Date()
57+
}
58+
]
59+
})
60+
61+
await expect(
62+
waitUntilStackOperationComplete(
63+
{ client: cfn, maxWaitTime: 60, minDelay: 1 },
64+
{ StackName: 'TestStack' }
65+
)
66+
).rejects.toThrow(
67+
'Stack operation failed with status: UPDATE_ROLLBACK_COMPLETE'
68+
)
69+
})
70+
71+
it('throws error on ROLLBACK_FAILED status', async () => {
72+
mockCfnClient.on(DescribeStacksCommand).resolves({
73+
Stacks: [
74+
{
75+
StackName: 'TestStack',
76+
StackStatus: StackStatus.ROLLBACK_FAILED,
77+
CreationTime: new Date()
78+
}
79+
]
80+
})
81+
82+
await expect(
83+
waitUntilStackOperationComplete(
84+
{ client: cfn, maxWaitTime: 60, minDelay: 1 },
85+
{ StackName: 'TestStack' }
86+
)
87+
).rejects.toThrow('Stack operation failed with status: ROLLBACK_FAILED')
88+
})
89+
90+
it('throws error on UPDATE_FAILED status', async () => {
91+
mockCfnClient.on(DescribeStacksCommand).resolves({
92+
Stacks: [
93+
{
94+
StackName: 'TestStack',
95+
StackStatus: StackStatus.UPDATE_FAILED,
96+
CreationTime: new Date()
97+
}
98+
]
99+
})
100+
101+
await expect(
102+
waitUntilStackOperationComplete(
103+
{ client: cfn, maxWaitTime: 60, minDelay: 1 },
104+
{ StackName: 'TestStack' }
105+
)
106+
).rejects.toThrow('Stack operation failed with status: UPDATE_FAILED')
107+
})
108+
109+
it('throws error on DELETE_FAILED status', async () => {
110+
mockCfnClient.on(DescribeStacksCommand).resolves({
111+
Stacks: [
112+
{
113+
StackName: 'TestStack',
114+
StackStatus: StackStatus.DELETE_FAILED,
115+
CreationTime: new Date()
116+
}
117+
]
118+
})
119+
120+
await expect(
121+
waitUntilStackOperationComplete(
122+
{ client: cfn, maxWaitTime: 60, minDelay: 1 },
123+
{ StackName: 'TestStack' }
124+
)
125+
).rejects.toThrow('Stack operation failed with status: DELETE_FAILED')
126+
})
127+
128+
it('throws error on ROLLBACK_COMPLETE status', async () => {
129+
mockCfnClient.on(DescribeStacksCommand).resolves({
130+
Stacks: [
131+
{
132+
StackName: 'TestStack',
133+
StackStatus: StackStatus.ROLLBACK_COMPLETE,
134+
CreationTime: new Date()
135+
}
136+
]
137+
})
138+
139+
await expect(
140+
waitUntilStackOperationComplete(
141+
{ client: cfn, maxWaitTime: 60, minDelay: 1 },
142+
{ StackName: 'TestStack' }
143+
)
144+
).rejects.toThrow('Stack operation failed with status: ROLLBACK_COMPLETE')
145+
})
146+
147+
it('throws error on UPDATE_ROLLBACK_FAILED status', async () => {
148+
mockCfnClient.on(DescribeStacksCommand).resolves({
149+
Stacks: [
150+
{
151+
StackName: 'TestStack',
152+
StackStatus: StackStatus.UPDATE_ROLLBACK_FAILED,
153+
CreationTime: new Date()
154+
}
155+
]
156+
})
157+
158+
await expect(
159+
waitUntilStackOperationComplete(
160+
{ client: cfn, maxWaitTime: 60, minDelay: 1 },
161+
{ StackName: 'TestStack' }
162+
)
163+
).rejects.toThrow(
164+
'Stack operation failed with status: UPDATE_ROLLBACK_FAILED'
165+
)
166+
})
167+
168+
it('throws error on IMPORT_ROLLBACK_COMPLETE status', async () => {
169+
mockCfnClient.on(DescribeStacksCommand).resolves({
170+
Stacks: [
171+
{
172+
StackName: 'TestStack',
173+
StackStatus: StackStatus.IMPORT_ROLLBACK_COMPLETE,
174+
CreationTime: new Date()
175+
}
176+
]
177+
})
178+
179+
await expect(
180+
waitUntilStackOperationComplete(
181+
{ client: cfn, maxWaitTime: 60, minDelay: 1 },
182+
{ StackName: 'TestStack' }
183+
)
184+
).rejects.toThrow(
185+
'Stack operation failed with status: IMPORT_ROLLBACK_COMPLETE'
186+
)
187+
})
188+
189+
it('throws error on IMPORT_ROLLBACK_FAILED status', async () => {
190+
mockCfnClient.on(DescribeStacksCommand).resolves({
191+
Stacks: [
192+
{
193+
StackName: 'TestStack',
194+
StackStatus: StackStatus.IMPORT_ROLLBACK_FAILED,
195+
CreationTime: new Date()
196+
}
197+
]
198+
})
199+
200+
await expect(
201+
waitUntilStackOperationComplete(
202+
{ client: cfn, maxWaitTime: 60, minDelay: 1 },
203+
{ StackName: 'TestStack' }
204+
)
205+
).rejects.toThrow(
206+
'Stack operation failed with status: IMPORT_ROLLBACK_FAILED'
207+
)
208+
})
209+
210+
it('throws stack does not exist error', async () => {
211+
mockCfnClient
212+
.on(DescribeStacksCommand)
213+
.rejects(new Error('Stack with id TestStack does not exist'))
214+
215+
await expect(
216+
waitUntilStackOperationComplete(
217+
{ client: cfn, maxWaitTime: 60, minDelay: 1 },
218+
{ StackName: 'TestStack' }
219+
)
220+
).rejects.toThrow('Stack TestStack does not exist')
221+
})
222+
223+
it('waits for in-progress stack and succeeds', async () => {
224+
let callCount = 0
225+
mockCfnClient.on(DescribeStacksCommand).callsFake(() => {
226+
callCount++
227+
if (callCount === 1) {
228+
return {
229+
Stacks: [
230+
{
231+
StackName: 'TestStack',
232+
StackStatus: StackStatus.CREATE_IN_PROGRESS,
233+
CreationTime: new Date()
234+
}
235+
]
236+
}
237+
}
238+
return {
239+
Stacks: [
240+
{
241+
StackName: 'TestStack',
242+
StackStatus: StackStatus.CREATE_COMPLETE,
243+
CreationTime: new Date()
244+
}
245+
]
246+
}
247+
})
248+
249+
const promise = waitUntilStackOperationComplete(
250+
{ client: cfn, maxWaitTime: 60, minDelay: 1 },
251+
{ StackName: 'TestStack' }
252+
)
253+
254+
// Advance timers to trigger the wait
255+
await jest.advanceTimersByTimeAsync(1500)
256+
257+
await expect(promise).resolves.toBeUndefined()
258+
expect(callCount).toBe(2)
259+
})
260+
261+
it('throws error when stack not found in response', async () => {
262+
mockCfnClient.on(DescribeStacksCommand).resolves({
263+
Stacks: []
264+
})
265+
266+
await expect(
267+
waitUntilStackOperationComplete(
268+
{ client: cfn, maxWaitTime: 60, minDelay: 1 },
269+
{ StackName: 'TestStack' }
270+
)
271+
).rejects.toThrow('Stack TestStack not found')
272+
})
273+
})
274+
275+
describe('updateStack with validation errors', () => {
276+
it('includes validation error details when change set fails', async () => {
277+
const mockStack = {
278+
StackId: 'test-stack-id',
279+
StackName: 'TestStack',
280+
StackStatus: StackStatus.CREATE_COMPLETE,
281+
CreationTime: new Date()
282+
}
283+
284+
mockCfnClient
285+
.on(CreateChangeSetCommand)
286+
.resolves({ Id: 'test-cs-id' })
287+
.on(DescribeStacksCommand)
288+
.resolves({ Stacks: [mockStack] })
289+
.on(DescribeChangeSetCommand)
290+
.resolves({
291+
ChangeSetId: 'test-cs-id',
292+
Status: ChangeSetStatus.FAILED,
293+
ExecutionStatus: 'UNAVAILABLE',
294+
StatusReason: 'Validation failed'
295+
})
296+
.on(DescribeEventsCommand)
297+
.resolves({
298+
OperationEvents: [
299+
{
300+
EventType: 'VALIDATION_ERROR',
301+
ValidationPath: '/Resources/MyResource',
302+
ValidationStatusReason: 'Invalid property value'
303+
}
304+
]
305+
})
306+
307+
await expect(
308+
updateStack(
309+
cfn,
310+
mockStack,
311+
{
312+
StackName: 'TestStack',
313+
ChangeSetName: 'test-cs',
314+
ChangeSetType: 'UPDATE'
315+
},
316+
true,
317+
false,
318+
false
319+
)
320+
).rejects.toThrow('Validation errors')
321+
})
322+
323+
it('handles error when fetching validation events fails', async () => {
324+
const mockStack = {
325+
StackId: 'test-stack-id',
326+
StackName: 'TestStack',
327+
StackStatus: StackStatus.CREATE_COMPLETE,
328+
CreationTime: new Date()
329+
}
330+
331+
mockCfnClient
332+
.on(CreateChangeSetCommand)
333+
.resolves({ Id: 'test-cs-id' })
334+
.on(DescribeStacksCommand)
335+
.resolves({ Stacks: [mockStack] })
336+
.on(DescribeChangeSetCommand)
337+
.resolves({
338+
ChangeSetId: 'test-cs-id',
339+
Status: ChangeSetStatus.FAILED,
340+
ExecutionStatus: 'UNAVAILABLE',
341+
StatusReason: 'Validation failed'
342+
})
343+
.on(DescribeEventsCommand)
344+
.rejects(new Error('Access denied'))
345+
346+
await expect(
347+
updateStack(
348+
cfn,
349+
mockStack,
350+
{
351+
StackName: 'TestStack',
352+
ChangeSetName: 'test-cs',
353+
ChangeSetType: 'UPDATE'
354+
},
355+
true,
356+
false,
357+
false
358+
)
359+
).rejects.toThrow('Failed to create Change Set')
360+
361+
expect(core.info).toHaveBeenCalledWith(
362+
expect.stringContaining('Failed to get validation event details')
363+
)
364+
})
365+
})
366+
})

0 commit comments

Comments
 (0)