import { trackNoteUsage, getNoteUsageCount, getRecentlyUsedNotes, getUsageStats, UsageEventType } from '@/lib/usage' // Mock prisma before importing usage module jest.mock('@/lib/prisma', () => ({ prisma: { noteUsage: { create: jest.fn(), count: jest.fn(), groupBy: jest.fn(), }, note: { findMany: jest.fn(), }, }, })) import { prisma } from '@/lib/prisma' describe('usage.ts', () => { beforeEach(() => { jest.clearAllMocks() }) describe('trackNoteUsage', () => { it('should create a usage event with all fields', async () => { ;(prisma.noteUsage.create as jest.Mock).mockResolvedValue({ id: '1' }) await trackNoteUsage({ noteId: 'note-1', eventType: 'view', query: 'docker commands', metadata: { source: 'search' }, }) expect(prisma.noteUsage.create).toHaveBeenCalledWith({ data: { noteId: 'note-1', eventType: 'view', query: 'docker commands', metadata: JSON.stringify({ source: 'search' }), }, }) }) it('should create a usage event without optional fields', async () => { ;(prisma.noteUsage.create as jest.Mock).mockResolvedValue({ id: '1' }) await trackNoteUsage({ noteId: 'note-1', eventType: 'view', }) expect(prisma.noteUsage.create).toHaveBeenCalledWith({ data: { noteId: 'note-1', eventType: 'view', query: null, metadata: null, }, }) }) it('should silently fail on database error', async () => { ;(prisma.noteUsage.create as jest.Mock).mockRejectedValue(new Error('DB error')) // Should not throw await expect( trackNoteUsage({ noteId: 'note-1', eventType: 'view' }) ).resolves.not.toThrow() }) }) describe('getNoteUsageCount', () => { it('should return count for a specific note', async () => { ;(prisma.noteUsage.count as jest.Mock).mockResolvedValue(5) const result = await getNoteUsageCount('note-1') expect(result).toBe(5) expect(prisma.noteUsage.count).toHaveBeenCalledWith({ where: expect.objectContaining({ noteId: 'note-1' }), }) }) it('should filter by event type when specified', async () => { ;(prisma.noteUsage.count as jest.Mock).mockResolvedValue(3) const result = await getNoteUsageCount('note-1', 'search_click') expect(result).toBe(3) expect(prisma.noteUsage.count).toHaveBeenCalledWith({ where: expect.objectContaining({ noteId: 'note-1', eventType: 'search_click', }), }) }) it('should apply default 30 days filter', async () => { ;(prisma.noteUsage.count as jest.Mock).mockResolvedValue(0) await getNoteUsageCount('note-1') const call = (prisma.noteUsage.count as jest.Mock).mock.calls[0][0] expect(call.where.createdAt.gte).toBeInstanceOf(Date) }) it('should return 0 on database error', async () => { ;(prisma.noteUsage.count as jest.Mock).mockRejectedValue(new Error('DB error')) const result = await getNoteUsageCount('note-1') expect(result).toBe(0) }) }) describe('getRecentlyUsedNotes', () => { it('should return recently used notes with counts', async () => { const mockGroupBy = [ { noteId: 'note-1', _count: { id: 10 } }, { noteId: 'note-2', _count: { id: 5 } }, ] ;(prisma.noteUsage.groupBy as jest.Mock).mockResolvedValue(mockGroupBy) ;(prisma.note.findMany as jest.Mock).mockResolvedValue([ { id: 'note-1', updatedAt: new Date('2024-01-15') }, { id: 'note-2', updatedAt: new Date('2024-01-14') }, ]) const result = await getRecentlyUsedNotes('user-1', 10, 30) expect(result).toHaveLength(2) expect(result[0]).toEqual({ noteId: 'note-1', count: 10, lastUsed: expect.any(Date), }) }) it('should return empty array on database error', async () => { ;(prisma.noteUsage.groupBy as jest.Mock).mockRejectedValue(new Error('DB error')) const result = await getRecentlyUsedNotes('user-1') expect(result).toEqual([]) }) it('should respect limit parameter', async () => { ;(prisma.noteUsage.groupBy as jest.Mock).mockResolvedValue([]) ;(prisma.note.findMany as jest.Mock).mockResolvedValue([]) await getRecentlyUsedNotes('user-1', 5) expect(prisma.noteUsage.groupBy).toHaveBeenCalledWith( expect.objectContaining({ take: 5 }) ) }) }) describe('getUsageStats', () => { it('should return usage stats for a note', async () => { ;(prisma.noteUsage.count as jest.Mock) .mockResolvedValueOnce(15) // views .mockResolvedValueOnce(8) // clicks .mockResolvedValueOnce(3) // relatedClicks const result = await getUsageStats('note-1', 30) expect(result).toEqual({ views: 15, clicks: 8, relatedClicks: 3, }) }) it('should return zeros on database error', async () => { ;(prisma.noteUsage.count as jest.Mock).mockRejectedValue(new Error('DB error')) const result = await getUsageStats('note-1') expect(result).toEqual({ views: 0, clicks: 0, relatedClicks: 0, }) }) it('should query with correct date filter', async () => { ;(prisma.noteUsage.count as jest.Mock).mockResolvedValue(0) await getUsageStats('note-1', 7) // All three counts should use the same date filter for (const call of (prisma.noteUsage.count as jest.Mock).mock.calls) { expect(call[0].where.createdAt.gte).toBeInstanceOf(Date) } }) it('should return zeros when any count fails', async () => { // Promise.all will reject on first failure, so we can't get partial results ;(prisma.noteUsage.count as jest.Mock) .mockResolvedValueOnce(10) // views .mockRejectedValueOnce(new Error('DB error')) // clicks fails .mockResolvedValueOnce(5) // relatedClicks never called const result = await getUsageStats('note-1') // All zeros because the whole operation fails expect(result.views).toBe(0) expect(result.clicks).toBe(0) expect(result.relatedClicks).toBe(0) }) }) describe('UsageEventType', () => { it('should accept all valid event types', () => { const eventTypes: UsageEventType[] = ['view', 'search_click', 'related_click', 'link_click'] eventTypes.forEach((eventType) => { expect(['view', 'search_click', 'related_click', 'link_click']).toContain(eventType) }) }) }) })