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;