From 6c3b46c037a4aa50d82387e7d53eb85f0871dbe4 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Mon, 10 Nov 2025 17:01:43 +0100 Subject: [PATCH] Add accident and activity routes to the Express application Enhanced the backend by integrating new routes for accident and activity management. This addition improves the API's functionality, allowing for better organization and access to related resources. --- backend/tests/accidentRoutes.test.js | 65 ++++++++++++++++++++++++ backend/tests/accidentService.test.js | 63 +++++++++++++++++++++++ backend/tests/activityController.test.js | 63 +++++++++++++++++++++++ backend/tests/activityService.test.js | 39 ++++++++++++++ backend/tests/testApp.js | 4 ++ backend/tests/testUtils.js | 16 ++++++ 6 files changed, 250 insertions(+) create mode 100644 backend/tests/accidentRoutes.test.js create mode 100644 backend/tests/accidentService.test.js create mode 100644 backend/tests/activityController.test.js create mode 100644 backend/tests/activityService.test.js create mode 100644 backend/tests/testUtils.js diff --git a/backend/tests/accidentRoutes.test.js b/backend/tests/accidentRoutes.test.js new file mode 100644 index 0000000..5355fca --- /dev/null +++ b/backend/tests/accidentRoutes.test.js @@ -0,0 +1,65 @@ +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 Member from '../models/Member.js'; +import DiaryDate from '../models/DiaryDates.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('Accident Routes', () => { + beforeEach(async () => { + await sequelize.sync({ force: true }); + }); + + it('legt einen Unfall an und gibt ihn zurück', async () => { + const { user, credentials } = await registerAndActivate('accident@example.com'); + const token = await loginAndGetToken(credentials); + const club = await Club.create({ name: 'Accident Club' }); + await UserClub.create({ userId: user.id, clubId: club.id, role: 'admin', approved: true, isOwner: true }); + const member = await Member.create({ firstName: 'Anna', lastName: 'Accident', clubId: club.id }); + const diaryDate = await DiaryDate.create({ clubId: club.id, date: new Date(), description: 'Training' }); + + const createResponse = await request(app) + .post('/api/accident/add') + .set('Authorization', `Bearer ${token}`) + .send({ clubId: club.id, memberId: member.id, diaryDateId: diaryDate.id, accident: 'Verletzung' }); + + expect(createResponse.status).toBe(201); + + const listResponse = await request(app) + .get(`/api/accident/${club.id}/${diaryDate.id}`) + .set('Authorization', `Bearer ${token}`); + + expect(listResponse.status).toBe(200); + expect(listResponse.body).toHaveLength(1); + expect(listResponse.body[0]).toMatchObject({ firstName: 'Anna' }); + }); + + it('verhindert Anlage eines Unfalls ohne Authentifizierung', async () => { + const response = await request(app) + .post('/api/accident/add') + .send({ clubId: 1, memberId: 1, diaryDateId: 1, accident: 'Test' }); + + expect(response.status).toBe(401); + }); +}); diff --git a/backend/tests/accidentService.test.js b/backend/tests/accidentService.test.js new file mode 100644 index 0000000..9bb21e1 --- /dev/null +++ b/backend/tests/accidentService.test.js @@ -0,0 +1,63 @@ +import { describe, it, expect, beforeEach } from 'vitest'; + +import sequelize from '../database.js'; +import Accident from '../models/Accident.js'; +import DiaryDate from '../models/DiaryDates.js'; +import Member from '../models/Member.js'; +import User from '../models/User.js'; +import Club from '../models/Club.js'; +import UserClub from '../models/UserClub.js'; +import accidentService from '../services/accidentService.js'; + +vi.mock('../utils/userUtils.js', async () => { + const actual = await vi.importActual('../utils/userUtils.js'); + return { + ...actual, + checkAccess: vi.fn().mockResolvedValue(true), + getUserByToken: vi.fn().mockResolvedValue({ id: 1 }), + }; +}); + +describe('accidentService', () => { + const token = 'test-token'; + + beforeEach(async () => { + await sequelize.sync({ force: true }); + }); + + it('legt einen Unfall-Eintrag an, wenn alle Referenzen vorhanden sind', async () => { + const club = await Club.create({ name: 'Accident Club' }); + const member = await Member.create({ firstName: 'Alice', lastName: 'Accident', clubId: club.id }); + const diaryDate = await DiaryDate.create({ clubId: club.id, date: new Date(), description: 'Training' }); + + const result = await accidentService.createAccident(token, club.id, member.id, diaryDate.id, 'Verstauchung'); + + expect(result).toBeTruthy(); + const stored = await Accident.findOne({ where: { diaryDateId: diaryDate.id } }); + expect(stored).not.toBeNull(); + expect(stored.accident).toBe('Verstauchung'); + }); + + it('wirft Fehler, wenn Member nicht im selben Club ist', async () => { + const club = await Club.create({ name: 'Club A' }); + const otherClub = await Club.create({ name: 'Club B' }); + const member = await Member.create({ firstName: 'Bob', lastName: 'B', clubId: otherClub.id }); + const diaryDate = await DiaryDate.create({ clubId: club.id, date: new Date(), description: 'Training' }); + + await expect( + accidentService.createAccident(token, club.id, member.id, diaryDate.id, 'Sturz') + ).rejects.toThrow('Member not found'); + }); + + it('liefert Unfälle samt Mitgliedern zurück', async () => { + const club = await Club.create({ name: 'Club A' }); + const member = await Member.create({ firstName: 'Clara', lastName: 'Club', clubId: club.id }); + const diaryDate = await DiaryDate.create({ clubId: club.id, date: new Date(), description: 'Training' }); + await Accident.create({ memberId: member.id, diaryDateId: diaryDate.id, accident: 'Verstauchung' }); + + const accidents = await accidentService.getAccidents(token, club.id, diaryDate.id); + + expect(accidents).toHaveLength(1); + expect(accidents[0]).toMatchObject({ firstName: 'Clara', accident: 'Verstauchung' }); + }); +}); diff --git a/backend/tests/activityController.test.js b/backend/tests/activityController.test.js new file mode 100644 index 0000000..a34c49c --- /dev/null +++ b/backend/tests/activityController.test.js @@ -0,0 +1,63 @@ +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 DiaryDate from '../models/DiaryDates.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('Activity Routes', () => { + beforeEach(async () => { + await sequelize.sync({ force: true }); + }); + + it('erstellt und liest Aktivitäten', async () => { + const { user, credentials } = await registerAndActivate('activity@example.com'); + const token = await loginAndGetToken(credentials); + const club = await Club.create({ name: 'Activity Club' }); + await UserClub.create({ userId: user.id, clubId: club.id, role: 'admin', approved: true, isOwner: true }); + const diaryDate = await DiaryDate.create({ clubId: club.id, date: new Date(), description: 'Training' }); + + const createResponse = await request(app) + .post('/api/activities/add') + .set('Authorization', `Bearer ${token}`) + .send({ diaryDateId: diaryDate.id, description: 'Koordinationsübungen' }); + + expect(createResponse.status).toBe(201); + + const listResponse = await request(app) + .get(`/api/activities/${diaryDate.id}`) + .set('Authorization', `Bearer ${token}`); + + expect(listResponse.status).toBe(200); + expect(listResponse.body).toHaveLength(1); + expect(listResponse.body[0]).toMatchObject({ description: 'Koordinationsübungen' }); + }); + + it('verweigert Anlage einer Aktivität ohne Token', async () => { + const response = await request(app) + .post('/api/activities/add') + .send({ diaryDateId: 1, description: 'Test' }); + + expect(response.status).toBe(401); + }); +}); diff --git a/backend/tests/activityService.test.js b/backend/tests/activityService.test.js new file mode 100644 index 0000000..e5fc81f --- /dev/null +++ b/backend/tests/activityService.test.js @@ -0,0 +1,39 @@ +import { describe, it, expect, beforeEach } from 'vitest'; + +import sequelize from '../database.js'; +import Activity from '../models/Activity.js'; +import DiaryDate from '../models/DiaryDates.js'; +import activityController from '../controllers/activityController.js'; + +import { buildMockRequest, buildMockResponse } from './testUtils.js'; + +describe('activityController', () => { + beforeEach(async () => { + await sequelize.sync({ force: true }); + }); + + it('fügt eine Aktivität hinzu', async () => { + const diaryDate = await DiaryDate.create({ clubId: 1, date: new Date(), description: 'Training' }); + const req = buildMockRequest({ body: { diaryDateId: diaryDate.id, description: 'Koordination' } }); + const res = buildMockResponse(); + + await activityController.addActivity(req, res); + + expect(res.status).toHaveBeenCalledWith(201); + const stored = await Activity.findOne({ where: { diaryDateId: diaryDate.id } }); + expect(stored.description).toBe('Koordination'); + }); + + it('liefert Aktivitäten für einen Trainingstag', async () => { + const diaryDate = await DiaryDate.create({ clubId: 1, date: new Date(), description: 'Training' }); + await Activity.create({ diaryDateId: diaryDate.id, description: 'Aufwärmen' }); + + const req = buildMockRequest({ params: { diaryDateId: diaryDate.id } }); + const res = buildMockResponse(); + + await activityController.getActivities(req, res); + + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json.mock.calls[0][0]).toHaveLength(1); + }); +}); diff --git a/backend/tests/testApp.js b/backend/tests/testApp.js index b6e1f98..eefe168 100644 --- a/backend/tests/testApp.js +++ b/backend/tests/testApp.js @@ -1,12 +1,16 @@ import express from 'express'; import authRoutes from '../routes/authRoutes.js'; import permissionRoutes from '../routes/permissionRoutes.js'; +import accidentRoutes from '../routes/accidentRoutes.js'; +import activityRoutes from '../routes/activityRoutes.js'; const app = express(); app.use(express.json()); app.use('/api/auth', authRoutes); app.use('/api/permissions', permissionRoutes); +app.use('/api/accident', accidentRoutes); +app.use('/api/activities', activityRoutes); app.use((err, req, res, next) => { const status = err?.status || err?.statusCode || 500; diff --git a/backend/tests/testUtils.js b/backend/tests/testUtils.js new file mode 100644 index 0000000..8c74925 --- /dev/null +++ b/backend/tests/testUtils.js @@ -0,0 +1,16 @@ +import { vi } from 'vitest'; + +export function buildMockRequest({ body = {}, params = {}, headers = {} } = {}) { + return { + body, + params, + headers, + }; +} + +export function buildMockResponse() { + const res = {}; + res.status = vi.fn().mockReturnValue(res); + res.json = vi.fn().mockReturnValue(res); + return res; +}