Files
samreshu_backend/tests/integration/auth.routes.test.ts
Anton 223feed0e0 feat: синхронизация бэкенда с документацией (AGENT_TASK_BACKEND_SYNC)
- Добвлен @fastify/cookie и настройку httpOnly cookie для refresh token
- Добавлен префикс /api/v1 для auth, profile, tests, admin
- Скорректировано в Login: возвращать user (id, nickname, avatarUrl, role, emailVerified),
  ставить refreshToken в Set-Cookie
- Скорректировано в Logout: Bearer + cookie, пустое тело, 200 + { message }, очищать cookie
- Скорректировано в Refresh: token из cookie, пустое тело, 200 + { accessToken }, Set-Cookie
- Добавлено в getPrivateProfile: поля role и plan
- Скорректировано в Tests: score = количество правильных, ответ { score, totalQuestions, percentage }
- Добавлено в question_cache_meta: поля valid, retryCount, questionsGenerated
- Обновлены тесты
2026-03-06 13:58:34 +03:00

292 lines
8.9 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
vi.mock('../../src/config/env.js', () => ({
env: {
NODE_ENV: 'test',
JWT_SECRET: 'test-secret',
},
}));
import { buildAuthTestApp } from '../helpers/build-test-app.js';
import {
createMockDb,
selectChain,
insertChain,
updateChain,
deleteChain,
} from '../test-utils.js';
vi.mock('../../src/utils/password.js', () => ({
hashPassword: vi.fn().mockResolvedValue('hashed-password'),
verifyPassword: vi.fn().mockResolvedValue(true),
}));
vi.mock('../../src/utils/jwt.js', () => ({
signAccessToken: vi.fn().mockResolvedValue('access-token'),
signRefreshToken: vi.fn().mockResolvedValue('refresh-token'),
verifyToken: vi.fn(),
isAccessPayload: vi.fn((p: { type?: string }) => p?.type === 'access'),
isRefreshPayload: vi.fn(),
hashToken: vi.fn((t: string) => `hash-${t}`),
}));
vi.mock('node:crypto', () => ({
randomBytes: vi.fn(() => ({ toString: () => 'abc123' })),
randomUUID: vi.fn(() => 'uuid-session-1'),
}));
import { isRefreshPayload, verifyToken } from '../../src/utils/jwt.js';
describe('Auth routes integration', () => {
let app: Awaited<ReturnType<typeof buildAuthTestApp>>;
let mockDb: ReturnType<typeof createMockDb>;
beforeEach(async () => {
vi.clearAllMocks();
mockDb = createMockDb();
app = await buildAuthTestApp(mockDb as never);
});
describe('POST /auth/register', () => {
it('returns 201 with userId and verificationCode when registration succeeds', async () => {
(mockDb.select as ReturnType<typeof vi.fn>)
.mockReturnValueOnce(selectChain([]))
.mockReturnValueOnce(selectChain([]));
(mockDb.insert as ReturnType<typeof vi.fn>)
.mockReturnValueOnce(insertChain([{ id: 'user-123' }]))
.mockReturnValueOnce(insertChain([]));
const res = await app.inject({
method: 'POST',
url: '/api/v1/auth/register',
payload: {
email: 'test@example.com',
password: 'password123',
nickname: 'tester',
},
});
expect(res.statusCode).toBe(201);
const body = JSON.parse(res.body);
expect(body.userId).toBe('user-123');
expect(body.message).toContain('verify your email');
expect(body.verificationCode).toBeDefined();
});
it('returns 409 when email is already taken', async () => {
(mockDb.select as ReturnType<typeof vi.fn>).mockReturnValueOnce(
selectChain([{ id: 'existing', email: 'test@example.com' }])
);
const res = await app.inject({
method: 'POST',
url: '/api/v1/auth/register',
payload: {
email: 'test@example.com',
password: 'password123',
nickname: 'tester',
},
});
expect(res.statusCode).toBe(409);
const body = JSON.parse(res.body);
expect(body.error?.code).toBe('EMAIL_TAKEN');
});
it('returns 422 when validation fails', async () => {
const res = await app.inject({
method: 'POST',
url: '/api/v1/auth/register',
payload: {
email: 'short',
password: '123', // too short
nickname: 'x', // too short
},
});
expect([400, 422]).toContain(res.statusCode);
const body = JSON.parse(res.body);
expect(body.error?.code ?? body.error).toBeDefined();
});
});
describe('POST /auth/login', () => {
it('returns user and tokens when credentials are valid', async () => {
const mockUser = {
id: 'user-1',
email: 'test@example.com',
nickname: 'tester',
avatarUrl: null,
role: 'free',
emailVerifiedAt: null,
passwordHash: 'hashed',
};
(mockDb.select as ReturnType<typeof vi.fn>).mockReturnValueOnce(
selectChain([mockUser])
);
(mockDb.insert as ReturnType<typeof vi.fn>).mockReturnValueOnce(
insertChain([])
);
const res = await app.inject({
method: 'POST',
url: '/api/v1/auth/login',
payload: { email: 'test@example.com', password: 'password123' },
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.user).toBeDefined();
expect(body.user.id).toBe('user-1');
expect(body.user.email).toBe('test@example.com');
expect(body.user.nickname).toBeDefined();
expect(body.accessToken).toBe('access-token');
expect(body.refreshToken).toBe('refresh-token');
});
it('returns 401 when credentials are invalid', async () => {
(mockDb.select as ReturnType<typeof vi.fn>).mockReturnValueOnce(
selectChain([])
);
const res = await app.inject({
method: 'POST',
url: '/api/v1/auth/login',
payload: { email: 'unknown@example.com', password: 'wrong' },
});
expect(res.statusCode).toBe(401);
});
});
describe('POST /auth/logout', () => {
it('returns 200 with message on success (Bearer + cookie)', async () => {
vi.mocked(verifyToken).mockResolvedValueOnce({
sub: 'user-1',
email: 'test@example.com',
type: 'access',
} as never);
(mockDb.delete as ReturnType<typeof vi.fn>).mockReturnValueOnce(
deleteChain()
);
const res = await app.inject({
method: 'POST',
url: '/api/v1/auth/logout',
headers: { authorization: 'Bearer valid-access-token' },
payload: {},
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.message).toBe('Logged out successfully');
});
});
describe('POST /auth/refresh', () => {
it('returns accessToken when refresh token from cookie is valid', async () => {
vi.mocked(verifyToken).mockResolvedValueOnce({
sub: 'user-1',
sid: 'sid-1',
type: 'refresh',
} as never);
vi.mocked(isRefreshPayload).mockReturnValueOnce(true);
(mockDb.select as ReturnType<typeof vi.fn>)
.mockReturnValueOnce(selectChain([{ id: 'sess-1', userId: 'user-1' }]))
.mockReturnValueOnce(selectChain([{ id: 'user-1', email: 'test@example.com' }]));
(mockDb.delete as ReturnType<typeof vi.fn>).mockReturnValueOnce(
deleteChain()
);
(mockDb.insert as ReturnType<typeof vi.fn>).mockReturnValueOnce(
insertChain([])
);
const res = await app.inject({
method: 'POST',
url: '/api/v1/auth/refresh',
payload: {},
headers: { cookie: 'refreshToken=valid-refresh-token' },
});
expect(res.statusCode).toBe(200);
const body = JSON.parse(res.body);
expect(body.accessToken).toBe('access-token');
expect(body.refreshToken).toBeUndefined();
});
});
describe('POST /auth/verify-email', () => {
it('returns success when code is valid', async () => {
const mockCode = {
id: 'code-1',
userId: 'user-1',
code: 'ABC123',
expiresAt: new Date(Date.now() + 60000),
};
(mockDb.select as ReturnType<typeof vi.fn>)
.mockReturnValueOnce(selectChain([mockCode]))
.mockReturnValueOnce(selectChain([{ id: 'user-1', emailVerifiedAt: null }]));
(mockDb.update as ReturnType<typeof vi.fn>).mockReturnValueOnce(
updateChain([{ id: 'user-1' }])
);
(mockDb.delete as ReturnType<typeof vi.fn>).mockReturnValueOnce(
deleteChain()
);
const res = await app.inject({
method: 'POST',
url: '/api/v1/auth/verify-email',
payload: { userId: 'user-1', code: 'ABC123' },
});
expect(res.statusCode).toBe(200);
});
});
describe('POST /auth/forgot-password', () => {
it('returns 200 with generic message', async () => {
(mockDb.select as ReturnType<typeof vi.fn>).mockReturnValueOnce(
selectChain([{ id: 'user-1', email: 'test@example.com' }])
);
(mockDb.insert as ReturnType<typeof vi.fn>).mockReturnValueOnce(
insertChain([])
);
const res = await app.inject({
method: 'POST',
url: '/api/v1/auth/forgot-password',
payload: { email: 'test@example.com' },
});
expect(res.statusCode).toBe(200);
});
});
describe('POST /auth/reset-password', () => {
it('returns 200 when token is valid', async () => {
const mockRecord = {
id: 'rec-1',
userId: 'user-1',
expiresAt: new Date(Date.now() + 60000),
};
(mockDb.select as ReturnType<typeof vi.fn>).mockReturnValueOnce(
selectChain([mockRecord])
);
(mockDb.update as ReturnType<typeof vi.fn>).mockReturnValueOnce(
updateChain([{ id: 'user-1' }])
);
(mockDb.delete as ReturnType<typeof vi.fn>).mockReturnValueOnce(
deleteChain()
);
const res = await app.inject({
method: 'POST',
url: '/api/v1/auth/reset-password',
payload: { token: 'valid-token', newPassword: 'newPassword123' },
});
expect(res.statusCode).toBe(200);
});
});
});