manicanldes-backend/test/recovery-service.spec.ts

66 lines
2.7 KiB
TypeScript

import { Logger } from '@nestjs/common';
import { RecoveryService } from '../src/identity/recovery.service';
import { AuthStore } from '../src/identity/auth.store';
import { RecoveryStore } from '../src/identity/recovery.store';
import { RecoveryMailer } from '../src/identity/recovery-mailer';
import { RateLimitService } from '../src/identity/rate-limit.service';
import { PasswordService } from '../src/identity/password.service';
import { parseEnvironment } from '../src/config/environment';
import { hashToken } from '../src/identity/tokens';
describe('password recovery policy', () => {
const users = { findUser: jest.fn() };
const store = { create: jest.fn(), discard: jest.fn(), reset: jest.fn() };
const mailer = { assertConfigured: jest.fn(), send: jest.fn() };
const limits = { consume: jest.fn() };
const passwords = { hash: jest.fn() };
const service = new RecoveryService(
users as unknown as AuthStore,
store as unknown as RecoveryStore,
mailer as unknown as RecoveryMailer,
limits as unknown as RateLimitService,
passwords as unknown as PasswordService,
parseEnvironment({ DATABASE_URL: 'postgresql://localhost/mani' }),
);
const input = { organizationId: 'org', email: 'user@example.com' };
beforeEach(() => {
jest.resetAllMocks();
users.findUser.mockResolvedValue({
id: 'user',
email: input.email,
status: 'ACTIVE',
});
store.create.mockResolvedValue(true);
});
afterEach(() => jest.restoreAllMocks());
it.each([null, { status: 'PENDING' }, { status: 'SUSPENDED' }])(
'does not disclose or deliver for ineligible accounts %j',
async (user) => {
users.findUser.mockResolvedValue(user);
await expect(service.request(input)).resolves.toBeUndefined();
expect(mailer.send).not.toHaveBeenCalled();
},
);
it('does not deliver if eligibility changes during the request', async () => {
store.create.mockResolvedValue(false);
await service.request(input);
expect(mailer.send).not.toHaveBeenCalled();
});
it('discards undelivered tokens without logging the raw SMTP error', async () => {
const logger = jest.spyOn(Logger.prototype, 'error').mockImplementation();
mailer.send.mockRejectedValue(new Error('smtp-password-secret'));
await service.request(input);
expect(store.discard).toHaveBeenCalledWith(
expect.stringMatching(/^[a-f0-9]{64}$/),
);
expect(JSON.stringify(logger.mock.calls)).not.toContain(
'smtp-password-secret',
);
});
it('hashes both password and token before reset persistence', async () => {
passwords.hash.mockResolvedValue('hash');
await service.reset('token', 'new password');
expect(store.reset).toHaveBeenCalledWith(hashToken('token'), 'hash');
});
});