Skip to content

Commit 6cb01e8

Browse files
authored
Add e2e tests for deployment (#275)
Add e2e tests for validation, deployment, and artifact upload
1 parent 7a8f3e5 commit 6cb01e8

10 files changed

+741
-0
lines changed
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import { readFileSync } from 'fs';
2+
import { join } from 'path';
3+
import { WaiterState } from '@smithy/util-waiter';
4+
import { describe, it, expect, beforeEach } from 'vitest';
5+
import { DocumentType } from '../../../src/document/Document';
6+
import { DeploymentWorkflow } from '../../../src/stacks/actions/DeploymentWorkflow';
7+
import {
8+
CreateDeploymentParams,
9+
StackActionPhase,
10+
StackActionState,
11+
} from '../../../src/stacks/actions/StackActionRequestType';
12+
import { createMockComponents } from '../../utils/MockServerComponents';
13+
14+
const TEMPLATE_PATH = join(__dirname, '../../resources/templates/simple.yaml');
15+
const TEST_TEMPLATE_URI = `file://${TEMPLATE_PATH}`;
16+
17+
describe('DeploymentWorkflow', () => {
18+
let mockComponents: ReturnType<typeof createMockComponents>;
19+
let deploymentWorkflow: DeploymentWorkflow;
20+
21+
beforeEach(() => {
22+
mockComponents = createMockComponents();
23+
24+
const templateContent = readFileSync(TEMPLATE_PATH, 'utf8');
25+
mockComponents.documentManager.get.withArgs(TEST_TEMPLATE_URI).returns({
26+
contents: () => templateContent,
27+
uri: TEST_TEMPLATE_URI,
28+
documentType: DocumentType.YAML,
29+
} as any);
30+
31+
mockComponents.cfnService.describeChangeSet.resolves({
32+
Status: 'CREATE_COMPLETE',
33+
Changes: [{ ResourceChange: { Action: 'Add', LogicalResourceId: 'TestResource' } }],
34+
$metadata: {},
35+
});
36+
mockComponents.cfnService.describeStacks.resolves({
37+
Stacks: [
38+
{
39+
StackName: 'test-stack',
40+
StackStatus: 'CREATE_COMPLETE',
41+
CreationTime: new Date(),
42+
},
43+
],
44+
$metadata: {},
45+
});
46+
mockComponents.cfnService.executeChangeSet.resolves({ $metadata: {} });
47+
mockComponents.cfnService.waitUntilStackUpdateComplete.resolves({ state: WaiterState.SUCCESS });
48+
mockComponents.cfnService.waitUntilStackImportComplete.resolves({ state: WaiterState.SUCCESS });
49+
mockComponents.cfnService.describeStackEvents.resolves({
50+
StackEvents: [
51+
{
52+
EventId: 'event-1',
53+
StackId:
54+
'arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/12345678-1234-1234-1234-123456789012',
55+
StackName: 'test-stack',
56+
LogicalResourceId: 'TestResource',
57+
ResourceStatus: 'CREATE_COMPLETE',
58+
Timestamp: new Date(),
59+
},
60+
],
61+
$metadata: {},
62+
});
63+
64+
deploymentWorkflow = new DeploymentWorkflow(mockComponents.cfnService, mockComponents.documentManager);
65+
});
66+
67+
it('should complete deployment workflow', async () => {
68+
const params: CreateDeploymentParams = {
69+
id: 'test-deployment-1',
70+
changeSetName: 'test-changeset',
71+
stackName: 'test-stack',
72+
};
73+
74+
const result = await deploymentWorkflow.start(params);
75+
await new Promise((resolve) => setTimeout(resolve, 25));
76+
77+
expect(result.id).toBe('test-deployment-1');
78+
expect(result.changeSetName).toBe('test-changeset');
79+
expect(result.stackName).toBe('test-stack');
80+
81+
expect(mockComponents.cfnService.describeChangeSet.called).toBe(true);
82+
expect(mockComponents.cfnService.executeChangeSet.called).toBe(true);
83+
84+
const executeChangeSetArgs = mockComponents.cfnService.executeChangeSet.getCall(0).args[0];
85+
expect(executeChangeSetArgs.ChangeSetName).toBe('test-changeset');
86+
expect(executeChangeSetArgs.StackName).toBe('test-stack');
87+
88+
const status = deploymentWorkflow.getStatus({ id: 'test-deployment-1' });
89+
expect(status.phase).toBe(StackActionPhase.DEPLOYMENT_COMPLETE);
90+
expect(status.state).toBe(StackActionState.SUCCESSFUL);
91+
});
92+
93+
it('should handle import deployment', async () => {
94+
mockComponents.cfnService.describeChangeSet.resolves({
95+
Status: 'CREATE_COMPLETE',
96+
Changes: [{ ResourceChange: { Action: 'Import', LogicalResourceId: 'ImportedResource' } }],
97+
$metadata: {},
98+
});
99+
mockComponents.cfnService.describeStacks.resolves({
100+
Stacks: [
101+
{
102+
StackName: 'test-stack',
103+
StackStatus: 'CREATE_COMPLETE',
104+
CreationTime: new Date(),
105+
},
106+
],
107+
$metadata: {},
108+
});
109+
mockComponents.cfnService.executeChangeSet.resolves({ $metadata: {} });
110+
mockComponents.cfnService.waitUntilStackUpdateComplete.resolves({ state: WaiterState.SUCCESS });
111+
112+
const params: CreateDeploymentParams = {
113+
id: 'test-deployment-import',
114+
changeSetName: 'test-import-changeset',
115+
stackName: 'test-stack',
116+
};
117+
118+
await deploymentWorkflow.start(params);
119+
await new Promise((resolve) => setTimeout(resolve, 25));
120+
121+
expect(mockComponents.cfnService.executeChangeSet.called).toBe(true);
122+
const executeChangeSetArgs = mockComponents.cfnService.executeChangeSet.getCall(0).args[0];
123+
expect(executeChangeSetArgs.ChangeSetName).toBe('test-import-changeset');
124+
125+
const status = deploymentWorkflow.getStatus({ id: 'test-deployment-import' });
126+
expect(status.phase).toBe(StackActionPhase.DEPLOYMENT_COMPLETE);
127+
expect(status.state).toBe(StackActionState.SUCCESSFUL);
128+
});
129+
130+
it('should handle deployment failure', async () => {
131+
mockComponents.cfnService.executeChangeSet.rejects(new Error('Deployment failed'));
132+
133+
const params: CreateDeploymentParams = {
134+
id: 'test-deployment-2',
135+
changeSetName: 'test-changeset',
136+
stackName: 'test-stack',
137+
};
138+
139+
await expect(deploymentWorkflow.start(params)).rejects.toThrow('Deployment failed');
140+
141+
expect(mockComponents.cfnService.executeChangeSet.called).toBe(true);
142+
143+
const status = deploymentWorkflow.getStatus({ id: 'test-deployment-2' });
144+
expect(status.phase).toBe(StackActionPhase.DEPLOYMENT_FAILED);
145+
expect(status.state).toBe(StackActionState.FAILED);
146+
});
147+
148+
it('should handle changeset not found', async () => {
149+
mockComponents.cfnService.describeChangeSet.rejects(new Error('ChangeSet not found'));
150+
151+
const params: CreateDeploymentParams = {
152+
id: 'test-deployment-3',
153+
changeSetName: 'non-existent-changeset',
154+
stackName: 'test-stack',
155+
};
156+
157+
await expect(deploymentWorkflow.start(params)).rejects.toThrow('ChangeSet not found');
158+
159+
expect(mockComponents.cfnService.executeChangeSet.called).toBe(false);
160+
161+
const status = deploymentWorkflow.getStatus({ id: 'test-deployment-3' });
162+
expect(status.phase).toBe(StackActionPhase.DEPLOYMENT_FAILED);
163+
expect(status.state).toBe(StackActionState.FAILED);
164+
});
165+
});
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
import { readFileSync } from 'fs';
2+
import { resolve } from 'path';
3+
import { load } from 'js-yaml';
4+
import { describe, it, expect } from 'vitest';
5+
import { ArtifactExporter } from '../../../src/artifactexporter/ArtifactExporter';
6+
import { DocumentType } from '../../../src/document/Document';
7+
import { createMockComponents } from '../../utils/MockServerComponents';
8+
9+
describe('Upload Artifact to S3', () => {
10+
describe('YAML template', () => {
11+
it('should identify template artifacts', () => {
12+
const mockComponents = createMockComponents();
13+
const mockS3Service = mockComponents.s3Service;
14+
15+
const templatePath = resolve(__dirname, '../../resources/templates/template_with_folder_artifact.yaml');
16+
const templateContent = readFileSync(templatePath, 'utf8');
17+
const exporter = new ArtifactExporter(mockS3Service, DocumentType.YAML, templatePath, templateContent);
18+
const artifacts = exporter.getTemplateArtifacts();
19+
20+
expect(artifacts).toHaveLength(1);
21+
expect(artifacts[0].resourceType).toBe('AWS::Serverless::Function');
22+
expect(artifacts[0].filePath).toBe('./artifact/code');
23+
});
24+
25+
it('should zip and upload folder artifact to S3', async () => {
26+
const mockComponents = createMockComponents();
27+
const mockS3Service = mockComponents.s3Service;
28+
mockS3Service.putObject.resolves({ VersionId: 'v1', $metadata: {} });
29+
30+
const templatePath = resolve(__dirname, '../../resources/templates/template_with_folder_artifact.yaml');
31+
const originalContent = readFileSync(templatePath, 'utf8');
32+
const originalTemplate = load(originalContent) as any;
33+
34+
const exporter = new ArtifactExporter(mockS3Service, DocumentType.YAML, templatePath, originalContent);
35+
const exportedTemplate = (await exporter.export('test-bucket', 'test-prefix')) as any;
36+
37+
expect(mockS3Service.putObject.called).toBe(true);
38+
const [filePath, s3Url] = mockS3Service.putObject.getCall(0).args;
39+
expect(filePath).toMatch(/\.zip$/);
40+
expect(s3Url).toMatch(/^s3:\/\/test-bucket\/test-prefix\/artifact\//);
41+
42+
expect(exportedTemplate.Resources.MyFunction.Properties.CodeUri).toMatch(
43+
/^s3:\/\/test-bucket\/test-prefix\/artifact\//,
44+
);
45+
46+
delete originalTemplate.Resources.MyFunction.Properties.CodeUri;
47+
delete exportedTemplate.Resources.MyFunction.Properties.CodeUri;
48+
49+
expect(JSON.stringify(exportedTemplate)).toBe(JSON.stringify(originalTemplate));
50+
});
51+
52+
it('should upload file artifact to S3', async () => {
53+
const mockComponents = createMockComponents();
54+
const mockS3Service = mockComponents.s3Service;
55+
mockS3Service.putObject.resolves({ VersionId: 'v1', $metadata: {} });
56+
57+
const templatePath = resolve(__dirname, '../../resources/templates/template_with_file_artifact.yaml');
58+
const originalContent = readFileSync(templatePath, 'utf8');
59+
const originalTemplate = load(originalContent) as any;
60+
61+
const exporter = new ArtifactExporter(mockS3Service, DocumentType.YAML, templatePath, originalContent);
62+
const exportedTemplate = (await exporter.export('test-bucket', 'test-prefix')) as any;
63+
64+
expect(exportedTemplate.Resources.MyApi.Properties.DefinitionUri).toMatch(
65+
/^s3:\/\/test-bucket\/test-prefix\/artifact\//,
66+
);
67+
68+
delete originalTemplate.Resources.MyApi.Properties.DefinitionUri;
69+
delete exportedTemplate.Resources.MyApi.Properties.DefinitionUri;
70+
71+
expect(JSON.stringify(exportedTemplate)).toBe(JSON.stringify(originalTemplate));
72+
});
73+
74+
it('should do nothing when no artifacts exist', async () => {
75+
const mockComponents = createMockComponents();
76+
const mockS3Service = mockComponents.s3Service;
77+
78+
const templatePath = resolve(__dirname, '../../resources/templates/simple.yaml');
79+
const templateContent = readFileSync(templatePath, 'utf8');
80+
const originalTemplate = load(templateContent) as any;
81+
82+
const exporter = new ArtifactExporter(mockS3Service, DocumentType.YAML, templatePath, templateContent);
83+
const exportedTemplate = (await exporter.export('test-bucket', 'test-prefix')) as any;
84+
85+
expect(mockS3Service.putObject.called).toBe(false);
86+
87+
expect(JSON.stringify(exportedTemplate)).toBe(JSON.stringify(originalTemplate));
88+
});
89+
90+
it('should handle S3 upload failures', async () => {
91+
const mockComponents = createMockComponents();
92+
const mockS3Service = mockComponents.s3Service;
93+
mockS3Service.putObject.rejects(new Error('S3 upload failed'));
94+
95+
const templatePath = resolve(__dirname, '../../resources/templates/template_with_folder_artifact.yaml');
96+
const templateContent = readFileSync(templatePath, 'utf8');
97+
98+
const exporter = new ArtifactExporter(mockS3Service, DocumentType.YAML, templatePath, templateContent);
99+
100+
await expect(exporter.export('test-bucket', 'test-prefix')).rejects.toThrow('S3 upload failed');
101+
});
102+
103+
it('should throw exception for broken templates', () => {
104+
const mockComponents = createMockComponents();
105+
const mockS3Service = mockComponents.s3Service;
106+
107+
const templatePath = resolve(__dirname, '../../resources/templates/broken.yaml');
108+
const templateContent = readFileSync(templatePath, 'utf8');
109+
110+
expect(() => {
111+
new ArtifactExporter(mockS3Service, DocumentType.YAML, templatePath, templateContent);
112+
}).toThrow();
113+
});
114+
});
115+
116+
describe('JSON template', () => {
117+
it('should identify template artifacts', () => {
118+
const mockComponents = createMockComponents();
119+
const mockS3Service = mockComponents.s3Service;
120+
121+
const templatePath = resolve(__dirname, '../../resources/templates/template_with_folder_artifact.json');
122+
const templateContent = readFileSync(templatePath, 'utf8');
123+
const exporter = new ArtifactExporter(mockS3Service, DocumentType.JSON, templatePath, templateContent);
124+
const artifacts = exporter.getTemplateArtifacts();
125+
126+
expect(artifacts).toHaveLength(1);
127+
expect(artifacts[0].resourceType).toBe('AWS::Serverless::Function');
128+
expect(artifacts[0].filePath).toBe('./artifact/code');
129+
});
130+
131+
it('should zip and upload folder artifact to S3', async () => {
132+
const mockComponents = createMockComponents();
133+
const mockS3Service = mockComponents.s3Service;
134+
mockS3Service.putObject.resolves({ VersionId: 'v1', $metadata: {} });
135+
136+
const templatePath = resolve(__dirname, '../../resources/templates/template_with_folder_artifact.json');
137+
const originalContent = readFileSync(templatePath, 'utf8');
138+
const originalTemplate = JSON.parse(originalContent);
139+
140+
const exporter = new ArtifactExporter(mockS3Service, DocumentType.JSON, templatePath, originalContent);
141+
const exportedTemplate = (await exporter.export('test-bucket', 'test-prefix')) as any;
142+
143+
expect(mockS3Service.putObject.called).toBe(true);
144+
const [filePath, s3Url] = mockS3Service.putObject.getCall(0).args;
145+
expect(filePath).toMatch(/\.zip$/);
146+
expect(s3Url).toMatch(/^s3:\/\/test-bucket\/test-prefix\/artifact\//);
147+
148+
expect(exportedTemplate.Resources.MyFunction.Properties.CodeUri).toMatch(
149+
/^s3:\/\/test-bucket\/test-prefix\/artifact\//,
150+
);
151+
152+
delete originalTemplate.Resources.MyFunction.Properties.CodeUri;
153+
delete exportedTemplate.Resources.MyFunction.Properties.CodeUri;
154+
155+
expect(JSON.stringify(exportedTemplate)).toBe(JSON.stringify(originalTemplate));
156+
});
157+
158+
it('should upload file artifact to S3', async () => {
159+
const mockComponents = createMockComponents();
160+
const mockS3Service = mockComponents.s3Service;
161+
mockS3Service.putObject.resolves({ VersionId: 'v1', $metadata: {} });
162+
163+
const templatePath = resolve(__dirname, '../../resources/templates/template_with_file_artifact.json');
164+
const originalContent = readFileSync(templatePath, 'utf8');
165+
const originalTemplate = JSON.parse(originalContent);
166+
167+
const exporter = new ArtifactExporter(mockS3Service, DocumentType.JSON, templatePath, originalContent);
168+
const exportedTemplate = (await exporter.export('test-bucket', 'test-prefix')) as any;
169+
170+
expect(exportedTemplate.Resources.MyApi.Properties.DefinitionUri).toMatch(
171+
/^s3:\/\/test-bucket\/test-prefix\/artifact\//,
172+
);
173+
174+
delete originalTemplate.Resources.MyApi.Properties.DefinitionUri;
175+
delete exportedTemplate.Resources.MyApi.Properties.DefinitionUri;
176+
177+
expect(JSON.stringify(exportedTemplate)).toBe(JSON.stringify(originalTemplate));
178+
});
179+
180+
it('should do nothing when no artifacts exist', async () => {
181+
const mockComponents = createMockComponents();
182+
const mockS3Service = mockComponents.s3Service;
183+
184+
const templatePath = resolve(__dirname, '../../resources/templates/simple.json');
185+
const templateContent = readFileSync(templatePath, 'utf8');
186+
const originalTemplate = JSON.parse(templateContent);
187+
188+
const exporter = new ArtifactExporter(mockS3Service, DocumentType.JSON, templatePath, templateContent);
189+
const exportedTemplate = (await exporter.export('test-bucket', 'test-prefix')) as any;
190+
191+
expect(mockS3Service.putObject.called).toBe(false);
192+
193+
expect(JSON.stringify(exportedTemplate)).toBe(JSON.stringify(originalTemplate));
194+
});
195+
196+
it('should handle S3 upload failures', async () => {
197+
const mockComponents = createMockComponents();
198+
const mockS3Service = mockComponents.s3Service;
199+
mockS3Service.putObject.rejects(new Error('S3 upload failed'));
200+
201+
const templatePath = resolve(__dirname, '../../resources/templates/template_with_folder_artifact.json');
202+
const templateContent = readFileSync(templatePath, 'utf8');
203+
204+
const exporter = new ArtifactExporter(mockS3Service, DocumentType.JSON, templatePath, templateContent);
205+
206+
await expect(exporter.export('test-bucket', 'test-prefix')).rejects.toThrow('S3 upload failed');
207+
});
208+
209+
it('should throw exception for broken templates', () => {
210+
const mockComponents = createMockComponents();
211+
const mockS3Service = mockComponents.s3Service;
212+
213+
const templatePath = resolve(__dirname, '../../resources/templates/broken.json');
214+
const templateContent = readFileSync(templatePath, 'utf8');
215+
216+
expect(() => {
217+
new ArtifactExporter(mockS3Service, DocumentType.JSON, templatePath, templateContent);
218+
}).toThrow();
219+
});
220+
});
221+
});

0 commit comments

Comments
 (0)