From b8191e41ee56f68ff728d8c1754252b5b8aa37ef Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Mon, 10 Nov 2025 18:29:55 +0100 Subject: [PATCH] Add club team and diary routes to the Express application Integrated new routes for club team management and diary functionality into the backend. This enhancement improves the API's structure and accessibility, allowing for better organization of related resources and expanding the application's capabilities. --- backend/tests/clubTeamRoutes.test.js | 119 +++++++++++++++++++ backend/tests/clubTeamService.test.js | 104 +++++++++++++++++ backend/tests/diaryRoutes.test.js | 158 ++++++++++++++++++++++++++ backend/tests/diaryService.test.js | 124 ++++++++++++++++++++ backend/tests/testApp.js | 4 + 5 files changed, 509 insertions(+) create mode 100644 backend/tests/clubTeamRoutes.test.js create mode 100644 backend/tests/clubTeamService.test.js create mode 100644 backend/tests/diaryRoutes.test.js create mode 100644 backend/tests/diaryService.test.js diff --git a/backend/tests/clubTeamRoutes.test.js b/backend/tests/clubTeamRoutes.test.js new file mode 100644 index 0000000..1b361e2 --- /dev/null +++ b/backend/tests/clubTeamRoutes.test.js @@ -0,0 +1,119 @@ +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 '../models/index.js'; + +import ClubTeam from '../models/ClubTeam.js'; +import League from '../models/League.js'; +import Season from '../models/Season.js'; +import Club from '../models/Club.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('ClubTeam Routes', () => { + beforeEach(async () => { + await sequelize.sync({ force: true }); + }); + + it('erstellt ClubTeams und listet sie auf', async () => { + const { user, credentials } = await registerAndActivate('clubteam@example.com'); + const token = await loginAndGetToken(credentials); + + const clubResponse = await request(app) + .post('/api/clubs') + .set(authHeaders(token)) + .send({ name: 'Team Club' }); + + const clubId = clubResponse.body.id; + + const createResponse = await request(app) + .post(`/api/clubteam/club/${clubId}`) + .set(authHeaders(token)) + .send({ name: 'Erstes Team' }); + + expect(createResponse.status).toBe(201); + + const listResponse = await request(app) + .get(`/api/clubteam/club/${clubId}`) + .set(authHeaders(token)); + + expect(listResponse.status).toBe(200); + expect(listResponse.body[0].name).toBe('Erstes Team'); + }); + + it('aktualisiert und löscht ClubTeams', async () => { + const { credentials } = await registerAndActivate('clubteam2@example.com'); + const token = await loginAndGetToken(credentials); + + const clubResponse = await request(app) + .post('/api/clubs') + .set(authHeaders(token)) + .send({ name: 'Club Zwei' }); + const clubId = clubResponse.body.id; + + const teamResponse = await request(app) + .post(`/api/clubteam/club/${clubId}`) + .set(authHeaders(token)) + .send({ name: 'Team Alt' }); + const teamId = teamResponse.body.id; + + const updateResponse = await request(app) + .put(`/api/clubteam/${teamId}`) + .set(authHeaders(token)) + .send({ name: 'Team Neu' }); + + expect(updateResponse.status).toBe(200); + expect(updateResponse.body.name).toBe('Team Neu'); + + const deleteResponse = await request(app) + .delete(`/api/clubteam/${teamId}`) + .set(authHeaders(token)); + + expect(deleteResponse.status).toBe(200); + }); + + it('liefert Ligen für einen Club', async () => { + const { credentials } = await registerAndActivate('clubteam3@example.com'); + const token = await loginAndGetToken(credentials); + + const clubResponse = await request(app) + .post('/api/clubs') + .set(authHeaders(token)) + .send({ name: 'Club Drei' }); + const clubId = clubResponse.body.id; + + const season = await Season.create({ season: '2024/2025' }); + await League.create({ name: 'Verbandsliga', clubId, seasonId: season.id, association: 'BV', groupname: 'Nord' }); + + const response = await request(app) + .get(`/api/clubteam/leagues/${clubId}`) + .set(authHeaders(token)); + + expect(response.status).toBe(200); + expect(response.body[0].name).toBe('Verbandsliga'); + }); +}); diff --git a/backend/tests/clubTeamService.test.js b/backend/tests/clubTeamService.test.js new file mode 100644 index 0000000..dc46e49 --- /dev/null +++ b/backend/tests/clubTeamService.test.js @@ -0,0 +1,104 @@ +import { describe, it, expect, beforeEach } from 'vitest'; + +import sequelize from '../database.js'; +import '../models/index.js'; + +import Club from '../models/Club.js'; +import ClubTeam from '../models/ClubTeam.js'; +import League from '../models/League.js'; +import Season from '../models/Season.js'; +import clubTeamService from '../services/clubTeamService.js'; + +describe('clubTeamService', () => { + beforeEach(async () => { + await sequelize.sync({ force: true }); + }); + + it('liefert ClubTeams mit Liga- und Saisoninformationen', async () => { + const club = await Club.create({ name: 'Testclub' }); + const season = await Season.create({ season: '2024/2025' }); + const league = await League.create({ + name: '1. Liga', + clubId: club.id, + seasonId: season.id, + association: 'BV', + groupname: 'Nord', + }); + + await ClubTeam.create({ + name: 'Team A', + clubId: club.id, + seasonId: season.id, + leagueId: league.id, + myTischtennisTeamId: 'MT-1', + }); + + const teams = await clubTeamService.getAllClubTeamsByClub(club.id, season.id); + + expect(teams).toHaveLength(1); + expect(teams[0].league.name).toBe('1. Liga'); + expect(teams[0].season.season).toBe('2024/2025'); + }); + + it('erstellt neue ClubTeams und weist die aktuelle Saison zu', async () => { + const club = await Club.create({ name: 'Neuer Club' }); + + const team = await clubTeamService.createClubTeam({ + name: 'Frisches Team', + clubId: club.id, + }); + + expect(team.id).toBeTruthy(); + expect(team.seasonId).toBeTruthy(); + }); + + it('aktualisiert ClubTeams', async () => { + const club = await Club.create({ name: 'Update Club' }); + const season = await Season.create({ season: '2023/2024' }); + const team = await ClubTeam.create({ + name: 'Team Alt', + clubId: club.id, + seasonId: season.id, + }); + + const updated = await clubTeamService.updateClubTeam(team.id, { name: 'Team Neu' }); + + expect(updated).toBe(true); + const refreshed = await ClubTeam.findByPk(team.id); + expect(refreshed.name).toBe('Team Neu'); + }); + + it('löscht ClubTeams und bestätigt die Entfernung', async () => { + const club = await Club.create({ name: 'Delete Club' }); + const season = await Season.create({ season: '2022/2023' }); + const team = await ClubTeam.create({ + name: 'Team Delete', + clubId: club.id, + seasonId: season.id, + }); + + const removed = await clubTeamService.deleteClubTeam(team.id); + + expect(removed).toBe(true); + const exists = await ClubTeam.findByPk(team.id); + expect(exists).toBeNull(); + }); + + it('liefert Ligen eines Clubs für die aktuelle Saison', async () => { + const club = await Club.create({ name: 'Liga Club' }); + const season = await Season.create({ season: '2025/2026' }); + + await League.create({ + name: 'Regionalliga', + clubId: club.id, + seasonId: season.id, + association: 'TT', + groupname: 'Süd', + }); + + const leagues = await clubTeamService.getLeaguesByClub(club.id, season.id); + + expect(leagues).toHaveLength(1); + expect(leagues[0].name).toBe('Regionalliga'); + }); +}); diff --git a/backend/tests/diaryRoutes.test.js b/backend/tests/diaryRoutes.test.js new file mode 100644 index 0000000..20a0cfe --- /dev/null +++ b/backend/tests/diaryRoutes.test.js @@ -0,0 +1,158 @@ +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 '../models/index.js'; + +import DiaryDate from '../models/DiaryDates.js'; +import DiaryDateActivity from '../models/DiaryDateActivity.js'; +import DiaryDateTag from '../models/DiaryDateTag.js'; +import { DiaryTag } from '../models/DiaryTag.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; +}; + +const authHeaders = (token) => ({ + Authorization: `Bearer ${token}`, + authcode: token, +}); + +describe('Diary Routes', () => { + beforeEach(async () => { + await sequelize.sync({ force: true }); + }); + + it('erstellt Diary-Daten und listet sie auf', async () => { + const { credentials } = await registerAndActivate('diary@example.com'); + const token = await loginAndGetToken(credentials); + + const clubResponse = await request(app) + .post('/api/clubs') + .set(authHeaders(token)) + .send({ name: 'Diary Club' }); + const clubId = clubResponse.body.id; + + const createResponse = await request(app) + .post(`/api/diary/${clubId}`) + .set(authHeaders(token)) + .send({ date: '2025-09-01', trainingStart: '18:00', trainingEnd: '20:00' }); + + expect(createResponse.status).toBe(201); + + const listResponse = await request(app) + .get(`/api/diary/${clubId}`) + .set(authHeaders(token)); + + expect(listResponse.status).toBe(200); + expect(listResponse.body).toHaveLength(1); + expect(listResponse.body[0].trainingStart).toBe('18:00:00'); + }); + + it('aktualisiert Trainingszeiten eines Diary-Eintrags', async () => { + const { credentials } = await registerAndActivate('diary2@example.com'); + const token = await loginAndGetToken(credentials); + + const clubResponse = await request(app) + .post('/api/clubs') + .set(authHeaders(token)) + .send({ name: 'Update Diary Club' }); + const clubId = clubResponse.body.id; + + const dateResponse = await request(app) + .post(`/api/diary/${clubId}`) + .set(authHeaders(token)) + .send({ date: '2025-10-01' }); + const dateId = dateResponse.body.id; + + const updateResponse = await request(app) + .put(`/api/diary/${clubId}`) + .set(authHeaders(token)) + .send({ dateId, trainingStart: '17:00', trainingEnd: '19:00' }); + + expect(updateResponse.status).toBe(200); + expect(updateResponse.body.trainingStart).toBe('17:00:00'); + }); + + it('verhindert das Löschen bei vorhandenen Aktivitäten', async () => { + const { credentials } = await registerAndActivate('diary3@example.com'); + const token = await loginAndGetToken(credentials); + + const clubResponse = await request(app) + .post('/api/clubs') + .set(authHeaders(token)) + .send({ name: 'Activity Diary Club' }); + const clubId = clubResponse.body.id; + + const dateResponse = await request(app) + .post(`/api/diary/${clubId}`) + .set(authHeaders(token)) + .send({ date: '2025-11-01' }); + const dateId = dateResponse.body.id; + + await DiaryDateActivity.create({ diaryDateId: dateId, orderId: 1, isTimeblock: false }); + + const deleteResponse = await request(app) + .delete(`/api/diary/${clubId}/${dateId}`) + .set(authHeaders(token)); + + expect(deleteResponse.status).toBe(409); + expect(deleteResponse.body.error).toBe('Cannot delete date with activities'); + }); + + it('verwaltet Tags über die Diary-API', async () => { + const { credentials } = await registerAndActivate('diary4@example.com'); + const token = await loginAndGetToken(credentials); + + const clubResponse = await request(app) + .post('/api/clubs') + .set(authHeaders(token)) + .send({ name: 'Tag Diary Club' }); + const clubId = clubResponse.body.id; + + const dateResponse = await request(app) + .post(`/api/diary/${clubId}`) + .set(authHeaders(token)) + .send({ date: '2025-12-01' }); + const dateId = dateResponse.body.id; + + const createTagResponse = await request(app) + .post('/api/diary/tag') + .set(authHeaders(token)) + .send({ clubId, diaryDateId: dateId, tagName: 'Ausdauer' }); + + expect(createTagResponse.status).toBe(201); + const tagId = createTagResponse.body[0].id; + + const linkResponse = await request(app) + .post(`/api/diary/tag/${clubId}/add-tag`) + .set(authHeaders(token)) + .send({ diaryDateId: dateId, tagId }); + + expect(linkResponse.status).toBe(200); + + const deleteResponse = await request(app) + .delete(`/api/diary/${clubId}/tag`) + .set(authHeaders(token)) + .query({ tagId }); + + expect(deleteResponse.status).toBe(200); + const remaining = await DiaryDateTag.findAll({ where: { diaryDateId: dateId } }); + expect(remaining).toHaveLength(0); + }); +}); diff --git a/backend/tests/diaryService.test.js b/backend/tests/diaryService.test.js new file mode 100644 index 0000000..029f984 --- /dev/null +++ b/backend/tests/diaryService.test.js @@ -0,0 +1,124 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; + +vi.mock('../utils/userUtils.js', async () => { + const actual = await vi.importActual('../utils/userUtils.js'); + return { + ...actual, + checkAccess: vi.fn().mockResolvedValue(true), + }; +}); + +import sequelize from '../database.js'; +import '../models/index.js'; + +import diaryService from '../services/diaryService.js'; +import Club from '../models/Club.js'; +import DiaryDate from '../models/DiaryDates.js'; +import DiaryDateActivity from '../models/DiaryDateActivity.js'; +import DiaryDateTag from '../models/DiaryDateTag.js'; +import { DiaryTag } from '../models/DiaryTag.js'; +import { checkAccess } from '../utils/userUtils.js'; + +describe('diaryService', () => { + beforeEach(async () => { + await sequelize.sync({ force: true }); + vi.clearAllMocks(); + }); + + it('liefert alle Diary-Daten eines Clubs', async () => { + const club = await Club.create({ name: 'Diary Club' }); + await DiaryDate.bulkCreate([ + { date: '2025-01-01', clubId: club.id, trainingStart: '18:00', trainingEnd: '20:00' }, + { date: '2025-01-05', clubId: club.id, trainingStart: '19:00', trainingEnd: '21:00' }, + ]); + + const dates = await diaryService.getDatesForClub('token', club.id); + + expect(checkAccess).toHaveBeenCalledWith('token', club.id); + expect(dates).toHaveLength(2); + expect(dates[0].clubId).toBe(club.id); + }); + + it('erstellt neue Diary-Daten und validiert Zeiten', async () => { + const club = await Club.create({ name: 'Create Club' }); + + const created = await diaryService.createDateForClub('token', club.id, '2025-02-02', '18:00', '20:00'); + + expect(created.id).toBeTruthy(); + expect(created.trainingStart).toBe('18:00:00'); + + await expect( + diaryService.createDateForClub('token', club.id, '2025-02-03', '20:00', '19:00') + ).rejects.toThrow('Training start time must be before training end time'); + }); + + it('aktualisiert Trainingszeiten und wirft Fehler bei fehlendem Eintrag', async () => { + const club = await Club.create({ name: 'Update Club' }); + const date = await DiaryDate.create({ date: '2025-03-01', clubId: club.id }); + + const updated = await diaryService.updateTrainingTimes('token', club.id, date.id, '17:00', '19:00'); + expect(updated.trainingStart).toBe('17:00:00'); + + await expect( + diaryService.updateTrainingTimes('token', club.id, 9999, '10:00', '12:00') + ).rejects.toThrow('Diary entry not found'); + }); + + it('fügt Tags über Namen hinzu und liefert zugehörige Tag-Liste', async () => { + const club = await Club.create({ name: 'Tag Club' }); + const date = await DiaryDate.create({ date: '2025-04-01', clubId: club.id }); + + const tags = await diaryService.addTagToDate('token', date.id, 'Intensiv'); + + expect(checkAccess).toHaveBeenCalledWith('token', date.id); + expect(tags.length).toBe(1); + expect(tags[0].name).toBe('Intensiv'); + }); + + it('verknüpft bestehende Tags mit Diary-Daten', async () => { + const club = await Club.create({ name: 'Tag Link Club' }); + const date = await DiaryDate.create({ date: '2025-05-01', clubId: club.id }); + const tag = await DiaryTag.create({ name: 'Technik' }); + + const result = await diaryService.addTagToDiaryDate('token', club.id, date.id, tag.id); + + expect(result).toHaveLength(1); + expect(result[0].name).toBe('Technik'); + + const second = await diaryService.addTagToDiaryDate('token', club.id, date.id, tag.id); + expect(second).toBeUndefined(); + }); + + it('entfernt Tags von Diary-Daten', async () => { + const club = await Club.create({ name: 'Remove Tag Club' }); + const date = await DiaryDate.create({ date: '2025-06-01', clubId: club.id }); + const tag = await DiaryTag.create({ name: 'Kondition' }); + await DiaryDateTag.create({ diaryDateId: date.id, tagId: tag.id }); + + await diaryService.removeTagFromDiaryDate('token', club.id, tag.id); + + const remaining = await DiaryDateTag.findAll({ where: { diaryDateId: date.id } }); + expect(remaining).toHaveLength(0); + }); + + it('verhindert das Löschen bei vorhandenen Aktivitäten', async () => { + const club = await Club.create({ name: 'Activity Club' }); + const date = await DiaryDate.create({ date: '2025-07-01', clubId: club.id }); + await DiaryDateActivity.create({ diaryDateId: date.id, orderId: 1, isTimeblock: false }); + + await expect( + diaryService.removeDateForClub('token', club.id, date.id) + ).rejects.toThrow('Cannot delete date with activities'); + }); + + it('löscht Diary-Daten ohne Aktivitäten', async () => { + const club = await Club.create({ name: 'Delete Diary Club' }); + const date = await DiaryDate.create({ date: '2025-08-01', clubId: club.id }); + + const result = await diaryService.removeDateForClub('token', club.id, date.id); + + expect(result).toEqual({ ok: true }); + const exists = await DiaryDate.findByPk(date.id); + expect(exists).toBeNull(); + }); +}); diff --git a/backend/tests/testApp.js b/backend/tests/testApp.js index 3db8d96..3b63435 100644 --- a/backend/tests/testApp.js +++ b/backend/tests/testApp.js @@ -5,6 +5,8 @@ import accidentRoutes from '../routes/accidentRoutes.js'; import activityRoutes from '../routes/activityRoutes.js'; import apiLogRoutes from '../routes/apiLogRoutes.js'; import clubRoutes from '../routes/clubRoutes.js'; +import clubTeamRoutes from '../routes/clubTeamRoutes.js'; +import diaryRoutes from '../routes/diaryRoutes.js'; const app = express(); @@ -15,6 +17,8 @@ app.use('/api/accident', accidentRoutes); app.use('/api/activities', activityRoutes); app.use('/api/logs', apiLogRoutes); app.use('/api/clubs', clubRoutes); +app.use('/api/clubteam', clubTeamRoutes); +app.use('/api/diary', diaryRoutes); app.use((err, req, res, next) => { const status = err?.status || err?.statusCode || 500;