template-saas/apps/backend/dist/modules/feature-flags/__tests__/feature-flags.service.spec.js
rckrdmrd 50a821a415
Some checks failed
CI / Backend CI (push) Has been cancelled
CI / Frontend CI (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / CI Summary (push) Has been cancelled
[SIMCO-V38] feat: Actualizar a SIMCO v3.8.0
- HERENCIA-SIMCO.md actualizado con directivas v3.7 y v3.8
- Actualizaciones de configuracion

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 08:53:08 -06:00

448 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 common_1 = require("@nestjs/common");
const feature_flags_service_1 = require("../services/feature-flags.service");
const feature_flag_entity_1 = require("../entities/feature-flag.entity");
const tenant_flag_entity_1 = require("../entities/tenant-flag.entity");
const user_flag_entity_1 = require("../entities/user-flag.entity");
describe('FeatureFlagsService', () => {
let service;
let flagRepository;
let tenantFlagRepository;
let userFlagRepository;
const mockTenantId = '550e8400-e29b-41d4-a716-446655440001';
const mockUserId = '550e8400-e29b-41d4-a716-446655440000';
const mockFlag = {
id: 'flag-001',
key: 'new_feature',
name: 'New Feature',
description: 'Test feature flag',
flag_type: feature_flag_entity_1.FlagType.BOOLEAN,
scope: feature_flag_entity_1.FlagScope.GLOBAL,
is_enabled: true,
default_value: true,
category: 'features',
rollout_percentage: undefined,
};
const mockTenantFlag = {
id: 'tf-001',
tenant_id: mockTenantId,
flag_id: 'flag-001',
is_enabled: false,
value: null,
};
const mockUserFlag = {
id: 'uf-001',
tenant_id: mockTenantId,
user_id: mockUserId,
flag_id: 'flag-001',
is_enabled: true,
value: { customSetting: true },
};
beforeEach(async () => {
const mockFlagRepo = {
create: jest.fn(),
save: jest.fn(),
findOne: jest.fn(),
find: jest.fn(),
remove: jest.fn(),
};
const mockTenantFlagRepo = {
create: jest.fn(),
save: jest.fn(),
findOne: jest.fn(),
find: jest.fn(),
remove: jest.fn(),
};
const mockUserFlagRepo = {
create: jest.fn(),
save: jest.fn(),
findOne: jest.fn(),
find: jest.fn(),
remove: jest.fn(),
};
const module = await testing_1.Test.createTestingModule({
providers: [
feature_flags_service_1.FeatureFlagsService,
{ provide: (0, typeorm_1.getRepositoryToken)(feature_flag_entity_1.FeatureFlag), useValue: mockFlagRepo },
{ provide: (0, typeorm_1.getRepositoryToken)(tenant_flag_entity_1.TenantFlag), useValue: mockTenantFlagRepo },
{ provide: (0, typeorm_1.getRepositoryToken)(user_flag_entity_1.UserFlag), useValue: mockUserFlagRepo },
],
}).compile();
service = module.get(feature_flags_service_1.FeatureFlagsService);
flagRepository = module.get((0, typeorm_1.getRepositoryToken)(feature_flag_entity_1.FeatureFlag));
tenantFlagRepository = module.get((0, typeorm_1.getRepositoryToken)(tenant_flag_entity_1.TenantFlag));
userFlagRepository = module.get((0, typeorm_1.getRepositoryToken)(user_flag_entity_1.UserFlag));
});
afterEach(() => {
jest.clearAllMocks();
});
describe('createFlag', () => {
const createDto = {
key: 'new_feature',
name: 'New Feature',
description: 'A new feature flag',
flag_type: feature_flag_entity_1.FlagType.BOOLEAN,
scope: feature_flag_entity_1.FlagScope.GLOBAL,
default_value: true,
};
it('should create a new flag successfully', async () => {
flagRepository.findOne.mockResolvedValue(null);
flagRepository.create.mockReturnValue(mockFlag);
flagRepository.save.mockResolvedValue(mockFlag);
const result = await service.createFlag(createDto);
expect(result).toHaveProperty('key', 'new_feature');
expect(flagRepository.create).toHaveBeenCalledWith(createDto);
expect(flagRepository.save).toHaveBeenCalled();
});
it('should throw ConflictException if key already exists', async () => {
flagRepository.findOne.mockResolvedValue(mockFlag);
await expect(service.createFlag(createDto)).rejects.toThrow(common_1.ConflictException);
});
});
describe('updateFlag', () => {
const updateDto = {
name: 'Updated Feature',
is_enabled: false,
};
it('should update flag successfully', async () => {
flagRepository.findOne.mockResolvedValue(mockFlag);
flagRepository.save.mockResolvedValue({
...mockFlag,
...updateDto,
});
const result = await service.updateFlag('flag-001', updateDto);
expect(result.name).toBe('Updated Feature');
expect(result.is_enabled).toBe(false);
});
it('should throw NotFoundException for invalid flag ID', async () => {
flagRepository.findOne.mockResolvedValue(null);
await expect(service.updateFlag('invalid-id', updateDto)).rejects.toThrow(common_1.NotFoundException);
});
});
describe('deleteFlag', () => {
it('should delete flag successfully', async () => {
flagRepository.findOne.mockResolvedValue(mockFlag);
flagRepository.remove.mockResolvedValue(mockFlag);
await service.deleteFlag('flag-001');
expect(flagRepository.remove).toHaveBeenCalledWith(mockFlag);
});
it('should throw NotFoundException for invalid flag ID', async () => {
flagRepository.findOne.mockResolvedValue(null);
await expect(service.deleteFlag('invalid-id')).rejects.toThrow(common_1.NotFoundException);
});
});
describe('getAllFlags', () => {
it('should return all flags ordered by category and key', async () => {
const flags = [mockFlag];
flagRepository.find.mockResolvedValue(flags);
const result = await service.getAllFlags();
expect(result).toHaveLength(1);
expect(flagRepository.find).toHaveBeenCalledWith({
order: { category: 'ASC', key: 'ASC' },
});
});
});
describe('toggleFlag', () => {
it('should enable flag', async () => {
flagRepository.findOne.mockResolvedValue({
...mockFlag,
is_enabled: false,
});
flagRepository.save.mockResolvedValue({
...mockFlag,
is_enabled: true,
});
const result = await service.toggleFlag('flag-001', true);
expect(result.is_enabled).toBe(true);
});
it('should disable flag', async () => {
flagRepository.findOne.mockResolvedValue(mockFlag);
flagRepository.save.mockResolvedValue({
...mockFlag,
is_enabled: false,
});
const result = await service.toggleFlag('flag-001', false);
expect(result.is_enabled).toBe(false);
});
it('should throw NotFoundException for invalid flag ID', async () => {
flagRepository.findOne.mockResolvedValue(null);
await expect(service.toggleFlag('invalid-id', true)).rejects.toThrow(common_1.NotFoundException);
});
});
describe('setTenantFlag', () => {
const setDto = {
flag_id: 'flag-001',
is_enabled: false,
value: { custom: 'value' },
};
it('should create new tenant flag', async () => {
flagRepository.findOne.mockResolvedValue(mockFlag);
tenantFlagRepository.findOne.mockResolvedValue(null);
tenantFlagRepository.create.mockReturnValue(mockTenantFlag);
tenantFlagRepository.save.mockResolvedValue(mockTenantFlag);
const result = await service.setTenantFlag(mockTenantId, setDto);
expect(result).toHaveProperty('tenant_id', mockTenantId);
expect(tenantFlagRepository.create).toHaveBeenCalled();
});
it('should update existing tenant flag', async () => {
flagRepository.findOne.mockResolvedValue(mockFlag);
tenantFlagRepository.findOne.mockResolvedValue(mockTenantFlag);
tenantFlagRepository.save.mockResolvedValue({
...mockTenantFlag,
...setDto,
});
const result = await service.setTenantFlag(mockTenantId, setDto);
expect(result.is_enabled).toBe(false);
expect(tenantFlagRepository.create).not.toHaveBeenCalled();
});
it('should throw NotFoundException for invalid flag ID', async () => {
flagRepository.findOne.mockResolvedValue(null);
await expect(service.setTenantFlag(mockTenantId, setDto)).rejects.toThrow(common_1.NotFoundException);
});
});
describe('removeTenantFlag', () => {
it('should remove tenant flag if exists', async () => {
tenantFlagRepository.findOne.mockResolvedValue(mockTenantFlag);
tenantFlagRepository.remove.mockResolvedValue(mockTenantFlag);
await service.removeTenantFlag(mockTenantId, 'flag-001');
expect(tenantFlagRepository.remove).toHaveBeenCalledWith(mockTenantFlag);
});
it('should not throw if tenant flag does not exist', async () => {
tenantFlagRepository.findOne.mockResolvedValue(null);
await expect(service.removeTenantFlag(mockTenantId, 'flag-001')).resolves.not.toThrow();
});
});
describe('setUserFlag', () => {
const setDto = {
user_id: mockUserId,
flag_id: 'flag-001',
is_enabled: true,
value: { beta: true },
};
it('should create new user flag', async () => {
flagRepository.findOne.mockResolvedValue(mockFlag);
userFlagRepository.findOne.mockResolvedValue(null);
userFlagRepository.create.mockReturnValue(mockUserFlag);
userFlagRepository.save.mockResolvedValue(mockUserFlag);
const result = await service.setUserFlag(mockTenantId, setDto);
expect(result).toHaveProperty('user_id', mockUserId);
});
it('should update existing user flag', async () => {
flagRepository.findOne.mockResolvedValue(mockFlag);
userFlagRepository.findOne.mockResolvedValue(mockUserFlag);
userFlagRepository.save.mockResolvedValue({
...mockUserFlag,
...setDto,
});
const result = await service.setUserFlag(mockTenantId, setDto);
expect(result.is_enabled).toBe(true);
});
});
describe('evaluateFlag', () => {
const context = {
tenantId: mockTenantId,
userId: mockUserId,
};
beforeEach(() => {
flagRepository.findOne.mockReset();
userFlagRepository.findOne.mockReset();
tenantFlagRepository.findOne.mockReset();
});
it('should return default for unknown flag', async () => {
flagRepository.findOne.mockResolvedValue(null);
const result = await service.evaluateFlag('unknown_flag', context);
expect(result.enabled).toBe(false);
expect(result.source).toBe('default');
});
it('should return global disabled when flag is disabled', async () => {
flagRepository.findOne.mockResolvedValue({
...mockFlag,
is_enabled: false,
});
const result = await service.evaluateFlag('new_feature', context);
expect(result.enabled).toBe(false);
expect(result.source).toBe('global');
});
it('should evaluate flag with user context', async () => {
const flagWithNoRollout = {
...mockFlag,
rollout_percentage: 100,
};
flagRepository.findOne.mockResolvedValue(flagWithNoRollout);
userFlagRepository.findOne.mockResolvedValue(null);
tenantFlagRepository.findOne.mockResolvedValue(null);
const result = await service.evaluateFlag('new_feature', context);
expect(result).toHaveProperty('key', 'new_feature');
expect(result).toHaveProperty('enabled');
expect(result).toHaveProperty('source');
});
it('should evaluate flag with tenant context only', async () => {
const flagWithNoRollout = {
...mockFlag,
rollout_percentage: 100,
};
flagRepository.findOne.mockResolvedValue(flagWithNoRollout);
tenantFlagRepository.findOne.mockResolvedValue(null);
const result = await service.evaluateFlag('new_feature', { tenantId: mockTenantId });
expect(result).toHaveProperty('key', 'new_feature');
expect(result).toHaveProperty('enabled');
expect(result).toHaveProperty('source');
});
it('should return global when no overrides exist', async () => {
const globalFlag = {
id: 'flag-global',
key: 'global_feature',
name: 'Global Feature',
flag_type: feature_flag_entity_1.FlagType.BOOLEAN,
scope: feature_flag_entity_1.FlagScope.GLOBAL,
is_enabled: true,
default_value: true,
rollout_percentage: 100,
};
flagRepository.findOne.mockResolvedValue(globalFlag);
userFlagRepository.findOne.mockResolvedValue(null);
tenantFlagRepository.findOne.mockResolvedValue(null);
const result = await service.evaluateFlag('global_feature', context);
expect(result.enabled).toBe(true);
expect(result.source).toBe('global');
});
it('should evaluate rollout percentage', async () => {
const rolloutFlag = {
id: 'flag-rollout',
key: 'rollout_feature',
name: 'Rollout Feature',
flag_type: feature_flag_entity_1.FlagType.BOOLEAN,
scope: feature_flag_entity_1.FlagScope.GLOBAL,
is_enabled: true,
default_value: true,
rollout_percentage: 50,
};
flagRepository.findOne.mockResolvedValue(rolloutFlag);
userFlagRepository.findOne.mockResolvedValue(null);
tenantFlagRepository.findOne.mockResolvedValue(null);
const result = await service.evaluateFlag('rollout_feature', context);
expect(result.source).toBe('rollout');
expect(typeof result.enabled).toBe('boolean');
});
it('should be deterministic for same user/flag combination', async () => {
const rolloutFlag = {
id: 'flag-rollout',
key: 'rollout_feature',
name: 'Rollout Feature',
flag_type: feature_flag_entity_1.FlagType.BOOLEAN,
scope: feature_flag_entity_1.FlagScope.GLOBAL,
is_enabled: true,
default_value: true,
rollout_percentage: 50,
};
flagRepository.findOne.mockResolvedValue(rolloutFlag);
userFlagRepository.findOne.mockResolvedValue(null);
tenantFlagRepository.findOne.mockResolvedValue(null);
const result1 = await service.evaluateFlag('rollout_feature', context);
const result2 = await service.evaluateFlag('rollout_feature', context);
expect(result1.enabled).toBe(result2.enabled);
});
});
describe('evaluateAllFlags', () => {
beforeEach(() => {
flagRepository.find.mockReset();
flagRepository.findOne.mockReset();
userFlagRepository.findOne.mockReset();
tenantFlagRepository.findOne.mockReset();
});
it('should evaluate all flags', async () => {
const flags = [
{ ...mockFlag, key: 'flag1' },
{ ...mockFlag, key: 'flag2', is_enabled: false },
];
flagRepository.find.mockResolvedValue(flags);
flagRepository.findOne
.mockResolvedValueOnce(flags[0])
.mockResolvedValueOnce(flags[1]);
userFlagRepository.findOne.mockResolvedValue(null);
tenantFlagRepository.findOne.mockResolvedValue(null);
const result = await service.evaluateAllFlags({
tenantId: mockTenantId,
});
expect(Object.keys(result)).toHaveLength(2);
expect(result['flag1']).toBeDefined();
expect(result['flag2']).toBeDefined();
});
});
describe('isEnabled', () => {
beforeEach(() => {
flagRepository.findOne.mockReset();
userFlagRepository.findOne.mockReset();
tenantFlagRepository.findOne.mockReset();
});
it('should return false for unknown flag', async () => {
flagRepository.findOne.mockResolvedValue(null);
const result = await service.isEnabled('unknown_flag', {
tenantId: mockTenantId,
});
expect(result).toBe(false);
});
it('should return false for disabled flag', async () => {
flagRepository.findOne.mockResolvedValue({
...mockFlag,
is_enabled: false,
});
const result = await service.isEnabled('new_feature', {
tenantId: mockTenantId,
});
expect(result).toBe(false);
});
});
describe('getValue', () => {
beforeEach(() => {
flagRepository.findOne.mockReset();
userFlagRepository.findOne.mockReset();
tenantFlagRepository.findOne.mockReset();
});
it('should return default value for unknown flag', async () => {
flagRepository.findOne.mockResolvedValue(null);
const result = await service.getValue('unknown', {
tenantId: mockTenantId,
}, { limit: 50 });
expect(result).toEqual({ limit: 50 });
});
it('should return default value when disabled', async () => {
flagRepository.findOne.mockResolvedValue({
...mockFlag,
is_enabled: false,
});
const result = await service.getValue('new_feature', {
tenantId: mockTenantId,
}, { limit: 0 });
expect(result).toEqual({ limit: 0 });
});
});
describe('getTenantFlags', () => {
it('should return tenant flags with relations', async () => {
const tenantFlags = [mockTenantFlag];
tenantFlagRepository.find.mockResolvedValue(tenantFlags);
const result = await service.getTenantFlags(mockTenantId);
expect(result).toHaveLength(1);
expect(tenantFlagRepository.find).toHaveBeenCalledWith({
where: { tenant_id: mockTenantId },
relations: ['flag'],
});
});
});
describe('getUserFlags', () => {
it('should return user flags with relations', async () => {
const userFlags = [mockUserFlag];
userFlagRepository.find.mockResolvedValue(userFlags);
const result = await service.getUserFlags(mockTenantId, mockUserId);
expect(result).toHaveLength(1);
expect(userFlagRepository.find).toHaveBeenCalledWith({
where: { tenant_id: mockTenantId, user_id: mockUserId },
relations: ['flag'],
});
});
});
});
//# sourceMappingURL=feature-flags.service.spec.js.map