70 lines
3.0 KiB
TypeScript
70 lines
3.0 KiB
TypeScript
import { ConfigService } from '@nestjs/config';
|
|
import { JwtService } from '@nestjs/jwt';
|
|
import { Repository } from 'typeorm';
|
|
import { AuthService, JwtPayload } from './auth.service';
|
|
import { User, UserRole } from '@domain/entities/user.entity';
|
|
import { UserRepository } from '@domain/ports/out/user.repository';
|
|
import { OrganizationRepository } from '@domain/ports/out/organization.repository';
|
|
import { EmailPort } from '@domain/ports/out/email.port';
|
|
import { CachePort } from '@domain/ports/out/cache.port';
|
|
import { PasswordResetTokenOrmEntity } from '@infrastructure/persistence/typeorm/entities/password-reset-token.orm-entity';
|
|
import { SubscriptionService } from '../services/subscription.service';
|
|
|
|
jest.mock('argon2', () => ({ verify: jest.fn().mockResolvedValue(true) }));
|
|
|
|
describe('password-bound sessions', () => {
|
|
let user: User;
|
|
let auth: AuthService;
|
|
let jwt: JwtService;
|
|
beforeEach(() => {
|
|
user = User.create({
|
|
id: 'user-1',
|
|
organizationId: 'org-1',
|
|
email: 'test@example.org',
|
|
firstName: 'Test',
|
|
lastName: 'User',
|
|
role: UserRole.ADMIN,
|
|
passwordHash: 'old-salted-hash',
|
|
});
|
|
jwt = new JwtService({ secret: 'test-only-session-secret' });
|
|
auth = new AuthService(
|
|
{
|
|
findById: jest.fn(async () => user),
|
|
findByEmail: jest.fn(async () => user),
|
|
} as unknown as UserRepository,
|
|
{} as OrganizationRepository,
|
|
{} as EmailPort,
|
|
{ get: jest.fn(async () => null) } as unknown as CachePort,
|
|
{} as Repository<PasswordResetTokenOrmEntity>,
|
|
jwt,
|
|
new ConfigService({ JWT_SECRET: 'test-only-session-secret' }),
|
|
{} as SubscriptionService
|
|
);
|
|
});
|
|
|
|
it('rejects old access and refresh tokens after a password change, but accepts a new login', async () => {
|
|
const tokens = await auth.login(user.email, 'password');
|
|
const payload = jwt.verify<JwtPayload>(tokens.accessToken);
|
|
expect(await auth.validateUser(payload)).toBe(user);
|
|
expect(payload.credentialVersion).not.toContain(user.passwordHash);
|
|
user.updatePassword('new-salted-hash');
|
|
expect(await auth.validateUser(payload)).toBeNull();
|
|
await expect(auth.refreshAccessToken(tokens.refreshToken)).rejects.toThrow();
|
|
const fresh = await auth.login(user.email, 'new-password');
|
|
expect(await auth.validateUser(jwt.verify<JwtPayload>(fresh.accessToken))).toBe(user);
|
|
await expect(auth.refreshAccessToken(fresh.refreshToken)).resolves.toHaveProperty(
|
|
'accessToken'
|
|
);
|
|
});
|
|
|
|
it('preserves sessions after a profile change and rejects legacy or disabled sessions', async () => {
|
|
const tokens = await auth.login(user.email, 'password');
|
|
const payload = jwt.verify<JwtPayload>(tokens.accessToken);
|
|
user.updateFirstName('New name');
|
|
expect(await auth.validateUser(payload)).toBe(user);
|
|
expect(await auth.validateUser({ ...payload, credentialVersion: undefined })).toBeNull();
|
|
user.deactivate();
|
|
expect(await auth.validateUser(payload)).toBeNull();
|
|
});
|
|
});
|