Skip to main content
Glama
bobmaertz

Bitbucket MCP Server

by bobmaertz
client.test.ts9.06 kB
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import axios from 'axios'; import { BitbucketClient } from './client.js'; import { BitbucketError, AuthenticationError, NotFoundError, RateLimitError, PermissionError, } from './utils/errors.js'; // Mock axios vi.mock('axios'); interface MockAxiosInstance { get: ReturnType<typeof vi.fn>; post: ReturnType<typeof vi.fn>; put: ReturnType<typeof vi.fn>; delete: ReturnType<typeof vi.fn>; interceptors: { response: { use: ReturnType<typeof vi.fn>; }; }; } describe('BitbucketClient', () => { let mockAxiosInstance: MockAxiosInstance; beforeEach(() => { mockAxiosInstance = { get: vi.fn(), post: vi.fn(), put: vi.fn(), delete: vi.fn(), interceptors: { response: { use: vi.fn(), }, }, }; vi.mocked(axios.create).mockReturnValue(mockAxiosInstance as never); }); afterEach(() => { vi.clearAllMocks(); }); describe('constructor', () => { it('should create a client with default baseURL', () => { new BitbucketClient({ username: 'testuser', appPassword: 'testpass', }); const createMock = vi.mocked(axios.create); expect(createMock).toHaveBeenCalledWith( expect.objectContaining({ baseURL: 'https://api.bitbucket.org/2.0', timeout: 30000, }) ); }); it('should create a client with custom baseURL', () => { new BitbucketClient({ username: 'testuser', appPassword: 'testpass', baseURL: 'https://custom.api.com', }); const createMock = vi.mocked(axios.create); expect(createMock).toHaveBeenCalledWith( expect.objectContaining({ baseURL: 'https://custom.api.com', }) ); }); it('should create a client with custom timeout', () => { new BitbucketClient({ username: 'testuser', appPassword: 'testpass', timeout: 60000, }); const createMock = vi.mocked(axios.create); expect(createMock).toHaveBeenCalledWith( expect.objectContaining({ timeout: 60000, }) ); }); it('should set up response interceptor', () => { new BitbucketClient({ username: 'testuser', appPassword: 'testpass', }); expect(mockAxiosInstance.interceptors.response.use).toHaveBeenCalled(); }); }); describe('get', () => { it('should make a GET request and return data', async () => { const mockData = { test: 'data' }; mockAxiosInstance.get.mockResolvedValue({ data: mockData }); const client = new BitbucketClient({ username: 'testuser', appPassword: 'testpass', }); const result = await client.get('/test'); expect(mockAxiosInstance.get).toHaveBeenCalledWith('/test', undefined); expect(result).toEqual(mockData); }); it('should pass config to GET request', async () => { mockAxiosInstance.get.mockResolvedValue({ data: {} }); const client = new BitbucketClient({ username: 'testuser', appPassword: 'testpass', }); await client.get('/test', { params: { page: 1 } }); expect(mockAxiosInstance.get).toHaveBeenCalledWith('/test', { params: { page: 1 } }); }); }); describe('post', () => { it('should make a POST request and return data', async () => { const mockData = { id: 1 }; mockAxiosInstance.post.mockResolvedValue({ data: mockData }); const client = new BitbucketClient({ username: 'testuser', appPassword: 'testpass', }); const result = await client.post('/test', { name: 'test' }); expect(mockAxiosInstance.post).toHaveBeenCalledWith('/test', { name: 'test' }, undefined); expect(result).toEqual(mockData); }); }); describe('put', () => { it('should make a PUT request and return data', async () => { const mockData = { id: 1, updated: true }; mockAxiosInstance.put.mockResolvedValue({ data: mockData }); const client = new BitbucketClient({ username: 'testuser', appPassword: 'testpass', }); const result = await client.put('/test/1', { name: 'updated' }); expect(mockAxiosInstance.put).toHaveBeenCalledWith('/test/1', { name: 'updated' }, undefined); expect(result).toEqual(mockData); }); }); describe('delete', () => { it('should make a DELETE request and return data', async () => { mockAxiosInstance.delete.mockResolvedValue({ data: {} }); const client = new BitbucketClient({ username: 'testuser', appPassword: 'testpass', }); const result = await client.delete('/test/1'); expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/test/1', undefined); expect(result).toEqual({}); }); }); describe('error handling', () => { let errorHandler: (error: unknown) => never; beforeEach(() => { new BitbucketClient({ username: 'testuser', appPassword: 'testpass', }); // Capture the error handler from the interceptor const interceptorCall = vi.mocked(mockAxiosInstance.interceptors.response.use).mock.calls[0]; errorHandler = interceptorCall[1] as (error: unknown) => never; }); it('should throw BitbucketError for network errors', () => { const networkError = { message: 'Network Error', response: undefined, }; expect(() => errorHandler(networkError)).toThrow(BitbucketError); expect(() => errorHandler(networkError)).toThrow('Network Error'); }); it('should throw AuthenticationError for 401 status', () => { const error = { response: { status: 401, data: { error: { message: 'Invalid credentials' } }, headers: {}, }, }; expect(() => errorHandler(error)).toThrow(AuthenticationError); }); it('should throw PermissionError for 403 status', () => { const error = { response: { status: 403, data: { error: { message: 'Forbidden' } }, headers: {}, }, }; expect(() => errorHandler(error)).toThrow(PermissionError); }); it('should throw NotFoundError for 404 status', () => { const error = { response: { status: 404, data: { error: { message: 'Not found' } }, headers: {}, }, }; expect(() => errorHandler(error)).toThrow(NotFoundError); }); it('should throw RateLimitError for 429 status', () => { const error = { response: { status: 429, data: { error: { message: 'Rate limit exceeded' } }, headers: { 'retry-after': '60' }, }, }; expect(() => errorHandler(error)).toThrow(RateLimitError); try { errorHandler(error); } catch (e) { expect((e as RateLimitError).retryAfter).toBe(60); } }); it('should throw RateLimitError without retryAfter if header is missing', () => { const error = { response: { status: 429, data: { error: { message: 'Rate limit exceeded' } }, headers: {}, }, }; try { errorHandler(error); } catch (e) { expect(e).toBeInstanceOf(RateLimitError); expect((e as RateLimitError).retryAfter).toBeUndefined(); } }); it('should throw generic BitbucketError for other status codes', () => { const error = { response: { status: 500, data: { error: { message: 'Internal server error' } }, headers: {}, }, }; expect(() => errorHandler(error)).toThrow(BitbucketError); }); it('should extract error message from different response formats', () => { const errorWithNestedMessage = { response: { status: 400, data: { error: { message: 'Nested error message' } }, headers: {}, }, }; expect(() => errorHandler(errorWithNestedMessage)).toThrow('Nested error message'); const errorWithDirectMessage = { response: { status: 400, data: { message: 'Direct error message' }, headers: {}, }, }; expect(() => errorHandler(errorWithDirectMessage)).toThrow('Direct error message'); const errorWithStringData = { response: { status: 400, data: 'String error message', headers: {}, }, }; expect(() => errorHandler(errorWithStringData)).toThrow('String error message'); }); }); describe('getBaseURL', () => { it('should return the configured base URL', () => { const client = new BitbucketClient({ username: 'testuser', appPassword: 'testpass', baseURL: 'https://custom.api.com', }); expect(client.getBaseURL()).toBe('https://custom.api.com'); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/bobmaertz/bitbucket-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server