From 24aa6afefc4c92e85afda4ea374a973678a78249 Mon Sep 17 00:00:00 2001 From: Michael Borohovski Date: Wed, 4 Mar 2026 16:57:02 -0800 Subject: [PATCH] Add tests for admin token creation role enforcement --- tests/api/admin-api-tokens.routes.test.ts | 215 ++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 tests/api/admin-api-tokens.routes.test.ts diff --git a/tests/api/admin-api-tokens.routes.test.ts b/tests/api/admin-api-tokens.routes.test.ts new file mode 100644 index 0000000..c7865b8 --- /dev/null +++ b/tests/api/admin-api-tokens.routes.test.ts @@ -0,0 +1,215 @@ +/** + * 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'); + }); + }); +});