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,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);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user