- Replace legacy directory structure (api/, db/, functions/, logs/, pooler/) with single docker-compose.yml based self-hosted setup - Add selfhosted-supabase-mcp TypeScript MCP server for database management - Add .dockerignore for Docker build context - Update .gitignore to exclude .env files, volumes/, backups, logs
212 lines
7.6 KiB
TypeScript
212 lines
7.6 KiB
TypeScript
/**
|
|
* 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);
|
|
}
|
|
});
|
|
});
|
|
});
|