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); }); });