Skip to content

Commit 11a18ae

Browse files
authored
Merge pull request #19 from ModusCreateOrg/ADE-98
[ADE-98] Setting up ALB with Cognito
2 parents 611626e + eb094e3 commit 11a18ae

File tree

5 files changed

+423
-128
lines changed

5 files changed

+423
-128
lines changed

backend/package-lock.json

Lines changed: 40 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 213 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,84 +1,239 @@
11
import * as cdk from 'aws-cdk-lib';
2-
import { Template } from 'aws-cdk-lib/assertions';
2+
import { Template, Match } from 'aws-cdk-lib/assertions';
33
import { BackendStack } from './backend-stack';
44

55
describe('BackendStack', () => {
6-
const app = new cdk.App();
7-
const stack = new BackendStack(app, 'TestStack');
8-
const template = Template.fromStack(stack);
9-
10-
it('should create a VPC', () => {
11-
template.hasResourceProperties('AWS::EC2::VPC', {
12-
EnableDnsHostnames: true,
13-
EnableDnsSupport: true,
14-
CidrBlock: '10.0.0.0/16',
15-
InstanceTenancy: 'default',
6+
let app: cdk.App;
7+
let stackProps: cdk.StackProps;
8+
let stagingStack: BackendStack;
9+
let productionStack: BackendStack;
10+
let stagingTemplate: Template;
11+
let productionTemplate: Template;
12+
13+
beforeEach(() => {
14+
app = new cdk.App();
15+
stackProps = {
16+
env: { account: '123456789012', region: 'us-east-1' }
17+
};
18+
19+
// Create both staging and production stacks for testing
20+
stagingStack = new BackendStack(app, 'StagingStack', {
21+
...stackProps,
22+
environment: 'staging'
1623
});
24+
stagingTemplate = Template.fromStack(stagingStack);
1725

18-
// Verify we have the correct number of subnets for 2 AZs
19-
template.resourceCountIs('AWS::EC2::Subnet', 4); // 2 public + 2 private subnets
26+
productionStack = new BackendStack(app, 'ProductionStack', {
27+
...stackProps,
28+
environment: 'production'
29+
});
30+
productionTemplate = Template.fromStack(productionStack);
2031
});
2132

22-
it('should create an ECS cluster', () => {
23-
template.hasResourceProperties('AWS::ECS::Cluster', {
24-
ClusterSettings: [
25-
{
26-
Name: 'containerInsights',
27-
Value: 'enabled',
28-
},
29-
],
33+
describe('VPC Resources', () => {
34+
it('should create a VPC with the correct configuration', () => {
35+
// Test VPC in staging
36+
stagingTemplate.hasResourceProperties('AWS::EC2::VPC', {
37+
CidrBlock: '10.0.0.0/16',
38+
EnableDnsHostnames: true,
39+
EnableDnsSupport: true,
40+
Tags: Match.arrayWith([
41+
{
42+
Key: 'Name',
43+
Value: Match.stringLikeRegexp('AIMedicalReportVPC')
44+
}
45+
])
46+
});
47+
48+
// Verify subnet count for staging (2 AZs = 4 subnets)
49+
stagingTemplate.resourceCountIs('AWS::EC2::Subnet', 4);
50+
51+
// Verify subnet count for production (3 AZs = 6 subnets)
52+
productionTemplate.resourceCountIs('AWS::EC2::Subnet', 6);
3053
});
3154
});
3255

33-
it('should create a Fargate service', () => {
34-
template.hasResourceProperties('AWS::ECS::Service', {
35-
LaunchType: 'FARGATE',
36-
DesiredCount: 2,
56+
describe('ECS Resources', () => {
57+
it('should create an ECS cluster with container insights', () => {
58+
stagingTemplate.hasResourceProperties('AWS::ECS::Cluster', {
59+
ClusterName: 'AIMedicalReportCluster',
60+
ClusterSettings: [
61+
{
62+
Name: 'containerInsights',
63+
Value: 'enabled'
64+
}
65+
]
66+
});
3767
});
3868

39-
template.hasResourceProperties('AWS::ECS::TaskDefinition', {
40-
ContainerDefinitions: [
41-
{
42-
Environment: [
43-
{
44-
Name: 'NODE_ENV',
45-
Value: 'production',
46-
},
47-
],
48-
PortMappings: [
49-
{
50-
ContainerPort: 3000,
51-
Protocol: 'tcp',
52-
},
53-
],
54-
},
55-
],
56-
Cpu: '256',
57-
Memory: '512',
58-
NetworkMode: 'awsvpc',
59-
RequiresCompatibilities: ['FARGATE'],
69+
it('should create a Fargate task definition with correct resources', () => {
70+
// Test staging task definition
71+
stagingTemplate.hasResourceProperties('AWS::ECS::TaskDefinition', {
72+
Cpu: '256',
73+
Memory: '512',
74+
NetworkMode: 'awsvpc',
75+
RequiresCompatibilities: ['FARGATE'],
76+
ContainerDefinitions: Match.arrayWith([
77+
Match.objectLike({
78+
Environment: Match.arrayWith([
79+
{ Name: 'NODE_ENV', Value: 'staging' },
80+
{ Name: 'PERPLEXITY_MODEL', Value: 'sonar' },
81+
{ Name: 'PERPLEXITY_MAX_TOKENS', Value: '2048' }
82+
]),
83+
LogConfiguration: Match.objectLike({
84+
LogDriver: 'awslogs'
85+
}),
86+
PortMappings: [
87+
{
88+
ContainerPort: 3000,
89+
Protocol: 'tcp'
90+
}
91+
]
92+
})
93+
])
94+
});
95+
96+
// Test production task definition has higher resources
97+
productionTemplate.hasResourceProperties('AWS::ECS::TaskDefinition', {
98+
Cpu: '512',
99+
Memory: '1024'
100+
});
101+
});
102+
103+
it('should create a Fargate service with correct configuration', () => {
104+
// Test staging service
105+
stagingTemplate.hasResourceProperties('AWS::ECS::Service', {
106+
LaunchType: 'FARGATE',
107+
DesiredCount: 1,
108+
NetworkConfiguration: Match.objectLike({
109+
AwsvpcConfiguration: {
110+
AssignPublicIp: 'DISABLED'
111+
}
112+
})
113+
});
114+
115+
// Test production service has higher count
116+
productionTemplate.hasResourceProperties('AWS::ECS::Service', {
117+
DesiredCount: 2
118+
});
119+
});
120+
121+
it('should create auto-scaling only for production', () => {
122+
// Staging should not have auto-scaling
123+
stagingTemplate.resourceCountIs('AWS::ApplicationAutoScaling::ScalableTarget', 0);
124+
125+
// Production should have auto-scaling
126+
productionTemplate.hasResourceProperties('AWS::ApplicationAutoScaling::ScalableTarget', {
127+
MinCapacity: 2,
128+
MaxCapacity: 10
129+
});
130+
131+
productionTemplate.hasResourceProperties('AWS::ApplicationAutoScaling::ScalingPolicy', {
132+
TargetTrackingScalingPolicyConfiguration: {
133+
TargetValue: 70
134+
}
135+
});
60136
});
61137
});
62138

63-
it('should create an API Gateway', () => {
64-
template.hasResourceProperties('AWS::ApiGateway::RestApi', {
65-
Name: 'Medical Reports API',
139+
describe('CloudWatch Logs', () => {
140+
it('should create log groups with correct retention', () => {
141+
// Staging log group with 1 week retention
142+
stagingTemplate.hasResourceProperties('AWS::Logs::LogGroup', {
143+
LogGroupName: '/ecs/AIMedicalReport-staging',
144+
RetentionInDays: 7
145+
});
146+
147+
// Production log group with 1 month retention
148+
productionTemplate.hasResourceProperties('AWS::Logs::LogGroup', {
149+
LogGroupName: '/ecs/AIMedicalReport-production',
150+
RetentionInDays: 30
151+
});
66152
});
153+
});
67154

68-
template.hasResourceProperties('AWS::ApiGateway::Method', {
69-
HttpMethod: 'ANY',
70-
AuthorizationType: 'NONE',
155+
describe('Cognito Resources', () => {
156+
it('should create a Cognito domain', () => {
157+
stagingTemplate.hasResourceProperties('AWS::Cognito::UserPoolDomain', {
158+
Domain: 'aimedicalreport-auth',
159+
UserPoolId: Match.anyValue()
160+
});
161+
});
162+
163+
it('should create a Cognito user pool client', () => {
164+
stagingTemplate.hasResourceProperties('AWS::Cognito::UserPoolClient', {
165+
GenerateSecret: true,
166+
AllowedOAuthFlows: ['code'],
167+
CallbackURLs: Match.arrayWith([
168+
Match.stringLikeRegexp('http://aimedicalreport.example.com/oauth2/idpresponse')
169+
]),
170+
ExplicitAuthFlows: Match.arrayWith([
171+
'ALLOW_USER_PASSWORD_AUTH',
172+
'ALLOW_USER_SRP_AUTH'
173+
])
174+
});
71175
});
72176
});
73177

74-
it('should create load balancer', () => {
75-
template.hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', {
76-
Type: 'application',
178+
describe('Load Balancer Resources', () => {
179+
it('should create an ALB with correct configuration', () => {
180+
stagingTemplate.hasResourceProperties('AWS::ElasticLoadBalancingV2::LoadBalancer', {
181+
Scheme: 'internet-facing',
182+
LoadBalancerAttributes: Match.arrayWith([
183+
{
184+
Key: 'deletion_protection.enabled',
185+
Value: 'false'
186+
}
187+
]),
188+
Type: 'application'
189+
});
190+
});
191+
192+
it('should create a target group pointing to the Fargate service', () => {
193+
stagingTemplate.hasResourceProperties('AWS::ElasticLoadBalancingV2::TargetGroup', {
194+
Port: 3000,
195+
Protocol: 'HTTP',
196+
TargetType: 'ip',
197+
HealthCheckPath: '/health',
198+
HealthCheckTimeoutSeconds: 5,
199+
HealthCheckIntervalSeconds: 30
200+
});
201+
});
202+
203+
it('should create an HTTP listener with Cognito authentication', () => {
204+
stagingTemplate.hasResourceProperties('AWS::ElasticLoadBalancingV2::Listener', {
205+
Port: 80,
206+
Protocol: 'HTTP',
207+
DefaultActions: Match.arrayWith([
208+
Match.objectLike({
209+
Type: 'authenticate-cognito',
210+
AuthenticateCognitoConfig: {
211+
UserPoolArn: Match.anyValue(),
212+
UserPoolClientId: Match.anyValue(),
213+
UserPoolDomain: Match.anyValue(),
214+
OnUnauthenticatedRequest: 'authenticate'
215+
},
216+
Order: 1
217+
}),
218+
Match.objectLike({
219+
Type: 'forward',
220+
TargetGroupArn: Match.anyValue(),
221+
Order: 2
222+
})
223+
])
224+
});
77225
});
78226
});
79227

80-
it('should output API URL and Load Balancer DNS', () => {
81-
template.hasOutput('ApiUrl', {});
82-
template.hasOutput('LoadBalancerDNS', {});
228+
describe('Stack Outputs', () => {
229+
it('should output the load balancer DNS', () => {
230+
stagingTemplate.hasOutput('LoadBalancerDNS', {});
231+
});
232+
233+
it('should output Cognito information', () => {
234+
stagingTemplate.hasOutput('UserPoolId', {});
235+
stagingTemplate.hasOutput('UserPoolClientId', {});
236+
stagingTemplate.hasOutput('CognitoDomain', {});
237+
});
83238
});
84239
});

0 commit comments

Comments
 (0)