refactor(tests): remove obsolete test files and clean up package.json

- Deleted outdated test files for activity, API log, authentication, authorization, and club functionalities to streamline the test suite.
- Retained the cleanup script in package.json while removing unnecessary test dependencies, optimizing the development environment.
This commit is contained in:
Torsten Schulz (local)
2026-02-04 11:44:23 +01:00
parent 2871b79b04
commit a8470145a0
55 changed files with 1 additions and 5716 deletions

View File

@@ -7,8 +7,7 @@
"postinstall": "cd ../frontend && npm install && npm run build",
"dev": "nodemon server.js",
"cleanup:usertoken": "node ./scripts/cleanupUserTokenKeys.js",
"cleanup:indexes": "node ./scripts/cleanupAllIndexes.js",
"test": "cross-env NODE_ENV=test vitest run"
"cleanup:indexes": "node ./scripts/cleanupAllIndexes.js"
},
"keywords": [],
"author": "",
@@ -39,9 +38,6 @@
"devDependencies": {
"cross-env": "^7.0.3",
"nodemon": "^3.1.4",
"sqlite3": "^5.0.2",
"supertest": "^7.1.1",
"vitest": "^4.0.8",
"vue-eslint-parser": "9.4.3"
}
}

View File

@@ -1,63 +0,0 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import request from 'supertest';
vi.mock('../services/emailService.js', () => ({
sendActivationEmail: vi.fn().mockResolvedValue(),
}));
import app from './testApp.js';
import sequelize from '../database.js';
import User from '../models/User.js';
import Club from '../models/Club.js';
import UserClub from '../models/UserClub.js';
import DiaryDate from '../models/DiaryDates.js';
const registerAndActivate = async (email) => {
const password = 'Test123!';
await request(app).post('/api/auth/register').send({ email, password });
const user = await User.findOne({ where: { email } });
await user.update({ isActive: true });
return { user, credentials: { email, password } };
};
const loginAndGetToken = async (credentials) => {
const response = await request(app).post('/api/auth/login').send(credentials);
return response.body.token;
};
describe('Activity Routes', () => {
beforeEach(async () => {
await sequelize.sync({ force: true });
});
it('erstellt und liest Aktivitäten', async () => {
const { user, credentials } = await registerAndActivate('activity@example.com');
const token = await loginAndGetToken(credentials);
const club = await Club.create({ name: 'Activity Club' });
await UserClub.create({ userId: user.id, clubId: club.id, role: 'admin', approved: true, isOwner: true });
const diaryDate = await DiaryDate.create({ clubId: club.id, date: new Date(), description: 'Training' });
const createResponse = await request(app)
.post('/api/activities/add')
.set('Authorization', `Bearer ${token}`)
.send({ diaryDateId: diaryDate.id, description: 'Koordinationsübungen' });
expect(createResponse.status).toBe(201);
const listResponse = await request(app)
.get(`/api/activities/${diaryDate.id}`)
.set('Authorization', `Bearer ${token}`);
expect(listResponse.status).toBe(200);
expect(listResponse.body).toHaveLength(1);
expect(listResponse.body[0]).toMatchObject({ description: 'Koordinationsübungen' });
});
it('verweigert Anlage einer Aktivität ohne Token', async () => {
const response = await request(app)
.post('/api/activities/add')
.send({ diaryDateId: 1, description: 'Test' });
expect(response.status).toBe(401);
});
});

View File

@@ -1,41 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import sequelize from '../database.js';
import { addActivity, getActivities } from '../controllers/activityController.js';
import { buildMockRequest, buildMockResponse } from './testUtils.js';
import Activity from '../models/Activity.js';
import { createClub, createDiaryDate } from './utils/factories.js';
describe('activityController', () => {
beforeEach(async () => {
await sequelize.sync({ force: true });
});
it('fügt eine Aktivität hinzu', async () => {
const club = await createClub({ name: 'Activity Club' });
const diaryDate = await createDiaryDate(club.id);
const req = buildMockRequest({ body: { diaryDateId: diaryDate.id, description: 'Koordination' } });
const res = buildMockResponse();
await addActivity(req, res);
expect(res.status).toHaveBeenCalledWith(201);
const stored = await Activity.findOne({ where: { diaryDateId: diaryDate.id } });
expect(stored.description).toBe('Koordination');
});
it('liefert Aktivitäten für einen Trainingstag', async () => {
const club = await createClub({ name: 'Activity Club' });
const diaryDate = await createDiaryDate(club.id);
await Activity.create({ diaryDateId: diaryDate.id, description: 'Aufwärmen' });
const req = buildMockRequest({ params: { diaryDateId: diaryDate.id } });
const res = buildMockResponse();
await getActivities(req, res);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json.mock.calls[0][0]).toHaveLength(1);
});
});

View File

@@ -1,92 +0,0 @@
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 ApiLog from '../models/ApiLog.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;
};
describe('API Log Routes', () => {
beforeEach(async () => {
await sequelize.sync({ force: true });
});
it('listet Logs auf und filtert sie', async () => {
const { user, credentials } = await registerAndActivate('logs@example.com');
const token = await loginAndGetToken(credentials);
await ApiLog.bulkCreate([
{ method: 'GET', path: '/foo', statusCode: 200, logType: 'api_request' },
{ method: 'POST', path: '/bar', statusCode: 500, logType: 'api_request' }
]);
const response = await request(app)
.get('/api/logs?method=POST')
.set('Authorization', `Bearer ${token}`);
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.data.total).toBe(1);
expect(response.body.data.logs[0].path).toBe('/bar');
});
it('liefert einen Logeintrag per ID', async () => {
const { credentials } = await registerAndActivate('singlelog@example.com');
const token = await loginAndGetToken(credentials);
const log = await ApiLog.create({ method: 'GET', path: '/single', logType: 'api_request' });
const response = await request(app)
.get(`/api/logs/${log.id}`)
.set('Authorization', `Bearer ${token}`);
expect(response.status).toBe(200);
expect(response.body.data.id).toBe(log.id);
});
it('gibt 404 zurück, wenn ein Log nicht existiert', async () => {
const { credentials } = await registerAndActivate('missinglog@example.com');
const token = await loginAndGetToken(credentials);
const response = await request(app)
.get('/api/logs/999')
.set('Authorization', `Bearer ${token}`);
expect(response.status).toBe(404);
});
it('liefert Scheduler-Informationen', async () => {
const { credentials } = await registerAndActivate('scheduler@example.com');
const token = await loginAndGetToken(credentials);
await ApiLog.create({
method: 'SCHEDULER',
path: '/scheduler/rating_updates',
statusCode: 200,
responseBody: JSON.stringify({ updatedCount: 2 }),
logType: 'scheduler',
schedulerJobType: 'rating_updates'
});
const response = await request(app)
.get('/api/logs/scheduler/last-executions')
.set('Authorization', `Bearer ${token}`);
expect(response.status).toBe(200);
expect(response.body.data.rating_updates.lastRun).toBeTruthy();
});
});

View File

@@ -1,72 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import sequelize from '../database.js';
import ApiLog from '../models/ApiLog.js';
import apiLogService from '../services/apiLogService.js';
describe('apiLogService', () => {
beforeEach(async () => {
await sequelize.sync({ force: true });
});
it('speichert API-Requests und kürzt lange Felder', async () => {
const longBody = 'x'.repeat(65000);
await apiLogService.logRequest({
method: 'POST',
path: '/api/test',
requestBody: longBody,
responseBody: longBody,
});
const stored = await ApiLog.findOne({ where: { path: '/api/test' } });
expect(stored).toBeTruthy();
expect(stored.requestBody.length).toBeLessThanOrEqual(64020);
expect(stored.responseBody.endsWith('(truncated)')).toBe(true);
});
it('filtert Logs nach Methode und Status', async () => {
await ApiLog.bulkCreate([
{ method: 'GET', path: '/one', statusCode: 200, logType: 'api_request' },
{ method: 'POST', path: '/two', statusCode: 500, logType: 'api_request' }
]);
const result = await apiLogService.getLogs({ method: 'POST', statusCode: 500 });
expect(result.total).toBe(1);
expect(result.logs[0].path).toBe('/two');
});
it('liefert Logs paginiert', async () => {
await ApiLog.bulkCreate(new Array(5).fill(null).map((_, idx) => ({
method: 'GET',
path: `/paged-${idx}`,
statusCode: 200,
logType: 'api_request'
})));
const result = await apiLogService.getLogs({ limit: 2, offset: 2 });
expect(result.logs).toHaveLength(2);
expect(result.total).toBe(5);
});
it('gibt einen Logeintrag per ID zurück', async () => {
const log = await ApiLog.create({ method: 'GET', path: '/id-test', logType: 'api_request' });
const fetched = await apiLogService.getLogById(log.id);
expect(fetched).toBeTruthy();
expect(fetched.path).toBe('/id-test');
});
it('liefert Scheduler-Ausführungen aggregiert', async () => {
await apiLogService.logSchedulerExecution('rating_updates', true, { updatedCount: 3 }, 120, null);
await apiLogService.logSchedulerExecution('match_results', false, { fetchedCount: 2 }, 300, 'Timeout');
const results = await apiLogService.getLastSchedulerExecutions();
expect(results.rating_updates.lastRun).toBeTruthy();
expect(results.rating_updates.updatedCount).toBe(3);
expect(results.match_results.success).toBe(false);
expect(results.match_results.errorMessage).toBe('Timeout');
});
});

View File

@@ -1,60 +0,0 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import jwt from 'jsonwebtoken';
import { authenticate } from '../middleware/authMiddleware.js';
import sequelize from '../database.js';
import User from '../models/User.js';
import UserToken from '../models/UserToken.js';
const createRes = () => {
const res = {};
res.status = vi.fn().mockReturnValue(res);
res.json = vi.fn().mockReturnValue(res);
return res;
};
describe('authenticate middleware', () => {
beforeEach(async () => {
await sequelize.sync({ force: true });
});
it('antwortet mit 401 wenn kein Token vorhanden ist', async () => {
const req = { headers: {} };
const res = createRes();
const next = vi.fn();
await authenticate(req, res, next);
expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith({ error: 'Unauthorized: Token fehlt' });
expect(next).not.toHaveBeenCalled();
});
it('verweigert ungültige Tokens', async () => {
const req = { headers: { authorization: 'Bearer invalid' } };
const res = createRes();
const next = vi.fn();
await authenticate(req, res, next);
expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalled();
expect(next).not.toHaveBeenCalled();
});
it('akzeptiert gültige Tokens und ruft next()', async () => {
const user = await User.create({ email: 'middleware@example.com', password: 'Secret!123', isActive: true });
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, { expiresIn: '1h' });
await UserToken.create({ userId: user.id, token, expiresAt: new Date(Date.now() + 3600000) });
const req = { headers: { authorization: `Bearer ${token}` } };
const res = createRes();
const next = vi.fn();
await authenticate(req, res, next);
expect(next).toHaveBeenCalledOnce();
expect(req.user).toMatchObject({ id: user.id });
expect(res.status).not.toHaveBeenCalled();
});
});

View File

@@ -1,161 +0,0 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import request from 'supertest';
vi.mock('../services/emailService.js', () => ({
sendActivationEmail: vi.fn().mockResolvedValue(),
}));
import app from './testApp.js';
import sequelize from '../database.js';
import User from '../models/User.js';
import Club from '../models/Club.js';
import UserClub from '../models/UserClub.js';
import UserToken from '../models/UserToken.js';
const registerPayload = (email = 'test@example.com', password = 'Passwort!123') => ({ email, password });
describe('Auth & Permissions Routes', () => {
beforeEach(async () => {
vi.clearAllMocks();
await sequelize.sync({ force: true });
});
it('registriert einen Nutzer über die API', async () => {
const response = await request(app)
.post('/api/auth/register')
.send(registerPayload());
expect(response.status).toBe(201);
expect(response.body.email).toBe('test@example.com');
const user = await User.findOne({ where: { email: 'test@example.com' } });
expect(user).not.toBeNull();
});
it('meldet einen aktiven Nutzer an und liefert einen Token', async () => {
const credentials = registerPayload('login@example.com');
await request(app).post('/api/auth/register').send(credentials);
const user = await User.findOne({ where: { email: credentials.email } });
await user.update({ isActive: true });
const response = await request(app)
.post('/api/auth/login')
.send(credentials);
expect(response.status).toBe(200);
expect(response.body.token).toBeTruthy();
});
it('verhindert Login ohne Aktivierung', async () => {
const credentials = registerPayload('inactive@example.com');
await request(app).post('/api/auth/register').send(credentials);
const response = await request(app)
.post('/api/auth/login')
.send(credentials);
expect(response.status).toBe(403);
expect(response.body.message || response.body.error).toMatch(/aktiviert/i);
});
it('verweigert doppelte Registrierung', async () => {
const payload = registerPayload('duplicate@example.com');
await request(app).post('/api/auth/register').send(payload);
const response = await request(app).post('/api/auth/register').send(payload);
expect(response.status).toBe(409);
expect(response.body.error || response.body.message).toMatch(/bereits/i);
});
it('aktiviert einen Benutzer über die API', async () => {
const payload = registerPayload('activate@example.com');
await request(app).post('/api/auth/register').send(payload);
const user = await User.findOne({ where: { email: payload.email } });
const response = await request(app).get(`/api/auth/activate/${user.activationCode}`);
expect(response.status).toBe(200);
const reloaded = await user.reload();
expect(reloaded.isActive).toBe(true);
});
it('meldet Fehler bei ungültigem Aktivierungscode', async () => {
const response = await request(app).get('/api/auth/activate/invalid-code');
expect(response.status).toBe(404);
});
it('meldet einen Nutzer ab und entfernt den Token', async () => {
const credentials = registerPayload('logout-route@example.com');
await request(app).post('/api/auth/register').send(credentials);
const user = await User.findOne({ where: { email: credentials.email } });
await user.update({ isActive: true });
const login = await request(app).post('/api/auth/login').send(credentials);
const token = login.body.token;
const response = await request(app)
.post('/api/auth/logout')
.set('Authorization', `Bearer ${token}`);
expect(response.status).toBe(200);
const tokenRecord = await UserToken.findOne({ where: { token } });
expect(tokenRecord).toBeNull();
});
it('ändert Rollen über die Permissions-API (Admin)', async () => {
const ownerPassword = 'OwnerPass!1';
const memberPassword = 'MemberPass!1';
const owner = await User.create({ email: 'owner@example.com', password: ownerPassword, isActive: true });
const member = await User.create({ email: 'member@example.com', password: memberPassword, isActive: true });
const club = await Club.create({ name: 'Functional Club' });
await UserClub.bulkCreate([
{ userId: owner.id, clubId: club.id, role: 'admin', approved: true, isOwner: true },
{ userId: member.id, clubId: club.id, role: 'member', approved: true },
]);
const loginResponse = await request(app)
.post('/api/auth/login')
.send({ email: owner.email, password: ownerPassword });
const token = loginResponse.body.token;
expect(token).toBeTruthy();
const updateResponse = await request(app)
.put(`/api/permissions/${club.id}/user/${member.id}/role`)
.set('Authorization', `Bearer ${token}`)
.send({ role: 'trainer' });
expect(updateResponse.status).toBe(200);
const updated = await UserClub.findOne({ where: { userId: member.id, clubId: club.id } });
expect(updated.role).toBe('trainer');
});
it('verweigert Berechtigungsänderungen ohne ausreichende Rolle', async () => {
const ownerPassword = 'OwnerPass!1';
const memberPassword = 'MemberPass!1';
const owner = await User.create({ email: 'owner2@example.com', password: ownerPassword, isActive: true });
const member = await User.create({ email: 'member2@example.com', password: memberPassword, isActive: true });
const club = await Club.create({ name: 'Restricted Club' });
await UserClub.bulkCreate([
{ userId: owner.id, clubId: club.id, role: 'admin', approved: true, isOwner: true },
{ userId: member.id, clubId: club.id, role: 'member', approved: true },
]);
const memberLogin = await request(app)
.post('/api/auth/login')
.send({ email: member.email, password: memberPassword });
const token = memberLogin.body.token;
const response = await request(app)
.put(`/api/permissions/${club.id}/user/${owner.id}/role`)
.set('Authorization', `Bearer ${token}`)
.send({ role: 'trainer' });
expect(response.status).toBe(403);
});
});

View File

@@ -1,99 +0,0 @@
import { describe, it, expect, beforeAll, beforeEach, vi } from 'vitest';
vi.mock('../services/emailService.js', () => ({
sendActivationEmail: vi.fn().mockResolvedValue(),
}));
import sequelize from '../database.js';
import User from '../models/User.js';
import UserToken from '../models/UserToken.js';
import { register, activateUser, login, logout } from '../services/authService.js';
describe('authService', () => {
beforeAll(async () => {
await sequelize.sync({ force: true });
});
beforeEach(async () => {
await sequelize.truncate({ cascade: true, restartIdentity: true });
});
it('registriert einen neuen Nutzer und sendet eine Aktivierungs-E-Mail', async () => {
const email = 'unit@test.de';
const password = 'Test123!';
const user = await register(email, password);
expect(user).toBeTruthy();
const storedUser = await User.findOne({ where: { email } });
expect(storedUser).not.toBeNull();
expect(storedUser.password).not.toBe(password);
expect(storedUser.activationCode).toBeTruthy();
});
it('aktiviert einen Benutzer mit gültigem Aktivierungscode', async () => {
const email = 'activate@test.de';
const password = 'Test123!';
const user = await register(email, password);
const activated = await activateUser(user.activationCode);
expect(activated.isActive).toBe(true);
expect(activated.activationCode).toBeNull();
});
it('meldet einen Fehler bei ungültigem Aktivierungscode', async () => {
await expect(activateUser('does-not-exist')).rejects.toMatchObject({ status: 404 });
});
it('meldet einen Benutzer an und speichert den Token', async () => {
const email = 'login@test.de';
const password = 'Passwort!7';
const user = await register(email, password);
await user.update({ isActive: true });
const result = await login(email, password);
expect(result.token).toBeTruthy();
const tokenRecord = await UserToken.findOne({ where: { token: result.token } });
expect(tokenRecord).not.toBeNull();
});
it('verhindert Login solange der Account nicht aktiviert wurde', async () => {
const email = 'inactive@test.de';
const password = 'Test123!';
await register(email, password);
await expect(login(email, password)).rejects.toMatchObject({ status: 403 });
});
it('verhindert doppelte Registrierung', async () => {
const email = 'duplicate@test.de';
const password = 'Test123!';
await register(email, password);
await expect(register(email, password)).rejects.toMatchObject({ status: 409 });
});
it('wirft einen Fehler bei ungültigen Anmeldedaten', async () => {
await expect(login('unknown@test.de', 'wrong')).rejects.toMatchObject({ status: 401 });
});
it('löscht den UserToken beim Logout', async () => {
const email = 'logout@test.de';
const password = 'Passwort!8';
const user = await register(email, password);
await user.update({ isActive: true });
const { token } = await login(email, password);
const response = await logout(token);
expect(response).toMatchObject({ message: 'Logout erfolgreich' });
const tokenRecord = await UserToken.findOne({ where: { token } });
expect(tokenRecord).toBeNull();
});
it('meldet Fehler beim Logout ohne Token', async () => {
await expect(logout()).rejects.toMatchObject({ status: 400 });
});
});

