mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-14 01:16:17 +02:00
Removing the `flagsReleaseManagementUI` and `flagsOverviewSearch` feature flags - we're keeping these enabled.
1539 lines
41 KiB
TypeScript
1539 lines
41 KiB
TypeScript
import dbInit, {
|
|
type ITestDb,
|
|
} from '../../../test/e2e/helpers/database-init.js';
|
|
import {
|
|
insertLastSeenAt,
|
|
type IUnleashTest,
|
|
setupAppWithAuth,
|
|
} from '../../../test/e2e/helpers/test-helper.js';
|
|
import getLogger from '../../../test/fixtures/no-logger.js';
|
|
import type { FeatureSearchQueryParameters } from '../../openapi/spec/feature-search-query-parameters.js';
|
|
import {
|
|
CREATE_FEATURE_STRATEGY,
|
|
DEFAULT_PROJECT,
|
|
type IUnleashStores,
|
|
UPDATE_FEATURE_ENVIRONMENT,
|
|
} from '../../types/index.js';
|
|
import { DEFAULT_ENV } from '../../util/index.js';
|
|
|
|
let app: IUnleashTest;
|
|
let db: ITestDb;
|
|
let stores: IUnleashStores;
|
|
|
|
beforeAll(async () => {
|
|
db = await dbInit('feature_search', getLogger, {
|
|
dbInitMethod: 'legacy' as const,
|
|
});
|
|
app = await setupAppWithAuth(
|
|
db.stores,
|
|
{
|
|
experimental: {
|
|
flags: {
|
|
strictSchemaValidation: true,
|
|
anonymiseEventLog: true,
|
|
},
|
|
},
|
|
},
|
|
db.rawDatabase,
|
|
);
|
|
stores = db.stores;
|
|
|
|
const { body } = await app.request
|
|
.post(`/auth/demo/login`)
|
|
.send({
|
|
email: 'user@getunleash.io',
|
|
})
|
|
.expect(200);
|
|
|
|
await stores.environmentStore.create({
|
|
name: 'development',
|
|
type: 'development',
|
|
});
|
|
|
|
await app.linkProjectToEnvironment('default', 'development');
|
|
|
|
await stores.accessStore.addPermissionsToRole(
|
|
body.rootRole,
|
|
[
|
|
{ name: UPDATE_FEATURE_ENVIRONMENT },
|
|
{ name: CREATE_FEATURE_STRATEGY },
|
|
],
|
|
'development',
|
|
);
|
|
|
|
await stores.environmentStore.create({
|
|
name: 'production',
|
|
type: 'production',
|
|
});
|
|
|
|
await app.linkProjectToEnvironment('default', 'production');
|
|
|
|
await stores.accessStore.addPermissionsToRole(
|
|
body.rootRole,
|
|
[
|
|
{ name: UPDATE_FEATURE_ENVIRONMENT },
|
|
{ name: CREATE_FEATURE_STRATEGY },
|
|
],
|
|
'production',
|
|
);
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await app.destroy();
|
|
await db.destroy();
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await db.stores.dependentFeaturesStore.deleteAll();
|
|
await db.stores.featureToggleStore.deleteAll();
|
|
await db.stores.segmentStore.deleteAll();
|
|
});
|
|
|
|
const searchFeatures = async (
|
|
{
|
|
query = '',
|
|
project = 'IS:default',
|
|
archived = 'IS:false',
|
|
}: FeatureSearchQueryParameters,
|
|
expectedCode = 200,
|
|
) => {
|
|
return app.request
|
|
.get(
|
|
`/api/admin/search/features?query=${query}&project=${project}&archived=${archived}`,
|
|
)
|
|
.expect(expectedCode);
|
|
};
|
|
|
|
const searchFeaturesWithLifecycle = async (
|
|
{
|
|
query = '',
|
|
project = 'IS:default',
|
|
archived = 'IS:false',
|
|
lifecycle = 'IS:initial',
|
|
}: FeatureSearchQueryParameters,
|
|
expectedCode = 200,
|
|
) => {
|
|
return app.request
|
|
.get(
|
|
`/api/admin/search/features?query=${query}&project=${project}&archived=${archived}&lifecycle=${lifecycle}`,
|
|
)
|
|
.expect(expectedCode);
|
|
};
|
|
|
|
const sortFeatures = async (
|
|
{
|
|
sortBy = '',
|
|
sortOrder = '',
|
|
project = 'default',
|
|
favoritesFirst = 'false',
|
|
}: FeatureSearchQueryParameters,
|
|
expectedCode = 200,
|
|
) => {
|
|
return app.request
|
|
.get(
|
|
`/api/admin/search/features?sortBy=${sortBy}&sortOrder=${sortOrder}&project=IS:${project}&favoritesFirst=${favoritesFirst}`,
|
|
)
|
|
.expect(expectedCode);
|
|
};
|
|
|
|
const searchFeaturesWithOffset = async (
|
|
{
|
|
query = '',
|
|
project = 'default',
|
|
offset = '0',
|
|
limit = '10',
|
|
}: FeatureSearchQueryParameters,
|
|
expectedCode = 200,
|
|
) => {
|
|
return app.request
|
|
.get(
|
|
`/api/admin/search/features?query=${query}&project=IS:${project}&offset=${offset}&limit=${limit}`,
|
|
)
|
|
.expect(expectedCode);
|
|
};
|
|
|
|
const filterFeaturesByType = async (typeParams: string, expectedCode = 200) => {
|
|
return app.request
|
|
.get(`/api/admin/search/features?type=${typeParams}`)
|
|
.expect(expectedCode);
|
|
};
|
|
|
|
const filterFeaturesByCreatedBy = async (
|
|
createdByParams: string,
|
|
expectedCode = 200,
|
|
) => {
|
|
return app.request
|
|
.get(`/api/admin/search/features?createdBy=${createdByParams}`)
|
|
.expect(expectedCode);
|
|
};
|
|
|
|
const filterFeaturesByTag = async (tag: string, expectedCode = 200) => {
|
|
return app.request
|
|
.get(`/api/admin/search/features?tag=${tag}`)
|
|
.expect(expectedCode);
|
|
};
|
|
|
|
const filterFeaturesBySegment = async (segment: string, expectedCode = 200) => {
|
|
return app.request
|
|
.get(`/api/admin/search/features?segment=${segment}`)
|
|
.expect(expectedCode);
|
|
};
|
|
|
|
const filterFeaturesByState = async (state: string, expectedCode = 200) => {
|
|
return app.request
|
|
.get(`/api/admin/search/features?state=${state}`)
|
|
.expect(expectedCode);
|
|
};
|
|
|
|
const filterFeaturesByOperators = async (
|
|
state: string,
|
|
tag: string,
|
|
createdAt: string,
|
|
expectedCode = 200,
|
|
) => {
|
|
return app.request
|
|
.get(
|
|
`/api/admin/search/features?createdAt=${createdAt}&state=${state}&tag=${tag}`,
|
|
)
|
|
.expect(expectedCode);
|
|
};
|
|
|
|
const filterFeaturesByCreated = async (
|
|
createdAt: string,
|
|
expectedCode = 200,
|
|
) => {
|
|
return app.request
|
|
.get(`/api/admin/search/features?createdAt=${createdAt}`)
|
|
.expect(expectedCode);
|
|
};
|
|
|
|
const filterFeaturesByEnvironmentStatus = async (
|
|
environmentStatuses: string[],
|
|
expectedCode = 200,
|
|
) => {
|
|
const statuses = environmentStatuses
|
|
.map((status) => `status[]=${status}`)
|
|
.join('&');
|
|
return app.request
|
|
.get(`/api/admin/search/features?${statuses}`)
|
|
.expect(expectedCode);
|
|
};
|
|
|
|
const searchFeaturesWithoutQueryParams = async (expectedCode = 200) => {
|
|
return app.request.get(`/api/admin/search/features`).expect(expectedCode);
|
|
};
|
|
const getProjectArchive = async (projectId = 'default', expectedCode = 200) => {
|
|
return app.request
|
|
.get(
|
|
`/api/admin/search/features?project=IS%3A${projectId}&archived=IS%3Atrue`,
|
|
)
|
|
.expect(expectedCode);
|
|
};
|
|
|
|
test('should search matching features by name', async () => {
|
|
await app.createFeature('my_feature_a');
|
|
await app.createFeature('my_feature_b');
|
|
await app.createFeature('my_feat_c');
|
|
|
|
const { body } = await searchFeatures({ query: 'feature' });
|
|
|
|
expect(body).toMatchObject({
|
|
features: [
|
|
{
|
|
name: 'my_feature_a',
|
|
createdBy: {
|
|
id: 1,
|
|
name: '3957b71c0@unleash.run',
|
|
imageUrl:
|
|
'https://gravatar.com/avatar/3957b71c0a6d2528f03b423f432ed2efe855d263400f960248a1080493d9d68a?s=42&d=retro&r=g',
|
|
},
|
|
},
|
|
{
|
|
name: 'my_feature_b',
|
|
createdBy: {
|
|
id: 1,
|
|
name: '3957b71c0@unleash.run',
|
|
imageUrl:
|
|
'https://gravatar.com/avatar/3957b71c0a6d2528f03b423f432ed2efe855d263400f960248a1080493d9d68a?s=42&d=retro&r=g',
|
|
},
|
|
},
|
|
],
|
|
total: 2,
|
|
});
|
|
});
|
|
|
|
test('should paginate with offset', async () => {
|
|
await app.createFeature('my_feature_a');
|
|
await app.createFeature('my_feature_b');
|
|
await app.createFeature('my_feature_c');
|
|
await app.createFeature('my_feature_d');
|
|
|
|
const { body: firstPage, headers: firstHeaders } =
|
|
await searchFeaturesWithOffset({
|
|
query: 'feature',
|
|
offset: '0',
|
|
limit: '2',
|
|
});
|
|
|
|
expect(firstPage).toMatchObject({
|
|
features: [{ name: 'my_feature_a' }, { name: 'my_feature_b' }],
|
|
total: 4,
|
|
});
|
|
|
|
const { body: secondPage, headers: secondHeaders } =
|
|
await searchFeaturesWithOffset({
|
|
query: 'feature',
|
|
offset: '2',
|
|
limit: '2',
|
|
});
|
|
|
|
expect(secondPage).toMatchObject({
|
|
features: [{ name: 'my_feature_c' }, { name: 'my_feature_d' }],
|
|
total: 4,
|
|
});
|
|
});
|
|
|
|
test('should filter features by type', async () => {
|
|
await app.createFeature({
|
|
name: 'my_feature_a',
|
|
type: 'release',
|
|
});
|
|
await app.createFeature({
|
|
name: 'my_feature_b',
|
|
type: 'experiment',
|
|
});
|
|
|
|
const { body } = await filterFeaturesByType(
|
|
'IS_ANY_OF:experiment,kill-switch',
|
|
);
|
|
|
|
expect(body).toMatchObject({
|
|
features: [{ name: 'my_feature_b' }],
|
|
});
|
|
});
|
|
|
|
test('should filter features by created by', async () => {
|
|
await app.createFeature({
|
|
name: 'my_feature_a',
|
|
type: 'release',
|
|
});
|
|
await app.createFeature({
|
|
name: 'my_feature_b',
|
|
type: 'experiment',
|
|
});
|
|
|
|
const { body } = await filterFeaturesByCreatedBy('IS:1');
|
|
|
|
expect(body).toMatchObject({
|
|
features: [{ name: 'my_feature_a' }, { name: 'my_feature_b' }],
|
|
});
|
|
|
|
const { body: emptyResults } = await filterFeaturesByCreatedBy('IS:2');
|
|
|
|
expect(emptyResults).toMatchObject({
|
|
features: [],
|
|
});
|
|
});
|
|
|
|
test('should filter features by tag', async () => {
|
|
await app.createFeature('my_feature_a');
|
|
await app.addTag('my_feature_a', {
|
|
type: 'simple',
|
|
value: 'my_tag',
|
|
});
|
|
await app.createFeature('my_feature_b');
|
|
await app.createFeature('my_feature_c');
|
|
await app.addTag('my_feature_c', {
|
|
type: 'simple',
|
|
value: 'tag_c',
|
|
});
|
|
await app.createFeature('my_feature_d');
|
|
await app.addTag('my_feature_d', {
|
|
type: 'simple',
|
|
value: 'tag_c',
|
|
});
|
|
await app.addTag('my_feature_d', {
|
|
type: 'simple',
|
|
value: 'my_tag',
|
|
});
|
|
|
|
const { body } = await filterFeaturesByTag('INCLUDE:simple:my_tag');
|
|
|
|
expect(body).toMatchObject({
|
|
features: [{ name: 'my_feature_a' }, { name: 'my_feature_d' }],
|
|
});
|
|
|
|
const { body: notIncludeBody } = await filterFeaturesByTag(
|
|
'DO_NOT_INCLUDE:simple:my_tag',
|
|
);
|
|
|
|
expect(notIncludeBody).toMatchObject({
|
|
features: [{ name: 'my_feature_b' }, { name: 'my_feature_c' }],
|
|
});
|
|
|
|
const { body: includeAllOf } = await filterFeaturesByTag(
|
|
'INCLUDE_ALL_OF:simple:my_tag, simple:tag_c',
|
|
);
|
|
|
|
expect(includeAllOf).toMatchObject({
|
|
features: [{ name: 'my_feature_d' }],
|
|
});
|
|
|
|
const { body: includeAnyOf } = await filterFeaturesByTag(
|
|
'INCLUDE_ANY_OF:simple:my_tag, simple:tag_c',
|
|
);
|
|
|
|
expect(includeAnyOf).toMatchObject({
|
|
features: [
|
|
{ name: 'my_feature_a' },
|
|
{ name: 'my_feature_c' },
|
|
{ name: 'my_feature_d' },
|
|
],
|
|
});
|
|
|
|
const { body: excludeIfAnyOf } = await filterFeaturesByTag(
|
|
'EXCLUDE_IF_ANY_OF:simple:my_tag, simple:tag_c',
|
|
);
|
|
|
|
expect(excludeIfAnyOf).toMatchObject({
|
|
features: [{ name: 'my_feature_b' }],
|
|
});
|
|
|
|
const { body: excludeAll } = await filterFeaturesByTag(
|
|
'EXCLUDE_ALL:simple:my_tag, simple:tag_c',
|
|
);
|
|
|
|
expect(excludeAll).toMatchObject({
|
|
features: [
|
|
{ name: 'my_feature_a' },
|
|
{ name: 'my_feature_b' },
|
|
{ name: 'my_feature_c' },
|
|
],
|
|
});
|
|
|
|
await filterFeaturesByTag('EXCLUDE_ALL:simple', 400);
|
|
await filterFeaturesByTag('EXCLUDE_ALL:simple,simple', 400);
|
|
await filterFeaturesByTag('EXCLUDE_ALL:simple,simple:jest', 400);
|
|
});
|
|
|
|
test('should filter features by tag that has colon inside', async () => {
|
|
await app.createFeature('my_feature_a');
|
|
await app.addTag('my_feature_a', {
|
|
type: 'simple',
|
|
value: 'my_tag:colon',
|
|
});
|
|
|
|
const { body } = await filterFeaturesByTag('INCLUDE:simple:my_tag:colon');
|
|
|
|
expect(body).toMatchObject({
|
|
features: [{ name: 'my_feature_a' }],
|
|
});
|
|
});
|
|
|
|
test('should filter features by environment status', async () => {
|
|
await app.createFeature('my_feature_a');
|
|
await app.createFeature('my_feature_b');
|
|
await app.enableFeature('my_feature_a', 'default');
|
|
|
|
const { body } = await filterFeaturesByEnvironmentStatus([
|
|
'default:enabled',
|
|
'nonexistentEnv:disabled',
|
|
'default:wrongStatus',
|
|
]);
|
|
|
|
expect(body).toMatchObject({
|
|
features: [{ name: 'my_feature_a' }],
|
|
});
|
|
});
|
|
|
|
test('should return all feature tags', async () => {
|
|
await app.createFeature('my_feature_a');
|
|
await app.addTag('my_feature_a', {
|
|
type: 'simple',
|
|
value: 'my_tag',
|
|
});
|
|
await app.addTag('my_feature_a', {
|
|
type: 'simple',
|
|
value: 'second_tag',
|
|
});
|
|
|
|
const { body } = await searchFeatures({});
|
|
|
|
expect(body).toMatchObject({
|
|
features: [
|
|
{
|
|
name: 'my_feature_a',
|
|
tags: [
|
|
{
|
|
type: 'simple',
|
|
value: 'my_tag',
|
|
},
|
|
{
|
|
type: 'simple',
|
|
value: 'second_tag',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test('should return empty features', async () => {
|
|
const { body } = await searchFeatures({ query: '' });
|
|
expect(body).toMatchObject({ features: [] });
|
|
});
|
|
|
|
test('should not search features from another project', async () => {
|
|
await app.createFeature('my_feature_a');
|
|
await app.createFeature('my_feature_b');
|
|
|
|
const { body } = await searchFeatures({
|
|
query: '',
|
|
project: 'IS:another_project',
|
|
});
|
|
|
|
expect(body).toMatchObject({ features: [] });
|
|
});
|
|
|
|
test('should return features without query', async () => {
|
|
await app.createFeature('my_feature_a');
|
|
await app.createFeature('my_feature_b');
|
|
|
|
const { body } = await searchFeaturesWithoutQueryParams();
|
|
|
|
expect(body).toMatchObject({
|
|
features: [{ name: 'my_feature_a' }, { name: 'my_feature_b' }],
|
|
});
|
|
});
|
|
|
|
test('should sort features', async () => {
|
|
await app.createFeature('my_feature_a');
|
|
await app.createFeature('my_feature_c');
|
|
await app.createFeature('my_feature_b');
|
|
await app.enableFeature('my_feature_c', 'default');
|
|
await app.favoriteFeature('my_feature_b');
|
|
|
|
await insertLastSeenAt('my_feature_c', db.rawDatabase, 'default');
|
|
|
|
const { body: ascName } = await sortFeatures({
|
|
sortBy: 'name',
|
|
sortOrder: 'asc',
|
|
});
|
|
|
|
expect(ascName).toMatchObject({
|
|
features: [
|
|
{ name: 'my_feature_a' },
|
|
{ name: 'my_feature_b' },
|
|
{ name: 'my_feature_c' },
|
|
],
|
|
total: 3,
|
|
});
|
|
|
|
const { body: descName } = await sortFeatures({
|
|
sortBy: 'name',
|
|
sortOrder: 'desc',
|
|
});
|
|
|
|
expect(descName).toMatchObject({
|
|
features: [
|
|
{ name: 'my_feature_c' },
|
|
{ name: 'my_feature_b' },
|
|
{ name: 'my_feature_a' },
|
|
],
|
|
total: 3,
|
|
});
|
|
|
|
const { body: defaultCreatedAt } = await sortFeatures({
|
|
sortBy: '',
|
|
sortOrder: 'asc',
|
|
});
|
|
|
|
expect(defaultCreatedAt).toMatchObject({
|
|
features: [
|
|
{ name: 'my_feature_a' },
|
|
{ name: 'my_feature_c' },
|
|
{ name: 'my_feature_b' },
|
|
],
|
|
total: 3,
|
|
});
|
|
|
|
const { body: environmentAscSort } = await sortFeatures({
|
|
sortBy: 'environment:default',
|
|
sortOrder: 'asc',
|
|
});
|
|
|
|
expect(environmentAscSort).toMatchObject({
|
|
features: [
|
|
{ name: 'my_feature_a' },
|
|
{ name: 'my_feature_b' },
|
|
{ name: 'my_feature_c' },
|
|
],
|
|
total: 3,
|
|
});
|
|
|
|
const { body: environmentDescSort } = await sortFeatures({
|
|
sortBy: 'environment:default',
|
|
sortOrder: 'desc',
|
|
});
|
|
|
|
expect(environmentDescSort).toMatchObject({
|
|
features: [
|
|
{ name: 'my_feature_c' },
|
|
{ name: 'my_feature_a' },
|
|
{ name: 'my_feature_b' },
|
|
],
|
|
total: 3,
|
|
});
|
|
|
|
const { body: favoriteEnvironmentDescSort } = await sortFeatures({
|
|
sortBy: 'environment:default',
|
|
sortOrder: 'desc',
|
|
favoritesFirst: 'true',
|
|
});
|
|
|
|
expect(favoriteEnvironmentDescSort).toMatchObject({
|
|
features: [
|
|
{ name: 'my_feature_b' },
|
|
{ name: 'my_feature_c' },
|
|
{ name: 'my_feature_a' },
|
|
],
|
|
total: 3,
|
|
});
|
|
|
|
const { body: lastSeenAscSort } = await sortFeatures({
|
|
sortBy: 'lastSeenAt',
|
|
sortOrder: 'asc',
|
|
});
|
|
|
|
expect(lastSeenAscSort).toMatchObject({
|
|
features: [
|
|
{ name: 'my_feature_c' },
|
|
{ name: 'my_feature_a' },
|
|
{ name: 'my_feature_b' },
|
|
],
|
|
total: 3,
|
|
});
|
|
});
|
|
|
|
test('should sort features when feature names are numbers', async () => {
|
|
await app.createFeature('my_feature_a');
|
|
await app.createFeature('my_feature_c');
|
|
await app.createFeature('my_feature_b');
|
|
await app.createFeature('1234');
|
|
await app.favoriteFeature('my_feature_b');
|
|
|
|
const { body: favoriteSortByName } = await sortFeatures({
|
|
sortBy: 'name',
|
|
sortOrder: 'asc',
|
|
favoritesFirst: 'true',
|
|
});
|
|
|
|
expect(favoriteSortByName).toMatchObject({
|
|
features: [
|
|
{ name: 'my_feature_b' },
|
|
{ name: '1234' },
|
|
{ name: 'my_feature_a' },
|
|
{ name: 'my_feature_c' },
|
|
],
|
|
total: 4,
|
|
});
|
|
});
|
|
|
|
test('should paginate correctly when using tags', async () => {
|
|
await app.createFeature('my_feature_a');
|
|
await app.createFeature('my_feature_b');
|
|
await app.createFeature('my_feature_c');
|
|
await app.createFeature('my_feature_d');
|
|
|
|
await app.addTag('my_feature_b', {
|
|
type: 'simple',
|
|
value: 'first_tag',
|
|
});
|
|
await app.addTag('my_feature_b', {
|
|
type: 'simple',
|
|
value: 'second_tag',
|
|
});
|
|
await app.addTag('my_feature_a', {
|
|
type: 'simple',
|
|
value: 'second_tag',
|
|
});
|
|
await app.addTag('my_feature_c', {
|
|
type: 'simple',
|
|
value: 'second_tag',
|
|
});
|
|
await app.addTag('my_feature_c', {
|
|
type: 'simple',
|
|
value: 'first_tag',
|
|
});
|
|
|
|
const { body: secondPage } = await searchFeaturesWithOffset({
|
|
query: 'feature',
|
|
offset: '2',
|
|
limit: '2',
|
|
});
|
|
|
|
expect(secondPage).toMatchObject({
|
|
features: [{ name: 'my_feature_c' }, { name: 'my_feature_d' }],
|
|
total: 4,
|
|
});
|
|
});
|
|
|
|
test('should not return duplicate entries when sorting by environments', async () => {
|
|
await app.createFeature('my_feature_a');
|
|
await app.createFeature('my_feature_b');
|
|
await app.createFeature('my_feature_c');
|
|
await app.enableFeature('my_feature_a', 'production');
|
|
await app.enableFeature('my_feature_b', 'production');
|
|
|
|
const { body } = await sortFeatures({
|
|
sortBy: 'environment:production',
|
|
sortOrder: 'desc',
|
|
});
|
|
|
|
expect(body).toMatchObject({
|
|
features: [
|
|
{ name: 'my_feature_a' },
|
|
{ name: 'my_feature_b' },
|
|
{ name: 'my_feature_c' },
|
|
],
|
|
total: 3,
|
|
});
|
|
|
|
const { body: ascendingBody } = await sortFeatures({
|
|
sortBy: 'environment:production',
|
|
sortOrder: 'asc',
|
|
});
|
|
|
|
expect(ascendingBody).toMatchObject({
|
|
features: [
|
|
{ name: 'my_feature_c' },
|
|
{ name: 'my_feature_a' },
|
|
{ name: 'my_feature_b' },
|
|
],
|
|
total: 3,
|
|
});
|
|
});
|
|
|
|
test('should search features by description', async () => {
|
|
const description = 'secretdescription';
|
|
await app.createFeature('my_feature_a');
|
|
await app.createFeature({
|
|
name: 'my_feature_b',
|
|
description,
|
|
});
|
|
|
|
const { body } = await searchFeatures({
|
|
query: 'descr',
|
|
});
|
|
expect(body).toMatchObject({
|
|
features: [
|
|
{
|
|
name: 'my_feature_b',
|
|
description,
|
|
project: DEFAULT_PROJECT,
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test('should support multiple search values', async () => {
|
|
const description = 'secretdescription';
|
|
await app.createFeature('my_feature_a');
|
|
await app.createFeature({
|
|
name: 'my_feature_b',
|
|
description,
|
|
});
|
|
await app.createFeature('my_feature_c');
|
|
|
|
const { body } = await searchFeatures({
|
|
query: 'descr,c',
|
|
});
|
|
expect(body).toMatchObject({
|
|
features: [
|
|
{
|
|
name: 'my_feature_b',
|
|
description,
|
|
},
|
|
{ name: 'my_feature_c' },
|
|
],
|
|
});
|
|
|
|
const { body: emptyQuery } = await searchFeatures({
|
|
query: ' , ',
|
|
});
|
|
expect(emptyQuery).toMatchObject({
|
|
features: [
|
|
{ name: 'my_feature_a' },
|
|
{ name: 'my_feature_b' },
|
|
{ name: 'my_feature_c' },
|
|
],
|
|
});
|
|
});
|
|
|
|
test('should search features by project with operators', async () => {
|
|
await app.createFeature('my_feature_a');
|
|
|
|
await db.stores.projectStore.create({
|
|
name: 'project_b',
|
|
description: '',
|
|
id: 'project_b',
|
|
});
|
|
|
|
await db.stores.featureToggleStore.create('project_b', {
|
|
name: 'my_feature_b',
|
|
createdByUserId: 9999,
|
|
});
|
|
|
|
await db.stores.projectStore.create({
|
|
name: 'project_c',
|
|
description: '',
|
|
id: 'project_c',
|
|
});
|
|
|
|
await db.stores.featureToggleStore.create('project_c', {
|
|
name: 'my_feature_c',
|
|
createdByUserId: 9999,
|
|
});
|
|
|
|
const { body } = await searchFeatures({
|
|
project: 'IS:default',
|
|
});
|
|
expect(body).toMatchObject({
|
|
features: [{ name: 'my_feature_a' }],
|
|
});
|
|
|
|
const { body: isNotBody } = await searchFeatures({
|
|
project: 'IS_NOT:default',
|
|
});
|
|
expect(isNotBody).toMatchObject({
|
|
features: [{ name: 'my_feature_b' }, { name: 'my_feature_c' }],
|
|
});
|
|
|
|
const { body: isAnyOfBody } = await searchFeatures({
|
|
project: 'IS_ANY_OF:default,project_c',
|
|
});
|
|
expect(isAnyOfBody).toMatchObject({
|
|
features: [{ name: 'my_feature_a' }, { name: 'my_feature_c' }],
|
|
});
|
|
|
|
const { body: isNotAnyBody } = await searchFeatures({
|
|
project: 'IS_NONE_OF:default,project_c',
|
|
});
|
|
expect(isNotAnyBody).toMatchObject({
|
|
features: [{ name: 'my_feature_b' }],
|
|
});
|
|
});
|
|
|
|
test('should return segments in payload with no duplicates/nulls', async () => {
|
|
await app.createFeature('my_feature_a');
|
|
const { body: mySegment } = await app.createSegment({
|
|
name: 'my_segment_a',
|
|
constraints: [],
|
|
});
|
|
await app.addStrategyToFeatureEnv(
|
|
{
|
|
name: 'default',
|
|
segments: [mySegment.id],
|
|
},
|
|
DEFAULT_ENV,
|
|
'my_feature_a',
|
|
);
|
|
await app.enableFeature('my_feature_a', 'development');
|
|
|
|
const { body } = await searchFeatures({});
|
|
|
|
expect(body).toMatchObject({
|
|
features: [
|
|
{
|
|
name: 'my_feature_a',
|
|
segments: [mySegment.name],
|
|
environments: [
|
|
{
|
|
name: 'default',
|
|
hasStrategies: true,
|
|
hasEnabledStrategies: true,
|
|
},
|
|
{
|
|
name: 'development',
|
|
hasStrategies: true,
|
|
hasEnabledStrategies: true,
|
|
},
|
|
{
|
|
name: 'production',
|
|
hasStrategies: false,
|
|
hasEnabledStrategies: false,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test('should filter features by segment', async () => {
|
|
await app.createFeature('my_feature_a');
|
|
const { body: mySegmentA } = await app.createSegment({
|
|
name: 'my_segment_a',
|
|
constraints: [],
|
|
});
|
|
await app.addStrategyToFeatureEnv(
|
|
{
|
|
name: 'default',
|
|
segments: [mySegmentA.id],
|
|
},
|
|
DEFAULT_ENV,
|
|
'my_feature_a',
|
|
);
|
|
await app.createFeature('my_feature_b');
|
|
await app.createFeature('my_feature_c');
|
|
const { body: mySegmentC } = await app.createSegment({
|
|
name: 'my_segment_c',
|
|
constraints: [],
|
|
});
|
|
await app.addStrategyToFeatureEnv(
|
|
{
|
|
name: 'default',
|
|
segments: [mySegmentC.id],
|
|
},
|
|
DEFAULT_ENV,
|
|
'my_feature_c',
|
|
);
|
|
await app.createFeature('my_feature_d');
|
|
await app.addStrategyToFeatureEnv(
|
|
{
|
|
name: 'default',
|
|
segments: [mySegmentC.id],
|
|
},
|
|
DEFAULT_ENV,
|
|
'my_feature_d',
|
|
);
|
|
await app.addStrategyToFeatureEnv(
|
|
{
|
|
name: 'default',
|
|
segments: [mySegmentA.id],
|
|
},
|
|
DEFAULT_ENV,
|
|
'my_feature_d',
|
|
);
|
|
|
|
const { body } = await filterFeaturesBySegment('INCLUDE:my_segment_a');
|
|
|
|
expect(body).toMatchObject({
|
|
features: [{ name: 'my_feature_a' }, { name: 'my_feature_d' }],
|
|
});
|
|
|
|
const { body: notIncludeBody } = await filterFeaturesBySegment(
|
|
'DO_NOT_INCLUDE:my_segment_a',
|
|
);
|
|
|
|
expect(notIncludeBody).toMatchObject({
|
|
features: [{ name: 'my_feature_b' }, { name: 'my_feature_c' }],
|
|
});
|
|
|
|
const { body: includeAllOf } = await filterFeaturesBySegment(
|
|
'INCLUDE_ALL_OF:my_segment_a, my_segment_c',
|
|
);
|
|
|
|
expect(includeAllOf).toMatchObject({
|
|
features: [{ name: 'my_feature_d' }],
|
|
});
|
|
|
|
const { body: includeAnyOf } = await filterFeaturesBySegment(
|
|
'INCLUDE_ANY_OF:my_segment_a, my_segment_c',
|
|
);
|
|
|
|
expect(includeAnyOf).toMatchObject({
|
|
features: [
|
|
{ name: 'my_feature_a' },
|
|
{ name: 'my_feature_c' },
|
|
{ name: 'my_feature_d' },
|
|
],
|
|
});
|
|
|
|
const { body: excludeIfAnyOf } = await filterFeaturesBySegment(
|
|
'EXCLUDE_IF_ANY_OF:my_segment_a, my_segment_c',
|
|
);
|
|
|
|
expect(excludeIfAnyOf).toMatchObject({
|
|
features: [{ name: 'my_feature_b' }],
|
|
});
|
|
|
|
const { body: excludeAll } = await filterFeaturesBySegment(
|
|
'EXCLUDE_ALL:my_segment_a, my_segment_c',
|
|
);
|
|
|
|
expect(excludeAll).toMatchObject({
|
|
features: [
|
|
{ name: 'my_feature_a' },
|
|
{ name: 'my_feature_b' },
|
|
{ name: 'my_feature_c' },
|
|
],
|
|
});
|
|
});
|
|
|
|
test('should search features by state with operators', async () => {
|
|
await app.createFeature({
|
|
name: 'my_feature_a',
|
|
stale: false,
|
|
});
|
|
await app.createFeature({
|
|
name: 'my_feature_b',
|
|
stale: true,
|
|
});
|
|
await app.createFeature({
|
|
name: 'my_feature_c',
|
|
stale: true,
|
|
});
|
|
|
|
const { body } = await filterFeaturesByState('IS:active');
|
|
expect(body).toMatchObject({
|
|
features: [{ name: 'my_feature_a' }],
|
|
});
|
|
|
|
const { body: isNotBody } = await filterFeaturesByState('IS_NOT:active');
|
|
expect(isNotBody).toMatchObject({
|
|
features: [{ name: 'my_feature_b' }, { name: 'my_feature_c' }],
|
|
});
|
|
|
|
const { body: isAnyOfBody } = await filterFeaturesByState(
|
|
'IS_ANY_OF:active, stale',
|
|
);
|
|
expect(isAnyOfBody).toMatchObject({
|
|
features: [
|
|
{ name: 'my_feature_a' },
|
|
{ name: 'my_feature_b' },
|
|
{ name: 'my_feature_c' },
|
|
],
|
|
});
|
|
|
|
const { body: isNotAnyBody } = await filterFeaturesByState(
|
|
'IS_NONE_OF:active, stale',
|
|
);
|
|
expect(isNotAnyBody).toMatchObject({
|
|
features: [],
|
|
});
|
|
});
|
|
|
|
test('should search features by potentially stale', async () => {
|
|
await app.createFeature({
|
|
name: 'my_feature_a',
|
|
stale: false,
|
|
});
|
|
await app.createFeature({
|
|
name: 'my_feature_b',
|
|
stale: true,
|
|
});
|
|
await app.createFeature({
|
|
name: 'my_feature_c',
|
|
stale: false,
|
|
});
|
|
await app.createFeature({
|
|
name: 'my_feature_d',
|
|
stale: true,
|
|
});
|
|
|
|
// this is all done on a schedule, so there's no imperative way to mark something as potentially stale today.
|
|
await db
|
|
.rawDatabase('features')
|
|
.update('potentially_stale', true)
|
|
.whereIn('name', ['my_feature_c', 'my_feature_d']);
|
|
|
|
const check = async (filter: string, expectedFlags: string[]) => {
|
|
const { body } = await filterFeaturesByState(filter);
|
|
expect(body).toMatchObject({
|
|
features: expectedFlags.map((flag) => ({ name: flag })),
|
|
});
|
|
};
|
|
|
|
// single filters work
|
|
await check('IS:potentially-stale', ['my_feature_c']);
|
|
// (stale or !potentially-stale)
|
|
await check('IS_NOT:potentially-stale', [
|
|
'my_feature_a',
|
|
'my_feature_b',
|
|
'my_feature_d',
|
|
]);
|
|
|
|
// combo filters work
|
|
await check('IS_ANY_OF:active,potentially-stale', [
|
|
'my_feature_a',
|
|
'my_feature_c',
|
|
]);
|
|
|
|
// (potentially-stale OR stale)
|
|
await check('IS_ANY_OF:potentially-stale, stale', [
|
|
'my_feature_b',
|
|
'my_feature_c',
|
|
'my_feature_d',
|
|
]);
|
|
|
|
await check('IS_ANY_OF:active,potentially-stale,stale', [
|
|
'my_feature_a',
|
|
'my_feature_b',
|
|
'my_feature_c',
|
|
'my_feature_d',
|
|
]);
|
|
|
|
await check('IS_NONE_OF:active,potentially-stale,stale', []);
|
|
|
|
await check('IS_NONE_OF:active,potentially-stale', [
|
|
'my_feature_b',
|
|
'my_feature_d',
|
|
]);
|
|
|
|
await check('IS_NONE_OF:potentially-stale,stale', ['my_feature_a']);
|
|
});
|
|
|
|
test('should filter features by combined operators', async () => {
|
|
await app.createFeature({
|
|
name: 'my_feature_a',
|
|
createdAt: '2023-01-27T15:21:39.975Z',
|
|
stale: true,
|
|
});
|
|
await app.createFeature({
|
|
name: 'my_feature_b',
|
|
createdAt: '2023-01-29T15:21:39.975Z',
|
|
});
|
|
|
|
await app.addTag('my_feature_b', {
|
|
type: 'simple',
|
|
value: 'my_tag',
|
|
});
|
|
|
|
const { body } = await filterFeaturesByOperators(
|
|
'IS_NOT:active',
|
|
'DO_NOT_INCLUDE:simple:my_tag',
|
|
'IS_BEFORE:2023-01-28',
|
|
);
|
|
expect(body).toMatchObject({
|
|
features: [{ name: 'my_feature_a' }],
|
|
});
|
|
});
|
|
|
|
test('should return environment usage metrics and lifecycle', async () => {
|
|
await app.createFeature({
|
|
name: 'my_feature_b',
|
|
createdAt: '2023-01-29T15:21:39.975Z',
|
|
});
|
|
await app.createFeature({
|
|
name: 'my_feature_c',
|
|
createdAt: '2023-01-29T15:21:39.975Z',
|
|
});
|
|
|
|
await stores.clientMetricsStoreV2.batchInsertMetrics([
|
|
{
|
|
featureName: `my_feature_b`,
|
|
appName: `web`,
|
|
environment: 'development',
|
|
timestamp: new Date(),
|
|
yes: 5,
|
|
no: 2,
|
|
},
|
|
{
|
|
featureName: `my_feature_b`,
|
|
appName: `web2`,
|
|
environment: 'development',
|
|
timestamp: new Date(),
|
|
yes: 5,
|
|
no: 2,
|
|
},
|
|
{
|
|
featureName: `my_feature_b`,
|
|
appName: `web`,
|
|
environment: 'production',
|
|
timestamp: new Date(),
|
|
yes: 2,
|
|
no: 2,
|
|
},
|
|
]);
|
|
|
|
await stores.featureLifecycleStore.insert([
|
|
{ feature: 'my_feature_b', stage: 'initial' },
|
|
]);
|
|
await stores.featureLifecycleStore.insert([
|
|
{ feature: 'my_feature_c', stage: 'initial' },
|
|
]);
|
|
await stores.featureLifecycleStore.insert([
|
|
{ feature: 'my_feature_b', stage: 'completed', status: 'discarded' },
|
|
]);
|
|
|
|
const { body: noExplicitLifecycle } = await searchFeatures({
|
|
query: 'my_feature_b',
|
|
});
|
|
expect(noExplicitLifecycle).toMatchObject({
|
|
total: 1,
|
|
features: [
|
|
{
|
|
name: 'my_feature_b',
|
|
lifecycle: { stage: 'completed', status: 'discarded' },
|
|
environments: [
|
|
{
|
|
name: 'default',
|
|
yes: 0,
|
|
no: 0,
|
|
},
|
|
{
|
|
name: 'development',
|
|
yes: 10,
|
|
no: 4,
|
|
},
|
|
{
|
|
name: 'production',
|
|
yes: 2,
|
|
no: 2,
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
|
|
const { body: noFeaturesWithOtherLifecycle } =
|
|
await searchFeaturesWithLifecycle({
|
|
query: 'my_feature_b',
|
|
lifecycle: 'IS:initial',
|
|
});
|
|
expect(noFeaturesWithOtherLifecycle).toMatchObject({
|
|
total: 0,
|
|
features: [],
|
|
});
|
|
|
|
const { body: featureWithMatchingLifecycle } =
|
|
await searchFeaturesWithLifecycle({
|
|
lifecycle: 'IS:completed',
|
|
});
|
|
expect(featureWithMatchingLifecycle).toMatchObject({
|
|
total: 1,
|
|
features: [{ name: 'my_feature_b' }],
|
|
});
|
|
});
|
|
|
|
test('should return dependencyType', async () => {
|
|
await app.createFeature({
|
|
name: 'my_feature_a',
|
|
createdAt: '2023-01-29T15:21:39.975Z',
|
|
});
|
|
await app.createFeature({
|
|
name: 'my_feature_b',
|
|
createdAt: '2023-01-29T15:21:39.975Z',
|
|
});
|
|
await app.createFeature({
|
|
name: 'my_feature_c',
|
|
createdAt: '2023-01-29T15:21:39.975Z',
|
|
});
|
|
await app.createFeature({
|
|
name: 'my_feature_d',
|
|
createdAt: '2023-01-29T15:21:39.975Z',
|
|
});
|
|
|
|
await stores.dependentFeaturesStore.upsert({
|
|
child: 'my_feature_b',
|
|
parent: 'my_feature_a',
|
|
enabled: true,
|
|
});
|
|
await stores.dependentFeaturesStore.upsert({
|
|
child: 'my_feature_c',
|
|
parent: 'my_feature_a',
|
|
enabled: true,
|
|
});
|
|
|
|
const { body } = await searchFeatures({
|
|
query: 'my_feature',
|
|
});
|
|
expect(body).toMatchObject({
|
|
features: [
|
|
{
|
|
name: 'my_feature_a',
|
|
dependencyType: 'parent',
|
|
},
|
|
{
|
|
name: 'my_feature_b',
|
|
dependencyType: 'child',
|
|
},
|
|
{
|
|
name: 'my_feature_c',
|
|
dependencyType: 'child',
|
|
},
|
|
{
|
|
name: 'my_feature_d',
|
|
dependencyType: null,
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test('should return archived when query param set', async () => {
|
|
await app.createFeature({
|
|
name: 'my_feature_a',
|
|
createdAt: '2023-01-29T15:21:39.975Z',
|
|
});
|
|
await app.createFeature({
|
|
name: 'my_feature_b',
|
|
createdAt: '2023-01-29T15:21:39.975Z',
|
|
archived: true,
|
|
});
|
|
|
|
const { body } = await searchFeatures({
|
|
query: 'my_feature',
|
|
});
|
|
expect(body).toMatchObject({
|
|
features: [
|
|
{
|
|
name: 'my_feature_a',
|
|
archivedAt: null,
|
|
},
|
|
],
|
|
});
|
|
|
|
const { body: archivedFeatures } = await searchFeatures({
|
|
query: 'my_feature',
|
|
archived: 'IS:true',
|
|
});
|
|
|
|
const { body: archive } = await getProjectArchive();
|
|
|
|
expect(archivedFeatures).toMatchObject({
|
|
features: [
|
|
{
|
|
name: 'my_feature_b',
|
|
archivedAt: archive.features[0].archivedAt,
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test('should return tags with color information from tag type', async () => {
|
|
await app.createFeature('my_feature_a');
|
|
|
|
await app.request
|
|
.put('/api/admin/tag-types/simple')
|
|
.send({
|
|
name: 'simple',
|
|
color: '#FF0000',
|
|
})
|
|
.expect(200);
|
|
|
|
await app.addTag('my_feature_a', {
|
|
type: 'simple',
|
|
value: 'my_tag',
|
|
});
|
|
|
|
const { body } = await searchFeatures({});
|
|
|
|
expect(body).toMatchObject({
|
|
features: [
|
|
{
|
|
name: 'my_feature_a',
|
|
tags: [
|
|
{
|
|
type: 'simple',
|
|
value: 'my_tag',
|
|
color: '#FF0000',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
const createChangeRequest = async ({
|
|
id,
|
|
feature,
|
|
environment,
|
|
state,
|
|
}: { id: number; feature: string; environment: string; state: string }) => {
|
|
await db
|
|
.rawDatabase('change_requests')
|
|
.insert({ id, environment, state, project: 'default', created_by: 1 });
|
|
await db.rawDatabase('change_request_events').insert({
|
|
id,
|
|
feature,
|
|
action: 'updateEnabled',
|
|
created_by: 1,
|
|
change_request_id: id,
|
|
});
|
|
};
|
|
|
|
test('should return change request ids per environment', async () => {
|
|
await app.createFeature('my_feature_a');
|
|
await app.createFeature('my_feature_b');
|
|
|
|
await createChangeRequest({
|
|
id: 1,
|
|
feature: 'my_feature_a',
|
|
environment: 'production',
|
|
state: 'In review',
|
|
});
|
|
await createChangeRequest({
|
|
id: 2,
|
|
feature: 'my_feature_a',
|
|
environment: 'production',
|
|
state: 'Applied',
|
|
});
|
|
await createChangeRequest({
|
|
id: 3,
|
|
feature: 'my_feature_a',
|
|
environment: 'production',
|
|
state: 'Cancelled',
|
|
});
|
|
await createChangeRequest({
|
|
id: 4,
|
|
feature: 'my_feature_a',
|
|
environment: 'production',
|
|
state: 'Rejected',
|
|
});
|
|
await createChangeRequest({
|
|
id: 5,
|
|
feature: 'my_feature_a',
|
|
environment: 'development',
|
|
state: 'Draft',
|
|
});
|
|
await createChangeRequest({
|
|
id: 6,
|
|
feature: 'my_feature_a',
|
|
environment: 'development',
|
|
state: 'Scheduled',
|
|
});
|
|
await createChangeRequest({
|
|
id: 7,
|
|
feature: 'my_feature_a',
|
|
environment: 'development',
|
|
state: 'Approved',
|
|
});
|
|
await createChangeRequest({
|
|
id: 8,
|
|
feature: 'my_feature_b',
|
|
environment: 'development',
|
|
state: 'Approved',
|
|
});
|
|
|
|
const { body } = await searchFeatures({});
|
|
|
|
expect(body).toMatchObject({
|
|
features: [
|
|
{
|
|
name: 'my_feature_a',
|
|
environments: [
|
|
{ name: 'default', changeRequestIds: [] },
|
|
{ name: 'development', changeRequestIds: [5, 6, 7] },
|
|
{ name: 'production', changeRequestIds: [1] },
|
|
],
|
|
},
|
|
{
|
|
name: 'my_feature_b',
|
|
environments: [
|
|
{ name: 'default', changeRequestIds: [] },
|
|
{ name: 'development', changeRequestIds: [8] },
|
|
{ name: 'production', changeRequestIds: [] },
|
|
],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
const createReleasePlan = async (
|
|
{
|
|
feature,
|
|
environment,
|
|
planId,
|
|
}: { feature: string; environment: string; planId: string },
|
|
milestones: {
|
|
name: string;
|
|
order: number;
|
|
}[],
|
|
) => {
|
|
const result = await db.stores.releasePlanTemplateStore.insert({
|
|
name: 'plan',
|
|
createdByUserId: 1,
|
|
discriminator: 'template',
|
|
});
|
|
const releasePlan = await db.stores.releasePlanStore.insert({
|
|
id: planId,
|
|
name: 'plan',
|
|
featureName: feature,
|
|
environment: environment,
|
|
createdByUserId: 1,
|
|
releasePlanTemplateId: result.id,
|
|
});
|
|
const milestoneResults = await Promise.all(
|
|
milestones.map((milestone) =>
|
|
createMilestone({
|
|
...milestone,
|
|
planId: releasePlan.id,
|
|
}),
|
|
),
|
|
);
|
|
return { releasePlan, milestones: milestoneResults };
|
|
};
|
|
|
|
const createMilestone = async ({
|
|
name,
|
|
order,
|
|
planId,
|
|
}: { name: string; order: number; planId: string }) => {
|
|
return db.stores.releasePlanMilestoneStore.insert({
|
|
name,
|
|
sortOrder: order,
|
|
releasePlanDefinitionId: planId,
|
|
});
|
|
};
|
|
|
|
const activateMilestone = async ({
|
|
planId,
|
|
milestoneId,
|
|
}: { planId: string; milestoneId: string }) => {
|
|
await db.stores.releasePlanStore.update(planId, {
|
|
activeMilestoneId: milestoneId,
|
|
});
|
|
};
|
|
|
|
test('should return release plan milestones', async () => {
|
|
await app.createFeature('my_feature_a');
|
|
|
|
const { releasePlan, milestones } = await createReleasePlan(
|
|
{
|
|
feature: 'my_feature_a',
|
|
environment: 'development',
|
|
planId: 'plan0',
|
|
},
|
|
[
|
|
{
|
|
name: 'Milestone 1',
|
|
order: 0,
|
|
},
|
|
{
|
|
name: 'Milestone 2',
|
|
order: 1,
|
|
},
|
|
{
|
|
name: 'Milestone 3',
|
|
order: 2,
|
|
},
|
|
],
|
|
);
|
|
await activateMilestone({
|
|
planId: releasePlan.id,
|
|
milestoneId: milestones[1].id,
|
|
});
|
|
|
|
const { body } = await searchFeatures({});
|
|
|
|
expect(body).toMatchObject({
|
|
features: [
|
|
{
|
|
name: 'my_feature_a',
|
|
environments: [
|
|
{ name: 'default' },
|
|
{
|
|
name: 'development',
|
|
totalMilestones: 3,
|
|
milestoneName: 'Milestone 2',
|
|
milestoneOrder: 1,
|
|
},
|
|
{ name: 'production' },
|
|
],
|
|
},
|
|
],
|
|
});
|
|
expect(body.features[0].environments[0].milestoneName).toBeUndefined();
|
|
expect(body.features[0].environments[2].milestoneName).toBeUndefined();
|
|
});
|