Files
ReadMeABook/tests/api/goodreads-shelves-id.routes.test.ts
kikootwo 8aac63715a Pass user ID to addSyncShelvesJob
Include the requesting user's ID as an additional argument when enqueueing immediate shelf sync jobs so the job has user context. Updated the route implementation and adjusted affected tests (goodreads-shelves-id, hardcover-shelves-id, and hardcover-shelves routes tests) to expect the extra 'user-1' parameter.
2026-03-11 09:59:54 -04:00

187 lines
6.9 KiB
TypeScript

/**
* Component: Goodreads Shelves [id] API Route Tests
* Documentation: documentation/backend/services/goodreads-sync.md
*/
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { createPrismaMock } from '../helpers/prisma';
let authRequest: any;
const requireAuthMock = vi.hoisted(() => vi.fn());
const prismaMock = createPrismaMock();
const jobQueueMock = vi.hoisted(() => ({
addSyncShelvesJob: vi.fn(() => Promise.resolve()),
}));
vi.mock('@/lib/middleware/auth', () => ({
requireAuth: requireAuthMock,
}));
vi.mock('@/lib/db', () => ({
prisma: prismaMock,
}));
vi.mock('@/lib/services/job-queue.service', () => ({
getJobQueueService: () => jobQueueMock,
}));
const SHELF = {
id: 'shelf-1',
userId: 'user-1',
name: 'Want to Read',
rssUrl: 'https://www.goodreads.com/review/list_rss/12345',
lastSyncAt: null,
bookCount: 5,
coverUrls: null,
createdAt: new Date().toISOString(),
};
describe('DELETE /api/user/goodreads-shelves/[id]', () => {
beforeEach(() => {
vi.clearAllMocks();
authRequest = { user: { id: 'user-1', role: 'user' } };
requireAuthMock.mockImplementation((_req: any, handler: any) => handler(authRequest));
});
it('returns 404 when shelf does not exist', async () => {
prismaMock.goodreadsShelf.findUnique.mockResolvedValueOnce(null);
const { DELETE } = await import('@/app/api/user/goodreads-shelves/[id]/route');
const response = await DELETE({} as any, { params: Promise.resolve({ id: 'shelf-1' }) });
const payload = await response.json();
expect(response.status).toBe(404);
expect(payload.error).toBe('Shelf not found');
});
it('returns 403 when shelf belongs to another user', async () => {
prismaMock.goodreadsShelf.findUnique.mockResolvedValueOnce({ ...SHELF, userId: 'other-user' });
const { DELETE } = await import('@/app/api/user/goodreads-shelves/[id]/route');
const response = await DELETE({} as any, { params: Promise.resolve({ id: 'shelf-1' }) });
const payload = await response.json();
expect(response.status).toBe(403);
expect(payload.error).toBe('Forbidden');
});
it('deletes the shelf and returns success', async () => {
prismaMock.goodreadsShelf.findUnique.mockResolvedValueOnce(SHELF);
prismaMock.goodreadsShelf.delete.mockResolvedValueOnce({});
const { DELETE } = await import('@/app/api/user/goodreads-shelves/[id]/route');
const response = await DELETE({} as any, { params: Promise.resolve({ id: 'shelf-1' }) });
const payload = await response.json();
expect(response.status).toBe(200);
expect(payload.success).toBe(true);
expect(prismaMock.goodreadsShelf.delete).toHaveBeenCalledWith({ where: { id: 'shelf-1' } });
});
it('returns 500 when deletion throws', async () => {
prismaMock.goodreadsShelf.findUnique.mockResolvedValueOnce(SHELF);
prismaMock.goodreadsShelf.delete.mockRejectedValueOnce(new Error('db error'));
const { DELETE } = await import('@/app/api/user/goodreads-shelves/[id]/route');
const response = await DELETE({} as any, { params: Promise.resolve({ id: 'shelf-1' }) });
const payload = await response.json();
expect(response.status).toBe(500);
expect(payload.error).toBe('Failed to delete shelf');
});
});
describe('PATCH /api/user/goodreads-shelves/[id]', () => {
const NEW_RSS = 'https://www.goodreads.com/review/list_rss/99999';
beforeEach(() => {
vi.clearAllMocks();
authRequest = {
user: { id: 'user-1', role: 'user' },
json: vi.fn().mockResolvedValue({ rssUrl: NEW_RSS }),
};
requireAuthMock.mockImplementation((_req: any, handler: any) => handler(authRequest));
});
it('returns 404 when shelf does not exist', async () => {
prismaMock.goodreadsShelf.findUnique.mockResolvedValueOnce(null);
const { PATCH } = await import('@/app/api/user/goodreads-shelves/[id]/route');
const response = await PATCH(
{ json: vi.fn().mockResolvedValue({ rssUrl: NEW_RSS }) } as any,
{ params: Promise.resolve({ id: 'shelf-1' }) }
);
const payload = await response.json();
expect(response.status).toBe(404);
expect(payload.error).toBe('Shelf not found');
});
it('returns 403 when shelf belongs to another user', async () => {
prismaMock.goodreadsShelf.findUnique.mockResolvedValueOnce({ ...SHELF, userId: 'other-user' });
const { PATCH } = await import('@/app/api/user/goodreads-shelves/[id]/route');
const response = await PATCH(
{ json: vi.fn().mockResolvedValue({ rssUrl: NEW_RSS }) } as any,
{ params: Promise.resolve({ id: 'shelf-1' }) }
);
const payload = await response.json();
expect(response.status).toBe(403);
expect(payload.error).toBe('Forbidden');
});
it('returns 400 for an invalid (non-URL) rssUrl', async () => {
prismaMock.goodreadsShelf.findUnique.mockResolvedValueOnce(SHELF);
const { PATCH } = await import('@/app/api/user/goodreads-shelves/[id]/route');
const response = await PATCH(
{ json: vi.fn().mockResolvedValue({ rssUrl: 'not-a-url' }) } as any,
{ params: Promise.resolve({ id: 'shelf-1' }) }
);
const payload = await response.json();
expect(response.status).toBe(400);
expect(payload.error).toBe('ValidationError');
});
it('updates the shelf, clears sync metadata, and triggers a sync job', async () => {
prismaMock.goodreadsShelf.findUnique.mockResolvedValueOnce(SHELF);
const updatedShelf = { ...SHELF, rssUrl: NEW_RSS, lastSyncAt: null };
prismaMock.goodreadsShelf.update.mockResolvedValueOnce(updatedShelf);
const { PATCH } = await import('@/app/api/user/goodreads-shelves/[id]/route');
const response = await PATCH(
{ json: vi.fn().mockResolvedValue({ rssUrl: NEW_RSS }) } as any,
{ params: Promise.resolve({ id: 'shelf-1' }) }
);
const payload = await response.json();
expect(response.status).toBe(200);
expect(payload.success).toBe(true);
expect(prismaMock.goodreadsShelf.update).toHaveBeenCalledWith({
where: { id: 'shelf-1' },
data: { rssUrl: NEW_RSS, lastSyncAt: null, bookCount: null, coverUrls: null },
});
expect(jobQueueMock.addSyncShelvesJob).toHaveBeenCalledWith(undefined, updatedShelf.id, 'goodreads', 0, 'user-1');
});
it('still returns 200 even when the sync job fails to enqueue', async () => {
prismaMock.goodreadsShelf.findUnique.mockResolvedValueOnce(SHELF);
prismaMock.goodreadsShelf.update.mockResolvedValueOnce({ ...SHELF, rssUrl: NEW_RSS });
jobQueueMock.addSyncShelvesJob.mockRejectedValueOnce(new Error('queue down'));
const { PATCH } = await import('@/app/api/user/goodreads-shelves/[id]/route');
const response = await PATCH(
{ json: vi.fn().mockResolvedValue({ rssUrl: NEW_RSS }) } as any,
{ params: Promise.resolve({ id: 'shelf-1' }) }
);
const payload = await response.json();
// Sync job failure is swallowed; shelf update should still succeed
expect(response.status).toBe(200);
expect(payload.success).toBe(true);
});
});