import axios from 'axios'; import { OpenAPIV3 } from 'openapi-types'; import { resolveRef, ActionRequest, openapiToFunction, FunctionSignature, validateAndParseOpenAPISpec, } from '../src/actions'; import { getWeatherOpenapiSpec, whimsicalOpenapiSpec, scholarAIOpenapiSpec } from './openapiSpecs'; import { AuthorizationTypeEnum, AuthTypeEnum } from '../src/types/assistants'; import type { FlowchartSchema } from './openapiSpecs'; import type { ParametersSchema } from '../src/actions'; jest.mock('axios'); const mockedAxios = axios as jest.Mocked; describe('FunctionSignature', () => { it('creates a function signature and converts to JSON tool', () => { const signature = new FunctionSignature('testFunction', 'A test function', { param1: { type: 'string' }, } as unknown as ParametersSchema); expect(signature.name).toBe('testFunction'); expect(signature.description).toBe('A test function'); expect(signature.toObjectTool()).toEqual({ type: 'function', function: { name: 'testFunction', description: 'A test function', parameters: { param1: { type: 'string' }, }, }, }); }); }); describe('ActionRequest', () => { // Mocking responses for each method beforeEach(() => { mockedAxios.get.mockResolvedValue({ data: { success: true, method: 'GET' } }); mockedAxios.post.mockResolvedValue({ data: { success: true, method: 'POST' } }); mockedAxios.put.mockResolvedValue({ data: { success: true, method: 'PUT' } }); mockedAxios.delete.mockResolvedValue({ data: { success: true, method: 'DELETE' } }); mockedAxios.patch.mockResolvedValue({ data: { success: true, method: 'PATCH' } }); }); afterEach(() => { jest.clearAllMocks(); }); it('should make a GET request', async () => { const actionRequest = new ActionRequest( 'https://example.com', '/test', 'GET', 'testOp', false, 'application/json', ); await actionRequest.setParams({ param1: 'value1' }); const response = await actionRequest.execute(); expect(mockedAxios.get).toHaveBeenCalledWith('https://example.com/test', expect.anything()); expect(response.data).toEqual({ success: true, method: 'GET' }); }); describe('ActionRequest', () => { beforeEach(() => { mockedAxios.get.mockClear(); mockedAxios.post.mockClear(); mockedAxios.put.mockClear(); mockedAxios.delete.mockClear(); mockedAxios.patch.mockClear(); }); it('handles GET requests', async () => { mockedAxios.get.mockResolvedValue({ data: { success: true } }); const actionRequest = new ActionRequest( 'https://example.com', '/get', 'GET', 'testGet', false, 'application/json', ); await actionRequest.setParams({ param: 'test' }); const response = await actionRequest.execute(); expect(mockedAxios.get).toHaveBeenCalled(); expect(response.data.success).toBe(true); }); it('handles POST requests', async () => { mockedAxios.post.mockResolvedValue({ data: { success: true } }); const actionRequest = new ActionRequest( 'https://example.com', '/post', 'POST', 'testPost', false, 'application/json', ); await actionRequest.setParams({ param: 'test' }); const response = await actionRequest.execute(); expect(mockedAxios.post).toHaveBeenCalled(); expect(response.data.success).toBe(true); }); it('handles PUT requests', async () => { mockedAxios.put.mockResolvedValue({ data: { success: true } }); const actionRequest = new ActionRequest( 'https://example.com', '/put', 'PUT', 'testPut', false, 'application/json', ); await actionRequest.setParams({ param: 'test' }); const response = await actionRequest.execute(); expect(mockedAxios.put).toHaveBeenCalled(); expect(response.data.success).toBe(true); }); it('handles DELETE requests', async () => { mockedAxios.delete.mockResolvedValue({ data: { success: true } }); const actionRequest = new ActionRequest( 'https://example.com', '/delete', 'DELETE', 'testDelete', false, 'application/json', ); await actionRequest.setParams({ param: 'test' }); const response = await actionRequest.execute(); expect(mockedAxios.delete).toHaveBeenCalled(); expect(response.data.success).toBe(true); }); it('handles PATCH requests', async () => { mockedAxios.patch.mockResolvedValue({ data: { success: true } }); const actionRequest = new ActionRequest( 'https://example.com', '/patch', 'PATCH', 'testPatch', false, 'application/json', ); await actionRequest.setParams({ param: 'test' }); const response = await actionRequest.execute(); expect(mockedAxios.patch).toHaveBeenCalled(); expect(response.data.success).toBe(true); }); it('throws an error for unsupported HTTP methods', async () => { const actionRequest = new ActionRequest( 'https://example.com', '/invalid', 'INVALID', 'testInvalid', false, 'application/json', ); await expect(actionRequest.execute()).rejects.toThrow('Unsupported HTTP method: INVALID'); }); }); it('throws an error for unsupported HTTP method', async () => { const actionRequest = new ActionRequest( 'https://example.com', '/test', 'INVALID', 'testOp', false, 'application/json', ); await expect(actionRequest.execute()).rejects.toThrow('Unsupported HTTP method: INVALID'); }); }); describe('Authentication Handling', () => { it('correctly sets Basic Auth header', async () => { const actionRequest = new ActionRequest( 'https://example.com', '/test', 'GET', 'testOp', false, 'application/json', ); const api_key = 'user:pass'; const encodedCredentials = Buffer.from('user:pass').toString('base64'); actionRequest.setAuth({ auth: { type: AuthTypeEnum.ServiceHttp, authorization_type: AuthorizationTypeEnum.Basic, }, api_key, }); await actionRequest.setParams({ param1: 'value1' }); await actionRequest.execute(); expect(mockedAxios.get).toHaveBeenCalledWith('https://example.com/test', { headers: expect.objectContaining({ Authorization: `Basic ${encodedCredentials}`, }), params: expect.anything(), }); }); it('correctly sets Bearer token', async () => { const actionRequest = new ActionRequest( 'https://example.com', '/test', 'GET', 'testOp', false, 'application/json', ); actionRequest.setAuth({ auth: { type: AuthTypeEnum.ServiceHttp, authorization_type: AuthorizationTypeEnum.Bearer, }, api_key: 'token123', }); await actionRequest.setParams({ param1: 'value1' }); await actionRequest.execute(); expect(mockedAxios.get).toHaveBeenCalledWith('https://example.com/test', { headers: expect.objectContaining({ Authorization: 'Bearer token123', }), params: expect.anything(), }); }); it('correctly sets API Key', async () => { const actionRequest = new ActionRequest( 'https://example.com', '/test', 'GET', 'testOp', false, 'application/json', ); // Updated to match ActionMetadata structure actionRequest.setAuth({ auth: { type: AuthTypeEnum.ServiceHttp, // Assuming this is a valid enum or value for your context authorization_type: AuthorizationTypeEnum.Custom, // Assuming Custom means using a custom header custom_auth_header: 'X-API-KEY', }, api_key: 'abc123', }); await actionRequest.setParams({ param1: 'value1' }); await actionRequest.execute(); expect(mockedAxios.get).toHaveBeenCalledWith('https://example.com/test', { headers: expect.objectContaining({ 'X-API-KEY': 'abc123', }), params: expect.anything(), }); }); }); describe('resolveRef', () => { it('correctly resolves $ref references in the OpenAPI spec', () => { const openapiSpec = whimsicalOpenapiSpec; const flowchartRequestRef = ( openapiSpec.paths['/ai.chatgpt.render-flowchart']?.post ?.requestBody as OpenAPIV3.RequestBodyObject )?.content['application/json'].schema; expect(flowchartRequestRef).toBeDefined(); const resolvedFlowchartRequest = resolveRef( flowchartRequestRef as OpenAPIV3.RequestBodyObject, openapiSpec.components, ); expect(resolvedFlowchartRequest).toBeDefined(); expect(resolvedFlowchartRequest.type).toBe('object'); const properties = resolvedFlowchartRequest.properties as FlowchartSchema; expect(properties).toBeDefined(); expect(properties.mermaid).toBeDefined(); expect(properties.mermaid.type).toBe('string'); }); }); describe('openapiToFunction', () => { it('converts OpenAPI spec to function signatures and request builders', () => { const { functionSignatures, requestBuilders } = openapiToFunction(getWeatherOpenapiSpec); expect(functionSignatures.length).toBe(1); expect(functionSignatures[0].name).toBe('GetCurrentWeather'); const parameters = functionSignatures[0].parameters as ParametersSchema & { properties: { location: { type: 'string'; }; locations: { type: 'array'; items: { type: 'object'; properties: { city: { type: 'string'; }; state: { type: 'string'; }; countryCode: { type: 'string'; }; time: { type: 'string'; }; }; }; }; }; }; expect(parameters).toBeDefined(); expect(parameters.properties.locations).toBeDefined(); expect(parameters.properties.locations.type).toBe('array'); expect(parameters.properties.locations.items.type).toBe('object'); expect(parameters.properties.locations.items.properties.city.type).toBe('string'); expect(parameters.properties.locations.items.properties.state.type).toBe('string'); expect(parameters.properties.locations.items.properties.countryCode.type).toBe('string'); expect(parameters.properties.locations.items.properties.time.type).toBe('string'); expect(requestBuilders).toHaveProperty('GetCurrentWeather'); expect(requestBuilders.GetCurrentWeather).toBeInstanceOf(ActionRequest); }); describe('openapiToFunction with $ref resolution', () => { it('correctly converts OpenAPI spec to function signatures and request builders, resolving $ref references', () => { const { functionSignatures, requestBuilders } = openapiToFunction(whimsicalOpenapiSpec); expect(functionSignatures.length).toBeGreaterThan(0); const postRenderFlowchartSignature = functionSignatures.find( (sig) => sig.name === 'postRenderFlowchart', ); expect(postRenderFlowchartSignature).toBeDefined(); expect(postRenderFlowchartSignature?.name).toBe('postRenderFlowchart'); expect(postRenderFlowchartSignature?.parameters).toBeDefined(); expect(requestBuilders).toHaveProperty('postRenderFlowchart'); const postRenderFlowchartRequestBuilder = requestBuilders['postRenderFlowchart']; expect(postRenderFlowchartRequestBuilder).toBeDefined(); expect(postRenderFlowchartRequestBuilder.method).toBe('post'); expect(postRenderFlowchartRequestBuilder.path).toBe('/ai.chatgpt.render-flowchart'); }); }); }); const invalidServerURL = 'Could not find a valid URL in `servers`'; describe('validateAndParseOpenAPISpec', () => { it('validates a correct OpenAPI spec successfully', () => { const validSpec = JSON.stringify({ openapi: '3.0.0', info: { title: 'Test API', version: '1.0.0' }, servers: [{ url: 'https://test.api' }], paths: { '/test': {} }, components: { schemas: {} }, }); const result = validateAndParseOpenAPISpec(validSpec); expect(result.status).toBe(true); expect(result.message).toBe('OpenAPI spec is valid.'); }); it('returns an error for spec with no servers', () => { const noServerSpec = JSON.stringify({ openapi: '3.0.0', info: { title: 'Test API', version: '1.0.0' }, paths: { '/test': {} }, components: { schemas: {} }, }); const result = validateAndParseOpenAPISpec(noServerSpec); expect(result.status).toBe(false); expect(result.message).toBe(invalidServerURL); }); it('returns an error for spec with empty server URL', () => { const emptyURLSpec = `{ "openapi": "3.1.0", "info": { "title": "Untitled", "description": "Your OpenAPI specification", "version": "v1.0.0" }, "servers": [ { "url": "" } ], "paths": {}, "components": { "schemas": {} } }`; const result = validateAndParseOpenAPISpec(emptyURLSpec); expect(result.status).toBe(false); expect(result.message).toBe(invalidServerURL); }); it('returns an error for spec with no paths', () => { const noPathsSpec = JSON.stringify({ openapi: '3.0.0', info: { title: 'Test API', version: '1.0.0' }, servers: [{ url: 'https://test.api' }], components: { schemas: {} }, }); const result = validateAndParseOpenAPISpec(noPathsSpec); expect(result.status).toBe(false); expect(result.message).toBe('No paths found in the OpenAPI spec.'); }); it('detects missing components in spec', () => { const missingComponentSpec = JSON.stringify({ openapi: '3.0.0', info: { title: 'Test API', version: '1.0.0' }, servers: [{ url: 'https://test.api' }], paths: { '/test': { get: { responses: { '200': { content: { 'application/json': { schema: { $ref: '#/components/schemas/Missing' } }, }, }, }, }, }, }, }); const result = validateAndParseOpenAPISpec(missingComponentSpec); expect(result.status).toBe(true); expect(result.message).toContain('reference to unknown component Missing'); expect(result.spec).toBeDefined(); }); it('handles invalid spec formats', () => { const invalidSpec = 'not a valid spec'; const result = validateAndParseOpenAPISpec(invalidSpec); expect(result.status).toBe(false); expect(result.message).toBe(invalidServerURL); }); it('handles YAML spec and correctly converts to Function Signatures', () => { const result = validateAndParseOpenAPISpec(scholarAIOpenapiSpec); expect(result.status).toBe(true); const spec = result.spec; expect(spec).toBeDefined(); const { functionSignatures, requestBuilders } = openapiToFunction(spec as OpenAPIV3.Document); expect(functionSignatures.length).toBe(3); expect(requestBuilders).toHaveProperty('searchAbstracts'); expect(requestBuilders).toHaveProperty('getFullText'); expect(requestBuilders).toHaveProperty('saveCitation'); }); });