Files
waggle-os/packages/agent/tests/connectors/gcal-connector.test.ts
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

233 lines
7.4 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { GoogleCalendarConnector } from '../../src/connectors/gcal-connector.js';
import type { VaultStore } from '@waggle/core';
function createMockVault(opts?: {
accessToken?: string;
refreshToken?: string;
expiresAt?: string;
clientId?: string;
clientSecret?: string;
}): VaultStore {
return {
getConnectorCredential: vi.fn((id: string) => {
if (id === 'gcal' && opts?.accessToken) {
return {
value: opts.accessToken,
type: 'oauth2',
isExpired: false,
refreshToken: opts.refreshToken,
expiresAt: opts.expiresAt,
};
}
return null;
}),
get: vi.fn((name: string) => {
if (name === 'connector:gcal:client_id' && opts?.clientId) return { value: opts.clientId };
if (name === 'connector:gcal:client_secret' && opts?.clientSecret) return { value: opts.clientSecret };
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;
}
describe('GoogleCalendarConnector', () => {
let connector: GoogleCalendarConnector;
let originalFetch: typeof globalThis.fetch;
beforeEach(() => {
connector = new GoogleCalendarConnector();
originalFetch = globalThis.fetch;
});
afterEach(() => {
globalThis.fetch = originalFetch;
});
it('implements WaggleConnector interface', () => {
expect(connector.id).toBe('gcal');
expect(connector.name).toBe('Google Calendar');
expect(connector.authType).toBe('oauth2');
expect(connector.actions).toHaveLength(4);
});
it('connect() retrieves OAuth tokens from vault', async () => {
const vault = createMockVault({
accessToken: 'ya29.test_token',
refreshToken: 'rt_test',
expiresAt: new Date(Date.now() + 3600000).toISOString(),
});
await connector.connect(vault);
expect(vault.getConnectorCredential).toHaveBeenCalledWith('gcal');
});
it('healthCheck() validates access token', async () => {
const vault = createMockVault({ accessToken: 'ya29.test_token' });
await connector.connect(vault);
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true, json: async () => ({ items: [] }),
}) as unknown as typeof fetch;
const health = await connector.healthCheck();
expect(health.status).toBe('connected');
});
it('healthCheck() auto-refreshes expired access token', async () => {
const vault = createMockVault({
accessToken: 'ya29.expired',
refreshToken: 'rt_test',
expiresAt: '2020-01-01T00:00:00.000Z', // Expired
clientId: 'client_123',
clientSecret: 'secret_456',
});
await connector.connect(vault);
globalThis.fetch = vi.fn()
// First call: token refresh
.mockResolvedValueOnce({
ok: true, json: async () => ({
access_token: 'ya29.refreshed',
expires_in: 3600,
}),
})
// Second call: calendar list (health check)
.mockResolvedValueOnce({
ok: true, json: async () => ({ items: [] }),
}) as unknown as typeof fetch;
const health = await connector.healthCheck();
expect(health.status).toBe('connected');
// Verify tokens were stored back in vault
expect(vault.setConnectorCredential).toHaveBeenCalledWith('gcal', expect.objectContaining({
type: 'oauth2',
value: 'ya29.refreshed',
}));
});
it('execute(list_events) returns events', async () => {
const vault = createMockVault({ accessToken: 'ya29.test_token' });
await connector.connect(vault);
const mockEvents = { items: [{ summary: 'Meeting', start: { dateTime: '2026-03-18T10:00:00Z' } }] };
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true, json: async () => mockEvents,
}) as unknown as typeof fetch;
const result = await connector.execute('list_events', {});
expect(result.success).toBe(true);
expect((result.data as Record<string, unknown>).items).toHaveLength(1);
});
it('execute(create_event) creates event (medium risk)', async () => {
const vault = createMockVault({ accessToken: 'ya29.test_token' });
await connector.connect(vault);
const mockEvent = { id: 'evt_123', summary: 'Team standup' };
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true, json: async () => mockEvent,
}) as unknown as typeof fetch;
const result = await connector.execute('create_event', {
summary: 'Team standup',
start: '2026-03-19T09:00:00Z',
end: '2026-03-19T09:15:00Z',
});
expect(result.success).toBe(true);
expect((result.data as Record<string, unknown>).id).toBe('evt_123');
});
it('execute(find_free_time) returns available slots', async () => {
const vault = createMockVault({ accessToken: 'ya29.test_token' });
await connector.connect(vault);
const mockFreeBusy = { calendars: { primary: { busy: [] } } };
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true, json: async () => mockFreeBusy,
}) as unknown as typeof fetch;
const result = await connector.execute('find_free_time', {
duration: 30,
timeMin: '2026-03-19T08:00:00Z',
timeMax: '2026-03-19T18:00:00Z',
});
expect(result.success).toBe(true);
});
it('toDefinition() correctly reports OAuth2 auth type', () => {
const def = connector.toDefinition('connected');
expect(def.authType).toBe('oauth2');
expect(def.tools).toContain('connector_gcal_list_events');
expect(def.tools).toContain('connector_gcal_create_event');
expect(def.tools).toHaveLength(4);
});
});
describe('OAuth2 token refresh', () => {
let connector: GoogleCalendarConnector;
let originalFetch: typeof globalThis.fetch;
beforeEach(() => {
connector = new GoogleCalendarConnector();
originalFetch = globalThis.fetch;
});
afterEach(() => {
globalThis.fetch = originalFetch;
});
it('stores new access + refresh tokens back in vault', async () => {
const vault = createMockVault({
accessToken: 'ya29.expired',
refreshToken: 'rt_test',
expiresAt: '2020-01-01T00:00:00.000Z',
clientId: 'client_123',
clientSecret: 'secret_456',
});
await connector.connect(vault);
globalThis.fetch = vi.fn()
.mockResolvedValueOnce({
ok: true, json: async () => ({
access_token: 'ya29.new',
expires_in: 3600,
refresh_token: 'rt_new',
}),
})
.mockResolvedValueOnce({
ok: true, json: async () => ({ items: [] }),
}) as unknown as typeof fetch;
await connector.execute('list_events', {});
expect(vault.setConnectorCredential).toHaveBeenCalledWith('gcal', expect.objectContaining({
value: 'ya29.new',
refreshToken: 'rt_new',
}));
});
it('returns error when refresh token is invalid', async () => {
const vault = createMockVault({
accessToken: 'ya29.expired',
refreshToken: 'rt_invalid',
expiresAt: '2020-01-01T00:00:00.000Z',
clientId: 'client_123',
clientSecret: 'secret_456',
});
await connector.connect(vault);
globalThis.fetch = vi.fn().mockResolvedValue({
ok: false, status: 400, text: async () => 'invalid_grant',
}) as unknown as typeof fetch;
const result = await connector.execute('list_events', {});
expect(result.success).toBe(false);
expect(result.error).toContain('Token refresh failed');
});
});