/** * Component: Home Sections API Route Tests * Documentation: documentation/features/home-sections.md */ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { createPrismaMock } from '../helpers/prisma'; const prismaMock = createPrismaMock(); vi.mock('@/lib/db', () => ({ prisma: prismaMock, })); vi.mock('@/lib/middleware/auth', () => ({ requireAuth: vi.fn((_req: any, handler: any) => { const mockReq = { user: { id: 'user-1', sub: 'user-1', role: 'user' }, json: async () => (globalThis as any).__testBody || {}, }; return handler(mockReq); }), getCurrentUser: vi.fn(() => ({ sub: 'user-1' })), })); vi.mock('@/lib/utils/logger', () => ({ RMABLogger: { create: () => ({ info: vi.fn(), error: vi.fn(), warn: vi.fn(), debug: vi.fn() }) }, })); describe('GET /api/user/home-sections', () => { beforeEach(() => { vi.clearAllMocks(); // Re-apply default mock implementations after clearAllMocks prismaMock.userHomeSection.createMany.mockResolvedValue({ count: 0 }); prismaMock.userHomeSection.deleteMany.mockResolvedValue({ count: 0 }); prismaMock.$transaction.mockImplementation(async (fn: any) => fn(prismaMock)); }); it('returns default sections for new user', async () => { // ensureDefaultSections check: no existing sections prismaMock.userHomeSection.findMany .mockResolvedValueOnce([]) // ensureDefaultSections .mockResolvedValueOnce([ // actual fetch after defaults created { id: '1', sectionType: 'popular', categoryId: null, categoryName: null, sortOrder: 0 }, { id: '2', sectionType: 'new_releases', categoryId: null, categoryName: null, sortOrder: 1 }, ]); prismaMock.scheduledJob.findFirst.mockResolvedValueOnce(null); const { GET } = await import('@/app/api/user/home-sections/route'); const request = new Request('http://localhost/api/user/home-sections'); const response = await GET(request as any); const data = await response.json(); expect(data.success).toBe(true); expect(data.sections).toHaveLength(2); expect(data.sections[0].sectionType).toBe('popular'); expect(data.sections[1].sectionType).toBe('new_releases'); }); it('returns existing sections without creating defaults', async () => { prismaMock.userHomeSection.findMany .mockResolvedValueOnce([{ id: '1' }]) // has existing .mockResolvedValueOnce([ { id: '1', sectionType: 'category', categoryId: '123', categoryName: 'Sci-Fi', sortOrder: 0 }, ]); prismaMock.scheduledJob.findFirst.mockResolvedValueOnce({ nextRun: new Date('2026-03-05T00:00:00Z'), }); const { GET } = await import('@/app/api/user/home-sections/route'); const request = new Request('http://localhost/api/user/home-sections'); const response = await GET(request as any); const data = await response.json(); expect(data.success).toBe(true); expect(data.sections).toHaveLength(1); expect(data.sections[0].categoryName).toBe('Sci-Fi'); expect(data.nextRefresh).toBe('2026-03-05T00:00:00.000Z'); expect(prismaMock.userHomeSection.createMany).not.toHaveBeenCalled(); }); }); describe('PUT /api/user/home-sections', () => { beforeEach(() => { vi.clearAllMocks(); prismaMock.userHomeSection.createMany.mockResolvedValue({ count: 0 }); prismaMock.userHomeSection.deleteMany.mockResolvedValue({ count: 0 }); prismaMock.$transaction.mockImplementation(async (fn: any) => fn(prismaMock)); }); it('saves new section configuration', async () => { (globalThis as any).__testBody = { sections: [ { sectionType: 'new_releases', sortOrder: 0 }, { sectionType: 'popular', sortOrder: 1 }, { sectionType: 'category', categoryId: '123', categoryName: 'Sci-Fi', sortOrder: 2 }, ], }; prismaMock.userHomeSection.findMany.mockResolvedValueOnce([ { id: '1', sectionType: 'new_releases', categoryId: null, categoryName: null, sortOrder: 0 }, { id: '2', sectionType: 'popular', categoryId: null, categoryName: null, sortOrder: 1 }, { id: '3', sectionType: 'category', categoryId: '123', categoryName: 'Sci-Fi', sortOrder: 2 }, ]); const { PUT } = await import('@/app/api/user/home-sections/route'); const request = new Request('http://localhost/api/user/home-sections', { method: 'PUT' }); const response = await PUT(request as any); const data = await response.json(); expect(data.success).toBe(true); expect(data.sections).toHaveLength(3); expect(prismaMock.userHomeSection.deleteMany).toHaveBeenCalledWith({ where: { userId: 'user-1' }, }); expect(prismaMock.userHomeSection.createMany).toHaveBeenCalled(); }); it('rejects more than 10 sections', async () => { (globalThis as any).__testBody = { sections: Array.from({ length: 11 }, (_, i) => ({ sectionType: 'category', categoryId: `cat-${i}`, categoryName: `Cat ${i}`, sortOrder: i, })), }; const { PUT } = await import('@/app/api/user/home-sections/route'); const request = new Request('http://localhost/api/user/home-sections', { method: 'PUT' }); const response = await PUT(request as any); expect(response.status).toBe(400); }); it('rejects duplicate sections', async () => { (globalThis as any).__testBody = { sections: [ { sectionType: 'popular', sortOrder: 0 }, { sectionType: 'popular', sortOrder: 1 }, ], }; const { PUT } = await import('@/app/api/user/home-sections/route'); const request = new Request('http://localhost/api/user/home-sections', { method: 'PUT' }); const response = await PUT(request as any); expect(response.status).toBe(400); const data = await response.json(); expect(data.message).toContain('Duplicate'); }); it('rejects category section without categoryId', async () => { (globalThis as any).__testBody = { sections: [{ sectionType: 'category', sortOrder: 0 }], }; const { PUT } = await import('@/app/api/user/home-sections/route'); const request = new Request('http://localhost/api/user/home-sections', { method: 'PUT' }); const response = await PUT(request as any); expect(response.status).toBe(400); const data = await response.json(); expect(data.message).toContain('categoryId'); }); });