-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[#IP-145] New table for users with failed user_data_processing (#138)
- Loading branch information
1 parent
63007f6
commit 28d6f91
Showing
19 changed files
with
1,208 additions
and
210 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}); | ||
} | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.