From 7af789c49097181f5749fe0459f9419f5a31fb67 Mon Sep 17 00:00:00 2001 From: KamyCoding Date: Sun, 9 Aug 2026 19:39:52 +0200 Subject: [PATCH] fix(server): make Brevo timeout configurable --- server/.env.example | 1 + server/src/config/env.ts | 8 ++++++- server/src/services/email.service.ts | 3 +-- server/test/contact.integration.test.ts | 32 +++++++++++++++++++++++++ 4 files changed, 41 insertions(+), 3 deletions(-) diff --git a/server/.env.example b/server/.env.example index 7a9ca6c..a750853 100644 --- a/server/.env.example +++ b/server/.env.example @@ -2,6 +2,7 @@ PORT=3000 NODE_ENV=development ALLOWED_ORIGIN=http://localhost:4200 BREVO_API_KEY=replace-with-your-brevo-api-key +BREVO_REQUEST_TIMEOUT_MS=15000 CONTACT_TO_EMAIL=recipient@example.com CONTACT_FROM_EMAIL=verified-sender@example.com CONTACT_FROM_NAME=Portfolio Contact Form diff --git a/server/src/config/env.ts b/server/src/config/env.ts index 3776c62..a2f45dd 100644 --- a/server/src/config/env.ts +++ b/server/src/config/env.ts @@ -9,6 +9,8 @@ const envSchema = z.object({ BREVO_API_KEY: z.string().min(1, 'BREVO_API_KEY is required.'), + BREVO_REQUEST_TIMEOUT_MS: z.coerce.number().int().positive().default(15_000), + CONTACT_TO_EMAIL: z.email(), CONTACT_FROM_EMAIL: z.email(), @@ -16,7 +18,11 @@ const envSchema = z.object({ CONTACT_FROM_NAME: z.string().trim().min(1, 'CONTACT_FROM_NAME is required.').max(100), }); -const parsedEnv = envSchema.safeParse(process.env); +export function validateEnvironment(environment: NodeJS.ProcessEnv) { + return envSchema.safeParse(environment); +} + +const parsedEnv = validateEnvironment(process.env); if (!parsedEnv.success) { console.error('Invalid environment variables:', parsedEnv.error.flatten().fieldErrors); diff --git a/server/src/services/email.service.ts b/server/src/services/email.service.ts index 0320ff7..c42c7a2 100644 --- a/server/src/services/email.service.ts +++ b/server/src/services/email.service.ts @@ -1,7 +1,6 @@ import { env } from '../config/env.js'; const BREVO_API_URL = 'https://api.brevo.com/v3/smtp/email'; -const BREVO_REQUEST_TIMEOUT_MS = 8_000; export type EmailDeliveryErrorCategory = 'network' | 'provider-response' | 'timeout'; @@ -27,7 +26,7 @@ export type ContactEmailSender = (input: SendContactEmailInput) => Promise export async function sendContactEmail( input: SendContactEmailInput, - timeoutMs = BREVO_REQUEST_TIMEOUT_MS, + timeoutMs = env.BREVO_REQUEST_TIMEOUT_MS, ): Promise { const abortController = new AbortController(); const timeout = setTimeout(() => abortController.abort(), timeoutMs); diff --git a/server/test/contact.integration.test.ts b/server/test/contact.integration.test.ts index c04409e..35c4d99 100644 --- a/server/test/contact.integration.test.ts +++ b/server/test/contact.integration.test.ts @@ -10,11 +10,13 @@ process.env.PORT = '3000'; process.env.NODE_ENV = 'test'; process.env.ALLOWED_ORIGIN = 'http://localhost:4200'; process.env.BREVO_API_KEY = 'test-api-key'; +process.env.BREVO_REQUEST_TIMEOUT_MS = '17500'; process.env.CONTACT_TO_EMAIL = 'recipient@example.com'; process.env.CONTACT_FROM_EMAIL = 'verified-sender@example.com'; process.env.CONTACT_FROM_NAME = 'Portfolio Contact Form'; const { createApp } = await import('../src/app.js'); +const { env, validateEnvironment } = await import('../src/config/env.js'); const { EmailDeliveryError, sendContactEmail } = await import('../src/services/email.service.js'); const validContactRequest = { @@ -25,6 +27,36 @@ const validContactRequest = { company: '', }; +test('accepts a configured Brevo request timeout', () => { + assert.equal(env.BREVO_REQUEST_TIMEOUT_MS, 17_500); +}); + +test('rejects an invalid Brevo request timeout', () => { + const result = validateEnvironment({ + ...process.env, + BREVO_REQUEST_TIMEOUT_MS: 'not-a-number', + }); + + assert.equal(result.success, false); + + if (!result.success) { + assert.ok(result.error.flatten().fieldErrors.BREVO_REQUEST_TIMEOUT_MS?.length); + } +}); + +test('uses the safe default when the Brevo request timeout is omitted', () => { + const environment = { ...process.env }; + delete environment.BREVO_REQUEST_TIMEOUT_MS; + + const result = validateEnvironment(environment); + + assert.equal(result.success, true); + + if (result.success) { + assert.equal(result.data.BREVO_REQUEST_TIMEOUT_MS, 15_000); + } +}); + test('delivers a valid contact request with the validated Reply-To details', async () => { let deliveredMessage: SendContactEmailInput | undefined; const emailSender: ContactEmailSender = async (input) => {