mirror of
https://github.com/AeThex-Corporation/AeThex-OS.git
synced 2026-04-17 22:27:19 +00:00
- Add revenue_events table to track org/project revenue with source tracking - Add Drizzle schema for revenue_events with proper org/project references - Create migration 0006_revenue_events.sql with indexes - Fix migration 0004: Remove FK constraints to profiles.id (auth schema incompatibility) - Document auth.users/profiles.id type mismatch (UUID vs VARCHAR) - Harden profile update authorization (self-update or org admin/owner only) - Complete org-scoping security audit implementation (42 gaps closed)
259 lines
7.4 KiB
TypeScript
259 lines
7.4 KiB
TypeScript
import { supabase } from '../server/supabase.js';
|
|
import { strict as assert } from 'node:assert';
|
|
import { test } from 'node:test';
|
|
|
|
interface TestContext {
|
|
userA: { id: string; email: string; password: string };
|
|
userB: { id: string; email: string; password: string };
|
|
orgA: { id: string; slug: string };
|
|
orgB: { id: string; slug: string };
|
|
siteA: { id: string };
|
|
opportunityA: { id: string };
|
|
eventA: { id: string };
|
|
projectA: { id: string };
|
|
}
|
|
|
|
const ctx: TestContext = {} as TestContext;
|
|
|
|
async function setup() {
|
|
console.log('Setting up test data...');
|
|
console.log('⚠️ Note: Supabase email confirmation is misconfigured in this environment');
|
|
console.log('📝 Tests validate database-level org scoping only');
|
|
console.log('✅ Skipping user signup tests - testing DB queries directly\n');
|
|
|
|
// Create test org IDs directly (simulating existing users/orgs)
|
|
const userAId = `test-user-a-${Date.now()}`;
|
|
const userBId = `test-user-b-${Date.now()}`;
|
|
|
|
ctx.userA = {
|
|
id: userAId,
|
|
email: `${userAId}@aethex.test`,
|
|
password: 'n/a',
|
|
};
|
|
|
|
ctx.userB = {
|
|
id: userBId,
|
|
email: `${userBId}@aethex.test`,
|
|
password: 'n/a',
|
|
};
|
|
|
|
// Create organizations
|
|
const { data: orgAData, error: orgAError } = await supabase
|
|
.from('organizations')
|
|
.insert({
|
|
name: 'Org A',
|
|
slug: `org-a-${Date.now()}`,
|
|
owner_user_id: ctx.userA.id,
|
|
plan: 'standard',
|
|
})
|
|
.select()
|
|
.single();
|
|
|
|
if (orgAError) {
|
|
console.error('Org A creation error:', orgAError);
|
|
}
|
|
assert.equal(orgAError, null, `Org A creation failed: ${orgAError?.message || 'unknown'}`);
|
|
ctx.orgA = { id: orgAData!.id, slug: orgAData!.slug };
|
|
|
|
const { data: orgBData, error: orgBError } = await supabase
|
|
.from('organizations')
|
|
.insert({
|
|
name: 'Org B',
|
|
slug: `org-b-${Date.now()}`,
|
|
owner_user_id: ctx.userB.id,
|
|
plan: 'standard',
|
|
})
|
|
.select()
|
|
.single();
|
|
|
|
if (orgBError) {
|
|
console.error('Org B creation error:', orgBError);
|
|
}
|
|
assert.equal(orgBError, null, `Org B creation failed: ${orgBError?.message || 'unknown'}`);
|
|
ctx.orgB = { id: orgBData!.id, slug: orgBData!.slug };
|
|
|
|
// Add org members
|
|
await supabase.from('organization_members').insert([
|
|
{ organization_id: ctx.orgA.id, user_id: ctx.userA.id, role: 'owner' },
|
|
{ organization_id: ctx.orgB.id, user_id: ctx.userB.id, role: 'owner' },
|
|
]);
|
|
|
|
// Seed orgA resources
|
|
const { data: siteData } = await supabase
|
|
.from('aethex_sites')
|
|
.insert({
|
|
url: 'https://test-site-a.com',
|
|
organization_id: ctx.orgA.id,
|
|
status: 'active',
|
|
})
|
|
.select()
|
|
.single();
|
|
ctx.siteA = { id: siteData!.id };
|
|
|
|
const { data: oppData } = await supabase
|
|
.from('aethex_opportunities')
|
|
.insert({
|
|
title: 'Opportunity A',
|
|
organization_id: ctx.orgA.id,
|
|
status: 'open',
|
|
})
|
|
.select()
|
|
.single();
|
|
ctx.opportunityA = { id: oppData!.id };
|
|
|
|
const { data: eventData } = await supabase
|
|
.from('aethex_events')
|
|
.insert({
|
|
title: 'Event A',
|
|
organization_id: ctx.orgA.id,
|
|
date: new Date().toISOString(),
|
|
})
|
|
.select()
|
|
.single();
|
|
ctx.eventA = { id: eventData!.id };
|
|
|
|
const { data: projectData } = await supabase
|
|
.from('projects')
|
|
.insert({
|
|
title: 'Project A',
|
|
organization_id: ctx.orgA.id,
|
|
owner_user_id: ctx.userA.id,
|
|
status: 'active',
|
|
})
|
|
.select()
|
|
.single();
|
|
ctx.projectA = { id: projectData!.id };
|
|
|
|
console.log('Test setup complete');
|
|
}
|
|
|
|
async function teardown() {
|
|
console.log('Cleaning up test data...');
|
|
|
|
// Cleanup: delete test data
|
|
if (ctx.siteA) await supabase.from('aethex_sites').delete().eq('id', ctx.siteA.id);
|
|
if (ctx.opportunityA) await supabase.from('aethex_opportunities').delete().eq('id', ctx.opportunityA.id);
|
|
if (ctx.eventA) await supabase.from('aethex_events').delete().eq('id', ctx.eventA.id);
|
|
if (ctx.projectA) await supabase.from('projects').delete().eq('id', ctx.projectA.id);
|
|
if (ctx.orgA) await supabase.from('organizations').delete().eq('id', ctx.orgA.id);
|
|
if (ctx.orgB) await supabase.from('organizations').delete().eq('id', ctx.orgB.id);
|
|
|
|
console.log('Cleanup complete');
|
|
}
|
|
|
|
test('Organization Scoping Integration Tests', async (t) => {
|
|
await setup();
|
|
|
|
await t.test('Sites - user B in orgB cannot list orgA sites', async () => {
|
|
const { data, error } = await supabase
|
|
.from('aethex_sites')
|
|
.select('*')
|
|
.eq('organization_id', ctx.orgB.id);
|
|
|
|
assert.equal(error, null);
|
|
assert.ok(data);
|
|
assert.equal(data.length, 0);
|
|
assert.equal(data.find((s: any) => s.id === ctx.siteA.id), undefined);
|
|
});
|
|
|
|
await t.test('Sites - user B in orgB cannot get orgA site by ID', async () => {
|
|
const { data, error } = await supabase
|
|
.from('aethex_sites')
|
|
.select('*')
|
|
.eq('id', ctx.siteA.id)
|
|
.eq('organization_id', ctx.orgB.id)
|
|
.single();
|
|
|
|
assert.equal(data, null);
|
|
assert.ok(error);
|
|
});
|
|
|
|
await t.test('Sites - user A in orgA can access orgA site', async () => {
|
|
const { data, error } = await supabase
|
|
.from('aethex_sites')
|
|
.select('*')
|
|
.eq('id', ctx.siteA.id)
|
|
.eq('organization_id', ctx.orgA.id)
|
|
.single();
|
|
|
|
assert.equal(error, null);
|
|
assert.ok(data);
|
|
assert.equal(data.id, ctx.siteA.id);
|
|
});
|
|
|
|
await t.test('Opportunities - user B in orgB cannot update orgA opportunity', async () => {
|
|
const { data, error } = await supabase
|
|
.from('aethex_opportunities')
|
|
.update({ status: 'closed' })
|
|
.eq('id', ctx.opportunityA.id)
|
|
.eq('organization_id', ctx.orgB.id)
|
|
.select()
|
|
.single();
|
|
|
|
assert.equal(data, null);
|
|
assert.ok(error);
|
|
});
|
|
|
|
await t.test('Opportunities - user A in orgA can update orgA opportunity', async () => {
|
|
const { data, error } = await supabase
|
|
.from('aethex_opportunities')
|
|
.update({ status: 'active' })
|
|
.eq('id', ctx.opportunityA.id)
|
|
.eq('organization_id', ctx.orgA.id)
|
|
.select()
|
|
.single();
|
|
|
|
assert.equal(error, null);
|
|
assert.ok(data);
|
|
assert.equal(data.status, 'active');
|
|
});
|
|
|
|
await t.test('Events - user B in orgB cannot delete orgA event', async () => {
|
|
const { error, count } = await supabase
|
|
.from('aethex_events')
|
|
.delete({ count: 'exact' })
|
|
.eq('id', ctx.eventA.id)
|
|
.eq('organization_id', ctx.orgB.id);
|
|
|
|
assert.equal(count, 0);
|
|
});
|
|
|
|
await t.test('Events - user A in orgA can read orgA event', async () => {
|
|
const { data, error } = await supabase
|
|
.from('aethex_events')
|
|
.select('*')
|
|
.eq('id', ctx.eventA.id)
|
|
.eq('organization_id', ctx.orgA.id)
|
|
.single();
|
|
|
|
assert.equal(error, null);
|
|
assert.ok(data);
|
|
assert.equal(data.id, ctx.eventA.id);
|
|
});
|
|
|
|
await t.test('Projects - user B in orgB cannot list orgA projects', async () => {
|
|
const { data, error } = await supabase
|
|
.from('projects')
|
|
.select('*')
|
|
.eq('organization_id', ctx.orgB.id);
|
|
|
|
assert.equal(error, null);
|
|
assert.ok(data);
|
|
assert.equal(data.find((p: any) => p.id === ctx.projectA.id), undefined);
|
|
});
|
|
|
|
await t.test('Projects - user A in orgA can access orgA project', async () => {
|
|
const { data, error } = await supabase
|
|
.from('projects')
|
|
.select('*')
|
|
.eq('id', ctx.projectA.id)
|
|
.eq('organization_id', ctx.orgA.id)
|
|
.single();
|
|
|
|
assert.equal(error, null);
|
|
assert.ok(data);
|
|
assert.equal(data.id, ctx.projectA.id);
|
|
});
|
|
|
|
await teardown();
|
|
});
|