From 621264a5aacf479ea9fdf53309bbbb8dc77998b0 Mon Sep 17 00:00:00 2001 From: Jeffrey Lembeck Date: Fri, 13 Mar 2026 17:06:06 -0700 Subject: [PATCH] feat: add exclusiveMinimum and exclusiveMaximum Add @exclusiveMinimum and @exclusiveMaximum JSDoc annotations for strict inequality bounds (> / <), complementing @minimum/@maximum (>= / <=). Spec output follows each OpenAPI version's JSON Schema draft: - Swagger 2.0 / OAS 3.0: boolean form (minimum: X, exclusiveMinimum: true) - OAS 3.1: numeric form (exclusiveMinimum: X) When both @minimum and @exclusiveMinimum are present, v2/v3.0 errors; v3.1 emits both. Closes #1842 --- packages/cli/src/swagger/specGenerator2.ts | 49 +++++---- packages/cli/src/swagger/specGenerator3.ts | 59 ++++++----- packages/cli/src/swagger/specGenerator31.ts | 12 +++ packages/cli/src/utils/validatorUtils.ts | 6 ++ .../src/routeGeneration/templateHelpers.ts | 40 +++++++ packages/runtime/src/swagger/swagger.ts | 10 +- tests/fixtures/testModel.ts | 16 +++ tests/fixtures/testModel31.ts | 16 +++ ...dynamic-controllers-express-server.spec.ts | 20 ++++ tests/integration/express-server.spec.ts | 20 ++++ tests/integration/hapi-server.spec.ts | 20 ++++ .../koa-server-no-additional-allowed.spec.ts | 8 ++ tests/integration/koa-server.spec.ts | 20 ++++ tests/integration/openapi3-express.spec.ts | 20 ++++ .../definitionsGeneration/definitions.spec.ts | 35 ++++++ tests/unit/swagger/schemaDetails3.spec.ts | 38 +++++++ tests/unit/swagger/schemaDetails31.spec.ts | 42 ++++++++ tests/unit/swagger/templateHelpers.spec.ts | 100 ++++++++++++++++++ 18 files changed, 482 insertions(+), 49 deletions(-) diff --git a/packages/cli/src/swagger/specGenerator2.ts b/packages/cli/src/swagger/specGenerator2.ts index dcd272fb8..fd09a8cc6 100644 --- a/packages/cli/src/swagger/specGenerator2.ts +++ b/packages/cli/src/swagger/specGenerator2.ts @@ -18,6 +18,32 @@ export class SpecGenerator2 extends SpecGenerator { super(metadata, config); } + // In Swagger 2.0, exclusiveMinimum/exclusiveMaximum are boolean modifiers on minimum/maximum. + private transformValidatorsForSchema(validators: Tsoa.Validators): Record { + if (validators.exclusiveMinimum !== undefined && validators.minimum !== undefined) { + throw new Error('Cannot use both @minimum and @exclusiveMinimum in Swagger 2.0. Use one or the other, or target OpenAPI 3.1.'); + } + if (validators.exclusiveMaximum !== undefined && validators.maximum !== undefined) { + throw new Error('Cannot use both @maximum and @exclusiveMaximum in Swagger 2.0. Use one or the other, or target OpenAPI 3.1.'); + } + + const result: Record = {}; + Object.keys(validators) + .filter(shouldIncludeValidatorInSchema) + .forEach(key => { + if (key === 'exclusiveMinimum') { + result['minimum'] = validators.exclusiveMinimum!.value; + result['exclusiveMinimum'] = true; + } else if (key === 'exclusiveMaximum') { + result['maximum'] = validators.exclusiveMaximum!.value; + result['exclusiveMaximum'] = true; + } else { + result[key] = validators[key]!.value; + } + }); + return result; + } + public GetSpec() { let spec: Swagger.Spec2 = { basePath: normalisePath(this.config.basePath as string, '/', undefined, false), @@ -129,14 +155,7 @@ export class SpecGenerator2 extends SpecGenerator { } else if (referenceType.dataType === 'refAlias') { const swaggerType = this.getSwaggerType(referenceType.type); const format = referenceType.format as Swagger.DataFormat; - const validators = Object.keys(referenceType.validators) - .filter(shouldIncludeValidatorInSchema) - .reduce((acc, key) => { - return { - ...acc, - [key]: referenceType.validators[key]!.value, - }; - }, {}); + const validators = this.transformValidatorsForSchema(referenceType.validators); definitions[referenceType.refName] = { ...(swaggerType as Swagger.Schema2), @@ -365,12 +384,7 @@ export class SpecGenerator2 extends SpecGenerator { return parameter; } - const validatorObjs: Partial> = {}; - Object.keys(source.validators) - .filter(shouldIncludeValidatorInSchema) - .forEach(key => { - validatorObjs[key] = source.validators[key]!.value; - }); + const validatorObjs = this.transformValidatorsForSchema(source.validators); if (source.in === 'body' && source.type.dataType === 'array') { parameter.schema = { @@ -414,11 +428,8 @@ export class SpecGenerator2 extends SpecGenerator { if (!swaggerType.$ref) { swaggerType.default = property.default; - Object.keys(property.validators) - .filter(shouldIncludeValidatorInSchema) - .forEach(key => { - swaggerType = { ...swaggerType, [key]: property.validators[key]!.value }; - }); + const propertyValidators = this.transformValidatorsForSchema(property.validators); + swaggerType = { ...swaggerType, ...propertyValidators }; } if (property.deprecated) { swaggerType['x-deprecated'] = true; diff --git a/packages/cli/src/swagger/specGenerator3.ts b/packages/cli/src/swagger/specGenerator3.ts index bd675eaef..3e677faa4 100644 --- a/packages/cli/src/swagger/specGenerator3.ts +++ b/packages/cli/src/swagger/specGenerator3.ts @@ -27,6 +27,33 @@ export class SpecGenerator3 extends SpecGenerator { super(metadata, config); } + // In OAS 3.0, exclusiveMinimum/exclusiveMaximum are boolean modifiers on minimum/maximum. + // OAS 3.1 overrides this to pass through numeric values directly. + protected transformValidatorsForSchema(validators: Tsoa.Validators): Record { + if (validators.exclusiveMinimum !== undefined && validators.minimum !== undefined) { + throw new Error('Cannot use both @minimum and @exclusiveMinimum in OpenAPI 3.0. Use one or the other, or target OpenAPI 3.1.'); + } + if (validators.exclusiveMaximum !== undefined && validators.maximum !== undefined) { + throw new Error('Cannot use both @maximum and @exclusiveMaximum in OpenAPI 3.0. Use one or the other, or target OpenAPI 3.1.'); + } + + const result: Record = {}; + Object.keys(validators) + .filter(shouldIncludeValidatorInSchema) + .forEach(key => { + if (key === 'exclusiveMinimum') { + result['minimum'] = validators.exclusiveMinimum!.value; + result['exclusiveMinimum'] = true; + } else if (key === 'exclusiveMaximum') { + result['maximum'] = validators.exclusiveMaximum!.value; + result['exclusiveMaximum'] = true; + } else { + result[key] = validators[key]!.value; + } + }); + return result; + } + public GetSpec(): Swagger.Spec3 { let spec: Swagger.Spec30 = { openapi: '3.0.0', @@ -214,14 +241,7 @@ export class SpecGenerator3 extends SpecGenerator { } else if (referenceType.dataType === 'refAlias') { const swaggerType = this.getSwaggerType(referenceType.type); const format = referenceType.format as Swagger.DataFormat; - const validators = Object.keys(referenceType.validators) - .filter(shouldIncludeValidatorInSchema) - .reduce((acc, key) => { - return { - ...acc, - [key]: referenceType.validators[key]!.value, - }; - }, {}); + const validators = this.transformValidatorsForSchema(referenceType.validators); schema[referenceType.refName] = { ...(swaggerType as Swagger.Schema3), @@ -448,14 +468,7 @@ export class SpecGenerator3 extends SpecGenerator { } protected buildMediaType(controllerName: string, method: Tsoa.Method, parameter: Tsoa.Parameter): Swagger.MediaType { - const validators = Object.keys(parameter.validators) - .filter(shouldIncludeValidatorInSchema) - .reduce((acc, key) => { - return { - ...acc, - [key]: parameter.validators[key]!.value, - }; - }, {}); + const validators = this.transformValidatorsForSchema(parameter.validators); const mediaType: Swagger.MediaType = { schema: { @@ -516,12 +529,7 @@ export class SpecGenerator3 extends SpecGenerator { return Object.assign(parameter, this.buildExamples(source)); } - const validatorObjs: { [key in Tsoa.SchemaValidatorKey]?: unknown } = {}; - Object.keys(source.validators) - .filter(shouldIncludeValidatorInSchema) - .forEach(key => { - validatorObjs[key] = source.validators[key]!.value; - }); + const validatorObjs = this.transformValidatorsForSchema(source.validators); if (source.type.dataType === 'any') { parameter.schema.type = 'string'; @@ -578,11 +586,8 @@ export class SpecGenerator3 extends SpecGenerator { if (!swaggerType.$ref) { swaggerType.default = property.default; - Object.keys(property.validators) - .filter(shouldIncludeValidatorInSchema) - .forEach(key => { - swaggerType = { ...swaggerType, [key]: property.validators[key]!.value }; - }); + const propertyValidators = this.transformValidatorsForSchema(property.validators); + swaggerType = { ...swaggerType, ...propertyValidators }; } if (property.deprecated) { swaggerType.deprecated = true; diff --git a/packages/cli/src/swagger/specGenerator31.ts b/packages/cli/src/swagger/specGenerator31.ts index 36d07d2bb..3a50b31b7 100644 --- a/packages/cli/src/swagger/specGenerator31.ts +++ b/packages/cli/src/swagger/specGenerator31.ts @@ -4,6 +4,7 @@ import { merge as deepMerge } from 'ts-deepmerge'; import { ExtendedSpecConfig } from '../cli'; import { UnspecifiedObject } from '../utils/unspecifiedObject'; +import { shouldIncludeValidatorInSchema } from '../utils/validatorUtils'; import { SpecGenerator3 } from './specGenerator3'; /** @@ -50,6 +51,17 @@ export class SpecGenerator31 extends SpecGenerator3 { return spec; } + // In OAS 3.1, exclusiveMinimum/exclusiveMaximum are standalone numbers. + protected override transformValidatorsForSchema(validators: Tsoa.Validators): Record { + const result: Record = {}; + Object.keys(validators) + .filter(shouldIncludeValidatorInSchema) + .forEach(key => { + result[key] = validators[key]!.value; + }); + return result; + } + /** * Override to add tuple type support (OpenAPI 3.1 feature via prefixItems) */ diff --git a/packages/cli/src/utils/validatorUtils.ts b/packages/cli/src/utils/validatorUtils.ts index 8271060a8..49f09b3f6 100644 --- a/packages/cli/src/utils/validatorUtils.ts +++ b/packages/cli/src/utils/validatorUtils.ts @@ -53,6 +53,8 @@ export function getParameterValidators(parameter: ts.ParameterDeclaration, param break; case 'minimum': case 'maximum': + case 'exclusiveMinimum': + case 'exclusiveMaximum': case 'minItems': case 'maxItems': case 'minLength': @@ -152,6 +154,8 @@ export function getPropertyValidators(property: ts.Node): Tsoa.Validators | unde break; case 'minimum': case 'maximum': + case 'exclusiveMinimum': + case 'exclusiveMaximum': case 'minItems': case 'maxItems': case 'minLength': @@ -228,6 +232,8 @@ function getParameterTagSupport() { 'pattern', 'minimum', 'maximum', + 'exclusiveMinimum', + 'exclusiveMaximum', 'minDate', 'maxDate', 'title', diff --git a/packages/runtime/src/routeGeneration/templateHelpers.ts b/packages/runtime/src/routeGeneration/templateHelpers.ts index dc26b0248..b663a451b 100644 --- a/packages/runtime/src/routeGeneration/templateHelpers.ts +++ b/packages/runtime/src/routeGeneration/templateHelpers.ts @@ -223,6 +223,24 @@ export class ValidationService { return; } } + if (validators.exclusiveMinimum && validators.exclusiveMinimum.value !== undefined) { + if (validators.exclusiveMinimum.value >= numberValue) { + fieldErrors[parent + name] = { + message: validators.exclusiveMinimum.errorMsg || `exclusiveMin ${validators.exclusiveMinimum.value}`, + value, + }; + return; + } + } + if (validators.exclusiveMaximum && validators.exclusiveMaximum.value !== undefined) { + if (validators.exclusiveMaximum.value <= numberValue) { + fieldErrors[parent + name] = { + message: validators.exclusiveMaximum.errorMsg || `exclusiveMax ${validators.exclusiveMaximum.value}`, + value, + }; + return; + } + } return numberValue; } @@ -266,6 +284,24 @@ export class ValidationService { return; } } + if (validators.exclusiveMinimum && validators.exclusiveMinimum.value !== undefined) { + if (validators.exclusiveMinimum.value >= numberValue) { + fieldErrors[parent + name] = { + message: validators.exclusiveMinimum.errorMsg || `exclusiveMin ${validators.exclusiveMinimum.value}`, + value, + }; + return; + } + } + if (validators.exclusiveMaximum && validators.exclusiveMaximum.value !== undefined) { + if (validators.exclusiveMaximum.value <= numberValue) { + fieldErrors[parent + name] = { + message: validators.exclusiveMaximum.errorMsg || `exclusiveMax ${validators.exclusiveMaximum.value}`, + value, + }; + return; + } + } return numberValue; } @@ -940,6 +976,8 @@ export interface IntegerValidator { isLong?: { errorMsg?: string }; minimum?: { value: number; errorMsg?: string }; maximum?: { value: number; errorMsg?: string }; + exclusiveMinimum?: { value: number; errorMsg?: string }; + exclusiveMaximum?: { value: number; errorMsg?: string }; } export interface FloatValidator { @@ -947,6 +985,8 @@ export interface FloatValidator { isDouble?: { errorMsg?: string }; minimum?: { value: number; errorMsg?: string }; maximum?: { value: number; errorMsg?: string }; + exclusiveMinimum?: { value: number; errorMsg?: string }; + exclusiveMaximum?: { value: number; errorMsg?: string }; } export interface DateValidator { diff --git a/packages/runtime/src/swagger/swagger.ts b/packages/runtime/src/swagger/swagger.ts index 775faebb2..8bced6391 100644 --- a/packages/runtime/src/swagger/swagger.ts +++ b/packages/runtime/src/swagger/swagger.ts @@ -339,9 +339,7 @@ export namespace Swagger { default?: string | boolean | number | unknown; multipleOf?: number; maximum?: number; - exclusiveMaximum?: number; minimum?: number; - exclusiveMinimum?: number; maxLength?: number; minLength?: number; pattern?: string; @@ -368,8 +366,10 @@ export namespace Swagger { items?: BaseSchema; } - export interface Schema31 extends Omit { + export interface Schema31 extends Omit { examples?: unknown[]; + exclusiveMinimum?: number; + exclusiveMaximum?: number; properties?: { [key: string]: Schema31 }; additionalProperties?: boolean | Schema31; @@ -397,6 +397,8 @@ export namespace Swagger { allOf?: BaseSchema[]; deprecated?: boolean; properties?: { [propertyName: string]: Schema3 }; + exclusiveMinimum?: boolean; + exclusiveMaximum?: boolean; } export interface Schema2 extends BaseSchema { @@ -404,6 +406,8 @@ export namespace Swagger { properties?: { [propertyName: string]: Schema2 }; ['x-nullable']?: boolean; ['x-deprecated']?: boolean; + exclusiveMinimum?: boolean; + exclusiveMaximum?: boolean; } export interface Header { diff --git a/tests/fixtures/testModel.ts b/tests/fixtures/testModel.ts index 807cf73d5..1122cb606 100644 --- a/tests/fixtures/testModel.ts +++ b/tests/fixtures/testModel.ts @@ -880,6 +880,14 @@ export class ValidateModel { * @minimum 5 */ public numberMin5!: number; + /** + * @exclusiveMinimum 5 + */ + public numberExclusiveMin5!: number; + /** + * @exclusiveMaximum 10 + */ + public numberExclusiveMax10!: number; /** * @maxLength 10 */ @@ -990,6 +998,14 @@ export class ValidateModel { * @minimum 5 */ numberMin5: number; + /** + * @exclusiveMinimum 5 + */ + numberExclusiveMin5: number; + /** + * @exclusiveMaximum 10 + */ + numberExclusiveMax10: number; /** * @maxLength 10 */ diff --git a/tests/fixtures/testModel31.ts b/tests/fixtures/testModel31.ts index 9ef00b4e0..f22c94036 100644 --- a/tests/fixtures/testModel31.ts +++ b/tests/fixtures/testModel31.ts @@ -872,6 +872,14 @@ export class ValidateModel { * @minimum 5 */ public numberMin5!: number; + /** + * @exclusiveMinimum 5 + */ + public numberExclusiveMin5!: number; + /** + * @exclusiveMaximum 10 + */ + public numberExclusiveMax10!: number; /** * @maxLength 10 */ @@ -982,6 +990,14 @@ export class ValidateModel { * @minimum 5 */ numberMin5: number; + /** + * @exclusiveMinimum 5 + */ + numberExclusiveMin5: number; + /** + * @exclusiveMaximum 10 + */ + numberExclusiveMax10: number; /** * @maxLength 10 */ diff --git a/tests/integration/dynamic-controllers-express-server.spec.ts b/tests/integration/dynamic-controllers-express-server.spec.ts index d66a3353c..bf23c591c 100644 --- a/tests/integration/dynamic-controllers-express-server.spec.ts +++ b/tests/integration/dynamic-controllers-express-server.spec.ts @@ -623,6 +623,8 @@ describe('Express Server', () => { bodyModel.numberMax10 = 10; bodyModel.numberMin5 = 5; + bodyModel.numberExclusiveMin5 = 6; + bodyModel.numberExclusiveMax10 = 9; bodyModel.stringMax10Lenght = 'abcdef'; bodyModel.stringMin5Lenght = 'abcdef'; bodyModel.stringPatternAZaz = 'aBcD'; @@ -648,6 +650,8 @@ describe('Express Server', () => { numberMax10: 10, numberMin5: 5, + numberExclusiveMin5: 6, + numberExclusiveMax10: 9, stringMax10Lenght: 'abcdef', stringMin5Lenght: 'abcdef', stringPatternAZaz: 'aBcD', @@ -701,6 +705,8 @@ describe('Express Server', () => { expect(body.numberMax10).to.equal(bodyModel.numberMax10); expect(body.numberMin5).to.equal(bodyModel.numberMin5); + expect(body.numberExclusiveMin5).to.equal(bodyModel.numberExclusiveMin5); + expect(body.numberExclusiveMax10).to.equal(bodyModel.numberExclusiveMax10); expect(body.stringMax10Lenght).to.equal(bodyModel.stringMax10Lenght); expect(body.stringMin5Lenght).to.equal(bodyModel.stringMin5Lenght); expect(body.stringPatternAZaz).to.equal(bodyModel.stringPatternAZaz); @@ -726,6 +732,8 @@ describe('Express Server', () => { expect(body.nestedObject.numberMax10).to.equal(bodyModel.nestedObject.numberMax10); expect(body.nestedObject.numberMin5).to.equal(bodyModel.nestedObject.numberMin5); + expect(body.nestedObject.numberExclusiveMin5).to.equal(bodyModel.nestedObject.numberExclusiveMin5); + expect(body.nestedObject.numberExclusiveMax10).to.equal(bodyModel.nestedObject.numberExclusiveMax10); expect(body.nestedObject.stringMax10Lenght).to.equal(bodyModel.nestedObject.stringMax10Lenght); expect(body.nestedObject.stringMin5Lenght).to.equal(bodyModel.nestedObject.stringMin5Lenght); expect(body.nestedObject.stringPatternAZaz).to.equal(bodyModel.nestedObject.stringPatternAZaz); @@ -761,6 +769,8 @@ describe('Express Server', () => { bodyModel.numberMax10 = 20; bodyModel.numberMin5 = 0; + bodyModel.numberExclusiveMin5 = 5; + bodyModel.numberExclusiveMax10 = 10; bodyModel.stringMax10Lenght = 'abcdefghijk'; bodyModel.stringMin5Lenght = 'abcd'; bodyModel.stringPatternAZaz = 'ab01234'; @@ -785,6 +795,8 @@ describe('Express Server', () => { numberMax10: 20, numberMin5: 0, + numberExclusiveMin5: 5, + numberExclusiveMax10: 10, stringMax10Lenght: 'abcdefghijk', stringMin5Lenght: 'abcd', stringPatternAZaz: 'ab01234', @@ -845,6 +857,10 @@ describe('Express Server', () => { expect(body.fields['body.numberMax10'].value).to.be.undefined; expect(body.fields['body.numberMin5'].message).to.equal('min 5'); expect(body.fields['body.numberMin5'].value).to.be.undefined; + expect(body.fields['body.numberExclusiveMin5'].message).to.equal('exclusiveMin 5'); + expect(body.fields['body.numberExclusiveMin5'].value).to.be.undefined; + expect(body.fields['body.numberExclusiveMax10'].message).to.equal('exclusiveMax 10'); + expect(body.fields['body.numberExclusiveMax10'].value).to.be.undefined; expect(body.fields['body.stringMax10Lenght'].message).to.equal('maxLength 10'); expect(body.fields['body.stringMax10Lenght'].value).to.be.undefined; expect(body.fields['body.stringMin5Lenght'].message).to.equal('minLength 5'); @@ -888,6 +904,10 @@ describe('Express Server', () => { expect(body.fields['body.nestedObject.numberMax10'].value).to.be.undefined; expect(body.fields['body.nestedObject.numberMin5'].message).to.equal('min 5'); expect(body.fields['body.nestedObject.numberMin5'].value).to.be.undefined; + expect(body.fields['body.nestedObject.numberExclusiveMin5'].message).to.equal('exclusiveMin 5'); + expect(body.fields['body.nestedObject.numberExclusiveMin5'].value).to.be.undefined; + expect(body.fields['body.nestedObject.numberExclusiveMax10'].message).to.equal('exclusiveMax 10'); + expect(body.fields['body.nestedObject.numberExclusiveMax10'].value).to.be.undefined; expect(body.fields['body.nestedObject.stringMax10Lenght'].message).to.equal('maxLength 10'); expect(body.fields['body.nestedObject.stringMax10Lenght'].value).to.be.undefined; expect(body.fields['body.nestedObject.stringMin5Lenght'].message).to.equal('minLength 5'); diff --git a/tests/integration/express-server.spec.ts b/tests/integration/express-server.spec.ts index 0c041a67a..35bd4990c 100644 --- a/tests/integration/express-server.spec.ts +++ b/tests/integration/express-server.spec.ts @@ -853,6 +853,8 @@ describe('Express Server', () => { bodyModel.numberMax10 = 10; bodyModel.numberMin5 = 5; + bodyModel.numberExclusiveMin5 = 6; + bodyModel.numberExclusiveMax10 = 9; bodyModel.stringMax10Lenght = 'abcdef'; bodyModel.stringMin5Lenght = 'abcdef'; bodyModel.stringPatternAZaz = 'aBcD'; @@ -878,6 +880,8 @@ describe('Express Server', () => { numberMax10: 10, numberMin5: 5, + numberExclusiveMin5: 6, + numberExclusiveMax10: 9, stringMax10Lenght: 'abcdef', stringMin5Lenght: 'abcdef', stringPatternAZaz: 'aBcD', @@ -931,6 +935,8 @@ describe('Express Server', () => { expect(body.numberMax10).to.equal(bodyModel.numberMax10); expect(body.numberMin5).to.equal(bodyModel.numberMin5); + expect(body.numberExclusiveMin5).to.equal(bodyModel.numberExclusiveMin5); + expect(body.numberExclusiveMax10).to.equal(bodyModel.numberExclusiveMax10); expect(body.stringMax10Lenght).to.equal(bodyModel.stringMax10Lenght); expect(body.stringMin5Lenght).to.equal(bodyModel.stringMin5Lenght); expect(body.stringPatternAZaz).to.equal(bodyModel.stringPatternAZaz); @@ -956,6 +962,8 @@ describe('Express Server', () => { expect(body.nestedObject.numberMax10).to.equal(bodyModel.nestedObject.numberMax10); expect(body.nestedObject.numberMin5).to.equal(bodyModel.nestedObject.numberMin5); + expect(body.nestedObject.numberExclusiveMin5).to.equal(bodyModel.nestedObject.numberExclusiveMin5); + expect(body.nestedObject.numberExclusiveMax10).to.equal(bodyModel.nestedObject.numberExclusiveMax10); expect(body.nestedObject.stringMax10Lenght).to.equal(bodyModel.nestedObject.stringMax10Lenght); expect(body.nestedObject.stringMin5Lenght).to.equal(bodyModel.nestedObject.stringMin5Lenght); expect(body.nestedObject.stringPatternAZaz).to.equal(bodyModel.nestedObject.stringPatternAZaz); @@ -991,6 +999,8 @@ describe('Express Server', () => { bodyModel.numberMax10 = 20; bodyModel.numberMin5 = 0; + bodyModel.numberExclusiveMin5 = 5; + bodyModel.numberExclusiveMax10 = 10; bodyModel.stringMax10Lenght = 'abcdefghijk'; bodyModel.stringMin5Lenght = 'abcd'; bodyModel.stringPatternAZaz = 'ab01234'; @@ -1015,6 +1025,8 @@ describe('Express Server', () => { numberMax10: 20, numberMin5: 0, + numberExclusiveMin5: 5, + numberExclusiveMax10: 10, stringMax10Lenght: 'abcdefghijk', stringMin5Lenght: 'abcd', stringPatternAZaz: 'ab01234', @@ -1075,6 +1087,10 @@ describe('Express Server', () => { expect(body.fields['body.numberMax10'].value).to.be.undefined; expect(body.fields['body.numberMin5'].message).to.equal('min 5'); expect(body.fields['body.numberMin5'].value).to.be.undefined; + expect(body.fields['body.numberExclusiveMin5'].message).to.equal('exclusiveMin 5'); + expect(body.fields['body.numberExclusiveMin5'].value).to.be.undefined; + expect(body.fields['body.numberExclusiveMax10'].message).to.equal('exclusiveMax 10'); + expect(body.fields['body.numberExclusiveMax10'].value).to.be.undefined; expect(body.fields['body.stringMax10Lenght'].message).to.equal('maxLength 10'); expect(body.fields['body.stringMax10Lenght'].value).to.be.undefined; expect(body.fields['body.stringMin5Lenght'].message).to.equal('minLength 5'); @@ -1118,6 +1134,10 @@ describe('Express Server', () => { expect(body.fields['body.nestedObject.numberMax10'].value).to.be.undefined; expect(body.fields['body.nestedObject.numberMin5'].message).to.equal('min 5'); expect(body.fields['body.nestedObject.numberMin5'].value).to.be.undefined; + expect(body.fields['body.nestedObject.numberExclusiveMin5'].message).to.equal('exclusiveMin 5'); + expect(body.fields['body.nestedObject.numberExclusiveMin5'].value).to.be.undefined; + expect(body.fields['body.nestedObject.numberExclusiveMax10'].message).to.equal('exclusiveMax 10'); + expect(body.fields['body.nestedObject.numberExclusiveMax10'].value).to.be.undefined; expect(body.fields['body.nestedObject.stringMax10Lenght'].message).to.equal('maxLength 10'); expect(body.fields['body.nestedObject.stringMax10Lenght'].value).to.be.undefined; expect(body.fields['body.nestedObject.stringMin5Lenght'].message).to.equal('minLength 5'); diff --git a/tests/integration/hapi-server.spec.ts b/tests/integration/hapi-server.spec.ts index d958f298a..65650b6e6 100644 --- a/tests/integration/hapi-server.spec.ts +++ b/tests/integration/hapi-server.spec.ts @@ -676,6 +676,8 @@ describe('Hapi Server', () => { bodyModel.numberMax10 = 10; bodyModel.numberMin5 = 5; + bodyModel.numberExclusiveMin5 = 6; + bodyModel.numberExclusiveMax10 = 9; bodyModel.stringMax10Lenght = 'abcdef'; bodyModel.stringMin5Lenght = 'abcdef'; bodyModel.stringPatternAZaz = 'aBcD'; @@ -701,6 +703,8 @@ describe('Hapi Server', () => { numberMax10: 10, numberMin5: 5, + numberExclusiveMin5: 6, + numberExclusiveMax10: 9, stringMax10Lenght: 'abcdef', stringMin5Lenght: 'abcdef', stringPatternAZaz: 'aBcD', @@ -754,6 +758,8 @@ describe('Hapi Server', () => { expect(body.numberMax10).to.equal(bodyModel.numberMax10); expect(body.numberMin5).to.equal(bodyModel.numberMin5); + expect(body.numberExclusiveMin5).to.equal(bodyModel.numberExclusiveMin5); + expect(body.numberExclusiveMax10).to.equal(bodyModel.numberExclusiveMax10); expect(body.stringMax10Lenght).to.equal(bodyModel.stringMax10Lenght); expect(body.stringMin5Lenght).to.equal(bodyModel.stringMin5Lenght); expect(body.stringPatternAZaz).to.equal(bodyModel.stringPatternAZaz); @@ -779,6 +785,8 @@ describe('Hapi Server', () => { expect(body.nestedObject.numberMax10).to.equal(bodyModel.nestedObject.numberMax10); expect(body.nestedObject.numberMin5).to.equal(bodyModel.nestedObject.numberMin5); + expect(body.nestedObject.numberExclusiveMin5).to.equal(bodyModel.nestedObject.numberExclusiveMin5); + expect(body.nestedObject.numberExclusiveMax10).to.equal(bodyModel.nestedObject.numberExclusiveMax10); expect(body.nestedObject.stringMax10Lenght).to.equal(bodyModel.nestedObject.stringMax10Lenght); expect(body.nestedObject.stringMin5Lenght).to.equal(bodyModel.nestedObject.stringMin5Lenght); expect(body.nestedObject.stringPatternAZaz).to.equal(bodyModel.nestedObject.stringPatternAZaz); @@ -814,6 +822,8 @@ describe('Hapi Server', () => { bodyModel.numberMax10 = 20; bodyModel.numberMin5 = 0; + bodyModel.numberExclusiveMin5 = 5; + bodyModel.numberExclusiveMax10 = 10; bodyModel.stringMax10Lenght = 'abcdefghijk'; bodyModel.stringMin5Lenght = 'abcd'; bodyModel.stringPatternAZaz = 'ab01234'; @@ -838,6 +848,8 @@ describe('Hapi Server', () => { numberMax10: 20, numberMin5: 0, + numberExclusiveMin5: 5, + numberExclusiveMax10: 10, stringMax10Lenght: 'abcdefghijk', stringMin5Lenght: 'abcd', stringPatternAZaz: 'ab01234', @@ -898,6 +910,10 @@ describe('Hapi Server', () => { expect(body.fields['body.numberMax10'].value).to.be.undefined; expect(body.fields['body.numberMin5'].message).to.equal('min 5'); expect(body.fields['body.numberMin5'].value).to.be.undefined; + expect(body.fields['body.numberExclusiveMin5'].message).to.equal('exclusiveMin 5'); + expect(body.fields['body.numberExclusiveMin5'].value).to.be.undefined; + expect(body.fields['body.numberExclusiveMax10'].message).to.equal('exclusiveMax 10'); + expect(body.fields['body.numberExclusiveMax10'].value).to.be.undefined; expect(body.fields['body.stringMax10Lenght'].message).to.equal('maxLength 10'); expect(body.fields['body.stringMax10Lenght'].value).to.be.undefined; expect(body.fields['body.stringMin5Lenght'].message).to.equal('minLength 5'); @@ -941,6 +957,10 @@ describe('Hapi Server', () => { expect(body.fields['body.nestedObject.numberMax10'].value).to.be.undefined; expect(body.fields['body.nestedObject.numberMin5'].message).to.equal('min 5'); expect(body.fields['body.nestedObject.numberMin5'].value).to.be.undefined; + expect(body.fields['body.nestedObject.numberExclusiveMin5'].message).to.equal('exclusiveMin 5'); + expect(body.fields['body.nestedObject.numberExclusiveMin5'].value).to.be.undefined; + expect(body.fields['body.nestedObject.numberExclusiveMax10'].message).to.equal('exclusiveMax 10'); + expect(body.fields['body.nestedObject.numberExclusiveMax10'].value).to.be.undefined; expect(body.fields['body.nestedObject.stringMax10Lenght'].message).to.equal('maxLength 10'); expect(body.fields['body.nestedObject.stringMax10Lenght'].value).to.be.undefined; expect(body.fields['body.nestedObject.stringMin5Lenght'].message).to.equal('minLength 5'); diff --git a/tests/integration/koa-server-no-additional-allowed.spec.ts b/tests/integration/koa-server-no-additional-allowed.spec.ts index 28803bace..983ec81c8 100644 --- a/tests/integration/koa-server-no-additional-allowed.spec.ts +++ b/tests/integration/koa-server-no-additional-allowed.spec.ts @@ -213,6 +213,8 @@ describe('Koa Server (with noImplicitAdditionalProperties turned on)', () => { bodyModel.numberMax10 = 10; bodyModel.numberMin5 = 5; + bodyModel.numberExclusiveMin5 = 6; + bodyModel.numberExclusiveMax10 = 9; bodyModel.stringMax10Lenght = 'abcdef'; bodyModel.stringMin5Lenght = 'abcdef'; bodyModel.stringPatternAZaz = 'aBcD'; @@ -238,6 +240,8 @@ describe('Koa Server (with noImplicitAdditionalProperties turned on)', () => { numberMax10: 10, numberMin5: 5, + numberExclusiveMin5: 6, + numberExclusiveMax10: 9, stringMax10Lenght: 'abcdef', stringMin5Lenght: 'abcdef', stringPatternAZaz: 'aBcD', @@ -291,6 +295,8 @@ describe('Koa Server (with noImplicitAdditionalProperties turned on)', () => { expect(body.numberMax10).to.equal(bodyModel.numberMax10); expect(body.numberMin5).to.equal(bodyModel.numberMin5); + expect(body.numberExclusiveMin5).to.equal(bodyModel.numberExclusiveMin5); + expect(body.numberExclusiveMax10).to.equal(bodyModel.numberExclusiveMax10); expect(body.stringMax10Lenght).to.equal(bodyModel.stringMax10Lenght); expect(body.stringMin5Lenght).to.equal(bodyModel.stringMin5Lenght); expect(body.stringPatternAZaz).to.equal(bodyModel.stringPatternAZaz); @@ -316,6 +322,8 @@ describe('Koa Server (with noImplicitAdditionalProperties turned on)', () => { expect(body.nestedObject.numberMax10).to.equal(bodyModel.nestedObject.numberMax10); expect(body.nestedObject.numberMin5).to.equal(bodyModel.nestedObject.numberMin5); + expect(body.nestedObject.numberExclusiveMin5).to.equal(bodyModel.nestedObject.numberExclusiveMin5); + expect(body.nestedObject.numberExclusiveMax10).to.equal(bodyModel.nestedObject.numberExclusiveMax10); expect(body.nestedObject.stringMax10Lenght).to.equal(bodyModel.nestedObject.stringMax10Lenght); expect(body.nestedObject.stringMin5Lenght).to.equal(bodyModel.nestedObject.stringMin5Lenght); expect(body.nestedObject.stringPatternAZaz).to.equal(bodyModel.nestedObject.stringPatternAZaz); diff --git a/tests/integration/koa-server.spec.ts b/tests/integration/koa-server.spec.ts index 25eca0aeb..7a9a05f99 100644 --- a/tests/integration/koa-server.spec.ts +++ b/tests/integration/koa-server.spec.ts @@ -591,6 +591,8 @@ describe('Koa Server', () => { bodyModel.numberMax10 = 10; bodyModel.numberMin5 = 5; + bodyModel.numberExclusiveMin5 = 6; + bodyModel.numberExclusiveMax10 = 9; bodyModel.stringMax10Lenght = 'abcdef'; bodyModel.stringMin5Lenght = 'abcdef'; bodyModel.stringPatternAZaz = 'aBcD'; @@ -616,6 +618,8 @@ describe('Koa Server', () => { numberMax10: 10, numberMin5: 5, + numberExclusiveMin5: 6, + numberExclusiveMax10: 9, stringMax10Lenght: 'abcdef', stringMin5Lenght: 'abcdef', stringPatternAZaz: 'aBcD', @@ -669,6 +673,8 @@ describe('Koa Server', () => { expect(body.numberMax10).to.equal(bodyModel.numberMax10); expect(body.numberMin5).to.equal(bodyModel.numberMin5); + expect(body.numberExclusiveMin5).to.equal(bodyModel.numberExclusiveMin5); + expect(body.numberExclusiveMax10).to.equal(bodyModel.numberExclusiveMax10); expect(body.stringMax10Lenght).to.equal(bodyModel.stringMax10Lenght); expect(body.stringMin5Lenght).to.equal(bodyModel.stringMin5Lenght); expect(body.stringPatternAZaz).to.equal(bodyModel.stringPatternAZaz); @@ -694,6 +700,8 @@ describe('Koa Server', () => { expect(body.nestedObject.numberMax10).to.equal(bodyModel.nestedObject.numberMax10); expect(body.nestedObject.numberMin5).to.equal(bodyModel.nestedObject.numberMin5); + expect(body.nestedObject.numberExclusiveMin5).to.equal(bodyModel.nestedObject.numberExclusiveMin5); + expect(body.nestedObject.numberExclusiveMax10).to.equal(bodyModel.nestedObject.numberExclusiveMax10); expect(body.nestedObject.stringMax10Lenght).to.equal(bodyModel.nestedObject.stringMax10Lenght); expect(body.nestedObject.stringMin5Lenght).to.equal(bodyModel.nestedObject.stringMin5Lenght); expect(body.nestedObject.stringPatternAZaz).to.equal(bodyModel.nestedObject.stringPatternAZaz); @@ -729,6 +737,8 @@ describe('Koa Server', () => { bodyModel.numberMax10 = 20; bodyModel.numberMin5 = 0; + bodyModel.numberExclusiveMin5 = 5; + bodyModel.numberExclusiveMax10 = 10; bodyModel.stringMax10Lenght = 'abcdefghijk'; bodyModel.stringMin5Lenght = 'abcd'; bodyModel.stringPatternAZaz = 'ab01234'; @@ -753,6 +763,8 @@ describe('Koa Server', () => { numberMax10: 20, numberMin5: 0, + numberExclusiveMin5: 5, + numberExclusiveMax10: 10, stringMax10Lenght: 'abcdefghijk', stringMin5Lenght: 'abcd', stringPatternAZaz: 'ab01234', @@ -813,6 +825,10 @@ describe('Koa Server', () => { expect(body.fields['body.numberMax10'].value).to.be.undefined; expect(body.fields['body.numberMin5'].message).to.equal('min 5'); expect(body.fields['body.numberMin5'].value).to.be.undefined; + expect(body.fields['body.numberExclusiveMin5'].message).to.equal('exclusiveMin 5'); + expect(body.fields['body.numberExclusiveMin5'].value).to.be.undefined; + expect(body.fields['body.numberExclusiveMax10'].message).to.equal('exclusiveMax 10'); + expect(body.fields['body.numberExclusiveMax10'].value).to.be.undefined; expect(body.fields['body.stringMax10Lenght'].message).to.equal('maxLength 10'); expect(body.fields['body.stringMax10Lenght'].value).to.be.undefined; expect(body.fields['body.stringMin5Lenght'].message).to.equal('minLength 5'); @@ -856,6 +872,10 @@ describe('Koa Server', () => { expect(body.fields['body.nestedObject.numberMax10'].value).to.be.undefined; expect(body.fields['body.nestedObject.numberMin5'].message).to.equal('min 5'); expect(body.fields['body.nestedObject.numberMin5'].value).to.be.undefined; + expect(body.fields['body.nestedObject.numberExclusiveMin5'].message).to.equal('exclusiveMin 5'); + expect(body.fields['body.nestedObject.numberExclusiveMin5'].value).to.be.undefined; + expect(body.fields['body.nestedObject.numberExclusiveMax10'].message).to.equal('exclusiveMax 10'); + expect(body.fields['body.nestedObject.numberExclusiveMax10'].value).to.be.undefined; expect(body.fields['body.nestedObject.stringMax10Lenght'].message).to.equal('maxLength 10'); expect(body.fields['body.nestedObject.stringMax10Lenght'].value).to.be.undefined; expect(body.fields['body.nestedObject.stringMin5Lenght'].message).to.equal('minLength 5'); diff --git a/tests/integration/openapi3-express.spec.ts b/tests/integration/openapi3-express.spec.ts index 8ca71aab1..59eb11346 100644 --- a/tests/integration/openapi3-express.spec.ts +++ b/tests/integration/openapi3-express.spec.ts @@ -20,6 +20,8 @@ describe('OpenAPI3 Express Server', () => { bodyModel.numberMax10 = 10; bodyModel.numberMin5 = 5; + bodyModel.numberExclusiveMin5 = 6; + bodyModel.numberExclusiveMax10 = 9; bodyModel.stringMax10Lenght = 'abcdef'; bodyModel.stringMin5Lenght = 'abcdef'; bodyModel.stringPatternAZaz = 'aBcD'; @@ -45,6 +47,8 @@ describe('OpenAPI3 Express Server', () => { numberMax10: 10, numberMin5: 5, + numberExclusiveMin5: 6, + numberExclusiveMax10: 9, stringMax10Lenght: 'abcdef', stringMin5Lenght: 'abcdef', stringPatternAZaz: 'aBcD', @@ -103,6 +107,8 @@ describe('OpenAPI3 Express Server', () => { expect(body.numberMax10).to.equal(bodyModel.numberMax10); expect(body.numberMin5).to.equal(bodyModel.numberMin5); + expect(body.numberExclusiveMin5).to.equal(bodyModel.numberExclusiveMin5); + expect(body.numberExclusiveMax10).to.equal(bodyModel.numberExclusiveMax10); expect(body.stringMax10Lenght).to.equal(bodyModel.stringMax10Lenght); expect(body.stringMin5Lenght).to.equal(bodyModel.stringMin5Lenght); expect(body.stringPatternAZaz).to.equal(bodyModel.stringPatternAZaz); @@ -128,6 +134,8 @@ describe('OpenAPI3 Express Server', () => { expect(body.nestedObject.numberMax10).to.equal(bodyModel.nestedObject.numberMax10); expect(body.nestedObject.numberMin5).to.equal(bodyModel.nestedObject.numberMin5); + expect(body.nestedObject.numberExclusiveMin5).to.equal(bodyModel.nestedObject.numberExclusiveMin5); + expect(body.nestedObject.numberExclusiveMax10).to.equal(bodyModel.nestedObject.numberExclusiveMax10); expect(body.nestedObject.stringMax10Lenght).to.equal(bodyModel.nestedObject.stringMax10Lenght); expect(body.nestedObject.stringMin5Lenght).to.equal(bodyModel.nestedObject.stringMin5Lenght); expect(body.nestedObject.stringPatternAZaz).to.equal(bodyModel.nestedObject.stringPatternAZaz); @@ -165,6 +173,8 @@ describe('OpenAPI3 Express Server', () => { bodyModel.numberMax10 = 20; bodyModel.numberMin5 = 0; + bodyModel.numberExclusiveMin5 = 5; + bodyModel.numberExclusiveMax10 = 10; bodyModel.stringMax10Lenght = 'abcdefghijk'; bodyModel.stringMin5Lenght = 'abcd'; bodyModel.stringPatternAZaz = 'ab01234'; @@ -191,6 +201,8 @@ describe('OpenAPI3 Express Server', () => { numberMax10: 20, numberMin5: 0, + numberExclusiveMin5: 5, + numberExclusiveMax10: 10, stringMax10Lenght: 'abcdefghijk', stringMin5Lenght: 'abcd', stringPatternAZaz: 'ab01234', @@ -256,6 +268,10 @@ describe('OpenAPI3 Express Server', () => { expect(body.fields['body.numberMax10'].value).to.be.undefined; expect(body.fields['body.numberMin5'].message).to.equal('min 5'); expect(body.fields['body.numberMin5'].value).to.be.undefined; + expect(body.fields['body.numberExclusiveMin5'].message).to.equal('exclusiveMin 5'); + expect(body.fields['body.numberExclusiveMin5'].value).to.be.undefined; + expect(body.fields['body.numberExclusiveMax10'].message).to.equal('exclusiveMax 10'); + expect(body.fields['body.numberExclusiveMax10'].value).to.be.undefined; expect(body.fields['body.stringMax10Lenght'].message).to.equal('maxLength 10'); expect(body.fields['body.stringMax10Lenght'].value).to.be.undefined; expect(body.fields['body.stringMin5Lenght'].message).to.equal('minLength 5'); @@ -303,6 +319,10 @@ describe('OpenAPI3 Express Server', () => { expect(body.fields['body.nestedObject.numberMax10'].value).to.be.undefined; expect(body.fields['body.nestedObject.numberMin5'].message).to.equal('min 5'); expect(body.fields['body.nestedObject.numberMin5'].value).to.be.undefined; + expect(body.fields['body.nestedObject.numberExclusiveMin5'].message).to.equal('exclusiveMin 5'); + expect(body.fields['body.nestedObject.numberExclusiveMin5'].value).to.be.undefined; + expect(body.fields['body.nestedObject.numberExclusiveMax10'].message).to.equal('exclusiveMax 10'); + expect(body.fields['body.nestedObject.numberExclusiveMax10'].value).to.be.undefined; expect(body.fields['body.nestedObject.stringMax10Lenght'].message).to.equal('maxLength 10'); expect(body.fields['body.nestedObject.stringMax10Lenght'].value).to.be.undefined; expect(body.fields['body.nestedObject.stringMin5Lenght'].message).to.equal('minLength 5'); diff --git a/tests/unit/swagger/definitionsGeneration/definitions.spec.ts b/tests/unit/swagger/definitionsGeneration/definitions.spec.ts index d2daa03fb..5d3e3e288 100644 --- a/tests/unit/swagger/definitionsGeneration/definitions.spec.ts +++ b/tests/unit/swagger/definitionsGeneration/definitions.spec.ts @@ -3691,4 +3691,39 @@ describe('Definition generation', () => { }); }); }); + + describe('ValidateModel schema should include exclusiveMinimum and exclusiveMaximum (Swagger 2.0 boolean form)', () => { + const validateMetadata = new MetadataGenerator('./fixtures/controllers/validateController.ts').Generate(); + const validateSpec = new SpecGenerator2(validateMetadata, defaultOptions).GetSpec(); + + it('should emit minimum + exclusiveMinimum:true for @exclusiveMinimum without @minimum', () => { + const schema = validateSpec.definitions!['ValidateModel']; + const properties = (schema as any).properties; + expect(properties).to.have.property('numberExclusiveMin5'); + expect(properties.numberExclusiveMin5).to.have.property('minimum', 5); + expect(properties.numberExclusiveMin5).to.have.property('exclusiveMinimum', true); + }); + + it('should emit maximum + exclusiveMaximum:true for @exclusiveMaximum without @maximum', () => { + const schema = validateSpec.definitions!['ValidateModel']; + const properties = (schema as any).properties; + expect(properties).to.have.property('numberExclusiveMax10'); + expect(properties.numberExclusiveMax10).to.have.property('maximum', 10); + expect(properties.numberExclusiveMax10).to.have.property('exclusiveMaximum', true); + }); + + it('should throw when both @minimum and @exclusiveMinimum are present', () => { + const generator = new SpecGenerator2(validateMetadata, defaultOptions); + const validators = { minimum: { value: 3 }, exclusiveMinimum: { value: 5 } }; + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + expect(() => (generator as any).transformValidatorsForSchema(validators)).to.throw(/Cannot use both @minimum and @exclusiveMinimum/); + }); + + it('should throw when both @maximum and @exclusiveMaximum are present', () => { + const generator = new SpecGenerator2(validateMetadata, defaultOptions); + const validators = { maximum: { value: 12 }, exclusiveMaximum: { value: 10 } }; + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + expect(() => (generator as any).transformValidatorsForSchema(validators)).to.throw(/Cannot use both @maximum and @exclusiveMaximum/); + }); + }); }); diff --git a/tests/unit/swagger/schemaDetails3.spec.ts b/tests/unit/swagger/schemaDetails3.spec.ts index 61d755359..a03922489 100644 --- a/tests/unit/swagger/schemaDetails3.spec.ts +++ b/tests/unit/swagger/schemaDetails3.spec.ts @@ -4889,6 +4889,44 @@ describe('Definition generation for OpenAPI 3.0.0', () => { }); }); + describe('ValidateModel schema should include exclusiveMinimum and exclusiveMaximum (OAS 3.0 boolean form)', () => { + const validateMetadata = new MetadataGenerator('./fixtures/controllers/validateController.ts').Generate(); + const validateSpec: SpecAndName = { + spec: new SpecGenerator3(validateMetadata, getDefaultExtendedOptions()).GetSpec(), + specName: 'specDefault', + }; + + it('should emit minimum + exclusiveMinimum:true for @exclusiveMinimum without @minimum', () => { + const schema = getComponentSchema('ValidateModel', validateSpec); + const properties = (schema as any).properties; + expect(properties).to.have.property('numberExclusiveMin5'); + expect(properties.numberExclusiveMin5).to.have.property('minimum', 5); + expect(properties.numberExclusiveMin5).to.have.property('exclusiveMinimum', true); + }); + + it('should emit maximum + exclusiveMaximum:true for @exclusiveMaximum without @maximum', () => { + const schema = getComponentSchema('ValidateModel', validateSpec); + const properties = (schema as any).properties; + expect(properties).to.have.property('numberExclusiveMax10'); + expect(properties.numberExclusiveMax10).to.have.property('maximum', 10); + expect(properties.numberExclusiveMax10).to.have.property('exclusiveMaximum', true); + }); + + it('should throw when both @minimum and @exclusiveMinimum are present', () => { + const generator = new SpecGenerator3(validateMetadata, getDefaultExtendedOptions()); + const validators = { minimum: { value: 3 }, exclusiveMinimum: { value: 5 } }; + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + expect(() => (generator as any).transformValidatorsForSchema(validators)).to.throw(/Cannot use both @minimum and @exclusiveMinimum/); + }); + + it('should throw when both @maximum and @exclusiveMaximum are present', () => { + const generator = new SpecGenerator3(validateMetadata, getDefaultExtendedOptions()); + const validators = { maximum: { value: 12 }, exclusiveMaximum: { value: 10 } }; + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + expect(() => (generator as any).transformValidatorsForSchema(validators)).to.throw(/Cannot use both @maximum and @exclusiveMaximum/); + }); + }); + describe('should exclude @RequestProp', () => { it('should exclude request-prop from method parameters', () => { const metadata = new MetadataGenerator('./fixtures/controllers/parameterController.ts').Generate(); diff --git a/tests/unit/swagger/schemaDetails31.spec.ts b/tests/unit/swagger/schemaDetails31.spec.ts index 5806afb1e..0aaef17ed 100644 --- a/tests/unit/swagger/schemaDetails31.spec.ts +++ b/tests/unit/swagger/schemaDetails31.spec.ts @@ -4919,4 +4919,46 @@ describe('Definition generation for OpenAPI 3.1.0', () => { expect(variadicItems).to.deep.include({ type: 'number' }); }); }); + + describe('ValidateModel schema should include exclusiveMinimum and exclusiveMaximum (OAS 3.1 numeric form)', () => { + const validateMetadata = new MetadataGenerator('./fixtures/controllers/validateController.ts').Generate(); + const validateSpec: SpecAndName = { + spec: new SpecGenerator31(validateMetadata, getDefaultExtendedOptions()).GetSpec(), + specName: 'specDefault', + }; + + it('should emit exclusiveMinimum as a number, not a boolean', () => { + const schema = getComponentSchema('ValidateModel', validateSpec); + const properties = (schema as any).properties; + expect(properties).to.have.property('numberExclusiveMin5'); + expect(properties.numberExclusiveMin5).to.have.property('exclusiveMinimum', 5); + expect(properties.numberExclusiveMin5.exclusiveMinimum).to.be.a('number'); + }); + + it('should emit exclusiveMaximum as a number, not a boolean', () => { + const schema = getComponentSchema('ValidateModel', validateSpec); + const properties = (schema as any).properties; + expect(properties).to.have.property('numberExclusiveMax10'); + expect(properties.numberExclusiveMax10).to.have.property('exclusiveMaximum', 10); + expect(properties.numberExclusiveMax10.exclusiveMaximum).to.be.a('number'); + }); + + it('should allow both minimum and exclusiveMinimum as numbers when both are present', () => { + const generator = new SpecGenerator31(validateMetadata, getDefaultExtendedOptions()); + const validators = { minimum: { value: 3 }, exclusiveMinimum: { value: 5 } }; + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + const result = (generator as any).transformValidatorsForSchema(validators) as Record; + expect(result).to.have.property('minimum', 3); + expect(result).to.have.property('exclusiveMinimum', 5); + }); + + it('should allow both maximum and exclusiveMaximum as numbers when both are present', () => { + const generator = new SpecGenerator31(validateMetadata, getDefaultExtendedOptions()); + const validators = { maximum: { value: 12 }, exclusiveMaximum: { value: 10 } }; + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + const result = (generator as any).transformValidatorsForSchema(validators) as Record; + expect(result).to.have.property('maximum', 12); + expect(result).to.have.property('exclusiveMaximum', 10); + }); + }); }); diff --git a/tests/unit/swagger/templateHelpers.spec.ts b/tests/unit/swagger/templateHelpers.spec.ts index 9a8d1bdf8..98d34254b 100644 --- a/tests/unit/swagger/templateHelpers.spec.ts +++ b/tests/unit/swagger/templateHelpers.spec.ts @@ -447,6 +447,56 @@ describe('ValidationService', () => { expect(error[name].message).to.equal('invalid integer number'); expect(error[name].value).to.equal('10'); }); + + it('should validate exclusiveMinimum for integers', () => { + const name = 'name'; + const value: any = 6; + const error: FieldErrors = {}; + const validator = { exclusiveMinimum: { value: 5 } }; + const result = new ValidationService( + {}, + { noImplicitAdditionalProperties: 'ignore', bodyCoercion: true }, + ).validateInt(name, value, error, true, validator); + expect(result).to.equal(6); + expect(Object.keys(error)).to.be.empty; + }); + + it('should reject value equal to exclusiveMinimum for integers', () => { + const name = 'name'; + const value: any = 5; + const error: FieldErrors = {}; + const validator = { exclusiveMinimum: { value: 5 } }; + new ValidationService( + {}, + { noImplicitAdditionalProperties: 'ignore', bodyCoercion: true }, + ).validateInt(name, value, error, true, validator); + expect(error[name].message).to.equal('exclusiveMin 5'); + }); + + it('should validate exclusiveMaximum for integers', () => { + const name = 'name'; + const value: any = 9; + const error: FieldErrors = {}; + const validator = { exclusiveMaximum: { value: 10 } }; + const result = new ValidationService( + {}, + { noImplicitAdditionalProperties: 'ignore', bodyCoercion: true }, + ).validateInt(name, value, error, true, validator); + expect(result).to.equal(9); + expect(Object.keys(error)).to.be.empty; + }); + + it('should reject value equal to exclusiveMaximum for integers', () => { + const name = 'name'; + const value: any = 10; + const error: FieldErrors = {}; + const validator = { exclusiveMaximum: { value: 10 } }; + new ValidationService( + {}, + { noImplicitAdditionalProperties: 'ignore', bodyCoercion: true }, + ).validateInt(name, value, error, true, validator); + expect(error[name].message).to.equal('exclusiveMax 10'); + }); }); describe('Float validate', () => { @@ -533,6 +583,56 @@ describe('ValidationService', () => { expect(error[name].message).to.equal('invalid float number'); expect(error[name].value).to.equal('10.1'); }); + + it('should validate exclusiveMinimum for floats', () => { + const name = 'name'; + const value: any = 5.5; + const error: FieldErrors = {}; + const validator = { exclusiveMinimum: { value: 5 } }; + const result = new ValidationService( + {}, + { noImplicitAdditionalProperties: 'ignore', bodyCoercion: true }, + ).validateFloat(name, value, error, true, validator); + expect(result).to.equal(5.5); + expect(Object.keys(error)).to.be.empty; + }); + + it('should reject value equal to exclusiveMinimum for floats', () => { + const name = 'name'; + const value: any = 5; + const error: FieldErrors = {}; + const validator = { exclusiveMinimum: { value: 5 } }; + new ValidationService( + {}, + { noImplicitAdditionalProperties: 'ignore', bodyCoercion: true }, + ).validateFloat(name, value, error, true, validator); + expect(error[name].message).to.equal('exclusiveMin 5'); + }); + + it('should validate exclusiveMaximum for floats', () => { + const name = 'name'; + const value: any = 9.5; + const error: FieldErrors = {}; + const validator = { exclusiveMaximum: { value: 10 } }; + const result = new ValidationService( + {}, + { noImplicitAdditionalProperties: 'ignore', bodyCoercion: true }, + ).validateFloat(name, value, error, true, validator); + expect(result).to.equal(9.5); + expect(Object.keys(error)).to.be.empty; + }); + + it('should reject value equal to exclusiveMaximum for floats', () => { + const name = 'name'; + const value: any = 10; + const error: FieldErrors = {}; + const validator = { exclusiveMaximum: { value: 10 } }; + new ValidationService( + {}, + { noImplicitAdditionalProperties: 'ignore', bodyCoercion: true }, + ).validateFloat(name, value, error, true, validator); + expect(error[name].message).to.equal('exclusiveMax 10'); + }); }); describe('Boolean validate', () => {