Add permission management tests for role and custom permissions updates
Implemented new tests in permissionRoutes.test.js to validate role changes and custom permissions for users within clubs. Added checks for successful updates and error handling for invalid club IDs. This enhancement improves test coverage for permission management features in the application.
This commit is contained in:
35
backend/tests/myTischtennisFetchLogService.test.js
Normal file
35
backend/tests/myTischtennisFetchLogService.test.js
Normal file
@@ -0,0 +1,35 @@
|
||||
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';
|
||||
|
||||
describe('myTischtennisFetchLogService', () => {
|
||||
beforeEach(async () => {
|
||||
await sequelize.sync({ force: true });
|
||||
});
|
||||
|
||||
it('loggt Abrufe und liefert die letzten Einträge', async () => {
|
||||
await myTischtennisFetchLogService.logFetch(1, 'ratings', true, 'OK', { recordsProcessed: 5 });
|
||||
await myTischtennisFetchLogService.logFetch(1, 'match_results', false, 'Error', { errorDetails: 'Timeout' });
|
||||
|
||||
const logs = await myTischtennisFetchLogService.getFetchLogs(1, { limit: 10 });
|
||||
expect(logs).toHaveLength(2);
|
||||
expect(logs[0].fetchType).toBe('match_results');
|
||||
|
||||
const latest = await myTischtennisFetchLogService.getLatestSuccessfulFetches(1);
|
||||
expect(latest.ratings).toBeTruthy();
|
||||
expect(latest.match_results).toBeNull();
|
||||
});
|
||||
|
||||
it('aggregiert Statistiken nach Typ', async () => {
|
||||
await myTischtennisFetchLogService.logFetch(2, 'ratings', true, 'OK', { recordsProcessed: 3, executionTime: 100 });
|
||||
await myTischtennisFetchLogService.logFetch(2, 'ratings', false, 'Fail', { recordsProcessed: 0, executionTime: 200 });
|
||||
|
||||
const stats = await myTischtennisFetchLogService.getFetchStatistics(2, 7);
|
||||
expect(stats).toHaveLength(1);
|
||||
expect(stats[0].fetchType).toBe('ratings');
|
||||
});
|
||||
});
|
||||
92
backend/tests/myTischtennisRoutes.test.js
Normal file
92
backend/tests/myTischtennisRoutes.test.js
Normal file
@@ -0,0 +1,92 @@
|
||||
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 { 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: upsertResponse.body.account.id ? upsertResponse.body.account.userId : 1 } });
|
||||
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');
|
||||
});
|
||||
});
|
||||
96
backend/tests/myTischtennisService.test.js
Normal file
96
backend/tests/myTischtennisService.test.js
Normal file
@@ -0,0 +1,96 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
|
||||
vi.mock('../clients/myTischtennisClient.js', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
login: vi.fn(),
|
||||
getUserProfile: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../models/User.js', async () => {
|
||||
const actual = await vi.importActual('../models/User.js');
|
||||
return {
|
||||
__esModule: true,
|
||||
default: class MockUser extends actual.default {
|
||||
static #instances = [];
|
||||
static async findByPk(id) {
|
||||
return MockUser.#instances.find((user) => user.id === id) || null;
|
||||
}
|
||||
static async create(values) {
|
||||
const instance = new MockUser(values);
|
||||
MockUser.#instances.push(instance);
|
||||
return instance;
|
||||
}
|
||||
constructor(values) {
|
||||
super();
|
||||
Object.assign(this, values);
|
||||
this.validatePassword = vi.fn().mockResolvedValue(true);
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
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';
|
||||
|
||||
const clientMock = myTischtennisClient as unknown as {
|
||||
login: vi.Mock;
|
||||
getUserProfile: vi.Mock;
|
||||
};
|
||||
|
||||
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 userId = 1;
|
||||
const userModel = (await import('../models/User.js')).default;
|
||||
await userModel.create({ id: userId, email: 'user@example.com', password: 'hashed' });
|
||||
|
||||
const account = await myTischtennisService.upsertAccount(userId, 'user@example.com', 'pass', true, true, 'appPass');
|
||||
expect(account.email).toBe('user@example.com');
|
||||
|
||||
const stored = await MyTischtennis.findOne({ where: { userId } });
|
||||
expect(stored.savePassword).toBe(true);
|
||||
expect(stored.clubId).toBe(123);
|
||||
expect(clientMock.login).toHaveBeenCalledWith('user@example.com', 'pass');
|
||||
});
|
||||
|
||||
it('verifiziert Logins mit gespeicherter Session', async () => {
|
||||
const userId = 2;
|
||||
const userModel = (await import('../models/User.js')).default;
|
||||
await userModel.create({ id: userId, email: 'session@example.com', password: 'hash' });
|
||||
|
||||
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, 'appPass');
|
||||
|
||||
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 userId = 3;
|
||||
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();
|
||||
});
|
||||
});
|
||||
90
backend/tests/myTischtennisUrlRoutes.test.js
Normal file
90
backend/tests/myTischtennisUrlRoutes.test.js
Normal file
@@ -0,0 +1,90 @@
|
||||
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: '2025/2026' });
|
||||
const league = await League.create({ name: 'URL Liga', association: 'TT', groupname: 'Gruppe A', 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/tt/24-25/ligen/gruppe/team?id=12345&teamid=67890',
|
||||
clubTeamId: team.id,
|
||||
createLeague: false,
|
||||
createSeason: false,
|
||||
});
|
||||
|
||||
expect(configureResponse.status).toBe(200);
|
||||
await team.reload();
|
||||
expect(team.myTischtennisTeamId).toBeTruthy();
|
||||
});
|
||||
|
||||
it('gibt Team-URLs zurück', async () => {
|
||||
const { 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: '2025/2026' });
|
||||
const league = await League.create({ name: 'URL Liga', association: 'TT', groupname: 'Gruppe A', 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, 1));
|
||||
|
||||
expect(urlResponse.status).toBe(200);
|
||||
expect(urlResponse.body.url).toContain('98765');
|
||||
});
|
||||
});
|
||||
124
backend/tests/pdfParserService.test.js
Normal file
124
backend/tests/pdfParserService.test.js
Normal file
@@ -0,0 +1,124 @@
|
||||
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(match.homePin).toBe('1234');
|
||||
expect(match.guestPin).toBe('5678');
|
||||
});
|
||||
|
||||
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).toBe('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');
|
||||
});
|
||||
});
|
||||
@@ -106,4 +106,54 @@ describe('Permission Routes', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
95
backend/tests/predefinedActivityRoutes.test.js
Normal file
95
backend/tests/predefinedActivityRoutes.test.js
Normal file
@@ -0,0 +1,95 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
89
backend/tests/predefinedActivityService.test.js
Normal file
89
backend/tests/predefinedActivityService.test.js
Normal file
@@ -0,0 +1,89 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
120
backend/tests/schedulerService.test.js
Normal file
120
backend/tests/schedulerService.test.js
Normal file
@@ -0,0 +1,120 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
|
||||
const 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';
|
||||
|
||||
describe('schedulerService', () => {
|
||||
beforeEach(() => {
|
||||
schedulerService.stop();
|
||||
mockJobs.length = 0;
|
||||
(cron.schedule as unknown as vi.Mock).mockClear();
|
||||
(autoUpdateRatingsService.executeAutomaticUpdates as vi.Mock).mockClear();
|
||||
(autoFetchMatchResultsService.executeAutomaticFetch as vi.Mock).mockClear();
|
||||
(apiLogService.logSchedulerExecution as vi.Mock).mockClear();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
schedulerService.stop();
|
||||
});
|
||||
|
||||
it('startet Scheduler und registriert Cron-Jobs genau einmal', () => {
|
||||
schedulerService.start();
|
||||
expect((cron.schedule as unknown as vi.Mock).mock.calls).toHaveLength(2);
|
||||
expect(schedulerService.getStatus()).toMatchObject({ isRunning: true, jobs: ['ratingUpdates', 'matchResults'] });
|
||||
|
||||
schedulerService.start();
|
||||
expect((cron.schedule as unknown as vi.Mock).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(autoUpdateRatingsService.executeAutomaticUpdates).toHaveBeenCalled();
|
||||
|
||||
const matches = await schedulerService.triggerMatchResultsFetch();
|
||||
expect(matches.success).toBe(true);
|
||||
expect(autoFetchMatchResultsService.executeAutomaticFetch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('führt geplante Jobs aus und protokolliert Ergebnisse', async () => {
|
||||
schedulerService.start();
|
||||
|
||||
const [ratingJob, matchJob] = mockJobs;
|
||||
|
||||
await ratingJob.handler();
|
||||
expect(apiLogService.logSchedulerExecution).toHaveBeenCalledWith(
|
||||
'rating_updates',
|
||||
true,
|
||||
expect.any(Object),
|
||||
expect.any(Number),
|
||||
null
|
||||
);
|
||||
|
||||
await matchJob.handler();
|
||||
expect(apiLogService.logSchedulerExecution).toHaveBeenCalledWith(
|
||||
'match_results',
|
||||
true,
|
||||
expect.any(Object),
|
||||
expect.any(Number),
|
||||
null
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -17,6 +17,7 @@ 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';
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -39,6 +40,7 @@ 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((err, req, res, next) => {
|
||||
const status = err?.status || err?.statusCode || 500;
|
||||
|
||||
Reference in New Issue
Block a user