- HERENCIA-SIMCO.md actualizado con directivas v3.7 y v3.8 - Actualizaciones de configuracion Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
448 lines
20 KiB
JavaScript
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
|