test: add database and rls policy verification test suite

This commit is contained in:
Luis Gabriel Ramos Robles 2026-06-11 14:50:12 +00:00
parent f6b8f019cd
commit a2238bd7e3
4 changed files with 8034 additions and 3 deletions

7767
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

33
package.json Normal file
View file

@ -0,0 +1,33 @@
{
"name": "tmp_app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint",
"test:rls": "node prisma/test-rls.js",
"test": "npm run test:rls"
},
"dependencies": {
"@prisma/adapter-pg": "^7.8.0",
"@prisma/client": "^7.8.0",
"forgejo-mcp": "^1.2.0",
"mcp-git": "^0.0.4",
"next": "16.2.9",
"pg": "^8.21.0",
"react": "19.2.4",
"react-dom": "19.2.4"
},
"devDependencies": {
"@types/node": "^20",
"@types/pg": "^8.20.0",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "16.2.9",
"prisma": "^7.8.0",
"typescript": "^5"
}
}

View file

@ -1,4 +1,5 @@
-- RLS Policies and Seeding Script for Hoteles Estelar Variable Remuneration System
SET app.current_user_role = 'ADMIN';
-- ==========================================
-- 1. SEED DATA
@ -85,7 +86,10 @@ CREATE POLICY audit_logs_insert_policy ON "audit_logs"
FOR INSERT WITH CHECK (true);
CREATE POLICY audit_logs_select_policy ON "audit_logs"
FOR SELECT USING (current_setting('app.current_user_role', true) IN ('ADMIN', 'ANALISTA'));
FOR SELECT USING (
current_setting('app.current_user_role', true) IN ('ADMIN', 'ANALISTA')
OR user_id = NULLIF(current_setting('app.current_user_id', true), '')::integer
);
-- B. Regions Policies
@ -115,8 +119,14 @@ CREATE POLICY hotels_modify_policy ON "hotels"
CREATE POLICY users_select_policy ON "users"
FOR SELECT USING (
current_setting('app.current_user_role', true) IN ('ADMIN', 'ANALISTA', 'DIRECTOR')
OR hotel_id = NULLIF(current_setting('app.current_hotel_id', true), '')::integer
OR hotel_id IN (SELECT id FROM hotels WHERE region_id = NULLIF(current_setting('app.current_region_id', true), '')::integer)
OR (
current_setting('app.current_user_role', true) = 'GERENTE'
AND hotel_id = NULLIF(current_setting('app.current_hotel_id', true), '')::integer
)
OR (
current_setting('app.current_user_role', true) = 'LIDER'
AND hotel_id IN (SELECT id FROM hotels WHERE region_id = NULLIF(current_setting('app.current_region_id', true), '')::integer)
)
OR id = NULLIF(current_setting('app.current_user_id', true), '')::integer
);

221
prisma/test-rls.js Normal file
View file

@ -0,0 +1,221 @@
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...");
try {
// Clean up existing sales results to isolate the test
await runWithContext(adminUser, tx => tx.salesResult.deleteMany());
// Create sales results as Admin (bypass RLS)
const 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
}
}));
const 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 and audit logs
console.log("\nCleaning up test records...");
await runWithContext(adminUser, tx => tx.salesResult.deleteMany());
await runWithContext(adminUser, tx => tx.auditLog.deleteMany());
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);
});