66 lines
2.7 KiB
TypeScript
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');
|
|
});
|
|
});
|