UNPKG

unleash-server

Version:

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

238 lines • 8.99 kB
import { setupAppWithAuth, } from '../../helpers/test-helper.js'; import dbInit from '../../helpers/database-init.js'; import getLogger from '../../../fixtures/no-logger.js'; import { CHANGE_REQUEST_CREATED } from '../../../../lib/events/index.js'; import { CLIENT, DEFAULT_ENV } from '../../../../lib/server-impl.js'; import { ApiTokenType } from '../../../../lib/types/model.js'; const validTokens = [ { tokenName: `client-dev-token`, permissions: [CLIENT], projects: ['*'], environment: 'development', type: ApiTokenType.CLIENT, secret: '*:development.client', }, { tokenName: `client-prod-token`, permissions: [CLIENT], projects: ['*'], environment: 'production', type: ApiTokenType.CLIENT, secret: '*:production.client', }, { tokenName: 'all-envs-client', permissions: [CLIENT], projects: ['*'], environment: '*', type: ApiTokenType.CLIENT, secret: '*:*.hungry-client', }, ]; const devTokenSecret = validTokens[0].secret; const prodTokenSecret = validTokens[1].secret; const allEnvsTokenSecret = validTokens[2].secret; async function setup() { const db = await dbInit(`ignored`, getLogger); // Create per-environment client tokens so we can request specific environment snapshots const app = await setupAppWithAuth(db.stores, { authentication: { enableApiToken: true, initApiTokens: validTokens, }, experimental: { flags: { strictSchemaValidation: true, }, }, }, db.rawDatabase); return { app, db }; } async function initialize({ app, db }) { const allEnvs = await app.services.environmentService.getAll(); const nonDefaultEnv = allEnvs.find((env) => env.name !== DEFAULT_ENV).name; await app.createFeature('X'); await app.createFeature('Y'); await app.archiveFeature('Y'); await app.createFeature('Z'); await app.enableFeature('Z', DEFAULT_ENV); await app.enableFeature('Z', nonDefaultEnv); await app.services.eventService.storeEvent({ type: CHANGE_REQUEST_CREATED, createdBy: 'some@example.com', createdByUserId: 123, ip: '127.0.0.1', featureName: `X`, }); } async function validateInitialState({ app, db, }) { /** * This helps reason about the etag, which is formed by <query-hash>:<event-id> * To see the output you need to run this test with --silent=false * You can see the expected output in the expect statement below */ const { events } = await app.services.eventService.getEvents(); // NOTE: events could be processed in different order resulting in a flaky test const actualEvents = events .reverse() .map(({ id, environment, featureName, type }) => ({ id, environment, featureName, type, })); let nextId = 8; // this is the first id after the token creation events const expectedEvents = [ { id: nextId++, featureName: 'X', type: 'feature-created', }, { id: nextId++, featureName: 'Y', type: 'feature-created', }, { id: nextId++, featureName: 'Y', type: 'feature-archived', }, { id: nextId++, featureName: 'Z', type: 'feature-created', }, { id: nextId++, environment: 'development', featureName: 'Z', type: 'feature-strategy-add', }, { id: nextId++, environment: 'development', featureName: 'Z', type: 'feature-environment-enabled', }, { id: nextId++, environment: 'production', featureName: 'Z', type: 'feature-strategy-add', }, { id: nextId++, environment: 'production', featureName: 'Z', type: 'feature-environment-enabled', }, { id: nextId++, featureName: 'X', type: 'change-request-created', }, ]; // We only require that all expectedEvents exist within actualEvents, matching // only on the properties explicitly specified in each expected object. // This lets us omit properties (like id) from some expected entries that might // arrive in different order, without breaking the test. for (const expectedEvent of expectedEvents) { expect(actualEvents).toContainEqual(expect.objectContaining(expectedEvent)); } } describe('feature 304 api client', () => { let app; let db; const expectedDevEventId = 13; beforeAll(async () => { ({ app, db } = await setup()); await initialize({ app, db }); await validateInitialState({ app, db }); }); afterAll(async () => { await app.destroy(); await db.destroy(); }); test('returns calculated hash without if-none-match header (dev env token)', async () => { const res = await app.request .get('/api/client/features') .set('Authorization', devTokenSecret) .expect('Content-Type', /json/) .expect(200); expect(res.headers.etag).toBe(`"76d8bb0e:${expectedDevEventId}:v1"`); expect(res.body.meta.etag).toBe(`"76d8bb0e:${expectedDevEventId}:v1"`); }); test(`returns 200 for pre-calculated hash because hash changed (dev env token)`, async () => { const res = await app.request .get('/api/client/features') .set('Authorization', devTokenSecret) .set('if-none-match', `"76d8bb0e:${expectedDevEventId}"`) .expect(200); expect(res.headers.etag).toBe(`"76d8bb0e:${expectedDevEventId}:v1"`); expect(res.body.meta.etag).toBe(`"76d8bb0e:${expectedDevEventId}:v1"`); }); test('creating a new feature does not modify etag', async () => { await app.createFeature('new'); await app.services.configurationRevisionService.updateMaxRevisionId(); await app.request .get('/api/client/features') .set('Authorization', devTokenSecret) .set('if-none-match', `"76d8bb0e:${expectedDevEventId}:v1"`) .expect(304); }); test('a token with all envs should get the max id regardless of the environment', async () => { const currentProdEtag = `"67e24428:15:v1"`; const { headers } = await app.request .get('/api/client/features') .set('if-none-match', currentProdEtag) .set('Authorization', allEnvsTokenSecret) .expect(200); // it's a different hash than prod, but gets the max id expect(headers.etag).toEqual(`"ae443048:15:v1"`); }); test('production environment gets a different etag than development', async () => { const { headers: prodHeaders } = await app.request .get('/api/client/features?bla=1') .set('Authorization', prodTokenSecret) .expect(200); expect(prodHeaders.etag).toEqual(`"67e24428:15:v1"`); const { headers: devHeaders } = await app.request .get('/api/client/features') .set('Authorization', devTokenSecret) .expect(200); expect(devHeaders.etag).toEqual(`"76d8bb0e:13:v1"`); }); test('modifying dev environment should only invalidate dev tokens', async () => { const currentDevEtag = `"76d8bb0e:13:v1"`; const currentProdEtag = `"67e24428:15:v1"`; await app.request .get('/api/client/features') .set('if-none-match', currentProdEtag) .set('Authorization', prodTokenSecret) .expect(304); await app.request .get('/api/client/features') .set('Authorization', devTokenSecret) .set('if-none-match', currentDevEtag) .expect(304); await app.enableFeature('X', DEFAULT_ENV); await app.services.configurationRevisionService.updateMaxRevisionId(); await app.request .get('/api/client/features') .set('Authorization', prodTokenSecret) .set('if-none-match', currentProdEtag) .expect(304); const { headers: devHeaders } = await app.request .get('/api/client/features') .set('Authorization', devTokenSecret) .set('if-none-match', currentDevEtag) .expect(200); // Note: this test yields a different result if run in isolation // this is because the id 19 depends on a previous test adding a feature // otherwise the id will be 18 expect(devHeaders.etag).toEqual(`"76d8bb0e:19:v1"`); }); }); //# sourceMappingURL=feature.optimal304.e2e.test.js.map