unleash-server
Version:
Unleash is an enterprise ready feature flag service. It provides different strategies for handling feature flags.
524 lines • 18.1 kB
JavaScript
import { EventEmitter } from 'events';
import { ClientFeatureToggleDelta, filterEventsByQuery, } from './client-feature-toggle-delta.js';
import { DeltaCache } from './delta-cache.js';
import { FEATURE_PROJECT_CHANGE } from '../../../events/index.js';
describe('filterEventsByQuery', () => {
const mockEvents = [
{
eventId: 1,
type: 'feature-updated',
feature: {
name: 'test-feature',
project: 'project1',
enabled: true,
},
},
{
eventId: 2,
type: 'feature-updated',
feature: {
name: 'alpha-feature',
project: 'project2',
enabled: true,
},
},
{
eventId: 3,
type: 'feature-removed',
featureName: 'beta-feature',
project: 'project3',
},
{
eventId: 4,
type: 'segment-updated',
segment: { id: 1, name: 'my-segment', constraints: [] },
},
{ eventId: 5, type: 'segment-removed', segmentId: 2 },
];
test('filters events based on eventId', () => {
const requiredRevisionId = 2;
const result = filterEventsByQuery(mockEvents, requiredRevisionId, ['project3'], '');
expect(result).toEqual([
{
eventId: 3,
type: 'feature-removed',
featureName: 'beta-feature',
project: 'project3',
},
{
eventId: 4,
type: 'segment-updated',
segment: { id: 1, name: 'my-segment', constraints: [] },
},
{ eventId: 5, type: 'segment-removed', segmentId: 2 },
]);
});
test('returns all projects', () => {
const result = filterEventsByQuery(mockEvents, 0, ['*'], '');
expect(result).toEqual(mockEvents);
});
test('filters by name prefix', () => {
const result = filterEventsByQuery(mockEvents, 0, ['project1', 'project2'], 'alpha');
expect(result).toEqual([
{
eventId: 2,
type: 'feature-updated',
feature: {
name: 'alpha-feature',
project: 'project2',
enabled: true,
},
},
{
eventId: 4,
type: 'segment-updated',
segment: { id: 1, name: 'my-segment', constraints: [] },
},
{ eventId: 5, type: 'segment-removed', segmentId: 2 },
]);
});
test('filters by project list', () => {
const result = filterEventsByQuery(mockEvents, 0, ['project3'], 'beta');
expect(result).toEqual([
{
eventId: 3,
type: 'feature-removed',
featureName: 'beta-feature',
project: 'project3',
},
{
eventId: 4,
type: 'segment-updated',
segment: { id: 1, name: 'my-segment', constraints: [] },
},
{ eventId: 5, type: 'segment-removed', segmentId: 2 },
]);
});
});
describe('DeltaCache hydration ordering', () => {
test('keeps hydration features sorted after updates', () => {
const cache = new DeltaCache({
eventId: 1,
type: 'hydration',
features: [
{ name: 'bravo', enabled: true },
{ name: 'charlie', enabled: true },
],
segments: [{ id: 2, name: 'segment-b', constraints: [] }],
});
cache.addEvents([
{
eventId: 2,
type: 'feature-updated',
feature: { name: 'alpha', enabled: true },
},
]);
const hydration = cache.getHydrationEvent();
expect(hydration.features.map((feature) => feature.name)).toEqual([
'alpha',
'bravo',
'charlie',
]);
});
test('keeps hydration segments sorted after updates', () => {
const cache = new DeltaCache({
eventId: 1,
type: 'hydration',
features: [{ name: 'alpha', enabled: true }],
segments: [
{ id: 3, name: 'segment-c', constraints: [] },
{ id: 4, name: 'segment-d', constraints: [] },
],
});
cache.addEvents([
{
eventId: 2,
type: 'segment-updated',
segment: { id: 2, name: 'segment-b', constraints: [] },
},
{
eventId: 3,
type: 'segment-updated',
segment: { id: 1, name: 'segment-b', constraints: [] },
},
]);
const hydration = cache.getHydrationEvent();
expect(hydration.segments.map((segment) => segment.id)).toEqual([
1, 2, 3, 4,
]);
});
});
describe('ClientFeatureToggleDelta bootstrap behavior', () => {
test('returns the same wildcard hydration revision for identical environment state across pods', async () => {
const createDelta = (globalRevisionId) => new ClientFeatureToggleDelta({
getAll: async ({ environment, }) => environment === 'production'
? [
{
name: 'first',
project: 'default',
enabled: true,
},
]
: [],
}, {
getAllForClientIds: async () => [],
}, {
getDeltaRevisionState: async () => ({
projectRevisions: new Map([['default', 85815]]),
globalSegmentRevision: 0,
}),
getMaxRevisionId: async () => globalRevisionId,
}, {
on: () => undefined,
}, {}, {
eventBus: new EventEmitter(),
getLogger: () => ({
error: () => undefined,
info: () => undefined,
}),
});
const stalePodDelta = createDelta(85815);
const freshPodDelta = createDelta(85923);
const stalePodResult = await stalePodDelta.getDelta(undefined, {
environment: 'production',
project: ['*'],
});
const freshPodResult = await freshPodDelta.getDelta(undefined, {
environment: 'production',
project: ['*'],
});
expect(stalePodResult).toBeDefined();
expect(freshPodResult).toBeDefined();
expect(stalePodResult?.events[0]?.eventId).toBe(freshPodResult?.events[0]?.eventId);
});
test('returns an empty hydration event on initial request for an empty environment', async () => {
const delta = new ClientFeatureToggleDelta({
getAll: async () => [],
}, {
getAllForClientIds: async () => [],
}, {
getDeltaRevisionState: async () => ({
projectRevisions: new Map(),
globalSegmentRevision: 0,
}),
getMaxRevisionId: async () => 0,
}, {
on: () => undefined,
}, {}, {
eventBus: new EventEmitter(),
getLogger: () => ({
error: () => undefined,
info: () => undefined,
}),
});
const result = await delta.getDelta(undefined, {
environment: 'production',
});
expect(result).toEqual({
events: [
{
eventId: 0,
type: 'hydration',
features: [],
segments: [],
},
],
});
});
test('returns no delta when client explicitly requests revision 0 for an empty environment', async () => {
const delta = new ClientFeatureToggleDelta({
getAll: async () => [],
}, {
getAllForClientIds: async () => [],
}, {
getDeltaRevisionState: async () => ({
projectRevisions: new Map(),
globalSegmentRevision: 0,
}),
getMaxRevisionId: async () => 0,
}, {
on: () => undefined,
}, {}, {
eventBus: new EventEmitter(),
getLogger: () => ({
error: () => undefined,
info: () => undefined,
}),
});
const result = await delta.getDelta(0, {
environment: 'production',
});
expect(result).toBeUndefined();
});
test('does not emit a no-op delta for an unrelated environment change', async () => {
let currentRevisionId = 1;
const delta = new ClientFeatureToggleDelta({
getAll: async ({ environment, toggleNames = [] }) => {
const developmentFeature = {
name: 'first',
project: 'default',
enabled: false,
};
if (environment !== 'development') {
return [];
}
if (toggleNames.length === 0) {
return [developmentFeature];
}
// @ts-expect-error - toggle name not defined
return toggleNames.includes('first')
? [developmentFeature]
: [];
},
}, {
getAllForClientIds: async () => [],
}, {
getDeltaRevisionState: async () => ({
projectRevisions: new Map([['default', 1]]),
globalSegmentRevision: 0,
}),
getRevisionRange: async () => [
{
id: 2,
type: 'feature-updated',
featureName: 'first',
project: 'default',
environment: 'production',
},
],
getMaxRevisionId: async () => currentRevisionId,
}, {
on: () => undefined,
}, {
isEnabled: (name) => name === 'deltaApi',
}, {
eventBus: new EventEmitter(),
getLogger: () => ({
error: () => undefined,
info: () => undefined,
}),
});
await delta.getDelta(undefined, {
environment: 'development',
project: ['default'],
});
currentRevisionId = 2;
await delta.onUpdateRevisionEvent();
const result = await delta.getDelta(1, {
environment: 'development',
project: ['default'],
});
expect(result).toBeUndefined();
});
test('applies global events without environment to all initialized environments', async () => {
let currentRevisionId = 1;
const delta = new ClientFeatureToggleDelta({
getAll: async ({ environment, toggleNames = [] }) => {
const featuresByEnvironment = {
development: {
name: 'first',
project: 'default',
enabled: false,
},
production: {
name: 'first',
project: 'default',
enabled: true,
},
};
const feature = featuresByEnvironment[environment];
if (!feature) {
return [];
}
if (toggleNames.length === 0) {
return [feature];
}
// @ts-expect-error - toggle name not defined
return toggleNames.includes('first') ? [feature] : [];
},
}, {
getAllForClientIds: async () => [],
}, {
getDeltaRevisionState: async () => ({
projectRevisions: new Map([['default', 1]]),
globalSegmentRevision: 0,
}),
getRevisionRange: async () => [
{
id: 2,
type: 'feature-updated',
featureName: 'first',
project: 'default',
environment: null,
},
],
getMaxRevisionId: async () => currentRevisionId,
}, {
on: () => undefined,
}, {
isEnabled: (name) => name === 'deltaApi',
}, {
eventBus: new EventEmitter(),
getLogger: () => ({
error: () => undefined,
info: () => undefined,
}),
});
await delta.getDelta(undefined, {
environment: 'development',
project: ['default'],
});
await delta.getDelta(undefined, {
environment: 'production',
project: ['default'],
});
currentRevisionId = 2;
await delta.onUpdateRevisionEvent();
const developmentResult = await delta.getDelta(1, {
environment: 'development',
project: ['default'],
});
const productionResult = await delta.getDelta(1, {
environment: 'production',
project: ['default'],
});
expect(developmentResult).toEqual({
events: [
{
eventId: 2,
type: 'feature-updated',
feature: {
name: 'first',
project: 'default',
enabled: false,
},
},
],
});
expect(productionResult).toEqual({
events: [
{
eventId: 2,
type: 'feature-updated',
feature: {
name: 'first',
project: 'default',
enabled: true,
},
},
],
});
});
test('feature project move emits feature-removed for old project and feature-updated for new project', async () => {
let currentRevisionId = 1;
const delta = new ClientFeatureToggleDelta({
getAll: async ({ environment, toggleNames = [] }) => {
const feature = {
name: 'moved-feature',
project: 'new-project',
enabled: true,
};
if (environment !== 'development')
return [];
if (toggleNames.length === 0)
return [feature];
// @ts-expect-error - toggle name not defined
return toggleNames.includes('moved-feature')
? [feature]
: [];
},
}, {
getAllForClientIds: async () => [],
}, {
getDeltaRevisionState: async () => ({
projectRevisions: new Map([['old-project', 1]]),
globalSegmentRevision: 0,
}),
getRevisionRange: async () => [
{
id: 2,
type: FEATURE_PROJECT_CHANGE,
featureName: 'moved-feature',
project: 'new-project',
environment: null,
data: {
oldProject: 'old-project',
newProject: 'new-project',
},
},
],
getMaxRevisionId: async () => currentRevisionId,
}, {
on: () => undefined,
}, {
isEnabled: (name) => name === 'deltaApi',
}, {
eventBus: new EventEmitter(),
getLogger: () => ({
error: () => undefined,
info: () => undefined,
}),
});
await delta.getDelta(undefined, {
environment: 'development',
project: ['*'],
});
currentRevisionId = 2;
await delta.onUpdateRevisionEvent();
const oldProjectResult = await delta.getDelta(1, {
environment: 'development',
project: ['old-project'],
});
const newProjectResult = await delta.getDelta(1, {
environment: 'development',
project: ['new-project'],
});
const bothProjectsResult = await delta.getDelta(1, {
environment: 'development',
project: ['old-project', 'new-project'],
});
expect(oldProjectResult).toEqual({
events: [
{
eventId: 2,
type: 'feature-removed',
featureName: 'moved-feature',
project: 'old-project',
},
],
});
expect(newProjectResult).toEqual({
events: [
{
eventId: 2,
type: 'feature-updated',
feature: {
name: 'moved-feature',
project: 'new-project',
enabled: true,
},
},
],
});
expect(bothProjectsResult).toEqual({
events: [
{
eventId: 2,
type: 'feature-removed',
featureName: 'moved-feature',
project: 'old-project',
},
{
eventId: 2,
type: 'feature-updated',
feature: {
name: 'moved-feature',
project: 'new-project',
enabled: true,
},
},
],
});
});
});
//# sourceMappingURL=client-feature-toggle-delta.test.js.map