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

522 lines
17 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;
const deployedWorkflowIds = [];
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) => {
// Clean sales results
await tx.salesResult.deleteMany({
where: {
OR: [
{ idempotencyKey: { startsWith: 'key-integration' } },
{ period: '2026-07' }
]
}
});
// Clean import jobs
if (tx.salesImportJob) {
await tx.salesImportJob.deleteMany({
where: {
idempotencyKey: { startsWith: 'key-integration' }
}
});
}
// Clean settlements
await tx.settlement.deleteMany({
where: {
period: '2026-07'
}
});
// Clean goals
await tx.goal.deleteMany({
where: {
period: '2026-07'
}
});
// Clean test plans
await tx.calculationRule.deleteMany({
where: { plan: { code: 'PLAN-INT-P5' } }
});
await tx.compensationPlan.deleteMany({
where: { code: 'PLAN-INT-P5' }
});
// 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. Fetch existing workflows to retrieve the project ID dynamically
console.log("Fetching project ID from existing workflows...");
let projectId;
try {
const listRes = await fetch(`${n8nUrl}/api/v1/workflows`, {
method: 'GET',
headers: {
'X-N8N-API-KEY': n8nApiKey
}
});
if (listRes.ok) {
const listData = await listRes.json();
const firstWf = listData.data.find(w => w.shared && w.shared.length > 0);
if (firstWf) {
projectId = firstWf.shared[0].projectId;
console.log(`Resolved project ID dynamically: ${projectId}`);
}
}
} catch (err) {
console.warn("Warning: Could not fetch project ID dynamically, falling back to default.", err.message);
}
// 4. Fetch credentials dynamically to resolve AI accounts
console.log("Fetching credentials from n8n to resolve AI accounts...");
let deepSeekCred = null;
let geminiCred = null;
try {
const credsRes = await fetch(`${n8nUrl}/api/v1/credentials`, {
method: 'GET',
headers: {
'X-N8N-API-KEY': n8nApiKey
}
});
if (credsRes.ok) {
const credsData = await credsRes.json();
const credentials = Array.isArray(credsData.data) ? credsData.data : (credsData.data.credentials || []);
const deepSeekCreds = credentials.filter(c => c.type === 'deepSeekApi');
if (deepSeekCreds.length > 0) {
deepSeekCred = deepSeekCreds.find(c => c.name.toLowerCase().includes('deepseek')) || deepSeekCreds[0];
}
const geminiCreds = credentials.filter(c => c.type === 'googlePalmApi');
if (geminiCreds.length > 0) {
geminiCred = geminiCreds.find(c => c.name.toLowerCase().includes('gemini') || c.name.toLowerCase().includes('google')) || geminiCreds[0];
}
}
} catch (err) {
console.warn("Warning: Could not fetch credentials dynamically:", err.message);
}
// 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);
}
// ==========================================
// TEST CASE 1: SALES IMPORT WORKFLOW
// ==========================================
console.log("\n[Test 1] Deploying and triggering Sales Import Workflow...");
const importWorkflowPath = path.join(__dirname, '../n8n/sales_import_workflow.json');
if (!fs.existsSync(importWorkflowPath)) {
console.error("Error: n8n/sales_import_workflow.json not found.");
process.exit(1);
}
const importWfJson = JSON.parse(fs.readFileSync(importWorkflowPath, 'utf8'));
const importWebhookPath = `calculate-commissions-${Date.now()}`;
// Configure dev app urls and webhooks
for (const node of importWfJson.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 = importWebhookPath;
}
if (node.type === '@n8n/n8n-nodes-langchain.lmChatDeepSeek' && deepSeekCred) {
node.credentials = { deepSeekApi: { id: deepSeekCred.id, name: deepSeekCred.name } };
}
if (node.type === '@n8n/n8n-nodes-langchain.lmChatGoogleGemini' && geminiCred) {
node.credentials = { googlePalmApi: { id: geminiCred.id, name: geminiCred.name } };
}
}
const importWfName = `Semillero E2E Integration: Sales Import - ${Date.now()}`;
const importCreateRes = await fetch(`${n8nUrl}/api/v1/workflows`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-N8N-API-KEY': n8nApiKey },
body: JSON.stringify({
name: importWfName,
nodes: importWfJson.nodes,
connections: importWfJson.connections,
settings: importWfJson.settings || {},
projectId
})
});
if (!importCreateRes.ok) {
console.error("Failed to deploy import workflow:", await importCreateRes.text());
process.exit(1);
}
const importWfData = await importCreateRes.json();
deployedWorkflowIds.push(importWfData.id);
// Activate import workflow
await fetch(`${n8nUrl}/api/v1/workflows/${importWfData.id}/activate`, {
method: 'POST',
headers: { 'X-N8N-API-KEY': n8nApiKey }
});
await sleep(3000);
const importIdempotencyKey = `key-integration-import-${Date.now()}`;
const uploadRes = await fetch(`${n8nUrl}/webhook/${importWebhookPath}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-n8n-signature': process.env.N8N_WEBHOOK_SECRET_DEV || 'qUHuPqPjA65psdtwQU7zgp/DVkvd1xXk2WP/vzbnEdc='
},
body: JSON.stringify({
idempotencyKey: importIdempotencyKey,
uploaderId: 1,
sales: [
{
username: 'colaborador_mde',
hotelCode: 'EST-MDE',
period: '2026-06',
amount: 15000,
salesCount: 5,
transactionId: `TX-INT-COMM-${Date.now()}`
}
]
})
});
assert(uploadRes.status === 200 || uploadRes.status === 202, `Import webhook response code: ${uploadRes.status}`);
const uploadData = await uploadRes.json();
assert(uploadData.success && uploadData.code === 'IMPORT_ACCEPTED', "Import accepted by n8n");
let importCallbackCompleted = false;
for (let i = 0; i < 20; i++) {
await sleep(2000);
const dbSales = await runAsAdmin(tx => tx.salesResult.findMany({
where: { idempotencyKey: { startsWith: importIdempotencyKey } }
}));
if (dbSales.length > 0) {
assert(dbSales.length === 1, "Sales result saved to dev database by n8n callback");
assert(parseFloat(dbSales[0].amount) === 15000.00, "Imported amount verified");
importCallbackCompleted = true;
break;
}
}
assert(importCallbackCompleted, "Sales import integration completed successfully");
// ==========================================
// TEST CASE 2: SETTLEMENT CALCULATION WORKFLOW
// ==========================================
console.log("\n[Test 2] Deploying and triggering Settlement Calculation Workflow...");
const calcWorkflowPath = path.join(__dirname, '../n8n/settlement_calculation_workflow.json');
if (!fs.existsSync(calcWorkflowPath)) {
console.error("Error: n8n/settlement_calculation_workflow.json not found.");
await cleanup();
process.exit(1);
}
const calcWfJson = JSON.parse(fs.readFileSync(calcWorkflowPath, 'utf8'));
const calcWebhookPath = `calculate-settlements-${Date.now()}`;
// Configure dev app urls and webhooks
for (const node of calcWfJson.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 = calcWebhookPath;
}
if (node.type === '@n8n/n8n-nodes-langchain.lmChatDeepSeek' && deepSeekCred) {
node.credentials = { deepSeekApi: { id: deepSeekCred.id, name: deepSeekCred.name } };
}
if (node.type === '@n8n/n8n-nodes-langchain.lmChatGoogleGemini' && geminiCred) {
node.credentials = { googlePalmApi: { id: geminiCred.id, name: geminiCred.name } };
}
}
const calcWfName = `Semillero E2E Integration: Settlement Calc - ${Date.now()}`;
const calcCreateRes = await fetch(`${n8nUrl}/api/v1/workflows`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-N8N-API-KEY': n8nApiKey },
body: JSON.stringify({
name: calcWfName,
nodes: calcWfJson.nodes,
connections: calcWfJson.connections,
settings: calcWfJson.settings || {},
projectId
})
});
if (!calcCreateRes.ok) {
console.error("Failed to deploy calculation workflow:", await calcCreateRes.text());
await cleanup();
process.exit(1);
}
const calcWfData = await calcCreateRes.json();
deployedWorkflowIds.push(calcWfData.id);
// Activate calculation workflow
await fetch(`${n8nUrl}/api/v1/workflows/${calcWfData.id}/activate`, {
method: 'POST',
headers: { 'X-N8N-API-KEY': n8nApiKey }
});
await sleep(3000);
// Seed DB records for the calculation E2E test
console.log("Seeding test database with plan, rules, goal, and sales results for calculation E2E test...");
const adminUser = (await runAsAdmin(tx => tx.user.findMany())).find(u => u.username === 'admin');
const colaboradorMde = (await runAsAdmin(tx => tx.user.findMany())).find(u => u.username === 'colaborador_mde');
const testPlan = await runAsAdmin(async (tx) => {
// Delete existing PLAN-INT-P5 if any
await tx.calculationRule.deleteMany({ where: { plan: { code: 'PLAN-INT-P5' } } });
await tx.compensationPlan.deleteMany({ where: { code: 'PLAN-INT-P5' } });
const plan = await tx.compensationPlan.create({
data: {
name: 'Integration Test Plan Phase 5',
code: 'PLAN-INT-P5',
validityStart: new Date('2026-01-01'),
type: 'SCALE',
status: 'ACTIVE',
version: 1,
createdBy: adminUser.id
}
});
await tx.calculationRule.create({
data: {
planId: plan.id,
type: 'TIER',
minAchievement: 0.0,
maxAchievement: 2.0,
rate: 0.02,
payoutAmount: 0.0
}
});
return plan;
});
// Seed Goal and SalesResult for period 2026-07
await runAsAdmin(async (tx) => {
await tx.goal.deleteMany({ where: { period: '2026-07', targetId: colaboradorMde.id } });
await tx.goal.create({
data: {
targetType: 'INDIVIDUAL',
targetId: colaboradorMde.id,
period: '2026-07',
amount: 10000.00
}
});
await tx.salesResult.deleteMany({ where: { period: '2026-07', userId: colaboradorMde.id } });
await tx.salesResult.create({
data: {
source: 'API',
hotelId: colaboradorMde.hotelId,
userId: colaboradorMde.id,
period: '2026-07',
amount: 5000.00,
salesCount: 2,
idempotencyKey: 'key-integration-sales-p5',
uploadedBy: adminUser.id,
status: 'PENDING'
}
});
});
// Trigger calculation webhook (commit mode)
console.log(`Triggering settlement calculation webhook directly at ${n8nUrl}/webhook/${calcWebhookPath}...`);
const calcRes = await fetch(`${n8nUrl}/webhook/${calcWebhookPath}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-n8n-signature': process.env.N8N_WEBHOOK_SECRET_DEV || 'qUHuPqPjA65psdtwQU7zgp/DVkvd1xXk2WP/vzbnEdc='
},
body: JSON.stringify({
period: '2026-07',
simulateOnly: false,
uploaderId: adminUser.id
})
});
assert(calcRes.status === 200 || calcRes.status === 201, `Calculation webhook response code: ${calcRes.status}`);
const calcData = await calcRes.json();
assert(calcData.success && calcData.code === 'SETTLEMENTS_CALCULATED', "Calculation execution successful");
// Verify settlement created in database
const finalSettlement = await runAsAdmin(tx => tx.settlement.findFirst({
where: {
userId: colaboradorMde.id,
period: '2026-07',
planId: testPlan.id
}
}));
assert(finalSettlement !== null, "Settlement record created in database for colaborador_mde in 2026-07");
if (finalSettlement) {
assert(finalSettlement.status === 'PENDING', "Settlement status is set to PENDING");
assert(parseFloat(finalSettlement.calculatedCommission) === 100.00, "Calculated commission is correctly 100.00 (5,000 * 2%)");
assert(parseFloat(finalSettlement.totalPayout) === 100.00, "Total payout is correctly 100.00");
}
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 all deployed workflows from n8n
for (const id of deployedWorkflowIds) {
if (failed > 0) {
console.log(`[TEST FAILED] Skipping test workflow ${id} deletion to allow troubleshooting in n8n.gaboggamer.online.`);
} else {
console.log(`Deleting n8n test workflow ${id}...`);
try {
const delRes = await fetch(`${n8nUrl}/api/v1/workflows/${id}`, {
method: 'DELETE',
headers: {
'X-N8N-API-KEY': n8nApiKey
}
});
if (delRes.ok) {
console.log(`Test workflow ${id} deleted successfully from n8n.`);
} else {
console.error(`Failed to delete test workflow ${id}:`, await delRes.text());
}
} catch (e) {
console.error(`Error deleting workflow ${id}:`, 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);
failed++;
await cleanup();
process.exit(1);
});