/** * @vitest-environment node */ import { authMockFns, createMockRequest } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' import { apportionCredits } from '@/lib/billing/credits/conversion' const { mockGetUserUsageLogs, mockGetUsageCreditsByLogId } = vi.hoisted(() => ({ mockGetUserUsageLogs: vi.fn(), mockGetUsageCreditsByLogId: vi.fn(), })) vi.mock('@/lib/billing/core/usage-log', () => ({ getUserUsageLogs: mockGetUserUsageLogs, getUsageCreditsByLogId: mockGetUsageCreditsByLogId, })) import { GET } from '@/app/api/users/me/usage-logs/route' describe('GET /api/users/me/usage-logs', () => { beforeEach(() => { vi.clearAllMocks() authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-1' } }) mockGetUserUsageLogs.mockResolvedValue({ logs: [ { id: 'log-1', createdAt: '2026-07-01T00:00:00.000Z', category: 'model', source: 'workflow', description: 'gpt-4o', cost: 0.5, }, ], summary: { totalCost: 0.5, bySource: { workflow: 0.5 } }, pagination: { hasMore: false }, }) mockGetUsageCreditsByLogId.mockResolvedValue(apportionCredits([{ key: 'log-1', dollars: 0.5 }])) }) it('returns 401 when unauthenticated', async () => { authMockFns.mockGetSession.mockResolvedValue(null) const response = await GET(createMockRequest('GET')) expect(response.status).toBe(401) }) it('converts dollar costs to credits in the logs and summary', async () => { const response = await GET(createMockRequest('GET')) const body = await response.json() expect(body.logs).toEqual([ { id: 'log-1', createdAt: '2026-07-01T00:00:00.000Z', source: 'workflow', workflowName: null, creditCost: 100, dollarCost: 0.5, }, ]) expect(body.summary).toEqual({ totalCredits: 100, bySourceCredits: { workflow: 100 }, }) }) it('passes through the workflow name for workflow-sourced rows', async () => { mockGetUserUsageLogs.mockResolvedValue({ logs: [ { id: 'log-1', createdAt: '2026-07-01T00:00:00.000Z', category: 'fixed', source: 'workflow', description: 'execution_fee', cost: 0.01, workflowId: 'wf-1', workflowName: 'ITSM_Prod_main', }, ], summary: { totalCost: 0.01, bySource: { workflow: 0.01 } }, pagination: { hasMore: false }, }) mockGetUsageCreditsByLogId.mockResolvedValue( apportionCredits([{ key: 'log-1', dollars: 0.01 }]) ) const response = await GET(createMockRequest('GET')) const body = await response.json() expect(body.logs[0].workflowName).toBe('ITSM_Prod_main') }) it('rejects "custom" period without a startDate', async () => { const response = await GET( createMockRequest('GET', undefined, {}, 'http://localhost:3000/api/test?period=custom') ) expect(response.status).toBe(400) expect(mockGetUserUsageLogs).not.toHaveBeenCalled() }) it('apportions row credits so they sum exactly to the page total, instead of rounding each row independently', async () => { mockGetUserUsageLogs.mockResolvedValue({ logs: [ { id: 'log-a', createdAt: '2026-07-01T00:00:00.000Z', source: 'workflow', cost: 0.002 }, { id: 'log-b', createdAt: '2026-07-01T00:00:00.000Z', source: 'workflow', cost: 0.002 }, { id: 'log-c', createdAt: '2026-07-01T00:00:00.000Z', source: 'workflow', cost: 0.002 }, ].map((log) => ({ ...log, category: 'model', description: 'gpt-4o' })), summary: { totalCost: 0.006, bySource: { workflow: 0.006 } }, pagination: { hasMore: false }, }) mockGetUsageCreditsByLogId.mockResolvedValue( apportionCredits([ { key: 'log-a', dollars: 0.002 }, { key: 'log-b', dollars: 0.002 }, { key: 'log-c', dollars: 0.002 }, ]) ) const response = await GET(createMockRequest('GET')) const body = await response.json() const rowCreditSum = body.logs.reduce( (sum: number, log: { creditCost: number }) => sum + log.creditCost, 0 ) expect(rowCreditSum).toBe(body.summary.totalCredits) }) it('rejects an invalid period', async () => { const response = await GET( createMockRequest('GET', undefined, {}, 'http://localhost:3000/api/test?period=1y') ) expect(response.status).toBe(400) expect(mockGetUserUsageLogs).not.toHaveBeenCalled() }) it('skips the whole-filter credit apportionment scan when includeCredits=false', async () => { await GET( createMockRequest( 'GET', undefined, {}, 'http://localhost:3000/api/test?limit=1&includeCredits=false' ) ) expect(mockGetUsageCreditsByLogId).not.toHaveBeenCalled() }) it('defaults creditCost to 0 (not undefined) when credits were skipped', async () => { const response = await GET( createMockRequest( 'GET', undefined, {}, 'http://localhost:3000/api/test?limit=1&includeCredits=false' ) ) const body = await response.json() expect(body.logs[0].creditCost).toBe(0) }) it('resolves the start date from the period filter', async () => { await GET(createMockRequest('GET', undefined, {}, 'http://localhost:3000/api/test?period=7d')) expect(mockGetUserUsageLogs).toHaveBeenCalledWith( 'user-1', expect.objectContaining({ startDate: expect.any(Date) }) ) }) it('omits the start date for the "all" period', async () => { await GET(createMockRequest('GET', undefined, {}, 'http://localhost:3000/api/test?period=all')) expect(mockGetUserUsageLogs).toHaveBeenCalledWith( 'user-1', expect.objectContaining({ startDate: undefined }) ) }) })