Files
harheimertc/tests/config-profile-endpoints.spec.ts
Torsten Schulz (local) e033d716dd
All checks were successful
Code Analysis and Production Deploy / analyze (push) Successful in 5m23s
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Successful in 2m18s
feat: Add CMS and Member Area screens with ViewModels
- Implemented CmsViewModel to manage CMS data loading and state.
- Created MemberAreaDetailScreens for displaying member information and news.
- Added MembersViewModel and MemberNewsViewModel for managing member data and news.
- Developed MemberAreaScreen to provide navigation and display member-related options.
- Introduced ProfileScreen and ProfileViewModel for user profile management.
- Implemented state management for loading, error handling, and form updates across screens.
2026-05-28 08:01:35 +02:00

285 lines
12 KiB
TypeScript
Raw Permalink 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')
})
it('akzeptiert Bearer-Token für Android-Clients', async () => {
const event = createEvent({ headers: { authorization: 'Bearer android-token' } })
authUtils.verifyToken.mockReturnValue({ id: '1' })
authUtils.getUserFromToken.mockResolvedValue({
id: '1', name: 'Android User', email: 'android@test.de', roles: ['mitglied']
})
const result = await profileGetHandler(event)
expect(result.success).toBe(true)
expect(authUtils.verifyToken).toHaveBeenCalledWith('android-token')
expect(result.user.email).toBe('android@test.de')
})
})
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('aktualisiert Profil per Bearer-Token für Android-Clients', async () => {
const event = createEvent({ headers: { authorization: 'Bearer android-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: 'Android Neu', email: 'android@test.de', phone: '0987' })
const result = await profilePutHandler(event)
expect(result.success).toBe(true)
expect(authUtils.verifyToken).toHaveBeenCalledWith('android-token')
expect(result.user.name).toBe('Android Neu')
})
it('lehnt widerrufene Android-Sessions beim Profil-Update ab', async () => {
const event = createEvent({ headers: { authorization: 'Bearer android-token' } })
authUtils.verifyToken.mockReturnValue({ id: '1', sid: 'session-1' })
authUtils.getUserFromToken.mockResolvedValue(null)
mockSuccessReadBody({ name: 'Android Neu', email: 'android@test.de' })
await expect(profilePutHandler(event)).rejects.toMatchObject({ statusCode: 401 })
})
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')
})
})
})