Files
ReadMeABook/tests/api/requests-approval.routes.test.ts
T
kikootwo dc7e557694 Add notification system with admin UI and backend
Introduces a full notification system with support for Discord and Pushover backends, event triggers, and message formatting. Adds backend services, processors, and API endpoints for managing notifications, as well as a new Notifications tab in the admin settings UI. Updates documentation, database schema, and tests to cover notification features and approval workflow improvements. Also changes project license from MIT to AGPL v3.
2026-01-28 11:42:00 -05:00

839 lines
28 KiB
TypeScript

/**
* Component: Request Approval API Route Tests
* Documentation: documentation/admin-features/request-approval.md
*/
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { createPrismaMock } from '../helpers/prisma';
let authRequest: any;
const requireAuthMock = vi.hoisted(() => vi.fn());
const requireAdminMock = vi.hoisted(() => vi.fn());
const prismaMock = createPrismaMock();
const jobQueueMock = vi.hoisted(() => ({
addSearchJob: vi.fn(),
addNotificationJob: vi.fn(() => Promise.resolve()),
addDownloadJob: vi.fn(),
}));
const findPlexMatchMock = vi.hoisted(() => vi.fn());
vi.mock('@/lib/middleware/auth', () => ({
requireAuth: requireAuthMock,
requireAdmin: requireAdminMock,
}));
vi.mock('@/lib/db', () => ({
prisma: prismaMock,
}));
vi.mock('@/lib/services/job-queue.service', () => ({
getJobQueueService: () => jobQueueMock,
}));
vi.mock('@/lib/utils/audiobook-matcher', () => ({
findPlexMatch: findPlexMatchMock,
}));
vi.mock('@/lib/integrations/audible.service', () => ({
getAudibleService: () => ({
getAudiobookDetails: vi.fn().mockResolvedValue(null),
}),
}));
describe('Request Approval Workflow', () => {
beforeEach(() => {
vi.clearAllMocks();
authRequest = {
user: { id: 'user-1', sub: 'user-1', role: 'user' },
nextUrl: new URL('http://localhost/api/requests'),
json: vi.fn(),
};
requireAuthMock.mockImplementation((_req: any, handler: any) => handler(authRequest));
requireAdminMock.mockImplementation((_req: any, handler: any) => handler());
});
describe('1. Request Creation with Approval Logic', () => {
beforeEach(() => {
// Setup common mocks for request creation
prismaMock.request.findFirst.mockResolvedValue(null);
findPlexMatchMock.mockResolvedValue(null);
prismaMock.audiobook.findFirst.mockResolvedValue(null);
prismaMock.audiobook.create.mockResolvedValue({
id: 'ab-1',
title: 'Test Book',
author: 'Test Author',
audibleAsin: 'ASIN-1',
});
});
it('Admin user creates request → should auto-approve (status: pending)', async () => {
authRequest.user = { id: 'admin-1', sub: 'admin-1', role: 'admin' };
authRequest.json.mockResolvedValue({
audiobook: { asin: 'ASIN-1', title: 'Test Book', author: 'Test Author' },
});
prismaMock.user.findUnique.mockResolvedValue({
id: 'admin-1',
role: 'admin',
autoApproveRequests: null,
} as any);
prismaMock.request.create.mockResolvedValue({
id: 'req-1',
status: 'pending',
userId: 'admin-1',
audiobook: { id: 'ab-1', title: 'Test Book', author: 'Test Author', audibleAsin: 'ASIN-1' },
user: { id: 'admin-1', plexUsername: 'admin' },
} as any);
const { POST } = await import('@/app/api/requests/route');
const response = await POST({} as any);
const payload = await response.json();
expect(response.status).toBe(201);
expect(payload.success).toBe(true);
expect(prismaMock.request.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({ status: 'pending' }),
})
);
expect(jobQueueMock.addSearchJob).toHaveBeenCalledWith('req-1', expect.any(Object));
});
it('User with autoApproveRequests=true → should auto-approve (status: pending)', async () => {
authRequest.json.mockResolvedValue({
audiobook: { asin: 'ASIN-2', title: 'Test Book', author: 'Test Author' },
});
prismaMock.user.findUnique.mockResolvedValue({
id: 'user-1',
role: 'user',
autoApproveRequests: true,
} as any);
prismaMock.request.create.mockResolvedValue({
id: 'req-2',
status: 'pending',
userId: 'user-1',
audiobook: { id: 'ab-1', title: 'Test Book', author: 'Test Author', audibleAsin: 'ASIN-2' },
user: { id: 'user-1', plexUsername: 'testuser' },
} as any);
const { POST } = await import('@/app/api/requests/route');
const response = await POST({} as any);
const payload = await response.json();
expect(response.status).toBe(201);
expect(payload.success).toBe(true);
expect(prismaMock.request.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({ status: 'pending' }),
})
);
expect(jobQueueMock.addSearchJob).toHaveBeenCalledWith('req-2', expect.any(Object));
});
it('User with autoApproveRequests=false → should require approval (status: awaiting_approval)', async () => {
authRequest.json.mockResolvedValue({
audiobook: { asin: 'ASIN-3', title: 'Test Book', author: 'Test Author' },
});
prismaMock.user.findUnique.mockResolvedValue({
id: 'user-1',
role: 'user',
autoApproveRequests: false,
} as any);
prismaMock.request.create.mockResolvedValue({
id: 'req-3',
status: 'awaiting_approval',
userId: 'user-1',
audiobook: { id: 'ab-1', title: 'Test Book', author: 'Test Author', audibleAsin: 'ASIN-3' },
user: { id: 'user-1', plexUsername: 'testuser' },
} as any);
const { POST } = await import('@/app/api/requests/route');
const response = await POST({} as any);
const payload = await response.json();
expect(response.status).toBe(201);
expect(payload.success).toBe(true);
expect(prismaMock.request.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({ status: 'awaiting_approval' }),
})
);
});
it('User with autoApproveRequests=null + global=true → should auto-approve (status: pending)', async () => {
authRequest.json.mockResolvedValue({
audiobook: { asin: 'ASIN-4', title: 'Test Book', author: 'Test Author' },
});
prismaMock.user.findUnique.mockResolvedValue({
id: 'user-1',
role: 'user',
autoApproveRequests: null,
} as any);
prismaMock.configuration.findUnique.mockResolvedValue({
key: 'auto_approve_requests',
value: 'true',
} as any);
prismaMock.request.create.mockResolvedValue({
id: 'req-4',
status: 'pending',
userId: 'user-1',
audiobook: { id: 'ab-1', title: 'Test Book', author: 'Test Author', audibleAsin: 'ASIN-4' },
user: { id: 'user-1', plexUsername: 'testuser' },
} as any);
const { POST } = await import('@/app/api/requests/route');
const response = await POST({} as any);
const payload = await response.json();
expect(response.status).toBe(201);
expect(payload.success).toBe(true);
expect(prismaMock.request.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({ status: 'pending' }),
})
);
expect(jobQueueMock.addSearchJob).toHaveBeenCalledWith('req-4', expect.any(Object));
});
it('User with autoApproveRequests=null + global=false → should require approval (status: awaiting_approval)', async () => {
authRequest.json.mockResolvedValue({
audiobook: { asin: 'ASIN-5', title: 'Test Book', author: 'Test Author' },
});
prismaMock.user.findUnique.mockResolvedValue({
id: 'user-1',
role: 'user',
autoApproveRequests: null,
} as any);
prismaMock.configuration.findUnique.mockResolvedValue({
key: 'auto_approve_requests',
value: 'false',
} as any);
prismaMock.request.create.mockResolvedValue({
id: 'req-5',
status: 'awaiting_approval',
userId: 'user-1',
audiobook: { id: 'ab-1', title: 'Test Book', author: 'Test Author', audibleAsin: 'ASIN-5' },
user: { id: 'user-1', plexUsername: 'testuser' },
} as any);
const { POST } = await import('@/app/api/requests/route');
const response = await POST({} as any);
const payload = await response.json();
expect(response.status).toBe(201);
expect(payload.success).toBe(true);
expect(prismaMock.request.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({ status: 'awaiting_approval' }),
})
);
});
it('Request requiring approval should NOT trigger search job', async () => {
authRequest.json.mockResolvedValue({
audiobook: { asin: 'ASIN-6', title: 'Test Book', author: 'Test Author' },
});
prismaMock.user.findUnique.mockResolvedValue({
id: 'user-1',
role: 'user',
autoApproveRequests: false,
} as any);
prismaMock.request.create.mockResolvedValue({
id: 'req-6',
status: 'awaiting_approval',
userId: 'user-1',
audiobook: { id: 'ab-1', title: 'Test Book', author: 'Test Author', audibleAsin: 'ASIN-6' },
user: { id: 'user-1', plexUsername: 'testuser' },
} as any);
const { POST } = await import('@/app/api/requests/route');
await POST({} as any);
expect(jobQueueMock.addSearchJob).not.toHaveBeenCalled();
});
it('Auto-approved request SHOULD trigger search job', async () => {
authRequest.json.mockResolvedValue({
audiobook: { asin: 'ASIN-7', title: 'Test Book', author: 'Test Author' },
});
// Mock first request.findFirst call (check for existing requests by ASIN)
prismaMock.request.findFirst.mockResolvedValueOnce(null);
// Mock findPlexMatch
findPlexMatchMock.mockResolvedValueOnce(null);
// Mock audiobook.findFirst
prismaMock.audiobook.findFirst.mockResolvedValueOnce(null);
prismaMock.audiobook.create.mockResolvedValueOnce({
id: 'ab-7',
title: 'Test Book',
author: 'Test Author',
audibleAsin: 'ASIN-7',
} as any);
// Mock second request.findFirst call (check for user's existing request)
prismaMock.request.findFirst.mockResolvedValueOnce(null);
prismaMock.user.findUnique.mockResolvedValue({
id: 'user-1',
role: 'user',
autoApproveRequests: true,
plexUsername: 'testuser',
} as any);
prismaMock.request.create.mockResolvedValue({
id: 'req-7',
status: 'pending',
userId: 'user-1',
audiobook: { id: 'ab-7', title: 'Test Book', author: 'Test Author', audibleAsin: 'ASIN-7' },
user: { id: 'user-1', plexUsername: 'testuser' },
} as any);
const { POST } = await import('@/app/api/requests/route');
await POST({} as any);
expect(jobQueueMock.addSearchJob).toHaveBeenCalledWith('req-7', {
id: 'ab-7',
title: 'Test Book',
author: 'Test Author',
asin: 'ASIN-7',
});
});
it('Request with skipAutoSearch=true should have status awaiting_search and not trigger job', async () => {
authRequest.nextUrl = new URL('http://localhost/api/requests?skipAutoSearch=true');
authRequest.json.mockResolvedValue({
audiobook: { asin: 'ASIN-8', title: 'Test Book', author: 'Test Author' },
});
prismaMock.user.findUnique.mockResolvedValue({
id: 'user-1',
role: 'user',
autoApproveRequests: true,
} as any);
prismaMock.request.create.mockResolvedValue({
id: 'req-8',
status: 'awaiting_search',
userId: 'user-1',
audiobook: { id: 'ab-1', title: 'Test Book', author: 'Test Author', audibleAsin: 'ASIN-8' },
user: { id: 'user-1', plexUsername: 'testuser' },
} as any);
const { POST } = await import('@/app/api/requests/route');
await POST({} as any);
expect(prismaMock.request.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({ status: 'awaiting_search' }),
})
);
expect(jobQueueMock.addSearchJob).not.toHaveBeenCalled();
});
});
describe('2. Global Auto-Approve Settings API', () => {
beforeEach(() => {
authRequest.user = { id: 'admin-1', sub: 'admin-1', role: 'admin' };
});
it('GET /api/admin/settings/auto-approve returns current setting', async () => {
prismaMock.configuration.findUnique.mockResolvedValue({
key: 'auto_approve_requests',
value: 'true',
} as any);
const { GET } = await import('@/app/api/admin/settings/auto-approve/route');
const response = await GET({} as any);
const payload = await response.json();
expect(response.status).toBe(200);
expect(payload.autoApproveRequests).toBe(true);
expect(prismaMock.configuration.findUnique).toHaveBeenCalledWith({
where: { key: 'auto_approve_requests' },
});
});
it('PATCH /api/admin/settings/auto-approve updates setting', async () => {
const mockRequest = {
json: vi.fn().mockResolvedValue({ autoApproveRequests: false }),
};
prismaMock.configuration.upsert.mockResolvedValue({
key: 'auto_approve_requests',
value: 'false',
} as any);
const { PATCH } = await import('@/app/api/admin/settings/auto-approve/route');
const response = await PATCH(mockRequest as any);
const payload = await response.json();
expect(response.status).toBe(200);
expect(payload.autoApproveRequests).toBe(false);
expect(prismaMock.configuration.upsert).toHaveBeenCalledWith({
where: { key: 'auto_approve_requests' },
create: {
key: 'auto_approve_requests',
value: 'false',
},
update: {
value: 'false',
},
});
});
it('Non-admin user cannot access endpoint (403)', async () => {
authRequest.user = { id: 'user-1', sub: 'user-1', role: 'user' };
requireAdminMock.mockImplementation((_req: any, _handler: any) => {
return Promise.resolve(
new Response(JSON.stringify({ error: 'Forbidden' }), { status: 403 })
);
});
const { GET } = await import('@/app/api/admin/settings/auto-approve/route');
const response = await GET({} as any);
const payload = await response.json();
expect(response.status).toBe(403);
expect(payload.error).toBeDefined();
});
it('Missing/invalid values handled properly', async () => {
const mockRequest = {
json: vi.fn().mockResolvedValue({ autoApproveRequests: 'invalid' }),
};
const { PATCH } = await import('@/app/api/admin/settings/auto-approve/route');
const response = await PATCH(mockRequest as any);
const payload = await response.json();
expect(response.status).toBe(400);
expect(payload.error).toContain('must be a boolean');
});
});
describe('3. Per-User Auto-Approve Settings', () => {
beforeEach(() => {
authRequest.user = { id: 'admin-1', sub: 'admin-1', role: 'admin' };
});
it('PUT /api/admin/users/[id] can update autoApproveRequests', async () => {
const mockRequest = {
json: vi.fn().mockResolvedValue({
role: 'user',
autoApproveRequests: true,
}),
};
prismaMock.user.findUnique.mockResolvedValue({
id: 'user-1',
isSetupAdmin: false,
authProvider: 'plex',
plexUsername: 'testuser',
deletedAt: null,
} as any);
prismaMock.user.update.mockResolvedValue({
id: 'user-1',
plexUsername: 'testuser',
role: 'user',
autoApproveRequests: true,
} as any);
const { PUT } = await import('@/app/api/admin/users/[id]/route');
const response = await PUT(mockRequest as any, { params: Promise.resolve({ id: 'user-1' }) });
const payload = await response.json();
expect(response.status).toBe(200);
expect(payload.user.autoApproveRequests).toBe(true);
expect(prismaMock.user.update).toHaveBeenCalledWith({
where: { id: 'user-1' },
data: { role: 'user', autoApproveRequests: true },
select: {
id: true,
plexUsername: true,
role: true,
autoApproveRequests: true,
},
});
});
it('Cannot set admin user autoApproveRequests to false (validation error)', async () => {
const mockRequest = {
json: vi.fn().mockResolvedValue({
role: 'admin',
autoApproveRequests: false,
}),
};
prismaMock.user.findUnique.mockResolvedValue({
id: 'user-2',
isSetupAdmin: false,
authProvider: 'plex',
plexUsername: 'adminuser',
deletedAt: null,
} as any);
const { PUT } = await import('@/app/api/admin/users/[id]/route');
const response = await PUT(mockRequest as any, { params: Promise.resolve({ id: 'user-2' }) });
const payload = await response.json();
expect(response.status).toBe(400);
expect(payload.error).toContain('Admins must always auto-approve');
});
it('Non-admin user cannot update user settings (403)', async () => {
authRequest.user = { id: 'user-1', sub: 'user-1', role: 'user' };
requireAdminMock.mockImplementation((_req: any, _handler: any) => {
return Promise.resolve(
new Response(JSON.stringify({ error: 'Forbidden' }), { status: 403 })
);
});
const mockRequest = {
json: vi.fn().mockResolvedValue({
role: 'user',
autoApproveRequests: true,
}),
};
const { PUT } = await import('@/app/api/admin/users/[id]/route');
const response = await PUT(mockRequest as any, { params: Promise.resolve({ id: 'user-2' }) });
const payload = await response.json();
expect(response.status).toBe(403);
expect(payload.error).toBeDefined();
});
});
describe('4. Request Approval API', () => {
beforeEach(() => {
authRequest.user = { id: 'admin-1', sub: 'admin-1', role: 'admin' };
});
it('POST /api/admin/requests/[id]/approve with action=approve changes status to pending and triggers search job', async () => {
const mockRequest = {
json: vi.fn().mockResolvedValue({ action: 'approve' }),
};
prismaMock.request.findUnique.mockResolvedValue({
id: 'req-1',
status: 'awaiting_approval',
selectedTorrent: null,
userId: 'user-1',
audiobook: { id: 'ab-1', title: 'Test Book', author: 'Test Author', audibleAsin: 'ASIN-1' },
user: { id: 'user-1', plexUsername: 'testuser' },
} as any);
prismaMock.request.update.mockResolvedValue({
id: 'req-1',
status: 'pending',
userId: 'user-1',
audiobook: { id: 'ab-1', title: 'Test Book', author: 'Test Author', audibleAsin: 'ASIN-1' },
user: { id: 'user-1', plexUsername: 'testuser' },
} as any);
const { POST } = await import('@/app/api/admin/requests/[id]/approve/route');
const response = await POST(mockRequest as any, { params: Promise.resolve({ id: 'req-1' }) });
const payload = await response.json();
expect(response.status).toBe(200);
expect(payload.success).toBe(true);
expect(payload.message).toContain('approved');
expect(prismaMock.request.update).toHaveBeenCalledWith({
where: { id: 'req-1' },
data: { status: 'pending' },
include: {
audiobook: true,
user: {
select: {
id: true,
plexUsername: true,
},
},
},
});
expect(jobQueueMock.addSearchJob).toHaveBeenCalledWith('req-1', {
id: 'ab-1',
title: 'Test Book',
author: 'Test Author',
asin: 'ASIN-1',
});
});
it('POST /api/admin/requests/[id]/approve with action=deny changes status to denied and does NOT trigger search job', async () => {
const mockRequest = {
json: vi.fn().mockResolvedValue({ action: 'deny' }),
};
prismaMock.request.findUnique.mockResolvedValue({
id: 'req-2',
status: 'awaiting_approval',
selectedTorrent: null,
userId: 'user-1',
audiobook: { id: 'ab-2', title: 'Test Book 2', author: 'Test Author 2', audibleAsin: 'ASIN-2' },
user: { id: 'user-1', plexUsername: 'testuser' },
} as any);
prismaMock.request.update.mockResolvedValue({
id: 'req-2',
status: 'denied',
userId: 'user-1',
audiobook: { id: 'ab-2', title: 'Test Book 2', author: 'Test Author 2', audibleAsin: 'ASIN-2' },
user: { id: 'user-1', plexUsername: 'testuser' },
} as any);
const { POST } = await import('@/app/api/admin/requests/[id]/approve/route');
const response = await POST(mockRequest as any, { params: Promise.resolve({ id: 'req-2' }) });
const payload = await response.json();
expect(response.status).toBe(200);
expect(payload.success).toBe(true);
expect(payload.message).toContain('denied');
expect(prismaMock.request.update).toHaveBeenCalledWith({
where: { id: 'req-2' },
data: { status: 'denied' },
include: {
audiobook: true,
user: {
select: {
id: true,
plexUsername: true,
},
},
},
});
expect(jobQueueMock.addSearchJob).not.toHaveBeenCalled();
});
it('Cannot approve request that is not in awaiting_approval status (400)', async () => {
const mockRequest = {
json: vi.fn().mockResolvedValue({ action: 'approve' }),
};
prismaMock.request.findUnique.mockResolvedValue({
id: 'req-3',
status: 'pending',
userId: 'user-1',
audiobook: { id: 'ab-3', title: 'Test Book 3', author: 'Test Author 3', audibleAsin: 'ASIN-3' },
user: { id: 'user-1', plexUsername: 'testuser' },
} as any);
const { POST } = await import('@/app/api/admin/requests/[id]/approve/route');
const response = await POST(mockRequest as any, { params: Promise.resolve({ id: 'req-3' }) });
const payload = await response.json();
expect(response.status).toBe(400);
expect(payload.error).toBe('InvalidStatus');
expect(payload.message).toContain('not awaiting approval');
expect(payload.currentStatus).toBe('pending');
});
it('Cannot approve non-existent request (404)', async () => {
const mockRequest = {
json: vi.fn().mockResolvedValue({ action: 'approve' }),
};
prismaMock.request.findUnique.mockResolvedValue(null);
const { POST } = await import('@/app/api/admin/requests/[id]/approve/route');
const response = await POST(mockRequest as any, { params: Promise.resolve({ id: 'non-existent' }) });
const payload = await response.json();
expect(response.status).toBe(404);
expect(payload.error).toBe('NotFound');
expect(payload.message).toContain('not found');
});
it('Non-admin user cannot approve requests (403)', async () => {
authRequest.user = { id: 'user-1', sub: 'user-1', role: 'user' };
requireAdminMock.mockImplementation((_req: any, _handler: any) => {
return Promise.resolve(
new Response(JSON.stringify({ error: 'Forbidden' }), { status: 403 })
);
});
const mockRequest = {
json: vi.fn().mockResolvedValue({ action: 'approve' }),
};
const { POST } = await import('@/app/api/admin/requests/[id]/approve/route');
const response = await POST(mockRequest as any, { params: Promise.resolve({ id: 'req-1' }) });
const payload = await response.json();
expect(response.status).toBe(403);
expect(payload.error).toBeDefined();
});
it('Missing action parameter returns error (400)', async () => {
const mockRequest = {
json: vi.fn().mockResolvedValue({}),
};
prismaMock.request.findUnique.mockResolvedValue({
id: 'req-4',
status: 'awaiting_approval',
userId: 'user-1',
audiobook: { id: 'ab-4', title: 'Test Book 4', author: 'Test Author 4', audibleAsin: 'ASIN-4' },
user: { id: 'user-1', plexUsername: 'testuser' },
} as any);
const { POST } = await import('@/app/api/admin/requests/[id]/approve/route');
const response = await POST(mockRequest as any, { params: Promise.resolve({ id: 'req-4' }) });
const payload = await response.json();
expect(response.status).toBe(400);
expect(payload.error).toBe('ValidationError');
expect(payload.message).toContain('approve');
});
it('Invalid action parameter returns error (400)', async () => {
const mockRequest = {
json: vi.fn().mockResolvedValue({ action: 'invalid' }),
};
prismaMock.request.findUnique.mockResolvedValue({
id: 'req-5',
status: 'awaiting_approval',
userId: 'user-1',
audiobook: { id: 'ab-5', title: 'Test Book 5', author: 'Test Author 5', audibleAsin: 'ASIN-5' },
user: { id: 'user-1', plexUsername: 'testuser' },
} as any);
const { POST } = await import('@/app/api/admin/requests/[id]/approve/route');
const response = await POST(mockRequest as any, { params: Promise.resolve({ id: 'req-5' }) });
const payload = await response.json();
expect(response.status).toBe(400);
expect(payload.error).toBe('ValidationError');
expect(payload.message).toContain('approve');
});
});
describe('5. Pending Approval Requests API', () => {
beforeEach(() => {
authRequest.user = { id: 'admin-1', sub: 'admin-1', role: 'admin' };
});
it('GET /api/admin/requests/pending-approval returns only awaiting_approval requests', async () => {
prismaMock.request.findMany.mockResolvedValue([
{
id: 'req-1',
status: 'awaiting_approval',
userId: 'user-1',
audiobook: { id: 'ab-1', title: 'Test Book 1', author: 'Test Author 1' },
user: { id: 'user-1', plexUsername: 'user1', avatarUrl: null },
createdAt: new Date(),
},
{
id: 'req-2',
status: 'awaiting_approval',
userId: 'user-2',
audiobook: { id: 'ab-2', title: 'Test Book 2', author: 'Test Author 2' },
user: { id: 'user-2', plexUsername: 'user2', avatarUrl: null },
createdAt: new Date(),
},
] as any);
const { GET } = await import('@/app/api/admin/requests/pending-approval/route');
const response = await GET({} as any);
const payload = await response.json();
expect(response.status).toBe(200);
expect(payload.success).toBe(true);
expect(payload.requests).toHaveLength(2);
expect(payload.count).toBe(2);
expect(prismaMock.request.findMany).toHaveBeenCalledWith({
where: {
status: 'awaiting_approval',
deletedAt: null,
},
include: {
audiobook: true,
user: {
select: {
id: true,
plexUsername: true,
avatarUrl: true,
},
},
},
orderBy: { createdAt: 'desc' },
});
});
it('Returns requests with audiobook and user details', async () => {
const mockDate = new Date('2024-01-01');
prismaMock.request.findMany.mockResolvedValue([
{
id: 'req-1',
status: 'awaiting_approval',
userId: 'user-1',
audiobook: {
id: 'ab-1',
title: 'Test Book',
author: 'Test Author',
audibleAsin: 'ASIN-1',
coverArtUrl: 'https://example.com/cover.jpg',
},
user: {
id: 'user-1',
plexUsername: 'testuser',
avatarUrl: 'https://example.com/avatar.jpg',
},
createdAt: mockDate,
},
] as any);
const { GET } = await import('@/app/api/admin/requests/pending-approval/route');
const response = await GET({} as any);
const payload = await response.json();
expect(response.status).toBe(200);
expect(payload.requests[0]).toMatchObject({
id: 'req-1',
status: 'awaiting_approval',
audiobook: {
id: 'ab-1',
title: 'Test Book',
author: 'Test Author',
},
user: {
id: 'user-1',
plexUsername: 'testuser',
},
});
});
it('Non-admin user cannot access endpoint (403)', async () => {
authRequest.user = { id: 'user-1', sub: 'user-1', role: 'user' };
requireAdminMock.mockImplementation((_req: any, _handler: any) => {
return Promise.resolve(
new Response(JSON.stringify({ error: 'Forbidden' }), { status: 403 })
);
});
const { GET } = await import('@/app/api/admin/requests/pending-approval/route');
const response = await GET({} as any);
const payload = await response.json();
expect(response.status).toBe(403);
expect(payload.error).toBeDefined();
});
});
});