mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-02 20:30:10 +00:00
4322c3af90
Add sessions_invalidated_at to users (migration + Prisma schema) to support immediate session revocation. Set sessionsInvalidatedAt when an admin revokes a user's login token and enforce revocation checks in auth middleware and the refresh endpoint (compare token iat against sessionsInvalidatedAt). Add optional iat fields to JWT payload types. Scrub token from browser history after token-login. Consolidate rate-limiting logic into src/lib/utils/rateLimit.ts (rename/merge previous auth/apiToken rate limiter implementations), remove the old apiTokenRateLimit.ts, and update imports and tests to use the new module.
96 lines
3.2 KiB
TypeScript
96 lines
3.2 KiB
TypeScript
/**
|
|
* Component: Token Login Route Tests
|
|
* Documentation: documentation/testing.md
|
|
*/
|
|
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { createPrismaMock } from '../helpers/prisma';
|
|
|
|
const prismaMock = createPrismaMock();
|
|
const generateAccessTokenMock = vi.hoisted(() => vi.fn());
|
|
const generateRefreshTokenMock = vi.hoisted(() => vi.fn());
|
|
const checkTokenLoginRateLimitMock = vi.hoisted(() => vi.fn());
|
|
|
|
vi.mock('@/lib/db', () => ({
|
|
prisma: prismaMock,
|
|
}));
|
|
|
|
vi.mock('@/lib/utils/jwt', () => ({
|
|
generateAccessToken: generateAccessTokenMock,
|
|
generateRefreshToken: generateRefreshTokenMock,
|
|
}));
|
|
|
|
vi.mock('@/lib/utils/rateLimit', () => ({
|
|
checkTokenLoginRateLimit: checkTokenLoginRateLimitMock,
|
|
}));
|
|
|
|
function makeRequest(body: Record<string, unknown>, ip = '127.0.0.1') {
|
|
return {
|
|
headers: { get: vi.fn().mockReturnValue(ip) },
|
|
json: vi.fn().mockResolvedValue(body),
|
|
};
|
|
}
|
|
|
|
describe('POST /api/auth/token/login', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
generateAccessTokenMock.mockReturnValue('access-token');
|
|
generateRefreshTokenMock.mockReturnValue('refresh-token');
|
|
checkTokenLoginRateLimitMock.mockReturnValue({ allowed: true, retryAfterSeconds: 900 });
|
|
});
|
|
|
|
it('authenticates user with a valid token', async () => {
|
|
prismaMock.user.findFirst.mockResolvedValueOnce({
|
|
id: 'u1',
|
|
plexId: 'plex-1',
|
|
plexUsername: 'testuser',
|
|
plexEmail: 'test@example.com',
|
|
avatarUrl: null,
|
|
role: 'user',
|
|
});
|
|
prismaMock.user.update.mockResolvedValueOnce({});
|
|
|
|
const { POST } = await import('@/app/api/auth/token/login/route');
|
|
const response = await POST(makeRequest({ token: 'rmab_valid_token' }) as any);
|
|
const payload = await response.json();
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(payload.accessToken).toBe('access-token');
|
|
expect(payload.refreshToken).toBe('refresh-token');
|
|
expect(payload.user.username).toBe('testuser');
|
|
expect(payload.user.email).toBe('test@example.com');
|
|
});
|
|
|
|
it('returns 400 when token parameter is missing', async () => {
|
|
const { POST } = await import('@/app/api/auth/token/login/route');
|
|
const response = await POST(makeRequest({}) as any);
|
|
const payload = await response.json();
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(payload.error).toMatch(/Missing token/);
|
|
});
|
|
|
|
it('returns 401 when token is invalid or user not found', async () => {
|
|
prismaMock.user.findFirst.mockResolvedValueOnce(null);
|
|
|
|
const { POST } = await import('@/app/api/auth/token/login/route');
|
|
const response = await POST(makeRequest({ token: 'rmab_invalid' }) as any);
|
|
const payload = await response.json();
|
|
|
|
expect(response.status).toBe(401);
|
|
expect(payload.error).toMatch(/Invalid token/);
|
|
});
|
|
|
|
it('returns 429 when rate limit is exceeded', async () => {
|
|
checkTokenLoginRateLimitMock.mockReturnValue({ allowed: false, retryAfterSeconds: 600 });
|
|
|
|
const { POST } = await import('@/app/api/auth/token/login/route');
|
|
const response = await POST(makeRequest({ token: 'rmab_any' }) as any);
|
|
const payload = await response.json();
|
|
|
|
expect(response.status).toBe(429);
|
|
expect(payload.error).toMatch(/Too many login attempts/);
|
|
expect(response.headers.get('Retry-After')).toBe('600');
|
|
});
|
|
});
|