const { spawn } = require('child_process'); const { PrismaClient } = require('@prisma/client'); const { PrismaPg } = require('@prisma/adapter-pg'); const { Pool } = require('pg'); require('dotenv').config(); const PORT = 3009; const BASE_URL = `http://localhost:${PORT}`; let nextProcess; let prisma; let pool; function getPrisma() { if (!prisma) { const dbUrl = new URL(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; } // Helper to run query with admin context 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)); } 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 runTests() { console.log("=== STARTING PHASE 2 AUTH & RLS INTEGRATION TEST SUITE ==="); const db = getPrisma(); // 1. Seed test sales data as Admin for the test queries console.log("\nSeeding test sales results..."); const users = await runAsAdmin(tx => tx.user.findMany()); const colaboradorMde = users.find(u => u.username === 'colaborador_mde'); const gerenteMde = users.find(u => u.username === 'gerente_mde'); const liderCtg = users.find(u => u.username === 'lider_ctg'); const adminUser = users.find(u => u.username === 'admin'); await runAsAdmin(async (tx) => { await tx.salesResult.deleteMany(); await tx.auditLog.deleteMany(); // Colaborador sale await tx.salesResult.create({ data: { source: 'EXCEL', hotelId: colaboradorMde.hotelId, userId: colaboradorMde.id, period: '2026-06', amount: 3000.00, salesCount: 5, idempotencyKey: 'auth-test-colab', uploadedBy: adminUser.id } }); // Lider sale await tx.salesResult.create({ data: { source: 'EXCEL', hotelId: liderCtg.hotelId, userId: liderCtg.id, period: '2026-06', amount: 6000.00, salesCount: 8, idempotencyKey: 'auth-test-lider', uploadedBy: adminUser.id } }); }); // 2. Start Next.js server console.log(`\nStarting Next.js dev server on port ${PORT}...`); nextProcess = spawn('node', ['node_modules/next/dist/bin/next', 'dev', '--port', String(PORT)], { env: { ...process.env, PORT: String(PORT) } }); nextProcess.stdout.on('data', (data) => console.log(`[Next.js] ${data.toString().trim()}`)); nextProcess.stderr.on('data', (data) => console.error(`[Next.js ERR] ${data.toString().trim()}`)); // Wait for server to start up let serverReady = false; for (let i = 0; i < 15; i++) { await sleep(2000); try { await fetch(`${BASE_URL}/api/auth/me`); serverReady = true; break; } catch (e) { // Server not ready yet } } if (!serverReady) { console.error("Error: Next.js dev server failed to start within timeout."); cleanup(); process.exit(1); } console.log("Next.js dev server is ready!"); // --- TEST A: Unauthenticated me query --- console.log("\nRunning TEST A: Unauthenticated /api/auth/me..."); try { const res = await fetch(`${BASE_URL}/api/auth/me`); assert(res.status === 401, "GET /api/auth/me returns 401 Unauthorized when no cookie is set"); } catch (err) { console.error("Test A error:", err); failed++; } // --- TEST B: Authentication with Invalid Credentials --- console.log("\nRunning TEST B: Authentication with Invalid Credentials..."); try { const res = await fetch(`${BASE_URL}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: 'colaborador_mde', password: 'wrongpassword' }) }); assert(res.status === 401, "POST /api/auth/login returns 401 for incorrect password"); } catch (err) { console.error("Test B error:", err); failed++; } // --- TEST C: Authenticate Colaborador and verify RLS --- console.log("\nRunning TEST C: Authenticate Colaborador & Verify RLS..."); try { // 1. Login const loginRes = await fetch(`${BASE_URL}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: 'colaborador_mde', password: 'password123' }) }); if (loginRes.status !== 200) { console.error("Login failed response:", loginRes.status, await loginRes.text()); } assert(loginRes.status === 200, "POST /api/auth/login returns 200 OK for valid Colaborador credentials"); // Save session cookie const setCookie = loginRes.headers.get('set-cookie'); assert(setCookie !== null && setCookie.includes('session='), "Response contains 'session' cookie"); const cookie = setCookie ? setCookie.split(';')[0] : ''; // 2. Query /api/auth/me const meRes = await fetch(`${BASE_URL}/api/auth/me`, { headers: { Cookie: cookie } }); const meData = await meRes.json(); if (meRes.status !== 200) { console.error("GET /api/auth/me failed status:", meRes.status, "body:", meData); } assert( meRes.status === 200 && meData.user && meData.user.username === 'colaborador_mde', "GET /api/auth/me returns user details for active session" ); // 3. Query /api/test-auth (verify RLS) const testAuthRes = await fetch(`${BASE_URL}/api/test-auth`, { headers: { Cookie: cookie } }); const testAuthData = await testAuthRes.json(); assert( testAuthData.users.length === 1 && testAuthData.users[0].username === 'colaborador_mde', "RLS restriction: Colaborador only sees their own user record in queries" ); assert( testAuthData.sales.length === 1 && testAuthData.sales[0].userId === colaboradorMde.id, "RLS restriction: Colaborador only sees their own sales results in queries" ); } catch (err) { console.error("Test C error:", err); failed++; } // --- TEST D: Authenticate Gerente and verify RLS --- console.log("\nRunning TEST D: Authenticate Gerente & Verify RLS..."); try { // 1. Login const loginRes = await fetch(`${BASE_URL}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: 'gerente_mde', password: 'password123' }) }); const setCookie = loginRes.headers.get('set-cookie'); const cookie = setCookie ? setCookie.split(';')[0] : ''; // 2. Query /api/test-auth (verify RLS) const testAuthRes = await fetch(`${BASE_URL}/api/test-auth`, { headers: { Cookie: cookie } }); const testAuthData = await testAuthRes.json(); const mdeUsers = users.filter(u => u.hotelId === gerenteMde.hotelId); const visibleToGerente = testAuthData.users.every(u => u.hotelId === gerenteMde.hotelId); assert( testAuthData.users.length === mdeUsers.length && visibleToGerente, `RLS restriction: Gerente only sees users within their hotel (EST-MDE, count: ${mdeUsers.length})` ); const visibleSales = testAuthData.sales.every(s => s.hotelId === gerenteMde.hotelId); assert( testAuthData.sales.length === 1 && visibleSales, "RLS restriction: Gerente only sees sales results within their hotel" ); } catch (err) { console.error("Test D error:", err); failed++; } // --- TEST E: Authenticate Admin and verify RLS bypass --- console.log("\nRunning TEST E: Authenticate Admin & Verify RLS Bypass..."); try { // 1. Login const loginRes = await fetch(`${BASE_URL}/api/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: 'admin', password: 'password123' }) }); const setCookie = loginRes.headers.get('set-cookie'); const cookie = setCookie ? setCookie.split(';')[0] : ''; // 2. Query /api/test-auth (verify RLS) const testAuthRes = await fetch(`${BASE_URL}/api/test-auth`, { headers: { Cookie: cookie } }); const testAuthData = await testAuthRes.json(); assert( testAuthData.users.length === users.length, "RLS bypass: Admin sees all users in the system" ); assert( testAuthData.sales.length === 2, "RLS bypass: Admin sees all sales results in the system" ); } catch (err) { console.error("Test E error:", err); failed++; } await cleanup(); console.log(`\n=== TEST RESULTS: ${passed} PASSED, ${failed} FAILED ===`); if (failed > 0) { process.exit(1); } else { process.exit(0); } } async function cleanup() { console.log("\nCleaning up test records and shutting down servers..."); try { if (prisma) { await runAsAdmin(async (tx) => { await tx.salesResult.deleteMany(); await tx.auditLog.deleteMany(); }); await prisma.$disconnect(); } if (pool) { await pool.end(); } } catch (e) { console.error("Error during DB cleanup:", e); } if (nextProcess) { nextProcess.kill(); } } // Handle exit gracefully process.on('SIGINT', () => { cleanup(); process.exit(1); }); runTests().catch(err => { console.error("Fatal test runner error:", err); cleanup(); process.exit(1); });