"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