UNPKG

@toolbox-sdk/core

Version:
502 lines (446 loc) 18.6 kB
// Copyright 2025 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import {ToolboxClient} from '../../src/toolbox_core/client'; import {ToolboxTool} from '../../src/toolbox_core/tool'; import {AxiosError} from 'axios'; import {CustomGlobal} from './types'; import {authTokenGetter} from './utils'; describe('ToolboxClient E2E Tests', () => { let commonToolboxClient: ToolboxClient; let getNRowsTool: ReturnType<typeof ToolboxTool>; const testBaseUrl = 'http://localhost:5000'; const projectId = (globalThis as CustomGlobal).__GOOGLE_CLOUD_PROJECT__; beforeAll(async () => { commonToolboxClient = new ToolboxClient(testBaseUrl); }); beforeEach(async () => { getNRowsTool = await commonToolboxClient.loadTool('get-n-rows'); expect(getNRowsTool.getName()).toBe('get-n-rows'); }); describe('invokeTool', () => { it('should invoke the getNRowsTool', async () => { const response = await getNRowsTool({num_rows: '2'}); expect(typeof response).toBe('string'); expect(response).toContain('row1'); expect(response).toContain('row2'); expect(response).not.toContain('row3'); }); it('should invoke the getNRowsTool with missing params', async () => { await expect(getNRowsTool()).rejects.toThrow( /num_rows: Invalid input: expected string, received undefined/, ); }); it('should invoke the getNRowsTool with wrong param type', async () => { await expect(getNRowsTool({num_rows: 2})).rejects.toThrow( /num_rows: Invalid input: expected string, received number/, ); }); }); describe('loadToolset', () => { const specificToolsetTestCases = [ { name: 'my-toolset', expectedLength: 1, expectedTools: ['get-row-by-id'], }, { name: 'my-toolset-2', expectedLength: 2, expectedTools: ['get-n-rows', 'get-row-by-id'], }, ]; specificToolsetTestCases.forEach(testCase => { it(`should successfully load the specific toolset "${testCase.name}"`, async () => { const loadedTools = await commonToolboxClient.loadToolset( testCase.name, ); expect(Array.isArray(loadedTools)).toBe(true); expect(loadedTools.length).toBe(testCase.expectedLength); const loadedToolNames = new Set( loadedTools.map(tool => tool.getName()), ); expect(loadedToolNames).toEqual(new Set(testCase.expectedTools)); for (const tool of loadedTools) { expect(typeof tool).toBe('function'); expect(tool.getName).toBeInstanceOf(Function); expect(tool.getDescription).toBeInstanceOf(Function); expect(tool.getParamSchema).toBeInstanceOf(Function); } }); }); it('should successfully load the default toolset (all tools)', async () => { const loadedTools = await commonToolboxClient.loadToolset(); // Load the default toolset (no name provided) expect(Array.isArray(loadedTools)).toBe(true); expect(loadedTools.length).toBeGreaterThan(0); const getNRowsToolFromSet = loadedTools.find( tool => tool.getName() === 'get-n-rows', ); expect(getNRowsToolFromSet).toBeDefined(); expect(typeof getNRowsToolFromSet).toBe('function'); expect(getNRowsToolFromSet?.getName()).toBe('get-n-rows'); expect(getNRowsToolFromSet?.getDescription()).toBeDefined(); expect(getNRowsToolFromSet?.getParamSchema()).toBeDefined(); const loadedToolNames = new Set(loadedTools.map(tool => tool.getName())); const expectedDefaultTools = new Set([ 'get-row-by-content-auth', 'get-row-by-email-auth', 'get-row-by-id-auth', 'get-row-by-id', 'get-n-rows', 'search-rows', ]); expect(loadedToolNames).toEqual(expectedDefaultTools); for (const tool of loadedTools) { expect(typeof tool).toBe('function'); expect(tool.getName).toBeInstanceOf(Function); expect(tool.getDescription).toBeInstanceOf(Function); expect(tool.getParamSchema).toBeInstanceOf(Function); } }); it('should throw an error when trying to load a non-existent toolset', async () => { await expect( commonToolboxClient.loadToolset('non-existent-toolset'), ).rejects.toThrow('Request failed with status code 404'); }); }); describe('bindParams', () => { it('should successfully bind a parameter with bindParam and invoke', async () => { const newTool = getNRowsTool.bindParam('num_rows', '3'); const response = await newTool(); // Invoke with no args expect(response).toContain('row1'); expect(response).toContain('row2'); expect(response).toContain('row3'); expect(response).not.toContain('row4'); }); it('should successfully bind parameters with bindParams and invoke', async () => { const newTool = getNRowsTool.bindParams({num_rows: '3'}); const response = await newTool(); // Invoke with no args expect(response).toContain('row1'); expect(response).toContain('row2'); expect(response).toContain('row3'); expect(response).not.toContain('row4'); }); it('should successfully bind a synchronous function value', async () => { const newTool = getNRowsTool.bindParams({num_rows: () => '1'}); const response = await newTool(); expect(response).toContain('row1'); expect(response).not.toContain('row2'); }); it('should successfully bind an asynchronous function value', async () => { const asyncNumProvider = async () => { await new Promise(resolve => setTimeout(resolve, 10)); return '1'; }; const newTool = getNRowsTool.bindParams({num_rows: asyncNumProvider}); const response = await newTool(); expect(response).toContain('row1'); expect(response).not.toContain('row2'); }); it('should successfully bind parameters at load time', async () => { const tool = await commonToolboxClient.loadTool('get-n-rows', null, { num_rows: '3', }); const response = await tool(); expect(response).toContain('row1'); expect(response).toContain('row2'); expect(response).toContain('row3'); expect(response).not.toContain('row4'); }); it('should throw an error when re-binding an existing parameter', () => { const newTool = getNRowsTool.bindParam('num_rows', '1'); expect(() => { newTool.bindParam('num_rows', '2'); }).toThrow( "Cannot re-bind parameter: parameter 'num_rows' is already bound in tool 'get-n-rows'.", ); }); it('should throw an error when binding a non-existent parameter', () => { expect(() => { getNRowsTool.bindParam('non_existent_param', '2'); }).toThrow( "Unable to bind parameter: no parameter named 'non_existent_param' in tool 'get-n-rows'.", ); }); }); describe('Auth E2E Tests', () => { let authToken1: string; let authToken2: string; let authToken1Getter: () => string; let authToken2Getter: () => string; beforeAll(async () => { if (!projectId) { throw new Error( 'GOOGLE_CLOUD_PROJECT is not defined. Cannot run Auth E2E tests.', ); } authToken1 = await authTokenGetter(projectId, 'sdk_testing_client1'); authToken2 = await authTokenGetter(projectId, 'sdk_testing_client2'); authToken1Getter = () => authToken1; authToken2Getter = () => authToken2; }); it('should fail when running a tool that does not require auth with auth provided', async () => { await expect( commonToolboxClient.loadTool('get-row-by-id', { 'my-test-auth': authToken2Getter, }), ).rejects.toThrow( "Validation failed for tool 'get-row-by-id': unused auth tokens: my-test-auth", ); }); it('should fail when running a tool requiring auth without providing auth', async () => { const tool = await commonToolboxClient.loadTool('get-row-by-id-auth'); await expect(tool({id: '2'})).rejects.toThrow( 'One or more of the following authn services are required to invoke this tool: my-test-auth', ); }); it('should fail when running a tool with incorrect auth', async () => { const tool = await commonToolboxClient.loadTool('get-row-by-id-auth'); const authTool = tool.addAuthTokenGetters({ 'my-test-auth': authToken2Getter, }); try { await authTool({id: '2'}); } catch (error) { expect(error).toBeInstanceOf(AxiosError); const axiosError = error as AxiosError; expect(axiosError.response?.status).toBe(401); expect(axiosError.response?.data).toEqual( expect.objectContaining({ error: 'tool invocation not authorized. Please make sure your specify correct auth headers', }), ); } }); it('should succeed when running a tool with correct auth', async () => { const tool = await commonToolboxClient.loadTool('get-row-by-id-auth'); const authTool = tool.addAuthTokenGetters({ 'my-test-auth': authToken1Getter, }); const response = await authTool({id: '2'}); expect(response).toContain('row2'); }); it('should succeed when running a tool with correct async auth', async () => { const tool = await commonToolboxClient.loadTool('get-row-by-id-auth'); const getAsyncToken = async () => { return authToken1Getter(); }; const authTool = tool.addAuthTokenGetters({ 'my-test-auth': getAsyncToken, }); const response = await authTool({id: '2'}); expect(response).toContain('row2'); }); it('should fail when a tool with a param requiring auth is run without auth', async () => { const tool = await commonToolboxClient.loadTool('get-row-by-email-auth'); await expect(tool()).rejects.toThrow( 'One or more of the following authn services are required to invoke this tool: my-test-auth', ); }); it('should succeed when a tool with a param requiring auth is run with correct auth', async () => { const tool = await commonToolboxClient.loadTool('get-row-by-email-auth', { 'my-test-auth': authToken1Getter, }); const response = await tool(); expect(response).toContain('row4'); expect(response).toContain('row5'); expect(response).toContain('row6'); }); it('should fail when a tool with a param requiring auth is run with insufficient auth claims', async () => { expect.assertions(3); // An AxiosError is expected. const tool = await commonToolboxClient.loadTool( 'get-row-by-content-auth', { 'my-test-auth': authToken1Getter, }, ); try { await tool(); } catch (error) { expect(error).toBeInstanceOf(AxiosError); const axiosError = error as AxiosError; expect(axiosError.response?.data).toEqual( expect.objectContaining({ error: 'provided parameters were invalid: error parsing authenticated parameter "data": no field named row_data in claims', }), ); } }); }); describe('Optional Params E2E Tests', () => { let searchRowsTool: ReturnType<typeof ToolboxTool>; beforeAll(async () => { searchRowsTool = await commonToolboxClient.loadTool('search-rows'); }); it('should correctly identify required and optional parameters in the schema', () => { const paramSchema = searchRowsTool.getParamSchema(); // Test the behavior of the required 'email' parameter expect(paramSchema.safeParse({email: 'test@example.com'}).success).toBe( true, ); expect(paramSchema.safeParse({}).success).toBe(false); // Fails when missing expect(paramSchema.safeParse({email: null}).success).toBe(false); // Fails when null // Test the behavior of the optional 'data' parameter expect( paramSchema.safeParse({email: 'test@example.com', data: 'some data'}) .success, ).toBe(true); expect( paramSchema.safeParse({email: 'test@example.com', data: null}).success, ).toBe(true); // Should succeed with null expect(paramSchema.safeParse({email: 'test@example.com'}).success).toBe( true, ); // Should succeed when omitted // Test the behavior of the optional 'id' parameter expect( paramSchema.safeParse({email: 'test@example.com', id: 123}).success, ).toBe(true); expect( paramSchema.safeParse({email: 'test@example.com', id: null}).success, ).toBe(true); // Should succeed with null expect(paramSchema.safeParse({email: 'test@example.com'}).success).toBe( true, ); // Should succeed when omitted }); it('should run tool with optional params omitted', async () => { const response = await searchRowsTool({email: 'twishabansal@google.com'}); expect(typeof response).toBe('string'); expect(response).toContain('"email":"twishabansal@google.com"'); expect(response).not.toContain('row1'); expect(response).toContain('row2'); expect(response).not.toContain('row3'); expect(response).not.toContain('row4'); expect(response).not.toContain('row5'); expect(response).not.toContain('row6'); }); it('should run tool with optional data provided', async () => { const response = await searchRowsTool({ email: 'twishabansal@google.com', data: 'row3', }); expect(typeof response).toBe('string'); expect(response).toContain('"email":"twishabansal@google.com"'); expect(response).not.toContain('row1'); expect(response).not.toContain('row2'); expect(response).toContain('row3'); expect(response).not.toContain('row4'); expect(response).not.toContain('row5'); expect(response).not.toContain('row6'); }); it('should run tool with optional data as null', async () => { const response = await searchRowsTool({ email: 'twishabansal@google.com', data: null, }); expect(typeof response).toBe('string'); expect(response).toContain('"email":"twishabansal@google.com"'); expect(response).not.toContain('row1'); expect(response).toContain('row2'); expect(response).not.toContain('row3'); expect(response).not.toContain('row4'); expect(response).not.toContain('row5'); expect(response).not.toContain('row6'); }); it('should run tool with optional id provided', async () => { const response = await searchRowsTool({ email: 'twishabansal@google.com', id: 1, }); expect(typeof response).toBe('string'); expect(response).toBe('null'); }); it('should run tool with optional id as null', async () => { const response = await searchRowsTool({ email: 'twishabansal@google.com', id: null, }); expect(typeof response).toBe('string'); expect(response).toContain('"email":"twishabansal@google.com"'); expect(response).not.toContain('row1'); expect(response).toContain('row2'); expect(response).not.toContain('row3'); expect(response).not.toContain('row4'); expect(response).not.toContain('row5'); expect(response).not.toContain('row6'); }); it('should fail when a required param is missing', async () => { await expect(searchRowsTool({id: 5, data: 'row5'})).rejects.toThrow( /email: Invalid input: expected string, received undefined/, ); }); it('should fail when a required param is null', async () => { await expect( searchRowsTool({email: null, id: 5, data: 'row5'}), ).rejects.toThrow(/email: Invalid input: expected string, received null/); }); it('should run tool with all default params', async () => { const response = await searchRowsTool({ email: 'twishabansal@google.com', id: 0, data: 'row2', }); expect(typeof response).toBe('string'); expect(response).toContain('"email":"twishabansal@google.com"'); expect(response).not.toContain('row1'); expect(response).toContain('row2'); expect(response).not.toContain('row3'); expect(response).not.toContain('row4'); expect(response).not.toContain('row5'); expect(response).not.toContain('row6'); }); it('should run tool with all valid params', async () => { const response = await searchRowsTool({ email: 'twishabansal@google.com', id: 3, data: 'row3', }); expect(typeof response).toBe('string'); expect(response).toContain('"email":"twishabansal@google.com"'); expect(response).not.toContain('row1'); expect(response).not.toContain('row2'); expect(response).toContain('row3'); expect(response).not.toContain('row4'); expect(response).not.toContain('row5'); expect(response).not.toContain('row6'); }); it('should return null when called with a different email', async () => { const response = await searchRowsTool({ email: 'anubhavdhawan@google.com', id: 3, data: 'row3', }); expect(typeof response).toBe('string'); expect(response).toBe('null'); }); it('should return null when called with different data', async () => { const response = await searchRowsTool({ email: 'twishabansal@google.com', id: 3, data: 'row4', }); expect(typeof response).toBe('string'); expect(response).toBe('null'); }); it('should return null when called with a different id', async () => { const response = await searchRowsTool({ email: 'twishabansal@google.com', id: 4, data: 'row3', }); expect(typeof response).toBe('string'); expect(response).toBe('null'); }); }); });