diff --git a/src/lib/features/feature-toggle/feature-toggle-service.ts b/src/lib/features/feature-toggle/feature-toggle-service.ts index 3ebb9e8121..b5d32fc9eb 100644 --- a/src/lib/features/feature-toggle/feature-toggle-service.ts +++ b/src/lib/features/feature-toggle/feature-toggle-service.ts @@ -396,23 +396,39 @@ class FeatureToggleService { }) { if (!this.flagResolver.isEnabled('resourceLimits')) return; - const constraintsLimit = this.resourceLimits.constraints; + const { + constraints: constraintsLimit, + constraintValues: constraintValuesLimit, + } = this.resourceLimits; + if ( constraints.updated.length > constraintsLimit && constraints.updated.length > constraints.existing.length ) { throwExceedsLimitError(this.eventBus, { - resource: `constraints`, + resource: 'constraints', limit: constraintsLimit, }); } - const constraintValuesLimit = this.resourceLimits.constraintValues; + const isSameLength = + constraints.existing.length === constraints.updated.length; const constraintOverLimit = constraints.updated.find( - (constraint) => - Array.isArray(constraint.values) && - constraint.values?.length > constraintValuesLimit, + (constraint, i) => { + const updatedCount = constraint.values?.length ?? 0; + const existingCount = + constraints.existing[i]?.values?.length ?? 0; + + const isOverLimit = + Array.isArray(constraint.values) && + updatedCount > constraintValuesLimit; + const allowAnyway = + isSameLength && existingCount >= updatedCount; + + return isOverLimit && !allowAnyway; + }, ); + if (constraintOverLimit) { throwExceedsLimitError(this.eventBus, { resource: `constraint values for ${constraintOverLimit.contextName}`, @@ -421,7 +437,6 @@ class FeatureToggleService { }); } } - async validateStrategyType( strategyName: string | undefined, ): Promise { diff --git a/src/lib/features/feature-toggle/tests/feature-toggle-service.limit.test.ts b/src/lib/features/feature-toggle/tests/feature-toggle-service.limit.test.ts index 2e22b25506..210a6fcc5d 100644 --- a/src/lib/features/feature-toggle/tests/feature-toggle-service.limit.test.ts +++ b/src/lib/features/feature-toggle/tests/feature-toggle-service.limit.test.ts @@ -195,6 +195,73 @@ describe('Strategy limits', () => { "Failed to create constraint values for userId. You can't create more than the established limit of 3", ); }); + + test('Should not throw limit exceeded errors for constraint values if the new values are less than or equal to the old values AND there have been no other constraint updates (re-ordering or deleting)', async () => { + const LIMIT = 1; + const { featureToggleService, featureStrategiesStore } = + createFakeFeatureToggleService({ + getLogger, + flagResolver: alwaysOnFlagResolver, + resourceLimits: { + constraintValues: LIMIT, + }, + } as unknown as IUnleashConfig); + + const constraints = (valueCount: number) => + [ + { + values: Array.from({ length: valueCount }).map((_, i) => + i.toString(), + ), + operator: 'IN', + contextName: 'appName', + }, + { + values: ['a', 'b', 'c'], + operator: 'IN', + contextName: 'appName', + }, + ] as IConstraint[]; + + const flagName = 'feature'; + + await featureStrategiesStore.createFeature({ + name: flagName, + createdByUserId: 1, + }); + + const initialConstraintValueCount = LIMIT + 2; + const strategy = await featureStrategiesStore.createStrategyFeatureEnv({ + parameters: {}, + strategyName: 'default', + featureName: flagName, + constraints: constraints(initialConstraintValueCount), + projectId: 'default', + environment: 'default', + }); + + const updateStrategy = (valueCount) => + featureToggleService.unprotectedUpdateStrategy( + strategy.id, + { + constraints: constraints(valueCount), + }, + { projectId: 'default', featureName: 'feature' } as any, + {} as IAuditUser, + ); + + // check that you can save the same amount of constraint values + await updateStrategy(initialConstraintValueCount); + // check that you can save fewer constraint values but still over the limit + await updateStrategy(initialConstraintValueCount - 1); + + // check that you can't save more constraint values + await expect(async () => + updateStrategy(initialConstraintValueCount + 1), + ).rejects.toThrow( + new ExceedsLimitError('constraint values for appName', LIMIT), + ); + }); }); describe('Flag limits', () => {