/** * Tests for SelfhostedSupabaseClient * * These tests verify the core client functionality including: * - Client initialization and validation * - SQL execution via RPC * - SQL execution via direct pg connection * - Transaction handling * - Getter methods */ import { describe, test, expect, mock, beforeEach, spyOn } from 'bun:test'; import { SelfhostedSupabaseClient } from '../../client/index.js'; import type { SelfhostedSupabaseClientOptions } from '../../types/index.js'; // Mock the external dependencies const mockSupabaseClient = { rpc: mock(() => Promise.resolve({ data: [], error: null })), }; const mockCreateClient = mock(() => mockSupabaseClient); // Mock @supabase/supabase-js mock.module('@supabase/supabase-js', () => ({ createClient: mockCreateClient, })); // Mock pg Pool const mockPoolClient = { query: mock(() => Promise.resolve({ rows: [] })), release: mock(() => {}), }; const mockPool = { connect: mock(() => Promise.resolve(mockPoolClient)), end: mock(() => Promise.resolve()), on: mock(() => {}), }; const mockPoolConstructor = mock(() => mockPool); mock.module('pg', () => ({ Pool: mockPoolConstructor, })); describe('SelfhostedSupabaseClient', () => { const validOptions: SelfhostedSupabaseClientOptions = { supabaseUrl: 'https://test.supabase.co', supabaseAnonKey: 'test-anon-key', supabaseServiceRoleKey: 'test-service-role-key', databaseUrl: 'postgresql://postgres:postgres@localhost:5432/postgres', jwtSecret: 'test-jwt-secret', }; beforeEach(() => { // Reset all mocks mockCreateClient.mockClear(); mockSupabaseClient.rpc.mockClear(); mockPool.connect.mockClear(); mockPool.end.mockClear(); mockPoolClient.query.mockClear(); mockPoolClient.release.mockClear(); // Reset to default successful behavior mockSupabaseClient.rpc.mockImplementation(() => Promise.resolve({ data: [], error: null }) ); mockPoolClient.query.mockImplementation(() => Promise.resolve({ rows: [] }) ); }); describe('create() factory method', () => { test('creates client with valid options', async () => { const client = await SelfhostedSupabaseClient.create(validOptions); expect(client).toBeDefined(); expect(mockCreateClient).toHaveBeenCalledWith( validOptions.supabaseUrl, validOptions.supabaseAnonKey, undefined ); }); test('throws error when supabaseUrl is missing', async () => { const invalidOptions = { ...validOptions, supabaseUrl: '', }; await expect(SelfhostedSupabaseClient.create(invalidOptions)).rejects.toThrow(); }); test('throws error when supabaseAnonKey is missing', async () => { const invalidOptions = { ...validOptions, supabaseAnonKey: '', }; await expect(SelfhostedSupabaseClient.create(invalidOptions)).rejects.toThrow(); }); }); describe('getters', () => { test('getSupabaseUrl returns configured URL', async () => { const client = await SelfhostedSupabaseClient.create(validOptions); expect(client.getSupabaseUrl()).toBe(validOptions.supabaseUrl); }); test('getAnonKey returns configured anon key', async () => { const client = await SelfhostedSupabaseClient.create(validOptions); expect(client.getAnonKey()).toBe(validOptions.supabaseAnonKey); }); test('getServiceRoleKey returns configured service role key', async () => { const client = await SelfhostedSupabaseClient.create(validOptions); expect(client.getServiceRoleKey()).toBe(validOptions.supabaseServiceRoleKey); }); test('getServiceRoleKey returns undefined when not configured', async () => { const optionsWithoutServiceKey = { supabaseUrl: validOptions.supabaseUrl, supabaseAnonKey: validOptions.supabaseAnonKey, }; const client = await SelfhostedSupabaseClient.create(optionsWithoutServiceKey); expect(client.getServiceRoleKey()).toBeUndefined(); }); test('getJwtSecret returns configured JWT secret', async () => { const client = await SelfhostedSupabaseClient.create(validOptions); expect(client.getJwtSecret()).toBe(validOptions.jwtSecret); }); test('getDbUrl returns configured database URL', async () => { const client = await SelfhostedSupabaseClient.create(validOptions); expect(client.getDbUrl()).toBe(validOptions.databaseUrl); }); test('isPgAvailable returns true when databaseUrl is configured', async () => { const client = await SelfhostedSupabaseClient.create(validOptions); expect(client.isPgAvailable()).toBe(true); }); test('isPgAvailable returns false when databaseUrl is not configured', async () => { const optionsWithoutDb = { supabaseUrl: validOptions.supabaseUrl, supabaseAnonKey: validOptions.supabaseAnonKey, }; const client = await SelfhostedSupabaseClient.create(optionsWithoutDb); expect(client.isPgAvailable()).toBe(false); }); }); describe('executeSqlViaRpc', () => { test('returns success response for valid query', async () => { const expectedData = [{ id: 1, name: 'test' }]; mockSupabaseClient.rpc.mockImplementation(() => Promise.resolve({ data: expectedData, error: null }) ); const client = await SelfhostedSupabaseClient.create(validOptions); const result = await client.executeSqlViaRpc('SELECT * FROM users'); expect(result).toEqual(expectedData); expect(mockSupabaseClient.rpc).toHaveBeenCalledWith('execute_sql', { query: 'SELECT * FROM users', read_only: false, }); }); test('passes read_only parameter correctly', async () => { mockSupabaseClient.rpc.mockImplementation(() => Promise.resolve({ data: [], error: null }) ); const client = await SelfhostedSupabaseClient.create(validOptions); await client.executeSqlViaRpc('SELECT 1', true); expect(mockSupabaseClient.rpc).toHaveBeenCalledWith('execute_sql', { query: 'SELECT 1', read_only: true, }); }); test('returns error response when RPC fails', async () => { // First call succeeds (initialization check), second call fails let callCount = 0; mockSupabaseClient.rpc.mockImplementation(() => { callCount++; if (callCount === 1) { // Initialization check succeeds return Promise.resolve({ data: [], error: null }); } // Actual query fails return Promise.resolve({ data: null, error: { message: 'Query failed', code: 'P0001', details: 'Some details', hint: 'Try something else', }, }); }); const client = await SelfhostedSupabaseClient.create(validOptions); const result = await client.executeSqlViaRpc('INVALID SQL'); expect(result).toHaveProperty('error'); expect((result as { error: { message: string } }).error.message).toBe('Query failed'); expect((result as { error: { code: string } }).error.code).toBe('P0001'); }); test('returns error when RPC function does not exist', async () => { // First call during initialization - function doesn't exist mockSupabaseClient.rpc.mockImplementation(() => Promise.resolve({ data: null, error: { message: 'Function not found', code: '42883' }, }) ); const client = await SelfhostedSupabaseClient.create({ ...validOptions, supabaseServiceRoleKey: undefined, databaseUrl: undefined, }); const result = await client.executeSqlViaRpc('SELECT 1'); expect(result).toHaveProperty('error'); expect((result as { error: { message: string } }).error.message).toContain( 'execute_sql RPC function not found' ); }); test('handles unexpected response format', async () => { // First call succeeds (initialization), second returns bad format let callCount = 0; mockSupabaseClient.rpc.mockImplementation(() => { callCount++; if (callCount === 1) { return Promise.resolve({ data: [], error: null }); } return Promise.resolve({ data: 'not an array', error: null }); }); const client = await SelfhostedSupabaseClient.create(validOptions); const result = await client.executeSqlViaRpc('SELECT 1'); expect(result).toHaveProperty('error'); expect((result as { error: { code: string } }).error.code).toBe('MCP_RPC_FORMAT_ERROR'); }); test('handles RPC exceptions during query', async () => { // First call succeeds (initialization), second throws let callCount = 0; mockSupabaseClient.rpc.mockImplementation(() => { callCount++; if (callCount === 1) { return Promise.resolve({ data: [], error: null }); } return Promise.reject(new Error('Network error')); }); const client = await SelfhostedSupabaseClient.create(validOptions); const result = await client.executeSqlViaRpc('SELECT 1'); expect(result).toHaveProperty('error'); expect((result as { error: { code: string } }).error.code).toBe('MCP_RPC_EXCEPTION'); expect((result as { error: { message: string } }).error.message).toContain('Network error'); }); }); describe('executeSqlWithPg', () => { test('returns success response for valid query', async () => { const expectedRows = [{ id: 1, name: 'test' }]; mockPoolClient.query.mockImplementation(() => Promise.resolve({ rows: expectedRows }) ); const client = await SelfhostedSupabaseClient.create(validOptions); const result = await client.executeSqlWithPg('SELECT * FROM users'); expect(result).toEqual(expectedRows); }); test('returns error when databaseUrl is not configured', async () => { const optionsWithoutDb = { supabaseUrl: validOptions.supabaseUrl, supabaseAnonKey: validOptions.supabaseAnonKey, }; const client = await SelfhostedSupabaseClient.create(optionsWithoutDb); const result = await client.executeSqlWithPg('SELECT 1'); expect(result).toHaveProperty('error'); expect((result as { error: { message: string } }).error.message).toContain( 'DATABASE_URL is not configured' ); }); test('handles database errors', async () => { const dbError = new Error('Connection refused') as Error & { code: string }; dbError.code = 'ECONNREFUSED'; mockPoolClient.query.mockImplementation(() => Promise.reject(dbError)); const client = await SelfhostedSupabaseClient.create(validOptions); const result = await client.executeSqlWithPg('SELECT 1'); expect(result).toHaveProperty('error'); expect((result as { error: { message: string } }).error.message).toContain( 'Connection refused' ); expect((result as { error: { code: string } }).error.code).toBe('ECONNREFUSED'); }); test('releases client after successful query', async () => { mockPoolClient.query.mockImplementation(() => Promise.resolve({ rows: [] }) ); const client = await SelfhostedSupabaseClient.create(validOptions); await client.executeSqlWithPg('SELECT 1'); expect(mockPoolClient.release).toHaveBeenCalled(); }); test('releases client after failed query', async () => { mockPoolClient.query.mockImplementation(() => Promise.reject(new Error('Query failed')) ); const client = await SelfhostedSupabaseClient.create(validOptions); await client.executeSqlWithPg('SELECT 1'); expect(mockPoolClient.release).toHaveBeenCalled(); }); }); describe('executeTransactionWithPg', () => { test('commits transaction on success', async () => { const expectedResult = { success: true }; mockPoolClient.query.mockImplementation(() => Promise.resolve({ rows: [] }) ); const client = await SelfhostedSupabaseClient.create(validOptions); const result = await client.executeTransactionWithPg(async (pgClient) => { await pgClient.query('INSERT INTO users (name) VALUES ($1)', ['test']); return expectedResult; }); expect(result).toEqual(expectedResult); // Check that BEGIN was called expect(mockPoolClient.query).toHaveBeenCalledWith('BEGIN'); // Check that COMMIT was called expect(mockPoolClient.query).toHaveBeenCalledWith('COMMIT'); }); test('rolls back transaction on failure', async () => { let beginCalled = false; mockPoolClient.query.mockImplementation((query: string) => { if (query === 'BEGIN') { beginCalled = true; return Promise.resolve({ rows: [] }); } if (query === 'ROLLBACK') { return Promise.resolve({ rows: [] }); } if (query === 'COMMIT') { return Promise.resolve({ rows: [] }); } // Fail on the actual operation return Promise.reject(new Error('Insert failed')); }); const client = await SelfhostedSupabaseClient.create(validOptions); await expect( client.executeTransactionWithPg(async (pgClient) => { await pgClient.query('INSERT INTO users (name) VALUES ($1)', ['test']); }) ).rejects.toThrow('Insert failed'); expect(beginCalled).toBe(true); expect(mockPoolClient.query).toHaveBeenCalledWith('ROLLBACK'); }); test('throws error when databaseUrl is not configured', async () => { const optionsWithoutDb = { supabaseUrl: validOptions.supabaseUrl, supabaseAnonKey: validOptions.supabaseAnonKey, }; const client = await SelfhostedSupabaseClient.create(optionsWithoutDb); await expect( client.executeTransactionWithPg(async () => {}) ).rejects.toThrow('DATABASE_URL is not configured'); }); test('releases client after transaction', async () => { mockPoolClient.query.mockImplementation(() => Promise.resolve({ rows: [] }) ); const client = await SelfhostedSupabaseClient.create(validOptions); await client.executeTransactionWithPg(async () => {}); expect(mockPoolClient.release).toHaveBeenCalled(); }); test('releases client after failed transaction', async () => { mockPoolClient.query.mockImplementation((query: string) => { if (query === 'BEGIN' || query === 'ROLLBACK') { return Promise.resolve({ rows: [] }); } return Promise.reject(new Error('Failed')); }); const client = await SelfhostedSupabaseClient.create(validOptions); try { await client.executeTransactionWithPg(async (pgClient) => { await pgClient.query('FAIL'); }); } catch { // Expected to throw } expect(mockPoolClient.release).toHaveBeenCalled(); }); }); describe('supabase client access', () => { test('exposes supabase client instance', async () => { const client = await SelfhostedSupabaseClient.create(validOptions); expect(client.supabase).toBeDefined(); expect(client.supabase).toBe(mockSupabaseClient); }); }); });