diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0928dc992..7c3966f5b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -520,7 +520,7 @@ jobs: report_name: Coverage Backend type: lcov result_path: ./backend/coverage/lcov.info - min_coverage: 40 + min_coverage: 41 token: ${{ github.token }} ############################################################################## diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 88c1424b3..871e1b9d6 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -6,7 +6,7 @@ import { Resolver, Query, Args, Authorized, Ctx, Mutation } from 'type-graphql' import { getCustomRepository, getConnection, QueryRunner } from 'typeorm' import CONFIG from '../../config' -import { sendEMail } from '../../util/sendEMail' +import { sendTransactionReceivedEmail } from '../../mailer/sendTransactionReceivedEmail' import { Transaction } from '../model/Transaction' import { TransactionList } from '../model/TransactionList' @@ -651,21 +651,14 @@ export class TransactionResolver { } // send notification email // TODO: translate - await sendEMail({ - from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`, - to: `${recipiantUser.firstName} ${recipiantUser.lastName} <${recipiantUser.email}>`, - subject: 'Gradido Überweisung', - text: `Hallo ${recipiantUser.firstName} ${recipiantUser.lastName} - - Du hast soeben ${amount} GDD von ${senderUser.firstName} ${senderUser.lastName} erhalten. - ${senderUser.firstName} ${senderUser.lastName} schreibt: - - ${memo} - - Bitte antworte nicht auf diese E-Mail! - - Mit freundlichen Grüßen, - dein Gradido-Team`, + await sendTransactionReceivedEmail({ + senderFirstName: senderUser.firstName, + senderLastName: senderUser.lastName, + recipientFirstName: recipiantUser.firstName, + recipientLastName: recipiantUser.lastName, + email: recipiantUser.email, + amount, + memo, }) return 'success' diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index d0c144e22..1f0bce30f 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -12,12 +12,12 @@ import { LoginUserBackup } from '@entity/LoginUserBackup' import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' import { User } from '@entity/User' import CONFIG from '../../config' -import { sendEMail } from '../../util/sendEMail' +import { sendAccountActivationEmail } from '../../mailer/sendAccountActivationEmail' -jest.mock('../../util/sendEMail', () => { +jest.mock('../../mailer/sendAccountActivationEmail', () => { return { __esModule: true, - sendEMail: jest.fn(), + sendAccountActivationEmail: jest.fn(), } }) @@ -62,7 +62,6 @@ describe('UserResolver', () => { let result: any let emailOptIn: string - let newUser: User beforeAll(async () => { result = await mutate({ mutation, variables }) @@ -90,7 +89,6 @@ describe('UserResolver', () => { loginEmailOptIn = await getRepository(LoginEmailOptIn) .createQueryBuilder('login_email_optin') .getMany() - newUser = user[0] emailOptIn = loginEmailOptIn[0].verificationCode.toString() }) @@ -165,13 +163,11 @@ describe('UserResolver', () => { describe('account activation email', () => { it('sends an account activation email', () => { const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(/\$1/g, emailOptIn) - expect(sendEMail).toBeCalledWith({ - from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`, - to: `${newUser.firstName} ${newUser.lastName} <${newUser.email}>`, - subject: 'Gradido: E-Mail Überprüfung', - text: - expect.stringContaining(`Hallo ${newUser.firstName} ${newUser.lastName},`) && - expect.stringContaining(activationLink), + expect(sendAccountActivationEmail).toBeCalledWith({ + link: activationLink, + firstName: 'Peter', + lastName: 'Lustig', + email: 'peter@lustig.de', }) }) }) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 769bb4426..b751eb633 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -20,7 +20,8 @@ import { UserRepository } from '../../typeorm/repository/User' import { LoginUser } from '@entity/LoginUser' import { LoginUserBackup } from '@entity/LoginUserBackup' import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' -import { sendEMail } from '../../util/sendEMail' +import { sendResetPasswordEmail } from '../../mailer/sendResetPasswordEmail' +import { sendAccountActivationEmail } from '../../mailer/sendAccountActivationEmail' import { LoginElopageBuysRepository } from '../../typeorm/repository/LoginElopageBuys' import { signIn } from '../../apis/KlicktippController' import { RIGHTS } from '../../auth/RIGHTS' @@ -450,12 +451,12 @@ export class UserResolver { /\$1/g, emailOptIn.verificationCode.toString(), ) - const emailSent = await this.sendAccountActivationEmail( - activationLink, + const emailSent = await sendAccountActivationEmail({ + link: activationLink, firstName, lastName, email, - ) + }) // In case EMails are disabled log the activation link for the user if (!emailSent) { @@ -472,29 +473,6 @@ export class UserResolver { return 'success' } - private sendAccountActivationEmail( - activationLink: string, - firstName: string, - lastName: string, - email: string, - ): Promise { - return sendEMail({ - from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`, - to: `${firstName} ${lastName} <${email}>`, - subject: 'Gradido: E-Mail Überprüfung', - text: `Hallo ${firstName} ${lastName}, - - Deine EMail wurde soeben bei Gradido registriert. - - Klicke bitte auf diesen Link, um die Registrierung abzuschließen und dein Gradido-Konto zu aktivieren: - ${activationLink} - oder kopiere den obigen Link in dein Browserfenster. - - Mit freundlichen Grüßen, - dein Gradido-Team`, - }) - } - @Mutation(() => Boolean) async sendActivationEmail(@Arg('email') email: string): Promise { const loginUserRepository = getCustomRepository(LoginUserRepository) @@ -512,12 +490,12 @@ export class UserResolver { emailOptIn.verificationCode.toString(), ) - const emailSent = await this.sendAccountActivationEmail( - activationLink, - loginUser.firstName, - loginUser.lastName, + const emailSent = await sendAccountActivationEmail({ + link: activationLink, + firstName: loginUser.firstName, + lastName: loginUser.lastName, email, - ) + }) // In case EMails are disabled log the activation link for the user if (!emailSent) { @@ -549,18 +527,11 @@ export class UserResolver { optInCode.verificationCode.toString(), ) - const emailSent = await sendEMail({ - from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`, - to: `${loginUser.firstName} ${loginUser.lastName} <${email}>`, - subject: 'Gradido: Reset Password', - text: `Hallo ${loginUser.firstName} ${loginUser.lastName}, - - Du oder jemand anderes hat für dieses Konto ein Zurücksetzen des Passworts angefordert. - Wenn du es warst, klicke bitte auf den Link: ${link} - oder kopiere den obigen Link in Dein Browserfenster. - - Mit freundlichen Grüßen, - dein Gradido-Team`, + const emailSent = await sendResetPasswordEmail({ + link, + firstName: loginUser.firstName, + lastName: loginUser.lastName, + email, }) // In case EMails are disabled log the activation link for the user diff --git a/backend/src/mailer/sendAccountActivationEmail.test.ts b/backend/src/mailer/sendAccountActivationEmail.test.ts new file mode 100644 index 000000000..c53fc0994 --- /dev/null +++ b/backend/src/mailer/sendAccountActivationEmail.test.ts @@ -0,0 +1,29 @@ +import { sendAccountActivationEmail } from './sendAccountActivationEmail' +import { sendEMail } from './sendEMail' + +jest.mock('./sendEMail', () => { + return { + __esModule: true, + sendEMail: jest.fn(), + } +}) + +describe('sendAccountActivationEmail', () => { + beforeEach(async () => { + await sendAccountActivationEmail({ + link: 'activationLink', + firstName: 'Peter', + lastName: 'Lustig', + email: 'peter@lustig.de', + }) + }) + + it('calls sendEMail', () => { + expect(sendEMail).toBeCalledWith({ + to: `Peter Lustig `, + subject: 'Gradido: E-Mail Überprüfung', + text: + expect.stringContaining('Hallo Peter Lustig') && expect.stringContaining('activationLink'), + }) + }) +}) diff --git a/backend/src/mailer/sendAccountActivationEmail.ts b/backend/src/mailer/sendAccountActivationEmail.ts new file mode 100644 index 000000000..05c3104cb --- /dev/null +++ b/backend/src/mailer/sendAccountActivationEmail.ts @@ -0,0 +1,15 @@ +import { sendEMail } from './sendEMail' +import { accountActivation } from './text/accountActivation' + +export const sendAccountActivationEmail = (data: { + link: string + firstName: string + lastName: string + email: string +}): Promise => { + return sendEMail({ + to: `${data.firstName} ${data.lastName} <${data.email}>`, + subject: accountActivation.de.subject, + text: accountActivation.de.text(data), + }) +} diff --git a/backend/src/mailer/sendEMail.test.ts b/backend/src/mailer/sendEMail.test.ts new file mode 100644 index 000000000..5baae00ab --- /dev/null +++ b/backend/src/mailer/sendEMail.test.ts @@ -0,0 +1,92 @@ +import { sendEMail } from './sendEMail' +import { createTransport } from 'nodemailer' +import CONFIG from '../config' + +CONFIG.EMAIL = false +CONFIG.EMAIL_SMTP_URL = 'EMAIL_SMTP_URL' +CONFIG.EMAIL_SMTP_PORT = '1234' +CONFIG.EMAIL_USERNAME = 'user' +CONFIG.EMAIL_PASSWORD = 'pwd' + +jest.mock('nodemailer', () => { + return { + __esModule: true, + createTransport: jest.fn(() => { + return { + sendMail: jest.fn(() => { + return { + messageId: 'message', + } + }), + } + }), + } +}) + +describe('sendEMail', () => { + let result: boolean + describe('config email is false', () => { + // eslint-disable-next-line no-console + const consoleLog = console.log + const consoleLogMock = jest.fn() + // eslint-disable-next-line no-console + console.log = consoleLogMock + beforeEach(async () => { + result = await sendEMail({ + to: 'receiver@mail.org', + subject: 'Subject', + text: 'Text text text', + }) + }) + + afterAll(() => { + // eslint-disable-next-line no-console + console.log = consoleLog + }) + + it('logs warining to console', () => { + expect(consoleLogMock).toBeCalledWith('Emails are disabled via config') + }) + + it('returns false', () => { + expect(result).toBeFalsy() + }) + }) + + describe('config email is true', () => { + beforeEach(async () => { + CONFIG.EMAIL = true + result = await sendEMail({ + to: 'receiver@mail.org', + subject: 'Subject', + text: 'Text text text', + }) + }) + + it('calls the transporter', () => { + expect(createTransport).toBeCalledWith({ + host: 'EMAIL_SMTP_URL', + port: 1234, + secure: false, + requireTLS: true, + auth: { + user: 'user', + pass: 'pwd', + }, + }) + }) + + it('calls sendMail of transporter', () => { + expect((createTransport as jest.Mock).mock.results[0].value.sendMail).toBeCalledWith({ + from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`, + to: 'receiver@mail.org', + subject: 'Subject', + text: 'Text text text', + }) + }) + + it('returns true', () => { + expect(result).toBeTruthy() + }) + }) +}) diff --git a/backend/src/util/sendEMail.ts b/backend/src/mailer/sendEMail.ts similarity index 85% rename from backend/src/util/sendEMail.ts rename to backend/src/mailer/sendEMail.ts index 4c239980d..e26589347 100644 --- a/backend/src/util/sendEMail.ts +++ b/backend/src/mailer/sendEMail.ts @@ -3,7 +3,6 @@ import { createTransport } from 'nodemailer' import CONFIG from '../config' export const sendEMail = async (emailDef: { - from: string to: string subject: string text: string @@ -23,7 +22,10 @@ export const sendEMail = async (emailDef: { pass: CONFIG.EMAIL_PASSWORD, }, }) - const info = await transporter.sendMail(emailDef) + const info = await transporter.sendMail({ + ...emailDef, + from: `Gradido (nicht antworten) <${CONFIG.EMAIL_SENDER}>`, + }) if (!info.messageId) { throw new Error('error sending notification email, but transaction succeed') } diff --git a/backend/src/mailer/sendResetPasswordEmail.test.ts b/backend/src/mailer/sendResetPasswordEmail.test.ts new file mode 100644 index 000000000..4bc8ceba0 --- /dev/null +++ b/backend/src/mailer/sendResetPasswordEmail.test.ts @@ -0,0 +1,28 @@ +import { sendResetPasswordEmail } from './sendResetPasswordEmail' +import { sendEMail } from './sendEMail' + +jest.mock('./sendEMail', () => { + return { + __esModule: true, + sendEMail: jest.fn(), + } +}) + +describe('sendResetPasswordEmail', () => { + beforeEach(async () => { + await sendResetPasswordEmail({ + link: 'resetLink', + firstName: 'Peter', + lastName: 'Lustig', + email: 'peter@lustig.de', + }) + }) + + it('calls sendEMail', () => { + expect(sendEMail).toBeCalledWith({ + to: `Peter Lustig `, + subject: 'Gradido: Passwort zurücksetzen', + text: expect.stringContaining('Hallo Peter Lustig') && expect.stringContaining('resetLink'), + }) + }) +}) diff --git a/backend/src/mailer/sendResetPasswordEmail.ts b/backend/src/mailer/sendResetPasswordEmail.ts new file mode 100644 index 000000000..c9f5b23e9 --- /dev/null +++ b/backend/src/mailer/sendResetPasswordEmail.ts @@ -0,0 +1,15 @@ +import { sendEMail } from './sendEMail' +import { resetPassword } from './text/resetPassword' + +export const sendResetPasswordEmail = (data: { + link: string + firstName: string + lastName: string + email: string +}): Promise => { + return sendEMail({ + to: `${data.firstName} ${data.lastName} <${data.email}>`, + subject: resetPassword.de.subject, + text: resetPassword.de.text(data), + }) +} diff --git a/backend/src/mailer/sendTransactionReceivedEmail.test.ts b/backend/src/mailer/sendTransactionReceivedEmail.test.ts new file mode 100644 index 000000000..29f227185 --- /dev/null +++ b/backend/src/mailer/sendTransactionReceivedEmail.test.ts @@ -0,0 +1,35 @@ +import { sendTransactionReceivedEmail } from './sendTransactionReceivedEmail' +import { sendEMail } from './sendEMail' + +jest.mock('./sendEMail', () => { + return { + __esModule: true, + sendEMail: jest.fn(), + } +}) + +describe('sendTransactionReceivedEmail', () => { + beforeEach(async () => { + await sendTransactionReceivedEmail({ + senderFirstName: 'Bibi', + senderLastName: 'Bloxberg', + recipientFirstName: 'Peter', + recipientLastName: 'Lustig', + email: 'peter@lustig.de', + amount: 42.0, + memo: 'Vielen herzlichen Dank für den neuen Hexenbesen!', + }) + }) + + it('calls sendEMail', () => { + expect(sendEMail).toBeCalledWith({ + to: `Peter Lustig `, + subject: 'Gradido Überweisung', + text: + expect.stringContaining('Hallo Peter Lustig') && + expect.stringContaining('42,00 GDD') && + expect.stringContaining('Bibi Bloxberg') && + expect.stringContaining('Vielen herzlichen Dank für den neuen Hexenbesen!'), + }) + }) +}) diff --git a/backend/src/mailer/sendTransactionReceivedEmail.ts b/backend/src/mailer/sendTransactionReceivedEmail.ts new file mode 100644 index 000000000..3560f6548 --- /dev/null +++ b/backend/src/mailer/sendTransactionReceivedEmail.ts @@ -0,0 +1,18 @@ +import { sendEMail } from './sendEMail' +import { transactionReceived } from './text/transactionReceived' + +export const sendTransactionReceivedEmail = (data: { + senderFirstName: string + senderLastName: string + recipientFirstName: string + recipientLastName: string + email: string + amount: number + memo: string +}): Promise => { + return sendEMail({ + to: `${data.recipientFirstName} ${data.recipientLastName} <${data.email}>`, + subject: transactionReceived.de.subject, + text: transactionReceived.de.text(data), + }) +} diff --git a/backend/src/mailer/text/accountActivation.ts b/backend/src/mailer/text/accountActivation.ts new file mode 100644 index 000000000..c4f70ff0f --- /dev/null +++ b/backend/src/mailer/text/accountActivation.ts @@ -0,0 +1,16 @@ +export const accountActivation = { + de: { + subject: 'Gradido: E-Mail Überprüfung', + text: (data: { link: string; firstName: string; lastName: string; email: string }): string => + `Hallo ${data.firstName} ${data.lastName}, + +Deine EMail wurde soeben bei Gradido registriert. + +Klicke bitte auf diesen Link, um die Registrierung abzuschließen und dein Gradido-Konto zu aktivieren: +${data.link} +oder kopiere den obigen Link in dein Browserfenster. + +Mit freundlichen Grüßen, +dein Gradido-Team`, + }, +} diff --git a/backend/src/mailer/text/resetPassword.ts b/backend/src/mailer/text/resetPassword.ts new file mode 100644 index 000000000..58b13cbcd --- /dev/null +++ b/backend/src/mailer/text/resetPassword.ts @@ -0,0 +1,14 @@ +export const resetPassword = { + de: { + subject: 'Gradido: Passwort zurücksetzen', + text: (data: { link: string; firstName: string; lastName: string; email: string }): string => + `Hallo ${data.firstName} ${data.lastName}, + +Du oder jemand anderes hat für dieses Konto ein Zurücksetzen des Passworts angefordert. +Wenn du es warst, klicke bitte auf den Link: ${data.link} +oder kopiere den obigen Link in Dein Browserfenster. + +Mit freundlichen Grüßen, +dein Gradido-Team`, + }, +} diff --git a/backend/src/mailer/text/transactionReceived.ts b/backend/src/mailer/text/transactionReceived.ts new file mode 100644 index 000000000..3df2b718a --- /dev/null +++ b/backend/src/mailer/text/transactionReceived.ts @@ -0,0 +1,27 @@ +export const transactionReceived = { + de: { + subject: 'Gradido Überweisung', + text: (data: { + senderFirstName: string + senderLastName: string + recipientFirstName: string + recipientLastName: string + email: string + amount: number + memo: string + }): string => + `Hallo ${data.recipientFirstName} ${data.recipientLastName} + +Du hast soeben ${data.amount.toFixed(2).replace('.', ',')} GDD von ${data.senderFirstName} ${ + data.senderLastName + } erhalten. +${data.senderFirstName} ${data.senderLastName} schreibt: + +${data.memo} + +Bitte antworte nicht auf diese E-Mail! + +Mit freundlichen Grüßen, +dein Gradido-Team`, + }, +}