Skip to content

Commit

Permalink
[#IP-145] New table for users with failed user_data_processing (#138)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaeldisaro authored May 10, 2021
1 parent 63007f6 commit 28d6f91
Show file tree
Hide file tree
Showing 19 changed files with 1,208 additions and 210 deletions.
4 changes: 2 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@
"port": 5861,
"restart": true,
"localRoot": "${workspaceFolder}",
"remoteRoot": "${workspaceFolder}",
"remoteRoot": "/usr/src/app",
"outFiles": [
"${workspaceFolder}/dist/**/*.js"
"${workspaceFolder}/src/**/*.js"
],
"skipFiles": [
"<node_internals>/**/*.js"
Expand Down
157 changes: 157 additions & 0 deletions GetFailedUserDataProcessing/__tests__/handler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/* tslint:disable: no-any */

import { FiscalCode, NonEmptyString } from "@pagopa/ts-commons/lib/strings";
import { TableService } from "azure-storage";
import {
UserDataProcessingChoice,
UserDataProcessingChoiceEnum
} from "io-functions-commons/dist/generated/definitions/UserDataProcessingChoice";
import { GetFailedUserDataProcessingHandler } from "../handler";

const findEntry = (
entries: ReadonlyArray<{
PartitionKey: UserDataProcessingChoice;
RowKey: FiscalCode;
}>
) => (choice, fiscalCode) =>
entries.length > 0
? entries
.filter(e => e.PartitionKey === choice && e.RowKey === fiscalCode)
.map(e => ({
RowKey: { _: e.RowKey }
}))[0]
: null;

const retrieveEntityFailedUserDataProcessingMock = (
entries: ReadonlyArray<{
PartitionKey: UserDataProcessingChoice;
RowKey: FiscalCode;
}>
) =>
jest.fn((_, choice, fiscalCode, ____, cb) => {
return cb(
findEntry(entries)(choice, fiscalCode)
? null
: new Error("Internal error"),
findEntry(entries)(choice, fiscalCode),
{
isSuccessful: findEntry(entries)(choice, fiscalCode),
statusCode: findEntry(entries)(choice, fiscalCode) ? 200 : 404
}
);
});

const internalErrorRetrieveEntityFailedUserDataProcessingMock = (
entries: ReadonlyArray<{
PartitionKey: UserDataProcessingChoice;
RowKey: FiscalCode;
}>
) =>
jest.fn((_, choice, fiscalCode, ____, cb) => {
return cb(new Error("Internal error"), null, { isSuccessful: false });
});

const storageTableMock = "FailedUserDataProcessing" as NonEmptyString;

const fiscalCode1 = "UEEFON48A55Y758X" as FiscalCode;
const fiscalCode2 = "VEEGON48A55Y758Z" as FiscalCode;

const noFailedRequests = [];

const failedRequests = [
{
PartitionKey: UserDataProcessingChoiceEnum.DELETE,
RowKey: fiscalCode1
},
{
PartitionKey: UserDataProcessingChoiceEnum.DOWNLOAD,
RowKey: fiscalCode1
},
{
PartitionKey: UserDataProcessingChoiceEnum.DELETE,
RowKey: fiscalCode2
},
{
PartitionKey: UserDataProcessingChoiceEnum.DOWNLOAD,
RowKey: fiscalCode2
}
];

beforeEach(() => {
jest.clearAllMocks();
});

describe("GetFailedUserDataProcessingHandler", () => {
it("should return a not found error response if no failed user data processing request is present", async () => {
const tableServiceMock = ({
retrieveEntity: retrieveEntityFailedUserDataProcessingMock(
noFailedRequests
)
} as any) as TableService;

const getFailedUserDataProcessingHandler = GetFailedUserDataProcessingHandler(
tableServiceMock,
storageTableMock
);

const result = await getFailedUserDataProcessingHandler(
{} as any,
{} as any,
UserDataProcessingChoiceEnum.DELETE,
fiscalCode1
);

expect(result.kind).toBe("IResponseErrorNotFound");
});

it("should return an internal error response if retrieve entity fails", async () => {
const tableServiceMock = ({
retrieveEntity: internalErrorRetrieveEntityFailedUserDataProcessingMock(
noFailedRequests
)
} as any) as TableService;

const getFailedUserDataProcessingHandler = GetFailedUserDataProcessingHandler(
tableServiceMock,
storageTableMock
);

const result = await getFailedUserDataProcessingHandler(
{} as any,
{} as any,
UserDataProcessingChoiceEnum.DELETE,
fiscalCode1
);

expect(result.kind).toBe("IResponseErrorInternal");
});

it("should return a fiscalcode if a failed request is present", async () => {
const tableServiceMock = ({
retrieveEntity: retrieveEntityFailedUserDataProcessingMock(failedRequests)
} as any) as TableService;

const getFailedUserDataProcessingHandler = GetFailedUserDataProcessingHandler(
tableServiceMock,
storageTableMock
);

const result = await getFailedUserDataProcessingHandler(
{} as any,
{} as any,
UserDataProcessingChoiceEnum.DELETE,
fiscalCode1
);

expect(result.kind).toBe("IResponseSuccessJson");
if (result.kind === "IResponseSuccessJson") {
expect(result.value).toEqual({
failedDataProcessingUser: fiscalCode1
});

expect(result.value).not.toEqual({
failedDataProcessingUser: fiscalCode2
});
}
});
});
20 changes: 20 additions & 0 deletions GetFailedUserDataProcessing/function.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"bindings": [
{
"authLevel": "function",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"route": "adm/user-data-processing/failed/{choice}/{fiscalCode}",
"methods": [
"get"
]
},
{
"type": "http",
"direction": "out",
"name": "res"
}
],
"scriptFile": "../dist/GetFailedUserDataProcessing/index.js"
}
120 changes: 120 additions & 0 deletions GetFailedUserDataProcessing/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import * as express from "express";

import { Context } from "@azure/functions";
import { ServiceModel } from "io-functions-commons/dist/src/models/service";
import {
AzureUserAttributesMiddleware,
IAzureUserAttributes
} from "io-functions-commons/dist/src/utils/middlewares/azure_user_attributes";
import { ContextMiddleware } from "io-functions-commons/dist/src/utils/middlewares/context_middleware";
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 {
IResponseSuccessJson,
ResponseSuccessJson,
IResponseErrorInternal,
ResponseErrorInternal,
IResponseErrorNotFound
} from "@pagopa/ts-commons/lib/responses";
import { ServiceResponse, TableService } from "azure-storage";
import { fromOption, toError } from "fp-ts/lib/Either";
import { NonEmptyString, FiscalCode } from "@pagopa/ts-commons/lib/strings";
import { UserDataProcessingChoice } from "io-functions-commons/dist/generated/definitions/UserDataProcessingChoice";
import { fromEither, tryCatch } from "fp-ts/lib/TaskEither";
import { none, Option, some } from "fp-ts/lib/Option";
import { ResponseErrorNotFound } from "italia-ts-commons/lib/responses";

type TableEntry = Readonly<{
readonly RowKey: Readonly<{
readonly _: FiscalCode;
}>;
}>;

type ResultSet = Readonly<{
readonly failedDataProcessingUser: FiscalCode;
}>;

type IHttpHandler = (
context: Context,
userAttrs: IAzureUserAttributes,
param1: UserDataProcessingChoice,
param2: FiscalCode
) => Promise<
| IResponseSuccessJson<ResultSet>
| IResponseErrorInternal
| IResponseErrorNotFound
>;

export const GetFailedUserDataProcessingHandler = (
tableService: TableService,
failedUserDataProcessingTable: NonEmptyString
): IHttpHandler => async (
_,
__,
choice,
fiscalCode
): Promise<
| IResponseSuccessJson<ResultSet>
| IResponseErrorInternal
| IResponseErrorNotFound
> =>
tryCatch(
() =>
new Promise<Option<TableEntry>>((resolve, reject) =>
tableService.retrieveEntity(
failedUserDataProcessingTable,
choice,
fiscalCode,
null,
(error: Error, result: TableEntry, response: ServiceResponse) =>
response.isSuccessful
? resolve(some(result))
: response.statusCode === 404
? resolve(none)
: reject(error)
)
),
toError
)
.mapLeft<IResponseErrorInternal | IResponseErrorNotFound>(er =>
ResponseErrorInternal(er.message)
)
.chain(maybeTableEntry =>
fromEither(
fromOption(ResponseErrorNotFound("Not found!", "No record found."))(
maybeTableEntry
)
)
)
.map(rs => ({
failedDataProcessingUser: rs.RowKey._
}))
.fold<
| IResponseSuccessJson<ResultSet>
| IResponseErrorInternal
| IResponseErrorNotFound
>(er => er, ResponseSuccessJson)
.run();

export const GetFailedUserDataProcessing = (
serviceModel: ServiceModel,
tableService: TableService,
failedUserDataProcessingTable: NonEmptyString
): express.RequestHandler => {
const handler = GetFailedUserDataProcessingHandler(
tableService,
failedUserDataProcessingTable
);

const middlewaresWrap = withRequestMiddlewares(
ContextMiddleware(),
AzureUserAttributesMiddleware(serviceModel),
RequiredParamMiddleware("choice", UserDataProcessingChoice),
RequiredParamMiddleware("fiscalCode", FiscalCode)
);

return wrapRequestHandler(middlewaresWrap(handler));
};
67 changes: 67 additions & 0 deletions GetFailedUserDataProcessing/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import * as express from "express";
import * as winston from "winston";

import { Context } from "@azure/functions";
import {
SERVICE_COLLECTION_NAME,
ServiceModel
} from "io-functions-commons/dist/src/models/service";
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 { createTableService } from "azure-storage";
import { getConfigOrThrow } from "../utils/config";
import { cosmosdbClient } from "../utils/cosmosdb";
import { GetFailedUserDataProcessing } from "./handler";

/**
* Table service
*/
const config = getConfigOrThrow();
const storageConnectionString =
config.FailedUserDataProcessingStorageConnection;
const failedUserDataProcessingTable = config.FAILED_USER_DATA_PROCESSING_TABLE;
const tableService = createTableService(storageConnectionString);

/**
* Service container
*/
const servicesContainer = cosmosdbClient
.database(config.COSMOSDB_NAME)
.container(SERVICE_COLLECTION_NAME);

const serviceModel = new ServiceModel(servicesContainer);

// eslint-disable-next-line functional/no-let
let logger: Context["log"] | undefined;
const contextTransport = new AzureContextTransport(() => logger, {
level: "debug"
});
winston.add(contextTransport);

// Setup Express
const app = express();
secureExpressApp(app);

// Add express route
app.get(
"/adm/user-data-processing/failed/:choice/:fiscalCode",
GetFailedUserDataProcessing(
serviceModel,
tableService,
failedUserDataProcessingTable
)
);

const azureFunctionHandler = createAzureFunctionHandler(app);

// Binds the express app to an Azure Function handler
const httpStart = (context: Context): void => {
logger = context.log;
setAppContext(app, context);
azureFunctionHandler(context);
};

export default httpStart;
Loading

0 comments on commit 28d6f91

Please sign in to comment.