- HERENCIA-SIMCO.md actualizado con directivas v3.7 y v3.8 - Actualizaciones de configuracion Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
663 lines
32 KiB
JavaScript
663 lines
32 KiB
JavaScript
"use strict";
|
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
}
|
|
Object.defineProperty(o, k2, desc);
|
|
}) : (function(o, m, k, k2) {
|
|
if (k2 === undefined) k2 = k;
|
|
o[k2] = m[k];
|
|
}));
|
|
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
}) : function(o, v) {
|
|
o["default"] = v;
|
|
});
|
|
var __importStar = (this && this.__importStar) || (function () {
|
|
var ownKeys = function(o) {
|
|
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
var ar = [];
|
|
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
return ar;
|
|
};
|
|
return ownKeys(o);
|
|
};
|
|
return function (mod) {
|
|
if (mod && mod.__esModule) return mod;
|
|
var result = {};
|
|
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
__setModuleDefault(result, mod);
|
|
return result;
|
|
};
|
|
})();
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
const testing_1 = require("@nestjs/testing");
|
|
const typeorm_1 = require("@nestjs/typeorm");
|
|
const jwt_1 = require("@nestjs/jwt");
|
|
const config_1 = require("@nestjs/config");
|
|
const typeorm_2 = require("typeorm");
|
|
const common_1 = require("@nestjs/common");
|
|
const bcrypt = __importStar(require("bcrypt"));
|
|
const auth_service_1 = require("../services/auth.service");
|
|
const entities_1 = require("../entities");
|
|
jest.mock('bcrypt');
|
|
const mockedBcrypt = bcrypt;
|
|
describe('AuthService', () => {
|
|
let service;
|
|
let userRepository;
|
|
let sessionRepository;
|
|
let tokenRepository;
|
|
let jwtService;
|
|
let configService;
|
|
const mockUser = {
|
|
id: '550e8400-e29b-41d4-a716-446655440000',
|
|
tenant_id: '550e8400-e29b-41d4-a716-446655440001',
|
|
email: 'test@example.com',
|
|
password_hash: 'hashed_password',
|
|
first_name: 'Test',
|
|
last_name: 'User',
|
|
status: 'active',
|
|
email_verified: true,
|
|
};
|
|
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
|
|
beforeEach(async () => {
|
|
const mockUserRepo = {
|
|
findOne: jest.fn(),
|
|
create: jest.fn(),
|
|
save: jest.fn(),
|
|
update: jest.fn(),
|
|
};
|
|
const mockSessionRepo = {
|
|
findOne: jest.fn(),
|
|
save: jest.fn(),
|
|
update: jest.fn(),
|
|
};
|
|
const mockTokenRepo = {
|
|
findOne: jest.fn(),
|
|
save: jest.fn(),
|
|
update: jest.fn(),
|
|
};
|
|
const mockJwtService = {
|
|
sign: jest.fn(),
|
|
verify: jest.fn(),
|
|
};
|
|
const mockConfigService = {
|
|
get: jest.fn(),
|
|
};
|
|
const mockDataSource = {
|
|
createQueryRunner: jest.fn(),
|
|
};
|
|
const module = await testing_1.Test.createTestingModule({
|
|
providers: [
|
|
auth_service_1.AuthService,
|
|
{ provide: (0, typeorm_1.getRepositoryToken)(entities_1.User), useValue: mockUserRepo },
|
|
{ provide: (0, typeorm_1.getRepositoryToken)(entities_1.Session), useValue: mockSessionRepo },
|
|
{ provide: (0, typeorm_1.getRepositoryToken)(entities_1.Token), useValue: mockTokenRepo },
|
|
{ provide: jwt_1.JwtService, useValue: mockJwtService },
|
|
{ provide: config_1.ConfigService, useValue: mockConfigService },
|
|
{ provide: typeorm_2.DataSource, useValue: mockDataSource },
|
|
],
|
|
}).compile();
|
|
service = module.get(auth_service_1.AuthService);
|
|
userRepository = module.get((0, typeorm_1.getRepositoryToken)(entities_1.User));
|
|
sessionRepository = module.get((0, typeorm_1.getRepositoryToken)(entities_1.Session));
|
|
tokenRepository = module.get((0, typeorm_1.getRepositoryToken)(entities_1.Token));
|
|
jwtService = module.get(jwt_1.JwtService);
|
|
configService = module.get(config_1.ConfigService);
|
|
});
|
|
afterEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
describe('register', () => {
|
|
const registerDto = {
|
|
email: 'newuser@example.com',
|
|
password: 'SecurePass123!',
|
|
first_name: 'New',
|
|
last_name: 'User',
|
|
};
|
|
it('should register a new user successfully', async () => {
|
|
userRepository.findOne.mockResolvedValue(null);
|
|
mockedBcrypt.hash.mockResolvedValue('hashed_password');
|
|
userRepository.create.mockReturnValue({
|
|
...mockUser,
|
|
email: registerDto.email,
|
|
status: 'pending_verification',
|
|
});
|
|
userRepository.save.mockResolvedValue({
|
|
...mockUser,
|
|
email: registerDto.email,
|
|
status: 'pending_verification',
|
|
});
|
|
sessionRepository.save.mockResolvedValue({});
|
|
tokenRepository.save.mockResolvedValue({});
|
|
jwtService.sign.mockReturnValueOnce('access_token').mockReturnValueOnce('refresh_token');
|
|
configService.get.mockReturnValue('15m');
|
|
const result = await service.register(registerDto, mockTenantId);
|
|
expect(result).toHaveProperty('user');
|
|
expect(result).toHaveProperty('accessToken');
|
|
expect(result).toHaveProperty('refreshToken');
|
|
expect(userRepository.findOne).toHaveBeenCalled();
|
|
expect(userRepository.save).toHaveBeenCalled();
|
|
});
|
|
it('should throw ConflictException if email already exists', async () => {
|
|
userRepository.findOne.mockResolvedValue(mockUser);
|
|
await expect(service.register(registerDto, mockTenantId)).rejects.toThrow(common_1.ConflictException);
|
|
});
|
|
});
|
|
describe('login', () => {
|
|
const loginDto = {
|
|
email: 'test@example.com',
|
|
password: 'password123',
|
|
};
|
|
it('should login user successfully', async () => {
|
|
userRepository.findOne.mockResolvedValue(mockUser);
|
|
mockedBcrypt.compare.mockResolvedValue(true);
|
|
userRepository.save.mockResolvedValue(mockUser);
|
|
sessionRepository.save.mockResolvedValue({});
|
|
jwtService.sign.mockReturnValueOnce('access_token').mockReturnValueOnce('refresh_token');
|
|
configService.get.mockReturnValue('15m');
|
|
const result = await service.login(loginDto, mockTenantId);
|
|
expect(result).toHaveProperty('user');
|
|
expect(result).toHaveProperty('accessToken');
|
|
expect(result).toHaveProperty('refreshToken');
|
|
expect(result.user).not.toHaveProperty('password_hash');
|
|
});
|
|
it('should throw UnauthorizedException for invalid email', async () => {
|
|
userRepository.findOne.mockResolvedValue(null);
|
|
await expect(service.login(loginDto, mockTenantId)).rejects.toThrow(common_1.UnauthorizedException);
|
|
});
|
|
it('should throw UnauthorizedException for invalid password', async () => {
|
|
userRepository.findOne.mockResolvedValue(mockUser);
|
|
mockedBcrypt.compare.mockResolvedValue(false);
|
|
await expect(service.login(loginDto, mockTenantId)).rejects.toThrow(common_1.UnauthorizedException);
|
|
});
|
|
it('should throw UnauthorizedException for suspended user', async () => {
|
|
userRepository.findOne.mockResolvedValue({
|
|
...mockUser,
|
|
status: 'suspended',
|
|
});
|
|
mockedBcrypt.compare.mockResolvedValue(true);
|
|
await expect(service.login(loginDto, mockTenantId)).rejects.toThrow(common_1.UnauthorizedException);
|
|
});
|
|
it('should throw UnauthorizedException for inactive user', async () => {
|
|
userRepository.findOne.mockResolvedValue({
|
|
...mockUser,
|
|
status: 'inactive',
|
|
});
|
|
mockedBcrypt.compare.mockResolvedValue(true);
|
|
await expect(service.login(loginDto, mockTenantId)).rejects.toThrow(common_1.UnauthorizedException);
|
|
});
|
|
});
|
|
describe('logout', () => {
|
|
it('should invalidate session successfully', async () => {
|
|
sessionRepository.update.mockResolvedValue({ affected: 1 });
|
|
await service.logout(mockUser.id, 'session_token');
|
|
expect(sessionRepository.update).toHaveBeenCalledWith({ user_id: mockUser.id, session_token: 'session_token' }, { is_active: false });
|
|
});
|
|
});
|
|
describe('logoutAll', () => {
|
|
it('should invalidate all sessions for user', async () => {
|
|
sessionRepository.update.mockResolvedValue({ affected: 3 });
|
|
await service.logoutAll(mockUser.id);
|
|
expect(sessionRepository.update).toHaveBeenCalledWith({ user_id: mockUser.id }, { is_active: false });
|
|
});
|
|
});
|
|
describe('changePassword', () => {
|
|
const changePasswordDto = {
|
|
currentPassword: 'oldPassword123',
|
|
newPassword: 'newPassword456',
|
|
};
|
|
it('should change password successfully', async () => {
|
|
userRepository.findOne.mockResolvedValue(mockUser);
|
|
mockedBcrypt.compare.mockResolvedValue(true);
|
|
mockedBcrypt.hash.mockResolvedValue('new_hashed_password');
|
|
userRepository.update.mockResolvedValue({ affected: 1 });
|
|
const result = await service.changePassword(mockUser.id, changePasswordDto);
|
|
expect(result).toHaveProperty('message');
|
|
expect(userRepository.update).toHaveBeenCalled();
|
|
});
|
|
it('should throw NotFoundException if user not found', async () => {
|
|
userRepository.findOne.mockResolvedValue(null);
|
|
await expect(service.changePassword('invalid-id', changePasswordDto)).rejects.toThrow(common_1.NotFoundException);
|
|
});
|
|
it('should throw BadRequestException for incorrect current password', async () => {
|
|
userRepository.findOne.mockResolvedValue(mockUser);
|
|
mockedBcrypt.compare.mockResolvedValue(false);
|
|
await expect(service.changePassword(mockUser.id, changePasswordDto)).rejects.toThrow(common_1.BadRequestException);
|
|
});
|
|
it('should throw BadRequestException if new password same as current', async () => {
|
|
const samePasswordDto = {
|
|
currentPassword: 'samePassword',
|
|
newPassword: 'samePassword',
|
|
};
|
|
userRepository.findOne.mockResolvedValue(mockUser);
|
|
mockedBcrypt.compare.mockResolvedValue(true);
|
|
await expect(service.changePassword(mockUser.id, samePasswordDto)).rejects.toThrow(common_1.BadRequestException);
|
|
});
|
|
});
|
|
describe('requestPasswordReset', () => {
|
|
it('should create reset token for existing user', async () => {
|
|
userRepository.findOne.mockResolvedValue(mockUser);
|
|
tokenRepository.save.mockResolvedValue({});
|
|
const result = await service.requestPasswordReset(mockUser.email, mockTenantId);
|
|
expect(result).toHaveProperty('message');
|
|
expect(tokenRepository.save).toHaveBeenCalled();
|
|
});
|
|
it('should return success message even for non-existing email (security)', async () => {
|
|
userRepository.findOne.mockResolvedValue(null);
|
|
const result = await service.requestPasswordReset('nonexistent@example.com', mockTenantId);
|
|
expect(result).toHaveProperty('message');
|
|
expect(tokenRepository.save).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
describe('resetPassword', () => {
|
|
const mockToken = {
|
|
id: 'token-id',
|
|
user_id: mockUser.id,
|
|
tenant_id: mockTenantId,
|
|
token_type: 'password_reset',
|
|
is_used: false,
|
|
expires_at: new Date(Date.now() + 3600000),
|
|
};
|
|
it('should reset password successfully', async () => {
|
|
tokenRepository.findOne.mockResolvedValue(mockToken);
|
|
mockedBcrypt.hash.mockResolvedValue('new_hashed_password');
|
|
userRepository.update.mockResolvedValue({ affected: 1 });
|
|
tokenRepository.update.mockResolvedValue({ affected: 1 });
|
|
sessionRepository.update.mockResolvedValue({ affected: 1 });
|
|
const result = await service.resetPassword('valid_token', 'newPassword123', mockTenantId);
|
|
expect(result).toHaveProperty('message');
|
|
expect(userRepository.update).toHaveBeenCalled();
|
|
expect(tokenRepository.update).toHaveBeenCalled();
|
|
});
|
|
it('should throw BadRequestException for invalid token', async () => {
|
|
tokenRepository.findOne.mockResolvedValue(null);
|
|
await expect(service.resetPassword('invalid_token', 'newPassword123', mockTenantId)).rejects.toThrow(common_1.BadRequestException);
|
|
});
|
|
it('should throw BadRequestException for expired token', async () => {
|
|
tokenRepository.findOne.mockResolvedValue({
|
|
...mockToken,
|
|
expires_at: new Date(Date.now() - 3600000),
|
|
});
|
|
await expect(service.resetPassword('expired_token', 'newPassword123', mockTenantId)).rejects.toThrow(common_1.BadRequestException);
|
|
});
|
|
});
|
|
describe('verifyEmail', () => {
|
|
const mockToken = {
|
|
id: 'token-id',
|
|
user_id: mockUser.id,
|
|
tenant_id: mockTenantId,
|
|
token_type: 'email_verification',
|
|
is_used: false,
|
|
expires_at: new Date(Date.now() + 3600000),
|
|
};
|
|
it('should verify email successfully', async () => {
|
|
tokenRepository.findOne.mockResolvedValue(mockToken);
|
|
userRepository.update.mockResolvedValue({ affected: 1 });
|
|
tokenRepository.update.mockResolvedValue({ affected: 1 });
|
|
const result = await service.verifyEmail('valid_token', mockTenantId);
|
|
expect(result).toHaveProperty('message');
|
|
expect(userRepository.update).toHaveBeenCalledWith({ id: mockToken.user_id }, expect.objectContaining({
|
|
email_verified: true,
|
|
status: 'active',
|
|
}));
|
|
});
|
|
it('should throw BadRequestException for invalid token', async () => {
|
|
tokenRepository.findOne.mockResolvedValue(null);
|
|
await expect(service.verifyEmail('invalid_token', mockTenantId)).rejects.toThrow(common_1.BadRequestException);
|
|
});
|
|
});
|
|
describe('validateUser', () => {
|
|
it('should return user if active', async () => {
|
|
userRepository.findOne.mockResolvedValue(mockUser);
|
|
const result = await service.validateUser(mockUser.id);
|
|
expect(result).toEqual(mockUser);
|
|
});
|
|
it('should return null if user not found or not active', async () => {
|
|
userRepository.findOne.mockResolvedValue(null);
|
|
const result = await service.validateUser('invalid-id');
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
describe('getProfile', () => {
|
|
it('should return sanitized user profile', async () => {
|
|
userRepository.findOne.mockResolvedValue(mockUser);
|
|
const result = await service.getProfile(mockUser.id);
|
|
expect(result).not.toHaveProperty('password_hash');
|
|
expect(result).toHaveProperty('email');
|
|
});
|
|
it('should throw NotFoundException if user not found', async () => {
|
|
userRepository.findOne.mockResolvedValue(null);
|
|
await expect(service.getProfile('invalid-id')).rejects.toThrow(common_1.NotFoundException);
|
|
});
|
|
});
|
|
describe('refreshToken', () => {
|
|
const mockSession = {
|
|
id: 'session-id',
|
|
user_id: mockUser.id,
|
|
tenant_id: mockTenantId,
|
|
refresh_token_hash: 'hashed_refresh_token',
|
|
is_active: true,
|
|
expires_at: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
|
|
};
|
|
it('should refresh tokens successfully with valid refresh token', async () => {
|
|
jwtService.verify.mockReturnValue({
|
|
sub: mockUser.id,
|
|
email: mockUser.email,
|
|
tenant_id: mockTenantId,
|
|
});
|
|
configService.get.mockReturnValue('test-secret');
|
|
userRepository.findOne.mockResolvedValue(mockUser);
|
|
sessionRepository.findOne.mockResolvedValue(mockSession);
|
|
sessionRepository.update.mockResolvedValue({ affected: 1 });
|
|
jwtService.sign.mockReturnValueOnce('new_access_token').mockReturnValueOnce('new_refresh_token');
|
|
const result = await service.refreshToken('valid_refresh_token', '127.0.0.1', 'Mozilla/5.0');
|
|
expect(result).toHaveProperty('accessToken', 'new_access_token');
|
|
expect(result).toHaveProperty('refreshToken', 'new_refresh_token');
|
|
expect(jwtService.verify).toHaveBeenCalled();
|
|
expect(sessionRepository.update).toHaveBeenCalled();
|
|
});
|
|
it('should throw UnauthorizedException when user not found', async () => {
|
|
jwtService.verify.mockReturnValue({
|
|
sub: 'non-existent-user-id',
|
|
email: 'test@example.com',
|
|
tenant_id: mockTenantId,
|
|
});
|
|
configService.get.mockReturnValue('test-secret');
|
|
userRepository.findOne.mockResolvedValue(null);
|
|
await expect(service.refreshToken('valid_token_but_user_deleted')).rejects.toThrow(common_1.UnauthorizedException);
|
|
});
|
|
it('should throw UnauthorizedException when session not found', async () => {
|
|
jwtService.verify.mockReturnValue({
|
|
sub: mockUser.id,
|
|
email: mockUser.email,
|
|
tenant_id: mockTenantId,
|
|
});
|
|
configService.get.mockReturnValue('test-secret');
|
|
userRepository.findOne.mockResolvedValue(mockUser);
|
|
sessionRepository.findOne.mockResolvedValue(null);
|
|
await expect(service.refreshToken('token_with_no_session')).rejects.toThrow(common_1.UnauthorizedException);
|
|
});
|
|
it('should throw UnauthorizedException when session is expired', async () => {
|
|
const expiredSession = {
|
|
...mockSession,
|
|
expires_at: new Date(Date.now() - 3600000),
|
|
};
|
|
jwtService.verify.mockReturnValue({
|
|
sub: mockUser.id,
|
|
email: mockUser.email,
|
|
tenant_id: mockTenantId,
|
|
});
|
|
configService.get.mockReturnValue('test-secret');
|
|
userRepository.findOne.mockResolvedValue(mockUser);
|
|
sessionRepository.findOne.mockResolvedValue(expiredSession);
|
|
sessionRepository.update.mockResolvedValue({ affected: 1 });
|
|
await expect(service.refreshToken('token_with_expired_session')).rejects.toThrow(common_1.UnauthorizedException);
|
|
});
|
|
it('should throw UnauthorizedException for invalid JWT token', async () => {
|
|
jwtService.verify.mockImplementation(() => {
|
|
throw new Error('Invalid token');
|
|
});
|
|
configService.get.mockReturnValue('test-secret');
|
|
await expect(service.refreshToken('invalid_jwt_token')).rejects.toThrow(common_1.UnauthorizedException);
|
|
});
|
|
it('should deactivate expired session when detected', async () => {
|
|
const expiredSession = {
|
|
...mockSession,
|
|
expires_at: new Date(Date.now() - 3600000),
|
|
};
|
|
jwtService.verify.mockReturnValue({
|
|
sub: mockUser.id,
|
|
email: mockUser.email,
|
|
tenant_id: mockTenantId,
|
|
});
|
|
configService.get.mockReturnValue('test-secret');
|
|
userRepository.findOne.mockResolvedValue(mockUser);
|
|
sessionRepository.findOne.mockResolvedValue(expiredSession);
|
|
sessionRepository.update.mockResolvedValue({ affected: 1 });
|
|
await expect(service.refreshToken('token')).rejects.toThrow(common_1.UnauthorizedException);
|
|
expect(sessionRepository.update).toHaveBeenCalledWith({ id: expiredSession.id }, { is_active: false });
|
|
});
|
|
});
|
|
describe('verifyEmail - additional cases', () => {
|
|
const mockToken = {
|
|
id: 'token-id',
|
|
user_id: mockUser.id,
|
|
tenant_id: mockTenantId,
|
|
token_type: 'email_verification',
|
|
is_used: false,
|
|
expires_at: new Date(Date.now() + 3600000),
|
|
};
|
|
it('should throw BadRequestException for expired verification token', async () => {
|
|
tokenRepository.findOne.mockResolvedValue({
|
|
...mockToken,
|
|
expires_at: new Date(Date.now() - 3600000),
|
|
});
|
|
await expect(service.verifyEmail('expired_verification_token', mockTenantId)).rejects.toThrow(common_1.BadRequestException);
|
|
});
|
|
});
|
|
describe('register - additional cases', () => {
|
|
const registerDto = {
|
|
email: 'newuser@example.com',
|
|
password: 'SecurePass123!',
|
|
};
|
|
it('should register user with IP and userAgent metadata', async () => {
|
|
userRepository.findOne.mockResolvedValue(null);
|
|
mockedBcrypt.hash.mockResolvedValue('hashed_password');
|
|
userRepository.create.mockReturnValue({
|
|
...mockUser,
|
|
email: registerDto.email,
|
|
status: 'pending_verification',
|
|
});
|
|
userRepository.save.mockResolvedValue({
|
|
...mockUser,
|
|
email: registerDto.email,
|
|
status: 'pending_verification',
|
|
});
|
|
sessionRepository.save.mockResolvedValue({});
|
|
tokenRepository.save.mockResolvedValue({});
|
|
jwtService.sign.mockReturnValueOnce('access_token').mockReturnValueOnce('refresh_token');
|
|
configService.get.mockReturnValue('15m');
|
|
const ip = '192.168.1.100';
|
|
const userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)';
|
|
const result = await service.register(registerDto, mockTenantId, ip, userAgent);
|
|
expect(result).toHaveProperty('user');
|
|
expect(result).toHaveProperty('accessToken');
|
|
expect(sessionRepository.save).toHaveBeenCalledWith(expect.objectContaining({
|
|
ip_address: ip,
|
|
user_agent: userAgent,
|
|
}));
|
|
});
|
|
it('should register user without optional fields', async () => {
|
|
userRepository.findOne.mockResolvedValue(null);
|
|
mockedBcrypt.hash.mockResolvedValue('hashed_password');
|
|
userRepository.create.mockReturnValue({
|
|
...mockUser,
|
|
email: registerDto.email,
|
|
first_name: null,
|
|
last_name: null,
|
|
phone: null,
|
|
status: 'pending_verification',
|
|
});
|
|
userRepository.save.mockResolvedValue({
|
|
...mockUser,
|
|
email: registerDto.email,
|
|
status: 'pending_verification',
|
|
});
|
|
sessionRepository.save.mockResolvedValue({});
|
|
tokenRepository.save.mockResolvedValue({});
|
|
jwtService.sign.mockReturnValueOnce('access_token').mockReturnValueOnce('refresh_token');
|
|
configService.get.mockReturnValue('15m');
|
|
const result = await service.register(registerDto, mockTenantId);
|
|
expect(result).toHaveProperty('user');
|
|
expect(userRepository.create).toHaveBeenCalledWith(expect.objectContaining({
|
|
first_name: null,
|
|
last_name: null,
|
|
phone: null,
|
|
}));
|
|
});
|
|
it('should create email verification token on registration', async () => {
|
|
userRepository.findOne.mockResolvedValue(null);
|
|
mockedBcrypt.hash.mockResolvedValue('hashed_password');
|
|
userRepository.create.mockReturnValue({
|
|
...mockUser,
|
|
email: registerDto.email,
|
|
status: 'pending_verification',
|
|
});
|
|
userRepository.save.mockResolvedValue({
|
|
...mockUser,
|
|
email: registerDto.email,
|
|
status: 'pending_verification',
|
|
});
|
|
sessionRepository.save.mockResolvedValue({});
|
|
tokenRepository.save.mockResolvedValue({});
|
|
jwtService.sign.mockReturnValueOnce('access_token').mockReturnValueOnce('refresh_token');
|
|
configService.get.mockReturnValue('15m');
|
|
await service.register(registerDto, mockTenantId);
|
|
expect(tokenRepository.save).toHaveBeenCalledWith(expect.objectContaining({
|
|
token_type: 'email_verification',
|
|
}));
|
|
});
|
|
});
|
|
describe('login - additional cases', () => {
|
|
const loginDto = {
|
|
email: 'test@example.com',
|
|
password: 'password123',
|
|
};
|
|
it('should update last_login_at and last_login_ip on successful login', async () => {
|
|
userRepository.findOne.mockResolvedValue(mockUser);
|
|
mockedBcrypt.compare.mockResolvedValue(true);
|
|
userRepository.save.mockResolvedValue(mockUser);
|
|
sessionRepository.save.mockResolvedValue({});
|
|
jwtService.sign.mockReturnValueOnce('access_token').mockReturnValueOnce('refresh_token');
|
|
configService.get.mockReturnValue('15m');
|
|
const ip = '10.0.0.1';
|
|
await service.login(loginDto, mockTenantId, ip);
|
|
expect(userRepository.save).toHaveBeenCalledWith(expect.objectContaining({
|
|
last_login_ip: ip,
|
|
}));
|
|
});
|
|
it('should detect device type from userAgent - mobile', async () => {
|
|
userRepository.findOne.mockResolvedValue(mockUser);
|
|
mockedBcrypt.compare.mockResolvedValue(true);
|
|
userRepository.save.mockResolvedValue(mockUser);
|
|
sessionRepository.save.mockResolvedValue({});
|
|
jwtService.sign.mockReturnValueOnce('access_token').mockReturnValueOnce('refresh_token');
|
|
configService.get.mockReturnValue('15m');
|
|
await service.login(loginDto, mockTenantId, '127.0.0.1', 'Mozilla/5.0 (iPhone; CPU iPhone OS)');
|
|
expect(sessionRepository.save).toHaveBeenCalledWith(expect.objectContaining({
|
|
device_type: 'mobile',
|
|
}));
|
|
});
|
|
it('should detect device type from userAgent - tablet', async () => {
|
|
userRepository.findOne.mockResolvedValue(mockUser);
|
|
mockedBcrypt.compare.mockResolvedValue(true);
|
|
userRepository.save.mockResolvedValue(mockUser);
|
|
sessionRepository.save.mockResolvedValue({});
|
|
jwtService.sign.mockReturnValueOnce('access_token').mockReturnValueOnce('refresh_token');
|
|
configService.get.mockReturnValue('15m');
|
|
await service.login(loginDto, mockTenantId, '127.0.0.1', 'Mozilla/5.0 (iPad; CPU OS)');
|
|
expect(sessionRepository.save).toHaveBeenCalledWith(expect.objectContaining({
|
|
device_type: 'tablet',
|
|
}));
|
|
});
|
|
it('should detect device type from userAgent - desktop', async () => {
|
|
userRepository.findOne.mockResolvedValue(mockUser);
|
|
mockedBcrypt.compare.mockResolvedValue(true);
|
|
userRepository.save.mockResolvedValue(mockUser);
|
|
sessionRepository.save.mockResolvedValue({});
|
|
jwtService.sign.mockReturnValueOnce('access_token').mockReturnValueOnce('refresh_token');
|
|
configService.get.mockReturnValue('15m');
|
|
await service.login(loginDto, mockTenantId, '127.0.0.1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)');
|
|
expect(sessionRepository.save).toHaveBeenCalledWith(expect.objectContaining({
|
|
device_type: 'desktop',
|
|
}));
|
|
});
|
|
it('should detect device type as unknown when no userAgent', async () => {
|
|
userRepository.findOne.mockResolvedValue(mockUser);
|
|
mockedBcrypt.compare.mockResolvedValue(true);
|
|
userRepository.save.mockResolvedValue(mockUser);
|
|
sessionRepository.save.mockResolvedValue({});
|
|
jwtService.sign.mockReturnValueOnce('access_token').mockReturnValueOnce('refresh_token');
|
|
configService.get.mockReturnValue('15m');
|
|
await service.login(loginDto, mockTenantId, '127.0.0.1');
|
|
expect(sessionRepository.save).toHaveBeenCalledWith(expect.objectContaining({
|
|
device_type: 'unknown',
|
|
}));
|
|
});
|
|
it('should login with Android device', async () => {
|
|
userRepository.findOne.mockResolvedValue(mockUser);
|
|
mockedBcrypt.compare.mockResolvedValue(true);
|
|
userRepository.save.mockResolvedValue(mockUser);
|
|
sessionRepository.save.mockResolvedValue({});
|
|
jwtService.sign.mockReturnValueOnce('access_token').mockReturnValueOnce('refresh_token');
|
|
configService.get.mockReturnValue('15m');
|
|
await service.login(loginDto, mockTenantId, '127.0.0.1', 'Mozilla/5.0 (Linux; Android 10)');
|
|
expect(sessionRepository.save).toHaveBeenCalledWith(expect.objectContaining({
|
|
device_type: 'mobile',
|
|
}));
|
|
});
|
|
});
|
|
describe('requestPasswordReset - additional cases', () => {
|
|
it('should create token with correct expiry time (1 hour)', async () => {
|
|
const beforeCall = Date.now();
|
|
userRepository.findOne.mockResolvedValue(mockUser);
|
|
tokenRepository.save.mockResolvedValue({});
|
|
await service.requestPasswordReset(mockUser.email, mockTenantId);
|
|
const savedToken = tokenRepository.save.mock.calls[0][0];
|
|
expect(savedToken.expires_at).toBeDefined();
|
|
const expiryTime = savedToken.expires_at.getTime();
|
|
const expectedMinExpiry = beforeCall + 60 * 60 * 1000 - 1000;
|
|
const expectedMaxExpiry = beforeCall + 60 * 60 * 1000 + 1000;
|
|
expect(expiryTime).toBeGreaterThanOrEqual(expectedMinExpiry);
|
|
expect(expiryTime).toBeLessThanOrEqual(expectedMaxExpiry);
|
|
expect(savedToken.token_type).toBe('password_reset');
|
|
});
|
|
});
|
|
describe('resetPassword - additional cases', () => {
|
|
const mockToken = {
|
|
id: 'token-id',
|
|
user_id: mockUser.id,
|
|
tenant_id: mockTenantId,
|
|
token_type: 'password_reset',
|
|
is_used: false,
|
|
expires_at: new Date(Date.now() + 3600000),
|
|
};
|
|
it('should invalidate all sessions after password reset', async () => {
|
|
tokenRepository.findOne.mockResolvedValue(mockToken);
|
|
mockedBcrypt.hash.mockResolvedValue('new_hashed_password');
|
|
userRepository.update.mockResolvedValue({ affected: 1 });
|
|
tokenRepository.update.mockResolvedValue({ affected: 1 });
|
|
sessionRepository.update.mockResolvedValue({ affected: 3 });
|
|
await service.resetPassword('valid_token', 'newPassword123', mockTenantId);
|
|
expect(sessionRepository.update).toHaveBeenCalledWith({ user_id: mockToken.user_id }, { is_active: false });
|
|
});
|
|
it('should mark token as used with timestamp', async () => {
|
|
const beforeCall = Date.now();
|
|
tokenRepository.findOne.mockResolvedValue(mockToken);
|
|
mockedBcrypt.hash.mockResolvedValue('new_hashed_password');
|
|
userRepository.update.mockResolvedValue({ affected: 1 });
|
|
tokenRepository.update.mockResolvedValue({ affected: 1 });
|
|
sessionRepository.update.mockResolvedValue({ affected: 1 });
|
|
await service.resetPassword('valid_token', 'newPassword123', mockTenantId);
|
|
expect(tokenRepository.update).toHaveBeenCalledWith({ id: mockToken.id }, expect.objectContaining({
|
|
is_used: true,
|
|
}));
|
|
const updateCall = tokenRepository.update.mock.calls[0][1];
|
|
expect(updateCall.used_at).toBeDefined();
|
|
expect(updateCall.used_at.getTime()).toBeGreaterThanOrEqual(beforeCall);
|
|
});
|
|
});
|
|
describe('validateUser - additional cases', () => {
|
|
it('should query for active users only', async () => {
|
|
userRepository.findOne.mockResolvedValue(mockUser);
|
|
await service.validateUser(mockUser.id);
|
|
expect(userRepository.findOne).toHaveBeenCalledWith({
|
|
where: { id: mockUser.id, status: 'active' },
|
|
});
|
|
});
|
|
});
|
|
});
|
|
//# sourceMappingURL=auth.service.spec.js.map
|