- API PATCH …/fiche, POST/DELETE …/enfants/:enfantId, GET avec amChildren - Table enfants_assistantes_maternelles (option D, 1 garde active/enfant) - Statuts enfant: garde/sans_garde remplacent actif (inscription → sans_garde) - BDD.sql canonique réécrit; migration pour BDD existantes - Seed test: hash bcrypt corrigé pour mot de passe « password » Co-authored-by: Cursor <cursoragent@cursor.com>
223 lines
8.3 KiB
TypeScript
223 lines
8.3 KiB
TypeScript
import { NotFoundException } from '@nestjs/common';
|
|
import { Test, TestingModule } from '@nestjs/testing';
|
|
import { JwtService } from '@nestjs/jwt';
|
|
import { getRepositoryToken } from '@nestjs/typeorm';
|
|
import { ConfigService } from '@nestjs/config';
|
|
import { AuthService } from './auth.service';
|
|
import { UserService } from '../user/user.service';
|
|
import { ParentsService } from '../parents/parents.service';
|
|
import { DossiersService } from '../dossiers/dossiers.service';
|
|
import { AppConfigService } from 'src/modules/config/config.service';
|
|
import { MailService } from 'src/modules/mail/mail.service';
|
|
import { NumeroDossierService } from 'src/modules/numero-dossier/numero-dossier.service';
|
|
import { Parents } from 'src/entities/parents.entity';
|
|
import { Users, RoleType, StatutUtilisateurType } from 'src/entities/users.entity';
|
|
import { Children } from 'src/entities/children.entity';
|
|
import { AssistanteMaternelle } from 'src/entities/assistantes_maternelles.entity';
|
|
|
|
describe('AuthService (#118 create-password API)', () => {
|
|
let service: AuthService;
|
|
const usersRepoMock = {
|
|
findOne: jest.fn(),
|
|
createQueryBuilder: jest.fn(),
|
|
update: jest.fn(),
|
|
manager: { transaction: jest.fn() },
|
|
};
|
|
|
|
const userServiceMock = {
|
|
findByTokenReprise: jest.fn(),
|
|
};
|
|
|
|
const parentsServiceMock = {
|
|
getDossierFamilleByNumero: jest.fn(),
|
|
getFamilyUserIds: jest.fn(),
|
|
};
|
|
|
|
const dossiersServiceMock = {
|
|
getDossierByNumero: jest.fn(),
|
|
};
|
|
|
|
const mailServiceMock = {
|
|
sendPasswordResetEmail: jest.fn().mockResolvedValue(undefined),
|
|
};
|
|
|
|
const appConfigMock = {
|
|
get: jest.fn().mockReturnValue(7),
|
|
};
|
|
|
|
beforeEach(async () => {
|
|
jest.clearAllMocks();
|
|
const qbChain: any = {};
|
|
qbChain.where = jest.fn().mockReturnValue(qbChain);
|
|
qbChain.select = jest.fn().mockReturnValue(qbChain);
|
|
qbChain.getOne = jest.fn().mockResolvedValue(null);
|
|
qbChain.update = jest.fn().mockReturnValue(qbChain);
|
|
qbChain.set = jest.fn().mockReturnValue(qbChain);
|
|
qbChain.andWhere = jest.fn().mockReturnValue(qbChain);
|
|
qbChain.execute = jest.fn().mockResolvedValue({ affected: 0 });
|
|
usersRepoMock.createQueryBuilder.mockReturnValue(qbChain);
|
|
usersRepoMock.update.mockResolvedValue({ affected: 1, raw: [] });
|
|
|
|
const module: TestingModule = await Test.createTestingModule({
|
|
providers: [
|
|
AuthService,
|
|
{ provide: UserService, useValue: userServiceMock },
|
|
{ provide: ParentsService, useValue: parentsServiceMock },
|
|
{ provide: DossiersService, useValue: dossiersServiceMock },
|
|
{ provide: JwtService, useValue: {} },
|
|
{ provide: ConfigService, useValue: { get: jest.fn() } },
|
|
{ provide: AppConfigService, useValue: appConfigMock },
|
|
{ provide: MailService, useValue: mailServiceMock },
|
|
{ provide: NumeroDossierService, useValue: {} },
|
|
{ provide: getRepositoryToken(Parents), useValue: {} },
|
|
{ provide: getRepositoryToken(Users), useValue: usersRepoMock },
|
|
{ provide: getRepositoryToken(Children), useValue: {} },
|
|
{ provide: getRepositoryToken(AssistanteMaternelle), useValue: {} },
|
|
],
|
|
}).compile();
|
|
|
|
service = module.get<AuthService>(AuthService);
|
|
});
|
|
|
|
it('should be defined', () => {
|
|
expect(service).toBeDefined();
|
|
});
|
|
|
|
it('returns 404-like error when verify token is missing', async () => {
|
|
await expect(service.verifyCreatePasswordToken('')).rejects.toThrow(NotFoundException);
|
|
});
|
|
|
|
it('returns valid=true when token exists and is not expired', async () => {
|
|
usersRepoMock.findOne.mockResolvedValue({
|
|
id: 'u1',
|
|
password: null,
|
|
token_creation_mdp_expire_le: new Date(Date.now() + 60_000),
|
|
});
|
|
|
|
await expect(service.verifyCreatePasswordToken('tok-123')).resolves.toEqual({
|
|
valid: true,
|
|
message: 'Token valide',
|
|
});
|
|
});
|
|
|
|
describe('#127 forgot / reset password', () => {
|
|
it('requestPasswordReset returns generic message when email unknown', async () => {
|
|
const r = await service.requestPasswordReset('unknown@test.fr');
|
|
expect(r.message).toContain('Si cette adresse');
|
|
expect(mailServiceMock.sendPasswordResetEmail).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('requestPasswordReset sends mail when user has password', async () => {
|
|
usersRepoMock.createQueryBuilder.mockReturnValueOnce({
|
|
where: jest.fn().mockReturnThis(),
|
|
select: jest.fn().mockReturnThis(),
|
|
getOne: jest.fn().mockResolvedValue({
|
|
id: 'u1',
|
|
email: 'a@test.fr',
|
|
prenom: 'A',
|
|
nom: 'B',
|
|
password: 'hashed',
|
|
}),
|
|
});
|
|
const r = await service.requestPasswordReset('a@test.fr');
|
|
expect(r.message).toContain('Si cette adresse');
|
|
expect(usersRepoMock.update).toHaveBeenCalled();
|
|
expect(mailServiceMock.sendPasswordResetEmail).toHaveBeenCalledWith(
|
|
expect.objectContaining({ email: 'a@test.fr', token: expect.any(String) }),
|
|
);
|
|
});
|
|
|
|
it('resetPasswordWithResetToken throws when token unknown', async () => {
|
|
usersRepoMock.findOne.mockResolvedValue(null);
|
|
await expect(service.resetPasswordWithResetToken('bad', 'Password1')).rejects.toThrow(
|
|
NotFoundException,
|
|
);
|
|
});
|
|
|
|
it('resetPasswordWithResetToken succeeds when update affects row', async () => {
|
|
usersRepoMock.findOne.mockResolvedValue({
|
|
id: 'u1',
|
|
password: 'oldhash',
|
|
password_reset_expires: new Date(Date.now() + 60_000),
|
|
});
|
|
const exec = jest.fn().mockResolvedValue({ affected: 1 });
|
|
const chain: any = {};
|
|
chain.update = jest.fn().mockReturnValue(chain);
|
|
chain.set = jest.fn().mockReturnValue(chain);
|
|
chain.where = jest.fn().mockReturnValue(chain);
|
|
chain.andWhere = jest.fn().mockReturnValue(chain);
|
|
chain.execute = exec;
|
|
usersRepoMock.createQueryBuilder.mockReturnValueOnce(chain);
|
|
|
|
const r = await service.resetPasswordWithResetToken('good-token', 'Password1');
|
|
expect(r.message.toLowerCase()).toContain('mis à jour');
|
|
expect(exec).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('Reprise après refus (#112)', () => {
|
|
const token = '11111111-1111-1111-1111-111111111111';
|
|
|
|
it('getRepriseDossier throws when token invalid', async () => {
|
|
userServiceMock.findByTokenReprise.mockResolvedValue(null);
|
|
await expect(service.getRepriseDossier(token)).rejects.toThrow(NotFoundException);
|
|
});
|
|
|
|
it('getRepriseDossier returns famille complète pour parent', async () => {
|
|
userServiceMock.findByTokenReprise.mockResolvedValue({
|
|
id: 'p1',
|
|
email: 'claire@test.fr',
|
|
prenom: 'Claire',
|
|
nom: 'MARTIN',
|
|
role: RoleType.PARENT,
|
|
numero_dossier: '2026-000021',
|
|
statut: StatutUtilisateurType.REFUSE,
|
|
});
|
|
parentsServiceMock.getDossierFamilleByNumero.mockResolvedValue({
|
|
numero_dossier: '2026-000021',
|
|
parents: [{ user_id: 'p1', email: 'claire@test.fr', statut: StatutUtilisateurType.REFUSE }],
|
|
enfants: [{ id: 'e1', first_name: 'Emma', status: 'sans_garde' }],
|
|
texte_motivation: 'Motivation test',
|
|
});
|
|
|
|
const result = await service.getRepriseDossier(token);
|
|
expect(result.parents).toHaveLength(1);
|
|
expect(result.enfants).toHaveLength(1);
|
|
expect(result.texte_motivation).toBe('Motivation test');
|
|
expect(result.numero_dossier).toBe('2026-000021');
|
|
});
|
|
|
|
it('getRepriseDossier returns fiche AM complète', async () => {
|
|
userServiceMock.findByTokenReprise.mockResolvedValue({
|
|
id: 'am1',
|
|
email: 'am@test.fr',
|
|
role: RoleType.ASSISTANTE_MATERNELLE,
|
|
numero_dossier: '2026-000003',
|
|
});
|
|
dossiersServiceMock.getDossierByNumero.mockResolvedValue({
|
|
type: 'am',
|
|
dossier: {
|
|
numero_dossier: '2026-000003',
|
|
user: {
|
|
id: 'am1',
|
|
email: 'am@test.fr',
|
|
prenom: 'Marie',
|
|
nom: 'DUPONT',
|
|
statut: StatutUtilisateurType.REFUSE,
|
|
},
|
|
numero_agrement: 'AGR-1',
|
|
nir: '123456789012345',
|
|
biographie: 'Bio',
|
|
nb_max_enfants: 4,
|
|
place_disponible: 2,
|
|
},
|
|
});
|
|
|
|
const result = await service.getRepriseDossier(token);
|
|
expect(result.numero_agrement).toBe('AGR-1');
|
|
expect(result.biographie).toBe('Bio');
|
|
expect(result.nb_max_enfants).toBe(4);
|
|
});
|
|
});
|
|
});
|