Files
harheimertc/tests/config-profile-endpoints.spec.ts
Torsten Schulz (local) 19d7aeefb0
Some checks failed
Code Analysis and Production Deploy / analyze (push) Failing after 15s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Has been skipped
test: expand endpoint coverage and harden deploy gate
2026-05-21 08:03:59 +02:00

238 lines
9.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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'
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: 'falsch', newPassword: 'neuesPasswort123'
})
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: 'richtig', newPassword: 'neuesPasswort123'
})
const result = await profilePutHandler(event)
expect(result.success).toBe(true)
expect(authUtils.hashPassword).toHaveBeenCalledWith('neuesPasswort123')
})
})
})