Skip to content

Commit

Permalink
[#172621882] send a message to the user that requested her own data (#50
Browse files Browse the repository at this point in the history
)
  • Loading branch information
gunzip authored May 17, 2020
1 parent 548a9ed commit 2fa9b2c
Show file tree
Hide file tree
Showing 11 changed files with 348 additions and 98 deletions.
10 changes: 10 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Current TS File",
"type": "node",
"request": "launch",
"args": ["${relativeFile}"],
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"sourceMaps": true,
"cwd": "${workspaceRoot}",
"protocol": "inspector",
},
{
"type": "node",
"request": "launch",
Expand Down
12 changes: 3 additions & 9 deletions ExtractUserDataActivity/__tests__/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,15 +125,9 @@ describe("createExtractUserDataActivityHandler", () => {

const result = await handler(contextMock, input);

result.fold(
response => fail(`Failing result, response: ${JSON.stringify(response)}`),
response => {
ActivityResultSuccess.decode(response).fold(
err =>
fail(`Failing decoding result, response: ${readableReport(err)}`),
e => expect(e.kind).toBe("SUCCESS")
);
}
ActivityResultSuccess.decode(result).fold(
err => fail(`Failing decoding result, response: ${readableReport(err)}`),
e => expect(e.kind).toBe("SUCCESS")
);
});

Expand Down
8 changes: 3 additions & 5 deletions ExtractUserDataActivity/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -531,10 +531,7 @@ export function createExtractUserDataActivityHandler(
profileModel: ProfileModel,
blobService: BlobService,
userDataContainerName: NonEmptyString
): (
context: Context,
input: unknown
) => Promise<Either<ActivityResultFailure, ActivityResultSuccess>> {
): (context: Context, input: unknown) => Promise<ActivityResult> {
return (context: Context, input: unknown) =>
fromEither(
ActivityInput.decode(input).mapLeft<ActivityResultFailure>(
Expand Down Expand Up @@ -583,5 +580,6 @@ export function createExtractUserDataActivityHandler(
value: archiveInfo
})
)
.run();
.run()
.then(e => e.value);
}
10 changes: 10 additions & 0 deletions SendUserDataDownloadMessageActivity/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/SendUserDataDownloadMessageActivity/index.js"
}
148 changes: 148 additions & 0 deletions SendUserDataDownloadMessageActivity/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { Context } from "@azure/functions";
import { NewMessage } from "io-functions-commons/dist/generated/definitions/NewMessage";
import { readableReport } from "italia-ts-commons/lib/reporters";

import * as t from "io-ts";
import { FiscalCode, NonEmptyString } from "italia-ts-commons/lib/strings";

// TODO: switch text based on user's preferred_language
const userDataDownloadMessage = (
blobName: string,
password: string,
publicDownloadBaseUrl: string
) =>
NewMessage.decode({
content: {
markdown: `Caro/a Utente,
Abbiamo completato la gestione della tua richiesta di accesso.
Puoi scaricare al link che segue i tuoi dati personali che trattiamo tramite l’App IO utilizzando la relativa password.
Se hai necessità di maggiori dettagli o informazioni su questi dati o vuoi riceverne dettaglio,
ti invitiamo a scrivere all’indirizzo email dpo@pagopa.it.
Nel caso in cui tu non sia soddisfatto/a dalla modalità con cui abbiamo gestito la tua richiesta,
siamo a disposizione per risolvere domande o dubbi aggiuntivi, che puoi indicare scrivendo all’indirizzo email indicato sopra.
[Link all'archivio ZIP](${publicDownloadBaseUrl}/${blobName})
Password dell'archivio ZIP:
${password}
Grazie ancora per aver utilizzato IO,
il Team Privacy di PagoPA
`,
subject: `IO App - richiesta di accesso ai dati`
}
}).getOrElseL(errs => {
throw new Error("Invalid MessageContent: " + readableReport(errs));
});

/**
* Send a single user data download message
* using the IO Notification API (REST).
*/
async function sendMessage(
fiscalCode: FiscalCode,
apiUrl: string,
apiKey: string,
newMessage: NewMessage,
timeoutFetch: typeof fetch
): Promise<number> {
const response = await timeoutFetch(
`${apiUrl}/api/v1/messages/${fiscalCode}`,
{
body: JSON.stringify(newMessage),
headers: {
"Content-Type": "application/json",
"Ocp-Apim-Subscription-Key": apiKey
},
method: "POST"
}
);
return response.status;
}

// Activity result
const ActivityResultSuccess = t.interface({
kind: t.literal("SUCCESS")
});

type ActivityResultSuccess = t.TypeOf<typeof ActivityResultSuccess>;

const ActivityResultFailure = t.interface({
kind: t.literal("FAILURE"),
reason: t.string
});

type ActivityResultFailure = t.TypeOf<typeof ActivityResultFailure>;

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

export const ActivityInput = t.interface({
blobName: t.string,
fiscalCode: FiscalCode,
password: t.string
});
export type ActivityInput = t.TypeOf<typeof ActivityInput>;

export const getActivityFunction = (
publicApiUrl: NonEmptyString,
publicApiKey: NonEmptyString,
publicDownloadBaseUrl: NonEmptyString,
timeoutFetch: typeof fetch
) => (context: Context, input: unknown): Promise<ActivityResult> => {
const failure = (reason: string) => {
context.log.error(reason);
return ActivityResultFailure.encode({
kind: "FAILURE",
reason
});
};

const success = () =>
ActivityResultSuccess.encode({
kind: "SUCCESS"
});

return ActivityInput.decode(input).fold<Promise<ActivityResult>>(
async errs =>
failure(
`SendUserDataDownloadMessageActivity|Cannot decode input|ERROR=${readableReport(
errs
)}|INPUT=${JSON.stringify(input)}`
),
async ({ blobName, fiscalCode, password }) => {
const logPrefix = `SendUserDataDownloadMessageActivity|PROFILE=${fiscalCode}`;
context.log.verbose(`${logPrefix}|Sending user data download message`);

// throws in case of timeout so
// the orchestrator can schedule a retry
const status = await sendMessage(
fiscalCode,
publicApiUrl,
publicApiKey,
userDataDownloadMessage(blobName, password, publicDownloadBaseUrl),
timeoutFetch
);

if (status !== 201) {
const msg = `${logPrefix}|ERROR=${status}`;
if (status >= 500) {
throw new Error(msg);
} else {
return failure(msg);
}
}

context.log.verbose(`${logPrefix}|RESPONSE=${status}`);
return success();
}
);
};

export default getActivityFunction;
36 changes: 36 additions & 0 deletions SendUserDataDownloadMessageActivity/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { getRequiredStringEnv } from "io-functions-commons/dist/src/utils/env";
import { agent } from "italia-ts-commons";
import {
AbortableFetch,
setFetchTimeout,
toFetch
} from "italia-ts-commons/lib/fetch";
import { Millisecond } from "italia-ts-commons/lib/units";
import { getActivityFunction } from "./handler";

// HTTP external requests timeout in milliseconds
const DEFAULT_REQUEST_TIMEOUT_MS = 10000;

// Needed to call notifications API
const publicApiUrl = getRequiredStringEnv("PUBLIC_API_URL");
const publicApiKey = getRequiredStringEnv("PUBLIC_API_KEY");
const publicDownloadBaseUrl = getRequiredStringEnv("PUBLIC_DOWNLOAD_BASE_URL");

// HTTP-only fetch with optional keepalive agent
// @see https://github.com/pagopa/io-ts-commons/blob/master/src/agent.ts#L10
const httpApiFetch = agent.getHttpFetch(process.env);

// a fetch that can be aborted and that gets cancelled after fetchTimeoutMs
const abortableFetch = AbortableFetch(httpApiFetch);
const timeoutFetch = toFetch(
setFetchTimeout(DEFAULT_REQUEST_TIMEOUT_MS as Millisecond, abortableFetch)
);

const index = getActivityFunction(
publicApiUrl,
publicApiKey,
publicDownloadBaseUrl,
timeoutFetch
);

export default index;
58 changes: 19 additions & 39 deletions SetUserDataProcessingStatusActivity/__tests__/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,10 @@ describe("SetUserDataProcessingStatusActivityHandler", () => {
};
const result = await handler(contextMock, input);

result.fold(
response => fail(`Failing result, reason: ${response.reason}`),
response => {
expect(response.value.status).toEqual(UserDataProcessingStatusEnum.WIP);
}
);
expect(result.kind).toEqual("SUCCESS");
if (result.kind === "SUCCESS") {
expect(result.value.status === UserDataProcessingStatusEnum.WIP);
}
});

