Merge pull request 'dev' (#32) from dev into main
All checks were successful
Code Analysis and Production Deploy / analyze (push) Successful in 2m53s
Code Analysis and Production Deploy / deploy-production (push) Successful in 2m7s
Code Analysis and Production Deploy / deploy-test (push) Has been skipped

Reviewed-on: #32
This commit is contained in:
2026-05-22 14:05:21 +02:00
12 changed files with 1052 additions and 32 deletions

View File

@@ -100,7 +100,7 @@ jobs:
deploy-production: deploy-production:
needs: analyze needs: analyze
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main' if: success() && github.event_name == 'push' && github.ref == 'refs/heads/main'
steps: steps:
- name: Prepare SSH - name: Prepare SSH
run: | run: |
@@ -131,7 +131,7 @@ jobs:
deploy-test: deploy-test:
needs: analyze needs: analyze
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/dev' if: success() && github.event_name == 'push' && github.ref == 'refs/heads/dev'
steps: steps:
- name: Prepare SSH - name: Prepare SSH
run: | run: |

View File

@@ -1,6 +1,6 @@
{ {
"name": "harheimertc-website", "name": "harheimertc-website",
"version": "1.5.1", "version": "1.5.2",
"description": "Moderne Webseite für den Harheimer Tischtennis Club", "description": "Moderne Webseite für den Harheimer Tischtennis Club",
"private": true, "private": true,
"type": "module", "type": "module",

View File

@@ -1,6 +1,6 @@
import fs from 'fs/promises' import fs from 'fs/promises'
import path from 'path' import path from 'path'
import { getUserFromToken, verifyToken } from '../../utils/auth.js' import { getUserFromToken, hasAnyRole, verifyToken } from '../../utils/auth.js'
// Handle both dev and production paths // Handle both dev and production paths
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal

View File

@@ -1,24 +1,46 @@
import { promises as fs } from 'fs' import { promises as fs } from 'fs'
import path from 'path' import path from 'path'
async function exists(filePath) {
try {
await fs.access(filePath)
return true
} catch {
return false
}
}
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
try { try {
const cwd = process.cwd() const cwd = process.cwd()
// Prefer internal server/data, fallback to public/data // Prefer CMS write target first (server/data/public-data), then legacy locations.
let csvPath const candidates = [
if (cwd.endsWith('.output')) { path.join(cwd, 'server/data/public-data/vereinsmeisterschaften.csv'),
csvPath = path.join(cwd, '../server/data/vereinsmeisterschaften.csv') path.join(cwd, '../server/data/public-data/vereinsmeisterschaften.csv'),
if (!(await fs.access(csvPath).then(()=>true).catch(()=>false))) { path.join(cwd, '.output/server/data/vereinsmeisterschaften.csv'),
csvPath = path.join(cwd, '../public/data/vereinsmeisterschaften.csv') path.join(cwd, 'server/data/vereinsmeisterschaften.csv'),
} path.join(cwd, '.output/public/data/vereinsmeisterschaften.csv'),
} else { path.join(cwd, 'public/data/vereinsmeisterschaften.csv'),
csvPath = path.join(cwd, 'server/data/vereinsmeisterschaften.csv') path.join(cwd, '../.output/public/data/vereinsmeisterschaften.csv'),
if (!(await fs.access(csvPath).then(()=>true).catch(()=>false))) { path.join(cwd, '../public/data/vereinsmeisterschaften.csv')
csvPath = path.join(cwd, 'public/data/vereinsmeisterschaften.csv') ]
let csvPath = null
for (const candidate of candidates) {
if (await exists(candidate)) {
csvPath = candidate
break
} }
} }
if (!csvPath) {
throw createError({
statusCode: 404,
statusMessage: 'Vereinsmeisterschaften-Datei nicht gefunden'
})
}
// CSV-Datei direkt als Text zurückgeben (keine Caching-Probleme) // CSV-Datei direkt als Text zurückgeben (keine Caching-Probleme)
const csv = await fs.readFile(csvPath, 'utf-8') const csv = await fs.readFile(csvPath, 'utf-8')
@@ -30,12 +52,7 @@ export default defineEventHandler(async (event) => {
return csv return csv
} catch (error) { } catch (error) {
if (error.code === 'ENOENT') { if (error?.statusCode) throw error
throw createError({
statusCode: 404,
statusMessage: 'Vereinsmeisterschaften-Datei nicht gefunden'
})
}
console.error('Fehler beim Laden der Vereinsmeisterschaften:', error) console.error('Fehler beim Laden der Vereinsmeisterschaften:', error)
throw createError({ throw createError({
statusCode: 500, statusCode: 500,

View File

@@ -2,15 +2,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createEvent, mockSuccessReadBody } from './setup' import { createEvent, mockSuccessReadBody } from './setup'
import fs from 'fs/promises' import fs from 'fs/promises'
vi.mock('../server/utils/auth.js', () => ({
getUserFromToken: vi.fn(),
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('multer', () => { vi.mock('multer', () => {
const single = vi.fn((field) => (req, _res, cb) => { const single = vi.fn((field) => (req, _res, cb) => {
if (req.__mockMulterError) { if (req.__mockMulterError) {
@@ -31,6 +22,18 @@ vi.mock('multer', () => {
diskStorage diskStorage
} }
}) })
vi.mock('../server/utils/contact-requests.js', () => ({
readContactRequests: vi.fn()
}))
vi.mock('../server/utils/auth.js', () => ({
getUserFromToken: vi.fn(),
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('child_process', () => ({ vi.mock('child_process', () => ({
exec: vi.fn() exec: vi.fn()
@@ -74,8 +77,11 @@ vi.mock('../server/utils/upload-validation.js', () => ({
import saveCsvHandler from '../server/api/cms/save-csv.post.js' import saveCsvHandler from '../server/api/cms/save-csv.post.js'
import uploadSpielplanHandler from '../server/api/cms/upload-spielplan-pdf.post.js' import uploadSpielplanHandler from '../server/api/cms/upload-spielplan-pdf.post.js'
import satzungUploadHandler from '../server/api/cms/satzung-upload.post.js' import satzungUploadHandler from '../server/api/cms/satzung-upload.post.js'
import contactRequestsHandler from '../server/api/cms/contact-requests.get.js'
const { getUserFromToken } = await import('../server/utils/auth.js') const contactRequestUtils = await import('../server/utils/contact-requests.js')
const { getUserFromToken, hasAnyRole } = await import('../server/utils/auth.js')
describe('CMS File Endpoints', () => { describe('CMS File Endpoints', () => {
beforeEach(() => { beforeEach(() => {
@@ -155,4 +161,46 @@ describe('CMS File Endpoints', () => {
expect(fs.writeFile).toHaveBeenCalled() expect(fs.writeFile).toHaveBeenCalled()
}) })
}) })
describe('GET /api/cms/contact-requests', () => {
it('gibt 403 wenn kein Token vorhanden', async () => {
const event = createEvent()
await expect(contactRequestsHandler(event)).rejects.toMatchObject({ statusCode: 403 })
})
it('gibt 403 bei unzureichender Rolle', async () => {
const event = createEvent({ cookies: { auth_token: 'token' } })
getUserFromToken.mockResolvedValue({ id: '1', roles: ['mitglied'] })
hasAnyRole.mockReturnValue(false)
await expect(contactRequestsHandler(event)).rejects.toMatchObject({ statusCode: 403 })
})
it('liefert Kontaktanfragen für Admin', async () => {
const event = createEvent({ cookies: { auth_token: 'token' } })
getUserFromToken.mockResolvedValue({ id: '1', roles: ['admin'] })
hasAnyRole.mockReturnValue(true)
contactRequestUtils.readContactRequests.mockResolvedValue([
{ name: 'Test User', email: 'test@example.com', message: 'Hallo' }
])
const result = await contactRequestsHandler(event)
expect(Array.isArray(result)).toBe(true)
expect(result).toHaveLength(1)
expect(result[0].name).toBe('Test User')
})
it('liefert Kontaktanfragen für Trainer', async () => {
const event = createEvent({ cookies: { auth_token: 'token' } })
getUserFromToken.mockResolvedValue({ id: '2', roles: ['trainer'] })
hasAnyRole.mockReturnValue(true)
contactRequestUtils.readContactRequests.mockResolvedValue([])
const result = await contactRequestsHandler(event)
expect(Array.isArray(result)).toBe(true)
})
})
}) })

View File

@@ -0,0 +1,241 @@
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)
})
})
})

View File

@@ -51,6 +51,7 @@ const authUtils = await import('../server/utils/auth.js')
import uploadHandler from '../server/api/galerie/upload.post.js' import uploadHandler from '../server/api/galerie/upload.post.js'
import listHandler from '../server/api/galerie/list.get.js' import listHandler from '../server/api/galerie/list.get.js'
import imageHandler from '../server/api/galerie/[id].get.js' import imageHandler from '../server/api/galerie/[id].get.js'
import deleteImageHandler from '../server/api/galerie/[id].delete.js'
describe('Galerie API Endpoints', () => { describe('Galerie API Endpoints', () => {
beforeEach(() => { beforeEach(() => {
@@ -185,5 +186,71 @@ describe('Galerie API Endpoints', () => {
expect(response).toBeInstanceOf(Buffer) expect(response).toBeInstanceOf(Buffer)
}) })
}) })
describe('DELETE /api/galerie/[id]', () => {
it('verlangt Authentifizierung', async () => {
const event = createEvent({ method: 'DELETE' })
event.context.params = { id: '1' }
await expect(deleteImageHandler(event)).rejects.toMatchObject({ statusCode: 401 })
})
it('verlangt Admin- oder Vorstand-Rolle', async () => {
const event = createEvent({ method: 'DELETE', cookies: { auth_token: 'token' } })
event.context.params = { id: '1' }
authUtils.verifyToken.mockReturnValue({ id: '1' })
authUtils.getUserFromToken.mockResolvedValue({ id: '1', roles: ['mitglied'] })
authUtils.hasAnyRole.mockReturnValue(false)
await expect(deleteImageHandler(event)).rejects.toMatchObject({ statusCode: 403 })
})
it('gibt 400 wenn keine Bild-ID übergeben', async () => {
const event = createEvent({ method: 'DELETE', cookies: { auth_token: 'token' } })
// kein context.params.id
authUtils.verifyToken.mockReturnValue({ id: '1' })
authUtils.getUserFromToken.mockResolvedValue({ id: '1', roles: ['admin'] })
authUtils.hasAnyRole.mockReturnValue(true)
await expect(deleteImageHandler(event)).rejects.toMatchObject({ statusCode: 400 })
})
it('gibt 404 wenn Bild nicht in Metadaten gefunden', async () => {
const event = createEvent({ method: 'DELETE', cookies: { auth_token: 'token' } })
event.context.params = { id: 'unbekannt' }
authUtils.verifyToken.mockReturnValue({ id: '1' })
authUtils.getUserFromToken.mockResolvedValue({ id: '1', roles: ['admin'] })
authUtils.hasAnyRole.mockReturnValue(true)
vi.spyOn(fs, 'readFile').mockResolvedValue(JSON.stringify([
{ id: 'anderes-bild', filename: 'other.jpg', previewFilename: 'preview_other.jpg' }
]))
await expect(deleteImageHandler(event)).rejects.toMatchObject({ statusCode: 404 })
})
it('löscht Bild und aktualisiert Metadaten', async () => {
const event = createEvent({ method: 'DELETE', cookies: { auth_token: 'token' } })
event.context.params = { id: 'img-1' }
authUtils.verifyToken.mockReturnValue({ id: '1' })
authUtils.getUserFromToken.mockResolvedValue({ id: '1', roles: ['admin'] })
authUtils.hasAnyRole.mockReturnValue(true)
vi.spyOn(fs, 'readFile').mockResolvedValue(JSON.stringify([
{ id: 'img-1', filename: 'bild.jpg', previewFilename: 'preview_bild.jpg' },
{ id: 'img-2', filename: 'bild2.jpg', previewFilename: 'preview_bild2.jpg' }
]))
const result = await deleteImageHandler(event)
expect(result.success).toBe(true)
expect(fs.writeFile).toHaveBeenCalledWith(
expect.any(String),
expect.stringContaining('img-2'),
expect.any(String)
)
// img-1 darf nicht mehr in den Metadaten sein
const writtenData = JSON.parse((fs.writeFile as any).mock.calls[0][1])
expect(writtenData.find((img: any) => img.id === 'img-1')).toBeUndefined()
})
})
}) })

View File

@@ -49,6 +49,8 @@ import membersGetHandler from '../server/api/members.get.js'
import membersPostHandler from '../server/api/members.post.js' import membersPostHandler from '../server/api/members.post.js'
import membersDeleteHandler from '../server/api/members.delete.js' import membersDeleteHandler from '../server/api/members.delete.js'
import membersBulkHandler from '../server/api/members/bulk.post.js' import membersBulkHandler from '../server/api/members/bulk.post.js'
import membersBulkHandler from '../server/api/members/bulk.post.js'
import toggleMannschaftsspielerHandler from '../server/api/members/toggle-mannschaftsspieler.post.js'
describe('Members API Endpoints', () => { describe('Members API Endpoints', () => {
beforeEach(() => { beforeEach(() => {
@@ -226,4 +228,83 @@ describe('Members API Endpoints', () => {
expect(memberUtils.writeMembers).toHaveBeenCalled() expect(memberUtils.writeMembers).toHaveBeenCalled()
}) })
}) })
describe('POST /api/members/toggle-mannschaftsspieler', () => {
it('verlangt Authentifizierung', async () => {
const event = createEvent()
mockSuccessReadBody({ memberId: '1' })
await expect(toggleMannschaftsspielerHandler(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({ memberId: '2' })
await expect(toggleMannschaftsspielerHandler(event)).rejects.toMatchObject({ statusCode: 403 })
})
it('gibt 400 wenn memberId fehlt', 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({})
await expect(toggleMannschaftsspielerHandler(event)).rejects.toMatchObject({ statusCode: 400 })
})
it('gibt 404 wenn Mitglied nicht gefunden', async () => {
const event = createEvent({ cookies: { auth_token: 'token' } })
authUtils.verifyToken.mockReturnValue({ id: '1' })
authUtils.getUserById.mockResolvedValue({ id: '1', roles: ['admin'] })
authUtils.hasAnyRole.mockReturnValue(true)
authUtils.readUsers.mockResolvedValue([])
memberUtils.readMembers.mockResolvedValue([])
mockSuccessReadBody({ memberId: 'unbekannt' })
await expect(toggleMannschaftsspielerHandler(event)).rejects.toMatchObject({ statusCode: 404 })
})
it('toggled isMannschaftsspieler für eingeloggten User', async () => {
const event = createEvent({ cookies: { auth_token: 'token' } })
authUtils.verifyToken.mockReturnValue({ id: '1' })
authUtils.getUserById.mockResolvedValue({ id: '1', roles: ['admin'] })
authUtils.hasAnyRole.mockReturnValue(true)
authUtils.readUsers.mockResolvedValue([
{ id: 'user-5', name: 'Max Muster', isMannschaftsspieler: false, roles: ['mitglied'] }
])
authUtils.writeUsers.mockResolvedValue(undefined)
memberUtils.readMembers.mockResolvedValue([])
mockSuccessReadBody({ memberId: 'user-5' })
const result = await toggleMannschaftsspielerHandler(event)
expect(result.success).toBe(true)
expect(result.isMannschaftsspieler).toBe(true)
expect(authUtils.writeUsers).toHaveBeenCalled()
})
it('toggled isMannschaftsspieler für manuelles Mitglied', async () => {
const event = createEvent({ cookies: { auth_token: 'token' } })
authUtils.verifyToken.mockReturnValue({ id: '1' })
authUtils.getUserById.mockResolvedValue({ id: '1', roles: ['admin'] })
authUtils.hasAnyRole.mockReturnValue(true)
authUtils.readUsers.mockResolvedValue([])
memberUtils.readMembers.mockResolvedValue([
{ id: 'manual-1', firstName: 'Paul', lastName: 'Team', isMannschaftsspieler: true }
])
memberUtils.writeMembers.mockResolvedValue(undefined)
mockSuccessReadBody({ memberId: 'manual-1' })
const result = await toggleMannschaftsspielerHandler(event)
expect(result.success).toBe(true)
expect(result.isMannschaftsspieler).toBe(false)
expect(memberUtils.writeMembers).toHaveBeenCalled()
})
})
}) })

View File

@@ -0,0 +1,160 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createEvent, mockSuccessReadBody } from './setup'
vi.mock('../server/utils/auth.js', () => ({
verifyToken: vi.fn(),
getUserById: vi.fn(),
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/news.js', () => ({
readNews: vi.fn(),
saveNews: vi.fn(),
deleteNews: vi.fn()
}))
const authUtils = await import('../server/utils/auth.js')
const newsUtils = await import('../server/utils/news.js')
import newsGetHandler from '../server/api/news.get.js'
import newsPostHandler from '../server/api/news.post.js'
import newsDeleteHandler from '../server/api/news.delete.js'
describe('News API Endpoints', () => {
beforeEach(() => {
vi.clearAllMocks()
})
const adminEvent = () => {
const event = createEvent({ cookies: { auth_token: 'token' } })
authUtils.verifyToken.mockReturnValue({ id: '1' })
authUtils.getUserById.mockResolvedValue({ id: '1', roles: ['admin'], name: 'Admin' })
authUtils.hasAnyRole.mockReturnValue(true)
return event
}
describe('GET /api/news', () => {
it('verlangt Authentifizierung', async () => {
const event = createEvent()
await expect(newsGetHandler(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(newsGetHandler(event)).rejects.toMatchObject({ statusCode: 401 })
})
it('liefert News nach Datum sortiert (neueste zuerst)', async () => {
const event = createEvent({ cookies: { auth_token: 'token' } })
authUtils.verifyToken.mockReturnValue({ id: '1' })
newsUtils.readNews.mockResolvedValue([
{ id: 'a', title: 'Alt', created: '2024-01-01' },
{ id: 'b', title: 'Neu', created: '2025-06-01' }
])
const result = await newsGetHandler(event)
expect(result.success).toBe(true)
expect(result.news[0].id).toBe('b')
expect(result.news[1].id).toBe('a')
})
})
describe('POST /api/news', () => {
it('verlangt Authentifizierung', async () => {
const event = createEvent()
mockSuccessReadBody({})
await expect(newsPostHandler(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: 'T', content: 'C' })
await expect(newsPostHandler(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: 'T', content: 'C' })
await expect(newsPostHandler(event)).rejects.toMatchObject({ statusCode: 403 })
})
it('validiert Pflichtfelder kein Titel', async () => {
const event = adminEvent()
mockSuccessReadBody({ content: 'Nur Inhalt ohne Titel' })
await expect(newsPostHandler(event)).rejects.toMatchObject({ statusCode: 400 })
})
it('validiert Pflichtfelder kein Inhalt', async () => {
const event = adminEvent()
mockSuccessReadBody({ title: 'Nur Titel' })
await expect(newsPostHandler(event)).rejects.toMatchObject({ statusCode: 400 })
})
it('speichert News erfolgreich', async () => {
const event = adminEvent()
newsUtils.saveNews.mockResolvedValue(undefined)
mockSuccessReadBody({ title: 'Neue Info', content: 'Inhalt hier', isPublic: true })
const result = await newsPostHandler(event)
expect(result.success).toBe(true)
expect(newsUtils.saveNews).toHaveBeenCalledWith(
expect.objectContaining({ title: 'Neue Info', content: 'Inhalt hier', isPublic: true })
)
})
it('setzt autor auf den angemeldeten Benutzer', async () => {
const event = adminEvent()
newsUtils.saveNews.mockResolvedValue(undefined)
mockSuccessReadBody({ title: 'Titel', content: 'Inhalt' })
await newsPostHandler(event)
expect(newsUtils.saveNews).toHaveBeenCalledWith(
expect.objectContaining({ author: 'Admin' })
)
})
})
describe('DELETE /api/news', () => {
it('verlangt Authentifizierung', async () => {
const event = createEvent()
await expect(newsDeleteHandler(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)
await expect(newsDeleteHandler(event)).rejects.toMatchObject({ statusCode: 403 })
})
it('validiert fehlende News-ID', async () => {
const event = adminEvent()
// query ist leer → kein id
await expect(newsDeleteHandler(event)).rejects.toMatchObject({ statusCode: 400 })
})
it('löscht News erfolgreich', async () => {
const event = adminEvent()
event.__query = { id: 'abc-123' }
newsUtils.deleteNews.mockResolvedValue(undefined)
const result = await newsDeleteHandler(event)
expect(result.success).toBe(true)
expect(newsUtils.deleteNews).toHaveBeenCalledWith('abc-123')
})
})
})

View File

@@ -1,4 +1,4 @@
import { vi } from 'vitest' import { vi, beforeAll, afterAll, beforeEach } from 'vitest'
type CookieStore = Record<string, { value: string; options?: Record<string, any> }> type CookieStore = Record<string, { value: string; options?: Record<string, any> }>
@@ -129,6 +129,15 @@ export const mockSuccessReadBody = (payload: any) => {
(global.readBody as any).mockResolvedValue(payload) (global.readBody as any).mockResolvedValue(payload)
} }
beforeAll(() => {
vi.spyOn(console, 'error').mockImplementation(() => {})
vi.spyOn(console, 'warn').mockImplementation(() => {})
})
afterAll(() => {
vi.restoreAllMocks()
})
beforeEach(() => { beforeEach(() => {
resetEventMocks() resetEventMocks()
}) })

View File

@@ -0,0 +1,248 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createEvent } from './setup'
import fs from 'fs/promises'
vi.mock('../server/utils/spielplan-data.js', () => ({
listSpielplanSeasons: vi.fn().mockResolvedValue(['25--26']),
readSpielplanData: vi.fn(),
validateSeasonSlug: vi.fn().mockReturnValue(true),
getCurrentSeasonSlug: vi.fn().mockReturnValue('25--26')
}))
vi.mock('../server/utils/logger.js', () => ({
error: vi.fn(),
info: vi.fn(),
warn: vi.fn()
}))
vi.mock('../server/utils/auth.js', () => ({
verifyToken: vi.fn(),
getUserFromToken: vi.fn(),
readUsers: vi.fn().mockResolvedValue([]),
migrateUserRoles: vi.fn(user => user)
}))
vi.mock('../server/utils/members.js', () => ({
readMembers: vi.fn().mockResolvedValue([]),
normalizeDate: vi.fn(v => v)
}))
const spielplanData = await import('../server/utils/spielplan-data.js')
const authUtils = await import('../server/utils/auth.js')
const memberUtils = await import('../server/utils/members.js')
import spielplanHandler from '../server/api/spielplan.get.js'
import mannschaftenHandler from '../server/api/mannschaften.get.js'
import vereinsmeisterschaftenHandler from '../server/api/vereinsmeisterschaften.get.js'
import birthdaysHandler from '../server/api/birthdays.get.js'
describe('Spielplan, Mannschaften & öffentliche Endpoints', () => {
beforeEach(() => {
vi.clearAllMocks()
spielplanData.validateSeasonSlug.mockReturnValue(true)
spielplanData.getCurrentSeasonSlug.mockReturnValue('25--26')
spielplanData.listSpielplanSeasons.mockResolvedValue(['25--26'])
authUtils.readUsers.mockResolvedValue([])
memberUtils.readMembers.mockResolvedValue([])
vi.spyOn(fs, 'readFile').mockResolvedValue('csv,data\nrow1,row2')
vi.spyOn(fs, 'access').mockResolvedValue(undefined)
})
describe('GET /api/spielplan', () => {
it('gibt Fehler bei ungültigem Saison-Slug zurück', async () => {
const event = createEvent({ query: { season: 'ungueltig' } })
spielplanData.validateSeasonSlug.mockReturnValue(false)
const result = await spielplanHandler(event)
expect(result.success).toBe(false)
})
it('gibt Fehler zurück wenn Spielplan leer ist', async () => {
const event = createEvent()
spielplanData.readSpielplanData.mockResolvedValue({ data: [], headers: [] })
const result = await spielplanHandler(event)
expect(result.success).toBe(false)
expect(result.data).toHaveLength(0)
})
it('liefert Spielplandaten mit Saisons', async () => {
const event = createEvent()
spielplanData.readSpielplanData.mockResolvedValue({
data: [['Harheimer TC 1', 'Gegner', '3:1']],
headers: ['Heimteam', 'Gastteam', 'Ergebnis'],
source: 'file',
filePath: '/some/path',
season: '25--26'
})
const result = await spielplanHandler(event)
expect(result.success).toBe(true)
expect(result.data).toHaveLength(1)
expect(result.seasons).toContain('25--26')
})
it('liefert Spielplandaten für angeforderte Saison', async () => {
const event = createEvent({ query: { season: '24--25' } })
spielplanData.readSpielplanData.mockResolvedValue({
data: [['Team A', 'Team B', '2:3']],
headers: ['Heim', 'Gast', 'Ergebnis'],
source: 'file',
filePath: '/path',
season: '24--25'
})
const result = await spielplanHandler(event)
expect(result.success).toBe(true)
expect(result.season).toBe('24--25')
})
})
describe('GET /api/mannschaften', () => {
it('lehnt ungültigen Saison-Slug ab', async () => {
const event = createEvent({ query: { season: 'invalid' } })
spielplanData.validateSeasonSlug.mockReturnValue(false)
await expect(mannschaftenHandler(event)).rejects.toMatchObject({ statusCode: 400 })
})
it('gibt 404 wenn keine Mannschaftsdatei gefunden', async () => {
const event = createEvent()
vi.spyOn(fs, 'access').mockRejectedValue(
Object.assign(new Error('ENOENT'), { code: 'ENOENT' })
)
await expect(mannschaftenHandler(event)).rejects.toMatchObject({ statusCode: 404 })
})
it('liefert CSV-Inhalt der Mannschaftsdaten', async () => {
const event = createEvent()
vi.spyOn(fs, 'access').mockResolvedValue(undefined)
vi.spyOn(fs, 'readFile').mockResolvedValue('Name,Liga\nHarheimer TC 1,Kreisliga')
const result = await mannschaftenHandler(event)
expect(result).toContain('Harheimer TC 1')
expect(result).toContain('Kreisliga')
})
it('liefert CSV für angeforderte Saison', async () => {
const event = createEvent({ query: { season: '24--25' } })
vi.spyOn(fs, 'access').mockResolvedValue(undefined)
vi.spyOn(fs, 'readFile').mockResolvedValue('Name,Liga\nHarheimer TC 2,Bezirksliga')
const result = await mannschaftenHandler(event)
expect(result).toContain('Harheimer TC 2')
})
})
describe('GET /api/vereinsmeisterschaften', () => {
it('gibt 404 wenn Datei nicht gefunden', async () => {
vi.spyOn(fs, 'access').mockRejectedValue(
Object.assign(new Error('ENOENT'), { code: 'ENOENT' })
)
vi.spyOn(fs, 'readFile').mockRejectedValue(
Object.assign(new Error('ENOENT'), { code: 'ENOENT' })
)
const event = createEvent()
await expect(vereinsmeisterschaftenHandler(event)).rejects.toMatchObject({ statusCode: 404 })
})
it('liefert CSV-Inhalt der Vereinsmeisterschaften', async () => {
vi.spyOn(fs, 'access').mockResolvedValue(undefined)
vi.spyOn(fs, 'readFile').mockResolvedValue('Jahr,Sieger\n2024,Max Muster')
const event = createEvent()
const result = await vereinsmeisterschaftenHandler(event)
expect(result).toContain('Max Muster')
expect(result).toContain('2024')
})
})
describe('GET /api/birthdays', () => {
it('gibt leere Liste zurück wenn keine Mitglieder', async () => {
const event = createEvent()
memberUtils.readMembers.mockResolvedValue([])
authUtils.readUsers.mockResolvedValue([])
const result = await birthdaysHandler(event)
expect(result.success).toBe(true)
expect(result.birthdays).toHaveLength(0)
})
it('zeigt kommende Geburtstage der nächsten 28 Tage', async () => {
const event = createEvent()
const inDays = 7
const targetDate = new Date()
targetDate.setDate(targetDate.getDate() + inDays)
const geburtsdatum = `${targetDate.getFullYear() - 30}-${String(targetDate.getMonth() + 1).padStart(2, '0')}-${String(targetDate.getDate()).padStart(2, '0')}`
memberUtils.readMembers.mockResolvedValue([
{ firstName: 'Max', lastName: 'Muster', geburtsdatum, visibility: { showBirthday: true } }
])
authUtils.readUsers.mockResolvedValue([])
const result = await birthdaysHandler(event)
expect(result.birthdays).toHaveLength(1)
expect(result.birthdays[0].name).toBe('Max Muster')
})
it('versteckt Geburtstage wenn showBirthday=false', async () => {
const event = createEvent()
const inDays = 7
const targetDate = new Date()
targetDate.setDate(targetDate.getDate() + inDays)
const geburtsdatum = `${targetDate.getFullYear() - 30}-${String(targetDate.getMonth() + 1).padStart(2, '0')}-${String(targetDate.getDate()).padStart(2, '0')}`
memberUtils.readMembers.mockResolvedValue([
{ firstName: 'Privat', lastName: 'Person', geburtsdatum, visibility: { showBirthday: false } }
])
authUtils.readUsers.mockResolvedValue([])
const result = await birthdaysHandler(event)
expect(result.birthdays).toHaveLength(0)
})
it('zeigt Geburtstage mit deaktivierter Sichtbarkeit für Vorstand', async () => {
const event = createEvent({ cookies: { auth_token: 'token' } })
const inDays = 7
const targetDate = new Date()
targetDate.setDate(targetDate.getDate() + inDays)
const geburtsdatum = `${targetDate.getFullYear() - 30}-${String(targetDate.getMonth() + 1).padStart(2, '0')}-${String(targetDate.getDate()).padStart(2, '0')}`
authUtils.verifyToken.mockReturnValue({ id: 'v1' })
authUtils.getUserFromToken.mockResolvedValue({ id: 'v1', roles: ['vorstand'], active: true })
memberUtils.readMembers.mockResolvedValue([
{ firstName: 'Privat', lastName: 'Person', geburtsdatum, visibility: { showBirthday: false } }
])
authUtils.readUsers.mockResolvedValue([])
const result = await birthdaysHandler(event)
// Vorstand sieht alle, auch private
expect(result.birthdays).toHaveLength(1)
})
it('ignoriert Mitglieder ohne Geburtsdatum', async () => {
const event = createEvent()
memberUtils.readMembers.mockResolvedValue([
{ firstName: 'Kein', lastName: 'Datum', geburtsdatum: null }
])
authUtils.readUsers.mockResolvedValue([])
const result = await birthdaysHandler(event)
expect(result.birthdays).toHaveLength(0)
})
})
})

View File

@@ -0,0 +1,149 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createEvent, mockSuccessReadBody } from './setup'
vi.mock('../server/utils/auth.js', () => ({
verifyToken: vi.fn(),
getUserById: vi.fn(),
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/termine.js', () => ({
readTermine: vi.fn(),
saveTermin: vi.fn(),
deleteTermin: vi.fn()
}))
const authUtils = await import('../server/utils/auth.js')
const termineUtils = await import('../server/utils/termine.js')
import termineGetHandler from '../server/api/termine-manage.get.js'
import terminePostHandler from '../server/api/termine-manage.post.js'
import termineDeleteHandler from '../server/api/termine-manage.delete.js'
describe('Termine-Manage API Endpoints', () => {
beforeEach(() => {
vi.clearAllMocks()
})
const adminEvent = () => {
const event = createEvent({ cookies: { auth_token: 'token' } })
authUtils.verifyToken.mockReturnValue({ id: '1' })
authUtils.getUserById.mockResolvedValue({ id: '1', roles: ['admin'] })
authUtils.hasAnyRole.mockReturnValue(true)
return event
}
describe('GET /api/termine-manage', () => {
it('verlangt Authentifizierung', async () => {
const event = createEvent()
await expect(termineGetHandler(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(termineGetHandler(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)
await expect(termineGetHandler(event)).rejects.toMatchObject({ statusCode: 403 })
})
it('liefert Terminliste', async () => {
const event = adminEvent()
termineUtils.readTermine.mockResolvedValue([
{ datum: '2025-06-01', titel: 'Hauptversammlung', kategorie: 'Verein' }
])
const result = await termineGetHandler(event)
expect(result.success).toBe(true)
expect(result.termine).toHaveLength(1)
expect(result.termine[0].titel).toBe('Hauptversammlung')
})
})
describe('POST /api/termine-manage', () => {
it('verlangt Authentifizierung', async () => {
const event = createEvent()
mockSuccessReadBody({})
await expect(terminePostHandler(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({ datum: '2025-06-01', titel: 'Test' })
await expect(terminePostHandler(event)).rejects.toMatchObject({ statusCode: 403 })
})
it('validiert Pflichtfelder kein Titel', async () => {
const event = adminEvent()
mockSuccessReadBody({ datum: '2025-06-01' })
await expect(terminePostHandler(event)).rejects.toMatchObject({ statusCode: 400 })
})
it('validiert Pflichtfelder kein Datum', async () => {
const event = adminEvent()
mockSuccessReadBody({ titel: 'Training' })
await expect(terminePostHandler(event)).rejects.toMatchObject({ statusCode: 400 })
})
it('speichert Termin erfolgreich', async () => {
const event = adminEvent()
termineUtils.saveTermin.mockResolvedValue(undefined)
mockSuccessReadBody({ datum: '2025-06-01', titel: 'Hauptversammlung', kategorie: 'Verein', uhrzeit: '19:00' })
const result = await terminePostHandler(event)
expect(result.success).toBe(true)
expect(termineUtils.saveTermin).toHaveBeenCalledWith(
expect.objectContaining({ datum: '2025-06-01', titel: 'Hauptversammlung' })
)
})
})
describe('DELETE /api/termine-manage', () => {
it('verlangt Authentifizierung', async () => {
const event = createEvent()
await expect(termineDeleteHandler(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)
await expect(termineDeleteHandler(event)).rejects.toMatchObject({ statusCode: 403 })
})
it('validiert Pflichtfelder datum und titel fehlen', async () => {
const event = adminEvent()
// __query ist leer → keine datum/titel
await expect(termineDeleteHandler(event)).rejects.toMatchObject({ statusCode: 400 })
})
it('löscht Termin erfolgreich', async () => {
const event = adminEvent()
event.__query = { datum: '2025-06-01', titel: 'Hauptversammlung', kategorie: 'Verein' }
termineUtils.deleteTermin.mockResolvedValue(undefined)
const result = await termineDeleteHandler(event)
expect(result.success).toBe(true)
expect(termineUtils.deleteTermin).toHaveBeenCalledWith(
expect.objectContaining({ datum: '2025-06-01', titel: 'Hauptversammlung' })
)
})
})
})