diff --git a/tests/api/hardcover-shelves-id.routes.test.ts b/tests/api/hardcover-shelves-id.routes.test.ts index 7d084ea..dc10efe 100644 --- a/tests/api/hardcover-shelves-id.routes.test.ts +++ b/tests/api/hardcover-shelves-id.routes.test.ts @@ -164,6 +164,40 @@ describe('PATCH /api/user/hardcover-shelves/[id]', () => { expect(jobQueueMock.addSyncShelvesJob).not.toHaveBeenCalled(); }); + it('triggers a sync when forceSync is true, even if no fields changed', async () => { + prismaMock.hardcoverShelf.findUnique.mockResolvedValueOnce(SHELF); + prismaMock.hardcoverShelf.update.mockResolvedValueOnce(SHELF); + + const { PATCH } = + await import('@/app/api/user/hardcover-shelves/[id]/route'); + const response = await PATCH( + { + json: vi + .fn() + .mockResolvedValue({ listId: SHELF.listId, forceSync: true }), + } as any, + { params: Promise.resolve({ id: 'hc-shelf-1' }) }, + ); + const payload = await response.json(); + + expect(response.status).toBe(200); + expect(payload.success).toBe(true); + expect(prismaMock.hardcoverShelf.update).toHaveBeenCalledWith({ + where: { id: 'hc-shelf-1' }, + data: expect.objectContaining({ + lastSyncAt: null, + bookCount: null, + coverUrls: null, + }), + }); + expect(jobQueueMock.addSyncShelvesJob).toHaveBeenCalledWith( + undefined, + SHELF.id, + 'hardcover', + 0, + ); + }); + it('updates listId, clears metadata, and triggers a sync job', async () => { prismaMock.hardcoverShelf.findUnique.mockResolvedValueOnce(SHELF); const updated = { ...SHELF, listId: 'status-3', lastSyncAt: null }; diff --git a/tests/api/shelves-sync.routes.test.ts b/tests/api/shelves-sync.routes.test.ts new file mode 100644 index 0000000..1a2ada7 --- /dev/null +++ b/tests/api/shelves-sync.routes.test.ts @@ -0,0 +1,96 @@ +/** + * Component: Shelves Sync API Route Tests + * Documentation: documentation/backend/services/goodreads-sync.md + */ + +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +let authRequest: any; + +const requireAuthMock = vi.hoisted(() => vi.fn()); +const jobQueueMock = vi.hoisted(() => ({ + addSyncShelvesJob: vi.fn(() => Promise.resolve()), +})); + +vi.mock('@/lib/middleware/auth', () => ({ + requireAuth: requireAuthMock, +})); + +vi.mock('@/lib/services/job-queue.service', () => ({ + getJobQueueService: () => jobQueueMock, +})); + +describe('POST /api/user/shelves/sync', () => { + beforeEach(() => { + vi.clearAllMocks(); + authRequest = { user: { id: 'user-1', role: 'user' } }; + requireAuthMock.mockImplementation((_req: any, handler: any) => handler(authRequest)); + }); + + it('triggers a manual sync for all shelves when no parameters provided', async () => { + const { POST } = await import('@/app/api/user/shelves/sync/route'); + const response = await POST( + { json: vi.fn().mockResolvedValue({}) } as any, + ); + const payload = await response.json(); + + expect(response.status).toBe(200); + expect(payload.success).toBe(true); + expect(jobQueueMock.addSyncShelvesJob).toHaveBeenCalledWith( + undefined, // scheduledJobId + undefined, // shelfId + undefined, // shelfType + 0, // maxLookupsPerShelf (unlimited for manual) + 'user-1' // userId + ); + }); + + it('triggers a manual sync for a specific shelf', async () => { + const { POST } = await import('@/app/api/user/shelves/sync/route'); + const response = await POST( + { json: vi.fn().mockResolvedValue({ shelfId: 'shelf-123', shelfType: 'goodreads' }) } as any, + ); + const payload = await response.json(); + + expect(response.status).toBe(200); + expect(payload.success).toBe(true); + expect(jobQueueMock.addSyncShelvesJob).toHaveBeenCalledWith( + undefined, // scheduledJobId + 'shelf-123', // shelfId + 'goodreads', // shelfType + 0, // maxLookupsPerShelf + 'user-1' // userId + ); + }); + + it('handles invalid body gracefully', async () => { + const { POST } = await import('@/app/api/user/shelves/sync/route'); + const response = await POST( + { json: vi.fn().mockRejectedValue(new Error('Invalid JSON')) } as any, + ); + const payload = await response.json(); + + expect(response.status).toBe(200); + expect(payload.success).toBe(true); + // Since body parsing fails gracefully with catching () => ({}), it treats it as sync all + expect(jobQueueMock.addSyncShelvesJob).toHaveBeenCalledWith( + undefined, + undefined, + undefined, + 0, + 'user-1' + ); + }); + + it('validates wrong shelfType', async () => { + const { POST } = await import('@/app/api/user/shelves/sync/route'); + const response = await POST( + { json: vi.fn().mockResolvedValue({ shelfType: 'invalid-type' }) } as any, + ); + const payload = await response.json(); + + expect(response.status).toBe(400); + expect(payload.error).toBe('ValidationError'); + expect(jobQueueMock.addSyncShelvesJob).not.toHaveBeenCalled(); + }); +});