feat: migrate to docker-compose with selfhosted-supabase-mcp
- 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
This commit is contained in:
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user