diff --git a/scripts/generate-api-models.sh b/scripts/generate-api-models.sh index d30e7400..d1660273 100755 --- a/scripts/generate-api-models.sh +++ b/scripts/generate-api-models.sh @@ -5,6 +5,8 @@ IO_BACKEND_VERSION=v17.5.2 IO_SERVICES_METADATA_VERSION=1.0.93 # Session manager version IO_SESSION_MANAGER_VERSION=1.8.0 +# Send Functions +IO_SEND_FUNC=1.5.5 declare -a noParams=( "./generated/definitions/backend https://raw.githubusercontent.com/pagopa/io-backend/$IO_BACKEND_VERSION/api_public.yaml" @@ -31,7 +33,8 @@ declare -a noStrict=( ) declare -a noStrictRequestTypesRespondeDecoders=( - "./generated/definitions/pn/aar https://raw.githubusercontent.com/pagopa/io-messages/send-func@1.4.1/apps/send-func/openapi/aar-notification.yaml" + "./generated/definitions/pn/aar https://raw.githubusercontent.com/pagopa/io-messages/send-func@$IO_SEND_FUNC/apps/send-func/openapi/aar-notification.yaml" + "./generated/definitions/pn/lollipopLambda https://raw.githubusercontent.com/pagopa/io-messages/send-func@$IO_SEND_FUNC/apps/send-func/openapi/lollipop-integration-check.yaml" "./generated/definitions/pn https://raw.githubusercontent.com/pagopa/io-backend/$IO_BACKEND_VERSION/api_pn.yaml" "./generated/definitions/trial_system https://raw.githubusercontent.com/pagopa/io-backend/$IO_BACKEND_VERSION/api_trial_system.yaml" "./generated/definitions/fims_history https://raw.githubusercontent.com/pagopa/io-backend/$IO_BACKEND_VERSION/api_io_fims.yaml" diff --git a/src/config.ts b/src/config.ts index 5c6110b1..4dc05eef 100644 --- a/src/config.ts +++ b/src/config.ts @@ -119,6 +119,7 @@ const defaultConfig: IoDevServerConfig = { send: { isServiceUpsertRateLimited: false, aarQRCodeUrl: "https://cittadini.notifichedigitali.it/io", + lollipopLambdaResponseCode: 200, mandateTimeToLiveSeconds: 120, paymentDocumentExpirationTimeSeconds: 10, paymentDocumentGenerationTimeSeconds: 3, diff --git a/src/features/messages/routers/ioSendRouter.ts b/src/features/messages/routers/ioSendRouter.ts index 0af297cd..eaf2995e 100644 --- a/src/features/messages/routers/ioSendRouter.ts +++ b/src/features/messages/routers/ioSendRouter.ts @@ -21,6 +21,7 @@ import { } from "../services/ioSendService"; import MessagesService from "../services/messagesService"; import { bodyToString } from "../utils"; +import { generateLollipopLambdaGetPath } from "../../pn/routers/lollipopLambda"; export const ioSendRouter = Router(); @@ -163,21 +164,68 @@ addHandler( () => Math.random() * 500 ); +addHandler( + ioSendRouter, + "get", + "/api/com/v1/send/lollipop-check/test", + lollipopMiddleware(async (req, res) => { + const sendLollipopLambdaGetUrl = `${serverUrl}${generateLollipopLambdaGetPath()}`; + commonHandleIsTestQueryParam(req); + const sendLollipopLambdaGetFetch = () => + fetch(sendLollipopLambdaGetUrl, { + method: "get", + headers: generateRequestHeaders(req.headers, "application/json", true) + }); + await fetchSENDDataAndForwardResponse( + sendLollipopLambdaGetFetch, + "lollipop-test", + res + ); + }), + () => Math.random() * 500 +); + +addHandler( + ioSendRouter, + "post", + "/api/com/v1/send/lollipop-check/test", + lollipopMiddleware(async (req, res) => { + const sendLollipopLambdaPostUrl = `${serverUrl}${generateLollipopLambdaGetPath()}`; + const sendLollipopLambdaPostBodyEither = bodyToString(req.body); + if (handleLeftEitherIfNeeded(sendLollipopLambdaPostBodyEither, res)) { + return; + } + commonHandleIsTestQueryParam(req); + const sendLambdaLollipopPostFetch = () => + fetch(sendLollipopLambdaPostUrl, { + method: "post", + headers: generateRequestHeaders(req.headers, "application/json", true), + body: sendLollipopLambdaPostBodyEither.right + }); + await fetchSENDDataAndForwardResponse( + sendLambdaLollipopPostFetch, + "lollipop-test", + res + ); + }), + () => Math.random() * 500 +); + const fetchSENDDataAndForwardResponse = async ( fetchFunction: () => Promise, endpointName: string, res: Response ) => { try { - const sendQResponse = await fetchFunction(); + const sendResponse = await fetchFunction(); - const contentType = sendQResponse.headers.get("content-type"); - const responseBodyBuffer = await sendQResponse.arrayBuffer(); + const contentType = sendResponse.headers.get("content-type"); + const responseBodyBuffer = await sendResponse.arrayBuffer(); const body = Buffer.from(responseBodyBuffer); if (contentType) { res.setHeader("Content-Type", contentType); } - res.status(sendQResponse.status).send(body); + res.status(sendResponse.status).send(body); } catch (e) { const errorMessage = unknownToString(e); res diff --git a/src/features/messages/services/ioSendService.ts b/src/features/messages/services/ioSendService.ts index 590d7892..aa3e1424 100644 --- a/src/features/messages/services/ioSendService.ts +++ b/src/features/messages/services/ioSendService.ts @@ -29,16 +29,19 @@ export const isTestOrUndefinedFromQuery = ( export const generateRequestHeaders = ( headers: IncomingHttpHeaders, - contentType: string = "application/json" + contentType: string = "application/json", + excludeTaxId: boolean = false ): Record => ({ ...MessagesService.lollipopClientHeadersFromHeaders(headers), ...MessagesService.generateFakeLollipopServerHeaders( ioDevServerConfig.profile.attrs.fiscal_code ), ...MessagesService.sendAPIKeyHeader(), - ...MessagesService.sendTaxIdHeader( - ioDevServerConfig.profile.attrs.fiscal_code - ), + ...(excludeTaxId + ? {} + : MessagesService.sendTaxIdHeader( + ioDevServerConfig.profile.attrs.fiscal_code + )), "Content-Type": contentTypeHeaderFromHeaders(headers, contentType), ...ioSourceHeaderFromRequestHeaders(headers) }); diff --git a/src/features/pn/routers/lollipopLambda.ts b/src/features/pn/routers/lollipopLambda.ts new file mode 100644 index 00000000..3e34fbe5 --- /dev/null +++ b/src/features/pn/routers/lollipopLambda.ts @@ -0,0 +1,69 @@ +import { Request, Response, Router } from "express"; +import { addHandler } from "../../../payloads/response"; +import { initializationMiddleware } from "../middlewares/initializationMiddleware"; +import { authenticationMiddleware } from "../middlewares/authenticationMiddleware"; +import { ioDevServerConfig } from "../../../config"; +import { handleLeftEitherIfNeeded } from "../../../utils/error"; +import { checkAndValidateLollipopHeaders } from "../services/lollipopService"; +import { lollipopLambdaResponseFromBodyAndConfig } from "../services/lollipopLambdaService"; + +const lollipopLambdaPath = "/aws/send/lollipop-test"; + +export const generateLollipopLambdaGetPath = () => lollipopLambdaPath; +export const generateLollipopLambdaPostPath = () => lollipopLambdaPath; + +export const sendLollipopLambdaRouter = Router(); + +addHandler( + sendLollipopLambdaRouter, + "get", + lollipopLambdaPath, + // Middleware have to be used like this (instead of directly giving the middleware to the router via use) + // because supertest (when testing) calls every middleware upon test initialization, even if it not in a + // router directly called by the test, thus making every test fail due to the authentication middleware + authenticationMiddleware( + initializationMiddleware((req, res) => + commonLollipopLambdaHandling("GET", req, res) + ) + ) +); + +addHandler( + sendLollipopLambdaRouter, + "post", + lollipopLambdaPath, + // Middleware have to be used like this (instead of directly giving the middleware to the router via use) + // because supertest (when testing) calls every middleware upon test initialization, even if it not in a + // router directly called by the test, thus making every test fail due to the authentication middleware + authenticationMiddleware( + initializationMiddleware((req, res) => + commonLollipopLambdaHandling("POST", req, res) + ) + ) +); + +const commonLollipopLambdaHandling = ( + requestVerb: "GET" | "POST", + req: Request, + res: Response +) => { + const sendConfig = ioDevServerConfig.send; + if (!sendConfig.skipLollipopVerification) { + const lollipopHeadersEither = checkAndValidateLollipopHeaders(req.headers); + if (handleLeftEitherIfNeeded(lollipopHeadersEither, res)) { + return; + } + } + + const requestBody = requestVerb === "POST" ? req.body : undefined; + const lollipopLambdaGetResponseEither = + lollipopLambdaResponseFromBodyAndConfig( + requestVerb, + requestBody, + sendConfig + ); + if (handleLeftEitherIfNeeded(lollipopLambdaGetResponseEither, res)) { + return; + } + res.status(200).json(lollipopLambdaGetResponseEither.right); +}; diff --git a/src/features/pn/services/lollipopLambdaService.ts b/src/features/pn/services/lollipopLambdaService.ts new file mode 100644 index 00000000..e3438d53 --- /dev/null +++ b/src/features/pn/services/lollipopLambdaService.ts @@ -0,0 +1,102 @@ +import { Either, isLeft, left } from "fp-ts/lib/Either"; +import { readableReportSimplified } from "@pagopa/ts-commons/lib/reporters"; +import { SendConfig } from "../types/sendConfig"; +import { ExpressFailure } from "../../../utils/expressDTO"; +import { SuccessResponse } from "../../../../generated/definitions/pn/lollipopLambda/SuccessResponse"; +import { getProblemJson } from "../../../payloads/error"; +import { ErrorResponse } from "../../../../generated/definitions/pn/lollipopLambda/ErrorResponse"; + +export const lollipopLambdaResponseFromBodyAndConfig = ( + requestVerb: "GET" | "POST", + requestBody: unknown, + sendConfig: SendConfig +): Either => { + const timestamp = new Date(); + const statusCode = sendConfig.lollipopLambdaResponseCode ?? 200; + if (statusCode === 401) { + return left({ + httpStatusCode: 401, + reason: {} + }); + } else if (statusCode === 400 || statusCode === 403 || statusCode === 500) { + const errorResponseEither = ErrorResponse.decode({ + success: false, + timestamp, + error: { + message: `Server was configured to send a ${statusCode} status code`, + statusCode + } + }); + if (isLeft(errorResponseEither)) { + return left({ + httpStatusCode: 500, + reason: getProblemJson( + 500, + "Conversion to ErrorResponse failed", + `Unable to convert input data to the ErrorResponse data structure (${readableReportSimplified( + errorResponseEither.left + )})` + ) + }); + } + return left({ + httpStatusCode: statusCode, + reason: errorResponseEither.right + }); + } + const bodyStringOrUndefined = unknownBodyToStringOrUndefined(requestBody); + const bodyLengthOrUndefined = stringOrUndefinedToByteLengthOrUndefined( + bodyStringOrUndefined + ); + const successResponseEither = SuccessResponse.decode({ + success: true, + timestamp, + data: { + message: "Server was configured to send a 200 status code", + timestamp, + request: { + method: requestVerb, + path: "lollipop-test", + hasBody: !!bodyLengthOrUndefined, + bodyLength: bodyLengthOrUndefined ?? 0 + } + } + }); + if (isLeft(successResponseEither)) { + return left({ + httpStatusCode: 500, + reason: getProblemJson( + 500, + "Conversion to SuccessResponse failed", + `Unable to convert input data to the SuccessResponse data structure (${readableReportSimplified( + successResponseEither.left + )})` + ) + }); + } + return successResponseEither; +}; + +const stringOrUndefinedToByteLengthOrUndefined = ( + input: string | undefined +): number | undefined => (input ? Buffer.byteLength(input, "utf8") : undefined); + +const unknownBodyToStringOrUndefined = (body: unknown): string | undefined => { + if (typeof body === "string") { + return body; + } else if (typeof body === "object") { + try { + return JSON.stringify(body); + } catch { + return undefined; + } + } else if ( + typeof body === "number" || + (typeof BigInt !== "undefined" && typeof body === "bigint") || + typeof body === "boolean" || + typeof body === "symbol" + ) { + return String(body); + } + return undefined; +}; diff --git a/src/features/pn/types/sendConfig.ts b/src/features/pn/types/sendConfig.ts index b5e58fa0..d9ab8a2d 100644 --- a/src/features/pn/types/sendConfig.ts +++ b/src/features/pn/types/sendConfig.ts @@ -71,6 +71,13 @@ export const SendConfig = t.intersection([ }), t.partial({ aarQRCodeUrl: t.string, + lollipopLambdaResponseCode: t.union([ + t.literal(200), + t.literal(400), + t.literal(401), + t.literal(403), + t.literal(500) + ]), mandateTimeToLiveSeconds: t.number, paymentDocumentExpirationTimeSeconds: t.number, paymentDocumentGenerationTimeSeconds: t.number, diff --git a/src/server.ts b/src/server.ts index a2fed89c..583336b5 100644 --- a/src/server.ts +++ b/src/server.ts @@ -44,6 +44,7 @@ import { sendServiceRouter } from "./features/pn/routers/serviceRouter"; import { sendMandatesRouter } from "./features/pn/routers/mandatesRouter"; import { ioSendRouter } from "./features/messages/routers/ioSendRouter"; import { cdcRouter } from "./features/cdc/routers"; +import { sendLollipopLambdaRouter } from "./features/pn/routers/lollipopLambda"; // create express server const app: Application = express(); @@ -87,6 +88,7 @@ app.use(fastLoginMiddleware); fciRouter, sendAARRouter, sendDocumentsRouter, + sendLollipopLambdaRouter, sendMandatesRouter, sendNotificationsRouter, sendPrevalidatedUrisRouter,