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>
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user