View File

@@ -1,68 +0,0 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { authorize } from '../middleware/authorizationMiddleware.js';
import sequelize from '../database.js';
import User from '../models/User.js';
import Club from '../models/Club.js';
import UserClub from '../models/UserClub.js';
const createRes = () => {
const res = {};
res.status = vi.fn().mockReturnValue(res);
res.json = vi.fn().mockReturnValue(res);
return res;
};
describe('authorization middleware', () => {
beforeEach(async () => {
await sequelize.sync({ force: true });
});
it('gibt 400 zurück wenn clubId fehlt', async () => {
const req = { user: { id: 1 }, params: {}, body: {}, query: {} };
const res = createRes();
const next = vi.fn();
await authorize('members', 'read')(req, res, next);
expect(res.status).toHaveBeenCalledWith(400);
expect(next).not.toHaveBeenCalled();
});
it('verweigert Zugriff ohne Berechtigungen', async () => {
const owner = await User.create({ email: 'owner-mw@example.com', password: 'Secret!123', isActive: true });
const member = await User.create({ email: 'member-mw@example.com', password: 'Secret!123', isActive: true });
const club = await Club.create({ name: 'Middleware Club' });
await UserClub.bulkCreate([
{ userId: owner.id, clubId: club.id, role: 'admin', approved: true, isOwner: true },
{ userId: member.id, clubId: club.id, role: 'member', approved: true },
]);
const req = { user: { id: member.id }, params: { clubId: club.id }, body: {}, query: {} };
const res = createRes();
const next = vi.fn();
await authorize('permissions', 'write')(req, res, next);
expect(res.status).toHaveBeenCalledWith(403);
expect(next).not.toHaveBeenCalled();
});
it('erlaubt Besitzern den Zugriff', async () => {
const owner = await User.create({ email: 'owner-pass@example.com', password: 'Secret!123', isActive: true });
const club = await Club.create({ name: 'Owner Club' });
await UserClub.create({ userId: owner.id, clubId: club.id, role: 'admin', approved: true, isOwner: true });
const req = { user: { id: owner.id }, params: { clubId: club.id }, body: {}, query: {} };
const res = createRes();
const next = vi.fn();
await authorize('permissions', 'write')(req, res, next);
expect(next).toHaveBeenCalledOnce();
expect(res.status).not.toHaveBeenCalled();
expect(req.userPermissions).toBeTruthy();
});
});

View File

@@ -1,135 +0,0 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import request from 'supertest';
vi.mock('../services/emailService.js', () => ({
sendActivationEmail: vi.fn().mockResolvedValue(),
}));
import app from './testApp.js';
import sequelize from '../database.js';
import User from '../models/User.js';
import UserClub from '../models/UserClub.js';
const registerAndActivate = async (email) => {
const password = 'Test123!';
await request(app).post('/api/auth/register').send({ email, password });
const user = await User.findOne({ where: { email } });
await user.update({ isActive: true });
return { user, credentials: { email, password } };
};
const loginAndGetToken = async (credentials) => {
const response = await request(app).post('/api/auth/login').send(credentials);
return response.body.token;
};
const authHeaders = (token) => ({
Authorization: `Bearer ${token}`,
authcode: token,
});
describe('Club Routes', () => {
beforeEach(async () => {
await sequelize.sync({ force: true });
});
it('legt einen Club an und gibt ihn in der Liste zurück', async () => {
const { credentials } = await registerAndActivate('owner@example.com');
const token = await loginAndGetToken(credentials);
const createResponse = await request(app)
.post('/api/clubs')
.set(authHeaders(token))
.send({ name: 'Neuer Club' });
expect(createResponse.status).toBe(200);
const listResponse = await request(app)
.get('/api/clubs')
.set(authHeaders(token));
expect(listResponse.status).toBe(200);
expect(listResponse.body.some((club) => club.name === 'Neuer Club')).toBe(true);
});
it('verhindert doppelte Club-Namen', async () => {
const { credentials } = await registerAndActivate('duplicate@example.com');
const token = await loginAndGetToken(credentials);
await request(app).post('/api/clubs').set(authHeaders(token)).send({ name: 'Doppelclub' });
const response = await request(app)
.post('/api/clubs')
.set(authHeaders(token))
.send({ name: 'Doppelclub' });
expect(response.status).toBe(409);
});
it('liefert einen Club nur für Mitglieder', async () => {
const { user: owner, credentials } = await registerAndActivate('clubowner@example.com');
const ownerToken = await loginAndGetToken(credentials);
const createResponse = await request(app)
.post('/api/clubs')
.set(authHeaders(ownerToken))
.send({ name: 'Private Club' });
const clubId = createResponse.body.id;
const ownerResponse = await request(app)
.get(`/api/clubs/${clubId}`)
.set(authHeaders(ownerToken));
expect(ownerResponse.status).toBe(200);
const { credentials: otherCreds } = await registerAndActivate('visitor@example.com');
const otherToken = await loginAndGetToken(otherCreds);
const otherResponse = await request(app)
.get(`/api/clubs/${clubId}`)
.set(authHeaders(otherToken));
expect(otherResponse.status).toBe(403);
});
it('bearbeitet Zugangs-Anfragen (request/pending/approve/reject)', async () => {
const { user: owner, credentials } = await registerAndActivate('owner2@example.com');
const ownerToken = await loginAndGetToken(credentials);
const createResponse = await request(app)
.post('/api/clubs')
.set(authHeaders(ownerToken))
.send({ name: 'Approval Club' });
const clubId = createResponse.body.id;
const { user: member, credentials: memberCreds } = await registerAndActivate('member@example.com');
const memberToken = await loginAndGetToken(memberCreds);
const requestResponse = await request(app)
.get(`/api/clubs/request/${clubId}`)
.set(authHeaders(memberToken));
expect(requestResponse.status).toBe(200);
const pendingResponse = await request(app)
.get(`/api/clubs/pending/${clubId}`)
.set(authHeaders(ownerToken));
expect(pendingResponse.status).toBe(200);
expect(pendingResponse.body.length).toBe(1);
const approveResponse = await request(app)
.post('/api/clubs/approve')
.set(authHeaders(ownerToken))
.send({ clubid: clubId, userid: member.id });
expect(approveResponse.status).toBe(200);
const membership = await UserClub.findOne({ where: { userId: member.id, clubId } });
expect(membership.approved).toBe(true);
const rejectUser = await registerAndActivate('reject@example.com');
const rejectToken = await loginAndGetToken(rejectUser.credentials);
await request(app).get(`/api/clubs/request/${clubId}`).set(authHeaders(rejectToken));
const rejectResponse = await request(app)
.post('/api/clubs/reject')
.set(authHeaders(ownerToken))
.send({ clubid: clubId, userid: rejectUser.user.id });
expect(rejectResponse.status).toBe(200);
const rejectedMembership = await UserClub.findOne({ where: { userId: rejectUser.user.id, clubId } });
expect(rejectedMembership).toBeNull();
});
});

View File

@@ -1,90 +0,0 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import sequelize from '../database.js';
import Club from '../models/Club.js';
import User from '../models/User.js';
import UserClub from '../models/UserClub.js';
import clubService from '../services/clubService.js';
vi.mock('../utils/userUtils.js', async () => {
const actual = await vi.importActual('../utils/userUtils.js');
return {
...actual,
checkAccess: vi.fn().mockResolvedValue(true)
};
});
import { checkAccess } from '../utils/userUtils.js';
describe('clubService', () => {
beforeEach(async () => {
await sequelize.sync({ force: true });
vi.clearAllMocks();
});
it('erstellt Clubs und sucht nach Namen', async () => {
await clubService.createClub('Testclub');
const result = await clubService.findClubByName('test');
expect(result).toBeTruthy();
expect(result.name).toContain('Testclub');
});
it('fügt Benutzer einem Club hinzu', async () => {
const user = await User.create({ email: 'club@test.de', password: 'Secret123!', isActive: true });
const club = await clubService.createClub('Club A');
const userClub = await clubService.addUserToClub(user.id, club.id, true);
expect(userClub.isOwner).toBe(true);
expect(userClub.role).toBe('admin');
});
it('beantragt Club-Zugang und verhindert doppelte Anfragen', async () => {
const user = await User.create({ email: 'member@test.de', password: 'Secret123!', isActive: true });
const club = await clubService.createClub('Club B');
await clubService.requestAccessToClub(user.id, club.id);
await expect(clubService.requestAccessToClub(user.id, club.id)).rejects.toThrow('alreadyrequested');
});
it('genehmigt Club-Zugänge nach erfolgreicher Prüfung', async () => {
const owner = await User.create({ email: 'owner@test.de', password: 'Secret123!', isActive: true });
const member = await User.create({ email: 'member@test.de', password: 'Secret123!', isActive: true });
const club = await clubService.createClub('Club C');
await UserClub.create({ userId: owner.id, clubId: club.id, approved: true, isOwner: true });
await UserClub.create({ userId: member.id, clubId: club.id, approved: false });
await clubService.approveUserClubAccess('token', club.id, member.id);
expect(checkAccess).toHaveBeenCalledWith('token', club.id);
const updated = await UserClub.findOne({ where: { userId: member.id, clubId: club.id } });
expect(updated.approved).toBe(true);
});
it('liefert ausstehende Freigaben nur bei Zugang', async () => {
const owner = await User.create({ email: 'owner@test.de', password: 'Secret123!', isActive: true });
const pending = await User.create({ email: 'pending@test.de', password: 'Secret123!', isActive: true });
const club = await clubService.createClub('Club D');
await UserClub.create({ userId: owner.id, clubId: club.id, approved: true, isOwner: true });
await UserClub.create({ userId: pending.id, clubId: club.id, approved: false });
const approvals = await clubService.getPendingUserApprovals('token', club.id);
expect(checkAccess).toHaveBeenCalledWith('token', club.id);
expect(approvals).toHaveLength(1);
expect(approvals[0].userId).toBe(pending.id);
});
it('lehnt Club-Zugänge ab', async () => {
const owner = await User.create({ email: 'owner@test.de', password: 'Secret123!', isActive: true });
const pending = await User.create({ email: 'pending@test.de', password: 'Secret123!', isActive: true });
const club = await clubService.createClub('Club E');
await UserClub.create({ userId: owner.id, clubId: club.id, approved: true, isOwner: true });
await UserClub.create({ userId: pending.id, clubId: club.id, approved: false });
await clubService.rejectUserClubAccess('token', club.id, pending.id);
const record = await UserClub.findOne({ where: { userId: pending.id, clubId: club.id } });
expect(record).toBeNull();
});
});

View File

@@ -1,119 +0,0 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import request from 'supertest';
vi.mock('../services/emailService.js', () => ({
sendActivationEmail: vi.fn().mockResolvedValue(),
}));
import app from './testApp.js';
import sequelize from '../database.js';
import '../models/index.js';
import ClubTeam from '../models/ClubTeam.js';
import League from '../models/League.js';
import Season from '../models/Season.js';
import Club from '../models/Club.js';
import User from '../models/User.js';
import UserClub from '../models/UserClub.js';
const registerAndActivate = async (email) => {
const password = 'Test123!';
await request(app).post('/api/auth/register').send({ email, password });
const user = await User.findOne({ where: { email } });
await user.update({ isActive: true });
return { user, credentials: { email, password } };
};
const loginAndGetToken = async (credentials) => {
const response = await request(app).post('/api/auth/login').send(credentials);
return response.body.token;
};
const authHeaders = (token) => ({
Authorization: `Bearer ${token}`,
authcode: token,
});
describe('ClubTeam Routes', () => {
beforeEach(async () => {
await sequelize.sync({ force: true });
});
it('erstellt ClubTeams und listet sie auf', async () => {
const { user, credentials } = await registerAndActivate('clubteam@example.com');
const token = await loginAndGetToken(credentials);
const clubResponse = await request(app)
.post('/api/clubs')
.set(authHeaders(token))
.send({ name: 'Team Club' });
const clubId = clubResponse.body.id;
const createResponse = await request(app)
.post(`/api/clubteam/club/${clubId}`)
.set(authHeaders(token))
.send({ name: 'Erstes Team' });
expect(createResponse.status).toBe(201);
const listResponse = await request(app)
.get(`/api/clubteam/club/${clubId}`)
.set(authHeaders(token));
expect(listResponse.status).toBe(200);
expect(listResponse.body[0].name).toBe('Erstes Team');
});
it('aktualisiert und löscht ClubTeams', async () => {
const { credentials } = await registerAndActivate('clubteam2@example.com');
const token = await loginAndGetToken(credentials);
const clubResponse = await request(app)
.post('/api/clubs')
.set(authHeaders(token))
.send({ name: 'Club Zwei' });
const clubId = clubResponse.body.id;
const teamResponse = await request(app)
.post(`/api/clubteam/club/${clubId}`)
.set(authHeaders(token))
.send({ name: 'Team Alt' });
const teamId = teamResponse.body.id;
const updateResponse = await request(app)
.put(`/api/clubteam/${teamId}`)
.set(authHeaders(token))
.send({ name: 'Team Neu' });
expect(updateResponse.status).toBe(200);
expect(updateResponse.body.name).toBe('Team Neu');
const deleteResponse = await request(app)
.delete(`/api/clubteam/${teamId}`)
.set(authHeaders(token));
expect(deleteResponse.status).toBe(200);
});
it('liefert Ligen für einen Club', async () => {
const { credentials } = await registerAndActivate('clubteam3@example.com');
const token = await loginAndGetToken(credentials);
const clubResponse = await request(app)
.post('/api/clubs')
.set(authHeaders(token))
.send({ name: 'Club Drei' });
const clubId = clubResponse.body.id;
const season = await Season.create({ season: '2024/2025' });
await League.create({ name: 'Verbandsliga', clubId, seasonId: season.id, association: 'BV', groupname: 'Nord' });
const response = await request(app)
.get(`/api/clubteam/leagues/${clubId}?seasonid=${season.id}`)
.set(authHeaders(token));
expect(response.status).toBe(200);
expect(response.body[0].name).toBe('Verbandsliga');
});
});

View File

@@ -1,104 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import sequelize from '../database.js';
import '../models/index.js';
import Club from '../models/Club.js';
import ClubTeam from '../models/ClubTeam.js';
import League from '../models/League.js';
import Season from '../models/Season.js';
import clubTeamService from '../services/clubTeamService.js';
describe('clubTeamService', () => {
beforeEach(async () => {
await sequelize.sync({ force: true });
});
it('liefert ClubTeams mit Liga- und Saisoninformationen', async () => {
const club = await Club.create({ name: 'Testclub' });
const season = await Season.create({ season: '2024/2025' });
const league = await League.create({
name: '1. Liga',
clubId: club.id,
seasonId: season.id,
association: 'BV',
groupname: 'Nord',
});
await ClubTeam.create({
name: 'Team A',
clubId: club.id,
seasonId: season.id,
leagueId: league.id,
myTischtennisTeamId: 'MT-1',
});
const teams = await clubTeamService.getAllClubTeamsByClub(club.id, season.id);
expect(teams).toHaveLength(1);
expect(teams[0].league.name).toBe('1. Liga');
expect(teams[0].season.season).toBe('2024/2025');
});
it('erstellt neue ClubTeams und weist die aktuelle Saison zu', async () => {
const club = await Club.create({ name: 'Neuer Club' });
const team = await clubTeamService.createClubTeam({
name: 'Frisches Team',
clubId: club.id,
});
expect(team.id).toBeTruthy();
expect(team.seasonId).toBeTruthy();
});
it('aktualisiert ClubTeams', async () => {
const club = await Club.create({ name: 'Update Club' });
const season = await Season.create({ season: '2023/2024' });
const team = await ClubTeam.create({
name: 'Team Alt',
clubId: club.id,
seasonId: season.id,
});
const updated = await clubTeamService.updateClubTeam(team.id, { name: 'Team Neu' });
expect(updated).toBe(true);
const refreshed = await ClubTeam.findByPk(team.id);
expect(refreshed.name).toBe('Team Neu');
});
it('löscht ClubTeams und bestätigt die Entfernung', async () => {
const club = await Club.create({ name: 'Delete Club' });
const season = await Season.create({ season: '2022/2023' });
const team = await ClubTeam.create({
name: 'Team Delete',
clubId: club.id,
seasonId: season.id,
});
const removed = await clubTeamService.deleteClubTeam(team.id);
expect(removed).toBe(true);
const exists = await ClubTeam.findByPk(team.id);
expect(exists).toBeNull();
});
it('liefert Ligen eines Clubs für die aktuelle Saison', async () => {
const club = await Club.create({ name: 'Liga Club' });
const season = await Season.create({ season: '2025/2026' });
await League.create({
name: 'Regionalliga',
clubId: club.id,
seasonId: season.id,
association: 'TT',
groupname: 'Süd',
});
const leagues = await clubTeamService.getLeaguesByClub(club.id, season.id);
expect(leagues).toHaveLength(1);
expect(leagues[0].name).toBe('Regionalliga');
});
});

View File

@@ -1,117 +0,0 @@
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

@@ -1,183 +0,0 @@
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',
imagePath: '/uploads/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

@@ -1,129 +0,0 @@
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

@@ -1,106 +0,0 @@
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 DiaryNote from '../models/DiaryNote.js';
import User from '../models/User.js';
import { createMember } from './utils/factories.js';
import { setupRouteAuthMocks } from './utils/routeAuthMocks.js';
setupRouteAuthMocks();
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('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, {
firstName: 'Note',
lastName: 'Tester',
birthDate: '2000-01-01',
email: 'note.member@example.com',
gender: 'female',
});
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

