285 lines
11 KiB
TypeScript
285 lines
11 KiB
TypeScript
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)
|
|
})
|
|
})
|
|
})
|