const fs = require('fs'); const path = require('path'); const migrationPath = path.join(__dirname, '../../supabase/migrations/001_initial_schema.sql'); const sql = fs.readFileSync(migrationPath, 'utf8'); describe('Database Schema (001_initial_schema.sql)', () => { describe('Table definitions', () => { const expectedTables = ['users', 'picks', 'scan_sessions', 'bets', 'outcomes', 'performance']; test.each(expectedTables)('defines CREATE TABLE for %s', (table) => { const regex = new RegExp(`CREATE TABLE public\\.${table}\\s*\\(`, 'i'); expect(sql).toMatch(regex); }); }); describe('RLS enabled', () => { const tables = ['users', 'picks', 'scan_sessions', 'bets', 'outcomes', 'performance']; test.each(tables)('enables RLS on %s', (table) => { expect(sql).toContain(`ALTER TABLE public.${table} ENABLE ROW LEVEL SECURITY`); }); }); describe('RLS policies', () => { test('users has select, update, insert policies', () => { expect(sql).toContain('CREATE POLICY "users_select_own"'); expect(sql).toContain('CREATE POLICY "users_update_own"'); expect(sql).toContain('CREATE POLICY "users_insert_own"'); }); test('picks has select and insert policies', () => { expect(sql).toContain('CREATE POLICY "picks_select_own"'); expect(sql).toContain('CREATE POLICY "picks_insert_own"'); }); test('scan_sessions has select and insert policies', () => { expect(sql).toContain('CREATE POLICY "scan_sessions_select_own"'); expect(sql).toContain('CREATE POLICY "scan_sessions_insert_own"'); }); test('bets has select, insert, and update policies', () => { expect(sql).toContain('CREATE POLICY "bets_select_own"'); expect(sql).toContain('CREATE POLICY "bets_insert_own"'); expect(sql).toContain('CREATE POLICY "bets_update_own"'); }); test('outcomes has select policy with picks join', () => { expect(sql).toContain('CREATE POLICY "outcomes_select_own"'); expect(sql).toContain('picks.user_id = auth.uid()'); }); test('performance has select policy', () => { expect(sql).toContain('CREATE POLICY "performance_select_own"'); }); test('all policies use auth.uid()', () => { const policyBlocks = sql.match(/CREATE POLICY[\s\S]*?;/g) || []; for (const block of policyBlocks) { expect(block).toContain('auth.uid()'); } }); }); describe('Constraints', () => { test('users.tier constrained to free/analyst/desk', () => { expect(sql).toMatch(/tier.*CHECK.*\(tier IN \('free',\s*'analyst',\s*'desk'\)\)/); }); test('picks.grade constrained to A/B/C/D', () => { expect(sql).toMatch(/grade.*CHECK.*\(grade IN \('A',\s*'B',\s*'C',\s*'D'\)\)/); }); test('picks.direction constrained to over/under', () => { expect(sql).toMatch(/direction.*CHECK.*\(direction IN \('over',\s*'under'\)\)/); }); test('bets.bet_type constrained', () => { expect(sql).toMatch(/bet_type.*CHECK.*\(bet_type IN/); }); test('bets.status constrained', () => { expect(sql).toMatch(/status.*CHECK.*\(status IN/); }); test('outcomes.result constrained to hit/miss/push', () => { expect(sql).toMatch(/result.*CHECK.*\(result IN \('hit',\s*'miss',\s*'push'\)\)/); }); test('performance.period constrained', () => { expect(sql).toMatch(/period.*CHECK.*\(period IN/); }); }); describe('Indexes', () => { test('picks has user_id and created_at indexes', () => { expect(sql).toContain('CREATE INDEX idx_picks_user_id'); expect(sql).toContain('CREATE INDEX idx_picks_created_at'); }); test('bets has user_id, status, and placed_at indexes', () => { expect(sql).toContain('CREATE INDEX idx_bets_user_id'); expect(sql).toContain('CREATE INDEX idx_bets_status'); expect(sql).toContain('CREATE INDEX idx_bets_placed_at'); }); test('outcomes has unique index on pick_id', () => { expect(sql).toContain('CREATE UNIQUE INDEX idx_outcomes_pick_id'); }); test('performance has unique index on user_id + period', () => { expect(sql).toContain('CREATE UNIQUE INDEX idx_performance_user_period'); }); }); describe('Foreign keys', () => { test('users references auth.users with CASCADE', () => { expect(sql).toMatch(/users[\s\S]*?REFERENCES auth\.users\(id\) ON DELETE CASCADE/); }); test('picks references users with CASCADE', () => { expect(sql).toMatch(/picks[\s\S]*?REFERENCES public\.users\(id\) ON DELETE CASCADE/); }); test('outcomes references picks with CASCADE', () => { expect(sql).toMatch(/outcomes[\s\S]*?REFERENCES public\.picks\(id\) ON DELETE CASCADE/); }); }); describe('Triggers', () => { test('handle_new_user trigger exists', () => { expect(sql).toContain('CREATE OR REPLACE FUNCTION public.handle_new_user()'); expect(sql).toContain('CREATE TRIGGER on_auth_user_created'); expect(sql).toContain('AFTER INSERT ON auth.users'); }); test('updated_at trigger exists', () => { expect(sql).toContain('CREATE OR REPLACE FUNCTION public.update_updated_at()'); expect(sql).toContain('CREATE TRIGGER users_updated_at'); expect(sql).toContain('BEFORE UPDATE ON public.users'); }); test('scan count reset trigger exists', () => { expect(sql).toContain('CREATE OR REPLACE FUNCTION public.reset_scan_count()'); expect(sql).toContain('CREATE TRIGGER users_scan_reset'); }); }); describe('Timestamps', () => { test('all timestamp columns use TIMESTAMPTZ', () => { // Ensure no plain TIMESTAMP without TZ const timestampMatches = sql.match(/\bTIMESTAMP\b(?!TZ)/gi) || []; expect(timestampMatches.length).toBe(0); }); }); });