- 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
266 lines
8.2 KiB
TypeScript
266 lines
8.2 KiB
TypeScript
/**
|
|
* Shared test mocks and helpers for the selfhosted-supabase-mcp test suite.
|
|
*/
|
|
|
|
import { mock } from 'bun:test';
|
|
import type { SelfhostedSupabaseClient } from '../../client/index.js';
|
|
import type { ToolContext } from '../../tools/types.js';
|
|
import type { SqlExecutionResult, SqlSuccessResponse, SqlErrorResponse } from '../../types/index.js';
|
|
|
|
/**
|
|
* Options for creating a mock SelfhostedSupabaseClient
|
|
*/
|
|
export interface MockClientOptions {
|
|
pgAvailable?: boolean;
|
|
serviceRoleAvailable?: boolean;
|
|
rpcResult?: SqlExecutionResult;
|
|
pgResult?: SqlExecutionResult;
|
|
serviceRoleRpcResult?: SqlExecutionResult;
|
|
supabaseUrl?: string;
|
|
anonKey?: string;
|
|
serviceRoleKey?: string;
|
|
jwtSecret?: string;
|
|
dbUrl?: string;
|
|
supabaseClient?: MockSupabaseClient;
|
|
}
|
|
|
|
/**
|
|
* Mock Supabase client type for auth operations
|
|
*/
|
|
export interface MockSupabaseClient {
|
|
auth: {
|
|
admin: {
|
|
listUsers: ReturnType<typeof mock>;
|
|
getUserById: ReturnType<typeof mock>;
|
|
createUser: ReturnType<typeof mock>;
|
|
updateUserById: ReturnType<typeof mock>;
|
|
deleteUser: ReturnType<typeof mock>;
|
|
};
|
|
};
|
|
rpc: ReturnType<typeof mock>;
|
|
}
|
|
|
|
/**
|
|
* Creates a mock Supabase client with configurable auth admin methods
|
|
*/
|
|
export function createMockSupabaseClient(overrides?: Partial<MockSupabaseClient>): MockSupabaseClient {
|
|
return {
|
|
auth: {
|
|
admin: {
|
|
listUsers: mock(() => Promise.resolve({ data: { users: [] }, error: null })),
|
|
getUserById: mock(() => Promise.resolve({ data: { user: null }, error: null })),
|
|
createUser: mock(() => Promise.resolve({ data: { user: null }, error: null })),
|
|
updateUserById: mock(() => Promise.resolve({ data: { user: null }, error: null })),
|
|
deleteUser: mock(() => Promise.resolve({ data: null, error: null })),
|
|
...overrides?.auth?.admin,
|
|
},
|
|
},
|
|
rpc: mock(() => Promise.resolve({ data: [], error: null })),
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Creates a mock SelfhostedSupabaseClient for testing tools
|
|
*/
|
|
export function createMockClient(options: MockClientOptions = {}): SelfhostedSupabaseClient {
|
|
const {
|
|
pgAvailable = true,
|
|
serviceRoleAvailable = true,
|
|
rpcResult = [] as SqlSuccessResponse,
|
|
pgResult = [] as SqlSuccessResponse,
|
|
serviceRoleRpcResult = [] as SqlSuccessResponse,
|
|
supabaseUrl = 'https://test.supabase.co',
|
|
anonKey = 'test-anon-key',
|
|
serviceRoleKey = 'test-service-role-key',
|
|
jwtSecret = 'test-jwt-secret',
|
|
dbUrl = 'postgresql://test:test@localhost:5432/test',
|
|
supabaseClient = createMockSupabaseClient(),
|
|
} = options;
|
|
|
|
// Create a mock that satisfies the SelfhostedSupabaseClient interface
|
|
const mockClient = {
|
|
supabase: supabaseClient,
|
|
|
|
executeSqlViaRpc: mock(async (_query: string, _readOnly?: boolean) => rpcResult),
|
|
executeSqlWithPg: mock(async (_query: string) => pgResult),
|
|
executeSqlViaServiceRoleRpc: mock(async (_query: string, _readOnly?: boolean) => serviceRoleRpcResult),
|
|
executeTransactionWithPg: mock(async <T>(callback: (client: unknown) => Promise<T>) => {
|
|
const mockPgClient = {
|
|
query: mock(() => Promise.resolve({ rows: [] })),
|
|
};
|
|
return callback(mockPgClient);
|
|
}),
|
|
|
|
isPgAvailable: () => pgAvailable,
|
|
isServiceRoleAvailable: () => serviceRoleAvailable,
|
|
getSupabaseUrl: () => supabaseUrl,
|
|
getAnonKey: () => anonKey,
|
|
getServiceRoleKey: () => (serviceRoleKey ? serviceRoleKey : undefined),
|
|
getJwtSecret: () => (jwtSecret ? jwtSecret : undefined),
|
|
getDbUrl: () => (pgAvailable ? dbUrl : undefined),
|
|
} as unknown as SelfhostedSupabaseClient;
|
|
|
|
return mockClient;
|
|
}
|
|
|
|
/**
|
|
* Creates a mock ToolContext for testing tool execute functions
|
|
*/
|
|
export function createMockContext(client?: SelfhostedSupabaseClient): ToolContext {
|
|
return {
|
|
selfhostedClient: client ?? createMockClient(),
|
|
log: mock((_message: string, _level?: 'info' | 'warn' | 'error') => {}),
|
|
workspacePath: '/test/workspace',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Creates a SQL success response
|
|
*/
|
|
export function createSuccessResponse(rows: Record<string, unknown>[]): SqlSuccessResponse {
|
|
return rows;
|
|
}
|
|
|
|
/**
|
|
* Creates a SQL error response
|
|
*/
|
|
export function createErrorResponse(
|
|
message: string,
|
|
code?: string,
|
|
details?: string,
|
|
hint?: string
|
|
): SqlErrorResponse {
|
|
return {
|
|
error: {
|
|
message,
|
|
code,
|
|
details,
|
|
hint,
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Sample test data for various entity types
|
|
*/
|
|
export const testData = {
|
|
users: [
|
|
{
|
|
id: '550e8400-e29b-41d4-a716-446655440001',
|
|
email: '[email protected]',
|
|
role: 'authenticated',
|
|
created_at: '2024-01-01T00:00:00Z',
|
|
last_sign_in_at: '2024-01-15T12:00:00Z',
|
|
raw_app_meta_data: { provider: 'email' },
|
|
raw_user_meta_data: { name: 'Test User 1' },
|
|
},
|
|
{
|
|
id: '550e8400-e29b-41d4-a716-446655440002',
|
|
email: '[email protected]',
|
|
role: 'authenticated',
|
|
created_at: '2024-01-02T00:00:00Z',
|
|
last_sign_in_at: null,
|
|
raw_app_meta_data: {},
|
|
raw_user_meta_data: {},
|
|
},
|
|
],
|
|
|
|
tables: [
|
|
{
|
|
table_schema: 'public',
|
|
table_name: 'users',
|
|
table_type: 'BASE TABLE',
|
|
is_insertable_into: 'YES',
|
|
},
|
|
{
|
|
table_schema: 'public',
|
|
table_name: 'posts',
|
|
table_type: 'BASE TABLE',
|
|
is_insertable_into: 'YES',
|
|
},
|
|
],
|
|
|
|
extensions: [
|
|
{ name: 'plpgsql', installed_version: '1.0', comment: 'PL/pgSQL procedural language' },
|
|
{ name: 'uuid-ossp', installed_version: '1.1', comment: 'generate universally unique identifiers' },
|
|
],
|
|
|
|
buckets: [
|
|
{
|
|
id: 'bucket-1',
|
|
name: 'avatars',
|
|
owner: null,
|
|
public: true,
|
|
avif_autodetection: false,
|
|
file_size_limit: 5242880,
|
|
allowed_mime_types: ['image/png', 'image/jpeg'],
|
|
created_at: '2024-01-01T00:00:00Z',
|
|
updated_at: '2024-01-01T00:00:00Z',
|
|
},
|
|
],
|
|
|
|
storageObjects: [
|
|
{
|
|
id: '550e8400-e29b-41d4-a716-446655440003',
|
|
name: 'avatar.png',
|
|
bucket_id: 'avatars',
|
|
owner: '550e8400-e29b-41d4-a716-446655440001',
|
|
version: null,
|
|
mimetype: 'image/png',
|
|
size: 1024,
|
|
metadata: { mimetype: 'image/png', size: 1024 },
|
|
created_at: '2024-01-01T00:00:00Z',
|
|
updated_at: '2024-01-01T00:00:00Z',
|
|
last_accessed_at: null,
|
|
},
|
|
],
|
|
|
|
migrations: [
|
|
{
|
|
version: '20240101000000',
|
|
name: 'initial_schema',
|
|
executed_at: '2024-01-01T00:00:00Z',
|
|
},
|
|
],
|
|
|
|
connections: [
|
|
{
|
|
pid: 12345,
|
|
usename: 'postgres',
|
|
datname: 'postgres',
|
|
client_addr: '127.0.0.1',
|
|
state: 'active',
|
|
query: 'SELECT 1',
|
|
backend_start: '2024-01-01T00:00:00Z',
|
|
},
|
|
],
|
|
};
|
|
|
|
/**
|
|
* Helper to create Express-like request/response mocks for middleware testing
|
|
*/
|
|
export function createMockExpressReqRes() {
|
|
const req = {
|
|
headers: {} as Record<string, string>,
|
|
user: undefined as unknown,
|
|
};
|
|
|
|
const res = {
|
|
statusCode: 200,
|
|
jsonBody: null as unknown,
|
|
status: mock(function(this: typeof res, code: number) {
|
|
this.statusCode = code;
|
|
return this;
|
|
}),
|
|
json: mock(function(this: typeof res, body: unknown) {
|
|
this.jsonBody = body;
|
|
return this;
|
|
}),
|
|
};
|
|
|
|
const next = mock(() => {});
|
|
|
|
return { req, res, next };
|
|
}
|