- 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>
218 lines
7.0 KiB
TypeScript
218 lines
7.0 KiB
TypeScript
/**
|
|
* E2E Workflow Test — MetallKart ERP
|
|
* Tests the complete flow: Auth → Orders → Estimates → PRs → Tenders → POs
|
|
*
|
|
* Usage: npx tsx tests/e2e-workflow.ts
|
|
* Requires: backend running on localhost:3001
|
|
*/
|
|
|
|
const BASE = 'http://localhost:3001/api/v1';
|
|
|
|
interface TestContext {
|
|
token: string;
|
|
orderId?: number;
|
|
smetaId?: number;
|
|
prIds?: number[];
|
|
tenderId?: number;
|
|
}
|
|
|
|
const ctx: TestContext = { token: '' };
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
async function api(method: string, path: string, body?: any): Promise<any> {
|
|
const headers: Record<string, string> = {};
|
|
if (ctx.token) headers['Authorization'] = `Bearer ${ctx.token}`;
|
|
if (body) headers['Content-Type'] = 'application/json';
|
|
|
|
const res = await fetch(`${BASE}${path}`, {
|
|
method,
|
|
headers,
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
});
|
|
const data = await res.json().catch(() => null);
|
|
if (!res.ok) {
|
|
throw new Error(`${method} ${path} → ${res.status}: ${JSON.stringify(data)}`);
|
|
}
|
|
return data;
|
|
}
|
|
|
|
async function test(name: string, fn: () => Promise<void>) {
|
|
try {
|
|
await fn();
|
|
console.log(` ✅ ${name}`);
|
|
passed++;
|
|
} catch (err: any) {
|
|
console.log(` ❌ ${name}: ${err.message}`);
|
|
failed++;
|
|
}
|
|
}
|
|
|
|
async function run() {
|
|
console.log('\n🔧 MetallKart ERP — E2E Workflow Test\n');
|
|
|
|
// === AUTH ===
|
|
console.log('📋 Authentication');
|
|
await test('Login as ADMIN', async () => {
|
|
const data = await api('POST', '/auth/login', {
|
|
email: 'louis@metallcart.ru',
|
|
password: 'MetallKart2026!',
|
|
});
|
|
if (!data.accessToken) throw new Error('No token');
|
|
ctx.token = data.accessToken;
|
|
});
|
|
|
|
// === HEALTH ===
|
|
console.log('\n📋 Health');
|
|
await test('Backend health', async () => {
|
|
const res = await fetch('http://localhost:3001/health');
|
|
if (!res.ok) throw new Error(`Status ${res.status}`);
|
|
const data = await res.json();
|
|
if (!data.dbConnected) throw new Error('DB not connected');
|
|
});
|
|
|
|
// === ORDERS ===
|
|
console.log('\n📋 Orders');
|
|
await test('List orders', async () => {
|
|
const data = await api('GET', '/orders');
|
|
const orders = Array.isArray(data) ? data : data.data || [];
|
|
if (!Array.isArray(orders)) throw new Error('Expected array');
|
|
console.log(` Found ${orders.length} orders`);
|
|
});
|
|
|
|
await test('List orders forSelect (exclude ARCHIVED/CANCELLED)', async () => {
|
|
const data = await api('GET', '/orders?forSelect=true');
|
|
const orders = Array.isArray(data) ? data : data.data || [];
|
|
const bad = orders.filter((o: any) => ['ARCHIVED', 'CANCELLED'].includes(o.status));
|
|
if (bad.length > 0) throw new Error(`Found ${bad.length} ARCHIVED/CANCELLED orders`);
|
|
});
|
|
|
|
// === ESTIMATES ===
|
|
console.log('\n📋 Estimates');
|
|
await test('List estimates', async () => {
|
|
const data = await api('GET', '/estimates');
|
|
const items = Array.isArray(data) ? data : data.data || [];
|
|
console.log(` Found ${items.length} estimates`);
|
|
if (items.length > 0) {
|
|
ctx.smetaId = items[0].id;
|
|
ctx.orderId = items[0].orderId;
|
|
}
|
|
});
|
|
|
|
// === PURCHASE REQUIREMENTS ===
|
|
console.log('\n📋 Purchase Requirements');
|
|
await test('List PRs', async () => {
|
|
const data = await api('GET', '/purchase-requirements');
|
|
const items = Array.isArray(data) ? data : data.data || [];
|
|
console.log(` Found ${items.length} PRs`);
|
|
if (items.length > 0) {
|
|
ctx.prIds = items.slice(0, 3).map((pr: any) => pr.id);
|
|
}
|
|
});
|
|
|
|
await test('Consolidated PRs', async () => {
|
|
const data = await api('GET', '/purchase-requirements/consolidated');
|
|
const groups = Array.isArray(data) ? data : data.groups || data.data || [];
|
|
console.log(` Found ${groups.length} groups`);
|
|
});
|
|
|
|
// === TENDERS ===
|
|
console.log('\n📋 Tenders');
|
|
await test('List tenders', async () => {
|
|
const data = await api('GET', '/tenders');
|
|
const items = Array.isArray(data) ? data : data.data || [];
|
|
console.log(` Found ${items.length} tenders`);
|
|
if (items.length > 0) {
|
|
ctx.tenderId = items[0].id;
|
|
}
|
|
});
|
|
|
|
// === PURCHASE ORDERS ===
|
|
console.log('\n📋 Purchase Orders');
|
|
await test('List POs', async () => {
|
|
const data = await api('GET', '/purchase-orders');
|
|
const items = Array.isArray(data) ? data : data.data || [];
|
|
console.log(` Found ${items.length} POs`);
|
|
});
|
|
|
|
// === SUPPLIERS ===
|
|
console.log('\n📋 Suppliers');
|
|
await test('List suppliers', async () => {
|
|
const data = await api('GET', '/suppliers');
|
|
const items = Array.isArray(data) ? data : data.data || [];
|
|
console.log(` Found ${items.length} suppliers`);
|
|
});
|
|
|
|
// === DOCUMENTS ===
|
|
console.log('\n📋 Documents');
|
|
await test('List documents (contract-tracking)', async () => {
|
|
const data = await api('GET', '/contract-tracking');
|
|
const items = Array.isArray(data) ? data : data.data || [];
|
|
console.log(` Found ${items.length} documents`);
|
|
});
|
|
|
|
// === PAYMENT REQUESTS ===
|
|
console.log('\n📋 Payment Requests');
|
|
await test('List payment requests', async () => {
|
|
const data = await api('GET', '/payment-requests');
|
|
const items = Array.isArray(data) ? data : data.data || [];
|
|
console.log(` Found ${items.length} payment requests`);
|
|
});
|
|
|
|
// === CLIENTS ===
|
|
console.log('\n📋 Clients');
|
|
await test('List clients', async () => {
|
|
const data = await api('GET', '/clients');
|
|
const items = Array.isArray(data) ? data : data.data || [];
|
|
console.log(` Found ${items.length} clients`);
|
|
});
|
|
|
|
// === FINANCIAL ===
|
|
console.log('\n📋 Financial');
|
|
await test('Financial dashboard', async () => {
|
|
const data = await api('GET', '/financial/dashboard');
|
|
if (!data) throw new Error('No dashboard data');
|
|
});
|
|
|
|
// === CASHFLOW ===
|
|
console.log('\n📋 Cashflow');
|
|
await test('Cashflow list', async () => {
|
|
const data = await api('GET', '/cashflow');
|
|
const items = Array.isArray(data) ? data : data.data || data.items || [];
|
|
console.log(` Found ${items.length} cashflow entries`);
|
|
});
|
|
|
|
// === NOTIFICATIONS ===
|
|
console.log('\n📋 Notifications');
|
|
await test('Notifications count', async () => {
|
|
const data = await api('GET', '/notifications/count');
|
|
console.log(` Unread: ${data.count ?? data.unread ?? 0}`);
|
|
});
|
|
|
|
// === ADMIN ===
|
|
console.log('\n📋 Admin');
|
|
await test('List users', async () => {
|
|
const data = await api('GET', '/admin/users');
|
|
const items = Array.isArray(data) ? data : data.data || data.items || [];
|
|
console.log(` Found ${items.length} users`);
|
|
});
|
|
|
|
await test('System config', async () => {
|
|
const data = await api('GET', '/admin/config');
|
|
const items = Array.isArray(data) ? data : data.data || data.items || [];
|
|
console.log(` Found ${items.length} config entries`);
|
|
});
|
|
|
|
// === SUMMARY ===
|
|
console.log(`\n${'='.repeat(50)}`);
|
|
console.log(`Results: ${passed} passed, ${failed} failed, ${passed + failed} total`);
|
|
console.log(`${'='.repeat(50)}\n`);
|
|
|
|
process.exit(failed > 0 ? 1 : 0);
|
|
}
|
|
|
|
run().catch(err => {
|
|
console.error('Fatal error:', err);
|
|
process.exit(1);
|
|
});
|