diff --git a/OnFailedProcessMessage/__tests__/handler.test.ts b/OnFailedProcessMessage/__tests__/handler.test.ts new file mode 100644 index 00000000..28c07739 --- /dev/null +++ b/OnFailedProcessMessage/__tests__/handler.test.ts @@ -0,0 +1,137 @@ +import * as TE from "fp-ts/lib/TaskEither"; +import * as E from "fp-ts/lib/Either"; +import * as O from "fp-ts/lib/Option"; +import * as MS from "@pagopa/io-functions-commons/dist/src/models/message_status"; +import { initTelemetryClient } from "../../utils/appinsights"; +import { MessageStatusValueEnum } from "@pagopa/io-functions-commons/dist/generated/definitions/MessageStatusValue"; +import { NonNegativeNumber } from "@pagopa/ts-commons/lib/numbers"; +import { FiscalCode, NonEmptyString } from "@pagopa/ts-commons/lib/strings"; +import { getOnFailedProcessMessageHandler } from "../handler"; +import { MessageModel } from "@pagopa/io-functions-commons/dist/src/models/message"; +import { Context } from "@azure/functions"; +import { CreatedMessageEvent } from "../../utils/events/message"; +import { + aNewMessageWithoutContent, + aRetrievedMessage, + aRetrievedMessageStatus +} from "../../__mocks__/mocks"; + +const contextMock = ({ + bindings: {}, + executionContext: { functionName: "funcname" }, + // eslint-disable no-console + log: { ...console, verbose: console.log } +} as unknown) as Context; + +const mockTelemetryClient = ({ + trackEvent: jest.fn() +} as unknown) as ReturnType; + +const getQueryIteratorMock = jest.fn(); +const lMessageModel = ({ + getQueryIterator: getQueryIteratorMock +} as unknown) as MessageModel; + +const lMessageStatusModel = ({ + upsert: (...args) => TE.of({} /* anything */), + findLastVersionByModelId: (...args) => TE.right(O.none) +} as unknown) as MS.MessageStatusModel; +const getMessageStatusUpdaterMock = jest.spyOn(MS, "getMessageStatusUpdater"); + +const aCreatedMessageEvent: CreatedMessageEvent = { + messageId: aNewMessageWithoutContent.id, + serviceVersion: 1 as NonNegativeNumber +}; + +beforeEach(() => { + jest.clearAllMocks(); + // Mock getMessageStatusUpdater + getMessageStatusUpdaterMock.mockImplementation( + ( + _messageStatusModel: MS.MessageStatusModel, + messageId: NonEmptyString, + fiscalCode: FiscalCode + ) => (status: MessageStatusValueEnum) => + TE.right({ + ...aRetrievedMessageStatus, + id: messageId, + messageId, + status, + fiscalCode + }) + ); + getQueryIteratorMock.mockImplementation(() => { + const asyncIterable = { + [Symbol.asyncIterator]() { + return { + i: 0, + async next() { + if (this.i++ < 1) { + return await Promise.resolve({ + value: [E.right(aRetrievedMessage)], + done: false + }); + } + + return { done: true }; + } + }; + } + }; + return asyncIterable; + }); +}); + +describe("getOnFailedProcessMessageHandler", () => { + it("GIVEN a created message event with an existing messageId WHEN the failed handler is called THEN the message status is created with input messageId and retreived fiscalCode", async () => { + await getOnFailedProcessMessageHandler({ + lMessageStatusModel, + lMessageModel, + telemetryClient: mockTelemetryClient + })(contextMock, aCreatedMessageEvent); + + expect(getMessageStatusUpdaterMock).toBeCalledWith( + lMessageStatusModel, + aCreatedMessageEvent.messageId, + aRetrievedMessage.fiscalCode + ); + + expect(getQueryIteratorMock).toBeCalledWith( + expect.objectContaining({ + parameters: expect.arrayContaining([ + expect.objectContaining({ value: aCreatedMessageEvent.messageId }) + ]) + }) + ); + + expect(mockTelemetryClient.trackEvent).toBeCalledWith( + expect.objectContaining({ + name: "api.messages.create.failedprocessing", + properties: expect.objectContaining({ messageId: "A_MESSAGE_ID" }) + }) + ); + }); + + it("GIVEN a created message event with an not existing messageId WHEN the failed handler is called THEN a cosmos exception is thrown", async () => { + getQueryIteratorMock.mockImplementationOnce(() => ({ + [Symbol.asyncIterator]() { + return { + i: 0, + async next() { + return { done: true }; + } + }; + } + })); + + await expect( + getOnFailedProcessMessageHandler({ + lMessageStatusModel, + lMessageModel, + telemetryClient: mockTelemetryClient + })(contextMock, aCreatedMessageEvent) + ).rejects.toEqual( + expect.objectContaining({ kind: "COSMOS_ERROR_RESPONSE" }) + ); + }); +}); diff --git a/OnFailedProcessMessage/handler.ts b/OnFailedProcessMessage/handler.ts index 3be2b69e..2e1b475d 100644 --- a/OnFailedProcessMessage/handler.ts +++ b/OnFailedProcessMessage/handler.ts @@ -6,13 +6,26 @@ import { MessageStatusModel } from "@pagopa/io-functions-commons/dist/src/models/message_status"; import { MessageStatusValueEnum } from "@pagopa/io-functions-commons/dist/generated/definitions/MessageStatusValue"; -import { initTelemetryClient } from "../utils/appinsights"; -import { withJsonInput } from "../utils/with-json-input"; -import { withDecodedInput } from "../utils/with-decoded-input"; +import { MessageModel } from "@pagopa/io-functions-commons/dist/src/models/message"; +import { constant, pipe } from "fp-ts/lib/function"; +import * as E from "fp-ts/Either"; +import * as TE from "fp-ts/TaskEither"; +import { + CosmosDecodingError, + CosmosErrorResponse +} from "@pagopa/io-functions-commons/dist/src/utils/cosmosdb_model"; +import { + asyncIterableToArray, + flattenAsyncIterable +} from "@pagopa/io-functions-commons/dist/src/utils/async"; import { CreatedMessageEvent } from "../utils/events/message"; +import { withDecodedInput } from "../utils/with-decoded-input"; +import { withJsonInput } from "../utils/with-json-input"; +import { initTelemetryClient } from "../utils/appinsights"; export interface IOnFailedProcessMessageHandlerInput { readonly lMessageStatusModel: MessageStatusModel; + readonly lMessageModel: MessageModel; readonly telemetryClient: ReturnType; } @@ -23,21 +36,54 @@ type Handler = (c: Context, i: unknown) => Promise; */ export const getOnFailedProcessMessageHandler = ({ lMessageStatusModel, + lMessageModel, telemetryClient }: IOnFailedProcessMessageHandlerInput): Handler => withJsonInput( - withDecodedInput(CreatedMessageEvent, async (_, { messageId }) => { - await getMessageStatusUpdater( - lMessageStatusModel, - messageId - )(MessageStatusValueEnum.FAILED)(); - - telemetryClient.trackEvent({ - name: "api.messages.create.failedprocessing", - properties: { - messageId - }, - tagOverrides: { samplingEnabled: "false" } - }); - }) + withDecodedInput(CreatedMessageEvent, async (_, { messageId }) => + pipe( + // query for message with input messageId in order to retrieve the fiscalCode + lMessageModel.getQueryIterator({ + parameters: [{ name: "@messageId", value: messageId }], + query: `SELECT TOP 1 * FROM m WHERE m.id = @messageId` + }), + flattenAsyncIterable, + asyncIterableToArray, + constant, + TE.fromTask, + TE.filterOrElse( + messages => messages.length === 1, + () => + CosmosErrorResponse({ + code: 404, + message: "Missing message", + name: "Not Found" + }) + ), + TE.chainEitherKW(messages => + pipe(messages[0], E.mapLeft(CosmosDecodingError)) + ), + // create the message status for the failed message + TE.chain(message => + getMessageStatusUpdater( + lMessageStatusModel, + messageId, + message.fiscalCode + )(MessageStatusValueEnum.FAILED) + ), + TE.map(() => { + telemetryClient.trackEvent({ + name: "api.messages.create.failedprocessing", + properties: { + messageId + }, + tagOverrides: { samplingEnabled: "false" } + }); + }), + // throw error to trigger retry + TE.getOrElse(e => { + throw e; + }) + )() + ) ); diff --git a/OnFailedProcessMessage/index.ts b/OnFailedProcessMessage/index.ts index f9068e4e..f67052a0 100644 --- a/OnFailedProcessMessage/index.ts +++ b/OnFailedProcessMessage/index.ts @@ -4,6 +4,10 @@ import { MESSAGE_STATUS_COLLECTION_NAME, MessageStatusModel } from "@pagopa/io-functions-commons/dist/src/models/message_status"; +import { + MessageModel, + MESSAGE_COLLECTION_NAME +} from "@pagopa/io-functions-commons/dist/src/models/message"; import { cosmosdbInstance } from "../utils/cosmosdb"; import { getConfigOrThrow } from "../utils/config"; import { initTelemetryClient } from "../utils/appinsights"; @@ -15,12 +19,18 @@ const messageStatusModel = new MessageStatusModel( cosmosdbInstance.container(MESSAGE_STATUS_COLLECTION_NAME) ); +const messageModel = new MessageModel( + cosmosdbInstance.container(MESSAGE_COLLECTION_NAME), + config.MESSAGE_CONTAINER_NAME +); + const telemetryClient = initTelemetryClient( config.APPINSIGHTS_INSTRUMENTATIONKEY ); const activityFunctionHandler: AzureFunction = getOnFailedProcessMessageHandler( { + lMessageModel: messageModel, lMessageStatusModel: messageStatusModel, telemetryClient } diff --git a/ProcessMessage/__tests__/handler.test.ts b/ProcessMessage/__tests__/handler.test.ts index 24cb6d4f..a14c4457 100644 --- a/ProcessMessage/__tests__/handler.test.ts +++ b/ProcessMessage/__tests__/handler.test.ts @@ -34,11 +34,11 @@ import { } from "../../__mocks__/mocks"; import { getProcessMessageHandler } from "../handler"; import { + FiscalCode, NonEmptyString, OrganizationFiscalCode } from "@pagopa/ts-commons/lib/strings"; import { Context } from "@azure/functions"; -import { MessageStatusModel } from "@pagopa/io-functions-commons/dist/src/models/message_status"; import { pipe } from "fp-ts/lib/function"; import { readableReport } from "@pagopa/ts-commons/lib/reporters"; import { @@ -56,6 +56,8 @@ import { Second } from "@pagopa/ts-commons/lib/units"; import * as lolex from "lolex"; import { subSeconds } from "date-fns"; import { DEFAULT_PENDING_ACTIVATION_GRACE_PERIOD_SECONDS } from "../../utils/config"; +import * as MS from "@pagopa/io-functions-commons/dist/src/models/message_status"; +import { MessageStatusValueEnum } from "@pagopa/io-functions-commons/dist/generated/definitions/MessageStatusValue"; const createContext = (): Context => (({ @@ -97,7 +99,8 @@ const lServicePreferencesModel = ({ const lMessageStatusModel = ({ upsert: (...args) => TE.of({} /* anything */), findLastVersionByModelId: (...args) => TE.right(O.none) -} as unknown) as MessageStatusModel; +} as unknown) as MS.MessageStatusModel; +const getMessageStatusUpdaterMock = jest.spyOn(MS, "getMessageStatusUpdater"); const activationFindLastVersionMock = jest.fn(); const lActivation = ({ @@ -227,6 +230,29 @@ beforeEach(() => { // we should refactor them to have them independent, however for now we keep the workaround jest.resetAllMocks(); clock = lolex.install({ now: ExecutionDateContext }); + // Mock getMessageStatusUpdater + getMessageStatusUpdaterMock.mockImplementation( + ( + _messageStatusModel: MS.MessageStatusModel, + messageId: NonEmptyString, + fiscalCode: FiscalCode + ) => (status: MessageStatusValueEnum) => + TE.right({ + _etag: "a", + _rid: "a", + _self: "self", + _ts: 0, + kind: "IRetrievedMessageStatus", + id: messageId, + version: 0 as NonNegativeInteger, + messageId, + status, + updatedAt: new Date(), + isRead: false, + isArchived: false, + fiscalCode + }) + ); }); afterEach(() => { @@ -310,6 +336,12 @@ describe("getprocessMessageHandler", () => { await processMessageHandler(context, JSON.stringify(messageEvent)); + expect(getMessageStatusUpdaterMock).toHaveBeenCalledWith( + lMessageStatusModel, + messageEvent.messageId, + profileResult.fiscalCode + ); + pipe( context.bindings.processedMessage, ProcessedMessageEvent.decode, @@ -398,6 +430,12 @@ describe("getprocessMessageHandler", () => { await processMessageHandler(context, JSON.stringify(messageEvent)); + expect(getMessageStatusUpdaterMock).toHaveBeenCalledWith( + lMessageStatusModel, + messageEvent.messageId, + profileResult.fiscalCode + ); + pipe( context.bindings.processedMessage, ProcessedMessageEvent.decode, diff --git a/ProcessMessage/handler.ts b/ProcessMessage/handler.ts index 10b387e3..bc3689ae 100644 --- a/ProcessMessage/handler.ts +++ b/ProcessMessage/handler.ts @@ -405,7 +405,8 @@ export const getProcessMessageHandler = ({ context.log.warn(`${logPrefix}|RESULT=PROFILE_NOT_FOUND`); await getMessageStatusUpdater( lMessageStatusModel, - createdMessageEvent.message.id + createdMessageEvent.message.id, + newMessageWithoutContent.fiscalCode )(MessageStatusValueEnum.REJECTED)(); return; @@ -425,7 +426,8 @@ export const getProcessMessageHandler = ({ context.log.warn(`${logPrefix}|RESULT=MASTER_INBOX_DISABLED`); await getMessageStatusUpdater( lMessageStatusModel, - createdMessageEvent.message.id + createdMessageEvent.message.id, + newMessageWithoutContent.fiscalCode )(MessageStatusValueEnum.REJECTED)(); return; } @@ -510,7 +512,8 @@ export const getProcessMessageHandler = ({ context.log.warn(`${logPrefix}|RESULT=SENDER_BLOCKED`); await getMessageStatusUpdater( lMessageStatusModel, - createdMessageEvent.message.id + createdMessageEvent.message.id, + profile.fiscalCode )(MessageStatusValueEnum.REJECTED)(); return; } @@ -526,7 +529,8 @@ export const getProcessMessageHandler = ({ await getMessageStatusUpdater( lMessageStatusModel, - createdMessageEvent.message.id + createdMessageEvent.message.id, + profile.fiscalCode )(MessageStatusValueEnum.PROCESSED)(); telemetryClient.trackEvent({ diff --git a/__mocks__/mocks.ts b/__mocks__/mocks.ts index aa9c9a87..4c2939e1 100644 --- a/__mocks__/mocks.ts +++ b/__mocks__/mocks.ts @@ -46,6 +46,8 @@ import { } from "@pagopa/io-functions-commons/dist/src/models/activation"; import { ActivationStatusEnum } from "@pagopa/io-functions-commons/dist/generated/definitions/ActivationStatus"; import { generateComposedVersionedModelId } from "@pagopa/io-functions-commons/dist/src/utils/cosmosdb_model_composed_versioned"; +import { RetrievedMessageStatus } from "@pagopa/io-functions-commons/dist/src/models/message_status"; +import { MessageStatusValueEnum } from "@pagopa/io-functions-commons/dist/generated/definitions/MessageStatusValue"; export const aFiscalCode = "AAABBB01C02D345D" as FiscalCode; export const anotherFiscalCode = "AAABBB01C02D345W" as FiscalCode; @@ -262,3 +264,17 @@ export const anActivation: RetrievedActivation = { version: 1 as NonNegativeInteger, kind: "IRetrievedActivation" }; + +export const aMessageId = "A_MESSAGE_ID" as NonEmptyString; +export const aRetrievedMessageStatus: RetrievedMessageStatus = { + ...aCosmosResourceMetadata, + kind: "IRetrievedMessageStatus", + id: aMessageId, + version: 0 as NonNegativeInteger, + messageId: aMessageId, + status: MessageStatusValueEnum.PROCESSED, + updatedAt: new Date(), + isRead: false, + isArchived: false, + fiscalCode: aFiscalCode +}; diff --git a/package.json b/package.json index 44f31bb7..4a929a8e 100644 --- a/package.json +++ b/package.json @@ -59,9 +59,9 @@ "typescript": "^4.3.5" }, "dependencies": { - "@azure/cosmos": "^3.11.5", + "@azure/cosmos": "^3.15.1", "@pagopa/express-azure-functions": "^2.0.0", - "@pagopa/io-functions-commons": "^23.1.0", + "@pagopa/io-functions-commons": "^23.3.0", "@pagopa/ts-commons": "^10.2.0", "applicationinsights": "^1.7.4", "azure-storage": "^2.10.4", diff --git a/yarn.lock b/yarn.lock index 5ef3aa78..1c20ee7a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17,16 +17,16 @@ "@azure/abort-controller" "^1.0.0" tslib "^2.2.0" -"@azure/core-rest-pipeline@^1.1.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@azure/core-rest-pipeline/-/core-rest-pipeline-1.2.0.tgz#c6b749f1f8fbf3f52d842b0a5cfe2d46f3e0ae06" - integrity sha512-oOd8feRcuoSUwflPNLPO8x6v+m4TcJ9DmazlouuG9d64zJJEwaU757ovpRss9zaL8cggUAdm84C4EbtZ/ltMAw== +"@azure/core-rest-pipeline@^1.2.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@azure/core-rest-pipeline/-/core-rest-pipeline-1.6.0.tgz#f833a0836779a40300a3633fa73ad8a63186ca73" + integrity sha512-9Euoat1TPR97Q1l5aylxhDyKbtp2hv15AoFeOwC5frQAFNJegtDDf6BUBr7OiAggzjGAYidxkyhL0T6Yu05XWQ== dependencies: "@azure/abort-controller" "^1.0.0" "@azure/core-auth" "^1.3.0" "@azure/core-tracing" "1.0.0-preview.13" "@azure/logger" "^1.0.0" - form-data "^3.0.0" + form-data "^4.0.0" http-proxy-agent "^4.0.1" https-proxy-agent "^5.0.0" tslib "^2.2.0" @@ -40,17 +40,17 @@ "@opentelemetry/api" "^1.0.1" tslib "^2.2.0" -"@azure/cosmos@^3.11.5": - version "3.12.3" - resolved "https://registry.yarnpkg.com/@azure/cosmos/-/cosmos-3.12.3.tgz#959483daf625da100331b20ffd6db09289456736" - integrity sha512-MXObNoDnK4rIBIsI+fFxi/zYHx/cJCR/sauBYFWWRV6nVy5Ra8rEefc3Seai5vOCjTIP/notkDorYP9AA894Ww== +"@azure/cosmos@^3.15.1": + version "3.15.1" + resolved "https://registry.yarnpkg.com/@azure/cosmos/-/cosmos-3.15.1.tgz#5b206f061cdf0bb387cf92ac13e25c9190f929c4" + integrity sha512-Pjn9CcoipUSsm/r9ZqAeyrqIQnh0AOeIDwYs/3pcLumK9ezCVFrfeLoOfas4Dm2X8bwL+/9puKAM55LJEaQwWg== dependencies: "@azure/core-auth" "^1.3.0" - "@azure/core-rest-pipeline" "^1.1.0" + "@azure/core-rest-pipeline" "^1.2.0" debug "^4.1.1" - fast-json-stable-stringify "^2.0.0" + fast-json-stable-stringify "^2.1.0" jsbi "^3.1.3" - node-abort-controller "^1.2.0" + node-abort-controller "^3.0.0" priorityqueuejs "^1.0.0" semaphore "^1.0.5" tslib "^2.2.0" @@ -738,12 +738,12 @@ resolved "https://registry.yarnpkg.com/@pagopa/express-azure-functions/-/express-azure-functions-2.0.0.tgz#eb52a0b997d931c1509372e2a9bea22a8ca85c17" integrity sha512-IFZqtk0e2sfkMZIxYqPORzxcKRkbIrVJesR6eMLNwzh1rA4bl2uh9ZHk1m55LNq4ZmaxREDu+1JcGlIaZQgKNQ== -"@pagopa/io-functions-commons@^23.1.0": - version "23.1.0" - resolved "https://registry.yarnpkg.com/@pagopa/io-functions-commons/-/io-functions-commons-23.1.0.tgz#4d82025c93f9451148ba6ceb2b67fbef4e6eccb1" - integrity sha512-Iv7prP1dEH9s/D2aAInevoZs9cestzYGsVtlRkgXq9DUJ5JN5ZrkDDmUo1FDYt7lyTBVl0R1XaBRvs9j5NEvUQ== +"@pagopa/io-functions-commons@^23.3.0": + version "23.3.0" + resolved "https://registry.yarnpkg.com/@pagopa/io-functions-commons/-/io-functions-commons-23.3.0.tgz#17cc669c29da6e713c69980937d3500e17995f03" + integrity sha512-Em2yDGGkX0olRqPvh+hzLT3UdkmZgSdOvDq9WXHgpdMj+WF2eJOApu2yzmnKaZoAQ9LhCerhO/GSfEX3UKz0kw== dependencies: - "@azure/cosmos" "^3.11.5" + "@azure/cosmos" "^3.15.1" "@pagopa/ts-commons" "^10.0.1" applicationinsights "^1.8.10" azure-storage "^2.10.5" @@ -3176,7 +3176,7 @@ fast-json-patch@^3.0.0-1: resolved "https://registry.yarnpkg.com/fast-json-patch/-/fast-json-patch-3.1.0.tgz#ec8cd9b9c4c564250ec8b9140ef7a55f70acaee6" integrity sha512-IhpytlsVTRndz0hU5t0/MGzS/etxLlfrpG5V5M9mVbuj9TrJLWaMfsox9REM5rkuGX0T+5qjpe8XA1o0gZ42nA== -fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: +fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== @@ -3335,10 +3335,10 @@ form-data@^2.3.1, form-data@^2.5.0: combined-stream "^1.0.6" mime-types "^2.1.12" -form-data@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" - integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== dependencies: asynckit "^0.4.0" combined-stream "^1.0.8" @@ -5590,10 +5590,10 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== -node-abort-controller@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-1.2.1.tgz#1eddb57eb8fea734198b11b28857596dc6165708" - integrity sha512-79PYeJuj6S9+yOHirR0JBLFOgjB6sQCir10uN6xRx25iD+ZD4ULqgRn3MwWBRaQGB0vEgReJzWwJo42T1R6YbQ== +node-abort-controller@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.0.1.tgz#f91fa50b1dee3f909afabb7e261b1e1d6b0cb74e" + integrity sha512-/ujIVxthRs+7q6hsdjHMaj8hRG9NuWmwrz+JdRwZ14jdFoKSkm+vDsCbF9PLpnSqjaWQJuTmVtcWHNLr+vrOFw== node-cleanup@^2.1.2: version "2.1.2"