@nic0xflamel/lunarcrush-mcp-server
Version:
MCP server for the LunarCrush API (Enterprise)
477 lines • 18.1 kB
JavaScript
import { HttpClient } from '../http-client';
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import OpenAPIClientAxios from 'openapi-client-axios';
// Mock the OpenAPIClientAxios initialization
vi.mock('openapi-client-axios', () => {
const mockApi = {
getPet: vi.fn(),
testOperation: vi.fn(),
complexOperation: vi.fn(),
};
return {
default: vi.fn().mockImplementation(() => ({
init: vi.fn().mockResolvedValue(mockApi),
})),
};
});
describe('HttpClient', () => {
let client;
let mockApi;
const sampleSpec = {
openapi: '3.0.0',
info: { title: 'Test API', version: '1.0.0' },
paths: {
'/pets/{petId}': {
get: {
operationId: 'getPet',
parameters: [
{
name: 'petId',
in: 'path',
required: true,
schema: { type: 'integer' },
},
],
responses: {
'200': {
description: 'OK',
content: {
'application/json': {
schema: { type: 'object' },
},
},
},
},
},
},
},
};
const getPetOperation = sampleSpec.paths['/pets/{petId}']?.get;
if (!getPetOperation) {
throw new Error('Test setup error: getPet operation not found in sample spec');
}
beforeEach(async () => {
// Create a new instance of HttpClient
client = new HttpClient({ baseUrl: 'https://api.example.com' }, sampleSpec);
// Await the initialization to ensure mockApi is set correctly
mockApi = await client['api'];
});
afterEach(() => {
vi.clearAllMocks();
});
it('successfully executes an operation', async () => {
const mockResponse = {
data: { id: 1, name: 'Fluffy' },
status: 200,
headers: {
'content-type': 'application/json',
},
};
mockApi.getPet.mockResolvedValueOnce(mockResponse);
const response = await client.executeOperation(getPetOperation, { petId: 1 });
// Note GET requests should have a null Content-Type header!
expect(mockApi.getPet).toHaveBeenCalledWith({ petId: 1 }, undefined, { headers: { 'Content-Type': null } });
expect(response.data).toEqual(mockResponse.data);
expect(response.status).toBe(200);
expect(response.headers).toBeInstanceOf(Headers);
expect(response.headers.get('content-type')).toBe('application/json');
});
it('throws error when operation ID is missing', async () => {
const operationWithoutId = {
method: 'GET',
path: '/unknown',
responses: {
'200': {
description: 'OK',
},
},
};
await expect(client.executeOperation(operationWithoutId)).rejects.toThrow('Operation ID is required');
});
it('throws error when operation is not found', async () => {
const operation = {
method: 'GET',
path: '/unknown',
operationId: 'nonexistentOperation',
responses: {
'200': {
description: 'OK',
},
},
};
await expect(client.executeOperation(operation)).rejects.toThrow('Operation nonexistentOperation not found');
});
it('handles API errors correctly', async () => {
const error = {
response: {
status: 404,
statusText: 'Not Found',
data: {
code: 'RESOURCE_NOT_FOUND',
message: 'Pet not found',
petId: 999,
},
headers: {
'content-type': 'application/json',
},
},
};
mockApi.getPet.mockRejectedValueOnce(error);
await expect(client.executeOperation(getPetOperation, { petId: 999 })).rejects.toMatchObject({
status: 404,
message: '404 Not Found',
data: {
code: 'RESOURCE_NOT_FOUND',
message: 'Pet not found',
petId: 999,
},
});
});
it('handles validation errors (400) correctly', async () => {
const error = {
response: {
status: 400,
statusText: 'Bad Request',
data: {
code: 'VALIDATION_ERROR',
message: 'Invalid input data',
errors: [
{
field: 'age',
message: 'Age must be a positive number',
},
{
field: 'name',
message: 'Name is required',
},
],
},
headers: {
'content-type': 'application/json',
},
},
};
mockApi.getPet.mockRejectedValueOnce(error);
await expect(client.executeOperation(getPetOperation, { petId: 1 })).rejects.toMatchObject({
status: 400,
message: '400 Bad Request',
data: {
code: 'VALIDATION_ERROR',
message: 'Invalid input data',
errors: [
{
field: 'age',
message: 'Age must be a positive number',
},
{
field: 'name',
message: 'Name is required',
},
],
},
});
});
it('handles server errors (500) with HTML response', async () => {
const error = {
response: {
status: 500,
statusText: 'Internal Server Error',
data: '<html><body><h1>500 Internal Server Error</h1></body></html>',
headers: {
'content-type': 'text/html',
},
},
};
mockApi.getPet.mockRejectedValueOnce(error);
await expect(client.executeOperation(getPetOperation, { petId: 1 })).rejects.toMatchObject({
status: 500,
message: '500 Internal Server Error',
data: '<html><body><h1>500 Internal Server Error</h1></body></html>',
});
});
it('handles rate limit errors (429)', async () => {
const error = {
response: {
status: 429,
statusText: 'Too Many Requests',
data: {
code: 'RATE_LIMIT_EXCEEDED',
message: 'Rate limit exceeded',
retryAfter: 60,
},
headers: {
'content-type': 'application/json',
'retry-after': '60',
},
},
};
mockApi.getPet.mockRejectedValueOnce(error);
await expect(client.executeOperation(getPetOperation, { petId: 1 })).rejects.toMatchObject({
status: 429,
message: '429 Too Many Requests',
data: {
code: 'RATE_LIMIT_EXCEEDED',
message: 'Rate limit exceeded',
retryAfter: 60,
},
});
});
it('should send body parameters in request body for POST operations', async () => {
// Setup mock API with the new operation
mockApi.testOperation = vi.fn().mockResolvedValue({
data: {},
status: 200,
headers: {},
});
const testSpec = {
openapi: '3.0.0',
info: { title: 'Test API', version: '1.0.0' },
paths: {
'/test': {
post: {
operationId: 'testOperation',
requestBody: {
content: {
'application/json': {
schema: {
type: 'object',
properties: {
foo: { type: 'string' },
},
},
},
},
},
responses: {
'200': {
description: 'Success response',
content: {
'application/json': {
schema: {
type: 'object',
},
},
},
},
},
},
},
},
};
const postOperation = testSpec.paths['/test']?.post;
if (!postOperation) {
throw new Error('Test setup error: post operation not found');
}
const client = new HttpClient({ baseUrl: 'http://test.com' }, testSpec);
await client.executeOperation(postOperation, { foo: 'bar' });
expect(mockApi.testOperation).toHaveBeenCalledWith({}, { foo: 'bar' }, { headers: { 'Content-Type': 'application/json' } });
});
it('should handle query, path, and body parameters correctly', async () => {
mockApi.complexOperation = vi.fn().mockResolvedValue({
data: { success: true },
status: 200,
headers: {
'content-type': 'application/json',
},
});
const complexSpec = {
openapi: '3.0.0',
info: { title: 'Test API', version: '1.0.0' },
paths: {
'/users/{userId}/posts': {
post: {
operationId: 'complexOperation',
parameters: [
{
name: 'userId',
in: 'path',
required: true,
schema: { type: 'integer' },
},
{
name: 'include',
in: 'query',
required: false,
schema: { type: 'string' },
},
],
requestBody: {
content: {
'application/json': {
schema: {
type: 'object',
properties: {
title: { type: 'string' },
content: { type: 'string' },
},
},
},
},
},
responses: {
'200': {
description: 'Success response',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
success: { type: 'boolean' },
},
},
},
},
},
},
},
},
},
};
const complexOperation = complexSpec.paths['/users/{userId}/posts']?.post;
if (!complexOperation) {
throw new Error('Test setup error: complex operation not found');
}
const client = new HttpClient({ baseUrl: 'http://test.com' }, complexSpec);
await client.executeOperation(complexOperation, {
// Path parameter
userId: 123,
// Query parameter
include: 'comments',
// Body parameters
title: 'Test Post',
content: 'Test Content',
});
expect(mockApi.complexOperation).toHaveBeenCalledWith({
userId: 123,
include: 'comments',
}, {
title: 'Test Post',
content: 'Test Content',
}, { headers: { 'Content-Type': 'application/json' } });
});
const mockOpenApiSpec = {
openapi: '3.0.0',
info: { title: 'Test API', version: '1.0.0' },
paths: {
'/test': {
post: {
operationId: 'testOperation',
parameters: [
{
name: 'queryParam',
in: 'query',
schema: { type: 'string' },
},
{
name: 'pathParam',
in: 'path',
schema: { type: 'string' },
},
],
requestBody: {
content: {
'application/json': {
schema: {
type: 'object',
properties: {
bodyParam: { type: 'string' },
},
},
},
},
},
responses: {
'200': {
description: 'Success',
},
'400': {
description: 'Bad Request',
},
},
},
},
},
};
const mockConfig = {
baseUrl: 'http://test-api.com',
};
beforeEach(() => {
vi.clearAllMocks();
});
it('should properly propagate structured error responses', async () => {
const errorResponse = {
response: {
data: {
code: 'VALIDATION_ERROR',
message: 'Invalid input',
details: ['Field x is required'],
},
status: 400,
statusText: 'Bad Request',
headers: {
'content-type': 'application/json',
},
},
};
// Mock axios instance
const mockAxiosInstance = {
testOperation: vi.fn().mockRejectedValue(errorResponse),
};
// Mock the OpenAPIClientAxios initialization
const MockOpenAPIClientAxios = vi.fn().mockImplementation(() => ({
init: () => Promise.resolve(mockAxiosInstance),
}));
vi.mocked(OpenAPIClientAxios).mockImplementation(() => MockOpenAPIClientAxios());
const client = new HttpClient(mockConfig, mockOpenApiSpec);
const operation = mockOpenApiSpec.paths['/test']?.post;
if (!operation) {
throw new Error('Operation not found in mock spec');
}
try {
await client.executeOperation(operation, {});
// Should not reach here
expect(true).toBe(false);
}
catch (error) {
expect(error.status).toBe(400);
expect(error.data).toEqual({
code: 'VALIDATION_ERROR',
message: 'Invalid input',
details: ['Field x is required'],
});
expect(error.message).toBe('400 Bad Request');
}
});
it('should handle query, path, and body parameters correctly', async () => {
const mockAxiosInstance = {
testOperation: vi.fn().mockResolvedValue({
data: { success: true },
status: 200,
headers: { 'content-type': 'application/json' },
}),
};
const MockOpenAPIClientAxios = vi.fn().mockImplementation(() => ({
init: () => Promise.resolve(mockAxiosInstance),
}));
vi.mocked(OpenAPIClientAxios).mockImplementation(() => MockOpenAPIClientAxios());
const client = new HttpClient(mockConfig, mockOpenApiSpec);
const operation = mockOpenApiSpec.paths['/test']?.post;
if (!operation) {
throw new Error('Operation not found in mock spec');
}
const response = await client.executeOperation(operation, {
queryParam: 'query1',
pathParam: 'path1',
bodyParam: 'body1',
});
expect(mockAxiosInstance.testOperation).toHaveBeenCalledWith({
queryParam: 'query1',
pathParam: 'path1',
}, {
bodyParam: 'body1',
}, { headers: { 'Content-Type': 'application/json' } });
// Additional check to ensure headers are correctly processed
expect(response.headers.get('content-type')).toBe('application/json');
});
});
//# sourceMappingURL=http-client.test.js.map