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), normalizeUserEmail: vi.fn(email => String(email || '').trim().toLowerCase()), isHiddenUser: vi.fn(user => user?.hidden === true || user?.invisible === true || user?.isHidden === true || user?.systemAccount === true || user?.accountType === 'playstore_review') })) 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('akzeptiert Bearer-Token für Android-Clients', async () => { const event = createEvent({ headers: { authorization: 'Bearer android-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: 'Android', lastName: 'Privat', geburtsdatum, visibility: { showBirthday: false } } ]) authUtils.readUsers.mockResolvedValue([]) const result = await birthdaysHandler(event) expect(authUtils.verifyToken).toHaveBeenCalledWith('android-token') 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) }) it('blendet unsichtbare Playstore-Benutzer auch für Vorstand aus', 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 }) authUtils.readUsers.mockResolvedValue([ { id: 'u2', name: 'Playstore Review', email: 'review@club.de', active: true, geburtsdatum, accountType: 'playstore_review' } ]) memberUtils.readMembers.mockResolvedValue([ { firstName: 'Play', lastName: 'Store', email: 'review@club.de', geburtsdatum, visibility: { showBirthday: true } } ]) const result = await birthdaysHandler(event) expect(result.birthdays).toHaveLength(0) }) }) })