1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-20 00:08:02 +01:00

docs: openapi schema specifications for Projects tag (#3571)

<!-- Thanks for creating a PR! To make it easier for reviewers and
everyone else to understand what your changes relate to, please add some
relevant content to the headings below. Feel free to ignore or delete
sections that you don't think are relevant. Thank you! ❤️ -->

## About the changes
<!-- Describe the changes introduced. What are they and why are they
being introduced? Feel free to also add screenshots or steps to view the
changes if they're visual. -->

Improves the openapi schema specifications for the schemas belonging to
the "Projects" tag.
Expected error codes/http statues, descriptions, and example data

---------

Co-authored-by: Christopher Kolstad <chriswk@getunleash.ai>
Co-authored-by: Thomas Heartman <thomas@getunleash.ai>
This commit is contained in:
David Leek 2023-05-19 09:07:23 +02:00 committed by GitHub
parent f9409fc0e6
commit 485dab87d4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 979 additions and 80 deletions

View File

@ -79,8 +79,6 @@ const metaRules: Rule[] = [
},
},
knownExceptions: [
'apiTokenSchema',
'apiTokensSchema',
'batchFeaturesSchema',
'batchStaleSchema',
'changePasswordSchema',
@ -115,8 +113,6 @@ const metaRules: Rule[] = [
'groupsSchema',
'groupUserModelSchema',
'healthCheckSchema',
'healthOverviewSchema',
'healthReportSchema',
'idSchema',
'instanceAdminStatsSchema',
'legalValueSchema',
@ -132,7 +128,6 @@ const metaRules: Rule[] = [
'playgroundFeatureSchema',
'playgroundRequestSchema',
'profileSchema',
'projectEnvironmentSchema',
'projectSchema',
'projectsSchema',
'proxyClientSchema',
@ -187,8 +182,6 @@ const metaRules: Rule[] = [
},
knownExceptions: [
'adminFeaturesQuerySchema',
'apiTokenSchema',
'apiTokensSchema',
'applicationSchema',
'applicationsSchema',
'batchFeaturesSchema',
@ -225,8 +218,6 @@ const metaRules: Rule[] = [
'groupsSchema',
'groupUserModelSchema',
'healthCheckSchema',
'healthOverviewSchema',
'healthReportSchema',
'idSchema',
'instanceAdminStatsSchema',
'legalValueSchema',
@ -244,7 +235,6 @@ const metaRules: Rule[] = [
'playgroundSegmentSchema',
'playgroundStrategySchema',
'profileSchema',
'projectEnvironmentSchema',
'proxyClientSchema',
'proxyFeatureSchema',
'proxyFeaturesSchema',

View File

@ -6,11 +6,11 @@ exports[`apiTokenSchema empty 1`] = `
{
"instancePath": "",
"keyword": "required",
"message": "must have required property 'username'",
"message": "must have required property 'secret'",
"params": {
"missingProperty": "username",
"missingProperty": "secret",
},
"schemaPath": "#/anyOf/0/required",
"schemaPath": "#/required",
},
{
"instancePath": "",
@ -19,14 +19,7 @@ exports[`apiTokenSchema empty 1`] = `
"params": {
"missingProperty": "tokenName",
},
"schemaPath": "#/anyOf/1/required",
},
{
"instancePath": "",
"keyword": "anyOf",
"message": "must match a schema in anyOf",
"params": {},
"schemaPath": "#/anyOf",
"schemaPath": "#/required",
},
{
"instancePath": "",
@ -37,6 +30,33 @@ exports[`apiTokenSchema empty 1`] = `
},
"schemaPath": "#/required",
},
{
"instancePath": "",
"keyword": "required",
"message": "must have required property 'project'",
"params": {
"missingProperty": "project",
},
"schemaPath": "#/required",
},
{
"instancePath": "",
"keyword": "required",
"message": "must have required property 'projects'",
"params": {
"missingProperty": "projects",
},
"schemaPath": "#/required",
},
{
"instancePath": "",
"keyword": "required",
"message": "must have required property 'createdAt'",
"params": {
"missingProperty": "createdAt",
},
"schemaPath": "#/required",
},
],
"schema": "#/components/schemas/apiTokenSchema",
}

View File

@ -5,6 +5,7 @@ import { ApiTokenSchema } from './api-token-schema';
const defaultData: ApiTokenSchema = {
secret: '',
username: '',
tokenName: '',
type: ApiTokenType.CLIENT,
environment: '',
projects: [],

View File

@ -5,16 +5,28 @@ export const apiTokenSchema = {
$id: '#/components/schemas/apiTokenSchema',
type: 'object',
additionalProperties: false,
required: ['type'],
required: [
'secret',
'tokenName',
'type',
'project',
'projects',
'createdAt',
],
description:
'An overview of an [Unleash API token](https://docs.getunleash.io/reference/api-tokens-and-client-keys).',
properties: {
secret: {
type: 'string',
description: 'The token used for authentication.',
example: 'project:environment.xyzrandomstring',
},
username: {
type: 'string',
deprecated: true,
description:
'This property was deprecated in Unleash v5. Prefer the `tokenName` property instead.',
example: 'a-name',
},
tokenName: {
type: 'string',
@ -24,57 +36,57 @@ export const apiTokenSchema = {
type: {
type: 'string',
enum: Object.values(ApiTokenType),
description: 'The type of API token',
example: 'client',
},
environment: {
type: 'string',
description:
'The environment the token has access to. `*` if it has access to all environments.',
example: 'development',
},
project: {
type: 'string',
description: 'The project this token belongs to.',
example: 'developerexperience',
},
projects: {
type: 'array',
description:
'The list of projects this token has access to. If the token has access to specific projects they will be listed here. If the token has access to all projects it will be represented as `[*]`',
items: {
type: 'string',
},
example: ['developerexperience', 'enterprisegrowth'],
},
expiresAt: {
type: 'string',
format: 'date-time',
nullable: true,
description: `The token's expiration date. NULL if the token doesn't have an expiration set.`,
example: '2023-04-19T08:15:14.000Z',
},
createdAt: {
type: 'string',
format: 'date-time',
nullable: true,
example: '2023-04-19T08:15:14.000Z',
description: 'When the token was created.',
},
seenAt: {
type: 'string',
format: 'date-time',
nullable: true,
example: '2023-04-19T08:15:14.000Z',
description:
'When the token was last seen/used to authenticate with. NULL if the token has not yet been used for authentication.',
},
alias: {
type: 'string',
nullable: true,
description: `Alias is no longer in active use and will often be NULL. It's kept around as a way of allowing old proxy tokens created with the old metadata format to keep working.`,
example: 'randomid-or-some-alias',
},
},
anyOf: [
{
properties: {
username: {
type: 'string',
},
},
required: ['username'],
},
{
properties: {
tokenName: {
type: 'string',
},
},
required: ['tokenName'],
},
],
components: {},
} as const;

View File

@ -6,12 +6,14 @@ export const apiTokensSchema = {
type: 'object',
additionalProperties: false,
required: ['tokens'],
description: 'Contains a list of API tokens.',
properties: {
tokens: {
type: 'array',
items: {
$ref: '#/components/schemas/apiTokenSchema',
},
description: 'A list of API tokens.',
},
},
components: {

View File

@ -15,17 +15,33 @@ export const healthOverviewSchema = {
$id: '#/components/schemas/healthOverviewSchema',
type: 'object',
additionalProperties: false,
required: ['version', 'name'],
required: [
'version',
'name',
'defaultStickiness',
'mode',
'members',
'health',
'environments',
'features',
],
description: `An overview of a project's stats and its health as described in the documentation on [technical debt](https://docs.getunleash.io/reference/technical-debt)`,
properties: {
version: {
type: 'number',
type: 'integer',
description: 'The project overview version.',
example: 1,
},
name: {
type: 'string',
description: `The project's name`,
example: 'enterprisegrowth',
},
description: {
type: 'string',
nullable: true,
description: `The project's description`,
example: 'The project for all things enterprisegrowth',
},
defaultStickiness: {
type: 'string',
@ -41,30 +57,45 @@ export const healthOverviewSchema = {
"The project's [collaboration mode](https://docs.getunleash.io/reference/project-collaboration-mode). Determines whether non-project members can submit change requests or not.",
},
members: {
type: 'number',
type: 'integer',
description: 'The number of users/members in the project.',
example: 5,
minimum: 0,
},
health: {
type: 'number',
type: 'integer',
description:
'The overall [health rating](https://docs.getunleash.io/reference/technical-debt#health-rating) of the project.',
example: 95,
},
environments: {
type: 'array',
items: {
$ref: '#/components/schemas/projectEnvironmentSchema',
},
description:
'An array containing the names of all the environments configured for the project.',
},
features: {
type: 'array',
items: {
$ref: '#/components/schemas/featureSchema',
},
description:
'An array containing an overview of all the features of the project and their individual status',
},
updatedAt: {
type: 'string',
format: 'date-time',
nullable: true,
description: 'When the project was last updated.',
example: '2023-04-19T08:15:14.000Z',
},
favorite: {
type: 'boolean',
description:
'Indicates if the project has been marked as a favorite by the current user requesting the project health overview.',
example: true,
},
stats: {
$ref: '#/components/schemas/projectStatsSchema',

View File

@ -4,6 +4,8 @@ import { healthOverviewSchema } from './health-overview-schema';
export const healthReportSchema = {
...healthOverviewSchema,
$id: '#/components/schemas/healthReportSchema',
description:
'A report of the current health of the requested project, with datapoints like counters of currently active, stale, and potentially stale feature toggles.',
required: [
...healthOverviewSchema.required,
'potentiallyStaleCount',
@ -14,12 +16,18 @@ export const healthReportSchema = {
...healthOverviewSchema.properties,
potentiallyStaleCount: {
type: 'number',
description: 'The number of potentially stale feature toggles.',
example: 5,
},
activeCount: {
type: 'number',
description: 'The number of active feature toggles.',
example: 2,
},
staleCount: {
type: 'number',
description: 'The number of stale feature toggles.',
example: 10,
},
},
} as const;

View File

@ -5,16 +5,25 @@ export const projectEnvironmentSchema = {
$id: '#/components/schemas/projectEnvironmentSchema',
type: 'object',
additionalProperties: false,
description:
'Add an environment to a project, optionally also sets if change requests are enabled for this environment on the project',
required: ['environment'],
properties: {
environment: {
type: 'string',
description: 'The environment to add to the project',
example: 'development',
},
changeRequestsEnabled: {
type: 'boolean',
description:
'Whether change requests should be enabled or for this environment on the project or not',
example: true,
},
defaultStrategy: {
$ref: '#/components/schemas/createFeatureStrategySchema',
description:
'A default strategy to create for this environment on the project.',
},
},
components: {

View File

@ -8,6 +8,7 @@ import {
emptyResponse,
resourceCreatedResponseSchema,
} from '../../../openapi';
import { getStandardResponses } from '../../../openapi/util/standard-responses';
import User from '../../../types/user';
import {
ADMIN,
@ -82,8 +83,12 @@ export class ProjectApiTokenController extends Controller {
openApiService.validPath({
tags: ['Projects'],
operationId: 'getProjectApiTokens',
summary: 'Get api tokens for project.',
description:
'Returns the [project API tokens](https://docs.getunleash.io/how-to/how-to-create-project-api-tokens) that have been created for this project.',
responses: {
200: createResponseSchema('apiTokensSchema'),
...getStandardResponses(401, 403, 404),
},
}),
],
@ -99,9 +104,12 @@ export class ProjectApiTokenController extends Controller {
tags: ['Projects'],
operationId: 'createProjectApiToken',
requestBody: createRequestSchema('createApiTokenSchema'),
summary: 'Create a project API token.',
description:
'Endpoint that allows creation of [project API tokens](https://docs.getunleash.io/reference/api-tokens-and-client-keys#api-token-visibility) for the specified project.',
responses: {
201: resourceCreatedResponseSchema('apiTokenSchema'),
400: emptyResponse,
...getStandardResponses(400, 401, 403),
},
}),
],
@ -117,8 +125,11 @@ export class ProjectApiTokenController extends Controller {
openApiService.validPath({
tags: ['Projects'],
operationId: 'deleteProjectApiToken',
summary: 'Delete a project API token.',
description: `This operation deletes the API token specified in the request URL. If the token doesn't exist, returns an OK response (status code 200).`,
responses: {
200: emptyResponse,
...getStandardResponses(401, 403),
},
}),
],

View File

@ -55,10 +55,16 @@ export default class EnvironmentsController extends Controller {
openApiService.validPath({
tags: ['Projects'],
operationId: 'addEnvironmentToProject',
summary: 'Add an environment to a project.',
description:
'This endpoint adds the provided environment to the specified project, with optional support for enabling and disabling change requests for the environment and project.',
requestBody: createRequestSchema(
'projectEnvironmentSchema',
),
responses: { 200: emptyResponse },
responses: {
200: emptyResponse,
...getStandardResponses(401, 403, 409),
},
}),
],
});
@ -73,7 +79,13 @@ export default class EnvironmentsController extends Controller {
openApiService.validPath({
tags: ['Projects'],
operationId: 'removeEnvironmentFromProject',
responses: { 200: emptyResponse },
summary: 'Remove an environment from a project.',
description:
'This endpoint removes the specified environment from the project.',
responses: {
200: emptyResponse,
...getStandardResponses(400, 401, 403),
},
}),
],
});

View File

@ -8,6 +8,7 @@ import { IProjectParam } from '../../../types/model';
import { NONE } from '../../../types/permissions';
import { OpenApiService } from '../../../services/openapi-service';
import { createResponseSchema } from '../../../openapi/util/create-response-schema';
import { getStandardResponses } from '../../../openapi/util/standard-responses';
import { serializeDates } from '../../../types/serialize-dates';
import {
healthReportSchema,
@ -42,8 +43,12 @@ export default class ProjectHealthReport extends Controller {
openApiService.validPath({
tags: ['Projects'],
operationId: 'getProjectHealthReport',
summary: 'Get a health report for a project.',
description:
'This endpoint returns a health report for the specified project. This data is used for [the technical debt dashboard](https://docs.getunleash.io/reference/technical-debt#the-technical-debt-dashboard)',
responses: {
200: createResponseSchema('healthReportSchema'),
...getStandardResponses(401, 403, 404),
},
}),
],

View File

@ -20,6 +20,7 @@ import {
projectsSchema,
ProjectsSchema,
} from '../../../openapi';
import { getStandardResponses } from '../../../openapi/util/standard-responses';
import { OpenApiService, SettingService } from '../../../services';
import { IAuthRequest } from '../../unleash-types';
import { ProjectApiTokenController } from './api-token';
@ -49,8 +50,12 @@ export default class ProjectApi extends Controller {
services.openApiService.validPath({
tags: ['Projects'],
operationId: 'getProjects',
summary: 'Get a list of all projects.',
description:
'This endpoint returns an list of all the projects in the Unleash instance.',
responses: {
200: createResponseSchema('projectsSchema'),
...getStandardResponses(401, 403),
},
}),
],
@ -65,8 +70,12 @@ export default class ProjectApi extends Controller {
services.openApiService.validPath({
tags: ['Projects'],
operationId: 'getProjectOverview',
summary: 'Get an overview of a project.',
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('projectOverviewSchema'),
...getStandardResponses(401, 403, 404),
},
}),
],

View File

@ -16,7 +16,7 @@ export interface ILegacyApiTokenCreate {
*/
username?: string;
type: ApiTokenType;
environment: string;
environment?: string;
project?: string;
projects?: string[];
expiresAt?: Date;
@ -42,7 +42,7 @@ export interface IApiToken extends Omit<IApiTokenCreate, 'alias'> {
seenAt?: Date;
environment: string;
project: string;
alias: string | null;
alias?: string | null;
}
export const isAllProjects = (projects: string[]): boolean => {

View File

@ -0,0 +1,115 @@
import dbInit, { ITestDb } from '../../../helpers/database-init';
import {
IUnleashTest,
setupAppWithCustomConfig,
} from '../../../helpers/test-helper';
import getLogger from '../../../../fixtures/no-logger';
import { ApiTokenType } from '../../../../../lib/types/models/api-token';
let app: IUnleashTest;
let db: ITestDb;
beforeAll(async () => {
db = await dbInit('project_api_tokens_serial', getLogger);
app = await setupAppWithCustomConfig(db.stores, {
experimental: {
flags: {
strictSchemaValidation: true,
},
},
});
});
afterEach(async () => {
await db.stores.apiTokenStore.deleteAll();
});
afterAll(async () => {
await app.destroy();
await db.destroy();
});
test('Returns empty list of tokens', async () => {
return app.request
.get('/api/admin/projects/default/api-tokens')
.expect('Content-Type', /json/)
.expect(200)
.expect((res) => {
expect(res.body.tokens.length).toBe(0);
});
});
test('Returns list of tokens', async () => {
const tokenSecret = 'random-secret';
await db.stores.apiTokenStore.insert({
tokenName: 'test',
secret: tokenSecret,
type: ApiTokenType.CLIENT,
environment: 'default',
projects: ['default'],
});
return app.request
.get('/api/admin/projects/default/api-tokens')
.expect('Content-Type', /json/)
.expect(200)
.expect((res) => {
expect(res.body.tokens.length).toBe(1);
expect(res.body.tokens[0].secret).toBe(tokenSecret);
});
});
test('Returns 404 when given non-existant projectId', async () => {
return app.request
.get('/api/admin/projects/wrong/api-tokens')
.expect('Content-Type', /json/)
.expect(200)
.expect((res) => {
expect(res.body.tokens.length).toBe(0);
});
});
test('fails to create new client token when given wrong project', async () => {
return app.request
.post('/api/admin/projects/wrong/api-tokens')
.send({
username: 'default-client',
type: 'client',
projects: ['wrong'],
environment: 'default',
})
.set('Content-Type', 'application/json')
.expect(400);
});
test('creates new client token', async () => {
return app.request
.post('/api/admin/projects/default/api-tokens')
.send({
username: 'default-client',
type: 'client',
projects: ['default'],
environment: 'default',
})
.set('Content-Type', 'application/json')
.expect(201)
.expect((res) => {
expect(res.body.username).toBe('default-client');
});
});
test('Deletes existing tokens', async () => {
const tokenSecret = 'random-secret';
await db.stores.apiTokenStore.insert({
tokenName: 'test',
secret: tokenSecret,
type: ApiTokenType.CLIENT,
environment: 'default',
projects: ['default'],
});
return app.request
.delete(`/api/admin/projects/default/api-tokens/${tokenSecret}`)
.set('Content-Type', 'application/json')
.expect(200);
});