@@ -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: |
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
241
tests/config-profile-endpoints.spec.ts
Normal file
241
tests/config-profile-endpoints.spec.ts
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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()
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
160
tests/news-endpoints.spec.ts
Normal file
160
tests/news-endpoints.spec.ts
Normal 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')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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()
|
||||||
})
|
})
|
||||||
|
|||||||
248
tests/spielplan-public-endpoints.spec.ts
Normal file
248
tests/spielplan-public-endpoints.spec.ts
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
149
tests/termine-manage-endpoints.spec.ts
Normal file
149
tests/termine-manage-endpoints.spec.ts
Normal 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' })
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user