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

feat: search now also returns segments used (#5429)

This commit is contained in:
Jaanus Sellin 2023-11-28 21:34:57 +02:00 committed by GitHub
parent 91d616cb6a
commit 51f87bdfd9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 331 additions and 58 deletions

View File

@ -6,6 +6,7 @@ import {
import getLogger from '../../../test/fixtures/no-logger';
import { FeatureSearchQueryParameters } from '../../openapi/spec/feature-search-query-parameters';
import { IUnleashStores } from '../../types';
import { DEFAULT_ENV } from '../../util';
let app: IUnleashTest;
let db: ITestDb;
@ -470,7 +471,7 @@ test('should not return duplicate entries when sorting by last seen', async () =
await stores.environmentStore.create({
name: 'production',
type: 'production',
type: 'development',
});
await app.linkProjectToEnvironment('default', 'production');
@ -509,20 +510,31 @@ test('should not return duplicate entries when sorting by last seen', async () =
test('should search features by description', async () => {
const description = 'secretdescription';
await app.createFeature('my_feature_a');
await app.createFeature({ name: 'my_feature_b', description });
await app.createFeature({
name: 'my_feature_b',
description,
});
const { body } = await searchFeatures({
query: 'descr',
});
expect(body).toMatchObject({
features: [{ name: 'my_feature_b', description }],
features: [
{
name: 'my_feature_b',
description,
},
],
});
});
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({
name: 'my_feature_b',
description,
});
await app.createFeature('my_feature_c');
const { body } = await searchFeatures({
@ -530,7 +542,10 @@ test('should support multiple search values', async () => {
});
expect(body).toMatchObject({
features: [
{ name: 'my_feature_b', description },
{
name: 'my_feature_b',
description,
},
{ name: 'my_feature_c' },
],
});
@ -598,3 +613,38 @@ test('should search features by project with operators', async () => {
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 stores.environmentStore.create({
name: 'development',
type: 'development',
});
await app.linkProjectToEnvironment('default', 'development');
await app.enableFeature('my_feature_a', 'development');
await app.addStrategyToFeatureEnv(
{
name: 'default',
segments: [mySegment.id],
},
DEFAULT_ENV,
'my_feature_a',
);
const { body } = await searchFeatures({});
expect(body).toMatchObject({
features: [
{
name: 'my_feature_a',
segments: [mySegment.name],
},
],
});
});

View File

@ -614,6 +614,21 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
'feature_tag as ft',
'ft.feature_name',
'features.name',
)
.leftJoin(
'feature_strategies',
'feature_strategies.feature_name',
'features.name',
)
.leftJoin(
'feature_strategy_segment',
'feature_strategy_segment.feature_strategy_id',
'feature_strategies.id',
)
.leftJoin(
'segments',
'feature_strategy_segment.segment_id',
'segments.id',
);
if (this.flagResolver.isEnabled('useLastSeenRefactor')) {
@ -645,6 +660,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
'environments.sort_order as environment_sort_order',
'ft.tag_value as tag_value',
'ft.tag_type as tag_type',
'segments.name as segment_name',
] as (string | Raw<any> | Knex.QueryBuilder)[];
let lastSeenQuery = 'feature_environments.last_seen_at';
@ -735,7 +751,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
const rows = await finalQuery;
if (rows.length > 0) {
const overview = this.getFeatureOverviewData(rows);
const overview = this.getAggregatedSearchData(rows);
const features = sortEnvironments(overview);
return {
features,
@ -861,6 +877,62 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
return [];
}
getAggregatedSearchData(rows): IFeatureOverview {
return rows.reduce((acc, row) => {
if (acc[row.feature_name] !== undefined) {
const environmentExists = acc[
row.feature_name
].environments.some(
(existingEnvironment) =>
existingEnvironment.name === row.environment,
);
if (!environmentExists) {
acc[row.feature_name].environments.push(
FeatureStrategiesStore.getEnvironment(row),
);
}
const segmentExists = acc[row.feature_name].segments.includes(
row.segment_name,
);
if (row.segment_name && !segmentExists) {
acc[row.feature_name].segments.push(row.segment_name);
}
if (this.isNewTag(acc[row.feature_name], row)) {
this.addTag(acc[row.feature_name], row);
}
} else {
acc[row.feature_name] = {
type: row.type,
description: row.description,
favorite: row.favorite,
name: row.feature_name,
createdAt: row.created_at,
stale: row.stale,
impressionData: row.impression_data,
lastSeenAt: row.last_seen_at,
environments: [FeatureStrategiesStore.getEnvironment(row)],
segments: row.segment_name ? [row.segment_name] : [],
};
if (this.isNewTag(acc[row.feature_name], row)) {
this.addTag(acc[row.feature_name], row);
}
}
const featureRow = acc[row.feature_name];
if (
featureRow.lastSeenAt === undefined ||
new Date(row.env_last_seen_at) >
new Date(featureRow.last_seen_at)
) {
featureRow.lastSeenAt = row.env_last_seen_at;
}
return acc;
}, {});
}
getFeatureOverviewData(rows): IFeatureOverview {
return rows.reduce((acc, row) => {
if (acc[row.feature_name] !== undefined) {

View File

@ -56,15 +56,6 @@ const fetchSegmentStrategies = (
.expect(200)
.then((res) => res.body);
const createSegment = (
postData: object,
expectStatusCode = 201,
): Promise<unknown> =>
app.request
.post(SEGMENTS_BASE_PATH)
.send(postData)
.expect(expectStatusCode);
const updateSegment = (
id: number,
postData: object,
@ -94,7 +85,13 @@ const addSegmentsToStrategy = (
const mockFeatureToggle = () => ({
name: randomId(),
strategies: [{ name: 'flexibleRollout', constraints: [], parameters: {} }],
strategies: [
{
name: 'flexibleRollout',
constraints: [],
parameters: {},
},
],
});
const validateSegment = (
@ -136,13 +133,38 @@ afterEach(async () => {
});
test('should validate segments', async () => {
await createSegment({ something: 'a' }, 400);
await createSegment({ name: randomId(), something: 'b' }, 400);
await createSegment({ name: randomId(), constraints: 'b' }, 400);
await createSegment({ constraints: [] }, 400);
await createSegment({ name: randomId(), constraints: [{}] }, 400);
await createSegment({ name: randomId(), constraints: [] });
await createSegment({ name: randomId(), description: '', constraints: [] });
await app.createSegment({ something: 'a' }, 400);
await app.createSegment(
{
name: randomId(),
something: 'b',
},
400,
);
await app.createSegment(
{
name: randomId(),
constraints: 'b',
},
400,
);
await app.createSegment({ constraints: [] }, 400);
await app.createSegment(
{
name: randomId(),
constraints: [{}],
},
400,
);
await app.createSegment({
name: randomId(),
constraints: [],
});
await app.createSegment({
name: randomId(),
description: '',
constraints: [],
});
});
test('should fail on missing properties', async () => {
@ -159,23 +181,38 @@ test('should fail on missing properties', async () => {
});
test('should create segments', async () => {
await createSegment({ name: 'a', constraints: [] });
await createSegment({ name: 'c', constraints: [] });
await createSegment({ name: 'b', constraints: [] });
await app.createSegment({
name: 'a',
constraints: [],
});
await app.createSegment({
name: 'c',
constraints: [],
});
await app.createSegment({
name: 'b',
constraints: [],
});
const segments = await fetchSegments();
expect(segments.map((s) => s.name)).toEqual(['a', 'b', 'c']);
});
test('should update segments', async () => {
await createSegment({ name: 'a', constraints: [] });
await app.createSegment({
name: 'a',
constraints: [],
});
const [segmentA] = await fetchSegments();
expect(segmentA.id).toBeGreaterThan(0);
expect(segmentA.name).toEqual('a');
expect(segmentA.createdAt).toBeDefined();
expect(segmentA.constraints.length).toEqual(0);
await updateSegment(segmentA.id, { ...segmentA, name: 'b' });
await updateSegment(segmentA.id, {
...segmentA,
name: 'b',
});
const [segmentB] = await fetchSegments();
expect(segmentB.id).toEqual(segmentA.id);
@ -185,15 +222,29 @@ test('should update segments', async () => {
});
test('should update segment constraints', async () => {
const constraintA = { contextName: 'a', operator: 'IN', values: ['x'] };
const constraintB = { contextName: 'b', operator: 'IN', values: ['y'] };
await createSegment({ name: 'a', constraints: [constraintA] });
const constraintA = {
contextName: 'a',
operator: 'IN',
values: ['x'],
};
const constraintB = {
contextName: 'b',
operator: 'IN',
values: ['y'],
};
await app.createSegment({
name: 'a',
constraints: [constraintA],
});
const [segmentA] = await fetchSegments();
expect(segmentA.constraints).toEqual([constraintA]);
await app.request
.put(`${SEGMENTS_BASE_PATH}/${segmentA.id}`)
.send({ ...segmentA, constraints: [constraintB, constraintA] })
.send({
...segmentA,
constraints: [constraintB, constraintA],
})
.expect(204);
const [segmentB] = await fetchSegments();
@ -201,7 +252,10 @@ test('should update segment constraints', async () => {
});
test('should delete segments', async () => {
await createSegment({ name: 'a', constraints: [] });
await app.createSegment({
name: 'a',
constraints: [],
});
const segments = await fetchSegments();
expect(segments.length).toEqual(1);
@ -213,7 +267,10 @@ test('should delete segments', async () => {
});
test('should not delete segments used by strategies', async () => {
await createSegment({ name: 'a', constraints: [] });
await app.createSegment({
name: 'a',
constraints: [],
});
const toggle = mockFeatureToggle();
await createFeatureToggle(app, toggle);
const [segment] = await fetchSegments();
@ -238,9 +295,18 @@ test('should not delete segments used by strategies', async () => {
});
test('should list strategies by segment', async () => {
await createSegment({ name: 'S1', constraints: [] });
await createSegment({ name: 'S2', constraints: [] });
await createSegment({ name: 'S3', constraints: [] });
await app.createSegment({
name: 'S1',
constraints: [],
});
await app.createSegment({
name: 'S2',
constraints: [],
});
await app.createSegment({
name: 'S3',
constraints: [],
});
const toggle1 = mockFeatureToggle();
const toggle2 = mockFeatureToggle();
const toggle3 = mockFeatureToggle();
@ -305,9 +371,18 @@ test('should list strategies by segment', async () => {
});
test('should list segments by strategy', async () => {
await createSegment({ name: 'S1', constraints: [] });
await createSegment({ name: 'S2', constraints: [] });
await createSegment({ name: 'S3', constraints: [] });
await app.createSegment({
name: 'S1',
constraints: [],
});
await app.createSegment({
name: 'S2',
constraints: [],
});
await app.createSegment({
name: 'S3',
constraints: [],
});
const toggle1 = mockFeatureToggle();
const toggle2 = mockFeatureToggle();
const toggle3 = mockFeatureToggle();
@ -376,7 +451,10 @@ test('should list segments by strategy', async () => {
test('should reject duplicate segment names', async () => {
await validateSegment({ name: 'a' });
await createSegment({ name: 'a', constraints: [] });
await app.createSegment({
name: 'a',
constraints: [],
});
await validateSegment({ name: 'a' }, 409);
await validateSegment({ name: 'b' });
});
@ -388,31 +466,75 @@ test('should reject empty segment names', async () => {
});
test('should reject duplicate segment names on create', async () => {
await createSegment({ name: 'a', constraints: [] });
await createSegment({ name: 'a', constraints: [] }, 409);
await app.createSegment({
name: 'a',
constraints: [],
});
await app.createSegment(
{
name: 'a',
constraints: [],
},
409,
);
await validateSegment({ name: 'b' });
});
test('should reject duplicate segment names on update', async () => {
await createSegment({ name: 'a', constraints: [] });
await createSegment({ name: 'b', constraints: [] });
await app.createSegment({
name: 'a',
constraints: [],
});
await app.createSegment({
name: 'b',
constraints: [],
});
const [segmentA, segmentB] = await fetchSegments();
await updateSegment(segmentA.id, { name: 'b', constraints: [] }, 409);
await updateSegment(segmentB.id, { name: 'a', constraints: [] }, 409);
await updateSegment(segmentA.id, { name: 'a', constraints: [] });
await updateSegment(segmentA.id, { name: 'c', constraints: [] });
await updateSegment(
segmentA.id,
{
name: 'b',
constraints: [],
},
409,
);
await updateSegment(
segmentB.id,
{
name: 'a',
constraints: [],
},
409,
);
await updateSegment(segmentA.id, {
name: 'a',
constraints: [],
});
await updateSegment(segmentA.id, {
name: 'c',
constraints: [],
});
});
test('Should anonymise createdBy field if anonymiseEventLog flag is set', async () => {
await createSegment({ name: 'a', constraints: [] });
await createSegment({ name: 'b', constraints: [] });
await app.createSegment({
name: 'a',
constraints: [],
});
await app.createSegment({
name: 'b',
constraints: [],
});
const segments = await fetchSegments();
expect(segments).toHaveLength(2);
expect(segments[0].createdBy).toContain('unleash.run');
});
test('Should show usage in features and projects', async () => {
await createSegment({ name: 'a', constraints: [] });
await app.createSegment({
name: 'a',
constraints: [],
});
const toggle = mockFeatureToggle();
await createFeatureToggle(app, toggle);
const [segment] = await fetchSegments();
@ -427,7 +549,12 @@ test('Should show usage in features and projects', async () => {
await addSegmentsToStrategy([segment.id], feature.strategies[0].id);
const segments = await fetchSegments();
expect(segments).toMatchObject([{ usedInFeatures: 1, usedInProjects: 1 }]);
expect(segments).toMatchObject([
{
usedInFeatures: 1,
usedInProjects: 1,
},
]);
});
describe('detect strategy usage in change requests', () => {
@ -497,7 +624,10 @@ describe('detect strategy usage in change requests', () => {
});
test('should not delete segments used by strategies in CRs', async () => {
await createSegment({ name: 'a', constraints: [] });
await app.createSegment({
name: 'a',
constraints: [],
});
const toggle = mockFeatureToggle();
await createFeatureToggle(enterpriseApp, toggle);
const [segment] = await enterpriseFetchSegments();
@ -538,7 +668,10 @@ describe('detect strategy usage in change requests', () => {
});
test('Should show segment usage in addStrategy events', async () => {
await createSegment({ name: 'a', constraints: [] });
await app.createSegment({
name: 'a',
constraints: [],
});
const toggle = mockFeatureToggle();
await createFeatureToggle(enterpriseApp, toggle);
const [segment] = await enterpriseFetchSegments();
@ -585,7 +718,10 @@ describe('detect strategy usage in change requests', () => {
});
test('Should show segment usage in updateStrategy events', async () => {
await createSegment({ name: 'a', constraints: [] });
await app.createSegment({
name: 'a',
constraints: [],
});
const toggle = mockFeatureToggle();
await createFeatureToggle(enterpriseApp, toggle);
const [segment] = await enterpriseFetchSegments();
@ -641,7 +777,10 @@ describe('detect strategy usage in change requests', () => {
});
test('If a segment is used in an existing strategy and in a CR for the same strategy, the strategy should be listed both places', async () => {
await createSegment({ name: 'a', constraints: [] });
await app.createSegment({
name: 'a',
constraints: [],
});
const toggle = mockFeatureToggle();
await createFeatureToggle(enterpriseApp, toggle);
const [segment] = await enterpriseFetchSegments();
@ -697,7 +836,7 @@ describe('detect strategy usage in change requests', () => {
// because they use the same db, we can use the regular app
// (through `createSegment` and `createFeatureToggle`) to
// create the segment and the flag
await createSegment({ name: 'a', constraints: [] });
await app.createSegment({ name: 'a', constraints: [] });
const toggle = mockFeatureToggle();
await createFeatureToggle(app, toggle);
const [segment] = await enterpriseFetchSegments();

View File

@ -99,6 +99,8 @@ export interface IUnleashHttpAPI {
): supertest.Test;
getRecordedEvents(): supertest.Test;
createSegment(postData: object, expectStatusCode?: number): supertest.Test;
}
function httpApis(
@ -260,6 +262,16 @@ function httpApis(
.set('Content-Type', 'application/json')
.expect(expectedResponseCode);
},
createSegment(
postData: object,
expectedResponseCode = 201,
): supertest.Test {
return request
.post(`/api/admin/segments`)
.send(postData)
.set('Content-Type', 'application/json')
.expect(expectedResponseCode);
},
getRecordedEvents(
project: string | null = null,