From 6bedf0b28a192b060322214cad93c828ccecdd7d Mon Sep 17 00:00:00 2001 From: Kirill Date: Fri, 22 May 2026 11:57:11 +0500 Subject: [PATCH] test(server): add password auth and account methods tests --- .../src/routes/__tests__/auth-methods.test.js | 170 ++++++++++++++++++ .../routes/__tests__/auth-password.test.js | 134 ++++++++++++++ 2 files changed, 304 insertions(+) create mode 100644 server/src/routes/__tests__/auth-methods.test.js create mode 100644 server/src/routes/__tests__/auth-password.test.js diff --git a/server/src/routes/__tests__/auth-methods.test.js b/server/src/routes/__tests__/auth-methods.test.js new file mode 100644 index 0000000..c0fa825 --- /dev/null +++ b/server/src/routes/__tests__/auth-methods.test.js @@ -0,0 +1,170 @@ +import Fastify from 'fastify' +import jwt from '@fastify/jwt' +import { afterAll, beforeEach, beforeAll, describe, expect, it } from 'vitest' +import { prisma } from '../../lib/prisma.js' +import { registerAuthRoutes } from '../auth.js' + +const JWT_SECRET = 'test-secret' + +async function buildApp() { + const app = Fastify({ logger: false }) + await app.register(jwt, { secret: JWT_SECRET }) + app.decorate('authenticate', async function (request, reply) { + try { await request.jwtVerify() } catch { + return reply.code(401).send({ error: 'Unauthorized' }) + } + }) + app.decorate('eventBus', { emit: () => {} }) + await registerAuthRoutes(app) + await app.ready() + return app +} + +function signToken(app, userId, email) { + return app.jwt.sign({ sub: userId, email }) +} + +async function createUser(email) { + const user = await prisma.user.create({ + data: { email, displayName: 'Test', avatar: null, avatarStyle: 'avataaars' }, + }) + await prisma.notificationPreference.create({ data: { userId: user.id, globalEnabled: true } }) + return user +} + +describe('GET /api/me/auth-methods', () => { + let app, user, token + const email = `test-methods-${Date.now()}@example.com` + + beforeAll(async () => { app = await buildApp() }) + afterAll(async () => { + await prisma.notificationPreference.deleteMany({ where: { userId: user?.id } }) + await prisma.user.deleteMany({ where: { email } }) + await app.close() + }) + + beforeEach(async () => { + await prisma.oAuthAccount.deleteMany({ where: { user: { email } } }) + await prisma.notificationPreference.deleteMany({ where: { user: { email } } }) + await prisma.user.deleteMany({ where: { email } }) + user = await createUser(email) + token = signToken(app, user.id, email) + }) + + it('returns methods for user without any method', async () => { + const res = await app.inject({ + method: 'GET', url: '/api/me/auth-methods', + headers: { authorization: `Bearer ${token}` }, + }) + expect(res.statusCode).toBe(200) + const body = JSON.parse(res.body) + expect(body.methods.find((m) => m.type === 'password').active).toBe(false) + expect(body.methods.find((m) => m.type === 'vk').active).toBe(false) + expect(body.methods.find((m) => m.type === 'yandex').active).toBe(false) + }) + + it('returns password as active after setting it', async () => { + await prisma.user.update({ where: { id: user.id }, data: { passwordHash: 'hashed' } }) + const res = await app.inject({ + method: 'GET', url: '/api/me/auth-methods', + headers: { authorization: `Bearer ${token}` }, + }) + expect(JSON.parse(res.body).methods.find((m) => m.type === 'password').active).toBe(true) + }) +}) + +describe('POST /api/me/password', () => { + let app, user, token + const email = `test-set-pw-${Date.now()}@example.com` + + beforeAll(async () => { app = await buildApp() }) + afterAll(async () => { + await prisma.notificationPreference.deleteMany({ where: { userId: user?.id } }) + await prisma.user.deleteMany({ where: { email } }) + await app.close() + }) + + beforeEach(async () => { + await prisma.notificationPreference.deleteMany({ where: { user: { email } } }) + await prisma.user.deleteMany({ where: { email } }) + user = await createUser(email) + token = signToken(app, user.id, email) + }) + + it('sets password', async () => { + const res = await app.inject({ + method: 'POST', url: '/api/me/password', + headers: { authorization: `Bearer ${token}` }, + payload: { password: 'Test123!@' }, + }) + expect(res.statusCode).toBe(200) + + const u = await prisma.user.findUnique({ where: { id: user.id } }) + expect(u.passwordHash).toBeTruthy() + }) + + it('rejects if password already set', async () => { + await prisma.user.update({ where: { id: user.id }, data: { passwordHash: 'existing' } }) + const res = await app.inject({ + method: 'POST', url: '/api/me/password', + headers: { authorization: `Bearer ${token}` }, + payload: { password: 'Test123!@' }, + }) + expect(res.statusCode).toBe(409) + }) +}) + +describe('DELETE /api/me/oauth/:provider', () => { + let app, user, token + const email = `test-unlink-${Date.now()}@example.com` + + beforeAll(async () => { app = await buildApp() }) + afterAll(async () => { + await prisma.oAuthAccount.deleteMany({ where: { user: { email } } }) + await prisma.notificationPreference.deleteMany({ where: { user: { email } } }) + await prisma.user.deleteMany({ where: { email } }) + await app.close() + }) + + beforeEach(async () => { + await prisma.oAuthAccount.deleteMany({ where: { user: { email } } }) + await prisma.user.deleteMany({ where: { email } }) + user = await createUser(email) + token = signToken(app, user.id, email) + }) + + it('returns 404 for non-linked provider', async () => { + const res = await app.inject({ + method: 'DELETE', url: '/api/me/oauth/vk', + headers: { authorization: `Bearer ${token}` }, + }) + expect(res.statusCode).toBe(404) + }) + + it('unlinks a provider', async () => { + await prisma.user.update({ where: { id: user.id }, data: { passwordHash: 'hashed' } }) + await prisma.oAuthAccount.create({ + data: { provider: 'vk', providerUserId: '123', userId: user.id }, + }) + const res = await app.inject({ + method: 'DELETE', url: '/api/me/oauth/vk', + headers: { authorization: `Bearer ${token}` }, + }) + expect(res.statusCode).toBe(200) + + const count = await prisma.oAuthAccount.count({ where: { userId: user.id } }) + expect(count).toBe(0) + }) + + it('rejects removing last method without password', async () => { + await prisma.oAuthAccount.create({ + data: { provider: 'vk', providerUserId: '123', userId: user.id }, + }) + const res = await app.inject({ + method: 'DELETE', url: '/api/me/oauth/vk', + headers: { authorization: `Bearer ${token}` }, + }) + expect(res.statusCode).toBe(400) + expect(JSON.parse(res.body).error).toContain('последний метод') + }) +}) diff --git a/server/src/routes/__tests__/auth-password.test.js b/server/src/routes/__tests__/auth-password.test.js new file mode 100644 index 0000000..c281c90 --- /dev/null +++ b/server/src/routes/__tests__/auth-password.test.js @@ -0,0 +1,134 @@ +import Fastify from 'fastify' +import jwt from '@fastify/jwt' +import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest' +import { prisma } from '../../lib/prisma.js' +import { registerAuthRoutes } from '../auth.js' + +const JWT_SECRET = 'test-secret' +const TEST_EMAIL = `test-reg-${Date.now()}@example.com` +const LOGIN_EMAIL = `test-login-${Date.now()}@example.com` + +async function buildApp() { + const app = Fastify({ logger: false }) + await app.register(jwt, { secret: JWT_SECRET }) + app.decorate('authenticate', async function (request, reply) { + try { + await request.jwtVerify() + } catch { + return reply.code(401).send({ error: 'Unauthorized' }) + } + }) + app.decorate('eventBus', { emit: () => {} }) + await registerAuthRoutes(app) + await app.ready() + return app +} + +describe('POST /api/auth/register', () => { + let app + beforeAll(async () => { app = await buildApp() }) + afterAll(async () => { await app.close() }) + afterEach(async () => { + await prisma.authCode.deleteMany({ where: { email: TEST_EMAIL } }) + await prisma.notificationPreference.deleteMany({ where: { user: { email: TEST_EMAIL } } }) + await prisma.user.deleteMany({ where: { email: TEST_EMAIL } }) + }) + + it('registers a new user with password', async () => { + const res = await app.inject({ + method: 'POST', + url: '/api/auth/register', + payload: { email: TEST_EMAIL, password: 'Test123!@' }, + }) + expect(res.statusCode).toBe(201) + const body = JSON.parse(res.body) + expect(body.token).toBeTruthy() + expect(body.user.email).toBe(TEST_EMAIL) + }) + + it('rejects duplicate email', async () => { + await app.inject({ + method: 'POST', url: '/api/auth/register', + payload: { email: TEST_EMAIL, password: 'Test123!@' }, + }) + const res = await app.inject({ + method: 'POST', url: '/api/auth/register', + payload: { email: TEST_EMAIL, password: 'Test123!@' }, + }) + expect(res.statusCode).toBe(409) + }) + + it('rejects weak password — too short', async () => { + const res = await app.inject({ + method: 'POST', url: '/api/auth/register', + payload: { email: TEST_EMAIL, password: 'Ab1!' }, + }) + expect(res.statusCode).toBe(400) + const body = JSON.parse(res.body) + expect(body.error).toContain('не менее 8') + }) + + it('rejects weak password — no digit', async () => { + const res = await app.inject({ + method: 'POST', url: '/api/auth/register', + payload: { email: TEST_EMAIL, password: 'Abcdefgh!' }, + }) + expect(res.statusCode).toBe(400) + expect(JSON.parse(res.body).error).toContain('цифру') + }) + + it('rejects weak password — no special char', async () => { + const res = await app.inject({ + method: 'POST', url: '/api/auth/register', + payload: { email: TEST_EMAIL, password: 'Abcdefg1' }, + }) + expect(res.statusCode).toBe(400) + expect(JSON.parse(res.body).error).toContain('спецсимвол') + }) +}) + +describe('POST /api/auth/login', () => { + let app + beforeAll(async () => { + app = await buildApp() + await app.inject({ + method: 'POST', url: '/api/auth/register', + payload: { email: LOGIN_EMAIL, password: 'Test123!@' }, + }) + }) + afterAll(async () => { + await prisma.authCode.deleteMany({ where: { email: LOGIN_EMAIL } }) + await prisma.notificationPreference.deleteMany({ where: { user: { email: LOGIN_EMAIL } } }) + await prisma.oAuthAccount.deleteMany({ where: { user: { email: LOGIN_EMAIL } } }) + await prisma.user.deleteMany({ where: { email: LOGIN_EMAIL } }) + await app.close() + }) + + it('logs in with correct password', async () => { + const res = await app.inject({ + method: 'POST', url: '/api/auth/login', + payload: { email: LOGIN_EMAIL, password: 'Test123!@' }, + headers: { 'x-forwarded-for': '1.1.1.1' }, + }) + expect(res.statusCode).toBe(200) + expect(JSON.parse(res.body).token).toBeTruthy() + }) + + it('rejects wrong password', async () => { + const res = await app.inject({ + method: 'POST', url: '/api/auth/login', + payload: { email: LOGIN_EMAIL, password: 'Wrong!!1!' }, + headers: { 'x-forwarded-for': '2.2.2.2' }, + }) + expect(res.statusCode).toBe(401) + }) + + it('rejects non-existent email', async () => { + const res = await app.inject({ + method: 'POST', url: '/api/auth/login', + payload: { email: 'nobody@nowhere.test', password: 'Test123!@' }, + headers: { 'x-forwarded-for': '3.3.3.3' }, + }) + expect(res.statusCode).toBe(401) + }) +})