39947-vm/backend/test/tenant-isolation.test.js
2026-05-10 21:24:49 +00:00

268 lines
8.1 KiB
JavaScript

const assert = require('assert');
const axios = require('axios');
const api = axios.create({
baseURL: 'http://127.0.0.1:3000/api',
validateStatus: () => true,
});
const SUPER_ADMIN = {
email: 'super_admin@flatlogic.com',
password: '252a3d31',
};
function decodeUserId(token) {
const payload = token.split('.')[1];
return JSON.parse(Buffer.from(payload, 'base64url').toString('utf8')).user.id;
}
async function signIn(credentials) {
const response = await api.post('/auth/signin/local', credentials);
assert.strictEqual(response.status, 200, `Expected signin to succeed for ${credentials.email}, got ${response.status}: ${response.data}`);
return response.data;
}
async function sql(token, sqlQuery) {
const response = await api.post(
'/sql',
{ sql: sqlQuery },
{
headers: {
Authorization: `Bearer ${token}`,
},
},
);
assert.strictEqual(response.status, 200, `Expected SQL helper to succeed, got ${response.status}: ${JSON.stringify(response.data)}`);
return response.data.rows;
}
async function createOrganization(token, name) {
const response = await api.post(
'/organizations',
{ data: { name } },
{
headers: {
Authorization: `Bearer ${token}`,
},
},
);
assert.strictEqual(response.status, 200, `Expected organization creation to succeed, got ${response.status}: ${response.data}`);
const rows = await sql(token, `select id from organizations where name = '${name.replace(/'/g, "''")}' order by "createdAt" desc limit 1`);
assert.ok(rows[0]?.id, 'Expected organization lookup to return an id');
return rows[0].id;
}
async function createTenant(token, { name, subdomain, organizationsId }) {
const response = await api.post(
'/tenants',
{
data: {
name,
subdomain,
organizations: organizationsId,
default_language: 'fr',
primary_currency: 'USD',
},
},
{
headers: {
Authorization: `Bearer ${token}`,
},
},
);
assert.strictEqual(response.status, 200, `Expected tenant creation to succeed, got ${response.status}: ${response.data}`);
}
async function promoteUser(token, { userId, email, organizationsId, roleId }) {
const response = await api.put(
`/users/${userId}`,
{
id: userId,
data: {
email,
firstName: email.split('@')[0],
emailVerified: true,
app_role: roleId,
organizations: organizationsId,
},
},
{
headers: {
Authorization: `Bearer ${token}`,
},
},
);
assert.strictEqual(response.status, 200, `Expected user promotion to succeed, got ${response.status}: ${response.data}`);
}
async function signupTenantUser({ subdomain, organizationsId, email, roleId, superAdminToken }) {
const signupResponse = await api.post(
'/auth/signup',
{
email,
password: 'demo1234',
organizationsId,
},
{
headers: {
Host: `${subdomain}.example.com`,
Referer: `http://${subdomain}.example.com/register`,
},
},
);
assert.strictEqual(signupResponse.status, 200, `Expected tenant signup to succeed, got ${signupResponse.status}: ${signupResponse.data}`);
const userId = decodeUserId(signupResponse.data);
await promoteUser(superAdminToken, {
userId,
email,
organizationsId,
roleId,
});
const rows = await sql(superAdminToken, `select "organizationsId" from users where id = '${userId}'`);
assert.strictEqual(rows[0].organizationsId, organizationsId, 'Expected signup to persist the resolved organizationsId');
return signIn({ email, password: 'demo1234' });
}
function buildContactPayload(suffix) {
return {
full_name: 'Jean Test',
company_name: `Company ${suffix}`,
email: `lead-${suffix}@company.com`,
phone: '+243800000000',
service_line: 'cloud',
company_size: '11_50',
budget_range: '20_50k',
preferred_channel: 'email',
urgency: 'urgent',
message: 'Nous avons besoin d\'une migration cloud sécurisée et bien cadrée.',
};
}
describe('tenant isolation and tenant-scoped public intake', function () {
this.timeout(120000);
it('rejects tenant discovery and contact intake when no valid tenant context is present', async () => {
const orgResponse = await api.get('/org-for-auth');
assert.strictEqual(orgResponse.status, 400);
assert.match(String(orgResponse.data), /tenant subdomain/i);
const intakeResponse = await api.post('/contact-form', buildContactPayload('unknown'), {
headers: {
Host: 'unknown.example.com',
Referer: 'http://unknown.example.com/',
},
});
assert.strictEqual(intakeResponse.status, 400);
assert.match(String(intakeResponse.data), /No tenant is configured/i);
});
it('prevents cross-organization read, update, delete, and direct SQL access by id', async () => {
const superAdminToken = await signIn(SUPER_ADMIN);
const timestamp = Date.now();
const adminRoleRows = await sql(superAdminToken, "select id from roles where name = 'Administrator' limit 1");
const adminRoleId = adminRoleRows[0].id;
const orgA = await createOrganization(superAdminToken, `Isolation Org A ${timestamp}`);
const orgB = await createOrganization(superAdminToken, `Isolation Org B ${timestamp}`);
const subA = `isolation-a-${timestamp}`;
const subB = `isolation-b-${timestamp}`;
await createTenant(superAdminToken, {
name: `Isolation Tenant A ${timestamp}`,
subdomain: subA,
organizationsId: orgA,
});
await createTenant(superAdminToken, {
name: `Isolation Tenant B ${timestamp}`,
subdomain: subB,
organizationsId: orgB,
});
const tenantOrgResponse = await api.get('/org-for-auth', {
headers: {
Host: `${subA}.example.com`,
},
});
assert.strictEqual(tenantOrgResponse.status, 200);
assert.strictEqual(tenantOrgResponse.data[0].id, orgA);
const userAToken = await signupTenantUser({
subdomain: subA,
organizationsId: orgA,
email: `tenant-a-${timestamp}@example.com`,
roleId: adminRoleId,
superAdminToken,
});
const userBToken = await signupTenantUser({
subdomain: subB,
organizationsId: orgB,
email: `tenant-b-${timestamp}@example.com`,
roleId: adminRoleId,
superAdminToken,
});
const intakeResponse = await api.post('/contact-form', buildContactPayload(timestamp), {
headers: {
Host: `${subA}.example.com`,
Referer: `http://${subA}.example.com/`,
},
});
assert.strictEqual(intakeResponse.status, 200, JSON.stringify(intakeResponse.data));
const { leadId } = intakeResponse.data;
assert.ok(leadId, 'Expected contact form to create a lead');
const readOwnLead = await api.get(`/leads/${leadId}`, {
headers: {
Authorization: `Bearer ${userAToken}`,
},
});
assert.strictEqual(readOwnLead.status, 200);
assert.strictEqual(readOwnLead.data.id, leadId);
const readOtherLead = await api.get(`/leads/${leadId}`, {
headers: {
Authorization: `Bearer ${userBToken}`,
},
});
assert.strictEqual(readOtherLead.status, 404);
const updateOtherLead = await api.put(
`/leads/${leadId}`,
{
id: leadId,
data: {
status: 'new',
},
},
{
headers: {
Authorization: `Bearer ${userBToken}`,
},
},
);
assert.ok([400, 404].includes(updateOtherLead.status), `Expected cross-org update to fail, got ${updateOtherLead.status}`);
const deleteOtherLead = await api.delete(`/leads/${leadId}`, {
headers: {
Authorization: `Bearer ${userBToken}`,
},
});
assert.strictEqual(deleteOtherLead.status, 404);
const visibleRowsForOwner = await sql(userAToken, `select id from leads where id = '${leadId}'`);
assert.strictEqual(visibleRowsForOwner.length, 1, 'Expected owner SQL query to return the lead');
const visibleRowsForOtherOrg = await sql(userBToken, `select id from leads where id = '${leadId}'`);
assert.strictEqual(visibleRowsForOtherOrg.length, 0, 'Expected RLS to hide the lead from another organization');
});
});