Files
2026-07-13 12:38:34 +08:00

843 lines
24 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { AuthConfigs } from '../../src/models/AuthConfigs';
import ComposioClient from '@composio/client';
import { ValidationError } from '../../src/errors/ValidationErrors';
import {
AuthConfigRetrieveResponse as ComposioAuthConfigRetrieveResponse,
AuthConfigDeleteResponse,
AuthConfigUpdateResponse,
AuthConfigUpdateStatusResponse,
} from '@composio/client/resources/auth-configs';
import { transformAuthConfigRetrieveResponse } from '../../src/utils/transformers/authConfigs';
// Mock data for testing
const mockComposioAuthConfigResponse: ComposioAuthConfigRetrieveResponse = {
id: 'auth_12345',
name: 'Test Auth Config',
no_of_connections: 5,
status: 'ENABLED',
type: 'custom',
toolkit: {
logo: 'https://example.com/logo.png',
slug: 'github',
},
auth_scheme: 'OAUTH2',
credentials: {
client_id: 'test_client_id',
client_secret: 'test_client_secret',
},
expected_input_fields: [
{ name: 'client_id', type: 'string' },
{ name: 'client_secret', type: 'string' },
],
is_composio_managed: true,
created_by: 'user_123',
created_at: '2023-01-01T00:00:00Z',
last_updated_at: '2023-01-01T00:00:00Z',
tool_access_config: {
tools_for_connected_account_creation: undefined,
},
};
const mockTransformedAuthConfigResponse = {
id: 'auth_12345',
name: 'Test Auth Config',
noOfConnections: 5,
status: 'ENABLED',
toolkit: {
logo: 'https://example.com/logo.png',
slug: 'github',
},
authScheme: 'OAUTH2',
credentials: {
client_id: 'test_client_id',
client_secret: 'test_client_secret',
},
expectedInputFields: [
{ name: 'client_id', type: 'string' },
{ name: 'client_secret', type: 'string' },
],
isComposioManaged: true,
createdBy: 'user_123',
createdAt: '2023-01-01T00:00:00Z',
lastUpdatedAt: '2023-01-01T00:00:00Z',
restrictToFollowingTools: undefined,
toolAccessConfig: {
toolsAvailableForExecution: undefined,
toolsForConnectedAccountCreation: undefined,
},
};
// Extended mock client with all AuthConfigs methods
const mockClient = {
authConfigs: {
list: vi.fn(),
create: vi.fn(),
retrieve: vi.fn(),
update: vi.fn(),
delete: vi.fn(),
updateStatus: vi.fn(),
},
};
describe('AuthConfigs', () => {
let authConfigs: AuthConfigs;
beforeEach(() => {
vi.clearAllMocks();
authConfigs = new AuthConfigs(mockClient as unknown as ComposioClient);
});
describe('constructor', () => {
it('should create an instance successfully with valid client', () => {
expect(authConfigs).toBeInstanceOf(AuthConfigs);
});
it('should have protected getClient method for testing', () => {
// @ts-expect-error - Accessing protected method for testing
expect(authConfigs.getClient()).toBe(mockClient);
});
});
describe('parseAuthConfigRetrieveResponse', () => {
it('should transform API response to SDK format correctly', () => {
const result = transformAuthConfigRetrieveResponse(mockComposioAuthConfigResponse);
expect(result).toEqual(mockTransformedAuthConfigResponse);
});
// We have relaxed the validation for the response data, so we don't expect a ValidationError to be thrown
// it('should throw ValidationError for invalid response data', () => {
// const invalidResponse = {
// ...mockComposioAuthConfigResponse,
// id: null, // Invalid: id should be string
// } as unknown as ComposioAuthConfigRetrieveResponse;
// expect(() => {
// // @ts-expect-error - Accessing private method for testing
// authConfigs.parseAuthConfigRetrieveResponse(invalidResponse);
// }).toThrow(ValidationError);
// });
it('should handle optional fields correctly', () => {
const responseWithOptionalFields = {
id: 'auth_12345',
name: 'Test Auth Config',
no_of_connections: 5,
status: 'ENABLED',
type: 'default',
toolkit: {
logo: 'https://example.com/logo.png',
slug: 'github',
},
// Optional fields omitted
} as ComposioAuthConfigRetrieveResponse;
const result = transformAuthConfigRetrieveResponse(responseWithOptionalFields);
expect(result).toMatchObject({
id: 'auth_12345',
name: 'Test Auth Config',
noOfConnections: 5,
status: 'ENABLED',
toolkit: {
logo: 'https://example.com/logo.png',
slug: 'github',
},
});
});
});
describe('list', () => {
const mockListResponse = {
items: [mockComposioAuthConfigResponse],
next_cursor: 'next_cursor_123',
total_pages: 1,
};
it('should list auth configs without query parameters', async () => {
mockClient.authConfigs.list.mockResolvedValueOnce(mockListResponse);
const result = await authConfigs.list();
expect(mockClient.authConfigs.list).toHaveBeenCalledWith(
{
cursor: undefined,
is_composio_managed: undefined,
limit: undefined,
search: undefined,
show_disabled: undefined,
toolkit_slug: undefined,
},
undefined
);
expect(result).toEqual({
items: [mockTransformedAuthConfigResponse],
nextCursor: 'next_cursor_123',
totalPages: 1,
});
});
it('should list auth configs with query parameters', async () => {
mockClient.authConfigs.list.mockResolvedValueOnce(mockListResponse);
const query = {
cursor: 'cursor_123',
isComposioManaged: true,
limit: 10,
search: 'github config',
showDisabled: true,
toolkit: 'github',
};
const result = await authConfigs.list(query);
expect(mockClient.authConfigs.list).toHaveBeenCalledWith(
{
cursor: 'cursor_123',
is_composio_managed: true,
limit: 10,
search: 'github config',
show_disabled: true,
toolkit_slug: 'github',
},
undefined
);
expect(result).toEqual({
items: [mockTransformedAuthConfigResponse],
nextCursor: 'next_cursor_123',
totalPages: 1,
});
});
it('should throw ValidationError for invalid query parameters', async () => {
const invalidQuery = {
limit: 'invalid_limit', // Should be number
};
await expect(
// @ts-expect-error - Testing invalid input
authConfigs.list(invalidQuery)
).rejects.toThrow(); // Just expect any error since Zod throws ZodError directly
});
it('should handle empty list response', async () => {
const emptyResponse = {
items: [],
next_cursor: null, // Changed back to null as schema expects nullable string
total_pages: 0,
};
mockClient.authConfigs.list.mockResolvedValueOnce(emptyResponse);
const result = await authConfigs.list();
expect(result).toEqual({
items: [],
nextCursor: null,
totalPages: 0,
});
});
});
describe('create', () => {
const mockCreateResponse = {
auth_config: {
id: 'auth_12345',
auth_scheme: 'OAUTH2',
is_composio_managed: true,
},
toolkit: {
slug: 'github',
},
};
it('should create auth config with default Composio managed type', async () => {
mockClient.authConfigs.create.mockResolvedValueOnce(mockCreateResponse);
const result = await authConfigs.create('github');
expect(mockClient.authConfigs.create).toHaveBeenCalledWith(
{
toolkit: {
slug: 'github',
},
auth_config: {
type: 'use_composio_managed_auth',
credentials: undefined,
name: undefined,
tool_access_config: undefined,
},
},
undefined
);
expect(result).toEqual({
id: 'auth_12345',
authScheme: 'OAUTH2',
isComposioManaged: true,
toolkit: 'github',
});
});
it('should create custom auth config with credentials', async () => {
mockClient.authConfigs.create.mockResolvedValueOnce({
...mockCreateResponse,
auth_config: {
...mockCreateResponse.auth_config,
is_composio_managed: false,
},
});
const options = {
type: 'use_custom_auth' as const,
name: 'Custom GitHub Auth',
tool_access_config: {
tools_for_connected_account_creation: undefined,
},
authScheme: 'OAUTH2' as const,
credentials: {
client_id: 'test_client_id',
client_secret: 'test_client_secret',
},
};
const result = await authConfigs.create('github', options);
expect(mockClient.authConfigs.create).toHaveBeenCalledWith(
{
toolkit: {
slug: 'github',
},
auth_config: {
type: 'use_custom_auth',
name: 'Custom GitHub Auth',
authScheme: 'OAUTH2',
credentials: {
client_id: 'test_client_id',
client_secret: 'test_client_secret',
},
proxy_config: undefined,
tool_access_config: undefined,
},
},
undefined
);
expect(result).toEqual({
id: 'auth_12345',
authScheme: 'OAUTH2',
isComposioManaged: false,
toolkit: 'github',
});
});
it('should create Composio managed auth config with optional name', async () => {
mockClient.authConfigs.create.mockResolvedValueOnce(mockCreateResponse);
const options = {
type: 'use_composio_managed_auth' as const,
name: 'My GitHub Config',
tool_access_config: {
tools_for_connected_account_creation: undefined,
},
credentials: {
custom_field: 'value',
},
};
const result = await authConfigs.create('github', options);
expect(mockClient.authConfigs.create).toHaveBeenCalledWith(
{
toolkit: {
slug: 'github',
},
auth_config: {
type: 'use_composio_managed_auth',
credentials: {
custom_field: 'value',
},
name: 'My GitHub Config',
tool_access_config: undefined,
},
},
undefined
);
expect(result).toEqual({
id: 'auth_12345',
authScheme: 'OAUTH2',
isComposioManaged: true,
toolkit: 'github',
});
});
it('should throw ValidationError for invalid create options', async () => {
const invalidOptions = {
type: 'use_custom_auth' as const,
// Missing required authScheme for custom auth
credentials: {
client_id: 'test',
},
};
await expect(
// @ts-expect-error - Testing invalid input
authConfigs.create('github', invalidOptions)
).rejects.toThrow(ValidationError);
});
});
describe('get', () => {
it('should retrieve auth config by nanoid', async () => {
mockClient.authConfigs.retrieve.mockResolvedValueOnce(mockComposioAuthConfigResponse);
const result = await authConfigs.get('auth_12345');
expect(mockClient.authConfigs.retrieve).toHaveBeenCalledWith('auth_12345', undefined);
expect(result).toEqual(mockTransformedAuthConfigResponse);
});
it('should handle API errors when retrieving auth config', async () => {
const apiError = new Error('Auth config not found');
mockClient.authConfigs.retrieve.mockRejectedValueOnce(apiError);
await expect(authConfigs.get('nonexistent_auth')).rejects.toThrow('Auth config not found');
});
it('should retrieve auth config with isEnabledForToolRouter set to true', async () => {
const responseWithToolRouter = {
...mockComposioAuthConfigResponse,
is_enabled_for_tool_router: true,
};
mockClient.authConfigs.retrieve.mockResolvedValueOnce(responseWithToolRouter);
const result = await authConfigs.get('auth_12345');
expect(result.isEnabledForToolRouter).toBe(true);
});
it('should retrieve auth config with isEnabledForToolRouter set to false', async () => {
const responseWithoutToolRouter = {
...mockComposioAuthConfigResponse,
is_enabled_for_tool_router: false,
};
mockClient.authConfigs.retrieve.mockResolvedValueOnce(responseWithoutToolRouter);
const result = await authConfigs.get('auth_12345');
expect(result.isEnabledForToolRouter).toBe(false);
});
it('should retrieve auth config with isEnabledForToolRouter undefined', async () => {
const responseWithoutField = {
...mockComposioAuthConfigResponse,
is_enabled_for_tool_router: undefined,
};
mockClient.authConfigs.retrieve.mockResolvedValueOnce(responseWithoutField);
const result = await authConfigs.get('auth_12345');
expect(result.isEnabledForToolRouter).toBeUndefined();
});
});
describe('update', () => {
const mockUpdateResponse: AuthConfigUpdateResponse = {
id: 'auth_12345',
status: 'success',
};
it('should update custom auth config with credentials', async () => {
mockClient.authConfigs.update.mockResolvedValueOnce(mockUpdateResponse);
const updateData = {
type: 'custom' as const,
credentials: {
client_id: 'new_client_id',
client_secret: 'new_client_secret',
},
};
const result = await authConfigs.update('auth_12345', updateData);
expect(mockClient.authConfigs.update).toHaveBeenCalledWith(
'auth_12345',
{
type: 'custom',
credentials: {
client_id: 'new_client_id',
client_secret: 'new_client_secret',
},
tool_access_config: {
tools_for_connected_account_creation: undefined,
},
},
undefined
);
expect(result).toEqual(mockUpdateResponse);
});
it('should update default auth config with scopes', async () => {
mockClient.authConfigs.update.mockResolvedValueOnce(mockUpdateResponse);
const updateData = {
type: 'default' as const,
scopes: 'read:user,repo',
};
const result = await authConfigs.update('auth_12345', updateData);
expect(mockClient.authConfigs.update).toHaveBeenCalledWith(
'auth_12345',
{
type: 'default',
scopes: 'read:user,repo',
tool_access_config: {
tools_for_connected_account_creation: undefined,
},
},
undefined
);
expect(result).toEqual(mockUpdateResponse);
});
it('should handle optional restrictToFollowingTools parameter', async () => {
mockClient.authConfigs.update.mockResolvedValueOnce(mockUpdateResponse);
const updateData = {
type: 'custom' as const,
credentials: {
api_key: 'new_api_key',
},
};
const result = await authConfigs.update('auth_12345', updateData);
expect(mockClient.authConfigs.update).toHaveBeenCalledWith(
'auth_12345',
{
type: 'custom',
credentials: {
api_key: 'new_api_key',
},
tool_access_config: {
tools_for_connected_account_creation: undefined,
},
},
undefined
);
expect(result).toEqual(mockUpdateResponse);
});
it('should throw ValidationError for invalid update data', async () => {
const invalidUpdateData = {
type: 'invalid_type' as const,
credentials: {},
};
await expect(
// @ts-expect-error - Testing invalid input
authConfigs.update('auth_12345', invalidUpdateData)
).rejects.toThrow(ValidationError);
});
it('should handle API errors during update', async () => {
const apiError = new Error('Update failed');
mockClient.authConfigs.update.mockRejectedValueOnce(apiError);
const updateData = {
type: 'custom' as const,
credentials: {
api_key: 'key',
},
};
await expect(authConfigs.update('auth_12345', updateData)).rejects.toThrow('Update failed');
});
});
describe('delete', () => {
const mockDeleteResponse: AuthConfigDeleteResponse = {
id: 'auth_12345',
status: 'deleted',
};
it('should delete auth config by nanoid', async () => {
mockClient.authConfigs.delete.mockResolvedValueOnce(mockDeleteResponse);
const result = await authConfigs.delete('auth_12345');
expect(mockClient.authConfigs.delete).toHaveBeenCalledWith(
'auth_12345',
undefined,
undefined
);
expect(result).toEqual(mockDeleteResponse);
});
it('should handle API errors during deletion', async () => {
const apiError = new Error('Delete failed');
mockClient.authConfigs.delete.mockRejectedValueOnce(apiError);
await expect(authConfigs.delete('auth_12345')).rejects.toThrow('Delete failed');
});
});
describe('updateStatus', () => {
const mockStatusUpdateResponse: AuthConfigUpdateStatusResponse = {
id: 'auth_12345',
status: 'ENABLED',
};
it('should update status to ENABLED', async () => {
mockClient.authConfigs.updateStatus.mockResolvedValueOnce(mockStatusUpdateResponse);
const result = await authConfigs.updateStatus('ENABLED', 'auth_12345');
expect(mockClient.authConfigs.updateStatus).toHaveBeenCalledWith(
'ENABLED',
{
nanoid: 'auth_12345',
},
undefined
);
expect(result).toEqual(mockStatusUpdateResponse);
});
it('should update status to DISABLED', async () => {
const disabledResponse = {
id: 'auth_12345',
status: 'DISABLED',
};
mockClient.authConfigs.updateStatus.mockResolvedValueOnce(disabledResponse);
const result = await authConfigs.updateStatus('DISABLED', 'auth_12345');
expect(mockClient.authConfigs.updateStatus).toHaveBeenCalledWith(
'DISABLED',
{
nanoid: 'auth_12345',
},
undefined
);
expect(result).toEqual(disabledResponse);
});
it('should handle API errors during status update', async () => {
const apiError = new Error('Status update failed');
mockClient.authConfigs.updateStatus.mockRejectedValueOnce(apiError);
await expect(authConfigs.updateStatus('ENABLED', 'auth_12345')).rejects.toThrow(
'Status update failed'
);
});
});
describe('enable', () => {
const mockStatusUpdateResponse: AuthConfigUpdateStatusResponse = {
id: 'auth_12345',
status: 'ENABLED',
};
it('should enable auth config', async () => {
mockClient.authConfigs.updateStatus.mockResolvedValueOnce(mockStatusUpdateResponse);
const result = await authConfigs.enable('auth_12345');
expect(mockClient.authConfigs.updateStatus).toHaveBeenCalledWith(
'ENABLED',
{
nanoid: 'auth_12345',
},
undefined
);
expect(result).toEqual(mockStatusUpdateResponse);
});
it('should handle API errors during enable', async () => {
const apiError = new Error('Enable failed');
mockClient.authConfigs.updateStatus.mockRejectedValueOnce(apiError);
await expect(authConfigs.enable('auth_12345')).rejects.toThrow('Enable failed');
});
});
describe('disable', () => {
const mockStatusUpdateResponse: AuthConfigUpdateStatusResponse = {
id: 'auth_12345',
status: 'DISABLED',
};
it('should disable auth config', async () => {
mockClient.authConfigs.updateStatus.mockResolvedValueOnce(mockStatusUpdateResponse);
const result = await authConfigs.disable('auth_12345');
expect(mockClient.authConfigs.updateStatus).toHaveBeenCalledWith(
'DISABLED',
{
nanoid: 'auth_12345',
},
undefined
);
expect(result).toEqual(mockStatusUpdateResponse);
});
it('should handle API errors during disable', async () => {
const apiError = new Error('Disable failed');
mockClient.authConfigs.updateStatus.mockRejectedValueOnce(apiError);
await expect(authConfigs.disable('auth_12345')).rejects.toThrow('Disable failed');
});
});
describe('error handling', () => {
// We have relaxed the validation for auth config response, so we don't throw an error when parsing fails
// it('should preserve ValidationError details when parsing fails', async () => {
// const invalidResponse = {
// id: null, // Invalid: id should be string
// name: 'Test Config',
// no_of_connections: 5,
// status: 'ENABLED',
// deprecated_params: {
// default_connector_id: null,
// },
// type: 'custom',
// toolkit: {
// logo: 'valid_logo',
// slug: 'valid_slug',
// },
// uuid: 'uuid-123',
// } as unknown as ComposioAuthConfigRetrieveResponse;
// expect(() => {
// // @ts-expect-error - Accessing private method for testing
// authConfigs.parseAuthConfigRetrieveResponse(invalidResponse);
// }).toThrow(ValidationError);
// try {
// // @ts-expect-error - Accessing private method for testing
// authConfigs.parseAuthConfigRetrieveResponse(invalidResponse);
// } catch (error) {
// expect(error).toBeInstanceOf(ValidationError);
// expect((error as ValidationError).message).toContain(
// 'Failed to parse auth config response'
// );
// expect((error as ValidationError).cause).toBeInstanceOf(ZodError);
// }
// });
it('should handle network errors gracefully', async () => {
const networkError = new Error('Network error');
mockClient.authConfigs.list.mockRejectedValueOnce(networkError);
await expect(authConfigs.list()).rejects.toThrow('Network error');
});
});
describe('edge cases', () => {
it('should handle auth config with minimal fields', () => {
const minimalResponse = {
id: 'auth_minimal',
name: 'Minimal Config',
no_of_connections: 0,
status: 'DISABLED',
type: 'default',
toolkit: {
logo: '',
slug: 'minimal-toolkit',
},
} as ComposioAuthConfigRetrieveResponse;
const result = transformAuthConfigRetrieveResponse(minimalResponse);
expect(result).toMatchObject({
id: 'auth_minimal',
name: 'Minimal Config',
noOfConnections: 0,
status: 'DISABLED',
toolkit: {
logo: '',
slug: 'minimal-toolkit',
},
});
});
it('should handle large credential objects', async () => {
const largeCredentials = {
field1: 'value1',
field2: 'value2',
field3: { nested: 'object' },
field4: ['array', 'values'],
field5: 12345,
field6: true,
};
const updateData = {
type: 'custom' as const,
credentials: largeCredentials,
};
const mockUpdateResponse: AuthConfigUpdateResponse = {
id: 'auth_12345',
status: 'success',
};
mockClient.authConfigs.update.mockResolvedValueOnce(mockUpdateResponse);
const result = await authConfigs.update('auth_12345', updateData);
expect(mockClient.authConfigs.update).toHaveBeenCalledWith(
'auth_12345',
{
type: 'custom',
credentials: largeCredentials,
tool_access_config: {
tools_for_connected_account_creation: undefined,
},
},
undefined
);
expect(result).toEqual(mockUpdateResponse);
});
it('should handle very long scopes string', async () => {
const longScopes = Array(100).fill('scope').join(',');
const updateData = {
type: 'default' as const,
scopes: longScopes,
};
const mockUpdateResponse: AuthConfigUpdateResponse = {
id: 'auth_12345',
status: 'success',
};
mockClient.authConfigs.update.mockResolvedValueOnce(mockUpdateResponse);
const result = await authConfigs.update('auth_12345', updateData);
expect(mockClient.authConfigs.update).toHaveBeenCalledWith(
'auth_12345',
{
type: 'default',
scopes: longScopes,
tool_access_config: {
tools_for_connected_account_creation: undefined,
},
},
undefined
);
expect(result).toEqual(mockUpdateResponse);
});
});
});