From 794082667f595fd2c44beaa864f97b4e418a84db Mon Sep 17 00:00:00 2001 From: Federico Feroldi Date: Wed, 21 Aug 2019 16:54:39 +0200 Subject: [PATCH] Adds GetService function --- GetService/__tests__/handler.test.ts | 118 +++++++++++++++++++++++++++ GetService/function.json | 21 +++++ GetService/handler.ts | 96 ++++++++++++++++++++++ GetService/index.ts | 64 +++++++++++++++ 4 files changed, 299 insertions(+) create mode 100644 GetService/__tests__/handler.test.ts create mode 100644 GetService/function.json create mode 100644 GetService/handler.ts create mode 100644 GetService/index.ts diff --git a/GetService/__tests__/handler.test.ts b/GetService/__tests__/handler.test.ts new file mode 100644 index 00000000..3c6cbb9e --- /dev/null +++ b/GetService/__tests__/handler.test.ts @@ -0,0 +1,118 @@ +// tslint:disable:no-any + +import { none, some } from "fp-ts/lib/Option"; + +import { left, right } from "fp-ts/lib/Either"; +import { NonNegativeNumber } from "italia-ts-commons/lib/numbers"; +import { + NonEmptyString, + OrganizationFiscalCode +} from "italia-ts-commons/lib/strings"; + +import { + NewService, + RetrievedService, + Service, + toAuthorizedCIDRs, + toAuthorizedRecipients +} from "io-functions-commons/dist/src/models/service"; + +import { MaxAllowedPaymentAmount } from "io-functions-commons/dist/generated/definitions/MaxAllowedPaymentAmount"; +import { ServicePublic } from "io-functions-commons/dist/generated/definitions/ServicePublic"; + +import { GetServiceHandler } from "../handler"; + +afterEach(() => { + jest.resetAllMocks(); + jest.restoreAllMocks(); +}); + +const anOrganizationFiscalCode = "01234567890" as OrganizationFiscalCode; + +const aServicePayload: ServicePublic = { + department_name: "MyDeptName" as NonEmptyString, + organization_fiscal_code: anOrganizationFiscalCode, + organization_name: "MyOrgName" as NonEmptyString, + service_id: "MySubscriptionId" as NonEmptyString, + service_name: "MyServiceName" as NonEmptyString, + version: 1 +}; + +const aService: Service = { + authorizedCIDRs: toAuthorizedCIDRs([]), + authorizedRecipients: toAuthorizedRecipients([]), + departmentName: "MyDeptName" as NonEmptyString, + isVisible: true, + maxAllowedPaymentAmount: 0 as MaxAllowedPaymentAmount, + organizationFiscalCode: anOrganizationFiscalCode, + organizationName: "MyOrgName" as NonEmptyString, + serviceId: "MySubscriptionId" as NonEmptyString, + serviceName: "MyServiceName" as NonEmptyString +}; + +const aNewService: NewService = { + ...aService, + id: "123" as NonEmptyString, + kind: "INewService", + version: 1 as NonNegativeNumber +}; + +const aRetrievedService: RetrievedService = { + ...aNewService, + _self: "123", + _ts: 123, + kind: "IRetrievedService" +}; + +const aSeralizedService: ServicePublic = { + ...aServicePayload, + version: 1 as NonNegativeNumber +}; + +describe("GetServiceHandler", () => { + it("should get an existing service", async () => { + const serviceModelMock = { + findOneByServiceId: jest.fn(() => { + return Promise.resolve(right(some(aRetrievedService))); + }) + }; + const aServiceId = "1" as NonEmptyString; + const getServiceHandler = GetServiceHandler(serviceModelMock as any); + const response = await getServiceHandler(aServiceId); + expect(serviceModelMock.findOneByServiceId).toHaveBeenCalledWith( + aServiceId + ); + expect(response.kind).toBe("IResponseSuccessJson"); + if (response.kind === "IResponseSuccessJson") { + expect(response.value).toEqual(aSeralizedService); + } + }); + it("should fail on errors during get", async () => { + const serviceModelMock = { + findOneByServiceId: jest.fn(() => { + return Promise.resolve(left(none)); + }) + }; + const aServiceId = "1" as NonEmptyString; + const getServiceHandler = GetServiceHandler(serviceModelMock as any); + const response = await getServiceHandler(aServiceId); + expect(serviceModelMock.findOneByServiceId).toHaveBeenCalledWith( + aServiceId + ); + expect(response.kind).toBe("IResponseErrorQuery"); + }); + it("should return not found if the service does not exist", async () => { + const serviceModelMock = { + findOneByServiceId: jest.fn(() => { + return Promise.resolve(right(none)); + }) + }; + const aServiceId = "1" as NonEmptyString; + const getServiceHandler = GetServiceHandler(serviceModelMock as any); + const response = await getServiceHandler(aServiceId); + expect(serviceModelMock.findOneByServiceId).toHaveBeenCalledWith( + aServiceId + ); + expect(response.kind).toBe("IResponseErrorNotFound"); + }); +}); diff --git a/GetService/function.json b/GetService/function.json new file mode 100644 index 00000000..529804fe --- /dev/null +++ b/GetService/function.json @@ -0,0 +1,21 @@ +{ + "bindings": [ + { + "authLevel": "function", + "type": "httpTrigger", + "direction": "in", + "name": "req", + "route": "v1/services/{serviceid}", + "methods": [ + "get", + "post" + ] + }, + { + "type": "http", + "direction": "out", + "name": "res" + } + ], + "scriptFile": "../dist/GetService/index.js" +} \ No newline at end of file diff --git a/GetService/handler.ts b/GetService/handler.ts new file mode 100644 index 00000000..44002ce4 --- /dev/null +++ b/GetService/handler.ts @@ -0,0 +1,96 @@ +/* + * Implements the Public API handlers for the Services resource. + */ + +import * as express from "express"; + +import { + IResponseErrorNotFound, + IResponseSuccessJson, + ResponseErrorNotFound, + ResponseSuccessJson +} from "italia-ts-commons/lib/responses"; + +import { NonEmptyString } from "italia-ts-commons/lib/strings"; + +import { RequiredParamMiddleware } from "io-functions-commons/dist/src/utils/middlewares/required_param"; +import { + withRequestMiddlewares, + wrapRequestHandler +} from "io-functions-commons/dist/src/utils/request_middleware"; +import { + IResponseErrorQuery, + ResponseErrorQuery +} from "io-functions-commons/dist/src/utils/response"; + +import { + RetrievedService, + ServiceModel +} from "io-functions-commons/dist/src/models/service"; + +import { ServiceId } from "io-functions-commons/dist/generated/definitions/ServiceId"; +import { ServicePublic } from "io-functions-commons/dist/generated/definitions/ServicePublic"; + +type IGetServiceHandlerRet = + | IResponseSuccessJson + | IResponseErrorNotFound + | IResponseErrorQuery; + +type IGetServiceHandler = ( + serviceId: ServiceId +) => Promise; + +/** + * Converts a retrieved service to a service that can be shared via API + */ +function retrievedServiceToPublic( + retrievedService: RetrievedService +): ServicePublic { + return { + department_name: retrievedService.departmentName, + organization_fiscal_code: retrievedService.organizationFiscalCode, + organization_name: retrievedService.organizationName, + service_id: retrievedService.serviceId, + service_name: retrievedService.serviceName, + version: retrievedService.version + }; +} + +/** + * Extracts the serviceId value from the URL path parameter. + */ +const requiredServiceIdMiddleware = RequiredParamMiddleware( + "serviceid", + NonEmptyString +); + +export function GetServiceHandler( + serviceModel: ServiceModel +): IGetServiceHandler { + return async serviceId => + (await serviceModel.findOneByServiceId(serviceId)).fold< + IGetServiceHandlerRet + >( + error => ResponseErrorQuery("Error while retrieving the service", error), + maybeService => + maybeService.foldL< + IResponseErrorNotFound | IResponseSuccessJson + >( + () => + ResponseErrorNotFound( + "Service not found", + "The service you requested was not found in the system." + ), + service => ResponseSuccessJson(retrievedServiceToPublic(service)) + ) + ); +} + +/** + * Wraps a GetService handler inside an Express request handler. + */ +export function GetService(serviceModel: ServiceModel): express.RequestHandler { + const handler = GetServiceHandler(serviceModel); + const middlewaresWrap = withRequestMiddlewares(requiredServiceIdMiddleware); + return wrapRequestHandler(middlewaresWrap(handler)); +} diff --git a/GetService/index.ts b/GetService/index.ts new file mode 100644 index 00000000..243bcea8 --- /dev/null +++ b/GetService/index.ts @@ -0,0 +1,64 @@ +import { Context } from "@azure/functions"; + +import * as cors from "cors"; +import * as express from "express"; +import * as winston from "winston"; + +import { DocumentClient as DocumentDBClient } from "documentdb"; + +import { + SERVICE_COLLECTION_NAME, + ServiceModel +} from "io-functions-commons/dist/src/models/service"; +import * as documentDbUtils from "io-functions-commons/dist/src/utils/documentdb"; +import { getRequiredStringEnv } from "io-functions-commons/dist/src/utils/env"; +import { secureExpressApp } from "io-functions-commons/dist/src/utils/express"; +import { AzureContextTransport } from "io-functions-commons/dist/src/utils/logging"; +import { setAppContext } from "io-functions-commons/dist/src/utils/middlewares/context_middleware"; + +import createAzureFunctionHandler from "io-functions-express/dist/src/createAzureFunctionsHandler"; + +import { GetService } from "./handler"; + +// Setup Express +const app = express(); +secureExpressApp(app); + +// Set up CORS (free access to the API from browser clients) +app.use(cors()); + +const cosmosDbUri = getRequiredStringEnv("CUSTOMCONNSTR_COSMOSDB_URI"); +const cosmosDbKey = getRequiredStringEnv("CUSTOMCONNSTR_COSMOSDB_KEY"); +const cosmosDbName = getRequiredStringEnv("COSMOSDB_NAME"); + +const documentDbDatabaseUrl = documentDbUtils.getDatabaseUri(cosmosDbName); +const servicesCollectionUrl = documentDbUtils.getCollectionUri( + documentDbDatabaseUrl, + SERVICE_COLLECTION_NAME +); + +const documentClient = new DocumentDBClient(cosmosDbUri, { + masterKey: cosmosDbKey +}); + +const serviceModel = new ServiceModel(documentClient, servicesCollectionUrl); + +app.get("/api/v1/services/:serviceid", GetService(serviceModel)); + +const azureFunctionHandler = createAzureFunctionHandler(app); + +// tslint:disable-next-line: no-let +let logger: Context["log"] | undefined; +const contextTransport = new AzureContextTransport(() => logger, { + level: "debug" +}); +winston.add(contextTransport); + +// Binds the express app to an Azure Function handler +function httpStart(context: Context): void { + logger = context.log; + setAppContext(app, context); + azureFunctionHandler(context); +} + +export default httpStart;