Enhance diary note and tag management in backend controllers

Updated the diaryNoteController to require diaryDateId in note creation and improved error handling for missing fields. Enhanced the createTag function in diaryTagController to validate tag names and return appropriate responses. Additionally, refined the deleteTag function to ensure proper error handling when a tag is not found. These changes improve the robustness and usability of the diary management features.
This commit is contained in:
Torsten Schulz (local)
2025-11-11 08:29:18 +01:00
parent b8191e41ee
commit 20f204e70b
25 changed files with 1451 additions and 59 deletions

View File

@@ -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);
});
});

View File

@@ -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();
});
});

View File

@@ -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);
});
});

View File

@@ -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);
});
});

View File

@@ -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');
});
});

View File

@@ -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);
});
});

View File

@@ -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');
});
});

View File

@@ -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');
});
});

View File

@@ -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']);
});
});

View File

@@ -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']);
});
});

View File

@@ -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');
});
});

View File

@@ -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);
});
});

View File

@@ -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);
});
});

View File

@@ -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);
});
});

View File

@@ -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();

View File

@@ -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;