Skip to content

Commit 529e079

Browse files
committed
Add e2e tests for deployment
1 parent e99f68d commit 529e079

10 files changed

+742
-0
lines changed
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import { readFileSync } from 'fs';
2+
import { WaiterState } from '@smithy/util-waiter';
3+
import { describe, it, expect, beforeEach } from 'vitest';
4+
import { DocumentType } from '../../../src/document/Document';
5+
import { DeploymentWorkflow } from '../../../src/stacks/actions/DeploymentWorkflow';
6+
import {
7+
CreateDeploymentParams,
8+
StackActionPhase,
9+
StackActionState,
10+
} from '../../../src/stacks/actions/StackActionRequestType';
11+
import { createMockComponents } from '../../utils/MockServerComponents';
12+
13+
const TEMPLATE_PATH = '/Volumes/workplace/IDEPlugin/cloudformation-languageserver/tst/resources/templates/simple.yaml';
14+
const TEST_TEMPLATE_URI = `file://${TEMPLATE_PATH}`;
15+
16+
describe('DeploymentWorkflow', () => {
17+
let mockComponents: ReturnType<typeof createMockComponents>;
18+
let deploymentWorkflow: DeploymentWorkflow;
19+
20+
beforeEach(() => {
21+
mockComponents = createMockComponents();
22+
23+
const templateContent = readFileSync(TEMPLATE_PATH, 'utf8');
24+
mockComponents.documentManager.get.withArgs(TEST_TEMPLATE_URI).returns({
25+
contents: () => templateContent,
26+
uri: TEST_TEMPLATE_URI,
27+
documentType: DocumentType.YAML,
28+
} as any);
29+
30+
mockComponents.cfnService.describeChangeSet.resolves({
31+
Status: 'CREATE_COMPLETE',
32+
Changes: [{ ResourceChange: { Action: 'Add', LogicalResourceId: 'TestResource' } }],
33+
$metadata: {},
34+
});
35+
mockComponents.cfnService.describeStacks.resolves({
36+
Stacks: [
37+
{
38+
StackName: 'test-stack',
39+
StackStatus: 'CREATE_COMPLETE',
40+
CreationTime: new Date(),
41+
},
42+
],
43+
$metadata: {},
44+
});
45+
mockComponents.cfnService.executeChangeSet.resolves({ $metadata: {} });
46+
mockComponents.cfnService.waitUntilStackUpdateComplete.resolves({ state: WaiterState.SUCCESS });
47+
mockComponents.cfnService.waitUntilStackImportComplete.resolves({ state: WaiterState.SUCCESS });
48+
mockComponents.cfnService.describeStackEvents.resolves({
49+
StackEvents: [
50+
{
51+
EventId: 'event-1',
52+
StackId:
53+
'arn:aws:cloudformation:us-east-1:123456789012:stack/test-stack/12345678-1234-1234-1234-123456789012',
54+
StackName: 'test-stack',
55+
LogicalResourceId: 'TestResource',
56+
ResourceStatus: 'CREATE_COMPLETE',
57+
Timestamp: new Date(),
58+
},
59+
],
60+
$metadata: {},
61+
});
62+
63+
deploymentWorkflow = new DeploymentWorkflow(mockComponents.cfnService, mockComponents.documentManager);
64+
});
65+
66+
it('should complete deployment workflow', async () => {
67+
const params: CreateDeploymentParams = {
68+
id: 'test-deployment-1',
69+
changeSetName: 'test-changeset',
70+
stackName: 'test-stack',
71+
};
72+
73+
const result = await deploymentWorkflow.start(params);
74+
await new Promise((resolve) => setTimeout(resolve, 25));
75+
76+
expect(result.id).toBe('test-deployment-1');
77+
expect(result.changeSetName).toBe('test-changeset');
78+
expect(result.stackName).toBe('test-stack');
79+
80+
expect(mockComponents.cfnService.describeChangeSet.called).toBe(true);
81+
expect(mockComponents.cfnService.executeChangeSet.called).toBe(true);
82+
83+
const executeChangeSetArgs = mockComponents.cfnService.executeChangeSet.getCall(0).args[0];
84+
expect(executeChangeSetArgs.ChangeSetName).toBe('test-changeset');
85+
expect(executeChangeSetArgs.StackName).toBe('test-stack');
86+
87+
const status = deploymentWorkflow.getStatus({ id: 'test-deployment-1' });
88+
expect(status.phase).toBe(StackActionPhase.DEPLOYMENT_COMPLETE);
89+
expect(status.state).toBe(StackActionState.SUCCESSFUL);
90+
});
91+
92+
it('should handle import deployment', async () => {
93+
mockComponents.cfnService.describeChangeSet.resolves({
94+
Status: 'CREATE_COMPLETE',
95+
Changes: [{ ResourceChange: { Action: 'Import', LogicalResourceId: 'ImportedResource' } }],
96+
$metadata: {},
97+
});
98+
mockComponents.cfnService.describeStacks.resolves({
99+
Stacks: [
100+
{
101+
StackName: 'test-stack',
102+
StackStatus: 'CREATE_COMPLETE',
103+
CreationTime: new Date(),
104+
},
105+
],
106+
$metadata: {},
107+
});
108+
mockComponents.cfnService.executeChangeSet.resolves({ $metadata: {} });
109+
mockComponents.cfnService.waitUntilStackUpdateComplete.resolves({ state: WaiterState.SUCCESS });
110+
111+
const params: CreateDeploymentParams = {
112+
id: 'test-deployment-import',
113+
changeSetName: 'test-import-changeset',
114+
stackName: 'test-stack',
115+
};
116+
117+
await deploymentWorkflow.start(params);
118+
await new Promise((resolve) => setTimeout(resolve, 25));
119+
120+
expect(mockComponents.cfnService.executeChangeSet.called).toBe(true);
121+
const executeChangeSetArgs = mockComponents.cfnService.executeChangeSet.getCall(0).args[0];
122+
expect(executeChangeSetArgs.ChangeSetName).toBe('test-import-changeset');
123+
124+
const status = deploymentWorkflow.getStatus({ id: 'test-deployment-import' });
125+
expect(status.phase).toBe(StackActionPhase.DEPLOYMENT_COMPLETE);
126+
expect(status.state).toBe(StackActionState.SUCCESSFUL);
127+
});
128+
129+
it('should handle deployment failure', async () => {
130+
mockComponents.cfnService.executeChangeSet.rejects(new Error('Deployment failed'));
131+
132+
const params: CreateDeploymentParams = {
133+
id: 'test-deployment-2',
134+
changeSetName: 'test-changeset',
135+
stackName: 'test-stack',
136+
};
137+
138+
await expect(deploymentWorkflow.start(params)).rejects.toThrow('Deployment failed');
139+
140+
expect(mockComponents.cfnService.executeChangeSet.called).toBe(true);
141+
142+
const status = deploymentWorkflow.getStatus({ id: 'test-deployment-2' });
143+
expect(status.phase).toBe(StackActionPhase.DEPLOYMENT_FAILED);
144+
expect(status.state).toBe(StackActionState.FAILED);
145+
});
146+
147+
it('should handle changeset not found', async () => {
148+
mockComponents.cfnService.describeChangeSet.rejects(new Error('ChangeSet not found'));
149+
150+
const params: CreateDeploymentParams = {
151+
id: 'test-deployment-3',
152+
changeSetName: 'non-existent-changeset',
153+
stackName: 'test-stack',
154+
};
155+
156+
await expect(deploymentWorkflow.start(params)).rejects.toThrow('ChangeSet not found');
157+
158+
expect(mockComponents.cfnService.executeChangeSet.called).toBe(false);
159+
160+
const status = deploymentWorkflow.getStatus({ id: 'test-deployment-3' });
161+
expect(status.phase).toBe(StackActionPhase.DEPLOYMENT_FAILED);
162+
expect(status.state).toBe(StackActionState.FAILED);
163+
});
164+
});
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)