315 lines
9.6 KiB
JavaScript
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);
|
|
});
|