- Vitest unit tests for POST /api/erase: validation, form-only providers, all email providers, CRLF sanitization - Snapshot on PROVIDER_DATA to catch provider config drift - Mock via require cache to avoid nodemailer/sendmail dependency - sendErasureMail call assertions (args + call count) - fetch-test-emails.mjs script to refresh disposable email pool - mailer: add Reply-To header pointing to requester email - mailer: fallback to direct transport when sendmail unavailable
159 lines
6.3 KiB
JavaScript
159 lines
6.3 KiB
JavaScript
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');
|
|
});
|
|
});
|