- HERENCIA-SIMCO.md actualizado con directivas v3.7 y v3.8 - Actualizaciones de configuracion Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
427 lines
20 KiB
JavaScript
427 lines
20 KiB
JavaScript
"use strict";
|
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
const testing_1 = require("@nestjs/testing");
|
|
const typeorm_1 = require("@nestjs/typeorm");
|
|
const audit_service_1 = require("../services/audit.service");
|
|
const audit_log_entity_1 = require("../entities/audit-log.entity");
|
|
const activity_log_entity_1 = require("../entities/activity-log.entity");
|
|
describe('AuditService', () => {
|
|
let service;
|
|
let auditLogRepo;
|
|
let activityLogRepo;
|
|
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
|
|
const mockUserId = '550e8400-e29b-41d4-a716-446655440002';
|
|
const mockAuditLog = {
|
|
id: 'audit-001',
|
|
tenant_id: mockTenantId,
|
|
user_id: mockUserId,
|
|
action: audit_log_entity_1.AuditAction.CREATE,
|
|
entity_type: 'user',
|
|
entity_id: 'user-001',
|
|
new_values: { email: 'test@example.com' },
|
|
changed_fields: ['email'],
|
|
ip_address: '192.168.1.1',
|
|
created_at: new Date(),
|
|
};
|
|
const mockActivityLog = {
|
|
id: 'activity-001',
|
|
tenant_id: mockTenantId,
|
|
user_id: mockUserId,
|
|
activity_type: activity_log_entity_1.ActivityType.PAGE_VIEW,
|
|
resource_type: 'dashboard',
|
|
description: 'Viewed dashboard',
|
|
ip_address: '192.168.1.1',
|
|
created_at: new Date(),
|
|
};
|
|
beforeEach(async () => {
|
|
const mockAuditLogRepo = {
|
|
create: jest.fn(),
|
|
save: jest.fn(),
|
|
findOne: jest.fn(),
|
|
find: jest.fn(),
|
|
count: jest.fn(),
|
|
createQueryBuilder: jest.fn(),
|
|
};
|
|
const mockActivityLogRepo = {
|
|
create: jest.fn(),
|
|
save: jest.fn(),
|
|
findOne: jest.fn(),
|
|
find: jest.fn(),
|
|
createQueryBuilder: jest.fn(),
|
|
};
|
|
const module = await testing_1.Test.createTestingModule({
|
|
providers: [
|
|
audit_service_1.AuditService,
|
|
{ provide: (0, typeorm_1.getRepositoryToken)(audit_log_entity_1.AuditLog), useValue: mockAuditLogRepo },
|
|
{ provide: (0, typeorm_1.getRepositoryToken)(activity_log_entity_1.ActivityLog), useValue: mockActivityLogRepo },
|
|
],
|
|
}).compile();
|
|
service = module.get(audit_service_1.AuditService);
|
|
auditLogRepo = module.get((0, typeorm_1.getRepositoryToken)(audit_log_entity_1.AuditLog));
|
|
activityLogRepo = module.get((0, typeorm_1.getRepositoryToken)(activity_log_entity_1.ActivityLog));
|
|
});
|
|
afterEach(() => {
|
|
jest.clearAllMocks();
|
|
});
|
|
describe('createAuditLog', () => {
|
|
it('should create audit log successfully', async () => {
|
|
auditLogRepo.create.mockReturnValue(mockAuditLog);
|
|
auditLogRepo.save.mockResolvedValue(mockAuditLog);
|
|
const result = await service.createAuditLog({
|
|
tenant_id: mockTenantId,
|
|
user_id: mockUserId,
|
|
action: audit_log_entity_1.AuditAction.CREATE,
|
|
entity_type: 'user',
|
|
entity_id: 'user-001',
|
|
new_values: { email: 'test@example.com' },
|
|
});
|
|
expect(result).toEqual(mockAuditLog);
|
|
expect(auditLogRepo.create).toHaveBeenCalled();
|
|
expect(auditLogRepo.save).toHaveBeenCalled();
|
|
});
|
|
it('should detect changed fields', async () => {
|
|
auditLogRepo.create.mockReturnValue(mockAuditLog);
|
|
auditLogRepo.save.mockResolvedValue(mockAuditLog);
|
|
await service.createAuditLog({
|
|
tenant_id: mockTenantId,
|
|
user_id: mockUserId,
|
|
action: audit_log_entity_1.AuditAction.UPDATE,
|
|
entity_type: 'user',
|
|
entity_id: 'user-001',
|
|
old_values: { email: 'old@example.com', name: 'Old Name' },
|
|
new_values: { email: 'new@example.com', name: 'Old Name' },
|
|
});
|
|
expect(auditLogRepo.create).toHaveBeenCalledWith(expect.objectContaining({
|
|
changed_fields: ['email'],
|
|
}));
|
|
});
|
|
it('should handle null old/new values', async () => {
|
|
auditLogRepo.create.mockReturnValue(mockAuditLog);
|
|
auditLogRepo.save.mockResolvedValue(mockAuditLog);
|
|
await service.createAuditLog({
|
|
tenant_id: mockTenantId,
|
|
action: audit_log_entity_1.AuditAction.DELETE,
|
|
entity_type: 'user',
|
|
entity_id: 'user-001',
|
|
});
|
|
expect(auditLogRepo.create).toHaveBeenCalledWith(expect.objectContaining({
|
|
changed_fields: [],
|
|
}));
|
|
});
|
|
it('should include request metadata', async () => {
|
|
auditLogRepo.create.mockReturnValue(mockAuditLog);
|
|
auditLogRepo.save.mockResolvedValue(mockAuditLog);
|
|
await service.createAuditLog({
|
|
tenant_id: mockTenantId,
|
|
user_id: mockUserId,
|
|
action: audit_log_entity_1.AuditAction.READ,
|
|
entity_type: 'document',
|
|
ip_address: '192.168.1.1',
|
|
user_agent: 'Mozilla/5.0',
|
|
endpoint: '/api/documents/1',
|
|
http_method: 'GET',
|
|
response_status: 200,
|
|
duration_ms: 150,
|
|
});
|
|
expect(auditLogRepo.create).toHaveBeenCalledWith(expect.objectContaining({
|
|
ip_address: '192.168.1.1',
|
|
endpoint: '/api/documents/1',
|
|
duration_ms: 150,
|
|
}));
|
|
});
|
|
});
|
|
describe('queryAuditLogs', () => {
|
|
it('should return paginated audit logs', async () => {
|
|
const qb = {
|
|
where: jest.fn().mockReturnThis(),
|
|
andWhere: jest.fn().mockReturnThis(),
|
|
orderBy: jest.fn().mockReturnThis(),
|
|
skip: jest.fn().mockReturnThis(),
|
|
take: jest.fn().mockReturnThis(),
|
|
getManyAndCount: jest.fn().mockResolvedValue([[mockAuditLog], 1]),
|
|
};
|
|
auditLogRepo.createQueryBuilder.mockReturnValue(qb);
|
|
const result = await service.queryAuditLogs(mockTenantId, {});
|
|
expect(result.data).toHaveLength(1);
|
|
expect(result.total).toBe(1);
|
|
expect(result.page).toBe(1);
|
|
expect(result.limit).toBe(20);
|
|
});
|
|
it('should filter by user_id', async () => {
|
|
const qb = {
|
|
where: jest.fn().mockReturnThis(),
|
|
andWhere: jest.fn().mockReturnThis(),
|
|
orderBy: jest.fn().mockReturnThis(),
|
|
skip: jest.fn().mockReturnThis(),
|
|
take: jest.fn().mockReturnThis(),
|
|
getManyAndCount: jest.fn().mockResolvedValue([[], 0]),
|
|
};
|
|
auditLogRepo.createQueryBuilder.mockReturnValue(qb);
|
|
await service.queryAuditLogs(mockTenantId, { user_id: mockUserId });
|
|
expect(qb.andWhere).toHaveBeenCalledWith('audit.user_id = :user_id', {
|
|
user_id: mockUserId,
|
|
});
|
|
});
|
|
it('should filter by action', async () => {
|
|
const qb = {
|
|
where: jest.fn().mockReturnThis(),
|
|
andWhere: jest.fn().mockReturnThis(),
|
|
orderBy: jest.fn().mockReturnThis(),
|
|
skip: jest.fn().mockReturnThis(),
|
|
take: jest.fn().mockReturnThis(),
|
|
getManyAndCount: jest.fn().mockResolvedValue([[], 0]),
|
|
};
|
|
auditLogRepo.createQueryBuilder.mockReturnValue(qb);
|
|
await service.queryAuditLogs(mockTenantId, { action: audit_log_entity_1.AuditAction.CREATE });
|
|
expect(qb.andWhere).toHaveBeenCalledWith('audit.action = :action', {
|
|
action: audit_log_entity_1.AuditAction.CREATE,
|
|
});
|
|
});
|
|
it('should filter by entity_type', async () => {
|
|
const qb = {
|
|
where: jest.fn().mockReturnThis(),
|
|
andWhere: jest.fn().mockReturnThis(),
|
|
orderBy: jest.fn().mockReturnThis(),
|
|
skip: jest.fn().mockReturnThis(),
|
|
take: jest.fn().mockReturnThis(),
|
|
getManyAndCount: jest.fn().mockResolvedValue([[], 0]),
|
|
};
|
|
auditLogRepo.createQueryBuilder.mockReturnValue(qb);
|
|
await service.queryAuditLogs(mockTenantId, { entity_type: 'user' });
|
|
expect(qb.andWhere).toHaveBeenCalledWith('audit.entity_type = :entity_type', {
|
|
entity_type: 'user',
|
|
});
|
|
});
|
|
it('should filter by date range', async () => {
|
|
const qb = {
|
|
where: jest.fn().mockReturnThis(),
|
|
andWhere: jest.fn().mockReturnThis(),
|
|
orderBy: jest.fn().mockReturnThis(),
|
|
skip: jest.fn().mockReturnThis(),
|
|
take: jest.fn().mockReturnThis(),
|
|
getManyAndCount: jest.fn().mockResolvedValue([[], 0]),
|
|
};
|
|
auditLogRepo.createQueryBuilder.mockReturnValue(qb);
|
|
const from_date = '2026-01-01';
|
|
const to_date = '2026-01-31';
|
|
await service.queryAuditLogs(mockTenantId, { from_date, to_date });
|
|
expect(qb.andWhere).toHaveBeenCalledWith('audit.created_at BETWEEN :from_date AND :to_date', { from_date, to_date });
|
|
});
|
|
it('should handle pagination correctly', async () => {
|
|
const qb = {
|
|
where: jest.fn().mockReturnThis(),
|
|
andWhere: jest.fn().mockReturnThis(),
|
|
orderBy: jest.fn().mockReturnThis(),
|
|
skip: jest.fn().mockReturnThis(),
|
|
take: jest.fn().mockReturnThis(),
|
|
getManyAndCount: jest.fn().mockResolvedValue([[], 100]),
|
|
};
|
|
auditLogRepo.createQueryBuilder.mockReturnValue(qb);
|
|
const result = await service.queryAuditLogs(mockTenantId, {
|
|
page: 3,
|
|
limit: 10,
|
|
});
|
|
expect(qb.skip).toHaveBeenCalledWith(20);
|
|
expect(qb.take).toHaveBeenCalledWith(10);
|
|
expect(result.totalPages).toBe(10);
|
|
});
|
|
});
|
|
describe('getAuditLogById', () => {
|
|
it('should return audit log by id', async () => {
|
|
auditLogRepo.findOne.mockResolvedValue(mockAuditLog);
|
|
const result = await service.getAuditLogById(mockTenantId, 'audit-001');
|
|
expect(result).toEqual(mockAuditLog);
|
|
expect(auditLogRepo.findOne).toHaveBeenCalledWith({
|
|
where: { id: 'audit-001', tenant_id: mockTenantId },
|
|
});
|
|
});
|
|
it('should return null when not found', async () => {
|
|
auditLogRepo.findOne.mockResolvedValue(null);
|
|
const result = await service.getAuditLogById(mockTenantId, 'invalid');
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
describe('getEntityAuditHistory', () => {
|
|
it('should return audit history for entity', async () => {
|
|
auditLogRepo.find.mockResolvedValue([mockAuditLog]);
|
|
const result = await service.getEntityAuditHistory(mockTenantId, 'user', 'user-001');
|
|
expect(result).toHaveLength(1);
|
|
expect(auditLogRepo.find).toHaveBeenCalledWith({
|
|
where: {
|
|
tenant_id: mockTenantId,
|
|
entity_type: 'user',
|
|
entity_id: 'user-001',
|
|
},
|
|
order: { created_at: 'DESC' },
|
|
});
|
|
});
|
|
it('should return empty array for no history', async () => {
|
|
auditLogRepo.find.mockResolvedValue([]);
|
|
const result = await service.getEntityAuditHistory(mockTenantId, 'document', 'doc-999');
|
|
expect(result).toHaveLength(0);
|
|
});
|
|
});
|
|
describe('createActivityLog', () => {
|
|
it('should create activity log successfully', async () => {
|
|
activityLogRepo.create.mockReturnValue(mockActivityLog);
|
|
activityLogRepo.save.mockResolvedValue(mockActivityLog);
|
|
const result = await service.createActivityLog(mockTenantId, mockUserId, {
|
|
activity_type: activity_log_entity_1.ActivityType.PAGE_VIEW,
|
|
resource_type: 'dashboard',
|
|
description: 'Viewed dashboard',
|
|
}, { ip_address: '192.168.1.1' });
|
|
expect(result).toEqual(mockActivityLog);
|
|
expect(activityLogRepo.create).toHaveBeenCalled();
|
|
});
|
|
it('should include session context', async () => {
|
|
activityLogRepo.create.mockReturnValue(mockActivityLog);
|
|
activityLogRepo.save.mockResolvedValue(mockActivityLog);
|
|
await service.createActivityLog(mockTenantId, mockUserId, {
|
|
activity_type: activity_log_entity_1.ActivityType.FEATURE_USE,
|
|
description: 'Used export feature',
|
|
}, {
|
|
ip_address: '192.168.1.1',
|
|
user_agent: 'Mozilla/5.0',
|
|
session_id: 'session-001',
|
|
});
|
|
expect(activityLogRepo.create).toHaveBeenCalledWith(expect.objectContaining({
|
|
session_id: 'session-001',
|
|
}));
|
|
});
|
|
});
|
|
describe('queryActivityLogs', () => {
|
|
it('should return paginated activity logs', async () => {
|
|
const qb = {
|
|
where: jest.fn().mockReturnThis(),
|
|
andWhere: jest.fn().mockReturnThis(),
|
|
orderBy: jest.fn().mockReturnThis(),
|
|
skip: jest.fn().mockReturnThis(),
|
|
take: jest.fn().mockReturnThis(),
|
|
getManyAndCount: jest
|
|
.fn()
|
|
.mockResolvedValue([[mockActivityLog], 1]),
|
|
};
|
|
activityLogRepo.createQueryBuilder.mockReturnValue(qb);
|
|
const result = await service.queryActivityLogs(mockTenantId, {});
|
|
expect(result.data).toHaveLength(1);
|
|
expect(result.total).toBe(1);
|
|
});
|
|
it('should filter by activity_type', async () => {
|
|
const qb = {
|
|
where: jest.fn().mockReturnThis(),
|
|
andWhere: jest.fn().mockReturnThis(),
|
|
orderBy: jest.fn().mockReturnThis(),
|
|
skip: jest.fn().mockReturnThis(),
|
|
take: jest.fn().mockReturnThis(),
|
|
getManyAndCount: jest.fn().mockResolvedValue([[], 0]),
|
|
};
|
|
activityLogRepo.createQueryBuilder.mockReturnValue(qb);
|
|
await service.queryActivityLogs(mockTenantId, {
|
|
activity_type: activity_log_entity_1.ActivityType.PAGE_VIEW,
|
|
});
|
|
expect(qb.andWhere).toHaveBeenCalledWith('activity.activity_type = :activity_type', { activity_type: activity_log_entity_1.ActivityType.PAGE_VIEW });
|
|
});
|
|
it('should filter by resource_type', async () => {
|
|
const qb = {
|
|
where: jest.fn().mockReturnThis(),
|
|
andWhere: jest.fn().mockReturnThis(),
|
|
orderBy: jest.fn().mockReturnThis(),
|
|
skip: jest.fn().mockReturnThis(),
|
|
take: jest.fn().mockReturnThis(),
|
|
getManyAndCount: jest.fn().mockResolvedValue([[], 0]),
|
|
};
|
|
activityLogRepo.createQueryBuilder.mockReturnValue(qb);
|
|
await service.queryActivityLogs(mockTenantId, { resource_type: 'document' });
|
|
expect(qb.andWhere).toHaveBeenCalledWith('activity.resource_type = :resource_type', { resource_type: 'document' });
|
|
});
|
|
});
|
|
describe('getUserActivitySummary', () => {
|
|
it('should return activity summary by type', async () => {
|
|
const qb = {
|
|
select: jest.fn().mockReturnThis(),
|
|
addSelect: jest.fn().mockReturnThis(),
|
|
where: jest.fn().mockReturnThis(),
|
|
andWhere: jest.fn().mockReturnThis(),
|
|
groupBy: jest.fn().mockReturnThis(),
|
|
getRawMany: jest.fn().mockResolvedValue([
|
|
{ activity_type: activity_log_entity_1.ActivityType.PAGE_VIEW, count: '50' },
|
|
{ activity_type: activity_log_entity_1.ActivityType.FEATURE_USE, count: '25' },
|
|
]),
|
|
};
|
|
activityLogRepo.createQueryBuilder.mockReturnValue(qb);
|
|
const result = await service.getUserActivitySummary(mockTenantId, mockUserId, 30);
|
|
expect(result).toHaveLength(2);
|
|
expect(result[0]).toEqual({
|
|
activity_type: activity_log_entity_1.ActivityType.PAGE_VIEW,
|
|
count: 50,
|
|
});
|
|
});
|
|
it('should use default 30 days', async () => {
|
|
const qb = {
|
|
select: jest.fn().mockReturnThis(),
|
|
addSelect: jest.fn().mockReturnThis(),
|
|
where: jest.fn().mockReturnThis(),
|
|
andWhere: jest.fn().mockReturnThis(),
|
|
groupBy: jest.fn().mockReturnThis(),
|
|
getRawMany: jest.fn().mockResolvedValue([]),
|
|
};
|
|
activityLogRepo.createQueryBuilder.mockReturnValue(qb);
|
|
await service.getUserActivitySummary(mockTenantId, mockUserId);
|
|
expect(qb.andWhere).toHaveBeenCalledWith('activity.created_at >= :fromDate', expect.any(Object));
|
|
});
|
|
});
|
|
describe('getAuditStats', () => {
|
|
it('should return audit statistics', async () => {
|
|
auditLogRepo.count.mockResolvedValue(100);
|
|
const actionsByTypeQb = {
|
|
select: jest.fn().mockReturnThis(),
|
|
addSelect: jest.fn().mockReturnThis(),
|
|
where: jest.fn().mockReturnThis(),
|
|
andWhere: jest.fn().mockReturnThis(),
|
|
groupBy: jest.fn().mockReturnThis(),
|
|
getRawMany: jest.fn().mockResolvedValue([
|
|
{ action: audit_log_entity_1.AuditAction.CREATE, count: '30' },
|
|
{ action: audit_log_entity_1.AuditAction.UPDATE, count: '50' },
|
|
{ action: audit_log_entity_1.AuditAction.DELETE, count: '20' },
|
|
]),
|
|
};
|
|
const topUsersQb = {
|
|
select: jest.fn().mockReturnThis(),
|
|
addSelect: jest.fn().mockReturnThis(),
|
|
where: jest.fn().mockReturnThis(),
|
|
andWhere: jest.fn().mockReturnThis(),
|
|
groupBy: jest.fn().mockReturnThis(),
|
|
orderBy: jest.fn().mockReturnThis(),
|
|
limit: jest.fn().mockReturnThis(),
|
|
getRawMany: jest.fn().mockResolvedValue([
|
|
{ user_id: mockUserId, count: '45' },
|
|
{ user_id: 'user-002', count: '30' },
|
|
]),
|
|
};
|
|
auditLogRepo.createQueryBuilder
|
|
.mockReturnValueOnce(actionsByTypeQb)
|
|
.mockReturnValueOnce(topUsersQb);
|
|
const result = await service.getAuditStats(mockTenantId, 7);
|
|
expect(result.total_actions).toBe(100);
|
|
expect(result.actions_by_type).toHaveLength(3);
|
|
expect(result.top_users).toHaveLength(2);
|
|
});
|
|
it('should use default 7 days', async () => {
|
|
auditLogRepo.count.mockResolvedValue(0);
|
|
const qb = {
|
|
select: jest.fn().mockReturnThis(),
|
|
addSelect: jest.fn().mockReturnThis(),
|
|
where: jest.fn().mockReturnThis(),
|
|
andWhere: jest.fn().mockReturnThis(),
|
|
groupBy: jest.fn().mockReturnThis(),
|
|
orderBy: jest.fn().mockReturnThis(),
|
|
limit: jest.fn().mockReturnThis(),
|
|
getRawMany: jest.fn().mockResolvedValue([]),
|
|
};
|
|
auditLogRepo.createQueryBuilder.mockReturnValue(qb);
|
|
await service.getAuditStats(mockTenantId);
|
|
expect(auditLogRepo.count).toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|
|
//# sourceMappingURL=audit.service.spec.js.map
|