| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450 |
- 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> = {}): 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()
- })
- })
- })
|