-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapi-stack.test.ts
More file actions
92 lines (81 loc) · 3.6 KB
/
Copy pathapi-stack.test.ts
File metadata and controls
92 lines (81 loc) · 3.6 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
import { App, Stack } from 'aws-cdk-lib';
import { Match, Template } from 'aws-cdk-lib/assertions';
import { AttributeType, BillingMode, Table } from 'aws-cdk-lib/aws-dynamodb';
import { ApiStack } from '../lib/api-stack';
import { ControlPlaneStack } from '../lib/control-plane-stack';
import { EnvName } from '../lib/env';
function synth(envName: EnvName): Template {
const app = new App();
// A minimal stand-in table, not the real DataPlaneStack — this suite is about ApiStack's own
// wiring (IAM grant shape, authorizer, route), not a retest of ADR-0021's table design.
const tableStack = new Stack(app, `TenantTableStack-${envName}`);
const table = new Table(tableStack, 'TenantTable', {
partitionKey: { name: 'userId', type: AttributeType.STRING },
sortKey: { name: 'sk', type: AttributeType.STRING },
billingMode: BillingMode.PAY_PER_REQUEST,
});
const controlPlane = new ControlPlaneStack(app, `Test-ControlPlane-${envName}`, {
envName,
callbackUrls: ['https://dev.example.com/callback'],
logoutUrls: ['https://dev.example.com/'],
});
const stack = new ApiStack(app, `Test-Api-${envName}`, {
envName,
tenantTable: table,
userPool: controlPlane.userPool,
userPoolClient: controlPlane.userPoolClient,
});
return Template.fromStack(stack);
}
describe('ApiStack', () => {
it('creates exactly one HTTP API, one Lambda function, and one Cognito authorizer', () => {
const template = synth('dev');
template.resourceCountIs('AWS::ApiGatewayV2::Api', 1);
template.resourceCountIs('AWS::Lambda::Function', 1);
template.resourceCountIs('AWS::ApiGatewayV2::Authorizer', 1);
});
it('wires GET /me behind the Cognito JWT authorizer', () => {
const template = synth('dev');
template.hasResourceProperties('AWS::ApiGatewayV2::Route', {
RouteKey: 'GET /me',
AuthorizationType: 'JWT',
});
template.hasResourceProperties('AWS::ApiGatewayV2::Authorizer', {
AuthorizerType: 'JWT',
});
});
it('points the Python handler at openjobradar.lambda_handlers.me.handler', () => {
const template = synth('dev');
template.hasResourceProperties('AWS::Lambda::Function', {
Handler: 'openjobradar.lambda_handlers.me.handler',
Runtime: 'python3.12',
});
});
it('grants the Lambda read/write on the tenant table and nothing broader', () => {
const template = synth('dev');
const policies = template.findResources('AWS::IAM::Policy');
const statements = Object.values(policies).flatMap(
(policy) => policy.Properties.PolicyDocument.Statement as Array<{ Action: string[] | string }>,
);
const dynamoStatement = statements.find((statement) =>
Array.isArray(statement.Action) ? statement.Action.some((action) => action.startsWith('dynamodb:')) : false,
);
expect(dynamoStatement).toBeDefined();
// Least-privilege scoping beyond "this one table" (e.g. per-entity sk-prefix conditions) is
// a documented follow-up, not silently assumed done here — see ADR-0024's consequences.
});
it('restricts CORS origins outside dev', () => {
const devTemplate = synth('dev');
devTemplate.hasResourceProperties('AWS::ApiGatewayV2::Api', {
CorsConfiguration: Match.objectLike({ AllowOrigins: ['*'] }),
});
const stageTemplate = synth('stage');
stageTemplate.hasResourceProperties('AWS::ApiGatewayV2::Api', {
CorsConfiguration: Match.objectLike({ AllowOrigins: [] }),
});
});
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/);
});
});