Double mapping @Column/@JoinColumn sur id_card ; persist response via card/user relations. Garde update() sur l’instance (évite save parent qui nullifiait les FK enfants). Co-authored-by: Cursor <cursoragent@cursor.com>
285 lines
9.3 KiB
TypeScript
285 lines
9.3 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);
|
|
});
|
|
});
|