Files
petitspas/backend/src/modules/cards/cards.service.spec.ts
T

377 lines
12 KiB
TypeScript

import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { ForbiddenException } from '@nestjs/common';
import { CardsService } from './cards.service';
import { CardsRealtimeService } from './cards-realtime.service';
import { EvenementsAgendaService } from '../evenements-agenda/evenements-agenda.service';
import { CardType, CardResponseModeType } from 'src/entities/card_types.entity';
import {
CardInstance,
CardInstanceStatutType,
CardOperationType,
} from 'src/entities/card_instances.entity';
import { CardAudienceMember } from 'src/entities/card_audience_members.entity';
import { CardResponse } from 'src/entities/card_responses.entity';
import { AmChildren } from 'src/entities/am_children.entity';
import { ParentsChildren } from 'src/entities/parents_children.entity';
import { RoleType } from 'src/entities/users.entity';
import { StatutEvenementAgendaType, TypeEvenementAgendaType } from 'src/entities/evenements_agenda.entity';
describe('CardsService (#194)', () => {
let service: CardsService;
const typesRepo = { find: jest.fn(), findOne: jest.fn() };
const cardsRepo = {
create: jest.fn((x) => x),
save: jest.fn(async (x) => ({ ...x, id: x.id ?? 'card-1', cree_le: new Date(), modifie_le: new Date() })),
update: jest.fn(async () => ({ affected: 1 })),
findOne: jest.fn(),
delete: jest.fn(),
createQueryBuilder: jest.fn(),
manager: { query: jest.fn() },
};
const audienceRepo = {
create: jest.fn((x) => x),
save: jest.fn(),
findOne: jest.fn(),
find: jest.fn(),
};
const responsesRepo = {
create: jest.fn((x) => x),
save: jest.fn(async (x) => x),
insert: jest.fn(),
};
const amChildrenRepo = { findOne: jest.fn() };
const parentsChildrenRepo = { find: jest.fn(), findOne: jest.fn() };
const evenementsService = {
creer: jest.fn(),
maj: jest.fn(),
supprimer: jest.fn(),
};
const realtime = { emitToUsers: jest.fn() };
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
CardsService,
{ provide: getRepositoryToken(CardType), useValue: typesRepo },
{ provide: getRepositoryToken(CardInstance), useValue: cardsRepo },
{ provide: getRepositoryToken(CardAudienceMember), useValue: audienceRepo },
{ provide: getRepositoryToken(CardResponse), useValue: responsesRepo },
{ provide: getRepositoryToken(AmChildren), useValue: amChildrenRepo },
{ provide: getRepositoryToken(ParentsChildren), useValue: parentsChildrenRepo },
{ provide: EvenementsAgendaService, useValue: evenementsService },
{ provide: CardsRealtimeService, useValue: realtime },
],
}).compile();
service = module.get(CardsService);
jest.clearAllMocks();
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('AM crée congé → absence + carte ouverte + audience parents', async () => {
typesRepo.findOne.mockResolvedValue({
code: 'conge_am',
system: true,
titre: 'Congé AM',
emitter_roles: [RoleType.ASSISTANTE_MATERNELLE],
recipient_roles: [RoleType.PARENT],
audience_resolver: 'couple_parents',
response_mode: CardResponseModeType.ACCEPT_REFUSE,
retention_days: 14,
});
amChildrenRepo.findOne.mockResolvedValue({
id: 'pl-1',
amId: 'am-1',
enfantId: 'e-1',
date_fin: null,
});
evenementsService.creer.mockResolvedValue({
id: 'abs-1',
type: TypeEvenementAgendaType.CONGE_AM,
statut: StatutEvenementAgendaType.EN_ATTENTE,
});
parentsChildrenRepo.find.mockResolvedValue([
{ parentId: 'p-1', enfantId: 'e-1' },
{ parentId: 'p-2', enfantId: 'e-1' },
]);
cardsRepo.findOne.mockResolvedValue({
id: 'card-1',
type_code: 'conge_am',
id_placement: 'pl-1',
id_evenement: 'abs-1',
cree_par: 'am-1',
operation: CardOperationType.CREATE,
statut: CardInstanceStatutType.OUVERTE,
payload: { date_debut: '2026-11-01', date_fin: '2026-11-07' },
purge_at: new Date(),
cree_le: new Date(),
modifie_le: new Date(),
type: {
titre: 'Congé AM',
response_mode: CardResponseModeType.ACCEPT_REFUSE,
retention_days: 14,
},
responses: [],
audience: [
{ id_utilisateur: 'am-1' },
{ id_utilisateur: 'p-1' },
{ id_utilisateur: 'p-2' },
],
});
const res = await service.creer('am-1', RoleType.ASSISTANTE_MATERNELLE, {
type_code: 'conge_am',
id_placement: 'pl-1',
date_debut: '2026-11-01',
date_fin: '2026-11-07',
});
expect(evenementsService.creer).toHaveBeenCalled();
expect(audienceRepo.save).toHaveBeenCalled();
expect(realtime.emitToUsers).toHaveBeenCalledWith(
['am-1', 'p-1', 'p-2'],
'card.created',
'card-1',
expect.any(Object),
);
expect(res.type_code).toBe('conge_am');
expect(res.statut).toBe(CardInstanceStatutType.OUVERTE);
});
it('parent ne peut pas émettre conge_am', async () => {
typesRepo.findOne.mockResolvedValue({
code: 'conge_am',
system: true,
emitter_roles: [RoleType.ASSISTANTE_MATERNELLE],
});
await expect(
service.creer('p-1', RoleType.PARENT, {
type_code: 'conge_am',
id_placement: 'pl-1',
date_debut: '2026-11-01',
date_fin: '2026-11-07',
}),
).rejects.toBeInstanceOf(ForbiddenException);
});
it('parent crée absence_enfant → audience AM + co-parent', async () => {
typesRepo.findOne.mockResolvedValue({
code: 'absence_enfant',
system: true,
titre: 'Absence enfant',
emitter_roles: [RoleType.PARENT],
recipient_roles: [RoleType.ASSISTANTE_MATERNELLE],
audience_resolver: 'couple_am',
response_mode: CardResponseModeType.NONE,
retention_days: 7,
});
amChildrenRepo.findOne.mockResolvedValue({
id: 'pl-1',
amId: 'am-1',
enfantId: 'e-1',
date_fin: null,
});
parentsChildrenRepo.findOne.mockResolvedValue({
parentId: 'p-1',
enfantId: 'e-1',
});
parentsChildrenRepo.find.mockResolvedValue([
{ parentId: 'p-1', enfantId: 'e-1' },
{ parentId: 'p-2', enfantId: 'e-1' },
]);
evenementsService.creer.mockResolvedValue({
id: 'abs-1',
type: TypeEvenementAgendaType.ABSENCE_ENFANT,
statut: StatutEvenementAgendaType.ACCEPTE,
});
cardsRepo.findOne.mockResolvedValue({
id: 'card-abs',
type_code: 'absence_enfant',
id_placement: 'pl-1',
id_evenement: 'abs-1',
cree_par: 'p-1',
operation: CardOperationType.CREATE,
statut: CardInstanceStatutType.OUVERTE,
payload: { date_debut: '2026-10-28', date_fin: '2026-10-29' },
purge_at: new Date(),
cree_le: new Date(),
modifie_le: new Date(),
type: {
titre: 'Absence enfant',
response_mode: CardResponseModeType.NONE,
retention_days: 7,
},
responses: [],
audience: [
{ id_utilisateur: 'p-1' },
{ id_utilisateur: 'am-1' },
{ id_utilisateur: 'p-2' },
],
});
await service.creer('p-1', RoleType.PARENT, {
type_code: 'absence_enfant',
id_placement: 'pl-1',
date_debut: '2026-10-28',
date_fin: '2026-10-29',
});
const saved = audienceRepo.save.mock.calls[0][0] as Array<{
id_utilisateur: string;
is_creator: boolean;
}>;
const ids = saved.map((m) => m.id_utilisateur).sort();
expect(ids).toEqual(['am-1', 'p-1', 'p-2']);
expect(saved.find((m) => m.id_utilisateur === 'p-2')?.is_creator).toBe(false);
});
it('parent accepte conge_am → response via relation + carte traitee', async () => {
cardsRepo.findOne.mockResolvedValue({
id: 'card-1',
type_code: 'conge_am',
id_placement: 'pl-1',
id_evenement: 'ev-1',
cree_par: 'am-1',
operation: CardOperationType.CREATE,
statut: CardInstanceStatutType.OUVERTE,
payload: { date_debut: '2026-10-27', date_fin: '2026-10-31' },
purge_at: new Date(),
cree_le: new Date(),
modifie_le: new Date(),
type: {
titre: 'Congé AM',
response_mode: CardResponseModeType.ACCEPT_REFUSE,
retention_days: 14,
},
responses: [],
audience: [
{ id_utilisateur: 'am-1' },
{ id_utilisateur: 'p-1' },
{ id_utilisateur: 'p-2' },
],
});
audienceRepo.findOne.mockResolvedValue({ id_card: 'card-1', id_utilisateur: 'p-1' });
evenementsService.maj.mockResolvedValue({});
const res = await service.repondre('p-1', RoleType.PARENT, 'card-1', {
action: 'accept' as never,
});
expect(responsesRepo.create).toHaveBeenCalledWith(
expect.objectContaining({
card: { id: 'card-1' },
user: { id: 'p-1' },
action: 'accept',
}),
);
expect(responsesRepo.save).toHaveBeenCalled();
expect(evenementsService.maj).toHaveBeenCalledWith(
'p-1',
RoleType.PARENT,
'ev-1',
expect.objectContaining({ statut: StatutEvenementAgendaType.ACCEPTE }),
);
expect(cardsRepo.update).toHaveBeenCalledWith(
{ id: 'card-1' },
expect.objectContaining({ statut: CardInstanceStatutType.TRAITEE }),
);
expect(res.statut).toBe(CardInstanceStatutType.TRAITEE);
});
it('AM crée soin_enfant (fievre) → motif dérivé + payload structuré', async () => {
typesRepo.findOne.mockResolvedValue({
code: 'soin_enfant',
system: true,
emitter_roles: [RoleType.ASSISTANTE_MATERNELLE],
response_mode: CardResponseModeType.ACK,
retention_days: 7,
audience_resolver: 'couple_parents',
});
evenementsService.creer.mockResolvedValue({ id: 'ev-soin' });
cardsRepo.create.mockImplementation((c) => ({ ...c, id: 'card-soin' }));
cardsRepo.save.mockResolvedValue({ id: 'card-soin' });
cardsRepo.findOne.mockResolvedValue({
id: 'card-soin',
type_code: 'soin_enfant',
statut: CardInstanceStatutType.OUVERTE,
payload: {
date_debut: '2026-10-28',
date_fin: '2026-10-28',
motif: '38,5 °C à 14:30 (Médicament : Doliprane)',
sous_type: 'fievre',
heure_releve: '14:30',
temperature_c: 38.5,
medicament_donne: 'Doliprane',
},
type: { response_mode: CardResponseModeType.ACK },
});
const res = await service.creer('am-1', RoleType.ASSISTANTE_MATERNELLE, {
type_code: 'soin_enfant',
id_placement: 'pl-1',
date_debut: '2026-10-28',
date_fin: '2026-10-28',
payload: {
sous_type: 'fievre',
heure_releve: '14:30',
temperature_c: 38.5,
medicament_donne: 'Doliprane',
},
});
expect(evenementsService.creer).toHaveBeenCalledWith(
'am-1',
RoleType.ASSISTANTE_MATERNELLE,
expect.objectContaining({
type: TypeEvenementAgendaType.SOIN_ENFANT,
motif: '38,5 °C à 14:30 (Médicament : Doliprane)',
}),
);
expect(res.payload.motif).toBe('38,5 °C à 14:30 (Médicament : Doliprane)');
});
it('AM corrige soin_enfant dans les 24h → historique + re-ack', async () => {
const creeLe = new Date();
cardsRepo.findOne.mockResolvedValue({
id: 'card-soin',
type_code: 'soin_enfant',
id_placement: 'pl-1',
id_evenement: 'ev-soin',
cree_par: 'am-1',
cree_le: creeLe,
statut: CardInstanceStatutType.TRAITEE, // déjà acquitté
payload: {
date_debut: '2026-10-28',
date_fin: '2026-10-28',
sous_type: 'fievre',
heure_releve: '14:30',
temperature_c: 38.5,
},
type: { retention_days: 7 },
});
evenementsService.maj.mockResolvedValue({});
await service.majEnAttente('am-1', RoleType.ASSISTANTE_MATERNELLE, 'card-soin', {
date_debut: '2026-10-28',
date_fin: '2026-10-28',
payload: {
sous_type: 'fievre',
heure_releve: '14:30',
temperature_c: 37.5, // correction
},
});
expect(cardsRepo.manager.query).toHaveBeenCalledWith(
expect.stringContaining('UPDATE card_instances'),
expect.arrayContaining([
CardInstanceStatutType.OUVERTE, // repasse ouverte
expect.stringContaining('37,5'), // nouveau payload stringifié
]),
);
});
});