- DB erp_test isolée (ports 3002/5174, erp_local intouché) - seed-test.ts déterministe: 3 users, 2 clients, 2 suppliers, 2 orders, 59 help articles - globalSetup truncate+reseed, webServer dual backend+frontend - 10 smoke tests: login, RBAC, orders, documents, help drawer, Ctrl+/ - Réorganisation tests/: e2e/ (Playwright) vs backend/ (vitest) - cross-env + dotenv pour portabilité Windows/Linux - vite.config.ts proxy dynamique via BACKEND_PORT env var Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
209 lines
6.3 KiB
TypeScript
209 lines
6.3 KiB
TypeScript
/**
|
|
* Tests ACHATS-B — Email service SMTP/IMAP
|
|
* Run: tsx tests/email.test.ts
|
|
* Requires server running on localhost:3001 + seeded DB
|
|
*/
|
|
|
|
const BASE = 'http://localhost:3001/api/v1';
|
|
const AUTH = `${BASE}/auth`;
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
const failures: string[] = [];
|
|
|
|
function assert(condition: boolean, testName: string, detail?: string) {
|
|
if (condition) {
|
|
passed++;
|
|
console.log(` ✓ ${testName}`);
|
|
} else {
|
|
failed++;
|
|
const msg = detail ? `${testName} — ${detail}` : testName;
|
|
failures.push(msg);
|
|
console.log(` ✗ ${msg}`);
|
|
}
|
|
}
|
|
|
|
async function login(email: string, password: string): Promise<string> {
|
|
const res = await fetch(`${AUTH}/login`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ email, password }),
|
|
});
|
|
const data = await res.json() as any;
|
|
return data.accessToken;
|
|
}
|
|
|
|
function authHeaders(token: string): Record<string, string> {
|
|
return {
|
|
Authorization: `Bearer ${token}`,
|
|
'Content-Type': 'application/json',
|
|
};
|
|
}
|
|
|
|
function authOnly(token: string): Record<string, string> {
|
|
return { Authorization: `Bearer ${token}` };
|
|
}
|
|
|
|
let adminToken: string;
|
|
let acheteurToken: string;
|
|
let commercialToken: string;
|
|
|
|
// --- Test: POST /email/test-send — success ---
|
|
async function testSendEmail() {
|
|
console.log('\n--- POST /email/test-send ---');
|
|
|
|
// Happy path: admin sends test email
|
|
const res = await fetch(`${BASE}/email/test-send`, {
|
|
method: 'POST',
|
|
headers: authHeaders(adminToken),
|
|
body: JSON.stringify({
|
|
to: 'purchase@metdesigntver.ru',
|
|
subject: 'ERP Test Email ' + new Date().toISOString(),
|
|
message: 'This is a test email from MetallKart ERP email module.',
|
|
}),
|
|
});
|
|
const data = await res.json() as any;
|
|
assert(
|
|
res.status === 200 && data.success === true && typeof data.messageId === 'string',
|
|
'test-send success with messageId',
|
|
`status=${res.status} success=${data.success} messageId=${data.messageId}`,
|
|
);
|
|
}
|
|
|
|
// --- Test: POST /email/test-send — validation error ---
|
|
async function testSendEmailValidation() {
|
|
console.log('\n--- POST /email/test-send validation ---');
|
|
|
|
const res = await fetch(`${BASE}/email/test-send`, {
|
|
method: 'POST',
|
|
headers: authHeaders(adminToken),
|
|
body: JSON.stringify({ to: 'not-an-email', subject: '', message: '' }),
|
|
});
|
|
assert(res.status === 400, 'test-send validation rejects invalid email', `status=${res.status}`);
|
|
}
|
|
|
|
// --- Test: POST /email/test-send — RBAC non-admin 403 ---
|
|
async function testSendEmailRBAC() {
|
|
console.log('\n--- POST /email/test-send RBAC ---');
|
|
|
|
// ACHETEUR should be forbidden
|
|
const res1 = await fetch(`${BASE}/email/test-send`, {
|
|
method: 'POST',
|
|
headers: authHeaders(acheteurToken),
|
|
body: JSON.stringify({
|
|
to: 'test@example.com',
|
|
subject: 'test',
|
|
message: 'test',
|
|
}),
|
|
});
|
|
assert(res1.status === 403, 'test-send rejected for ACHETEUR (403)', `status=${res1.status}`);
|
|
|
|
// COMMERCIAL should be forbidden
|
|
const res2 = await fetch(`${BASE}/email/test-send`, {
|
|
method: 'POST',
|
|
headers: authHeaders(commercialToken),
|
|
body: JSON.stringify({
|
|
to: 'test@example.com',
|
|
subject: 'test',
|
|
message: 'test',
|
|
}),
|
|
});
|
|
assert(res2.status === 403, 'test-send rejected for COMMERCIAL (403)', `status=${res2.status}`);
|
|
}
|
|
|
|
// --- Test: POST /email/test-send — unauthenticated 401 ---
|
|
async function testSendEmailUnauth() {
|
|
console.log('\n--- POST /email/test-send unauthenticated ---');
|
|
|
|
const res = await fetch(`${BASE}/email/test-send`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
to: 'test@example.com',
|
|
subject: 'test',
|
|
message: 'test',
|
|
}),
|
|
});
|
|
assert(res.status === 401, 'test-send rejected without auth (401)', `status=${res.status}`);
|
|
}
|
|
|
|
// --- Test: GET /email/inbox — success ---
|
|
async function testInbox() {
|
|
console.log('\n--- GET /email/inbox ---');
|
|
|
|
const res = await fetch(`${BASE}/email/inbox`, {
|
|
headers: authOnly(adminToken),
|
|
});
|
|
const data = await res.json() as any;
|
|
assert(
|
|
res.status === 200 && Array.isArray(data.items) && typeof data.total === 'number',
|
|
'inbox returns items array and total',
|
|
`status=${res.status} total=${data.total}`,
|
|
);
|
|
}
|
|
|
|
// --- Test: GET /email/inbox with since param ---
|
|
async function testInboxWithSince() {
|
|
console.log('\n--- GET /email/inbox with since ---');
|
|
|
|
const since = new Date(Date.now() - 3600_000).toISOString();
|
|
const res = await fetch(`${BASE}/email/inbox?since=${encodeURIComponent(since)}`, {
|
|
headers: authOnly(adminToken),
|
|
});
|
|
const data = await res.json() as any;
|
|
assert(
|
|
res.status === 200 && Array.isArray(data.items),
|
|
'inbox with since filter returns items',
|
|
`status=${res.status}`,
|
|
);
|
|
}
|
|
|
|
// --- Test: GET /email/inbox — RBAC non-admin 403 ---
|
|
async function testInboxRBAC() {
|
|
console.log('\n--- GET /email/inbox RBAC ---');
|
|
|
|
const res = await fetch(`${BASE}/email/inbox`, {
|
|
headers: authOnly(acheteurToken),
|
|
});
|
|
assert(res.status === 403, 'inbox rejected for ACHETEUR (403)', `status=${res.status}`);
|
|
}
|
|
|
|
// --- Test: GET /email/inbox — unauthenticated 401 ---
|
|
async function testInboxUnauth() {
|
|
console.log('\n--- GET /email/inbox unauthenticated ---');
|
|
|
|
const res = await fetch(`${BASE}/email/inbox`);
|
|
assert(res.status === 401, 'inbox rejected without auth (401)', `status=${res.status}`);
|
|
}
|
|
|
|
async function main() {
|
|
console.log('=== EMAIL MODULE TESTS (ACHATS-B) ===\n');
|
|
console.log('Logging in...');
|
|
|
|
adminToken = await login('louis@metallcart.ru', 'MetallKart2026!');
|
|
acheteurToken = await login('evgeny@metallcart.ru', 'MetallKart2026!');
|
|
commercialToken = await login('anna.v@metallcart.ru', 'MetallKart2026!');
|
|
|
|
assert(!!adminToken, 'Admin login OK');
|
|
assert(!!acheteurToken, 'Acheteur login OK');
|
|
assert(!!commercialToken, 'Commercial login OK');
|
|
|
|
await testSendEmail();
|
|
await testSendEmailValidation();
|
|
await testSendEmailRBAC();
|
|
await testSendEmailUnauth();
|
|
await testInbox();
|
|
await testInboxWithSince();
|
|
await testInboxRBAC();
|
|
await testInboxUnauth();
|
|
|
|
console.log(`\n=== RESULTS: ${passed} passed, ${failed} failed ===`);
|
|
if (failures.length > 0) {
|
|
console.log('Failures:');
|
|
failures.forEach(f => console.log(` - ${f}`));
|
|
}
|
|
process.exit(failed > 0 ? 1 : 0);
|
|
}
|
|
|
|
main();
|