mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-02 20:30:10 +00:00
216 lines
6.8 KiB
TypeScript
216 lines
6.8 KiB
TypeScript
/**
|
|
* Component: Admin API Tokens Route Tests
|
|
* Documentation: documentation/testing.md
|
|
*/
|
|
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { createPrismaMock } from '../helpers/prisma';
|
|
|
|
// Valid UUIDs for testing
|
|
const ADMIN_ID = '11111111-1111-1111-1111-111111111111';
|
|
const USER_ID = '22222222-2222-2222-2222-222222222222';
|
|
const ADMIN2_ID = '33333333-3333-3333-3333-333333333333';
|
|
const NONEXISTENT_ID = '99999999-9999-9999-9999-999999999999';
|
|
|
|
let authRequest: any;
|
|
|
|
const prismaMock = createPrismaMock();
|
|
const requireAuthMock = vi.hoisted(() => vi.fn());
|
|
const requireAdminMock = vi.hoisted(() => vi.fn());
|
|
const checkApiTokenCreateRateLimitMock = vi.hoisted(() => vi.fn());
|
|
const generateApiTokenMock = vi.hoisted(() => vi.fn());
|
|
|
|
vi.mock('@/lib/db', () => ({
|
|
prisma: prismaMock,
|
|
}));
|
|
|
|
vi.mock('@/lib/middleware/auth', () => ({
|
|
requireAuth: requireAuthMock,
|
|
requireAdmin: requireAdminMock,
|
|
}));
|
|
|
|
vi.mock('@/lib/utils/apiTokenRateLimit', () => ({
|
|
checkApiTokenCreateRateLimit: checkApiTokenCreateRateLimitMock,
|
|
}));
|
|
|
|
vi.mock('@/lib/utils/api-token', () => ({
|
|
generateApiToken: generateApiTokenMock,
|
|
}));
|
|
|
|
describe('Admin API tokens routes', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
authRequest = {
|
|
user: { id: ADMIN_ID, username: 'admin', role: 'admin' },
|
|
json: vi.fn(),
|
|
};
|
|
requireAuthMock.mockImplementation((_req: any, handler: any) => handler(authRequest));
|
|
requireAdminMock.mockImplementation((_req: any, handler: any) => handler());
|
|
checkApiTokenCreateRateLimitMock.mockReturnValue({ allowed: true });
|
|
generateApiTokenMock.mockReturnValue({
|
|
fullToken: 'rmab_test_full_token',
|
|
tokenHash: 'hashed_token',
|
|
tokenPrefix: 'rmab_test',
|
|
});
|
|
});
|
|
|
|
describe('POST /api/admin/api-tokens', () => {
|
|
it('creates token for self with own role when no userId specified', async () => {
|
|
authRequest.json.mockResolvedValueOnce({ name: 'Test Token' });
|
|
|
|
prismaMock.user.findUnique.mockResolvedValueOnce({
|
|
id: ADMIN_ID,
|
|
role: 'admin',
|
|
plexUsername: 'admin',
|
|
});
|
|
prismaMock.apiToken.count.mockResolvedValueOnce(0);
|
|
prismaMock.apiToken.create.mockResolvedValueOnce({
|
|
id: 'token-1',
|
|
name: 'Test Token',
|
|
tokenPrefix: 'rmab_test',
|
|
role: 'admin',
|
|
expiresAt: null,
|
|
createdAt: new Date(),
|
|
});
|
|
|
|
const { POST } = await import('@/app/api/admin/api-tokens/route');
|
|
const response = await POST({} as any);
|
|
const payload = await response.json();
|
|
|
|
expect(response.status).toBe(201);
|
|
expect(payload.token.role).toBe('admin');
|
|
expect(payload.fullToken).toBe('rmab_test_full_token');
|
|
});
|
|
|
|
it('creates token for another user with their role', async () => {
|
|
authRequest.json.mockResolvedValueOnce({
|
|
name: 'Token for User',
|
|
userId: USER_ID,
|
|
});
|
|
|
|
prismaMock.user.findUnique.mockResolvedValueOnce({
|
|
id: USER_ID,
|
|
role: 'user',
|
|
plexUsername: 'regularuser',
|
|
});
|
|
prismaMock.apiToken.count.mockResolvedValueOnce(0);
|
|
prismaMock.apiToken.create.mockResolvedValueOnce({
|
|
id: 'token-2',
|
|
name: 'Token for User',
|
|
tokenPrefix: 'rmab_test',
|
|
role: 'user',
|
|
expiresAt: null,
|
|
createdAt: new Date(),
|
|
});
|
|
|
|
const { POST } = await import('@/app/api/admin/api-tokens/route');
|
|
const response = await POST({} as any);
|
|
const payload = await response.json();
|
|
|
|
expect(response.status).toBe(201);
|
|
expect(payload.token.role).toBe('user');
|
|
});
|
|
|
|
it('rejects role override when role differs from target user role', async () => {
|
|
authRequest.json.mockResolvedValueOnce({
|
|
name: 'Escalation Attempt',
|
|
userId: USER_ID,
|
|
role: 'admin', // Trying to give admin role to a regular user
|
|
});
|
|
|
|
prismaMock.user.findUnique.mockResolvedValueOnce({
|
|
id: USER_ID,
|
|
role: 'user', // Target user is actually a regular user
|
|
plexUsername: 'regularuser',
|
|
});
|
|
|
|
const { POST } = await import('@/app/api/admin/api-tokens/route');
|
|
const response = await POST({} as any);
|
|
const payload = await response.json();
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(payload.error).toContain("must match target user's role");
|
|
});
|
|
|
|
it('rejects role downgrade when role differs from target user role', async () => {
|
|
authRequest.json.mockResolvedValueOnce({
|
|
name: 'Downgrade Attempt',
|
|
userId: ADMIN2_ID,
|
|
role: 'user', // Trying to give user role to an admin
|
|
});
|
|
|
|
prismaMock.user.findUnique.mockResolvedValueOnce({
|
|
id: ADMIN2_ID,
|
|
role: 'admin', // Target user is actually an admin
|
|
plexUsername: 'otheradmin',
|
|
});
|
|
|
|
const { POST } = await import('@/app/api/admin/api-tokens/route');
|
|
const response = await POST({} as any);
|
|
const payload = await response.json();
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(payload.error).toContain("must match target user's role");
|
|
});
|
|
|
|
it('accepts role when it matches target user role', async () => {
|
|
authRequest.json.mockResolvedValueOnce({
|
|
name: 'Matching Role',
|
|
userId: USER_ID,
|
|
role: 'user', // Explicitly specifying role that matches
|
|
});
|
|
|
|
prismaMock.user.findUnique.mockResolvedValueOnce({
|
|
id: USER_ID,
|
|
role: 'user',
|
|
plexUsername: 'regularuser',
|
|
});
|
|
prismaMock.apiToken.count.mockResolvedValueOnce(0);
|
|
prismaMock.apiToken.create.mockResolvedValueOnce({
|
|
id: 'token-3',
|
|
name: 'Matching Role',
|
|
tokenPrefix: 'rmab_test',
|
|
role: 'user',
|
|
expiresAt: null,
|
|
createdAt: new Date(),
|
|
});
|
|
|
|
const { POST } = await import('@/app/api/admin/api-tokens/route');
|
|
const response = await POST({} as any);
|
|
|
|
expect(response.status).toBe(201);
|
|
});
|
|
|
|
it('returns 404 when target user does not exist', async () => {
|
|
authRequest.json.mockResolvedValueOnce({
|
|
name: 'Token for Ghost',
|
|
userId: NONEXISTENT_ID,
|
|
});
|
|
|
|
prismaMock.user.findUnique.mockResolvedValueOnce(null);
|
|
|
|
const { POST } = await import('@/app/api/admin/api-tokens/route');
|
|
const response = await POST({} as any);
|
|
const payload = await response.json();
|
|
|
|
expect(response.status).toBe(404);
|
|
expect(payload.error).toBe('Target user not found');
|
|
});
|
|
|
|
it('returns 429 when rate limited', async () => {
|
|
checkApiTokenCreateRateLimitMock.mockReturnValueOnce({
|
|
allowed: false,
|
|
retryAfterSeconds: 60,
|
|
});
|
|
|
|
authRequest.json.mockResolvedValueOnce({ name: 'Rate Limited Token' });
|
|
|
|
const { POST } = await import('@/app/api/admin/api-tokens/route');
|
|
const response = await POST({} as any);
|
|
|
|
expect(response.status).toBe(429);
|
|
expect(response.headers.get('Retry-After')).toBe('60');
|
|
});
|
|
});
|
|
});
|