const fs = require('fs'); const path = require('path'); const { PrismaClient } = require('@prisma/client'); const { PrismaPg } = require('@prisma/adapter-pg'); const { Pool } = require('pg'); require('dotenv').config(); // Base configurations - Target the dev server directly via localhost port const BASE_URL = "http://127.0.0.1:3001"; let prisma; let pool; let workflowId; let n8nUrl; let n8nApiKey; // Helper to run query with admin credentials function getPrisma() { if (!prisma) { const dbUrl = new URL(process.env.DATABASE_URL_DEV || process.env.DATABASE_URL); pool = new Pool({ host: dbUrl.hostname, port: dbUrl.port ? parseInt(dbUrl.port) : 5432, user: decodeURIComponent(dbUrl.username), password: decodeURIComponent(dbUrl.password), database: dbUrl.pathname.substring(1).split('?')[0], ssl: false }); const adapter = new PrismaPg(pool); prisma = new PrismaClient({ adapter }); } return prisma; } async function runAsAdmin(queryFn) { const db = getPrisma(); return db.$transaction(async (tx) => { await tx.$executeRawUnsafe(`SET LOCAL app.current_user_role = 'admin';`); return queryFn(tx); }); } async function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } // Custom simple assertion let passed = 0; let failed = 0; function assert(condition, message) { if (condition) { console.log(` ✓ PASS: ${message}`); passed++; } else { console.error(` ✗ FAIL: ${message}`); failed++; } } async function cleanupDb() { console.log("Cleaning up integration test records in test database..."); try { await runAsAdmin(async (tx) => { console.log("DEBUG: Available models on tx:", Object.keys(tx).filter(k => !k.startsWith('$'))); // Clean sales results await tx.salesResult.deleteMany({ where: { idempotencyKey: { startsWith: 'key-integration' } } }); // Clean import jobs if (tx.salesImportJob) { await tx.salesImportJob.deleteMany({ where: { idempotencyKey: { startsWith: 'key-integration' } } }); } else { console.warn("WARNING: tx.salesImportJob is undefined, skipping deleteMany."); } // Clean audit logs await tx.auditLog.deleteMany({ where: { action: 'CREATE', targetTable: 'sales_results' } }); }); } catch (err) { console.error("Error during DB cleanup:", err); } } async function runTests() { console.log("=== STARTING REAL N8N E2E INTEGRATION TEST SUITE ==="); // 1. Clean DB await cleanupDb(); // 2. Fetch n8n API configuration const mcpConfigPath = path.join(__dirname, '../.agents/mcp_config.json'); if (!fs.existsSync(mcpConfigPath)) { console.error("Error: .agents/mcp_config.json not found."); process.exit(1); } const mcpConfig = JSON.parse(fs.readFileSync(mcpConfigPath, 'utf8')); const n8nEnv = mcpConfig.mcpServers.n8n.env; n8nUrl = n8nEnv.N8N_API_URL || "https://n8n.gaboggamer.online"; n8nApiKey = n8nEnv.N8N_API_KEY; if (!n8nApiKey) { console.error("Error: N8N_API_KEY is missing in mcp_config.json."); process.exit(1); } // 3. Load and modify n8n workflow JSON to point to our local test server via Docker bridge const workflowPath = path.join(__dirname, '../n8n/sales_import_workflow.json'); if (!fs.existsSync(workflowPath)) { console.error("Error: n8n/sales_import_workflow.json not found."); process.exit(1); } const workflowJson = JSON.parse(fs.readFileSync(workflowPath, 'utf8')); const webhookPath = `calculate-commissions-${Date.now()}`; // Point callbacks to dev server, set dev test signature, and randomize path to prevent webhook conflicts for (const node of workflowJson.nodes) { if (node.id === 'Set-Dev-Env' || node.id === 'Set-Prod-Env') { node.parameters.values.string = [ { name: 'appUrl', value: 'http://special-hotel-dev:3000' }, { name: 'signature', value: process.env.N8N_WEBHOOK_SECRET_DEV || 'qUHuPqPjA65psdtwQU7zgp/DVkvd1xXk2WP/vzbnEdc=' } ]; } if (node.type === 'n8n-nodes-base.webhook') { node.parameters.path = webhookPath; } } // 4. Create workflow in n8n console.log("Deploying workflow to real n8n instance..."); const workflowName = `Semillero E2E Integration: Sales Import - ${Date.now()}`; const createRes = await fetch(`${n8nUrl}/api/v1/workflows`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-N8N-API-KEY': n8nApiKey }, body: JSON.stringify({ name: workflowName, nodes: workflowJson.nodes, connections: workflowJson.connections, settings: workflowJson.settings || {} }) }); if (!createRes.ok) { const errorText = await createRes.text(); console.error(`Error: Failed to create n8n workflow. Status: ${createRes.status}. Output: ${errorText}`); process.exit(1); } const createData = await createRes.json(); workflowId = createData.id; console.log(`Workflow deployed successfully! ID: ${workflowId}. Full response:`, JSON.stringify(createData)); // Activate the workflow if it is inactive (normally setting active in POST is enough, but double-checking) if (!createData.active) { console.log("Activating workflow..."); const activateRes = await fetch(`${n8nUrl}/api/v1/workflows/${workflowId}/activate`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-N8N-API-KEY': n8nApiKey }, body: JSON.stringify({}) }); if (!activateRes.ok) { console.error("Failed to activate workflow:", await activateRes.text()); } else { console.log("Workflow activated successfully! Waiting 5 seconds for n8n webhooks to register..."); await sleep(5000); } } // 5. Verify the external dev server is running console.log(`Verifying target dev server at ${BASE_URL}...`); try { const res = await fetch(`${BASE_URL}/api/auth/me`); if (!res.ok) { console.warn(`Warning: Target server returned status ${res.status}`); } } catch (e) { console.error(`Error: Dev server is unreachable at ${BASE_URL}. Error:`, e.message); await cleanup(); process.exit(1); } // 6. Generate test parameters const idempotencyKey = `key-integration-${Date.now()}`; // 7. Trigger the n8n webhook directly console.log(`Triggering n8n webhook directly at ${n8nUrl}/webhook/${webhookPath}...`); const uploadRes = await fetch(`${n8nUrl}/webhook/${webhookPath}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'x-n8n-signature': process.env.N8N_WEBHOOK_SECRET_DEV || 'qUHuPqPjA65psdtwQU7zgp/DVkvd1xXk2WP/vzbnEdc=' }, body: JSON.stringify({ idempotencyKey, uploaderId: 1, sales: [ { username: 'colaborador_mde', hotelCode: 'EST-MDE', period: '2026-06', amount: 15000, salesCount: 5, transactionId: 'TX-EST-MDE-001' } ] }) }); assert(uploadRes.status === 200 || uploadRes.status === 202, `n8n webhook response code is ${uploadRes.status} (expected 200/202)`); const uploadData = await uploadRes.json(); assert(uploadData.success && uploadData.code === 'IMPORT_ACCEPTED', "n8n accepted the webhook trigger"); // 8. Poll DB for record insert from n8n callback console.log("Waiting for n8n to complete processing and send callback to Next.js..."); let callbackCompleted = false; for (let i = 0; i < 20; i++) { await sleep(2000); const dbSales = await runAsAdmin(tx => tx.salesResult.findMany({ where: { idempotencyKey: { startsWith: idempotencyKey } } })); if (dbSales.length > 0) { assert(dbSales.length === 1, "1 sales result record successfully processed and saved via n8n integration!"); assert(parseFloat(dbSales[0].amount) === 15000.00, "Sales amount verified (15,000.00)"); callbackCompleted = true; break; } } assert(callbackCompleted, "n8n background execution and database write callback verified"); await cleanup(); console.log(`\n=== REAL N8N INTEGRATION TEST RESULTS: ${passed} PASSED, ${failed} FAILED ===`); if (failed > 0) { process.exit(1); } else { process.exit(0); } } async function cleanup() { console.log("\nCleaning up integration test processes..."); // 1. Delete workflow from n8n if (workflowId) { if (failed > 0) { console.log(`[TEST FAILED] Skipping test workflow ${workflowId} deletion to allow troubleshooting in n8n.gaboggamer.online.`); } else { console.log(`Deleting n8n test workflow ${workflowId}...`); try { const delRes = await fetch(`${n8nUrl}/api/v1/workflows/${workflowId}`, { method: 'DELETE', headers: { 'X-N8N-API-KEY': n8nApiKey } }); if (delRes.ok) { console.log("Test workflow deleted successfully from n8n."); } else { console.error("Failed to delete test workflow:", await delRes.text()); } } catch (e) { console.error("Error deleting workflow:", e); } } } // 3. Clean DB records await cleanupDb(); // 4. Disconnect Prisma try { if (prisma) { await prisma.$disconnect(); } if (pool) { await pool.end(); } } catch (e) {} } process.on('SIGINT', async () => { await cleanup(); process.exit(1); }); runTests().catch(async err => { console.error("Fatal E2E integration test runner error:", err); await cleanup(); process.exit(1); });