Files
harheimertc/tests/config-profile-endpoints.spec.ts
Torsten Schulz (local) 58fd7fa5c6
Some checks failed
Code Analysis and Production Deploy / analyze (push) Failing after 5m7s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Has been skipped
feat(auth): implement Android refresh token handling and session management
- Added support for generating Android access tokens and managing refresh sessions in the auth endpoints.
- Implemented new tests for login, logout, and refresh functionalities specific to Android clients.
- Enhanced password reset logging with normalization and masking of email addresses.
- Created a new diagnostics endpoint for password reset attempts, including filtering and summarizing logs.
- Introduced a new utility for managing password reset logs with retention policies.
- Added tests for password reset log utilities to ensure proper functionality and privacy compliance.
- Updated WebAuthn configuration tests to validate origin handling for production and allowed origins.
2026-05-27 19:34:53 +02:00

245 lines
9.8 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(),
revokeRefreshSessionsForUser: 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()
expect(authUtils.revokeRefreshSessionsForUser).not.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)
expect(authUtils.revokeRefreshSessionsForUser).toHaveBeenCalledWith('1', 'password_changed')
})
})
})