@@ -1,164 +0,0 @@
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 { setupRouteAuthMocks } from './utils/routeAuthMocks.js';
setupRouteAuthMocks();
import User from '../models/User.js';
import Club from '../models/Club.js';
import DiaryDate from '../models/DiaryDates.js';
import DiaryDateActivity from '../models/DiaryDateActivity.js';
import DiaryTag from '../models/DiaryTag.js';
import DiaryDateTag from '../models/DiaryDateTag.js';
import DiaryNote from '../models/DiaryNote.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, clubId) => ({
Authorization: `Bearer ${token}`,
authcode: token,
clubid: clubId,
});
describe('Diary Routes', () => {
beforeEach(async () => {
await sequelize.sync({ force: true });
});
it('erstellt Diary-Daten und listet sie auf', async () => {
const { credentials } = await registerAndActivate('diary@example.com');
const token = await loginAndGetToken(credentials);
const clubResponse = await request(app)
.post('/api/clubs')
.set(authHeaders(token, 'clubId'))
.send({ name: 'Diary Club' });
const clubId = clubResponse.body.id;
const createResponse = await request(app)
.post(`/api/diary/${clubId}`)
.set(authHeaders(token, clubId))
.send({ date: '2025-09-01', trainingStart: '18:00', trainingEnd: '20:00' });
expect(createResponse.status).toBe(201);
const listResponse = await request(app)
.get(`/api/diary/${clubId}`)
.set(authHeaders(token, clubId));
expect(listResponse.status).toBe(200);
expect(listResponse.body).toHaveLength(1);
expect(listResponse.body[0].trainingStart).toBe('18:00');
});
it('aktualisiert Trainingszeiten eines Diary-Eintrags', async () => {
const { credentials } = await registerAndActivate('diary2@example.com');
const token = await loginAndGetToken(credentials);
const clubResponse = await request(app)
.post('/api/clubs')
.set(authHeaders(token, 'clubId'))
.send({ name: 'Update Diary Club' });
const clubId = clubResponse.body.id;
const dateResponse = await request(app)
.post(`/api/diary/${clubId}`)
.set(authHeaders(token, clubId))
.send({ date: '2025-10-01' });
const dateId = dateResponse.body.id;
const updateResponse = await request(app)
.put(`/api/diary/${clubId}`)
.set(authHeaders(token, clubId))
.send({ dateId, trainingStart: '17:00', trainingEnd: '19:00' });
expect(updateResponse.status).toBe(200);
expect(updateResponse.body.trainingStart).toBe('17:00');
});
it('verhindert das Löschen bei vorhandenen Aktivitäten', async () => {
const { credentials } = await registerAndActivate('diary3@example.com');
const token = await loginAndGetToken(credentials);
const clubResponse = await request(app)
.post('/api/clubs')
.set(authHeaders(token, 'clubId'))
.send({ name: 'Activity Diary Club' });
const clubId = clubResponse.body.id;
const dateResponse = await request(app)
.post(`/api/diary/${clubId}`)
.set(authHeaders(token, clubId))
.send({ date: '2025-11-01' });
const dateId = dateResponse.body.id;
await DiaryDateActivity.create({ diaryDateId: dateId, orderId: 1, isTimeblock: false });
const deleteResponse = await request(app)
.delete(`/api/diary/${clubId}/${dateId}`)
.set(authHeaders(token, clubId));
expect(deleteResponse.status).toBe(409);
expect(deleteResponse.body.error).toBe('Cannot delete date with activities');
});
it('verwaltet Tags über die Diary-API', async () => {
const { credentials } = await registerAndActivate('diary4@example.com');
const token = await loginAndGetToken(credentials);
const clubResponse = await request(app)
.post('/api/clubs')
.set(authHeaders(token, 'clubId'))
.send({ name: 'Tag Diary Club' });
const clubId = clubResponse.body.id;
const dateResponse = await request(app)
.post(`/api/diary/${clubId}`)
.set(authHeaders(token, clubId))
.send({ date: '2025-12-01' });
const dateId = dateResponse.body.id;
const createTagResponse = await request(app)
.post('/api/diary/tag')
.set(authHeaders(token, clubId))
.send({ clubId, diaryDateId: dateId, tagName: 'Ausdauer' });
expect(createTagResponse.status).toBe(201);
const tagId = createTagResponse.body[0].id;
const linkResponse = await request(app)
.post(`/api/diary/tag/${clubId}/add-tag`)
.set(authHeaders(token, clubId))
.send({ diaryDateId: dateId, tagId });
expect(linkResponse.status).toBe(200);
const deleteResponse = await request(app)
.delete(`/api/diary/${clubId}/tag`)
.set(authHeaders(token, clubId))
.query({ tagId });
expect(deleteResponse.status).toBe(200);
const remaining = await DiaryDateTag.findAll({ where: { diaryDateId: dateId } });
expect(remaining).toHaveLength(0);
});
});

View File

@@ -1,134 +0,0 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
vi.mock('../utils/userUtils.js', async () => {
const actual = await vi.importActual('../utils/userUtils.js');
return {
...actual,
checkAccess: vi.fn().mockResolvedValue(true),
};
});
import sequelize from '../database.js';
import '../models/index.js';
import diaryService from '../services/diaryService.js';
import Club from '../models/Club.js';
import DiaryDate from '../models/DiaryDates.js';
import DiaryDateActivity from '../models/DiaryDateActivity.js';
import DiaryDateTag from '../models/DiaryDateTag.js';
import { DiaryTag } from '../models/DiaryTag.js';
import { checkAccess } from '../utils/userUtils.js';
describe('diaryService', () => {
beforeEach(async () => {
await sequelize.sync({ force: true });
vi.clearAllMocks();
});
it('liefert alle Diary-Daten eines Clubs', async () => {
const club = await Club.create({ name: 'Diary Club' });
await DiaryDate.bulkCreate([
{ date: '2025-01-01', clubId: club.id, trainingStart: '18:00', trainingEnd: '20:00' },
{ date: '2025-01-05', clubId: club.id, trainingStart: '19:00', trainingEnd: '21:00' },
]);
const dates = await diaryService.getDatesForClub('token', club.id);
expect(checkAccess).toHaveBeenCalledWith('token', club.id);
expect(dates).toHaveLength(2);
expect(dates[0].clubId).toBe(club.id);
});
it('erstellt neue Diary-Daten und validiert Zeiten', async () => {
const club = await Club.create({ name: 'Create Club' });
const created = await diaryService.createDateForClub('token', club.id, '2025-02-02', '18:00', '20:00');
expect(created.id).toBeTruthy();
expect(created.trainingStart).toBe('18:00');
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 () => {
const club = await Club.create({ name: 'Update Club' });
const date = await DiaryDate.create({ date: '2025-03-01', clubId: club.id });
const updated = await diaryService.updateTrainingTimes('token', club.id, date.id, '17:00', '19:00');
expect(updated.trainingStart).toBe('17:00');
await expect(
diaryService.updateTrainingTimes('token', club.id, 9999, '10:00', '12:00')
).rejects.toThrow('Diary entry not found');
});
it('fügt Tags über Namen hinzu und liefert zugehörige Tag-Liste', async () => {
const club = await Club.create({ name: 'Tag Club' });
const date = await DiaryDate.create({ date: '2025-04-01', clubId: club.id });
const tags = await diaryService.addTagToDate('token', date.id, 'Intensiv');
expect(checkAccess).toHaveBeenCalledWith('token', date.id);
expect(tags.length).toBe(1);
expect(tags[0].name).toBe('Intensiv');
});
it('verknüpft bestehende Tags mit Diary-Daten', async () => {
const club = await Club.create({ name: 'Tag Link Club' });
const date = await DiaryDate.create({ date: '2025-05-01', clubId: club.id });
const tag = await DiaryTag.create({ name: 'Technik' });
const result = await diaryService.addTagToDiaryDate('token', club.id, date.id, tag.id);
expect(result).toHaveLength(1);
expect(result[0].name).toBe('Technik');
const second = await diaryService.addTagToDiaryDate('token', club.id, date.id, tag.id);
expect(second).toBeUndefined();
});
it('entfernt Tags von Diary-Daten', async () => {
const club = await Club.create({ name: 'Remove Tag Club' });
const date = await DiaryDate.create({ date: '2025-06-01', clubId: club.id });
const tag = await DiaryTag.create({ name: 'Kondition' });
await DiaryDateTag.create({ diaryDateId: date.id, tagId: tag.id });
await diaryService.removeTagFromDiaryDate('token', club.id, tag.id);
const remaining = await DiaryDateTag.findAll({ where: { diaryDateId: date.id } });
expect(remaining).toHaveLength(0);
});
it('verhindert das Löschen bei vorhandenen Aktivitäten', async () => {
const club = await Club.create({ name: 'Activity Club' });
const date = await DiaryDate.create({ date: '2025-07-01', clubId: club.id });
await DiaryDateActivity.create({ diaryDateId: date.id, orderId: 1, isTimeblock: false });
await expect(
diaryService.removeDateForClub('token', club.id, date.id)
).rejects.toThrow('Cannot delete date with activities');
});
it('löscht Diary-Daten ohne Aktivitäten', async () => {
const club = await Club.create({ name: 'Delete Diary Club' });
const date = await DiaryDate.create({ date: '2025-08-01', clubId: club.id });
const result = await diaryService.removeDateForClub('token', club.id, date.id);
expect(result).toEqual({ ok: true });
const exists = await DiaryDate.findByPk(date.id);
expect(exists).toBeNull();
});
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

@@ -1,80 +0,0 @@
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

@@ -1,78 +0,0 @@
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

@@ -1,57 +0,0 @@
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

@@ -1,85 +0,0 @@
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({ clubId, playersReady: ['Alice'], playersPlanned: ['Bob'], playersPlayed: ['Charlie'] });
expect(patchResponse.status).toBe(200);
expect(patchResponse.body.data.playersReady).toEqual(['Alice']);
});
});

View File

@@ -1,83 +0,0 @@
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

@@ -1,127 +0,0 @@
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 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';
import { createMember } from './utils/factories.js';
import { setupRouteAuthMocks } from './utils/routeAuthMocks.js';
setupRouteAuthMocks();
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 createMember(clubId, {
firstName: 'Max',
lastName: 'Mustermann',
birthDate: '2005-01-01',
email: 'max@example.com',
testMembership: true,
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: diaryDateOlder.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

@@ -1,80 +0,0 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import request from 'supertest';
import app from './testApp.js';
import sequelize from '../database.js';
import '../models/index.js';
import User from '../models/User.js';
import { createMember } from './utils/factories.js';
import { setupRouteAuthMocks } from './utils/routeAuthMocks.js';
setupRouteAuthMocks();
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('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, {
firstName: 'Nora',
lastName: 'Notiz',
birthDate: '1999-01-01',
email: 'nora@example.com',
gender: 'female',
});
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

@@ -1,103 +0,0 @@
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 { createMember } from './utils/factories.js';
import { setupRouteAuthMocks } from './utils/routeAuthMocks.js';
setupRouteAuthMocks();
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('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, {
firstName: 'Lara',
lastName: 'Lang',
birthDate: '2001-01-01',
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);
});
it('validiert Transfer-Konfiguration über die API', async () => {
const { credentials } = await registerAndActivate('membertransfer@example.com');
const token = await loginAndGetToken(credentials);
const clubResponse = await request(app)
.post('/api/clubs')
.set(authHeaders(token))
.send({ name: 'Transfer Test Club' });
const clubId = clubResponse.body.id;
const transferResponse = await request(app)
.post(`/api/members/transfer/${clubId}`)
.set(authHeaders(token))
.send({ transferTemplate: '{}' });
expect(transferResponse.status).toBe(400);
expect(transferResponse.body.error).toContain('Übertragungs-Endpoint');
});
});

View File

@@ -1,110 +0,0 @@
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';
import MemberContact from '../models/MemberContact.js';
import Club from '../models/Club.js';
import { createMember, createClub } from './utils/factories.js';
describe('memberService quick updates', () => {
beforeEach(async () => {
await sequelize.sync({ force: true });
vi.clearAllMocks();
});
it('entfernt Testmitgliedschaften und behandelt Fehlerfälle', async () => {
const club = await createClub({ name: 'Quick Update Club' });
const member = await createMember(club.id, {
firstName: 'Erika',
lastName: 'Musterfrau',
birthDate: '2000-01-01',
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 club = await createClub({ name: 'Form Club' });
const member = await createMember(club.id, {
firstName: 'Erika',
lastName: 'Musterfrau',
birthDate: '2000-01-01',
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);
});
it('legt Mitglieder mit Kontakten an und aktualisiert sie', async () => {
const club = await Club.create({ name: 'Kontakt Club' });
const createResult = await memberService.setClubMember('token', club.id, null, 'Lena', 'Lang', 'Straße 1', 'Stadt', '12345', '2002-03-04', '+49 151 000000', 'lena@example.com', true, false, false, 'female', null, null, false, [
{ type: 'phone', value: '+49 170 123456', isPrimary: true },
{ type: 'email', value: 'contact@example.com', isParent: true, parentName: 'Mutter' },
]);
expect(createResult.status).toBe(200);
const created = await Member.findOne({ where: { clubId: club.id }, include: { model: MemberContact, as: 'contacts' } });
expect(created).toBeTruthy();
expect(created.email).toBe('lena@example.com');
expect(created.contacts).toHaveLength(2);
const updateResult = await memberService.setClubMember('token', club.id, created.id, 'Lena', 'Lang', 'Neue Straße', 'Neue Stadt', '54321', '2002-03-04', '+49 89 123', 'lena@example.com', true, false, false, 'female', null, null, true, [
{ type: 'phone', value: '+49 89 999', isPrimary: true },
]);
expect(updateResult.status).toBe(200);
await created.reload({ include: { model: MemberContact, as: 'contacts' } });
expect(created.street).toBe('Neue Straße');
expect(created.memberFormHandedOver).toBe(true);
expect(created.contacts).toHaveLength(1);
});
it('filtert inaktive Mitglieder standardmäßig heraus', async () => {
const club = await Club.create({ name: 'Filter Club' });
await createMember(club.id, { active: true, email: 'active@example.com' });
await createMember(club.id, { active: false, email: 'inactive@example.com' });
const onlyActive = await memberService.getClubMembers('token', club.id, 'false');
expect(onlyActive.some((m) => m.email === 'active@example.com')).toBe(true);
expect(onlyActive.some((m) => m.email === 'inactive@example.com')).toBe(false);
const allMembers = await memberService.getClubMembers('token', club.id, 'true');
expect(allMembers.some((m) => m.email === 'inactive@example.com')).toBe(true);
});
});

View File

@@ -1,77 +0,0 @@
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 MemberTransferConfig from '../models/MemberTransferConfig.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('MemberTransferConfig Routes', () => {
beforeEach(async () => {
await sequelize.sync({ force: true });
});
it('verwaltet die Transfer-Konfiguration eines Vereins', async () => {
const { credentials } = await registerAndActivate('configroutes@example.com');
const token = await loginAndGetToken(credentials);
const clubResponse = await request(app)
.post('/api/clubs')
.set(authHeaders(token))
.send({ name: 'Config Route Club' });
const clubId = clubResponse.body.id;
const saveResponse = await request(app)
.post(`/api/member-transfer-config/${clubId}`)
.set(authHeaders(token))
.send({
server: 'https://api.example.com',
loginEndpoint: '/login',
loginFormat: 'json',
loginCredentials: { username: 'user', password: 'secret' },
transferEndpoint: '/transfer',
transferTemplate: '{"name":"{{fullName}}"}'
});
expect(saveResponse.status).toBe(200);
const stored = await MemberTransferConfig.findOne({ where: { clubId } });
expect(stored).toBeTruthy();
const getResponse = await request(app)
.get(`/api/member-transfer-config/${clubId}`)
.set(authHeaders(token));
expect(getResponse.status).toBe(200);
expect(getResponse.body.config.loginCredentials.username).toBe('user');
const deleteResponse = await request(app)
.delete(`/api/member-transfer-config/${clubId}`)
.set(authHeaders(token));
expect(deleteResponse.status).toBe(200);
});
});

View File

@@ -1,62 +0,0 @@
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 memberTransferConfigService from '../services/memberTransferConfigService.js';
import { checkAccess } from '../utils/userUtils.js';
import MemberTransferConfig from '../models/MemberTransferConfig.js';
import Club from '../models/Club.js';
describe('memberTransferConfigService', () => {
beforeEach(async () => {
await sequelize.sync({ force: true });
vi.clearAllMocks();
});
it('liefert 404 wenn keine Konfiguration existiert', async () => {
const club = await Club.create({ name: 'Config Club' });
const result = await memberTransferConfigService.getConfig('token', club.id);
expect(checkAccess).toHaveBeenCalledWith('token', club.id);
expect(result.status).toBe(404);
});
it('speichert, lädt und löscht Konfigurationen mit Login-Daten', async () => {
const club = await Club.create({ name: 'Config Club' });
const saveResult = await memberTransferConfigService.saveConfig('token', club.id, {
server: 'https://api.example.com',
loginEndpoint: '/login',
loginFormat: 'json',
loginCredentials: { username: 'user', password: 'secret' },
transferEndpoint: '/transfer',
transferMethod: 'POST',
transferFormat: 'json',
transferTemplate: '{"name":"{{fullName}}"}',
useBulkMode: true,
bulkWrapperTemplate: '{"members":{{members}}}'
});
expect(saveResult.status).toBe(200);
const getResult = await memberTransferConfigService.getConfig('token', club.id);
expect(getResult.status).toBe(200);
expect(getResult.response.config.loginCredentials.username).toBe('user');
const configInDb = await MemberTransferConfig.findOne({ where: { clubId: club.id } });
expect(configInDb).toBeTruthy();
expect(typeof configInDb.encryptedLoginCredentials).toBe('string');
const deleteResult = await memberTransferConfigService.deleteConfig('token', club.id);
expect(deleteResult.status).toBe(200);
});
});

View File

@@ -1,101 +0,0 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
vi.mock('axios', () => {
return {
__esModule: true,
default: vi.fn(),
};
});
vi.mock('../utils/userUtils.js', async () => {
const actual = await vi.importActual('../utils/userUtils.js');
return {
...actual,
checkAccess: vi.fn().mockResolvedValue(true),
};
});
import axios from 'axios';
import sequelize from '../database.js';
import '../models/index.js';
import memberTransferService from '../services/memberTransferService.js';
import { checkAccess } from '../utils/userUtils.js';
import Club from '../models/Club.js';
import Member from '../models/Member.js';
import { buildMemberData, createMember } from './utils/factories.js';
const axiosMock = /** @type {vi.Mock} */ (axios);
describe('memberTransferService', () => {
beforeEach(async () => {
await sequelize.sync({ force: true });
axiosMock.mockReset();
axiosMock.mockResolvedValue({ status: 200, data: { success: true } });
vi.clearAllMocks();
});
it('filtert ungültige Mitglieder heraus und überträgt gültige', async () => {
const club = await Club.create({ name: 'Transfer Club' });
await Member.bulkCreate([
buildMemberData(club.id, {
firstName: 'Anna',
lastName: 'Aktiv',
birthDate: '2001-02-03',
email: 'anna@example.com',
}),
buildMemberData(club.id, {
firstName: '',
lastName: 'OhneVorname',
birthDate: null,
email: 'invalid@example.com',
}),
]);
const result = await memberTransferService.transferMembers('token', club.id, {
transferEndpoint: 'http://example.com/transfer',
transferTemplate: '{"firstName":"{{firstName}}","birth":"{{birthDate}}"}',
transferMethod: 'POST',
transferFormat: 'json',
useBulkMode: false,
});
expect(checkAccess).toHaveBeenCalledWith('token', club.id);
expect(result.status).toBe(200);
expect(result.response.transferred).toBe(1);
expect(result.response.invalidCount).toBe(1);
expect(axiosMock).toHaveBeenCalledTimes(1);
const callConfig = axiosMock.mock.calls[0][0];
expect(callConfig.url).toBe('http://example.com/transfer');
});
it('nutzt Bulk-Modus und Wrapper-Template', async () => {
const club = await Club.create({ name: 'Bulk Club' });
await createMember(club.id, {
firstName: 'Ben',
lastName: 'Bulk',
birthDate: '1999-05-06',
email: 'ben@example.com',
});
axiosMock.mockResolvedValue({ status: 200, data: { success: true, summary: { imported: 1 } } });
const result = await memberTransferService.transferMembers('token', club.id, {
transferEndpoint: 'http://example.com/bulk',
transferTemplate: '{"name":"{{fullName}}"}',
transferMethod: 'POST',
transferFormat: 'json',
useBulkMode: true,
bulkWrapperTemplate: '{"payload":{"members":{{members}}}}',
});
expect(result.status).toBe(200);
const callConfig = axiosMock.mock.calls[0][0];
expect(callConfig.data).toEqual({ payload: { members: [{ name: 'Ben Bulk' }] } });
});
it('formatiert Geburtsdaten korrekt', () => {
expect(memberTransferService.formatBirthDate('01.02.2000')).toBe('2000-02-01');
expect(memberTransferService.formatBirthDate('2000-02-01')).toBe('2000-02-01');
});
});

View File

@@ -1,38 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import sequelize from '../database.js';
import '../models/index.js';
import myTischtennisFetchLogService from '../services/myTischtennisFetchLogService.js';
import MyTischtennisFetchLog from '../models/MyTischtennisFetchLog.js';
import { createUser } from './utils/factories.js';
describe('myTischtennisFetchLogService', () => {
beforeEach(async () => {
await sequelize.sync({ force: true });
});
it('loggt Abrufe und liefert die letzten Einträge', async () => {
const user = await createUser({ email: 'fetch@example.com' });
await myTischtennisFetchLogService.logFetch(user.id, 'ratings', true, 'OK', { recordsProcessed: 5 });
await myTischtennisFetchLogService.logFetch(user.id, 'match_results', false, 'Error', { errorDetails: 'Timeout' });
const logs = await myTischtennisFetchLogService.getFetchLogs(user.id, { limit: 10 });
expect(logs).toHaveLength(2);
expect(logs[0].fetchType).toBe('match_results');
const latest = await myTischtennisFetchLogService.getLatestSuccessfulFetches(user.id);
expect(latest.ratings).toBeTruthy();
expect(latest.match_results).toBeNull();
});
it('aggregiert Statistiken nach Typ', async () => {
const user = await createUser({ email: 'stats@example.com' });
await myTischtennisFetchLogService.logFetch(user.id, 'ratings', true, 'OK', { recordsProcessed: 3, executionTime: 100 });
await myTischtennisFetchLogService.logFetch(user.id, 'ratings', false, 'Fail', { recordsProcessed: 0, executionTime: 200 });
const stats = await myTischtennisFetchLogService.getFetchStatistics(user.id, 7);
expect(stats).toHaveLength(1);
expect(stats[0].fetchType).toBe('ratings');
});
});

View File

@@ -1,92 +0,0 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import request from 'supertest';
vi.mock('../services/emailService.js', () => ({
sendActivationEmail: vi.fn().mockResolvedValue(),
}));
vi.mock('../clients/myTischtennisClient.js', () => ({
__esModule: true,
default: {
login: vi.fn().mockResolvedValue({ success: true, accessToken: 'token', refreshToken: 'refresh', expiresAt: Date.now() / 1000 + 3600, cookie: 'cookie', user: { id: 1 } }),
getUserProfile: vi.fn().mockResolvedValue({ success: true, clubId: 1, clubName: 'Club', fedNickname: 'fed' }),
},
}));
import app from './testApp.js';
import sequelize from '../database.js';
import '../models/index.js';
import User from '../models/User.js';
import MyTischtennis from '../models/MyTischtennis.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('MyTischtennis Routes', () => {
beforeEach(async () => {
await sequelize.sync({ force: true });
});
it('erstellt Accounts und liefert Status', async () => {
const { user, credentials } = await registerAndActivate('mytt@example.com');
const token = await loginAndGetToken(credentials);
const upsertResponse = await request(app)
.post('/api/mytischtennis/account')
.set(authHeaders(token))
.send({ email: 'mytt@example.com', password: 'secret', savePassword: true, userPassword: 'Test123!' });
expect(upsertResponse.status).toBe(200);
const statusResponse = await request(app)
.get('/api/mytischtennis/status')
.set(authHeaders(token));
expect(statusResponse.status).toBe(200);
expect(statusResponse.body.exists).toBe(true);
const account = await MyTischtennis.findOne({ where: { userId: user.id } });
expect(account).toBeTruthy();
});
it('prüft Logins und liefert Sessiondaten', async () => {
const { credentials } = await registerAndActivate('verify@example.com');
const token = await loginAndGetToken(credentials);
await request(app)
.post('/api/mytischtennis/account')
.set(authHeaders(token))
.send({ email: 'verify@example.com', password: 'secret', savePassword: true, userPassword: 'Test123!' });
const verifyResponse = await request(app)
.post('/api/mytischtennis/verify')
.set(authHeaders(token))
.send({ password: 'secret' });
expect(verifyResponse.status).toBe(200);
expect(verifyResponse.body.success).toBe(true);
const sessionResponse = await request(app)
.get('/api/mytischtennis/session')
.set(authHeaders(token));
expect(sessionResponse.status).toBe(200);
expect(sessionResponse.body.session.accessToken).toBe('token');
});
});

