Benachrichtigungen erweitert
Emails korrigiert
This commit is contained in:
116
tests/email-service.spec.ts
Normal file
116
tests/email-service.spec.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import fs from 'fs/promises'
|
||||
|
||||
vi.mock('nodemailer', () => {
|
||||
const sendMail = vi.fn().mockResolvedValue({ messageId: 'test-message' })
|
||||
const createTransport = vi.fn(() => ({ sendMail }))
|
||||
return {
|
||||
default: { createTransport },
|
||||
createTransport
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('../server/utils/auth.js', () => ({
|
||||
readUsers: 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
|
||||
}),
|
||||
isHiddenUser: vi.fn(user => user?.hidden === true || user?.invisible === true || user?.isHidden === true || user?.systemAccount === true || user?.accountType === 'playstore_review')
|
||||
}))
|
||||
|
||||
const nodemailer = await import('nodemailer')
|
||||
const authUtils = await import('../server/utils/auth.js')
|
||||
const emailService = await import('../server/utils/email-service.js')
|
||||
|
||||
describe('Email service recipients', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
vi.clearAllMocks()
|
||||
process.env.SMTP_USER = 'smtp@example.com'
|
||||
process.env.SMTP_PASS = 'smtp-password'
|
||||
authUtils.readUsers.mockResolvedValue([])
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env.SMTP_USER
|
||||
delete process.env.SMTP_PASS
|
||||
delete process.env.NODE_ENV
|
||||
delete process.env.APP_ENV
|
||||
delete process.env.DEBUG
|
||||
})
|
||||
|
||||
it('sendet bei DEBUG=FALSE in production an Vorstand statt Entwickleradresse', async () => {
|
||||
process.env.NODE_ENV = 'production'
|
||||
process.env.APP_ENV = 'test'
|
||||
process.env.DEBUG = 'FALSE'
|
||||
vi.spyOn(fs, 'readFile').mockResolvedValue(JSON.stringify({
|
||||
vorstand: {
|
||||
vorsitzender: { email: 'vorstand@example.com' }
|
||||
}
|
||||
}))
|
||||
|
||||
await emailService.sendRegistrationNotification({
|
||||
name: 'Max Muster',
|
||||
email: 'max@example.com',
|
||||
phone: '069123456'
|
||||
})
|
||||
|
||||
const transporter = nodemailer.default.createTransport.mock.results[0].value
|
||||
expect(transporter.sendMail).toHaveBeenCalledWith(expect.objectContaining({
|
||||
to: 'vorstand@example.com'
|
||||
}))
|
||||
expect(transporter.sendMail.mock.calls[0][0].to).not.toContain('tsschulz@tsschulz.de')
|
||||
})
|
||||
|
||||
it('bevorzugt aktive Vorstand-Benutzer vor config.json', async () => {
|
||||
process.env.NODE_ENV = 'production'
|
||||
process.env.DEBUG = 'FALSE'
|
||||
authUtils.readUsers.mockResolvedValue([
|
||||
{ id: '1', email: 'rolle-vorstand@example.com', roles: ['vorstand'], active: true },
|
||||
{ id: '2', email: 'inaktiv@example.com', roles: ['vorstand'], active: false },
|
||||
{ id: '3', email: 'mitglied@example.com', roles: ['mitglied'], active: true }
|
||||
])
|
||||
vi.spyOn(fs, 'readFile').mockResolvedValue(JSON.stringify({
|
||||
vorstand: {
|
||||
vorsitzender: { email: 'config-vorstand@example.com' }
|
||||
}
|
||||
}))
|
||||
|
||||
await emailService.sendRegistrationNotification({
|
||||
name: 'Max Muster',
|
||||
email: 'max@example.com'
|
||||
})
|
||||
|
||||
const transporter = nodemailer.default.createTransport.mock.results[0].value
|
||||
const to = transporter.sendMail.mock.calls[0][0].to
|
||||
expect(to).toBe('rolle-vorstand@example.com')
|
||||
expect(to).not.toContain('config-vorstand@example.com')
|
||||
expect(to).not.toContain('inaktiv@example.com')
|
||||
})
|
||||
|
||||
it('sendet nur bei explizitem DEBUG=true an die Entwickleradresse', async () => {
|
||||
process.env.NODE_ENV = 'production'
|
||||
process.env.DEBUG = 'true'
|
||||
vi.spyOn(fs, 'readFile').mockResolvedValue(JSON.stringify({
|
||||
vorstand: {
|
||||
vorsitzender: { email: 'vorstand@example.com' }
|
||||
}
|
||||
}))
|
||||
|
||||
await emailService.sendRegistrationNotification({
|
||||
name: 'Max Muster',
|
||||
email: 'max@example.com'
|
||||
})
|
||||
|
||||
const transporter = nodemailer.default.createTransport.mock.results[0].value
|
||||
expect(transporter.sendMail.mock.calls[0][0].to).toBe('tsschulz@tsschulz.de')
|
||||
})
|
||||
})
|
||||
@@ -51,12 +51,13 @@ import membersGetHandler from '../server/api/members.get.js'
|
||||
import membersPostHandler from '../server/api/members.post.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 toggleMannschaftsspielerHandler from '../server/api/members/toggle-mannschaftsspieler.post.js'
|
||||
|
||||
describe('Members API Endpoints', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
authUtils.readUsers.mockResolvedValue([])
|
||||
memberUtils.readMembers.mockResolvedValue([])
|
||||
})
|
||||
|
||||
describe('GET /api/members', () => {
|
||||
@@ -100,6 +101,38 @@ describe('Members API Endpoints', () => {
|
||||
expect(response.members[0].name).toBe('Anna Muster')
|
||||
})
|
||||
|
||||
it('liefert Geburtstags-Sichtbarkeit für Admin/Vorstand-Bearbeitung', async () => {
|
||||
const event = createEvent({ cookies: { auth_token: 'token' } })
|
||||
authUtils.verifyToken.mockReturnValue({ id: '1' })
|
||||
memberUtils.readMembers.mockResolvedValue([
|
||||
{ id: 'm1', firstName: 'Anna', lastName: 'Muster', geburtsdatum: '2000-01-01', visibility: { showBirthday: false } }
|
||||
])
|
||||
authUtils.readUsers.mockResolvedValue([])
|
||||
authUtils.getUserFromToken.mockResolvedValue({ id: '1', role: 'admin' })
|
||||
|
||||
const response = await membersGetHandler(event)
|
||||
|
||||
expect(response.members).toHaveLength(1)
|
||||
expect(response.members[0].showBirthday).toBe(false)
|
||||
})
|
||||
|
||||
it('uebernimmt Geburtstags-Sichtbarkeit vom Login-Benutzer beim Merge', async () => {
|
||||
const event = createEvent({ cookies: { auth_token: 'token' } })
|
||||
authUtils.verifyToken.mockReturnValue({ id: '1' })
|
||||
memberUtils.readMembers.mockResolvedValue([
|
||||
{ id: 'm1', firstName: 'Anna', lastName: 'Muster', email: 'anna@club.de', geburtsdatum: '2000-01-01', visibility: { showBirthday: true } }
|
||||
])
|
||||
authUtils.readUsers.mockResolvedValue([
|
||||
{ id: 'u1', name: 'Anna Muster', email: 'anna@club.de', active: true, visibility: { showBirthday: false } }
|
||||
])
|
||||
authUtils.getUserFromToken.mockResolvedValue({ id: '1', role: 'admin' })
|
||||
|
||||
const response = await membersGetHandler(event)
|
||||
|
||||
expect(response.members).toHaveLength(1)
|
||||
expect(response.members[0].showBirthday).toBe(false)
|
||||
})
|
||||
|
||||
|
||||
it('blendet unsichtbare Playstore-Benutzer und passende manuelle Einträge aus', async () => {
|
||||
const event = createEvent({ cookies: { auth_token: 'token' } })
|
||||
@@ -159,6 +192,8 @@ describe('Members API Endpoints', () => {
|
||||
const event = createEvent({ cookies: { auth_token: 'token' } })
|
||||
mockSuccessReadBody(baseBody)
|
||||
authUtils.getUserFromToken.mockResolvedValue({ id: '2', role: 'admin' })
|
||||
authUtils.readUsers.mockResolvedValue([])
|
||||
memberUtils.readMembers.mockResolvedValue([])
|
||||
memberUtils.saveMember.mockResolvedValue(true)
|
||||
|
||||
const response = await membersPostHandler(event)
|
||||
@@ -168,6 +203,76 @@ describe('Members API Endpoints', () => {
|
||||
}))
|
||||
})
|
||||
|
||||
it('speichert Geburtstags-Sichtbarkeit für manuelle Mitglieder', async () => {
|
||||
const event = createEvent({ cookies: { auth_token: 'token' } })
|
||||
mockSuccessReadBody({ ...baseBody, showBirthday: false })
|
||||
authUtils.getUserFromToken.mockResolvedValue({ id: '2', role: 'admin' })
|
||||
authUtils.readUsers.mockResolvedValue([])
|
||||
memberUtils.readMembers.mockResolvedValue([
|
||||
{ id: 'manual-1', firstName: 'Lisa', lastName: 'Beispiel', email: 'lisa@example.com', visibility: { showBirthday: true } }
|
||||
])
|
||||
memberUtils.saveMember.mockResolvedValue(true)
|
||||
|
||||
const response = await membersPostHandler(event)
|
||||
|
||||
expect(response.success).toBe(true)
|
||||
expect(memberUtils.saveMember).toHaveBeenCalledWith(expect.objectContaining({
|
||||
visibility: expect.objectContaining({ showBirthday: false })
|
||||
}))
|
||||
expect(authUtils.writeUsers).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('kann Geburtstags-Sichtbarkeit auch am Login-Benutzer ausschalten', async () => {
|
||||
const event = createEvent({ cookies: { auth_token: 'token' } })
|
||||
mockSuccessReadBody({
|
||||
id: 'user-1',
|
||||
...baseBody,
|
||||
email: 'lisa@example.com',
|
||||
visibility: { showBirthday: false }
|
||||
})
|
||||
authUtils.getUserFromToken.mockResolvedValue({ id: '2', role: 'vorstand' })
|
||||
memberUtils.readMembers.mockResolvedValue([])
|
||||
authUtils.readUsers.mockResolvedValue([
|
||||
{ id: 'user-1', email: 'lisa@example.com', visibility: { showBirthday: true, showEmail: true } }
|
||||
])
|
||||
authUtils.writeUsers.mockResolvedValue(undefined)
|
||||
memberUtils.saveMember.mockResolvedValue(true)
|
||||
|
||||
const response = await membersPostHandler(event)
|
||||
|
||||
expect(response.success).toBe(true)
|
||||
expect(authUtils.writeUsers).toHaveBeenCalledWith([
|
||||
expect.objectContaining({
|
||||
id: 'user-1',
|
||||
visibility: expect.objectContaining({ showBirthday: false, showEmail: true })
|
||||
})
|
||||
])
|
||||
})
|
||||
|
||||
it('darf Geburtstags-Sichtbarkeit nicht für Login-Benutzer einschalten', async () => {
|
||||
const event = createEvent({ cookies: { auth_token: 'token' } })
|
||||
mockSuccessReadBody({
|
||||
id: 'user-1',
|
||||
...baseBody,
|
||||
email: 'lisa@example.com',
|
||||
visibility: { showBirthday: true }
|
||||
})
|
||||
authUtils.getUserFromToken.mockResolvedValue({ id: '2', role: 'vorstand' })
|
||||
memberUtils.readMembers.mockResolvedValue([])
|
||||
authUtils.readUsers.mockResolvedValue([
|
||||
{ id: 'user-1', email: 'lisa@example.com', visibility: { showBirthday: false, showEmail: true } }
|
||||
])
|
||||
memberUtils.saveMember.mockResolvedValue(true)
|
||||
|
||||
const response = await membersPostHandler(event)
|
||||
|
||||
expect(response.success).toBe(true)
|
||||
expect(memberUtils.saveMember).toHaveBeenCalledWith(expect.objectContaining({
|
||||
visibility: expect.objectContaining({ showBirthday: false })
|
||||
}))
|
||||
expect(authUtils.writeUsers).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('erlaubt vorstand beim Speichern', async () => {
|
||||
const event = createEvent({ cookies: { auth_token: 'token' } })
|
||||
mockSuccessReadBody(baseBody)
|
||||
@@ -187,6 +292,7 @@ describe('Members API Endpoints', () => {
|
||||
email: 'lisa@example.com'
|
||||
})
|
||||
authUtils.getUserFromToken.mockResolvedValue({ id: '3', role: 'vorstand' })
|
||||
authUtils.readUsers.mockResolvedValue([])
|
||||
memberUtils.saveMember.mockResolvedValue(true)
|
||||
|
||||
const response = await membersPostHandler(event)
|
||||
|
||||
93
tests/notification-scheduler.spec.ts
Normal file
93
tests/notification-scheduler.spec.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import fs from 'fs/promises'
|
||||
|
||||
vi.mock('../server/utils/auth.js', () => ({
|
||||
readUsers: vi.fn(),
|
||||
isHiddenUser: vi.fn(user => user?.hidden === true || user?.invisible === true || user?.isHidden === true || user?.systemAccount === true)
|
||||
}))
|
||||
|
||||
vi.mock('../server/utils/members.js', () => ({
|
||||
readMembers: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('../server/utils/termine.js', () => ({
|
||||
readTermine: vi.fn().mockResolvedValue([])
|
||||
}))
|
||||
|
||||
vi.mock('../server/utils/news.js', () => ({
|
||||
readNews: vi.fn().mockResolvedValue([])
|
||||
}))
|
||||
|
||||
vi.mock('../server/utils/spielplan-data.js', () => ({
|
||||
getDefaultSpielplanSeason: vi.fn().mockResolvedValue('25--26'),
|
||||
readSpielplanData: vi.fn().mockResolvedValue({ data: [] })
|
||||
}))
|
||||
|
||||
vi.mock('../server/utils/push-notifications.js', () => ({
|
||||
sendPushToUsers: vi.fn().mockResolvedValue({ sent: 1, failed: 0, removed: 0, recipients: 1, tokenCount: 1, skipped: false })
|
||||
}))
|
||||
|
||||
vi.mock('../server/utils/logger.js', () => ({
|
||||
error: vi.fn(),
|
||||
info: vi.fn(),
|
||||
warn: vi.fn()
|
||||
}))
|
||||
|
||||
const authUtils = await import('../server/utils/auth.js')
|
||||
const memberUtils = await import('../server/utils/members.js')
|
||||
const pushUtils = await import('../server/utils/push-notifications.js')
|
||||
|
||||
const { runNotificationSchedulerTick } = await import('../server/utils/notification-scheduler.js')
|
||||
|
||||
const schedulerNow = new Date('2026-06-14T07:00:00.000Z')
|
||||
const recipient = {
|
||||
id: 'recipient',
|
||||
name: 'Push Empfaenger',
|
||||
active: true,
|
||||
notificationSettings: {
|
||||
birthdays: true,
|
||||
notificationTime: '09:00'
|
||||
}
|
||||
}
|
||||
|
||||
describe('Notification Scheduler', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.spyOn(fs, 'readFile').mockRejectedValue(Object.assign(new Error('ENOENT'), { code: 'ENOENT' }))
|
||||
vi.spyOn(fs, 'mkdir').mockResolvedValue(undefined)
|
||||
vi.spyOn(fs, 'writeFile').mockResolvedValue(undefined)
|
||||
memberUtils.readMembers.mockResolvedValue([])
|
||||
authUtils.readUsers.mockResolvedValue([recipient])
|
||||
})
|
||||
|
||||
it('sendet Geburtstags-Push nur fuer Mitglieder mit expliziter Geburtstagsfreigabe', async () => {
|
||||
memberUtils.readMembers.mockResolvedValue([
|
||||
{ firstName: 'Erlaubt', lastName: 'Person', active: true, geburtsdatum: '1990-06-14', visibility: { showBirthday: true } },
|
||||
{ firstName: 'Privat', lastName: 'Person', active: true, geburtsdatum: '1990-06-14', visibility: { showBirthday: false } },
|
||||
{ firstName: 'Unklar', lastName: 'Person', active: true, geburtsdatum: '1990-06-14' }
|
||||
])
|
||||
|
||||
await runNotificationSchedulerTick(schedulerNow)
|
||||
|
||||
expect(pushUtils.sendPushToUsers).toHaveBeenCalledTimes(1)
|
||||
expect(pushUtils.sendPushToUsers).toHaveBeenCalledWith(expect.objectContaining({
|
||||
title: 'Geburtstage heute',
|
||||
body: 'Erlaubt Person hat heute Geburtstag.',
|
||||
data: { type: 'birthdays', date: '2026-06-14' }
|
||||
}))
|
||||
})
|
||||
|
||||
it('nennt bei mehreren Geburtstags-Pushes nur Namen und kein Alter', async () => {
|
||||
memberUtils.readMembers.mockResolvedValue([
|
||||
{ firstName: 'Anna', lastName: 'Beispiel', active: true, geburtsdatum: '1980-06-14', visibility: { showBirthday: true } },
|
||||
{ firstName: 'Bert', lastName: 'Beispiel', active: true, geburtsdatum: '2010-06-14', visibility: { showBirthday: true } }
|
||||
])
|
||||
|
||||
await runNotificationSchedulerTick(schedulerNow)
|
||||
|
||||
const payload = pushUtils.sendPushToUsers.mock.calls[0][0]
|
||||
expect(payload.body).toBe('Geburtstage heute: Anna Beispiel, Bert Beispiel.')
|
||||
expect(payload.body).not.toMatch(/\b\d+\b/)
|
||||
expect(payload.body).not.toContain('Jahre')
|
||||
})
|
||||
})
|
||||
@@ -16,8 +16,25 @@ vi.mock('../server/utils/news.js', () => ({
|
||||
readNews: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('../server/utils/auth.js', () => ({
|
||||
readUsers: 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
|
||||
}),
|
||||
isHiddenUser: vi.fn(user => user?.hidden === true || user?.invisible === true || user?.isHidden === true || user?.systemAccount === true || user?.accountType === 'playstore_review')
|
||||
}))
|
||||
|
||||
const nodemailer = await import('nodemailer')
|
||||
const newsUtils = await import('../server/utils/news.js')
|
||||
const authUtils = await import('../server/utils/auth.js')
|
||||
|
||||
import contactHandler from '../server/api/contact.post.js'
|
||||
import galerieHandler from '../server/api/galerie.get.js'
|
||||
@@ -29,14 +46,17 @@ describe('Öffentliche API-Endpunkte', () => {
|
||||
afterEach(() => {
|
||||
delete process.env.NODE_ENV
|
||||
delete process.env.APP_ENV
|
||||
delete process.env.DEBUG
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
// Setze SMTP-Credentials für Tests
|
||||
process.env.SMTP_USER = 'test@example.com'
|
||||
process.env.SMTP_PASS = 'test-password'
|
||||
authUtils.readUsers.mockResolvedValue([])
|
||||
vi.restoreAllMocks()
|
||||
vi.clearAllMocks()
|
||||
authUtils.readUsers.mockResolvedValue([])
|
||||
})
|
||||
|
||||
describe('POST /api/contact', () => {
|
||||
@@ -78,6 +98,53 @@ describe('Öffentliche API-Endpunkte', () => {
|
||||
to: 'tsschulz@tsschulz.de'
|
||||
}))
|
||||
})
|
||||
|
||||
it('sendet bei DEBUG=FALSE an konfigurierte Empfänger', async () => {
|
||||
process.env.NODE_ENV = 'production'
|
||||
process.env.APP_ENV = 'test'
|
||||
process.env.DEBUG = 'FALSE'
|
||||
vi.spyOn(fs, 'readFile').mockResolvedValue(JSON.stringify({
|
||||
vorstand: {
|
||||
vorsitzender: { email: 'vorstand@example.com' }
|
||||
}
|
||||
}))
|
||||
|
||||
const event = createEvent()
|
||||
mockSuccessReadBody({ name: 'Max', email: 'max@example.com', subject: 'Frage', message: 'Hallo' })
|
||||
|
||||
await contactHandler(event)
|
||||
|
||||
const transporter = nodemailer.default.createTransport.mock.results[0].value
|
||||
const to = transporter.sendMail.mock.calls[0][0].to
|
||||
expect(to).toContain('vorstand@example.com')
|
||||
expect(to).not.toContain('tsschulz@tsschulz.de')
|
||||
})
|
||||
|
||||
it('bevorzugt aktive Vorstand-Benutzer vor config.json', async () => {
|
||||
process.env.NODE_ENV = 'production'
|
||||
process.env.DEBUG = 'FALSE'
|
||||
authUtils.readUsers.mockResolvedValue([
|
||||
{ id: '1', email: 'rolle-vorstand@example.com', roles: ['vorstand'], active: true },
|
||||
{ id: '2', email: 'hidden@example.com', roles: ['vorstand'], active: true, hidden: true },
|
||||
{ id: '3', email: 'trainer@example.com', roles: ['trainer'], active: true }
|
||||
])
|
||||
vi.spyOn(fs, 'readFile').mockResolvedValue(JSON.stringify({
|
||||
vorstand: {
|
||||
vorsitzender: { email: 'config-vorstand@example.com' }
|
||||
}
|
||||
}))
|
||||
|
||||
const event = createEvent()
|
||||
mockSuccessReadBody({ name: 'Max', email: 'max@example.com', subject: 'Frage', message: 'Hallo' })
|
||||
|
||||
await contactHandler(event)
|
||||
|
||||
const transporter = nodemailer.default.createTransport.mock.results[0].value
|
||||
const to = transporter.sendMail.mock.calls[0][0].to
|
||||
expect(to).toContain('rolle-vorstand@example.com')
|
||||
expect(to).not.toContain('config-vorstand@example.com')
|
||||
expect(to).not.toContain('hidden@example.com')
|
||||
})
|
||||
})
|
||||
|
||||
describe('GET /api/galerie', () => {
|
||||
|
||||
Reference in New Issue
Block a user