import { describe, it, expect, vi, beforeAll, beforeEach } from 'vitest'; import { createRequire } from 'module'; import { fileURLToPath } from 'url'; import { dirname, resolve } from 'path'; import http from 'http'; const require = createRequire(import.meta.url); const __dirname = dirname(fileURLToPath(import.meta.url)); // ── Load real PROVIDER_DATA by stubbing only nodemailer ─────────── // This way the mock never drifts from mailer.js. const nodemailerPath = require.resolve('nodemailer'); require.cache[nodemailerPath] = { id: nodemailerPath, filename: nodemailerPath, loaded: true, exports: { createTransport: () => ({ sendMail: async () => ({}) }) }, children: [], paths: [], }; const { PROVIDER_DATA } = require('../services/mailer'); // ── Replace mailer in cache with full stub (sendErasureMail mocked) ── const mockSendErasureMail = vi.fn().mockImplementation(({ provider }) => { const info = PROVIDER_DATA[provider]; if (!info?.email) return Promise.resolve({ skipped: true, reason: 'use_form', formUrl: info?.formUrl }); return Promise.resolve({ skipped: false }); }); const mailerPath = resolve(__dirname, '../services/mailer.js'); require.cache[mailerPath] = { id: mailerPath, filename: mailerPath, loaded: true, exports: { PROVIDER_DATA, sendErasureMail: mockSendErasureMail }, children: [], paths: [], }; // Load route AFTER cache is patched const eraseRoute = require('../routes/erase'); const express = require('express'); function buildApp() { const app = express(); app.use(express.json({ limit: '10kb' })); app.post('/api/erase', eraseRoute); return app; } function request(app, body) { return new Promise((resolve, reject) => { const server = http.createServer(app); server.listen(0, '127.0.0.1', () => { const { port } = server.address(); fetch(`http://127.0.0.1:${port}/api/erase`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }) .then(res => res.json().then(json => ({ status: res.status, body: json }))) .then(result => { server.close(); resolve(result); }) .catch(err => { server.close(); reject(err); }); }); }); } let app; beforeAll(() => { app = buildApp(); }); beforeEach(() => { mockSendErasureMail.mockClear(); }); // ── Snapshot: catch drift in provider list ──────────────────────── describe('provider registry', () => { it('matches snapshot', () => { expect(PROVIDER_DATA).toMatchSnapshot(); }); }); // ── Validation ──────────────────────────────────────────────────── describe('validation', () => { it('rejects missing provider', async () => { const { status, body } = await request(app, { email: 'test@example.com' }); expect(status).toBe(400); expect(body.error).toMatch(/provider/i); expect(mockSendErasureMail).not.toHaveBeenCalled(); }); it('rejects missing email', async () => { const { status, body } = await request(app, { provider: 'instagram' }); expect(status).toBe(400); expect(body.error).toMatch(/email/i); expect(mockSendErasureMail).not.toHaveBeenCalled(); }); it('rejects invalid email format', async () => { const { status, body } = await request(app, { provider: 'instagram', email: 'not-an-email' }); expect(status).toBe(400); expect(body.error).toMatch(/email/i); expect(mockSendErasureMail).not.toHaveBeenCalled(); }); it('rejects unknown provider', async () => { const { status, body } = await request(app, { provider: 'myspace', email: 'test@example.com' }); expect(status).toBe(400); expect(body.error).toMatch(/proveedor/i); expect(mockSendErasureMail).not.toHaveBeenCalled(); }); }); // ── Form-only providers ─────────────────────────────────────────── describe('form-only providers', () => { it('google returns use_form with formUrl', async () => { const { status, body } = await request(app, { provider: 'google', email: 'test@example.com' }); expect(status).toBe(200); expect(body.status).toBe('use_form'); expect(body.formUrl).toMatch(/^https?:\/\//); expect(mockSendErasureMail).toHaveBeenCalledOnce(); expect(mockSendErasureMail).toHaveBeenCalledWith(expect.objectContaining({ provider: 'google', email: 'test@example.com' })); }); it('amazon returns use_form with formUrl', async () => { const { status, body } = await request(app, { provider: 'amazon', email: 'test@example.com' }); expect(status).toBe(200); expect(body.status).toBe('use_form'); expect(body.formUrl).toMatch(/^https?:\/\//); expect(mockSendErasureMail).toHaveBeenCalledOnce(); expect(mockSendErasureMail).toHaveBeenCalledWith(expect.objectContaining({ provider: 'amazon', email: 'test@example.com' })); }); }); // ── Successful sends (mocked transport) ────────────────────────── describe('email providers', () => { const providers = Object.entries(PROVIDER_DATA) .filter(([, info]) => info.email !== null) .map(([key]) => key); for (const provider of providers) { it(`sends for ${provider}`, async () => { const { status, body } = await request(app, { provider, email: 'smoke@example.com', nickname: 'Test User', }); expect(status).toBe(200); expect(body.status).toBe('ok'); expect(body.reference).toHaveLength(12); expect(mockSendErasureMail).toHaveBeenCalledOnce(); expect(mockSendErasureMail).toHaveBeenCalledWith(expect.objectContaining({ provider, email: 'smoke@example.com', nickname: 'Test User' })); }); } }); // ── CRLF injection sanitization ─────────────────────────────────── describe('sanitization', () => { it('strips CRLF from optional fields without erroring', async () => { const { status, body } = await request(app, { provider: 'instagram', email: 'test@example.com', nickname: 'user\r\nX-Injected: evil', extra: 'line1\nline2', }); expect(status).toBe(200); expect(body.status).toBe('ok'); }); });