diff --git a/package.json b/package.json index 463798817..0ce37eb90 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "generate:proxy:api-session": "rimraf generated/session && gen-api-models --api-spec api_session.yaml --out-dir generated/session", "generate:lollipop-definitions": "rimraf generated/lollipop && gen-api-models --api-spec openapi/lollipop_definitions.yaml --out-dir generated/lollipop", "generate:lollipop-first-sign": "rimraf generated/lollipop-first-consumer && gen-api-models --api-spec openapi/consumed/lollipop_first_consumer.yaml --out-dir generated/lollipop-first-consumer --request-types --response-decoders --client", + "generate:api:io-wallet": "rimraf generated/io-wallet-api && gen-api-models --api-spec https://raw.githubusercontent.com/pagopa/io-wallet/io-wallet-user-func@0.1.3/apps/io-wallet-user-func/openapi.yaml --no-strict --out-dir generated/io-wallet-api --request-types --response-decoders --client", "postversion": "git push && git push --tags", "dist:modules": "modclean -r -n default:safe && yarn install --production", "predeploy": "npm-run-all build dist:modules", diff --git a/src/clients/io-wallet.ts b/src/clients/io-wallet.ts new file mode 100644 index 000000000..99e4885b5 --- /dev/null +++ b/src/clients/io-wallet.ts @@ -0,0 +1,26 @@ +import { Client, createClient } from "../../generated/io-wallet-api/client"; + +type Fetch = ( + input: RequestInfo | URL, + init?: RequestInit | undefined +) => Promise; + +export function IoWalletAPIClient( + token: string, + basePath: string, + baseUrl: string, + fetchApi: Fetch +): Client<"FunctionsKey"> { + return createClient<"FunctionsKey">({ + basePath, + baseUrl, + fetchApi, + withDefaults: (op) => (params) => + op({ + ...params, + FunctionsKey: token, + }), + }); +} + +export type IoWalletAPIClient = typeof IoWalletAPIClient; diff --git a/src/controllers/ioWalletController.ts b/src/controllers/ioWalletController.ts new file mode 100644 index 000000000..6f935f60e --- /dev/null +++ b/src/controllers/ioWalletController.ts @@ -0,0 +1,35 @@ +/** + * This controller handles the IO_WALLET requests from the + * app by forwarding the call to the API system. + */ + +import * as TE from "fp-ts/TaskEither"; +import * as E from "fp-ts/Either"; + +import { IResponseSuccessJson } from "@pagopa/ts-commons/lib/responses"; + +import { pipe } from "fp-ts/lib/function"; +import { FiscalCode } from "@pagopa/ts-commons/lib/strings"; +import { UserDetailView } from "generated/io-wallet-api/UserDetailView"; +import IoWalletService from "../services/ioWalletService"; + +export const retrieveUserId = ( + ioWalletService: IoWalletService, + fiscalCode: FiscalCode +) => + pipe( + TE.tryCatch( + () => ioWalletService.getUserByFiscalCode(fiscalCode), + E.toError + ), + TE.chain( + TE.fromPredicate( + (r): r is IResponseSuccessJson => + r.kind === "IResponseSuccessJson", + (e) => + new Error( + `An error occurred while retrieving the User id. | ${e.detail}` + ) + ) + ) + ); diff --git a/src/services/__tests__/ioWalletService.test.ts b/src/services/__tests__/ioWalletService.test.ts new file mode 100644 index 000000000..f3ef9e38a --- /dev/null +++ b/src/services/__tests__/ioWalletService.test.ts @@ -0,0 +1,115 @@ +import * as t from "io-ts"; +import IoWalletService from "../ioWalletService"; +import { FiscalCode } from "@pagopa/ts-commons/lib/strings"; + +const mockGetEntityConfiguration = jest.fn(); +const mockGetNonce = jest.fn(); +const mockGetUserByFiscalCode = jest.fn(); +const mockCreateWalletInstance = jest.fn(); +const mockCreateWalletAttestation = jest.fn(); +const mockHealthCheck = jest.fn(); + +mockGetUserByFiscalCode.mockImplementation(() => + t.success({ + status: 200, + value: { + id: "000000000000", + }, + }) +); + +const api = { + getEntityConfiguration: mockGetEntityConfiguration, + getNonce: mockGetNonce, + getUserByFiscalCode: mockGetUserByFiscalCode, + createWalletInstance: mockCreateWalletInstance, + createWalletAttestation: mockCreateWalletAttestation, + healthCheck: mockHealthCheck, +}; + +const aFiscalCode = "GRBGPP87L04L741X" as FiscalCode; + +describe("IoWalletService#getUserByFiscalCode", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("should make the correct api call", async () => { + const service = new IoWalletService(api); + + await service.getUserByFiscalCode(aFiscalCode); + + expect(mockGetUserByFiscalCode).toHaveBeenCalledWith({ + body: { + fiscal_code: aFiscalCode, + }, + }); + }); + + it("should handle a success response", async () => { + const service = new IoWalletService(api); + + const res = await service.getUserByFiscalCode(aFiscalCode); + + expect(res).toMatchObject({ + kind: "IResponseSuccessJson", + }); + }); + + it("should handle an internal error when the API client returns 422", async () => { + mockGetUserByFiscalCode.mockImplementationOnce(() => + t.success({ status: 422 }) + ); + + const service = new IoWalletService(api); + + const res = await service.getUserByFiscalCode(aFiscalCode); + + expect(res).toMatchObject({ + kind: "IResponseErrorGeneric", + }); + }); + + it("should handle an internal error when the API client returns 500", async () => { + const aGenericProblem = {}; + mockGetUserByFiscalCode.mockImplementationOnce(() => + t.success({ status: 500, value: aGenericProblem }) + ); + + const service = new IoWalletService(api); + + const res = await service.getUserByFiscalCode(aFiscalCode); + + expect(res).toMatchObject({ + kind: "IResponseErrorInternal", + }); + }); + + it("should handle an internal error when the API client returns a code not specified in spec", async () => { + const aGenericProblem = {}; + mockGetUserByFiscalCode.mockImplementationOnce(() => + t.success({ status: 599, value: aGenericProblem }) + ); + + const service = new IoWalletService(api); + + const res = await service.getUserByFiscalCode(aFiscalCode); + + expect(res).toMatchObject({ + kind: "IResponseErrorInternal", + }); + }); + + it("should return an error if the api call throws an error", async () => { + mockGetUserByFiscalCode.mockImplementationOnce(() => { + throw new Error(); + }); + const service = new IoWalletService(api); + + const res = await service.getUserByFiscalCode(aFiscalCode); + + expect(res).toMatchObject({ + kind: "IResponseErrorInternal", + }); + }); +}); diff --git a/src/services/ioWalletService.ts b/src/services/ioWalletService.ts new file mode 100644 index 000000000..ae121fd55 --- /dev/null +++ b/src/services/ioWalletService.ts @@ -0,0 +1,61 @@ +/** + * This service interacts with the IO Wallet API + */ + +import { + IResponseErrorGeneric, + IResponseErrorInternal, + IResponseSuccessJson, + ResponseErrorGeneric, + ResponseErrorInternal, + ResponseSuccessJson, +} from "@pagopa/ts-commons/lib/responses"; + +import { FiscalCode } from "@pagopa/ts-commons/lib/strings"; +import { UserDetailView } from "generated/io-wallet-api/UserDetailView"; +import { IoWalletAPIClient } from "../clients/io-wallet"; +import { + ResponseErrorStatusNotDefinedInSpec, + withCatchAsInternalError, + withValidatedOrInternalError, +} from "../utils/responses"; + +export default class IoWalletService { + constructor( + private readonly ioWalletApiClient: ReturnType + ) {} + + /** + * Get the Wallet User id. + */ + public readonly getUserByFiscalCode = ( + fiscalCode: FiscalCode + ): Promise< + | IResponseErrorInternal + | IResponseErrorGeneric + | IResponseSuccessJson + > => + withCatchAsInternalError(async () => { + const validated = await this.ioWalletApiClient.getUserByFiscalCode({ + body: { fiscal_code: fiscalCode }, + }); + return withValidatedOrInternalError(validated, (response) => { + switch (response.status) { + case 200: + return ResponseSuccessJson(response.value); + case 422: + return ResponseErrorGeneric( + response.status, + "Unprocessable Content", + "Your request didn't validate" + ); + case 500: + return ResponseErrorInternal( + `Internal server error | ${response.value}` + ); + default: + return ResponseErrorStatusNotDefinedInSpec(response); + } + }); + }); +}