/**
* Integration tests for SelfhostedSupabaseClient
*
* These tests run against a real Supabase instance and are skipped
* when environment variables are not configured.
*
* Required environment variables:
* - SUPABASE_URL
* - SUPABASE_ANON_KEY
* - DATABASE_URL (optional, for direct pg connection tests)
*/
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import { SelfhostedSupabaseClient } from '../../client/index.js';
// Check if we have the required credentials
const hasCredentials = !!(
process.env.SUPABASE_URL &&
process.env.SUPABASE_ANON_KEY
);
const hasDatabaseUrl = !!process.env.DATABASE_URL;
// Skip all tests if credentials are not available
describe.skipIf(!hasCredentials)('SelfhostedSupabaseClient Integration Tests', () => {
let client: SelfhostedSupabaseClient;
beforeAll(async () => {
client = await SelfhostedSupabaseClient.create({
supabaseUrl: process.env.SUPABASE_URL!,
supabaseAnonKey: process.env.SUPABASE_ANON_KEY!,
supabaseServiceRoleKey: process.env.SUPABASE_SERVICE_ROLE_KEY,
databaseUrl: process.env.DATABASE_URL,
jwtSecret: process.env.JWT_SECRET,
});
});
describe('Client initialization', () => {
test('creates client successfully', () => {
expect(client).toBeDefined();
expect(client.supabase).toBeDefined();
});
test('getSupabaseUrl returns correct URL', () => {
expect(client.getSupabaseUrl()).toBe(process.env.SUPABASE_URL);
});
test('getAnonKey returns correct key', () => {
expect(client.getAnonKey()).toBe(process.env.SUPABASE_ANON_KEY);
});
test('isPgAvailable reflects DATABASE_URL configuration', () => {
expect(client.isPgAvailable()).toBe(hasDatabaseUrl);
});
});
describe('SQL execution via RPC', () => {
test('executes simple SELECT query', async () => {
const result = await client.executeSqlViaRpc('SELECT 1 as value', true);
// If RPC is not available, we'll get an error
if ('error' in result) {
console.log('RPC not available:', result.error.message);
// This is acceptable in integration tests - RPC may not be set up
expect(result.error).toBeDefined();
} else {
expect(Array.isArray(result)).toBe(true);
expect(result[0]?.value).toBe(1);
}
});
test('executes query returning multiple rows', async () => {
const result = await client.executeSqlViaRpc(
'SELECT generate_series(1, 3) as num',
true
);
if ('error' in result) {
console.log('RPC not available:', result.error.message);
expect(result.error).toBeDefined();
} else {
expect(Array.isArray(result)).toBe(true);
expect(result.length).toBe(3);
}
});
});
describe.skipIf(!hasDatabaseUrl)('SQL execution via direct pg', () => {
test('executes simple SELECT query', async () => {
const result = await client.executeSqlWithPg('SELECT 1 as value');
if ('error' in result) {
console.log('Direct pg error:', result.error.message);
throw new Error(result.error.message);
}
expect(Array.isArray(result)).toBe(true);
expect(result[0]?.value).toBe(1);
});
test('executes query with multiple columns', async () => {
const result = await client.executeSqlWithPg(
"SELECT 'hello' as greeting, 42 as answer"
);
if ('error' in result) {
throw new Error(result.error.message);
}
expect(result[0]?.greeting).toBe('hello');
expect(result[0]?.answer).toBe(42);
});
test('handles query with no results', async () => {
const result = await client.executeSqlWithPg(
'SELECT 1 WHERE false'
);
if ('error' in result) {
throw new Error(result.error.message);
}
expect(result).toEqual([]);
});
test('returns error for invalid SQL', async () => {
const result = await client.executeSqlWithPg('INVALID SQL QUERY');
expect('error' in result).toBe(true);
if ('error' in result) {
expect(result.error.message).toBeDefined();
}
});
});
describe.skipIf(!hasDatabaseUrl)('Transaction handling', () => {
test('commits transaction on success', async () => {
const testTableName = `test_integration_${Date.now()}`;
try {
// Create a test table in a transaction
await client.executeTransactionWithPg(async (pgClient) => {
await pgClient.query(`
CREATE TEMP TABLE ${testTableName} (id serial, name text)
`);
await pgClient.query(
`INSERT INTO ${testTableName} (name) VALUES ($1)`,
['test-value']
);
});
// Verify the table was created (temp tables are session-scoped)
// This test mainly verifies the transaction didn't throw
expect(true).toBe(true);
} catch (error) {
// If this fails, it's likely a permissions issue
console.log('Transaction test failed:', error);
expect(error).toBeDefined();
}
});
test('rolls back transaction on error', async () => {
try {
await client.executeTransactionWithPg(async (pgClient) => {
await pgClient.query('SELECT 1');
throw new Error('Intentional error for rollback test');
});
// Should not reach here
expect(true).toBe(false);
} catch (error) {
expect((error as Error).message).toContain('Intentional error');
}
});
});
describe('System catalog queries', () => {
test.skipIf(!hasDatabaseUrl)('lists database extensions', async () => {
const result = await client.executeSqlWithPg(`
SELECT extname as name
FROM pg_extension
LIMIT 5
`);
if ('error' in result) {
throw new Error(result.error.message);
}
expect(Array.isArray(result)).toBe(true);
// plpgsql is always installed
const hasPlpgsql = result.some((ext: { name: string }) => ext.name === 'plpgsql');
expect(hasPlpgsql).toBe(true);
});
test.skipIf(!hasDatabaseUrl)('queries pg_stat_activity', async () => {
const result = await client.executeSqlWithPg(`
SELECT pid, state
FROM pg_stat_activity
WHERE backend_type = 'client backend'
LIMIT 5
`);
if ('error' in result) {
// May fail due to permissions
console.log('pg_stat_activity query failed:', result.error.message);
expect(result.error).toBeDefined();
} else {
expect(Array.isArray(result)).toBe(true);
}
});
});
});