unleash-server
Version:
Unleash is an enterprise ready feature flag service. It provides different strategies for handling feature flags.
401 lines • 15.8 kB
JavaScript
import { ConstraintsReadModel } from './constraints-read-model.js';
import FakeContextFieldStore from '../context/fake-context-field-store.js';
const createReadModel = (contextFieldStore) => {
const store = contextFieldStore ?? new FakeContextFieldStore();
const readModel = new ConstraintsReadModel(store);
return { readModel, store };
};
describe('ConstraintsReadModel - validateConstraints', () => {
describe('valid constraints', () => {
test('validates a string operator constraint (IN)', async () => {
const { readModel } = createReadModel();
const constraints = [
{
contextName: 'someField',
operator: 'IN',
values: ['a', 'b'],
},
];
const result = await readModel.validateConstraints(constraints);
expect(result).toHaveLength(1);
expect(result[0].operator).toBe('IN');
});
test('validates a NOT_IN constraint', async () => {
const { readModel } = createReadModel();
const constraints = [
{
contextName: 'someField',
operator: 'NOT_IN',
values: ['x', 'y'],
},
];
const result = await readModel.validateConstraints(constraints);
expect(result).toHaveLength(1);
expect(result[0].operator).toBe('NOT_IN');
});
test('validates a numeric operator constraint', async () => {
const { readModel } = createReadModel();
const constraints = [
{
contextName: 'someField',
operator: 'NUM_EQ',
value: '5',
values: [],
},
];
const result = await readModel.validateConstraints(constraints);
expect(result).toHaveLength(1);
expect(result[0].operator).toBe('NUM_EQ');
});
test('validates a semver operator constraint', async () => {
const { readModel } = createReadModel();
const constraints = [
{
contextName: 'someField',
operator: 'SEMVER_EQ',
value: '1.2.3',
values: [],
},
];
const result = await readModel.validateConstraints(constraints);
expect(result).toHaveLength(1);
expect(result[0].operator).toBe('SEMVER_EQ');
});
test('validates a date operator constraint', async () => {
const { readModel } = createReadModel();
const constraints = [
{
contextName: 'someField',
operator: 'DATE_AFTER',
value: '2024-01-01T00:00:00.000Z',
values: [],
},
];
const result = await readModel.validateConstraints(constraints);
expect(result).toHaveLength(1);
expect(result[0].operator).toBe('DATE_AFTER');
});
test('validates a REGEX constraint', async () => {
const { readModel } = createReadModel();
const constraints = [
{
contextName: 'someField',
operator: 'REGEX',
value: '^test.*$',
values: [],
},
];
const result = await readModel.validateConstraints(constraints);
expect(result).toHaveLength(1);
expect(result[0].operator).toBe('REGEX');
});
test('validates multiple constraints at once', async () => {
const { readModel } = createReadModel();
const constraints = [
{
contextName: 'fieldA',
operator: 'IN',
values: ['a'],
},
{
contextName: 'fieldB',
operator: 'NUM_GT',
value: '10',
values: [],
},
{
contextName: 'fieldC',
operator: 'SEMVER_LT',
value: '2.0.0',
values: [],
},
];
const result = await readModel.validateConstraints(constraints);
expect(result).toHaveLength(3);
});
test('returns empty array for empty input', async () => {
const { readModel } = createReadModel();
const result = await readModel.validateConstraints([]);
expect(result).toEqual([]);
});
test('defaults values to empty array when not provided for non-string operators', async () => {
const { readModel } = createReadModel();
const constraints = [
{
contextName: 'someField',
operator: 'NUM_EQ',
value: '5',
},
];
const result = await readModel.validateConstraints(constraints);
expect(result[0].values).toEqual([]);
});
});
describe('invalid constraints', () => {
test('rejects invalid operator', async () => {
const { readModel } = createReadModel();
const constraints = [
{
contextName: 'someField',
operator: 'INVALID_OP',
values: ['a'],
},
];
await expect(readModel.validateConstraints(constraints)).rejects.toThrow();
});
test('rejects missing contextName', async () => {
const { readModel } = createReadModel();
const constraints = [
{
operator: 'IN',
values: ['a'],
},
];
await expect(readModel.validateConstraints(constraints)).rejects.toThrow();
});
test('rejects invalid number for NUM operator', async () => {
const { readModel } = createReadModel();
const constraints = [
{
contextName: 'someField',
operator: 'NUM_EQ',
value: 'not-a-number',
values: [],
},
];
await expect(readModel.validateConstraints(constraints)).rejects.toThrow();
});
test('rejects invalid semver for SEMVER operator', async () => {
const { readModel } = createReadModel();
const constraints = [
{
contextName: 'someField',
operator: 'SEMVER_EQ',
value: 'not-semver',
values: [],
},
];
await expect(readModel.validateConstraints(constraints)).rejects.toThrow();
});
test('rejects invalid date for DATE operator', async () => {
const { readModel } = createReadModel();
const constraints = [
{
contextName: 'someField',
operator: 'DATE_BEFORE',
value: 'not-a-date',
values: [],
},
];
await expect(readModel.validateConstraints(constraints)).rejects.toThrow();
});
test('rejects invalid regex for REGEX operator', async () => {
const { readModel } = createReadModel();
const constraints = [
{
contextName: 'someField',
operator: 'REGEX',
value: '(?!invalid)',
values: [],
},
];
await expect(readModel.validateConstraints(constraints)).rejects.toThrow();
});
test('rejects inverted REGEX constraint', async () => {
const { readModel } = createReadModel();
const constraints = [
{
contextName: 'someField',
operator: 'REGEX',
value: '^test.*$',
values: [],
inverted: true,
},
];
await expect(readModel.validateConstraints(constraints)).rejects.toThrow('REGEX operator cannot be inverted.');
});
});
describe('legal values validation', () => {
test('accepts values matching legal values for string operators', async () => {
const store = new FakeContextFieldStore();
await store.create({
name: 'country',
description: 'Country',
sortOrder: 0,
stickiness: false,
legalValues: [
{ value: 'US' },
{ value: 'UK' },
{ value: 'DE' },
],
});
const { readModel } = createReadModel(store);
const constraints = [
{
contextName: 'country',
operator: 'IN',
values: ['US', 'UK'],
},
];
const result = await readModel.validateConstraints(constraints);
expect(result).toHaveLength(1);
});
test('rejects values not in legal values for string operators', async () => {
const store = new FakeContextFieldStore();
await store.create({
name: 'country',
description: 'Country',
sortOrder: 0,
stickiness: false,
legalValues: [{ value: 'US' }, { value: 'UK' }],
});
const { readModel } = createReadModel(store);
const constraints = [
{
contextName: 'country',
operator: 'IN',
values: ['US', 'INVALID'],
},
];
await expect(readModel.validateConstraints(constraints)).rejects.toThrow();
});
test('validates single value against legal values for NUM operators', async () => {
const store = new FakeContextFieldStore();
await store.create({
name: 'priority',
description: 'Priority',
sortOrder: 0,
stickiness: false,
legalValues: [{ value: '1' }, { value: '2' }, { value: '3' }],
});
const { readModel } = createReadModel(store);
const constraints = [
{
contextName: 'priority',
operator: 'NUM_EQ',
value: '1',
values: [],
},
];
const result = await readModel.validateConstraints(constraints);
expect(result).toHaveLength(1);
});
test('rejects single value not in legal values for NUM operators', async () => {
const store = new FakeContextFieldStore();
await store.create({
name: 'priority',
description: 'Priority',
sortOrder: 0,
stickiness: false,
legalValues: [{ value: '1' }, { value: '2' }],
});
const { readModel } = createReadModel(store);
const constraints = [
{
contextName: 'priority',
operator: 'NUM_EQ',
value: '99',
values: [],
},
];
await expect(readModel.validateConstraints(constraints)).rejects.toThrow();
});
test('skips legal values check when context field has no legal values', async () => {
const store = new FakeContextFieldStore();
await store.create({
name: 'customField',
description: 'Custom',
sortOrder: 0,
stickiness: false,
legalValues: [],
});
const { readModel } = createReadModel(store);
const constraints = [
{
contextName: 'customField',
operator: 'IN',
values: ['anything'],
},
];
const result = await readModel.validateConstraints(constraints);
expect(result).toHaveLength(1);
});
test('skips legal values check when context field does not exist', async () => {
const { readModel } = createReadModel();
const constraints = [
{
contextName: 'nonExistent',
operator: 'IN',
values: ['anything'],
},
];
const result = await readModel.validateConstraints(constraints);
expect(result).toHaveLength(1);
});
test('validates value (not values) for SEMVER operators against legal values', async () => {
const store = new FakeContextFieldStore();
await store.create({
name: 'version',
description: 'Version',
sortOrder: 0,
stickiness: false,
legalValues: [{ value: '1.0.0' }, { value: '2.0.0' }],
});
const { readModel } = createReadModel(store);
const constraints = [
{
contextName: 'version',
operator: 'SEMVER_GT',
value: '1.0.0',
values: [],
},
];
const result = await readModel.validateConstraints(constraints);
expect(result).toHaveLength(1);
});
test('validates value (not values) for DATE operators against legal values', async () => {
const store = new FakeContextFieldStore();
const dateValue = '2024-06-01T00:00:00.000Z';
await store.create({
name: 'releaseDate',
description: 'Release date',
sortOrder: 0,
stickiness: false,
legalValues: [{ value: dateValue }],
});
const { readModel } = createReadModel(store);
const constraints = [
{
contextName: 'releaseDate',
operator: 'DATE_AFTER',
value: dateValue,
values: [],
},
];
const result = await readModel.validateConstraints(constraints);
expect(result).toHaveLength(1);
});
test('validates value (not values) for REGEX operator against legal values', async () => {
const store = new FakeContextFieldStore();
await store.create({
name: 'pattern',
description: 'Pattern',
sortOrder: 0,
stickiness: false,
legalValues: [{ value: '^abc$' }, { value: '^def$' }],
});
const { readModel } = createReadModel(store);
const constraints = [
{
contextName: 'pattern',
operator: 'REGEX',
value: '^abc$',
values: [],
},
];
const result = await readModel.validateConstraints(constraints);
expect(result).toHaveLength(1);
});
});
});
//# sourceMappingURL=constraints-read-model.test.js.map