Files
builtbykev 3da1b4242c feat: Feature 1.2 (NBA stats FastAPI service) + Feature 1.4 (database schema)
Feature 1.2: Python FastAPI microservice wrapping nba_api
- GET /stats/season-avg, /stats/last-n, /stats/splits, /players/search
- Redis caching (24hr/1hr/6hr/7day), 0.6s rate limiting, PRA derived stat
- 27 Python tests passing

Feature 1.4: Complete Supabase database schema
- 6 tables: users, picks, scan_sessions, bets, outcomes, performance
- RLS enabled on all tables with auth.uid() policies
- 3 triggers: auto-create user, updated_at, scan count reset
- 37 schema validation tests passing
- Migration SQL ready, pending manual apply (WSL2 DNS blocker)

Total: 92 tests (65 Node.js + 27 Python), all passing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 10:58:58 -04:00

157 lines
5.7 KiB
JavaScript

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);
});
});
});