diff --git a/infrastructure/terraform/modules/backend-api/README.md b/infrastructure/terraform/modules/backend-api/README.md index c91abab92..1a3c44461 100644 --- a/infrastructure/terraform/modules/backend-api/README.md +++ b/infrastructure/terraform/modules/backend-api/README.md @@ -56,6 +56,7 @@ No requirements. | [get\_client\_lambda](#module\_get\_client\_lambda) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a | | [get\_contact\_details\_lambda](#module\_get\_contact\_details\_lambda) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/4.0.0/terraform-lambda.zip | n/a | | [get\_letter\_variant\_lambda](#module\_get\_letter\_variant\_lambda) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.0.6/terraform-lambda.zip | n/a | +| [get\_proofing\_request\_lambda](#module\_get\_proofing\_request\_lambda) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.1.6/terraform-lambda.zip | n/a | | [get\_routing\_config\_lambda](#module\_get\_routing\_config\_lambda) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a | | [get\_routing\_configs\_by\_template\_id\_lambda](#module\_get\_routing\_configs\_by\_template\_id\_lambda) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a | | [get\_template\_lambda](#module\_get\_template\_lambda) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-lambda.zip | n/a | diff --git a/infrastructure/terraform/modules/backend-api/iam_role_api_gateway_execution_role.tf b/infrastructure/terraform/modules/backend-api/iam_role_api_gateway_execution_role.tf index e8e1e56f6..1c119fff1 100644 --- a/infrastructure/terraform/modules/backend-api/iam_role_api_gateway_execution_role.tf +++ b/infrastructure/terraform/modules/backend-api/iam_role_api_gateway_execution_role.tf @@ -66,6 +66,7 @@ data "aws_iam_policy_document" "api_gateway_execution_policy" { module.get_routing_config_lambda.function_arn, module.get_routing_configs_by_template_id_lambda.function_arn, module.get_template_lambda.function_arn, + module.get_proofing_request_lambda.function_arn, module.get_template_letter_variants_lambda.function_arn, module.list_contact_details_lambda.function_arn, module.list_routing_configs_lambda.function_arn, diff --git a/infrastructure/terraform/modules/backend-api/locals.tf b/infrastructure/terraform/modules/backend-api/locals.tf index 710b625bc..ddc7f4566 100644 --- a/infrastructure/terraform/modules/backend-api/locals.tf +++ b/infrastructure/terraform/modules/backend-api/locals.tf @@ -32,6 +32,7 @@ locals { GET_ROUTING_CONFIGS_BY_TEMPLATE_ID_LAMBDA_ARN = module.get_routing_configs_by_template_id_lambda.function_arn GET_TEMPLATE_LAMBDA_ARN = module.get_template_lambda.function_arn GET_TEMPLATE_LETTER_VARIANTS_LAMBDA_ARN = module.get_template_letter_variants_lambda.function_arn + GET_PROOFING_REQUEST_LAMBDA_ARN = module.get_proofing_request_lambda.function_arn LIST_CONTACT_DETAILS_LAMBDA_ARN = module.list_contact_details_lambda.function_arn LIST_ROUTING_CONFIGS_LAMBDA_ARN = module.list_routing_configs_lambda.function_arn LIST_TEMPLATES_LAMBDA_ARN = module.list_template_lambda.function_arn diff --git a/infrastructure/terraform/modules/backend-api/module_get_proofing_request_lambda.tf b/infrastructure/terraform/modules/backend-api/module_get_proofing_request_lambda.tf new file mode 100644 index 000000000..113e26206 --- /dev/null +++ b/infrastructure/terraform/modules/backend-api/module_get_proofing_request_lambda.tf @@ -0,0 +1,67 @@ +module "get_proofing_request_lambda" { + source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/3.1.6/terraform-lambda.zip" + + project = var.project + environment = var.environment + component = var.component + aws_account_id = var.aws_account_id + region = var.region + + kms_key_arn = var.kms_key_arn + + function_name = "get_routing_config_lambda-proofing-request" + + function_module_name = "get-proofing-request" + handler_function_name = "handler" + description = "Get proofing request API endpoint" + + memory = 2048 + timeout = 20 + runtime = "nodejs22.x" + + log_retention_in_days = var.log_retention_in_days + iam_policy_document = { + body = data.aws_iam_policy_document.get_proofing_request.json + } + + lambda_env_vars = local.backend_lambda_environment_variables + function_s3_bucket = var.function_s3_bucket + function_code_base_path = local.lambdas_dir + function_code_dir = "backend-api/dist/get-proofing-request" + + send_to_firehose = var.send_to_firehose + log_destination_arn = var.log_destination_arn + log_subscription_role_arn = var.log_subscription_role_arn +} + +data "aws_iam_policy_document" "get_proofing_request" { + statement { + sid = "AllowKMSAccess" + effect = "Allow" + + actions = [ + "kms:Decrypt", + "kms:DescribeKey", + "kms:Encrypt", + "kms:GenerateDataKey*", + "kms:ReEncrypt*", + ] + + resources = [ + var.kms_key_arn + ] + } + + statement { + sid = "AllowProofingRequestsRead" + effect = "Allow" + + actions = [ + "dynamodb:GetItem", + ] + + resources = [ + aws_dynamodb_table.proof_requests.arn, + ] + } +} diff --git a/infrastructure/terraform/modules/backend-api/spec.tmpl.json b/infrastructure/terraform/modules/backend-api/spec.tmpl.json index a46461db3..d09e7fb0a 100644 --- a/infrastructure/terraform/modules/backend-api/spec.tmpl.json +++ b/infrastructure/terraform/modules/backend-api/spec.tmpl.json @@ -3290,6 +3290,71 @@ } } }, + "/v1/proofing-request/{proofingRequestId}": { + "get": { + "description": "Get the proofing request for a digital-channel template", + "parameters": [ + { + "description": "ID of the proofing request to get", + "in": "path", + "name": "proofingRequestId", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProofRequestSuccess" + } + } + }, + "description": "200 response", + "headers": { + "Content-Type": { + "schema": { + "type": "string" + } + } + } + }, + "default": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Failure" + } + } + }, + "description": "Error" + } + }, + "security": [ + { + "authorizer": [] + } + ], + "summary": "Get the proofing request for a digital-channel template", + "x-amazon-apigateway-integration": { + "contentHandling": "CONVERT_TO_TEXT", + "credentials": "${APIG_EXECUTION_ROLE_ARN}", + "httpMethod": "POST", + "passthroughBehavior": "WHEN_NO_TEMPLATES", + "responses": { + ".*": { + "statusCode": "200" + } + }, + "timeoutInMillis": 29000, + "type": "AWS_PROXY", + "uri": "arn:aws:apigateway:${AWS_REGION}:lambda:path/2015-03-31/functions/${GET_PROOFING_REQUEST_LAMBDA_ARN}/invocations" + } + } + }, "/v1/template/{templateId}/routing-configurations": { "get": { "description": "Get all routing configurations that reference a specific template", diff --git a/lambdas/backend-api/README.md b/lambdas/backend-api/README.md index b1afbc190..99d9895cb 100644 --- a/lambdas/backend-api/README.md +++ b/lambdas/backend-api/README.md @@ -360,4 +360,15 @@ curl -X POST --location "${APIG_STAGE}/v1/template/${TEMPLATE_ID}/proofing-reque } }' ``` + +### GET - /v1/proofing-request/:proofingRequestId - Get a digital proofing request by id + +Gets a digital proof request by id + +```bash +curl -X GET --location "${APIG_STAGE}/v1/proofing-request/${PROOF_REQUEST_ID}" \ +--header 'Accept: application/json' \ +--header "Authorization: $SANDBOX_TOKEN" +``` + diff --git a/lambdas/backend-api/build.sh b/lambdas/backend-api/build.sh index af3e550bc..86e6321a1 100755 --- a/lambdas/backend-api/build.sh +++ b/lambdas/backend-api/build.sh @@ -31,6 +31,7 @@ npx esbuild \ src/get-routing-config.ts \ src/get-routing-configs-by-template-id.ts \ src/get-template-letter-variants.ts \ + src/get-proofing-request.ts \ src/get.ts \ src/generate-letter-proof.ts \ src/list-contact-details.ts \ diff --git a/lambdas/backend-api/src/__tests__/api/get-proofing-request.test.ts b/lambdas/backend-api/src/__tests__/api/get-proofing-request.test.ts new file mode 100644 index 000000000..02624826d --- /dev/null +++ b/lambdas/backend-api/src/__tests__/api/get-proofing-request.test.ts @@ -0,0 +1,174 @@ +import type { APIGatewayProxyEvent, Context } from 'aws-lambda'; +import { mock } from 'jest-mock-extended'; +import type { ProofRequest } from 'nhs-notify-web-template-management-types'; +import { createHandler } from '@backend-api/api/get-proofing-request'; +import type { ProofingRequestClient } from '@backend-api/app/proofing-request-client'; + +function setup() { + const proofingRequestClient = mock(); + const mocks = { proofingRequestClient }; + const handler = createHandler(mocks); + + return { handler, mocks }; +} + +describe('Get Proofing Request Handler', () => { + beforeEach(jest.resetAllMocks); + + test.each([ + [ + 'authorizer is undefined', + undefined, + { proofingRequestId: 'proofing-request-123' }, + ], + [ + 'internalUserId is missing', + { clientId: 'client-id', internalUserId: undefined }, + { proofingRequestId: 'proofing-request-123' }, + ], + [ + 'clientId is missing', + { clientId: undefined, internalUserId: 'user-1234' }, + { proofingRequestId: 'proofing-request-123' }, + ], + [ + 'proofingRequestId is missing', + { clientId: 'client-id', internalUserId: 'user-1234' }, + { proofingRequestId: undefined }, + ], + ])( + 'should return 500 - Invalid request when %s', + async (_, ctx, pathParameters) => { + const { handler, mocks } = setup(); + + const event = mock({ + requestContext: { authorizer: ctx }, + pathParameters, + }); + + const result = await handler(event, mock(), jest.fn()); + + expect(result).toEqual({ + statusCode: 500, + body: JSON.stringify({ + statusCode: 500, + technicalMessage: 'Invalid request', + }), + }); + + expect(mocks.proofingRequestClient.get).not.toHaveBeenCalled(); + } + ); + + test('should return error when client returns error', async () => { + const { handler, mocks } = setup(); + + mocks.proofingRequestClient.get.mockResolvedValueOnce({ + error: { + errorMeta: { + code: 500, + description: 'Internal server error', + }, + }, + }); + + const event = mock({ + requestContext: { + authorizer: { internalUserId: 'user-1234', clientId: 'client-id' }, + }, + pathParameters: { proofingRequestId: 'proofing-request-123' }, + }); + + const result = await handler(event, mock(), jest.fn()); + + expect(result).toEqual({ + statusCode: 500, + body: JSON.stringify({ + statusCode: 500, + technicalMessage: 'Internal server error', + }), + }); + + expect(mocks.proofingRequestClient.get).toHaveBeenCalledWith( + 'proofing-request-123', + { internalUserId: 'user-1234', clientId: 'client-id' } + ); + }); + + test('should pass validation error details from client', async () => { + const { handler, mocks } = setup(); + + mocks.proofingRequestClient.get.mockResolvedValueOnce({ + error: { + errorMeta: { + code: 400, + description: 'Request failed validation', + details: { + proofingRequestId: 'Required', + }, + }, + }, + }); + + const event = mock({ + requestContext: { + authorizer: { internalUserId: 'user-1234', clientId: 'client-id' }, + }, + pathParameters: { proofingRequestId: 'proofing-request-123' }, + body: undefined, + }); + + const result = await handler(event, mock(), jest.fn()); + + expect(result).toEqual({ + statusCode: 400, + body: JSON.stringify({ + statusCode: 400, + technicalMessage: 'Request failed validation', + details: { + proofingRequestId: 'Required', + }, + }), + }); + }); + + test('should return 200 with proof request', async () => { + const { handler, mocks } = setup(); + + const proofRequest: ProofRequest = { + contactDetailValue: 'test-patient-nhs-number', + createdAt: new Date().toISOString(), + createdBy: 'user-1234', + id: 'proofing-request-123', + templateId: 'template-123', + templateType: 'NHS_APP', + testPatientNhsNumber: 'test-patient-nhs-number', + }; + + mocks.proofingRequestClient.get.mockResolvedValueOnce({ + data: proofRequest, + }); + + const event = mock({ + requestContext: { + authorizer: { + internalUserId: 'user-1234', + clientId: 'client-id', + }, + }, + pathParameters: { proofingRequestId: 'proofing-request-123' }, + }); + + const result = await handler(event, mock(), jest.fn()); + + expect(result).toEqual({ + statusCode: 200, + body: JSON.stringify({ statusCode: 200, data: proofRequest }), + }); + + expect(mocks.proofingRequestClient.get).toHaveBeenCalledWith( + 'proofing-request-123', + { internalUserId: 'user-1234', clientId: 'client-id' } + ); + }); +}); diff --git a/lambdas/backend-api/src/__tests__/app/proofing-request-client.test.ts b/lambdas/backend-api/src/__tests__/app/proofing-request-client.test.ts index 2b8ca5ba0..e033a3591 100644 --- a/lambdas/backend-api/src/__tests__/app/proofing-request-client.test.ts +++ b/lambdas/backend-api/src/__tests__/app/proofing-request-client.test.ts @@ -514,4 +514,37 @@ describe('ProofingRequestClient', () => { } ); }); + + describe('get', () => { + it('returns error when repository returns error', async () => { + const { client, mocks } = setup(); + + const failure = { + error: { + errorMeta: { + code: 500, + description: 'Error fetching proofing request from database.', + }, + }, + }; + + mocks.proofRequestRepository.getById.mockResolvedValueOnce(failure); + + const result = await client.get('proofing-request-123', user); + + expect(result).toBe(failure); + }); + + it('returns proof request for proofingRequestId', async () => { + const { client, mocks } = setup(); + + mocks.proofRequestRepository.getById.mockResolvedValueOnce({ + data: proofRequest, + }); + + const result = await client.get('proofing-request-123', user); + + expect(result).toEqual({ data: proofRequest }); + }); + }); }); diff --git a/lambdas/backend-api/src/__tests__/infra/proof-request-repository.test.ts b/lambdas/backend-api/src/__tests__/infra/proof-request-repository.test.ts index d07351619..f691eef96 100644 --- a/lambdas/backend-api/src/__tests__/infra/proof-request-repository.test.ts +++ b/lambdas/backend-api/src/__tests__/infra/proof-request-repository.test.ts @@ -1,5 +1,9 @@ import crypto from 'node:crypto'; -import { DynamoDBDocumentClient, PutCommand } from '@aws-sdk/lib-dynamodb'; +import { + DynamoDBDocumentClient, + GetCommand, + PutCommand, +} from '@aws-sdk/lib-dynamodb'; import { mockClient } from 'aws-sdk-client-mock'; import 'aws-sdk-client-mock-jest'; import type { User } from 'nhs-notify-web-template-management-utils'; @@ -105,4 +109,60 @@ describe('ProofRequestRepository', () => { ); }); }); + + describe('getById', () => { + it('returns proof request when found', async () => { + const { repo, mocks } = setup(); + + const proofRequestItem = { + ...requestParams, + id: randomUuid, + createdAt: NOW.toISOString(), + createdBy: `INTERNAL_USER#${user.internalUserId}`, + }; + + mocks.dynamodb.on(GetCommand).resolvesOnce({ + Item: proofRequestItem, + }); + + const result = await repo.getById(randomUuid, user); + + expect(result.data).toEqual(proofRequestItem); + expect(mocks.dynamodb).toHaveReceivedCommandWith(GetCommand, { + TableName: tableName, + Key: { + id: randomUuid, + owner: `INTERNAL_USER#${user.internalUserId}`, + }, + }); + }); + + it('returns not found error when no record found', async () => { + const { repo, mocks } = setup(); + + mocks.dynamodb.on(GetCommand).resolvesOnce({}); + + const result = await repo.getById(randomUuid, user); + + expect(result.data).toBeUndefined(); + expect(result.error?.errorMeta.code).toBe(404); + expect(result.error?.errorMeta.description).toBe( + 'Proofing request not found.' + ); + }); + + it('returns internal error when DynamoDB read fails', async () => { + const { repo, mocks } = setup(); + + mocks.dynamodb.on(GetCommand).rejects(new Error('DynamoDB error')); + + const result = await repo.getById(randomUuid, user); + + expect(result.data).toBeUndefined(); + expect(result.error?.errorMeta.code).toBe(500); + expect(result.error?.errorMeta.description).toBe( + 'Error fetching proofing request from database.' + ); + }); + }); }); diff --git a/lambdas/backend-api/src/api/get-proofing-request.ts b/lambdas/backend-api/src/api/get-proofing-request.ts new file mode 100644 index 000000000..0e1a509a4 --- /dev/null +++ b/lambdas/backend-api/src/api/get-proofing-request.ts @@ -0,0 +1,48 @@ +import type { APIGatewayProxyHandler } from 'aws-lambda'; +import { logger } from 'nhs-notify-web-template-management-utils/logger'; +import type { ProofingRequestClient } from '@backend-api/app/proofing-request-client'; +import { apiFailure, apiSuccess } from '@backend-api/api/responses'; +import { ErrorCase } from 'nhs-notify-backend-client/types'; + +type Dependencies = { + proofingRequestClient: ProofingRequestClient; +}; + +export function createHandler({ + proofingRequestClient, +}: Dependencies): APIGatewayProxyHandler { + return async function handler(event) { + const { internalUserId, clientId } = event.requestContext.authorizer ?? {}; + + const user = { internalUserId, clientId }; + + const proofingRequestId = event.pathParameters?.proofingRequestId; + + const log = logger.child({ ...user, proofingRequestId }); + + if (!internalUserId || !clientId || !proofingRequestId) { + log.error('Invalid event received from API Gateway'); + + return apiFailure(ErrorCase.INTERNAL, 'Invalid request'); + } + + const { data, error } = await proofingRequestClient.get( + proofingRequestId, + user + ); + + if (error) { + log + .child(error.errorMeta) + .error('Failed to fetch proofing request', error.actualError); + + return apiFailure( + error.errorMeta.code, + error.errorMeta.description, + error.errorMeta.details + ); + } + + return apiSuccess(200, data); + }; +} diff --git a/lambdas/backend-api/src/api/responses.ts b/lambdas/backend-api/src/api/responses.ts index 4ce70aaef..db1b0d7d9 100644 --- a/lambdas/backend-api/src/api/responses.ts +++ b/lambdas/backend-api/src/api/responses.ts @@ -4,6 +4,7 @@ import type { RoutingConfig, RoutingConfigReference, LetterVariant, + ProofRequest, ContactDetailDto, } from 'nhs-notify-web-template-management-types'; @@ -16,6 +17,7 @@ export const apiSuccess = < | Count | LetterVariant | LetterVariant[] + | ProofRequest | RoutingConfig | RoutingConfig[] | RoutingConfigReference[] diff --git a/lambdas/backend-api/src/app/proofing-request-client.ts b/lambdas/backend-api/src/app/proofing-request-client.ts index b7cba684f..d989a8858 100644 --- a/lambdas/backend-api/src/app/proofing-request-client.ts +++ b/lambdas/backend-api/src/app/proofing-request-client.ts @@ -87,6 +87,13 @@ export class ProofingRequestClient { } } + async get( + proofingRequestId: string, + user: User + ): Promise> { + return await this.proofRequestRepository.getById(proofingRequestId, user); + } + private async handleApp( { contactDetailId, diff --git a/lambdas/backend-api/src/get-proofing-request.ts b/lambdas/backend-api/src/get-proofing-request.ts new file mode 100644 index 000000000..9399bfb1b --- /dev/null +++ b/lambdas/backend-api/src/get-proofing-request.ts @@ -0,0 +1,4 @@ +import { createHandler } from '@backend-api/api/get-proofing-request'; +import { proofingRequestContainer } from '@backend-api/container/proofing-request'; + +export const handler = createHandler(proofingRequestContainer()); diff --git a/lambdas/backend-api/src/infra/proof-request-repository.ts b/lambdas/backend-api/src/infra/proof-request-repository.ts index a979db98e..ff5e9a097 100644 --- a/lambdas/backend-api/src/infra/proof-request-repository.ts +++ b/lambdas/backend-api/src/infra/proof-request-repository.ts @@ -1,5 +1,9 @@ import { randomUUID } from 'node:crypto'; -import { PutCommand, type DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb'; +import { + GetCommand, + PutCommand, + type DynamoDBDocumentClient, +} from '@aws-sdk/lib-dynamodb'; import { $ProofRequest } from 'nhs-notify-backend-client/schemas'; import { ErrorCase } from 'nhs-notify-backend-client/types'; import type { ProofRequest } from 'nhs-notify-web-template-management-types'; @@ -17,7 +21,7 @@ export class ProofRequestRepository { user: User ): Promise> { const now = new Date().toISOString(); - const userKey = `INTERNAL_USER#${user.internalUserId}`; + const userKey = this.getOwnerKey(user); const record = $ProofRequest.parse({ ...params, @@ -47,4 +51,39 @@ export class ProofRequestRepository { ); } } + + async getById( + proofingRequestId: string, + user: User + ): Promise> { + try { + const response = await this.dynamodb.send( + new GetCommand({ + TableName: this.tableName, + Key: { + id: proofingRequestId, + owner: this.getOwnerKey(user), + }, + }) + ); + + if (!response?.Item) { + return failure(ErrorCase.NOT_FOUND, 'Proofing request not found.'); + } + + const proofRequestItem = $ProofRequest.parse(response.Item); + + return success(proofRequestItem); + } catch (error) { + return failure( + ErrorCase.INTERNAL, + 'Error fetching proofing request from database.', + error + ); + } + } + + private getOwnerKey(user: User) { + return `INTERNAL_USER#${user.internalUserId}`; + } } diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index a0b2952e3..14546312b 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -67,6 +67,11 @@ export type { GetV1LetterVariantByLetterVariantIdErrors, GetV1LetterVariantByLetterVariantIdResponse, GetV1LetterVariantByLetterVariantIdResponses, + GetV1ProofingRequestByProofingRequestIdData, + GetV1ProofingRequestByProofingRequestIdError, + GetV1ProofingRequestByProofingRequestIdErrors, + GetV1ProofingRequestByProofingRequestIdResponse, + GetV1ProofingRequestByProofingRequestIdResponses, GetV1RoutingConfigurationByRoutingConfigIdData, GetV1RoutingConfigurationByRoutingConfigIdError, GetV1RoutingConfigurationByRoutingConfigIdErrors, diff --git a/packages/types/src/types.gen.ts b/packages/types/src/types.gen.ts index d3bd2a610..b80e5e3be 100644 --- a/packages/types/src/types.gen.ts +++ b/packages/types/src/types.gen.ts @@ -1297,6 +1297,38 @@ export type PostV1TemplateByTemplateIdProofingRequestResponses = { export type PostV1TemplateByTemplateIdProofingRequestResponse = PostV1TemplateByTemplateIdProofingRequestResponses[keyof PostV1TemplateByTemplateIdProofingRequestResponses]; +export type GetV1ProofingRequestByProofingRequestIdData = { + body?: never; + path: { + /** + * ID of the proofing request to get + */ + proofingRequestId: string; + }; + query?: never; + url: '/v1/proofing-request/{proofingRequestId}'; +}; + +export type GetV1ProofingRequestByProofingRequestIdErrors = { + /** + * Error + */ + default: Failure; +}; + +export type GetV1ProofingRequestByProofingRequestIdError = + GetV1ProofingRequestByProofingRequestIdErrors[keyof GetV1ProofingRequestByProofingRequestIdErrors]; + +export type GetV1ProofingRequestByProofingRequestIdResponses = { + /** + * 200 response + */ + 200: ProofRequestSuccess; +}; + +export type GetV1ProofingRequestByProofingRequestIdResponse = + GetV1ProofingRequestByProofingRequestIdResponses[keyof GetV1ProofingRequestByProofingRequestIdResponses]; + export type GetV1TemplateByTemplateIdRoutingConfigurationsData = { body?: never; path: { diff --git a/tests/test-team/template-mgmt-api-tests/get-proofing-request.api.spec.ts b/tests/test-team/template-mgmt-api-tests/get-proofing-request.api.spec.ts new file mode 100644 index 000000000..ae911c9fe --- /dev/null +++ b/tests/test-team/template-mgmt-api-tests/get-proofing-request.api.spec.ts @@ -0,0 +1,231 @@ +import { test, expect, APIRequestContext } from '@playwright/test'; +import { type TestUser, testUsers } from '../helpers/auth/cognito-auth-helper'; +import { TemplateStorageHelper } from '../helpers/db/template-storage-helper'; +import { ContactDetailHelper } from '../helpers/db/contact-details-helper'; +import { TemplateAPIPayloadFactory } from '../helpers/factories/template-api-payload-factory'; +import { getTestContext } from 'helpers/context/context'; +import { ProofRequest } from 'nhs-notify-web-template-management-types'; +import { makeVerifiedContactDetail } from 'helpers/factories/contact-details-factory'; + +test.describe('GET /v1/proofing-request/:proofingRequestId', () => { + const context = getTestContext(); + const templateStorageHelper = new TemplateStorageHelper(); + const contactDetailHelper = new ContactDetailHelper(); + + let user: TestUser; + let userSharedClient: TestUser; + + test.beforeAll(async () => { + user = await context.auth.getTestUser( + testUsers.UserDigitalProofingEnabled.userId + ); + + userSharedClient = await context.auth.getTestUser( + testUsers.UserDigitalProofingEnabledSharedClient.userId + ); + }); + + test.afterAll(async () => { + await templateStorageHelper.deleteAdHocTemplates(); + await templateStorageHelper.deleteSeededTemplates(); + await contactDetailHelper.cleanup(); + }); + + const createTemplate = async ( + request: APIRequestContext, + templateType: 'NHS_APP' | 'EMAIL' | 'SMS', + testUser: TestUser + ) => { + const createResponse = await request.post( + `${process.env.API_BASE_URL}/v1/template`, + { + headers: { + Authorization: await testUser.getAccessToken(), + }, + data: TemplateAPIPayloadFactory.getCreateTemplatePayload({ + templateType, + }), + } + ); + + expect(createResponse.status()).toBe(201); + const created = await createResponse.json(); + + templateStorageHelper.addAdHocTemplateKey({ + templateId: created.data.id, + clientId: testUser.clientId, + }); + + return created.data; + }; + + const createProofingRequest = async ( + request: APIRequestContext, + templateId: string, + testUser: TestUser, + personalisation?: Record, + contactDetailId?: string + ) => { + const proofingResponse = await request.post( + `${process.env.API_BASE_URL}/v1/template/${templateId}/proofing-request`, + { + headers: { + Authorization: await testUser.getAccessToken(), + }, + data: { + testPatientNhsNumber: '9000000009', + personalisation, + contactDetailId, + }, + } + ); + + const debug = JSON.stringify(await proofingResponse.json()); + expect(proofingResponse.status(), debug).toBe(201); + const result = await proofingResponse.json(); + + return result.data as ProofRequest; + }; + + test.describe('happy path', () => { + test('returns statusCode 200 with proofing request', async ({ + request, + }) => { + const template = await createTemplate(request, 'NHS_APP', user); + const createdProofingRequest = await createProofingRequest( + request, + template.id, + user + ); + + const response = await request.get( + `${process.env.API_BASE_URL}/v1/proofing-request/${createdProofingRequest.id}`, + { + headers: { + Authorization: await user.getAccessToken(), + }, + } + ); + + expect(response.status()).toBe(200); + + const result = await response.json(); + + expect(result).toEqual({ + statusCode: 200, + data: createdProofingRequest, + }); + }); + + test('returns statusCode 200 with proofing request with personalisation', async ({ + request, + }) => { + const template = await createTemplate(request, 'NHS_APP', user); + + const createdProofingRequest = await createProofingRequest( + request, + template.id, + user, + { + name: 'John', + } + ); + + const response = await request.get( + `${process.env.API_BASE_URL}/v1/proofing-request/${createdProofingRequest.id}`, + { + headers: { + Authorization: await user.getAccessToken(), + }, + } + ); + + expect(response.status()).toBe(200); + + const result = await response.json(); + + expect(result).toEqual({ + statusCode: 200, + data: createdProofingRequest, + }); + }); + }); + + test.describe('auth', () => { + test('returns 401 if no auth token', async ({ request }) => { + const response = await request.get( + `${process.env.API_BASE_URL}/v1/proofing-request/anu-proof-id` + ); + + expect(response.status()).toBe(401); + expect(await response.json()).toEqual({ + message: 'Unauthorized', + }); + }); + }); + + test.describe('validation', () => { + test('returns 404 if no proofing request exists with the given proofingRequestId', async ({ + request, + }) => { + const response = await request.get( + `${process.env.API_BASE_URL}/v1/proofing-request/invalid-id`, + { + headers: { + Authorization: await user.getAccessToken(), + }, + } + ); + + const result = await response.json(); + const debug = JSON.stringify(result, null, 2); + + expect(response.status(), debug).toBe(404); + expect(result).toEqual({ + statusCode: 404, + technicalMessage: 'Proofing request not found.', + }); + }); + }); + + test.describe('ownership', () => { + test('returns 404 if contact detail is owned by a different user', async ({ + request, + }) => { + const template = await createTemplate(request, 'EMAIL', userSharedClient); + + const contactDetail = makeVerifiedContactDetail({ + type: 'EMAIL', + value: 'other-user@example.com', + owner: userSharedClient.internalUserId, + }); + await contactDetailHelper.seed([contactDetail]); + + const proofingRequest = await createProofingRequest( + request, + template.id, + userSharedClient, + undefined, + contactDetail.id + ); + + const response = await request.get( + `${process.env.API_BASE_URL}/v1/proofing-request/${proofingRequest.id}`, + { + headers: { + Authorization: await user.getAccessToken(), + }, + } + ); + + const result = await response.json(); + const debug = JSON.stringify(result, null, 2); + + expect(response.status(), debug).toBe(404); + expect(result).toEqual({ + statusCode: 404, + technicalMessage: 'Proofing request not found.', + }); + }); + }); +});