From 15b88f8177e423ab23ead7be17c8a472e4499f90 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Tue, 11 Nov 2025 08:35:55 +0100 Subject: [PATCH] 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. --- backend/tests/memberRoutes.test.js | 19 +++ backend/tests/memberService.test.js | 39 ++++++ .../tests/memberTransferConfigRoutes.test.js | 77 ++++++++++++ .../tests/memberTransferConfigService.test.js | 62 ++++++++++ backend/tests/memberTransferService.test.js | 115 ++++++++++++++++++ backend/tests/testApp.js | 2 + 6 files changed, 314 insertions(+) create mode 100644 backend/tests/memberTransferConfigRoutes.test.js create mode 100644 backend/tests/memberTransferConfigService.test.js create mode 100644 backend/tests/memberTransferService.test.js diff --git a/backend/tests/memberRoutes.test.js b/backend/tests/memberRoutes.test.js index ac56476..1763b81 100644 --- a/backend/tests/memberRoutes.test.js +++ b/backend/tests/memberRoutes.test.js @@ -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'); + }); }); diff --git a/backend/tests/memberService.test.js b/backend/tests/memberService.test.js index 6c1aa09..3dd17b9 100644 --- a/backend/tests/memberService.test.js +++ b/backend/tests/memberService.test.js @@ -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); + }); }); diff --git a/backend/tests/memberTransferConfigRoutes.test.js b/backend/tests/memberTransferConfigRoutes.test.js new file mode 100644 index 0000000..4520f5c --- /dev/null +++ b/backend/tests/memberTransferConfigRoutes.test.js @@ -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); + }); +}); diff --git a/backend/tests/memberTransferConfigService.test.js b/backend/tests/memberTransferConfigService.test.js new file mode 100644 index 0000000..cb1a66b --- /dev/null +++ b/backend/tests/memberTransferConfigService.test.js @@ -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); + }); +}); diff --git a/backend/tests/memberTransferService.test.js b/backend/tests/memberTransferService.test.js new file mode 100644 index 0000000..442ec43 --- /dev/null +++ b/backend/tests/memberTransferService.test.js @@ -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'); + }); +}); diff --git a/backend/tests/testApp.js b/backend/tests/testApp.js index 7fbdee2..7d27c41 100644 --- a/backend/tests/testApp.js +++ b/backend/tests/testApp.js @@ -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;