test: Add unit tests for 5 high-priority auth services
Created comprehensive test suites for authentication services: - MFA service (477 lines, 28 tests): TOTP setup, email MFA, backup codes - Biometric auth service (287 lines, 18 tests): WebAuthn/FIDO2 credentials - Session service (237 lines, 16 tests): Multi-device session management - Device trust service (134 lines, 10 tests): Device registry and trust - Password reset service (142 lines, 9 tests): Token generation and validation Total: 1,277 lines, 81 test cases Coverage: 27% → ~46% service coverage (12/26 services) All tests follow NestJS testing patterns with: - Mocked repositories and services - Success, error, and edge case coverage - TypeORM repository pattern testing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,287 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import {
|
||||
BadRequestException,
|
||||
NotFoundException,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { BiometricAuthService } from './biometric-auth.service';
|
||||
import { WebAuthnCredential } from './entities/webauthn-credential.entity';
|
||||
import { User } from '../../database/entities/user.entity';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
describe('BiometricAuthService', () => {
|
||||
let service: BiometricAuthService;
|
||||
let webauthnCredentialRepository: Repository<WebAuthnCredential>;
|
||||
let userRepository: Repository<User>;
|
||||
let authService: AuthService;
|
||||
|
||||
const mockUser = {
|
||||
id: 'user_123',
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
};
|
||||
|
||||
const mockCredential = {
|
||||
id: 'cred_123',
|
||||
userId: 'user_123',
|
||||
credentialId: 'credential_abc',
|
||||
publicKey: 'public_key_data',
|
||||
counter: 0,
|
||||
transports: ['internal'],
|
||||
friendlyName: 'iPhone 14',
|
||||
lastUsedAt: new Date(),
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
const mockWebAuthnCredentialRepository = {
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
};
|
||||
|
||||
const mockUserRepository = {
|
||||
findOne: jest.fn(),
|
||||
};
|
||||
|
||||
const mockAuthService = {
|
||||
generateTokens: jest.fn().mockResolvedValue({
|
||||
accessToken: 'access_token',
|
||||
refreshToken: 'refresh_token',
|
||||
}),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
BiometricAuthService,
|
||||
{
|
||||
provide: getRepositoryToken(WebAuthnCredential),
|
||||
useValue: mockWebAuthnCredentialRepository,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(User),
|
||||
useValue: mockUserRepository,
|
||||
},
|
||||
{
|
||||
provide: AuthService,
|
||||
useValue: mockAuthService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<BiometricAuthService>(BiometricAuthService);
|
||||
webauthnCredentialRepository = module.get<Repository<WebAuthnCredential>>(
|
||||
getRepositoryToken(WebAuthnCredential),
|
||||
);
|
||||
userRepository = module.get<Repository<User>>(getRepositoryToken(User));
|
||||
authService = module.get<AuthService>(AuthService);
|
||||
|
||||
// Reset mocks
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('generateRegistrationOptions', () => {
|
||||
it('should generate registration options for a user', async () => {
|
||||
mockUserRepository.findOne.mockResolvedValue(mockUser);
|
||||
mockWebAuthnCredentialRepository.find.mockResolvedValue([]);
|
||||
|
||||
const result = await service.generateRegistrationOptions({
|
||||
userId: 'user_123',
|
||||
friendlyName: 'iPhone 14',
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('challenge');
|
||||
expect(result).toHaveProperty('rp');
|
||||
expect(result).toHaveProperty('user');
|
||||
expect(result.rp.name).toBe('Maternal Care App');
|
||||
expect(result.user.name).toBe(mockUser.email);
|
||||
expect(result.user.displayName).toBe(mockUser.name);
|
||||
expect(userRepository.findOne).toHaveBeenCalledWith({
|
||||
where: { id: 'user_123' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should exclude existing credentials', async () => {
|
||||
mockUserRepository.findOne.mockResolvedValue(mockUser);
|
||||
mockWebAuthnCredentialRepository.find.mockResolvedValue([mockCredential]);
|
||||
|
||||
const result = await service.generateRegistrationOptions({
|
||||
userId: 'user_123',
|
||||
});
|
||||
|
||||
expect(result.excludeCredentials).toHaveLength(1);
|
||||
expect(result.excludeCredentials[0].id).toBe(mockCredential.credentialId);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException if user not found', async () => {
|
||||
mockUserRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.generateRegistrationOptions({ userId: 'user_123' }),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUserCredentials', () => {
|
||||
it('should return all credentials for a user', async () => {
|
||||
mockWebAuthnCredentialRepository.find.mockResolvedValue([mockCredential]);
|
||||
|
||||
const result = await service.getUserCredentials('user_123');
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual(mockCredential);
|
||||
expect(webauthnCredentialRepository.find).toHaveBeenCalledWith({
|
||||
where: { userId: 'user_123' },
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty array if no credentials', async () => {
|
||||
mockWebAuthnCredentialRepository.find.mockResolvedValue([]);
|
||||
|
||||
const result = await service.getUserCredentials('user_123');
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteCredential', () => {
|
||||
it('should delete a credential', async () => {
|
||||
mockWebAuthnCredentialRepository.findOne.mockResolvedValue(mockCredential);
|
||||
mockWebAuthnCredentialRepository.delete.mockResolvedValue({ affected: 1 });
|
||||
|
||||
const result = await service.deleteCredential('user_123', 'cred_123');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toContain('deleted successfully');
|
||||
expect(webauthnCredentialRepository.delete).toHaveBeenCalledWith(
|
||||
'cred_123',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException if credential not found', async () => {
|
||||
mockWebAuthnCredentialRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.deleteCredential('user_123', 'cred_123'),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('should throw BadRequestException if credential belongs to another user', async () => {
|
||||
mockWebAuthnCredentialRepository.findOne.mockResolvedValue({
|
||||
...mockCredential,
|
||||
userId: 'other_user',
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.deleteCredential('user_123', 'cred_123'),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateCredentialName', () => {
|
||||
it('should update credential friendly name', async () => {
|
||||
mockWebAuthnCredentialRepository.findOne.mockResolvedValue(mockCredential);
|
||||
mockWebAuthnCredentialRepository.update.mockResolvedValue({ affected: 1 });
|
||||
|
||||
const result = await service.updateCredentialName(
|
||||
'user_123',
|
||||
'cred_123',
|
||||
'My New Phone',
|
||||
);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toContain('updated successfully');
|
||||
expect(webauthnCredentialRepository.update).toHaveBeenCalledWith(
|
||||
'cred_123',
|
||||
{ friendlyName: 'My New Phone' },
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException if credential not found', async () => {
|
||||
mockWebAuthnCredentialRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.updateCredentialName('user_123', 'cred_123', 'New Name'),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('should throw BadRequestException if credential belongs to another user', async () => {
|
||||
mockWebAuthnCredentialRepository.findOne.mockResolvedValue({
|
||||
...mockCredential,
|
||||
userId: 'other_user',
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.updateCredentialName('user_123', 'cred_123', 'New Name'),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasCredentials', () => {
|
||||
it('should return true if user has credentials', async () => {
|
||||
mockWebAuthnCredentialRepository.find.mockResolvedValue([mockCredential]);
|
||||
|
||||
const result = await service.hasCredentials('user_123');
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false if user has no credentials', async () => {
|
||||
mockWebAuthnCredentialRepository.find.mockResolvedValue([]);
|
||||
|
||||
const result = await service.hasCredentials('user_123');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateAuthenticationOptions', () => {
|
||||
it('should generate authentication options with email', async () => {
|
||||
mockUserRepository.findOne.mockResolvedValue(mockUser);
|
||||
mockWebAuthnCredentialRepository.find.mockResolvedValue([mockCredential]);
|
||||
|
||||
const result = await service.generateAuthenticationOptions({
|
||||
email: 'test@example.com',
|
||||
});
|
||||
|
||||
expect(result).toHaveProperty('challenge');
|
||||
expect(result).toHaveProperty('allowCredentials');
|
||||
expect(result.allowCredentials).toHaveLength(1);
|
||||
expect(result.allowCredentials[0].id).toBe(mockCredential.credentialId);
|
||||
});
|
||||
|
||||
it('should generate authentication options without email', async () => {
|
||||
const result = await service.generateAuthenticationOptions({});
|
||||
|
||||
expect(result).toHaveProperty('challenge');
|
||||
expect(result.allowCredentials).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should throw BadRequestException if user has no credentials', async () => {
|
||||
mockUserRepository.findOne.mockResolvedValue(mockUser);
|
||||
mockWebAuthnCredentialRepository.find.mockResolvedValue([]);
|
||||
|
||||
await expect(
|
||||
service.generateAuthenticationOptions({ email: 'test@example.com' }),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException if user not found', async () => {
|
||||
mockUserRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.generateAuthenticationOptions({ email: 'test@example.com' }),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,134 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { DeviceTrustService } from './device-trust.service';
|
||||
import { DeviceRegistry } from './entities/device-registry.entity';
|
||||
|
||||
describe('DeviceTrustService', () => {
|
||||
let service: DeviceTrustService;
|
||||
let deviceRepository: Repository<DeviceRegistry>;
|
||||
|
||||
const mockDevice = {
|
||||
id: 'device_123',
|
||||
userId: 'user_123',
|
||||
deviceId: 'fingerprint_abc',
|
||||
isTrusted: false,
|
||||
deviceInfo: {
|
||||
platform: 'ios',
|
||||
model: 'iPhone 14',
|
||||
osVersion: '17.0',
|
||||
},
|
||||
lastSeenAt: new Date(),
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
const mockDeviceRepository = {
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
count: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
DeviceTrustService,
|
||||
{
|
||||
provide: getRepositoryToken(DeviceRegistry),
|
||||
useValue: mockDeviceRepository,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<DeviceTrustService>(DeviceTrustService);
|
||||
deviceRepository = module.get<Repository<DeviceRegistry>>(getRepositoryToken(DeviceRegistry));
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('getUserDevices', () => {
|
||||
it('should return all devices for a user', async () => {
|
||||
mockDeviceRepository.find.mockResolvedValue([mockDevice]);
|
||||
const result = await service.getUserDevices('user_123');
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0]).toEqual(expect.objectContaining({
|
||||
id: 'device_123',
|
||||
deviceId: 'fingerprint_abc',
|
||||
isTrusted: false,
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTrustedDevices', () => {
|
||||
it('should return only trusted devices', async () => {
|
||||
const trustedDevice = { ...mockDevice, isTrusted: true };
|
||||
mockDeviceRepository.find.mockResolvedValue([trustedDevice]);
|
||||
const result = await service.getTrustedDevices('user_123');
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].isTrusted).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('trustDevice', () => {
|
||||
it('should mark device as trusted', async () => {
|
||||
mockDeviceRepository.findOne.mockResolvedValue(mockDevice);
|
||||
mockDeviceRepository.update.mockResolvedValue({ affected: 1 });
|
||||
const result = await service.trustDevice('user_123', 'device_123');
|
||||
expect(result.success).toBe(true);
|
||||
expect(deviceRepository.update).toHaveBeenCalledWith('device_123', { isTrusted: true });
|
||||
});
|
||||
|
||||
it('should throw NotFoundException if device not found', async () => {
|
||||
mockDeviceRepository.findOne.mockResolvedValue(null);
|
||||
await expect(service.trustDevice('user_123', 'device_123')).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('revokeDeviceTrust', () => {
|
||||
it('should revoke device trust', async () => {
|
||||
mockDeviceRepository.findOne.mockResolvedValue({ ...mockDevice, isTrusted: true });
|
||||
mockDeviceRepository.update.mockResolvedValue({ affected: 1 });
|
||||
const result = await service.revokeDeviceTrust('user_123', 'device_123');
|
||||
expect(result.success).toBe(true);
|
||||
expect(deviceRepository.update).toHaveBeenCalledWith('device_123', { isTrusted: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeDevice', () => {
|
||||
it('should remove a device', async () => {
|
||||
mockDeviceRepository.findOne.mockResolvedValue(mockDevice);
|
||||
mockDeviceRepository.delete.mockResolvedValue({ affected: 1 });
|
||||
const result = await service.removeDevice('user_123', 'device_123');
|
||||
expect(result.success).toBe(true);
|
||||
expect(deviceRepository.delete).toHaveBeenCalledWith('device_123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDeviceCount', () => {
|
||||
it('should return device counts', async () => {
|
||||
mockDeviceRepository.count.mockResolvedValueOnce(5).mockResolvedValueOnce(3);
|
||||
const result = await service.getDeviceCount('user_123');
|
||||
expect(result.total).toBe(5);
|
||||
expect(result.trusted).toBe(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isDeviceTrusted', () => {
|
||||
it('should return true for trusted device', async () => {
|
||||
mockDeviceRepository.findOne.mockResolvedValue({ ...mockDevice, isTrusted: true });
|
||||
const result = await service.isDeviceTrusted('user_123', 'device_123');
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for untrusted device', async () => {
|
||||
mockDeviceRepository.findOne.mockResolvedValue(mockDevice);
|
||||
const result = await service.isDeviceTrusted('user_123', 'device_123');
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,477 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { BadRequestException, NotFoundException } from '@nestjs/common';
|
||||
import { MFAService } from './mfa.service';
|
||||
import { User } from '../../database/entities';
|
||||
import { EmailService } from '../../common/services/email.service';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { authenticator } from 'otplib';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
|
||||
describe('MFAService', () => {
|
||||
let service: MFAService;
|
||||
let userRepository: Repository<User>;
|
||||
let emailService: EmailService;
|
||||
let configService: ConfigService;
|
||||
|
||||
const mockUser = {
|
||||
id: 'user_123',
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
mfaEnabled: false,
|
||||
mfaMethod: null,
|
||||
totpSecret: null,
|
||||
mfaBackupCodes: [],
|
||||
emailMfaCode: null,
|
||||
emailMfaCodeExpiresAt: null,
|
||||
};
|
||||
|
||||
const mockUserRepository = {
|
||||
findOne: jest.fn(),
|
||||
update: jest.fn(),
|
||||
};
|
||||
|
||||
const mockEmailService = {
|
||||
sendEmail: jest.fn().mockResolvedValue(true),
|
||||
};
|
||||
|
||||
const mockConfigService = {
|
||||
get: jest.fn().mockReturnValue('Maternal App'),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
MFAService,
|
||||
{
|
||||
provide: getRepositoryToken(User),
|
||||
useValue: mockUserRepository,
|
||||
},
|
||||
{
|
||||
provide: EmailService,
|
||||
useValue: mockEmailService,
|
||||
},
|
||||
{
|
||||
provide: ConfigService,
|
||||
useValue: mockConfigService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<MFAService>(MFAService);
|
||||
userRepository = module.get<Repository<User>>(getRepositoryToken(User));
|
||||
emailService = module.get<EmailService>(EmailService);
|
||||
configService = module.get<ConfigService>(ConfigService);
|
||||
|
||||
// Reset mocks
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('getMFAStatus', () => {
|
||||
it('should return MFA status for a user', async () => {
|
||||
mockUserRepository.findOne.mockResolvedValue({
|
||||
...mockUser,
|
||||
mfaEnabled: true,
|
||||
mfaMethod: 'totp',
|
||||
mfaBackupCodes: ['hashed_code_1', 'hashed_code_2'],
|
||||
});
|
||||
|
||||
const result = await service.getMFAStatus('user_123');
|
||||
|
||||
expect(result).toEqual({
|
||||
enabled: true,
|
||||
method: 'totp',
|
||||
hasBackupCodes: true,
|
||||
});
|
||||
expect(userRepository.findOne).toHaveBeenCalledWith({
|
||||
where: { id: 'user_123' },
|
||||
select: ['id', 'mfaEnabled', 'mfaMethod', 'mfaBackupCodes'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return disabled status when MFA is not enabled', async () => {
|
||||
mockUserRepository.findOne.mockResolvedValue(mockUser);
|
||||
|
||||
const result = await service.getMFAStatus('user_123');
|
||||
|
||||
expect(result).toEqual({
|
||||
enabled: false,
|
||||
method: undefined,
|
||||
hasBackupCodes: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw NotFoundException if user not found', async () => {
|
||||
mockUserRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(service.getMFAStatus('user_123')).rejects.toThrow(
|
||||
NotFoundException,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setupTOTP', () => {
|
||||
it('should generate TOTP secret, QR code, and backup codes', async () => {
|
||||
mockUserRepository.findOne.mockResolvedValue(mockUser);
|
||||
mockUserRepository.update.mockResolvedValue({ affected: 1 });
|
||||
|
||||
const result = await service.setupTOTP('user_123');
|
||||
|
||||
expect(result).toHaveProperty('secret');
|
||||
expect(result).toHaveProperty('qrCodeUrl');
|
||||
expect(result).toHaveProperty('backupCodes');
|
||||
expect(result.secret).toBeTruthy();
|
||||
expect(result.qrCodeUrl).toContain('data:image/png;base64');
|
||||
expect(result.backupCodes).toHaveLength(10);
|
||||
expect(userRepository.update).toHaveBeenCalledWith(
|
||||
'user_123',
|
||||
expect.objectContaining({
|
||||
totpSecret: expect.any(String),
|
||||
mfaBackupCodes: expect.any(Array),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException if user not found', async () => {
|
||||
mockUserRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(service.setupTOTP('user_123')).rejects.toThrow(
|
||||
NotFoundException,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyAndEnableTOTP', () => {
|
||||
it('should enable TOTP MFA with valid code', async () => {
|
||||
const totpSecret = authenticator.generateSecret();
|
||||
const validCode = authenticator.generate(totpSecret);
|
||||
|
||||
mockUserRepository.findOne.mockResolvedValue({
|
||||
...mockUser,
|
||||
totpSecret,
|
||||
});
|
||||
mockUserRepository.update.mockResolvedValue({ affected: 1 });
|
||||
|
||||
const result = await service.verifyAndEnableTOTP('user_123', validCode);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toContain('enabled successfully');
|
||||
expect(userRepository.update).toHaveBeenCalledWith('user_123', {
|
||||
mfaEnabled: true,
|
||||
mfaMethod: 'totp',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw BadRequestException for invalid TOTP code', async () => {
|
||||
const totpSecret = authenticator.generateSecret();
|
||||
|
||||
mockUserRepository.findOne.mockResolvedValue({
|
||||
...mockUser,
|
||||
totpSecret,
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.verifyAndEnableTOTP('user_123', '000000'),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it('should throw BadRequestException if TOTP not set up', async () => {
|
||||
mockUserRepository.findOne.mockResolvedValue(mockUser);
|
||||
|
||||
await expect(
|
||||
service.verifyAndEnableTOTP('user_123', '123456'),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException if user not found', async () => {
|
||||
mockUserRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.verifyAndEnableTOTP('user_123', '123456'),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setupEmailMFA', () => {
|
||||
it('should enable Email MFA and send confirmation email', async () => {
|
||||
mockUserRepository.findOne.mockResolvedValue(mockUser);
|
||||
mockUserRepository.update.mockResolvedValue({ affected: 1 });
|
||||
|
||||
const result = await service.setupEmailMFA('user_123');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toContain('enabled successfully');
|
||||
expect(userRepository.update).toHaveBeenCalledWith(
|
||||
'user_123',
|
||||
expect.objectContaining({
|
||||
mfaEnabled: true,
|
||||
mfaMethod: 'email',
|
||||
mfaBackupCodes: expect.any(Array),
|
||||
}),
|
||||
);
|
||||
expect(emailService.sendEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
to: mockUser.email,
|
||||
subject: expect.stringContaining('Two-Factor Authentication'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException if user not found', async () => {
|
||||
mockUserRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(service.setupEmailMFA('user_123')).rejects.toThrow(
|
||||
NotFoundException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should still succeed if email sending fails', async () => {
|
||||
mockUserRepository.findOne.mockResolvedValue(mockUser);
|
||||
mockUserRepository.update.mockResolvedValue({ affected: 1 });
|
||||
mockEmailService.sendEmail.mockRejectedValue(
|
||||
new Error('Email service down'),
|
||||
);
|
||||
|
||||
const result = await service.setupEmailMFA('user_123');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
// MFA should still be enabled even if email fails
|
||||
expect(userRepository.update).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('sendEmailMFACode', () => {
|
||||
it('should generate and send 6-digit code via email', async () => {
|
||||
mockUserRepository.findOne.mockResolvedValue({
|
||||
...mockUser,
|
||||
mfaEnabled: true,
|
||||
mfaMethod: 'email',
|
||||
});
|
||||
mockUserRepository.update.mockResolvedValue({ affected: 1 });
|
||||
|
||||
const result = await service.sendEmailMFACode('user_123');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toContain('Verification code sent');
|
||||
expect(userRepository.update).toHaveBeenCalledWith(
|
||||
'user_123',
|
||||
expect.objectContaining({
|
||||
emailMfaCode: expect.any(String),
|
||||
emailMfaCodeExpiresAt: expect.any(Date),
|
||||
}),
|
||||
);
|
||||
expect(emailService.sendEmail).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
to: mockUser.email,
|
||||
subject: expect.stringContaining('verification code'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw BadRequestException if Email MFA not enabled', async () => {
|
||||
mockUserRepository.findOne.mockResolvedValue(mockUser);
|
||||
|
||||
await expect(service.sendEmailMFACode('user_123')).rejects.toThrow(
|
||||
BadRequestException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException if user not found', async () => {
|
||||
mockUserRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(service.sendEmailMFACode('user_123')).rejects.toThrow(
|
||||
NotFoundException,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyMFACode', () => {
|
||||
it('should verify valid TOTP code', async () => {
|
||||
const totpSecret = authenticator.generateSecret();
|
||||
const validCode = authenticator.generate(totpSecret);
|
||||
|
||||
mockUserRepository.findOne.mockResolvedValue({
|
||||
...mockUser,
|
||||
mfaEnabled: true,
|
||||
mfaMethod: 'totp',
|
||||
totpSecret,
|
||||
});
|
||||
|
||||
const result = await service.verifyMFACode('user_123', validCode);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject invalid TOTP code', async () => {
|
||||
const totpSecret = authenticator.generateSecret();
|
||||
|
||||
mockUserRepository.findOne.mockResolvedValue({
|
||||
...mockUser,
|
||||
mfaEnabled: true,
|
||||
mfaMethod: 'totp',
|
||||
totpSecret,
|
||||
});
|
||||
|
||||
await expect(service.verifyMFACode('user_123', '000000')).rejects.toThrow(
|
||||
BadRequestException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should verify valid email MFA code', async () => {
|
||||
const code = '123456';
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setMinutes(expiresAt.getMinutes() + 5);
|
||||
|
||||
mockUserRepository.findOne.mockResolvedValue({
|
||||
...mockUser,
|
||||
mfaEnabled: true,
|
||||
mfaMethod: 'email',
|
||||
emailMfaCode: code,
|
||||
emailMfaCodeExpiresAt: expiresAt,
|
||||
});
|
||||
mockUserRepository.update.mockResolvedValue({ affected: 1 });
|
||||
|
||||
const result = await service.verifyMFACode('user_123', code);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
// Code should be cleared after use
|
||||
expect(userRepository.update).toHaveBeenCalledWith('user_123', {
|
||||
emailMfaCode: null,
|
||||
emailMfaCodeExpiresAt: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject expired email MFA code', async () => {
|
||||
const code = '123456';
|
||||
const expiresAt = new Date();
|
||||
expiresAt.setMinutes(expiresAt.getMinutes() - 1); // Expired 1 minute ago
|
||||
|
||||
mockUserRepository.findOne.mockResolvedValue({
|
||||
...mockUser,
|
||||
mfaEnabled: true,
|
||||
mfaMethod: 'email',
|
||||
emailMfaCode: code,
|
||||
emailMfaCodeExpiresAt: expiresAt,
|
||||
});
|
||||
|
||||
await expect(service.verifyMFACode('user_123', code)).rejects.toThrow(
|
||||
BadRequestException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should verify valid backup code and mark it as used', async () => {
|
||||
const backupCode = 'BACKUP-123456';
|
||||
const hashedBackupCode = await bcrypt.hash(backupCode, 10);
|
||||
|
||||
mockUserRepository.findOne.mockResolvedValue({
|
||||
...mockUser,
|
||||
mfaEnabled: true,
|
||||
mfaMethod: 'totp',
|
||||
mfaBackupCodes: [hashedBackupCode, 'other_hashed_code'],
|
||||
});
|
||||
mockUserRepository.update.mockResolvedValue({ affected: 1 });
|
||||
|
||||
const result = await service.verifyMFACode('user_123', backupCode);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
// Backup code should be removed after use
|
||||
expect(userRepository.update).toHaveBeenCalledWith(
|
||||
'user_123',
|
||||
expect.objectContaining({
|
||||
mfaBackupCodes: expect.any(Array),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw BadRequestException if MFA not enabled', async () => {
|
||||
mockUserRepository.findOne.mockResolvedValue(mockUser);
|
||||
|
||||
await expect(service.verifyMFACode('user_123', '123456')).rejects.toThrow(
|
||||
BadRequestException,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('disableMFA', () => {
|
||||
it('should disable MFA and clear all MFA data', async () => {
|
||||
mockUserRepository.findOne.mockResolvedValue({
|
||||
...mockUser,
|
||||
mfaEnabled: true,
|
||||
mfaMethod: 'totp',
|
||||
});
|
||||
mockUserRepository.update.mockResolvedValue({ affected: 1 });
|
||||
|
||||
const result = await service.disableMFA('user_123');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toContain('disabled successfully');
|
||||
expect(userRepository.update).toHaveBeenCalledWith('user_123', {
|
||||
mfaEnabled: false,
|
||||
mfaMethod: null,
|
||||
totpSecret: null,
|
||||
emailMfaCode: null,
|
||||
emailMfaCodeExpiresAt: null,
|
||||
mfaBackupCodes: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw BadRequestException if MFA not enabled', async () => {
|
||||
mockUserRepository.findOne.mockResolvedValue(mockUser);
|
||||
|
||||
await expect(service.disableMFA('user_123')).rejects.toThrow(
|
||||
BadRequestException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException if user not found', async () => {
|
||||
mockUserRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(service.disableMFA('user_123')).rejects.toThrow(
|
||||
NotFoundException,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('regenerateBackupCodes', () => {
|
||||
it('should generate new backup codes', async () => {
|
||||
mockUserRepository.findOne.mockResolvedValue({
|
||||
...mockUser,
|
||||
mfaEnabled: true,
|
||||
mfaMethod: 'totp',
|
||||
});
|
||||
mockUserRepository.update.mockResolvedValue({ affected: 1 });
|
||||
|
||||
const result = await service.regenerateBackupCodes('user_123');
|
||||
|
||||
expect(result).toHaveLength(10);
|
||||
expect(result[0]).toMatch(/^BACKUP-[A-Z0-9]{6}$/);
|
||||
expect(userRepository.update).toHaveBeenCalledWith(
|
||||
'user_123',
|
||||
expect.objectContaining({
|
||||
mfaBackupCodes: expect.any(Array),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw BadRequestException if MFA not enabled', async () => {
|
||||
mockUserRepository.findOne.mockResolvedValue(mockUser);
|
||||
|
||||
await expect(service.regenerateBackupCodes('user_123')).rejects.toThrow(
|
||||
BadRequestException,
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw NotFoundException if user not found', async () => {
|
||||
mockUserRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(service.regenerateBackupCodes('user_123')).rejects.toThrow(
|
||||
NotFoundException,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,142 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { PasswordResetService } from './password-reset.service';
|
||||
import { PasswordResetToken } from './entities/password-reset-token.entity';
|
||||
import { User } from '../../database/entities/user.entity';
|
||||
import { EmailService } from '../../common/services/email.service';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
|
||||
describe('PasswordResetService', () => {
|
||||
let service: PasswordResetService;
|
||||
let passwordResetTokenRepository: Repository<PasswordResetToken>;
|
||||
let userRepository: Repository<User>;
|
||||
let emailService: EmailService;
|
||||
|
||||
const mockUser = {
|
||||
id: 'user_123',
|
||||
email: 'test@example.com',
|
||||
name: 'Test User',
|
||||
passwordHash: 'old_hash',
|
||||
};
|
||||
|
||||
const mockToken = {
|
||||
id: 'reset_123',
|
||||
userId: 'user_123',
|
||||
token: 'reset_token_abc',
|
||||
expiresAt: new Date(Date.now() + 60 * 60 * 1000),
|
||||
used: false,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
const mockPasswordResetTokenRepository = {
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
update: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
};
|
||||
|
||||
const mockUserRepository = {
|
||||
findOne: jest.fn(),
|
||||
update: jest.fn(),
|
||||
};
|
||||
|
||||
const mockEmailService = {
|
||||
sendEmail: jest.fn().mockResolvedValue(true),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
PasswordResetService,
|
||||
{ provide: getRepositoryToken(PasswordResetToken), useValue: mockPasswordResetTokenRepository },
|
||||
{ provide: getRepositoryToken(User), useValue: mockUserRepository },
|
||||
{ provide: EmailService, useValue: mockEmailService },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<PasswordResetService>(PasswordResetService);
|
||||
passwordResetTokenRepository = module.get<Repository<PasswordResetToken>>(getRepositoryToken(PasswordResetToken));
|
||||
userRepository = module.get<Repository<User>>(getRepositoryToken(User));
|
||||
emailService = module.get<EmailService>(EmailService);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('requestPasswordReset', () => {
|
||||
it('should create reset token and send email', async () => {
|
||||
mockUserRepository.findOne.mockResolvedValue(mockUser);
|
||||
mockPasswordResetTokenRepository.create.mockReturnValue(mockToken);
|
||||
mockPasswordResetTokenRepository.save.mockResolvedValue(mockToken);
|
||||
|
||||
const result = await service.requestPasswordReset('test@example.com');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(passwordResetTokenRepository.save).toHaveBeenCalled();
|
||||
expect(emailService.sendEmail).toHaveBeenCalledWith(expect.objectContaining({
|
||||
to: mockUser.email,
|
||||
subject: expect.stringContaining('Password Reset'),
|
||||
}));
|
||||
});
|
||||
|
||||
it('should not reveal if email does not exist', async () => {
|
||||
mockUserRepository.findOne.mockResolvedValue(null);
|
||||
const result = await service.requestPasswordReset('nonexistent@example.com');
|
||||
expect(result.success).toBe(true);
|
||||
expect(emailService.sendEmail).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('verifyResetToken', () => {
|
||||
it('should verify valid token', async () => {
|
||||
mockPasswordResetTokenRepository.findOne.mockResolvedValue(mockToken);
|
||||
const result = await service.verifyResetToken('reset_token_abc');
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('should reject expired token', async () => {
|
||||
const expiredToken = { ...mockToken, expiresAt: new Date(Date.now() - 1000) };
|
||||
mockPasswordResetTokenRepository.findOne.mockResolvedValue(expiredToken);
|
||||
const result = await service.verifyResetToken('reset_token_abc');
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
|
||||
it('should reject used token', async () => {
|
||||
mockPasswordResetTokenRepository.findOne.mockResolvedValue({ ...mockToken, used: true });
|
||||
const result = await service.verifyResetToken('reset_token_abc');
|
||||
expect(result.valid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resetPassword', () => {
|
||||
it('should reset password with valid token', async () => {
|
||||
mockPasswordResetTokenRepository.findOne.mockResolvedValue(mockToken);
|
||||
mockUserRepository.update.mockResolvedValue({ affected: 1 });
|
||||
mockPasswordResetTokenRepository.update.mockResolvedValue({ affected: 1 });
|
||||
|
||||
const result = await service.resetPassword('reset_token_abc', 'NewPassword123!');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(userRepository.update).toHaveBeenCalledWith('user_123', expect.objectContaining({
|
||||
passwordHash: expect.any(String),
|
||||
}));
|
||||
expect(passwordResetTokenRepository.update).toHaveBeenCalledWith('reset_123', { used: true });
|
||||
});
|
||||
|
||||
it('should throw BadRequestException for invalid token', async () => {
|
||||
mockPasswordResetTokenRepository.findOne.mockResolvedValue(null);
|
||||
await expect(service.resetPassword('invalid_token', 'NewPassword123!')).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
|
||||
it('should throw BadRequestException for expired token', async () => {
|
||||
const expiredToken = { ...mockToken, expiresAt: new Date(Date.now() - 1000) };
|
||||
mockPasswordResetTokenRepository.findOne.mockResolvedValue(expiredToken);
|
||||
await expect(service.resetPassword('reset_token_abc', 'NewPassword123!')).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,237 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { SessionService } from './session.service';
|
||||
import { RefreshToken } from './entities/refresh-token.entity';
|
||||
|
||||
describe('SessionService', () => {
|
||||
let service: SessionService;
|
||||
let refreshTokenRepository: Repository<RefreshToken>;
|
||||
|
||||
const mockRefreshToken = {
|
||||
id: 'token_123',
|
||||
userId: 'user_123',
|
||||
token: 'refresh_token_abc',
|
||||
deviceId: 'device_123',
|
||||
deviceInfo: {
|
||||
platform: 'ios',
|
||||
browser: 'Safari',
|
||||
os: 'iOS 17',
|
||||
ipAddress: '192.168.1.1',
|
||||
},
|
||||
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
||||
lastUsedAt: new Date(),
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
const mockRefreshTokenRepository = {
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
count: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
SessionService,
|
||||
{
|
||||
provide: getRepositoryToken(RefreshToken),
|
||||
useValue: mockRefreshTokenRepository,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<SessionService>(SessionService);
|
||||
refreshTokenRepository = module.get<Repository<RefreshToken>>(
|
||||
getRepositoryToken(RefreshToken),
|
||||
);
|
||||
|
||||
// Reset mocks
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('getUserSessions', () => {
|
||||
it('should return all active sessions for a user', async () => {
|
||||
const sessions = [
|
||||
mockRefreshToken,
|
||||
{ ...mockRefreshToken, id: 'token_456', deviceId: 'device_456' },
|
||||
];
|
||||
mockRefreshTokenRepository.find.mockResolvedValue(sessions);
|
||||
|
||||
const result = await service.getUserSessions('user_123');
|
||||
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result[0]).toHaveProperty('id');
|
||||
expect(result[0]).toHaveProperty('deviceInfo');
|
||||
expect(result[0]).toHaveProperty('lastUsedAt');
|
||||
expect(refreshTokenRepository.find).toHaveBeenCalledWith({
|
||||
where: { userId: 'user_123' },
|
||||
order: { lastUsedAt: 'DESC' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty array if no sessions', async () => {
|
||||
mockRefreshTokenRepository.find.mockResolvedValue([]);
|
||||
|
||||
const result = await service.getUserSessions('user_123');
|
||||
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should format session data correctly', async () => {
|
||||
mockRefreshTokenRepository.find.mockResolvedValue([mockRefreshToken]);
|
||||
|
||||
const result = await service.getUserSessions('user_123');
|
||||
|
||||
expect(result[0]).toEqual({
|
||||
id: mockRefreshToken.id,
|
||||
deviceId: mockRefreshToken.deviceId,
|
||||
deviceInfo: mockRefreshToken.deviceInfo,
|
||||
lastUsedAt: mockRefreshToken.lastUsedAt,
|
||||
createdAt: mockRefreshToken.createdAt,
|
||||
expiresAt: mockRefreshToken.expiresAt,
|
||||
isCurrent: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should mark current session correctly', async () => {
|
||||
mockRefreshTokenRepository.find.mockResolvedValue([mockRefreshToken]);
|
||||
|
||||
const result = await service.getUserSessions('user_123', 'token_123');
|
||||
|
||||
expect(result[0].isCurrent).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('revokeSession', () => {
|
||||
it('should revoke a specific session', async () => {
|
||||
mockRefreshTokenRepository.findOne.mockResolvedValue(mockRefreshToken);
|
||||
mockRefreshTokenRepository.delete.mockResolvedValue({ affected: 1 });
|
||||
|
||||
const result = await service.revokeSession('user_123', 'token_123');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.message).toContain('Session revoked successfully');
|
||||
expect(refreshTokenRepository.delete).toHaveBeenCalledWith({
|
||||
id: 'token_123',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw NotFoundException if session not found', async () => {
|
||||
mockRefreshTokenRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.revokeSession('user_123', 'token_123'),
|
||||
).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('should throw BadRequestException if session belongs to another user', async () => {
|
||||
mockRefreshTokenRepository.findOne.mockResolvedValue({
|
||||
...mockRefreshToken,
|
||||
userId: 'other_user',
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.revokeSession('user_123', 'token_123'),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('revokeAllSessions', () => {
|
||||
it('should revoke all sessions for a user', async () => {
|
||||
mockRefreshTokenRepository.delete.mockResolvedValue({ affected: 3 });
|
||||
|
||||
const result = await service.revokeAllSessions('user_123');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.sessionsRevoked).toBe(3);
|
||||
expect(result.message).toContain('3 sessions revoked successfully');
|
||||
expect(refreshTokenRepository.delete).toHaveBeenCalledWith({
|
||||
userId: 'user_123',
|
||||
});
|
||||
});
|
||||
|
||||
it('should succeed even if no sessions to revoke', async () => {
|
||||
mockRefreshTokenRepository.delete.mockResolvedValue({ affected: 0 });
|
||||
|
||||
const result = await service.revokeAllSessions('user_123');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.sessionsRevoked).toBe(0);
|
||||
});
|
||||
|
||||
it('should exclude current session if sessionId provided', async () => {
|
||||
mockRefreshTokenRepository.delete.mockResolvedValue({ affected: 2 });
|
||||
|
||||
const result = await service.revokeAllSessions('user_123', 'token_current');
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.sessionsRevoked).toBe(2);
|
||||
// Should delete all except current
|
||||
expect(refreshTokenRepository.delete).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getActiveSessionCount', () => {
|
||||
it('should return count of active sessions', async () => {
|
||||
mockRefreshTokenRepository.count.mockResolvedValue(5);
|
||||
|
||||
const result = await service.getActiveSessionCount('user_123');
|
||||
|
||||
expect(result).toBe(5);
|
||||
expect(refreshTokenRepository.count).toHaveBeenCalledWith({
|
||||
where: { userId: 'user_123' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should return 0 if no active sessions', async () => {
|
||||
mockRefreshTokenRepository.count.mockResolvedValue(0);
|
||||
|
||||
const result = await service.getActiveSessionCount('user_123');
|
||||
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSessionValid', () => {
|
||||
it('should return true for valid session', async () => {
|
||||
mockRefreshTokenRepository.findOne.mockResolvedValue(mockRefreshToken);
|
||||
|
||||
const result = await service.isSessionValid('token_123', 'user_123');
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(refreshTokenRepository.findOne).toHaveBeenCalledWith({
|
||||
where: {
|
||||
id: 'token_123',
|
||||
userId: 'user_123',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return false for non-existent session', async () => {
|
||||
mockRefreshTokenRepository.findOne.mockResolvedValue(null);
|
||||
|
||||
const result = await service.isSessionValid('token_123', 'user_123');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for expired session', async () => {
|
||||
const expiredToken = {
|
||||
...mockRefreshToken,
|
||||
expiresAt: new Date(Date.now() - 1000), // Expired 1 second ago
|
||||
};
|
||||
mockRefreshTokenRepository.findOne.mockResolvedValue(expiredToken);
|
||||
|
||||
const result = await service.isSessionValid('token_123', 'user_123');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user