/** * Component: BookDate Swipe Release-Date Gate Tests * Documentation: documentation/features/bookdate-prd.md * * Narrow coverage for the release-date gate on right-swipe request creation. * Broader swipe behavior is covered in tests/api/bookdate.routes.test.ts. */ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { createPrismaMock } from '../helpers/prisma'; let authRequest: any; const prismaMock = createPrismaMock(); const requireAuthMock = vi.hoisted(() => vi.fn()); const requireAdminMock = vi.hoisted(() => vi.fn()); const audibleServiceMock = vi.hoisted(() => ({ getAudiobookDetails: vi.fn(), })); const configServiceGet = vi.hoisted(() => vi.fn()); const jobQueueMock = vi.hoisted(() => ({ addSearchJob: vi.fn().mockResolvedValue(undefined), addNotificationJob: vi.fn().mockResolvedValue(undefined), })); vi.mock('@/lib/db', () => ({ prisma: prismaMock, })); vi.mock('@/lib/middleware/auth', () => ({ requireAuth: requireAuthMock, requireAdmin: requireAdminMock, })); vi.mock('@/lib/integrations/audible.service', () => ({ getAudibleService: () => audibleServiceMock, })); vi.mock('@/lib/services/config.service', () => ({ getConfigService: () => ({ get: configServiceGet }), })); vi.mock('@/lib/services/job-queue.service', () => ({ getJobQueueService: () => jobQueueMock, })); function futureIso(days = 30): string { return new Date(Date.now() + days * 24 * 60 * 60 * 1000).toISOString(); } function pastIso(days = 30): string { return new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString(); } describe('BookDate swipe — release-date gate', () => { beforeEach(() => { vi.clearAllMocks(); jobQueueMock.addSearchJob.mockResolvedValue(undefined); jobQueueMock.addNotificationJob.mockResolvedValue(undefined); authRequest = { user: { id: 'user-1', role: 'admin' }, json: vi.fn() }; requireAuthMock.mockImplementation((_req: any, handler: any) => handler(authRequest)); requireAdminMock.mockImplementation((_req: any, handler: any) => handler(authRequest)); }); it('creates request in awaiting_release with no search when unreleased + setting ON', async () => { authRequest.json.mockResolvedValue({ recommendationId: 'rec-future', action: 'right', markedAsKnown: false }); prismaMock.bookDateRecommendation.findUnique.mockResolvedValueOnce({ id: 'rec-future', userId: 'user-1', title: 'Future Book', author: 'Future Author', audnexusAsin: 'ASIN-FUTURE', } as any); prismaMock.bookDateSwipe.create.mockResolvedValueOnce({} as any); audibleServiceMock.getAudiobookDetails.mockResolvedValueOnce({ releaseDate: futureIso(45), }); prismaMock.audiobook.findFirst.mockResolvedValueOnce(null); prismaMock.audiobook.create.mockResolvedValueOnce({ id: 'ab-future', title: 'Future Book', author: 'Future Author', audibleAsin: 'ASIN-FUTURE', } as any); prismaMock.request.findFirst.mockResolvedValueOnce(null); prismaMock.user.findUnique.mockResolvedValueOnce({ id: 'user-1', role: 'admin', autoApproveRequests: null, plexUsername: 'admin', } as any); configServiceGet.mockResolvedValueOnce(null); // default → ON prismaMock.request.create.mockResolvedValueOnce({ id: 'req-future', audiobook: { title: 'Future Book' }, user: { id: 'user-1', plexUsername: 'admin' }, } as any); const { POST } = await import('@/app/api/bookdate/swipe/route'); const response = await POST({} as any); const payload = await response.json(); expect(payload.success).toBe(true); expect(prismaMock.request.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ status: 'awaiting_release', releaseDate: expect.any(Date), }), }) ); expect(jobQueueMock.addSearchJob).not.toHaveBeenCalled(); }); it('creates pending request and runs search when released + setting ON', async () => { authRequest.json.mockResolvedValue({ recommendationId: 'rec-past', action: 'right', markedAsKnown: false }); prismaMock.bookDateRecommendation.findUnique.mockResolvedValueOnce({ id: 'rec-past', userId: 'user-1', title: 'Old Book', author: 'Old Author', audnexusAsin: 'ASIN-PAST', } as any); prismaMock.bookDateSwipe.create.mockResolvedValueOnce({} as any); audibleServiceMock.getAudiobookDetails.mockResolvedValueOnce({ releaseDate: pastIso(365), }); prismaMock.audiobook.findFirst.mockResolvedValueOnce(null); prismaMock.audiobook.create.mockResolvedValueOnce({ id: 'ab-past', title: 'Old Book', author: 'Old Author', audibleAsin: 'ASIN-PAST', } as any); prismaMock.request.findFirst.mockResolvedValueOnce(null); prismaMock.user.findUnique.mockResolvedValueOnce({ id: 'user-1', role: 'admin', autoApproveRequests: null, plexUsername: 'admin', } as any); configServiceGet.mockResolvedValueOnce('true'); prismaMock.request.create.mockResolvedValueOnce({ id: 'req-past', audiobook: { title: 'Old Book' }, user: { id: 'user-1', plexUsername: 'admin' }, } as any); const { POST } = await import('@/app/api/bookdate/swipe/route'); const response = await POST({} as any); const payload = await response.json(); expect(payload.success).toBe(true); expect(prismaMock.request.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ status: 'pending', }), }) ); expect(jobQueueMock.addSearchJob).toHaveBeenCalled(); }); it('creates pending request and runs search when unreleased + setting OFF', async () => { authRequest.json.mockResolvedValue({ recommendationId: 'rec-off', action: 'right', markedAsKnown: false }); prismaMock.bookDateRecommendation.findUnique.mockResolvedValueOnce({ id: 'rec-off', userId: 'user-1', title: 'Off Book', author: 'Off Author', audnexusAsin: 'ASIN-OFF', } as any); prismaMock.bookDateSwipe.create.mockResolvedValueOnce({} as any); audibleServiceMock.getAudiobookDetails.mockResolvedValueOnce({ releaseDate: futureIso(45), }); prismaMock.audiobook.findFirst.mockResolvedValueOnce(null); prismaMock.audiobook.create.mockResolvedValueOnce({ id: 'ab-off', title: 'Off Book', author: 'Off Author', audibleAsin: 'ASIN-OFF', } as any); prismaMock.request.findFirst.mockResolvedValueOnce(null); prismaMock.user.findUnique.mockResolvedValueOnce({ id: 'user-1', role: 'admin', autoApproveRequests: null, plexUsername: 'admin', } as any); configServiceGet.mockResolvedValueOnce('false'); prismaMock.request.create.mockResolvedValueOnce({ id: 'req-off', audiobook: { title: 'Off Book' }, user: { id: 'user-1', plexUsername: 'admin' }, } as any); const { POST } = await import('@/app/api/bookdate/swipe/route'); const response = await POST({} as any); const payload = await response.json(); expect(payload.success).toBe(true); expect(prismaMock.request.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ status: 'pending', }), }) ); expect(jobQueueMock.addSearchJob).toHaveBeenCalled(); }); });