petitspas/backend/src/routes/auth/auth.service.spec.ts
Julien Martin 18f3a7f276 feat(#127): mot de passe oublié — flux complet (API + web + e-mail)
- API publique POST /auth/forgot-password et /auth/reset-password (jeton
  password_reset_*, anti-énumération, e-mail P'titsPas)
- BDD: colonnes password_reset_token / password_reset_expires + index
- Front: écrans /forgot-password, /reset-password, lien login, mutualisation
  formulaires par token
- Template e-mail + alignement couleur; tests unitaires (auth, mail)

Merge depuis develop: branche feature/127.

Made-with: Cursor
2026-04-24 12:53:42 +02:00

140 lines
5.4 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 { 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 } 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(),
};
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: {} },
{ 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();
});
});
});