Paßwort vergessen modernisiert
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { createEvent, mockSuccessReadBody } from './setup'
|
||||
import { readFileSync } from 'fs'
|
||||
import crypto from 'crypto'
|
||||
|
||||
vi.mock('../server/utils/auth.js', () => {
|
||||
return {
|
||||
@@ -74,6 +75,7 @@ import logoutHandler from '../server/api/auth/logout.post.js'
|
||||
import refreshHandler from '../server/api/auth/refresh.post.js'
|
||||
import registerHandler from '../server/api/auth/register.post.js'
|
||||
import resetPasswordHandler from '../server/api/auth/reset-password.post.js'
|
||||
import completePasswordResetHandler from '../server/api/auth/reset-password/complete.post.js'
|
||||
import statusHandler from '../server/api/auth/status.get.js'
|
||||
import versionHandler from '../server/api/app/version.get.js'
|
||||
|
||||
@@ -81,12 +83,15 @@ describe('Auth API Endpoints', () => {
|
||||
afterEach(() => {
|
||||
delete process.env.NODE_ENV
|
||||
delete process.env.APP_ENV
|
||||
delete process.env.NUXT_PUBLIC_BASE_URL
|
||||
delete process.env.PASSWORD_RESET_TTL_MIN
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
// Setze SMTP-Credentials für Tests
|
||||
process.env.SMTP_USER = 'test@example.com'
|
||||
process.env.SMTP_PASS = 'test-password'
|
||||
process.env.NUXT_PUBLIC_BASE_URL = 'https://harheimertc.de'
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
@@ -300,7 +305,7 @@ describe('Auth API Endpoints', () => {
|
||||
})
|
||||
|
||||
describe('POST /api/auth/reset-password', () => {
|
||||
it('prüft Pflichtfelder', async () => {
|
||||
it('prüft Pflichtfelder ohne öffentliche Fehlermeldung', async () => {
|
||||
const event = createEvent()
|
||||
mockSuccessReadBody({})
|
||||
|
||||
@@ -308,18 +313,27 @@ describe('Auth API Endpoints', () => {
|
||||
expect(response.success).toBe(true)
|
||||
})
|
||||
|
||||
it('aktualisiert Passwort bei vorhandenem Benutzer', async () => {
|
||||
it('speichert einen gehashten Reset-Token und lässt das alte Passwort unverändert', async () => {
|
||||
const event = createEvent()
|
||||
const user = { id: '1', email: 'user@example.com', name: 'User', password: 'hash' }
|
||||
const user = { id: '1', email: 'user@example.com', name: 'User', password: 'old-hash' }
|
||||
mockSuccessReadBody({ email: user.email })
|
||||
authUtils.readUsers.mockResolvedValue([user])
|
||||
authUtils.hashPassword.mockResolvedValue('new-hash')
|
||||
authUtils.writeUsers.mockResolvedValue(true)
|
||||
|
||||
const response = await resetPasswordHandler(event)
|
||||
|
||||
expect(response.success).toBe(true)
|
||||
expect(authUtils.writeUsers).toHaveBeenCalled()
|
||||
expect(authUtils.revokeRefreshSessionsForUser).toHaveBeenCalledWith('1', 'password_reset')
|
||||
expect(authUtils.hashPassword).not.toHaveBeenCalled()
|
||||
expect(authUtils.revokeRefreshSessionsForUser).not.toHaveBeenCalled()
|
||||
const writtenUser = authUtils.writeUsers.mock.calls[0][0][0]
|
||||
expect(writtenUser.password).toBe('old-hash')
|
||||
expect(writtenUser.passwordResetTokens).toHaveLength(1)
|
||||
expect(writtenUser.passwordResetTokens[0]).toMatchObject({ usedAt: null })
|
||||
expect(writtenUser.passwordResetTokens[0].tokenHash).toMatch(/^[a-f0-9]{64}$/)
|
||||
const transporter = nodemailer.default.createTransport.mock.results[0].value
|
||||
expect(transporter.sendMail).toHaveBeenCalledWith(expect.objectContaining({
|
||||
html: expect.stringContaining('https://harheimertc.de/passwort-zuruecksetzen?token=')
|
||||
}))
|
||||
expect(passwordResetLog.writePasswordResetLog).toHaveBeenCalledWith(expect.objectContaining({
|
||||
email: 'user@example.com',
|
||||
step: 'mail_send',
|
||||
@@ -332,7 +346,6 @@ describe('Auth API Endpoints', () => {
|
||||
const user = { id: '1', email: 'user@example.com', name: 'User', password: 'hash' }
|
||||
mockSuccessReadBody({ email: ' User@Example.com ' })
|
||||
authUtils.readUsers.mockResolvedValue([user])
|
||||
authUtils.hashPassword.mockResolvedValue('new-hash')
|
||||
authUtils.writeUsers.mockResolvedValue(true)
|
||||
|
||||
await resetPasswordHandler(event)
|
||||
@@ -341,11 +354,10 @@ describe('Auth API Endpoints', () => {
|
||||
expect(passwordResetLog.normalizeResetEmail).toHaveBeenCalledWith(' User@Example.com ')
|
||||
})
|
||||
|
||||
it('ändert das Passwort nicht, wenn SMTP nicht konfiguriert ist', async () => {
|
||||
it('ändert nichts, wenn SMTP nicht konfiguriert ist', async () => {
|
||||
const event = createEvent()
|
||||
mockSuccessReadBody({ email: 'user@example.com' })
|
||||
authUtils.readUsers.mockResolvedValue([{ id: '1', email: 'user@example.com', name: 'User', password: 'hash' }])
|
||||
authUtils.hashPassword.mockResolvedValue('new-hash')
|
||||
delete process.env.SMTP_USER
|
||||
delete process.env.SMTP_PASS
|
||||
|
||||
@@ -360,11 +372,12 @@ describe('Auth API Endpoints', () => {
|
||||
}))
|
||||
})
|
||||
|
||||
it('protokolliert einen Mailfehler ohne das Passwort zu aktivieren', async () => {
|
||||
it('protokolliert einen Mailfehler ohne das Passwort zu ersetzen', async () => {
|
||||
const event = createEvent()
|
||||
const user = { id: '1', email: 'user@example.com', name: 'User', password: 'hash' }
|
||||
mockSuccessReadBody({ email: 'user@example.com' })
|
||||
authUtils.readUsers.mockResolvedValue([{ id: '1', email: 'user@example.com', name: 'User', password: 'hash' }])
|
||||
authUtils.hashPassword.mockResolvedValue('new-hash')
|
||||
authUtils.readUsers.mockResolvedValue([user])
|
||||
authUtils.writeUsers.mockResolvedValue(true)
|
||||
nodemailer.default.createTransport.mockReturnValueOnce({
|
||||
sendMail: vi.fn().mockRejectedValue(Object.assign(new Error('SMTP fehlgeschlagen'), { code: 'EAUTH' }))
|
||||
})
|
||||
@@ -372,7 +385,8 @@ describe('Auth API Endpoints', () => {
|
||||
const response = await resetPasswordHandler(event)
|
||||
|
||||
expect(response.success).toBe(true)
|
||||
expect(authUtils.writeUsers).not.toHaveBeenCalled()
|
||||
expect(authUtils.hashPassword).not.toHaveBeenCalled()
|
||||
expect(authUtils.writeUsers.mock.calls[0][0][0].password).toBe('hash')
|
||||
expect(passwordResetLog.writePasswordResetLog).toHaveBeenCalledWith(expect.objectContaining({
|
||||
step: 'mail_send',
|
||||
status: 'failed'
|
||||
@@ -380,6 +394,61 @@ describe('Auth API Endpoints', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('POST /api/auth/reset-password/complete', () => {
|
||||
it('setzt ein neues Passwort mit gültigem Reset-Token', async () => {
|
||||
const token = 'reset-token'
|
||||
const tokenHash = crypto.createHash('sha256').update(token, 'utf8').digest('hex')
|
||||
const event = createEvent()
|
||||
const user = {
|
||||
id: '1',
|
||||
email: 'user@example.com',
|
||||
password: 'old-hash',
|
||||
passwordResetRequired: true,
|
||||
passwordResetTokens: [{ tokenHash, createdAt: new Date().toISOString(), expiresAt: new Date(Date.now() + 60000).toISOString(), usedAt: null }]
|
||||
}
|
||||
mockSuccessReadBody({ token, password: 'new-password' })
|
||||
authUtils.readUsers.mockResolvedValue([user])
|
||||
authUtils.hashPassword.mockResolvedValue('new-hash')
|
||||
authUtils.writeUsers.mockResolvedValue(true)
|
||||
|
||||
const response = await completePasswordResetHandler(event)
|
||||
|
||||
expect(response.success).toBe(true)
|
||||
expect(authUtils.hashPassword).toHaveBeenCalledWith('new-password')
|
||||
expect(authUtils.writeUsers.mock.calls[0][0][0]).toMatchObject({
|
||||
password: 'new-hash',
|
||||
passwordResetRequired: false
|
||||
})
|
||||
expect(authUtils.writeUsers.mock.calls[0][0][0].passwordResetTokens[0].usedAt).toEqual(expect.any(String))
|
||||
expect(authUtils.revokeRefreshSessionsForUser).toHaveBeenCalledWith('1', 'password_reset_completed')
|
||||
})
|
||||
|
||||
it('weist abgelaufene Reset-Tokens zurück', async () => {
|
||||
const token = 'reset-token'
|
||||
const tokenHash = crypto.createHash('sha256').update(token, 'utf8').digest('hex')
|
||||
const event = createEvent()
|
||||
mockSuccessReadBody({ token, password: 'new-password' })
|
||||
authUtils.readUsers.mockResolvedValue([{
|
||||
id: '1',
|
||||
email: 'user@example.com',
|
||||
password: 'old-hash',
|
||||
passwordResetTokens: [{ tokenHash, expiresAt: new Date(Date.now() - 60000).toISOString(), usedAt: null }]
|
||||
}])
|
||||
|
||||
await expect(completePasswordResetHandler(event)).rejects.toMatchObject({ statusCode: 400 })
|
||||
expect(authUtils.writeUsers).not.toHaveBeenCalled()
|
||||
expect(authUtils.hashPassword).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('verlangt mindestens acht Zeichen für das neue Passwort', async () => {
|
||||
const event = createEvent()
|
||||
mockSuccessReadBody({ token: 'reset-token', password: 'kurz' })
|
||||
|
||||
await expect(completePasswordResetHandler(event)).rejects.toMatchObject({ statusCode: 400 })
|
||||
expect(authUtils.readUsers).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /api/auth/status', () => {
|
||||
it('liefert loggedOut, wenn kein Cookie gesetzt ist', async () => {
|
||||
const event = createEvent()
|
||||
|
||||
Reference in New Issue
Block a user