Add member transfer validation and contact management tests

Implemented a new test for validating transfer configuration via the API in memberRoutes.test.js, ensuring proper error handling for invalid transfer requests. Additionally, enhanced memberService.test.js with tests for creating and updating members along with their contacts, including filtering inactive members. These additions improve the test coverage and reliability of member management features in the application.
This commit is contained in:
Torsten Schulz (local)
2025-11-11 08:35:55 +01:00
parent 20f204e70b
commit 15b88f8177
6 changed files with 314 additions and 0 deletions

View File

@@ -90,4 +90,23 @@ describe('Member quick action routes', () => {
await member.reload();
expect(member.active).toBe(false);
});
it('validiert Transfer-Konfiguration über die API', async () => {
const { credentials } = await registerAndActivate('membertransfer@example.com');
const token = await loginAndGetToken(credentials);
const clubResponse = await request(app)
.post('/api/clubs')
.set(authHeaders(token))
.send({ name: 'Transfer Test Club' });
const clubId = clubResponse.body.id;
const transferResponse = await request(app)
.post(`/api/members/transfer/${clubId}`)
.set(authHeaders(token))
.send({ transferTemplate: '{}' });
expect(transferResponse.status).toBe(400);
expect(transferResponse.body.error).toContain('Übertragungs-Endpoint');
});
});

View File

@@ -14,6 +14,8 @@ 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';
describe('memberService quick updates', () => {
beforeEach(async () => {
@@ -72,4 +74,41 @@ describe('memberService quick updates', () => {
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: { email: 'lena@example.com' }, include: { model: MemberContact, as: 'contacts' } });
expect(created).toBeTruthy();
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({ clubId: club.id, active: true, email: 'active@example.com' });
await createMember({ clubId: club.id, active: false, email: 'inactive@example.com' });
const onlyActive = await memberService.getClubMembers('token', club.id, 'false');
expect(onlyActive.some((m) => m.email === 'active@example.com')).toBe(true);
expect(onlyActive.some((m) => m.email === 'inactive@example.com')).toBe(false);
const allMembers = await memberService.getClubMembers('token', club.id, 'true');
expect(allMembers.some((m) => m.email === 'inactive@example.com')).toBe(true);
});
});

View File

@@ -0,0 +1,77 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import request from 'supertest';
vi.mock('../services/emailService.js', () => ({
sendActivationEmail: vi.fn().mockResolvedValue(),
}));
import app from './testApp.js';
import sequelize from '../database.js';
import '../models/index.js';
import User from '../models/User.js';
import MemberTransferConfig from '../models/MemberTransferConfig.js';
const registerAndActivate = async (email) => {
const password = 'Test123!';
await request(app).post('/api/auth/register').send({ email, password });
const user = await User.findOne({ where: { email } });
await user.update({ isActive: true });
return { user, credentials: { email, password } };
};
const loginAndGetToken = async (credentials) => {
const response = await request(app).post('/api/auth/login').send(credentials);
return response.body.token;
};
const authHeaders = (token) => ({
Authorization: `Bearer ${token}`,
authcode: token,
});
describe('MemberTransferConfig Routes', () => {
beforeEach(async () => {
await sequelize.sync({ force: true });
});
it('verwaltet die Transfer-Konfiguration eines Vereins', async () => {
const { credentials } = await registerAndActivate('configroutes@example.com');
const token = await loginAndGetToken(credentials);
const clubResponse = await request(app)
.post('/api/clubs')
.set(authHeaders(token))
.send({ name: 'Config Route Club' });
const clubId = clubResponse.body.id;
const saveResponse = await request(app)
.post(`/api/member-transfer-config/${clubId}`)
.set(authHeaders(token))
.send({
server: 'https://api.example.com',
loginEndpoint: '/login',
loginFormat: 'json',
loginCredentials: { username: 'user', password: 'secret' },
transferEndpoint: '/transfer',
transferTemplate: '{"name":"{{fullName}}"}'
});
expect(saveResponse.status).toBe(200);
const stored = await MemberTransferConfig.findOne({ where: { clubId } });
expect(stored).toBeTruthy();
const getResponse = await request(app)
.get(`/api/member-transfer-config/${clubId}`)
.set(authHeaders(token));
expect(getResponse.status).toBe(200);
expect(getResponse.body.config.loginCredentials.username).toBe('user');
const deleteResponse = await request(app)
.delete(`/api/member-transfer-config/${clubId}`)
.set(authHeaders(token));
expect(deleteResponse.status).toBe(200);
});
});

View File

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

View File

@@ -0,0 +1,115 @@
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 Member from '../models/Member.js';
import Club from '../models/Club.js';
const axiosMock = axios as unknown as vi.Mock;
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([
{
firstName: 'Anna',
lastName: 'Aktiv',
birthDate: '2001-02-03',
clubId: club.id,
email: 'anna@example.com',
street: 'Straße 1',
city: 'Stadt',
testMembership: false,
active: true,
},
{
firstName: '',
lastName: 'OhneVorname',
birthDate: null,
clubId: club.id,
email: 'invalid@example.com',
street: 'Straße 2',
city: 'Stadt',
testMembership: false,
active: true,
},
]);
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 Member.create({
firstName: 'Ben',
lastName: 'Bulk',
birthDate: '1999-05-06',
clubId: club.id,
email: 'ben@example.com',
street: 'Straße 3',
city: 'Stadt',
testMembership: false,
active: true,
});
axiosMock.mockResolvedValue({ status: 200, data: { success: true, summary: { imported: 1 } } });
const result = await memberTransferService.transferMembers('token', club.id, {
transferEndpoint: 'http://example.com/bulk',
transferTemplate: '{"name":"{{fullName}}"}',
transferMethod: 'POST',
transferFormat: 'json',
useBulkMode: true,
bulkWrapperTemplate: '{"payload":{"members":{{members}}}}',
});
expect(result.status).toBe(200);
const callConfig = axiosMock.mock.calls[0][0];
expect(callConfig.data).toEqual({ payload: { members: [{ name: 'Ben Bulk' }] } });
});
it('formatiert Geburtsdaten korrekt', () => {
expect(memberTransferService.formatBirthDate('01.02.2000')).toBe('2000-02-01');
expect(memberTransferService.formatBirthDate('2000-02-01')).toBe('2000-02-01');
});
});

View File

@@ -16,6 +16,7 @@ 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';
const app = express();
@@ -37,6 +38,7 @@ 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((err, req, res, next) => {
const status = err?.status || err?.statusCode || 500;