it("should handle a query error", async () => {
Expand All @@ -63,17 +61,11 @@ describe("SetUserDataProcessingStatusActivityHandler", () => {
};
const result = await handler(contextMock, input);

result.fold(
response => {
ActivityResultFailure.decode(response).fold(
err =>
fail(`Failing decoding result, response: ${JSON.stringify(err)}`),
failure => {
expect(failure.kind).toEqual(expect.any(String));
}
);
},
_ => fail(`Should not consider this a Right`)
ActivityResultFailure.decode(result).fold(
err => fail(`Failing decoding result, response: ${JSON.stringify(err)}`),
failure => {
expect(failure.kind).toEqual(expect.any(String));
}
);
});

Expand All @@ -94,17 +86,11 @@ describe("SetUserDataProcessingStatusActivityHandler", () => {
};
const result = await handler(contextMock, input);

result.fold(
response => {
ActivityResultFailure.decode(response).fold(
err =>
fail(`Failing decoding result, response: ${JSON.stringify(err)}`),
failure => {
expect(failure.kind).toEqual(expect.any(String));
}
);
},
_ => fail(`Should not consider this a Right`)
ActivityResultFailure.decode(result).fold(
err => fail(`Failing decoding result, response: ${JSON.stringify(err)}`),
failure => {
expect(failure.kind).toEqual(expect.any(String));
}
);
});

Expand All @@ -118,17 +104,11 @@ describe("SetUserDataProcessingStatusActivityHandler", () => {
invalid: "input"
});

result.fold(
response => {
ActivityResultFailure.decode(response).fold(
err =>
fail(`Failing decoding result, response: ${JSON.stringify(err)}`),
failure => {
expect(failure.kind).toEqual(expect.any(String));
}
);
},
_ => fail(`Should not consider this a Right`)
ActivityResultFailure.decode(result).fold(
err => fail(`Failing decoding result, response: ${JSON.stringify(err)}`),
failure => {
expect(failure.kind).toEqual(expect.any(String));
}
);
});
});
3 changes: 2 additions & 1 deletion SetUserDataProcessingStatusActivity/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,5 +170,6 @@ export const createSetUserDataProcessingStatusActivityHandler = (
logFailure(context)(failure);
return failure;
})
.run();
.run()
.then(e => e.value);
};
Loading

0 comments on commit 2fa9b2c

Please sign in to comment.