From a2623e1352702aeaf53874fa5cf2aef4fb765b0c Mon Sep 17 00:00:00 2001 From: Alex Yang Date: Sat, 2 Sep 2023 13:13:59 -0500 Subject: [PATCH] test: email sending e2e (#4130) --- .github/workflows/build-server.yml | 12 ++ apps/server/.env.example | 1 + apps/server/scripts/test-send-local-mail.ts | 18 +++ apps/server/src/config/def.ts | 5 + apps/server/src/config/default.ts | 2 + apps/server/src/modules/auth/mailer/mailer.ts | 11 ++ apps/server/src/prelude.ts | 4 + apps/server/src/tests/mailer.e2e.ts | 105 ++++++++++++++++++ tests/affine-cloud/playwright.config.ts | 1 + 9 files changed, 159 insertions(+) create mode 100644 apps/server/scripts/test-send-local-mail.ts create mode 100644 apps/server/src/tests/mailer.e2e.ts diff --git a/.github/workflows/build-server.yml b/.github/workflows/build-server.yml index 1879e71b7..1a13cb62a 100644 --- a/.github/workflows/build-server.yml +++ b/.github/workflows/build-server.yml @@ -75,6 +75,11 @@ jobs: --health-retries 5 ports: - 5432:5432 + mailer: + image: mailhog/mailhog + ports: + - 1025:1025 + - 8025:8025 steps: - uses: actions/checkout@v3 @@ -112,6 +117,7 @@ jobs: env: CARGO_TARGET_DIR: '${{ github.workspace }}/target' DATABASE_URL: postgresql://affine:affine@localhost:5432/affine + ENABLE_LOCAL_EMAIL: true - name: Upload server test coverage results uses: codecov/codecov-action@v3 @@ -139,6 +145,11 @@ jobs: --health-retries 5 ports: - 5432:5432 + mailer: + image: mailhog/mailhog + ports: + - 1025:1025 + - 8025:8025 steps: - uses: actions/checkout@v3 @@ -181,6 +192,7 @@ jobs: env: COVERAGE: true DATABASE_URL: postgresql://affine:affine@localhost:5432/affine + ENABLE_LOCAL_EMAIL: true - name: Collect code coverage report run: yarn exec nyc report -t .nyc_output --report-dir .coverage --reporter=lcov diff --git a/apps/server/.env.example b/apps/server/.env.example index 2dcf75924..01e76c182 100644 --- a/apps/server/.env.example +++ b/apps/server/.env.example @@ -3,3 +3,4 @@ NEXTAUTH_URL="http://localhost:8080" OAUTH_EMAIL_SENDER="noreply@toeverything.info" OAUTH_EMAIL_LOGIN="" OAUTH_EMAIL_PASSWORD="" +ENABLE_LOCAL_EMAIL="true" diff --git a/apps/server/scripts/test-send-local-mail.ts b/apps/server/scripts/test-send-local-mail.ts new file mode 100644 index 000000000..50e0872ab --- /dev/null +++ b/apps/server/scripts/test-send-local-mail.ts @@ -0,0 +1,18 @@ +import { createTransport } from 'nodemailer'; + +const transport = createTransport({ + host: '0.0.0.0', + port: 1025, + secure: false, + auth: { + user: 'himself65', + pass: '123456', + }, +}); + +await transport.sendMail({ + from: 'noreply@toeverything.info', + to: 'himself65@outlook.com', + subject: 'test', + html: `
hello world
`, +}); diff --git a/apps/server/src/config/def.ts b/apps/server/src/config/def.ts index e93047423..ef434575e 100644 --- a/apps/server/src/config/def.ts +++ b/apps/server/src/config/def.ts @@ -308,6 +308,11 @@ export interface AFFiNEConfig { } > >; + /** + * whether to use local email service to send email + * local debug only + */ + localEmail: boolean; email: { server: string; port: number; diff --git a/apps/server/src/config/default.ts b/apps/server/src/config/default.ts index 25f16e2f6..3aae4b9d0 100644 --- a/apps/server/src/config/default.ts +++ b/apps/server/src/config/default.ts @@ -86,6 +86,7 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => { 'doc.manager.experimentalMergeWithJwstCodec', 'boolean', ], + ENABLE_LOCAL_EMAIL: ['auth.localEmail', 'boolean'], } satisfies AFFiNEConfig['ENV_MAP'], affineEnv: 'dev', get affine() { @@ -152,6 +153,7 @@ export const getDefaultAFFiNEConfig: () => AFFiNEConfig = () => { return this.privateKey; }, oauthProviders: {}, + localEmail: false, email: { server: 'smtp.gmail.com', port: 465, diff --git a/apps/server/src/modules/auth/mailer/mailer.ts b/apps/server/src/modules/auth/mailer/mailer.ts index 6df367f5e..d1c8f65c3 100644 --- a/apps/server/src/modules/auth/mailer/mailer.ts +++ b/apps/server/src/modules/auth/mailer/mailer.ts @@ -15,6 +15,17 @@ export const MAILER: FactoryProvider< > = { provide: MAILER_SERVICE, useFactory: (config: Config) => { + if (config.auth.localEmail) { + return createTransport({ + host: '0.0.0.0', + port: 1025, + secure: false, + auth: { + user: config.auth.email.login, + pass: config.auth.email.password, + }, + }); + } return createTransport({ service: 'gmail', auth: { diff --git a/apps/server/src/prelude.ts b/apps/server/src/prelude.ts index 10c53c1be..0c0fae35f 100644 --- a/apps/server/src/prelude.ts +++ b/apps/server/src/prelude.ts @@ -4,3 +4,7 @@ import 'dotenv/config'; import { getDefaultAFFiNEConfig } from './config/default'; globalThis.AFFiNE = getDefaultAFFiNEConfig(); + +if (process.env.NODE_ENV === 'development') { + console.log('AFFiNE Config:', globalThis.AFFiNE); +} diff --git a/apps/server/src/tests/mailer.e2e.ts b/apps/server/src/tests/mailer.e2e.ts new file mode 100644 index 000000000..8bc885271 --- /dev/null +++ b/apps/server/src/tests/mailer.e2e.ts @@ -0,0 +1,105 @@ +/// +// This test case is for testing the mailer service. +// Please use local SMTP server for testing. +// See: https://github.com/mailhog/MailHog +import { Test, TestingModule } from '@nestjs/testing'; +import { PrismaClient } from '@prisma/client'; +import test from 'ava'; + +import { ConfigModule } from '../config'; +import { GqlModule } from '../graphql.module'; +import { MetricsModule } from '../metrics'; +import { AuthModule } from '../modules/auth'; +import { AuthService } from '../modules/auth/service'; +import { PrismaModule } from '../prisma'; +import { RateLimiterModule } from '../throttler'; + +let auth: AuthService; +let module: TestingModule; +let skip = false; + +test.before(async () => { + try { + const response = await fetch('http://localhost:8025/api/v2/messages'); + if (!response.ok && !process.env.CI) { + console.warn('local mail not found, skip the mailer.e2e.ts'); + skip = true; + } + } catch (error) { + if ( + error instanceof Error && + (error.cause as any)?.code === 'ECONNREFUSED' && + !process.env.CI + ) { + console.warn('local mail not found, skip the mailer.e2e.ts'); + skip = true; + } + } +}); + +// cleanup database before each test +test.beforeEach(async () => { + const client = new PrismaClient(); + await client.$connect(); + await client.user.deleteMany({}); +}); + +test.beforeEach(async () => { + module = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + auth: { + accessTokenExpiresIn: 1, + refreshTokenExpiresIn: 1, + leeway: 1, + }, + }), + PrismaModule, + GqlModule, + AuthModule, + MetricsModule, + RateLimiterModule, + ], + }).compile(); + auth = module.get(AuthService); +}); + +test.afterEach(async () => { + await module.close(); +}); + +const getCurrentMailMessageCount = async () => { + const response = await fetch('http://localhost:8025/api/v2/messages'); + const data = await response.json(); + return data.total; +}; + +const getLatestMailMessage = async () => { + const response = await fetch('http://localhost:8025/api/v2/messages'); + const data = await response.json(); + return data.items[0]; +}; + +test('should include callbackUrl in sending email', async t => { + if (skip) { + return t.pass(); + } + await auth.signUp('Alex Yang', 'alexyang@example.org', '123456'); + for (const fn of [ + 'sendSetPasswordEmail', + 'sendChangeEmail', + 'sendChangePasswordEmail', + ] as const) { + const prev = await getCurrentMailMessageCount(); + await auth[fn]('alexyang@example.org', 'https://test.com/callback'); + const current = await getCurrentMailMessageCount(); + const mail = await getLatestMailMessage(); + t.regex( + mail.Content.Body, + /https:\/\/test.com\/callback/, + `should include callbackUrl when calling ${fn}` + ); + t.is(current, prev + 1, `calling ${fn}`); + } + return; +}); diff --git a/tests/affine-cloud/playwright.config.ts b/tests/affine-cloud/playwright.config.ts index f88b60cea..7a63e657e 100644 --- a/tests/affine-cloud/playwright.config.ts +++ b/tests/affine-cloud/playwright.config.ts @@ -50,6 +50,7 @@ const config: PlaywrightTestConfig = { DEBUG: 'affine:*', FORCE_COLOR: 'true', DEBUG_COLORS: 'true', + ENABLE_LOCAL_EMAIL: process.env.ENABLE_LOCAL_EMAIL ?? 'true', NEXTAUTH_URL: 'http://localhost:8080', OAUTH_EMAIL_SENDER: 'noreply@toeverything.info', },