Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CCM-7498: add playwright test suite for templates api #262

Merged
merged 18 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,12 @@
"devDependencies": [
"jest.config.ts",
"jest.setup.ts",
"src/__tests__/**"
"**/__tests__/**"
]
}
],
"no-empty-function": "off"
"no-empty-function": "off",
"unicorn/prefer-module": "off"
},
"overrides": [
{
Expand Down
36 changes: 35 additions & 1 deletion .github/workflows/stage-4-acceptance.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -156,11 +156,45 @@ jobs:
with:
name: component test report
path: "tests/test-team/playwright-report"
test-api:
name: "API test"
runs-on: ubuntu-latest
needs: [sandbox-set-up]
environment: dev
timeout-minutes: 10
steps:
- name: "Checkout code"
uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: sandbox_tf_outputs.json
- name: "Repo setup"
run: |
npm ci
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ASSUME_ROLE_NAME }}
role-session-name: deployInfra
aws-region: eu-west-2
- name: "Run API test"
run: |
cd tests/test-team
npm run test:api
- name: Archive API test results
uses: actions/upload-artifact@v4
with:
name: API test report
path: "tests/test-team/playwright-report"

sandbox-tear-down:
name: "Sandbox tear down"
if: success() || failure()
runs-on: ubuntu-latest
needs: [test-accessibility, test-ui-component]
needs:
- test-accessibility
- test-ui-component
- test-api
environment: dev
steps:
- uses: hashicorp/setup-terraform@v3
Expand Down
2 changes: 1 addition & 1 deletion frontend/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const nextConfig = (phase) => {
basePath,
env: {
basePath,
BACKEND_API_URL: amplifyConfig?.meta?.backend_api_url,
API_BASE_URL: amplifyConfig?.meta?.api_base_url,
},

experimental: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
previewTemplatePages,
Template,
TemplateStatus,
templateStatustoDisplayMappings,
templateStatusToDisplayMappings,
templateTypeDisplayMappings,
viewSubmittedTemplatePages,
} from 'nhs-notify-web-template-management-utils';
Expand Down Expand Up @@ -78,7 +78,7 @@ export function ManageTemplates({
: undefined
}
>
{templateStatustoDisplayMappings(template.templateStatus)}
{templateStatusToDisplayMappings(template.templateStatus)}
</Tag>
</Table.Cell>
<Table.Cell>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import concatClassNames from '@utils/concat-class-names';
import {
Template,
TemplateStatus,
templateStatustoDisplayMappings,
templateStatusToDisplayMappings,
templateTypeDisplayMappings,
} from 'nhs-notify-web-template-management-utils';
import styles from './PreviewTemplate.module.scss';
Expand Down Expand Up @@ -53,7 +53,7 @@ export function PreviewTemplate({
: undefined
}
>
{templateStatustoDisplayMappings(template.templateStatus)}
{templateStatusToDisplayMappings(template.templateStatus)}
</Tag>
</Col>
</Row>
Expand Down
2 changes: 1 addition & 1 deletion infrastructure/terraform/components/app/amplify_app.tf
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ resource "aws_amplify_app" "main" {
ACCOUNT_ID = var.aws_account_id
NEXT_PUBLIC_DISABLE_CONTENT = var.disable_content
AMPLIFY_MONOREPO_APP_ROOT = "frontend"
BACKEND_API_URL = module.backend_api.api_base_url
API_BASE_URL = module.backend_api.api_base_url
USER_POOL_ID = jsondecode(data.aws_ssm_parameter.cognito_config.value)["USER_POOL_ID"]
USER_POOL_CLIENT_ID = jsondecode(data.aws_ssm_parameter.cognito_config.value)["USER_POOL_CLIENT_ID"]
}
Expand Down
4 changes: 2 additions & 2 deletions infrastructure/terraform/components/sandbox/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ output "cognito_user_pool_client_id" {
value = aws_cognito_user_pool_client.sandbox.id
}

output "dynamodb_table_templates" {
value = module.backend_api.dynamodb_table_templates
output "templates_table_name" {
value = module.backend_api.templates_table_name
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,64 @@ resource "aws_api_gateway_stage" "main" {
description = "Templates API stage ${var.environment}"
rest_api_id = aws_api_gateway_rest_api.main.id
deployment_id = aws_api_gateway_deployment.main.id

access_log_settings {
destination_arn = aws_cloudwatch_log_group.api_gateway_access.arn

// Context variables reference - https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html#context-variable-reference
// This is useful https://aws.amazon.com/blogs/compute/troubleshooting-amazon-api-gateway-with-enhanced-observability-variables/
// We're not using WAF, custom domain, mTLS etc at the moment, but we might want to add variables around these in the future if we start using them
format = jsonencode({
"accountId" : "$context.accountId"
"apiId" : "$context.apiId"
"authorize" : {
"error" : "$context.authorize.error"
"latency" : "$context.authorize.latency"
"status" : "$context.authorize.status"
}
"authorizer" : {
"error" : "$context.authorizer.error"
"integrationLatency" : "$context.authorizer.integrationLatency"
"integrationStatus" : "$context.authorizer.integrationStatus"
"latency" : "$context.authorizer.latency"
"principalId" : "$context.authorizer.principalId"
"requestId" : "$context.authorizer.requestId"
"status" : "$context.authorizer.status"
}
"awsEndpointRequestId" : "$context.awsEndpointRequestId"
"deploymentId" : "$context.deploymentId"
"domainName" : "$context.domainName"
"domainPrefix" : "$context.domainPrefix"
"endpointType" : "$context.endpointType"
"error" : {
"message" : "$context.error.message"
"responseType" : "$context.error.responseType"
"validationErrorString" : "$context.error.validationErrorString"
}
"extendedRequestId" : "$context.extendedRequestId"
"httpMethod" : "$context.httpMethod"
"identity" : {
"sourceIp" : "$context.identity.sourceIp"
"userAgent" : "$context.identity.userAgent"
}
"integration" : {
"error" : "$context.integration.error"
"integrationStatus" : "$context.integration.integrationStatus"
"latency" : "$context.integration.latency"
"requestId" : "$context.integration.requestId"
"status" : "$context.integration.status"
}
"path" : "$context.path"
"protocol" : "$context.protocol"
"requestId" : "$context.requestId"
"requestTime" : "$context.requestTime"
"requestTimeEpoch" : "$context.requestTimeEpoch"
"responseLatency" : "$context.responseLatency"
"responseLength" : "$context.responseLength"
"resourceId" : "$context.resourceId"
"resourcePath" : "$context.resourcePath"
"stage" : "$context.stage"
"status" : "$context.status"
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
resource "aws_cloudwatch_log_group" "api_gateway_access" {
name = "/aws/api-gateway/${aws_api_gateway_rest_api.main.id}/${var.environment}/access-logs"
retention_in_days = var.log_retention_in_days
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ resource "aws_iam_role" "api_gateway_execution_role" {
assume_role_policy = data.aws_iam_policy_document.api_gateway_service_trust_policy.json
}

resource "aws_iam_role_policy_attachment" "cloudwatch_logs" {
role = aws_iam_role.api_gateway_execution_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"

}

resource "aws_iam_role_policy" "api_gateway_execution_policy" {
role = aws_iam_role.api_gateway_execution_role.name
name = "${local.csi}-apig-execution-policy"
Expand Down
2 changes: 1 addition & 1 deletion infrastructure/terraform/modules/backend-api/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ output "api_base_url" {
value = aws_api_gateway_stage.main.invoke_url
}

output "dynamodb_table_templates" {
output "templates_table_name" {
value = aws_dynamodb_table.templates.name
}
6 changes: 3 additions & 3 deletions infrastructure/terraform/modules/backend-api/spec.tmpl.json
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@
}
],
"responses": {
"201": {
"200": {
"description": "200 response",
"headers": {
"Content-Type": {
Expand Down Expand Up @@ -332,8 +332,8 @@
"type": "request",
"authorizerUri": "arn:aws:apigateway:${AWS_REGION}:lambda:path/2015-03-31/functions/${AUTHORIZER_LAMBDA_ARN}/invocations",
"authorizerCredentials": "${APIG_EXECUTION_ROLE_ARN}",
"identitySource": "method.request.header.authorization,context.resourceId,context.httpMethod",
"authorizerResultTtlInSeconds": 0
"identitySource": "method.request.header.authorization",
"authorizerResultTtlInSeconds": 300
}
}
},
Expand Down
33 changes: 19 additions & 14 deletions lambdas/authorizer/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@ import { jwtDecode } from 'jwt-decode';
import { logger } from 'nhs-notify-web-template-management-utils/logger';
import { handler } from '../index';

const methodArn =
'arn:aws:execute-api:eu-west-2:000000000000:api-id/stage/GET/v1/example-endpoint';
const requestContext = {
accountId: '000000000000',
apiId: 'api-id',
stage: 'stage',
};

const methodArn = 'arn:aws:execute-api:eu-west-2:000000000000:api-id/stage/*';

jest.mock('@aws-sdk/client-cognito-identity-provider', () => {
class MockCognitoIdentityProvider {
Expand Down Expand Up @@ -135,7 +140,7 @@ test('returns Deny policy on lambda misconfiguration', async () => {

const res = await handler(
mock<APIGatewayRequestAuthorizerEvent>({
methodArn,
requestContext,
headers: { Authorization: '123' },
type: 'REQUEST',
}),
Expand All @@ -150,7 +155,7 @@ test('returns Deny policy on lambda misconfiguration', async () => {
test('returns Deny policy if no Authorization token in header', async () => {
const res = await handler(
mock<APIGatewayRequestAuthorizerEvent>({
methodArn,
requestContext,
headers: { Authorization: undefined },
type: 'REQUEST',
}),
Expand All @@ -164,7 +169,7 @@ test('returns Deny policy if no Authorization token in header', async () => {
test('returns Deny policy on malformed token', async () => {
const res = await handler(
mock<APIGatewayRequestAuthorizerEvent>({
methodArn,
requestContext,
headers: { Authorization: 'lemon' },
type: 'REQUEST',
}),
Expand Down Expand Up @@ -193,7 +198,7 @@ test('returns Deny policy on token with missing kid', async () => {

const res = await handler(
mock<APIGatewayRequestAuthorizerEvent>({
methodArn,
requestContext,
headers: { Authorization: jwt },
type: 'REQUEST',
}),
Expand All @@ -220,7 +225,7 @@ test('returns Deny policy on token with incorrect client_id claim', async () =>

const res = await handler(
mock<APIGatewayRequestAuthorizerEvent>({
methodArn,
requestContext,
headers: { Authorization: jwt },
type: 'REQUEST',
}),
Expand Down Expand Up @@ -249,7 +254,7 @@ test('returns Deny policy on token with incorrect iss claim', async () => {

const res = await handler(
mock<APIGatewayRequestAuthorizerEvent>({
methodArn,
requestContext,
headers: { Authorization: jwt },
type: 'REQUEST',
}),
Expand Down Expand Up @@ -281,7 +286,7 @@ test('returns Deny policy on token with incorrect token_use claim', async () =>

const res = await handler(
mock<APIGatewayRequestAuthorizerEvent>({
methodArn,
requestContext,
headers: { Authorization: jwt },
type: 'REQUEST',
}),
Expand Down Expand Up @@ -312,7 +317,7 @@ test('returns Deny policy on Cognito not validating the token', async () => {

const res = await handler(
mock<APIGatewayRequestAuthorizerEvent>({
methodArn,
requestContext,
headers: { Authorization: jwt },
type: 'REQUEST',
}),
Expand Down Expand Up @@ -348,7 +353,7 @@ test.each([

const res = await handler(
mock<APIGatewayRequestAuthorizerEvent>({
methodArn,
requestContext,
headers: { Authorization: jwt },
type: 'REQUEST',
}),
Expand Down Expand Up @@ -377,7 +382,7 @@ test('returns Deny policy, when no sub on Cognito UserAttributes', async () => {

const res = await handler(
mock<APIGatewayRequestAuthorizerEvent>({
methodArn,
requestContext,
headers: { Authorization: jwt },
type: 'REQUEST',
}),
Expand All @@ -404,7 +409,7 @@ test('returns Allow policy on valid token', async () => {

const res = await handler(
mock<APIGatewayRequestAuthorizerEvent>({
methodArn,
requestContext,
headers: { Authorization: jwt },
type: 'REQUEST',
}),
Expand Down Expand Up @@ -433,7 +438,7 @@ test('returns Deny policy on expired token', async () => {

const res = await handler(
mock<APIGatewayRequestAuthorizerEvent>({
methodArn,
requestContext,
headers: { Authorization: jwt },
type: 'REQUEST',
}),
Expand Down
17 changes: 13 additions & 4 deletions lambdas/authorizer/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import type { APIGatewayRequestAuthorizerHandler } from 'aws-lambda';
import type {
APIGatewayEventRequestContextWithAuthorizer,
APIGatewayRequestAuthorizerHandler,
} from 'aws-lambda';
import { z } from 'zod';
import {
CognitoIdentityProviderClient,
Expand All @@ -19,6 +22,13 @@ const $AccessToken = z.object({
token_use: z.string(),
});

const getEnvironmentVariable = (envName: string) => process.env[envName];

const generateMethodArn = (
requestContext: APIGatewayEventRequestContextWithAuthorizer<undefined>
) =>
`arn:aws:execute-api:eu-west-2:${requestContext.accountId}:${requestContext.apiId}/${requestContext.stage}/*`;

const generatePolicy = (
Resource: string,
Effect: 'Allow' | 'Deny',
Expand All @@ -38,12 +48,11 @@ const generatePolicy = (
context,
});

const getEnvironmentVariable = (envName: string) => process.env[envName];

export const handler: APIGatewayRequestAuthorizerHandler = async ({
methodArn,
headers,
requestContext,
}) => {
const methodArn = generateMethodArn(requestContext);
try {
if (!headers?.Authorization) {
return generatePolicy(methodArn, 'Deny');
Expand Down
Loading
Loading