UNPKG

unleash-server

Version:

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

1,203 lines • 41.4 kB
import { setupAppWithAuth, } from '../../../test/e2e/helpers/test-helper.js'; import dbInit from '../../../test/e2e/helpers/database-init.js'; import getLogger from '../../../test/fixtures/no-logger.js'; import { DEFAULT_ENV, randomId } from '../../util/index.js'; import { ApiTokenType, } from '../../types/model.js'; import { startOfHour } from 'date-fns'; import { SYSTEM_USER_AUDIT, TEST_AUDIT_USER, } from '../../types/index.js'; import { vi } from 'vitest'; let app; let db; let frontendApiService; beforeAll(async () => { db = await dbInit('frontend_api', getLogger); app = await setupAppWithAuth(db.stores, { frontendApiOrigins: ['https://example.com'], }, db.rawDatabase); frontendApiService = app.services.frontendApiService; }); afterEach(() => { app.services.frontendApiService.stopAll(); vi.clearAllMocks(); }); afterAll(async () => { await app.destroy(); await db.destroy(); }); beforeEach(async () => { await db.stores.segmentStore.deleteAll(); await db.stores.featureToggleStore.deleteAll(); await db.stores.clientMetricsStoreV2.deleteAll(); await db.stores.apiTokenStore.deleteAll(); }); export const createApiToken = (type, overrides = {}) => { return app.services.apiTokenService.createApiTokenWithProjects({ type, projects: ['*'], environment: DEFAULT_ENV, tokenName: `${type}-token-${randomId()}`, ...overrides, }); }; const createFeatureToggle = async ({ name, project = 'default', environment = DEFAULT_ENV, strategies, enabled, }) => { const createdFeature = await app.services.featureToggleService.createFeatureToggle(project, { name }, TEST_AUDIT_USER, true); const createdStrategies = await Promise.all((strategies ?? []).map(async (s) => app.services.featureToggleService.createStrategy(s, { projectId: project, featureName: name, environment }, TEST_AUDIT_USER))); await app.services.featureToggleService.updateEnabled(project, name, environment, enabled, TEST_AUDIT_USER); return [createdFeature, createdStrategies]; }; const createProject = async (id, name) => { const user = await db.stores.userStore.insert({ name: randomId(), email: `${randomId()}@example.com`, }); await app.services.projectService.createProject({ id, name, mode: 'open', defaultStickiness: 'default' }, user, TEST_AUDIT_USER); }; test('should require a frontend token or an admin token', async () => { await app.request .get('/api/frontend') .expect('Content-Type', /json/) .expect(401); }); test('should not allow requests with a client token', async () => { const clientToken = await createApiToken(ApiTokenType.CLIENT); await app.request .get('/api/frontend') .set('Authorization', clientToken.secret) .expect('Content-Type', /json/) .expect(403); }); test('should allow requests with a token secret alias', async () => { const featureA = randomId(); const featureB = randomId(); const envA = randomId(); const envB = randomId(); await db.stores.environmentStore.create({ name: envA, type: 'test' }); await db.stores.environmentStore.create({ name: envB, type: 'test' }); await db.stores.projectStore.addEnvironmentToProject('default', envA); await db.stores.projectStore.addEnvironmentToProject('default', envB); await createFeatureToggle({ name: featureA, enabled: true, environment: envA, strategies: [{ name: 'default', constraints: [], parameters: {} }], }); await createFeatureToggle({ name: featureB, enabled: true, environment: envB, strategies: [{ name: 'default', constraints: [], parameters: {} }], }); const tokenA = await createApiToken(ApiTokenType.FRONTEND, { alias: randomId(), environment: envA, }); const tokenB = await createApiToken(ApiTokenType.FRONTEND, { alias: randomId(), environment: envB, }); await frontendApiService.refreshData(); await app.request .get('/api/frontend') .expect('Content-Type', /json/) .expect(401); await app.request .get('/api/frontend') .set('Authorization', '') .expect('Content-Type', /json/) .expect(401); await app.request .get('/api/frontend') .set('Authorization', 'null') .expect('Content-Type', /json/) .expect(401); await app.request .get('/api/frontend') .set('Authorization', randomId()) .expect('Content-Type', /json/) .expect(401); await app.request .get('/api/frontend') .set('Authorization', tokenA.secret.slice(0, -1)) .expect('Content-Type', /json/) .expect(401); await app.request .get('/api/frontend') .set('Authorization', tokenA.secret) .expect('Content-Type', /json/) .expect(200) .expect((res) => expect(res.body.toggles).toHaveLength(1)) .expect((res) => expect(res.body.toggles[0].name).toEqual(featureA)); await app.request .get('/api/frontend') .set('Authorization', tokenB.secret) .expect('Content-Type', /json/) .expect(200) .expect((res) => expect(res.body.toggles).toHaveLength(1)) .expect((res) => expect(res.body.toggles[0].name).toEqual(featureB)); await app.request .get('/api/frontend') .set('Authorization', tokenA.alias) .expect('Content-Type', /json/) .expect(200) .expect((res) => expect(res.body.toggles).toHaveLength(1)) .expect((res) => expect(res.body.toggles[0].name).toEqual(featureA)); await app.request .get('/api/frontend') .set('Authorization', tokenB.alias) .expect('Content-Type', /json/) .expect(200) .expect((res) => expect(res.body.toggles).toHaveLength(1)) .expect((res) => expect(res.body.toggles[0].name).toEqual(featureB)); }); test('should allow requests with an admin token', async () => { const featureA = randomId(); await createFeatureToggle({ name: featureA, enabled: true, strategies: [{ name: 'default', constraints: [], parameters: {} }], }); const adminToken = await createApiToken(ApiTokenType.ADMIN, { projects: ['*'], environment: '*', }); await frontendApiService.refreshData(); const { body } = await app.request .get('/api/frontend') .set('Authorization', adminToken.secret) .expect('Content-Type', /json/) .expect(200); expect(body.toggles).toHaveLength(1); expect(body.toggles[0].name).toEqual(featureA); }); test('should not allow admin requests with a frontend token', async () => { const frontendToken = await createApiToken(ApiTokenType.FRONTEND); await app.request .get('/api/admin/projects') .set('Authorization', frontendToken.secret) .expect('Content-Type', /json/) .expect(403); }); test('should not allow client requests with a frontend token', async () => { const frontendToken = await createApiToken(ApiTokenType.FRONTEND); await app.request .get('/api/client/features') .set('Authorization', frontendToken.secret) .expect('Content-Type', /json/) .expect(403); }); test('should not allow requests with an invalid frontend token', async () => { const frontendToken = await createApiToken(ApiTokenType.FRONTEND); await app.request .get('/api/frontend') .set('Authorization', frontendToken.secret.slice(0, -1)) .expect('Content-Type', /json/) .expect(401); }); test('should allow requests with a frontend token', async () => { const frontendToken = await createApiToken(ApiTokenType.FRONTEND); await frontendApiService.refreshData(); await app.request .get('/api/frontend') .set('Authorization', frontendToken.secret) .expect('Content-Type', /json/) .expect(200) .expect((res) => expect(res.body).toEqual({ toggles: [] })); }); test('should return 405 from unimplemented endpoints', async () => { const frontendToken = await createApiToken(ApiTokenType.FRONTEND); await app.request .get('/api/frontend/client/features') .set('Authorization', frontendToken.secret) .expect('Content-Type', /json/) .expect(405); await app.request .get('/api/frontend/health') .set('Authorization', frontendToken.secret) .expect('Content-Type', /json/) .expect(405); await app.request .get('/api/frontend/internal-backstage/prometheus') .set('Authorization', frontendToken.secret) .expect('Content-Type', /json/) .expect(405); }); test('should enforce frontend API CORS config', async () => { const allowedOrigin = 'https://example.com'; const unknownOrigin = 'https://example.org'; const origin = 'access-control-allow-origin'; const frontendToken = await createApiToken(ApiTokenType.FRONTEND); await app.request .options('/api/frontend') .set('Origin', unknownOrigin) .set('Authorization', frontendToken.secret) .expect((res) => expect(res.headers[origin]).toBeUndefined()); await app.request .options('/api/frontend') .set('Origin', allowedOrigin) .set('Authorization', frontendToken.secret) .expect((res) => expect(res.headers[origin]).toEqual(allowedOrigin)); await app.request .get('/api/frontend') .set('Origin', unknownOrigin) .set('Authorization', frontendToken.secret) .expect((res) => expect(res.headers[origin]).toBeUndefined()); await app.request .get('/api/frontend') .set('Origin', allowedOrigin) .set('Authorization', frontendToken.secret) .expect((res) => expect(res.headers[origin]).toEqual(allowedOrigin)); }); test('should accept client registration requests', async () => { const frontendToken = await createApiToken(ApiTokenType.FRONTEND); await app.request .post('/api/frontend/client/register') .set('Authorization', frontendToken.secret) .send({}) .expect('Content-Type', /json/) .expect(400); await app.request .post('/api/frontend/client/register') .set('Authorization', frontendToken.secret) .send({ appName: randomId(), instanceId: randomId(), sdkVersion: randomId(), environment: DEFAULT_ENV, interval: 10000, started: new Date(), strategies: ['default'], }) .expect(200) .expect((res) => expect(res.text).toEqual('OK')); }); test('should store frontend api client metrics', async () => { const now = new Date(); const appName = randomId(); const instanceId = randomId(); const featureName = randomId(); const frontendToken = await createApiToken(ApiTokenType.FRONTEND); const adminToken = await createApiToken(ApiTokenType.ADMIN, { projects: ['*'], environment: '*', }); // @ts-expect-error - cachedFeatureNames is a private property in ClientMetricsServiceV2 app.services.clientMetricsServiceV2.cachedFeatureNames = vi .fn() .mockResolvedValue([featureName]); await app.request .get(`/api/admin/client-metrics/features/${featureName}`) .set('Authorization', adminToken.secret) .expect('Content-Type', /json/) .expect(200) .then((res) => { expect(res.body).toEqual({ featureName, lastHourUsage: [], maturity: 'stable', seenApplications: [], version: 1, }); }); await app.request .post('/api/frontend/client/metrics') .set('Authorization', frontendToken.secret) .send({ appName, instanceId, bucket: { start: now, stop: now, toggles: { [featureName]: { yes: 1, no: 10 } }, }, }) .expect(200) .expect((res) => expect(res.text).toEqual('OK')); await app.request .post('/api/frontend/client/metrics') .set('Authorization', frontendToken.secret) .send({ appName, instanceId, bucket: { start: now, stop: now, toggles: { [featureName]: { yes: 2, no: 20 } }, }, }) .expect(200) .expect((res) => expect(res.text).toEqual('OK')); await app.services.clientMetricsServiceV2.bulkAdd(); await app.request .get(`/api/admin/client-metrics/features/${featureName}`) .set('Authorization', adminToken.secret) .expect('Content-Type', /json/) .expect(200) .then((res) => { expect(res.body).toEqual({ featureName, lastHourUsage: [ { environment: DEFAULT_ENV, timestamp: startOfHour(now).toISOString(), yes: 3, no: 30, }, ], maturity: 'stable', seenApplications: [appName], version: 1, }); }); }); test('should filter features by enabled/disabled', async () => { const frontendToken = await createApiToken(ApiTokenType.FRONTEND); await createFeatureToggle({ name: 'enabledFeature1', enabled: true, strategies: [{ name: 'default', constraints: [], parameters: {} }], }); await createFeatureToggle({ name: 'enabledFeature2', enabled: true, strategies: [{ name: 'default', constraints: [], parameters: {} }], }); await createFeatureToggle({ name: 'disabledFeature', enabled: false, strategies: [{ name: 'default', constraints: [], parameters: {} }], }); await frontendApiService.refreshData(); await app.request .get('/api/frontend') .set('Authorization', frontendToken.secret) .expect('Content-Type', /json/) .expect(200) .expect((res) => { expect(res.body).toEqual({ toggles: [ { name: 'enabledFeature1', enabled: true, impressionData: false, variant: { enabled: false, name: 'disabled', feature_enabled: true, featureEnabled: true, }, }, { name: 'enabledFeature2', enabled: true, impressionData: false, variant: { enabled: false, name: 'disabled', feature_enabled: true, featureEnabled: true, }, }, ], }); }); }); test('should filter features by strategies', async () => { const frontendToken = await createApiToken(ApiTokenType.FRONTEND); await createFeatureToggle({ name: 'featureWithoutStrategies', enabled: false, strategies: [], }); await createFeatureToggle({ name: 'featureWithMultipleStrategies', enabled: true, strategies: [{ name: 'default', constraints: [], parameters: {} }], }); await frontendApiService.refreshData(); await app.request .get('/api/frontend') .set('Authorization', frontendToken.secret) .expect('Content-Type', /json/) .expect(200) .expect((res) => { expect(res.body).toEqual({ toggles: [ { name: 'featureWithMultipleStrategies', enabled: true, impressionData: false, variant: { enabled: false, name: 'disabled', feature_enabled: true, featureEnabled: true, }, }, ], }); }); }); test('should filter features by constraints', async () => { const frontendToken = await createApiToken(ApiTokenType.FRONTEND); await createFeatureToggle({ name: 'featureWithAppNameA', enabled: true, strategies: [ { name: 'default', constraints: [ { contextName: 'appName', operator: 'IN', values: ['a'] }, ], parameters: {}, }, ], }); await createFeatureToggle({ name: 'featureWithAppNameAorB', enabled: true, strategies: [ { name: 'default', constraints: [ { contextName: 'appName', operator: 'IN', values: ['a', 'b'], }, ], parameters: {}, }, ], }); await frontendApiService.refreshData(); await app.request .get('/api/frontend?appName=a') .set('Authorization', frontendToken.secret) .expect('Content-Type', /json/) .expect(200) .expect((res) => expect(res.body.toggles).toHaveLength(2)); await app.request .get('/api/frontend?appName=b') .set('Authorization', frontendToken.secret) .expect('Content-Type', /json/) .expect(200) .expect((res) => expect(res.body.toggles).toHaveLength(1)); await app.request .get('/api/frontend?appName=c') .set('Authorization', frontendToken.secret) .expect('Content-Type', /json/) .expect(200) .expect((res) => expect(res.body.toggles).toHaveLength(0)); }); test('should be able to set environment as a context variable', async () => { const frontendToken = await createApiToken(ApiTokenType.FRONTEND); const featureName = 'featureWithEnvironmentConstraint'; await createFeatureToggle({ name: featureName, enabled: true, strategies: [ { name: 'default', constraints: [ { contextName: 'environment', operator: 'IN', values: ['staging'], }, ], parameters: {}, }, ], }); await frontendApiService.refreshData(); await app.request .get('/api/frontend?environment=staging') .set('Authorization', frontendToken.secret) .expect('Content-Type', /json/) .expect(200) .expect((res) => { expect(res.body.toggles).toHaveLength(1); expect(res.body.toggles[0].name).toBe(featureName); }); await app.request .get('/api/frontend') .set('Authorization', frontendToken.secret) .expect('Content-Type', /json/) .expect(200) .expect((res) => { expect(res.body.toggles).toHaveLength(0); }); }); test('should filter features by project', async () => { const projectA = 'projectA'; const projectB = 'projectB'; await createProject(projectA, randomId()); await createProject(projectB, randomId()); const frontendTokenDefault = await createApiToken(ApiTokenType.FRONTEND, { projects: ['default'], }); const frontendTokenProjectA = await createApiToken(ApiTokenType.FRONTEND, { projects: [projectA], }); const frontendTokenProjectAB = await createApiToken(ApiTokenType.FRONTEND, { projects: [projectA, projectB], }); await createFeatureToggle({ name: 'featureInProjectDefault', enabled: true, strategies: [{ name: 'default', parameters: {} }], }); await createFeatureToggle({ name: 'featureInProjectA', project: projectA, enabled: true, strategies: [{ name: 'default', parameters: {} }], }); await createFeatureToggle({ name: 'featureInProjectB', project: projectB, enabled: true, strategies: [{ name: 'default', parameters: {} }], }); await frontendApiService.refreshData(); await app.request .get('/api/frontend') .set('Authorization', frontendTokenDefault.secret) .expect('Content-Type', /json/) .expect(200) .expect((res) => { expect(res.body).toEqual({ toggles: [ { name: 'featureInProjectDefault', enabled: true, impressionData: false, variant: { enabled: false, name: 'disabled', feature_enabled: true, featureEnabled: true, }, }, ], }); }); await app.request .get('/api/frontend') .set('Authorization', frontendTokenProjectA.secret) .expect('Content-Type', /json/) .expect(200) .expect((res) => { expect(res.body).toEqual({ toggles: [ { name: 'featureInProjectA', enabled: true, impressionData: false, variant: { enabled: false, name: 'disabled', feature_enabled: true, featureEnabled: true, }, }, ], }); }); await app.request .get('/api/frontend') .set('Authorization', frontendTokenProjectAB.secret) .expect('Content-Type', /json/) .expect(200) .expect((res) => { expect(res.body).toEqual({ toggles: [ { name: 'featureInProjectA', enabled: true, impressionData: false, variant: { enabled: false, name: 'disabled', feature_enabled: true, featureEnabled: true, }, }, { name: 'featureInProjectB', enabled: true, impressionData: false, variant: { enabled: false, name: 'disabled', feature_enabled: true, featureEnabled: true, }, }, ], }); }); }); test('should filter features by environment', async () => { const environmentA = 'environmentA'; const environmentB = 'environmentB'; await db.stores.environmentStore.create({ name: environmentA, type: 'production', }); await db.stores.environmentStore.create({ name: environmentB, type: 'production', }); await app.services.environmentService.addEnvironmentToProject(environmentA, 'default', SYSTEM_USER_AUDIT); await app.services.environmentService.addEnvironmentToProject(environmentB, 'default', SYSTEM_USER_AUDIT); const frontendTokenEnvironmentDefault = await createApiToken(ApiTokenType.FRONTEND); const frontendTokenEnvironmentA = await createApiToken(ApiTokenType.FRONTEND, { environment: environmentA, }); const frontendTokenEnvironmentB = await createApiToken(ApiTokenType.FRONTEND, { environment: environmentB, }); await createFeatureToggle({ name: 'featureInEnvironmentDefault', enabled: true, strategies: [{ name: 'default', parameters: {} }], }); await createFeatureToggle({ name: 'featureInEnvironmentA', environment: environmentA, enabled: true, strategies: [{ name: 'default', parameters: {} }], }); await createFeatureToggle({ name: 'featureInEnvironmentB', environment: environmentB, enabled: true, strategies: [{ name: 'default', parameters: {} }], }); await frontendApiService.refreshData(); await app.request .get('/api/frontend') .set('Authorization', frontendTokenEnvironmentDefault.secret) .expect('Content-Type', /json/) .expect(200) .expect((res) => { expect(res.body).toEqual({ toggles: [ { name: 'featureInEnvironmentDefault', enabled: true, impressionData: false, variant: { enabled: false, name: 'disabled', feature_enabled: true, featureEnabled: true, }, }, ], }); }); await app.request .get('/api/frontend') .set('Authorization', frontendTokenEnvironmentA.secret) .expect('Content-Type', /json/) .expect(200) .expect((res) => { expect(res.body).toEqual({ toggles: [ { name: 'featureInEnvironmentA', enabled: true, impressionData: false, variant: { enabled: false, name: 'disabled', feature_enabled: true, featureEnabled: true, }, }, ], }); }); await app.request .get('/api/frontend') .set('Authorization', frontendTokenEnvironmentB.secret) .expect('Content-Type', /json/) .expect(200) .expect((res) => { expect(res.body).toEqual({ toggles: [ { name: 'featureInEnvironmentB', enabled: true, impressionData: false, variant: { enabled: false, name: 'disabled', feature_enabled: true, featureEnabled: true, }, }, ], }); }); }); test('should filter features by segment', async () => { const [featureA, [strategyA]] = await createFeatureToggle({ name: randomId(), enabled: true, strategies: [{ name: 'default', parameters: {} }], }); const [featureB, [strategyB]] = await createFeatureToggle({ name: randomId(), enabled: true, strategies: [{ name: 'default', parameters: {} }], }); const constraintA = { operator: 'IN', contextName: 'appName', values: ['a'], }; const constraintB = { operator: 'IN', contextName: 'appName', values: ['b'], }; const segmentA = await app.services.segmentService.create({ name: randomId(), constraints: [constraintA] }, TEST_AUDIT_USER); const segmentB = await app.services.segmentService.create({ name: randomId(), constraints: [constraintB] }, TEST_AUDIT_USER); await app.services.segmentService.addToStrategy(segmentA.id, strategyA.id); await app.services.segmentService.addToStrategy(segmentB.id, strategyB.id); const frontendToken = await createApiToken(ApiTokenType.FRONTEND); await frontendApiService.refreshData(); await app.request .get('/api/frontend') .set('Authorization', frontendToken.secret) .expect('Content-Type', /json/) .expect(200) .expect((res) => expect(res.body).toEqual({ toggles: [] })); await app.request .get('/api/frontend?appName=a') .set('Authorization', frontendToken.secret) .expect('Content-Type', /json/) .expect(200) .expect((res) => expect(res.body.toggles).toHaveLength(1)) .expect((res) => expect(res.body.toggles[0].name).toEqual(featureA.name)); await app.request .get('/api/frontend?appName=b') .set('Authorization', frontendToken.secret) .expect('Content-Type', /json/) .expect(200) .expect((res) => expect(res.body.toggles).toHaveLength(1)) .expect((res) => expect(res.body.toggles[0].name).toEqual(featureB.name)); await app.request .get('/api/frontend?appName=c') .set('Authorization', frontendToken.secret) .expect('Content-Type', /json/) .expect(200) .expect((res) => expect(res.body).toEqual({ toggles: [] })); }); test('should return maxAge header on options call', async () => { await app.request .options('/api/frontend') .set('Origin', 'https://example.com') .expect(204) .expect((res) => { expect(res.headers['access-control-max-age']).toBe('86400'); }); }); test('should evaluate strategies when returning toggles', async () => { const frontendToken = await createApiToken(ApiTokenType.FRONTEND); await createFeatureToggle({ name: 'enabledFeature', enabled: true, strategies: [ { name: 'flexibleRollout', constraints: [], parameters: { rollout: '100', stickiness: 'default', groupId: 'some-new', }, }, ], }); await createFeatureToggle({ name: 'disabledFeature', enabled: true, strategies: [ { name: 'flexibleRollout', constraints: [], parameters: { rollout: '0', stickiness: 'default', groupId: 'some-new', }, }, ], }); await frontendApiService.refreshData(); await app.request .get('/api/frontend') .set('Authorization', frontendToken.secret) .expect('Content-Type', /json/) .expect(200) .expect((res) => { expect(res.body).toEqual({ toggles: [ { name: 'enabledFeature', enabled: true, impressionData: false, variant: { enabled: false, name: 'disabled', feature_enabled: true, featureEnabled: true, }, }, ], }); }); }); test('should not return all features', async () => { const frontendToken = await createApiToken(ApiTokenType.FRONTEND); await createFeatureToggle({ name: 'enabledFeature1', enabled: true, strategies: [{ name: 'default', constraints: [], parameters: {} }], }); await createFeatureToggle({ name: 'enabledFeature2', enabled: true, strategies: [ { name: 'flexibleRollout', constraints: [], parameters: { rollout: '100', stickiness: 'default', groupId: 'some-new', }, }, ], }); await createFeatureToggle({ name: 'disabledFeature', enabled: true, strategies: [ { name: 'flexibleRollout', constraints: [], parameters: { rollout: '0', stickiness: 'default', groupId: 'some-new', }, }, ], }); await frontendApiService.refreshData(); await app.request .get('/api/frontend') .set('Authorization', frontendToken.secret) .expect('Content-Type', /json/) .expect(200) .expect((res) => { expect(res.body).toEqual({ toggles: [ { name: 'enabledFeature1', enabled: true, impressionData: false, variant: { enabled: false, name: 'disabled', feature_enabled: true, featureEnabled: true, }, }, { name: 'enabledFeature2', enabled: true, impressionData: false, variant: { enabled: false, name: 'disabled', feature_enabled: true, featureEnabled: true, }, }, ], }); }); }); test('should NOT evaluate disabled strategies when returning toggles', async () => { const frontendToken = await createApiToken(ApiTokenType.FRONTEND); await createFeatureToggle({ name: 'enabledFeature', enabled: true, strategies: [ { name: 'flexibleRollout', constraints: [], parameters: { rollout: '100', stickiness: 'default', groupId: 'some-new', }, }, ], }); await createFeatureToggle({ name: 'disabledFeature', enabled: false, strategies: [ { name: 'flexibleRollout', constraints: [], disabled: true, parameters: { rollout: '100', stickiness: 'default', groupId: 'some-new', }, }, ], }); await createFeatureToggle({ name: 'disabledFeature2', enabled: true, strategies: [ { name: 'flexibleRollout', constraints: [], disabled: true, parameters: { rollout: '100', stickiness: 'default', groupId: 'some-new', }, }, { name: 'flexibleRollout', constraints: [], disabled: false, parameters: { rollout: '0', stickiness: 'default', groupId: 'some-new', }, }, ], }); await createFeatureToggle({ name: 'disabledFeature3', enabled: false, strategies: [ { name: 'flexibleRollout', constraints: [], disabled: true, parameters: { rollout: '100', stickiness: 'default', groupId: 'some-new', }, }, ], }); await frontendApiService.refreshData(); await app.request .get('/api/frontend') .set('Authorization', frontendToken.secret) .expect('Content-Type', /json/) .expect(200) .expect((res) => { expect(res.body).toEqual({ toggles: [ { name: 'enabledFeature', enabled: true, impressionData: false, variant: { enabled: false, name: 'disabled', feature_enabled: true, featureEnabled: true, }, }, ], }); }); }); test('should return 204 if metrics are disabled', async () => { const localApp = await setupAppWithAuth(db.stores, { frontendApiOrigins: ['https://example.com'], experimental: { flags: { disableMetrics: true, }, }, }, db.rawDatabase); const frontendToken = await localApp.services.apiTokenService.createApiTokenWithProjects({ type: ApiTokenType.FRONTEND, projects: ['*'], environment: DEFAULT_ENV, tokenName: `disabledMetric-token-${randomId()}`, }); const appName = randomId(); const instanceId = randomId(); const featureName = 'metricsDisabled'; const now = new Date(); await localApp.request .post('/api/frontend/client/metrics') .set('Authorization', frontendToken.secret) .send({ appName, instanceId, bucket: { start: now, stop: now, toggles: { [featureName]: { yes: 2, no: 20 } }, }, }) .expect(204); }); test('should resolve variable rollout percentage consistently', async () => { const frontendToken = await createApiToken(ApiTokenType.FRONTEND); await createFeatureToggle({ name: 'randomFeature', enabled: true, strategies: [ { name: 'flexibleRollout', constraints: [], parameters: { rollout: '50', stickiness: 'default', groupId: 'some-new', }, variants: [ { name: 'a', stickiness: 'default', weightType: 'variable', weight: 1000, }, ], }, ], }); await frontendApiService.refreshData(); for (let i = 0; i < 10; ++i) { const { body } = await app.request .get('/api/frontend') .set('Authorization', frontendToken.secret) .expect('Content-Type', /json/) .expect(200); if (body.toggles.length > 0) { // disabled variant should not be possible for enabled toggles expect(body.toggles[0].variant.name).toBe('a'); } } }); test('should return enabled feature flags using POST', async () => { const frontendToken = await createApiToken(ApiTokenType.FRONTEND); await createFeatureToggle({ name: 'enabledFeature1', enabled: true, strategies: [{ name: 'default', constraints: [], parameters: {} }], }); await createFeatureToggle({ name: 'enabledFeature2', enabled: true, strategies: [{ name: 'default', constraints: [], parameters: {} }], }); await createFeatureToggle({ name: 'disabledFeature', enabled: false, strategies: [{ name: 'default', constraints: [], parameters: {} }], }); await frontendApiService.refreshData(); await app.request .post('/api/frontend') .set('Authorization', frontendToken.secret) .set('Content-Type', 'application/json') .send() .expect('Content-Type', /json/) .expect(200) .expect((res) => { expect(res.body).toEqual({ toggles: [ { name: 'enabledFeature1', enabled: true, impressionData: false, variant: { enabled: false, name: 'disabled', feature_enabled: true, featureEnabled: true, }, }, { name: 'enabledFeature2', enabled: true, impressionData: false, variant: { enabled: false, name: 'disabled', feature_enabled: true, featureEnabled: true, }, }, ], }); }); }); test('should return enabled feature flags based on context using POST', async () => { const frontendToken = await createApiToken(ApiTokenType.FRONTEND); const featureName = 'featureWithEnvironmentConstraint'; await createFeatureToggle({ name: featureName, enabled: true, strategies: [ { name: 'default', constraints: [ { contextName: 'userId', operator: 'IN', values: ['1337'], }, ], parameters: {}, }, ], }); await frontendApiService.refreshData(); await app.request .post('/api/frontend') .set('Authorization', frontendToken.secret) .set('Content-Type', 'application/json') .send({ context: { userId: '1337' } }) .expect('Content-Type', /json/) .expect(200) .expect((res) => { expect(res.body.toggles).toHaveLength(1); expect(res.body.toggles[0].name).toBe(featureName); }); await app.request .post('/api/frontend') .set('Authorization', frontendToken.secret) .send({ context: { appName: 'test', userId: '42' } }) .expect('Content-Type', /json/) .expect(200) .expect((res) => { expect(res.body.toggles).toHaveLength(0); }); }); //# sourceMappingURL=frontend-api.e2e.test.js.map