diff --git a/backend/tests/apiLogRoutes.test.js b/backend/tests/apiLogRoutes.test.js new file mode 100644 index 0000000..913dce3 --- /dev/null +++ b/backend/tests/apiLogRoutes.test.js @@ -0,0 +1,92 @@ +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 ApiLog from '../models/ApiLog.js'; +import User from '../models/User.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('API Log Routes', () => { + beforeEach(async () => { + await sequelize.sync({ force: true }); + }); + + it('listet Logs auf und filtert sie', async () => { + const { user, credentials } = await registerAndActivate('logs@example.com'); + const token = await loginAndGetToken(credentials); + await ApiLog.bulkCreate([ + { method: 'GET', path: '/foo', statusCode: 200, logType: 'api_request' }, + { method: 'POST', path: '/bar', statusCode: 500, logType: 'api_request' } + ]); + + const response = await request(app) + .get('/api/logs?method=POST') + .set('Authorization', `Bearer ${token}`); + + expect(response.status).toBe(200); + expect(response.body.success).toBe(true); + expect(response.body.data.total).toBe(1); + expect(response.body.data.logs[0].path).toBe('/bar'); + }); + + it('liefert einen Logeintrag per ID', async () => { + const { credentials } = await registerAndActivate('singlelog@example.com'); + const token = await loginAndGetToken(credentials); + const log = await ApiLog.create({ method: 'GET', path: '/single', logType: 'api_request' }); + + const response = await request(app) + .get(`/api/logs/${log.id}`) + .set('Authorization', `Bearer ${token}`); + + expect(response.status).toBe(200); + expect(response.body.data.id).toBe(log.id); + }); + + it('gibt 404 zurück, wenn ein Log nicht existiert', async () => { + const { credentials } = await registerAndActivate('missinglog@example.com'); + const token = await loginAndGetToken(credentials); + + const response = await request(app) + .get('/api/logs/999') + .set('Authorization', `Bearer ${token}`); + + expect(response.status).toBe(404); + }); + + it('liefert Scheduler-Informationen', async () => { + const { credentials } = await registerAndActivate('scheduler@example.com'); + const token = await loginAndGetToken(credentials); + await ApiLog.create({ + method: 'SCHEDULER', + path: '/scheduler/rating_updates', + statusCode: 200, + responseBody: JSON.stringify({ updatedCount: 2 }), + logType: 'scheduler', + schedulerJobType: 'rating_updates' + }); + + const response = await request(app) + .get('/api/logs/scheduler/last-executions') + .set('Authorization', `Bearer ${token}`); + + expect(response.status).toBe(200); + expect(response.body.data.rating_updates.lastRun).toBeTruthy(); + }); +}); diff --git a/backend/tests/apiLogService.test.js b/backend/tests/apiLogService.test.js new file mode 100644 index 0000000..c9cabaf --- /dev/null +++ b/backend/tests/apiLogService.test.js @@ -0,0 +1,72 @@ +import { describe, it, expect, beforeEach } from 'vitest'; + +import sequelize from '../database.js'; +import ApiLog from '../models/ApiLog.js'; +import apiLogService from '../services/apiLogService.js'; + +describe('apiLogService', () => { + beforeEach(async () => { + await sequelize.sync({ force: true }); + }); + + it('speichert API-Requests und kürzt lange Felder', async () => { + const longBody = 'x'.repeat(65000); + await apiLogService.logRequest({ + method: 'POST', + path: '/api/test', + requestBody: longBody, + responseBody: longBody, + }); + + const stored = await ApiLog.findOne({ where: { path: '/api/test' } }); + expect(stored).toBeTruthy(); + expect(stored.requestBody.length).toBeLessThanOrEqual(64020); + expect(stored.responseBody.endsWith('(truncated)')).toBe(true); + }); + + it('filtert Logs nach Methode und Status', async () => { + await ApiLog.bulkCreate([ + { method: 'GET', path: '/one', statusCode: 200, logType: 'api_request' }, + { method: 'POST', path: '/two', statusCode: 500, logType: 'api_request' } + ]); + + const result = await apiLogService.getLogs({ method: 'POST', statusCode: 500 }); + + expect(result.total).toBe(1); + expect(result.logs[0].path).toBe('/two'); + }); + + it('liefert Logs paginiert', async () => { + await ApiLog.bulkCreate(new Array(5).fill(null).map((_, idx) => ({ + method: 'GET', + path: `/paged-${idx}`, + statusCode: 200, + logType: 'api_request' + }))); + + const result = await apiLogService.getLogs({ limit: 2, offset: 2 }); + + expect(result.logs).toHaveLength(2); + expect(result.total).toBe(5); + }); + + it('gibt einen Logeintrag per ID zurück', async () => { + const log = await ApiLog.create({ method: 'GET', path: '/id-test', logType: 'api_request' }); + const fetched = await apiLogService.getLogById(log.id); + + expect(fetched).toBeTruthy(); + expect(fetched.path).toBe('/id-test'); + }); + + it('liefert Scheduler-Ausführungen aggregiert', async () => { + await apiLogService.logSchedulerExecution('rating_updates', true, { updatedCount: 3 }, 120, null); + await apiLogService.logSchedulerExecution('match_results', false, { fetchedCount: 2 }, 300, 'Timeout'); + + const results = await apiLogService.getLastSchedulerExecutions(); + + expect(results.rating_updates.lastRun).toBeTruthy(); + expect(results.rating_updates.updatedCount).toBe(3); + expect(results.match_results.success).toBe(false); + expect(results.match_results.errorMessage).toBe('Timeout'); + }); +}); diff --git a/backend/tests/clubRoutes.test.js b/backend/tests/clubRoutes.test.js new file mode 100644 index 0000000..672fc62 --- /dev/null +++ b/backend/tests/clubRoutes.test.js @@ -0,0 +1,135 @@ +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 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; +}; + +const authHeaders = (token) => ({ + Authorization: `Bearer ${token}`, + authcode: token, +}); + +describe('Club Routes', () => { + beforeEach(async () => { + await sequelize.sync({ force: true }); + }); + + it('legt einen Club an und gibt ihn in der Liste zurück', async () => { + const { credentials } = await registerAndActivate('owner@example.com'); + const token = await loginAndGetToken(credentials); + + const createResponse = await request(app) + .post('/api/clubs') + .set(authHeaders(token)) + .send({ name: 'Neuer Club' }); + + expect(createResponse.status).toBe(200); + + const listResponse = await request(app) + .get('/api/clubs') + .set(authHeaders(token)); + + expect(listResponse.status).toBe(200); + expect(listResponse.body.some((club) => club.name === 'Neuer Club')).toBe(true); + }); + + it('verhindert doppelte Club-Namen', async () => { + const { credentials } = await registerAndActivate('duplicate@example.com'); + const token = await loginAndGetToken(credentials); + + await request(app).post('/api/clubs').set(authHeaders(token)).send({ name: 'Doppelclub' }); + const response = await request(app) + .post('/api/clubs') + .set(authHeaders(token)) + .send({ name: 'Doppelclub' }); + + expect(response.status).toBe(409); + }); + + it('liefert einen Club nur für Mitglieder', async () => { + const { user: owner, credentials } = await registerAndActivate('clubowner@example.com'); + const ownerToken = await loginAndGetToken(credentials); + const createResponse = await request(app) + .post('/api/clubs') + .set(authHeaders(ownerToken)) + .send({ name: 'Private Club' }); + const clubId = createResponse.body.id; + + const ownerResponse = await request(app) + .get(`/api/clubs/${clubId}`) + .set(authHeaders(ownerToken)); + expect(ownerResponse.status).toBe(200); + + const { credentials: otherCreds } = await registerAndActivate('visitor@example.com'); + const otherToken = await loginAndGetToken(otherCreds); + const otherResponse = await request(app) + .get(`/api/clubs/${clubId}`) + .set(authHeaders(otherToken)); + expect(otherResponse.status).toBe(403); + }); + + it('bearbeitet Zugangs-Anfragen (request/pending/approve/reject)', async () => { + const { user: owner, credentials } = await registerAndActivate('owner2@example.com'); + const ownerToken = await loginAndGetToken(credentials); + const createResponse = await request(app) + .post('/api/clubs') + .set(authHeaders(ownerToken)) + .send({ name: 'Approval Club' }); + const clubId = createResponse.body.id; + + const { user: member, credentials: memberCreds } = await registerAndActivate('member@example.com'); + const memberToken = await loginAndGetToken(memberCreds); + + const requestResponse = await request(app) + .get(`/api/clubs/request/${clubId}`) + .set(authHeaders(memberToken)); + expect(requestResponse.status).toBe(200); + + const pendingResponse = await request(app) + .get(`/api/clubs/pending/${clubId}`) + .set(authHeaders(ownerToken)); + expect(pendingResponse.status).toBe(200); + expect(pendingResponse.body.length).toBe(1); + + const approveResponse = await request(app) + .post('/api/clubs/approve') + .set(authHeaders(ownerToken)) + .send({ clubid: clubId, userid: member.id }); + expect(approveResponse.status).toBe(200); + + const membership = await UserClub.findOne({ where: { userId: member.id, clubId } }); + expect(membership.approved).toBe(true); + + const rejectUser = await registerAndActivate('reject@example.com'); + const rejectToken = await loginAndGetToken(rejectUser.credentials); + await request(app).get(`/api/clubs/request/${clubId}`).set(authHeaders(rejectToken)); + + const rejectResponse = await request(app) + .post('/api/clubs/reject') + .set(authHeaders(ownerToken)) + .send({ clubid: clubId, userid: rejectUser.user.id }); + expect(rejectResponse.status).toBe(200); + + const rejectedMembership = await UserClub.findOne({ where: { userId: rejectUser.user.id, clubId } }); + expect(rejectedMembership).toBeNull(); + }); +}); diff --git a/backend/tests/clubService.test.js b/backend/tests/clubService.test.js new file mode 100644 index 0000000..adefe45 --- /dev/null +++ b/backend/tests/clubService.test.js @@ -0,0 +1,90 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; + +import sequelize from '../database.js'; +import Club from '../models/Club.js'; +import User from '../models/User.js'; +import UserClub from '../models/UserClub.js'; +import clubService from '../services/clubService.js'; + +vi.mock('../utils/userUtils.js', async () => { + const actual = await vi.importActual('../utils/userUtils.js'); + return { + ...actual, + checkAccess: vi.fn().mockResolvedValue(true) + }; +}); + +import { checkAccess } from '../utils/userUtils.js'; + +describe('clubService', () => { + beforeEach(async () => { + await sequelize.sync({ force: true }); + vi.clearAllMocks(); + }); + + it('erstellt Clubs und sucht nach Namen', async () => { + await clubService.createClub('Testclub'); + const result = await clubService.findClubByName('test'); + expect(result).toBeTruthy(); + expect(result.name).toContain('Testclub'); + }); + + it('fügt Benutzer einem Club hinzu', async () => { + const user = await User.create({ email: 'club@test.de', password: 'Secret123!', isActive: true }); + const club = await clubService.createClub('Club A'); + + const userClub = await clubService.addUserToClub(user.id, club.id, true); + + expect(userClub.isOwner).toBe(true); + expect(userClub.role).toBe('admin'); + }); + + it('beantragt Club-Zugang und verhindert doppelte Anfragen', async () => { + const user = await User.create({ email: 'member@test.de', password: 'Secret123!', isActive: true }); + const club = await clubService.createClub('Club B'); + + await clubService.requestAccessToClub(user.id, club.id); + await expect(clubService.requestAccessToClub(user.id, club.id)).rejects.toThrow('alreadyrequested'); + }); + + it('genehmigt Club-Zugänge nach erfolgreicher Prüfung', async () => { + const owner = await User.create({ email: 'owner@test.de', password: 'Secret123!', isActive: true }); + const member = await User.create({ email: 'member@test.de', password: 'Secret123!', isActive: true }); + const club = await clubService.createClub('Club C'); + await UserClub.create({ userId: owner.id, clubId: club.id, approved: true, isOwner: true }); + await UserClub.create({ userId: member.id, clubId: club.id, approved: false }); + + await clubService.approveUserClubAccess('token', club.id, member.id); + + expect(checkAccess).toHaveBeenCalledWith('token', club.id); + const updated = await UserClub.findOne({ where: { userId: member.id, clubId: club.id } }); + expect(updated.approved).toBe(true); + }); + + it('liefert ausstehende Freigaben nur bei Zugang', async () => { + const owner = await User.create({ email: 'owner@test.de', password: 'Secret123!', isActive: true }); + const pending = await User.create({ email: 'pending@test.de', password: 'Secret123!', isActive: true }); + const club = await clubService.createClub('Club D'); + await UserClub.create({ userId: owner.id, clubId: club.id, approved: true, isOwner: true }); + await UserClub.create({ userId: pending.id, clubId: club.id, approved: false }); + + const approvals = await clubService.getPendingUserApprovals('token', club.id); + + expect(checkAccess).toHaveBeenCalledWith('token', club.id); + expect(approvals).toHaveLength(1); + expect(approvals[0].userId).toBe(pending.id); + }); + + it('lehnt Club-Zugänge ab', async () => { + const owner = await User.create({ email: 'owner@test.de', password: 'Secret123!', isActive: true }); + const pending = await User.create({ email: 'pending@test.de', password: 'Secret123!', isActive: true }); + const club = await clubService.createClub('Club E'); + await UserClub.create({ userId: owner.id, clubId: club.id, approved: true, isOwner: true }); + await UserClub.create({ userId: pending.id, clubId: club.id, approved: false }); + + await clubService.rejectUserClubAccess('token', club.id, pending.id); + + const record = await UserClub.findOne({ where: { userId: pending.id, clubId: club.id } }); + expect(record).toBeNull(); + }); +}); diff --git a/backend/tests/testApp.js b/backend/tests/testApp.js index eefe168..3db8d96 100644 --- a/backend/tests/testApp.js +++ b/backend/tests/testApp.js @@ -3,6 +3,8 @@ import authRoutes from '../routes/authRoutes.js'; import permissionRoutes from '../routes/permissionRoutes.js'; import accidentRoutes from '../routes/accidentRoutes.js'; import activityRoutes from '../routes/activityRoutes.js'; +import apiLogRoutes from '../routes/apiLogRoutes.js'; +import clubRoutes from '../routes/clubRoutes.js'; const app = express(); @@ -11,6 +13,8 @@ app.use('/api/auth', authRoutes); app.use('/api/permissions', permissionRoutes); app.use('/api/accident', accidentRoutes); app.use('/api/activities', activityRoutes); +app.use('/api/logs', apiLogRoutes); +app.use('/api/clubs', clubRoutes); app.use((err, req, res, next) => { const status = err?.status || err?.statusCode || 500;