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:
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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 });
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
@@ -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']);
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user