@utcp/sdk
Version:
Universal Tool Calling Protocol (UTCP) client library for TypeScript
377 lines • 18.2 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const nock_1 = __importDefault(require("nock"));
const http_transport_1 = require("../../../src/client/transport-interfaces/http-transport");
const express_1 = __importDefault(require("express"));
const BASE_URL = 'http://localhost:8888';
describe('HttpClientTransport', () => {
let transport;
let logger;
let server = null;
let expressApp = null;
let serverPort = 0;
beforeEach(() => {
logger = jest.fn();
transport = new http_transport_1.HttpClientTransport(logger);
});
afterEach(() => {
nock_1.default.cleanAll();
if (server) {
server.close();
server = null;
expressApp = null;
}
});
// Helper to create a local test server for path parameter tests
async function createTestServer() {
return new Promise((resolve, reject) => {
try {
expressApp = (0, express_1.default)();
// Path parameter handler route
if (expressApp) {
expressApp.get('/users/:userId/posts/:postId', (req, res) => {
const userId = req.params.userId;
const postId = req.params.postId;
const limit = req.query.limit || '10';
res.json({
user_id: userId,
post_id: postId,
limit: limit,
message: `Retrieved post ${postId} for user ${userId} with limit ${limit}`
});
});
server = expressApp.listen(0, () => {
const address = server?.address();
serverPort = address.port;
resolve(serverPort);
});
}
else {
reject(new Error('Failed to create Express app'));
}
}
catch (error) {
reject(error instanceof Error ? error : new Error(String(error)));
}
});
}
describe('register_tool_provider', () => {
it('should register a provider and discover tools from a UTCP manual', async () => {
const provider = {
name: 'test-provider',
provider_type: 'http',
url: `${BASE_URL}/tools`,
http_method: 'GET',
};
// Create a valid UTCP manual structure that matches the schema
const utcpManual = {
version: '1.0',
tools: [
{
name: 'test_tool',
description: 'A test tool',
inputs: {
type: 'object',
properties: {}
},
outputs: {
type: 'object',
properties: {}
},
tags: [],
tool_provider: {
name: 'test-provider',
provider_type: 'http',
url: `${BASE_URL}/tool`,
http_method: 'GET'
}
},
],
};
// Log the request for debugging
nock_1.default.emitter.on('no match', (req) => {
console.log('No match for request:', req.path, req.method, req.options);
});
// Set up nock interceptor
(0, nock_1.default)(BASE_URL).get('/tools').reply(200, utcpManual);
const tools = await transport.register_tool_provider(provider);
expect(tools).toHaveLength(1);
expect(tools?.[0]?.name).toBe('test_tool');
});
it('should handle HTTP errors during registration', async () => {
const provider = {
name: 'error-provider',
provider_type: 'http',
url: `${BASE_URL}/error`,
http_method: 'GET',
};
(0, nock_1.default)(BASE_URL).get('/error').reply(500, 'Internal Server Error');
const tools = await transport.register_tool_provider(provider);
expect(tools).toEqual([]);
});
});
describe('deregister_tool_provider', () => {
it('should be a no-op', async () => {
const provider = { name: 'test', provider_type: 'http', url: BASE_URL, http_method: 'GET' };
await transport.deregister_tool_provider(provider);
// No assertion needed, just checking that it doesn't throw.
});
});
describe('_buildUrlWithPathParams', () => {
it('should build URLs with path parameters', () => {
// Test 1: Simple single parameter
const args1 = { user_id: '123', limit: '10' };
const url1 = transport._buildUrlWithPathParams('https://api.example.com/users/{user_id}', args1);
expect(url1).toBe('https://api.example.com/users/123');
expect(args1).toEqual({ limit: '10' }); // Path parameter should be removed
// Test 2: Multiple path parameters (like OpenLibrary API)
const args2 = { key_type: 'isbn', value: '9780140328721', format: 'json' };
const url2 = transport._buildUrlWithPathParams('https://openlibrary.org/api/volumes/brief/{key_type}/{value}.json', args2);
expect(url2).toBe('https://openlibrary.org/api/volumes/brief/isbn/9780140328721.json');
expect(args2).toEqual({ format: 'json' }); // Path parameters should be removed
// Test 3: Complex URL with multiple parameters
const args3 = { user_id: '123', post_id: '456', comment_id: '789', limit: '10', offset: '0' };
const url3 = transport._buildUrlWithPathParams('https://api.example.com/users/{user_id}/posts/{post_id}/comments/{comment_id}', args3);
expect(url3).toBe('https://api.example.com/users/123/posts/456/comments/789');
expect(args3).toEqual({ limit: '10', offset: '0' }); // Path parameters should be removed
// Test 4: URL with no path parameters
const args4 = { param1: 'value1', param2: 'value2' };
const url4 = transport._buildUrlWithPathParams('https://api.example.com/endpoint', args4);
expect(url4).toBe('https://api.example.com/endpoint');
expect(args4).toEqual({ param1: 'value1', param2: 'value2' }); // Arguments should remain unchanged
});
it('should throw an error for missing path parameters', () => {
// Test 5: Error case - missing parameter
const args5 = { user_id: '123' };
expect(() => {
transport._buildUrlWithPathParams('https://api.example.com/users/{user_id}/posts/{post_id}', args5);
}).toThrow('Missing required path parameter: post_id');
});
});
describe('call_tool', () => {
const toolProvider = {
name: 'tool-provider',
provider_type: 'http',
url: `${BASE_URL}/tool`,
http_method: 'GET',
};
it('should make a basic tool call', async () => {
(0, nock_1.default)(BASE_URL).get('/tool').query({ param1: 'value1' }).reply(200, { result: 'success' });
const result = await transport.call_tool('test_tool', { param1: 'value1' }, toolProvider);
expect(result).toEqual({ result: 'success' });
});
it('should handle API key authentication', async () => {
const auth = {
auth_type: 'api_key',
var_name: 'X-API-Key',
api_key: 'test-key',
location: 'header'
};
const provider = {
...toolProvider,
auth
};
// Define the nock scope with debug logging to diagnose issues
const scope = (0, nock_1.default)(BASE_URL, { allowUnmocked: false })
.get('/tool')
.query({ param1: 'value1' })
.matchHeader('X-API-Key', 'test-key')
.reply(200, { result: 'success' });
const result = await transport.call_tool('test_tool', { param1: 'value1' }, provider);
expect(result).toEqual({ result: 'success' });
expect(scope.isDone()).toBe(true);
});
it('should handle Basic authentication', async () => {
const auth = { auth_type: 'basic', username: 'user', password: 'pass' };
const provider = { ...toolProvider, auth };
(0, nock_1.default)(BASE_URL)
.get('/tool')
.query(true)
.basicAuth({ user: 'user', pass: 'pass' })
.reply(200, { result: 'success' });
const result = await transport.call_tool('test_tool', { param1: 'value1' }, provider);
expect(result).toEqual({ result: 'success' });
});
it('should handle OAuth2 authentication with credentials in body', async () => {
const auth = {
auth_type: 'oauth2',
token_url: `${BASE_URL}/token`,
client_id: 'id',
client_secret: 'secret',
};
const provider = { ...toolProvider, auth };
(0, nock_1.default)(BASE_URL)
.post('/token', 'grant_type=client_credentials&client_id=id&client_secret=secret&scope=')
.reply(200, { access_token: 'test-token' });
(0, nock_1.default)(BASE_URL)
.get('/tool')
.query(true)
.matchHeader('Authorization', 'Bearer test-token')
.reply(200, { result: 'success' });
const result = await transport.call_tool('test_tool', { param1: 'value1' }, provider);
expect(result).toEqual({ result: 'success' });
});
it('should handle OAuth2 authentication with Basic Auth header fallback', async () => {
const auth = {
auth_type: 'oauth2',
token_url: `${BASE_URL}/token_header_auth`,
client_id: 'id',
client_secret: 'secret',
};
const provider = { ...toolProvider, auth };
// First request fails (credentials in body)
(0, nock_1.default)(BASE_URL)
.post('/token_header_auth', 'grant_type=client_credentials&client_id=id&client_secret=secret&scope=')
.reply(401, { error: 'invalid_client' });
// Second request succeeds (credentials in header)
(0, nock_1.default)(BASE_URL)
.post('/token_header_auth', 'grant_type=client_credentials&scope=')
.basicAuth({ user: 'id', pass: 'secret' })
.reply(200, { access_token: 'test-token-header' });
(0, nock_1.default)(BASE_URL)
.get('/tool')
.query(true)
.matchHeader('Authorization', 'Bearer test-token-header')
.reply(200, { result: 'success' });
const result = await transport.call_tool('test_tool', { param1: 'value1' }, provider);
expect(result).toEqual({ result: 'success' });
});
it('should handle body_field for POST requests', async () => {
const provider = {
...toolProvider,
http_method: 'POST',
body_field: 'data',
};
const body = { key: 'value' };
(0, nock_1.default)(BASE_URL)
.post('/tool', body)
.query({ param1: 'value1' })
.reply(200, { result: 'success' });
const result = await transport.call_tool('test_tool', { param1: 'value1', data: body }, provider);
expect(result).toEqual({ result: 'success' });
});
it('should handle header_fields', async () => {
const provider = {
...toolProvider,
header_fields: ['X-Custom-Header'],
};
(0, nock_1.default)(BASE_URL)
.get('/tool')
.query(true)
.matchHeader('X-Custom-Header', 'custom-value')
.reply(200, { result: 'success' });
const result = await transport.call_tool('test_tool', { param1: 'value1', 'X-Custom-Header': 'custom-value' }, provider);
expect(result).toEqual({ result: 'success' });
});
it('should throw an error on failed tool call', async () => {
(0, nock_1.default)(BASE_URL).get('/tool').query(true).reply(500);
await expect(transport.call_tool('test_tool', { param1: 'value1' }, toolProvider)).rejects.toThrow();
});
it('should handle API key auth in different locations', async () => {
// Test header location
const headerAuth = {
auth_type: 'api_key',
var_name: 'X-API-Key',
api_key: 'test-key',
location: 'header'
};
const headerProvider = { ...toolProvider, auth: headerAuth };
(0, nock_1.default)(BASE_URL)
.get('/tool')
.query(true)
.matchHeader('X-API-Key', 'test-key')
.reply(200, { result: 'header-success' });
const headerResult = await transport.call_tool('test_tool', { param1: 'value1' }, headerProvider);
expect(headerResult).toEqual({ result: 'header-success' });
// Test query location
const queryAuth = {
auth_type: 'api_key',
var_name: 'api_key',
api_key: 'test-key',
location: 'query'
};
const queryProvider = { ...toolProvider, auth: queryAuth };
(0, nock_1.default)(BASE_URL)
.get('/tool')
.query(params => params.api_key === 'test-key')
.reply(200, { result: 'query-success' });
const queryResult = await transport.call_tool('test_tool', { param1: 'value1' }, queryProvider);
expect(queryResult).toEqual({ result: 'query-success' });
// Test cookie location
const cookieAuth = {
auth_type: 'api_key',
var_name: 'auth_token',
api_key: 'test-key',
location: 'cookie'
};
const cookieProvider = { ...toolProvider, auth: cookieAuth };
(0, nock_1.default)(BASE_URL)
.get('/tool')
.query(true)
.matchHeader('Cookie', 'auth_token=test-key')
.reply(200, { result: 'cookie-success' });
const cookieResult = await transport.call_tool('test_tool', { param1: 'value1' }, cookieProvider);
expect(cookieResult).toEqual({ result: 'cookie-success' });
});
it('should handle missing API key', async () => {
const auth = {
auth_type: 'api_key',
var_name: 'X-API-Key',
location: 'header'
};
const provider = { ...toolProvider, auth };
await expect(transport.call_tool('test_tool', { param1: 'value1' }, provider))
.rejects.toThrow('API key for ApiKeyAuth not found.');
});
});
describe('call_tool with path parameters', () => {
it('should handle URLs with path parameters', async () => {
// Create test server for path parameters
const port = await createTestServer();
const baseUrl = `http://localhost:${port}`;
// Create provider with path parameters in URL
const provider = {
name: 'test-provider',
provider_type: 'http',
url: `${baseUrl}/users/{user_id}/posts/{post_id}`,
http_method: 'GET'
};
// Call with path parameters
const result = await transport.call_tool('get_user_post', { user_id: '123', post_id: '456', limit: '20' }, provider);
// Verify result
expect(result.user_id).toBe('123');
expect(result.post_id).toBe('456');
expect(result.limit).toBe('20');
expect(result.message).toBe('Retrieved post 456 for user 123 with limit 20');
});
it('should throw error for missing path parameters', async () => {
// Provider with path parameters
const provider = {
name: 'test-provider',
provider_type: 'http',
url: 'https://api.example.com/users/{user_id}/posts/{post_id}',
http_method: 'GET'
};
// Missing post_id parameter
await expect(transport.call_tool('test_tool', { user_id: '123' }, provider)).rejects.toThrow('Missing required path parameter: post_id');
});
it('should handle OpenLibrary-style URLs', () => {
// Create a provider with OpenLibrary-style URL
const provider = {
name: 'openlibrary-provider',
provider_type: 'http',
url: 'https://openlibrary.org/api/volumes/brief/{key_type}/{value}.json',
http_method: 'GET'
};
// Test URL building
const args = { key_type: 'isbn', value: '9780140328721', format: 'json' };
const url = transport._buildUrlWithPathParams(provider.url, args);
expect(url).toBe('https://openlibrary.org/api/volumes/brief/isbn/9780140328721.json');
expect(args).toEqual({ format: 'json' }); // Path parameters should be removed
});
});
});
//# sourceMappingURL=http-transport.test.js.map