From 8d7a0fdd7fa4f614b771fd52a1d5b00a2c5ecda2 Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Wed, 4 Jun 2025 09:59:36 +0200 Subject: [PATCH] chore(adminapi)!: Remove deprecated project overview (#10069) BREAKING CHANGE: As part of the preparation for a new major (7.0) this removes /api/admin/projects/{projectId} endpoint. It has been deprecated since 5.8, and we don't use it anymore in our frontend. --- .../tests/feature-toggles.e2e.test.ts | 96 +-------- .../features/project/project-controller.ts | 46 ---- src/lib/features/project/projects.e2e.test.ts | 197 ------------------ ...deprecated-project-overview-schema.test.ts | 28 --- .../deprecated-project-overview-schema.ts | 155 -------------- src/lib/openapi/spec/index.ts | 1 - src/test/e2e/api/admin/favorites.e2e.test.ts | 24 +-- .../admin/project/project.health.e2e.test.ts | 93 +-------- 8 files changed, 3 insertions(+), 637 deletions(-) delete mode 100644 src/lib/openapi/spec/deprecated-project-overview-schema.test.ts delete mode 100644 src/lib/openapi/spec/deprecated-project-overview-schema.ts diff --git a/src/lib/features/feature-toggle/tests/feature-toggles.e2e.test.ts b/src/lib/features/feature-toggle/tests/feature-toggles.e2e.test.ts index 3bf485b6e8..e4001b2e9b 100644 --- a/src/lib/features/feature-toggle/tests/feature-toggles.e2e.test.ts +++ b/src/lib/features/feature-toggle/tests/feature-toggles.e2e.test.ts @@ -203,30 +203,6 @@ test('Trying to add a strategy configuration to environment not connected to fla }); }); -test('Can get project overview', async () => { - await app.request - .post('/api/admin/projects/default/features') - .send({ - name: 'project-overview', - enabled: false, - strategies: [{ name: 'default' }], - }) - .set('Content-Type', 'application/json') - .expect(201) - .expect((res) => { - expect(res.body.name).toBe('project-overview'); - expect(res.body.createdAt).toBeTruthy(); - }); - await app.request - .get('/api/admin/projects/default') - .expect(200) - .expect((r) => { - expect(r.body.name).toBe('Default'); - expect(r.body.features).toHaveLength(2); - expect(r.body.members).toBe(0); - }); -}); - test('should list dependencies and children', async () => { const parent = uuidv4(); const child = uuidv4(); @@ -390,76 +366,6 @@ test('Can get features for project', async () => { }); }); -test('Project overview includes environment connected to feature', async () => { - await app.request - .post('/api/admin/projects/default/features') - .send({ - name: 'com.test.environment', - enabled: false, - strategies: [{ name: 'default' }], - }) - .set('Content-Type', 'application/json') - .expect(201) - .expect((res) => { - expect(res.body.name).toBe('com.test.environment'); - expect(res.body.createdAt).toBeTruthy(); - }); - await db.stores.environmentStore.create({ - name: 'project-overview', - type: 'production', - }); - await app.request - .post('/api/admin/projects/default/environments') - .send({ environment: 'project-overview' }) - .expect(200); - return app.request - .get('/api/admin/projects/default') - .expect(200) - .expect((r) => { - expect(r.body.features[0].environments[0].name).toBe(DEFAULT_ENV); - expect(r.body.features[0].environments[1].name).toBe( - 'project-overview', - ); - }); -}); - -test('Disconnecting environment from project, removes environment from features in project overview', async () => { - await app.request - .post('/api/admin/projects/default/features') - .send({ - name: 'com.test.disconnect.environment', - enabled: false, - strategies: [{ name: 'default' }], - }) - .set('Content-Type', 'application/json') - .expect(201) - .expect((res) => { - expect(res.body.name).toBe('com.test.disconnect.environment'); - expect(res.body.createdAt).toBeTruthy(); - }); - await db.stores.environmentStore.create({ - name: 'dis-project-overview', - type: 'production', - }); - await app.request - .post('/api/admin/projects/default/environments') - .send({ environment: 'dis-project-overview' }) - .expect(200); - await app.request - .delete('/api/admin/projects/default/environments/dis-project-overview') - .expect(200); - return app.request - .get('/api/admin/projects/default') - .expect(200) - .expect((r) => { - expect( - r.body.features.some( - (e) => e.environment === 'dis-project-overview', - ), - ).toBeFalsy(); - }); -}); - test('Can enable/disable environment for feature with strategies', async () => { const envName = 'enable-feature-environment'; const featureName = 'com.test.enable.environment'; @@ -756,7 +662,7 @@ describe('Interacting with features using project IDs that belong to other proje // ensure the new project has been created await app.request - .get(`/api/admin/projects/${otherProject}`) + .get(`/api/admin/projects/${otherProject}/health-report`) .expect(200); // create flag in default project diff --git a/src/lib/features/project/project-controller.ts b/src/lib/features/project/project-controller.ts index ba96477732..462bf354a7 100644 --- a/src/lib/features/project/project-controller.ts +++ b/src/lib/features/project/project-controller.ts @@ -15,8 +15,6 @@ import type ProjectService from './project-service.js'; import VariantsController from '../../routes/admin-api/project/variants.js'; import { createResponseSchema, - type DeprecatedProjectOverviewSchema, - deprecatedProjectOverviewSchema, outdatedSdksSchema, type OutdatedSdksSchema, type ProjectDoraMetricsSchema, @@ -95,29 +93,6 @@ export default class ProjectController extends Controller { ], }); - this.route({ - method: 'get', - path: '/:projectId', - handler: this.getDeprecatedProjectOverview, - permission: NONE, - middleware: [ - this.openApiService.validPath({ - tags: ['Projects'], - operationId: 'getDeprecatedProjectOverview', - summary: 'Get an overview of a project. (deprecated)', - deprecated: true, - description: - 'This endpoint returns an overview of the specified projects stats, project health, number of members, which environments are configured, and the features in the project.', - responses: { - 200: createResponseSchema( - 'deprecatedProjectOverviewSchema', - ), - ...getStandardResponses(401, 403, 404), - }, - }), - ], - }); - this.route({ method: 'get', path: '/:projectId/overview', @@ -260,27 +235,6 @@ export default class ProjectController extends Controller { ); } - async getDeprecatedProjectOverview( - req: IAuthRequest, - res: Response, - ): Promise { - const { projectId } = req.params; - const { archived } = req.query; - const { user } = req; - const overview = await this.projectService.getProjectHealth( - projectId, - archived, - user.id, - ); - - this.openApiService.respondWithValidation( - 200, - res, - deprecatedProjectOverviewSchema.$id, - serializeDates(overview), - ); - } - async getProjectOverview( req: IAuthRequest, res: Response, diff --git a/src/lib/features/project/projects.e2e.test.ts b/src/lib/features/project/projects.e2e.test.ts index d5799d0a80..802aec3f7d 100644 --- a/src/lib/features/project/projects.e2e.test.ts +++ b/src/lib/features/project/projects.e2e.test.ts @@ -2,15 +2,12 @@ import dbInit, { type ITestDb, } from '../../../test/e2e/helpers/database-init.js'; import { - insertFeatureEnvironmentsLastSeen, - insertLastSeenAt, type IUnleashTest, setupAppWithCustomConfig, } from '../../../test/e2e/helpers/test-helper.js'; import getLogger from '../../../test/fixtures/no-logger.js'; import type { IProjectStore } from '../../types/index.js'; -import { DEFAULT_ENV } from '../../util/index.js'; let app: IUnleashTest; let db: ITestDb; @@ -45,76 +42,6 @@ afterAll(async () => { await db.destroy(); }); -test('should report has strategies and enabled strategies', async () => { - const app = await setupAppWithCustomConfig( - db.stores, - { - experimental: { - flags: {}, - }, - }, - db.rawDatabase, - ); - await app.createFeature('featureWithStrategies'); - await app.createFeature('featureWithoutStrategies'); - await app.createFeature('featureWithDisabledStrategies'); - await app.addStrategyToFeatureEnv( - { - name: 'default', - }, - DEFAULT_ENV, - 'featureWithStrategies', - ); - await app.addStrategyToFeatureEnv( - { - name: 'default', - disabled: true, - }, - DEFAULT_ENV, - 'featureWithDisabledStrategies', - ); - - const { body } = await app.request - .get('/api/admin/projects/default') - .expect('Content-Type', /json/) - .expect(200); - - expect(body).toMatchObject({ - features: [ - { - name: 'featureWithStrategies', - environments: [ - { - name: 'default', - hasStrategies: true, - hasEnabledStrategies: true, - }, - ], - }, - { - name: 'featureWithoutStrategies', - environments: [ - { - name: 'default', - hasStrategies: false, - hasEnabledStrategies: false, - }, - ], - }, - { - name: 'featureWithDisabledStrategies', - environments: [ - { - name: 'default', - hasStrategies: true, - hasEnabledStrategies: false, - }, - ], - }, - ], - }); -}); - test('Should ONLY return default project', async () => { projectStore.create({ id: 'test2', @@ -140,13 +67,6 @@ test('response should include created_at', async () => { expect(body.projects[0].createdAt).toBeDefined(); }); -test('response for default project should include created_at', async () => { - const { body } = await app.request - .get('/api/admin/projects/default') - .expect('Content-Type', /json/) - .expect(200); - expect(body.createdAt).toBeDefined(); -}); test('response for project overview should include feature type counts', async () => { await app.createFeature({ name: 'my-new-release-toggle', @@ -173,120 +93,3 @@ test('response for project overview should include feature type counts', async ( ], }); }); - -test('response should include last seen at per environment', async () => { - await app.createFeature('my-new-feature-toggle'); - - await insertLastSeenAt( - 'my-new-feature-toggle', - db.rawDatabase, - 'default', - testDate, - ); - await insertFeatureEnvironmentsLastSeen( - 'my-new-feature-toggle', - db.rawDatabase, - 'default', - ); - - const { body } = await app.request - .get('/api/admin/projects/default') - .expect('Content-Type', /json/) - .expect(200); - - expect(body.features[0].environments[0].lastSeenAt).toEqual(testDate); - expect(body.features[0].lastSeenAt).toEqual(testDate); - - const appWithLastSeenRefactor = await setupAppWithCustomConfig( - db.stores, - {}, - db.rawDatabase, - ); - - const response = await appWithLastSeenRefactor.request - .get('/api/admin/projects/default') - .expect('Content-Type', /json/) - .expect(200); - - expect(response.body.features[0].environments[0].lastSeenAt).toEqual( - '2023-10-01T12:34:56.000Z', - ); - expect(response.body.features[0].lastSeenAt).toEqual( - '2023-10-01T12:34:56.000Z', - ); -}); - -test('response should include last seen at per environment for multiple environments', async () => { - const appWithLastSeenRefactor = await setupAppWithCustomConfig( - db.stores, - {}, - db.rawDatabase, - ); - await app.createFeature('my-new-feature-toggle'); - - await db.stores.environmentStore.create({ - name: 'development', - type: 'development', - sortOrder: 1, - enabled: true, - }); - - await db.stores.environmentStore.create({ - name: 'production', - type: 'production', - sortOrder: 2, - enabled: true, - }); - - await appWithLastSeenRefactor.services.projectService.addEnvironmentToProject( - 'default', - 'development', - ); - await appWithLastSeenRefactor.services.projectService.addEnvironmentToProject( - 'default', - 'production', - ); - - await appWithLastSeenRefactor.createFeature( - 'multiple-environment-last-seen-at', - ); - - await insertLastSeenAt( - 'multiple-environment-last-seen-at', - db.rawDatabase, - 'default', - '2023-10-01T12:32:56.000Z', - ); - await insertLastSeenAt( - 'multiple-environment-last-seen-at', - db.rawDatabase, - 'development', - '2023-10-01T12:34:56.000Z', - ); - await insertLastSeenAt( - 'multiple-environment-last-seen-at', - db.rawDatabase, - 'production', - '2023-10-01T12:33:56.000Z', - ); - - const { body } = await appWithLastSeenRefactor.request - .get('/api/admin/projects/default') - .expect('Content-Type', /json/) - .expect(200); - - const featureEnvironments = body.features[1].environments; - - const [def, development, production] = featureEnvironments; - - expect(def.name).toBe('default'); - expect(def.lastSeenAt).toEqual('2023-10-01T12:32:56.000Z'); - - expect(development.name).toBe('development'); - expect(development.lastSeenAt).toEqual('2023-10-01T12:34:56.000Z'); - - expect(production.name).toBe('production'); - expect(production.lastSeenAt).toEqual('2023-10-01T12:33:56.000Z'); - - expect(body.features[1].lastSeenAt).toBe('2023-10-01T12:34:56.000Z'); -}); diff --git a/src/lib/openapi/spec/deprecated-project-overview-schema.test.ts b/src/lib/openapi/spec/deprecated-project-overview-schema.test.ts deleted file mode 100644 index 407f466077..0000000000 --- a/src/lib/openapi/spec/deprecated-project-overview-schema.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { DeprecatedProjectOverviewSchema } from './deprecated-project-overview-schema.js'; -import { validateSchema } from '../validate.js'; - -test('deprecatedProjectOverviewSchema', () => { - const data: DeprecatedProjectOverviewSchema = { - name: 'project', - version: 3, - featureNaming: { - description: 'naming description', - example: 'a', - pattern: '[aZ]', - }, - }; - - expect( - validateSchema( - '#/components/schemas/deprecatedProjectOverviewSchema', - data, - ), - ).toBeUndefined(); - - expect( - validateSchema( - '#/components/schemas/deprecatedProjectOverviewSchema', - {}, - ), - ).toMatchSnapshot(); -}); diff --git a/src/lib/openapi/spec/deprecated-project-overview-schema.ts b/src/lib/openapi/spec/deprecated-project-overview-schema.ts deleted file mode 100644 index a362b0770e..0000000000 --- a/src/lib/openapi/spec/deprecated-project-overview-schema.ts +++ /dev/null @@ -1,155 +0,0 @@ -import type { FromSchema } from 'json-schema-to-ts'; -import { parametersSchema } from './parameters-schema.js'; -import { variantSchema } from './variant-schema.js'; -import { overrideSchema } from './override-schema.js'; -import { featureStrategySchema } from './feature-strategy-schema.js'; -import { featureSchema } from './feature-schema.js'; -import { constraintSchema } from './constraint-schema.js'; -import { environmentSchema } from './environment-schema.js'; -import { featureEnvironmentSchema } from './feature-environment-schema.js'; -import { projectStatsSchema } from './project-stats-schema.js'; -import { createFeatureStrategySchema } from './create-feature-strategy-schema.js'; -import { projectEnvironmentSchema } from './project-environment-schema.js'; -import { createStrategyVariantSchema } from './create-strategy-variant-schema.js'; -import { strategyVariantSchema } from './strategy-variant-schema.js'; -import { createFeatureNamingPatternSchema } from './create-feature-naming-pattern-schema.js'; -import { tagSchema } from './tag-schema.js'; - -export const deprecatedProjectOverviewSchema = { - $id: '#/components/schemas/deprecatedProjectOverviewSchema', - type: 'object', - additionalProperties: false, - required: ['version', 'name'], - description: - 'A high-level overview of a project. It contains information such as project statistics, the name of the project, what members and what features it contains, etc.', - properties: { - stats: { - $ref: '#/components/schemas/projectStatsSchema', - description: 'Project statistics', - }, - version: { - type: 'integer', - example: 1, - description: - 'The schema version used to describe the project overview', - }, - name: { - type: 'string', - example: 'dx-squad', - description: 'The name of this project', - }, - description: { - type: 'string', - nullable: true, - example: 'DX squad feature release', - description: 'Additional information about the project', - }, - defaultStickiness: { - type: 'string', - example: 'userId', - description: - 'A default stickiness for the project affecting the default stickiness value for variants and Gradual Rollout strategy', - }, - mode: { - type: 'string', - enum: ['open', 'protected', 'private'], - example: 'open', - description: - "The project's [collaboration mode](https://docs.getunleash.io/reference/project-collaboration-mode). Determines whether non-project members can submit change requests or not.", - }, - featureLimit: { - type: 'number', - nullable: true, - example: 100, - description: - 'A limit on the number of features allowed in the project. Null if no limit.', - }, - featureNaming: { - $ref: '#/components/schemas/createFeatureNamingPatternSchema', - }, - members: { - type: 'number', - example: 4, - description: 'The number of members this project has', - }, - health: { - type: 'number', - example: 50, - description: - "An indicator of the [project's health](https://docs.getunleash.io/reference/technical-debt#project-status) on a scale from 0 to 100", - }, - environments: { - type: 'array', - items: { - $ref: '#/components/schemas/projectEnvironmentSchema', - }, - example: [ - { environment: 'development' }, - { - environment: 'production', - defaultStrategy: { - name: 'flexibleRollout', - constraints: [], - parameters: { - rollout: '50', - stickiness: 'customAppName', - groupId: 'stickytoggle', - }, - }, - }, - ], - description: 'The environments that are enabled for this project', - }, - features: { - type: 'array', - items: { - $ref: '#/components/schemas/featureSchema', - }, - description: - 'The full list of features in this project (excluding archived features)', - }, - updatedAt: { - type: 'string', - format: 'date-time', - nullable: true, - example: '2023-02-10T08:36:35.262Z', - description: 'When the project was last updated.', - }, - createdAt: { - type: 'string', - format: 'date-time', - nullable: true, - example: '2023-02-10T08:36:35.262Z', - description: 'When the project was created.', - }, - favorite: { - type: 'boolean', - example: true, - description: - '`true` if the project was favorited, otherwise `false`.', - }, - }, - components: { - schemas: { - environmentSchema, - projectEnvironmentSchema, - createFeatureStrategySchema, - createStrategyVariantSchema, - constraintSchema, - featureSchema, - featureEnvironmentSchema, - overrideSchema, - parametersSchema, - featureStrategySchema, - strategyVariantSchema, - variantSchema, - projectStatsSchema, - createFeatureNamingPatternSchema, - tagSchema, - }, - }, -} as const; - -export type DeprecatedProjectOverviewSchema = FromSchema< - typeof deprecatedProjectOverviewSchema ->; diff --git a/src/lib/openapi/spec/index.ts b/src/lib/openapi/spec/index.ts index be6c1f84f3..f48bcddf5c 100644 --- a/src/lib/openapi/spec/index.ts +++ b/src/lib/openapi/spec/index.ts @@ -63,7 +63,6 @@ export * from './custom-metrics-schema.js'; export * from './date-schema.js'; export * from './dependencies-exist-schema.js'; export * from './dependent-feature-schema.js'; -export * from './deprecated-project-overview-schema.js'; export * from './dora-features-schema.js'; export * from './edge-token-schema.js'; export * from './email-schema.js'; diff --git a/src/test/e2e/api/admin/favorites.e2e.test.ts b/src/test/e2e/api/admin/favorites.e2e.test.ts index 95202c03f9..d400db4e08 100644 --- a/src/test/e2e/api/admin/favorites.e2e.test.ts +++ b/src/test/e2e/api/admin/favorites.e2e.test.ts @@ -71,7 +71,7 @@ const unfavoriteProject = async (projectName = 'default') => { const getProject = async (projectName = 'default') => { return app.request - .get(`/api/admin/projects/${projectName}`) + .get(`/api/admin/projects/${projectName}/overview`) .set('Content-Type', 'application/json') .expect(200); }; @@ -122,28 +122,6 @@ beforeEach(async () => { await loginRegularUser(); }); -test('should be favorited in project endpoint', async () => { - const featureName = 'test-feature'; - await createFeature(featureName); - await favoriteFeature(featureName); - await favoriteProject(); - - const { body } = await app.request - .get('/api/admin/projects/default') - .set('Content-Type', 'application/json') - .expect(200); - - expect(body).toMatchObject({ - favorite: true, - features: [ - { - name: featureName, - favorite: true, - }, - ], - }); -}); - test('feature should not be favorited by default', async () => { const featureName = 'test-feature'; await createFeature(featureName); diff --git a/src/test/e2e/api/admin/project/project.health.e2e.test.ts b/src/test/e2e/api/admin/project/project.health.e2e.test.ts index bad978efab..5e0f6454ef 100644 --- a/src/test/e2e/api/admin/project/project.health.e2e.test.ts +++ b/src/test/e2e/api/admin/project/project.health.e2e.test.ts @@ -65,15 +65,11 @@ test('Project with no stale toggles should have 100% health rating', async () => }) .expect(201); await app.request - .get('/api/admin/projects/fresh') + .get('/api/admin/projects/fresh/health-report') .expect(200) .expect('Content-Type', /json/) .expect((res) => { expect(res.body.health).toBe(100); - expect(res.body.environments).toHaveLength(1); - expect(res.body.environments).toStrictEqual([ - { environment: 'default' }, - ]); }); }); @@ -251,93 +247,6 @@ test('Health report for non-existing project yields 404', async () => { .expect(404); }); -test('Sorts environments by sort order', async () => { - const envOne = 'my-sorted-env1'; - const envTwo = 'my-sorted-env2'; - const featureName = 'My-new-toggle'; - const defaultEnvName = 'default'; - await db.stores.environmentStore.create({ - name: envOne, - type: 'production', - sortOrder: 0, - }); - - await db.stores.environmentStore.create({ - name: envTwo, - type: 'production', - sortOrder: 500, - }); - - await app.request - .post('/api/admin/projects/default/environments') - .send({ - environment: envOne, - }) - .expect(200); - - await app.request - .post('/api/admin/projects/default/environments') - .send({ - environment: envTwo, - }) - .expect(200); - - await app.request - .post('/api/admin/projects/default/features') - .send({ name: featureName }) - .expect(201); - - await app.request.get('/api/admin/projects/default').expect((res) => { - const feature = res.body.features[0]; - expect(feature.environments[0].name).toBe(envOne); - expect(feature.environments[1].name).toBe(defaultEnvName); - expect(feature.environments[2].name).toBe(envTwo); - }); -}); - -test('Sorts environments correctly if sort order is equal', async () => { - const envOne = 'my-sorted-env3'; - const envTwo = 'my-sorted-env4'; - const featureName = 'My-new-toggle-2'; - - await db.stores.environmentStore.create({ - name: envOne, - type: 'production', - sortOrder: -5, - }); - - await db.stores.environmentStore.create({ - name: envTwo, - type: 'production', - sortOrder: -5, - }); - - await app.request - .post('/api/admin/projects/default/environments') - .send({ - environment: envOne, - }) - .expect(200); - - await app.request - .post('/api/admin/projects/default/environments') - .send({ - environment: envTwo, - }) - .expect(200); - - await app.request - .post('/api/admin/projects/default/features') - .send({ name: featureName }) - .expect(201); - - await app.request.get('/api/admin/projects/default').expect((res) => { - const feature = res.body.features[0]; - expect(feature.environments[0].name).toBe(envOne); - expect(feature.environments[1].name).toBe(envTwo); - }); -}); - test('Update update_at when setHealth runs', async () => { await app.services.projectHealthService.setProjectHealthRating('default'); await app.request