Skip to content

Commit

Permalink
Add mail common module and new unit test (#102)
Browse files Browse the repository at this point in the history
Co-authored-by: Danilo Spinelli <gunzip@users.noreply.github.com>
  • Loading branch information
infantesimone and gunzip authored Nov 5, 2020
1 parent d3feb72 commit 6da4ef9
Show file tree
Hide file tree
Showing 8 changed files with 121 additions and 258 deletions.
85 changes: 85 additions & 0 deletions SendUserDataDeleteEmailActivity/__tests__/handler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/* tslint:disable:no-any */
/* tslint:disable:no-duplicate-string */
/* tslint:disable:no-big-function */
/* tslint:disable: no-identical-functions */

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

import { ActivityInput, getActivityFunction } from "../handler";

import { fromLeft, taskEither } from "fp-ts/lib/TaskEither";

import * as HtmlToText from "html-to-text";

import { EmailAddress } from "io-functions-commons/dist/generated/definitions/EmailAddress";
import * as mail from "io-functions-commons/dist/src/mailer";

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

const mockContext = {
log: {
// tslint:disable-next-line: no-console
error: console.error,
// tslint:disable-next-line: no-console
info: console.log,
// tslint:disable-next-line: no-console
verbose: console.log,
// tslint:disable-next-line: no-console
warn: console.warn
}
} as any;

const HTML_TO_TEXT_OPTIONS: HtmlToText.HtmlToTextOptions = {
ignoreImage: true, // ignore all document images
tables: true
};

const MAIL_FROM = "IO - l’app dei servizi pubblici <no-reply@io.italia.it>" as NonEmptyString;
const defaultNotificationParams = {
HTML_TO_TEXT_OPTIONS,
MAIL_FROM
};

const input: ActivityInput = {
fiscalCode: "FRLFRC74E04B157I" as FiscalCode,
toAddress: "email@example.com" as EmailAddress
};

const lMailerTransporterMock = ({} as unknown) as mail.MailerTransporter;

describe("SendUserDataDeleteEmailActivity", () => {
it("should respond with 'SUCCESS' if the mail is sent", async () => {
jest.spyOn(mail, "sendMail").mockReturnValueOnce(taskEither.of("SUCCESS"));

const SendUserDataDeleteEmailActivityHandler = getActivityFunction(
lMailerTransporterMock,
defaultNotificationParams
);

const result = await SendUserDataDeleteEmailActivityHandler(
mockContext,
input
);

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

it("should respond with 'ERROR' if the mail is not sent", async () => {
const errorMessage: string = "Test Error";

jest
.spyOn(mail, "sendMail")
.mockReturnValueOnce(fromLeft(new Error(errorMessage)));

const SendUserDataDeleteEmailActivityHandler = getActivityFunction(
lMailerTransporterMock,
defaultNotificationParams
);

try {
await SendUserDataDeleteEmailActivityHandler(mockContext, input);
} catch (e) {
expect(e.message).toBe("Error while sending email: " + errorMessage);
}
});
});
29 changes: 13 additions & 16 deletions SendUserDataDeleteEmailActivity/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ import { Context } from "@azure/functions";
import { NewMessage } from "io-functions-commons/dist/generated/definitions/NewMessage";
import { readableReport } from "italia-ts-commons/lib/reporters";

import { Either } from "fp-ts/lib/Either";
import * as HtmlToText from "html-to-text";
import { sendMail } from "io-functions-commons/dist/src/utils/email";
import { markdownToHtml } from "io-functions-commons/dist/src/utils/markdown";
import * as t from "io-ts";
import { FiscalCode, NonEmptyString } from "italia-ts-commons/lib/strings";
import * as NodeMailer from "nodemailer";
import { EmailAddress } from "../generated/definitions/EmailAddress";

import { sendMail } from "io-functions-commons/dist/src/mailer";

// TODO: switch text based on user's preferred_language
const userDataDeleteMessage = NewMessage.decode({
content: {
Expand Down Expand Up @@ -93,25 +93,22 @@ export const getActivityFunction = (
notificationDefaultParams.HTML_TO_TEXT_OPTIONS
);

const sendResult: Either<
Error,
NodeMailer.SendMailOptions
> = await sendMail(lMailerTransporter, {
// trigger email delivery
await sendMail(lMailerTransporter, {
from: notificationDefaultParams.MAIL_FROM,
html: documentHtml,
subject: content.subject,
text: bodyText,
to: toAddress
});

if (sendResult.isLeft()) {
const error = sendResult.value;
// track the event of failed delivery
context.log.error(`${logPrefix}|ERROR=${error.message}`);
throw new Error(`Error while sending email: ${error.message}`);
}

context.log.verbose(`${logPrefix}|RESULT=SUCCESS`);
})
.bimap(
error => {
context.log.error(`${logPrefix}|ERROR=${error.message}`);
throw new Error(`Error while sending email: ${error.message}`);
},
() => context.log.verbose(`${logPrefix}|RESULT=SUCCESS`)
)
.run();

return success();
}
Expand Down
28 changes: 2 additions & 26 deletions SendUserDataDeleteEmailActivity/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import * as HtmlToText from "html-to-text";
import { MultiTransport } from "io-functions-commons/dist/src/utils/nodemailer";
import * as NodeMailer from "nodemailer";
import { getMailerTransporter } from "io-functions-commons/dist/src/mailer";
import { getConfigOrThrow } from "../utils/config";
import {
getMailerTransporter,
getTransportsForConnections
} from "../utils/email";
import { getActivityFunction } from "./handler";

const config = getConfigOrThrow();
Expand All @@ -18,26 +13,7 @@ const HTML_TO_TEXT_OPTIONS: HtmlToText.HtmlToTextOptions = {
tables: true
};

// if we have a valid multi transport configuration, configure a
// Multi transport, or else fall back to the default logic
const mailerTransporter =
typeof config.MAIL_TRANSPORTS !== "undefined"
? NodeMailer.createTransport(
MultiTransport({
transports: getTransportsForConnections(config.MAIL_TRANSPORTS)
})
)
: getMailerTransporter({
isProduction: config.isProduction,
...(typeof config.SENDGRID_API_KEY !== "undefined"
? {
sendgridApiKey: config.SENDGRID_API_KEY
}
: {
mailupSecret: config.MAILUP_SECRET,
mailupUsername: config.MAILUP_USERNAME
})
});
const mailerTransporter = getMailerTransporter(config);

const index = getActivityFunction(mailerTransporter, {
HTML_TO_TEXT_OPTIONS,
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
"durable-functions": "^1.4.3",
"express": "^4.15.3",
"fp-ts": "1.17.0",
"io-functions-commons": "^14.6.0",
"io-functions-commons": "^16.0.0",
"html-to-text": "^5.1.1",
"io-functions-express": "^0.1.0",
"io-ts": "1.8.5",
Expand Down
3 changes: 1 addition & 2 deletions utils/__tests__/config.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Either } from "fp-ts/lib/Either";
import { NonEmptyString } from "italia-ts-commons/lib/strings";
import { MailerConfig } from "../config";
import { MailerConfig } from "io-functions-commons/dist/src/mailer";

const aMailFrom = "example@test.com";

Expand Down
79 changes: 1 addition & 78 deletions utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,88 +5,11 @@
* The configuration is evaluate eagerly at the first access to the module. The module exposes convenient methods to access such value.
*/

import { MailMultiTransportConnectionsFromString } from "io-functions-commons/dist/src/utils/multi_transport_connection";
import { MailerConfig } from "io-functions-commons/dist/src/mailer";
import * as t from "io-ts";
import { readableReport } from "italia-ts-commons/lib/reporters";
import { NonEmptyString } from "italia-ts-commons/lib/strings";

// exclude a specific value from a type
// as strict equality is performed, allowed input types are constrained to be values not references (object, arrays, etc)
// tslint:disable-next-line max-union-size
const AnyBut = <A extends string | number | boolean | symbol, O = A>(
but: A,
base: t.Type<A, O> = t.any
) =>
t.brand(
base,
(
s
): s is t.Branded<
t.TypeOf<typeof base>,
{ readonly AnyBut: unique symbol }
> => s !== but,
"AnyBut"
);

// configuration to send email
export type MailerConfig = t.TypeOf<typeof MailerConfig>;
export const MailerConfig = t.intersection([
// common required fields
t.interface({
MAIL_FROM: NonEmptyString
}),
// the following union includes the possible configuration variants for different mail transports we use in prod
// undefined values are kept for easy usage
t.union([
// Using sendgrid
// we allow mailup values as well, as sendgrid would be selected first if present
// see here for the rationale: https://github.com/pagopa/io-functions-admin/pull/89#commitcomment-42917672
t.intersection([
t.interface({
MAILHOG_HOSTNAME: t.undefined,
MAIL_TRANSPORTS: t.undefined,
NODE_ENV: t.literal("production"),
SENDGRID_API_KEY: NonEmptyString
}),
t.partial({
MAILUP_SECRET: NonEmptyString,
MAILUP_USERNAME: NonEmptyString
})
]),
// using mailup
t.interface({
MAILHOG_HOSTNAME: t.undefined,
MAILUP_SECRET: NonEmptyString,
MAILUP_USERNAME: NonEmptyString,
MAIL_TRANSPORTS: t.undefined,
NODE_ENV: t.literal("production"),
SENDGRID_API_KEY: t.undefined
}),
// Using multi-transport definition
// Optional multi provider connection string
// The connection string must be in the format:
// [mailup:username:password;][sendgrid:apikey:;]
// Note that multiple instances of the same provider can be provided.
t.interface({
MAILHOG_HOSTNAME: t.undefined,
MAILUP_SECRET: t.undefined,
MAILUP_USERNAME: t.undefined,
MAIL_TRANSPORTS: MailMultiTransportConnectionsFromString,
NODE_ENV: t.literal("production"),
SENDGRID_API_KEY: t.undefined
}),
// the following states that a mailhog configuration is optional and can be provided only if not in prod
t.interface({
MAILHOG_HOSTNAME: NonEmptyString,
MAILUP_SECRET: t.undefined,
MAILUP_USERNAME: t.undefined,
MAIL_TRANSPORTS: t.undefined,
NODE_ENV: AnyBut("production", t.string),
SENDGRID_API_KEY: t.undefined
})
])
]);

// global app configuration
export type IConfig = t.TypeOf<typeof IConfig>;
export const IConfig = t.intersection([
Expand Down
Loading

0 comments on commit 6da4ef9

Please sign in to comment.