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:
2026-02-21 19:32:57 +00:00
parent a7cca50f17
commit 95af17c02d
107 changed files with 12575 additions and 3295 deletions
@@ -0,0 +1,637 @@
/**
* Tests for authentication-related tools
*
* Tools tested:
* - list_auth_users
* - get_auth_user
* - create_auth_user
* - update_auth_user
* - delete_auth_user
*/
import { describe, test, expect, mock } from 'bun:test';
import { listAuthUsersTool } from '../../tools/list_auth_users.js';
import { createAuthUserTool } from '../../tools/create_auth_user.js';
import { deleteAuthUserTool } from '../../tools/delete_auth_user.js';
import { updateAuthUserTool } from '../../tools/update_auth_user.js';
import {
createMockClient,
createMockContext,
createSuccessResponse,
createErrorResponse,
testData,
} from '../helpers/mocks.js';
describe('listAuthUsersTool', () => {
describe('metadata', () => {
test('has correct name', () => {
expect(listAuthUsersTool.name).toBe('list_auth_users');
});
test('has description', () => {
expect(listAuthUsersTool.description).toContain('user');
});
test('has input and output schemas', () => {
expect(listAuthUsersTool.inputSchema).toBeDefined();
expect(listAuthUsersTool.outputSchema).toBeDefined();
});
});
describe('input validation', () => {
test('accepts empty input with defaults', () => {
const result = listAuthUsersTool.inputSchema.safeParse({});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.limit).toBe(50);
expect(result.data.offset).toBe(0);
}
});
test('accepts custom limit and offset', () => {
const result = listAuthUsersTool.inputSchema.safeParse({ limit: 10, offset: 20 });
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.limit).toBe(10);
expect(result.data.offset).toBe(20);
}
});
test('rejects negative limit', () => {
const result = listAuthUsersTool.inputSchema.safeParse({ limit: -1 });
expect(result.success).toBe(false);
});
test('rejects negative offset', () => {
const result = listAuthUsersTool.inputSchema.safeParse({ offset: -1 });
expect(result.success).toBe(false);
});
});
describe('execute', () => {
test('returns list of users', async () => {
const mockClient = createMockClient({
pgAvailable: true,
pgResult: createSuccessResponse(testData.users),
});
const context = createMockContext(mockClient);
const result = await listAuthUsersTool.execute({}, context);
expect(result).toEqual(testData.users);
});
test('returns empty array when no users', async () => {
const mockClient = createMockClient({
pgAvailable: true,
pgResult: createSuccessResponse([]),
});
const context = createMockContext(mockClient);
const result = await listAuthUsersTool.execute({}, context);
expect(result).toEqual([]);
});
test('throws error when pg is not available', async () => {
const mockClient = createMockClient({ pgAvailable: false });
const context = createMockContext(mockClient);
await expect(listAuthUsersTool.execute({}, context)).rejects.toThrow(
'Direct database connection'
);
});
test('throws error on SQL failure', async () => {
const mockClient = createMockClient({
pgAvailable: true,
pgResult: createErrorResponse('permission denied for table users', '42501'),
});
const context = createMockContext(mockClient);
await expect(listAuthUsersTool.execute({}, context)).rejects.toThrow('SQL Error');
});
test('uses pg connection directly (not RPC)', async () => {
const mockClient = createMockClient({
pgAvailable: true,
pgResult: createSuccessResponse([]),
});
const context = createMockContext(mockClient);
await listAuthUsersTool.execute({}, context);
expect(mockClient.executeSqlWithPg).toHaveBeenCalled();
expect(mockClient.executeSqlViaRpc).not.toHaveBeenCalled();
});
});
describe('output validation', () => {
test('validates correct user structure', () => {
const result = listAuthUsersTool.outputSchema.safeParse(testData.users);
expect(result.success).toBe(true);
});
test('rejects invalid UUID for id', () => {
const invalidUser = [{ ...testData.users[0], id: 'not-a-uuid' }];
const result = listAuthUsersTool.outputSchema.safeParse(invalidUser);
expect(result.success).toBe(false);
});
test('accepts null values for nullable fields', () => {
const userWithNulls = [{
id: '123e4567-e89b-12d3-a456-426614174000',
email: null,
role: null,
created_at: null,
last_sign_in_at: null,
raw_app_meta_data: null,
raw_user_meta_data: null,
}];
const result = listAuthUsersTool.outputSchema.safeParse(userWithNulls);
expect(result.success).toBe(true);
});
});
});
describe('createAuthUserTool', () => {
describe('metadata', () => {
test('has correct name', () => {
expect(createAuthUserTool.name).toBe('create_auth_user');
});
test('has warning in description', () => {
expect(createAuthUserTool.description).toContain('WARNING');
});
});
describe('input validation', () => {
test('requires email', () => {
const result = createAuthUserTool.inputSchema.safeParse({ password: 'password123' });
expect(result.success).toBe(false);
});
test('requires password', () => {
const result = createAuthUserTool.inputSchema.safeParse({ email: '[email protected]' });
expect(result.success).toBe(false);
});
test('validates email format', () => {
const result = createAuthUserTool.inputSchema.safeParse({
email: 'not-an-email',
password: 'password123',
});
expect(result.success).toBe(false);
});
test('requires minimum password length', () => {
const result = createAuthUserTool.inputSchema.safeParse({
email: '[email protected]',
password: '12345', // 5 chars, needs 6
});
expect(result.success).toBe(false);
});
test('accepts valid input', () => {
const result = createAuthUserTool.inputSchema.safeParse({
email: '[email protected]',
password: 'password123',
});
expect(result.success).toBe(true);
});
test('accepts optional role and metadata', () => {
const result = createAuthUserTool.inputSchema.safeParse({
email: '[email protected]',
password: 'password123',
role: 'admin',
app_metadata: { custom: 'data' },
user_metadata: { name: 'Test User' },
});
expect(result.success).toBe(true);
});
});
describe('execute', () => {
test('throws error when pg is not available', async () => {
const mockClient = createMockClient({ pgAvailable: false });
const context = createMockContext(mockClient);
await expect(
createAuthUserTool.execute(
{ email: '[email protected]', password: 'password123' },
context
)
).rejects.toThrow('Direct database connection');
});
test('creates user via transaction', async () => {
const createdUser = {
id: '123e4567-e89b-12d3-a456-426614174000',
email: '[email protected]',
role: 'authenticated',
created_at: '2024-01-01T00:00:00Z',
last_sign_in_at: null,
raw_app_meta_data: {},
raw_user_meta_data: {},
};
const mockPgClient = {
query: mock(async (sql: string, _params?: unknown[]) => {
// The crypt test SELECT query doesn't have INSERT
if (sql.includes('crypt') && sql.includes('SELECT') && !sql.includes('INSERT')) {
return { rows: [{ crypt: 'test' }] };
}
// The INSERT query that creates the user
return { rows: [createdUser] };
}),
};
const mockClient = createMockClient({ pgAvailable: true });
(mockClient.executeTransactionWithPg as ReturnType<typeof mock>).mockImplementation(
async (callback: (client: unknown) => Promise<unknown>) => {
return callback(mockPgClient);
}
);
const context = createMockContext(mockClient);
const result = await createAuthUserTool.execute(
{ email: '[email protected]', password: 'password123' },
context
);
expect(result).toEqual(createdUser);
});
test('throws error when pgcrypto is not available', async () => {
const mockPgClient = {
query: mock(async (sql: string) => {
if (sql.includes('crypt')) {
throw new Error('function crypt does not exist');
}
return { rows: [] };
}),
};
const mockClient = createMockClient({ pgAvailable: true });
(mockClient.executeTransactionWithPg as ReturnType<typeof mock>).mockImplementation(
async (callback: (client: unknown) => Promise<unknown>) => {
return callback(mockPgClient);
}
);
const context = createMockContext(mockClient);
await expect(
createAuthUserTool.execute(
{ email: '[email protected]', password: 'password123' },
context
)
).rejects.toThrow('pgcrypto');
});
test('handles unique violation error for duplicate email', async () => {
const mockPgClient = {
query: mock(async (sql: string) => {
if (sql.includes('crypt') && !sql.includes('INSERT')) {
return { rows: [{ crypt: 'test' }] };
}
const error = new Error('duplicate key value violates unique constraint');
(error as unknown as { code: string }).code = '23505';
throw error;
}),
};
const mockClient = createMockClient({ pgAvailable: true });
(mockClient.executeTransactionWithPg as ReturnType<typeof mock>).mockImplementation(
async (callback: (client: unknown) => Promise<unknown>) => {
return callback(mockPgClient);
}
);
const context = createMockContext(mockClient);
await expect(
createAuthUserTool.execute(
{ email: '[email protected]', password: 'password123' },
context
)
).rejects.toThrow('already exists');
});
});
});
describe('deleteAuthUserTool', () => {
describe('metadata', () => {
test('has correct name', () => {
expect(deleteAuthUserTool.name).toBe('delete_auth_user');
});
test('has description', () => {
expect(deleteAuthUserTool.description).toContain('Delete');
});
});
describe('input validation', () => {
test('requires user_id', () => {
const result = deleteAuthUserTool.inputSchema.safeParse({});
expect(result.success).toBe(false);
});
test('validates user_id is UUID', () => {
const result = deleteAuthUserTool.inputSchema.safeParse({ user_id: 'not-a-uuid' });
expect(result.success).toBe(false);
});
test('accepts valid UUID', () => {
const result = deleteAuthUserTool.inputSchema.safeParse({
user_id: '123e4567-e89b-12d3-a456-426614174000',
});
expect(result.success).toBe(true);
});
});
describe('execute', () => {
test('throws error when pg is not available', async () => {
const mockClient = createMockClient({ pgAvailable: false });
const context = createMockContext(mockClient);
await expect(
deleteAuthUserTool.execute(
{ user_id: '123e4567-e89b-12d3-a456-426614174000' },
context
)
).rejects.toThrow('Direct database connection');
});
test('returns success when user is deleted', async () => {
const mockPgClient = {
query: mock(async () => ({ rowCount: 1 })),
};
const mockClient = createMockClient({ pgAvailable: true });
(mockClient.executeTransactionWithPg as ReturnType<typeof mock>).mockImplementation(
async (callback: (client: unknown) => Promise<unknown>) => {
return callback(mockPgClient);
}
);
const context = createMockContext(mockClient);
const result = await deleteAuthUserTool.execute(
{ user_id: '123e4567-e89b-12d3-a456-426614174000' },
context
);
expect(result.success).toBe(true);
expect(result.message).toContain('Successfully deleted');
});
test('returns failure when user is not found', async () => {
const mockPgClient = {
query: mock(async () => ({ rowCount: 0 })),
};
const mockClient = createMockClient({ pgAvailable: true });
(mockClient.executeTransactionWithPg as ReturnType<typeof mock>).mockImplementation(
async (callback: (client: unknown) => Promise<unknown>) => {
return callback(mockPgClient);
}
);
const context = createMockContext(mockClient);
const result = await deleteAuthUserTool.execute(
{ user_id: '123e4567-e89b-12d3-a456-426614174000' },
context
);
expect(result.success).toBe(false);
expect(result.message).toContain('not found');
});
test('throws error on database failure', async () => {
const mockPgClient = {
query: mock(async () => {
throw new Error('Database error');
}),
};
const mockClient = createMockClient({ pgAvailable: true });
(mockClient.executeTransactionWithPg as ReturnType<typeof mock>).mockImplementation(
async (callback: (client: unknown) => Promise<unknown>) => {
return callback(mockPgClient);
}
);
const context = createMockContext(mockClient);
await expect(
deleteAuthUserTool.execute(
{ user_id: '123e4567-e89b-12d3-a456-426614174000' },
context
)
).rejects.toThrow('Failed to delete user');
});
});
describe('output validation', () => {
test('validates success response', () => {
const result = deleteAuthUserTool.outputSchema.safeParse({
success: true,
message: 'User deleted',
});
expect(result.success).toBe(true);
});
test('validates failure response', () => {
const result = deleteAuthUserTool.outputSchema.safeParse({
success: false,
message: 'User not found',
});
expect(result.success).toBe(true);
});
});
});
describe('updateAuthUserTool', () => {
describe('metadata', () => {
test('has correct name', () => {
expect(updateAuthUserTool.name).toBe('update_auth_user');
});
test('has warning in description', () => {
expect(updateAuthUserTool.description).toContain('WARNING');
});
});
describe('input validation', () => {
test('requires user_id', () => {
const result = updateAuthUserTool.inputSchema.safeParse({ email: '[email protected]' });
expect(result.success).toBe(false);
});
test('validates user_id is UUID', () => {
const result = updateAuthUserTool.inputSchema.safeParse({
user_id: 'not-a-uuid',
email: '[email protected]',
});
expect(result.success).toBe(false);
});
test('requires at least one field to update', () => {
const result = updateAuthUserTool.inputSchema.safeParse({
user_id: '123e4567-e89b-12d3-a456-426614174000',
});
expect(result.success).toBe(false);
});
test('accepts email update', () => {
const result = updateAuthUserTool.inputSchema.safeParse({
user_id: '123e4567-e89b-12d3-a456-426614174000',
email: '[email protected]',
});
expect(result.success).toBe(true);
});
test('accepts password update', () => {
const result = updateAuthUserTool.inputSchema.safeParse({
user_id: '123e4567-e89b-12d3-a456-426614174000',
password: 'newpassword123',
});
expect(result.success).toBe(true);
});
test('accepts role update', () => {
const result = updateAuthUserTool.inputSchema.safeParse({
user_id: '123e4567-e89b-12d3-a456-426614174000',
role: 'admin',
});
expect(result.success).toBe(true);
});
test('accepts metadata updates', () => {
const result = updateAuthUserTool.inputSchema.safeParse({
user_id: '123e4567-e89b-12d3-a456-426614174000',
user_metadata: { name: 'New Name' },
});
expect(result.success).toBe(true);
});
test('validates minimum password length', () => {
const result = updateAuthUserTool.inputSchema.safeParse({
user_id: '123e4567-e89b-12d3-a456-426614174000',
password: '12345',
});
expect(result.success).toBe(false);
});
test('validates email format', () => {
const result = updateAuthUserTool.inputSchema.safeParse({
user_id: '123e4567-e89b-12d3-a456-426614174000',
email: 'not-an-email',
});
expect(result.success).toBe(false);
});
});
describe('execute', () => {
test('throws error when pg is not available', async () => {
const mockClient = createMockClient({ pgAvailable: false });
const context = createMockContext(mockClient);
await expect(
updateAuthUserTool.execute(
{
user_id: '123e4567-e89b-12d3-a456-426614174000',
email: '[email protected]',
},
context
)
).rejects.toThrow('Direct database connection');
});
test('updates user via transaction', async () => {
const updatedUser = {
id: '123e4567-e89b-12d3-a456-426614174000',
email: '[email protected]',
role: 'authenticated',
created_at: '2024-01-01T00:00:00Z',
updated_at: '2024-01-02T00:00:00Z',
last_sign_in_at: null,
raw_app_meta_data: {},
raw_user_meta_data: {},
};
const mockPgClient = {
query: mock(async () => ({ rows: [updatedUser] })),
};
const mockClient = createMockClient({ pgAvailable: true });
(mockClient.executeTransactionWithPg as ReturnType<typeof mock>).mockImplementation(
async (callback: (client: unknown) => Promise<unknown>) => {
return callback(mockPgClient);
}
);
const context = createMockContext(mockClient);
const result = await updateAuthUserTool.execute(
{
user_id: '123e4567-e89b-12d3-a456-426614174000',
email: '[email protected]',
},
context
);
expect(result).toEqual(updatedUser);
});
test('throws error when user is not found', async () => {
const mockPgClient = {
query: mock(async () => ({ rows: [] })),
};
const mockClient = createMockClient({ pgAvailable: true });
(mockClient.executeTransactionWithPg as ReturnType<typeof mock>).mockImplementation(
async (callback: (client: unknown) => Promise<unknown>) => {
return callback(mockPgClient);
}
);
const context = createMockContext(mockClient);
await expect(
updateAuthUserTool.execute(
{
user_id: '123e4567-e89b-12d3-a456-426614174000',
email: '[email protected]',
},
context
)
).rejects.toThrow('not found');
});
test('checks pgcrypto when updating password', async () => {
const mockPgClient = {
query: mock(async (sql: string) => {
if (sql.includes('crypt') && sql.includes('SELECT')) {
throw new Error('function crypt does not exist');
}
return { rows: [] };
}),
};
const mockClient = createMockClient({ pgAvailable: true });
(mockClient.executeTransactionWithPg as ReturnType<typeof mock>).mockImplementation(
async (callback: (client: unknown) => Promise<unknown>) => {
return callback(mockPgClient);
}
);
const context = createMockContext(mockClient);
await expect(
updateAuthUserTool.execute(
{
user_id: '123e4567-e89b-12d3-a456-426614174000',
password: 'newpassword123',
},
context
)
).rejects.toThrow('pgcrypto');
});
});
});
@@ -0,0 +1,499 @@
/**
* Tests for database-related tools
*
* Tools tested:
* - list_tables
* - list_extensions
* - get_database_connections
* - get_database_stats
* - list_migrations
* - apply_migration
*/
import { describe, test, expect } from 'bun:test';
import { listTablesTool } from '../../tools/list_tables.js';
import { listExtensionsTool } from '../../tools/list_extensions.js';
import { getDatabaseConnectionsTool } from '../../tools/get_database_connections.js';
import { getDatabaseStatsTool } from '../../tools/get_database_stats.js';
import {
createMockClient,
createMockContext,
createSuccessResponse,
createErrorResponse,
testData,
} from '../helpers/mocks.js';
describe('listTablesTool', () => {
describe('metadata', () => {
test('has correct name', () => {
expect(listTablesTool.name).toBe('list_tables');
});
test('has description', () => {
expect(listTablesTool.description).toBeDefined();
expect(listTablesTool.description).toContain('table');
});
test('has input and output schemas', () => {
expect(listTablesTool.inputSchema).toBeDefined();
expect(listTablesTool.outputSchema).toBeDefined();
expect(listTablesTool.mcpInputSchema).toBeDefined();
});
});
describe('execute', () => {
test('returns list of tables', async () => {
const tables = [
{ schema: 'public', name: 'users', comment: 'User accounts' },
{ schema: 'public', name: 'posts', comment: null },
];
const mockClient = createMockClient({
pgAvailable: true,
pgResult: createSuccessResponse(tables),
});
const context = createMockContext(mockClient);
const result = await listTablesTool.execute({}, context);
expect(result).toEqual(tables);
});
test('returns empty array when no tables exist', async () => {
const mockClient = createMockClient({
pgAvailable: true,
pgResult: createSuccessResponse([]),
});
const context = createMockContext(mockClient);
const result = await listTablesTool.execute({}, context);
expect(result).toEqual([]);
});
test('throws error on SQL failure', async () => {
const mockClient = createMockClient({
pgAvailable: true,
pgResult: createErrorResponse('permission denied', '42501'),
});
const context = createMockContext(mockClient);
await expect(listTablesTool.execute({}, context)).rejects.toThrow('SQL Error');
});
test('uses read-only mode for query via service role RPC', async () => {
const mockClient = createMockClient({
pgAvailable: false,
serviceRoleAvailable: true,
serviceRoleRpcResult: createSuccessResponse([]),
});
const context = createMockContext(mockClient);
await listTablesTool.execute({}, context);
// When using service role RPC, should be called with readOnly=true
expect(mockClient.executeSqlViaServiceRoleRpc).toHaveBeenCalledWith(
expect.any(String),
true
);
});
});
describe('output validation', () => {
test('validates correct table structure', () => {
const validOutput = [
{ schema: 'public', name: 'users', comment: 'User table' },
{ schema: 'public', name: 'posts', comment: null },
];
const result = listTablesTool.outputSchema.safeParse(validOutput);
expect(result.success).toBe(true);
});
test('rejects missing schema field', () => {
const invalidOutput = [{ name: 'users', comment: null }];
const result = listTablesTool.outputSchema.safeParse(invalidOutput);
expect(result.success).toBe(false);
});
test('rejects missing name field', () => {
const invalidOutput = [{ schema: 'public', comment: null }];
const result = listTablesTool.outputSchema.safeParse(invalidOutput);
expect(result.success).toBe(false);
});
});
});
describe('listExtensionsTool', () => {
describe('metadata', () => {
test('has correct name', () => {
expect(listExtensionsTool.name).toBe('list_extensions');
});
test('has description', () => {
expect(listExtensionsTool.description).toContain('extension');
});
});
describe('execute', () => {
test('returns list of extensions', async () => {
const extensions = [
{ name: 'uuid-ossp', schema: 'extensions', version: '1.1', description: 'UUID functions' },
{ name: 'pgcrypto', schema: 'extensions', version: '1.3', description: null },
];
const mockClient = createMockClient({
pgAvailable: true,
pgResult: createSuccessResponse(extensions),
});
const context = createMockContext(mockClient);
const result = await listExtensionsTool.execute({}, context);
expect(result).toEqual(extensions);
});
test('returns empty array when no extensions installed', async () => {
const mockClient = createMockClient({
pgAvailable: true,
pgResult: createSuccessResponse([]),
});
const context = createMockContext(mockClient);
const result = await listExtensionsTool.execute({}, context);
expect(result).toEqual([]);
});
test('throws error on SQL failure', async () => {
const mockClient = createMockClient({
pgAvailable: true,
pgResult: createErrorResponse('access denied', '42501'),
});
const context = createMockContext(mockClient);
await expect(listExtensionsTool.execute({}, context)).rejects.toThrow('SQL Error');
});
});
describe('output validation', () => {
test('validates correct extension structure', () => {
const validOutput = [
{ name: 'uuid-ossp', schema: 'public', version: '1.1', description: 'UUID gen' },
];
const result = listExtensionsTool.outputSchema.safeParse(validOutput);
expect(result.success).toBe(true);
});
test('accepts null description', () => {
const output = [
{ name: 'ext', schema: 'public', version: '1.0', description: null },
];
const result = listExtensionsTool.outputSchema.safeParse(output);
expect(result.success).toBe(true);
});
test('rejects missing required fields', () => {
const invalidOutput = [{ name: 'ext' }];
const result = listExtensionsTool.outputSchema.safeParse(invalidOutput);
expect(result.success).toBe(false);
});
});
});
describe('getDatabaseConnectionsTool', () => {
describe('metadata', () => {
test('has correct name', () => {
expect(getDatabaseConnectionsTool.name).toBe('get_database_connections');
});
test('has description about connections', () => {
expect(getDatabaseConnectionsTool.description).toContain('connection');
});
});
describe('execute', () => {
test('returns list of connections', async () => {
const connections = [
{
pid: 12345,
datname: 'postgres',
usename: 'postgres',
application_name: 'psql',
client_addr: '127.0.0.1',
backend_start: '2024-01-01T00:00:00Z',
state: 'active',
query: 'SELECT 1',
},
];
const mockClient = createMockClient({
pgAvailable: true,
pgResult: createSuccessResponse(connections),
});
const context = createMockContext(mockClient);
const result = await getDatabaseConnectionsTool.execute({}, context);
expect(result).toEqual(connections);
});
test('returns empty array when no connections', async () => {
const mockClient = createMockClient({
pgAvailable: true,
pgResult: createSuccessResponse([]),
});
const context = createMockContext(mockClient);
const result = await getDatabaseConnectionsTool.execute({}, context);
expect(result).toEqual([]);
});
test('handles connections with null values', async () => {
const connections = [
{
pid: 1,
datname: null,
usename: null,
application_name: null,
client_addr: null,
backend_start: null,
state: null,
query: null,
},
];
const mockClient = createMockClient({
pgAvailable: true,
pgResult: createSuccessResponse(connections),
});
const context = createMockContext(mockClient);
const result = await getDatabaseConnectionsTool.execute({}, context);
expect(result).toEqual(connections);
});
test('throws error on SQL failure', async () => {
const mockClient = createMockClient({
pgAvailable: true,
pgResult: createErrorResponse('permission denied for pg_stat_activity', '42501'),
});
const context = createMockContext(mockClient);
await expect(getDatabaseConnectionsTool.execute({}, context)).rejects.toThrow('SQL Error');
});
});
describe('output validation', () => {
test('requires pid to be a number', () => {
const invalidOutput = [{ pid: 'not-a-number' }];
const result = getDatabaseConnectionsTool.outputSchema.safeParse(invalidOutput);
expect(result.success).toBe(false);
});
test('accepts complete connection object', () => {
const validOutput = [
{
pid: 123,
datname: 'db',
usename: 'user',
application_name: 'app',
client_addr: '127.0.0.1',
backend_start: '2024-01-01',
state: 'idle',
query: 'SELECT 1',
},
];
const result = getDatabaseConnectionsTool.outputSchema.safeParse(validOutput);
expect(result.success).toBe(true);
});
});
});
describe('getDatabaseStatsTool', () => {
describe('metadata', () => {
test('has correct name', () => {
expect(getDatabaseStatsTool.name).toBe('get_database_stats');
});
test('has description about statistics', () => {
expect(getDatabaseStatsTool.description).toContain('statistic');
});
});
describe('execute', () => {
test('returns combined database and bgwriter stats', async () => {
const dbStats = [
{
datname: 'postgres',
numbackends: 5,
xact_commit: '1000',
xact_rollback: '10',
blks_read: '500',
blks_hit: '9500',
tup_returned: '10000',
tup_fetched: '5000',
tup_inserted: '100',
tup_updated: '50',
tup_deleted: '10',
conflicts: '0',
temp_files: '0',
temp_bytes: '0',
deadlocks: '0',
checksum_failures: null,
checksum_last_failure: null,
blk_read_time: 1.5,
blk_write_time: 0.5,
stats_reset: '2024-01-01T00:00:00Z',
},
];
const bgWriterStats = [
{
checkpoints_timed: '100',
checkpoints_req: '5',
checkpoint_write_time: 1000.0,
checkpoint_sync_time: 50.0,
buffers_checkpoint: '500',
buffers_clean: '100',
maxwritten_clean: '0',
buffers_backend: '50',
buffers_backend_fsync: '0',
buffers_alloc: '1000',
stats_reset: '2024-01-01T00:00:00Z',
},
];
// Mock client needs to return different results for the two queries
let callCount = 0;
const mockClient = createMockClient({ pgAvailable: true });
(mockClient.executeSqlWithPg as ReturnType<typeof import('bun:test').mock>).mockImplementation(
async () => {
callCount++;
return callCount === 1 ? dbStats : bgWriterStats;
}
);
const context = createMockContext(mockClient);
const result = await getDatabaseStatsTool.execute({}, context);
expect(result).toHaveProperty('database_stats');
expect(result).toHaveProperty('bgwriter_stats');
expect(result.database_stats).toEqual(dbStats);
expect(result.bgwriter_stats).toEqual(bgWriterStats);
});
test('throws error when database stats query fails', async () => {
let callCount = 0;
const mockClient = createMockClient({ pgAvailable: true });
(mockClient.executeSqlWithPg as ReturnType<typeof import('bun:test').mock>).mockImplementation(
async () => {
callCount++;
if (callCount === 1) {
return createErrorResponse('query failed', 'ERROR');
}
return [];
}
);
const context = createMockContext(mockClient);
await expect(getDatabaseStatsTool.execute({}, context)).rejects.toThrow('SQL Error');
});
test('throws error when bgwriter stats query fails', async () => {
let callCount = 0;
const mockClient = createMockClient({ pgAvailable: true });
(mockClient.executeSqlWithPg as ReturnType<typeof import('bun:test').mock>).mockImplementation(
async () => {
callCount++;
if (callCount === 2) {
return createErrorResponse('query failed', 'ERROR');
}
return [
{
datname: 'test',
numbackends: 1,
xact_commit: '0',
xact_rollback: '0',
blks_read: '0',
blks_hit: '0',
tup_returned: '0',
tup_fetched: '0',
tup_inserted: '0',
tup_updated: '0',
tup_deleted: '0',
conflicts: '0',
temp_files: '0',
temp_bytes: '0',
deadlocks: '0',
checksum_failures: null,
checksum_last_failure: null,
blk_read_time: 0,
blk_write_time: 0,
stats_reset: null,
},
];
}
);
const context = createMockContext(mockClient);
await expect(getDatabaseStatsTool.execute({}, context)).rejects.toThrow('SQL Error');
});
});
describe('output validation', () => {
test('validates correct stats structure', () => {
const validOutput = {
database_stats: [
{
datname: 'test',
numbackends: 1,
xact_commit: '0',
xact_rollback: '0',
blks_read: '0',
blks_hit: '0',
tup_returned: '0',
tup_fetched: '0',
tup_inserted: '0',
tup_updated: '0',
tup_deleted: '0',
conflicts: '0',
temp_files: '0',
temp_bytes: '0',
deadlocks: '0',
checksum_failures: null,
checksum_last_failure: null,
blk_read_time: 0,
blk_write_time: 0,
stats_reset: null,
},
],
bgwriter_stats: [
{
checkpoints_timed: '0',
checkpoints_req: '0',
checkpoint_write_time: 0,
checkpoint_sync_time: 0,
buffers_checkpoint: '0',
buffers_clean: '0',
maxwritten_clean: '0',
buffers_backend: '0',
buffers_backend_fsync: '0',
buffers_alloc: '0',
stats_reset: null,
},
],
};
const result = getDatabaseStatsTool.outputSchema.safeParse(validOutput);
expect(result.success).toBe(true);
});
test('rejects missing database_stats', () => {
const invalidOutput = { bgwriter_stats: [] };
const result = getDatabaseStatsTool.outputSchema.safeParse(invalidOutput);
expect(result.success).toBe(false);
});
test('rejects missing bgwriter_stats', () => {
const invalidOutput = { database_stats: [] };
const result = getDatabaseStatsTool.outputSchema.safeParse(invalidOutput);
expect(result.success).toBe(false);
});
});
});
@@ -0,0 +1,248 @@
/**
* Tests for execute_sql tool
*
* Tests the SQL execution tool that allows arbitrary SQL queries.
*/
import { describe, test, expect, beforeEach } from 'bun:test';
import { executeSqlTool } from '../../tools/execute_sql.js';
import {
createMockClient,
createMockContext,
createSuccessResponse,
createErrorResponse,
} from '../helpers/mocks.js';
describe('executeSqlTool', () => {
describe('metadata', () => {
test('has correct name', () => {
expect(executeSqlTool.name).toBe('execute_sql');
});
test('has description', () => {
expect(executeSqlTool.description).toBeDefined();
expect(executeSqlTool.description.length).toBeGreaterThan(0);
});
test('has input schema', () => {
expect(executeSqlTool.inputSchema).toBeDefined();
});
test('has MCP input schema', () => {
expect(executeSqlTool.mcpInputSchema).toBeDefined();
expect(executeSqlTool.mcpInputSchema.type).toBe('object');
expect(executeSqlTool.mcpInputSchema.properties.sql).toBeDefined();
});
test('has output schema', () => {
expect(executeSqlTool.outputSchema).toBeDefined();
});
});
describe('input validation', () => {
test('validates sql is required', () => {
const result = executeSqlTool.inputSchema.safeParse({});
expect(result.success).toBe(false);
});
test('validates sql must be string', () => {
const result = executeSqlTool.inputSchema.safeParse({ sql: 123 });
expect(result.success).toBe(false);
});
test('accepts valid sql string', () => {
const result = executeSqlTool.inputSchema.safeParse({ sql: 'SELECT 1' });
expect(result.success).toBe(true);
});
test('read_only defaults to false', () => {
const result = executeSqlTool.inputSchema.safeParse({ sql: 'SELECT 1' });
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.read_only).toBe(false);
}
});
test('accepts read_only boolean', () => {
const result = executeSqlTool.inputSchema.safeParse({
sql: 'SELECT 1',
read_only: true,
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.read_only).toBe(true);
}
});
});
describe('execute', () => {
test('returns results for successful query', async () => {
const expectedRows = [{ id: 1, name: 'test' }, { id: 2, name: 'test2' }];
const mockClient = createMockClient({
pgAvailable: true,
pgResult: createSuccessResponse(expectedRows),
});
const context = createMockContext(mockClient);
const result = await executeSqlTool.execute({ sql: 'SELECT * FROM users' }, context);
expect(result).toEqual(expectedRows);
});
test('returns empty array for query with no results', async () => {
const mockClient = createMockClient({
pgAvailable: true,
pgResult: createSuccessResponse([]),
});
const context = createMockContext(mockClient);
const result = await executeSqlTool.execute(
{ sql: 'SELECT * FROM users WHERE 1=0' },
context
);
expect(result).toEqual([]);
});
test('throws error for SQL error response', async () => {
const mockClient = createMockClient({
pgAvailable: true,
pgResult: createErrorResponse('syntax error at position 1', '42601'),
});
const context = createMockContext(mockClient);
await expect(
executeSqlTool.execute({ sql: 'INVALID SQL' }, context)
).rejects.toThrow('SQL Error (42601): syntax error at position 1');
});
test('uses pg connection when available', async () => {
const mockClient = createMockClient({
pgAvailable: true,
pgResult: createSuccessResponse([{ result: 1 }]),
});
const context = createMockContext(mockClient);
await executeSqlTool.execute({ sql: 'SELECT 1 as result' }, context);
expect(mockClient.executeSqlWithPg).toHaveBeenCalled();
expect(mockClient.executeSqlViaRpc).not.toHaveBeenCalled();
});
test('falls back to service role RPC when pg is not available', async () => {
const mockClient = createMockClient({
pgAvailable: false,
serviceRoleAvailable: true,
serviceRoleRpcResult: createSuccessResponse([{ result: 1 }]),
});
const context = createMockContext(mockClient);
await executeSqlTool.execute({ sql: 'SELECT 1 as result' }, context);
expect(mockClient.executeSqlViaServiceRoleRpc).toHaveBeenCalled();
});
test('passes read_only flag to service role RPC', async () => {
const mockClient = createMockClient({
pgAvailable: false,
serviceRoleAvailable: true,
serviceRoleRpcResult: createSuccessResponse([]),
});
const context = createMockContext(mockClient);
await executeSqlTool.execute(
{ sql: 'SELECT 1', read_only: true },
context
);
expect(mockClient.executeSqlViaServiceRoleRpc).toHaveBeenCalledWith('SELECT 1', true);
});
test('throws error when neither pg nor service role is available', async () => {
const mockClient = createMockClient({
pgAvailable: false,
serviceRoleAvailable: false,
});
const context = createMockContext(mockClient);
await expect(
executeSqlTool.execute({ sql: 'SELECT 1' }, context)
).rejects.toThrow('execute_sql requires either a direct database connection');
});
test('handles complex query results', async () => {
const complexResult = [
{
id: 1,
created_at: '2024-01-01T00:00:00Z',
metadata: { key: 'value' },
tags: ['a', 'b', 'c'],
},
];
const mockClient = createMockClient({
pgAvailable: true,
pgResult: createSuccessResponse(complexResult),
});
const context = createMockContext(mockClient);
const result = await executeSqlTool.execute(
{ sql: 'SELECT * FROM complex_table' },
context
);
expect(result).toEqual(complexResult);
});
test('handles INSERT returning result', async () => {
const insertResult = [{ id: 42 }];
const mockClient = createMockClient({
pgAvailable: true,
pgResult: createSuccessResponse(insertResult),
});
const context = createMockContext(mockClient);
const result = await executeSqlTool.execute(
{ sql: "INSERT INTO users (name) VALUES ('test') RETURNING id" },
context
);
expect(result).toEqual(insertResult);
});
test('handles UPDATE with no rows affected', async () => {
const mockClient = createMockClient({
pgAvailable: true,
pgResult: createSuccessResponse([]),
});
const context = createMockContext(mockClient);
const result = await executeSqlTool.execute(
{ sql: "UPDATE users SET name = 'test' WHERE id = -1" },
context
);
expect(result).toEqual([]);
});
});
describe('output validation', () => {
test('output schema accepts array of objects', () => {
const result = executeSqlTool.outputSchema.safeParse([
{ id: 1, name: 'test' },
]);
expect(result.success).toBe(true);
});
test('output schema accepts empty array', () => {
const result = executeSqlTool.outputSchema.safeParse([]);
expect(result.success).toBe(true);
});
test('output schema accepts array with any structure', () => {
const result = executeSqlTool.outputSchema.safeParse([
{ complex: { nested: { data: [1, 2, 3] } } },
]);
expect(result.success).toBe(true);
});
});
});
@@ -0,0 +1,253 @@
/**
* Tests for miscellaneous tools
*
* Tools tested:
* - get_project_url
* - verify_jwt_secret
* - generate_typescript_types
* - list_realtime_publications
* - list_cron_jobs
* - list_vector_indexes
*/
import { describe, test, expect, mock } from 'bun:test';
import { getProjectUrlTool } from '../../tools/get_project_url.js';
import { verifyJwtSecretTool } from '../../tools/verify_jwt_secret.js';
import { generateTypesTool } from '../../tools/generate_typescript_types.js';
import {
createMockClient,
createMockContext,
} from '../helpers/mocks.js';
describe('getProjectUrlTool', () => {
describe('metadata', () => {
test('has correct name', () => {
expect(getProjectUrlTool.name).toBe('get_project_url');
});
test('has description', () => {
expect(getProjectUrlTool.description).toContain('URL');
});
});
describe('input validation', () => {
test('accepts empty input', () => {
const result = getProjectUrlTool.inputSchema.safeParse({});
expect(result.success).toBe(true);
});
});
describe('execute', () => {
test('returns project URL', async () => {
const mockClient = createMockClient({
supabaseUrl: 'https://my-project.supabase.co',
});
const context = createMockContext(mockClient);
const result = await getProjectUrlTool.execute({}, context);
expect(result.project_url).toBe('https://my-project.supabase.co');
});
test('returns configured URL from client', async () => {
const customUrl = 'https://custom.supabase.example.com';
const mockClient = createMockClient({ supabaseUrl: customUrl });
const context = createMockContext(mockClient);
const result = await getProjectUrlTool.execute({}, context);
expect(result.project_url).toBe(customUrl);
});
});
describe('output validation', () => {
test('validates URL format', () => {
const result = getProjectUrlTool.outputSchema.safeParse({
project_url: 'https://example.com',
});
expect(result.success).toBe(true);
});
test('rejects invalid URL', () => {
const result = getProjectUrlTool.outputSchema.safeParse({
project_url: 'not-a-url',
});
expect(result.success).toBe(false);
});
});
});
describe('verifyJwtSecretTool', () => {
describe('metadata', () => {
test('has correct name', () => {
expect(verifyJwtSecretTool.name).toBe('verify_jwt_secret');
});
test('has description about JWT', () => {
expect(verifyJwtSecretTool.description).toContain('JWT');
});
});
describe('execute', () => {
test('returns found status when JWT secret is configured (no preview for security)', async () => {
const mockClient = createMockClient({ jwtSecret: 'my-secret-jwt-key-12345' });
const context = createMockContext(mockClient);
const result = await verifyJwtSecretTool.execute({}, context);
expect(result.jwt_secret_status).toBe('found');
// SECURITY: jwt_secret_preview was removed to avoid leaking secret info
expect('jwt_secret_preview' in result).toBe(false);
});
test('returns not_configured status when JWT secret is missing', async () => {
const mockClient = createMockClient({ jwtSecret: undefined });
mockClient.getJwtSecret = () => undefined;
const context = createMockContext(mockClient);
const result = await verifyJwtSecretTool.execute({}, context);
expect(result.jwt_secret_status).toBe('not_configured');
});
});
describe('output validation', () => {
test('validates found status', () => {
const result = verifyJwtSecretTool.outputSchema.safeParse({
jwt_secret_status: 'found',
});
expect(result.success).toBe(true);
});
test('validates not_configured status', () => {
const result = verifyJwtSecretTool.outputSchema.safeParse({
jwt_secret_status: 'not_configured',
});
expect(result.success).toBe(true);
});
test('rejects invalid status', () => {
const result = verifyJwtSecretTool.outputSchema.safeParse({
jwt_secret_status: 'invalid',
});
expect(result.success).toBe(false);
});
});
});
describe('generateTypesTool', () => {
describe('metadata', () => {
test('has correct name', () => {
expect(generateTypesTool.name).toBe('generate_typescript_types');
});
test('has description about TypeScript types', () => {
expect(generateTypesTool.description).toContain('TypeScript');
});
});
describe('input validation', () => {
test('requires output_path', () => {
const result = generateTypesTool.inputSchema.safeParse({});
expect(result.success).toBe(false);
});
test('accepts valid input', () => {
const result = generateTypesTool.inputSchema.safeParse({
output_path: '/path/to/types.ts',
});
expect(result.success).toBe(true);
});
test('defaults included_schemas to public', () => {
const result = generateTypesTool.inputSchema.safeParse({
output_path: '/path/to/types.ts',
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.included_schemas).toEqual(['public']);
}
});
test('accepts custom schemas', () => {
const result = generateTypesTool.inputSchema.safeParse({
output_path: '/path/to/types.ts',
included_schemas: ['public', 'auth', 'storage'],
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.included_schemas).toEqual(['public', 'auth', 'storage']);
}
});
test('defaults output_filename', () => {
const result = generateTypesTool.inputSchema.safeParse({
output_path: '/path/to/types.ts',
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.output_filename).toBe('database.types.ts');
}
});
});
describe('execute', () => {
test('returns error when DATABASE_URL is not configured', async () => {
const mockClient = createMockClient({ dbUrl: undefined });
mockClient.getDbUrl = () => undefined;
const context = createMockContext(mockClient);
const result = await generateTypesTool.execute(
{ output_path: '/tmp/types.ts' },
context
);
expect(result.success).toBe(false);
expect(result.message).toContain('DATABASE_URL');
});
test('includes platform in response', async () => {
const mockClient = createMockClient({ dbUrl: undefined });
mockClient.getDbUrl = () => undefined;
const context = createMockContext(mockClient);
const result = await generateTypesTool.execute(
{ output_path: '/tmp/types.ts' },
context
);
expect(result.platform).toBeDefined();
expect(['win32', 'darwin', 'linux', 'freebsd', 'openbsd']).toContain(result.platform);
});
});
describe('output validation', () => {
test('validates success response', () => {
const result = generateTypesTool.outputSchema.safeParse({
success: true,
message: 'Types generated',
types: 'export type User = {...}',
file_path: '/path/to/types.ts',
platform: 'linux',
});
expect(result.success).toBe(true);
});
test('validates failure response', () => {
const result = generateTypesTool.outputSchema.safeParse({
success: false,
message: 'Failed to generate types',
platform: 'darwin',
});
expect(result.success).toBe(true);
});
test('requires platform field', () => {
const result = generateTypesTool.outputSchema.safeParse({
success: false,
message: 'Error',
});
expect(result.success).toBe(false);
});
});
});
@@ -0,0 +1,380 @@
/**
* Tests for storage-related tools
*
* Tools tested:
* - list_storage_buckets
* - list_storage_objects
* - get_storage_config
* - update_storage_config
*/
import { describe, test, expect, mock } from 'bun:test';
import { listStorageBucketsTool } from '../../tools/list_storage_buckets.js';
import { listStorageObjectsTool } from '../../tools/list_storage_objects.js';
import {
createMockClient,
createMockContext,
createSuccessResponse,
createErrorResponse,
testData,
} from '../helpers/mocks.js';
describe('listStorageBucketsTool', () => {
describe('metadata', () => {
test('has correct name', () => {
expect(listStorageBucketsTool.name).toBe('list_storage_buckets');
});
test('has description', () => {
expect(listStorageBucketsTool.description).toContain('bucket');
});
test('has input and output schemas', () => {
expect(listStorageBucketsTool.inputSchema).toBeDefined();
expect(listStorageBucketsTool.outputSchema).toBeDefined();
});
});
describe('input validation', () => {
test('accepts empty input', () => {
const result = listStorageBucketsTool.inputSchema.safeParse({});
expect(result.success).toBe(true);
});
});
describe('execute', () => {
test('returns list of buckets', async () => {
const mockClient = createMockClient({
pgAvailable: true,
pgResult: createSuccessResponse(testData.buckets),
});
const context = createMockContext(mockClient);
const result = await listStorageBucketsTool.execute({}, context);
expect(result).toEqual(testData.buckets);
});
test('returns empty array when no buckets', async () => {
const mockClient = createMockClient({
pgAvailable: true,
pgResult: createSuccessResponse([]),
});
const context = createMockContext(mockClient);
const result = await listStorageBucketsTool.execute({}, context);
expect(result).toEqual([]);
});
test('throws error when pg is not available', async () => {
const mockClient = createMockClient({ pgAvailable: false });
const context = createMockContext(mockClient);
await expect(listStorageBucketsTool.execute({}, context)).rejects.toThrow(
'Direct database connection'
);
});
test('throws error on SQL failure', async () => {
const mockClient = createMockClient({
pgAvailable: true,
pgResult: createErrorResponse('relation "storage.buckets" does not exist', '42P01'),
});
const context = createMockContext(mockClient);
await expect(listStorageBucketsTool.execute({}, context)).rejects.toThrow('SQL Error');
});
test('uses pg connection directly', async () => {
const mockClient = createMockClient({
pgAvailable: true,
pgResult: createSuccessResponse([]),
});
const context = createMockContext(mockClient);
await listStorageBucketsTool.execute({}, context);
expect(mockClient.executeSqlWithPg).toHaveBeenCalled();
});
});
describe('output validation', () => {
test('validates correct bucket structure', () => {
const result = listStorageBucketsTool.outputSchema.safeParse(testData.buckets);
expect(result.success).toBe(true);
});
test('accepts buckets with all nullable fields as null', () => {
const bucketWithNulls = [{
id: 'test-id',
name: 'test-bucket',
owner: null,
public: false,
avif_autodetection: false,
file_size_limit: null,
allowed_mime_types: null,
created_at: null,
updated_at: null,
}];
const result = listStorageBucketsTool.outputSchema.safeParse(bucketWithNulls);
expect(result.success).toBe(true);
});
test('rejects bucket without required id', () => {
const invalid = [{ name: 'test' }];
const result = listStorageBucketsTool.outputSchema.safeParse(invalid);
expect(result.success).toBe(false);
});
test('rejects bucket with invalid public type', () => {
const invalid = [{
id: 'test',
name: 'test',
owner: null,
public: 'yes', // should be boolean
avif_autodetection: false,
file_size_limit: null,
allowed_mime_types: null,
created_at: null,
updated_at: null,
}];
const result = listStorageBucketsTool.outputSchema.safeParse(invalid);
expect(result.success).toBe(false);
});
});
});
describe('listStorageObjectsTool', () => {
describe('metadata', () => {
test('has correct name', () => {
expect(listStorageObjectsTool.name).toBe('list_storage_objects');
});
test('has description', () => {
expect(listStorageObjectsTool.description).toContain('object');
});
});
describe('input validation', () => {
test('requires bucket_id', () => {
const result = listStorageObjectsTool.inputSchema.safeParse({});
expect(result.success).toBe(false);
});
test('accepts bucket_id only', () => {
const result = listStorageObjectsTool.inputSchema.safeParse({ bucket_id: 'test-bucket' });
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.limit).toBe(100);
expect(result.data.offset).toBe(0);
}
});
test('accepts all parameters', () => {
const result = listStorageObjectsTool.inputSchema.safeParse({
bucket_id: 'test-bucket',
limit: 50,
offset: 10,
prefix: 'public/',
});
expect(result.success).toBe(true);
});
test('rejects negative limit', () => {
const result = listStorageObjectsTool.inputSchema.safeParse({
bucket_id: 'test',
limit: -1,
});
expect(result.success).toBe(false);
});
test('rejects negative offset', () => {
const result = listStorageObjectsTool.inputSchema.safeParse({
bucket_id: 'test',
offset: -1,
});
expect(result.success).toBe(false);
});
});
describe('execute', () => {
test('returns list of objects', async () => {
const mockPgClient = {
query: mock(async () => ({ rows: testData.storageObjects })),
};
const mockClient = createMockClient({ pgAvailable: true });
(mockClient.executeTransactionWithPg as ReturnType<typeof mock>).mockImplementation(
async (callback: (client: unknown) => Promise<unknown>) => {
return callback(mockPgClient);
}
);
const context = createMockContext(mockClient);
const result = await listStorageObjectsTool.execute(
{ bucket_id: 'avatars' },
context
);
expect(result.length).toBe(testData.storageObjects.length);
});
test('returns empty array when no objects', async () => {
const mockPgClient = {
query: mock(async () => ({ rows: [] })),
};
const mockClient = createMockClient({ pgAvailable: true });
(mockClient.executeTransactionWithPg as ReturnType<typeof mock>).mockImplementation(
async (callback: (client: unknown) => Promise<unknown>) => {
return callback(mockPgClient);
}
);
const context = createMockContext(mockClient);
const result = await listStorageObjectsTool.execute(
{ bucket_id: 'empty-bucket' },
context
);
expect(result).toEqual([]);
});
test('throws error when pg is not available', async () => {
const mockClient = createMockClient({ pgAvailable: false });
const context = createMockContext(mockClient);
await expect(
listStorageObjectsTool.execute({ bucket_id: 'test' }, context)
).rejects.toThrow('Direct database connection');
});
test('uses transaction for parameterized query', async () => {
const mockPgClient = {
query: mock(async () => ({ rows: [] })),
};
const mockClient = createMockClient({ pgAvailable: true });
(mockClient.executeTransactionWithPg as ReturnType<typeof mock>).mockImplementation(
async (callback: (client: unknown) => Promise<unknown>) => {
return callback(mockPgClient);
}
);
const context = createMockContext(mockClient);
await listStorageObjectsTool.execute({ bucket_id: 'test' }, context);
expect(mockClient.executeTransactionWithPg).toHaveBeenCalled();
});
test('applies prefix filter in query', async () => {
let executedSql = '';
let executedParams: unknown[] = [];
const mockPgClient = {
query: mock(async (sql: string, params: unknown[]) => {
executedSql = sql;
executedParams = params;
return { rows: [] };
}),
};
const mockClient = createMockClient({ pgAvailable: true });
(mockClient.executeTransactionWithPg as ReturnType<typeof mock>).mockImplementation(
async (callback: (client: unknown) => Promise<unknown>) => {
return callback(mockPgClient);
}
);
const context = createMockContext(mockClient);
await listStorageObjectsTool.execute(
{ bucket_id: 'test', prefix: 'images/' },
context
);
expect(executedSql).toContain('LIKE');
expect(executedParams).toContain('images/%');
});
});
describe('output validation', () => {
test('validates correct object structure', () => {
const validObjects = [{
id: '123e4567-e89b-12d3-a456-426614174000',
name: 'file.txt',
bucket_id: 'test',
owner: '123e4567-e89b-12d3-a456-426614174001',
version: null,
mimetype: 'text/plain',
size: 1024,
metadata: { mimetype: 'text/plain', size: 1024 },
created_at: '2024-01-01',
updated_at: null,
last_accessed_at: null,
}];
const result = listStorageObjectsTool.outputSchema.safeParse(validObjects);
expect(result.success).toBe(true);
});
test('transforms string size to number', () => {
const objectWithStringSize = [{
id: '123e4567-e89b-12d3-a456-426614174000',
name: 'file.txt',
bucket_id: 'test',
owner: null,
version: null,
mimetype: null,
size: '1024', // string
metadata: null,
created_at: null,
updated_at: null,
last_accessed_at: null,
}];
const result = listStorageObjectsTool.outputSchema.safeParse(objectWithStringSize);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data[0].size).toBe(1024);
}
});
test('handles null size', () => {
const objectWithNullSize = [{
id: '123e4567-e89b-12d3-a456-426614174000',
name: null,
bucket_id: 'test',
owner: null,
version: null,
mimetype: null,
size: null,
metadata: null,
created_at: null,
updated_at: null,
last_accessed_at: null,
}];
const result = listStorageObjectsTool.outputSchema.safeParse(objectWithNullSize);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data[0].size).toBeNull();
}
});
test('rejects invalid UUID for id', () => {
const invalid = [{
id: 'not-a-uuid',
name: 'file.txt',
bucket_id: 'test',
owner: null,
version: null,
mimetype: null,
size: null,
metadata: null,
created_at: null,
updated_at: null,
last_accessed_at: null,
}];
const result = listStorageObjectsTool.outputSchema.safeParse(invalid);
expect(result.success).toBe(false);
});
});
});