Skip to content

Commit 4ed802a

Browse files
author
Deep Furiya
committed
added new unit tests to validate forEach entities
1 parent 7797ef3 commit 4ed802a

File tree

6 files changed

+209
-8
lines changed

6 files changed

+209
-8
lines changed

src/context/semantic/Entity.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,9 @@ export class Unknown extends Entity {
181181
export class ForEachResource extends Entity {
182182
constructor(
183183
public readonly name: string,
184-
public readonly identifier: string,
185-
public readonly collection: CfnValue,
186-
public readonly resource: Resource,
184+
public readonly identifier?: string,
185+
public readonly collection?: CfnValue,
186+
public readonly resource?: Resource,
187187
) {
188188
super(EntityType.ForEachResource);
189189
}

src/context/semantic/EntityBuilder.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,11 @@ export function createEntityFromObject(logicalId: string, entityObject: any, sec
6262
case TopLevelSection.Resources: {
6363
if (logicalId.startsWith('Fn::ForEach')) {
6464
const loopName = logicalId.replace('Fn::ForEach::', '');
65-
const identifier = Array.isArray(entityObject) ? entityObject[0] : '';
65+
const identifier = Array.isArray(entityObject) ? entityObject[0] : undefined;
6666
const collection = Array.isArray(entityObject) ? entityObject[1] : undefined;
6767
const outputMap = Array.isArray(entityObject) ? entityObject[2] : {};
68-
const [key, value]: [string, any] = Object.entries(outputMap ?? {})[0] || ['', {}];
69-
const resource = new Resource(
68+
const [key, value]: [string, any] = Object.entries(outputMap ?? {})[0] || [undefined, {}];
69+
const resourceInsideForEach = new Resource(
7070
key,
7171
value?.Type,
7272
value?.Properties,
@@ -78,7 +78,7 @@ export function createEntityFromObject(logicalId: string, entityObject: any, sec
7878
value?.UpdatePolicy,
7979
value?.UpdateReplacePolicy,
8080
);
81-
return new ForEachResource(loopName, identifier, collection, resource);
81+
return new ForEachResource(loopName, identifier, collection, resourceInsideForEach);
8282
}
8383
return new Resource(
8484
logicalId,
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"AWSTemplateFormatVersion": "2010-09-09",
3+
"Description": "Template with Fn::ForEach examples",
4+
"Transform": "AWS::LanguageExtensions",
5+
"Parameters": {
6+
"BucketNames": {
7+
"Type": "CommaDelimitedList",
8+
"Default": "bucket1,bucket2,bucket3",
9+
"Description": "List of bucket names for ForEach"
10+
}
11+
},
12+
"Resources": {
13+
"Fn::ForEach::Buckets": [
14+
"BucketName",
15+
{ "Ref": "BucketNames" },
16+
{
17+
"S3Bucket${BucketName}": {
18+
"Type": "AWS::S3::Bucket",
19+
"Properties": {
20+
"BucketName": { "Fn::Sub": "${BucketName}-${AWS::AccountId}-${AWS::Region}" },
21+
"VersioningConfiguration": {
22+
"Status": "Enabled"
23+
},
24+
"BucketEncryption": {
25+
"ServerSideEncryptionConfiguration": [
26+
{
27+
"ServerSideEncryptionByDefault": {
28+
"SSEAlgorithm": "AES256"
29+
}
30+
}
31+
]
32+
}
33+
}
34+
}
35+
}
36+
],
37+
"RegularResource": {
38+
"Type": "AWS::S3::Bucket",
39+
"Properties": {
40+
"BucketName": "regular-bucket"
41+
}
42+
}
43+
}
44+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
AWSTemplateFormatVersion: "2010-09-09"
2+
Description: "Template with Fn::ForEach examples"
3+
Transform: AWS::LanguageExtensions
4+
5+
Parameters:
6+
BucketNames:
7+
Type: CommaDelimitedList
8+
Default: "bucket1,bucket2,bucket3"
9+
Description: "List of bucket names for ForEach"
10+
11+
Resources:
12+
Fn::ForEach::Buckets:
13+
- BucketName
14+
- !Ref BucketNames
15+
- S3Bucket${BucketName}:
16+
Type: AWS::S3::Bucket
17+
Properties:
18+
BucketName: !Sub "${BucketName}-${AWS::AccountId}-${AWS::Region}"
19+
VersioningConfiguration:
20+
Status: Enabled
21+
BucketEncryption:
22+
ServerSideEncryptionConfiguration:
23+
- ServerSideEncryptionByDefault:
24+
SSEAlgorithm: AES256
25+
26+
RegularResource:
27+
Type: AWS::S3::Bucket
28+
Properties:
29+
BucketName: regular-bucket

tst/unit/context/Context.test.ts

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
22
import { Context } from '../../../src/context/Context';
33
import { ContextManager } from '../../../src/context/ContextManager';
44
import { TopLevelSection } from '../../../src/context/ContextType';
5-
import { Parameter, Resource, Condition, Mapping, Unknown } from '../../../src/context/semantic/Entity';
5+
import {
6+
Parameter,
7+
Resource,
8+
Condition,
9+
Mapping,
10+
Unknown,
11+
ForEachResource,
12+
} from '../../../src/context/semantic/Entity';
613
import { SyntaxTreeManager } from '../../../src/context/syntaxtree/SyntaxTreeManager';
714
import { docPosition, Templates } from '../../utils/TemplateUtils';
815

@@ -679,4 +686,111 @@ Resources:
679686
expect(context!.textInQuotes()).toBeUndefined();
680687
});
681688
});
689+
690+
describe('ForEachResource Entity Parsing', () => {
691+
const foreachYamlUri = Templates.foreach.yaml.fileName;
692+
const foreachJsonUri = Templates.foreach.json.fileName;
693+
const foreachYaml = Templates.foreach.yaml.contents;
694+
const foreachJson = Templates.foreach.json.contents;
695+
696+
beforeAll(() => {
697+
syntaxTreeManager.add(foreachYamlUri, foreachYaml);
698+
syntaxTreeManager.add(foreachJsonUri, foreachJson);
699+
});
700+
701+
describe('YAML ForEach', () => {
702+
it('should parse ForEach resource name', () => {
703+
const context = getContextAt(11, 4, foreachYamlUri); // Position at "Fn::ForEach::Buckets:"
704+
705+
expect(context).toBeDefined();
706+
expect(context!.section).toBe(TopLevelSection.Resources);
707+
expect(context!.logicalId).toBe('Fn::ForEach::Buckets');
708+
expect(context!.text).toBe('Fn::ForEach::Buckets');
709+
});
710+
711+
it('should create ForEachResource entity with correct properties', () => {
712+
const context = getContextAt(11, 4, foreachYamlUri); // Fn::ForEach::Buckets
713+
714+
expect(context).toBeDefined();
715+
const entity = context!.entity;
716+
expect(entity).toBeInstanceOf(ForEachResource);
717+
718+
const forEachResource = entity as ForEachResource;
719+
expect(forEachResource.name).toBe('Buckets');
720+
expect(forEachResource.identifier).toBe('BucketName');
721+
expect(forEachResource.collection).toBeDefined();
722+
expect(forEachResource.collection).toHaveProperty('!Ref', 'BucketNames');
723+
expect(forEachResource.resource).toBeInstanceOf(Resource);
724+
});
725+
726+
it('should parse nested resource in ForEach', () => {
727+
const context = getContextAt(11, 4, foreachYamlUri);
728+
729+
expect(context).toBeDefined();
730+
const entity = context!.entity as ForEachResource;
731+
const nestedResource = entity.resource;
732+
733+
expect(nestedResource).toBeInstanceOf(Resource);
734+
expect(nestedResource?.name).toBe('S3Bucket${BucketName}');
735+
expect(nestedResource?.Type).toBe('AWS::S3::Bucket');
736+
expect(nestedResource?.Properties).toBeDefined();
737+
expect(nestedResource?.Properties).toHaveProperty('BucketName');
738+
expect(nestedResource?.Properties).toHaveProperty('VersioningConfiguration');
739+
expect(nestedResource?.Properties?.VersioningConfiguration).toHaveProperty('Status', 'Enabled');
740+
expect(nestedResource?.Properties).toHaveProperty('BucketEncryption');
741+
});
742+
743+
it('should parse regular resource after ForEach', () => {
744+
const context = getContextAt(26, 4, foreachYamlUri); // Position at "RegularResource:"
745+
746+
expect(context).toBeDefined();
747+
expect(context!.section).toBe(TopLevelSection.Resources);
748+
expect(context!.logicalId).toBe('RegularResource');
749+
expect(context!.entity).toBeInstanceOf(Resource);
750+
expect(context!.entity).not.toBeInstanceOf(ForEachResource);
751+
});
752+
});
753+
754+
describe('JSON ForEach', () => {
755+
it('should parse ForEach resource name in JSON', () => {
756+
const context = getContextAt(12, 8, foreachJsonUri); // Position at "Fn::ForEach::Buckets"
757+
758+
expect(context).toBeDefined();
759+
expect(context!.section).toBe(TopLevelSection.Resources);
760+
expect(context!.logicalId).toBe('Fn::ForEach::Buckets');
761+
});
762+
763+
it('should create ForEachResource entity from JSON', () => {
764+
const context = getContextAt(12, 8, foreachJsonUri);
765+
766+
expect(context).toBeDefined();
767+
const entity = context!.entity;
768+
expect(entity).toBeInstanceOf(ForEachResource);
769+
770+
const forEachResource = entity as ForEachResource;
771+
expect(forEachResource.name).toBe('Buckets');
772+
expect(forEachResource.identifier).toBe('BucketName');
773+
expect(forEachResource.collection).toBeDefined();
774+
expect(forEachResource.collection).toHaveProperty('Ref', 'BucketNames');
775+
expect(forEachResource.resource).toBeInstanceOf(Resource);
776+
});
777+
778+
it('should parse nested resource in JSON ForEach', () => {
779+
const context = getContextAt(12, 8, foreachJsonUri);
780+
781+
expect(context).toBeDefined();
782+
const entity = context!.entity as ForEachResource;
783+
const nestedResource = entity.resource;
784+
785+
expect(nestedResource).toBeInstanceOf(Resource);
786+
expect(nestedResource?.name).toBe('S3Bucket${BucketName}');
787+
expect(nestedResource?.Type).toBe('AWS::S3::Bucket');
788+
expect(nestedResource?.Properties).toBeDefined();
789+
expect(nestedResource?.Properties).toHaveProperty('BucketName');
790+
expect(nestedResource?.Properties?.BucketName).toHaveProperty('Fn::Sub');
791+
expect(nestedResource?.Properties).toHaveProperty('VersioningConfiguration');
792+
expect(nestedResource?.Properties?.VersioningConfiguration).toHaveProperty('Status', 'Enabled');
793+
});
794+
});
795+
});
682796
});

tst/utils/TemplateUtils.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,20 @@ export const Templates: Record<string, Record<'json' | 'yaml', { fileName: strin
8989
},
9090
},
9191
},
92+
foreach: {
93+
json: {
94+
fileName: 'file://foreach_template.json',
95+
get contents() {
96+
return readFileSync(join(__dirname, '..', 'resources', 'templates', 'foreach_template.json'), 'utf8');
97+
},
98+
},
99+
yaml: {
100+
fileName: 'file://foreach_template.yaml',
101+
get contents() {
102+
return readFileSync(join(__dirname, '..', 'resources', 'templates', 'foreach_template.yaml'), 'utf8');
103+
},
104+
},
105+
},
92106
};
93107

94108
export function point(row: number, column: number): Point {

0 commit comments

Comments
 (0)