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, 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(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(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(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(); }); });