From 3f1018ef938c49ca1e2ec59e87d6d5593eadff09 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Mon, 10 Nov 2025 16:54:49 +0100 Subject: [PATCH] Refactor database configuration and enhance error handling in authentication services Updated the database configuration to centralize settings and improve maintainability. Enhanced error handling in the authentication service to provide clearer and more specific error messages for various failure scenarios, including registration, activation, and login processes. Additionally, added new dependencies for testing and SQLite support in the package.json file. --- backend/config.js | 9 +- backend/database.js | 3 +- backend/package.json | 7 +- backend/services/authService.js | 35 +++- backend/tests/authMiddleware.test.js | 60 +++++++ backend/tests/authRoutes.test.js | 161 ++++++++++++++++++ backend/tests/authService.test.js | 99 +++++++++++ backend/tests/authorizationMiddleware.test.js | 68 ++++++++ backend/tests/permissionRoutes.test.js | 109 ++++++++++++ backend/tests/permissionService.test.js | 128 ++++++++++++++ backend/tests/setupTestEnv.js | 13 ++ backend/tests/testApp.js | 17 ++ backend/vitest.config.js | 14 ++ frontend/src/views/TeamManagementView.vue | 4 +- 14 files changed, 716 insertions(+), 11 deletions(-) create mode 100644 backend/tests/authMiddleware.test.js create mode 100644 backend/tests/authRoutes.test.js create mode 100644 backend/tests/authService.test.js create mode 100644 backend/tests/authorizationMiddleware.test.js create mode 100644 backend/tests/permissionRoutes.test.js create mode 100644 backend/tests/permissionService.test.js create mode 100644 backend/tests/setupTestEnv.js create mode 100644 backend/tests/testApp.js create mode 100644 backend/vitest.config.js diff --git a/backend/config.js b/backend/config.js index 2db1943..d9bbb0f 100644 --- a/backend/config.js +++ b/backend/config.js @@ -7,7 +7,7 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); dotenv.config({ path: path.resolve(__dirname, '.env') }); -export const development = { +const baseConfig = { username: process.env.DB_USER || 'root', password: process.env.DB_PASSWORD || 'hitomisan', database: process.env.DB_NAME || 'trainingdiary', @@ -18,5 +18,12 @@ export const development = { underscored: true, underscoredAll: true, }, + logging: false, }; + +if (baseConfig.dialect === 'sqlite') { + baseConfig.storage = process.env.DB_STORAGE || ':memory:'; +} + +export const development = baseConfig; \ No newline at end of file diff --git a/backend/database.js b/backend/database.js index 8c31de0..19682fb 100644 --- a/backend/database.js +++ b/backend/database.js @@ -10,7 +10,8 @@ const sequelize = new Sequelize( host: development.host, dialect: development.dialect, define: development.define, - logging: false, // SQL-Logging deaktivieren + logging: development.logging ?? false, + storage: development.storage, } ); diff --git a/backend/package.json b/backend/package.json index d154c2c..aa24706 100644 --- a/backend/package.json +++ b/backend/package.json @@ -7,7 +7,8 @@ "postinstall": "cd ../frontend && npm install && npm run build", "dev": "nodemon server.js", "cleanup:usertoken": "node ./scripts/cleanupUserTokenKeys.js", - "cleanup:indexes": "node ./scripts/cleanupAllIndexes.js" + "cleanup:indexes": "node ./scripts/cleanupAllIndexes.js", + "test": "cross-env NODE_ENV=test vitest run --runInBand" }, "keywords": [], "author": "", @@ -34,7 +35,11 @@ "sharp": "^0.33.5" }, "devDependencies": { + "cross-env": "^7.0.3", "nodemon": "^3.1.4", + "supertest": "^7.1.1", + "sqlite3": "^5.1.7", + "vitest": "^1.6.0", "vue-eslint-parser": "9.4.3" } } diff --git a/backend/services/authService.js b/backend/services/authService.js index 5bd56a2..10975c3 100644 --- a/backend/services/authService.js +++ b/backend/services/authService.js @@ -13,13 +13,24 @@ const register = async (email, password) => { return user; } catch (error) { devLog(error); - return null; + if (error.name === 'SequelizeUniqueConstraintError') { + const err = new Error('E-Mail-Adresse wird bereits verwendet'); + err.status = 409; + throw err; + } + const err = new Error('Registrierung fehlgeschlagen'); + err.status = 400; + throw err; } }; const activateUser = async (activationCode) => { const user = await User.findOne({ where: { activationCode } }); - if (!user) throw new Error('Invalid activation code'); + if (!user) { + const err = new Error('Aktivierungscode ungültig'); + err.status = 404; + throw err; + } user.isActive = true; user.activationCode = null; await user.save(); @@ -28,11 +39,21 @@ const activateUser = async (activationCode) => { const login = async (email, password) => { if (!email || !password) { - throw { status: 400, message: 'Email und Passwort sind erforderlich.' }; + const err = new Error('Email und Passwort sind erforderlich.'); + err.status = 400; + throw err; } const user = await User.findOne({ where: { email } }); - if (!user || !(await bcrypt.compare(password, user.password))) { - throw { status: 401, message: 'Ungültige Anmeldedaten' }; + const validPassword = user && await bcrypt.compare(password, user.password); + if (!validPassword) { + const err = new Error('Ungültige Anmeldedaten'); + err.status = 401; + throw err; + } + if (!user.isActive) { + const err = new Error('Account wurde noch nicht aktiviert'); + err.status = 403; + throw err; } const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, { expiresIn: '3h' }); await UserToken.create({ @@ -45,7 +66,9 @@ const login = async (email, password) => { const logout = async (token) => { if (!token) { - throw { status: 400, message: 'Token fehlt' }; + const err = new Error('Token fehlt'); + err.status = 400; + throw err; } await UserToken.destroy({ where: { token } }); return { message: 'Logout erfolgreich' }; diff --git a/backend/tests/authMiddleware.test.js b/backend/tests/authMiddleware.test.js new file mode 100644 index 0000000..29395dd --- /dev/null +++ b/backend/tests/authMiddleware.test.js @@ -0,0 +1,60 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import jwt from 'jsonwebtoken'; + +import { authenticate } from '../middleware/authMiddleware.js'; +import sequelize from '../database.js'; +import User from '../models/User.js'; +import UserToken from '../models/UserToken.js'; + +const createRes = () => { + const res = {}; + res.status = vi.fn().mockReturnValue(res); + res.json = vi.fn().mockReturnValue(res); + return res; +}; + +describe('authenticate middleware', () => { + beforeEach(async () => { + await sequelize.sync({ force: true }); + }); + + it('antwortet mit 401 wenn kein Token vorhanden ist', async () => { + const req = { headers: {} }; + const res = createRes(); + const next = vi.fn(); + + await authenticate(req, res, next); + + expect(res.status).toHaveBeenCalledWith(401); + expect(res.json).toHaveBeenCalledWith({ error: 'Unauthorized: Token fehlt' }); + expect(next).not.toHaveBeenCalled(); + }); + + it('verweigert ungültige Tokens', async () => { + const req = { headers: { authorization: 'Bearer invalid' } }; + const res = createRes(); + const next = vi.fn(); + + await authenticate(req, res, next); + + expect(res.status).toHaveBeenCalledWith(401); + expect(res.json).toHaveBeenCalled(); + expect(next).not.toHaveBeenCalled(); + }); + + it('akzeptiert gültige Tokens und ruft next()', async () => { + const user = await User.create({ email: 'middleware@example.com', password: 'Secret!123', isActive: true }); + const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, { expiresIn: '1h' }); + await UserToken.create({ userId: user.id, token, expiresAt: new Date(Date.now() + 3600000) }); + + const req = { headers: { authorization: `Bearer ${token}` } }; + const res = createRes(); + const next = vi.fn(); + + await authenticate(req, res, next); + + expect(next).toHaveBeenCalledOnce(); + expect(req.user).toMatchObject({ id: user.id }); + expect(res.status).not.toHaveBeenCalled(); + }); +}); diff --git a/backend/tests/authRoutes.test.js b/backend/tests/authRoutes.test.js new file mode 100644 index 0000000..e2c5ea0 --- /dev/null +++ b/backend/tests/authRoutes.test.js @@ -0,0 +1,161 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import request from 'supertest'; + +vi.mock('../services/emailService.js', () => ({ + sendActivationEmail: vi.fn().mockResolvedValue(), +})); + +import app from './testApp.js'; +import sequelize from '../database.js'; +import User from '../models/User.js'; +import Club from '../models/Club.js'; +import UserClub from '../models/UserClub.js'; +import UserToken from '../models/UserToken.js'; + +const registerPayload = (email = 'test@example.com', password = 'Passwort!123') => ({ email, password }); + +describe('Auth & Permissions Routes', () => { + beforeEach(async () => { + vi.clearAllMocks(); + await sequelize.sync({ force: true }); + }); + + it('registriert einen Nutzer über die API', async () => { + const response = await request(app) + .post('/api/auth/register') + .send(registerPayload()); + + expect(response.status).toBe(201); + expect(response.body.email).toBe('test@example.com'); + const user = await User.findOne({ where: { email: 'test@example.com' } }); + expect(user).not.toBeNull(); + }); + + it('meldet einen aktiven Nutzer an und liefert einen Token', async () => { + const credentials = registerPayload('login@example.com'); + await request(app).post('/api/auth/register').send(credentials); + const user = await User.findOne({ where: { email: credentials.email } }); + await user.update({ isActive: true }); + + const response = await request(app) + .post('/api/auth/login') + .send(credentials); + + expect(response.status).toBe(200); + expect(response.body.token).toBeTruthy(); + }); + + it('verhindert Login ohne Aktivierung', async () => { + const credentials = registerPayload('inactive@example.com'); + await request(app).post('/api/auth/register').send(credentials); + + const response = await request(app) + .post('/api/auth/login') + .send(credentials); + + expect(response.status).toBe(403); + expect(response.body.message || response.body.error).toMatch(/aktiviert/i); + }); + + it('verweigert doppelte Registrierung', async () => { + const payload = registerPayload('duplicate@example.com'); + await request(app).post('/api/auth/register').send(payload); + + const response = await request(app).post('/api/auth/register').send(payload); + + expect(response.status).toBe(409); + expect(response.body.error || response.body.message).toMatch(/bereits/i); + }); + + it('aktiviert einen Benutzer über die API', async () => { + const payload = registerPayload('activate@example.com'); + await request(app).post('/api/auth/register').send(payload); + const user = await User.findOne({ where: { email: payload.email } }); + + const response = await request(app).get(`/api/auth/activate/${user.activationCode}`); + + expect(response.status).toBe(200); + const reloaded = await user.reload(); + expect(reloaded.isActive).toBe(true); + }); + + it('meldet Fehler bei ungültigem Aktivierungscode', async () => { + const response = await request(app).get('/api/auth/activate/invalid-code'); + expect(response.status).toBe(404); + }); + + it('meldet einen Nutzer ab und entfernt den Token', async () => { + const credentials = registerPayload('logout-route@example.com'); + await request(app).post('/api/auth/register').send(credentials); + const user = await User.findOne({ where: { email: credentials.email } }); + await user.update({ isActive: true }); + + const login = await request(app).post('/api/auth/login').send(credentials); + const token = login.body.token; + + const response = await request(app) + .post('/api/auth/logout') + .set('Authorization', `Bearer ${token}`); + + expect(response.status).toBe(200); + const tokenRecord = await UserToken.findOne({ where: { token } }); + expect(tokenRecord).toBeNull(); + }); + + it('ändert Rollen über die Permissions-API (Admin)', async () => { + const ownerPassword = 'OwnerPass!1'; + const memberPassword = 'MemberPass!1'; + + const owner = await User.create({ email: 'owner@example.com', password: ownerPassword, isActive: true }); + const member = await User.create({ email: 'member@example.com', password: memberPassword, isActive: true }); + const club = await Club.create({ name: 'Functional Club' }); + + await UserClub.bulkCreate([ + { userId: owner.id, clubId: club.id, role: 'admin', approved: true, isOwner: true }, + { userId: member.id, clubId: club.id, role: 'member', approved: true }, + ]); + + const loginResponse = await request(app) + .post('/api/auth/login') + .send({ email: owner.email, password: ownerPassword }); + + const token = loginResponse.body.token; + expect(token).toBeTruthy(); + + const updateResponse = await request(app) + .put(`/api/permissions/${club.id}/user/${member.id}/role`) + .set('Authorization', `Bearer ${token}`) + .send({ role: 'trainer' }); + + expect(updateResponse.status).toBe(200); + const updated = await UserClub.findOne({ where: { userId: member.id, clubId: club.id } }); + expect(updated.role).toBe('trainer'); + }); + + it('verweigert Berechtigungsänderungen ohne ausreichende Rolle', async () => { + const ownerPassword = 'OwnerPass!1'; + const memberPassword = 'MemberPass!1'; + + const owner = await User.create({ email: 'owner2@example.com', password: ownerPassword, isActive: true }); + const member = await User.create({ email: 'member2@example.com', password: memberPassword, isActive: true }); + const club = await Club.create({ name: 'Restricted Club' }); + + await UserClub.bulkCreate([ + { userId: owner.id, clubId: club.id, role: 'admin', approved: true, isOwner: true }, + { userId: member.id, clubId: club.id, role: 'member', approved: true }, + ]); + + const memberLogin = await request(app) + .post('/api/auth/login') + .send({ email: member.email, password: memberPassword }); + + const token = memberLogin.body.token; + + const response = await request(app) + .put(`/api/permissions/${club.id}/user/${owner.id}/role`) + .set('Authorization', `Bearer ${token}`) + .send({ role: 'trainer' }); + + expect(response.status).toBe(403); + }); +}); diff --git a/backend/tests/authService.test.js b/backend/tests/authService.test.js new file mode 100644 index 0000000..24e8fdf --- /dev/null +++ b/backend/tests/authService.test.js @@ -0,0 +1,99 @@ +import { describe, it, expect, beforeAll, beforeEach, vi } from 'vitest'; + +vi.mock('../services/emailService.js', () => ({ + sendActivationEmail: vi.fn().mockResolvedValue(), +})); + +import sequelize from '../database.js'; +import User from '../models/User.js'; +import UserToken from '../models/UserToken.js'; +import { register, activateUser, login, logout } from '../services/authService.js'; + +describe('authService', () => { + beforeAll(async () => { + await sequelize.sync({ force: true }); + }); + + beforeEach(async () => { + await sequelize.truncate({ cascade: true, restartIdentity: true }); + }); + + it('registriert einen neuen Nutzer und sendet eine Aktivierungs-E-Mail', async () => { + const email = 'unit@test.de'; + const password = 'Test123!'; + + const user = await register(email, password); + + expect(user).toBeTruthy(); + const storedUser = await User.findOne({ where: { email } }); + expect(storedUser).not.toBeNull(); + expect(storedUser.password).not.toBe(password); + expect(storedUser.activationCode).toBeTruthy(); + }); + + it('aktiviert einen Benutzer mit gültigem Aktivierungscode', async () => { + const email = 'activate@test.de'; + const password = 'Test123!'; + const user = await register(email, password); + + const activated = await activateUser(user.activationCode); + + expect(activated.isActive).toBe(true); + expect(activated.activationCode).toBeNull(); + }); + + it('meldet einen Fehler bei ungültigem Aktivierungscode', async () => { + await expect(activateUser('does-not-exist')).rejects.toMatchObject({ status: 404 }); + }); + + it('meldet einen Benutzer an und speichert den Token', async () => { + const email = 'login@test.de'; + const password = 'Passwort!7'; + const user = await register(email, password); + await user.update({ isActive: true }); + + const result = await login(email, password); + + expect(result.token).toBeTruthy(); + const tokenRecord = await UserToken.findOne({ where: { token: result.token } }); + expect(tokenRecord).not.toBeNull(); + }); + + it('verhindert Login solange der Account nicht aktiviert wurde', async () => { + const email = 'inactive@test.de'; + const password = 'Test123!'; + await register(email, password); + + await expect(login(email, password)).rejects.toMatchObject({ status: 403 }); + }); + + it('verhindert doppelte Registrierung', async () => { + const email = 'duplicate@test.de'; + const password = 'Test123!'; + await register(email, password); + + await expect(register(email, password)).rejects.toMatchObject({ status: 409 }); + }); + + it('wirft einen Fehler bei ungültigen Anmeldedaten', async () => { + await expect(login('unknown@test.de', 'wrong')).rejects.toMatchObject({ status: 401 }); + }); + + it('löscht den UserToken beim Logout', async () => { + const email = 'logout@test.de'; + const password = 'Passwort!8'; + const user = await register(email, password); + await user.update({ isActive: true }); + const { token } = await login(email, password); + + const response = await logout(token); + + expect(response).toMatchObject({ message: 'Logout erfolgreich' }); + const tokenRecord = await UserToken.findOne({ where: { token } }); + expect(tokenRecord).toBeNull(); + }); + + it('meldet Fehler beim Logout ohne Token', async () => { + await expect(logout()).rejects.toMatchObject({ status: 400 }); + }); +}); diff --git a/backend/tests/authorizationMiddleware.test.js b/backend/tests/authorizationMiddleware.test.js new file mode 100644 index 0000000..dbed86b --- /dev/null +++ b/backend/tests/authorizationMiddleware.test.js @@ -0,0 +1,68 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; + +import { authorize } from '../middleware/authorizationMiddleware.js'; +import sequelize from '../database.js'; +import User from '../models/User.js'; +import Club from '../models/Club.js'; +import UserClub from '../models/UserClub.js'; + +const createRes = () => { + const res = {}; + res.status = vi.fn().mockReturnValue(res); + res.json = vi.fn().mockReturnValue(res); + return res; +}; + +describe('authorization middleware', () => { + beforeEach(async () => { + await sequelize.sync({ force: true }); + }); + + it('gibt 400 zurück wenn clubId fehlt', async () => { + const req = { user: { id: 1 }, params: {}, body: {}, query: {} }; + const res = createRes(); + const next = vi.fn(); + + await authorize('members', 'read')(req, res, next); + + expect(res.status).toHaveBeenCalledWith(400); + expect(next).not.toHaveBeenCalled(); + }); + + it('verweigert Zugriff ohne Berechtigungen', async () => { + const owner = await User.create({ email: 'owner-mw@example.com', password: 'Secret!123', isActive: true }); + const member = await User.create({ email: 'member-mw@example.com', password: 'Secret!123', isActive: true }); + const club = await Club.create({ name: 'Middleware Club' }); + + await UserClub.bulkCreate([ + { userId: owner.id, clubId: club.id, role: 'admin', approved: true, isOwner: true }, + { userId: member.id, clubId: club.id, role: 'member', approved: true }, + ]); + + const req = { user: { id: member.id }, params: { clubId: club.id }, body: {}, query: {} }; + const res = createRes(); + const next = vi.fn(); + + await authorize('permissions', 'write')(req, res, next); + + expect(res.status).toHaveBeenCalledWith(403); + expect(next).not.toHaveBeenCalled(); + }); + + it('erlaubt Besitzern den Zugriff', async () => { + const owner = await User.create({ email: 'owner-pass@example.com', password: 'Secret!123', isActive: true }); + const club = await Club.create({ name: 'Owner Club' }); + + await UserClub.create({ userId: owner.id, clubId: club.id, role: 'admin', approved: true, isOwner: true }); + + const req = { user: { id: owner.id }, params: { clubId: club.id }, body: {}, query: {} }; + const res = createRes(); + const next = vi.fn(); + + await authorize('permissions', 'write')(req, res, next); + + expect(next).toHaveBeenCalledOnce(); + expect(res.status).not.toHaveBeenCalled(); + expect(req.userPermissions).toBeTruthy(); + }); +}); diff --git a/backend/tests/permissionRoutes.test.js b/backend/tests/permissionRoutes.test.js new file mode 100644 index 0000000..19387be --- /dev/null +++ b/backend/tests/permissionRoutes.test.js @@ -0,0 +1,109 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import request from 'supertest'; + +vi.mock('../services/emailService.js', () => ({ + sendActivationEmail: vi.fn().mockResolvedValue(), +})); + +import app from './testApp.js'; +import sequelize from '../database.js'; +import User from '../models/User.js'; +import Club from '../models/Club.js'; +import UserClub from '../models/UserClub.js'; + +const registerAndActivate = async (email) => { + const password = 'Test123!'; + await request(app).post('/api/auth/register').send({ email, password }); + const user = await User.findOne({ where: { email } }); + await user.update({ isActive: true }); + return { user, credentials: { email, password } }; +}; + +const loginAndGetToken = async (credentials) => { + const response = await request(app).post('/api/auth/login').send(credentials); + return response.body.token; +}; + +describe('Permission Routes', () => { + beforeEach(async () => { + vi.clearAllMocks(); + await sequelize.sync({ force: true }); + }); + + it('liefert die verfügbaren Rollen für authentifizierte Nutzer', async () => { + const { credentials } = await registerAndActivate('roles@example.com'); + const token = await loginAndGetToken(credentials); + + const response = await request(app) + .get('/api/permissions/roles/available') + .set('Authorization', `Bearer ${token}`); + + expect(response.status).toBe(200); + expect(Array.isArray(response.body)).toBe(true); + expect(response.body.length).toBeGreaterThan(0); + }); + + it('gibt Club-Mitglieder und Berechtigungen für Administratoren zurück', async () => { + const { user: owner, credentials } = await registerAndActivate('owner@example.com'); + const { user: member } = await registerAndActivate('member@example.com'); + const club = await Club.create({ name: 'Permissions Club' }); + + await UserClub.bulkCreate([ + { userId: owner.id, clubId: club.id, role: 'admin', approved: true, isOwner: true }, + { userId: member.id, clubId: club.id, role: 'member', approved: true }, + ]); + + const token = await loginAndGetToken(credentials); + + const response = await request(app) + .get(`/api/permissions/${club.id}/members`) + .set('Authorization', `Bearer ${token}`); + + expect(response.status).toBe(200); + expect(response.body).toHaveLength(2); + const memberEntry = response.body.find((entry) => entry.userId === member.id); + expect(memberEntry).toBeTruthy(); + expect(memberEntry.role).toBe('member'); + }); + + it('verweigert den Zugriff auf die Mitgliederliste für einfache Mitglieder', async () => { + const { user: owner } = await registerAndActivate('owner2@example.com'); + const { user: member, credentials } = await registerAndActivate('member2@example.com'); + const club = await Club.create({ name: 'Restricted Club' }); + + await UserClub.bulkCreate([ + { userId: owner.id, clubId: club.id, role: 'admin', approved: true, isOwner: true }, + { userId: member.id, clubId: club.id, role: 'member', approved: true }, + ]); + + const token = await loginAndGetToken(credentials); + + const response = await request(app) + .get(`/api/permissions/${club.id}/members`) + .set('Authorization', `Bearer ${token}`); + + expect(response.status).toBe(403); + }); + + it('ändert den Genehmigungsstatus eines Mitglieds über die API', async () => { + const { user: owner, credentials } = await registerAndActivate('owner3@example.com'); + const { user: member } = await registerAndActivate('member3@example.com'); + const club = await Club.create({ name: 'Status Club' }); + + await UserClub.bulkCreate([ + { userId: owner.id, clubId: club.id, role: 'admin', approved: true, isOwner: true }, + { userId: member.id, clubId: club.id, role: 'member', approved: true }, + ]); + + const token = await loginAndGetToken(credentials); + + const response = await request(app) + .put(`/api/permissions/${club.id}/user/${member.id}/status`) + .set('Authorization', `Bearer ${token}`) + .send({ approved: false }); + + expect(response.status).toBe(200); + const updated = await UserClub.findOne({ where: { userId: member.id, clubId: club.id } }); + expect(updated.approved).toBe(false); + }); +}); diff --git a/backend/tests/permissionService.test.js b/backend/tests/permissionService.test.js new file mode 100644 index 0000000..1112a23 --- /dev/null +++ b/backend/tests/permissionService.test.js @@ -0,0 +1,128 @@ +import { describe, it, expect, beforeAll, beforeEach } from 'vitest'; + +import sequelize from '../database.js'; +import User from '../models/User.js'; +import Club from '../models/Club.js'; +import UserClub from '../models/UserClub.js'; +import permissionService from '../services/permissionService.js'; + +async function createUser(email, password = 'Secret!123', overrides = {}) { + return User.create({ email, password, isActive: true, ...overrides }); +} + +async function createClub(name = 'Testclub') { + return Club.create({ name }); +} + +describe('permissionService', () => { + beforeAll(async () => { + await sequelize.sync({ force: true }); + }); + + beforeEach(async () => { + await sequelize.truncate({ cascade: true, restartIdentity: true }); + }); + + it('liefert Admin-Rechte für Club-Eigentümer', async () => { + const owner = await createUser('owner@test.de'); + const club = await createClub('Admin Club'); + + await UserClub.create({ + userId: owner.id, + clubId: club.id, + role: 'admin', + approved: true, + isOwner: true, + }); + + const permissions = await permissionService.getUserClubPermissions(owner.id, club.id); + expect(permissions).toMatchObject({ isOwner: true, role: 'admin' }); + expect(permissions.permissions.permissions.write).toBe(true); + }); + + it('verhindert Rollenänderung ohne Berechtigung', async () => { + const owner = await createUser('owner@test.de'); + const member = await createUser('member@test.de'); + const club = await createClub('Club'); + + await UserClub.bulkCreate([ + { userId: owner.id, clubId: club.id, role: 'admin', approved: true, isOwner: true }, + { userId: member.id, clubId: club.id, role: 'member', approved: true }, + ]); + + await expect( + permissionService.setUserRole(owner.id, club.id, 'trainer', member.id) + ).rejects.toThrow('Keine Berechtigung zum Ändern von Rollen'); + }); + + it('erlaubt Administratoren die Rollenänderung', async () => { + const owner = await createUser('owner@test.de'); + const admin = await createUser('admin@test.de'); + const member = await createUser('member@test.de'); + const club = await createClub('Club'); + + await UserClub.bulkCreate([ + { userId: owner.id, clubId: club.id, role: 'admin', approved: true, isOwner: true }, + { userId: admin.id, clubId: club.id, role: 'admin', approved: true }, + { userId: member.id, clubId: club.id, role: 'member', approved: true }, + ]); + + await permissionService.setUserRole(member.id, club.id, 'trainer', admin.id); + + const updated = await UserClub.findOne({ where: { userId: member.id, clubId: club.id } }); + expect(updated.role).toBe('trainer'); + }); + + it('erlaubt Custom-Permissions und fasst sie zusammen', async () => { + const owner = await createUser('owner@test.de'); + const member = await createUser('member@test.de'); + const club = await createClub('Club'); + + await UserClub.bulkCreate([ + { userId: owner.id, clubId: club.id, role: 'admin', approved: true, isOwner: true }, + { userId: member.id, clubId: club.id, role: 'member', approved: true }, + ]); + + await permissionService.setCustomPermissions( + member.id, + club.id, + { members: { read: true, write: true } }, + owner.id + ); + + const permissions = await permissionService.getUserClubPermissions(member.id, club.id); + expect(permissions.permissions.members.write).toBe(true); + }); + + it('liefert Fehlermeldung wenn Berechtigungsübersicht ohne Rechte angefordert wird', async () => { + const owner = await createUser('owner@test.de'); + const member = await createUser('member@test.de'); + const club = await createClub('Club'); + + await UserClub.bulkCreate([ + { userId: owner.id, clubId: club.id, role: 'admin', approved: true, isOwner: true }, + { userId: member.id, clubId: club.id, role: 'member', approved: true }, + ]); + + await expect( + permissionService.getClubMembersWithPermissions(club.id, member.id) + ).rejects.toThrow('Keine Berechtigung zum Anzeigen von Berechtigungen'); + }); + + it('liefert Mitgliederliste mit effektiven Berechtigungen für Administratoren', async () => { + const owner = await createUser('owner@test.de'); + const member = await createUser('member@test.de'); + const club = await createClub('Club'); + + await UserClub.bulkCreate([ + { userId: owner.id, clubId: club.id, role: 'admin', approved: true, isOwner: true }, + { userId: member.id, clubId: club.id, role: 'member', approved: true }, + ]); + + const members = await permissionService.getClubMembersWithPermissions(club.id, owner.id); + expect(members).toHaveLength(2); + const memberEntry = members.find((entry) => entry.userId === member.id); + expect(memberEntry).toBeTruthy(); + expect(memberEntry.effectivePermissions.statistics.read).toBe(true); + }); +}); diff --git a/backend/tests/setupTestEnv.js b/backend/tests/setupTestEnv.js new file mode 100644 index 0000000..dec1678 --- /dev/null +++ b/backend/tests/setupTestEnv.js @@ -0,0 +1,13 @@ +import { afterAll } from 'vitest'; +import sequelize from '../database.js'; + +process.env.NODE_ENV = process.env.NODE_ENV || 'test'; +process.env.DB_DIALECT = process.env.DB_DIALECT || 'sqlite'; +process.env.DB_STORAGE = process.env.DB_STORAGE || ':memory:'; +process.env.JWT_SECRET = process.env.JWT_SECRET || 'test-secret'; +process.env.EMAIL_USER = process.env.EMAIL_USER || 'noreply@example.com'; +process.env.BASE_URL = process.env.BASE_URL || 'http://localhost:3000'; + +afterAll(async () => { + await sequelize.close(); +}); diff --git a/backend/tests/testApp.js b/backend/tests/testApp.js new file mode 100644 index 0000000..b6e1f98 --- /dev/null +++ b/backend/tests/testApp.js @@ -0,0 +1,17 @@ +import express from 'express'; +import authRoutes from '../routes/authRoutes.js'; +import permissionRoutes from '../routes/permissionRoutes.js'; + +const app = express(); + +app.use(express.json()); +app.use('/api/auth', authRoutes); +app.use('/api/permissions', permissionRoutes); + +app.use((err, req, res, next) => { + const status = err?.status || err?.statusCode || 500; + const message = err?.message || 'Interner Serverfehler'; + res.status(status).json({ success: false, message, error: message }); +}); + +export default app; diff --git a/backend/vitest.config.js b/backend/vitest.config.js new file mode 100644 index 0000000..3609204 --- /dev/null +++ b/backend/vitest.config.js @@ -0,0 +1,14 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + setupFiles: ['./tests/setupTestEnv.js'], + include: ['tests/**/*.test.js'], + coverage: { + reporter: ['text', 'html'], + exclude: ['migrations/**', 'uploads/**', 'scripts/**'], + }, + }, +}); diff --git a/frontend/src/views/TeamManagementView.vue b/frontend/src/views/TeamManagementView.vue index a77cfeb..9e943df 100644 --- a/frontend/src/views/TeamManagementView.vue +++ b/frontend/src/views/TeamManagementView.vue @@ -346,7 +346,7 @@ import { getSafeErrorMessage, getSafeMessage } from '../utils/errorMessages.js'; import InfoDialog from '../components/InfoDialog.vue'; import ConfirmDialog from '../components/ConfirmDialog.vue'; -import { buildInfoConfig, buildConfirmConfig, safeErrorMessage } from '../utils/dialogUtils.js'; +import { buildInfoConfig, buildConfirmConfig } from '../utils/dialogUtils.js'; export default { name: 'TeamManagementView', @@ -720,7 +720,7 @@ export default { await loadTeamDocuments(); } catch (error) { console.error('Fehler beim Hochladen und Parsen der Datei:', error); - const message = safeErrorMessage(error, 'Fehler beim Hochladen und Parsen der Datei.'); + const message = getSafeErrorMessage(error, 'Fehler beim Hochladen und Parsen der Datei.'); await showInfo('Fehler', message, '', 'error'); } finally { parsingInProgress.value = false;