This commit is contained in:
490
packages/agent/tests/connectors/connectors-crm-data.test.ts
Normal file
490
packages/agent/tests/connectors/connectors-crm-data.test.ts
Normal file
@@ -0,0 +1,490 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { HubSpotConnector } from '../../src/connectors/hubspot-connector.js';
|
||||
import { SalesforceConnector } from '../../src/connectors/salesforce-connector.js';
|
||||
import { PipedriveConnector } from '../../src/connectors/pipedrive-connector.js';
|
||||
import { AirtableConnector } from '../../src/connectors/airtable-connector.js';
|
||||
import { GitLabConnector } from '../../src/connectors/gitlab-connector.js';
|
||||
import { BitbucketConnector } from '../../src/connectors/bitbucket-connector.js';
|
||||
import { DropboxConnector } from '../../src/connectors/dropbox-connector.js';
|
||||
import { PostgresConnector } from '../../src/connectors/postgres-connector.js';
|
||||
import type { VaultStore } from '@waggle/core';
|
||||
|
||||
function createMockVault(connectorId: string, cred?: { value: string; isExpired: boolean }, extras?: Record<string, string>): VaultStore {
|
||||
return {
|
||||
getConnectorCredential: vi.fn((id: string) => {
|
||||
if (id === connectorId && cred) return { ...cred, type: 'bearer' };
|
||||
return null;
|
||||
}),
|
||||
get: vi.fn((key: string) => {
|
||||
if (extras && extras[key]) return { value: extras[key] };
|
||||
return null;
|
||||
}),
|
||||
set: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
list: vi.fn(() => []),
|
||||
has: vi.fn(() => false),
|
||||
setConnectorCredential: vi.fn(),
|
||||
migrateFromConfig: vi.fn(() => 0),
|
||||
} as unknown as VaultStore;
|
||||
}
|
||||
|
||||
// ── CRM Connectors ──────────────────────────────────────────
|
||||
|
||||
describe('HubSpotConnector', () => {
|
||||
let connector: HubSpotConnector;
|
||||
let originalFetch: typeof globalThis.fetch;
|
||||
|
||||
beforeEach(() => {
|
||||
connector = new HubSpotConnector();
|
||||
originalFetch = globalThis.fetch;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
it('has correct identity', () => {
|
||||
expect(connector.id).toBe('hubspot');
|
||||
expect(connector.name).toBe('HubSpot');
|
||||
expect(connector.service).toBe('hubspot.com');
|
||||
expect(connector.authType).toBe('bearer');
|
||||
expect(connector.actions).toHaveLength(7);
|
||||
});
|
||||
|
||||
it('actions include expected names', () => {
|
||||
const names = connector.actions.map(a => a.name);
|
||||
expect(names).toContain('list_contacts');
|
||||
expect(names).toContain('get_contact');
|
||||
expect(names).toContain('create_contact');
|
||||
expect(names).toContain('search_contacts');
|
||||
expect(names).toContain('list_deals');
|
||||
expect(names).toContain('create_deal');
|
||||
expect(names).toContain('list_companies');
|
||||
});
|
||||
|
||||
it('execute returns error when not connected', async () => {
|
||||
const result = await connector.execute('list_contacts', {});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('Not connected');
|
||||
});
|
||||
|
||||
it('toDefinition maps tools correctly', () => {
|
||||
const def = connector.toDefinition('connected');
|
||||
expect(def.tools).toContain('connector_hubspot_list_contacts');
|
||||
expect(def.tools).toContain('connector_hubspot_create_deal');
|
||||
expect(def.tools).toHaveLength(7);
|
||||
});
|
||||
|
||||
it('execute(list_contacts) returns data when connected', async () => {
|
||||
const vault = createMockVault('hubspot', { value: 'test-token', isExpired: false });
|
||||
await connector.connect(vault);
|
||||
|
||||
const mockData = { results: [{ id: '1', properties: { email: 'test@example.com' } }] };
|
||||
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => mockData }) as unknown as typeof fetch;
|
||||
|
||||
const result = await connector.execute('list_contacts', { limit: 5 });
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toEqual(mockData);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SalesforceConnector', () => {
|
||||
let connector: SalesforceConnector;
|
||||
let originalFetch: typeof globalThis.fetch;
|
||||
|
||||
beforeEach(() => {
|
||||
connector = new SalesforceConnector();
|
||||
originalFetch = globalThis.fetch;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
it('has correct identity', () => {
|
||||
expect(connector.id).toBe('salesforce');
|
||||
expect(connector.name).toBe('Salesforce');
|
||||
expect(connector.service).toBe('salesforce.com');
|
||||
expect(connector.authType).toBe('bearer');
|
||||
expect(connector.actions).toHaveLength(6);
|
||||
});
|
||||
|
||||
it('actions include expected names', () => {
|
||||
const names = connector.actions.map(a => a.name);
|
||||
expect(names).toContain('search');
|
||||
expect(names).toContain('list_contacts');
|
||||
expect(names).toContain('get_record');
|
||||
expect(names).toContain('create_record');
|
||||
expect(names).toContain('update_record');
|
||||
expect(names).toContain('list_opportunities');
|
||||
});
|
||||
|
||||
it('execute returns error when not connected (no token)', async () => {
|
||||
const result = await connector.execute('search', { query: 'SELECT Id FROM Account' });
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('Not connected');
|
||||
});
|
||||
|
||||
it('execute returns error when no instance URL', async () => {
|
||||
const vault = createMockVault('salesforce', { value: 'token123', isExpired: false });
|
||||
await connector.connect(vault);
|
||||
// Token is set but no instance_url
|
||||
const result = await connector.execute('search', { query: 'SELECT Id FROM Account' });
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('Not connected');
|
||||
});
|
||||
|
||||
it('toDefinition maps tools correctly', () => {
|
||||
const def = connector.toDefinition('connected');
|
||||
expect(def.tools).toContain('connector_salesforce_search');
|
||||
expect(def.tools).toContain('connector_salesforce_create_record');
|
||||
expect(def.tools).toHaveLength(6);
|
||||
});
|
||||
|
||||
it('execute(search) works with instance URL', async () => {
|
||||
const vault = createMockVault('salesforce', { value: 'token123', isExpired: false }, {
|
||||
'connector:salesforce:instance_url': 'https://myco.salesforce.com',
|
||||
});
|
||||
await connector.connect(vault);
|
||||
|
||||
const mockData = { records: [{ Id: '001xx', Name: 'Test Account' }], totalSize: 1 };
|
||||
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => mockData }) as unknown as typeof fetch;
|
||||
|
||||
const result = await connector.execute('search', { query: 'SELECT Id, Name FROM Account LIMIT 1' });
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toEqual(mockData);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PipedriveConnector', () => {
|
||||
let connector: PipedriveConnector;
|
||||
let originalFetch: typeof globalThis.fetch;
|
||||
|
||||
beforeEach(() => {
|
||||
connector = new PipedriveConnector();
|
||||
originalFetch = globalThis.fetch;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
it('has correct identity', () => {
|
||||
expect(connector.id).toBe('pipedrive');
|
||||
expect(connector.name).toBe('Pipedrive');
|
||||
expect(connector.service).toBe('pipedrive.com');
|
||||
expect(connector.authType).toBe('api_key');
|
||||
expect(connector.actions).toHaveLength(7);
|
||||
});
|
||||
|
||||
it('actions include expected names', () => {
|
||||
const names = connector.actions.map(a => a.name);
|
||||
expect(names).toContain('list_deals');
|
||||
expect(names).toContain('get_deal');
|
||||
expect(names).toContain('create_deal');
|
||||
expect(names).toContain('search_deals');
|
||||
expect(names).toContain('list_persons');
|
||||
expect(names).toContain('create_person');
|
||||
expect(names).toContain('list_activities');
|
||||
});
|
||||
|
||||
it('execute returns error when not connected', async () => {
|
||||
const result = await connector.execute('list_deals', {});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('Not connected');
|
||||
});
|
||||
|
||||
it('toDefinition maps tools correctly', () => {
|
||||
const def = connector.toDefinition('connected');
|
||||
expect(def.tools).toContain('connector_pipedrive_list_deals');
|
||||
expect(def.tools).toContain('connector_pipedrive_create_person');
|
||||
expect(def.tools).toHaveLength(7);
|
||||
});
|
||||
|
||||
it('execute(list_deals) returns data when connected', async () => {
|
||||
const vault = createMockVault('pipedrive', { value: 'api-key-123', isExpired: false });
|
||||
await connector.connect(vault);
|
||||
|
||||
const mockData = { success: true, data: [{ id: 1, title: 'Big Deal' }] };
|
||||
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => mockData }) as unknown as typeof fetch;
|
||||
|
||||
const result = await connector.execute('list_deals', { limit: 10 });
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data).toEqual(mockData);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Data/Storage Connectors ────────────────────────────────
|
||||
|
||||
describe('AirtableConnector', () => {
|
||||
let connector: AirtableConnector;
|
||||
let originalFetch: typeof globalThis.fetch;
|
||||
|
||||
beforeEach(() => {
|
||||
connector = new AirtableConnector();
|
||||
originalFetch = globalThis.fetch;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
it('has correct identity', () => {
|
||||
expect(connector.id).toBe('airtable');
|
||||
expect(connector.name).toBe('Airtable');
|
||||
expect(connector.service).toBe('airtable.com');
|
||||
expect(connector.authType).toBe('bearer');
|
||||
expect(connector.actions).toHaveLength(6);
|
||||
});
|
||||
|
||||
it('actions include expected names', () => {
|
||||
const names = connector.actions.map(a => a.name);
|
||||
expect(names).toContain('list_bases');
|
||||
expect(names).toContain('list_records');
|
||||
expect(names).toContain('get_record');
|
||||
expect(names).toContain('create_record');
|
||||
expect(names).toContain('update_record');
|
||||
expect(names).toContain('search_records');
|
||||
});
|
||||
|
||||
it('execute returns error when not connected', async () => {
|
||||
const result = await connector.execute('list_bases', {});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('Not connected');
|
||||
});
|
||||
|
||||
it('toDefinition maps tools correctly', () => {
|
||||
const def = connector.toDefinition('connected');
|
||||
expect(def.tools).toContain('connector_airtable_list_bases');
|
||||
expect(def.tools).toContain('connector_airtable_create_record');
|
||||
expect(def.tools).toHaveLength(6);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GitLabConnector', () => {
|
||||
let connector: GitLabConnector;
|
||||
let originalFetch: typeof globalThis.fetch;
|
||||
|
||||
beforeEach(() => {
|
||||
connector = new GitLabConnector();
|
||||
originalFetch = globalThis.fetch;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
it('has correct identity', () => {
|
||||
expect(connector.id).toBe('gitlab');
|
||||
expect(connector.name).toBe('GitLab');
|
||||
expect(connector.service).toBe('gitlab.com');
|
||||
expect(connector.authType).toBe('bearer');
|
||||
expect(connector.actions).toHaveLength(6);
|
||||
});
|
||||
|
||||
it('actions include expected names', () => {
|
||||
const names = connector.actions.map(a => a.name);
|
||||
expect(names).toContain('list_projects');
|
||||
expect(names).toContain('list_issues');
|
||||
expect(names).toContain('create_issue');
|
||||
expect(names).toContain('list_merge_requests');
|
||||
expect(names).toContain('get_file');
|
||||
expect(names).toContain('search_code');
|
||||
});
|
||||
|
||||
it('execute returns error when not connected', async () => {
|
||||
const result = await connector.execute('list_projects', {});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('Not connected');
|
||||
});
|
||||
|
||||
it('toDefinition maps tools correctly', () => {
|
||||
const def = connector.toDefinition('connected');
|
||||
expect(def.tools).toContain('connector_gitlab_list_projects');
|
||||
expect(def.tools).toContain('connector_gitlab_create_issue');
|
||||
expect(def.tools).toHaveLength(6);
|
||||
});
|
||||
|
||||
it('supports self-hosted via vault base_url', async () => {
|
||||
const vault = createMockVault('gitlab', { value: 'glpat-test', isExpired: false }, {
|
||||
'connector:gitlab:base_url': 'https://gitlab.mycompany.com/api/v4',
|
||||
});
|
||||
await connector.connect(vault);
|
||||
|
||||
const mockProjects = [{ id: 1, name: 'myproject' }];
|
||||
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => mockProjects }) as unknown as typeof fetch;
|
||||
|
||||
const result = await connector.execute('list_projects', {});
|
||||
expect(result.success).toBe(true);
|
||||
// Verify the custom base URL was used
|
||||
const callUrl = vi.mocked(globalThis.fetch).mock.calls[0][0] as string;
|
||||
expect(callUrl).toContain('gitlab.mycompany.com');
|
||||
});
|
||||
});
|
||||
|
||||
describe('BitbucketConnector', () => {
|
||||
let connector: BitbucketConnector;
|
||||
let originalFetch: typeof globalThis.fetch;
|
||||
|
||||
beforeEach(() => {
|
||||
connector = new BitbucketConnector();
|
||||
originalFetch = globalThis.fetch;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
it('has correct identity', () => {
|
||||
expect(connector.id).toBe('bitbucket');
|
||||
expect(connector.name).toBe('Bitbucket');
|
||||
expect(connector.service).toBe('bitbucket.org');
|
||||
expect(connector.authType).toBe('bearer');
|
||||
expect(connector.actions).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('actions include expected names', () => {
|
||||
const names = connector.actions.map(a => a.name);
|
||||
expect(names).toContain('list_repos');
|
||||
expect(names).toContain('list_pull_requests');
|
||||
expect(names).toContain('get_file');
|
||||
expect(names).toContain('create_pull_request');
|
||||
expect(names).toContain('list_issues');
|
||||
});
|
||||
|
||||
it('execute returns error when not connected', async () => {
|
||||
const result = await connector.execute('list_repos', {});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('Not connected');
|
||||
});
|
||||
|
||||
it('toDefinition maps tools correctly', () => {
|
||||
const def = connector.toDefinition('connected');
|
||||
expect(def.tools).toContain('connector_bitbucket_list_repos');
|
||||
expect(def.tools).toContain('connector_bitbucket_create_pull_request');
|
||||
expect(def.tools).toHaveLength(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DropboxConnector', () => {
|
||||
let connector: DropboxConnector;
|
||||
let originalFetch: typeof globalThis.fetch;
|
||||
|
||||
beforeEach(() => {
|
||||
connector = new DropboxConnector();
|
||||
originalFetch = globalThis.fetch;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
it('has correct identity', () => {
|
||||
expect(connector.id).toBe('dropbox');
|
||||
expect(connector.name).toBe('Dropbox');
|
||||
expect(connector.service).toBe('dropbox.com');
|
||||
expect(connector.authType).toBe('bearer');
|
||||
expect(connector.actions).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('actions include expected names', () => {
|
||||
const names = connector.actions.map(a => a.name);
|
||||
expect(names).toContain('list_folder');
|
||||
expect(names).toContain('get_file_metadata');
|
||||
expect(names).toContain('search_files');
|
||||
expect(names).toContain('download_file');
|
||||
expect(names).toContain('upload_file');
|
||||
});
|
||||
|
||||
it('execute returns error when not connected', async () => {
|
||||
const result = await connector.execute('list_folder', { path: '' });
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('Not connected');
|
||||
});
|
||||
|
||||
it('toDefinition maps tools correctly', () => {
|
||||
const def = connector.toDefinition('connected');
|
||||
expect(def.tools).toContain('connector_dropbox_list_folder');
|
||||
expect(def.tools).toContain('connector_dropbox_upload_file');
|
||||
expect(def.tools).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('healthCheck uses POST (Dropbox convention)', async () => {
|
||||
const vault = createMockVault('dropbox', { value: 'sl.test-token', isExpired: false });
|
||||
await connector.connect(vault);
|
||||
|
||||
globalThis.fetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({ account_id: 'dbid:ABC', name: { display_name: 'Test' } }),
|
||||
}) as unknown as typeof fetch;
|
||||
|
||||
const health = await connector.healthCheck();
|
||||
expect(health.status).toBe('connected');
|
||||
// Verify POST method was used
|
||||
const fetchCall = vi.mocked(globalThis.fetch).mock.calls[0];
|
||||
expect(fetchCall[1].method).toBe('POST');
|
||||
});
|
||||
});
|
||||
|
||||
describe('PostgresConnector', () => {
|
||||
let connector: PostgresConnector;
|
||||
|
||||
beforeEach(() => {
|
||||
connector = new PostgresConnector();
|
||||
});
|
||||
|
||||
it('has correct identity', () => {
|
||||
expect(connector.id).toBe('postgres');
|
||||
expect(connector.name).toBe('PostgreSQL');
|
||||
expect(connector.service).toBe('local');
|
||||
expect(connector.authType).toBe('api_key');
|
||||
expect(connector.actions).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('actions include expected names', () => {
|
||||
const names = connector.actions.map(a => a.name);
|
||||
expect(names).toContain('query');
|
||||
expect(names).toContain('execute');
|
||||
expect(names).toContain('list_tables');
|
||||
expect(names).toContain('describe_table');
|
||||
});
|
||||
|
||||
it('risk levels are correct', () => {
|
||||
const queryAction = connector.actions.find(a => a.name === 'query');
|
||||
const executeAction = connector.actions.find(a => a.name === 'execute');
|
||||
const listAction = connector.actions.find(a => a.name === 'list_tables');
|
||||
const describeAction = connector.actions.find(a => a.name === 'describe_table');
|
||||
expect(queryAction?.riskLevel).toBe('low');
|
||||
expect(executeAction?.riskLevel).toBe('high');
|
||||
expect(listAction?.riskLevel).toBe('low');
|
||||
expect(describeAction?.riskLevel).toBe('low');
|
||||
});
|
||||
|
||||
it('execute returns error when not connected', async () => {
|
||||
const result = await connector.execute('query', { sql: 'SELECT 1' });
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('Not connected');
|
||||
});
|
||||
|
||||
it('execute returns error when pg module is missing', async () => {
|
||||
// Mock vault with connection string but pg module will fail to import
|
||||
const vault = createMockVault('postgres', { value: 'postgresql://user:pass@localhost:5432/testdb', isExpired: false });
|
||||
await connector.connect(vault);
|
||||
|
||||
// The dynamic import of 'pg' will likely fail in test env
|
||||
// The connector should handle this gracefully
|
||||
const result = await connector.execute('query', { sql: 'SELECT 1' });
|
||||
expect(result.success).toBe(false);
|
||||
// Either "pg module not installed" or some other error — both acceptable
|
||||
expect(result.error).toBeTruthy();
|
||||
});
|
||||
|
||||
it('toDefinition maps tools correctly', () => {
|
||||
const def = connector.toDefinition('connected');
|
||||
expect(def.tools).toContain('connector_postgres_query');
|
||||
expect(def.tools).toContain('connector_postgres_execute');
|
||||
expect(def.tools).toContain('connector_postgres_list_tables');
|
||||
expect(def.tools).toContain('connector_postgres_describe_table');
|
||||
expect(def.tools).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user