/** * @vitest-environment node */ import { createMockRequest } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' const { mockGetSession, mockRecordUsage, mockCheckActorUsageLimits, mockGetWorkspaceBilledAccountUserId, mockVerifyWorkspaceMembership, mockChatRows, } = vi.hoisted(() => ({ mockGetSession: vi.fn(), mockRecordUsage: vi.fn(), mockCheckActorUsageLimits: vi.fn(), mockGetWorkspaceBilledAccountUserId: vi.fn(), mockVerifyWorkspaceMembership: vi.fn(), mockChatRows: { value: [] as Array> }, })) vi.mock('@sim/db', () => ({ db: { select: () => { const chain: Record = {} chain.from = () => chain chain.leftJoin = () => chain chain.where = () => chain chain.limit = () => Promise.resolve(mockChatRows.value) return chain }, }, })) vi.mock('@/lib/auth', () => ({ getSession: mockGetSession })) vi.mock('@/lib/billing/core/usage-log', () => ({ recordUsage: mockRecordUsage })) vi.mock('@/lib/billing/calculations/usage-monitor', () => ({ checkActorUsageLimits: mockCheckActorUsageLimits, })) vi.mock('@/lib/workspaces/utils', () => ({ getWorkspaceBilledAccountUserId: mockGetWorkspaceBilledAccountUserId, })) vi.mock('@/app/api/workflows/utils', () => ({ verifyWorkspaceMembership: mockVerifyWorkspaceMembership, })) vi.mock('@/lib/core/config/env', () => ({ env: { ELEVENLABS_API_KEY: 'test-key' } })) vi.mock('@/lib/core/config/env-flags', () => ({ isBillingEnabled: false, getCostMultiplier: () => 1, })) vi.mock('@/lib/core/rate-limiter', () => ({ RateLimiter: class { checkRateLimitDirect = vi.fn().mockResolvedValue({ allowed: true }) }, })) vi.mock('@/lib/core/security/deployment', () => ({ validateAuthToken: vi.fn(() => false) })) import { POST } from '@/app/api/speech/token/route' const publicChatRow = { id: 'chat-1', userId: 'owner-1', isActive: true, authType: 'public', password: null, workspaceId: 'ws-1', } beforeEach(() => { vi.clearAllMocks() mockChatRows.value = [] mockGetSession.mockResolvedValue({ user: { id: 'member-1' } }) mockRecordUsage.mockResolvedValue(undefined) mockCheckActorUsageLimits.mockResolvedValue({ isExceeded: false }) mockGetWorkspaceBilledAccountUserId.mockResolvedValue('billed-acct') mockVerifyWorkspaceMembership.mockResolvedValue('admin') global.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ token: 'tok-123' }), // double-cast-allowed: minimal fetch stub for the ElevenLabs token call }) as unknown as typeof fetch }) describe('POST /api/speech/token — usage attribution', () => { it('editor voice: bills the session user and stamps the verified workspace', async () => { const res = await POST(createMockRequest('POST', { workspaceId: 'ws-1' })) expect(res.status).toBe(200) expect(mockVerifyWorkspaceMembership).toHaveBeenCalledWith('member-1', 'ws-1') expect(mockRecordUsage).toHaveBeenCalledTimes(1) expect(mockRecordUsage.mock.calls[0][0]).toMatchObject({ userId: 'member-1', workspaceId: 'ws-1', }) }) it('editor voice: rejects an unverified workspace id (requires an attributable workspace)', async () => { mockVerifyWorkspaceMembership.mockResolvedValue(null) const res = await POST(createMockRequest('POST', { workspaceId: 'ws-not-mine' })) expect(res.status).toBe(400) expect(mockRecordUsage).not.toHaveBeenCalled() }) it('deployed chat: bills the workspace billed account and stamps the chat workspace', async () => { mockChatRows.value = [publicChatRow] const res = await POST(createMockRequest('POST', { chatId: 'chat-1' })) expect(res.status).toBe(200) expect(mockGetSession).not.toHaveBeenCalled() expect(mockGetWorkspaceBilledAccountUserId).toHaveBeenCalledWith('ws-1') expect(mockRecordUsage.mock.calls[0][0]).toMatchObject({ userId: 'billed-acct', workspaceId: 'ws-1', }) }) it('deployed chat: falls back to the chat owner when no billed account resolves', async () => { mockChatRows.value = [publicChatRow] mockGetWorkspaceBilledAccountUserId.mockResolvedValue(null) const res = await POST(createMockRequest('POST', { chatId: 'chat-1' })) expect(res.status).toBe(200) expect(mockRecordUsage.mock.calls[0][0]).toMatchObject({ userId: 'owner-1', workspaceId: 'ws-1', }) }) it('rejects an oversized body before any auth/billing work runs', async () => { const oversizedBody = { chatId: 'x'.repeat(64 * 1024) } const res = await POST(createMockRequest('POST', oversizedBody)) expect(res.status).toBe(413) expect(mockGetSession).not.toHaveBeenCalled() expect(mockRecordUsage).not.toHaveBeenCalled() }) })