diff --git a/backend/controllers/diaryNoteController.js b/backend/controllers/diaryNoteController.js index dee309c..590d1a0 100644 --- a/backend/controllers/diaryNoteController.js +++ b/backend/controllers/diaryNoteController.js @@ -18,16 +18,24 @@ export const getNotes = async (req, res) => { export const createNote = async (req, res) => { try { - const { memberId, content, tags } = req.body; - const newNote = await DiaryNote.create({ memberId, content }); - if (tags && tags.length > 0) { + const { memberId, diaryDateId, content, tags } = req.body; + + if (!memberId || !diaryDateId || !content) { + return res.status(400).json({ error: 'memberId, diaryDateId und content sind erforderlich.' }); + } + + const newNote = await DiaryNote.create({ memberId, diaryDateId, content }); + + if (Array.isArray(tags) && tags.length > 0 && typeof newNote.addTags === 'function') { const tagInstances = await DiaryTag.findAll({ where: { id: tags } }); await newNote.addTags(tagInstances); } + const noteWithTags = await DiaryNote.findByPk(newNote.id, { - include: [{ model: DiaryTag, as: 'tags' }], + include: [{ model: DiaryTag, as: 'tags', required: false }], }); - res.status(201).json(noteWithTags); + + res.status(201).json(noteWithTags ?? newNote); } catch (error) { res.status(500).json({ error: 'Error creating note' }); } diff --git a/backend/controllers/diaryTagController.js b/backend/controllers/diaryTagController.js index 422e6b7..c0249d5 100644 --- a/backend/controllers/diaryTagController.js +++ b/backend/controllers/diaryTagController.js @@ -1,6 +1,5 @@ import { DiaryTag, DiaryDateTag } from '../models/index.js'; -import { devLog } from '../utils/logger.js'; export const getTags = async (req, res) => { try { const tags = await DiaryTag.findAll(); @@ -13,9 +12,12 @@ export const getTags = async (req, res) => { export const createTag = async (req, res) => { try { const { name } = req.body; - devLog(name); - const newTag = await DiaryTag.findOrCreate({ where: { name }, defaults: { name } }); - res.status(201).json(newTag); + if (!name) { + return res.status(400).json({ error: 'Der Name des Tags ist erforderlich.' }); + } + + const [tag, created] = await DiaryTag.findOrCreate({ where: { name }, defaults: { name } }); + res.status(created ? 201 : 200).json(tag); } catch (error) { res.status(500).json({ error: 'Error creating tag' }); } @@ -24,9 +26,14 @@ export const createTag = async (req, res) => { export const deleteTag = async (req, res) => { try { const { tagId } = req.params; - const { authcode: userToken } = req.headers; - const { clubId } = req.params; - await diaryService.removeTagFromDiaryDate(userToken, clubId, tagId); + + await DiaryDateTag.destroy({ where: { tagId } }); + const deleted = await DiaryTag.destroy({ where: { id: tagId } }); + + if (!deleted) { + return res.status(404).json({ error: 'Tag nicht gefunden' }); + } + res.status(200).json({ message: 'Tag deleted' }); } catch (error) { console.error('[deleteTag] - Error:', error); diff --git a/backend/services/autoFetchMatchResultsService.js b/backend/services/autoFetchMatchResultsService.js index 170ddf5..66d3e78 100644 --- a/backend/services/autoFetchMatchResultsService.js +++ b/backend/services/autoFetchMatchResultsService.js @@ -26,7 +26,7 @@ class AutoFetchMatchResultsService { autoUpdateRatings: true, // Nutze das gleiche Flag savePassword: true // Must have saved password }, - attributes: ['id', 'userId', 'email', 'encryptedPassword', 'accessToken', 'expiresAt', 'cookie'] + attributes: ['id', 'userId', 'email', 'savePassword', 'encryptedPassword', 'accessToken', 'expiresAt', 'cookie'] }); devLog(`Found ${accounts.length} accounts with auto-updates enabled for match results`); @@ -86,6 +86,7 @@ class AutoFetchMatchResultsService { account.refreshToken = loginResult.refreshToken; account.expiresAt = loginResult.expiresAt; account.cookie = loginResult.cookie; + account.savePassword = true; // ensure flag persists when saving await account.save(); devLog(`Successfully re-logged in for ${account.email}`); diff --git a/backend/services/autoUpdateRatingsService.js b/backend/services/autoUpdateRatingsService.js index 80fbb6f..387b06d 100644 --- a/backend/services/autoUpdateRatingsService.js +++ b/backend/services/autoUpdateRatingsService.js @@ -20,7 +20,7 @@ class AutoUpdateRatingsService { autoUpdateRatings: true, savePassword: true // Must have saved password }, - attributes: ['id', 'userId', 'email', 'encryptedPassword', 'accessToken', 'expiresAt', 'cookie'] + attributes: ['id', 'userId', 'email', 'savePassword', 'encryptedPassword', 'accessToken', 'expiresAt', 'cookie'] }); devLog(`Found ${accounts.length} accounts with auto-updates enabled`); @@ -78,6 +78,7 @@ class AutoUpdateRatingsService { account.refreshToken = loginResult.refreshToken; account.expiresAt = loginResult.expiresAt; account.cookie = loginResult.cookie; + account.savePassword = true; // Ensure flag persists when saving limited attributes await account.save(); devLog(`Successfully re-logged in for ${account.email}`); diff --git a/backend/tests/diaryDateActivityRoutes.test.js b/backend/tests/diaryDateActivityRoutes.test.js new file mode 100644 index 0000000..37bf017 --- /dev/null +++ b/backend/tests/diaryDateActivityRoutes.test.js @@ -0,0 +1,117 @@ +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 Group from '../models/Group.js'; +import GroupActivity from '../models/GroupActivity.js'; +import DiaryDateActivity from '../models/DiaryDateActivity.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('DiaryDateActivity Routes', () => { + beforeEach(async () => { + await sequelize.sync({ force: true }); + }); + + it('verwaltet Aktivitäten einschließlich Reihenfolge und Gruppenaktionen', async () => { + const { credentials } = await registerAndActivate('dda-routes@example.com'); + const token = await loginAndGetToken(credentials); + + const clubResponse = await request(app) + .post('/api/clubs') + .set(authHeaders(token)) + .send({ name: 'Activity Club' }); + const clubId = clubResponse.body.id; + + const diaryDateResponse = await request(app) + .post(`/api/diary/${clubId}`) + .set(authHeaders(token)) + .send({ date: '2026-02-01' }); + const diaryDateId = diaryDateResponse.body.id; + + const timeblockResponse = await request(app) + .post(`/api/diary-date-activities/${clubId}/`) + .set(authHeaders(token)) + .send({ diaryDateId, activity: 'Warmup', isTimeblock: true }); + const timeblockId = timeblockResponse.body.id; + + const activityResponse = await request(app) + .post(`/api/diary-date-activities/${clubId}/`) + .set(authHeaders(token)) + .send({ diaryDateId, activity: 'Topspin üben', duration: '30' }); + const activityId = activityResponse.body.id; + + const updateResponse = await request(app) + .put(`/api/diary-date-activities/${clubId}/${activityId}`) + .set(authHeaders(token)) + .send({ customActivityName: 'Topspin intensiv', duration: 35 }); + + expect(updateResponse.status).toBe(200); + expect(updateResponse.body.predefinedActivityId).toBeTruthy(); + + const reorderResponse = await request(app) + .put(`/api/diary-date-activities/${clubId}/${activityId}/order`) + .set(authHeaders(token)) + .send({ orderId: 1 }); + + expect(reorderResponse.status).toBe(200); + const persisted = await DiaryDateActivity.findByPk(activityId); + expect(persisted.orderId).toBe(1); + + const group = await Group.create({ diaryDateId, name: 'Gruppe A' }); + + const addGroupResponse = await request(app) + .post('/api/diary-date-activities/group') + .set(authHeaders(token)) + .send({ clubId, diaryDateId, groupId: group.id, activity: 'Match', timeblockId }); + + expect(addGroupResponse.status).toBe(201); + const groupActivityId = addGroupResponse.body.id; + + const listResponse = await request(app) + .get(`/api/diary-date-activities/${clubId}/${diaryDateId}`) + .set(authHeaders(token)); + + expect(listResponse.status).toBe(200); + expect(listResponse.body.length).toBeGreaterThanOrEqual(2); + + const deleteGroupResponse = await request(app) + .delete(`/api/diary-date-activities/group/${clubId}/${groupActivityId}`) + .set(authHeaders(token)); + + expect(deleteGroupResponse.status).toBe(200); + const groupExists = await GroupActivity.findByPk(groupActivityId); + expect(groupExists).toBeNull(); + + const deleteResponse = await request(app) + .delete(`/api/diary-date-activities/${clubId}/${activityId}`) + .set(authHeaders(token)); + + expect(deleteResponse.status).toBe(200); + }); +}); diff --git a/backend/tests/diaryDateActivityService.test.js b/backend/tests/diaryDateActivityService.test.js new file mode 100644 index 0000000..80b94a2 --- /dev/null +++ b/backend/tests/diaryDateActivityService.test.js @@ -0,0 +1,182 @@ +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 diaryDateActivityService from '../services/diaryDateActivityService.js'; +import { checkAccess } from '../utils/userUtils.js'; +import Club from '../models/Club.js'; +import DiaryDate from '../models/DiaryDates.js'; +import DiaryDateActivity from '../models/DiaryDateActivity.js'; +import PredefinedActivity from '../models/PredefinedActivity.js'; +import PredefinedActivityImage from '../models/PredefinedActivityImage.js'; +import Group from '../models/Group.js'; +import GroupActivity from '../models/GroupActivity.js'; + +const createClubAndDate = async () => { + const club = await Club.create({ name: 'Service Club' }); + const diaryDate = await DiaryDate.create({ date: '2026-01-01', clubId: club.id }); + return { club, diaryDate }; +}; + +describe('diaryDateActivityService', () => { + beforeEach(async () => { + await sequelize.sync({ force: true }); + vi.clearAllMocks(); + }); + + it('erstellt Aktivitäten mit automatisch berechneter Reihenfolge', async () => { + const { club, diaryDate } = await createClubAndDate(); + + const first = await diaryDateActivityService.createActivity('token', club.id, { + diaryDateId: diaryDate.id, + activity: 'Warmup', + duration: '30', + isTimeblock: false, + }); + + const second = await diaryDateActivityService.createActivity('token', club.id, { + diaryDateId: diaryDate.id, + activity: 'Drills', + duration: '45', + isTimeblock: false, + }); + + expect(checkAccess).toHaveBeenCalledTimes(2); + expect(first.orderId).toBe(1); + expect(second.orderId).toBe(2); + const all = await DiaryDateActivity.findAll({ where: { diaryDateId: diaryDate.id } }); + expect(all).toHaveLength(2); + }); + + it('aktualisiert Aktivitäten und legt neue vordefinierte Aktivitäten an', async () => { + const { club, diaryDate } = await createClubAndDate(); + const activity = await diaryDateActivityService.createActivity('token', club.id, { + diaryDateId: diaryDate.id, + activity: 'Blocken', + duration: '20', + isTimeblock: false, + }); + + const updated = await diaryDateActivityService.updateActivity('token', club.id, activity.id, { + customActivityName: 'Topspin', + duration: 25, + }); + + const predefined = await PredefinedActivity.findOne({ where: { name: 'Topspin' } }); + + expect(updated.predefinedActivityId).toBe(predefined.id); + expect(predefined.duration).toBe(25); + }); + + it('ändert die Reihenfolge und verschiebt Nachbarn korrekt', async () => { + const { club, diaryDate } = await createClubAndDate(); + const first = await diaryDateActivityService.createActivity('token', club.id, { + diaryDateId: diaryDate.id, + activity: 'A', + }); + const second = await diaryDateActivityService.createActivity('token', club.id, { + diaryDateId: diaryDate.id, + activity: 'B', + }); + const third = await diaryDateActivityService.createActivity('token', club.id, { + diaryDateId: diaryDate.id, + activity: 'C', + }); + + await diaryDateActivityService.updateActivityOrder('token', club.id, third.id, 1); + + const reloaded = await DiaryDateActivity.findAll({ + where: { diaryDateId: diaryDate.id }, + order: [['orderId', 'ASC']], + }); + + expect(reloaded[0].id).toBe(third.id); + expect(reloaded.map((item) => item.orderId)).toEqual([1, 2, 3]); + }); + + it('liefert Aktivitäten mit Bild-Links und Gruppendaten', async () => { + const { club, diaryDate } = await createClubAndDate(); + const predefined = await PredefinedActivity.create({ name: 'Vorhand', code: 'FH' }); + const activity = await DiaryDateActivity.create({ + diaryDateId: diaryDate.id, + predefinedActivityId: predefined.id, + orderId: 1, + isTimeblock: false, + }); + await PredefinedActivityImage.create({ + predefinedActivityId: predefined.id, + drawingData: JSON.stringify({ circles: 2 }), + mimeType: 'image/png', + fileName: 'test.png', + }); + + const group = await Group.create({ diaryDateId: diaryDate.id, name: 'Gruppe 1' }); + const groupPredefined = await PredefinedActivity.create({ name: 'Rally', code: 'RL' }); + await GroupActivity.create({ + diaryDateActivity: activity.id, + groupId: group.id, + customActivity: groupPredefined.id, + }); + + const result = await diaryDateActivityService.getActivities('token', club.id, diaryDate.id); + + expect(result).toHaveLength(1); + expect(result[0].predefinedActivity.imageUrl).toContain(`/api/predefined-activities/${predefined.id}/image/`); + expect(result[0].groupActivities).toHaveLength(1); + }); + + it('fügt Gruppenaktivitäten in Zeitblöcke ein', async () => { + const { club, diaryDate } = await createClubAndDate(); + const timeblock = await DiaryDateActivity.create({ + diaryDateId: diaryDate.id, + isTimeblock: true, + orderId: 1, + }); + const group = await Group.create({ diaryDateId: diaryDate.id, name: 'Gruppe 2' }); + + const created = await diaryDateActivityService.addGroupActivity('token', club.id, diaryDate.id, group.id, 'Abschlussspiel', timeblock.id); + + expect(created.diaryDateActivity).toBe(timeblock.id); + expect(created.groupId).toBe(group.id); + }); + + it('löscht Aktivitäten und zugehörige Gruppeneinträge', async () => { + const { club, diaryDate } = await createClubAndDate(); + const activity = await diaryDateActivityService.createActivity('token', club.id, { + diaryDateId: diaryDate.id, + activity: 'Auslaufen', + }); + + await diaryDateActivityService.deleteActivity('token', club.id, activity.id); + const remaining = await DiaryDateActivity.findByPk(activity.id); + expect(remaining).toBeNull(); + }); + + it('löscht Gruppenaktivitäten', async () => { + const { club, diaryDate } = await createClubAndDate(); + const timeblock = await DiaryDateActivity.create({ + diaryDateId: diaryDate.id, + isTimeblock: true, + orderId: 1, + }); + const group = await Group.create({ diaryDateId: diaryDate.id, name: 'Gruppe 3' }); + const created = await GroupActivity.create({ + diaryDateActivity: timeblock.id, + groupId: group.id, + customActivity: null, + }); + + await diaryDateActivityService.deleteGroupActivity('token', club.id, created.id); + const exists = await GroupActivity.findByPk(created.id); + expect(exists).toBeNull(); + }); +}); diff --git a/backend/tests/diaryMemberActivityRoutes.test.js b/backend/tests/diaryMemberActivityRoutes.test.js new file mode 100644 index 0000000..eecf4a7 --- /dev/null +++ b/backend/tests/diaryMemberActivityRoutes.test.js @@ -0,0 +1,129 @@ +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 Member from '../models/Member.js'; +import Participant from '../models/Participant.js'; +import DiaryDate from '../models/DiaryDates.js'; +import DiaryDateActivity from '../models/DiaryDateActivity.js'; +import DiaryMemberActivity from '../models/DiaryMemberActivity.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, +}); + +const createMember = async (clubId, firstName, lastName, email) => { + return Member.create({ + firstName, + lastName, + phone: '0123456789', + street: 'Teststraße 1', + city: 'Teststadt', + postalCode: '12345', + email, + clubId, + birthDate: '2000-01-01', + active: true, + testMembership: false, + picsInInternetAllowed: false, + gender: 'female', + }); +}; + +describe('DiaryMemberActivity Routes', () => { + beforeEach(async () => { + await sequelize.sync({ force: true }); + }); + + it('fügt Teilnehmer zu Aktivitäten hinzu, listet und entfernt sie wieder', async () => { + const { credentials } = await registerAndActivate('dma@example.com'); + const token = await loginAndGetToken(credentials); + + const clubResponse = await request(app) + .post('/api/clubs') + .set(authHeaders(token)) + .send({ name: 'Member Activity Club' }); + const clubId = clubResponse.body.id; + + const diaryDateResponse = await request(app) + .post(`/api/diary/${clubId}`) + .set(authHeaders(token)) + .send({ date: '2026-03-01' }); + const diaryDateId = diaryDateResponse.body.id; + + const activityResponse = await request(app) + .post(`/api/diary-date-activities/${clubId}/`) + .set(authHeaders(token)) + .send({ diaryDateId, activity: 'Koordination', isTimeblock: false }); + const activityId = activityResponse.body.id; + + const member = await createMember(clubId, 'Anna', 'Trainer', 'anna@example.com'); + const participant = await Participant.create({ diaryDateId, memberId: member.id }); + + const addResponse = await request(app) + .post(`/api/diary-member-activities/${clubId}/${activityId}`) + .set(authHeaders(token)) + .send({ participantIds: [participant.id] }); + + expect(addResponse.status).toBe(201); + expect(addResponse.body).toHaveLength(1); + + const listResponse = await request(app) + .get(`/api/diary-member-activities/${clubId}/${activityId}`) + .set(authHeaders(token)); + + expect(listResponse.status).toBe(200); + expect(listResponse.body[0].participantId).toBe(participant.id); + + const deleteResponse = await request(app) + .delete(`/api/diary-member-activities/${clubId}/${activityId}/${participant.id}`) + .set(authHeaders(token)); + + expect(deleteResponse.status).toBe(200); + const remaining = await DiaryMemberActivity.findAll({ where: { diaryDateActivityId: activityId } }); + expect(remaining).toHaveLength(0); + }); + + it('prüft ungültige Nutzlasten', async () => { + const { credentials } = await registerAndActivate('dma-invalid@example.com'); + const token = await loginAndGetToken(credentials); + + const clubResponse = await request(app) + .post('/api/clubs') + .set(authHeaders(token)) + .send({ name: 'Invalid Payload Club' }); + const clubId = clubResponse.body.id; + + const diaryDate = await DiaryDate.create({ date: '2026-04-01', clubId }); + const activity = await DiaryDateActivity.create({ diaryDateId: diaryDate.id, orderId: 1, isTimeblock: false }); + + const response = await request(app) + .post(`/api/diary-member-activities/${clubId}/${activity.id}`) + .set(authHeaders(token)) + .send({ participantIds: 'nicht-array' }); + + expect(response.status).toBe(400); + }); +}); diff --git a/backend/tests/diaryNoteRoutes.test.js b/backend/tests/diaryNoteRoutes.test.js new file mode 100644 index 0000000..50b23ee --- /dev/null +++ b/backend/tests/diaryNoteRoutes.test.js @@ -0,0 +1,114 @@ +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 Member from '../models/Member.js'; +import DiaryDate from '../models/DiaryDates.js'; +import DiaryNote from '../models/DiaryNote.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, +}); + +const createMember = (clubId, email) => + Member.create({ + firstName: 'Note', + lastName: 'Tester', + phone: '0123456789', + street: 'Straße 1', + city: 'Stadt', + postalCode: '12345', + email, + clubId, + birthDate: '2000-01-01', + active: true, + testMembership: false, + picsInInternetAllowed: false, + gender: 'female', + }); + +describe('DiaryNote Routes', () => { + beforeEach(async () => { + await sequelize.sync({ force: true }); + }); + + it('erstellt, listet und löscht Notizen', async () => { + const { credentials } = await registerAndActivate('diarynote@example.com'); + const token = await loginAndGetToken(credentials); + + const clubResponse = await request(app) + .post('/api/clubs') + .set(authHeaders(token)) + .send({ name: 'Notiz Club' }); + const clubId = clubResponse.body.id; + + const diaryDateResponse = await request(app) + .post(`/api/diary/${clubId}`) + .set(authHeaders(token)) + .send({ date: '2026-05-01' }); + const diaryDateId = diaryDateResponse.body.id; + + const member = await createMember(clubId, 'note.member@example.com'); + + const createResponse = await request(app) + .post('/api/diary-notes') + .set(authHeaders(token)) + .send({ memberId: member.id, diaryDateId, content: 'Gute Einheit' }); + + expect(createResponse.status).toBe(201); + expect(createResponse.body.content).toBe('Gute Einheit'); + + const listResponse = await request(app) + .get('/api/diary-notes') + .set(authHeaders(token)) + .query({ diaryDateId, memberId: member.id }); + + expect(listResponse.status).toBe(200); + expect(listResponse.body).toHaveLength(1); + expect(listResponse.body[0].content).toBe('Gute Einheit'); + + const noteId = createResponse.body.id; + + const deleteResponse = await request(app) + .delete(`/api/diary-notes/${noteId}`) + .set(authHeaders(token)); + + expect(deleteResponse.status).toBe(200); + const remaining = await DiaryNote.findByPk(noteId); + expect(remaining).toBeNull(); + }); + + it('validiert Pflichtfelder beim Erstellen', async () => { + const { credentials } = await registerAndActivate('diarynote-invalid@example.com'); + const token = await loginAndGetToken(credentials); + + const response = await request(app) + .post('/api/diary-notes') + .set(authHeaders(token)) + .send({ content: 'Fehlende Felder' }); + + expect(response.status).toBe(400); + }); +}); diff --git a/backend/tests/diaryService.test.js b/backend/tests/diaryService.test.js index 029f984..cb42687 100644 --- a/backend/tests/diaryService.test.js +++ b/backend/tests/diaryService.test.js @@ -50,6 +50,10 @@ describe('diaryService', () => { 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'); + + await expect( + diaryService.createDateForClub('token', club.id, 'ungültig', '20:00', '21:00') + ).rejects.toThrow('Invalid date format'); }); it('aktualisiert Trainingszeiten und wirft Fehler bei fehlendem Eintrag', async () => { @@ -121,4 +125,10 @@ describe('diaryService', () => { const exists = await DiaryDate.findByPk(date.id); expect(exists).toBeNull(); }); + + it('meldet Fehler bei fehlenden Clubs oder Einträgen', async () => { + await expect(diaryService.getDatesForClub('token', 9999)).rejects.toThrow('Club not found'); + const club = await Club.create({ name: '404 Club' }); + await expect(diaryService.removeDateForClub('token', club.id, 9999)).rejects.toThrow('Diary entry not found'); + }); }); diff --git a/backend/tests/diaryTagRoutes.test.js b/backend/tests/diaryTagRoutes.test.js new file mode 100644 index 0000000..f631b5e --- /dev/null +++ b/backend/tests/diaryTagRoutes.test.js @@ -0,0 +1,80 @@ +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 DiaryTag from '../models/DiaryTag.js'; +import DiaryDateTag from '../models/DiaryDateTag.js'; +import DiaryDate from '../models/DiaryDates.js'; +import Club from '../models/Club.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('DiaryTag Routes', () => { + beforeEach(async () => { + await sequelize.sync({ force: true }); + }); + + it('erstellt und listet Tags', async () => { + const { credentials } = await registerAndActivate('diarytag@example.com'); + const token = await loginAndGetToken(credentials); + + const createResponse = await request(app) + .post('/api/diary-tags') + .set(authHeaders(token)) + .send({ name: 'Technik' }); + + expect(createResponse.status).toBe(201); + expect(createResponse.body.name).toBe('Technik'); + + const listResponse = await request(app) + .get('/api/diary-tags') + .set(authHeaders(token)); + + expect(listResponse.status).toBe(200); + expect(listResponse.body).toHaveLength(1); + }); + + it('löscht Tags inklusive Zuordnungen', async () => { + const { credentials } = await registerAndActivate('diarytag-delete@example.com'); + const token = await loginAndGetToken(credentials); + + const club = await Club.create({ name: 'Tag Club' }); + const diaryDate = await DiaryDate.create({ date: '2026-06-01', clubId: club.id }); + const tag = await DiaryTag.create({ name: 'Ausdauer' }); + await DiaryDateTag.create({ diaryDateId: diaryDate.id, tagId: tag.id }); + + const deleteResponse = await request(app) + .delete(`/api/diary-tags/${tag.id}`) + .set(authHeaders(token)); + + expect(deleteResponse.status).toBe(200); + const tagExists = await DiaryTag.findByPk(tag.id); + expect(tagExists).toBeNull(); + const relations = await DiaryDateTag.findAll({ where: { tagId: tag.id } }); + expect(relations).toHaveLength(0); + }); +}); diff --git a/backend/tests/groupRoutes.test.js b/backend/tests/groupRoutes.test.js new file mode 100644 index 0000000..04d658e --- /dev/null +++ b/backend/tests/groupRoutes.test.js @@ -0,0 +1,78 @@ +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 User from '../models/User.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; +}; + +const authHeaders = (token) => ({ + Authorization: `Bearer ${token}`, + authcode: token, +}); + +describe('Group Routes', () => { + beforeEach(async () => { + await sequelize.sync({ force: true }); + }); + + it('erstellt, listet und aktualisiert Gruppen', async () => { + const { credentials } = await registerAndActivate('groups@example.com'); + const token = await loginAndGetToken(credentials); + + const clubResponse = await request(app) + .post('/api/clubs') + .set(authHeaders(token)) + .send({ name: 'Group Route Club' }); + const clubId = clubResponse.body.id; + + const diaryDateResponse = await request(app) + .post(`/api/diary/${clubId}`) + .set(authHeaders(token)) + .send({ date: '2026-08-01' }); + const diaryDateId = diaryDateResponse.body.id; + + const createResponse = await request(app) + .post('/api/groups') + .set(authHeaders(token)) + .send({ clubid: clubId, dateid: diaryDateId, name: 'Gruppe 1', lead: 'Coach' }); + + expect(createResponse.status).toBe(201); + const groupId = createResponse.body.id; + + const listResponse = await request(app) + .get(`/api/groups/${clubId}/${diaryDateId}`) + .set(authHeaders(token)); + + expect(listResponse.status).toBe(200); + expect(listResponse.body).toHaveLength(1); + expect(listResponse.body[0].name).toBe('Gruppe 1'); + + const updateResponse = await request(app) + .put(`/api/groups/${groupId}`) + .set(authHeaders(token)) + .send({ clubid: clubId, dateid: diaryDateId, name: 'Gruppe 1', lead: 'Neue Leitung' }); + + expect(updateResponse.status).toBe(200); + expect(updateResponse.body.lead).toBe('Neue Leitung'); + }); +}); diff --git a/backend/tests/groupService.test.js b/backend/tests/groupService.test.js new file mode 100644 index 0000000..19ef650 --- /dev/null +++ b/backend/tests/groupService.test.js @@ -0,0 +1,57 @@ +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 groupService from '../services/groupService.js'; +import { checkAccess } from '../utils/userUtils.js'; +import Club from '../models/Club.js'; +import DiaryDate from '../models/DiaryDates.js'; +import Group from '../models/Group.js'; + +describe('groupService', () => { + beforeEach(async () => { + await sequelize.sync({ force: true }); + vi.clearAllMocks(); + }); + + const setupClubAndDate = async () => { + const club = await Club.create({ name: 'Group Club' }); + const diaryDate = await DiaryDate.create({ date: '2026-07-01', clubId: club.id }); + return { club, diaryDate }; + }; + + it('legt Gruppen an und gibt sie zurück', async () => { + const { club, diaryDate } = await setupClubAndDate(); + + const group = await groupService.addGroup('token', club.id, diaryDate.id, 'Team Blau', 'Coach'); + + expect(checkAccess).toHaveBeenCalledWith('token', club.id); + expect(group.name).toBe('Team Blau'); + + const groups = await groupService.getGroups('token', club.id, diaryDate.id); + expect(groups).toHaveLength(1); + expect(groups[0].lead).toBe('Coach'); + }); + + it('aktualisiert Gruppenangaben und validiert Zugehörigkeit', async () => { + const { club, diaryDate } = await setupClubAndDate(); + const group = await groupService.addGroup('token', club.id, diaryDate.id, 'Team Rot', 'Trainerin'); + + const updated = await groupService.changeGroup('token', group.id, club.id, diaryDate.id, 'Team Rot', 'Neuer Lead'); + + expect(updated.lead).toBe('Neuer Lead'); + + await expect( + groupService.changeGroup('token', group.id, club.id, diaryDate.id + 1, 'Fail', 'Lead') + ).rejects.toThrow('Datum nicht gefunden oder passt nicht zum Verein'); + }); +}); diff --git a/backend/tests/matchRoutes.test.js b/backend/tests/matchRoutes.test.js new file mode 100644 index 0000000..ab34492 --- /dev/null +++ b/backend/tests/matchRoutes.test.js @@ -0,0 +1,85 @@ +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 Season from '../models/Season.js'; +import League from '../models/League.js'; +import Team from '../models/Team.js'; +import Match from '../models/Match.js'; +import Club from '../models/Club.js'; +import Location from '../models/Location.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('Match Routes', () => { + beforeEach(async () => { + await sequelize.sync({ force: true }); + }); + + it('liefert Matches und aktualisiert Spielerlisten', async () => { + const { credentials } = await registerAndActivate('matchroutes@example.com'); + const token = await loginAndGetToken(credentials); + + const clubResponse = await request(app) + .post('/api/clubs') + .set(authHeaders(token)) + .send({ name: 'Match Route Club' }); + const clubId = clubResponse.body.id; + + const season = await Season.create({ season: '2025/2026' }); + const league = await League.create({ name: 'Route Liga', clubId, seasonId: season.id }); + const homeTeam = await Team.create({ name: 'Route Club I', clubId, leagueId: league.id, seasonId: season.id }); + const guestTeam = await Team.create({ name: 'Route Club II', clubId, leagueId: league.id, seasonId: season.id }); + const location = await Location.create({ name: 'Route Halle', address: 'Straße 1', city: 'Stadt', zip: '12345' }); + const match = await Match.create({ + clubId, + leagueId: league.id, + homeTeamId: homeTeam.id, + guestTeamId: guestTeam.id, + locationId: location.id, + date: new Date('2025-09-01T18:00:00Z'), + time: '18:00', + }); + + const listResponse = await request(app) + .get(`/api/matches/leagues/${clubId}/matches`) + .set(authHeaders(token)) + .query({ seasonid: season.id }); + + expect(listResponse.status).toBe(200); + expect(listResponse.body).toHaveLength(1); + expect(listResponse.body[0].homeTeam.name).toBe('Route Club I'); + + const patchResponse = await request(app) + .patch(`/api/matches/${match.id}/players`) + .set(authHeaders(token)) + .send({ playersReady: ['Alice'], playersPlanned: ['Bob'], playersPlayed: ['Charlie'] }); + + expect(patchResponse.status).toBe(200); + expect(patchResponse.body.data.playersReady).toEqual(['Alice']); + }); +}); diff --git a/backend/tests/matchService.test.js b/backend/tests/matchService.test.js new file mode 100644 index 0000000..6e5b144 --- /dev/null +++ b/backend/tests/matchService.test.js @@ -0,0 +1,83 @@ +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 matchService from '../services/matchService.js'; +import { checkAccess } from '../utils/userUtils.js'; +import Season from '../models/Season.js'; +import League from '../models/League.js'; +import Team from '../models/Team.js'; +import Match from '../models/Match.js'; +import Club from '../models/Club.js'; +import Location from '../models/Location.js'; + +describe('matchService', () => { + beforeEach(async () => { + await sequelize.sync({ force: true }); + vi.clearAllMocks(); + }); + + it('formatiert Team-Namen anhand der Altersklasse', () => { + expect(matchService.formatTeamNameWithAgeClass('Harheimer TC', 'Jugend 11')).toBe('Harheimer TC (J11)'); + expect(matchService.formatTeamNameWithAgeClass('Harheimer TC', 'Senioren')).toBe('Harheimer TC (S)'); + expect(matchService.formatTeamNameWithAgeClass('Harheimer TC', 'Erwachsene')).toBe('Harheimer TC'); + }); + + it('erstellt Teams und gibt bestehende Einträge zurück', async () => { + const club = await Club.create({ name: 'Match Club' }); + const season = await Season.create({ season: '2025/2026' }); + const league = await League.create({ name: 'Verbandsliga', clubId: club.id, seasonId: season.id }); + + const teamId = await matchService.getOrCreateTeamId('Harheimer TC', 'Jugend 11', club.id, league.id, season.id); + const sameTeamId = await matchService.getOrCreateTeamId('Harheimer TC', 'Jugend 11', club.id, league.id, season.id); + + expect(teamId).toBeTruthy(); + expect(teamId).toBe(sameTeamId); + const team = await Team.findByPk(teamId); + expect(team.name).toBe('Harheimer TC (J11)'); + }); + + it('liefert Ligen und Matches für eine Saison und aktualisiert Spielerlisten', async () => { + const club = await Club.create({ name: 'Season Club' }); + const season = await Season.create({ season: '2025/2026' }); + const league = await League.create({ name: 'Oberliga', clubId: club.id, seasonId: season.id }); + const homeTeam = await Team.create({ name: 'Season Club I', clubId: club.id, leagueId: league.id, seasonId: season.id }); + const guestTeam = await Team.create({ name: 'Season Club II', clubId: club.id, leagueId: league.id, seasonId: season.id }); + const location = await Location.create({ name: 'Halle A', address: 'Straße 1', city: 'Stadt', zip: '12345' }); + + const match = await Match.create({ + clubId: club.id, + leagueId: league.id, + homeTeamId: homeTeam.id, + guestTeamId: guestTeam.id, + locationId: location.id, + date: new Date('2025-09-01T18:00:00Z'), + time: '18:00', + }); + + const leagues = await matchService.getLeaguesForCurrentSeason('token', club.id, season.id); + expect(checkAccess).toHaveBeenCalledWith('token', club.id); + expect(leagues).toHaveLength(1); + expect(leagues[0].name).toBe('Oberliga'); + + const matches = await matchService.getMatchesForLeagues('token', club.id, season.id); + expect(matches).toHaveLength(1); + expect(matches[0].homeTeam.name).toBe('Season Club I'); + expect(matches[0].location.name).toBe('Halle A'); + + await matchService.updateMatchPlayers('token', match.id, ['Alice'], ['Bob'], ['Charlie']); + const updated = await Match.findByPk(match.id); + expect(updated.playersReady).toEqual(['Alice']); + expect(updated.playersPlanned).toEqual(['Bob']); + expect(updated.playersPlayed).toEqual(['Charlie']); + }); +}); diff --git a/backend/tests/memberActivityRoutes.test.js b/backend/tests/memberActivityRoutes.test.js new file mode 100644 index 0000000..9da0d59 --- /dev/null +++ b/backend/tests/memberActivityRoutes.test.js @@ -0,0 +1,131 @@ +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 User from '../models/User.js'; +import Club from '../models/Club.js'; +import Member from '../models/Member.js'; +import DiaryDate from '../models/DiaryDates.js'; +import DiaryDateActivity from '../models/DiaryDateActivity.js'; +import DiaryMemberActivity from '../models/DiaryMemberActivity.js'; +import Participant from '../models/Participant.js'; +import PredefinedActivity from '../models/PredefinedActivity.js'; +import Group from '../models/Group.js'; +import GroupActivity from '../models/GroupActivity.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('MemberActivity Routes', () => { + beforeEach(async () => { + await sequelize.sync({ force: true }); + }); + + it('aggregiert Aktivitäten und liefert letzte Teilnahmen', async () => { + const { credentials } = await registerAndActivate('memberactivity@example.com'); + const token = await loginAndGetToken(credentials); + + const clubResponse = await request(app) + .post('/api/clubs') + .set(authHeaders(token)) + .send({ name: 'Activity Club' }); + const clubId = clubResponse.body.id; + + const member = await Member.create({ + firstName: 'Max', + lastName: 'Mustermann', + phone: '0123456789', + street: 'Straße 1', + city: 'Stadt', + postalCode: '12345', + email: 'max@example.com', + clubId, + birthDate: '2005-01-01', + active: true, + testMembership: true, + picsInInternetAllowed: false, + gender: 'male', + }); + + const diaryDateRecent = await DiaryDate.create({ date: '2026-09-01', clubId }); + const diaryDateOlder = await DiaryDate.create({ date: '2026-06-01', clubId }); + const group = await Group.create({ diaryDateId: diaryDateRecent.id, name: 'Gruppe A' }); + const otherGroup = await Group.create({ diaryDateId: diaryDateOlder.id, name: 'Gruppe B' }); + + const participant = await Participant.create({ diaryDateId: diaryDateRecent.id, memberId: member.id, groupId: group.id }); + + const activityGeneralDef = await PredefinedActivity.create({ name: 'Allgemeines Training' }); + const activityGroupDef = await PredefinedActivity.create({ name: 'Gruppenübung' }); + const activityOtherDef = await PredefinedActivity.create({ name: 'Falsche Gruppe' }); + + const generalActivity = await DiaryDateActivity.create({ + diaryDateId: diaryDateRecent.id, + predefinedActivityId: activityGeneralDef.id, + orderId: 1, + isTimeblock: false, + }); + + const groupActivity = await DiaryDateActivity.create({ + diaryDateId: diaryDateRecent.id, + predefinedActivityId: activityGroupDef.id, + orderId: 2, + isTimeblock: false, + }); + + await GroupActivity.create({ diaryDateActivity: groupActivity.id, groupId: group.id, customActivity: null }); + + const otherActivity = await DiaryDateActivity.create({ + diaryDateId: diaryDateOlder.id, + predefinedActivityId: activityOtherDef.id, + orderId: 1, + isTimeblock: false, + }); + await GroupActivity.create({ diaryDateActivity: otherActivity.id, groupId: otherGroup.id, customActivity: null }); + + await DiaryMemberActivity.bulkCreate([ + { diaryDateActivityId: generalActivity.id, participantId: participant.id }, + { diaryDateActivityId: groupActivity.id, participantId: participant.id }, + { diaryDateActivityId: otherActivity.id, participantId: participant.id }, + ]); + + const activitiesResponse = await request(app) + .get(`/api/member-activities/${clubId}/${member.id}`) + .set(authHeaders(token)) + .query({ period: 'year' }); + + expect(activitiesResponse.status).toBe(200); + const names = activitiesResponse.body.map((entry) => entry.name).sort(); + expect(names).toEqual(['Allgemeines Training', 'Gruppenübung']); + + const lastResponse = await request(app) + .get(`/api/member-activities/${clubId}/${member.id}/last-participations`) + .set(authHeaders(token)) + .query({ limit: 1 }); + + expect(lastResponse.status).toBe(200); + expect(lastResponse.body).toHaveLength(1); + expect(lastResponse.body[0].activityName).toBe('Gruppenübung'); + }); +}); diff --git a/backend/tests/memberNoteRoutes.test.js b/backend/tests/memberNoteRoutes.test.js new file mode 100644 index 0000000..2678686 --- /dev/null +++ b/backend/tests/memberNoteRoutes.test.js @@ -0,0 +1,93 @@ +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 User from '../models/User.js'; +import Member from '../models/Member.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, +}); + +const createMember = async (clubId) => { + return Member.create({ + firstName: 'Nora', + lastName: 'Notiz', + phone: '0123456789', + street: 'Straße 3', + city: 'Stadt', + postalCode: '12345', + email: 'nora@example.com', + clubId, + birthDate: '1999-01-01', + active: true, + testMembership: false, + picsInInternetAllowed: false, + gender: 'female', + }); +}; + +describe('MemberNote Routes', () => { + beforeEach(async () => { + await sequelize.sync({ force: true }); + }); + + it('erstellt, listet und löscht Member Notes', async () => { + const { credentials } = await registerAndActivate('membernote@example.com'); + const token = await loginAndGetToken(credentials); + + const clubResponse = await request(app) + .post('/api/clubs') + .set(authHeaders(token)) + .send({ name: 'Note Club' }); + const clubId = clubResponse.body.id; + + const member = await createMember(clubId); + + const createResponse = await request(app) + .post('/api/member-notes') + .set(authHeaders(token)) + .send({ memberId: member.id, clubId, content: 'Erste Notiz' }); + + expect(createResponse.status).toBe(201); + expect(createResponse.body[0].content).toBe('Erste Notiz'); + + const listResponse = await request(app) + .get(`/api/member-notes/${member.id}`) + .set(authHeaders(token)) + .query({ clubId }); + + expect(listResponse.status).toBe(200); + expect(listResponse.body).toHaveLength(1); + + const noteId = createResponse.body[0].id; + const deleteResponse = await request(app) + .delete(`/api/member-notes/${noteId}`) + .set(authHeaders(token)) + .send({ clubId }); + + expect(deleteResponse.status).toBe(200); + expect(deleteResponse.body).toHaveLength(0); + }); +}); diff --git a/backend/tests/memberRoutes.test.js b/backend/tests/memberRoutes.test.js new file mode 100644 index 0000000..ac56476 --- /dev/null +++ b/backend/tests/memberRoutes.test.js @@ -0,0 +1,93 @@ +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 User from '../models/User.js'; +import Member from '../models/Member.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, +}); + +const createMember = async (clubId, overrides = {}) => { + return Member.create({ + firstName: 'Lara', + lastName: 'Lang', + phone: '0123456789', + street: 'Straße 2', + city: 'Stadt', + postalCode: '12345', + email: 'lara@example.com', + clubId, + birthDate: '2001-01-01', + active: true, + testMembership: true, + picsInInternetAllowed: false, + gender: 'female', + ...overrides, + }); +}; + +describe('Member quick action routes', () => { + beforeEach(async () => { + await sequelize.sync({ force: true }); + }); + + it('setzt Testmitgliedschaft, Formularstatus und deaktiviert Mitglieder', async () => { + const { credentials } = await registerAndActivate('memberroutes@example.com'); + const token = await loginAndGetToken(credentials); + + const clubResponse = await request(app) + .post('/api/clubs') + .set(authHeaders(token)) + .send({ name: 'Member Route Club' }); + const clubId = clubResponse.body.id; + + const member = await createMember(clubId, { testMembership: true, memberFormHandedOver: false, active: true }); + + const testResponse = await request(app) + .post(`/api/members/quick-update-test-membership/${clubId}/${member.id}`) + .set(authHeaders(token)); + + expect(testResponse.status).toBe(200); + await member.reload(); + expect(member.testMembership).toBe(false); + + const formResponse = await request(app) + .post(`/api/members/quick-update-member-form/${clubId}/${member.id}`) + .set(authHeaders(token)); + + expect(formResponse.status).toBe(200); + await member.reload(); + expect(member.memberFormHandedOver).toBe(true); + + const deactivateResponse = await request(app) + .post(`/api/members/quick-deactivate/${clubId}/${member.id}`) + .set(authHeaders(token)); + + expect(deactivateResponse.status).toBe(200); + await member.reload(); + expect(member.active).toBe(false); + }); +}); diff --git a/backend/tests/memberService.test.js b/backend/tests/memberService.test.js new file mode 100644 index 0000000..6c1aa09 --- /dev/null +++ b/backend/tests/memberService.test.js @@ -0,0 +1,75 @@ +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 memberService from '../services/memberService.js'; +import { checkAccess } from '../utils/userUtils.js'; +import Member from '../models/Member.js'; + +describe('memberService quick updates', () => { + beforeEach(async () => { + await sequelize.sync({ force: true }); + vi.clearAllMocks(); + }); + + const createMember = async (overrides = {}) => { + return Member.create({ + firstName: 'Erika', + lastName: 'Musterfrau', + phone: '0123456789', + street: 'Straße 1', + city: 'Stadt', + postalCode: '12345', + email: 'erika@example.com', + clubId: 1, + birthDate: '2000-01-01', + active: true, + testMembership: true, + picsInInternetAllowed: false, + gender: 'female', + ...overrides, + }); + }; + + it('entfernt Testmitgliedschaften und behandelt Fehlerfälle', async () => { + const member = await createMember({ testMembership: true }); + + const result = await memberService.quickUpdateTestMembership('token', member.clubId, member.id); + + expect(checkAccess).toHaveBeenCalledWith('token', member.clubId); + expect(result.status).toBe(200); + await member.reload(); + expect(member.testMembership).toBe(false); + + const alreadyLive = await memberService.quickUpdateTestMembership('token', member.clubId, member.id); + expect(alreadyLive.status).toBe(400); + }); + + it('markiert Formular-Status und deaktiviert Mitglieder', async () => { + const member = await createMember({ testMembership: false, memberFormHandedOver: false }); + + const formResult = await memberService.quickUpdateMemberFormHandedOver('token', member.clubId, member.id); + expect(formResult.status).toBe(200); + await member.reload(); + expect(member.memberFormHandedOver).toBe(true); + + const deactivateResult = await memberService.quickDeactivateMember('token', member.clubId, member.id); + expect(deactivateResult.status).toBe(200); + await member.reload(); + expect(member.active).toBe(false); + }); + + it('meldet 404 für fehlende Mitglieder', async () => { + const result = await memberService.quickDeactivateMember('token', 99, 123); + expect(result.status).toBe(404); + }); +}); diff --git a/backend/tests/setupTestEnv.js b/backend/tests/setupTestEnv.js index dec1678..be05963 100644 --- a/backend/tests/setupTestEnv.js +++ b/backend/tests/setupTestEnv.js @@ -7,6 +7,7 @@ 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'; +process.env.ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef'; afterAll(async () => { await sequelize.close(); diff --git a/backend/tests/testApp.js b/backend/tests/testApp.js index 3b63435..7fbdee2 100644 --- a/backend/tests/testApp.js +++ b/backend/tests/testApp.js @@ -7,6 +7,15 @@ import apiLogRoutes from '../routes/apiLogRoutes.js'; import clubRoutes from '../routes/clubRoutes.js'; import clubTeamRoutes from '../routes/clubTeamRoutes.js'; import diaryRoutes from '../routes/diaryRoutes.js'; +import diaryDateActivityRoutes from '../routes/diaryDateActivityRoutes.js'; +import diaryMemberActivityRoutes from '../routes/diaryMemberActivityRoutes.js'; +import diaryNoteRoutes from '../routes/diaryNoteRoutes.js'; +import diaryTagRoutes from '../routes/diaryTagRoutes.js'; +import groupRoutes from '../routes/groupRoutes.js'; +import matchRoutes from '../routes/matchRoutes.js'; +import memberActivityRoutes from '../routes/memberActivityRoutes.js'; +import memberRoutes from '../routes/memberRoutes.js'; +import memberNoteRoutes from '../routes/memberNoteRoutes.js'; const app = express(); @@ -19,6 +28,15 @@ app.use('/api/logs', apiLogRoutes); app.use('/api/clubs', clubRoutes); app.use('/api/clubteam', clubTeamRoutes); app.use('/api/diary', diaryRoutes); +app.use('/api/diary-date-activities', diaryDateActivityRoutes); +app.use('/api/diary-member-activities', diaryMemberActivityRoutes); +app.use('/api/diary-notes', diaryNoteRoutes); +app.use('/api/diary-tags', diaryTagRoutes); +app.use('/api/groups', groupRoutes); +app.use('/api/matches', matchRoutes); +app.use('/api/member-activities', memberActivityRoutes); +app.use('/api/members', memberRoutes); +app.use('/api/member-notes', memberNoteRoutes); app.use((err, req, res, next) => { const status = err?.status || err?.statusCode || 500; diff --git a/frontend/src/utils/debounce.js b/frontend/src/utils/debounce.js new file mode 100644 index 0000000..df8e671 --- /dev/null +++ b/frontend/src/utils/debounce.js @@ -0,0 +1,10 @@ +export function debounce(fn, wait = 300) { + let timeoutId; + return function (...args) { + const context = this; + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + fn.apply(context, args); + }, wait); + }; +} diff --git a/frontend/src/views/DiaryView.vue b/frontend/src/views/DiaryView.vue index 29eb606..6ce604b 100644 --- a/frontend/src/views/DiaryView.vue +++ b/frontend/src/views/DiaryView.vue @@ -519,11 +519,6 @@ @@ -1292,4 +1318,13 @@ li { .btn-cancel:hover { background-color: #5a6268; } + +.output ul li.active { + font-weight: 600; + color: var(--primary-color, #2b7cff); +} + +.team-league { + color: var(--text-muted, #6c757d); +} diff --git a/frontend/src/views/TournamentsView.vue b/frontend/src/views/TournamentsView.vue index 893433b..3979af6 100644 --- a/frontend/src/views/TournamentsView.vue +++ b/frontend/src/views/TournamentsView.vue @@ -670,20 +670,9 @@