Skip to content

Commit 7a69fe3

Browse files
committed
LIME-2122 - Added Template Tests for CloudFormation Templates
Added tests for the public-api, private-api and template
1 parent 2d597b3 commit 7a69fe3

9 files changed

Lines changed: 499 additions & 102 deletions

File tree

.github/workflows/pr-check.yml

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,23 +103,33 @@ jobs:
103103
run: |
104104
aws cloudformation delete-stack --region eu-west-2 --stack-name ${{ needs.deploy-preview.outputs.stack-name }}
105105
106+
infra-tests:
107+
runs-on: ubuntu-latest
108+
permissions:
109+
contents: read
110+
steps:
111+
- name: Checkout
112+
uses: actions/checkout@v6
113+
- name: Setup Node
114+
uses: ./.github/actions/node-setup
115+
- name: CloudFormation template tests
116+
run: npm run test:infra
117+
106118
merge-status:
107119
name: Merge status
108120
runs-on: ubuntu-latest
109121
concurrency:
110122
group: merge-status-${{ github.event.pull_request.number }}
111123
cancel-in-progress: true
112-
needs:
113-
- pre-commit
114-
- unit-tests
115-
- api-tests
124+
needs: [pre-commit, type-check, unit-tests, infra-tests]
116125
if: always()
117126
steps:
118127
- run: |
119128
failed=()
120129
[[ "${{ needs.pre-commit.result }}" != "success" ]] && failed+=("pre-commit")
121130
[[ "${{ needs.unit-tests.result }}" != "success" ]] && failed+=("unit-tests")
122131
[[ "${{ needs.api-tests.result }}" != "success" ]] && failed+=("api-tests")
132+
[[ "${{ needs.infra-tests.result }}" != "success" ]] && failed+=("infra-tests")
123133
if [[ ${#failed[@]} -gt 0 ]]; then
124134
echo "The following jobs failed: ${failed[*]}"
125135
exit 1

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ See the Lime onboarding guide for detailed setup instructions.
3838
### Run with coverage
3939
`npm run test:coverage`
4040

41+
### Run Infra tests
42+
`npm run test:infra`
43+
4144
Coverage reports are generated in coverage/ directory.
4245

4346
## Code Quality

package-lock.json

Lines changed: 123 additions & 97 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"test:watch": "vitest",
2424
"test:coverage": "vitest run --coverage",
2525
"test:api": "cucumber-js --profile api",
26+
"test:infra": "vitest run --config vitest.infra.config.ts",
2627
"type-check": "tsc --noEmit"
2728
},
2829
"devDependencies": {
@@ -40,7 +41,9 @@
4041
"tsx": "4.19.4",
4142
"typescript": "5.9.3",
4243
"typescript-eslint": "8.56.0",
43-
"vitest": "4.1.5"
44+
"vitest": "4.1.5",
45+
"yaml": "2.8.3",
46+
"zod": "4.3.6"
4447
},
4548
"dependencies": {
4649
"@aws-lambda-powertools/logger": "2.31.0",

test/infra/helpers.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { readFileSync } from 'node:fs'
2+
import { resolve } from 'node:path'
3+
import { parse } from 'yaml'
4+
import { z } from 'zod'
5+
6+
const cfnTagNames = [
7+
'And', 'Base64', 'Cidr', 'Condition', 'Equals', 'FindInMap', 'GetAtt', 'GetAZs',
8+
'If', 'ImportValue', 'Join', 'Not', 'Or', 'Ref', 'Select', 'Split', 'Sub', 'Transform',
9+
]
10+
const cfnTags = cfnTagNames.flatMap((name) => [
11+
{ collection: 'seq' as const, resolve: (seq: unknown) => seq, tag: `!${name}` },
12+
{ resolve: (str: string) => str, tag: `!${name}` },
13+
])
14+
15+
export const loadTemplate = (relativePath: string): Record<string, unknown> =>
16+
parse(readFileSync(resolve(__dirname, '../../deploy', relativePath), 'utf-8'), {
17+
customTags: cfnTags,
18+
}) as Record<string, unknown>
19+
20+
// Common CloudFormation schemas
21+
export const cfnParameterSchema = z.object({
22+
AllowedValues: z.array(z.string()).optional(),
23+
Default: z.union([z.string(), z.number()]).optional(),
24+
Description: z.string().optional(),
25+
Type: z.string(),
26+
})
27+
28+
export const cfnOutputSchema = z.object({
29+
Condition: z.string().optional(),
30+
Description: z.string().optional(),
31+
Export: z.object({ Name: z.unknown() }).optional(),
32+
Value: z.unknown(),
33+
})
34+
35+
export const openApiSchema = z.object({
36+
info: z.object({
37+
title: z.string(),
38+
version: z.string(),
39+
}),
40+
openapi: z.string().regex(/^3\.\d+\.\d+$/),
41+
paths: z.record(z.string(), z.record(z.string(), z.unknown())),
42+
})

test/infra/private-api.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { loadTemplate, openApiSchema } from './helpers'
2+
import { describe, expect, it } from 'vitest'
3+
4+
const template = loadTemplate('private-api.yaml')
5+
6+
const paths = template['paths'] as Record<string, Record<string, unknown>>
7+
8+
describe('private-api.yaml structure', () => {
9+
it('is a valid OpenAPI 3.x document', () => {
10+
expect(openApiSchema.safeParse(template).success).toBe(true)
11+
})
12+
13+
it('has title', () => {
14+
const info = template['info'] as Record<string, string>
15+
expect(info['title']).toBe('Open Banking Credential Issuer Private API')
16+
})
17+
})
18+
19+
describe('private-api.yaml paths', () => {
20+
const expectedPaths = ['/basic-function', '/authorization', '/session']
21+
22+
it.each(expectedPaths)('has path: %s', (path) => {
23+
expect(paths).toHaveProperty(path)
24+
})
25+
26+
it('/basic-function has POST with aws_proxy integration', () => {
27+
const post = paths['/basic-function']?.['post'] as Record<string, unknown>
28+
const integration = post['x-amazon-apigateway-integration'] as Record<string, string>
29+
expect(integration['type']).toBe('aws_proxy')
30+
})
31+
32+
it('/authorization has GET method', () => {
33+
expect(paths['/authorization']).toHaveProperty('get')
34+
})
35+
36+
it('/session has POST method', () => {
37+
expect(paths['/session']).toHaveProperty('post')
38+
})
39+
})
40+
41+
describe('private-api.yaml components', () => {
42+
const components = template['components'] as Record<string, Record<string, unknown>>
43+
44+
it('defines SessionHeader parameter', () => {
45+
expect(components['parameters']).toHaveProperty('SessionHeader')
46+
})
47+
48+
it('defines required schemas', () => {
49+
const schemas = components['schemas']
50+
expect(schemas).toHaveProperty('Authorization')
51+
expect(schemas).toHaveProperty('AuthorizationResponse')
52+
expect(schemas).toHaveProperty('Error')
53+
expect(schemas).toHaveProperty('Session')
54+
})
55+
})
56+
57+
describe('private-api.yaml request validators', () => {
58+
const validators = template['x-amazon-apigateway-request-validators'] as Record<string, unknown>
59+
60+
it('defines Validate both', () => {
61+
expect(validators).toHaveProperty('Validate both')
62+
})
63+
64+
it('defines Validate Param only', () => {
65+
expect(validators).toHaveProperty('Validate Param only')
66+
})
67+
})

test/infra/public-api.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { loadTemplate, openApiSchema } from './helpers'
2+
import { describe, expect, it } from 'vitest'
3+
4+
const template = loadTemplate('public-api.yaml')
5+
6+
const paths = template['paths'] as Record<string, Record<string, unknown>>
7+
8+
describe('public-api.yaml structure', () => {
9+
it('is a valid OpenAPI 3.x document', () => {
10+
expect(openApiSchema.safeParse(template).success).toBe(true)
11+
})
12+
13+
it('has title', () => {
14+
const info = template['info'] as Record<string, string>
15+
expect(info['title']).toBe('Open Banking Credential Issuer Public API')
16+
})
17+
})
18+
19+
describe('public-api.yaml paths', () => {
20+
const expectedPaths = ['/health', '/.well-known/jwks.json', '/token']
21+
22+
it.each(expectedPaths)('has path: %s', (path) => {
23+
expect(paths).toHaveProperty(path)
24+
})
25+
26+
it('/health has GET with mock integration', () => {
27+
const get = paths['/health']?.['get'] as Record<string, unknown>
28+
const integration = get['x-amazon-apigateway-integration'] as Record<string, string>
29+
expect(integration['type']).toBe('mock')
30+
})
31+
32+
it('/token has POST method', () => {
33+
expect(paths['/token']).toHaveProperty('post')
34+
})
35+
36+
it('/.well-known/jwks.json has GET with S3 integration', () => {
37+
const get = paths['/.well-known/jwks.json']?.['get'] as Record<string, unknown>
38+
const integration = get['x-amazon-apigateway-integration'] as Record<string, string>
39+
expect(integration['type']).toBe('aws')
40+
})
41+
})
42+
43+
describe('public-api.yaml components', () => {
44+
const components = template['components'] as Record<string, Record<string, unknown>>
45+
const schemas = components['schemas']
46+
47+
it('defines required schemas', () => {
48+
expect(schemas).toHaveProperty('JWKSFile')
49+
expect(schemas).toHaveProperty('TokenResponse')
50+
expect(schemas).toHaveProperty('Error')
51+
})
52+
})
53+
54+
describe('public-api.yaml request validators', () => {
55+
const validators = template['x-amazon-apigateway-request-validators'] as Record<string, unknown>
56+
57+
it('defines Validate both', () => {
58+
expect(validators).toHaveProperty('Validate both')
59+
})
60+
61+
it('defines Validate Param only', () => {
62+
expect(validators).toHaveProperty('Validate Param only')
63+
})
64+
})

0 commit comments

Comments
 (0)