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;