@toolbox-sdk/core
Version:
JavaScript Base SDK for interacting with the Toolbox service
502 lines (446 loc) • 18.6 kB
text/typescript
// 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');
});
});
});