diff --git a/src/lib/db/addon-store.ts b/src/lib/db/addon-store.ts index 6efe130e72..e7513a8599 100644 --- a/src/lib/db/addon-store.ts +++ b/src/lib/db/addon-store.ts @@ -16,6 +16,7 @@ const COLUMNS = [ 'events', 'projects', 'environments', + 'created_at', ]; const TABLE = 'addons'; @@ -71,18 +72,19 @@ export default class AddonStore implements IAddonStore { stopTimer(); // eslint-disable-next-line @typescript-eslint/naming-convention const { id, created_at } = rows[0]; - return { id, createdAt: created_at, ...addon }; + return this.rowToAddon({ id, createdAt: created_at, ...addon }); } async update(id: number, addon: IAddonDto): Promise { const rows = await this.db(TABLE) .where({ id }) - .update(this.addonToRow(addon)); + .update(this.addonToRow(addon)) + .returning(COLUMNS); if (!rows) { throw new NotFoundError('Could not find addon'); } - return rows[0]; + return this.rowToAddon(rows[0]); } async delete(id: number): Promise { @@ -114,7 +116,7 @@ export default class AddonStore implements IAddonStore { id: row.id, provider: row.provider, enabled: row.enabled, - description: row.description, + description: row.description ?? null, parameters: row.parameters, events: row.events, projects: row.projects || [], diff --git a/src/lib/openapi/index.ts b/src/lib/openapi/index.ts index 5c92b339e7..7a627ec304 100644 --- a/src/lib/openapi/index.ts +++ b/src/lib/openapi/index.ts @@ -3,6 +3,7 @@ import { adminFeaturesQuerySchema, addonParameterSchema, addonSchema, + addonCreateUpdateSchema, addonsSchema, addonTypeSchema, apiTokenSchema, @@ -181,6 +182,7 @@ export const schemas: UnleashSchemas = { adminFeaturesQuerySchema, addonParameterSchema, addonSchema, + addonCreateUpdateSchema, addonsSchema, addonTypeSchema, apiTokenSchema, diff --git a/src/lib/openapi/meta-schema-rules.test.ts b/src/lib/openapi/meta-schema-rules.test.ts index 74e5a1e854..0c3a2df797 100644 --- a/src/lib/openapi/meta-schema-rules.test.ts +++ b/src/lib/openapi/meta-schema-rules.test.ts @@ -79,10 +79,6 @@ const metaRules: Rule[] = [ }, }, knownExceptions: [ - 'addonParameterSchema', - 'addonSchema', - 'addonsSchema', - 'addonTypeSchema', 'apiTokenSchema', 'apiTokensSchema', 'applicationSchema', @@ -202,10 +198,6 @@ const metaRules: Rule[] = [ }, knownExceptions: [ 'adminFeaturesQuerySchema', - 'addonParameterSchema', - 'addonSchema', - 'addonsSchema', - 'addonTypeSchema', 'apiTokenSchema', 'apiTokensSchema', 'applicationSchema', diff --git a/src/lib/openapi/spec/addon-create-update-schema.ts b/src/lib/openapi/spec/addon-create-update-schema.ts new file mode 100644 index 0000000000..fb6399e0b6 --- /dev/null +++ b/src/lib/openapi/spec/addon-create-update-schema.ts @@ -0,0 +1,79 @@ +import { FromSchema } from 'json-schema-to-ts'; + +export const addonCreateUpdateSchema = { + $id: '#/components/schemas/addonCreateUpdateSchema', + type: 'object', + required: ['provider', 'enabled', 'parameters', 'events'], + description: + 'Data required to create or update an [Unleash addon](https://docs.getunleash.io/reference/addons) instance.', + properties: { + provider: { + type: 'string', + + description: `The addon provider, such as "webhook" or "slack". This string is **case sensitive** and maps to the provider's \`name\` property. + +The list of all supported providers and their parameters for a specific Unleash instance can be found by making a GET request to the \`api/admin/addons\` endpoint: the \`providers\` property of that response will contain all available providers. + +The default set of providers can be found in the [addons reference documentation](https://docs.getunleash.io/reference/addons). The default supported options are: +- \`datadog\` for [Datadog](https://docs.getunleash.io/reference/addons/datadog) +- \`slack\` for [Slack](https://docs.getunleash.io/reference/addons/slack) +- \`teams\` for [Microsoft Teams](https://docs.getunleash.io/reference/addons/teams) +- \`webhook\` for [webhooks](https://docs.getunleash.io/reference/addons/webhook) + +The provider you choose for your addon dictates what properties the \`parameters\` object needs. Refer to the documentation for each provider for more information. +`, + example: 'webhook', + }, + description: { + type: 'string', + description: 'A description of the addon.', + example: + 'This addon posts updates to our internal feature tracking system whenever a feature is created or updated.', + }, + enabled: { + type: 'boolean', + description: 'Whether the addon should be enabled or not.', + }, + parameters: { + type: 'object', + additionalProperties: {}, + example: { + url: 'http://localhost:4242/webhook', + }, + description: + 'Parameters for the addon provider. This object has different required and optional properties depending on the provider you choose. Consult the documentation for details.', + }, + events: { + type: 'array', + description: + 'The event types that will trigger this specific addon.', + items: { + type: 'string', + }, + example: ['feature-created', 'feature-updated'], + }, + projects: { + type: 'array', + description: + 'The projects that this addon will listen to events from. An empty list means it will listen to events from **all** projects.', + example: ['new-landing-project', 'signups-v2'], + items: { + type: 'string', + }, + }, + environments: { + type: 'array', + description: + 'The list of environments that this addon will listen to events from. An empty list means it will listen to events from **all** environments.', + example: ['development', 'production'], + items: { + type: 'string', + }, + }, + }, + components: {}, +} as const; + +export type AddonCreateUpdateSchema = FromSchema< + typeof addonCreateUpdateSchema +>; diff --git a/src/lib/openapi/spec/addon-parameter-schema.ts b/src/lib/openapi/spec/addon-parameter-schema.ts index 640da55e67..ca3efdffb4 100644 --- a/src/lib/openapi/spec/addon-parameter-schema.ts +++ b/src/lib/openapi/spec/addon-parameter-schema.ts @@ -3,28 +3,51 @@ import { FromSchema } from 'json-schema-to-ts'; export const addonParameterSchema = { $id: '#/components/schemas/addonParameterSchema', type: 'object', + additionalProperties: false, required: ['name', 'displayName', 'type', 'required', 'sensitive'], + description: 'An addon parameter definition.', properties: { name: { type: 'string', + example: 'emojiIcon', + description: + 'The name of the parameter as it is used in code. References to this parameter should use this value.', }, displayName: { type: 'string', + example: 'Emoji Icon', + description: + 'The name of the parameter as it is shown to the end user in the Admin UI.', }, type: { type: 'string', + description: + 'The type of the parameter. Corresponds roughly to [HTML `input` field types](https://developer.mozilla.org/docs/Web/HTML/Element/Input#input_types). Multi-line inut fields are indicated as `textfield` (equivalent to the HTML `textarea` tag).', + example: 'text', }, description: { type: 'string', + example: + 'The emoji_icon to use when posting messages to slack. Defaults to ":unleash:".', + description: + 'A description of the parameter. This should explain to the end user what the parameter is used for.', }, placeholder: { type: 'string', + example: ':unleash:', + description: + 'The default value for this parameter. This value is used if no other value is provided.', }, required: { type: 'boolean', + example: false, + description: + 'Whether this parameter is required or not. If a parameter is required, you must give it a value when you create the addon. If it is not required it can be left out. It may receive a default value in those cases.', }, sensitive: { type: 'boolean', + example: false, + description: `Indicates whether this parameter is **sensitive** or not. Unleash will not return sensitive parameters to API requests. It will instead use a number of asterisks to indicate that a value is set, e.g. "******". The number of asterisks does not correlate to the parameter\'s value.`, }, }, components: {}, diff --git a/src/lib/openapi/spec/addon-schema.test.ts b/src/lib/openapi/spec/addon-schema.test.ts index f75b9798b8..772d76c027 100644 --- a/src/lib/openapi/spec/addon-schema.test.ts +++ b/src/lib/openapi/spec/addon-schema.test.ts @@ -5,6 +5,8 @@ test('addonSchema', () => { const data: AddonSchema = { provider: 'some-provider', enabled: true, + description: null, + id: 5, parameters: { someKey: 'some-value', }, diff --git a/src/lib/openapi/spec/addon-schema.ts b/src/lib/openapi/spec/addon-schema.ts index b7c92321b9..a7f585e59d 100644 --- a/src/lib/openapi/spec/addon-schema.ts +++ b/src/lib/openapi/spec/addon-schema.ts @@ -3,43 +3,70 @@ import { FromSchema } from 'json-schema-to-ts'; export const addonSchema = { $id: '#/components/schemas/addonSchema', type: 'object', - required: ['provider', 'enabled', 'parameters', 'events'], + description: `An [addon](https://docs.getunleash.io/reference/addons) instance description. Contains data about what kind of provider it uses, whether it's enabled or not, what events it listens for, and more.`, + required: [ + 'id', + 'description', + 'provider', + 'enabled', + 'parameters', + 'events', + ], properties: { id: { - type: 'number', - }, - createdAt: { - type: 'string', - format: 'date-time', - nullable: true, + type: 'integer', + minimum: 1, + example: 27, + description: "The addon's unique identifier.", }, provider: { type: 'string', + description: `The addon provider, such as "webhook" or "slack".`, + example: 'webhook', }, description: { type: 'string', + description: + 'A description of the addon. `null` if no description exists.', + example: + 'This addon posts updates to our internal feature tracking system whenever a feature is created or updated.', + nullable: true, }, enabled: { type: 'boolean', + description: 'Whether the addon is enabled or not.', }, parameters: { type: 'object', - additionalProperties: true, + additionalProperties: {}, + example: { + url: 'http://localhost:4242/webhook', + }, + description: + 'Parameters for the addon provider. This object has different required and optional properties depending on the provider you choose.', }, events: { type: 'array', + description: 'The event types that trigger this specific addon.', items: { type: 'string', }, + example: ['feature-created', 'feature-updated'], }, projects: { type: 'array', + description: + 'The projects that this addon listens to events from. An empty list means it listens to events from **all** projects.', + example: ['new-landing-project', 'signups-v2'], items: { type: 'string', }, }, environments: { type: 'array', + description: + 'The list of environments that this addon listens to events from. An empty list means it listens to events from **all** environments.', + example: ['development', 'production'], items: { type: 'string', }, diff --git a/src/lib/openapi/spec/addon-type-schema.ts b/src/lib/openapi/spec/addon-type-schema.ts index e25b820cb2..5605f5b1ff 100644 --- a/src/lib/openapi/spec/addon-type-schema.ts +++ b/src/lib/openapi/spec/addon-type-schema.ts @@ -5,37 +5,119 @@ import { tagTypeSchema } from './tag-type-schema'; export const addonTypeSchema = { $id: '#/components/schemas/addonTypeSchema', type: 'object', + additionalProperties: false, required: ['name', 'displayName', 'documentationUrl', 'description'], + description: + 'An addon provider. Defines a specific addon type and what the end user must configure when creating a new addon of that type.', properties: { name: { type: 'string', + description: + "The name of the addon type. When creating new addons, this goes in the payload's `type` field.", + example: 'slack', }, displayName: { type: 'string', + description: + "The addon type's name as it should be displayed in the admin UI.", + example: 'Slack', }, documentationUrl: { type: 'string', + description: + 'A URL to where you can find more information about using this addon type.', + example: 'https://docs.getunleash.io/docs/addons/slack', }, description: { type: 'string', + description: 'A description of the addon type.', + example: 'Allows Unleash to post updates to Slack.', }, tagTypes: { type: 'array', + description: `A list of [Unleash tag types](https://docs.getunleash.io/reference/tags#tag-types) that this addon uses. These tags will be added to the Unleash instance when an addon of this type is created.`, + example: [ + { + name: 'slack', + description: + 'Slack tag used by the slack-addon to specify the slack channel.', + icon: 'S', + }, + ], items: { $ref: '#/components/schemas/tagTypeSchema', }, }, parameters: { type: 'array', + description: + "The addon provider's parameters. Use these to configure an addon of this provider type. Items with `required: true` must be provided.", items: { $ref: '#/components/schemas/addonParameterSchema', }, + example: [ + { + name: 'url', + displayName: 'Slack webhook URL', + description: '(Required)', + type: 'url', + required: true, + sensitive: true, + }, + { + name: 'username', + displayName: 'Username', + placeholder: 'Unleash', + description: + 'The username to use when posting messages to slack. Defaults to "Unleash".', + type: 'text', + required: false, + sensitive: false, + }, + { + name: 'emojiIcon', + displayName: 'Emoji Icon', + placeholder: ':unleash:', + description: + 'The emoji_icon to use when posting messages to slack. Defaults to ":unleash:".', + type: 'text', + required: false, + sensitive: false, + }, + { + name: 'defaultChannel', + displayName: 'Default channel', + description: + '(Required) Default channel to post updates to if not specified in the slack-tag', + type: 'text', + required: true, + sensitive: false, + }, + ], }, events: { type: 'array', + description: + 'All the [event types](https://docs.getunleash.io/reference/api/legacy/unleash/admin/events#feature-toggle-events) that are available for this addon provider.', items: { type: 'string', }, + example: [ + 'feature-created', + 'feature-updated', + 'feature-archived', + 'feature-revived', + 'feature-stale-on', + 'feature-stale-off', + 'feature-environment-enabled', + 'feature-environment-disabled', + 'feature-strategy-remove', + 'feature-strategy-update', + 'feature-strategy-add', + 'feature-metadata-updated', + 'feature-variants-updated', + 'feature-project-change', + ], }, }, components: { diff --git a/src/lib/openapi/spec/addons-schema.test.ts b/src/lib/openapi/spec/addons-schema.test.ts index 0369cf7980..e763c44982 100644 --- a/src/lib/openapi/spec/addons-schema.test.ts +++ b/src/lib/openapi/spec/addons-schema.test.ts @@ -9,6 +9,8 @@ test('addonsSchema', () => { events: ['some-event'], enabled: true, provider: 'some-name', + description: null, + id: 5, }, ], providers: [ diff --git a/src/lib/openapi/spec/addons-schema.ts b/src/lib/openapi/spec/addons-schema.ts index a59757f5b2..763c23c133 100644 --- a/src/lib/openapi/spec/addons-schema.ts +++ b/src/lib/openapi/spec/addons-schema.ts @@ -8,15 +8,256 @@ export const addonsSchema = { $id: '#/components/schemas/addonsSchema', type: 'object', required: ['addons', 'providers'], + description: `An object containing two things: +1. A list of all [addons](https://docs.getunleash.io/reference/addons) defined on this Unleash instance +2. A list of all addon providers defined on this instance`, properties: { addons: { type: 'array', + description: + 'All the addons that exist on this instance of Unleash.', items: { $ref: '#/components/schemas/addonSchema', }, }, providers: { type: 'array', + description: + 'A list of all available addon providers, along with their parameters and descriptions.', + example: [ + { + name: 'webhook', + displayName: 'Webhook', + description: + 'A Webhook is a generic way to post messages from Unleash to third party services.', + documentationUrl: + 'https://docs.getunleash.io/docs/addons/webhook', + parameters: [ + { + name: 'url', + displayName: 'Webhook URL', + description: + '(Required) Unleash will perform a HTTP Post to the specified URL (one retry if first attempt fails)', + type: 'url', + required: true, + sensitive: true, + }, + { + name: 'contentType', + displayName: 'Content-Type', + placeholder: 'application/json', + description: + '(Optional) The Content-Type header to use. Defaults to "application/json".', + type: 'text', + required: false, + sensitive: false, + }, + { + name: 'authorization', + displayName: 'Authorization', + placeholder: '', + description: + '(Optional) The Authorization header to use. Not used if left blank.', + type: 'text', + required: false, + sensitive: true, + }, + { + name: 'bodyTemplate', + displayName: 'Body template', + placeholder: + '{\n "event": "{{event.type}}",\n "createdBy": "{{event.createdBy}}",\n "featureToggle": "{{event.data.name}}",\n "timestamp": "{{event.data.createdAt}}"\n}', + description: + "(Optional) You may format the body using a mustache template. If you don't specify anything, the format will similar to the events format (https://docs.getunleash.io/reference/api/legacy/unleash/admin/events)", + type: 'textfield', + required: false, + sensitive: false, + }, + ], + events: [ + 'feature-created', + 'feature-updated', + 'feature-archived', + 'feature-revived', + 'feature-stale-on', + 'feature-stale-off', + 'feature-environment-enabled', + 'feature-environment-disabled', + 'feature-strategy-remove', + 'feature-strategy-update', + 'feature-strategy-add', + 'feature-metadata-updated', + 'feature-variants-updated', + 'feature-project-change', + 'feature-tagged', + 'feature-untagged', + 'change-request-created', + 'change-request-discarded', + 'change-added', + 'change-discarded', + 'change-request-approved', + 'change-request-approval-added', + 'change-request-cancelled', + 'change-request-sent-to-review', + 'change-request-applied', + ], + }, + { + name: 'slack', + displayName: 'Slack', + description: 'Allows Unleash to post updates to Slack.', + documentationUrl: + 'https://docs.getunleash.io/docs/addons/slack', + parameters: [ + { + name: 'url', + displayName: 'Slack webhook URL', + description: '(Required)', + type: 'url', + required: true, + sensitive: true, + }, + { + name: 'username', + displayName: 'Username', + placeholder: 'Unleash', + description: + 'The username to use when posting messages to slack. Defaults to "Unleash".', + type: 'text', + required: false, + sensitive: false, + }, + { + name: 'emojiIcon', + displayName: 'Emoji Icon', + placeholder: ':unleash:', + description: + 'The emoji_icon to use when posting messages to slack. Defaults to ":unleash:".', + type: 'text', + required: false, + sensitive: false, + }, + { + name: 'defaultChannel', + displayName: 'Default channel', + description: + '(Required) Default channel to post updates to if not specified in the slack-tag', + type: 'text', + required: true, + sensitive: false, + }, + ], + events: [ + 'feature-created', + 'feature-updated', + 'feature-archived', + 'feature-revived', + 'feature-stale-on', + 'feature-stale-off', + 'feature-environment-enabled', + 'feature-environment-disabled', + 'feature-strategy-remove', + 'feature-strategy-update', + 'feature-strategy-add', + 'feature-metadata-updated', + 'feature-variants-updated', + 'feature-project-change', + ], + tagTypes: [ + { + name: 'slack', + description: + 'Slack tag used by the slack-addon to specify the slack channel.', + icon: 'S', + }, + ], + }, + { + name: 'teams', + displayName: 'Microsoft Teams', + description: + 'Allows Unleash to post updates to Microsoft Teams.', + documentationUrl: + 'https://docs.getunleash.io/docs/addons/teams', + parameters: [ + { + name: 'url', + displayName: 'Microsoft Teams webhook URL', + description: '(Required)', + type: 'url', + required: true, + sensitive: true, + }, + ], + events: [ + 'feature-created', + 'feature-updated', + 'feature-archived', + 'feature-revived', + 'feature-stale-on', + 'feature-stale-off', + 'feature-environment-enabled', + 'feature-environment-disabled', + 'feature-strategy-remove', + 'feature-strategy-update', + 'feature-strategy-add', + 'feature-metadata-updated', + 'feature-variants-updated', + 'feature-project-change', + ], + }, + { + name: 'datadog', + displayName: 'Datadog', + description: 'Allows Unleash to post updates to Datadog.', + documentationUrl: + 'https://docs.getunleash.io/docs/addons/datadog', + parameters: [ + { + name: 'url', + displayName: 'Datadog Events URL', + description: + 'Default url: https://api.datadoghq.com/api/v1/events. Needs to be changed if your not using the US1 site.', + type: 'url', + required: false, + sensitive: false, + }, + { + name: 'apiKey', + displayName: 'Datadog API key', + placeholder: 'j96c23b0f12a6b3434a8d710110bd862', + description: '(Required) API key from Datadog', + type: 'text', + required: true, + sensitive: true, + }, + ], + events: [ + 'feature-created', + 'feature-updated', + 'feature-archived', + 'feature-revived', + 'feature-stale-on', + 'feature-stale-off', + 'feature-environment-enabled', + 'feature-environment-disabled', + 'feature-strategy-remove', + 'feature-strategy-update', + 'feature-strategy-add', + 'feature-metadata-updated', + 'feature-project-change', + 'feature-variants-updated', + ], + tagTypes: [ + { + name: 'datadog', + description: + 'All Datadog tags added to a specific feature are sent to datadog event stream.', + icon: 'D', + }, + ], + }, + ], items: { $ref: '#/components/schemas/addonTypeSchema', }, diff --git a/src/lib/openapi/spec/index.ts b/src/lib/openapi/spec/index.ts index d9406044ab..0ae8539aa3 100644 --- a/src/lib/openapi/spec/index.ts +++ b/src/lib/openapi/spec/index.ts @@ -9,6 +9,7 @@ export * from './role-schema'; export * from './tags-schema'; export * from './user-schema'; export * from './addon-schema'; +export * from './addon-create-update-schema'; export * from './email-schema'; export * from './event-schema'; export * from './group-schema'; diff --git a/src/lib/routes/admin-api/addon.ts b/src/lib/routes/admin-api/addon.ts index ea118b4d4b..153d35ba95 100644 --- a/src/lib/routes/admin-api/addon.ts +++ b/src/lib/routes/admin-api/addon.ts @@ -18,7 +18,11 @@ import { OpenApiService } from '../../services/openapi-service'; import { AddonSchema, addonSchema } from '../../openapi/spec/addon-schema'; import { serializeDates } from '../../types/serialize-dates'; import { AddonsSchema, addonsSchema } from '../../openapi/spec/addons-schema'; -import { emptyResponse } from '../../openapi/util/standard-responses'; +import { + emptyResponse, + getStandardResponses, +} from '../../openapi/util/standard-responses'; +import { AddonCreateUpdateSchema } from 'lib/openapi/spec/addon-create-update-schema'; type AddonServices = Pick; @@ -47,9 +51,13 @@ class AddonController extends Controller { handler: this.getAddons, middleware: [ openApiService.validPath({ + summary: 'Get all addons and providers', + description: + 'Retrieve all addons and providers that are defined on this Unleash instance.', tags: ['Addons'], operationId: 'getAddons', responses: { + ...getStandardResponses(401), 200: createResponseSchema('addonsSchema'), }, }), @@ -63,10 +71,16 @@ class AddonController extends Controller { permission: CREATE_ADDON, middleware: [ openApiService.validPath({ + summary: 'Create a new addon', + description: + 'Create an addon instance. The addon must use one of the providers available on this Unleash instance.', tags: ['Addons'], operationId: 'createAddon', - requestBody: createRequestSchema('addonSchema'), - responses: { 200: createResponseSchema('addonSchema') }, + requestBody: createRequestSchema('addonCreateUpdateSchema'), + responses: { + 200: createResponseSchema('addonSchema'), + ...getStandardResponses(400, 401, 403, 413, 415), + }, }), ], }); @@ -78,9 +92,15 @@ class AddonController extends Controller { permission: NONE, middleware: [ openApiService.validPath({ + summary: 'Get a specific addon', + description: + 'Retrieve information about the addon whose ID matches the ID in the request URL.', tags: ['Addons'], operationId: 'getAddon', - responses: { 200: createResponseSchema('addonSchema') }, + responses: { + 200: createResponseSchema('addonSchema'), + ...getStandardResponses(401), + }, }), ], }); @@ -92,10 +112,17 @@ class AddonController extends Controller { permission: UPDATE_ADDON, middleware: [ openApiService.validPath({ + summary: 'Update an addon', + description: `Update the addon with a specific ID. Any fields in the update object will be updated. Properties that are not included in the update object will not be affected. To empty a property, pass \`null\` as that property's value. + +Note: passing \`null\` as a value for the description property will set it to an empty string.`, tags: ['Addons'], operationId: 'updateAddon', - requestBody: createRequestSchema('addonSchema'), - responses: { 200: createResponseSchema('addonSchema') }, + requestBody: createRequestSchema('addonCreateUpdateSchema'), + responses: { + 200: createResponseSchema('addonSchema'), + ...getStandardResponses(400, 401, 403, 413, 415), + }, }), ], }); @@ -108,9 +135,15 @@ class AddonController extends Controller { permission: DELETE_ADDON, middleware: [ openApiService.validPath({ + summary: 'Delete an addon', + description: + 'Delete the addon specified by the ID in the request path.', tags: ['Addons'], operationId: 'deleteAddon', - responses: { 200: emptyResponse }, + responses: { + 200: emptyResponse, + ...getStandardResponses(401, 403), + }, }), ], }); @@ -141,7 +174,7 @@ class AddonController extends Controller { } async updateAddon( - req: IAuthRequest<{ id: number }, any, any, any>, + req: IAuthRequest<{ id: number }, any, AddonCreateUpdateSchema, any>, res: Response, ): Promise { const { id } = req.params; @@ -159,7 +192,7 @@ class AddonController extends Controller { } async createAddon( - req: IAuthRequest, + req: IAuthRequest, res: Response, ): Promise { const createdBy = extractUsername(req); diff --git a/src/lib/routes/util.ts b/src/lib/routes/util.ts index 25ca60611a..eb043c8f69 100644 --- a/src/lib/routes/util.ts +++ b/src/lib/routes/util.ts @@ -70,6 +70,8 @@ export const handleErrors: ( return res.status(400).json(error).end(); case 'ProjectWithoutOwnerError': return res.status(409).json(error).end(); + case 'TypeError': + return res.status(400).json(error).end(); default: logger.error('Server failed executing request', error); return res.status(500).end(); diff --git a/src/lib/services/addon-service.test.ts b/src/lib/services/addon-service.test.ts index 3089dadec7..8f4c82269a 100644 --- a/src/lib/services/addon-service.test.ts +++ b/src/lib/services/addon-service.test.ts @@ -73,7 +73,7 @@ test('should not allow addon-config for unknown provider', async () => { }, 'test', ); - }).rejects.toThrow(new TypeError('Unknown addon provider unknown')); + }).rejects.toThrow(ValidationError); }); test('should trigger simple-addon eventHandler', async () => { diff --git a/src/lib/services/addon-service.ts b/src/lib/services/addon-service.ts index 723cbe2a09..a9be1c08a4 100644 --- a/src/lib/services/addon-service.ts +++ b/src/lib/services/addon-service.ts @@ -250,9 +250,21 @@ export default class AddonService { } async validateKnownProvider(config: Partial): Promise { + if (!config.provider) { + throw new ValidationError( + 'No addon provider supplied. The property was either missing or an empty value.', + [], + undefined, + ); + } + const p = this.addonProviders[config.provider]; if (!p) { - throw new TypeError(`Unknown addon provider ${config.provider}`); + throw new ValidationError( + `Unknown addon provider ${config.provider}`, + [], + undefined, + ); } else { return true; } diff --git a/src/lib/types/stores/addon-store.ts b/src/lib/types/stores/addon-store.ts index 3d6e8a7869..e5de327680 100644 --- a/src/lib/types/stores/addon-store.ts +++ b/src/lib/types/stores/addon-store.ts @@ -2,7 +2,7 @@ import { Store } from './store'; export interface IAddonDto { provider: string; - description: string; + description?: string | null; enabled: boolean; parameters: Record; projects?: string[]; @@ -13,6 +13,7 @@ export interface IAddonDto { export interface IAddon extends IAddonDto { id: number; createdAt: Date; + description: string | null; } export interface IAddonStore extends Store { diff --git a/src/test/e2e/api/admin/addon.e2e.test.ts b/src/test/e2e/api/admin/addon.e2e.test.ts index ea3628ea8c..027bfc039c 100644 --- a/src/test/e2e/api/admin/addon.e2e.test.ts +++ b/src/test/e2e/api/admin/addon.e2e.test.ts @@ -1,5 +1,5 @@ import dbInit from '../../helpers/database-init'; -import { setupApp } from '../../helpers/test-helper'; +import { setupAppWithCustomConfig } from '../../helpers/test-helper'; import getLogger from '../../../fixtures/no-logger'; const MASKED_VALUE = '*****'; @@ -9,7 +9,13 @@ let db; beforeAll(async () => { db = await dbInit('addon_api_serial', getLogger); - app = await setupApp(db.stores); + app = await setupAppWithCustomConfig(db.stores, { + experimental: { + flags: { + strictSchemaValidation: true, + }, + }, + }); }); afterAll(async () => { @@ -196,3 +202,98 @@ test('should not delete unknown addon configuration', async () => { return app.request.delete('/api/admin/addons/21231').expect(404); }); + +test("should return 400 if it doesn't recognize the provider", async () => { + const payload = { + provider: 'htni', + enabled: true, + parameters: {}, + events: [], + }; + + return app.request.post('/api/admin/addons').send(payload).expect(400); +}); + +test('updating an addon returns the new addon configuration', async () => { + const config = { + provider: 'webhook', + enabled: true, + parameters: { + url: 'http://localhost:4242/webhook', + }, + events: [], + }; + const { body } = await app.request.post('/api/admin/addons').send(config); + + const updatedConfig = { + ...config, + enabled: false, + parameters: { url: 'http://new-url:4343' }, + }; + + return app.request + .put(`/api/admin/addons/${body.id}`) + .send(updatedConfig) + .expect((res) => { + expect(res.body).toMatchObject(updatedConfig); + }); +}); + +describe('missing descriptions', () => { + const addonWithoutDescription = { + provider: 'webhook', + enabled: true, + parameters: { + url: 'http://localhost:4242/webhook', + }, + events: ['feature-created', 'feature-updated'], + }; + + test('creating an addon without a description, sets the description to `null`', async () => { + const { body } = await app.request + .post('/api/admin/addons') + .send(addonWithoutDescription) + .expect((res) => { + expect(res.body.description).toBeNull(); + }); + + return app.request + .get(`/api/admin/addons/${body.id}`) + .expect((getResponse) => { + expect(getResponse.body.description).toBeNull(); + }); + }); + + test('updating an addon without touching `description` keeps the original value', async () => { + const { body } = await app.request + .post('/api/admin/addons') + .send(addonWithoutDescription); + + const newUrl = 'http://localhost:4242/newUrl'; + return app.request + .put(`/api/admin/addons/${body.id}`) + .send({ ...addonWithoutDescription, parameters: { url: newUrl } }) + .expect((res) => { + expect(res.body.description).toBeNull(); + }); + }); + + test.each(['', null])( + 'sending a description value of "%s", sets a `null` sets the description to an empty string', + async (description) => { + const { body } = await app.request + .post('/api/admin/addons') + .send(addonWithoutDescription); + + return app.request + .put(`/api/admin/addons/${body.id}`) + .send({ + ...addonWithoutDescription, + description, + }) + .expect((res) => { + expect(res.body.description).toStrictEqual(''); + }); + }, + ); +}); diff --git a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap index 77c759380d..4ff4a28e8e 100644 --- a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap +++ b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap @@ -44,27 +44,121 @@ exports[`should serve the OpenAPI spec 1`] = ` { "components": { "schemas": { - "addonParameterSchema": { + "addonCreateUpdateSchema": { + "description": "Data required to create or update an [Unleash addon](https://docs.getunleash.io/reference/addons) instance.", "properties": { "description": { + "description": "A description of the addon.", + "example": "This addon posts updates to our internal feature tracking system whenever a feature is created or updated.", + "type": "string", + }, + "enabled": { + "description": "Whether the addon should be enabled or not.", + "type": "boolean", + }, + "environments": { + "description": "The list of environments that this addon will listen to events from. An empty list means it will listen to events from **all** environments.", + "example": [ + "development", + "production", + ], + "items": { + "type": "string", + }, + "type": "array", + }, + "events": { + "description": "The event types that will trigger this specific addon.", + "example": [ + "feature-created", + "feature-updated", + ], + "items": { + "type": "string", + }, + "type": "array", + }, + "parameters": { + "additionalProperties": {}, + "description": "Parameters for the addon provider. This object has different required and optional properties depending on the provider you choose. Consult the documentation for details.", + "example": { + "url": "http://localhost:4242/webhook", + }, + "type": "object", + }, + "projects": { + "description": "The projects that this addon will listen to events from. An empty list means it will listen to events from **all** projects.", + "example": [ + "new-landing-project", + "signups-v2", + ], + "items": { + "type": "string", + }, + "type": "array", + }, + "provider": { + "description": "The addon provider, such as "webhook" or "slack". This string is **case sensitive** and maps to the provider's \`name\` property. + +The list of all supported providers and their parameters for a specific Unleash instance can be found by making a GET request to the \`api/admin/addons\` endpoint: the \`providers\` property of that response will contain all available providers. + +The default set of providers can be found in the [addons reference documentation](https://docs.getunleash.io/reference/addons). The default supported options are: +- \`datadog\` for [Datadog](https://docs.getunleash.io/reference/addons/datadog) +- \`slack\` for [Slack](https://docs.getunleash.io/reference/addons/slack) +- \`teams\` for [Microsoft Teams](https://docs.getunleash.io/reference/addons/teams) +- \`webhook\` for [webhooks](https://docs.getunleash.io/reference/addons/webhook) + +The provider you choose for your addon dictates what properties the \`parameters\` object needs. Refer to the documentation for each provider for more information. +", + "example": "webhook", + "type": "string", + }, + }, + "required": [ + "provider", + "enabled", + "parameters", + "events", + ], + "type": "object", + }, + "addonParameterSchema": { + "additionalProperties": false, + "description": "An addon parameter definition.", + "properties": { + "description": { + "description": "A description of the parameter. This should explain to the end user what the parameter is used for.", + "example": "The emoji_icon to use when posting messages to slack. Defaults to ":unleash:".", "type": "string", }, "displayName": { + "description": "The name of the parameter as it is shown to the end user in the Admin UI.", + "example": "Emoji Icon", "type": "string", }, "name": { + "description": "The name of the parameter as it is used in code. References to this parameter should use this value.", + "example": "emojiIcon", "type": "string", }, "placeholder": { + "description": "The default value for this parameter. This value is used if no other value is provided.", + "example": ":unleash:", "type": "string", }, "required": { + "description": "Whether this parameter is required or not. If a parameter is required, you must give it a value when you create the addon. If it is not required it can be left out. It may receive a default value in those cases.", + "example": false, "type": "boolean", }, "sensitive": { + "description": "Indicates whether this parameter is **sensitive** or not. Unleash will not return sensitive parameters to API requests. It will instead use a number of asterisks to indicate that a value is set, e.g. "******". The number of asterisks does not correlate to the parameter's value.", + "example": false, "type": "boolean", }, "type": { + "description": "The type of the parameter. Corresponds roughly to [HTML \`input\` field types](https://developer.mozilla.org/docs/Web/HTML/Element/Input#input_types). Multi-line inut fields are indicated as \`textfield\` (equivalent to the HTML \`textarea\` tag).", + "example": "text", "type": "string", }, }, @@ -78,48 +172,74 @@ exports[`should serve the OpenAPI spec 1`] = ` "type": "object", }, "addonSchema": { + "description": "An [addon](https://docs.getunleash.io/reference/addons) instance description. Contains data about what kind of provider it uses, whether it's enabled or not, what events it listens for, and more.", "properties": { - "createdAt": { - "format": "date-time", + "description": { + "description": "A description of the addon. \`null\` if no description exists.", + "example": "This addon posts updates to our internal feature tracking system whenever a feature is created or updated.", "nullable": true, "type": "string", }, - "description": { - "type": "string", - }, "enabled": { + "description": "Whether the addon is enabled or not.", "type": "boolean", }, "environments": { + "description": "The list of environments that this addon listens to events from. An empty list means it listens to events from **all** environments.", + "example": [ + "development", + "production", + ], "items": { "type": "string", }, "type": "array", }, "events": { + "description": "The event types that trigger this specific addon.", + "example": [ + "feature-created", + "feature-updated", + ], "items": { "type": "string", }, "type": "array", }, "id": { - "type": "number", + "description": "The addon's unique identifier.", + "example": 27, + "minimum": 1, + "type": "integer", }, "parameters": { - "additionalProperties": true, + "additionalProperties": {}, + "description": "Parameters for the addon provider. This object has different required and optional properties depending on the provider you choose.", + "example": { + "url": "http://localhost:4242/webhook", + }, "type": "object", }, "projects": { + "description": "The projects that this addon listens to events from. An empty list means it listens to events from **all** projects.", + "example": [ + "new-landing-project", + "signups-v2", + ], "items": { "type": "string", }, "type": "array", }, "provider": { + "description": "The addon provider, such as "webhook" or "slack".", + "example": "webhook", "type": "string", }, }, "required": [ + "id", + "description", "provider", "enabled", "parameters", @@ -128,32 +248,104 @@ exports[`should serve the OpenAPI spec 1`] = ` "type": "object", }, "addonTypeSchema": { + "additionalProperties": false, + "description": "An addon provider. Defines a specific addon type and what the end user must configure when creating a new addon of that type.", "properties": { "description": { + "description": "A description of the addon type.", + "example": "Allows Unleash to post updates to Slack.", "type": "string", }, "displayName": { + "description": "The addon type's name as it should be displayed in the admin UI.", + "example": "Slack", "type": "string", }, "documentationUrl": { + "description": "A URL to where you can find more information about using this addon type.", + "example": "https://docs.getunleash.io/docs/addons/slack", "type": "string", }, "events": { + "description": "All the [event types](https://docs.getunleash.io/reference/api/legacy/unleash/admin/events#feature-toggle-events) that are available for this addon provider.", + "example": [ + "feature-created", + "feature-updated", + "feature-archived", + "feature-revived", + "feature-stale-on", + "feature-stale-off", + "feature-environment-enabled", + "feature-environment-disabled", + "feature-strategy-remove", + "feature-strategy-update", + "feature-strategy-add", + "feature-metadata-updated", + "feature-variants-updated", + "feature-project-change", + ], "items": { "type": "string", }, "type": "array", }, "name": { + "description": "The name of the addon type. When creating new addons, this goes in the payload's \`type\` field.", + "example": "slack", "type": "string", }, "parameters": { + "description": "The addon provider's parameters. Use these to configure an addon of this provider type. Items with \`required: true\` must be provided.", + "example": [ + { + "description": "(Required)", + "displayName": "Slack webhook URL", + "name": "url", + "required": true, + "sensitive": true, + "type": "url", + }, + { + "description": "The username to use when posting messages to slack. Defaults to "Unleash".", + "displayName": "Username", + "name": "username", + "placeholder": "Unleash", + "required": false, + "sensitive": false, + "type": "text", + }, + { + "description": "The emoji_icon to use when posting messages to slack. Defaults to ":unleash:".", + "displayName": "Emoji Icon", + "name": "emojiIcon", + "placeholder": ":unleash:", + "required": false, + "sensitive": false, + "type": "text", + }, + { + "description": "(Required) Default channel to post updates to if not specified in the slack-tag", + "displayName": "Default channel", + "name": "defaultChannel", + "required": true, + "sensitive": false, + "type": "text", + }, + ], "items": { "$ref": "#/components/schemas/addonParameterSchema", }, "type": "array", }, "tagTypes": { + "description": "A list of [Unleash tag types](https://docs.getunleash.io/reference/tags#tag-types) that this addon uses. These tags will be added to the Unleash instance when an addon of this type is created.", + "example": [ + { + "description": "Slack tag used by the slack-addon to specify the slack channel.", + "icon": "S", + "name": "slack", + }, + ], "items": { "$ref": "#/components/schemas/tagTypeSchema", }, @@ -169,14 +361,241 @@ exports[`should serve the OpenAPI spec 1`] = ` "type": "object", }, "addonsSchema": { + "description": "An object containing two things: +1. A list of all [addons](https://docs.getunleash.io/reference/addons) defined on this Unleash instance +2. A list of all addon providers defined on this instance", "properties": { "addons": { + "description": "All the addons that exist on this instance of Unleash.", "items": { "$ref": "#/components/schemas/addonSchema", }, "type": "array", }, "providers": { + "description": "A list of all available addon providers, along with their parameters and descriptions.", + "example": [ + { + "description": "A Webhook is a generic way to post messages from Unleash to third party services.", + "displayName": "Webhook", + "documentationUrl": "https://docs.getunleash.io/docs/addons/webhook", + "events": [ + "feature-created", + "feature-updated", + "feature-archived", + "feature-revived", + "feature-stale-on", + "feature-stale-off", + "feature-environment-enabled", + "feature-environment-disabled", + "feature-strategy-remove", + "feature-strategy-update", + "feature-strategy-add", + "feature-metadata-updated", + "feature-variants-updated", + "feature-project-change", + "feature-tagged", + "feature-untagged", + "change-request-created", + "change-request-discarded", + "change-added", + "change-discarded", + "change-request-approved", + "change-request-approval-added", + "change-request-cancelled", + "change-request-sent-to-review", + "change-request-applied", + ], + "name": "webhook", + "parameters": [ + { + "description": "(Required) Unleash will perform a HTTP Post to the specified URL (one retry if first attempt fails)", + "displayName": "Webhook URL", + "name": "url", + "required": true, + "sensitive": true, + "type": "url", + }, + { + "description": "(Optional) The Content-Type header to use. Defaults to "application/json".", + "displayName": "Content-Type", + "name": "contentType", + "placeholder": "application/json", + "required": false, + "sensitive": false, + "type": "text", + }, + { + "description": "(Optional) The Authorization header to use. Not used if left blank.", + "displayName": "Authorization", + "name": "authorization", + "placeholder": "", + "required": false, + "sensitive": true, + "type": "text", + }, + { + "description": "(Optional) You may format the body using a mustache template. If you don't specify anything, the format will similar to the events format (https://docs.getunleash.io/reference/api/legacy/unleash/admin/events)", + "displayName": "Body template", + "name": "bodyTemplate", + "placeholder": "{ + "event": "{{event.type}}", + "createdBy": "{{event.createdBy}}", + "featureToggle": "{{event.data.name}}", + "timestamp": "{{event.data.createdAt}}" +}", + "required": false, + "sensitive": false, + "type": "textfield", + }, + ], + }, + { + "description": "Allows Unleash to post updates to Slack.", + "displayName": "Slack", + "documentationUrl": "https://docs.getunleash.io/docs/addons/slack", + "events": [ + "feature-created", + "feature-updated", + "feature-archived", + "feature-revived", + "feature-stale-on", + "feature-stale-off", + "feature-environment-enabled", + "feature-environment-disabled", + "feature-strategy-remove", + "feature-strategy-update", + "feature-strategy-add", + "feature-metadata-updated", + "feature-variants-updated", + "feature-project-change", + ], + "name": "slack", + "parameters": [ + { + "description": "(Required)", + "displayName": "Slack webhook URL", + "name": "url", + "required": true, + "sensitive": true, + "type": "url", + }, + { + "description": "The username to use when posting messages to slack. Defaults to "Unleash".", + "displayName": "Username", + "name": "username", + "placeholder": "Unleash", + "required": false, + "sensitive": false, + "type": "text", + }, + { + "description": "The emoji_icon to use when posting messages to slack. Defaults to ":unleash:".", + "displayName": "Emoji Icon", + "name": "emojiIcon", + "placeholder": ":unleash:", + "required": false, + "sensitive": false, + "type": "text", + }, + { + "description": "(Required) Default channel to post updates to if not specified in the slack-tag", + "displayName": "Default channel", + "name": "defaultChannel", + "required": true, + "sensitive": false, + "type": "text", + }, + ], + "tagTypes": [ + { + "description": "Slack tag used by the slack-addon to specify the slack channel.", + "icon": "S", + "name": "slack", + }, + ], + }, + { + "description": "Allows Unleash to post updates to Microsoft Teams.", + "displayName": "Microsoft Teams", + "documentationUrl": "https://docs.getunleash.io/docs/addons/teams", + "events": [ + "feature-created", + "feature-updated", + "feature-archived", + "feature-revived", + "feature-stale-on", + "feature-stale-off", + "feature-environment-enabled", + "feature-environment-disabled", + "feature-strategy-remove", + "feature-strategy-update", + "feature-strategy-add", + "feature-metadata-updated", + "feature-variants-updated", + "feature-project-change", + ], + "name": "teams", + "parameters": [ + { + "description": "(Required)", + "displayName": "Microsoft Teams webhook URL", + "name": "url", + "required": true, + "sensitive": true, + "type": "url", + }, + ], + }, + { + "description": "Allows Unleash to post updates to Datadog.", + "displayName": "Datadog", + "documentationUrl": "https://docs.getunleash.io/docs/addons/datadog", + "events": [ + "feature-created", + "feature-updated", + "feature-archived", + "feature-revived", + "feature-stale-on", + "feature-stale-off", + "feature-environment-enabled", + "feature-environment-disabled", + "feature-strategy-remove", + "feature-strategy-update", + "feature-strategy-add", + "feature-metadata-updated", + "feature-project-change", + "feature-variants-updated", + ], + "name": "datadog", + "parameters": [ + { + "description": "Default url: https://api.datadoghq.com/api/v1/events. Needs to be changed if your not using the US1 site.", + "displayName": "Datadog Events URL", + "name": "url", + "required": false, + "sensitive": false, + "type": "url", + }, + { + "description": "(Required) API key from Datadog", + "displayName": "Datadog API key", + "name": "apiKey", + "placeholder": "j96c23b0f12a6b3434a8d710110bd862", + "required": true, + "sensitive": true, + "type": "text", + }, + ], + "tagTypes": [ + { + "description": "All Datadog tags added to a specific feature are sent to datadog event stream.", + "icon": "D", + "name": "datadog", + }, + ], + }, + ], "items": { "$ref": "#/components/schemas/addonTypeSchema", }, @@ -4619,6 +5038,7 @@ Stats are divided into current and previous **windows**. "paths": { "/api/admin/addons": { "get": { + "description": "Retrieve all addons and providers that are defined on this Unleash instance.", "operationId": "getAddons", "responses": { "200": { @@ -4631,22 +5051,27 @@ Stats are divided into current and previous **windows**. }, "description": "addonsSchema", }, + "401": { + "description": "Authorization information is missing or invalid. Provide a valid API token as the \`authorization\` header, e.g. \`authorization:*.*.my-admin-token\`.", + }, }, + "summary": "Get all addons and providers", "tags": [ "Addons", ], }, "post": { + "description": "Create an addon instance. The addon must use one of the providers available on this Unleash instance.", "operationId": "createAddon", "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/addonSchema", + "$ref": "#/components/schemas/addonCreateUpdateSchema", }, }, }, - "description": "addonSchema", + "description": "addonCreateUpdateSchema", "required": true, }, "responses": { @@ -4660,7 +5085,23 @@ Stats are divided into current and previous **windows**. }, "description": "addonSchema", }, + "400": { + "description": "The request data does not match what we expect.", + }, + "401": { + "description": "Authorization information is missing or invalid. Provide a valid API token as the \`authorization\` header, e.g. \`authorization:*.*.my-admin-token\`.", + }, + "403": { + "description": "User credentials are valid but does not have enough privileges to execute this operation", + }, + "413": { + "description": "The body request body is larger than what we accept. By default we only accept bodies of 100kB or less", + }, + "415": { + "description": "The operation does not support request payloads of the provided type. Please ensure that you're using one of the listed payload types and that you have specified the right content type in the "content-type" header.", + }, }, + "summary": "Create a new addon", "tags": [ "Addons", ], @@ -4668,6 +5109,7 @@ Stats are divided into current and previous **windows**. }, "/api/admin/addons/{id}": { "delete": { + "description": "Delete the addon specified by the ID in the request path.", "operationId": "deleteAddon", "parameters": [ { @@ -4683,12 +5125,20 @@ Stats are divided into current and previous **windows**. "200": { "description": "This response has no body.", }, + "401": { + "description": "Authorization information is missing or invalid. Provide a valid API token as the \`authorization\` header, e.g. \`authorization:*.*.my-admin-token\`.", + }, + "403": { + "description": "User credentials are valid but does not have enough privileges to execute this operation", + }, }, + "summary": "Delete an addon", "tags": [ "Addons", ], }, "get": { + "description": "Retrieve information about the addon whose ID matches the ID in the request URL.", "operationId": "getAddon", "parameters": [ { @@ -4711,12 +5161,19 @@ Stats are divided into current and previous **windows**. }, "description": "addonSchema", }, + "401": { + "description": "Authorization information is missing or invalid. Provide a valid API token as the \`authorization\` header, e.g. \`authorization:*.*.my-admin-token\`.", + }, }, + "summary": "Get a specific addon", "tags": [ "Addons", ], }, "put": { + "description": "Update the addon with a specific ID. Any fields in the update object will be updated. Properties that are not included in the update object will not be affected. To empty a property, pass \`null\` as that property's value. + +Note: passing \`null\` as a value for the description property will set it to an empty string.", "operationId": "updateAddon", "parameters": [ { @@ -4732,11 +5189,11 @@ Stats are divided into current and previous **windows**. "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/addonSchema", + "$ref": "#/components/schemas/addonCreateUpdateSchema", }, }, }, - "description": "addonSchema", + "description": "addonCreateUpdateSchema", "required": true, }, "responses": { @@ -4750,7 +5207,23 @@ Stats are divided into current and previous **windows**. }, "description": "addonSchema", }, + "400": { + "description": "The request data does not match what we expect.", + }, + "401": { + "description": "Authorization information is missing or invalid. Provide a valid API token as the \`authorization\` header, e.g. \`authorization:*.*.my-admin-token\`.", + }, + "403": { + "description": "User credentials are valid but does not have enough privileges to execute this operation", + }, + "413": { + "description": "The body request body is larger than what we accept. By default we only accept bodies of 100kB or less", + }, + "415": { + "description": "The operation does not support request payloads of the provided type. Please ensure that you're using one of the listed payload types and that you have specified the right content type in the "content-type" header.", + }, }, + "summary": "Update an addon", "tags": [ "Addons", ], diff --git a/src/test/fixtures/fake-addon-store.ts b/src/test/fixtures/fake-addon-store.ts index c710841a44..8ea48dce43 100644 --- a/src/test/fixtures/fake-addon-store.ts +++ b/src/test/fixtures/fake-addon-store.ts @@ -43,6 +43,7 @@ export default class FakeAddonStore implements IAddonStore { const ins: IAddon = { id: this.highestId++, createdAt: new Date(), + description: null, ...addon, }; this.addons.push(ins); @@ -51,7 +52,12 @@ export default class FakeAddonStore implements IAddonStore { async update(id: number, addon: IAddonDto): Promise { await this.delete(id); - const inserted: IAddon = { id, createdAt: new Date(), ...addon }; + const inserted: IAddon = { + id, + createdAt: new Date(), + description: null, + ...addon, + }; this.addons.push(inserted); return inserted; }