From bb8a886d6a7ddff634c616cf87d498a1892f4e83 Mon Sep 17 00:00:00 2001 From: AleDore Date: Tue, 12 Jan 2021 18:31:01 +0100 Subject: [PATCH 1/3] [#176466130] Add Update Cgn Sttus activity --- .../__tests__/handler.test.ts | 127 ++++++++++++++++++ UpdateCgnStatusActivity/function.json | 10 ++ UpdateCgnStatusActivity/handler.ts | 92 +++++++++++++ UpdateCgnStatusActivity/index.ts | 29 ++++ 4 files changed, 258 insertions(+) create mode 100644 UpdateCgnStatusActivity/__tests__/handler.test.ts create mode 100644 UpdateCgnStatusActivity/function.json create mode 100644 UpdateCgnStatusActivity/handler.ts create mode 100644 UpdateCgnStatusActivity/index.ts diff --git a/UpdateCgnStatusActivity/__tests__/handler.test.ts b/UpdateCgnStatusActivity/__tests__/handler.test.ts new file mode 100644 index 0000000..5714dce --- /dev/null +++ b/UpdateCgnStatusActivity/__tests__/handler.test.ts @@ -0,0 +1,127 @@ +/* tslint:disable: no-any */ +import { none, some } from "fp-ts/lib/Option"; +import { fromLeft, taskEither } from "fp-ts/lib/TaskEither"; +import { toCosmosErrorResponse } from "io-functions-commons/dist/src/utils/cosmosdb_model"; +import { FiscalCode, NonEmptyString } from "italia-ts-commons/lib/strings"; +import { context } from "../../__mocks__/durable-functions"; +import { + CgnPendingStatus, + StatusEnum +} from "../../generated/definitions/CgnPendingStatus"; +import { + CgnRevokedStatus, + StatusEnum as RevokedStatusEnum +} from "../../generated/definitions/CgnRevokedStatus"; +import { UserCgn } from "../../models/user_cgn"; +import { ActivityInput, getUpdateCgnStatusActivityHandler } from "../handler"; + +const now = new Date(); +const aFiscalCode = "RODFDS82S10H501T" as FiscalCode; +const aRevokationRequest = { + motivation: "aMotivation" as NonEmptyString +}; + +const aUserCgnRevokedStatus: CgnRevokedStatus = { + motivation: aRevokationRequest.motivation, + revokation_date: now, + status: RevokedStatusEnum.REVOKED +}; + +const aRevokedUserCgn: UserCgn = { + fiscalCode: aFiscalCode, + id: "ID" as NonEmptyString, + status: aUserCgnRevokedStatus +}; + +const aUserCgnPendingStatus: CgnPendingStatus = { + status: StatusEnum.PENDING +}; + +const findLastVersionByModelIdMock = jest.fn(); +const updateMock = jest.fn(); + +const userCgnModelMock = { + findLastVersionByModelId: findLastVersionByModelIdMock, + update: updateMock +}; + +const anActivityInput: ActivityInput = { + cgnStatus: aUserCgnRevokedStatus, + fiscalCode: aFiscalCode +}; +describe("UpdateCgnStatusActivity", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it("should return failure if an error occurs during UserCgn retrieve", async () => { + findLastVersionByModelIdMock.mockImplementationOnce(() => + fromLeft(toCosmosErrorResponse(new Error("query error"))) + ); + const updateCgnStatusActivityHandler = getUpdateCgnStatusActivityHandler( + userCgnModelMock as any + ); + const response = await updateCgnStatusActivityHandler( + context, + anActivityInput + ); + expect(response.kind).toBe("FAILURE"); + if (response.kind === "FAILURE") { + expect(response.reason).toBe( + "Cannot retrieve userCgn for the provided fiscalCode" + ); + } + }); + + it("should return failure if no UserCgn was found", async () => { + findLastVersionByModelIdMock.mockImplementationOnce(() => + taskEither.of(none) + ); + const updateCgnStatusActivityHandler = getUpdateCgnStatusActivityHandler( + userCgnModelMock as any + ); + const response = await updateCgnStatusActivityHandler( + context, + anActivityInput + ); + expect(response.kind).toBe("FAILURE"); + if (response.kind === "FAILURE") { + expect(response.reason).toBe( + "No userCgn found for the provided fiscalCode" + ); + } + }); + it("should return failure if userCgn' s update fails", async () => { + findLastVersionByModelIdMock.mockImplementationOnce(() => + taskEither.of(some(aRevokedUserCgn)) + ); + updateMock.mockImplementationOnce(() => + fromLeft(new Error("cannot update userCgn")) + ); + const updateCgnStatusActivityHandler = getUpdateCgnStatusActivityHandler( + userCgnModelMock as any + ); + const response = await updateCgnStatusActivityHandler( + context, + anActivityInput + ); + expect(response.kind).toBe("FAILURE"); + if (response.kind === "FAILURE") { + expect(response.reason).toBe("Cannot update userCgn"); + } + }); + + it("should return success if userCgn' s update success", async () => { + findLastVersionByModelIdMock.mockImplementationOnce(() => + taskEither.of(some({ ...aRevokedUserCgn, status: aUserCgnPendingStatus })) + ); + updateMock.mockImplementationOnce(() => taskEither.of(aRevokedUserCgn)); + const updateCgnStatusActivityHandler = getUpdateCgnStatusActivityHandler( + userCgnModelMock as any + ); + const response = await updateCgnStatusActivityHandler( + context, + anActivityInput + ); + expect(response.kind).toBe("SUCCESS"); + }); +}); diff --git a/UpdateCgnStatusActivity/function.json b/UpdateCgnStatusActivity/function.json new file mode 100644 index 0000000..b2d0a84 --- /dev/null +++ b/UpdateCgnStatusActivity/function.json @@ -0,0 +1,10 @@ +{ + "bindings": [ + { + "name": "name", + "type": "activityTrigger", + "direction": "in" + } + ], + "scriptFile": "../dist/UpdateCgnStatusActivity/index.js" +} diff --git a/UpdateCgnStatusActivity/handler.ts b/UpdateCgnStatusActivity/handler.ts new file mode 100644 index 0000000..4a67026 --- /dev/null +++ b/UpdateCgnStatusActivity/handler.ts @@ -0,0 +1,92 @@ +import { Context } from "@azure/functions"; +import { fromOption } from "fp-ts/lib/Either"; +import { identity } from "fp-ts/lib/function"; +import { fromEither } from "fp-ts/lib/TaskEither"; +import * as t from "io-ts"; +import { FiscalCode } from "italia-ts-commons/lib/strings"; +import { CgnStatus } from "../generated/definitions/CgnStatus"; +import { UserCgnModel } from "../models/user_cgn"; +import { errorsToError } from "../utils/conversions"; + +export const ActivityInput = t.interface({ + cgnStatus: CgnStatus, + fiscalCode: FiscalCode +}); + +export type ActivityInput = t.TypeOf; + +// Activity result +const ActivityResultSuccess = t.interface({ + kind: t.literal("SUCCESS") +}); + +type ActivityResultSuccess = t.TypeOf; + +const ActivityResultFailure = t.interface({ + kind: t.literal("FAILURE"), + reason: t.string +}); + +type ActivityResultFailure = t.TypeOf; + +export const ActivityResult = t.taggedUnion("kind", [ + ActivityResultSuccess, + ActivityResultFailure +]); + +export type ActivityResult = t.TypeOf; + +const failure = (context: Context, logPrefix: string) => ( + err: Error, + description: string = "" +) => { + const logMessage = + description === "" + ? `${logPrefix}|FAILURE=${err.message}` + : `${logPrefix}|${description}|FAILURE=${err.message}`; + context.log.verbose(logMessage); + return ActivityResultFailure.encode({ + kind: "FAILURE", + reason: err.message + }); +}; + +const success = () => + ActivityResultSuccess.encode({ + kind: "SUCCESS" + }); + +export const getUpdateCgnStatusActivityHandler = ( + userCgnModel: UserCgnModel, + logPrefix: string = "UpdateCgnStatusActivity" +) => (context: Context, input: unknown): Promise => { + const fail = failure(context, logPrefix); + return fromEither(ActivityInput.decode(input)) + .mapLeft(errs => fail(errorsToError(errs), "Cannot decode Activity Input")) + .chain(activityInput => + userCgnModel + .findLastVersionByModelId([activityInput.fiscalCode]) + .mapLeft(() => + fail(new Error("Cannot retrieve userCgn for the provided fiscalCode")) + ) + .chain(maybeUserCgn => + fromEither( + fromOption( + fail(new Error("No userCgn found for the provided fiscalCode")) + )(maybeUserCgn) + ) + ) + .map(userCgn => ({ + ...userCgn, + status: activityInput.cgnStatus + })) + ) + .chain(_ => + userCgnModel.update(_).bimap( + () => fail(new Error("Cannot update userCgn")), + () => success() + ) + ) + .fold(identity, identity) + .run(); +}; diff --git a/UpdateCgnStatusActivity/index.ts b/UpdateCgnStatusActivity/index.ts new file mode 100644 index 0000000..2cdea27 --- /dev/null +++ b/UpdateCgnStatusActivity/index.ts @@ -0,0 +1,29 @@ +/* + * This function is not intended to be invoked directly. Instead it will be + * triggered by an orchestrator function. + * + * Before running this sample, please: + * - create a Durable orchestration function + * - create a Durable HTTP starter function + * - run 'yarn add durable-functions' from the wwwroot folder of your + * function app in Kudu + */ + +import { USER_CGN_COLLECTION_NAME, UserCgnModel } from "../models/user_cgn"; +import { getConfigOrThrow } from "../utils/config"; +import { cosmosdbClient } from "../utils/cosmosdb"; +import { getUpdateCgnStatusActivityHandler } from "./handler"; + +const config = getConfigOrThrow(); + +const userCgnsContainer = cosmosdbClient + .database(config.COSMOSDB_NAME) + .container(USER_CGN_COLLECTION_NAME); + +const userCgnModel = new UserCgnModel(userCgnsContainer); + +const updateCgnStatusActivityHandler = getUpdateCgnStatusActivityHandler( + userCgnModel +); + +export default updateCgnStatusActivityHandler; From ae12e121bfa0386eedd4415058f46398bed8321c Mon Sep 17 00:00:00 2001 From: AleDore Date: Tue, 12 Jan 2021 18:33:06 +0100 Subject: [PATCH 2/3] [#176466130] Add conversions utility --- utils/conversions.ts | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 utils/conversions.ts diff --git a/utils/conversions.ts b/utils/conversions.ts new file mode 100644 index 0000000..24e197d --- /dev/null +++ b/utils/conversions.ts @@ -0,0 +1,6 @@ +import { Errors } from "io-ts"; +import { errorsToReadableMessages } from "italia-ts-commons/lib/reporters"; + +export function errorsToError(errors: Errors): Error { + return new Error(errorsToReadableMessages(errors).join(" / ")); +} From d5c93652762534b704c4d93985a82cf7f6fd3a99 Mon Sep 17 00:00:00 2001 From: AleDore Date: Wed, 13 Jan 2021 11:45:28 +0100 Subject: [PATCH 3/3] [#176466130] Refactor over review --- UpdateCgnStatusActivity/__tests__/handler.test.ts | 2 +- UpdateCgnStatusActivity/handler.ts | 6 +++--- UpdateCgnStatusActivity/index.ts | 13 +------------ 3 files changed, 5 insertions(+), 16 deletions(-) diff --git a/UpdateCgnStatusActivity/__tests__/handler.test.ts b/UpdateCgnStatusActivity/__tests__/handler.test.ts index 5714dce..16f4dcd 100644 --- a/UpdateCgnStatusActivity/__tests__/handler.test.ts +++ b/UpdateCgnStatusActivity/__tests__/handler.test.ts @@ -95,7 +95,7 @@ describe("UpdateCgnStatusActivity", () => { taskEither.of(some(aRevokedUserCgn)) ); updateMock.mockImplementationOnce(() => - fromLeft(new Error("cannot update userCgn")) + fromLeft(new Error("Cannot update userCgn")) ); const updateCgnStatusActivityHandler = getUpdateCgnStatusActivityHandler( userCgnModelMock as any diff --git a/UpdateCgnStatusActivity/handler.ts b/UpdateCgnStatusActivity/handler.ts index 4a67026..a426e19 100644 --- a/UpdateCgnStatusActivity/handler.ts +++ b/UpdateCgnStatusActivity/handler.ts @@ -1,5 +1,5 @@ import { Context } from "@azure/functions"; -import { fromOption } from "fp-ts/lib/Either"; +import { fromOption, toError } from "fp-ts/lib/Either"; import { identity } from "fp-ts/lib/function"; import { fromEither } from "fp-ts/lib/TaskEither"; import * as t from "io-ts"; @@ -44,7 +44,7 @@ const failure = (context: Context, logPrefix: string) => ( description === "" ? `${logPrefix}|FAILURE=${err.message}` : `${logPrefix}|${description}|FAILURE=${err.message}`; - context.log.verbose(logMessage); + context.log.info(logMessage); return ActivityResultFailure.encode({ kind: "FAILURE", reason: err.message @@ -83,7 +83,7 @@ export const getUpdateCgnStatusActivityHandler = ( ) .chain(_ => userCgnModel.update(_).bimap( - () => fail(new Error("Cannot update userCgn")), + err => fail(toError(err), "Cannot update userCgn"), () => success() ) ) diff --git a/UpdateCgnStatusActivity/index.ts b/UpdateCgnStatusActivity/index.ts index 2cdea27..0550982 100644 --- a/UpdateCgnStatusActivity/index.ts +++ b/UpdateCgnStatusActivity/index.ts @@ -1,15 +1,4 @@ -/* - * This function is not intended to be invoked directly. Instead it will be - * triggered by an orchestrator function. - * - * Before running this sample, please: - * - create a Durable orchestration function - * - create a Durable HTTP starter function - * - run 'yarn add durable-functions' from the wwwroot folder of your - * function app in Kudu - */ - -import { USER_CGN_COLLECTION_NAME, UserCgnModel } from "../models/user_cgn"; +import { USER_CGN_COLLECTION_NAME, UserCgnModel } from "../models/user_cgn"; import { getConfigOrThrow } from "../utils/config"; import { cosmosdbClient } from "../utils/cosmosdb"; import { getUpdateCgnStatusActivityHandler } from "./handler";