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>
337 lines
16 KiB
JavaScript
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
|