View File

@@ -1,70 +0,0 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
vi.mock('../clients/myTischtennisClient.js', () => ({
__esModule: true,
default: {
login: vi.fn(),
getUserProfile: vi.fn(),
},
}));
import sequelize from '../database.js';
import '../models/index.js';
import myTischtennisService from '../services/myTischtennisService.js';
import MyTischtennis from '../models/MyTischtennis.js';
import MyTischtennisUpdateHistory from '../models/MyTischtennisUpdateHistory.js';
import myTischtennisClient from '../clients/myTischtennisClient.js';
import { createUser } from './utils/factories.js';
const clientMock = /** @type {{ login: vi.Mock; getUserProfile: vi.Mock }} */ (myTischtennisClient);
describe('myTischtennisService', () => {
beforeEach(async () => {
await sequelize.sync({ force: true });
vi.clearAllMocks();
clientMock.login.mockResolvedValue({ success: true, accessToken: 'token', refreshToken: 'refresh', expiresAt: Date.now() / 1000 + 3600, cookie: 'cookie', user: { id: 1 } });
clientMock.getUserProfile.mockResolvedValue({ success: true, clubId: 123, clubName: 'Club', fedNickname: 'fed' });
});
it('legt Accounts an und aktualisiert Logins', async () => {
const user = await createUser({ email: 'user@example.com' });
const userId = user.id;
const account = await myTischtennisService.upsertAccount(userId, 'user@example.com', 'pass', true, true, 'Secret!123');
expect(account.email).toBe('user@example.com');
const stored = await MyTischtennis.findOne({ where: { userId } });
expect(stored.savePassword).toBe(true);
expect(clientMock.getUserProfile).toHaveBeenCalled();
expect(clientMock.login).toHaveBeenCalledWith('user@example.com', 'pass');
});
it('verifiziert Logins mit gespeicherter Session', async () => {
const user = await createUser({ email: 'session@example.com' });
const userId = user.id;
clientMock.login.mockResolvedValueOnce({ success: true, accessToken: 'token', refreshToken: 'refresh', expiresAt: Date.now() / 1000 + 3600, cookie: 'cookie', user: { id: 2 } });
await myTischtennisService.upsertAccount(userId, 'session@example.com', 'pass', true, false, 'Secret!123');
const result = await myTischtennisService.verifyLogin(userId);
expect(result.success).toBe(true);
expect(clientMock.login).toHaveBeenCalledWith('session@example.com', 'pass');
});
it('speichert Update-History und setzt lastUpdateRatings', async () => {
const user = await createUser({ email: 'history@example.com' });
const userId = user.id;
await MyTischtennis.create({ userId, email: 'history@example.com' });
await myTischtennisService.logUpdateAttempt(userId, true, 'OK', null, 5, 1234);
const history = await MyTischtennisUpdateHistory.findAll({ where: { userId } });
expect(history).toHaveLength(1);
expect(history[0].updatedCount).toBe(5);
const account = await MyTischtennis.findOne({ where: { userId } });
expect(account.lastUpdateRatings).toBeTruthy();
});
});

View File

@@ -1,90 +0,0 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import request from 'supertest';
vi.mock('../services/emailService.js', () => ({
sendActivationEmail: vi.fn().mockResolvedValue(),
}));
vi.mock('../clients/myTischtennisClient.js', () => ({
__esModule: true,
default: {
login: vi.fn().mockResolvedValue({ success: true, accessToken: 'token', refreshToken: 'refresh', expiresAt: Date.now() / 1000 + 3600, cookie: 'cookie', user: { id: 1 } }),
getUserProfile: vi.fn().mockResolvedValue({ success: true, clubId: 1, clubName: 'Club', fedNickname: 'fed' }),
},
}));
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 League from '../models/League.js';
import Season from '../models/Season.js';
import ClubTeam from '../models/ClubTeam.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, userId) => ({
Authorization: `Bearer ${token}`,
authcode: token,
userid: String(userId),
});
describe('MyTischtennis URL Routes', () => {
beforeEach(async () => {
await sequelize.sync({ force: true });
});
it('parst URLs und konfiguriert Teams', async () => {
const { user, credentials } = await registerAndActivate('url@example.com');
const token = await loginAndGetToken(credentials);
const club = await Club.create({ name: 'URL Club' });
const season = await Season.create({ season: '2024/2025' });
const league = await League.create({ name: 'Verbandsliga', association: 'HeTTV', groupname: 'Verbandsliga', myTischtennisGroupId: '12345', seasonId: season.id, clubId: club.id });
const team = await ClubTeam.create({ name: 'URL Team', clubId: club.id, leagueId: league.id, seasonId: season.id });
const configureResponse = await request(app)
.post('/api/mytischtennis/configure-team')
.set(authHeaders(token, user.id))
.send({
url: 'https://www.mytischtennis.de/click-tt/HeTTV/24--25/ligen/Verbandsliga/gruppe/12345/mannschaft/67890/URL_Team/spielerbilanzen/gesamt',
clubTeamId: team.id,
createLeague: true,
createSeason: true,
});
expect(configureResponse.status).toBe(200);
await team.reload();
expect(team.myTischtennisTeamId).toBeTruthy();
});
it('gibt Team-URLs zurück', async () => {
const { user, credentials } = await registerAndActivate('teamurl@example.com');
const token = await loginAndGetToken(credentials);
const club = await Club.create({ name: 'URL Club' });
const season = await Season.create({ season: '2024/2025' });
const league = await League.create({ name: 'Verbandsliga', association: 'HeTTV', groupname: 'Verbandsliga', myTischtennisGroupId: '12345', seasonId: season.id, clubId: club.id });
const team = await ClubTeam.create({ name: 'URL Team', clubId: club.id, leagueId: league.id, seasonId: season.id, myTischtennisTeamId: '98765' });
const urlResponse = await request(app)
.get(`/api/mytischtennis/team-url/${team.id}`)
.set(authHeaders(token, user.id));
expect(urlResponse.status).toBe(200);
expect(urlResponse.body.url).toContain('98765');
});
});

View File

@@ -1,124 +0,0 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import fs from 'fs';
import os from 'os';
import path from 'path';
import sequelize from '../database.js';
import '../models/index.js';
import pdfParserService from '../services/pdfParserService.js';
import Club from '../models/Club.js';
import Team from '../models/Team.js';
import League from '../models/League.js';
import Season from '../models/Season.js';
import Match from '../models/Match.js';
import Location from '../models/Location.js';
describe('pdfParserService', () => {
let tempFiles = [];
beforeEach(async () => {
await sequelize.sync({ force: true });
tempFiles = [];
});
afterEach(() => {
tempFiles.forEach((file) => {
try {
fs.unlinkSync(file);
} catch {
/* ignore */
}
});
});
const createTempFile = (content, ext = '.txt') => {
const filePath = path.join(os.tmpdir(), `pdfparser-${Date.now()}-${Math.random().toString(16).slice(2)}${ext}`);
fs.writeFileSync(filePath, content, 'utf8');
tempFiles.push(filePath);
return filePath;
};
it('parst Standard-Format aus einer Textdatei', async () => {
const lines = [
'Sa. 06.09.2025 10:00 Harheimer TC II SG Teststadt III ABC123DEF456'
];
const filePath = createTempFile(lines.join('\n'));
const result = await pdfParserService.parsePDF(filePath, 1);
expect(result.matches).toHaveLength(1);
const match = result.matches[0];
expect(match.homeTeamName).toBe('Harheimer TC II');
expect(match.guestTeamName).toBe('SG Teststadt III');
expect(match.code).toBe('ABC123DEF456');
});
it('parst Tabellen-Format', () => {
const text = [
'Datum Zeit Heim Gast Code Heim-Pin Gast-Pin',
'06.09.2025 10:00 Harheimer TC II SG Teststadt III ABC123DEF456 1234 5678'
].join('\n');
const result = pdfParserService.extractMatchData(text, 1);
expect(result.matches).toHaveLength(1);
const match = result.matches[0];
expect(match.homeTeamName).toContain('Harheimer');
expect(['1234', '5678', null, ''].includes(match.guestPin ?? null)).toBe(true);
expect(['1234', '5678', null, ''].includes(match.homePin ?? null)).toBe(true);
});
it('parst Listen-Format', () => {
const text = '1. 06.09.2025 10:00 Harheimer TC II vs SG Teststadt III code ABC123DEF456';
const result = pdfParserService.extractMatchData(text, 5);
expect(result.matches).toHaveLength(1);
expect(result.matches[0].guestTeamName).toContain('SG Teststadt III');
});
it('speichert Matches in der Datenbank und aktualisiert vorhandene Einträge', async () => {
const club = await Club.create({ name: 'Harheimer TC' });
const season = await Season.create({ season: '2025/2026' });
const league = await League.create({ name: 'Verbandsliga', clubId: club.id, seasonId: season.id });
const homeTeam = await Team.create({ name: 'Harheimer TC II', clubId: club.id, leagueId: league.id, seasonId: season.id });
const guestTeam = await Team.create({ name: 'SG Teststadt III', clubId: club.id, leagueId: league.id, seasonId: season.id });
await Location.create({ name: 'Halle', address: 'Straße 1', city: 'Stadt', zip: '12345' });
const matchDate = new Date('2025-09-06T10:00:00Z');
const matches = [
{
date: matchDate,
time: '10:00',
homeTeamName: homeTeam.name,
guestTeamName: guestTeam.name,
code: 'ABC123DEF456',
clubId: club.id,
rawLine: 'Sa. 06.09.2025 10:00 Harheimer TC II SG Teststadt III ABC123DEF456'
}
];
const createResult = await pdfParserService.saveMatchesToDatabase(matches, league.id);
expect(createResult.created).toBe(1);
expect(createResult.updated).toBe(0);
expect(await Match.count()).toBe(1);
const updateMatches = [
{
date: matchDate,
time: '10:00',
homeTeamName: homeTeam.name,
guestTeamName: guestTeam.name,
code: 'XYZ987654321',
clubId: club.id,
rawLine: 'Sa. 06.09.2025 10:00 Harheimer TC II SG Teststadt III XYZ987654321'
}
];
const updateResult = await pdfParserService.saveMatchesToDatabase(updateMatches, league.id);
expect(updateResult.updated).toBe(1);
const storedMatch = await Match.findOne();
expect(storedMatch.code).toBe('XYZ987654321');
});
});

View File

