semillero-special-hotel/prisma/test-auth-rls.js

315 lines
9.6 KiB
JavaScript

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