template-saas/apps/backend/dist/modules/auth/__tests__/auth.service.spec.js
rckrdmrd 26f0e52ca7 feat: Initial commit - template-saas
Template base para proyectos SaaS multi-tenant.

Estructura inicial:
- apps/backend (NestJS API)
- apps/frontend (React/Vite)
- apps/database (PostgreSQL DDL)
- docs/ (Documentación)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 04:41:24 -06:00

337 lines
16 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);
});
});
});
//# sourceMappingURL=auth.service.spec.js.map