1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01: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:
David Leek 2024-05-27 09:24:09 +02:00 committed by GitHub
parent 6f2a10922d
commit 9ea66e8850
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 85 additions and 272 deletions

View File

@ -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;

View File

@ -73,14 +73,6 @@ exports[`returns all baseRoutes 1`] = `
"title": "Create feature flag",
"type": "protected",
},
{
"component": [Function],
"menu": {},
"parent": "/features",
"path": "/projects/:projectId/features2/:featureId",
"title": ":featureId",
"type": "protected",
},
{
"component": {
"$$typeof": Symbol(react.lazy),
@ -106,14 +98,6 @@ exports[`returns all baseRoutes 1`] = `
"title": "Projects",
"type": "protected",
},
{
"component": [Function],
"menu": {},
"parent": "/features",
"path": "/features/:activeTab/:featureId",
"title": ":featureId",
"type": "protected",
},
{
"component": [Function],
"menu": {

View File

@ -18,7 +18,6 @@ import CreateTagType from 'component/tags/CreateTagType/CreateTagType';
import CreateFeature from 'component/feature/CreateFeature/CreateFeature';
import EditFeature from 'component/feature/EditFeature/EditFeature';
import ContextList from 'component/context/ContextList/ContextList/ContextList';
import RedirectFeatureView from 'component/feature/RedirectFeatureView/RedirectFeatureView';
import { CreateIntegration } from 'component/integrations/CreateIntegration/CreateIntegration';
import { EditIntegration } from 'component/integrations/EditIntegration/EditIntegration';
import { CopyFeatureToggle } from 'component/feature/CopyFeature/CopyFeature';
@ -110,14 +109,6 @@ export const routes: IRoute[] = [
type: 'protected',
menu: {},
},
{
path: '/projects/:projectId/features2/:featureId',
parent: '/features',
title: ':featureId',
component: RedirectFeatureView,
type: 'protected',
menu: {},
},
{
path: '/projects/:projectId/*',
parent: '/projects',
@ -136,14 +127,6 @@ export const routes: IRoute[] = [
},
// Features
{
path: '/features/:activeTab/:featureId',
parent: '/features',
title: ':featureId',
component: RedirectFeatureView,
type: 'protected',
menu: {},
},
{
path: '/search',
title: 'Search',

View File

@ -12,20 +12,17 @@ import RadioButtonChecked from '@mui/icons-material/RadioButtonChecked';
import { AppsLinkList } from 'component/common';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import styles from '../../strategies.module.scss';
import { TogglesLinkList } from 'component/strategies/TogglesLinkList/TogglesLinkList';
import type { IStrategy, IStrategyParameter } from 'interfaces/strategy';
import type { ApplicationSchema, FeatureSchema } from 'openapi';
import type { ApplicationSchema } from 'openapi';
interface IStrategyDetailsProps {
strategy: IStrategy;
applications: ApplicationSchema[];
toggles: FeatureSchema[];
}
export const StrategyDetails = ({
strategy,
applications,
toggles,
}: IStrategyDetailsProps) => {
const theme = useTheme();
const { parameters = [] } = strategy;
@ -84,7 +81,7 @@ export const StrategyDetails = ({
<List>{renderParameters(parameters)}</List>
</Grid>
<Grid item sm={12} md={toggles.length > 0 ? 6 : 12}>
<Grid item sm={12} md={12}>
<h6>
Applications using this strategy{' '}
{applications.length >= 1000 && '(Capped at 1000)'}
@ -92,17 +89,6 @@ export const StrategyDetails = ({
<hr />
<AppsLinkList apps={applications} />
</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>
</div>
);

View File

@ -3,7 +3,6 @@ import { useNavigate } from 'react-router-dom';
import { UPDATE_STRATEGY } from 'component/providers/AccessProvider/permissions';
import { PageContent } from 'component/common/PageContent/PageContent';
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 { StrategyDetails } from './StrategyDetails/StrategyDetails';
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 { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import type { FeatureSchema } from 'openapi/models';
export const StrategyView = () => {
const name = useRequiredPathParam('name');
const { strategies } = useStrategies();
const { features = [] } = useFeatures();
const { applications } = useApplications();
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 handleEdit = () => {
@ -64,7 +52,6 @@ export const StrategyView = () => {
<Grid item xs={12} sm={12}>
<StrategyDetails
strategy={strategy}
toggles={toggles}
applications={applications}
/>
</Grid>

View File

@ -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,
};
};

View File

@ -1099,41 +1099,6 @@ class FeatureToggleService {
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(
params: IFeatureProjectUserParams,
): Promise<IFeatureOverview[]> {
@ -1949,10 +1914,6 @@ class FeatureToggleService {
);
}
async getArchivedFeatures(): Promise<FeatureToggle[]> {
return this.getFeatureToggles({}, undefined, true);
}
// TODO: add project id.
async deleteFeature(
featureName: string,

View File

@ -10,13 +10,8 @@ import type { IFeatureToggleQuery } from '../../../types/model';
import type FeatureTagService from '../../../services/feature-tag-service';
import type { IAuthRequest } from '../../../routes/unleash-types';
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 { TagsSchema } from '../../../openapi/spec/tags-schema';
import { serializeDates } from '../../../types/serialize-dates';
import type { OpenApiService } from '../../../services/openapi-service';
import { createRequestSchema } from '../../../openapi/util/create-request-schema';
import {
@ -55,27 +50,6 @@ class FeatureController extends Controller {
this.openApiService = openApiService;
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({
method: 'post',
path: '/validate',
@ -210,23 +184,6 @@ class FeatureController extends Controller {
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(
req: Request<{ featureName: string }, any, any, any>,
res: Response,

View File

@ -70,7 +70,7 @@ test('should return last seen at per env for /api/admin/features', async () => {
await insertLastSeenAt('lastSeenAtPerEnv', db.rawDatabase, 'default');
const response = await app.request
.get('/api/admin/features')
.get('/api/admin/projects/default/features')
.expect('Content-Type', /json/)
.expect(200);
@ -88,7 +88,7 @@ test('response should include last seen at per environment for multiple environm
await setupLastSeenAtTest(featureName);
const { body } = await app.request
.get('/api/admin/features')
.get('/api/admin/projects/default/features')
.expect('Content-Type', /json/)
.expect(200);

View File

@ -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 () => {
await app.createFeature('metric-feature');

View File

@ -239,7 +239,7 @@ test('should allow requests with an admin token', async () => {
test('should not allow admin requests with a frontend token', async () => {
const frontendToken = await createApiToken(ApiTokenType.FRONTEND);
await app.request
.get('/api/admin/features')
.get('/api/admin/projects')
.set('Authorization', frontendToken.secret)
.expect('Content-Type', /json/)
.expect(403);

View File

@ -1,5 +1,5 @@
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 dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init';
import getLogger from '../../../test/fixtures/no-logger';
@ -12,13 +12,17 @@ import {
setupAppWithCustomConfig,
} from '../../../test/e2e/helpers/test-helper';
import type { StrategiesUsingSegment } from './segment-service-interface';
import type { IUser } from '../../types';
import type { IFeatureOverview, IUser } from '../../types';
let app: IUnleashTest;
let db: ITestDb;
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.
type SerializeDatesDeep<T> = {
@ -39,12 +43,18 @@ const fetchSegmentsByStrategy = (
.expect(200)
.then((res) => res.body.segments);
const fetchFeatures = (): Promise<IFeatureToggleClient[]> =>
const fetchFeatures = (): Promise<IFeatureOverview[]> =>
app.request
.get(FEATURES_LIST_BASE_PATH)
.expect(200)
.then((res) => res.body.features);
const fetchFeatureStrategies = (featureName: string) =>
app.request
.get(getFeatureStrategiesPath(featureName))
.expect(200)
.then((res) => res.body);
const fetchSegmentStrategies = (
segmentId: number,
): Promise<StrategiesUsingSegment> =>
@ -288,8 +298,9 @@ test('should not delete segments used by strategies', async () => {
flag.name,
);
const [feature] = await fetchFeatures();
const [strategy] = await fetchFeatureStrategies(feature.name);
//@ts-ignore
await addSegmentsToStrategy([segment.id], feature.strategies[0].id);
await addSegmentsToStrategy([segment.id], strategy.id);
const segments = await fetchSegments();
expect(segments.length).toEqual(1);
@ -316,8 +327,9 @@ test('should delete segments used by strategies in archived feature flags', asyn
flag.name,
);
const [feature] = await fetchFeatures();
const [strategy] = await fetchFeatureStrategies(feature.name);
//@ts-ignore
await addSegmentsToStrategy([segment.id], feature.strategies[0].id);
await addSegmentsToStrategy([segment.id], strategy.id);
const segments = await fetchSegments();
expect(segments.length).toEqual(1);
@ -372,36 +384,40 @@ test('should list strategies by segment', async () => {
const [feature1, feature2, feature3] = await fetchFeatures();
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(
[segment1.id, segment2.id, segment3.id],
//@ts-ignore
feature1.strategies[0].id,
feature1Strategies[0].id,
);
await addSegmentsToStrategy(
[segment2.id, segment3.id],
//@ts-ignore
feature2.strategies[0].id,
feature2Strategies[0].id,
);
//@ts-ignore
await addSegmentsToStrategy([segment3.id], feature3.strategies[0].id);
await addSegmentsToStrategy([segment3.id], feature3Strategies[0].id);
const segmentStrategies1 = await fetchSegmentStrategies(segment1.id);
const segmentStrategies2 = await fetchSegmentStrategies(segment2.id);
const segmentStrategies3 = await fetchSegmentStrategies(segment3.id);
expect(collectIds(segmentStrategies1.strategies)).toEqual(
collectIds(feature1.strategies),
collectIds(feature1Strategies),
);
expect(collectIds(segmentStrategies2.strategies)).toEqual(
collectIds([...feature1.strategies, ...feature2.strategies]),
collectIds([...feature1Strategies, ...feature2Strategies]),
);
expect(collectIds(segmentStrategies3.strategies)).toEqual(
collectIds([
...feature1.strategies,
...feature2.strategies,
...feature3.strategies,
...feature1Strategies,
...feature2Strategies,
...feature3Strategies,
]),
);
});
@ -448,30 +464,34 @@ test('should list segments by strategy', async () => {
const [feature1, feature2, feature3] = await fetchFeatures();
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(
[segment1.id, segment2.id, segment3.id],
//@ts-ignore
feature1.strategies[0].id,
feature1Strategy.id,
);
await addSegmentsToStrategy(
[segment2.id, segment3.id],
//@ts-ignore
feature2.strategies[0].id,
feature2Strategy.id,
);
//@ts-ignore
await addSegmentsToStrategy([segment3.id], feature3.strategies[0].id);
await addSegmentsToStrategy([segment3.id], feature3Strategy.id);
const strategySegments1 = await fetchSegmentsByStrategy(
//@ts-ignore
feature1.strategies[0].id,
feature1Strategy.id,
);
const strategySegments2 = await fetchSegmentsByStrategy(
//@ts-ignore
feature2.strategies[0].id,
feature2Strategy.id,
);
const strategySegments3 = await fetchSegmentsByStrategy(
//@ts-ignore
feature3.strategies[0].id,
feature3Strategy.id,
);
expect(collectIds(strategySegments1)).toEqual(
@ -581,8 +601,9 @@ test('Should show usage in features and projects', async () => {
flag.name,
);
const [feature] = await fetchFeatures();
const [strategy] = await fetchFeatureStrategies(feature.name);
//@ts-ignore
await addSegmentsToStrategy([segment.id], feature.strategies[0].id);
await addSegmentsToStrategy([segment.id], strategy.id);
const segments = await fetchSegments();
expect(segments).toMatchObject([
@ -768,8 +789,9 @@ describe('detect strategy usage in change requests', () => {
);
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({
feature: flag.name,
@ -827,8 +849,9 @@ describe('detect strategy usage in change requests', () => {
);
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 db.rawDatabase.table('change_request_events').insert({

View File

@ -6,6 +6,7 @@ import {
} from '../../../test/e2e/helpers/test-helper';
import type {
IConstraint,
IFeatureOverview,
IFeatureToggleClient,
ISegment,
} from '../../types/model';
@ -35,13 +36,23 @@ const fetchSegments = (): Promise<ISegment[]> => {
return app.services.segmentService.getAll();
};
const fetchFeatures = (): Promise<IFeatureToggleClient[]> => {
const fetchFeatures = (): Promise<IFeatureOverview[]> => {
return app.request
.get(`/api/admin/features`)
.get(`/api/admin/projects/default/features`)
.expect(200)
.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[]> => {
return app.request
.get(FEATURES_CLIENT_BASE_PATH)
@ -276,8 +287,11 @@ test('should clone feature strategy segments', async () => {
await createFeatureToggle(mockFeatureToggle());
const [feature1, feature2] = await fetchFeatures();
const strategy1 = feature1.strategies[0].id;
const strategy2 = feature2.strategies[0].id;
const [feature1Strategy] = await fetchFeatureStrategies(feature1.name);
const [feature2Strategy] = await fetchFeatureStrategies(feature2.name);
const strategy1 = feature1Strategy.id;
const strategy2 = feature2Strategy.id;
let segments1 = await app.services.segmentService.getByStrategy(strategy1!);
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
for (const feature of [feature1, feature2, feature3]) {
const [strt] = await fetchFeatureStrategies(feature.name);
const strategy = {
...feature.strategies[0],
segments: feature.strategies[0].segments ?? [],
id: strt.id,
name: strt.name,
constraints: strt.constraints,
parameters: strt.parameters,
variants: strt.variants,
segments: strt.segments ?? [],
};
await updateFeatureStrategy(feature.name, {
...strategy,

View File

@ -162,7 +162,7 @@ test('should be favorited in admin endpoint', async () => {
await favoriteFeature(featureName);
const { body } = await app.request
.get('/api/admin/features')
.get('/api/admin/projects/default/features')
.set('Content-Type', 'application/json')
.expect(200);

View File

@ -42,6 +42,6 @@ test('creates new feature flag with createdBy', async () => {
test('should require authenticated user', async () => {
expect.assertions(0);
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();
});

View File

@ -36,7 +36,7 @@ test('should require authenticated user', async () => {
);
};
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();
});

View File

@ -206,7 +206,7 @@ test('Calling validate endpoint with already existing session should destroy ses
email: 'user@mail.com',
})
.expect(200);
await request.get('/api/admin/features').expect(200);
await request.get('/api/admin/projects').expect(200);
const url = await resetTokenService.createResetPasswordUrl(
user.id,
adminUser.username!,
@ -214,7 +214,7 @@ test('Calling validate endpoint with already existing session should destroy ses
const relative = getBackendResetUrl(url);
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();
});
@ -240,7 +240,7 @@ test('Calling reset endpoint with already existing session should logout/destroy
email: 'user@mail.com',
})
.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
.post('/auth/reset/password')
.send({
@ -248,7 +248,7 @@ test('Calling reset endpoint with already existing session should logout/destroy
password,
})
.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();
});

View File

@ -43,7 +43,7 @@ test('Using custom auth type without defining custom middleware causes default D
undefined,
);
await request
.get('/api/admin/features')
.get('/api/admin/projects')
.expect(401)
.expect((res) => {
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 () => {
expect.assertions(0);
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();
});