UNPKG

unleash-server

Version:

Unleash is an enterprise ready feature flag service. It provides different strategies for handling feature flags.

524 lines • 18.1 kB
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