1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-12 01:17:04 +02:00
unleash.unleash/src/lib/features/client-feature-toggles/tests/client-feature-toggle.e2e.test.ts
Fredrik Strand Oseberg f34d187cd9
Refactor/separate client and admin store (#5006)
This PR is the first step in separating the client and admin stores.
Currently our feature toggle services uses the client store to serve
multiple purposes. 

Admin API uses the feature toggle service to serve both the feature
toggle list and playground features, while the client API uses the
feature toggle service to serve client features. The admin API can
change often and have very different requirements than the client API,
which changes infrequently and generally keeps the same stable structure
for long periods of time. This architecture is error prone, because when
you need to make changes to the admin API, you can very easily affect
the client API.

I aim to put up a stone wall between the two APIs. Complete separation
between the two APIs, at the cost of some duplication.

In this PR I have created a feature oriented architecture for client
features and disconnected the client API from the feature toggle
service. It now goes through it's own service to it's own store. For
feature toggle service I have duplicated and replaced the functionality
that serves /api/admin/features, I have kept a lot of the ugliness in
the code and haven't removed anything in order to avoid breaking
changes.

Next steps: 
* Move playground to admin API
* Remove client-feature-toggle-store from feature-toggle-service
2023-10-12 13:58:23 +02:00

161 lines
5.3 KiB
TypeScript

import supertest from 'supertest';
import createStores from '../../../../test/fixtures/store';
import getLogger from '../../../../test/fixtures/no-logger';
import getApp from '../../../app';
import { createServices } from '../../../services';
import FeatureController from '../client-feature-toggle.controller';
import { createTestConfig } from '../../../../test/config/test-config';
import { secondsToMilliseconds } from 'date-fns';
import { ClientSpecService } from '../../../services/client-spec-service';
let app;
async function getSetup() {
const base = `/random${Math.round(Math.random() * 1000)}`;
const stores = createStores();
const config = createTestConfig({
server: { baseUriPath: base },
});
const services = createServices(stores, config);
app = await getApp(config, stores, services);
return {
base,
clientFeatureToggleStore: stores.clientFeatureToggleStore,
request: supertest(app),
destroy: () => {
services.versionService.destroy();
services.clientInstanceService.destroy();
},
};
}
const callGetAll = async (controller: FeatureController) => {
await controller.getAll(
// @ts-expect-error
{ query: {}, header: () => undefined, headers: {} },
{
json: () => {},
setHeader: () => undefined,
},
);
};
let base;
let request;
let destroy;
let flagResolver;
beforeEach(async () => {
const setup = await getSetup();
base = setup.base;
request = setup.request;
destroy = setup.destroy;
flagResolver = {
isEnabled: () => {
return false;
},
};
});
afterEach(() => {
destroy();
});
test('should get empty getFeatures via client', () => {
expect.assertions(1);
return request
.get(`${base}/api/client/features`)
.expect('Content-Type', /json/)
.expect(200)
.expect((res) => {
expect(res.body.features.length === 0).toBe(true);
});
});
test('if caching is enabled should memoize', async () => {
const getClientFeatures = jest.fn().mockReturnValue([]);
const getActive = jest.fn().mockReturnValue([]);
const getActiveForClient = jest.fn().mockReturnValue([]);
const respondWithValidation = jest.fn().mockReturnValue({});
const validPath = jest.fn().mockReturnValue(jest.fn());
const clientSpecService = new ClientSpecService({ getLogger });
const openApiService = { respondWithValidation, validPath };
const clientFeatureToggleService = { getClientFeatures };
const featureToggleService = { getClientFeatures };
const segmentService = { getActive, getActiveForClient };
const configurationRevisionService = { getMaxRevisionId: () => 1 };
const controller = new FeatureController(
{
clientSpecService,
// @ts-expect-error due to partial implementation
openApiService,
// @ts-expect-error due to partial implementation
clientFeatureToggleService,
// @ts-expect-error due to partial implementation
featureToggleService,
// @ts-expect-error due to partial implementation
segmentService,
// @ts-expect-error due to partial implementation
configurationRevisionService,
},
{
getLogger,
clientFeatureCaching: {
enabled: true,
maxAge: secondsToMilliseconds(10),
},
flagResolver,
},
);
await callGetAll(controller);
await callGetAll(controller);
expect(getClientFeatures).toHaveBeenCalledTimes(1);
});
test('if caching is not enabled all calls goes to service', async () => {
const getClientFeatures = jest.fn().mockReturnValue([]);
const getActive = jest.fn().mockReturnValue([]);
const getActiveForClient = jest.fn().mockReturnValue([]);
const respondWithValidation = jest.fn().mockReturnValue({});
const validPath = jest.fn().mockReturnValue(jest.fn());
const clientSpecService = new ClientSpecService({ getLogger });
const clientFeatureToggleService = { getClientFeatures };
const segmentService = { getActive, getActiveForClient };
const featureToggleService = { getClientFeatures };
const openApiService = { respondWithValidation, validPath };
const configurationRevisionService = { getMaxRevisionId: () => 1 };
const controller = new FeatureController(
{
clientSpecService,
// @ts-expect-error due to partial implementation
openApiService,
// @ts-expect-error due to partial implementation
clientFeatureToggleService,
// @ts-expect-error due to partial implementation
featureToggleService,
// @ts-expect-error due to partial implementation
segmentService,
// @ts-expect-error due to partial implementation
configurationRevisionService,
},
{
getLogger,
clientFeatureCaching: {
enabled: false,
maxAge: secondsToMilliseconds(10),
},
flagResolver,
},
);
await callGetAll(controller);
await callGetAll(controller);
expect(getClientFeatures).toHaveBeenCalledTimes(2);
});