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;