import { beforeEach, describe, expect, it, vi } from 'vitest' import { createEvent, mockSuccessReadBody } from './setup' vi.mock('../server/utils/auth.js', () => ({ getUserFromToken: vi.fn(), readUsers: vi.fn(), writeUsers: vi.fn(), revokeRefreshSessionsForUser: vi.fn(), hasRole: vi.fn((user, role) => { if (!user) return false const userRoles = Array.isArray(user.roles) ? user.roles : (user.role ? [user.role] : []) return userRoles.includes(role) }), 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)) }), 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') })) vi.mock('nodemailer', () => { const sendMail = vi.fn().mockResolvedValue(true) const createTransport = vi.fn(() => ({ sendMail })) return { default: { createTransport }, createTransport } }) vi.mock('../server/utils/password-reset-log.js', () => ({ fingerprintResetEmail: vi.fn(email => `fingerprint:${String(email || '').trim().toLowerCase()}`), normalizeResetEmail: vi.fn(email => String(email || '').trim().toLowerCase()), PASSWORD_RESET_LOG_RETENTION_HOURS: 72, readPasswordResetLogs: vi.fn() })) const authUtils = await import('../server/utils/auth.js') const nodemailer = await import('nodemailer') const passwordResetLog = await import('../server/utils/password-reset-log.js') import usersListHandler from '../server/api/cms/users/list.get.js' import usersApproveHandler from '../server/api/cms/users/approve.post.js' import usersDeactivateHandler from '../server/api/cms/users/deactivate.post.js' import usersRejectHandler from '../server/api/cms/users/reject.post.js' import usersUpdateRoleHandler from '../server/api/cms/users/update-role.post.js' import passwordResetDiagnosticsHandler from '../server/api/cms/password-reset-diagnostics.get.js' describe('CMS User Management Endpoints', () => { beforeEach(() => { vi.clearAllMocks() }) const adminEvent = () => { const event = createEvent({ cookies: { auth_token: 'token' } }) authUtils.getUserFromToken.mockResolvedValue({ id: 'admin', roles: ['admin'] }) authUtils.hasAnyRole.mockReturnValue(true) return event } describe('GET /api/cms/users/list', () => { it('verweigert Zugriff für Nicht-Admins', async () => { const event = createEvent({ cookies: { auth_token: 'token' } }) authUtils.getUserFromToken.mockResolvedValue({ id: 'user', roles: ['mitglied'] }) authUtils.hasAnyRole.mockReturnValue(false) await expect(usersListHandler(event)).rejects.toMatchObject({ statusCode: 403 }) }) it('liefert Benutzerliste ohne Passwörter', async () => { const event = adminEvent() authUtils.readUsers.mockResolvedValue([{ id: '1', email: 'a@b.de', name: 'Anna', roles: ['mitglied'], phone: '1', active: true, created: 'now', lastLogin: null, password: 'secret' }]) authUtils.migrateUserRoles.mockImplementation((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 }) const response = await usersListHandler(event) expect(response.users[0]).not.toHaveProperty('password') expect(response.users).toHaveLength(1) }) it('blendet unsichtbare Playstore-Benutzer auch für Admins aus', async () => { const event = adminEvent() authUtils.readUsers.mockResolvedValue([ { id: '1', email: 'a@b.de', name: 'Anna', roles: ['mitglied'], active: true }, { id: '2', email: 'review@club.de', name: 'Playstore Review', roles: ['mitglied'], active: true, accountType: 'playstore_review' } ]) const response = await usersListHandler(event) expect(response.users).toHaveLength(1) expect(response.users[0].email).toBe('a@b.de') }) }) describe('POST /api/cms/users/approve', () => { it('erfordert administrative Rolle', async () => { const event = createEvent({ cookies: { auth_token: 'token' } }) authUtils.getUserFromToken.mockResolvedValue({ id: 'user', roles: ['mitglied'] }) authUtils.hasAnyRole.mockReturnValue(false) mockSuccessReadBody({ userId: '1' }) await expect(usersApproveHandler(event)).rejects.toMatchObject({ statusCode: 403 }) }) it('meldet 404 bei unbekanntem Benutzer', async () => { const event = adminEvent() mockSuccessReadBody({ userId: '1' }) authUtils.readUsers.mockResolvedValue([]) await expect(usersApproveHandler(event)).rejects.toMatchObject({ statusCode: 404 }) }) it('aktiviert Benutzer und sendet Mail', async () => { const event = adminEvent() mockSuccessReadBody({ userId: '1', roles: ['vorstand'] }) authUtils.readUsers.mockResolvedValue([{ id: '1', email: 'user@test.de', name: 'Udo', active: false }]) authUtils.writeUsers.mockResolvedValue(true) // Setze SMTP-Credentials für Tests process.env.SMTP_USER = 'test@example.com' process.env.SMTP_PASS = 'test-password' const response = await usersApproveHandler(event) expect(response.success).toBe(true) expect(authUtils.writeUsers).toHaveBeenCalled() expect(nodemailer.default.createTransport).toHaveBeenCalled() }) }) describe('POST /api/cms/users/deactivate', () => { it('verhindert Selbst-Deaktivierung', async () => { const event = adminEvent() mockSuccessReadBody({ userId: 'admin' }) await expect(usersDeactivateHandler(event)).rejects.toMatchObject({ statusCode: 400 }) }) it('deaktiviert Benutzer', async () => { const event = adminEvent() mockSuccessReadBody({ userId: '1' }) authUtils.readUsers.mockResolvedValue([{ id: '1', active: true }]) authUtils.writeUsers.mockResolvedValue(true) const response = await usersDeactivateHandler(event) expect(response.success).toBe(true) expect(authUtils.writeUsers).toHaveBeenCalled() expect(authUtils.revokeRefreshSessionsForUser).toHaveBeenCalledWith('1', 'account_deactivated') }) }) describe('POST /api/cms/users/reject', () => { it('löscht Benutzer aus der Liste', async () => { const event = adminEvent() mockSuccessReadBody({ userId: '2' }) authUtils.readUsers.mockResolvedValue([{ id: '1' }, { id: '2' }]) authUtils.writeUsers.mockResolvedValue(true) const response = await usersRejectHandler(event) expect(response.success).toBe(true) expect(authUtils.writeUsers).toHaveBeenCalledWith([{ id: '1' }]) }) }) describe('POST /api/cms/users/update-role', () => { it('validiert Rolle', async () => { const event = adminEvent() mockSuccessReadBody({ userId: '1', roles: ['invalid'] }) await expect(usersUpdateRoleHandler(event)).rejects.toMatchObject({ statusCode: 400 }) }) it('aktualisiert Rolle', async () => { const event = adminEvent() mockSuccessReadBody({ userId: '1', roles: ['vorstand'] }) authUtils.readUsers.mockResolvedValue([{ id: '1', roles: ['mitglied'] }]) authUtils.writeUsers.mockResolvedValue(true) authUtils.migrateUserRoles.mockImplementation((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 }) const response = await usersUpdateRoleHandler(event) expect(response.success).toBe(true) expect(authUtils.writeUsers).toHaveBeenCalled() }) }) describe('GET /api/cms/password-reset-diagnostics', () => { it('verweigert Zugriff für Vorstand ohne Admin-Rolle', async () => { const event = createEvent({ cookies: { auth_token: 'token' } }) authUtils.getUserFromToken.mockResolvedValue({ id: 'vorstand', roles: ['vorstand'] }) authUtils.hasRole.mockReturnValue(false) await expect(passwordResetDiagnosticsHandler(event)).rejects.toMatchObject({ statusCode: 403 }) }) it('findet Benutzer und gefilterte fehlgeschlagene Reset-Abläufe', async () => { const event = adminEvent() event.__query = { email: ' User@Example.com ', failedOnly: 'true' } authUtils.hasRole.mockReturnValue(true) authUtils.readUsers.mockResolvedValue([ { id: '1', email: 'user@example.com', name: 'User Beispiel', active: true } ]) passwordResetLog.readPasswordResetLogs.mockResolvedValue([ { requestId: 'r1', ts: '2026-05-27T10:00:01.000Z', emailMasked: 'us***@ex***.com', emailFingerprint: 'fingerprint:user@example.com', ip: '127.0.0.1', step: 'request_completed', status: 'no_account' }, { requestId: 'r2', ts: '2026-05-27T10:00:02.000Z', emailMasked: 'ot***@ex***.com', emailFingerprint: 'fingerprint:other@example.com', ip: '127.0.0.1', step: 'request_completed', status: 'failed' } ]) const response = await passwordResetDiagnosticsHandler(event) expect(response.matchingUsers).toHaveLength(1) expect(response.attempts).toHaveLength(1) expect(response.attempts[0]).toMatchObject({ requestId: 'r1', failed: true }) }) it('blendet unsichtbare Benutzer und ihre Reset-Logs in der Diagnose aus', async () => { const event = adminEvent() event.__query = { email: 'review@club.de', failedOnly: 'false' } authUtils.hasRole.mockReturnValue(true) authUtils.readUsers.mockResolvedValue([ { id: '2', email: 'review@club.de', name: 'Playstore Review', active: true, accountType: 'playstore_review' } ]) passwordResetLog.readPasswordResetLogs.mockResolvedValue([ { requestId: 'r-hidden', ts: '2026-05-27T10:00:01.000Z', emailMasked: 're***@cl***.de', emailFingerprint: 'fingerprint:review@club.de', ip: '127.0.0.1', step: 'request_completed', status: 'failed' } ]) const response = await passwordResetDiagnosticsHandler(event) expect(response.matchingUsers).toHaveLength(0) expect(response.attempts).toHaveLength(0) }) }) })