@@ -1,159 +0,0 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import request from 'supertest';
vi.mock('../services/emailService.js', () => ({
sendActivationEmail: vi.fn().mockResolvedValue(),
}));
import app from './testApp.js';
import sequelize from '../database.js';
import User from '../models/User.js';
import Club from '../models/Club.js';
import UserClub from '../models/UserClub.js';
const registerAndActivate = async (email) => {
const password = 'Test123!';
await request(app).post('/api/auth/register').send({ email, password });
const user = await User.findOne({ where: { email } });
await user.update({ isActive: true });
return { user, credentials: { email, password } };
};
const loginAndGetToken = async (credentials) => {
const response = await request(app).post('/api/auth/login').send(credentials);
return response.body.token;
};
describe('Permission Routes', () => {
beforeEach(async () => {
vi.clearAllMocks();
await sequelize.sync({ force: true });
});
it('liefert die verfügbaren Rollen für authentifizierte Nutzer', async () => {
const { credentials } = await registerAndActivate('roles@example.com');
const token = await loginAndGetToken(credentials);
const response = await request(app)
.get('/api/permissions/roles/available')
.set('Authorization', `Bearer ${token}`);
expect(response.status).toBe(200);
expect(Array.isArray(response.body)).toBe(true);
expect(response.body.length).toBeGreaterThan(0);
});
it('gibt Club-Mitglieder und Berechtigungen für Administratoren zurück', async () => {
const { user: owner, credentials } = await registerAndActivate('owner@example.com');
const { user: member } = await registerAndActivate('member@example.com');
const club = await Club.create({ name: 'Permissions Club' });
await UserClub.bulkCreate([
{ userId: owner.id, clubId: club.id, role: 'admin', approved: true, isOwner: true },
{ userId: member.id, clubId: club.id, role: 'member', approved: true },
]);
const token = await loginAndGetToken(credentials);
const response = await request(app)
.get(`/api/permissions/${club.id}/members`)
.set('Authorization', `Bearer ${token}`);
expect(response.status).toBe(200);
expect(response.body).toHaveLength(2);
const memberEntry = response.body.find((entry) => entry.userId === member.id);
expect(memberEntry).toBeTruthy();
expect(memberEntry.role).toBe('member');
});
it('verweigert den Zugriff auf die Mitgliederliste für einfache Mitglieder', async () => {
const { user: owner } = await registerAndActivate('owner2@example.com');
const { user: member, credentials } = await registerAndActivate('member2@example.com');
const club = await Club.create({ name: 'Restricted Club' });
await UserClub.bulkCreate([
{ userId: owner.id, clubId: club.id, role: 'admin', approved: true, isOwner: true },
{ userId: member.id, clubId: club.id, role: 'member', approved: true },
]);
const token = await loginAndGetToken(credentials);
const response = await request(app)
.get(`/api/permissions/${club.id}/members`)
.set('Authorization', `Bearer ${token}`);
expect(response.status).toBe(403);
});
it('ändert den Genehmigungsstatus eines Mitglieds über die API', async () => {
const { user: owner, credentials } = await registerAndActivate('owner3@example.com');
const { user: member } = await registerAndActivate('member3@example.com');
const club = await Club.create({ name: 'Status Club' });
await UserClub.bulkCreate([
{ userId: owner.id, clubId: club.id, role: 'admin', approved: true, isOwner: true },
{ userId: member.id, clubId: club.id, role: 'member', approved: true },
]);
const token = await loginAndGetToken(credentials);
const response = await request(app)
.put(`/api/permissions/${club.id}/user/${member.id}/status`)
.set('Authorization', `Bearer ${token}`)
.send({ approved: false });
expect(response.status).toBe(200);
const updated = await UserClub.findOne({ where: { userId: member.id, clubId: club.id } });
expect(updated.approved).toBe(false);
});
it('ändert Rollen und benutzerdefinierte Berechtigungen', async () => {
const { user: owner, credentials } = await registerAndActivate('owner4@example.com');
const { user: member } = await registerAndActivate('member4@example.com');
const club = await Club.create({ name: 'Role Club' });
await UserClub.bulkCreate([
{ userId: owner.id, clubId: club.id, role: 'admin', approved: true, isOwner: true },
{ userId: member.id, clubId: club.id, role: 'member', approved: true },
]);
const token = await loginAndGetToken(credentials);
const roleResponse = await request(app)
.put(`/api/permissions/${club.id}/user/${member.id}/role`)
.set('Authorization', `Bearer ${token}`)
.send({ role: 'trainer' });
expect(roleResponse.status).toBe(200);
const roleUpdated = await UserClub.findOne({ where: { userId: member.id, clubId: club.id } });
expect(roleUpdated.role).toBe('trainer');
const customPermissions = { diary: { read: true, write: true }, schedule: { read: true } };
const permissionResponse = await request(app)
.put(`/api/permissions/${club.id}/user/${member.id}/permissions`)
.set('Authorization', `Bearer ${token}`)
.send({ permissions: customPermissions });
expect(permissionResponse.status).toBe(200);
await roleUpdated.reload();
expect(roleUpdated.permissions.diary.write).toBe(true);
const ownPermissions = await request(app)
.get(`/api/permissions/${club.id}`)
.set('Authorization', `Bearer ${token}`);
expect(ownPermissions.status).toBe(200);
expect(ownPermissions.body.permissions.diary.write).toBe(true);
});
it('liefert 400 bei ungültiger Club-ID', async () => {
const { credentials } = await registerAndActivate('invalidclub@example.com');
const token = await loginAndGetToken(credentials);
const response = await request(app)
.get('/api/permissions/not-a-number')
.set('Authorization', `Bearer ${token}`);
expect(response.status).toBe(400);
});
});

View File

@@ -1,128 +0,0 @@
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
import sequelize from '../database.js';
import User from '../models/User.js';
import Club from '../models/Club.js';
import UserClub from '../models/UserClub.js';
import permissionService from '../services/permissionService.js';
async function createUser(email, password = 'Secret!123', overrides = {}) {
return User.create({ email, password, isActive: true, ...overrides });
}
async function createClub(name = 'Testclub') {
return Club.create({ name });
}
describe('permissionService', () => {
beforeAll(async () => {
await sequelize.sync({ force: true });
});
beforeEach(async () => {
await sequelize.truncate({ cascade: true, restartIdentity: true });
});
it('liefert Admin-Rechte für Club-Eigentümer', async () => {
const owner = await createUser('owner@test.de');
const club = await createClub('Admin Club');
await UserClub.create({
userId: owner.id,
clubId: club.id,
role: 'admin',
approved: true,
isOwner: true,
});
const permissions = await permissionService.getUserClubPermissions(owner.id, club.id);
expect(permissions).toMatchObject({ isOwner: true, role: 'admin' });
expect(permissions.permissions.permissions.write).toBe(true);
});
it('verhindert Rollenänderung ohne Berechtigung', async () => {
const owner = await createUser('owner@test.de');
const member = await createUser('member@test.de');
const club = await createClub('Club');
await UserClub.bulkCreate([
{ userId: owner.id, clubId: club.id, role: 'admin', approved: true, isOwner: true },
{ userId: member.id, clubId: club.id, role: 'member', approved: true },
]);
await expect(
permissionService.setUserRole(owner.id, club.id, 'trainer', member.id)
).rejects.toThrow('Keine Berechtigung zum Ändern von Rollen');
});
it('erlaubt Administratoren die Rollenänderung', async () => {
const owner = await createUser('owner@test.de');
const admin = await createUser('admin@test.de');
const member = await createUser('member@test.de');
const club = await createClub('Club');
await UserClub.bulkCreate([
{ userId: owner.id, clubId: club.id, role: 'admin', approved: true, isOwner: true },
{ userId: admin.id, clubId: club.id, role: 'admin', approved: true },
{ userId: member.id, clubId: club.id, role: 'member', approved: true },
]);
await permissionService.setUserRole(member.id, club.id, 'trainer', admin.id);
const updated = await UserClub.findOne({ where: { userId: member.id, clubId: club.id } });
expect(updated.role).toBe('trainer');
});
it('erlaubt Custom-Permissions und fasst sie zusammen', async () => {
const owner = await createUser('owner@test.de');
const member = await createUser('member@test.de');
const club = await createClub('Club');
await UserClub.bulkCreate([
{ userId: owner.id, clubId: club.id, role: 'admin', approved: true, isOwner: true },
{ userId: member.id, clubId: club.id, role: 'member', approved: true },
]);
await permissionService.setCustomPermissions(
member.id,
club.id,
{ members: { read: true, write: true } },
owner.id
);
const permissions = await permissionService.getUserClubPermissions(member.id, club.id);
expect(permissions.permissions.members.write).toBe(true);
});
it('liefert Fehlermeldung wenn Berechtigungsübersicht ohne Rechte angefordert wird', async () => {
const owner = await createUser('owner@test.de');
const member = await createUser('member@test.de');
const club = await createClub('Club');
await UserClub.bulkCreate([
{ userId: owner.id, clubId: club.id, role: 'admin', approved: true, isOwner: true },
{ userId: member.id, clubId: club.id, role: 'member', approved: true },
]);
await expect(
permissionService.getClubMembersWithPermissions(club.id, member.id)
).rejects.toThrow('Keine Berechtigung zum Anzeigen von Berechtigungen');
});
it('liefert Mitgliederliste mit effektiven Berechtigungen für Administratoren', async () => {
const owner = await createUser('owner@test.de');
const member = await createUser('member@test.de');
const club = await createClub('Club');
await UserClub.bulkCreate([
{ userId: owner.id, clubId: club.id, role: 'admin', approved: true, isOwner: true },
{ userId: member.id, clubId: club.id, role: 'member', approved: true },
]);
const members = await permissionService.getClubMembersWithPermissions(club.id, owner.id);
expect(members).toHaveLength(2);
const memberEntry = members.find((entry) => entry.userId === member.id);
expect(memberEntry).toBeTruthy();
expect(memberEntry.effectivePermissions.statistics.read).toBe(true);
});
});

View File

@@ -1,94 +0,0 @@
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 PredefinedActivity from '../models/PredefinedActivity.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('PredefinedActivity Routes', () => {
beforeEach(async () => {
await sequelize.sync({ force: true });
});
it('erstellt, aktualisiert und listet Aktivitäten', async () => {
const { credentials } = await registerAndActivate('predefined@example.com');
const token = await loginAndGetToken(credentials);
const createResponse = await request(app)
.post('/api/predefined-activities')
.set(authHeaders(token))
.send({ name: 'Aufwärmen', code: 'AW', description: 'Kurz' });
expect(createResponse.status).toBe(201);
const activityId = createResponse.body.id;
const updateResponse = await request(app)
.put(`/api/predefined-activities/${activityId}`)
.set(authHeaders(token))
.send({ name: 'Aufwärmen Lang', code: 'AWL' });
expect(updateResponse.status).toBe(200);
expect(updateResponse.body.code).toBe('AWL');
const listResponse = await request(app)
.get('/api/predefined-activities')
.set(authHeaders(token));
expect(listResponse.status).toBe(200);
expect(listResponse.body.some((entry) => entry.name === 'Aufwärmen Lang')).toBe(true);
});
it('sucht, merged und dedupliziert Aktivitäten', async () => {
const { credentials } = await registerAndActivate('predefined2@example.com');
const token = await loginAndGetToken(credentials);
const a1 = await PredefinedActivity.create({ name: 'Fangspiel', code: 'FANG' });
const a2 = await PredefinedActivity.create({ name: 'Fangspiel', code: 'FANG2' });
const searchResponse = await request(app)
.get('/api/predefined-activities/search/query')
.set(authHeaders(token))
.query({ q: 'FANG' });
expect(searchResponse.status).toBe(200);
expect(searchResponse.body.length).toBeGreaterThan(0);
const mergeResponse = await request(app)
.post('/api/predefined-activities/merge')
.set(authHeaders(token))
.send({ sourceId: a2.id, targetId: a1.id });
expect(mergeResponse.status).toBe(200);
expect(await PredefinedActivity.findByPk(a2.id)).toBeNull();
const dedupResponse = await request(app)
.post('/api/predefined-activities/deduplicate')
.set(authHeaders(token));
expect(dedupResponse.status).toBe(200);
});
});

View File

@@ -1,89 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import sequelize from '../database.js';
import '../models/index.js';
import predefinedActivityService from '../services/predefinedActivityService.js';
import PredefinedActivity from '../models/PredefinedActivity.js';
import PredefinedActivityImage from '../models/PredefinedActivityImage.js';
import DiaryDateActivity from '../models/DiaryDateActivity.js';
import DiaryDate from '../models/DiaryDates.js';
import Club from '../models/Club.js';
describe('predefinedActivityService', () => {
beforeEach(async () => {
await sequelize.sync({ force: true });
});
it('erstellt und aktualisiert Aktivitäten mit Zeichnungsdaten', async () => {
const created = await predefinedActivityService.createPredefinedActivity({
name: 'Aufwärmen',
code: 'AW',
description: 'Kurzes Warmup',
duration: 15,
drawingData: { lines: 3 },
});
expect(created.name).toBe('Aufwärmen');
expect(created.drawingData).toBe(JSON.stringify({ lines: 3 }));
const updated = await predefinedActivityService.updatePredefinedActivity(created.id, {
name: 'Aufwärmen Intensiv',
code: 'AWI',
description: 'Intensives Warmup',
duration: 20,
drawingData: { lines: 5 },
});
expect(updated.name).toBe('Aufwärmen Intensiv');
expect(updated.code).toBe('AWI');
});
it('sucht Aktivitäten anhand der Kürzel', async () => {
await PredefinedActivity.bulkCreate([
{ name: 'Fangspiel', code: 'FANG' },
{ name: 'Ballkontrolle', code: 'BALL' },
{ name: 'Freies Spiel', code: null },
]);
const result = await predefinedActivityService.searchPredefinedActivities('FA');
expect(result).toHaveLength(1);
expect(result[0].code).toBe('FANG');
});
it('führt Aktivitäten zusammen und passt Referenzen an', async () => {
const club = await Club.create({ name: 'Aktivitätsclub' });
const source = await PredefinedActivity.create({ name: 'Topspin', code: 'TS' });
const target = await PredefinedActivity.create({ name: 'Topspin Verbesserte', code: 'TSV' });
const diaryDate = await DiaryDate.create({ date: new Date('2025-09-01'), clubId: club.id });
const diaryDateActivity = await DiaryDateActivity.create({
diaryDateId: diaryDate.id,
predefinedActivityId: source.id,
isTimeblock: false,
orderId: 1,
});
await PredefinedActivityImage.create({ predefinedActivityId: source.id, imagePath: 'uploads/test.png' });
await predefinedActivityService.mergeActivities(source.id, target.id);
await diaryDateActivity.reload();
expect(diaryDateActivity.predefinedActivityId).toBe(target.id);
const images = await PredefinedActivityImage.findAll({ where: { predefinedActivityId: target.id } });
expect(images).toHaveLength(1);
expect(await PredefinedActivity.findByPk(source.id)).toBeNull();
});
it('dedupliziert Aktivitäten nach Namen', async () => {
await PredefinedActivity.bulkCreate([
{ name: 'Schmetterball', code: 'SM1' },
{ name: 'schmetterball', code: 'SM2' },
{ name: 'Aufschlag', code: 'AS' },
]);
const result = await predefinedActivityService.deduplicateActivities();
expect(result.mergedCount).toBe(1);
const remaining = await PredefinedActivity.findAll();
expect(remaining).toHaveLength(2);
});
});

View File

@@ -1,125 +0,0 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
const { mockJobs } = vi.hoisted(() => ({ mockJobs: [] }));
vi.mock('node-cron', () => {
const schedule = vi.fn((expression, handler) => {
const job = {
expression,
handler,
start: vi.fn(),
stop: vi.fn(),
};
mockJobs.push(job);
return job;
});
return {
__esModule: true,
default: {
schedule,
__mockJobs: mockJobs,
},
};
});
vi.mock('../services/autoUpdateRatingsService.js', () => ({
__esModule: true,
default: {
executeAutomaticUpdates: vi.fn().mockResolvedValue({ updatedCount: 3 }),
},
}));
vi.mock('../services/autoFetchMatchResultsService.js', () => ({
__esModule: true,
default: {
executeAutomaticFetch: vi.fn().mockResolvedValue({ fetchedCount: 4 }),
},
}));
vi.mock('../services/apiLogService.js', () => ({
__esModule: true,
default: {
logSchedulerExecution: vi.fn().mockResolvedValue(),
},
}));
import schedulerService from '../services/schedulerService.js';
import cron from 'node-cron';
import autoUpdateRatingsService from '../services/autoUpdateRatingsService.js';
import autoFetchMatchResultsService from '../services/autoFetchMatchResultsService.js';
import apiLogService from '../services/apiLogService.js';
const cronMock = /** @type {vi.Mock} */ (cron.schedule);
const autoUpdateMock = /** @type {vi.Mock} */ (autoUpdateRatingsService.executeAutomaticUpdates);
const autoFetchMock = /** @type {vi.Mock} */ (autoFetchMatchResultsService.executeAutomaticFetch);
const apiLogMock = /** @type {vi.Mock} */ (apiLogService.logSchedulerExecution);
describe('schedulerService', () => {
beforeEach(() => {
schedulerService.stop();
mockJobs.length = 0;
cronMock.mockClear();
autoUpdateMock.mockClear();
autoFetchMock.mockClear();
apiLogMock.mockClear();
});
afterEach(() => {
schedulerService.stop();
});
it('startet Scheduler und registriert Cron-Jobs genau einmal', () => {
schedulerService.start();
expect(cronMock.mock.calls).toHaveLength(2);
expect(schedulerService.getStatus()).toMatchObject({ isRunning: true, jobs: ['ratingUpdates', 'matchResults'] });
schedulerService.start();
expect(cronMock.mock.calls).toHaveLength(2);
});
it('stoppt Scheduler und ruft stop für jede Aufgabe auf', () => {
schedulerService.start();
const jobsSnapshot = [...mockJobs];
schedulerService.stop();
jobsSnapshot.forEach((job) => {
expect(job.stop).toHaveBeenCalled();
});
expect(schedulerService.getStatus().isRunning).toBe(false);
});
it('triggert manuelle Updates und Fetches', async () => {
const ratings = await schedulerService.triggerRatingUpdates();
expect(ratings.success).toBe(true);
expect(autoUpdateMock).toHaveBeenCalled();
const matches = await schedulerService.triggerMatchResultsFetch();
expect(matches.success).toBe(true);
expect(autoFetchMock).toHaveBeenCalled();
});
it('führt geplante Jobs aus und protokolliert Ergebnisse', async () => {
schedulerService.start();
const [ratingJob, matchJob] = mockJobs;
await ratingJob.handler();
expect(apiLogMock).toHaveBeenCalledWith(
'rating_updates',
true,
expect.any(Object),
expect.any(Number),
null
);
await matchJob.handler();
expect(apiLogMock).toHaveBeenCalledWith(
'match_results',
true,
expect.any(Object),
expect.any(Number),
null
);
});
});

View File

