Skip to content

Commit

Permalink
refactor: always add values to constraints (#1448)
Browse files Browse the repository at this point in the history
  • Loading branch information
olav committed Mar 17, 2022
1 parent 957ec1c commit 1cc43c0
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 16 deletions.
5 changes: 4 additions & 1 deletion src/lib/schema/constraint-value-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import joi from 'joi';

export const constraintNumberTypeSchema = joi.number();

export const constraintStringTypeSchema = joi.array().items(joi.string());
export const constraintStringTypeSchema = joi
.array()
.items(joi.string())
.min(1);

export const constraintDateTypeSchema = joi.date();
25 changes: 20 additions & 5 deletions src/lib/schema/feature-schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ test('should not accept empty constraint values', () => {
);
});

test('should not accept empty list of constraint values', () => {
test('should accept empty list of constraint values', async () => {
const toggle = {
name: 'app.constraints.empty.value.list',
type: 'release',
Expand All @@ -231,10 +231,10 @@ test('should not accept empty list of constraint values', () => {
],
};

const { error } = featureSchema.validate(toggle);
expect(error.details[0].message).toEqual(
'"strategies[0].constraints[0].values" must contain at least 1 items',
);
const validated = await featureSchema.validateAsync(toggle);
expect(validated.strategies.length).toEqual(1);
expect(validated.strategies[0].constraints.length).toEqual(1);
expect(validated.strategies[0].constraints[0].values).toEqual([]);
});

test('Filter queries should accept a list of tag values', () => {
Expand Down Expand Up @@ -289,3 +289,18 @@ test('constraint schema should only allow specified operators', async () => {
);
}
});

test('constraint schema should add a values array by default', async () => {
const validated = await constraintSchema.validateAsync({
contextName: 'x',
operator: 'NUM_EQ',
value: 1,
});

expect(validated).toEqual({
contextName: 'x',
operator: 'NUM_EQ',
value: 1,
values: [],
});
});
3 changes: 2 additions & 1 deletion src/lib/schema/feature-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ export const nameSchema = joi
export const constraintSchema = joi.object().keys({
contextName: joi.string(),
operator: joi.string().valid(...ALL_OPERATORS),
values: joi.array().items(joi.string().min(1).max(100)).min(1).optional(),
// Constraints must have a values array to support legacy SDKs.
values: joi.array().items(joi.string().min(1).max(100)).default([]),
value: joi.optional(),
caseInsensitive: joi.boolean().optional(),
inverted: joi.boolean().optional(),
Expand Down
26 changes: 17 additions & 9 deletions src/lib/services/feature-toggle-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,17 +165,19 @@ class FeatureToggleService {
}
}

async validateConstraints(constraints: IConstraint[]): Promise<void> {
async validateConstraints(
constraints: IConstraint[],
): Promise<IConstraint[]> {
const validations = constraints.map((constraint) => {
return this.validateConstraint(constraint);
});

await Promise.all(validations);
return Promise.all(validations);
}

async validateConstraint(constraint: IConstraint): Promise<void> {
async validateConstraint(input: IConstraint): Promise<IConstraint> {
const constraint = await constraintSchema.validateAsync(input);
const { operator } = constraint;
await constraintSchema.validateAsync(constraint);
const contextDefinition = await this.contextFieldStore.get(
constraint.contextName,
);
Expand Down Expand Up @@ -218,6 +220,8 @@ class FeatureToggleService {
);
}
}

return constraint;
}

async patchFeature(
Expand Down Expand Up @@ -282,7 +286,9 @@ class FeatureToggleService {
await this.validateFeatureContext(context);

if (strategyConfig.constraints?.length > 0) {
await this.validateConstraints(strategyConfig.constraints);
strategyConfig.constraints = await this.validateConstraints(
strategyConfig.constraints,
);
}

try {
Expand Down Expand Up @@ -342,15 +348,17 @@ class FeatureToggleService {
this.validateFeatureStrategyContext(existingStrategy, context);

if (existingStrategy.id === id) {
if (updates.constraints?.length > 0) {
updates.constraints = await this.validateConstraints(
updates.constraints,
);
}

const strategy = await this.featureStrategiesStore.updateStrategy(
id,
updates,
);

if (updates.constraints?.length > 0) {
await this.validateConstraints(updates.constraints);
}

// Store event!
const tags = await this.tagStore.getAllTagsForFeature(featureName);
const data = this.featureStrategyToPublic(strategy);
Expand Down
107 changes: 107 additions & 0 deletions src/test/e2e/api/admin/project/features.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import ApiUser from '../../../../../lib/types/api-user';
import { ApiTokenType } from '../../../../../lib/types/models/api-token';
import IncompatibleProjectError from '../../../../../lib/error/incompatible-project-error';
import { IVariant, WeightType } from '../../../../../lib/types/model';
import { v4 as uuidv4 } from 'uuid';
import supertest from 'supertest';

let app: IUnleashTest;
let db: ITestDb;
Expand Down Expand Up @@ -2066,3 +2068,108 @@ test('Can create toggle with impression data on different project', async () =>
expect(res.body.impressionData).toBe(false);
});
});

test('should reject invalid constraint values for multi-valued constraints', async () => {
const project = await db.stores.projectStore.create({
id: uuidv4(),
name: uuidv4(),
description: '',
});

const toggle = await db.stores.featureToggleStore.create(project.id, {
name: uuidv4(),
impressionData: true,
});

const mockStrategy = (values: string[]) => ({
name: uuidv4(),
constraints: [{ contextName: 'userId', operator: 'IN', values }],
});

const featureStrategiesPath = `/api/admin/projects/${project.id}/features/${toggle.name}/environments/default/strategies`;

await app.request
.post(featureStrategiesPath)
.send(mockStrategy([]))
.expect(400);
await app.request
.post(featureStrategiesPath)
.send(mockStrategy(['']))
.expect(400);
const { body: strategy } = await app.request
.post(featureStrategiesPath)
.send(mockStrategy(['a']))
.expect(200);

await app.request
.put(`${featureStrategiesPath}/${strategy.id}`)
.send(mockStrategy([]))
.expect(400);
await app.request
.put(`${featureStrategiesPath}/${strategy.id}`)
.send(mockStrategy(['']))
.expect(400);
await app.request
.put(`${featureStrategiesPath}/${strategy.id}`)
.send(mockStrategy(['a']))
.expect(200);
});

test('should add default constraint values for single-valued constraints', async () => {
const project = await db.stores.projectStore.create({
id: uuidv4(),
name: uuidv4(),
description: '',
});

const toggle = await db.stores.featureToggleStore.create(project.id, {
name: uuidv4(),
impressionData: true,
});

const constraintValue = {
contextName: 'userId',
operator: 'NUM_EQ',
value: '1',
};

const constraintValues = {
contextName: 'userId',
operator: 'IN',
values: ['a', 'b', 'c'],
};

const mockStrategy = (constraint: unknown) => ({
name: uuidv4(),
constraints: [constraint],
});

const expectValues = (res: supertest.Response, values: unknown[]) => {
expect(res.body.constraints.length).toEqual(1);
expect(res.body.constraints[0].values).toEqual(values);
};

const featureStrategiesPath = `/api/admin/projects/${project.id}/features/${toggle.name}/environments/default/strategies`;

await app.request
.post(featureStrategiesPath)
.send(mockStrategy(constraintValue))
.expect(200)
.expect((res) => expectValues(res, []));
const { body: strategy } = await app.request
.post(featureStrategiesPath)
.send(mockStrategy(constraintValues))
.expect(200)
.expect((res) => expectValues(res, constraintValues.values));

await app.request
.put(`${featureStrategiesPath}/${strategy.id}`)
.send(mockStrategy(constraintValue))
.expect(200)
.expect((res) => expectValues(res, []));
await app.request
.put(`${featureStrategiesPath}/${strategy.id}`)
.send(mockStrategy(constraintValues))
.expect(200)
.expect((res) => expectValues(res, constraintValues.values));
});

0 comments on commit 1cc43c0

Please sign in to comment.