test(server): add password auth and account methods tests
This commit is contained in:
@@ -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('последний метод')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user