@@ -1,94 +0,0 @@
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 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('Season Routes', () => {
beforeEach(async () => {
await sequelize.sync({ force: true });
});
it('legt Saisons an, listet und löscht sie', async () => {
const { credentials } = await registerAndActivate('season@example.com');
const token = await loginAndGetToken(credentials);
const createResponse = await request(app)
.post('/api/seasons')
.set(authHeaders(token))
.send({ season: '2022/2023' });
expect(createResponse.status).toBe(201);
const seasonId = createResponse.body.id;
const listResponse = await request(app)
.get('/api/seasons')
.set(authHeaders(token));
expect(listResponse.status).toBe(200);
expect(listResponse.body.some((entry) => entry.season === '2022/2023')).toBe(true);
const getResponse = await request(app)
.get(`/api/seasons/${seasonId}`)
.set(authHeaders(token));
expect(getResponse.status).toBe(200);
const deleteResponse = await request(app)
.delete(`/api/seasons/${seasonId}`)
.set(authHeaders(token));
expect(deleteResponse.status).toBe(200);
expect(deleteResponse.body.message).toBe('deleted');
});
it('validiert Saison-Format', async () => {
const { credentials } = await registerAndActivate('season-invalid@example.com');
const token = await loginAndGetToken(credentials);
const response = await request(app)
.post('/api/seasons')
.set(authHeaders(token))
.send({ season: 'invalid' });
expect(response.status).toBe(400);
});
it('gibt die aktuelle Saison zurück (mit Auto-Erstellung)', async () => {
const { credentials } = await registerAndActivate('season-current@example.com');
const token = await loginAndGetToken(credentials);
const response = await request(app)
.get('/api/seasons/current')
.set(authHeaders(token));
expect(response.status).toBe(200);
expect(response.body).toHaveProperty('season');
});
});

View File

@@ -1,50 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import sequelize from '../database.js';
import '../models/index.js';
import SeasonService from '../services/seasonService.js';
import Season from '../models/Season.js';
import Team from '../models/Team.js';
import League from '../models/League.js';
import Club from '../models/Club.js';
describe('seasonService', () => {
beforeEach(async () => {
await sequelize.sync({ force: true });
});
it('erstellt die aktuelle Saison nur einmal', async () => {
const first = await SeasonService.getOrCreateCurrentSeason();
const second = await SeasonService.getOrCreateCurrentSeason();
expect(first.id).toBe(second.id);
});
it('verhindert doppelte Saisons und löscht ungenutzte Einträge', async () => {
const seasonName = '2024/2025';
const created = await SeasonService.createSeason(seasonName);
expect(created.season).toBe(seasonName);
await expect(SeasonService.createSeason(seasonName)).rejects.toThrow('Season already exists');
const deleteResult = await SeasonService.deleteSeason(created.id);
expect(deleteResult).toBe(true);
});
it('verhindert das Löschen, wenn Teams oder Ligen referenzieren', async () => {
const club = await Club.create({ name: 'Season Club' });
const season = await SeasonService.createSeason('2023/2024');
await League.create({ name: 'Testliga', clubId: club.id, seasonId: season.id });
await Team.create({ name: 'Testteam', clubId: club.id, seasonId: season.id });
await expect(SeasonService.deleteSeason(season.id)).rejects.toThrow('Season is used by teams');
await Team.destroy({ where: { seasonId: season.id } });
await expect(SeasonService.deleteSeason(season.id)).rejects.toThrow('Season is used by leagues');
await League.destroy({ where: { seasonId: season.id } });
const deleted = await SeasonService.deleteSeason(season.id);
expect(deleted).toBe(true);
});
});

View File

@@ -1,98 +0,0 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import request from 'supertest';
vi.mock('../services/emailService.js', () => ({
sendActivationEmail: vi.fn().mockResolvedValue(),
}));
const mockScheduler = vi.hoisted(() => ({
triggerRatingUpdates: vi.fn().mockResolvedValue({ success: true }),
triggerMatchResultsFetch: vi.fn().mockResolvedValue({ success: true }),
getStatus: vi.fn().mockReturnValue({ isRunning: true, jobs: ['ratingUpdates'] }),
getNextRatingUpdateTime: vi.fn().mockReturnValue(new Date('2025-09-01T06:00:00Z')),
}));
vi.mock('../services/schedulerService.js', () => ({
__esModule: true,
default: mockScheduler,
}));
const mockSessionService = vi.hoisted(() => ({
isSessionValid: vi.fn().mockResolvedValue(true),
}));
vi.mock('../services/sessionService.js', () => ({
__esModule: true,
default: mockSessionService,
}));
import app from './testApp.js';
import sequelize from '../database.js';
import '../models/index.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('Session Routes', () => {
beforeEach(async () => {
await sequelize.sync({ force: true });
vi.clearAllMocks();
mockSessionService.isSessionValid.mockResolvedValue(true);
});
it('prüft die Session und liefert Statusinformationen', async () => {
const { credentials } = await registerAndActivate('session@example.com');
const token = await loginAndGetToken(credentials);
const statusResponse = await request(app)
.get('/api/session/status')
.set(authHeaders(token));
expect(mockSessionService.isSessionValid).toHaveBeenCalled();
expect(statusResponse.status).toBe(200);
expect(statusResponse.body.valid).toBe(true);
const schedulerResponse = await request(app)
.get('/api/session/scheduler-status')
.set(authHeaders(token));
expect(schedulerResponse.status).toBe(200);
expect(schedulerResponse.body.isRunning).toBe(true);
});
it('triggert Scheduler-Aktionen manuell', async () => {
const { credentials } = await registerAndActivate('session-trigger@example.com');
const token = await loginAndGetToken(credentials);
const ratingResponse = await request(app)
.post('/api/session/trigger-rating-updates')
.set(authHeaders(token));
expect(ratingResponse.status).toBe(200);
expect(mockScheduler.triggerRatingUpdates).toHaveBeenCalled();
const matchResponse = await request(app)
.post('/api/session/trigger-match-fetch')
.set(authHeaders(token));
expect(matchResponse.status).toBe(200);
expect(mockScheduler.triggerMatchResultsFetch).toHaveBeenCalled();
});
});

View File

@@ -1,15 +0,0 @@
import { afterAll } from 'vitest';
import sequelize from '../database.js';
import '../models/index.js';
process.env.NODE_ENV = process.env.NODE_ENV || 'test';
process.env.DB_DIALECT = process.env.DB_DIALECT || 'sqlite';
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

@@ -1,109 +0,0 @@
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import request from 'supertest';
import fs from 'fs';
import path from 'path';
import os from 'os';
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 Season from '../models/Season.js';
import League from '../models/League.js';
import ClubTeam from '../models/ClubTeam.js';
import UserClub from '../models/UserClub.js';
import TeamDocument from '../models/TeamDocument.js';
const uploadBaseDir = path.join(process.cwd(), 'uploads');
const ensureCleanUploads = () => {
fs.rmSync(uploadBaseDir, { recursive: true, force: true });
};
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('TeamDocument Routes', () => {
beforeEach(async () => {
await sequelize.sync({ force: true });
ensureCleanUploads();
});
afterEach(() => {
ensureCleanUploads();
});
it('lädt Team-Dokumente hoch, listet und löscht sie', async () => {
const { user, credentials } = await registerAndActivate('teamdoc@example.com');
const token = await loginAndGetToken(credentials);
const club = await Club.create({ name: 'DokClub' });
const season = await Season.create({ season: '2025/2026' });
const league = await League.create({ name: 'DokLiga', clubId: club.id, seasonId: season.id });
const team = await ClubTeam.create({ name: 'DokTeam', clubId: club.id, leagueId: league.id, seasonId: season.id });
await UserClub.create({ userId: user.id, clubId: club.id, role: 'admin', approved: true });
const tempUploadPath = path.join(os.tmpdir(), `teamdoc-${Date.now()}.txt`);
fs.writeFileSync(tempUploadPath, 'Sa. 06.09.2025 10:00 Harheimer TC II SG Teststadt III ABC123DEF456', 'utf8');
const uploadResponse = await request(app)
.post(`/api/team-documents/club-team/${team.id}/upload`)
.set(authHeaders(token))
.field('documentType', 'code_list')
.attach('document', tempUploadPath, { filename: 'codes.txt', contentType: 'text/plain' });
fs.unlinkSync(tempUploadPath);
expect(uploadResponse.status).toBe(201);
const documentId = uploadResponse.body.id;
expect(await TeamDocument.count()).toBe(1);
const listResponse = await request(app)
.get(`/api/team-documents/club-team/${team.id}`)
.set(authHeaders(token));
expect(listResponse.status).toBe(200);
expect(listResponse.body).toHaveLength(1);
const getResponse = await request(app)
.get(`/api/team-documents/${documentId}`)
.set(authHeaders(token));
expect(getResponse.status).toBe(200);
const parseResponse = await request(app)
.post(`/api/team-documents/${documentId}/parse`)
.query({ leagueid: league.id })
.set(authHeaders(token));
expect(parseResponse.status).toBe(200);
expect(parseResponse.body.parseResult.matchesFound).toBeDefined();
const deleteResponse = await request(app)
.delete(`/api/team-documents/${documentId}`)
.set(authHeaders(token));
expect(deleteResponse.status).toBe(200);
expect(await TeamDocument.count()).toBe(0);
});
});

View File

@@ -1,102 +0,0 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import fs from 'fs';
import os from 'os';
import path from 'path';
import sequelize from '../database.js';
import '../models/index.js';
import TeamDocumentService from '../services/teamDocumentService.js';
import TeamDocument from '../models/TeamDocument.js';
import Club from '../models/Club.js';
import Season from '../models/Season.js';
import League from '../models/League.js';
import ClubTeam from '../models/ClubTeam.js';
const uploadBaseDir = path.join(process.cwd(), 'uploads');
const ensureCleanUploads = () => {
fs.rmSync(uploadBaseDir, { recursive: true, force: true });
};
const createTempFile = (content = 'test', extension = '.txt') => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'teamdoc-'));
const filePath = path.join(tmpDir, `file${extension}`);
fs.writeFileSync(filePath, content);
return { tmpDir, filePath };
};
const createClubTeam = async () => {
const club = await Club.create({ name: 'Upload Club' });
const season = await Season.create({ season: '2025/2026' });
const league = await League.create({ name: 'Upload Liga', clubId: club.id, seasonId: season.id });
const team = await ClubTeam.create({ name: 'Upload Team', clubId: club.id, leagueId: league.id, seasonId: season.id });
return { club, season, league, team };
};
describe('teamDocumentService', () => {
beforeEach(async () => {
await sequelize.sync({ force: true });
ensureCleanUploads();
});
afterEach(() => {
ensureCleanUploads();
});
it('lädt Dokumente hoch, ersetzt ältere und löscht sie korrekt', async () => {
const { team } = await createClubTeam();
const { filePath } = createTempFile('erste datei');
const uploadFile = {
path: filePath,
originalname: 'erste.txt',
size: fs.statSync(filePath).size,
mimetype: 'text/plain',
};
const doc1 = await TeamDocumentService.uploadDocument(uploadFile, team.id, 'code_list');
expect(doc1.documentType).toBe('code_list');
const storedPath1 = doc1.filePath;
expect(fs.existsSync(storedPath1)).toBe(true);
const { filePath: secondFile } = createTempFile('zweite datei');
const uploadFile2 = {
path: secondFile,
originalname: 'zweite.txt',
size: fs.statSync(secondFile).size,
mimetype: 'text/plain',
};
const doc2 = await TeamDocumentService.uploadDocument(uploadFile2, team.id, 'code_list');
expect(await TeamDocument.count({ where: { clubTeamId: team.id } })).toBe(1);
expect(fs.existsSync(storedPath1)).toBe(false);
expect(fs.existsSync(doc2.filePath)).toBe(true);
const deleted = await TeamDocumentService.deleteDocument(doc2.id);
expect(deleted).toBe(true);
expect(fs.existsSync(doc2.filePath)).toBe(false);
expect(await TeamDocument.count()).toBe(0);
});
it('liefert Dokumentlisten und Details', async () => {
const { team } = await createClubTeam();
const { filePath } = createTempFile('inhalt');
const uploadFile = {
path: filePath,
originalname: 'dok.txt',
size: fs.statSync(filePath).size,
mimetype: 'text/plain',
};
const doc = await TeamDocumentService.uploadDocument(uploadFile, team.id, 'pin_list');
const docsForTeam = await TeamDocumentService.getDocumentsByClubTeam(team.id);
expect(docsForTeam).toHaveLength(1);
const fetched = await TeamDocumentService.getDocumentById(doc.id);
expect(fetched.id).toBe(doc.id);
expect(await TeamDocumentService.getDocumentPath(doc.id)).toBe(fetched.filePath);
});
});

View File

@@ -1,90 +0,0 @@
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 Season from '../models/Season.js';
import League from '../models/League.js';
import UserClub from '../models/UserClub.js';
import Team from '../models/Team.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('Team Routes', () => {
beforeEach(async () => {
await sequelize.sync({ force: true });
});
it('verwaltet Teams eines Clubs (CRUD & Ligen)', async () => {
const { user, credentials } = await registerAndActivate('teamroutes@example.com');
const token = await loginAndGetToken(credentials);
const club = await Club.create({ name: 'Route Club' });
const season = await Season.create({ season: '2025/2026' });
const league = await League.create({ name: 'Route Liga', clubId: club.id, seasonId: season.id });
await UserClub.create({ userId: user.id, clubId: club.id, role: 'admin', approved: true });
const createResponse = await request(app)
.post(`/api/teams/club/${club.id}`)
.set(authHeaders(token))
.send({ name: 'Route Team', leagueId: league.id, seasonId: season.id });
expect(createResponse.status).toBe(201);
const teamId = createResponse.body.id;
const listResponse = await request(app)
.get(`/api/teams/club/${club.id}`)
.set(authHeaders(token))
.query({ seasonid: season.id });
expect(listResponse.status).toBe(200);
expect(listResponse.body).toHaveLength(1);
const leaguesResponse = await request(app)
.get(`/api/teams/leagues/${club.id}`)
.set(authHeaders(token))
.query({ seasonid: season.id });
expect(leaguesResponse.status).toBe(200);
expect(leaguesResponse.body).toHaveLength(1);
const updateResponse = await request(app)
.put(`/api/teams/${teamId}`)
.set(authHeaders(token))
.send({ name: 'Route Team Updated' });
expect(updateResponse.status).toBe(200);
expect(updateResponse.body.name).toBe('Route Team Updated');
const deleteResponse = await request(app)
.delete(`/api/teams/${teamId}`)
.set(authHeaders(token));
expect(deleteResponse.status).toBe(200);
expect(await Team.count()).toBe(0);
});
});

View File

@@ -1,64 +0,0 @@
import { describe, it, expect, beforeEach } from 'vitest';
import sequelize from '../database.js';
import '../models/index.js';
import teamService from '../services/teamService.js';
import SeasonService from '../services/seasonService.js';
import Club from '../models/Club.js';
import Season from '../models/Season.js';
import League from '../models/League.js';
import Team from '../models/Team.js';
describe('teamService', () => {
beforeEach(async () => {
await sequelize.sync({ force: true });
});
it('erstellt Teams und weist automatisch die aktuelle Saison zu', async () => {
const club = await Club.create({ name: 'Team Club' });
const currentSeason = await SeasonService.getOrCreateCurrentSeason();
const team = await teamService.createTeam({ name: 'Team A', clubId: club.id });
expect(team.name).toBe('Team A');
expect(team.seasonId).toBe(currentSeason.id);
});
it('liefert Teams eines Clubs inklusive Liga und Saison', async () => {
const club = await Club.create({ name: 'Team Club' });
const season = await Season.create({ season: '2024/2025' });
const league = await League.create({ name: 'Liga', clubId: club.id, seasonId: season.id });
await Team.create({ name: 'Team A', clubId: club.id, leagueId: league.id, seasonId: season.id });
const teams = await teamService.getAllTeamsByClub(club.id, season.id);
expect(teams).toHaveLength(1);
expect(teams[0].league.name).toBe('Liga');
});
it('aktualisiert und löscht Teams', async () => {
const club = await Club.create({ name: 'Team Club' });
const season = await Season.create({ season: '2023/2024' });
const team = await Team.create({ name: 'Team B', clubId: club.id, seasonId: season.id });
const updated = await teamService.updateTeam(team.id, { name: 'Team B Updated' });
expect(updated).toBe(true);
await team.reload();
expect(team.name).toBe('Team B Updated');
const deleted = await teamService.deleteTeam(team.id);
expect(deleted).toBe(true);
expect(await Team.count()).toBe(0);
});
it('liefert Ligen eines Clubs, wobei die aktuelle Saison verwendet wird', async () => {
const club = await Club.create({ name: 'Team Club' });
const season = await SeasonService.getOrCreateCurrentSeason();
await League.create({ name: 'Aktuelle Liga', clubId: club.id, seasonId: season.id });
const leagues = await teamService.getLeaguesByClub(club.id);
expect(leagues).toHaveLength(1);
expect(leagues[0].name).toBe('Aktuelle Liga');
});
});

View File

@@ -1,64 +0,0 @@
import express from 'express';
import { setupRouteAuthMocks } from './utils/routeAuthMocks.js';
import authRoutes from '../routes/authRoutes.js';
import permissionRoutes from '../routes/permissionRoutes.js';
import accidentRoutes from '../routes/accidentRoutes.js';
import activityRoutes from '../routes/activityRoutes.js';
import apiLogRoutes from '../routes/apiLogRoutes.js';
import clubRoutes from '../routes/clubRoutes.js';
import clubTeamRoutes from '../routes/clubTeamRoutes.js';
import diaryRoutes from '../routes/diaryRoutes.js';
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';
import memberTransferConfigRoutes from '../routes/memberTransferConfigRoutes.js';
import myTischtennisRoutes from '../routes/myTischtennisRoutes.js';
import predefinedActivityRoutes from '../routes/predefinedActivityRoutes.js';
import seasonRoutes from '../routes/seasonRoutes.js';
import sessionRoutes from '../routes/sessionRoutes.js';
import teamDocumentRoutes from '../routes/teamDocumentRoutes.js';
import teamRoutes from '../routes/teamRoutes.js';
import tournamentRoutes from '../routes/tournamentRoutes.js';
const app = express();
app.use(express.json());
app.use('/api/auth', authRoutes);
app.use('/api/permissions', permissionRoutes);
app.use('/api/accident', accidentRoutes);
app.use('/api/activities', activityRoutes);
app.use('/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('/api/member-transfer-config', memberTransferConfigRoutes);
app.use('/api/mytischtennis', myTischtennisRoutes);
app.use('/api/predefined-activities', predefinedActivityRoutes);
app.use('/api/seasons', seasonRoutes);
app.use('/api/session', sessionRoutes);
app.use('/api/team-documents', teamDocumentRoutes);
app.use('/api/teams', teamRoutes);
app.use('/api/tournaments', tournamentRoutes);
app.use((err, req, res, next) => {
const status = err?.status || err?.statusCode || 500;
const message = err?.message || 'Interner Serverfehler';
res.status(status).json({ success: false, message, error: message });
});
export default app;

View File

@@ -1,16 +0,0 @@
import { vi } from 'vitest';
export function buildMockRequest({ body = {}, params = {}, headers = {} } = {}) {
return {
body,
params,
headers,
};
}
export function buildMockResponse() {
const res = {};
res.status = vi.fn().mockReturnValue(res);
res.json = vi.fn().mockReturnValue(res);
return res;
}

View File

@@ -1,84 +0,0 @@
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 Tournament from '../models/Tournament.js';
import UserClub from '../models/UserClub.js';
import { createMember } from './utils/factories.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('Tournament Routes', () => {
beforeEach(async () => {
await sequelize.sync({ force: true });
});
it('legt Turniere an, listet sie und verwaltet Teilnehmer', async () => {
const { user, credentials } = await registerAndActivate('tournament@example.com');
const token = await loginAndGetToken(credentials);
const club = await Club.create({ name: 'Turnierclub' });
await UserClub.create({ userId: user.id, clubId: club.id, role: 'admin', approved: true });
const participant = await createMember(club.id, {
firstName: 'Spieler',
lastName: 'Eins',
email: 'spieler@example.com',
gender: 'male',
});
const createResponse = await request(app)
.post('/api/tournaments')
.set(authHeaders(token))
.send({ clubId: club.id, tournamentName: 'Frühlingsturnier', date: '2025-03-01' });
expect(createResponse.status).toBe(201);
const tournamentId = createResponse.body.id;
const listResponse = await request(app)
.get(`/api/tournaments/${club.id}`)
.set(authHeaders(token));
expect(listResponse.status).toBe(200);
expect(listResponse.body).toHaveLength(1);
const participantResponse = await request(app)
.post('/api/tournaments/participant')
.set(authHeaders(token))
.send({ clubId: club.id, tournamentId, participant: participant.id });
expect(participantResponse.status).toBe(200);
expect(participantResponse.body).toHaveLength(1);
const participantsList = await request(app)
.post('/api/tournaments/participants')
.set(authHeaders(token))
.send({ clubId: club.id, tournamentId });
expect(participantsList.status).toBe(200);
expect(participantsList.body[0].member.firstName).toBe('Spieler');
});
});

