231 lines
7.6 KiB
JavaScript
231 lines
7.6 KiB
JavaScript
require('dotenv').config();
|
|
const { PrismaClient } = require('@prisma/client');
|
|
const { PrismaPg } = require('@prisma/adapter-pg');
|
|
const { Pool } = require('pg');
|
|
|
|
const dbUrl = new URL(process.env.DATABASE_URL);
|
|
const 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);
|
|
const prisma = new PrismaClient({ adapter });
|
|
|
|
async function runTests() {
|
|
console.log("=== STARTING RLS AND SEED TEST SUITE ===");
|
|
|
|
// 1. Fetch seeded data to map IDs (Run as ADMIN to bypass RLS)
|
|
const { users, hotels, regions } = await prisma.$transaction(async (tx) => {
|
|
await tx.$executeRawUnsafe(`SET LOCAL app.current_user_role = 'admin';`);
|
|
const users = await tx.user.findMany({ include: { hotel: true } });
|
|
const hotels = await tx.hotel.findMany();
|
|
const regions = await tx.region.findMany();
|
|
return { users, hotels, regions };
|
|
});
|
|
|
|
const adminUser = users.find(u => u.username === 'admin');
|
|
const gerenteMde = users.find(u => u.username === 'gerente_mde');
|
|
const liderCtg = users.find(u => u.username === 'lider_ctg');
|
|
const colaboradorMde = users.find(u => u.username === 'colaborador_mde');
|
|
|
|
if (!adminUser || !gerenteMde || !liderCtg || !colaboradorMde) {
|
|
console.error("Error: Could not find all required seeded users. Run database seeding first.");
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(`Seeded users found:
|
|
- Admin ID: ${adminUser.id}
|
|
- Gerente MDE ID: ${gerenteMde.id} (Hotel ID: ${gerenteMde.hotelId})
|
|
- Lider CTG ID: ${liderCtg.id} (Hotel ID: ${liderCtg.hotelId})
|
|
- Colaborador MDE ID: ${colaboradorMde.id} (Hotel ID: ${colaboradorMde.hotelId})`);
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
function assert(condition, message) {
|
|
if (condition) {
|
|
console.log(` ✓ PASS: ${message}`);
|
|
passed++;
|
|
} else {
|
|
console.error(` ✗ FAIL: ${message}`);
|
|
failed++;
|
|
}
|
|
}
|
|
|
|
// Helper to run query with context
|
|
async function runWithContext(user, queryFn) {
|
|
return prisma.$transaction(async (tx) => {
|
|
if (user) {
|
|
await tx.$executeRawUnsafe(`SET LOCAL app.current_user_id = '${user.id}';`);
|
|
await tx.$executeRawUnsafe(`SET LOCAL app.current_user_role = '${user.role}';`);
|
|
await tx.$executeRawUnsafe(`SET LOCAL app.current_hotel_id = '${user.hotelId}';`);
|
|
await tx.$executeRawUnsafe(`SET LOCAL app.current_region_id = '${user.hotel.regionId}';`);
|
|
} else {
|
|
// Clear context
|
|
await tx.$executeRawUnsafe(`RESET ALL;`);
|
|
}
|
|
return queryFn(tx);
|
|
});
|
|
}
|
|
|
|
// --- TEST 1: User Table Read Restrictions ---
|
|
console.log("\nRunning TEST 1: User Table Read Restrictions...");
|
|
try {
|
|
// Colaborador should only see their own user record
|
|
const colabUsers = await runWithContext(colaboradorMde, tx => tx.user.findMany());
|
|
assert(
|
|
colabUsers.length === 1 && colabUsers[0].id === colaboradorMde.id,
|
|
"Colaborador can only select their own user record"
|
|
);
|
|
|
|
// Gerente should see all users in their hotel
|
|
const gerenteUsers = await runWithContext(gerenteMde, tx => tx.user.findMany());
|
|
const allInMdeHotel = users.filter(u => u.hotelId === gerenteMde.hotelId);
|
|
const visibleToGerenteInMde = gerenteUsers.every(u => u.hotelId === gerenteMde.hotelId);
|
|
assert(
|
|
gerenteUsers.length === allInMdeHotel.length && visibleToGerenteInMde,
|
|
`Gerente MDE sees exactly ${allInMdeHotel.length} users in their hotel and no others`
|
|
);
|
|
|
|
// Admin should see all users
|
|
const adminUsers = await runWithContext(adminUser, tx => tx.user.findMany());
|
|
assert(
|
|
adminUsers.length === users.length,
|
|
"Admin sees all users in the system"
|
|
);
|
|
} catch (err) {
|
|
console.error("Error in TEST 1:", err);
|
|
failed++;
|
|
}
|
|
|
|
// --- TEST 2: Sales Results RLS ---
|
|
console.log("\nRunning TEST 2: Sales Results RLS...");
|
|
let saleColab, saleOther;
|
|
try {
|
|
// Clean up test sales results first (non-destructively)
|
|
await runWithContext(adminUser, tx => tx.salesResult.deleteMany({
|
|
where: { idempotencyKey: { in: ['test-key-colab-1', 'test-key-other-1'] } }
|
|
}));
|
|
|
|
// Create sales results as Admin (bypass RLS)
|
|
saleColab = await runWithContext(adminUser, tx => tx.salesResult.create({
|
|
data: {
|
|
source: 'EXCEL',
|
|
hotelId: colaboradorMde.hotelId,
|
|
userId: colaboradorMde.id,
|
|
period: '2026-06',
|
|
amount: 5000.00,
|
|
salesCount: 10,
|
|
idempotencyKey: 'test-key-colab-1',
|
|
uploadedBy: adminUser.id
|
|
}
|
|
}));
|
|
|
|
saleOther = await runWithContext(adminUser, tx => tx.salesResult.create({
|
|
data: {
|
|
source: 'EXCEL',
|
|
hotelId: liderCtg.hotelId,
|
|
userId: liderCtg.id,
|
|
period: '2026-06',
|
|
amount: 8000.00,
|
|
salesCount: 15,
|
|
idempotencyKey: 'test-key-other-1',
|
|
uploadedBy: adminUser.id
|
|
}
|
|
}));
|
|
|
|
// Colaborador queries sales results
|
|
const colabSales = await runWithContext(colaboradorMde, tx => tx.salesResult.findMany());
|
|
assert(
|
|
colabSales.length === 1 && colabSales[0].id === saleColab.id,
|
|
"Colaborador can only select their own sales results"
|
|
);
|
|
|
|
// Gerente queries sales results
|
|
const gerenteSales = await runWithContext(gerenteMde, tx => tx.salesResult.findMany());
|
|
assert(
|
|
gerenteSales.length === 1 && gerenteSales[0].id === saleColab.id,
|
|
"Gerente can only select sales results for their hotel"
|
|
);
|
|
|
|
// Admin queries sales results
|
|
const adminSales = await runWithContext(adminUser, tx => tx.salesResult.findMany());
|
|
assert(
|
|
adminSales.length === 2,
|
|
"Admin can select all sales results"
|
|
);
|
|
} catch (err) {
|
|
console.error("Error in TEST 2:", err);
|
|
failed++;
|
|
}
|
|
|
|
// --- TEST 3: Audit Log Immutability ---
|
|
console.log("\nRunning TEST 3: Audit Log Immutability...");
|
|
try {
|
|
// Create audit log entry
|
|
const log = await runWithContext(adminUser, tx => tx.auditLog.create({
|
|
data: {
|
|
userId: adminUser.id,
|
|
action: 'CREATE',
|
|
targetTable: 'compensation_plans',
|
|
targetId: 1,
|
|
ipAddress: '127.0.0.1'
|
|
}
|
|
}));
|
|
|
|
// Try to update audit log as Admin
|
|
let updateFailed = false;
|
|
try {
|
|
await runWithContext(adminUser, tx => tx.auditLog.update({
|
|
where: { id: log.id },
|
|
data: { ipAddress: '8.8.8.8' }
|
|
}));
|
|
} catch (e) {
|
|
updateFailed = true;
|
|
}
|
|
assert(updateFailed, "AuditLog update is blocked by RLS even for admin");
|
|
|
|
// Try to delete audit log as Admin
|
|
let deleteFailed = false;
|
|
try {
|
|
await runWithContext(adminUser, tx => tx.auditLog.delete({
|
|
where: { id: log.id }
|
|
}));
|
|
} catch (e) {
|
|
deleteFailed = true;
|
|
}
|
|
assert(deleteFailed, "AuditLog delete is blocked by RLS even for admin");
|
|
|
|
} catch (err) {
|
|
console.error("Error in TEST 3:", err);
|
|
failed++;
|
|
}
|
|
|
|
// Clean up test sales results
|
|
console.log("\nCleaning up test records...");
|
|
const salesResultIds = [];
|
|
if (saleColab) salesResultIds.push(saleColab.id);
|
|
if (saleOther) salesResultIds.push(saleOther.id);
|
|
if (salesResultIds.length > 0) {
|
|
await runWithContext(adminUser, tx => tx.salesResult.deleteMany({
|
|
where: { id: { in: salesResultIds } }
|
|
}));
|
|
}
|
|
// Audit logs are not deleted due to write-only RLS immutability
|
|
|
|
console.log(`\n=== TEST RESULTS: ${passed} PASSED, ${failed} FAILED ===`);
|
|
if (failed > 0) {
|
|
process.exit(1);
|
|
} else {
|
|
process.exit(0);
|
|
}
|
|
}
|
|
|
|
runTests().catch(err => {
|
|
console.error("Fatal test runner error:", err);
|
|
process.exit(1);
|
|
});
|