mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-19 01:17:18 +02:00
chore: remove deprecated legacy features endpoint (#7129)
This PR is part of #4380 - Remove legacy `/api/feature` endpoint. ## About the changes ### Frontend - Removes the useFeatures hook - Removes the part of StrategyView that displays features using this strategy (not been working since v4.4) - Removes 2 unused features entries from routes ### Backend - Removes the /api/admin/features endpoint - Moves a couple of non-feature related tests (auth etc) to use /admin/projects endpoint instead - Removes a test that was directly related to the removed endpoint - Moves a couple of tests to the projects/features endpoint - Reworks some tests to fetch features from projects features endpoint and strategies from project strategies
This commit is contained in:
parent
6f2a10922d
commit
9ea66e8850
@ -1,33 +0,0 @@
|
|||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { Navigate } from 'react-router-dom';
|
|
||||||
import { useFeatures } from 'hooks/api/getters/useFeatures/useFeatures';
|
|
||||||
import { getTogglePath } from 'utils/routePathHelpers';
|
|
||||||
import type { FeatureSchema } from 'openapi';
|
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
|
||||||
|
|
||||||
const RedirectFeatureView = () => {
|
|
||||||
const featureId = useRequiredPathParam('featureId');
|
|
||||||
const { features = [] } = useFeatures();
|
|
||||||
const [featureToggle, setFeatureToggle] = useState<FeatureSchema>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const toggle = features.find(
|
|
||||||
(toggle: FeatureSchema) => toggle.name === featureId,
|
|
||||||
);
|
|
||||||
|
|
||||||
setFeatureToggle(toggle);
|
|
||||||
}, [features, featureId]);
|
|
||||||
|
|
||||||
if (!featureToggle?.project) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Navigate
|
|
||||||
to={getTogglePath(featureToggle.project, featureToggle.name)}
|
|
||||||
replace
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RedirectFeatureView;
|
|
@ -73,14 +73,6 @@ exports[`returns all baseRoutes 1`] = `
|
|||||||
"title": "Create feature flag",
|
"title": "Create feature flag",
|
||||||
"type": "protected",
|
"type": "protected",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"component": [Function],
|
|
||||||
"menu": {},
|
|
||||||
"parent": "/features",
|
|
||||||
"path": "/projects/:projectId/features2/:featureId",
|
|
||||||
"title": ":featureId",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"component": {
|
"component": {
|
||||||
"$$typeof": Symbol(react.lazy),
|
"$$typeof": Symbol(react.lazy),
|
||||||
@ -106,14 +98,6 @@ exports[`returns all baseRoutes 1`] = `
|
|||||||
"title": "Projects",
|
"title": "Projects",
|
||||||
"type": "protected",
|
"type": "protected",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"component": [Function],
|
|
||||||
"menu": {},
|
|
||||||
"parent": "/features",
|
|
||||||
"path": "/features/:activeTab/:featureId",
|
|
||||||
"title": ":featureId",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"component": [Function],
|
"component": [Function],
|
||||||
"menu": {
|
"menu": {
|
||||||
|
@ -18,7 +18,6 @@ import CreateTagType from 'component/tags/CreateTagType/CreateTagType';
|
|||||||
import CreateFeature from 'component/feature/CreateFeature/CreateFeature';
|
import CreateFeature from 'component/feature/CreateFeature/CreateFeature';
|
||||||
import EditFeature from 'component/feature/EditFeature/EditFeature';
|
import EditFeature from 'component/feature/EditFeature/EditFeature';
|
||||||
import ContextList from 'component/context/ContextList/ContextList/ContextList';
|
import ContextList from 'component/context/ContextList/ContextList/ContextList';
|
||||||
import RedirectFeatureView from 'component/feature/RedirectFeatureView/RedirectFeatureView';
|
|
||||||
import { CreateIntegration } from 'component/integrations/CreateIntegration/CreateIntegration';
|
import { CreateIntegration } from 'component/integrations/CreateIntegration/CreateIntegration';
|
||||||
import { EditIntegration } from 'component/integrations/EditIntegration/EditIntegration';
|
import { EditIntegration } from 'component/integrations/EditIntegration/EditIntegration';
|
||||||
import { CopyFeatureToggle } from 'component/feature/CopyFeature/CopyFeature';
|
import { CopyFeatureToggle } from 'component/feature/CopyFeature/CopyFeature';
|
||||||
@ -110,14 +109,6 @@ export const routes: IRoute[] = [
|
|||||||
type: 'protected',
|
type: 'protected',
|
||||||
menu: {},
|
menu: {},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/projects/:projectId/features2/:featureId',
|
|
||||||
parent: '/features',
|
|
||||||
title: ':featureId',
|
|
||||||
component: RedirectFeatureView,
|
|
||||||
type: 'protected',
|
|
||||||
menu: {},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/projects/:projectId/*',
|
path: '/projects/:projectId/*',
|
||||||
parent: '/projects',
|
parent: '/projects',
|
||||||
@ -136,14 +127,6 @@ export const routes: IRoute[] = [
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Features
|
// Features
|
||||||
{
|
|
||||||
path: '/features/:activeTab/:featureId',
|
|
||||||
parent: '/features',
|
|
||||||
title: ':featureId',
|
|
||||||
component: RedirectFeatureView,
|
|
||||||
type: 'protected',
|
|
||||||
menu: {},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/search',
|
path: '/search',
|
||||||
title: 'Search',
|
title: 'Search',
|
||||||
|
@ -12,20 +12,17 @@ import RadioButtonChecked from '@mui/icons-material/RadioButtonChecked';
|
|||||||
import { AppsLinkList } from 'component/common';
|
import { AppsLinkList } from 'component/common';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import styles from '../../strategies.module.scss';
|
import styles from '../../strategies.module.scss';
|
||||||
import { TogglesLinkList } from 'component/strategies/TogglesLinkList/TogglesLinkList';
|
|
||||||
import type { IStrategy, IStrategyParameter } from 'interfaces/strategy';
|
import type { IStrategy, IStrategyParameter } from 'interfaces/strategy';
|
||||||
import type { ApplicationSchema, FeatureSchema } from 'openapi';
|
import type { ApplicationSchema } from 'openapi';
|
||||||
|
|
||||||
interface IStrategyDetailsProps {
|
interface IStrategyDetailsProps {
|
||||||
strategy: IStrategy;
|
strategy: IStrategy;
|
||||||
applications: ApplicationSchema[];
|
applications: ApplicationSchema[];
|
||||||
toggles: FeatureSchema[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StrategyDetails = ({
|
export const StrategyDetails = ({
|
||||||
strategy,
|
strategy,
|
||||||
applications,
|
applications,
|
||||||
toggles,
|
|
||||||
}: IStrategyDetailsProps) => {
|
}: IStrategyDetailsProps) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { parameters = [] } = strategy;
|
const { parameters = [] } = strategy;
|
||||||
@ -84,7 +81,7 @@ export const StrategyDetails = ({
|
|||||||
<List>{renderParameters(parameters)}</List>
|
<List>{renderParameters(parameters)}</List>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item sm={12} md={toggles.length > 0 ? 6 : 12}>
|
<Grid item sm={12} md={12}>
|
||||||
<h6>
|
<h6>
|
||||||
Applications using this strategy{' '}
|
Applications using this strategy{' '}
|
||||||
{applications.length >= 1000 && '(Capped at 1000)'}
|
{applications.length >= 1000 && '(Capped at 1000)'}
|
||||||
@ -92,17 +89,6 @@ export const StrategyDetails = ({
|
|||||||
<hr />
|
<hr />
|
||||||
<AppsLinkList apps={applications} />
|
<AppsLinkList apps={applications} />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={toggles.length > 0}
|
|
||||||
show={() => (
|
|
||||||
<Grid item sm={12} md={6}>
|
|
||||||
<h6>Toggles using this strategy</h6>
|
|
||||||
<hr />
|
|
||||||
<TogglesLinkList toggles={toggles} />
|
|
||||||
</Grid>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -3,7 +3,6 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { UPDATE_STRATEGY } from 'component/providers/AccessProvider/permissions';
|
import { UPDATE_STRATEGY } from 'component/providers/AccessProvider/permissions';
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies';
|
import { useStrategies } from 'hooks/api/getters/useStrategies/useStrategies';
|
||||||
import { useFeatures } from 'hooks/api/getters/useFeatures/useFeatures';
|
|
||||||
import useApplications from 'hooks/api/getters/useApplications/useApplications';
|
import useApplications from 'hooks/api/getters/useApplications/useApplications';
|
||||||
import { StrategyDetails } from './StrategyDetails/StrategyDetails';
|
import { StrategyDetails } from './StrategyDetails/StrategyDetails';
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
@ -11,24 +10,13 @@ import PermissionIconButton from 'component/common/PermissionIconButton/Permissi
|
|||||||
import Edit from '@mui/icons-material/Edit';
|
import Edit from '@mui/icons-material/Edit';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import type { FeatureSchema } from 'openapi/models';
|
|
||||||
|
|
||||||
export const StrategyView = () => {
|
export const StrategyView = () => {
|
||||||
const name = useRequiredPathParam('name');
|
const name = useRequiredPathParam('name');
|
||||||
const { strategies } = useStrategies();
|
const { strategies } = useStrategies();
|
||||||
const { features = [] } = useFeatures();
|
|
||||||
const { applications } = useApplications();
|
const { applications } = useApplications();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// Has been broken since the migration to environments. We need to create an
|
|
||||||
// endpoint that returns all environments and strategies for all features to make this
|
|
||||||
// work properly OR alternatively create an endpoint that abstracts this logic into the backend
|
|
||||||
const toggles = features.filter((toggle: FeatureSchema) => {
|
|
||||||
return toggle?.environments
|
|
||||||
?.flatMap((env) => env.strategies)
|
|
||||||
.some((strategy) => strategy && strategy.name === name);
|
|
||||||
});
|
|
||||||
|
|
||||||
const strategy = strategies.find((strategy) => strategy.name === name);
|
const strategy = strategies.find((strategy) => strategy.name === name);
|
||||||
|
|
||||||
const handleEdit = () => {
|
const handleEdit = () => {
|
||||||
@ -64,7 +52,6 @@ export const StrategyView = () => {
|
|||||||
<Grid item xs={12} sm={12}>
|
<Grid item xs={12} sm={12}>
|
||||||
<StrategyDetails
|
<StrategyDetails
|
||||||
strategy={strategy}
|
strategy={strategy}
|
||||||
toggles={toggles}
|
|
||||||
applications={applications}
|
applications={applications}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
import type { FeaturesSchema } from 'openapi';
|
|
||||||
import useSWR from 'swr';
|
|
||||||
import { formatApiPath } from 'utils/formatPath';
|
|
||||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
|
||||||
|
|
||||||
const fetcher = (path: string) => {
|
|
||||||
return fetch(path)
|
|
||||||
.then(handleErrorResponses('Feature flag'))
|
|
||||||
.then((res) => res.json());
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useFeatures = () => {
|
|
||||||
const { data, error, mutate } = useSWR<FeaturesSchema>(
|
|
||||||
formatApiPath('api/admin/features'),
|
|
||||||
fetcher,
|
|
||||||
{
|
|
||||||
refreshInterval: 15 * 1000, // ms
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
features: data?.features,
|
|
||||||
loading: !error && !data,
|
|
||||||
refetchFeatures: mutate,
|
|
||||||
error,
|
|
||||||
};
|
|
||||||
};
|
|
@ -1099,41 +1099,6 @@ class FeatureToggleService {
|
|||||||
return features as FeatureConfigurationClient[];
|
return features as FeatureConfigurationClient[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Legacy!
|
|
||||||
*
|
|
||||||
* Used to retrieve metadata of all feature flags defined in Unleash.
|
|
||||||
* @param query - Allow you to limit search based on criteria such as project, tags, namePrefix. See @IFeatureToggleQuery
|
|
||||||
* @param userId - Used to find / mark features as favorite based on users preferences
|
|
||||||
* @param archived - Return archived or active flags
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
async getFeatureToggles(
|
|
||||||
query?: IFeatureToggleQuery,
|
|
||||||
userId?: number,
|
|
||||||
archived: boolean = false,
|
|
||||||
): Promise<FeatureToggle[]> {
|
|
||||||
// Remove with with feature flag
|
|
||||||
const features = await this.featureToggleStore.getFeatureToggleList(
|
|
||||||
query,
|
|
||||||
userId,
|
|
||||||
archived,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (userId) {
|
|
||||||
const projectAccess =
|
|
||||||
await this.privateProjectChecker.getUserAccessibleProjects(
|
|
||||||
userId,
|
|
||||||
);
|
|
||||||
return projectAccess.mode === 'all'
|
|
||||||
? features
|
|
||||||
: features.filter((f) =>
|
|
||||||
projectAccess.projects.includes(f.project),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return features;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getFeatureOverview(
|
async getFeatureOverview(
|
||||||
params: IFeatureProjectUserParams,
|
params: IFeatureProjectUserParams,
|
||||||
): Promise<IFeatureOverview[]> {
|
): Promise<IFeatureOverview[]> {
|
||||||
@ -1949,10 +1914,6 @@ class FeatureToggleService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getArchivedFeatures(): Promise<FeatureToggle[]> {
|
|
||||||
return this.getFeatureToggles({}, undefined, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: add project id.
|
// TODO: add project id.
|
||||||
async deleteFeature(
|
async deleteFeature(
|
||||||
featureName: string,
|
featureName: string,
|
||||||
|
@ -10,13 +10,8 @@ import type { IFeatureToggleQuery } from '../../../types/model';
|
|||||||
import type FeatureTagService from '../../../services/feature-tag-service';
|
import type FeatureTagService from '../../../services/feature-tag-service';
|
||||||
import type { IAuthRequest } from '../../../routes/unleash-types';
|
import type { IAuthRequest } from '../../../routes/unleash-types';
|
||||||
import { DEFAULT_ENV } from '../../../util/constants';
|
import { DEFAULT_ENV } from '../../../util/constants';
|
||||||
import {
|
|
||||||
featuresSchema,
|
|
||||||
type FeaturesSchema,
|
|
||||||
} from '../../../openapi/spec/features-schema';
|
|
||||||
import type { TagSchema } from '../../../openapi/spec/tag-schema';
|
import type { TagSchema } from '../../../openapi/spec/tag-schema';
|
||||||
import type { TagsSchema } from '../../../openapi/spec/tags-schema';
|
import type { TagsSchema } from '../../../openapi/spec/tags-schema';
|
||||||
import { serializeDates } from '../../../types/serialize-dates';
|
|
||||||
import type { OpenApiService } from '../../../services/openapi-service';
|
import type { OpenApiService } from '../../../services/openapi-service';
|
||||||
import { createRequestSchema } from '../../../openapi/util/create-request-schema';
|
import { createRequestSchema } from '../../../openapi/util/create-request-schema';
|
||||||
import {
|
import {
|
||||||
@ -55,27 +50,6 @@ class FeatureController extends Controller {
|
|||||||
this.openApiService = openApiService;
|
this.openApiService = openApiService;
|
||||||
this.service = featureToggleServiceV2;
|
this.service = featureToggleServiceV2;
|
||||||
|
|
||||||
this.route({
|
|
||||||
method: 'get',
|
|
||||||
path: '',
|
|
||||||
handler: this.getAllToggles,
|
|
||||||
permission: NONE,
|
|
||||||
middleware: [
|
|
||||||
openApiService.validPath({
|
|
||||||
tags: ['Features'],
|
|
||||||
operationId: 'getAllToggles',
|
|
||||||
responses: {
|
|
||||||
200: createResponseSchema('featuresSchema'),
|
|
||||||
...getStandardResponses(401, 403),
|
|
||||||
},
|
|
||||||
summary: 'Get all feature flags (deprecated)',
|
|
||||||
description:
|
|
||||||
'Gets all feature flags with their full configuration. This endpoint is **deprecated**. You should use the project-based endpoint instead (`/api/admin/projects/<project-id>/features`).',
|
|
||||||
deprecated: true,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
this.route({
|
this.route({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
path: '/validate',
|
path: '/validate',
|
||||||
@ -210,23 +184,6 @@ class FeatureController extends Controller {
|
|||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllToggles(
|
|
||||||
req: IAuthRequest,
|
|
||||||
res: Response<FeaturesSchema>,
|
|
||||||
): Promise<void> {
|
|
||||||
const query = await this.prepQuery(req.query);
|
|
||||||
|
|
||||||
const { user } = req;
|
|
||||||
const features = await this.service.getFeatureToggles(query, user.id);
|
|
||||||
|
|
||||||
this.openApiService.respondWithValidation(
|
|
||||||
200,
|
|
||||||
res,
|
|
||||||
featuresSchema.$id,
|
|
||||||
{ version, features: serializeDates(features) },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async getToggle(
|
async getToggle(
|
||||||
req: Request<{ featureName: string }, any, any, any>,
|
req: Request<{ featureName: string }, any, any, any>,
|
||||||
res: Response,
|
res: Response,
|
||||||
|
@ -70,7 +70,7 @@ test('should return last seen at per env for /api/admin/features', async () => {
|
|||||||
await insertLastSeenAt('lastSeenAtPerEnv', db.rawDatabase, 'default');
|
await insertLastSeenAt('lastSeenAtPerEnv', db.rawDatabase, 'default');
|
||||||
|
|
||||||
const response = await app.request
|
const response = await app.request
|
||||||
.get('/api/admin/features')
|
.get('/api/admin/projects/default/features')
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
@ -88,7 +88,7 @@ test('response should include last seen at per environment for multiple environm
|
|||||||
|
|
||||||
await setupLastSeenAtTest(featureName);
|
await setupLastSeenAtTest(featureName);
|
||||||
const { body } = await app.request
|
const { body } = await app.request
|
||||||
.get('/api/admin/features')
|
.get('/api/admin/projects/default/features')
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
|
@ -3667,33 +3667,6 @@ test('should not be allowed to update with invalid strategy type name', async ()
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should return correct data structure for /api/admin/features', async () => {
|
|
||||||
await app.createFeature('refactor-features');
|
|
||||||
|
|
||||||
const result = await app.request.get('/api/admin/features').expect(200);
|
|
||||||
|
|
||||||
expect(result.body.features).toBeInstanceOf(Array);
|
|
||||||
|
|
||||||
const feature = result.body.features.find(
|
|
||||||
(features) => features.name === 'refactor-features',
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(feature).toMatchObject({
|
|
||||||
impressionData: false,
|
|
||||||
enabled: false,
|
|
||||||
name: 'refactor-features',
|
|
||||||
description: null,
|
|
||||||
project: 'default',
|
|
||||||
stale: false,
|
|
||||||
type: 'release',
|
|
||||||
lastSeenAt: null,
|
|
||||||
variants: [],
|
|
||||||
favorite: false,
|
|
||||||
createdAt: expect.anything(),
|
|
||||||
strategies: [],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test('can get evaluation metrics', async () => {
|
test('can get evaluation metrics', async () => {
|
||||||
await app.createFeature('metric-feature');
|
await app.createFeature('metric-feature');
|
||||||
|
|
||||||
|
@ -239,7 +239,7 @@ test('should allow requests with an admin token', async () => {
|
|||||||
test('should not allow admin requests with a frontend token', async () => {
|
test('should not allow admin requests with a frontend token', async () => {
|
||||||
const frontendToken = await createApiToken(ApiTokenType.FRONTEND);
|
const frontendToken = await createApiToken(ApiTokenType.FRONTEND);
|
||||||
await app.request
|
await app.request
|
||||||
.get('/api/admin/features')
|
.get('/api/admin/projects')
|
||||||
.set('Authorization', frontendToken.secret)
|
.set('Authorization', frontendToken.secret)
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(403);
|
.expect(403);
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { randomId } from '../../util/random-id';
|
import { randomId } from '../../util/random-id';
|
||||||
import type { IFeatureToggleClient, ISegment } from '../../types/model';
|
import type { ISegment } from '../../types/model';
|
||||||
import { collectIds } from '../../util/collect-ids';
|
import { collectIds } from '../../util/collect-ids';
|
||||||
import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init';
|
import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init';
|
||||||
import getLogger from '../../../test/fixtures/no-logger';
|
import getLogger from '../../../test/fixtures/no-logger';
|
||||||
@ -12,13 +12,17 @@ import {
|
|||||||
setupAppWithCustomConfig,
|
setupAppWithCustomConfig,
|
||||||
} from '../../../test/e2e/helpers/test-helper';
|
} from '../../../test/e2e/helpers/test-helper';
|
||||||
import type { StrategiesUsingSegment } from './segment-service-interface';
|
import type { StrategiesUsingSegment } from './segment-service-interface';
|
||||||
import type { IUser } from '../../types';
|
import type { IFeatureOverview, IUser } from '../../types';
|
||||||
|
|
||||||
let app: IUnleashTest;
|
let app: IUnleashTest;
|
||||||
let db: ITestDb;
|
let db: ITestDb;
|
||||||
|
|
||||||
const SEGMENTS_BASE_PATH = '/api/admin/segments';
|
const SEGMENTS_BASE_PATH = '/api/admin/segments';
|
||||||
const FEATURES_LIST_BASE_PATH = '/api/admin/features';
|
const FEATURES_LIST_BASE_PATH = '/api/admin/projects/default/features';
|
||||||
|
|
||||||
|
const getFeatureStrategiesPath = (featureName: string) => {
|
||||||
|
return `/api/admin/projects/default/features/${featureName}/environments/default/strategies`;
|
||||||
|
};
|
||||||
|
|
||||||
// Recursively change all Date properties to string properties.
|
// Recursively change all Date properties to string properties.
|
||||||
type SerializeDatesDeep<T> = {
|
type SerializeDatesDeep<T> = {
|
||||||
@ -39,12 +43,18 @@ const fetchSegmentsByStrategy = (
|
|||||||
.expect(200)
|
.expect(200)
|
||||||
.then((res) => res.body.segments);
|
.then((res) => res.body.segments);
|
||||||
|
|
||||||
const fetchFeatures = (): Promise<IFeatureToggleClient[]> =>
|
const fetchFeatures = (): Promise<IFeatureOverview[]> =>
|
||||||
app.request
|
app.request
|
||||||
.get(FEATURES_LIST_BASE_PATH)
|
.get(FEATURES_LIST_BASE_PATH)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then((res) => res.body.features);
|
.then((res) => res.body.features);
|
||||||
|
|
||||||
|
const fetchFeatureStrategies = (featureName: string) =>
|
||||||
|
app.request
|
||||||
|
.get(getFeatureStrategiesPath(featureName))
|
||||||
|
.expect(200)
|
||||||
|
.then((res) => res.body);
|
||||||
|
|
||||||
const fetchSegmentStrategies = (
|
const fetchSegmentStrategies = (
|
||||||
segmentId: number,
|
segmentId: number,
|
||||||
): Promise<StrategiesUsingSegment> =>
|
): Promise<StrategiesUsingSegment> =>
|
||||||
@ -288,8 +298,9 @@ test('should not delete segments used by strategies', async () => {
|
|||||||
flag.name,
|
flag.name,
|
||||||
);
|
);
|
||||||
const [feature] = await fetchFeatures();
|
const [feature] = await fetchFeatures();
|
||||||
|
const [strategy] = await fetchFeatureStrategies(feature.name);
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
await addSegmentsToStrategy([segment.id], feature.strategies[0].id);
|
await addSegmentsToStrategy([segment.id], strategy.id);
|
||||||
const segments = await fetchSegments();
|
const segments = await fetchSegments();
|
||||||
expect(segments.length).toEqual(1);
|
expect(segments.length).toEqual(1);
|
||||||
|
|
||||||
@ -316,8 +327,9 @@ test('should delete segments used by strategies in archived feature flags', asyn
|
|||||||
flag.name,
|
flag.name,
|
||||||
);
|
);
|
||||||
const [feature] = await fetchFeatures();
|
const [feature] = await fetchFeatures();
|
||||||
|
const [strategy] = await fetchFeatureStrategies(feature.name);
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
await addSegmentsToStrategy([segment.id], feature.strategies[0].id);
|
await addSegmentsToStrategy([segment.id], strategy.id);
|
||||||
const segments = await fetchSegments();
|
const segments = await fetchSegments();
|
||||||
expect(segments.length).toEqual(1);
|
expect(segments.length).toEqual(1);
|
||||||
|
|
||||||
@ -372,36 +384,40 @@ test('should list strategies by segment', async () => {
|
|||||||
const [feature1, feature2, feature3] = await fetchFeatures();
|
const [feature1, feature2, feature3] = await fetchFeatures();
|
||||||
const [segment1, segment2, segment3] = await fetchSegments();
|
const [segment1, segment2, segment3] = await fetchSegments();
|
||||||
|
|
||||||
|
const feature1Strategies = await fetchFeatureStrategies(feature1.name);
|
||||||
|
const feature2Strategies = await fetchFeatureStrategies(feature2.name);
|
||||||
|
const feature3Strategies = await fetchFeatureStrategies(feature3.name);
|
||||||
|
|
||||||
await addSegmentsToStrategy(
|
await addSegmentsToStrategy(
|
||||||
[segment1.id, segment2.id, segment3.id],
|
[segment1.id, segment2.id, segment3.id],
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
feature1.strategies[0].id,
|
feature1Strategies[0].id,
|
||||||
);
|
);
|
||||||
await addSegmentsToStrategy(
|
await addSegmentsToStrategy(
|
||||||
[segment2.id, segment3.id],
|
[segment2.id, segment3.id],
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
feature2.strategies[0].id,
|
feature2Strategies[0].id,
|
||||||
);
|
);
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
await addSegmentsToStrategy([segment3.id], feature3.strategies[0].id);
|
await addSegmentsToStrategy([segment3.id], feature3Strategies[0].id);
|
||||||
|
|
||||||
const segmentStrategies1 = await fetchSegmentStrategies(segment1.id);
|
const segmentStrategies1 = await fetchSegmentStrategies(segment1.id);
|
||||||
const segmentStrategies2 = await fetchSegmentStrategies(segment2.id);
|
const segmentStrategies2 = await fetchSegmentStrategies(segment2.id);
|
||||||
const segmentStrategies3 = await fetchSegmentStrategies(segment3.id);
|
const segmentStrategies3 = await fetchSegmentStrategies(segment3.id);
|
||||||
|
|
||||||
expect(collectIds(segmentStrategies1.strategies)).toEqual(
|
expect(collectIds(segmentStrategies1.strategies)).toEqual(
|
||||||
collectIds(feature1.strategies),
|
collectIds(feature1Strategies),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(collectIds(segmentStrategies2.strategies)).toEqual(
|
expect(collectIds(segmentStrategies2.strategies)).toEqual(
|
||||||
collectIds([...feature1.strategies, ...feature2.strategies]),
|
collectIds([...feature1Strategies, ...feature2Strategies]),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(collectIds(segmentStrategies3.strategies)).toEqual(
|
expect(collectIds(segmentStrategies3.strategies)).toEqual(
|
||||||
collectIds([
|
collectIds([
|
||||||
...feature1.strategies,
|
...feature1Strategies,
|
||||||
...feature2.strategies,
|
...feature2Strategies,
|
||||||
...feature3.strategies,
|
...feature3Strategies,
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -448,30 +464,34 @@ test('should list segments by strategy', async () => {
|
|||||||
const [feature1, feature2, feature3] = await fetchFeatures();
|
const [feature1, feature2, feature3] = await fetchFeatures();
|
||||||
const [segment1, segment2, segment3] = await fetchSegments();
|
const [segment1, segment2, segment3] = await fetchSegments();
|
||||||
|
|
||||||
|
const [feature1Strategy] = await fetchFeatureStrategies(feature1.name);
|
||||||
|
const [feature2Strategy] = await fetchFeatureStrategies(feature2.name);
|
||||||
|
const [feature3Strategy] = await fetchFeatureStrategies(feature3.name);
|
||||||
|
|
||||||
await addSegmentsToStrategy(
|
await addSegmentsToStrategy(
|
||||||
[segment1.id, segment2.id, segment3.id],
|
[segment1.id, segment2.id, segment3.id],
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
feature1.strategies[0].id,
|
feature1Strategy.id,
|
||||||
);
|
);
|
||||||
await addSegmentsToStrategy(
|
await addSegmentsToStrategy(
|
||||||
[segment2.id, segment3.id],
|
[segment2.id, segment3.id],
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
feature2.strategies[0].id,
|
feature2Strategy.id,
|
||||||
);
|
);
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
await addSegmentsToStrategy([segment3.id], feature3.strategies[0].id);
|
await addSegmentsToStrategy([segment3.id], feature3Strategy.id);
|
||||||
|
|
||||||
const strategySegments1 = await fetchSegmentsByStrategy(
|
const strategySegments1 = await fetchSegmentsByStrategy(
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
feature1.strategies[0].id,
|
feature1Strategy.id,
|
||||||
);
|
);
|
||||||
const strategySegments2 = await fetchSegmentsByStrategy(
|
const strategySegments2 = await fetchSegmentsByStrategy(
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
feature2.strategies[0].id,
|
feature2Strategy.id,
|
||||||
);
|
);
|
||||||
const strategySegments3 = await fetchSegmentsByStrategy(
|
const strategySegments3 = await fetchSegmentsByStrategy(
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
feature3.strategies[0].id,
|
feature3Strategy.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(collectIds(strategySegments1)).toEqual(
|
expect(collectIds(strategySegments1)).toEqual(
|
||||||
@ -581,8 +601,9 @@ test('Should show usage in features and projects', async () => {
|
|||||||
flag.name,
|
flag.name,
|
||||||
);
|
);
|
||||||
const [feature] = await fetchFeatures();
|
const [feature] = await fetchFeatures();
|
||||||
|
const [strategy] = await fetchFeatureStrategies(feature.name);
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
await addSegmentsToStrategy([segment.id], feature.strategies[0].id);
|
await addSegmentsToStrategy([segment.id], strategy.id);
|
||||||
|
|
||||||
const segments = await fetchSegments();
|
const segments = await fetchSegments();
|
||||||
expect(segments).toMatchObject([
|
expect(segments).toMatchObject([
|
||||||
@ -768,8 +789,9 @@ describe('detect strategy usage in change requests', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [feature] = await fetchFeatures();
|
const [feature] = await fetchFeatures();
|
||||||
|
const [strategy] = await fetchFeatureStrategies(feature.name);
|
||||||
|
|
||||||
const strategyId = feature.strategies[0].id;
|
const strategyId = strategy.id;
|
||||||
|
|
||||||
await db.rawDatabase.table('change_request_events').insert({
|
await db.rawDatabase.table('change_request_events').insert({
|
||||||
feature: flag.name,
|
feature: flag.name,
|
||||||
@ -827,8 +849,9 @@ describe('detect strategy usage in change requests', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [feature] = await fetchFeatures();
|
const [feature] = await fetchFeatures();
|
||||||
|
const [strategy] = await fetchFeatureStrategies(feature.name);
|
||||||
|
|
||||||
const strategyId = feature.strategies[0].id;
|
const strategyId = strategy.id;
|
||||||
await addSegmentsToStrategy([segment.id], strategyId!);
|
await addSegmentsToStrategy([segment.id], strategyId!);
|
||||||
|
|
||||||
await db.rawDatabase.table('change_request_events').insert({
|
await db.rawDatabase.table('change_request_events').insert({
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
} from '../../../test/e2e/helpers/test-helper';
|
} from '../../../test/e2e/helpers/test-helper';
|
||||||
import type {
|
import type {
|
||||||
IConstraint,
|
IConstraint,
|
||||||
|
IFeatureOverview,
|
||||||
IFeatureToggleClient,
|
IFeatureToggleClient,
|
||||||
ISegment,
|
ISegment,
|
||||||
} from '../../types/model';
|
} from '../../types/model';
|
||||||
@ -35,13 +36,23 @@ const fetchSegments = (): Promise<ISegment[]> => {
|
|||||||
return app.services.segmentService.getAll();
|
return app.services.segmentService.getAll();
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchFeatures = (): Promise<IFeatureToggleClient[]> => {
|
const fetchFeatures = (): Promise<IFeatureOverview[]> => {
|
||||||
return app.request
|
return app.request
|
||||||
.get(`/api/admin/features`)
|
.get(`/api/admin/projects/default/features`)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.then((res) => res.body.features);
|
.then((res) => res.body.features);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getFeatureStrategiesPath = (featureName: string) => {
|
||||||
|
return `/api/admin/projects/default/features/${featureName}/environments/default/strategies`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchFeatureStrategies = (featureName: string) =>
|
||||||
|
app.request
|
||||||
|
.get(getFeatureStrategiesPath(featureName))
|
||||||
|
.expect(200)
|
||||||
|
.then((res) => res.body);
|
||||||
|
|
||||||
const fetchClientFeatures = (): Promise<IFeatureToggleClient[]> => {
|
const fetchClientFeatures = (): Promise<IFeatureToggleClient[]> => {
|
||||||
return app.request
|
return app.request
|
||||||
.get(FEATURES_CLIENT_BASE_PATH)
|
.get(FEATURES_CLIENT_BASE_PATH)
|
||||||
@ -276,8 +287,11 @@ test('should clone feature strategy segments', async () => {
|
|||||||
await createFeatureToggle(mockFeatureToggle());
|
await createFeatureToggle(mockFeatureToggle());
|
||||||
|
|
||||||
const [feature1, feature2] = await fetchFeatures();
|
const [feature1, feature2] = await fetchFeatures();
|
||||||
const strategy1 = feature1.strategies[0].id;
|
const [feature1Strategy] = await fetchFeatureStrategies(feature1.name);
|
||||||
const strategy2 = feature2.strategies[0].id;
|
const [feature2Strategy] = await fetchFeatureStrategies(feature2.name);
|
||||||
|
|
||||||
|
const strategy1 = feature1Strategy.id;
|
||||||
|
const strategy2 = feature2Strategy.id;
|
||||||
|
|
||||||
let segments1 = await app.services.segmentService.getByStrategy(strategy1!);
|
let segments1 = await app.services.segmentService.getByStrategy(strategy1!);
|
||||||
let segments2 = await app.services.segmentService.getByStrategy(strategy2!);
|
let segments2 = await app.services.segmentService.getByStrategy(strategy2!);
|
||||||
@ -322,9 +336,14 @@ test('should inline segment constraints into features by default', async () => {
|
|||||||
|
|
||||||
// add segment3 to all features
|
// add segment3 to all features
|
||||||
for (const feature of [feature1, feature2, feature3]) {
|
for (const feature of [feature1, feature2, feature3]) {
|
||||||
|
const [strt] = await fetchFeatureStrategies(feature.name);
|
||||||
const strategy = {
|
const strategy = {
|
||||||
...feature.strategies[0],
|
id: strt.id,
|
||||||
segments: feature.strategies[0].segments ?? [],
|
name: strt.name,
|
||||||
|
constraints: strt.constraints,
|
||||||
|
parameters: strt.parameters,
|
||||||
|
variants: strt.variants,
|
||||||
|
segments: strt.segments ?? [],
|
||||||
};
|
};
|
||||||
await updateFeatureStrategy(feature.name, {
|
await updateFeatureStrategy(feature.name, {
|
||||||
...strategy,
|
...strategy,
|
||||||
|
@ -162,7 +162,7 @@ test('should be favorited in admin endpoint', async () => {
|
|||||||
await favoriteFeature(featureName);
|
await favoriteFeature(featureName);
|
||||||
|
|
||||||
const { body } = await app.request
|
const { body } = await app.request
|
||||||
.get('/api/admin/features')
|
.get('/api/admin/projects/default/features')
|
||||||
.set('Content-Type', 'application/json')
|
.set('Content-Type', 'application/json')
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
|
@ -42,6 +42,6 @@ test('creates new feature flag with createdBy', async () => {
|
|||||||
test('should require authenticated user', async () => {
|
test('should require authenticated user', async () => {
|
||||||
expect.assertions(0);
|
expect.assertions(0);
|
||||||
const { request, destroy } = await setupAppWithAuth(db.stores);
|
const { request, destroy } = await setupAppWithAuth(db.stores);
|
||||||
await request.get('/api/admin/features').expect(401);
|
await request.get('/api/admin/projects/default/features').expect(401);
|
||||||
await destroy();
|
await destroy();
|
||||||
});
|
});
|
||||||
|
@ -36,7 +36,7 @@ test('should require authenticated user', async () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
const { request, destroy } = await setupAppWithCustomAuth(stores, preHook);
|
const { request, destroy } = await setupAppWithCustomAuth(stores, preHook);
|
||||||
await request.get('/api/admin/features').expect(401);
|
await request.get('/api/admin/projects/default/features').expect(401);
|
||||||
await destroy();
|
await destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -206,7 +206,7 @@ test('Calling validate endpoint with already existing session should destroy ses
|
|||||||
email: 'user@mail.com',
|
email: 'user@mail.com',
|
||||||
})
|
})
|
||||||
.expect(200);
|
.expect(200);
|
||||||
await request.get('/api/admin/features').expect(200);
|
await request.get('/api/admin/projects').expect(200);
|
||||||
const url = await resetTokenService.createResetPasswordUrl(
|
const url = await resetTokenService.createResetPasswordUrl(
|
||||||
user.id,
|
user.id,
|
||||||
adminUser.username!,
|
adminUser.username!,
|
||||||
@ -214,7 +214,7 @@ test('Calling validate endpoint with already existing session should destroy ses
|
|||||||
const relative = getBackendResetUrl(url);
|
const relative = getBackendResetUrl(url);
|
||||||
|
|
||||||
await request.get(relative).expect(200).expect('Content-Type', /json/);
|
await request.get(relative).expect(200).expect('Content-Type', /json/);
|
||||||
await request.get('/api/admin/features').expect(401); // we no longer should have a valid session
|
await request.get('/api/admin/projects').expect(401); // we no longer should have a valid session
|
||||||
await destroy();
|
await destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -240,7 +240,7 @@ test('Calling reset endpoint with already existing session should logout/destroy
|
|||||||
email: 'user@mail.com',
|
email: 'user@mail.com',
|
||||||
})
|
})
|
||||||
.expect(200);
|
.expect(200);
|
||||||
await request.get('/api/admin/features').expect(200); // If we login we can access features endpoint
|
await request.get('/api/admin/projects').expect(200); // If we login we can access projects endpoint
|
||||||
await request
|
await request
|
||||||
.post('/auth/reset/password')
|
.post('/auth/reset/password')
|
||||||
.send({
|
.send({
|
||||||
@ -248,7 +248,7 @@ test('Calling reset endpoint with already existing session should logout/destroy
|
|||||||
password,
|
password,
|
||||||
})
|
})
|
||||||
.expect(200);
|
.expect(200);
|
||||||
await request.get('/api/admin/features').expect(401); // we no longer have a valid session after using the reset password endpoint
|
await request.get('/api/admin/projects').expect(401); // we no longer have a valid session after using the reset password endpoint
|
||||||
await destroy();
|
await destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ test('Using custom auth type without defining custom middleware causes default D
|
|||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
await request
|
await request
|
||||||
.get('/api/admin/features')
|
.get('/api/admin/projects')
|
||||||
.expect(401)
|
.expect(401)
|
||||||
.expect((res) => {
|
.expect((res) => {
|
||||||
expect(res.body.error).toBe(
|
expect(res.body.error).toBe(
|
||||||
@ -56,6 +56,6 @@ test('Using custom auth type without defining custom middleware causes default D
|
|||||||
test('If actually configuring a custom middleware should configure the middleware', async () => {
|
test('If actually configuring a custom middleware should configure the middleware', async () => {
|
||||||
expect.assertions(0);
|
expect.assertions(0);
|
||||||
const { request, destroy } = await setupAppWithCustomAuth(stores, preHook);
|
const { request, destroy } = await setupAppWithCustomAuth(stores, preHook);
|
||||||
await request.get('/api/admin/features').expect(200);
|
await request.get('/api/admin/projects').expect(200);
|
||||||
await destroy();
|
await destroy();
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user