Skip to main content
Glama
tally-api-mock.ts13.7 kB
/** * Tally API Mock Service * * Provides comprehensive mock responses for all Tally API endpoints * to enable testing without making actual API calls. */ import { AxiosResponse } from 'axios'; import { TallyForm, TallyFormsResponse, TallySubmission, TallySubmissionsResponse, TallyWorkspace, TallyWorkspacesResponse } from '../../models'; /** * Mock configuration options */ export interface MockConfig { /** Enable/disable mock responses */ enabled: boolean; /** Simulate network delays (ms) */ delay: number; /** Simulate error responses */ simulateErrors: boolean; /** Error rate (0-1) */ errorRate: number; /** Enable rate limiting simulation */ simulateRateLimit: boolean; /** Rate limit threshold */ rateLimitThreshold: number; } /** * Default mock configuration */ export const DEFAULT_MOCK_CONFIG: MockConfig = { enabled: false, delay: 100, simulateErrors: false, errorRate: 0.1, simulateRateLimit: false, rateLimitThreshold: 100, }; /** * Mock data fixtures */ export class MockDataFixtures { /** * Generate mock form data */ static createMockForm(overrides: Partial<TallyForm> = {}): TallyForm { return { id: 'form_123456789', title: 'Contact Us', description: 'Get in touch with our team', url: 'https://tally.so/r/form_123456789', embedUrl: 'https://tally.so/embed/form_123456789', status: 'published', createdAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z', isPublished: true, submissionsCount: 42, ...overrides, }; } /** * Generate mock forms response */ static createMockFormsResponse(count: number = 3): TallyFormsResponse { const forms: TallyForm[] = []; for (let i = 1; i <= count; i++) { forms.push(this.createMockForm({ id: `form_${i.toString().padStart(9, '0')}`, title: `Test Form ${i}`, url: `https://tally.so/r/form_${i.toString().padStart(9, '0')}`, embedUrl: `https://tally.so/embed/form_${i.toString().padStart(9, '0')}`, })); } return { forms: forms, page: 1, limit: 10, hasMore: false, }; } /** * Generate mock submission data */ static createMockSubmission(overrides: Partial<TallySubmission> = {}): TallySubmission { const base = { id: 'sub_123456789', formId: 'form_123456789', isCompleted: true, submittedAt: '2024-01-01T12:00:00Z', responses: [ { questionId: 'q1', value: 'John Doe' }, { questionId: 'q2', value: 'john.doe@example.com' }, { questionId: 'q3', value: 'I would like to learn more about your services.' }, ], ...overrides, }; return { ...base, respondentId: overrides.respondentId ?? 'resp_123456789', }; } /** * Generate mock submissions response */ static createMockSubmissionsResponse(count: number = 5): TallySubmissionsResponse { const submissions: TallySubmission[] = []; for (let i = 1; i <= count; i++) { submissions.push(this.createMockSubmission({ id: `sub_${i.toString().padStart(9, '0')}`, respondentId: `resp_${i.toString().padStart(9, '0')}`, submittedAt: new Date(Date.now() - i * 24 * 60 * 60 * 1000).toISOString(), responses: [ { questionId: 'q1', value: `User ${i}` }, { questionId: 'q2', value: `user${i}@example.com` }, { questionId: 'q3', value: `This is message ${i} from a test user.` }, ], })); } return { page: 1, limit: 10, hasMore: false, totalNumberOfSubmissionsPerFilter: { all: count, completed: count, partial: 0 }, questions: [], submissions, }; } /** * Generate mock workspace data */ static createMockWorkspace(overrides: Partial<TallyWorkspace> = {}): TallyWorkspace { return { id: 'workspace_123', name: 'My Workspace', description: 'Default workspace for testing', createdAt: '2024-01-01T00:00:00Z', updatedAt: '2024-01-01T00:00:00Z', ...overrides, }; } /** * Generate mock workspaces response */ static createMockWorkspacesResponse(count: number = 2): TallyWorkspacesResponse { const workspaces: TallyWorkspace[] = []; for (let i = 1; i <= count; i++) { workspaces.push(this.createMockWorkspace({ id: `workspace_${i}`, description: `Test workspace ${i}`, })); } return { workspaces, page: 1, limit: 10, hasMore: false, }; } } /** * Mock API response generator */ export class TallyApiMock { private config: MockConfig; private requestCount: number = 0; private rateLimitResetTime: number = Date.now() + 60000; // 1 minute from now constructor(config: Partial<MockConfig> = {}) { this.config = { ...DEFAULT_MOCK_CONFIG, ...config }; } /** * Update mock configuration */ public updateConfig(config: Partial<MockConfig>): void { this.config = { ...this.config, ...config }; } /** * Get current configuration */ public getConfig(): MockConfig { return { ...this.config }; } /** * Simulate network delay */ private async simulateDelay(): Promise<void> { if (this.config.delay > 0) { await new Promise(resolve => setTimeout(resolve, this.config.delay)); } } /** * Check if should simulate error */ private shouldSimulateError(): boolean { return this.config.simulateErrors && Math.random() < this.config.errorRate; } /** * Check rate limiting */ private checkRateLimit(): void { if (!this.config.simulateRateLimit) return; this.requestCount++; if (Date.now() > this.rateLimitResetTime) { this.requestCount = 1; this.rateLimitResetTime = Date.now() + 60000; } if (this.requestCount > this.config.rateLimitThreshold) { const error = new Error('Rate limit exceeded'); (error as any).response = { status: 429, statusText: 'Too Many Requests', data: { error: 'Rate limit exceeded', message: 'Too many requests. Please try again later.', retryAfter: Math.ceil((this.rateLimitResetTime - Date.now()) / 1000), }, headers: { 'X-RateLimit-Limit': this.config.rateLimitThreshold.toString(), 'X-RateLimit-Remaining': '0', 'X-RateLimit-Reset': this.rateLimitResetTime.toString(), }, }; throw error; } } /** * Simulate random error */ private simulateRandomError(): void { const errors = [ { status: 400, message: 'Bad Request' }, { status: 401, message: 'Unauthorized' }, { status: 403, message: 'Forbidden' }, { status: 404, message: 'Not Found' }, { status: 500, message: 'Internal Server Error' }, { status: 502, message: 'Bad Gateway' }, { status: 503, message: 'Service Unavailable' }, ]; const idx = Math.floor(Math.random() * errors.length); const randomError = errors[idx] || errors[0]!; const error = new Error(randomError.message); (error as any).response = { status: randomError.status, statusText: randomError.message, data: { error: randomError.message, message: `Simulated ${randomError.message} error`, }, }; throw error; } /** * Create mock response */ private createMockResponse<T>(data: T, status: number = 200): AxiosResponse<T> { return { data, status, statusText: 'OK', headers: { 'content-type': 'application/json', 'x-request-id': `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, }, config: {} as any, }; } /** * Mock GET /forms endpoint */ public async getForms(options: { page?: number; limit?: number; workspaceId?: string; } = {}): Promise<AxiosResponse<TallyFormsResponse>> { await this.simulateDelay(); this.checkRateLimit(); if (this.shouldSimulateError()) { this.simulateRandomError(); } const count = options.limit || 10; const response = MockDataFixtures.createMockFormsResponse(count); // Apply workspace filter if provided if (options.workspaceId) { response.forms = response.forms.filter((form: any) => form.workspaceId === options.workspaceId); } return this.createMockResponse(response); } /** * Mock GET /forms/:id endpoint */ public async getForm(formId: string): Promise<AxiosResponse<TallyForm>> { await this.simulateDelay(); this.checkRateLimit(); if (this.shouldSimulateError()) { this.simulateRandomError(); } const form = MockDataFixtures.createMockForm({ id: formId }); return this.createMockResponse(form); } /** * Mock GET /forms/:id/submissions endpoint */ public async getSubmissions( _formId: string, options: { page?: number; limit?: number; status?: 'all' | 'completed' | 'partial'; } = {} ): Promise<AxiosResponse<TallySubmissionsResponse>> { await this.simulateDelay(); this.checkRateLimit(); if (this.shouldSimulateError()) { this.simulateRandomError(); } const count = options.limit || 10; const response = MockDataFixtures.createMockSubmissionsResponse(count); // Apply status filter if provided if (options.status && options.status !== 'all') { // Only filter by isCompleted for 'completed' or 'partial' if (options.status === 'completed') { response.submissions = response.submissions.filter((sub: any) => sub.isCompleted === true); } else if (options.status === 'partial') { response.submissions = response.submissions.filter((sub: any) => sub.isCompleted === false); } } return this.createMockResponse(response); } /** * Mock GET /forms/:id/submissions/:submissionId endpoint */ public async getSubmission(formId: string, submissionId: string): Promise<AxiosResponse<TallySubmission>> { await this.simulateDelay(); this.checkRateLimit(); if (this.shouldSimulateError()) { this.simulateRandomError(); } const submission = MockDataFixtures.createMockSubmission({ id: submissionId, formId: formId, }); return this.createMockResponse(submission); } /** * Mock GET /workspaces endpoint */ public async getWorkspaces(options: { page?: number; limit?: number; } = {}): Promise<AxiosResponse<TallyWorkspacesResponse>> { await this.simulateDelay(); this.checkRateLimit(); if (this.shouldSimulateError()) { this.simulateRandomError(); } const count = options.limit || 10; const response = MockDataFixtures.createMockWorkspacesResponse(Math.min(count, 5)); return this.createMockResponse(response); } /** * Mock GET /workspaces/:id endpoint */ public async getWorkspace(workspaceId: string): Promise<AxiosResponse<TallyWorkspace>> { await this.simulateDelay(); this.checkRateLimit(); if (this.shouldSimulateError()) { this.simulateRandomError(); } const workspace = MockDataFixtures.createMockWorkspace({ id: workspaceId }); return this.createMockResponse(workspace); } /** * Mock POST /forms endpoint (create form) */ public async createForm(formData: any): Promise<AxiosResponse<TallyForm>> { await this.simulateDelay(); this.checkRateLimit(); if (this.shouldSimulateError()) { this.simulateRandomError(); } const newForm = MockDataFixtures.createMockForm({ id: `form_${Date.now()}`, title: formData.title || formData.name || 'New Form', description: formData.description || '', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }); return this.createMockResponse(newForm, 201); } /** * Mock PUT /forms/:id endpoint (update form) */ public async updateForm(formId: string, formData: any): Promise<AxiosResponse<TallyForm>> { await this.simulateDelay(); this.checkRateLimit(); if (this.shouldSimulateError()) { this.simulateRandomError(); } const updatedForm = MockDataFixtures.createMockForm({ id: formId, ...formData, updatedAt: new Date().toISOString(), }); return this.createMockResponse(updatedForm); } /** * Mock DELETE /forms/:id endpoint */ public async deleteForm(_formId: string): Promise<AxiosResponse<{ success: boolean }>> { await this.simulateDelay(); this.checkRateLimit(); if (this.shouldSimulateError()) { this.simulateRandomError(); } return this.createMockResponse({ success: true }, 204); } /** * Reset mock state */ public reset(): void { this.requestCount = 0; this.rateLimitResetTime = Date.now() + 60000; } /** * Get mock statistics */ public getStats(): { requestCount: number; rateLimitResetTime: number; config: MockConfig; } { return { requestCount: this.requestCount, rateLimitResetTime: this.rateLimitResetTime, config: this.getConfig(), }; } } /** * Global mock instance */ export const tallyApiMock = new TallyApiMock(); /** * Helper function to enable/disable mocking */ export function enableMocking(enabled: boolean = true): void { tallyApiMock.updateConfig({ enabled }); } /** * Helper function to configure mock behavior */ export function configureMock(config: Partial<MockConfig>): void { tallyApiMock.updateConfig(config); } /** * Helper function to reset mock state */ export function resetMock(): void { tallyApiMock.reset(); }

Implementation Reference

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/learnwithcc/tally-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server