diff --git a/src/lib/openapi/spec/__snapshots__/constraint-schema.test.ts.snap b/src/lib/openapi/spec/__snapshots__/constraint-schema.test.ts.snap new file mode 100644 index 0000000000..f6e5ab0b48 --- /dev/null +++ b/src/lib/openapi/spec/__snapshots__/constraint-schema.test.ts.snap @@ -0,0 +1,61 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`constraintSchema invalid operator name 1`] = ` +Object { + "data": Object { + "contextName": "a", + "operator": "b", + "value": "1", + }, + "errors": Array [ + Object { + "instancePath": "/operator", + "keyword": "enum", + "message": "must be equal to one of the allowed values", + "params": Object { + "allowedValues": Array [ + "NOT_IN", + "IN", + "STR_ENDS_WITH", + "STR_STARTS_WITH", + "STR_CONTAINS", + "NUM_EQ", + "NUM_GT", + "NUM_GTE", + "NUM_LT", + "NUM_LTE", + "DATE_AFTER", + "DATE_BEFORE", + "SEMVER_EQ", + "SEMVER_GT", + "SEMVER_LT", + ], + }, + "schemaPath": "#/properties/operator/enum", + }, + ], + "schema": "#/components/schemas/constraintSchema", +} +`; + +exports[`constraintSchema invalid value type 1`] = ` +Object { + "data": Object { + "contextName": "a", + "operator": "NUM_LTE", + "value": 1, + }, + "errors": Array [ + Object { + "instancePath": "/value", + "keyword": "type", + "message": "must be string", + "params": Object { + "type": "string", + }, + "schemaPath": "#/properties/value/type", + }, + ], + "schema": "#/components/schemas/constraintSchema", +} +`; diff --git a/src/lib/openapi/spec/constraint-schema.test.ts b/src/lib/openapi/spec/constraint-schema.test.ts new file mode 100644 index 0000000000..f38ad099c6 --- /dev/null +++ b/src/lib/openapi/spec/constraint-schema.test.ts @@ -0,0 +1,34 @@ +import { validateSchema } from '../validate'; +import { ConstraintSchema } from './constraint-schema'; + +test('constraintSchema', () => { + const data: ConstraintSchema = { + contextName: 'a', + operator: 'NUM_LTE', + value: '1', + }; + + expect( + validateSchema('#/components/schemas/constraintSchema', data), + ).toBeUndefined(); +}); + +test('constraintSchema invalid value type', () => { + expect( + validateSchema('#/components/schemas/constraintSchema', { + contextName: 'a', + operator: 'NUM_LTE', + value: 1, + }), + ).toMatchSnapshot(); +}); + +test('constraintSchema invalid operator name', () => { + expect( + validateSchema('#/components/schemas/constraintSchema', { + contextName: 'a', + operator: 'b', + value: '1', + }), + ).toMatchSnapshot(); +}); diff --git a/src/lib/openapi/validate.test.ts b/src/lib/openapi/validate.test.ts new file mode 100644 index 0000000000..196394ff3d --- /dev/null +++ b/src/lib/openapi/validate.test.ts @@ -0,0 +1,7 @@ +import { validateSchema } from './validate'; + +test('validateSchema', () => { + expect(() => validateSchema('unknownSchemaId' as any, {})).toThrow( + 'no schema with key or ref "unknownSchemaId"', + ); +}); diff --git a/src/lib/routes/admin-api/constraints.ts b/src/lib/routes/admin-api/constraints.ts index 2c2b056d29..9c6d18ec8a 100644 --- a/src/lib/routes/admin-api/constraints.ts +++ b/src/lib/routes/admin-api/constraints.ts @@ -6,27 +6,49 @@ import { IConstraint } from '../../types/model'; import { NONE } from '../../types/permissions'; import Controller from '../controller'; import { Logger } from '../../logger'; +import { OpenApiService } from '../../services/openapi-service'; +import { createRequestSchema } from '../../openapi'; export default class ConstraintController extends Controller { private featureService: FeatureToggleService; + private openApiService: OpenApiService; + private readonly logger: Logger; constructor( config: IUnleashConfig, { featureToggleServiceV2, - }: Pick, + openApiService, + }: Pick, ) { super(config); this.featureService = featureToggleServiceV2; + this.openApiService = openApiService; this.logger = config.getLogger('/admin-api/validation.ts'); - this.post('/validate', this.validateConstraint, NONE); + this.route({ + method: 'post', + path: '/validate', + handler: this.validateConstraint, + permission: NONE, + middleware: [ + openApiService.validPath({ + tags: ['admin'], + operationId: 'validateConstraint', + requestBody: createRequestSchema('constraintSchema'), + responses: { + 204: { description: 'validConstraint' }, + 400: { description: 'invalidConstraint' }, + }, + }), + ], + }); } async validateConstraint( - req: Request<{}, undefined, IConstraint>, + req: Request, res: Response, ): Promise { await this.featureService.validateConstraint(req.body); 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 aeb39d2d7c..59d5f6051a 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 @@ -987,6 +987,33 @@ Object { ], }, }, + "/api/admin/constraints/validate": Object { + "post": Object { + "operationId": "validateConstraint", + "requestBody": Object { + "content": Object { + "application/json": Object { + "schema": Object { + "$ref": "#/components/schemas/constraintSchema", + }, + }, + }, + "description": "constraintSchema", + "required": true, + }, + "responses": Object { + "204": Object { + "description": "validConstraint", + }, + "400": Object { + "description": "invalidConstraint", + }, + }, + "tags": Array [ + "admin", + ], + }, + }, "/api/admin/features": Object { "get": Object { "deprecated": true,