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

284 lines
9.4 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { createConnectionRequest } from '../../src/models/ConnectionRequest';
import { ConnectionRequest } from '../../src/types/connectionRequest.types';
import ComposioClient, { ComposioError } from '@composio/client';
import { ConnectionRequestFailedError, ConnectionRequestTimeoutError } from '../../src/errors';
import { ConnectedAccountStatuses } from '../../src/types/connectedAccounts.types';
import { AuthSchemeTypes } from '../../src/types/authConfigs.types';
// Mock ComposioClient
const mockClient = {
connectedAccounts: {
retrieve: vi.fn(),
},
};
describe('ConnectionRequest', () => {
let connectionRequest: ConnectionRequest;
const connectedAccountId = 'conn_123';
const redirectUrl = 'https://example.com/callback';
beforeEach(() => {
vi.clearAllMocks();
// Reset timers before each test
vi.useRealTimers();
});
describe('createConnectionRequest', () => {
it('should create an instance with an INITIATED status', () => {
connectionRequest = createConnectionRequest(
mockClient as unknown as ComposioClient,
connectedAccountId,
ConnectedAccountStatuses.INITIATED,
redirectUrl
);
expect(connectionRequest).toHaveProperty('id', connectedAccountId);
expect(connectionRequest).toHaveProperty('status', ConnectedAccountStatuses.INITIATED);
expect(connectionRequest).toHaveProperty('redirectUrl', redirectUrl);
});
it('should create an instance without a callbackUrl', () => {
connectionRequest = createConnectionRequest(
mockClient as unknown as ComposioClient,
connectedAccountId
);
expect(connectionRequest).toHaveProperty('redirectUrl', undefined);
});
});
describe('waitForConnection', () => {
it('should immediately resolve if status is already ACTIVE', async () => {
// Create the connection request with ACTIVE status
connectionRequest = createConnectionRequest(
mockClient as unknown as ComposioClient,
connectedAccountId,
ConnectedAccountStatuses.ACTIVE,
redirectUrl
);
// Mock the retrieve response
const mockResponse = {
id: connectedAccountId,
status: ConnectedAccountStatuses.ACTIVE,
auth_config: {
id: 'auth_config_123',
is_composio_managed: true,
is_disabled: false,
},
state: {
authScheme: AuthSchemeTypes.OAUTH2,
val: {
status: 'ACTIVE',
access_token: 'access_token_123',
token_type: 'Bearer',
},
},
user_id: 'user_123',
data: {},
params: {},
is_disabled: false,
created_at: '2023-01-01T00:00:00Z',
updated_at: '2023-01-01T00:00:00Z',
status_reason: null,
toolkit: {
slug: 'test-toolkit',
},
};
mockClient.connectedAccounts.retrieve.mockResolvedValueOnce(mockResponse);
const result = await connectionRequest.waitForConnection();
expect(mockClient.connectedAccounts.retrieve).toHaveBeenCalledWith(connectedAccountId);
expect(mockClient.connectedAccounts.retrieve).toHaveBeenCalledTimes(1);
expect(result).toHaveProperty('id', connectedAccountId);
expect(result).toHaveProperty('status', ConnectedAccountStatuses.ACTIVE);
expect(result).toHaveProperty('state.authScheme', 'OAUTH2');
});
it('should poll until status becomes ACTIVE', async () => {
// Use fake timers to control timing
vi.useFakeTimers();
// Create the connection request with INITIATED status
connectionRequest = createConnectionRequest(
mockClient as unknown as ComposioClient,
connectedAccountId,
ConnectedAccountStatuses.INITIATED,
redirectUrl
);
// Mock initial "initiated" response
const pendingResponse = {
id: connectedAccountId,
status: ConnectedAccountStatuses.INITIATED,
auth_config: {
id: 'auth_config_123',
auth_scheme: 'OAUTH2',
is_composio_managed: true,
is_disabled: false,
},
user_id: 'user_123',
data: {},
params: {},
is_disabled: false,
created_at: '2023-01-01T00:00:00Z',
updated_at: '2023-01-01T00:00:00Z',
status_reason: null,
toolkit: {
slug: 'test-toolkit',
},
};
// Mock "active" response for the second call
const activeResponse = {
...pendingResponse,
status: ConnectedAccountStatuses.ACTIVE,
};
mockClient.connectedAccounts.retrieve
.mockResolvedValueOnce(pendingResponse)
.mockResolvedValueOnce(activeResponse);
// Start the waitForConnection call but don't await it yet
const connectionPromise = connectionRequest.waitForConnection();
// Advance timer by 1 second (first poll)
await vi.advanceTimersByTimeAsync(1000);
// Advance timer by 1 more second (second poll)
await vi.advanceTimersByTimeAsync(1000);
// Resolve the promise
const result = await connectionPromise;
expect(mockClient.connectedAccounts.retrieve).toHaveBeenCalledWith(connectedAccountId);
expect(mockClient.connectedAccounts.retrieve).toHaveBeenCalledTimes(2);
expect(result).toHaveProperty('id', connectedAccountId);
expect(result).toHaveProperty('status', ConnectedAccountStatuses.ACTIVE);
});
it('should throw ConnectionRequestTimeoutError if the request times out', async () => {
// Create connection request with INITIATED status
connectionRequest = createConnectionRequest(
mockClient as unknown as ComposioClient,
connectedAccountId,
ConnectedAccountStatuses.INITIATED,
redirectUrl
);
// Mock initiated response that never changes to active
const initiatedResponse = {
id: connectedAccountId,
status: ConnectedAccountStatuses.INITIATED,
auth_config: {
id: 'auth_config_123',
auth_scheme: 'OAUTH2',
is_composio_managed: true,
is_disabled: false,
},
user_id: 'user_123',
data: {},
params: {},
is_disabled: false,
created_at: '2023-01-01T00:00:00Z',
updated_at: '2023-01-01T00:00:00Z',
status_reason: null,
toolkit: {
slug: 'test-toolkit',
},
};
mockClient.connectedAccounts.retrieve.mockResolvedValue(initiatedResponse);
// Start the waitForConnection call with a short timeout
try {
await connectionRequest.waitForConnection(3000);
} catch (error) {
expect(error).toBeInstanceOf(ConnectionRequestTimeoutError);
}
});
it('should reject if the API call fails', async () => {
// Use fake timers to control timing
vi.useFakeTimers();
// Create connection request with INITIATED status
connectionRequest = createConnectionRequest(
mockClient as unknown as ComposioClient,
connectedAccountId,
ConnectedAccountStatuses.INITIATED,
redirectUrl
);
// Mock the API call to fail
const apiError = new ComposioError('API error');
mockClient.connectedAccounts.retrieve.mockRejectedValueOnce(apiError);
// Start the waitForConnection call but don't await it yet
const connectionPromise = connectionRequest.waitForConnection(50000);
// Now check for the rejection
await expect(connectionPromise).rejects.toBe(apiError);
});
it('should fail when the connection is revoked while polling', async () => {
connectionRequest = createConnectionRequest(
mockClient as unknown as ComposioClient,
connectedAccountId,
ConnectedAccountStatuses.INITIATED,
redirectUrl
);
const revokedResponse = {
id: connectedAccountId,
status: ConnectedAccountStatuses.REVOKED,
status_reason: 'revoked by user',
};
mockClient.connectedAccounts.retrieve.mockResolvedValueOnce(revokedResponse);
await expect(connectionRequest.waitForConnection(3000)).rejects.toThrow(
ConnectionRequestFailedError
);
});
});
describe('serialization methods', () => {
it('should return a JSON-serializable object with toJSON()', () => {
// Create a connection request instance
connectionRequest = createConnectionRequest(
mockClient as unknown as ComposioClient,
connectedAccountId,
ConnectedAccountStatuses.INITIATED,
redirectUrl
);
const jsonObj = connectionRequest.toJSON();
expect(jsonObj).toHaveProperty('id', connectedAccountId);
expect(jsonObj).toHaveProperty('status', ConnectedAccountStatuses.INITIATED);
expect(jsonObj).toHaveProperty('redirectUrl', redirectUrl);
});
it('should return a formatted JSON string with toString()', () => {
// Create a connection request instance
connectionRequest = createConnectionRequest(
mockClient as unknown as ComposioClient,
connectedAccountId,
ConnectedAccountStatuses.INITIATED,
redirectUrl
);
const jsonString = connectionRequest.toString();
expect(typeof jsonString).toBe('string');
const parsedObj = JSON.parse(jsonString);
expect(parsedObj).toHaveProperty('id', connectedAccountId);
expect(parsedObj).toHaveProperty('status', ConnectedAccountStatuses.INITIATED);
expect(parsedObj).toHaveProperty('redirectUrl', redirectUrl);
});
});
});