-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontrol-plane-stack.test.ts
More file actions
96 lines (86 loc) · 3.59 KB
/
Copy pathcontrol-plane-stack.test.ts
File metadata and controls
96 lines (86 loc) · 3.59 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
import { App } from 'aws-cdk-lib';
import { Match, Template } from 'aws-cdk-lib/assertions';
import { ControlPlaneStack } from '../lib/control-plane-stack';
import { ENV_NAMES, EnvName } from '../lib/env';
function synth(envName: EnvName, overrides: Partial<{ callbackUrls: string[]; logoutUrls: string[] }> = {}): Template {
const app = new App();
const stack = new ControlPlaneStack(app, `Test-${envName}`, {
envName,
callbackUrls: overrides.callbackUrls ?? ['https://dev.example.com/callback'],
logoutUrls: overrides.logoutUrls ?? ['https://dev.example.com/'],
});
return Template.fromStack(stack);
}
describe('ControlPlaneStack', () => {
it('creates exactly one user pool, one domain, and one app client', () => {
const template = synth('dev');
template.resourceCountIs('AWS::Cognito::UserPool', 1);
template.resourceCountIs('AWS::Cognito::UserPoolDomain', 1);
template.resourceCountIs('AWS::Cognito::UserPoolClient', 1);
});
it('requires email sign-in with self-service sign-up and verification', () => {
const template = synth('dev');
template.hasResourceProperties('AWS::Cognito::UserPool', {
AdminCreateUserConfig: { AllowAdminCreateUserOnly: false },
AutoVerifiedAttributes: ['email'],
});
});
it('enforces a 12-character password floor with all four character classes', () => {
const template = synth('dev');
template.hasResourceProperties('AWS::Cognito::UserPool', {
Policies: {
PasswordPolicy: Match.objectLike({
MinimumLength: 12,
RequireLowercase: true,
RequireUppercase: true,
RequireNumbers: true,
RequireSymbols: true,
}),
},
});
});
it('makes MFA optional with TOTP only, never SMS', () => {
const template = synth('dev');
template.hasResourceProperties('AWS::Cognito::UserPool', {
MfaConfiguration: 'OPTIONAL',
EnabledMfas: ['SOFTWARE_TOKEN_MFA'],
});
});
it('gives the web app client no secret and only the PKCE-compatible auth code grant', () => {
const template = synth('dev');
template.hasResourceProperties('AWS::Cognito::UserPoolClient', {
GenerateSecret: false,
AllowedOAuthFlows: ['code'],
PreventUserExistenceErrors: 'ENABLED',
});
});
it('wires the caller-supplied callback and logout URLs through, not a hardcoded domain', () => {
const template = synth('stage', {
callbackUrls: ['https://stage.openjobradar.example/callback'],
logoutUrls: ['https://stage.openjobradar.example/'],
});
template.hasResourceProperties('AWS::Cognito::UserPoolClient', {
CallbackURLs: ['https://stage.openjobradar.example/callback'],
LogoutURLs: ['https://stage.openjobradar.example/'],
});
});
it('scopes the hosted UI domain prefix per environment so dev/stage/prod never collide', () => {
for (const envName of ENV_NAMES) {
synth(envName).hasResourceProperties('AWS::Cognito::UserPoolDomain', {
Domain: `openjobradar-${envName}`,
});
}
});
it('retains the pool in prod but destroys it in dev/stage', () => {
for (const envName of ['dev', 'stage'] as const) {
const [pool] = Object.values(synth(envName).findResources('AWS::Cognito::UserPool'));
expect(pool.DeletionPolicy).toBe('Delete');
}
const [prodPool] = Object.values(synth('prod').findResources('AWS::Cognito::UserPool'));
expect(prodPool.DeletionPolicy).toBe('Retain');
});
it('never lets a literal AWS account ID reach the synthesized template', () => {
const json = JSON.stringify(synth('prod').toJSON());
expect(json).not.toMatch(/\b\d{12}\b/);
});
});