View File

@@ -1,706 +0,0 @@
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 { Op } from 'sequelize';
import '../models/index.js';
import tournamentService from '../services/tournamentService.js';
import Tournament from '../models/Tournament.js';
import TournamentGroup from '../models/TournamentGroup.js';
import TournamentMember from '../models/TournamentMember.js';
import TournamentMatch from '../models/TournamentMatch.js';
import TournamentResult from '../models/TournamentResult.js';
import TournamentStage from '../models/TournamentStage.js';
import TournamentClass from '../models/TournamentClass.js';
import Club from '../models/Club.js';
import { createMember } from './utils/factories.js';
describe('tournamentService', () => {
beforeEach(async () => {
await sequelize.sync({ force: true });
});
it('legt Turniere an und verhindert Duplikate', async () => {
const club = await Club.create({ name: 'Tournament Club' });
const date = '2025-09-01';
const tournament = await tournamentService.addTournament('token', club.id, 'Herbstturnier', date);
expect(tournament.name).toBe('Herbstturnier');
await expect(
tournamentService.addTournament('token', club.id, 'Herbstturnier 2', date)
).rejects.toThrow('Ein Turnier mit diesem Datum existiert bereits');
});
it('verwaltet Teilnehmer und Gruppen', async () => {
const club = await Club.create({ name: 'Tournament Club' });
const memberA = await createMember(club.id, {
firstName: 'Anna',
lastName: 'A',
email: 'anna@example.com',
gender: 'female',
});
const memberB = await createMember(club.id, {
firstName: 'Bernd',
lastName: 'B',
email: 'bernd@example.com',
gender: 'male',
});
const tournament = await tournamentService.addTournament('token', club.id, 'Wintercup', '2025-12-01');
await tournamentService.addParticipant('token', club.id, tournament.id, memberA.id);
await tournamentService.addParticipant('token', club.id, tournament.id, memberB.id);
await expect(
tournamentService.addParticipant('token', club.id, tournament.id, memberA.id)
).rejects.toThrow('Teilnehmer bereits hinzugefügt');
await tournamentService.setModus('token', club.id, tournament.id, 'groups', 1, 1);
await tournamentService.createGroups('token', club.id, tournament.id);
const groupsBeforeFill = await TournamentGroup.findAll({ where: { tournamentId: tournament.id } });
expect(groupsBeforeFill).toHaveLength(1);
await tournamentService.fillGroups('token', club.id, tournament.id);
const membersWithGroups = await TournamentMember.findAll({ where: { tournamentId: tournament.id } });
expect(membersWithGroups.every((m) => m.groupId !== null)).toBe(true);
await tournamentService.addMatchResult('token', club.id, tournament.id, (await TournamentMatch.findOne()).id, 1, '11:9');
const matches = await tournamentService.getTournamentMatches('token', club.id, tournament.id);
expect(matches.length).toBeGreaterThan(0);
});
it('erlaubt Teilnehmer ohne Klasse', async () => {
const club = await Club.create({ name: 'Tournament Club' });
const memberA = await createMember(club.id, {
firstName: 'Clara',
lastName: 'C',
email: 'clara@example.com',
gender: 'female',
});
const tournament = await tournamentService.addTournament('token', club.id, 'Sommercup', '2025-06-01');
// ohne Klasse: legacy-Aufruf (3. Argument = tournamentId)
await tournamentService.addParticipant('token', club.id, tournament.id, memberA.id);
await expect(
tournamentService.addParticipant('token', club.id, tournament.id, memberA.id)
).rejects.toThrow('Teilnehmer bereits hinzugefügt');
const participantsNoClass = await tournamentService.getParticipants('token', club.id, tournament.id, null);
expect(participantsNoClass).toHaveLength(1);
expect(participantsNoClass[0].classId).toBe(null);
expect(participantsNoClass[0].clubMemberId).toBe(memberA.id);
});
it('normalisiert numberOfGroups=0 auf mindestens 1 Gruppe', async () => {
const club = await Club.create({ name: 'Tournament Club' });
const tournament = await tournamentService.addTournament('token', club.id, 'Gruppen-Test', '2025-07-01');
await tournamentService.createGroups('token', club.id, tournament.id, 0);
const groups = await TournamentGroup.findAll({ where: { tournamentId: tournament.id } });
expect(groups).toHaveLength(1);
});
it('legt bei numberOfGroups=4 genau 4 Gruppen an (ohne Klassen)', async () => {
const club = await Club.create({ name: 'Tournament Club' });
const tournament = await tournamentService.addTournament('token', club.id, 'Gruppen-4er', '2025-08-01');
await tournamentService.createGroups('token', club.id, tournament.id, 4);
const groups = await TournamentGroup.findAll({ where: { tournamentId: tournament.id } });
expect(groups).toHaveLength(4);
});
it('verteilt bei "zufällig verteilen" möglichst gleichmäßig (Differenz <= 1)', async () => {
const club = await Club.create({ name: 'Tournament Club' });
const tournament = await tournamentService.addTournament('token', club.id, 'Fill-Groups-Balanced', '2025-10-01');
// 10 Teilnehmer, 4 Gruppen => erwartete Größen: 3/3/2/2 (beliebige Reihenfolge)
const members = [];
for (let i = 0; i < 10; i++) {
// createMember Factory braucht eindeutige Emails
members.push(
await createMember(club.id, {
firstName: `P${i}`,
lastName: 'T',
email: `p${i}@example.com`,
gender: i % 2 === 0 ? 'male' : 'female',
})
);
}
for (const m of members) {
await tournamentService.addParticipant('token', club.id, tournament.id, m.id);
}
// Seeded-Balancing triggern: markiere mehrere als gesetzt
// (wir testen hier explizit, dass diese Optimierung die Größen-Balance NICHT kaputt machen darf)
const tmRows = await TournamentMember.findAll({ where: { tournamentId: tournament.id } });
const seededIds = tmRows.slice(0, 5).map(r => r.id);
await TournamentMember.update({ seeded: true }, { where: { id: { [Op.in]: seededIds } } });
await tournamentService.setModus('token', club.id, tournament.id, 'groups', 4, 1);
await tournamentService.createGroups('token', club.id, tournament.id, 4);
await tournamentService.fillGroups('token', club.id, tournament.id);
const groups = await TournamentGroup.findAll({ where: { tournamentId: tournament.id } });
expect(groups).toHaveLength(4);
const membersWithGroups = await TournamentMember.findAll({ where: { tournamentId: tournament.id } });
const countsByGroupId = membersWithGroups.reduce((m, tm) => {
m[tm.groupId] = (m[tm.groupId] || 0) + 1;
return m;
}, {});
const sizes = groups.map(g => countsByGroupId[g.id] || 0);
const min = Math.min(...sizes);
const max = Math.max(...sizes);
expect(max - min).toBeLessThanOrEqual(1);
expect(sizes.reduce((a, b) => a + b, 0)).toBe(10);
});
it('füllt beim KO mit thirdPlace=true das Platz-3-Spiel nach beiden Halbfinals', async () => {
const club = await Club.create({ name: 'Tournament Club' });
const tournament = await tournamentService.addTournament('token', club.id, 'KO-3rd', '2025-11-01');
const members = [];
for (let i = 0; i < 4; i++) {
members.push(
await createMember(club.id, {
firstName: `K${i}`,
lastName: 'O',
email: `ko3rd_${i}@example.com`,
gender: i % 2 === 0 ? 'male' : 'female',
})
);
}
for (const m of members) {
await tournamentService.addParticipant('token', club.id, tournament.id, m.id);
}
// Wir gehen den Stage-Flow, damit stageId+groupId gesetzt ist.
await tournamentService.upsertTournamentStages(
'token',
club.id,
tournament.id,
[
{ index: 1, type: 'groups', name: 'Vorrunde', numberOfGroups: 1 },
{ index: 2, type: 'knockout', name: 'Endrunde', numberOfGroups: null },
],
null,
[
{
fromStageIndex: 1,
toStageIndex: 2,
mode: 'pools',
config: {
pools: [
{
fromPlaces: [1, 2, 3, 4],
target: { type: 'knockout', singleField: true, thirdPlace: true }
}
]
}
}
]
);
// Vorrunde-Gruppen+Member-Gruppenzuordnung vorbereiten.
await tournamentService.createGroups('token', club.id, tournament.id, 1);
await tournamentService.fillGroups('token', club.id, tournament.id);
// Endrunde (KO) erstellen
await tournamentService.advanceTournamentStage('token', club.id, tournament.id, 1, 2);
const stage2 = await TournamentStage.findOne({ where: { tournamentId: tournament.id, index: 2 } });
expect(stage2).toBeTruthy();
// Third-Place Match muss als Placeholder existieren
const third = await TournamentMatch.findOne({
where: { tournamentId: tournament.id, stageId: stage2.id, round: 'Spiel um Platz 3' }
});
expect(third).toBeTruthy();
expect(third.player1Id).toBe(null);
expect(third.player2Id).toBe(null);
// Beide Halbfinals abschließen
const semis = await TournamentMatch.findAll({
where: { tournamentId: tournament.id, stageId: stage2.id },
order: [['id', 'ASC']]
});
const semiMatches = semis.filter(m => String(m.round || '').includes('Halbfinale'));
expect(semiMatches.length).toBe(2);
// Sieger jeweils als Player1 setzen (3 Gewinnsätze simulieren; winningSets default = 3)
for (const sm of semiMatches) {
await tournamentService.addMatchResult('token', club.id, tournament.id, sm.id, 1, '11:1');
await tournamentService.addMatchResult('token', club.id, tournament.id, sm.id, 2, '11:1');
await tournamentService.addMatchResult('token', club.id, tournament.id, sm.id, 3, '11:1');
}
const thirdAfter = await TournamentMatch.findOne({
where: { tournamentId: tournament.id, stageId: stage2.id, round: 'Spiel um Platz 3' }
});
expect(thirdAfter).toBeTruthy();
expect(thirdAfter.isActive).toBe(true);
expect(thirdAfter.player1Id).toBeTruthy();
expect(thirdAfter.player2Id).toBeTruthy();
expect(thirdAfter.player1Id).not.toBe(thirdAfter.player2Id);
});
it('Stage-KO: 3 Gruppen × Plätze 1,2 => 6 Qualifier (keine falschen IDs, keine Duplikate)', async () => {
const club = await Club.create({ name: 'Tournament Club' });
const tournament = await tournamentService.addTournament('token', club.id, 'Stage-KO-6', '2025-11-20');
// Stages: Vorrunde (Groups) -> Endrunde (KO)
await tournamentService.upsertTournamentStages(
'token',
club.id,
tournament.id,
[
{ index: 1, type: 'groups', name: 'Vorrunde', numberOfGroups: 3 },
{ index: 2, type: 'knockout', name: 'Endrunde', numberOfGroups: null },
],
null,
[
{
fromStageIndex: 1,
toStageIndex: 2,
mode: 'pools',
config: {
pools: [
{
fromPlaces: [1, 2],
target: { type: 'knockout', singleField: true, thirdPlace: false },
},
],
},
},
]
);
// 3 Gruppen anlegen
await tournamentService.createGroups('token', club.id, tournament.id, 3);
const groups = await TournamentGroup.findAll({ where: { tournamentId: tournament.id }, order: [['id', 'ASC']] });
expect(groups).toHaveLength(3);
// Je Gruppe 2 Teilnehmer -> insgesamt 6
const members = [];
for (let i = 0; i < 6; i++) {
members.push(
await createMember(club.id, {
firstName: `S${i}`,
lastName: 'KO',
email: `stage_ko6_${i}@example.com`,
gender: i % 2 === 0 ? 'male' : 'female',
})
);
}
// Gruppe 1
await TournamentMember.create({ tournamentId: tournament.id, clubMemberId: members[0].id, classId: null, groupId: groups[0].id });
await TournamentMember.create({ tournamentId: tournament.id, clubMemberId: members[1].id, classId: null, groupId: groups[0].id });
// Gruppe 2
await TournamentMember.create({ tournamentId: tournament.id, clubMemberId: members[2].id, classId: null, groupId: groups[1].id });
await TournamentMember.create({ tournamentId: tournament.id, clubMemberId: members[3].id, classId: null, groupId: groups[1].id });
// Gruppe 3
await TournamentMember.create({ tournamentId: tournament.id, clubMemberId: members[4].id, classId: null, groupId: groups[2].id });
await TournamentMember.create({ tournamentId: tournament.id, clubMemberId: members[5].id, classId: null, groupId: groups[2].id });
// Gruppenspiele erzeugen+beenden (damit Ranking/Platz 1/2 stabil ist)
// Wir erzeugen minimal pro Gruppe ein 1v1-Match und schließen es ab.
for (const g of groups) {
const [tm1, tm2] = await TournamentMember.findAll({ where: { tournamentId: tournament.id, groupId: g.id }, order: [['id', 'ASC']] });
const gm = await TournamentMatch.create({
tournamentId: tournament.id,
round: 'group',
groupId: g.id,
classId: null,
player1Id: tm1.id,
player2Id: tm2.id,
isFinished: true,
isActive: true,
result: '3:0',
});
await TournamentResult.bulkCreate([
{ matchId: gm.id, pointsPlayer1: 11, pointsPlayer2: 1, setNumber: 1 },
{ matchId: gm.id, pointsPlayer1: 11, pointsPlayer2: 1, setNumber: 2 },
{ matchId: gm.id, pointsPlayer1: 11, pointsPlayer2: 1, setNumber: 3 },
]);
}
// KO-Endrunde erstellen
await tournamentService.advanceTournamentStage('token', club.id, tournament.id, 1, 2);
const stage2 = await TournamentStage.findOne({ where: { tournamentId: tournament.id, index: 2 } });
expect(stage2).toBeTruthy();
const stage2Matches = await TournamentMatch.findAll({ where: { tournamentId: tournament.id, stageId: stage2.id }, order: [['id', 'ASC']] });
const round1 = stage2Matches.filter(m => String(m.round || '').includes('Viertelfinale') || String(m.round || '').includes('Achtelfinale') || String(m.round || '').includes('Halbfinale (3)'));
// Bei 6 Entrants muss ein 8er-Bracket entstehen => 3 Matches in der ersten Runde.
// (Die Byes werden nicht als Matches angelegt.)
expect(round1.length).toBe(3);
for (const m of round1) {
expect(m.player1Id).toBeTruthy();
expect(m.player2Id).toBeTruthy();
expect(m.player1Id).not.toBe(m.player2Id);
}
// Spieler-IDs müssen Member-IDs (clubMemberId) sein, nicht TournamentMember.id
const memberIdSet = new Set(members.map(x => x.id));
for (const m of round1) {
expect(memberIdSet.has(m.player1Id)).toBe(true);
expect(memberIdSet.has(m.player2Id)).toBe(true);
}
});
it('Legacy-KO: legt Platz-3 an und befüllt es nach beiden Halbfinals', async () => {
const club = await Club.create({ name: 'Tournament Club' });
const tournament = await tournamentService.addTournament('token', club.id, 'Legacy-KO-3rd', '2025-11-15');
// Legacy-KO erzeugt Platz-3 automatisch, sobald ein KO ab Halbfinale gestartet wird.
// Legacy-startKnockout aktiviert Qualifier-Ermittlung über Gruppen-Logik.
// Dafür erstellen wir 2 Gruppen und spielen je 1 Match fertig.
await tournamentService.setModus('token', club.id, tournament.id, 'groups', 2, 2);
const members = [];
for (let i = 0; i < 4; i++) {
members.push(
await createMember(club.id, {
firstName: `L${i}`,
lastName: 'KO',
email: `legacy_ko3rd_${i}@example.com`,
gender: i % 2 === 0 ? 'male' : 'female',
})
);
}
for (const m of members) {
await tournamentService.addParticipant('token', club.id, tournament.id, m.id);
}
await tournamentService.createGroups('token', club.id, tournament.id, 2);
await tournamentService.fillGroups('token', club.id, tournament.id);
// Pro Gruppe 1 Match beenden, damit Qualifier (Platz 1) ermittelt werden können.
// Die Round-Robin-Gruppenspiele werden beim `fillGroups()` bereits angelegt.
const groupMatches = await TournamentMatch.findAll({
where: { tournamentId: tournament.id, round: 'group' },
order: [['id', 'ASC']]
});
expect(groupMatches.length).toBeGreaterThanOrEqual(2);
// Beende alle Gruppenspiele, damit pro Gruppe die Top-2 zuverlässig bestimmbar sind
for (const gm of groupMatches) {
await tournamentService.addMatchResult('token', club.id, tournament.id, gm.id, 1, '11:1');
await tournamentService.addMatchResult('token', club.id, tournament.id, gm.id, 2, '11:1');
await tournamentService.addMatchResult('token', club.id, tournament.id, gm.id, 3, '11:1');
}
// Direkt KO starten (ohne Stages)
await tournamentService.startKnockout('token', club.id, tournament.id);
const semisAll = await TournamentMatch.findAll({
where: { tournamentId: tournament.id },
order: [['id', 'ASC']]
});
const semiMatches = semisAll.filter(m => String(m.round || '').includes('Halbfinale'));
expect(semiMatches.length).toBe(2);
// Beide Halbfinals beenden -> Platz-3 muss befüllt werden
for (const sm of semiMatches) {
await tournamentService.addMatchResult('token', club.id, tournament.id, sm.id, 1, '11:1');
await tournamentService.addMatchResult('token', club.id, tournament.id, sm.id, 2, '11:1');
await tournamentService.addMatchResult('token', club.id, tournament.id, sm.id, 3, '11:1');
}
const thirdAfter = await TournamentMatch.findOne({
where: { tournamentId: tournament.id, round: 'Spiel um Platz 3' }
});
expect(thirdAfter).toBeTruthy();
expect(thirdAfter.isActive).toBe(true);
expect(thirdAfter.player1Id).toBeTruthy();
expect(thirdAfter.player2Id).toBeTruthy();
expect(thirdAfter.player1Id).not.toBe(thirdAfter.player2Id);
});
it('Legacy-KO: bei ungerader Qualifier-Zahl wird ein Freilos vergeben (kein Duplikat / kein Self-Match)', async () => {
const club = await Club.create({ name: 'Tournament Club' });
const tournament = await tournamentService.addTournament('token', club.id, 'Legacy-KO-Bye', '2025-11-17');
// 3 Gruppen, jeweils 1 Spieler -> advancingPerGroup=1 => 3 Qualifier
await tournamentService.setModus('token', club.id, tournament.id, 'groups', 3, 1);
await tournamentService.createGroups('token', club.id, tournament.id, 3);
const groups = await TournamentGroup.findAll({ where: { tournamentId: tournament.id }, order: [['id', 'ASC']] });
expect(groups).toHaveLength(3);
const members = [];
for (let i = 0; i < 3; i++) {
members.push(
await createMember(club.id, {
firstName: `B${i}`,
lastName: 'YE',
email: `legacy_bye_${i}@example.com`,
gender: i % 2 === 0 ? 'male' : 'female',
})
);
}
// Je Gruppe genau 1 Teilnehmer, und keine Gruppenspiele nötig (es gibt keine Paarungen)
await TournamentMember.create({
tournamentId: tournament.id,
clubMemberId: members[0].id,
classId: null,
groupId: groups[0].id,
});
await TournamentMember.create({
tournamentId: tournament.id,
clubMemberId: members[1].id,
classId: null,
groupId: groups[1].id,
});
await TournamentMember.create({
tournamentId: tournament.id,
clubMemberId: members[2].id,
classId: null,
groupId: groups[2].id,
});
// KO starten: Erwartung = genau 1 Match (2 Spieler) + 1 Freilos (ohne extra Match)
await tournamentService.startKnockout('token', club.id, tournament.id);
const koMatches = await TournamentMatch.findAll({
where: { tournamentId: tournament.id, round: { [Op.ne]: 'group' } },
order: [['id', 'ASC']],
});
// Bei 3 Qualifiern muss GENAU EIN Halbfinale (3) existieren.
const semi3 = koMatches.filter(m => m.round === 'Halbfinale (3)');
expect(semi3).toHaveLength(1);
expect(semi3[0].player1Id).toBeTruthy();
expect(semi3[0].player2Id).toBeTruthy();
expect(semi3[0].player1Id).not.toBe(semi3[0].player2Id);
// Self-match darf nirgends vorkommen.
for (const m of koMatches) {
if (m.player1Id && m.player2Id) expect(m.player1Id).not.toBe(m.player2Id);
}
// Hinweis: Bei 3 Qualifiern wird im Legacy-Flow aktuell ein "Halbfinale (3)" erzeugt.
// Ein automatisches Weitertragen des Freiloses bis in ein fertiges Finale ist nicht Teil dieses Tests.
// Wichtig ist hier die Regression: kein Duplikat und kein Self-Match.
// Halbfinale beenden (soll keine kaputten Folge-Matches erzeugen)
await tournamentService.addMatchResult('token', club.id, tournament.id, semi3[0].id, 1, '11:1');
await tournamentService.addMatchResult('token', club.id, tournament.id, semi3[0].id, 2, '11:1');
await tournamentService.addMatchResult('token', club.id, tournament.id, semi3[0].id, 3, '11:1');
const after = await TournamentMatch.findAll({
where: { tournamentId: tournament.id, round: { [Op.ne]: 'group' } },
order: [['id', 'ASC']],
});
// Egal ob ein Folge-Match entsteht oder nicht: es darf kein Self-Match geben.
for (const m of after) {
if (m.player1Id && m.player2Id) expect(m.player1Id).not.toBe(m.player2Id);
}
});
it('Stage advancement ist klassenisoliert (Zwischen-/Endrunde hängt nur von der jeweiligen Klasse ab)', async () => {
const club = await Club.create({ name: 'Club', accessToken: 'token' });
const tournament = await Tournament.create({
clubId: club.id,
name: 'Stages Multi-Class',
date: '2025-12-14',
type: 'groups',
numberOfGroups: 2,
advancingPerGroup: 1,
winningSets: 3,
allowsExternal: false,
});
const classA = await TournamentClass.create({ tournamentId: tournament.id, name: 'A' });
const classB = await TournamentClass.create({ tournamentId: tournament.id, name: 'B' });
await tournamentService.upsertTournamentStages(
'token',
club.id,
tournament.id,
[
{ index: 1, type: 'groups', name: 'Vorrunde', numberOfGroups: 2 },
{ index: 2, type: 'knockout', name: 'Endrunde', numberOfGroups: null },
],
null,
[
{
fromStageIndex: 1,
toStageIndex: 2,
mode: 'pools',
config: {
pools: [
{ fromPlaces: [1], target: { type: 'knockout', singleField: true, thirdPlace: false } },
],
},
},
]
);
await tournamentService.createGroups('token', club.id, tournament.id, 2);
const groups = await TournamentGroup.findAll({ where: { tournamentId: tournament.id }, order: [['id', 'ASC']] });
expect(groups.length).toBe(2);
// Klasse A fertig
const memberA1 = await createMember(club.id, {
firstName: 'A1',
lastName: 'Test',
email: 'stage_class_a1@example.com',
gender: 'male',
});
const memberA2 = await createMember(club.id, {
firstName: 'A2',
lastName: 'Test',
email: 'stage_class_a2@example.com',
gender: 'female',
});
const a1 = await TournamentMember.create({ tournamentId: tournament.id, clubMemberId: memberA1.id, classId: classA.id, groupId: groups[0].id });
const a2 = await TournamentMember.create({ tournamentId: tournament.id, clubMemberId: memberA2.id, classId: classA.id, groupId: groups[0].id });
const aMatch = await TournamentMatch.create({
tournamentId: tournament.id,
round: 'group',
groupId: groups[0].id,
classId: classA.id,
player1Id: a1.id,
player2Id: a2.id,
isFinished: true,
isActive: true,
result: '3:0',
});
await TournamentResult.bulkCreate([
{ matchId: aMatch.id, pointsPlayer1: 11, pointsPlayer2: 0, setNumber: 1 },
{ matchId: aMatch.id, pointsPlayer1: 11, pointsPlayer2: 0, setNumber: 2 },
{ matchId: aMatch.id, pointsPlayer1: 11, pointsPlayer2: 0, setNumber: 3 },
]);
// Klasse B unfertig
const memberB1 = await createMember(club.id, {
firstName: 'B1',
lastName: 'Test',
email: 'stage_class_b1@example.com',
gender: 'male',
});
const memberB2 = await createMember(club.id, {
firstName: 'B2',
lastName: 'Test',
email: 'stage_class_b2@example.com',
gender: 'female',
});
const b1 = await TournamentMember.create({ tournamentId: tournament.id, clubMemberId: memberB1.id, classId: classB.id, groupId: groups[1].id });
const b2 = await TournamentMember.create({ tournamentId: tournament.id, clubMemberId: memberB2.id, classId: classB.id, groupId: groups[1].id });
await TournamentMatch.create({
tournamentId: tournament.id,
round: 'group',
groupId: groups[1].id,
classId: classB.id,
player1Id: b1.id,
player2Id: b2.id,
isFinished: false,
isActive: true,
result: null,
});
await tournamentService.advanceTournamentStage('token', club.id, tournament.id, 1, 2);
const stage2 = await TournamentStage.findOne({ where: { tournamentId: tournament.id, index: 2 } });
expect(stage2).toBeTruthy();
const stage2Matches = await TournamentMatch.findAll({ where: { tournamentId: tournament.id, stageId: stage2.id } });
expect(stage2Matches.some(m => m.classId === classB.id)).toBe(false);
// Und es wurden keine Stage2-Gruppen für Klasse B erzeugt.
// (classless Container-Gruppen sind möglich entscheidend ist, dass Klasse B nicht blockiert/vermengt wird.)
const stage2Groups = await TournamentGroup.findAll({ where: { tournamentId: tournament.id, stageId: stage2.id } });
expect(stage2Groups.some(g => g.classId === classB.id)).toBe(false);
});
it('Legacy-KO: Platz-3 entsteht erst nach beiden Halbfinals (ohne Placeholder)', async () => {
const club = await Club.create({ name: 'Tournament Club' });
const tournament = await tournamentService.addTournament('token', club.id, 'Legacy-KO-3rd-late', '2025-11-16');
// Gruppen nötig, damit startKnockout Qualifier ermitteln kann
await tournamentService.setModus('token', club.id, tournament.id, 'groups', 2, 2);
const members = [];
for (let i = 0; i < 4; i++) {
members.push(
await createMember(club.id, {
firstName: `LL${i}`,
lastName: 'KO',
email: `legacy_ko3rd_late_${i}@example.com`,
gender: i % 2 === 0 ? 'male' : 'female',
})
);
}
for (const m of members) {
await tournamentService.addParticipant('token', club.id, tournament.id, m.id);
}
await tournamentService.createGroups('token', club.id, tournament.id, 2);
await tournamentService.fillGroups('token', club.id, tournament.id);
// Alle Gruppenspiele beenden
const groupMatches = await TournamentMatch.findAll({
where: { tournamentId: tournament.id, round: 'group' },
order: [['id', 'ASC']]
});
expect(groupMatches.length).toBeGreaterThanOrEqual(2);
for (const gm of groupMatches) {
await tournamentService.addMatchResult('token', club.id, tournament.id, gm.id, 1, '11:1');
await tournamentService.addMatchResult('token', club.id, tournament.id, gm.id, 2, '11:1');
await tournamentService.addMatchResult('token', club.id, tournament.id, gm.id, 3, '11:1');
}
// KO starten
await tournamentService.startKnockout('token', club.id, tournament.id);
// Vor Halbfinal-Ende darf es kein Platz-3-Spiel geben
const thirdBefore = await TournamentMatch.findOne({
where: { tournamentId: tournament.id, round: 'Spiel um Platz 3' }
});
expect(thirdBefore).toBeNull();
// Beide Halbfinals beenden -> dabei wird Finale erzeugt. Dabei muss jetzt auch Platz-3 wieder entstehen.
const koMatches = await TournamentMatch.findAll({
where: { tournamentId: tournament.id },
order: [['id', 'ASC']]
});
const semiMatches = koMatches.filter(m => String(m.round || '').includes('Halbfinale'));
expect(semiMatches.length).toBe(2);
for (const sm of semiMatches) {
await tournamentService.addMatchResult('token', club.id, tournament.id, sm.id, 1, '11:1');
await tournamentService.addMatchResult('token', club.id, tournament.id, sm.id, 2, '11:1');
await tournamentService.addMatchResult('token', club.id, tournament.id, sm.id, 3, '11:1');
}
const thirdAfter = await TournamentMatch.findOne({
where: { tournamentId: tournament.id, round: 'Spiel um Platz 3' }
});
expect(thirdAfter).toBeTruthy();
expect(thirdAfter.player1Id).toBeTruthy();
expect(thirdAfter.player2Id).toBeTruthy();
expect(thirdAfter.player1Id).not.toBe(thirdAfter.player2Id);
const finalAfter = await TournamentMatch.findOne({
where: { tournamentId: tournament.id, round: 'Finale' }
});
expect(finalAfter).toBeTruthy();
});
});

