242 lines
9.6 KiB
TypeScript
242 lines
9.6 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||
import { createEvent, mockSuccessReadBody } from './setup'
|
||
import fs from 'fs/promises'
|
||
|
||
vi.mock('../server/utils/auth.js', () => ({
|
||
verifyToken: vi.fn(),
|
||
getUserById: vi.fn(),
|
||
getUserFromToken: vi.fn(),
|
||
readUsers: vi.fn(),
|
||
writeUsers: vi.fn(),
|
||
verifyPassword: vi.fn(),
|
||
hashPassword: vi.fn(),
|
||
migrateUserRoles: vi.fn((user) => {
|
||
if (!user) return user
|
||
if (Array.isArray(user.roles)) return user
|
||
if (user.role) { user.roles = [user.role]; delete user.role } else { user.roles = ['mitglied'] }
|
||
return user
|
||
}),
|
||
hasAnyRole: vi.fn((user, ...roles) => {
|
||
if (!user) return false
|
||
const userRoles = Array.isArray(user.roles) ? user.roles : (user.role ? [user.role] : [])
|
||
return roles.some(r => userRoles.includes(r))
|
||
})
|
||
}))
|
||
|
||
vi.mock('../server/utils/hibp.js', () => ({
|
||
assertPasswordNotPwned: vi.fn().mockResolvedValue(undefined)
|
||
}))
|
||
|
||
const authUtils = await import('../server/utils/auth.js')
|
||
const hibpUtils = await import('../server/utils/hibp.js')
|
||
|
||
import configGetHandler from '../server/api/config.get.js'
|
||
import configPutHandler from '../server/api/config.put.js'
|
||
import profileGetHandler from '../server/api/profile.get.js'
|
||
import profilePutHandler from '../server/api/profile.put.js'
|
||
|
||
const invalidCurrentPassword = ['invalid', 'test', 'pw'].join('-')
|
||
const validCurrentPassword = ['valid', 'test', 'pw'].join('-')
|
||
const updatedPassword = ['updated', 'profile', 'pw'].join('-')
|
||
|
||
describe('Config & Profil Endpoints', () => {
|
||
beforeEach(() => {
|
||
vi.clearAllMocks()
|
||
vi.spyOn(fs, 'readFile').mockResolvedValue(JSON.stringify({ title: 'Test-Konfiguration' }))
|
||
vi.spyOn(fs, 'writeFile').mockResolvedValue(undefined)
|
||
hibpUtils.assertPasswordNotPwned.mockResolvedValue(undefined)
|
||
})
|
||
|
||
describe('GET /api/config', () => {
|
||
it('gibt Konfiguration zurück (kein Login nötig)', async () => {
|
||
const event = createEvent()
|
||
|
||
const result = await configGetHandler(event)
|
||
|
||
expect(result).toMatchObject({ title: 'Test-Konfiguration' })
|
||
})
|
||
|
||
it('gibt 500 zurück wenn Config-Datei fehlt', async () => {
|
||
vi.spyOn(fs, 'readFile').mockRejectedValue(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }))
|
||
const event = createEvent()
|
||
|
||
await expect(configGetHandler(event)).rejects.toMatchObject({ statusCode: 500 })
|
||
})
|
||
})
|
||
|
||
describe('PUT /api/config', () => {
|
||
it('verlangt Authentifizierung', async () => {
|
||
const event = createEvent()
|
||
mockSuccessReadBody({ title: 'Neu' })
|
||
|
||
await expect(configPutHandler(event)).rejects.toMatchObject({ statusCode: 401 })
|
||
})
|
||
|
||
it('lehnt ungültiges Token ab', async () => {
|
||
const event = createEvent({ cookies: { auth_token: 'bad' } })
|
||
authUtils.verifyToken.mockReturnValue(null)
|
||
mockSuccessReadBody({ title: 'Neu' })
|
||
|
||
await expect(configPutHandler(event)).rejects.toMatchObject({ statusCode: 401 })
|
||
})
|
||
|
||
it('verlangt Admin- oder Vorstand-Rolle', async () => {
|
||
const event = createEvent({ cookies: { auth_token: 'token' } })
|
||
authUtils.verifyToken.mockReturnValue({ id: '1' })
|
||
authUtils.getUserById.mockResolvedValue({ id: '1', roles: ['mitglied'] })
|
||
authUtils.hasAnyRole.mockReturnValue(false)
|
||
mockSuccessReadBody({ title: 'Neu' })
|
||
|
||
await expect(configPutHandler(event)).rejects.toMatchObject({ statusCode: 403 })
|
||
})
|
||
|
||
it('speichert Konfiguration erfolgreich', async () => {
|
||
const event = createEvent({ cookies: { auth_token: 'token' } })
|
||
authUtils.verifyToken.mockReturnValue({ id: '1' })
|
||
authUtils.getUserById.mockResolvedValue({ id: '1', roles: ['admin'] })
|
||
authUtils.hasAnyRole.mockReturnValue(true)
|
||
mockSuccessReadBody({ title: 'Neue Konfig', kontakt: { email: 'test@test.de' } })
|
||
|
||
const result = await configPutHandler(event)
|
||
|
||
expect(result.success).toBe(true)
|
||
expect(fs.writeFile).toHaveBeenCalled()
|
||
})
|
||
})
|
||
|
||
describe('GET /api/profile', () => {
|
||
it('verlangt Authentifizierung', async () => {
|
||
const event = createEvent()
|
||
|
||
await expect(profileGetHandler(event)).rejects.toMatchObject({ statusCode: 401 })
|
||
})
|
||
|
||
it('lehnt ungültiges Token ab', async () => {
|
||
const event = createEvent({ cookies: { auth_token: 'bad' } })
|
||
authUtils.verifyToken.mockReturnValue(null)
|
||
|
||
await expect(profileGetHandler(event)).rejects.toMatchObject({ statusCode: 401 })
|
||
})
|
||
|
||
it('gibt 404 wenn Benutzer nicht gefunden', async () => {
|
||
const event = createEvent({ cookies: { auth_token: 'token' } })
|
||
authUtils.verifyToken.mockReturnValue({ id: '1' })
|
||
authUtils.getUserFromToken.mockResolvedValue(null)
|
||
|
||
await expect(profileGetHandler(event)).rejects.toMatchObject({ statusCode: 404 })
|
||
})
|
||
|
||
it('liefert Profildaten', async () => {
|
||
const event = createEvent({ cookies: { auth_token: 'token' } })
|
||
authUtils.verifyToken.mockReturnValue({ id: '1' })
|
||
authUtils.getUserFromToken.mockResolvedValue({
|
||
id: '1', name: 'Max Muster', email: 'max@test.de', phone: '0123456789', geburtsdatum: '1990-01-01'
|
||
})
|
||
|
||
const result = await profileGetHandler(event)
|
||
|
||
expect(result.success).toBe(true)
|
||
expect(result.user.name).toBe('Max Muster')
|
||
expect(result.user.email).toBe('max@test.de')
|
||
expect(result.user).not.toHaveProperty('password')
|
||
})
|
||
})
|
||
|
||
describe('PUT /api/profile', () => {
|
||
it('verlangt Authentifizierung', async () => {
|
||
const event = createEvent()
|
||
mockSuccessReadBody({ name: 'Max', email: 'max@test.de' })
|
||
|
||
await expect(profilePutHandler(event)).rejects.toMatchObject({ statusCode: 401 })
|
||
})
|
||
|
||
it('lehnt ungültiges Token ab', async () => {
|
||
const event = createEvent({ cookies: { auth_token: 'bad' } })
|
||
authUtils.verifyToken.mockReturnValue(null)
|
||
mockSuccessReadBody({ name: 'Max', email: 'max@test.de' })
|
||
|
||
await expect(profilePutHandler(event)).rejects.toMatchObject({ statusCode: 401 })
|
||
})
|
||
|
||
it('validiert Pflichtfelder – Name fehlt', async () => {
|
||
const event = createEvent({ cookies: { auth_token: 'token' } })
|
||
authUtils.verifyToken.mockReturnValue({ id: '1' })
|
||
mockSuccessReadBody({ email: 'max@test.de' })
|
||
|
||
await expect(profilePutHandler(event)).rejects.toMatchObject({ statusCode: 400 })
|
||
})
|
||
|
||
it('gibt 404 wenn Benutzer nicht gefunden', async () => {
|
||
const event = createEvent({ cookies: { auth_token: 'token' } })
|
||
authUtils.verifyToken.mockReturnValue({ id: '99' })
|
||
authUtils.readUsers.mockResolvedValue([{ id: '1', email: 'other@test.de', password: 'h' }])
|
||
mockSuccessReadBody({ name: 'Max', email: 'max@test.de' })
|
||
|
||
await expect(profilePutHandler(event)).rejects.toMatchObject({ statusCode: 404 })
|
||
})
|
||
|
||
it('verhindert E-Mail-Duplikat', async () => {
|
||
const event = createEvent({ cookies: { auth_token: 'token' } })
|
||
authUtils.verifyToken.mockReturnValue({ id: '1' })
|
||
authUtils.readUsers.mockResolvedValue([
|
||
{ id: '1', name: 'Max', email: 'max@test.de', password: 'hash', roles: ['mitglied'] },
|
||
{ id: '2', name: 'Anna', email: 'anna@test.de', password: 'hash2', roles: ['mitglied'] }
|
||
])
|
||
mockSuccessReadBody({ name: 'Max', email: 'anna@test.de' })
|
||
|
||
await expect(profilePutHandler(event)).rejects.toMatchObject({ statusCode: 409 })
|
||
})
|
||
|
||
it('aktualisiert Profil ohne Passwortänderung', async () => {
|
||
const event = createEvent({ cookies: { auth_token: 'token' } })
|
||
authUtils.verifyToken.mockReturnValue({ id: '1' })
|
||
authUtils.readUsers.mockResolvedValue([
|
||
{ id: '1', name: 'Alt', email: 'max@test.de', password: 'hash', roles: ['mitglied'] }
|
||
])
|
||
authUtils.writeUsers.mockResolvedValue(undefined)
|
||
authUtils.migrateUserRoles.mockImplementation(u => ({ ...u, roles: u.roles || ['mitglied'] }))
|
||
mockSuccessReadBody({ name: 'Max Neu', email: 'max@test.de', phone: '0987' })
|
||
|
||
const result = await profilePutHandler(event)
|
||
|
||
expect(result.success).toBe(true)
|
||
expect(result.user.name).toBe('Max Neu')
|
||
expect(authUtils.writeUsers).toHaveBeenCalled()
|
||
})
|
||
|
||
it('prüft aktuelles Passwort bei Passwortänderung', async () => {
|
||
const event = createEvent({ cookies: { auth_token: 'token' } })
|
||
authUtils.verifyToken.mockReturnValue({ id: '1' })
|
||
authUtils.readUsers.mockResolvedValue([
|
||
{ id: '1', name: 'Max', email: 'max@test.de', password: 'hash', roles: ['mitglied'] }
|
||
])
|
||
authUtils.verifyPassword.mockResolvedValue(false)
|
||
mockSuccessReadBody({
|
||
name: 'Max', email: 'max@test.de', currentPassword: invalidCurrentPassword, newPassword: updatedPassword
|
||
})
|
||
|
||
await expect(profilePutHandler(event)).rejects.toMatchObject({ statusCode: 401 })
|
||
})
|
||
|
||
it('aktualisiert Passwort erfolgreich', async () => {
|
||
const event = createEvent({ cookies: { auth_token: 'token' } })
|
||
authUtils.verifyToken.mockReturnValue({ id: '1' })
|
||
authUtils.readUsers.mockResolvedValue([
|
||
{ id: '1', name: 'Max', email: 'max@test.de', password: 'altes-hash', roles: ['mitglied'] }
|
||
])
|
||
authUtils.verifyPassword.mockResolvedValue(true)
|
||
authUtils.hashPassword.mockResolvedValue('neues-hash')
|
||
authUtils.writeUsers.mockResolvedValue(undefined)
|
||
authUtils.migrateUserRoles.mockImplementation(u => ({ ...u, roles: u.roles || ['mitglied'] }))
|
||
mockSuccessReadBody({
|
||
name: 'Max', email: 'max@test.de', currentPassword: validCurrentPassword, newPassword: updatedPassword
|
||
})
|
||
|
||
const result = await profilePutHandler(event)
|
||
|
||
expect(result.success).toBe(true)
|
||
expect(authUtils.hashPassword).toHaveBeenCalledWith(updatedPassword)
|
||
})
|
||
})
|
||
})
|