import { safeSql } from '@supabase/pg-meta' import { beforeEach, describe, expect, it, vi } from 'vitest' import { generateAiPoliciesForTable, generateProgrammaticPoliciesForTable, generateStartingPoliciesForTable, type GeneratedPolicy, } from './Policies.utils' import type { ForeignKeyConstraint } from '@/data/database/foreign-key-constraints-query' // Mock generateSqlPolicy for AI tests const mockGenerateSqlPolicy = vi.fn() vi.mock('@/data/ai/sql-policy-mutation', () => ({ generateSqlPolicy: (...args: unknown[]) => mockGenerateSqlPolicy(...args), })) // Helper to create a foreign key constraint const createForeignKey = (overrides: Partial = {}): ForeignKeyConstraint => ({ id: 1, constraint_name: 'fk_constraint', source_id: 100, source_schema: 'public', source_table: 'posts', source_columns: ['user_id'], target_id: 200, target_schema: 'auth', target_table: 'users', target_columns: ['id'], deletion_action: 'NO ACTION', update_action: 'NO ACTION', ...overrides, }) describe('Policies.utils - Policy Generation', () => { beforeEach(() => { vi.clearAllMocks() }) describe('generateProgrammaticPoliciesForTable', () => { it('should generate 4 CRUD policies for direct FK to auth.users', () => { const foreignKeyConstraints: ForeignKeyConstraint[] = [ createForeignKey({ source_schema: 'public', source_table: 'posts', source_columns: ['user_id'], target_schema: 'auth', target_table: 'users', target_columns: ['id'], }), ] const policies = generateProgrammaticPoliciesForTable({ table: { name: 'posts', schema: 'public' }, foreignKeyConstraints, }) expect(policies).toHaveLength(4) const commands = policies.map((p) => p.command) expect(commands).toContain('SELECT') expect(commands).toContain('INSERT') expect(commands).toContain('UPDATE') expect(commands).toContain('DELETE') }) it('should return empty array when no FK path to auth.users exists', () => { const foreignKeyConstraints: ForeignKeyConstraint[] = [ createForeignKey({ source_schema: 'public', source_table: 'posts', source_columns: ['category_id'], target_schema: 'public', target_table: 'categories', target_columns: ['id'], }), ] const policies = generateProgrammaticPoliciesForTable({ table: { name: 'posts', schema: 'public' }, foreignKeyConstraints, }) expect(policies).toHaveLength(0) }) it('should return empty array when foreignKeyConstraints is empty', () => { const policies = generateProgrammaticPoliciesForTable({ table: { name: 'posts', schema: 'public' }, foreignKeyConstraints: [], }) expect(policies).toHaveLength(0) }) it('should generate policies with EXISTS clause for indirect FK path (2 hops)', () => { // posts -> profiles -> auth.users const foreignKeyConstraints: ForeignKeyConstraint[] = [ createForeignKey({ id: 1, source_schema: 'public', source_table: 'posts', source_columns: ['profile_id'], target_schema: 'public', target_table: 'profiles', target_columns: ['id'], }), createForeignKey({ id: 2, source_schema: 'public', source_table: 'profiles', source_columns: ['user_id'], target_schema: 'auth', target_table: 'users', target_columns: ['id'], }), ] const policies = generateProgrammaticPoliciesForTable({ table: { name: 'posts', schema: 'public' }, foreignKeyConstraints, }) expect(policies).toHaveLength(4) // Check that the expression contains EXISTS for indirect path const selectPolicy = policies.find((p) => p.command === 'SELECT') expect(selectPolicy?.definition).toContain('exists') expect(selectPolicy?.sql).toContain('exists') }) describe('policy structure validation', () => { const foreignKeyConstraints: ForeignKeyConstraint[] = [ createForeignKey({ source_schema: 'public', source_table: 'posts', source_columns: ['user_id'], target_schema: 'auth', target_table: 'users', target_columns: ['id'], }), ] it('should include all required fields in generated policies', () => { const policies = generateProgrammaticPoliciesForTable({ table: { name: 'posts', schema: 'public' }, foreignKeyConstraints, }) for (const policy of policies) { expect(policy).toHaveProperty('name') expect(policy).toHaveProperty('sql') expect(policy).toHaveProperty('command') expect(policy).toHaveProperty('table', 'posts') expect(policy).toHaveProperty('schema', 'public') expect(policy).toHaveProperty('action', 'PERMISSIVE') expect(policy).toHaveProperty('roles') expect(policy.roles).toContain('authenticated') } }) it('SELECT policy should have definition but no check', () => { const policies = generateProgrammaticPoliciesForTable({ table: { name: 'posts', schema: 'public' }, foreignKeyConstraints, }) const selectPolicy = policies.find((p) => p.command === 'SELECT') expect(selectPolicy?.definition).toBeDefined() expect(selectPolicy?.check).toBeUndefined() }) it('DELETE policy should have definition but no check', () => { const policies = generateProgrammaticPoliciesForTable({ table: { name: 'posts', schema: 'public' }, foreignKeyConstraints, }) const deletePolicy = policies.find((p) => p.command === 'DELETE') expect(deletePolicy?.definition).toBeDefined() expect(deletePolicy?.check).toBeUndefined() }) it('INSERT policy should have check but no definition', () => { const policies = generateProgrammaticPoliciesForTable({ table: { name: 'posts', schema: 'public' }, foreignKeyConstraints, }) const insertPolicy = policies.find((p) => p.command === 'INSERT') expect(insertPolicy?.definition).toBeUndefined() expect(insertPolicy?.check).toBeDefined() }) it('UPDATE policy should have both definition and check', () => { const policies = generateProgrammaticPoliciesForTable({ table: { name: 'posts', schema: 'public' }, foreignKeyConstraints, }) const updatePolicy = policies.find((p) => p.command === 'UPDATE') expect(updatePolicy?.definition).toBeDefined() expect(updatePolicy?.check).toBeDefined() }) it('should generate correct SQL syntax for direct FK', () => { const policies = generateProgrammaticPoliciesForTable({ table: { name: 'posts', schema: 'public' }, foreignKeyConstraints, }) const selectPolicy = policies.find((p) => p.command === 'SELECT') expect(selectPolicy?.sql).toContain('CREATE POLICY') expect(selectPolicy?.sql).toContain('public.posts') expect(selectPolicy?.sql).toContain('AS PERMISSIVE FOR SELECT') expect(selectPolicy?.sql).toContain('TO authenticated') expect(selectPolicy?.sql).toContain('USING') expect(selectPolicy?.sql).toContain('auth.uid()') }) }) it('should handle non-public schema', () => { const foreignKeyConstraints: ForeignKeyConstraint[] = [ createForeignKey({ source_schema: 'private', source_table: 'documents', source_columns: ['owner_id'], target_schema: 'auth', target_table: 'users', target_columns: ['id'], }), ] const policies = generateProgrammaticPoliciesForTable({ table: { name: 'documents', schema: 'private' }, foreignKeyConstraints, }) expect(policies).toHaveLength(4) expect(policies[0].schema).toBe('private') expect(policies[0].sql).toContain('private.documents') }) }) describe('generateAiPoliciesForTable', () => { const mockAiPolicies: GeneratedPolicy[] = [ { name: 'ai_select_policy', sql: 'CREATE POLICY "ai_select_policy" ON public.posts FOR SELECT USING (true);', command: 'SELECT', table: 'posts', schema: 'public', definition: safeSql`true`, action: 'PERMISSIVE', roles: ['public'], }, ] it('should return policies from AI when called with valid inputs', async () => { mockGenerateSqlPolicy.mockResolvedValue(mockAiPolicies) const policies = await generateAiPoliciesForTable({ table: { name: 'posts', schema: 'public' }, columns: [{ name: 'id' }, { name: 'title' }], projectRef: 'test-project', connectionString: 'postgresql://localhost:5432/test', }) expect(mockGenerateSqlPolicy).toHaveBeenCalledWith({ tableName: 'posts', schema: 'public', columns: ['id', 'title'], projectRef: 'test-project', connectionString: 'postgresql://localhost:5432/test', }) expect(policies).toEqual(mockAiPolicies) }) it('should return empty array when connectionString is null', async () => { const policies = await generateAiPoliciesForTable({ table: { name: 'posts', schema: 'public' }, columns: [{ name: 'id' }], projectRef: 'test-project', connectionString: null, }) expect(mockGenerateSqlPolicy).not.toHaveBeenCalled() expect(policies).toEqual([]) }) it('should return empty array when connectionString is undefined', async () => { const policies = await generateAiPoliciesForTable({ table: { name: 'posts', schema: 'public' }, columns: [{ name: 'id' }], projectRef: 'test-project', connectionString: undefined, }) expect(mockGenerateSqlPolicy).not.toHaveBeenCalled() expect(policies).toEqual([]) }) it('should handle API errors gracefully and return empty array', async () => { const consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) mockGenerateSqlPolicy.mockRejectedValue(new Error('API error')) const policies = await generateAiPoliciesForTable({ table: { name: 'posts', schema: 'public' }, columns: [{ name: 'id' }], projectRef: 'test-project', connectionString: 'postgresql://localhost:5432/test', }) expect(policies).toEqual([]) expect(consoleLogSpy).toHaveBeenCalledWith('AI policy generation failed:', expect.any(Error)) consoleLogSpy.mockRestore() }) it('should trim column names before sending to API', async () => { mockGenerateSqlPolicy.mockResolvedValue([]) await generateAiPoliciesForTable({ table: { name: 'posts', schema: 'public' }, columns: [{ name: ' id ' }, { name: ' title ' }], projectRef: 'test-project', connectionString: 'postgresql://localhost:5432/test', }) expect(mockGenerateSqlPolicy).toHaveBeenCalledWith( expect.objectContaining({ columns: ['id', 'title'], }) ) }) }) describe('generateStartingPoliciesForTable', () => { const mockAiPolicies: GeneratedPolicy[] = [ { name: 'ai_policy', sql: 'CREATE POLICY "ai_policy" ON public.posts FOR SELECT USING (true);', command: 'SELECT', table: 'posts', schema: 'public', definition: safeSql`true`, action: 'PERMISSIVE', roles: ['public'], }, ] it('should use programmatic policies when FK path exists (does not call AI)', async () => { const foreignKeyConstraints: ForeignKeyConstraint[] = [ createForeignKey({ source_schema: 'public', source_table: 'posts', source_columns: ['user_id'], target_schema: 'auth', target_table: 'users', target_columns: ['id'], }), ] const policies = await generateStartingPoliciesForTable({ table: { name: 'posts', schema: 'public' }, foreignKeyConstraints, columns: [{ name: 'id' }], projectRef: 'test-project', connectionString: 'postgresql://localhost:5432/test', enableAi: true, }) expect(policies).toHaveLength(4) expect(mockGenerateSqlPolicy).not.toHaveBeenCalled() }) it('should fall back to AI when no FK path exists and enableAi is true', async () => { mockGenerateSqlPolicy.mockResolvedValue(mockAiPolicies) const policies = await generateStartingPoliciesForTable({ table: { name: 'posts', schema: 'public' }, foreignKeyConstraints: [], columns: [{ name: 'id' }], projectRef: 'test-project', connectionString: 'postgresql://localhost:5432/test', enableAi: true, }) expect(mockGenerateSqlPolicy).toHaveBeenCalled() expect(policies).toEqual(mockAiPolicies) }) it('should return empty array when no FK path exists and enableAi is false', async () => { const policies = await generateStartingPoliciesForTable({ table: { name: 'posts', schema: 'public' }, foreignKeyConstraints: [], columns: [{ name: 'id' }], projectRef: 'test-project', connectionString: 'postgresql://localhost:5432/test', enableAi: false, }) expect(mockGenerateSqlPolicy).not.toHaveBeenCalled() expect(policies).toEqual([]) }) it('should return empty array when no FK path and AI returns empty', async () => { mockGenerateSqlPolicy.mockResolvedValue([]) const policies = await generateStartingPoliciesForTable({ table: { name: 'posts', schema: 'public' }, foreignKeyConstraints: [], columns: [{ name: 'id' }], projectRef: 'test-project', connectionString: 'postgresql://localhost:5432/test', enableAi: true, }) expect(policies).toEqual([]) }) it('should prioritize programmatic over AI even when both could generate policies', async () => { mockGenerateSqlPolicy.mockResolvedValue(mockAiPolicies) const foreignKeyConstraints: ForeignKeyConstraint[] = [ createForeignKey({ source_schema: 'public', source_table: 'posts', source_columns: ['user_id'], target_schema: 'auth', target_table: 'users', target_columns: ['id'], }), ] const policies = await generateStartingPoliciesForTable({ table: { name: 'posts', schema: 'public' }, foreignKeyConstraints, columns: [{ name: 'id' }], projectRef: 'test-project', connectionString: 'postgresql://localhost:5432/test', enableAi: true, }) // Should return 4 programmatic policies, not 1 AI policy expect(policies).toHaveLength(4) expect(mockGenerateSqlPolicy).not.toHaveBeenCalled() }) }) })