semillero-special-hotel/prisma/test-n8n-real.js

313 lines
9.5 KiB
JavaScript

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);
});