1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

refactor: test composition and other error codes (#3348)

## About the changes
Small refactor to showcase how to use [composition to validate different
aspects of the
response](https://github.com/Unleash/unleash/pull/3348/files#diff-ee4c1bd501b1195162b7a85ed6be348a665288f871abc8e74f64d94361213f9eR361-R367)
and checking [different status
codes](https://github.com/Unleash/unleash/pull/3348/files#diff-4044a5da3280ef76960bbffd5f36eccb395ac319fe58c4d59ef68a878cbb1a5dR95)
This commit is contained in:
Gastón Fournier 2023-03-23 16:31:05 +01:00 committed by GitHub
parent 24f9d51e31
commit a79a76f497
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 185 additions and 213 deletions

View File

@ -112,12 +112,7 @@ const createProjects = async (projects: string[] = [DEFAULT_PROJECT]) => {
id: project, id: project,
mode: 'open' as const, mode: 'open' as const,
}); });
await app.request await app.linkProjectToEnvironment(project, DEFAULT_ENV);
.post(`/api/admin/projects/${project}/environments`)
.send({
environment: DEFAULT_ENV,
})
.expect(200);
} }
}; };
@ -127,18 +122,6 @@ const createSegment = (postData: UpsertSegmentSchema): Promise<ISegment> => {
}); });
}; };
const createContextField = async (contextField: IContextFieldDto) => {
await app.createContextField(contextField);
};
const createFeature = async (featureName: string) => {
await app.createFeature(featureName);
};
const archiveFeature = async (featureName: string) => {
await app.archiveFeature(featureName);
};
const unArchiveFeature = async (featureName: string) => { const unArchiveFeature = async (featureName: string) => {
await app.request await app.request
.post(`/api/admin/archive/revive/${featureName}`) .post(`/api/admin/archive/revive/${featureName}`)
@ -176,7 +159,7 @@ beforeEach(async () => {
await environmentStore.deleteAll(); await environmentStore.deleteAll();
await contextFieldStore.deleteAll(); await contextFieldStore.deleteAll();
await createContextField({ name: 'appName' }); await app.createContextField({ name: 'appName' });
}); });
afterAll(async () => { afterAll(async () => {
@ -475,18 +458,6 @@ test('returns no features, when no feature was requested', async () => {
expect(body.features).toHaveLength(0); expect(body.features).toHaveLength(0);
}); });
const importToggles = (
importPayload: ImportTogglesSchema,
status = 200,
expect: (response) => void = () => {},
) =>
app.request
.post('/api/admin/features-batch/import')
.send(importPayload)
.set('Content-Type', 'application/json')
.expect(status)
.expect(expect);
const defaultFeature = 'first_feature'; const defaultFeature = 'first_feature';
const variants: VariantsSchema = [ const variants: VariantsSchema = [
@ -610,7 +581,7 @@ const validateImport = (importPayload: ImportTogglesSchema, status = 200) =>
test('import features to existing project and environment', async () => { test('import features to existing project and environment', async () => {
await createProjects(); await createProjects();
await importToggles(defaultImportPayload); await app.importToggles(defaultImportPayload);
const { body: importedFeature } = await getFeature(defaultFeature); const { body: importedFeature } = await getFeature(defaultFeature);
expect(importedFeature).toMatchObject({ expect(importedFeature).toMatchObject({
@ -645,8 +616,8 @@ test('import features to existing project and environment', async () => {
test('importing same JSON should work multiple times in a row', async () => { test('importing same JSON should work multiple times in a row', async () => {
await createProjects(); await createProjects();
await importToggles(defaultImportPayload); await app.importToggles(defaultImportPayload);
await importToggles(defaultImportPayload); await app.importToggles(defaultImportPayload);
const { body: importedFeature } = await getFeature(defaultFeature); const { body: importedFeature } = await getFeature(defaultFeature);
expect(importedFeature).toMatchObject({ expect(importedFeature).toMatchObject({
@ -681,7 +652,7 @@ test('reject import with unknown context fields', async () => {
name: 'ContextField1', name: 'ContextField1',
legalValues: [{ value: 'Value1', description: '' }], legalValues: [{ value: 'Value1', description: '' }],
}; };
await createContextField(contextField); await app.createContextField(contextField);
const importPayloadWithContextFields: ImportTogglesSchema = { const importPayloadWithContextFields: ImportTogglesSchema = {
...defaultImportPayload, ...defaultImportPayload,
data: { data: {
@ -695,7 +666,10 @@ test('reject import with unknown context fields', async () => {
}, },
}; };
const { body } = await importToggles(importPayloadWithContextFields, 400); const { body } = await app.importToggles(
importPayloadWithContextFields,
400,
);
expect(body).toMatchObject({ expect(body).toMatchObject({
details: [ details: [
@ -718,7 +692,10 @@ test('reject import with unsupported strategies', async () => {
}, },
}; };
const { body } = await importToggles(importPayloadWithContextFields, 400); const { body } = await app.importToggles(
importPayloadWithContextFields,
400,
);
expect(body).toMatchObject({ expect(body).toMatchObject({
details: [ details: [
@ -741,10 +718,10 @@ test('validate import data', async () => {
legalValues: [{ value: 'new_value' }], legalValues: [{ value: 'new_value' }],
}; };
await createFeature(defaultFeature); await app.createFeature(defaultFeature);
await archiveFeature(defaultFeature); await app.archiveFeature(defaultFeature);
await createContextField(contextField); await app.createContextField(contextField);
const importPayloadWithContextFields: ImportTogglesSchema = { const importPayloadWithContextFields: ImportTogglesSchema = {
...defaultImportPayload, ...defaultImportPayload,
data: { data: {
@ -801,7 +778,7 @@ test('should create new context', async () => {
}, },
}; };
await importToggles(importPayloadWithContextFields, 200); await app.importToggles(importPayloadWithContextFields);
const { body } = await getContextField(context.name); const { body } = await getContextField(context.name);
expect(body).toMatchObject(context); expect(body).toMatchObject(context);
@ -809,11 +786,11 @@ test('should create new context', async () => {
test('should not import archived features tags', async () => { test('should not import archived features tags', async () => {
await createProjects(); await createProjects();
await importToggles(defaultImportPayload); await app.importToggles(defaultImportPayload);
await archiveFeature(defaultFeature); await app.archiveFeature(defaultFeature);
await importToggles({ await app.importToggles({
...defaultImportPayload, ...defaultImportPayload,
data: { data: {
...defaultImportPayload.data, ...defaultImportPayload.data,

View File

@ -1,19 +1,13 @@
import { setupAppWithCustomConfig } from '../../helpers/test-helper'; import {
import dbInit from '../../helpers/database-init'; IUnleashTest,
setupAppWithCustomConfig,
} from '../../helpers/test-helper';
import dbInit, { ITestDb } from '../../helpers/database-init';
import getLogger from '../../../fixtures/no-logger'; import getLogger from '../../../fixtures/no-logger';
import { DEFAULT_PROJECT } from '../../../../lib/types'; import { DEFAULT_PROJECT } from '../../../../lib/types';
let app; let app: IUnleashTest;
let db; let db: ITestDb;
const createFeatureToggle = (
featureName: string,
project = DEFAULT_PROJECT,
) => {
return app.request.post(`/api/admin/projects/${project}/features`).send({
name: featureName,
});
};
beforeAll(async () => { beforeAll(async () => {
db = await dbInit('archive_serial', getLogger); db = await dbInit('archive_serial', getLogger);
@ -25,78 +19,42 @@ beforeAll(async () => {
}, },
}, },
}); });
await app.services.featureToggleServiceV2.createFeatureToggle( await app.createFeature({
'default', name: 'featureX',
{ description: 'the #1 feature',
name: 'featureX', });
description: 'the #1 feature', await app.createFeature({
}, name: 'featureY',
'test', description: 'soon to be the #1 feature',
); });
await app.services.featureToggleServiceV2.createFeatureToggle( await app.createFeature({
'default', name: 'featureZ',
{ description: 'terrible feature',
name: 'featureY', });
description: 'soon to be the #1 feature', await app.createFeature({
}, name: 'featureArchivedX',
'test', description: 'the #1 feature',
); });
await app.services.featureToggleServiceV2.createFeatureToggle( await app.archiveFeature('featureArchivedX');
'default',
{ await app.createFeature({
name: 'featureZ', name: 'featureArchivedY',
description: 'terrible feature', description: 'soon to be the #1 feature',
}, });
'test', await app.archiveFeature('featureArchivedY');
); await app.createFeature({
await app.services.featureToggleServiceV2.createFeatureToggle( name: 'featureArchivedZ',
'default', description: 'terrible feature',
{ });
name: 'featureArchivedX', await app.archiveFeature('featureArchivedZ');
description: 'the #1 feature', await app.createFeature({
}, name: 'feature.with.variants',
'test', description: 'A feature toggle with variants',
); variants: [
await app.services.featureToggleServiceV2.archiveToggle( { name: 'control', weight: 50 },
'featureArchivedX', { name: 'new', weight: 50 },
'test', ],
); });
await app.services.featureToggleServiceV2.createFeatureToggle(
'default',
{
name: 'featureArchivedY',
description: 'soon to be the #1 feature',
},
'test',
);
await app.services.featureToggleServiceV2.archiveToggle(
'featureArchivedY',
'test',
);
await app.services.featureToggleServiceV2.createFeatureToggle(
'default',
{
name: 'featureArchivedZ',
description: 'terrible feature',
},
'test',
);
await app.services.featureToggleServiceV2.archiveToggle(
'featureArchivedZ',
'test',
);
await app.services.featureToggleServiceV2.createFeatureToggle(
'default',
{
name: 'feature.with.variants',
description: 'A feature toggle with variants',
variants: [
{ name: 'control', weight: 50 },
{ name: 'new', weight: 50 },
],
},
'test',
);
}); });
afterAll(async () => { afterAll(async () => {
@ -138,10 +96,8 @@ test('revives a feature by name', async () => {
test('archived feature is not accessible via /features/:featureName', async () => { test('archived feature is not accessible via /features/:featureName', async () => {
expect.assertions(0); expect.assertions(0);
return app.request await app.getFeatures('featureArchivedZ', 404);
.get('/api/admin/features/featureArchivedZ') await app.getProjectFeatures('default', 'featureArchivedZ', 404);
.set('Content-Type', 'application/json')
.expect(404);
}); });
test('must set name when reviving toggle', async () => { test('must set name when reviving toggle', async () => {
@ -267,8 +223,8 @@ test('Should be able to bulk archive features', async () => {
const featureName1 = 'archivedFeature1'; const featureName1 = 'archivedFeature1';
const featureName2 = 'archivedFeature2'; const featureName2 = 'archivedFeature2';
await createFeatureToggle(featureName1); await app.createFeature(featureName1);
await createFeatureToggle(featureName2); await app.createFeature(featureName2);
await app.request await app.request
.post(`/api/admin/projects/${DEFAULT_PROJECT}/archive`) .post(`/api/admin/projects/${DEFAULT_PROJECT}/archive`)

View File

@ -33,15 +33,6 @@ const sortOrderFirst = 0;
const sortOrderSecond = 10; const sortOrderSecond = 10;
const sortOrderDefault = 9999; const sortOrderDefault = 9999;
const createFeatureToggle = (
featureName: string,
project = DEFAULT_PROJECT,
) => {
return app.request.post(`/api/admin/projects/${project}/features`).send({
name: featureName,
});
};
const createSegment = async (segmentName: string) => { const createSegment = async (segmentName: string) => {
const segment = await app.services.segmentService.create( const segment = await app.services.segmentService.create(
{ {
@ -317,6 +308,7 @@ test('Disconnecting environment from project, removes environment from features
test('Can enable/disable environment for feature with strategies', async () => { test('Can enable/disable environment for feature with strategies', async () => {
const envName = 'enable-feature-environment'; const envName = 'enable-feature-environment';
const featureName = 'com.test.enable.environment'; const featureName = 'com.test.enable.environment';
const project = 'default';
// Create environment // Create environment
await db.stores.environmentStore.create({ await db.stores.environmentStore.create({
name: envName, name: envName,
@ -324,29 +316,22 @@ test('Can enable/disable environment for feature with strategies', async () => {
}); });
// Connect environment to project // Connect environment to project
await app.request await app.request
.post('/api/admin/projects/default/environments') .post(`/api/admin/projects/${project}/environments`)
.send({ .send({
environment: envName, environment: envName,
}) })
.expect(200); .expect(200);
// Create feature // Create feature
await app.request await app.createFeature(featureName).expect((res) => {
.post('/api/admin/projects/default/features') expect(res.body.name).toBe(featureName);
.send({ expect(res.body.createdAt).toBeTruthy();
name: featureName, });
})
.set('Content-Type', 'application/json')
.expect(201)
.expect((res) => {
expect(res.body.name).toBe(featureName);
expect(res.body.createdAt).toBeTruthy();
});
// Add strategy to it // Add strategy to it
await app.request await app.request
.post( .post(
`/api/admin/projects/default/features/${featureName}/environments/${envName}/strategies`, `/api/admin/projects/${project}/features/${featureName}/environments/${envName}/strategies`,
) )
.send({ .send({
name: 'default', name: 'default',
@ -357,38 +342,30 @@ test('Can enable/disable environment for feature with strategies', async () => {
.expect(200); .expect(200);
await app.request await app.request
.post( .post(
`/api/admin/projects/default/features/${featureName}/environments/${envName}/on`, `/api/admin/projects/${project}/features/${featureName}/environments/${envName}/on`,
) )
.set('Content-Type', 'application/json') .set('Content-Type', 'application/json')
.expect(200); .expect(200);
await app.request await app.getProjectFeatures(project, featureName).expect((res) => {
.get(`/api/admin/projects/default/features/${featureName}`) const enabledFeatureEnv = res.body.environments.find(
.expect(200) (e) => e.name === 'enable-feature-environment',
.expect('Content-Type', /json/) );
.expect((res) => { expect(enabledFeatureEnv).toBeTruthy();
const enabledFeatureEnv = res.body.environments.find( expect(enabledFeatureEnv.enabled).toBe(true);
(e) => e.name === 'enable-feature-environment', });
);
expect(enabledFeatureEnv).toBeTruthy();
expect(enabledFeatureEnv.enabled).toBe(true);
});
await app.request await app.request
.post( .post(
`/api/admin/projects/default/features/${featureName}/environments/${envName}/off`, `/api/admin/projects/${project}/features/${featureName}/environments/${envName}/off`,
) )
.send({}) .send({})
.expect(200); .expect(200);
await app.request await app.getProjectFeatures(project, featureName).expect((res) => {
.get(`/api/admin/projects/default/features/${featureName}`) const disabledFeatureEnv = res.body.environments.find(
.expect(200) (e) => e.name === 'enable-feature-environment',
.expect('Content-Type', /json/) );
.expect((res) => { expect(disabledFeatureEnv).toBeTruthy();
const disabledFeatureEnv = res.body.environments.find( expect(disabledFeatureEnv.enabled).toBe(false);
(e) => e.name === 'enable-feature-environment', });
);
expect(disabledFeatureEnv).toBeTruthy();
expect(disabledFeatureEnv.enabled).toBe(false);
});
}); });
test("Trying to get a project that doesn't exist yields 404", async () => { test("Trying to get a project that doesn't exist yields 404", async () => {
@ -1975,12 +1952,7 @@ test('Should not allow changing project to target project without the same enabl
'default', 'default',
); );
await app.request await app.createFeature(featureName, project);
.post(`/api/admin/projects/${project}/features`)
.send({
name: featureName,
})
.expect(201);
await app.request await app.request
.post( .post(
`/api/admin/projects/${project}/features/${featureName}/environments/default/strategies`, `/api/admin/projects/${project}/features/${featureName}/environments/default/strategies`,
@ -2060,12 +2032,7 @@ test('Should allow changing project to target project with the same enabled envi
); );
await db.stores.projectStore.addEnvironmentToProject(targetProject, inBoth); await db.stores.projectStore.addEnvironmentToProject(targetProject, inBoth);
await app.request await app.createFeature(featureName, project);
.post(`/api/admin/projects/${project}/features`)
.send({
name: featureName,
})
.expect(201);
await app.request await app.request
.post( .post(
`/api/admin/projects/${project}/features/${featureName}/environments/default/strategies`, `/api/admin/projects/${project}/features/${featureName}/environments/default/strategies`,
@ -2122,12 +2089,7 @@ test('Should allow changing project to target project with the same enabled envi
test(`a feature's variants should be sorted by name in increasing order`, async () => { test(`a feature's variants should be sorted by name in increasing order`, async () => {
const featureName = 'variants.are.sorted'; const featureName = 'variants.are.sorted';
const project = 'default'; const project = 'default';
await app.request await app.createFeature(featureName, project);
.post(`/api/admin/projects/${project}/features`)
.send({
name: featureName,
})
.expect(201);
const newVariants: IVariant[] = [ const newVariants: IVariant[] = [
{ {
@ -2652,7 +2614,7 @@ test('should return strategies in correct order when new strategies are added',
test('should create a strategy with segments', async () => { test('should create a strategy with segments', async () => {
const feature = { name: uuidv4(), impressionData: false }; const feature = { name: uuidv4(), impressionData: false };
await createFeatureToggle(feature.name); await app.createFeature(feature.name);
const segment = await createSegment('segmentOne'); const segment = await createSegment('segmentOne');
const { body: strategyOne } = await createStrategy(feature.name, { const { body: strategyOne } = await createStrategy(feature.name, {
name: 'default', name: 'default',
@ -2700,7 +2662,7 @@ test('should create a strategy with segments', async () => {
test('should add multiple segments to a strategy', async () => { test('should add multiple segments to a strategy', async () => {
const feature = { name: uuidv4(), impressionData: false }; const feature = { name: uuidv4(), impressionData: false };
await createFeatureToggle(feature.name); await app.createFeature(feature.name);
const segment = await createSegment('seg1'); const segment = await createSegment('seg1');
const segmentTwo = await createSegment('seg2'); const segmentTwo = await createSegment('seg2');
const segmentThree = await createSegment('seg3'); const segmentThree = await createSegment('seg3');
@ -2834,8 +2796,8 @@ test('Should batch stale features', async () => {
const staledFeatureName1 = 'staledFeature1'; const staledFeatureName1 = 'staledFeature1';
const staledFeatureName2 = 'staledFeature2'; const staledFeatureName2 = 'staledFeature2';
await createFeatureToggle(staledFeatureName1); await app.createFeature(staledFeatureName1);
await createFeatureToggle(staledFeatureName2); await app.createFeature(staledFeatureName2);
await app.request await app.request
.post(`/api/admin/projects/${DEFAULT_PROJECT}/stale`) .post(`/api/admin/projects/${DEFAULT_PROJECT}/stale`)

View File

@ -97,11 +97,7 @@ const createFeatureToggle = async (
{ status: 200 }, { status: 200 },
], ],
): Promise<void> => { ): Promise<void> => {
await app.request await app.createFeature(feature, project, expectStatusCode);
.post(`/api/admin/projects/${project}/features`)
.send(feature)
.expect(expectStatusCode);
let processed = 0; let processed = 0;
for (const strategy of strategies) { for (const strategy of strategies) {
const { body, status } = await app.request const { body, status } = await app.request

View File

@ -10,6 +10,8 @@ import { DEFAULT_PROJECT, IUnleashStores } from '../../../lib/types';
import { IUnleashServices } from '../../../lib/types/services'; import { IUnleashServices } from '../../../lib/types/services';
import { Db } from '../../../lib/db/db'; import { Db } from '../../../lib/db/db';
import { IContextFieldDto } from 'lib/types/stores/context-field-store'; import { IContextFieldDto } from 'lib/types/stores/context-field-store';
import { DEFAULT_ENV } from '../../../lib/util';
import { CreateFeatureSchema, ImportTogglesSchema } from '../../../lib/openapi';
process.env.NODE_ENV = 'test'; process.env.NODE_ENV = 'test';
@ -20,21 +22,47 @@ export interface IUnleashTest extends IUnleashHttpAPI {
config: IUnleashConfig; config: IUnleashConfig;
} }
/**
* This is a collection of API helpers. The response code is optional, and should default to the success code for the request.
*
* All functions return a supertest.Test object, which can be used to compose more assertions on the response.
*/
export interface IUnleashHttpAPI { export interface IUnleashHttpAPI {
createFeature( createFeature(
name: string, feature: string | CreateFeatureSchema,
project?: string, project?: string,
expectedResponseCode?: number, expectedResponseCode?: number,
): supertest.Test; ): supertest.Test;
getFeatures(name?: string, expectedResponseCode?: number): supertest.Test;
getProjectFeatures(
project: string,
name?: string,
expectedResponseCode?: number,
): supertest.Test;
archiveFeature( archiveFeature(
name: string, name: string,
project?: string, project?: string,
expectedResponseCode?: number, expectedResponseCode?: number,
): supertest.Test; ): supertest.Test;
createContextField( createContextField(
contextField: IContextFieldDto, contextField: IContextFieldDto,
expectedResponseCode?: number, expectedResponseCode?: number,
): supertest.Test; ): supertest.Test;
linkProjectToEnvironment(
project: string,
environment: string,
expectedResponseCode?: number,
): supertest.Test;
importToggles(
importPayload: ImportTogglesSchema,
expectedResponseCode?: number,
): supertest.Test;
} }
function httpApis( function httpApis(
@ -45,15 +73,44 @@ function httpApis(
return { return {
createFeature: ( createFeature: (
name: string, feature: string | CreateFeatureSchema,
project: string = DEFAULT_PROJECT, project: string = DEFAULT_PROJECT,
expectedResponseCode: number = 201, expectedResponseCode: number = 201,
) => { ) => {
let body = feature;
if (typeof feature === 'string') {
body = {
name: feature,
};
}
return request return request
.post(`${base}/api/admin/projects/${project}/features`) .post(`${base}/api/admin/projects/${project}/features`)
.send({ .send(body)
name, .set('Content-Type', 'application/json')
}) .expect(expectedResponseCode);
},
getFeatures(
name?: string,
expectedResponseCode: number = 200,
): supertest.Test {
const featuresUrl = `/api/admin/features${name ? `/${name}` : ''}`;
return request
.get(featuresUrl)
.set('Content-Type', 'application/json')
.expect(expectedResponseCode);
},
getProjectFeatures(
project: string = DEFAULT_PROJECT,
name?: string,
expectedResponseCode: number = 200,
): supertest.Test {
const featuresUrl = `/api/admin/projects/${project}/features${
name ? `/${name}` : ''
}`;
return request
.get(featuresUrl)
.set('Content-Type', 'application/json') .set('Content-Type', 'application/json')
.expect(expectedResponseCode); .expect(expectedResponseCode);
}, },
@ -80,6 +137,30 @@ function httpApis(
.send(contextField) .send(contextField)
.expect(expectedResponseCode); .expect(expectedResponseCode);
}, },
linkProjectToEnvironment(
project: string,
environment: string = DEFAULT_ENV,
expectedResponseCode: number = 200,
): supertest.Test {
return request
.post(`${base}/api/admin/projects/${project}/environments`)
.send({
environment,
})
.expect(expectedResponseCode);
},
importToggles(
importPayload: ImportTogglesSchema,
expectedResponseCode: number = 200,
): supertest.Test {
return request
.post('/api/admin/features-batch/import')
.send(importPayload)
.set('Content-Type', 'application/json')
.expect(expectedResponseCode);
},
}; };
} }