Skip to content

Commit

Permalink
[#IP-154] UserDataProcessingDeleteOrchestrator can manage previous fa…
Browse files Browse the repository at this point in the history
…iled requests if they are resumed (#156)
  • Loading branch information
michaeldisaro authored Jul 26, 2021
1 parent bf99991 commit 3636839
Show file tree
Hide file tree
Showing 8 changed files with 1,200 additions and 57 deletions.
2 changes: 1 addition & 1 deletion GetUserDataProcessingActivity/__tests__/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import { toCosmosErrorResponse } from "@pagopa/io-functions-commons/dist/src/uti

const aChoice = aUserDataProcessing.choice;

describe("SetUserDataProcessingStatusActivityHandler", () => {
describe("GetUserDataProcessingActivityHandler", () => {
it("should handle a result", async () => {
const mockModel = ({
findLastVersionByModelId: jest.fn(() =>
Expand Down
199 changes: 199 additions & 0 deletions IsFailedUserDataProcessingActivity/__tests__/handler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import { FiscalCode, NonEmptyString } from "@pagopa/ts-commons/lib/strings";
import { TableService } from "azure-storage";
import {
UserDataProcessingChoice,
UserDataProcessingChoiceEnum
} from "@pagopa/io-functions-commons/dist/generated/definitions/UserDataProcessingChoice";
import {
ActivityResultFailure,
ActivityResultSuccess,
IsFailedUserDataProcessing
} 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("IsFailedUserDataProcessingHandler", () => {
it("should fail if input is not valid", async () => {
const tableServiceMock = ({
retrieveEntity: retrieveEntityFailedUserDataProcessingMock(
noFailedRequests
)
} as any) as TableService;

const getFailedUserDataProcessingHandler = IsFailedUserDataProcessing(
tableServiceMock,
storageTableMock
);

const result = await getFailedUserDataProcessingHandler({} as any, {
a: "a",
b: "b",
c: "c"
});

expect(ActivityResultFailure.is(result)).toBe(true);
const decodedResult = ActivityResultFailure.decode(result);
expect(decodedResult.isRight()).toBe(true);
if (decodedResult.isRight()) {
expect(JSON.stringify(decodedResult.value)).toBe(
JSON.stringify({
kind: "FAILURE",
reason: "Invalid input"
})
);
}
});

it("should fail if any error occurs", async () => {
const tableServiceMock = ({
retrieveEntity: internalErrorRetrieveEntityFailedUserDataProcessingMock(
failedRequests
)
} as any) as TableService;

const getFailedUserDataProcessingHandler = IsFailedUserDataProcessing(
tableServiceMock,
storageTableMock
);

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

expect(ActivityResultFailure.is(result)).toBe(true);
const decodedResult = ActivityResultFailure.decode(result);
expect(decodedResult.isRight()).toBe(true);
if (decodedResult.isRight()) {
expect(JSON.stringify(decodedResult.value)).toBe(
JSON.stringify({
kind: "FAILURE",
reason: "ERROR|tableService.retrieveEntity|Cannot retrieve entity"
})
);
}
});

it("should succeed with false value if no failed user data processing is present", async () => {
const tableServiceMock = ({
retrieveEntity: retrieveEntityFailedUserDataProcessingMock(
noFailedRequests
)
} as any) as TableService;

const getFailedUserDataProcessingHandler = IsFailedUserDataProcessing(
tableServiceMock,
storageTableMock
);

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

expect(ActivityResultSuccess.is(result)).toBe(true);
const decodedResult = ActivityResultSuccess.decode(result);
expect(decodedResult.isRight()).toBe(true);
if (decodedResult.isRight()) {
expect(JSON.stringify(decodedResult.value)).toBe(
JSON.stringify({ kind: "SUCCESS", value: false })
);
}
});

it("should succeed with true value if failed user data processing is present", async () => {
const tableServiceMock = ({
retrieveEntity: retrieveEntityFailedUserDataProcessingMock(failedRequests)
} as any) as TableService;

const getFailedUserDataProcessingHandler = IsFailedUserDataProcessing(
tableServiceMock,
storageTableMock
);

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

expect(ActivityResultSuccess.is(result)).toBe(true);
const decodedResult = ActivityResultSuccess.decode(result);
expect(decodedResult.isRight()).toBe(true);
if (decodedResult.isRight()) {
expect(JSON.stringify(decodedResult.value)).toBe(
JSON.stringify({ kind: "SUCCESS", value: true })
);
}
});
});
10 changes: 10 additions & 0 deletions IsFailedUserDataProcessingActivity/function.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"bindings": [
{
"name": "name",
"type": "activityTrigger",
"direction": "in"
}
],
"scriptFile": "../dist/IsFailedUserDataProcessingActivity/index.js"
}
96 changes: 96 additions & 0 deletions IsFailedUserDataProcessingActivity/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Context } from "@azure/functions";
import { ServiceResponse, TableService } from "azure-storage";
import { fromOption } from "fp-ts/lib/Either";
import { NonEmptyString, FiscalCode } from "@pagopa/ts-commons/lib/strings";
import { UserDataProcessingChoice } from "@pagopa/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 * as t from "io-ts";
import { identity } from "fp-ts/lib/function";

// Activity input
export const ActivityInput = t.interface({
choice: UserDataProcessingChoice,
fiscalCode: FiscalCode
});
export type ActivityInput = t.TypeOf<typeof ActivityInput>;

// Activity result
export type ActivityResultSuccess = t.TypeOf<typeof ActivityResultSuccess>;
export const ActivityResultSuccess = t.interface({
kind: t.literal("SUCCESS"),
value: t.boolean
});

export type ActivityResultFailure = t.TypeOf<typeof ActivityResultFailure>;
export const ActivityResultFailure = t.interface({
kind: t.literal("FAILURE"),
reason: t.string
});

export const ActivityResult = t.taggedUnion("kind", [
ActivityResultSuccess,
ActivityResultFailure
]);
export type ActivityResult = t.TypeOf<typeof ActivityResult>;

// Table storage result
type TableEntry = Readonly<{
readonly RowKey: Readonly<{
readonly _: FiscalCode;
}>;
}>;

export const IsFailedUserDataProcessing = (
tableService: TableService,
failedUserDataProcessingTable: NonEmptyString
) => (context: Context, input: unknown): Promise<ActivityResult> =>
fromEither(ActivityInput.decode(input))
.mapLeft<ActivityResult>(_ =>
ActivityResultFailure.encode({
kind: "FAILURE",
reason: "Invalid input"
})
)
.chain(i =>
tryCatch(
() =>
new Promise<Option<TableEntry>>((resolve, reject) =>
tableService.retrieveEntity(
failedUserDataProcessingTable,
i.choice,
i.fiscalCode,
null,
(error: Error, result: TableEntry, response: ServiceResponse) =>
response.isSuccessful
? resolve(some(result))
: response.statusCode === 404
? resolve(none)
: reject(error)
)
),
_ =>
ActivityResultFailure.encode({
kind: "FAILURE",
reason: "ERROR|tableService.retrieveEntity|Cannot retrieve entity"
})
)
)
.chain(maybeTableEntry =>
fromEither(
fromOption(
ActivityResultSuccess.encode({
kind: "SUCCESS",
value: false
})
)(maybeTableEntry)
)
)
.map(_ =>
ActivityResultSuccess.encode({
kind: "SUCCESS",
value: true
})
)
.fold<ActivityResult>(identity, identity)
.run();
19 changes: 19 additions & 0 deletions IsFailedUserDataProcessingActivity/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { createTableService } from "azure-storage";
import { getConfigOrThrow } from "../utils/config";
import { IsFailedUserDataProcessing } from "./handler";

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

const activityFunctionHandler = IsFailedUserDataProcessing(
tableService,
failedUserDataProcessingTable
);

export default activityFunctionHandler;
Loading

0 comments on commit 3636839

Please sign in to comment.