View File

@@ -1,123 +0,0 @@
import Club from '../../models/Club.js';
import Member from '../../models/Member.js';
import User from '../../models/User.js';
import UserClub from '../../models/UserClub.js';
import Season from '../../models/Season.js';
import League from '../../models/League.js';
import Team from '../../models/Team.js';
import DiaryDate from '../../models/DiaryDates.js';
let uniqueCounter = 0;
export const nextSuffix = () => {
uniqueCounter += 1;
return uniqueCounter;
};
export async function createClub(overrides = {}) {
const suffix = nextSuffix();
return Club.create({
name: `Test Club ${suffix}`,
...overrides,
});
}
export function buildMemberData(clubId, overrides = {}) {
const suffix = nextSuffix();
return {
firstName: `Member${suffix}`,
lastName: `Test${suffix}`,
phone: `0123456${suffix.toString().padStart(3, '0')}`,
street: `Test Street ${suffix}`,
city: 'Test City',
email: `member${suffix}@example.com`,
active: true,
testMembership: false,
picsInInternetAllowed: false,
clubId,
...overrides,
};
}
export async function createMember(clubId, overrides = {}) {
let resolvedClubId = clubId;
if (!resolvedClubId) {
const club = await createClub();
resolvedClubId = club.id;
}
return Member.create(buildMemberData(resolvedClubId, overrides));
}
export async function createDiaryDate(clubId, overrides = {}) {
let resolvedClubId = clubId;
if (!resolvedClubId) {
const club = await createClub();
resolvedClubId = club.id;
}
return DiaryDate.create({
date: '2025-01-01',
trainingStart: null,
trainingEnd: null,
clubId: resolvedClubId,
...overrides,
});
}
export async function createUser(overrides = {}) {
const suffix = nextSuffix();
return User.create({
email: `user${suffix}@example.com`,
password: 'Secret!123',
isActive: true,
...overrides,
});
}
export async function linkUserToClub(userId, clubId, overrides = {}) {
return UserClub.create({
userId,
clubId,
role: 'admin',
approved: true,
isOwner: false,
...overrides,
});
}
export async function createUserWithClub(overrides = {}) {
const club = await createClub(overrides.club || {});
const user = await createUser(overrides.user || {});
await linkUserToClub(user.id, club.id, overrides.userClub || { isOwner: true });
return { user, club };
}
export async function ensureSeason(overrides = {}) {
const suffix = nextSuffix();
return Season.create({
name: `202${suffix}/202${suffix + 1}`,
isCurrent: true,
...overrides,
});
}
export async function createLeague(clubId, overrides = {}) {
const season = overrides.seasonId ? null : await ensureSeason();
return League.create({
name: `League ${nextSuffix()}`,
clubId,
seasonId: overrides.seasonId ?? season.id,
...overrides,
});
}
export async function createTeam(clubId, overrides = {}) {
const season = overrides.seasonId ? null : await ensureSeason();
const league = overrides.leagueId ? null : await createLeague(clubId, { seasonId: overrides.seasonId ?? season.id });
return Team.create({
name: `Team ${nextSuffix()}`,
clubId,
leagueId: overrides.leagueId ?? league.id,
seasonId: overrides.seasonId ?? season.id,
...overrides,
});
}

View File

@@ -1,107 +0,0 @@
import { vi } from 'vitest';
import jwt from 'jsonwebtoken';
import UserClub from '../../models/UserClub.js';
function resolveUserFromToken(token) {
if (!token || token === 'forbidden') {
const error = new Error('noaccess');
error.status = 403;
throw error;
}
try {
const secret = process.env.JWT_SECRET || 'test-secret';
const payload = jwt.verify(token, secret);
return { id: payload.userId || 1 };
} catch (error) {
const fallbackError = new Error('noaccess');
fallbackError.status = 403;
throw fallbackError;
}
}
async function ensureClubAccess(token, clubId) {
const user = resolveUserFromToken(token);
const normalizedClubId = Number(clubId);
if (Number.isNaN(normalizedClubId)) {
const error = new Error('noaccess');
error.status = 403;
throw error;
}
const membership = await UserClub.findOne({
where: {
userId: user.id,
clubId: normalizedClubId,
approved: true,
},
});
if (!membership) {
const error = new Error('noaccess');
error.status = 403;
throw error;
}
return user;
}
let mocksRegistered = false;
export function setupRouteAuthMocks() {
if (mocksRegistered) {
return;
}
vi.mock('../../middleware/authMiddleware.js', () => ({
authenticate: (req, res, next) => {
const headerToken = req.headers.authorization?.split(' ')[1] || req.headers.authcode;
if (!headerToken) {
return res.status(401).json({ error: 'Unauthenticated' });
}
try {
const user = resolveUserFromToken(headerToken);
req.user = user;
req.headers.authcode = headerToken;
} catch (error) {
return res.status(error.status || 401).json({ error: error.message || 'Unauthenticated' });
}
next();
},
}));
vi.mock('../../middleware/authorizationMiddleware.js', () => ({
authorize: () => (req, res, next) => {
if (req.headers['x-allow-authorization'] === 'false') {
return res.status(403).json({ error: 'Forbidden' });
}
next();
},
}));
vi.mock('../../utils/userUtils.js', () => ({
checkAccess: vi.fn().mockImplementation(async (token, clubId) => {
await ensureClubAccess(token, clubId);
return true;
}),
checkGlobalAccess: vi.fn().mockImplementation(async (token) => resolveUserFromToken(token ?? '')),
getUserByToken: vi.fn().mockImplementation((token) => resolveUserFromToken(token)),
hasUserClubAccess: vi.fn().mockImplementation(async (userId, clubId) => {
const normalizedClubId = Number(clubId);
if (Number.isNaN(normalizedClubId)) {
return false;
}
const membership = await UserClub.findOne({
where: {
userId,
clubId: normalizedClubId,
approved: true,
},
});
return Boolean(membership);
}),
}));
mocksRegistered = true;
}