From c0a6eff825664e4a3c6f7047a76fb0ffcbf482e7 Mon Sep 17 00:00:00 2001 From: AleDore Date: Mon, 2 Mar 2020 12:43:53 +0100 Subject: [PATCH 1/4] user-data-processing-API exposure --- api_backend.yaml | 150 +++++++--- package.json | 2 +- src/app.ts | 35 ++- src/clients/api.ts | 44 ++- .../userDataProcessingController.test.ts | 259 ++++++++++++++++++ .../userDataProcessingController.ts | 76 +++++ .../userDataProcessingService.test.ts | 239 ++++++++++++++++ src/services/userDataProcessingService.ts | 96 +++++++ 8 files changed, 858 insertions(+), 43 deletions(-) create mode 100644 src/controllers/__tests__/userDataProcessingController.test.ts create mode 100644 src/controllers/userDataProcessingController.ts create mode 100644 src/services/__tests__/userDataProcessingService.test.ts create mode 100644 src/services/userDataProcessingService.ts diff --git a/api_backend.yaml b/api_backend.yaml index d0175b9f3..dc8ba2ffa 100644 --- a/api_backend.yaml +++ b/api_backend.yaml @@ -645,84 +645,150 @@ paths: description: Unavailable service schema: $ref: "#/definitions/ProblemJson" + + "/user-data-processing": + x-swagger-router-controller: UserDataProcessingController + post: + operationId: upsertUserDataProcessing + summary: Set User's data processing choices + description: Upsert user data processing for the current authenticated user. + parameters: + - in: body + name: body + schema: + $ref: "#/definitions/UserDataProcessingChoiceRequest" + required: true + responses: + '200': + description: User Data processing created. + schema: + $ref: "#/definitions/UserDataProcessing" + "400": + description: Invalid payload. + schema: + $ref: "#/definitions/ProblemJson" + "401": + description: Bearer token null or expired. + "409": + description: Conflict. + schema: + $ref: "#/definitions/ProblemJson" + "500": + description: User Data processing choice cannot be taken in charge. + schema: + $ref: "#/definitions/ProblemJson" + "/user-data-processing/{choice}": + x-swagger-router-controller: UserDataProcessingController + post: + operationId: getUserDataProcessing + summary: Get User's data processing + description: Get user data processing for the current authenticated user and the given choice. + parameters: + - $ref: "#/parameters/UserDataProcessingChoiceParam" + responses: + "200": + description: User data processing retrieved + schema: + $ref: "#/definitions/UserDataProcessing" + "400": + description: Invalid request. + schema: + $ref: "#/definitions/ProblemJson" + "401": + description: Bearer token null or expired. + "404": + description: Not found. + schema: + $ref: "#/definitions/ProblemJson" + "429": + description: Too many requests definitions: # Definitions from the digital citizenship APIs AcceptedTosVersion: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/AcceptedTosVersion" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/AcceptedTosVersion" BlockedInboxOrChannels: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/BlockedInboxOrChannels" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/BlockedInboxOrChannels" DepartmentName: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/DepartmentName" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/DepartmentName" EmailAddress: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/EmailAddress" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/EmailAddress" Profile: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/Profile" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/Profile" ExtendedProfile: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/ExtendedProfile" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/ExtendedProfile" FiscalCode: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/FiscalCode" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/FiscalCode" IsEmailEnabled: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/IsEmailEnabled" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/IsEmailEnabled" IsInboxEnabled: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/IsInboxEnabled" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/IsInboxEnabled" IsEmailValidated: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/IsEmailValidated" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/IsEmailValidated" IsWebhookEnabled: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/IsWebhookEnabled" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/IsWebhookEnabled" LimitedProfile: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/LimitedProfile" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/LimitedProfile" MessageBodyMarkdown: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/MessageBodyMarkdown" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/MessageBodyMarkdown" MessageContent: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/MessageContent" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/MessageContent" MessageResponseNotificationStatus: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/MessageResponseNotificationStatus" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/MessageResponseNotificationStatus" NotificationChannelStatusValue: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/NotificationChannelStatusValue" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/NotificationChannelStatusValue" NotificationChannel: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/NotificationChannel" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/NotificationChannel" MessageSubject: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/MessageSubject" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/MessageSubject" OrganizationName: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/OrganizationName" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/OrganizationName" PaginationResponse: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/PaginationResponse" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/PaginationResponse" PreferredLanguage: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/PreferredLanguage" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/PreferredLanguage" PreferredLanguages: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/PreferredLanguages" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/PreferredLanguages" ProblemJson: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/ProblemJson" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/ProblemJson" ServiceId: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/ServiceId" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/ServiceId" ServiceName: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/ServiceName" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/ServiceName" ServicePublic: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/ServicePublic" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/ServicePublic" ServiceTuple: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/ServiceTuple" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/ServiceTuple" PaginatedServiceTupleCollection: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/PaginatedServiceTupleCollection" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/PaginatedServiceTupleCollection" Timestamp: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/Timestamp" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/Timestamp" PaymentNoticeNumber: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/PaymentNoticeNumber" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/PaymentNoticeNumber" PaymentAmount: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/PaymentAmount" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/PaymentAmount" PaymentData: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/PaymentData" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/PaymentData" TimeToLiveSeconds: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/TimeToLiveSeconds" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/TimeToLiveSeconds" CreatedMessageWithContent: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/CreatedMessageWithContent" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/CreatedMessageWithContent" CreatedMessageWithoutContent: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/CreatedMessageWithoutContent" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/CreatedMessageWithoutContent" CreatedMessageWithoutContentCollection: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/CreatedMessageWithoutContentCollection" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/CreatedMessageWithoutContentCollection" PaginatedCreatedMessageWithoutContentCollection: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/PaginatedCreatedMessageWithoutContentCollection" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/PaginatedCreatedMessageWithoutContentCollection" + UserDataProcessingStatus: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/UserDataProcessingStatus" + UserDataProcessingChoice: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/UserDataProcessingChoice" + UserDataProcessingChoiceRequest: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/UserDataProcessingChoiceRequest" + UserDataProcessing: + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/UserDataProcessing" MessageResponseWithContent: - $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.8.0/openapi/definitions.yaml#/MessageResponseWithContent" + $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v1.12.1/openapi/definitions.yaml#/MessageResponseWithContent" + # Definitions from pagopa-proxy PaymentProblemJson: $ref: "https://raw.githubusercontent.com/pagopa/io-pagopa-proxy/v0.8.6/api_pagopa.yaml#/definitions/PaymentProblemJson" @@ -955,6 +1021,14 @@ parameters: in: query minimum: 1 description: An opaque identifier that points to the next item in the collection. + UserDataProcessingChoiceParam: + name: choice + in: path + type: string + enum: [DOWNLOAD, DELETE] + description: A representation of a user data processing choice + required: true + x-example: DOWNLOAD consumes: - application/json produces: diff --git a/package.json b/package.json index 0fe49ac4d..4678ac640 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "generate:proxy:notification-models": "rimraf generated/notifications && mkdir -p generated/notifications && gen-api-models --api-spec api_notifications.yaml --out-dir generated/notifications", "generate:proxy:pagopa-models": "rimraf generated/pagopa && mkdir -p generated/pagopa && gen-api-models --api-spec api_pagopa.yaml --out-dir generated/pagopa", "generate:proxy:public-models": "rimraf generated/public && mkdir -p generated/public && gen-api-models --api-spec api_public.yaml --out-dir generated/public", - "generate:api:io": "rimraf generated/io-api && mkdir -p generated/io-api && gen-api-models --api-spec https://raw.githubusercontent.com/pagopa/io-functions-app/v0.7.0/openapi/index.yaml --no-strict --out-dir generated/io-api --request-types --response-decoders", + "generate:api:io": "rimraf generated/io-api && mkdir -p generated/io-api && gen-api-models --api-spec https://raw.githubusercontent.com/pagopa/io-functions-app/v0.9.0/openapi/index.yaml --no-strict --out-dir generated/io-api --request-types --response-decoders", "generate:api:pagopaproxy": "rimraf generated/pagopa-proxy && mkdir -p generated/pagopa-proxy && gen-api-models --api-spec https://raw.githubusercontent.com/pagopa/io-pagopa-proxy/v0.8.6/api_pagopa.yaml --no-strict --out-dir generated/pagopa-proxy --request-types --response-decoders", "generate:test-certs": "./scripts/generate-test-certs.sh certs", "postversion": "git push && git push --tags" diff --git a/src/app.ts b/src/app.ts index 5fdfe39c3..dda0de251 100644 --- a/src/app.ts +++ b/src/app.ts @@ -59,12 +59,14 @@ import { withSpid } from "@pagopa/io-spid-commons"; import { toError } from "fp-ts/lib/Either"; import { tryCatch } from "fp-ts/lib/TaskEither"; import { VersionPerPlatform } from "../generated/public/VersionPerPlatform"; +import UserDataProcessingController from "./controllers/userDataProcessingController"; import MessagesService from "./services/messagesService"; import PagoPAProxyService from "./services/pagoPAProxyService"; import ProfileService from "./services/profileService"; import RedisSessionStorage from "./services/redisSessionStorage"; import RedisUserMetadataStorage from "./services/redisUserMetadataStorage"; import TokenService from "./services/tokenService"; +import UserDataProcessingService from "./services/userDataProcessingService"; const defaultModule = { newApp @@ -175,6 +177,11 @@ export function newApp( // Create the profile service const PROFILE_SERVICE = new ProfileService(API_CLIENT); + // Create the user data processing service + const USER_DATA_PROCESSING_SERVICE = new UserDataProcessingService( + API_CLIENT + ); + const acsController: AuthenticationController = new AuthenticationController( SESSION_STORAGE, TOKEN_SERVICE, @@ -204,7 +211,8 @@ export function newApp( NOTIFICATION_SERVICE, SESSION_STORAGE, PAGOPA_PROXY_SERVICE, - USER_METADATA_STORAGE + USER_METADATA_STORAGE, + USER_DATA_PROCESSING_SERVICE ); registerPagoPARoutes( app, @@ -268,7 +276,8 @@ function registerAPIRoutes( notificationService: NotificationService, sessionStorage: RedisSessionStorage, pagoPaProxyService: PagoPAProxyService, - userMetadataStorage: RedisUserMetadataStorage + userMetadataStorage: RedisUserMetadataStorage, + userDataProcessingService: UserDataProcessingService ): void { const bearerTokenAuth = passport.authenticate("bearer", { session: false }); const urlTokenAuth = passport.authenticate("authtoken", { session: false }); @@ -302,6 +311,10 @@ function registerAPIRoutes( userMetadataStorage ); + const userDataProcessingController: UserDataProcessingController = new UserDataProcessingController( + userDataProcessingService + ); + app.get( `${basePath}/profile`, bearerTokenAuth, @@ -344,6 +357,24 @@ function registerAPIRoutes( ) ); + app.post( + `${basePath}/user-data-processing`, + bearerTokenAuth, + toExpressHandler( + userDataProcessingController.upsertUserDataProcessing, + userDataProcessingController + ) + ); + + app.get( + `${basePath}/user-data-processing/:choice`, + bearerTokenAuth, + toExpressHandler( + userDataProcessingController.getUserDataProcessing, + userDataProcessingController + ) + ); + app.get( `${basePath}/messages`, bearerTokenAuth, diff --git a/src/clients/api.ts b/src/clients/api.ts index 63b698c5a..e971d78b6 100644 --- a/src/clients/api.ts +++ b/src/clients/api.ts @@ -27,11 +27,15 @@ import { getServicesByRecipientDefaultDecoder, GetServicesByRecipientT, GetServiceT, + getUserDataProcessingDefaultDecoder, + GetUserDataProcessingT, getVisibleServicesDefaultDecoder, GetVisibleServicesT, StartEmailValidationProcessT, updateProfileDefaultDecoder, - UpdateProfileT + UpdateProfileT, + upsertUserDataProcessingDefaultDecoder, + UpsertUserDataProcessingT } from "../../generated/io-api/requestTypes"; // we want to authenticate against the platform APIs with the APIM header key or @@ -56,11 +60,15 @@ export function APIClient( readonly getMessages: TypeofApiCall; readonly getProfile: TypeofApiCall; readonly createProfile: TypeofApiCall; + readonly upsertUserDataProcessing: TypeofApiCall< + typeof upsertUserDataProcessingT + >; readonly emailValidationProcess: TypeofApiCall< typeof emailValidationProcessT >; readonly getService: TypeofApiCall; readonly getVisibleServices: TypeofApiCall; + readonly getUserDataProcessing: TypeofApiCall; readonly getServicesByRecipient: TypeofApiCall< typeof getServicesByRecipientT >; @@ -153,6 +161,18 @@ export function APIClient( url: params => `/profiles/${params.recipient}/sender-services` }; + const getUserDataProcessingT: ReplaceRequestParams< + GetUserDataProcessingT, + Omit, "SubscriptionKey"> + > = { + headers: tokenHeaderProducer, + method: "get", + query: _ => ({}), + response_decoder: getUserDataProcessingDefaultDecoder(), + url: params => + `/user-data-processing/${params.fiscalCode}/${params.userDataProcessingChoiceParam}` + }; + const getMessagesT: ReplaceRequestParams< GetMessagesByUserT, Omit, "SubscriptionKey"> @@ -197,6 +217,18 @@ export function APIClient( url: params => `/services/${params.service_id}` }; + const upsertUserDataProcessingT: ReplaceRequestParams< + UpsertUserDataProcessingT, + Omit, "SubscriptionKey"> + > = { + body: params => JSON.stringify(params.userDataProcessingChoiceRequest), + headers: composeHeaderProducers(tokenHeaderProducer, ApiHeaderJson), + method: "post", + query: _ => ({}), + response_decoder: upsertUserDataProcessingDefaultDecoder(), + url: params => `/user-data-processing/${params.fiscalCode}` + }; + return { createProfile: createFetchRequestForApi(createProfileT, options), emailValidationProcess: createFetchRequestForApi( @@ -211,8 +243,16 @@ export function APIClient( getServicesByRecipientT, options ), + getUserDataProcessing: createFetchRequestForApi( + getUserDataProcessingT, + options + ), getVisibleServices: createFetchRequestForApi(getVisibleServicesT, options), - updateProfile: createFetchRequestForApi(updateProfileT, options) + updateProfile: createFetchRequestForApi(updateProfileT, options), + upsertUserDataProcessing: createFetchRequestForApi( + upsertUserDataProcessingT, + options + ) }; } diff --git a/src/controllers/__tests__/userDataProcessingController.test.ts b/src/controllers/__tests__/userDataProcessingController.test.ts new file mode 100644 index 000000000..bad015df3 --- /dev/null +++ b/src/controllers/__tests__/userDataProcessingController.test.ts @@ -0,0 +1,259 @@ +/* tslint:disable:no-any */ +/* tslint:disable:no-object-mutation */ + +import { + ResponseErrorInternal, + ResponseErrorNotFound, + ResponseSuccessJson +} from "italia-ts-commons/lib/responses"; +import { NonEmptyString } from "italia-ts-commons/lib/strings"; + +import { isRight } from "fp-ts/lib/Either"; +import { + UserDataProcessingChoice, + UserDataProcessingChoiceEnum +} from "generated/io-api/UserDataProcessingChoice"; +import { UserDataProcessingChoiceRequest } from "generated/io-api/UserDataProcessingChoiceRequest"; +import UserDataProcessingService from "src/services/userDataProcessingService"; +import { EmailAddress } from "../../../generated/backend/EmailAddress"; +import { FiscalCode } from "../../../generated/backend/FiscalCode"; +import { SpidLevelEnum } from "../../../generated/backend/SpidLevel"; +import mockReq from "../../__mocks__/request"; +import mockRes from "../../__mocks__/response"; +import ApiClient from "../../services/apiClientFactory"; +import { SessionToken, WalletToken } from "../../types/token"; +import { User } from "../../types/user"; +import UserDataProcessingController from "../userDataProcessingController"; + +const aTimestamp = 1518010929530; +const aUserDataProcessingResponse = { + status: 200, + value: { + _etag: "bdb8f644-132c-4f3c-a051-5887fc8058b1", + _rid: "AAAAAQAAAAgAAAAAAAAAAQ==", + _self: "/dbs/AAAAAQ==/colls/AAAAAQAAAAg=/docs/AAAAAQAAAAgAAAAAAAAAAQ==/", + _ts: 1582553174, + choice: "DOWNLOAD", + createdAt: "2020-02-24T14:06:14.513Z", + fiscalCode: "SPNDNL80A13Y555X", + id: "SPNDNL80A13Y555X-DOWNLOAD-0000000000000000", + status: "PENDING", + userDataProcessingId: "SPNDNL80A13Y555X-DOWNLOAD", + version: 0 + } +}; + +const aFiscalNumber = "GRBGPP87L04L741X" as FiscalCode; +const anEmailAddress = "garibaldi@example.com" as EmailAddress; +const aValidName = "Giuseppe Maria"; +const aValidFamilyname = "Garibaldi"; +const aValidSpidLevel = SpidLevelEnum["https://www.spid.gov.it/SpidL2"]; + +const userDataProcessingMissingErrorResponse = ResponseErrorInternal( + "Not Found" +); + +// mock for a valid User +const mockedUser: User = { + created_at: aTimestamp, + family_name: aValidFamilyname, + fiscal_code: aFiscalNumber, + name: aValidName, + session_token: "123hexToken" as SessionToken, + spid_email: anEmailAddress, + spid_level: aValidSpidLevel, + spid_mobile_phone: "3222222222222" as NonEmptyString, + wallet_token: "123hexToken" as WalletToken +}; + +const mockedUserDataProcessingChoice: UserDataProcessingChoice = + UserDataProcessingChoiceEnum.DOWNLOAD; + +const mockedUserDataProcessingChoiceRequest: UserDataProcessingChoiceRequest = { + choice: mockedUserDataProcessingChoice +}; + +const badRequestErrorResponse = { + detail: expect.any(String), + status: 400, + title: expect.any(String), + type: undefined +}; + +const mockGetUserDataProcessing = jest.fn(); +const mockUpsertUserDataProcessing = jest.fn(); +jest.mock("../../services/userDataProcessingService", () => { + return { + default: jest.fn().mockImplementation(() => ({ + getUserDataProcessing: mockGetUserDataProcessing, + upsertUserDataProcessing: mockUpsertUserDataProcessing + })) + }; +}); + +describe("UserDataProcessingController#getUserDataProcessing", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("calls the getUserDataProcessing on the UserDataProcessingService with valid values", async () => { + const req = mockReq(); + + mockGetUserDataProcessing.mockReturnValue( + Promise.resolve(ResponseSuccessJson(aUserDataProcessingResponse)) + ); + + req.user = mockedUser; + + const apiClient = new ApiClient("XUZTCT88A51Y311X", ""); + const userDataProcessingService = new UserDataProcessingService(apiClient); + const controller = new UserDataProcessingController( + userDataProcessingService + ); + + const response = await controller.getUserDataProcessing(req); + + expect(mockGetUserDataProcessing).toHaveBeenCalledWith(mockedUser); + expect(response).toEqual({ + apply: expect.any(Function), + kind: "IResponseSuccessJson", + value: aUserDataProcessingResponse + }); + }); + + it("calls the getUserDataProcessing on the UserDataProcessingService with empty user", async () => { + const req = mockReq(); + const res = mockRes(); + + mockGetUserDataProcessing.mockReturnValue( + Promise.resolve(ResponseSuccessJson(aUserDataProcessingResponse)) + ); + + req.user = ""; + + const apiClient = new ApiClient("XUZTCT88A51Y311X", ""); + const userDataProcessingService = new UserDataProcessingService(apiClient); + const controller = new UserDataProcessingController( + userDataProcessingService + ); + + const response = await controller.getUserDataProcessing(req); + response.apply(res); + + // getUserDataProcessing is not called + expect(mockGetUserDataProcessing).not.toBeCalled(); + expect(res.json).toHaveBeenCalledWith(badRequestErrorResponse); + }); + + it("should return a ResponseErrorInternal if no user data processing was found", async () => { + const req = mockReq(); + const res = mockRes(); + + mockGetUserDataProcessing.mockReturnValue( + Promise.resolve( + ResponseErrorNotFound("Not found", "User data processing not found") + ) + ); + + req.user = mockedUser; + + const apiClient = new ApiClient("XUZTCT88A51Y311X", ""); + const userDataProcessingService = new UserDataProcessingService(apiClient); + const controller = new UserDataProcessingController( + userDataProcessingService + ); + const response = await controller.getUserDataProcessing(req); + response.apply(res); + + expect(mockGetUserDataProcessing).toHaveBeenCalledWith(mockedUser); + expect(response).toEqual({ + ...userDataProcessingMissingErrorResponse, + apply: expect.any(Function) + }); + }); +}); + +describe("UserDataProcessingController#upsertUserDataProcessing", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it("calls the upsertUserDataProcessing on the UserDataProcessingService with valid values", async () => { + const req = mockReq(); + + mockUpsertUserDataProcessing.mockReturnValue( + Promise.resolve(ResponseSuccessJson(aUserDataProcessingResponse)) + ); + + req.user = mockedUser; + req.body = mockedUserDataProcessingChoiceRequest; + + const apiClient = new ApiClient("XUZTCT88A51Y311X", ""); + const userDataProcessingService = new UserDataProcessingService(apiClient); + const controller = new UserDataProcessingController( + userDataProcessingService + ); + const response = await controller.upsertUserDataProcessing(req); + + const errorOrUserDataProcessingChoice = UserDataProcessingChoiceRequest.decode( + req.body + ); + expect(isRight(errorOrUserDataProcessingChoice)).toBeTruthy(); + + expect(mockUpsertUserDataProcessing).toHaveBeenCalledWith( + mockedUser, + errorOrUserDataProcessingChoice.value + ); + expect(response).toEqual({ + apply: expect.any(Function), + kind: "IResponseSuccessJson", + value: aUserDataProcessingResponse + }); + }); + + it("calls the upsertUserDataProcessing on the UserDataProcessingService with empty user and valid upsert user", async () => { + const req = mockReq(); + const res = mockRes(); + + mockUpsertUserDataProcessing.mockReturnValue( + Promise.resolve(ResponseSuccessJson(aUserDataProcessingResponse)) + ); + + req.user = ""; + req.body = mockedUserDataProcessingChoiceRequest; + + const apiClient = new ApiClient("XUZTCT88A51Y311X", ""); + const userDataProcessingService = new UserDataProcessingService(apiClient); + const controller = new UserDataProcessingController( + userDataProcessingService + ); + const response = await controller.upsertUserDataProcessing(req); + response.apply(res); + + expect(mockUpsertUserDataProcessing).not.toBeCalled(); + expect(res.json).toHaveBeenCalledWith(badRequestErrorResponse); + }); + + it("calls the upsertUserDataProcessing on the UserDataProcessingService with valid user and empty upsert profile", async () => { + const req = mockReq(); + const res = mockRes(); + + mockUpsertUserDataProcessing.mockReturnValue( + Promise.resolve(ResponseSuccessJson(aUserDataProcessingResponse)) + ); + + req.user = mockedUser; + req.body = ""; + + const apiClient = new ApiClient("XUZTCT88A51Y311X", ""); + const userDataProcessingService = new UserDataProcessingService(apiClient); + const controller = new UserDataProcessingController( + userDataProcessingService + ); + const response = await controller.upsertUserDataProcessing(req); + response.apply(res); + + expect(mockUpsertUserDataProcessing).not.toBeCalled(); + expect(res.json).toHaveBeenCalledWith(badRequestErrorResponse); + }); +}); diff --git a/src/controllers/userDataProcessingController.ts b/src/controllers/userDataProcessingController.ts new file mode 100644 index 000000000..d89d500d7 --- /dev/null +++ b/src/controllers/userDataProcessingController.ts @@ -0,0 +1,76 @@ +/** + * This controller handles reading the user profile from the + * app by forwarding the call to the API system. + */ + +import * as express from "express"; +import { + IResponseErrorInternal, + IResponseErrorNotFound, + IResponseErrorTooManyRequests, + IResponseErrorValidation, + IResponseSuccessJson +} from "italia-ts-commons/lib/responses"; + +import { UserDataProcessing } from "../../generated/backend/UserDataProcessing"; +import { UserDataProcessingChoice } from "../../generated/backend/UserDataProcessingChoice"; +import { UserDataProcessingChoiceRequest } from "../../generated/backend/UserDataProcessingChoiceRequest"; +import UserDataProcessingService from "../../src/services/userDataProcessingService"; +import { withUserFromRequest } from "../types/user"; +import { withValidatedOrValidationError } from "../utils/responses"; + +export default class UserDataProcessingController { + constructor( + private readonly userDataProcessingService: UserDataProcessingService + ) {} + + /** + * upsert a user data processing request for the user identified by the provided + * fiscal code. + */ + public readonly upsertUserDataProcessing = ( + req: express.Request + ): Promise< + // tslint:disable-next-line:max-union-size + | IResponseErrorValidation + | IResponseErrorNotFound + | IResponseErrorInternal + | IResponseErrorTooManyRequests + | IResponseSuccessJson + > => + withUserFromRequest(req, async user => + withValidatedOrValidationError( + UserDataProcessingChoiceRequest.decode(req.body), + dataProcessingChoice => + this.userDataProcessingService.upsertUserDataProcessing( + user, + dataProcessingChoice + ) + ) + ); + + /** + * Get a user data processing request for the user identified by the provided + * fiscal code and userDataProcessing choice. + */ + public readonly getUserDataProcessing = ( + req: express.Request + ): Promise< + // tslint:disable-next-line:max-union-size + | IResponseErrorValidation + | IResponseErrorNotFound + | IResponseErrorInternal + | IResponseErrorTooManyRequests + | IResponseSuccessJson + > => + withUserFromRequest(req, async user => + withValidatedOrValidationError( + UserDataProcessingChoice.decode(req.params.choice), + dataProcessingChoice => + this.userDataProcessingService.getUserDataProcessing( + user, + dataProcessingChoice + ) + ) + ); +} diff --git a/src/services/__tests__/userDataProcessingService.test.ts b/src/services/__tests__/userDataProcessingService.test.ts new file mode 100644 index 000000000..11cd97468 --- /dev/null +++ b/src/services/__tests__/userDataProcessingService.test.ts @@ -0,0 +1,239 @@ +/* tslint:disable:no-identical-functions */ + +import * as t from "io-ts"; +import { FiscalCode, NonEmptyString } from "italia-ts-commons/lib/strings"; + +import { SpidLevelEnum } from "generated/backend/SpidLevel"; +import { EmailAddress } from "generated/io-api/EmailAddress"; +import { + UserDataProcessingChoice, + UserDataProcessingChoiceEnum +} from "generated/io-api/UserDataProcessingChoice"; +import { UserDataProcessingChoiceRequest } from "generated/io-api/UserDataProcessingChoiceRequest"; +import { SessionToken, WalletToken } from "../../types/token"; +import { User } from "../../types/user"; +import ApiClientFactory from "../apiClientFactory"; +import UserDataProcessingService from "../userDataProcessingService"; + +const aValidEmail = "test@example.com" as EmailAddress; +const aValidFiscalCode = "SPNDNL80A13Y555X" as FiscalCode; +const aValidSpidLevel = SpidLevelEnum["https://www.spid.gov.it/SpidL2"]; +const validApiUserDataProcessingResponse = { + status: 200, + value: { + _etag: "bdb8f644-132c-4f3c-a051-5887fc8058b1", + _rid: "AAAAAQAAAAgAAAAAAAAAAQ==", + _self: "/dbs/AAAAAQ==/colls/AAAAAQAAAAg=/docs/AAAAAQAAAAgAAAAAAAAAAQ==/", + _ts: 1582553174, + choice: "DOWNLOAD", + createdAt: "2020-02-24T14:06:14.513Z", + fiscalCode: "SPNDNL80A13Y555X", + id: "SPNDNL80A13Y555X-DOWNLOAD-0000000000000000", + status: "PENDING", + userDataProcessingId: "SPNDNL80A13Y555X-DOWNLOAD", + version: 0 + } +}; + +const tooManyReqApiUserDataProcessingResponse = { + status: 429 +}; +const invalidApiUserDataProcessingResponse = { + status: 500 +}; +const problemJson = { + status: 500 +}; + +// mock for a valid User +const mockedUser: User = { + created_at: 1183518855, + family_name: "Garibaldi", + fiscal_code: aValidFiscalCode, + name: "Giuseppe Maria", + session_token: "HexToKen" as SessionToken, + spid_email: aValidEmail, + spid_level: aValidSpidLevel, + spid_mobile_phone: "3222222222222" as NonEmptyString, + wallet_token: "HexToKen" as WalletToken +}; + +const mockedUserDataProcessingChoice: UserDataProcessingChoice = + UserDataProcessingChoiceEnum.DOWNLOAD; + +const mockedUserDataProcessingChoiceRequest: UserDataProcessingChoiceRequest = { + choice: mockedUserDataProcessingChoice +}; +const mockGetUserDataProcessing = jest.fn(); +const mockGetClient = jest.fn().mockImplementation(() => { + return { + getUserDataProcessing: mockGetUserDataProcessing + }; +}); + +const mockUpsertUserDataProcessing = jest.fn(); + +beforeEach(() => { + jest.clearAllMocks(); +}); + +jest.mock("../../services/apiClientFactory", () => { + return { + default: jest.fn().mockImplementation(() => ({ + getClient: mockGetClient + })) + }; +}); + +const api = new ApiClientFactory("", ""); + +describe("UserDataProcessingService#getUserDataProcessing", () => { + it("returns a user data processing from the API", async () => { + mockGetUserDataProcessing.mockImplementation(() => { + return t.success(validApiUserDataProcessingResponse); + }); + + const service = new UserDataProcessingService(api); + + const res = await service.getUserDataProcessing( + mockedUser, + mockedUserDataProcessingChoice + ); + + expect(mockGetUserDataProcessing).toHaveBeenCalledWith({ + fiscalCode: mockedUser.fiscal_code, + userDataProcessingChoice: mockedUserDataProcessingChoice + }); + expect(res).toMatchObject({ + kind: "IResponseSuccessJson", + value: mockGetUserDataProcessing + }); + }); + + it("returns an 429 HTTP error from getUserDataProcessing upstream API", async () => { + mockGetUserDataProcessing.mockImplementation(() => + t.success(tooManyReqApiUserDataProcessingResponse) + ); + + const service = new UserDataProcessingService(api); + + const res = await service.getUserDataProcessing( + mockedUser, + mockedUserDataProcessingChoice + ); + + expect(res.kind).toEqual("IResponseErrorTooManyRequests"); + }); + + it("returns an error if the getUserDataProcessing API returns an error", async () => { + mockGetUserDataProcessing.mockImplementation(() => t.success(problemJson)); + + const service = new UserDataProcessingService(api); + + const res = await service.getUserDataProcessing( + mockedUser, + mockedUserDataProcessingChoice + ); + expect(mockGetUserDataProcessing).toHaveBeenCalledWith({ + fiscalCode: mockedUser.fiscal_code, + userDataProcessingChoice: mockedUserDataProcessingChoice + }); + expect(res.kind).toEqual("IResponseErrorInternal"); + }); + + it("returns a 500 response if the response from the getUserDataProcessing API returns something wrong", async () => { + mockGetUserDataProcessing.mockImplementation(() => + t.success(invalidApiUserDataProcessingResponse) + ); + + const service = new UserDataProcessingService(api); + + const res = await service.getUserDataProcessing( + mockedUser, + mockedUserDataProcessingChoice + ); + expect(mockGetUserDataProcessing).toHaveBeenCalledWith({ + fiscalCode: mockedUser.fiscal_code, + userDataProcessingChoice: mockedUserDataProcessingChoice + }); + expect(res.kind).toEqual("IResponseErrorInternal"); + }); +}); + +describe("UserDataProcessingService#upsertUserDataProcessing", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it("returns an upserted user data processing from the API", async () => { + mockUpsertUserDataProcessing.mockImplementation(() => { + return t.success(validApiUserDataProcessingResponse); + }); + + const service = new UserDataProcessingService(api); + + const res = await service.upsertUserDataProcessing( + mockedUser, + mockedUserDataProcessingChoiceRequest + ); + + expect(mockUpsertUserDataProcessing).toHaveBeenCalledWith({ + fiscalCode: mockedUser.fiscal_code, + userDataProcessingChoiceRequest: mockedUserDataProcessingChoiceRequest + }); + expect(res).toMatchObject({ + kind: "IResponseSuccessJson", + value: mockUpsertUserDataProcessing + }); + }); + + it("returns an 429 HTTP error from upsertUserDataProcessing upstream API", async () => { + mockUpsertUserDataProcessing.mockImplementation(() => + t.success(tooManyReqApiUserDataProcessingResponse) + ); + + const service = new UserDataProcessingService(api); + + const res = await service.upsertUserDataProcessing( + mockedUser, + mockedUserDataProcessingChoiceRequest + ); + + expect(res.kind).toEqual("IResponseErrorTooManyRequests"); + }); + + it("returns an error if the upsertUserDataProcessing API returns an error", async () => { + mockUpsertUserDataProcessing.mockImplementation(() => + t.success(problemJson) + ); + + const service = new UserDataProcessingService(api); + + const res = await service.upsertUserDataProcessing( + mockedUser, + mockedUserDataProcessingChoiceRequest + ); + expect(mockUpsertUserDataProcessing).toHaveBeenCalledWith({ + fiscalCode: mockedUser.fiscal_code, + userDataProcessingChoiceRequest: mockedUserDataProcessingChoiceRequest + }); + expect(res.kind).toEqual("IResponseErrorInternal"); + }); + + it("returns a 500 response if the response from the getMessagesByUser API returns something wrong", async () => { + mockUpsertUserDataProcessing.mockImplementation(() => + t.success(invalidApiUserDataProcessingResponse) + ); + + const service = new UserDataProcessingService(api); + + const res = await service.upsertUserDataProcessing( + mockedUser, + mockedUserDataProcessingChoiceRequest + ); + expect(mockUpsertUserDataProcessing).toHaveBeenCalledWith({ + fiscalCode: mockedUser.fiscal_code, + userDataProcessingChoiceRequest: mockedUserDataProcessingChoiceRequest + }); + expect(res.kind).toEqual("IResponseErrorInternal"); + }); +}); diff --git a/src/services/userDataProcessingService.ts b/src/services/userDataProcessingService.ts new file mode 100644 index 000000000..853ed9cd1 --- /dev/null +++ b/src/services/userDataProcessingService.ts @@ -0,0 +1,96 @@ +/** + * This service retrieves and updates the user profile from the API system using + * an API client. + */ + +import { + IResponseErrorInternal, + IResponseErrorNotFound, + IResponseErrorTooManyRequests, + IResponseSuccessJson, + ResponseErrorNotFound, + ResponseErrorTooManyRequests, + ResponseSuccessJson +} from "italia-ts-commons/lib/responses"; + +import { UserDataProcessing } from "generated/io-api/UserDataProcessing"; +import { UserDataProcessingChoice } from "generated/io-api/UserDataProcessingChoice"; +import { UserDataProcessingChoiceRequest } from "generated/io-api/UserDataProcessingChoiceRequest"; +import winston = require("winston"); +import { User } from "../types/user"; +import { + unhandledResponseStatus, + withCatchAsInternalError, + withValidatedOrInternalError +} from "../utils/responses"; +import { IApiClientFactoryInterface } from "./IApiClientFactory"; + +export default class UserDataProcessingService { + constructor(private readonly apiClient: IApiClientFactoryInterface) {} + + /** + * Create the user data processing of a specific user. + */ + public readonly upsertUserDataProcessing = async ( + user: User, + userDataProcessingChoiceRequest: UserDataProcessingChoiceRequest + ): Promise< + | IResponseErrorInternal + | IResponseErrorTooManyRequests + | IResponseSuccessJson + > => { + const client = this.apiClient.getClient(); + return withCatchAsInternalError(async () => { + const validated = await client.upsertUserDataProcessing({ + fiscalCode: user.fiscal_code, + userDataProcessingChoiceRequest + }); + + return withValidatedOrInternalError(validated, response => + response.status === 200 + ? ResponseSuccessJson(response.value) + : response.status === 429 + ? ResponseErrorTooManyRequests() + : unhandledResponseStatus(response.status) + ); + }); + }; + + /** + * Get the user data processing of a specific user. + */ + public readonly getUserDataProcessing = async ( + user: User, + userDataProcessingChoiceParam: UserDataProcessingChoice + ): Promise< + // tslint:disable-next-line: max-union-size + | IResponseErrorInternal + | IResponseErrorTooManyRequests + | IResponseErrorNotFound + | IResponseSuccessJson + > => { + const client = this.apiClient.getClient(); + return withCatchAsInternalError(async () => { + const validated = await client.getUserDataProcessing({ + fiscalCode: user.fiscal_code, + userDataProcessingChoiceParam + }); + winston.info(`VALIDATE.VALUE => ${validated.value}`); + + return withValidatedOrInternalError(validated, response => + response.status === 200 + ? ResponseSuccessJson(response.value) + : response.status === 404 + ? ResponseErrorNotFound( + "Not Found", + response.value.detail === undefined + ? "User data processing not found" + : response.value.detail + ) + : response.status === 429 + ? ResponseErrorTooManyRequests() + : unhandledResponseStatus(response.status) + ); + }); + }; +} From 26e9d42fcdf3d0c532c2819eea4eb09d7a129cc1 Mon Sep 17 00:00:00 2001 From: AleDore Date: Mon, 2 Mar 2020 17:00:43 +0100 Subject: [PATCH 2/4] fix requested changes --- api_backend.yaml | 8 +-- .../userDataProcessingController.test.ts | 48 ++++++++++------- .../userDataProcessingController.ts | 2 +- .../userDataProcessingService.test.ts | 54 ++++++++++--------- src/services/userDataProcessingService.ts | 2 - 5 files changed, 64 insertions(+), 50 deletions(-) diff --git a/api_backend.yaml b/api_backend.yaml index dc8ba2ffa..90b4ba4ee 100644 --- a/api_backend.yaml +++ b/api_backend.yaml @@ -651,7 +651,7 @@ paths: post: operationId: upsertUserDataProcessing summary: Set User's data processing choices - description: Upsert user data processing for the current authenticated user. + description: Let the authenticated user express his will to retrieve or delete his stored data. parameters: - in: body name: body @@ -673,16 +673,18 @@ paths: description: Conflict. schema: $ref: "#/definitions/ProblemJson" + "429": + description: Too may requests "500": description: User Data processing choice cannot be taken in charge. schema: $ref: "#/definitions/ProblemJson" "/user-data-processing/{choice}": x-swagger-router-controller: UserDataProcessingController - post: + get: operationId: getUserDataProcessing summary: Get User's data processing - description: Get user data processing for the current authenticated user and the given choice. + description: Get the user's request to delete or download his stored data by providing a kind of choice. parameters: - $ref: "#/parameters/UserDataProcessingChoiceParam" responses: diff --git a/src/controllers/__tests__/userDataProcessingController.test.ts b/src/controllers/__tests__/userDataProcessingController.test.ts index bad015df3..c763b4f0d 100644 --- a/src/controllers/__tests__/userDataProcessingController.test.ts +++ b/src/controllers/__tests__/userDataProcessingController.test.ts @@ -2,22 +2,21 @@ /* tslint:disable:no-object-mutation */ import { - ResponseErrorInternal, ResponseErrorNotFound, ResponseSuccessJson } from "italia-ts-commons/lib/responses"; import { NonEmptyString } from "italia-ts-commons/lib/strings"; import { isRight } from "fp-ts/lib/Either"; -import { - UserDataProcessingChoice, - UserDataProcessingChoiceEnum -} from "generated/io-api/UserDataProcessingChoice"; -import { UserDataProcessingChoiceRequest } from "generated/io-api/UserDataProcessingChoiceRequest"; -import UserDataProcessingService from "src/services/userDataProcessingService"; import { EmailAddress } from "../../../generated/backend/EmailAddress"; import { FiscalCode } from "../../../generated/backend/FiscalCode"; import { SpidLevelEnum } from "../../../generated/backend/SpidLevel"; +import { + UserDataProcessingChoice, + UserDataProcessingChoiceEnum +} from "../../../generated/io-api/UserDataProcessingChoice"; +import { UserDataProcessingChoiceRequest } from "../../../generated/io-api/UserDataProcessingChoiceRequest"; +import UserDataProcessingService from "../../../src/services/userDataProcessingService"; import mockReq from "../../__mocks__/request"; import mockRes from "../../__mocks__/response"; import ApiClient from "../../services/apiClientFactory"; @@ -26,6 +25,8 @@ import { User } from "../../types/user"; import UserDataProcessingController from "../userDataProcessingController"; const aTimestamp = 1518010929530; +const aUserDataProcessingChoice = "DOWNLOAD"; + const aUserDataProcessingResponse = { status: 200, value: { @@ -33,7 +34,7 @@ const aUserDataProcessingResponse = { _rid: "AAAAAQAAAAgAAAAAAAAAAQ==", _self: "/dbs/AAAAAQ==/colls/AAAAAQAAAAg=/docs/AAAAAQAAAAgAAAAAAAAAAQ==/", _ts: 1582553174, - choice: "DOWNLOAD", + choice: aUserDataProcessingChoice, createdAt: "2020-02-24T14:06:14.513Z", fiscalCode: "SPNDNL80A13Y555X", id: "SPNDNL80A13Y555X-DOWNLOAD-0000000000000000", @@ -49,8 +50,9 @@ const aValidName = "Giuseppe Maria"; const aValidFamilyname = "Garibaldi"; const aValidSpidLevel = SpidLevelEnum["https://www.spid.gov.it/SpidL2"]; -const userDataProcessingMissingErrorResponse = ResponseErrorInternal( - "Not Found" +const userDataProcessingMissingErrorResponse = ResponseErrorNotFound( + "Not Found", + "User data processing not found" ); // mock for a valid User @@ -96,7 +98,7 @@ describe("UserDataProcessingController#getUserDataProcessing", () => { jest.clearAllMocks(); }); - it("calls the getUserDataProcessing on the UserDataProcessingService with valid values", async () => { + it("should return a valid userDataProcessing by calling UserDataProcessingService with valid values", async () => { const req = mockReq(); mockGetUserDataProcessing.mockReturnValue( @@ -104,6 +106,7 @@ describe("UserDataProcessingController#getUserDataProcessing", () => { ); req.user = mockedUser; + req.params = { choice: aUserDataProcessingChoice }; const apiClient = new ApiClient("XUZTCT88A51Y311X", ""); const userDataProcessingService = new UserDataProcessingService(apiClient); @@ -113,7 +116,10 @@ describe("UserDataProcessingController#getUserDataProcessing", () => { const response = await controller.getUserDataProcessing(req); - expect(mockGetUserDataProcessing).toHaveBeenCalledWith(mockedUser); + expect(mockGetUserDataProcessing).toHaveBeenCalledWith( + mockedUser, + aUserDataProcessingChoice + ); expect(response).toEqual({ apply: expect.any(Function), kind: "IResponseSuccessJson", @@ -121,7 +127,7 @@ describe("UserDataProcessingController#getUserDataProcessing", () => { }); }); - it("calls the getUserDataProcessing on the UserDataProcessingService with empty user", async () => { + it("should return a BadRequestError response by calling UserDataProcessingService with empty user", async () => { const req = mockReq(); const res = mockRes(); @@ -145,17 +151,18 @@ describe("UserDataProcessingController#getUserDataProcessing", () => { expect(res.json).toHaveBeenCalledWith(badRequestErrorResponse); }); - it("should return a ResponseErrorInternal if no user data processing was found", async () => { + it("should return a ResponseErrorNotFound if no user data processing was found", async () => { const req = mockReq(); const res = mockRes(); mockGetUserDataProcessing.mockReturnValue( Promise.resolve( - ResponseErrorNotFound("Not found", "User data processing not found") + ResponseErrorNotFound("Not Found", "User data processing not found") ) ); req.user = mockedUser; + req.params = { choice: aUserDataProcessingChoice }; const apiClient = new ApiClient("XUZTCT88A51Y311X", ""); const userDataProcessingService = new UserDataProcessingService(apiClient); @@ -165,7 +172,10 @@ describe("UserDataProcessingController#getUserDataProcessing", () => { const response = await controller.getUserDataProcessing(req); response.apply(res); - expect(mockGetUserDataProcessing).toHaveBeenCalledWith(mockedUser); + expect(mockGetUserDataProcessing).toHaveBeenCalledWith( + mockedUser, + aUserDataProcessingChoice + ); expect(response).toEqual({ ...userDataProcessingMissingErrorResponse, apply: expect.any(Function) @@ -178,7 +188,7 @@ describe("UserDataProcessingController#upsertUserDataProcessing", () => { jest.clearAllMocks(); }); - it("calls the upsertUserDataProcessing on the UserDataProcessingService with valid values", async () => { + it("should return a valid upsertedUserDataProcessingby calling UserDataProcessingService with valid values", async () => { const req = mockReq(); mockUpsertUserDataProcessing.mockReturnValue( @@ -211,7 +221,7 @@ describe("UserDataProcessingController#upsertUserDataProcessing", () => { }); }); - it("calls the upsertUserDataProcessing on the UserDataProcessingService with empty user and valid upsert user", async () => { + it("should return a BadRequestError response by calling upsertUserDataProcessing on the UserDataProcessingService with empty user and valid upsert user", async () => { const req = mockReq(); const res = mockRes(); @@ -234,7 +244,7 @@ describe("UserDataProcessingController#upsertUserDataProcessing", () => { expect(res.json).toHaveBeenCalledWith(badRequestErrorResponse); }); - it("calls the upsertUserDataProcessing on the UserDataProcessingService with valid user and empty upsert profile", async () => { + it("should return a BadRequestError response by calling upsertUserDataProcessing on the UserDataProcessingService with valid user and empty upsert user data processing", async () => { const req = mockReq(); const res = mockRes(); diff --git a/src/controllers/userDataProcessingController.ts b/src/controllers/userDataProcessingController.ts index d89d500d7..4e2a5c69a 100644 --- a/src/controllers/userDataProcessingController.ts +++ b/src/controllers/userDataProcessingController.ts @@ -1,5 +1,5 @@ /** - * This controller handles reading the user profile from the + * This controller handles reading/upserting the user data processing from the * app by forwarding the call to the API system. */ diff --git a/src/services/__tests__/userDataProcessingService.test.ts b/src/services/__tests__/userDataProcessingService.test.ts index 11cd97468..6d61b37a4 100644 --- a/src/services/__tests__/userDataProcessingService.test.ts +++ b/src/services/__tests__/userDataProcessingService.test.ts @@ -3,13 +3,13 @@ import * as t from "io-ts"; import { FiscalCode, NonEmptyString } from "italia-ts-commons/lib/strings"; -import { SpidLevelEnum } from "generated/backend/SpidLevel"; -import { EmailAddress } from "generated/io-api/EmailAddress"; +import { SpidLevelEnum } from "../../../generated/backend/SpidLevel"; +import { EmailAddress } from "../../../generated/io-api/EmailAddress"; import { UserDataProcessingChoice, UserDataProcessingChoiceEnum -} from "generated/io-api/UserDataProcessingChoice"; -import { UserDataProcessingChoiceRequest } from "generated/io-api/UserDataProcessingChoiceRequest"; +} from "../../../generated/io-api/UserDataProcessingChoice"; +import { UserDataProcessingChoiceRequest } from "../../../generated/io-api/UserDataProcessingChoiceRequest"; import { SessionToken, WalletToken } from "../../types/token"; import { User } from "../../types/user"; import ApiClientFactory from "../apiClientFactory"; @@ -18,20 +18,24 @@ import UserDataProcessingService from "../userDataProcessingService"; const aValidEmail = "test@example.com" as EmailAddress; const aValidFiscalCode = "SPNDNL80A13Y555X" as FiscalCode; const aValidSpidLevel = SpidLevelEnum["https://www.spid.gov.it/SpidL2"]; +const aUserDataProcessingResponse = { + _etag: "bdb8f644-132c-4f3c-a051-5887fc8058b1", + _rid: "AAAAAQAAAAgAAAAAAAAAAQ==", + _self: "/dbs/AAAAAQ==/colls/AAAAAQAAAAg=/docs/AAAAAQAAAAgAAAAAAAAAAQ==/", + _ts: 1582553174, + choice: "DOWNLOAD", + createdAt: "2020-02-24T14:06:14.513Z", + fiscalCode: "SPNDNL80A13Y555X", + id: "SPNDNL80A13Y555X-DOWNLOAD-0000000000000000", + status: "PENDING", + userDataProcessingId: "SPNDNL80A13Y555X-DOWNLOAD", + version: 0 +}; + const validApiUserDataProcessingResponse = { status: 200, value: { - _etag: "bdb8f644-132c-4f3c-a051-5887fc8058b1", - _rid: "AAAAAQAAAAgAAAAAAAAAAQ==", - _self: "/dbs/AAAAAQ==/colls/AAAAAQAAAAg=/docs/AAAAAQAAAAgAAAAAAAAAAQ==/", - _ts: 1582553174, - choice: "DOWNLOAD", - createdAt: "2020-02-24T14:06:14.513Z", - fiscalCode: "SPNDNL80A13Y555X", - id: "SPNDNL80A13Y555X-DOWNLOAD-0000000000000000", - status: "PENDING", - userDataProcessingId: "SPNDNL80A13Y555X-DOWNLOAD", - version: 0 + ...aUserDataProcessingResponse } }; @@ -65,14 +69,14 @@ const mockedUserDataProcessingChoiceRequest: UserDataProcessingChoiceRequest = { choice: mockedUserDataProcessingChoice }; const mockGetUserDataProcessing = jest.fn(); +const mockUpsertUserDataProcessing = jest.fn(); const mockGetClient = jest.fn().mockImplementation(() => { return { - getUserDataProcessing: mockGetUserDataProcessing + getUserDataProcessing: mockGetUserDataProcessing, + upsertUserDataProcessing: mockUpsertUserDataProcessing }; }); -const mockUpsertUserDataProcessing = jest.fn(); - beforeEach(() => { jest.clearAllMocks(); }); @@ -88,7 +92,7 @@ jest.mock("../../services/apiClientFactory", () => { const api = new ApiClientFactory("", ""); describe("UserDataProcessingService#getUserDataProcessing", () => { - it("returns a user data processing from the API", async () => { + it("should returns a user data processing from the API", async () => { mockGetUserDataProcessing.mockImplementation(() => { return t.success(validApiUserDataProcessingResponse); }); @@ -102,11 +106,11 @@ describe("UserDataProcessingService#getUserDataProcessing", () => { expect(mockGetUserDataProcessing).toHaveBeenCalledWith({ fiscalCode: mockedUser.fiscal_code, - userDataProcessingChoice: mockedUserDataProcessingChoice + userDataProcessingChoiceParam: mockedUserDataProcessingChoice }); expect(res).toMatchObject({ kind: "IResponseSuccessJson", - value: mockGetUserDataProcessing + value: aUserDataProcessingResponse }); }); @@ -136,7 +140,7 @@ describe("UserDataProcessingService#getUserDataProcessing", () => { ); expect(mockGetUserDataProcessing).toHaveBeenCalledWith({ fiscalCode: mockedUser.fiscal_code, - userDataProcessingChoice: mockedUserDataProcessingChoice + userDataProcessingChoiceParam: mockedUserDataProcessingChoice }); expect(res.kind).toEqual("IResponseErrorInternal"); }); @@ -154,7 +158,7 @@ describe("UserDataProcessingService#getUserDataProcessing", () => { ); expect(mockGetUserDataProcessing).toHaveBeenCalledWith({ fiscalCode: mockedUser.fiscal_code, - userDataProcessingChoice: mockedUserDataProcessingChoice + userDataProcessingChoiceParam: mockedUserDataProcessingChoice }); expect(res.kind).toEqual("IResponseErrorInternal"); }); @@ -182,7 +186,7 @@ describe("UserDataProcessingService#upsertUserDataProcessing", () => { }); expect(res).toMatchObject({ kind: "IResponseSuccessJson", - value: mockUpsertUserDataProcessing + value: aUserDataProcessingResponse }); }); @@ -219,7 +223,7 @@ describe("UserDataProcessingService#upsertUserDataProcessing", () => { expect(res.kind).toEqual("IResponseErrorInternal"); }); - it("returns a 500 response if the response from the getMessagesByUser API returns something wrong", async () => { + it("should return a 500 response if the response from the upsertUserDataProcessing API returns something wrong", async () => { mockUpsertUserDataProcessing.mockImplementation(() => t.success(invalidApiUserDataProcessingResponse) ); diff --git a/src/services/userDataProcessingService.ts b/src/services/userDataProcessingService.ts index 853ed9cd1..e07276da4 100644 --- a/src/services/userDataProcessingService.ts +++ b/src/services/userDataProcessingService.ts @@ -16,7 +16,6 @@ import { import { UserDataProcessing } from "generated/io-api/UserDataProcessing"; import { UserDataProcessingChoice } from "generated/io-api/UserDataProcessingChoice"; import { UserDataProcessingChoiceRequest } from "generated/io-api/UserDataProcessingChoiceRequest"; -import winston = require("winston"); import { User } from "../types/user"; import { unhandledResponseStatus, @@ -75,7 +74,6 @@ export default class UserDataProcessingService { fiscalCode: user.fiscal_code, userDataProcessingChoiceParam }); - winston.info(`VALIDATE.VALUE => ${validated.value}`); return withValidatedOrInternalError(validated, response => response.status === 200 From c6ea0aeb2870545f2f017b57efbb75d218559f8d Mon Sep 17 00:00:00 2001 From: AleDore Date: Tue, 3 Mar 2020 09:38:37 +0100 Subject: [PATCH 3/4] resolve requested changes --- api_backend.yaml | 6 +----- .../userDataProcessingController.test.ts | 10 +++++----- .../__tests__/userDataProcessingService.test.ts | 16 ++++++++-------- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/api_backend.yaml b/api_backend.yaml index 90b4ba4ee..187c9072d 100644 --- a/api_backend.yaml +++ b/api_backend.yaml @@ -660,7 +660,7 @@ paths: required: true responses: '200': - description: User Data processing created. + description: User Data processing created / updated. schema: $ref: "#/definitions/UserDataProcessing" "400": @@ -692,10 +692,6 @@ paths: description: User data processing retrieved schema: $ref: "#/definitions/UserDataProcessing" - "400": - description: Invalid request. - schema: - $ref: "#/definitions/ProblemJson" "401": description: Bearer token null or expired. "404": diff --git a/src/controllers/__tests__/userDataProcessingController.test.ts b/src/controllers/__tests__/userDataProcessingController.test.ts index c763b4f0d..0f8e2c011 100644 --- a/src/controllers/__tests__/userDataProcessingController.test.ts +++ b/src/controllers/__tests__/userDataProcessingController.test.ts @@ -127,7 +127,7 @@ describe("UserDataProcessingController#getUserDataProcessing", () => { }); }); - it("should return a BadRequestError response by calling UserDataProcessingService with empty user", async () => { + it("should return an error by calling UserDataProcessingService with empty user", async () => { const req = mockReq(); const res = mockRes(); @@ -151,7 +151,7 @@ describe("UserDataProcessingController#getUserDataProcessing", () => { expect(res.json).toHaveBeenCalledWith(badRequestErrorResponse); }); - it("should return a ResponseErrorNotFound if no user data processing was found", async () => { + it("should return a 404 error if no user data processing was found", async () => { const req = mockReq(); const res = mockRes(); @@ -188,7 +188,7 @@ describe("UserDataProcessingController#upsertUserDataProcessing", () => { jest.clearAllMocks(); }); - it("should return a valid upsertedUserDataProcessingby calling UserDataProcessingService with valid values", async () => { + it("should return a valid upsertedUserDataProcessing by calling UserDataProcessingService with valid values", async () => { const req = mockReq(); mockUpsertUserDataProcessing.mockReturnValue( @@ -221,7 +221,7 @@ describe("UserDataProcessingController#upsertUserDataProcessing", () => { }); }); - it("should return a BadRequestError response by calling upsertUserDataProcessing on the UserDataProcessingService with empty user and valid upsert user", async () => { + it("should return an error response by calling UserDataProcessingService with empty user and a valid choice", async () => { const req = mockReq(); const res = mockRes(); @@ -244,7 +244,7 @@ describe("UserDataProcessingController#upsertUserDataProcessing", () => { expect(res.json).toHaveBeenCalledWith(badRequestErrorResponse); }); - it("should return a BadRequestError response by calling upsertUserDataProcessing on the UserDataProcessingService with valid user and empty upsert user data processing", async () => { + it("should return an error by calling UserDataProcessingService with a valid user and empty choice", async () => { const req = mockReq(); const res = mockRes(); diff --git a/src/services/__tests__/userDataProcessingService.test.ts b/src/services/__tests__/userDataProcessingService.test.ts index 6d61b37a4..d5b10b11d 100644 --- a/src/services/__tests__/userDataProcessingService.test.ts +++ b/src/services/__tests__/userDataProcessingService.test.ts @@ -92,7 +92,7 @@ jest.mock("../../services/apiClientFactory", () => { const api = new ApiClientFactory("", ""); describe("UserDataProcessingService#getUserDataProcessing", () => { - it("should returns a user data processing from the API", async () => { + it("should return a user data processing from the API", async () => { mockGetUserDataProcessing.mockImplementation(() => { return t.success(validApiUserDataProcessingResponse); }); @@ -114,7 +114,7 @@ describe("UserDataProcessingService#getUserDataProcessing", () => { }); }); - it("returns an 429 HTTP error from getUserDataProcessing upstream API", async () => { + it("should return a 429 HTTP error from getUserDataProcessing upstream API", async () => { mockGetUserDataProcessing.mockImplementation(() => t.success(tooManyReqApiUserDataProcessingResponse) ); @@ -129,7 +129,7 @@ describe("UserDataProcessingService#getUserDataProcessing", () => { expect(res.kind).toEqual("IResponseErrorTooManyRequests"); }); - it("returns an error if the getUserDataProcessing API returns an error", async () => { + it("should return an error if the getUserDataProcessing API returns an error", async () => { mockGetUserDataProcessing.mockImplementation(() => t.success(problemJson)); const service = new UserDataProcessingService(api); @@ -145,7 +145,7 @@ describe("UserDataProcessingService#getUserDataProcessing", () => { expect(res.kind).toEqual("IResponseErrorInternal"); }); - it("returns a 500 response if the response from the getUserDataProcessing API returns something wrong", async () => { + it("should return an error if the getUserDataProcessing API returns invalid data", async () => { mockGetUserDataProcessing.mockImplementation(() => t.success(invalidApiUserDataProcessingResponse) ); @@ -168,7 +168,7 @@ describe("UserDataProcessingService#upsertUserDataProcessing", () => { beforeEach(() => { jest.clearAllMocks(); }); - it("returns an upserted user data processing from the API", async () => { + it("should return an upserted user data processing from the API", async () => { mockUpsertUserDataProcessing.mockImplementation(() => { return t.success(validApiUserDataProcessingResponse); }); @@ -190,7 +190,7 @@ describe("UserDataProcessingService#upsertUserDataProcessing", () => { }); }); - it("returns an 429 HTTP error from upsertUserDataProcessing upstream API", async () => { + it("should return an 429 HTTP error from upsertUserDataProcessing upstream API", async () => { mockUpsertUserDataProcessing.mockImplementation(() => t.success(tooManyReqApiUserDataProcessingResponse) ); @@ -205,7 +205,7 @@ describe("UserDataProcessingService#upsertUserDataProcessing", () => { expect(res.kind).toEqual("IResponseErrorTooManyRequests"); }); - it("returns an error if the upsertUserDataProcessing API returns an error", async () => { + it("should return an error if the upsertUserDataProcessing API returns an error", async () => { mockUpsertUserDataProcessing.mockImplementation(() => t.success(problemJson) ); @@ -223,7 +223,7 @@ describe("UserDataProcessingService#upsertUserDataProcessing", () => { expect(res.kind).toEqual("IResponseErrorInternal"); }); - it("should return a 500 response if the response from the upsertUserDataProcessing API returns something wrong", async () => { + it("should return an error if the upsertUserDataProcessing API returns invalid data", async () => { mockUpsertUserDataProcessing.mockImplementation(() => t.success(invalidApiUserDataProcessingResponse) ); From 0ee10254b6dfc4e8e5ad18695bd7a395cdbc7677 Mon Sep 17 00:00:00 2001 From: AleDore Date: Tue, 3 Mar 2020 10:22:56 +0100 Subject: [PATCH 4/4] try to fix codecov --- src/services/userDataProcessingService.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/services/userDataProcessingService.ts b/src/services/userDataProcessingService.ts index e07276da4..40f5b8054 100644 --- a/src/services/userDataProcessingService.ts +++ b/src/services/userDataProcessingService.ts @@ -79,12 +79,7 @@ export default class UserDataProcessingService { response.status === 200 ? ResponseSuccessJson(response.value) : response.status === 404 - ? ResponseErrorNotFound( - "Not Found", - response.value.detail === undefined - ? "User data processing not found" - : response.value.detail - ) + ? ResponseErrorNotFound("Not Found", "User data processing not found") : response.status === 429 ? ResponseErrorTooManyRequests() : unhandledResponseStatus(response.status)