From 978627c382163d084daae329fd66440f87f23f43 Mon Sep 17 00:00:00 2001 From: WoH Date: Wed, 14 Aug 2019 10:51:59 +0200 Subject: [PATCH 1/9] ObjectTypeLiteral support --- src/metadataGeneration/tsoa.ts | 8 ++++- src/metadataGeneration/typeResolver.ts | 17 ++++++++++- src/swagger/specGenerator.ts | 20 +++++++++++++ tests/fixtures/controllers/getController.ts | 3 ++ tests/fixtures/inversify/managedService.ts | 3 ++ tests/fixtures/services/modelService.ts | 3 ++ tests/fixtures/testModel.ts | 18 +++++++---- ...dynamic-controllers-express-server.spec.ts | 3 ++ tests/integration/express-server.spec.ts | 3 ++ tests/integration/hapi-server.spec.ts | 3 ++ tests/integration/inversify-server.spec.ts | 3 ++ .../koa-server-no-additional-allowed.spec.ts | 3 ++ tests/integration/koa-server.spec.ts | 3 ++ .../definitionsGeneration/definitions.spec.ts | 28 +++++++++++++++++ tests/unit/swagger/schemaDetails3.spec.ts | 30 +++++++++++++++++++ 15 files changed, 140 insertions(+), 8 deletions(-) diff --git a/src/metadataGeneration/tsoa.ts b/src/metadataGeneration/tsoa.ts index a9fbb8c60..07ee7ac2c 100644 --- a/src/metadataGeneration/tsoa.ts +++ b/src/metadataGeneration/tsoa.ts @@ -87,12 +87,13 @@ export namespace Tsoa { | 'any' | 'refEnum' | 'refObject' + | 'objectLiteral' | 'union' | 'intersection'; export type RefTypeLiteral = 'refObject' | 'refEnum'; - export type PrimitiveTypeLiteral = Exclude; + export type PrimitiveTypeLiteral = Exclude; export interface Type { dataType: TypeStringLiteral; @@ -107,6 +108,11 @@ export namespace Tsoa { enums: string[]; } + export interface ObjectLiteralType extends Type { + dataType: 'objectLiteral'; + properties: Property[]; + } + export interface ArrayType extends Type { dataType: 'array'; elementType: Type; diff --git a/src/metadataGeneration/typeResolver.ts b/src/metadataGeneration/typeResolver.ts index 6c0d22322..a41c6b959 100644 --- a/src/metadataGeneration/typeResolver.ts +++ b/src/metadataGeneration/typeResolver.ts @@ -90,7 +90,22 @@ export class TypeResolver { } if (this.typeNode.kind === ts.SyntaxKind.TypeLiteral) { - return { dataType: 'any' } as Tsoa.Type; + const properties = (this.typeNode as ts.TypeLiteralNode).members + .filter(member => member.kind === ts.SyntaxKind.PropertySignature) + .reduce((res, propertySignature: ts.PropertySignature) => { + const type = new TypeResolver(propertySignature.type as ts.TypeNode, this.current, this.typeNode).resolve(); + + return [ + { + name: (propertySignature.name as ts.Identifier).text, + required: !propertySignature.questionToken, + type, + } as Tsoa.Property, + ...res, + ]; + }, []); + + return { dataType: 'objectLiteral', properties } as Tsoa.ObjectLiteralType; } if (this.typeNode.kind === ts.SyntaxKind.ObjectKeyword) { diff --git a/src/swagger/specGenerator.ts b/src/swagger/specGenerator.ts index 9fbfdbe62..9484d1705 100644 --- a/src/swagger/specGenerator.ts +++ b/src/swagger/specGenerator.ts @@ -79,6 +79,8 @@ export abstract class SpecGenerator { return this.getSwaggerTypeForUnionType(type as Tsoa.UnionType); } else if (type.dataType === 'intersection') { return this.getSwaggerTypeForIntersectionType(type as Tsoa.IntersectionType); + } else if (type.dataType === 'objectLiteral') { + return this.getSwaggerTypeForObjectLiteral(type as Tsoa.ObjectLiteralType); } else { return assertNever(type.dataType); } @@ -87,6 +89,24 @@ export abstract class SpecGenerator { protected abstract getSwaggerTypeForUnionType(type: Tsoa.UnionType); protected abstract getSwaggerTypeForIntersectionType(type: Tsoa.IntersectionType); + public getSwaggerTypeForObjectLiteral(objectLiteral: Tsoa.ObjectLiteralType): Swagger.Schema { + const properties = objectLiteral.properties.reduce((acc, property: Tsoa.Property) => { + return { + [property.name]: this.getSwaggerType(property.type), + ...acc, + }; + }, {}); + + const required = objectLiteral.properties.filter(prop => prop.required).map(prop => prop.name); + + // An empty list required: [] is not valid. + // If all properties are optional, do not specify the required keyword. + return { + properties, + ...(required && required.length && { required }), + type: 'object', + }; + } protected getSwaggerTypeForReferenceType(referenceType: Tsoa.ReferenceType): Swagger.BaseSchema { return { diff --git a/tests/fixtures/controllers/getController.ts b/tests/fixtures/controllers/getController.ts index 85f092611..dd38ab385 100644 --- a/tests/fixtures/controllers/getController.ts +++ b/tests/fixtures/controllers/getController.ts @@ -24,6 +24,9 @@ export class GetTestController extends Controller { modelsArray: new Array(), numberArray: [1, 2, 3], numberValue: 1, + objLiteral: { + name: 'a string', + }, object: { a: 'a', }, diff --git a/tests/fixtures/inversify/managedService.ts b/tests/fixtures/inversify/managedService.ts index 77ffb99fa..fcd13ad01 100644 --- a/tests/fixtures/inversify/managedService.ts +++ b/tests/fixtures/inversify/managedService.ts @@ -16,6 +16,9 @@ export class ManagedService { modelsArray: new Array(), numberArray: [1, 2, 3], numberValue: 1, + objLiteral: { + name: 'hello', + }, object: { a: 'a', }, diff --git a/tests/fixtures/services/modelService.ts b/tests/fixtures/services/modelService.ts index 2ae18c16a..3cb1eae8f 100644 --- a/tests/fixtures/services/modelService.ts +++ b/tests/fixtures/services/modelService.ts @@ -14,6 +14,9 @@ export class ModelService { modelsArray: new Array(), numberArray: [1, 2, 3], numberValue: 1, + objLiteral: { + name: 'hello', + }, object: { a: 'a', }, diff --git a/tests/fixtures/testModel.ts b/tests/fixtures/testModel.ts index 9c9c0c93d..0d826a228 100644 --- a/tests/fixtures/testModel.ts +++ b/tests/fixtures/testModel.ts @@ -75,6 +75,18 @@ export interface TestModel extends Model { genericNestedArrayKeyword2?: GenericRequest>; genericNestedArrayCharacter2?: GenericRequest; mixedUnion?: string | TypeAliasModel1; + + objLiteral: { + name: string; + nested?: { + bool: boolean; + optional?: number; + allOptional: { + one?: string; + two?: string; + }; + }; + }; } export interface TypeAliasModel1 { @@ -333,12 +345,6 @@ export class TestClassModel extends TestClassBaseModel { stringProperty: string; protected protectedStringProperty: string; - public static typeLiterals = { - booleanTypeLiteral: { $type: Boolean }, - numberTypeLiteral: { $type: Number }, - stringTypeLiteral: { $type: String }, - }; - /** * @param publicConstructorVar This is a description for publicConstructorVar */ diff --git a/tests/integration/dynamic-controllers-express-server.spec.ts b/tests/integration/dynamic-controllers-express-server.spec.ts index 84657a0cd..eb77b3726 100644 --- a/tests/integration/dynamic-controllers-express-server.spec.ts +++ b/tests/integration/dynamic-controllers-express-server.spec.ts @@ -937,6 +937,9 @@ describe('Express Server', () => { }, numberArray: [1, 2], numberValue: 5, + objLiteral: { + name: 'hello', + }, object: { foo: 'bar' }, objectArray: [{ foo1: 'bar1' }, { foo2: 'bar2' }], optionalString: 'test1234', diff --git a/tests/integration/express-server.spec.ts b/tests/integration/express-server.spec.ts index e3219e853..3601bdf3d 100644 --- a/tests/integration/express-server.spec.ts +++ b/tests/integration/express-server.spec.ts @@ -937,6 +937,9 @@ describe('Express Server', () => { }, numberArray: [1, 2], numberValue: 5, + objLiteral: { + name: 'hello', + }, object: { foo: 'bar' }, objectArray: [{ foo1: 'bar1' }, { foo2: 'bar2' }], optionalString: 'test1234', diff --git a/tests/integration/hapi-server.spec.ts b/tests/integration/hapi-server.spec.ts index 25e409e99..9b99bc07d 100644 --- a/tests/integration/hapi-server.spec.ts +++ b/tests/integration/hapi-server.spec.ts @@ -866,6 +866,9 @@ describe('Hapi Server', () => { }, numberArray: [1, 2], numberValue: 5, + objLiteral: { + name: 'hello', + }, object: { foo: 'bar' }, objectArray: [{ foo1: 'bar1' }, { foo2: 'bar2' }], optionalString: 'test1234', diff --git a/tests/integration/inversify-server.spec.ts b/tests/integration/inversify-server.spec.ts index 3360559ad..38b0f5b8b 100644 --- a/tests/integration/inversify-server.spec.ts +++ b/tests/integration/inversify-server.spec.ts @@ -41,6 +41,9 @@ describe('Inversify Express Server', () => { }, numberArray: [1, 2, 3], numberValue: 1, + objLiteral: { + name: 'hello', + }, object: { a: 'a', }, diff --git a/tests/integration/koa-server-no-additional-allowed.spec.ts b/tests/integration/koa-server-no-additional-allowed.spec.ts index 4aeecde58..43771871a 100644 --- a/tests/integration/koa-server-no-additional-allowed.spec.ts +++ b/tests/integration/koa-server-no-additional-allowed.spec.ts @@ -355,6 +355,9 @@ describe('Koa Server (with noImplicitAdditionalProperties turned on)', () => { modelsArray: [{ email: 'test@test.com', id: 1 }], numberArray: [1, 2], numberValue: 5, + objLiteral: { + name: 'hello', + }, object: { foo: 'bar' }, objectArray: [{ foo1: 'bar1' }, { foo2: 'bar2' }], optionalString: 'test1234', diff --git a/tests/integration/koa-server.spec.ts b/tests/integration/koa-server.spec.ts index 9ca9a309b..5947df7b0 100644 --- a/tests/integration/koa-server.spec.ts +++ b/tests/integration/koa-server.spec.ts @@ -851,6 +851,9 @@ describe('Koa Server', () => { modelsArray: [{ email: 'test@test.com', id: 1 }], numberArray: [1, 2], numberValue: 5, + objLiteral: { + name: 'hello', + }, object: { foo: 'bar' }, objectArray: [{ foo1: 'bar1' }, { foo2: 'bar2' }], optionalString: 'test1234', diff --git a/tests/unit/swagger/definitionsGeneration/definitions.spec.ts b/tests/unit/swagger/definitionsGeneration/definitions.spec.ts index 7cedc6c71..20c7b4b88 100644 --- a/tests/unit/swagger/definitionsGeneration/definitions.spec.ts +++ b/tests/unit/swagger/definitionsGeneration/definitions.spec.ts @@ -422,6 +422,29 @@ describe('Definition generation', () => { expect(propertySchema.type).to.eq('object', `for property ${propertyName}`); expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); }, + objLiteral: (propertyName, propertySchema) => { + expect(propertySchema).to.deep.include({ + properties: { + name: { + type: 'string', + }, + nested: { + properties: { + allOptional: { + properties: { one: { type: 'string' }, two: { type: 'string' } }, + type: 'object', + }, + bool: { type: 'boolean' }, + optional: { format: 'double', type: 'number' }, + }, + required: ['allOptional', 'bool'], + type: 'object', + }, + }, + required: ['name'], + type: 'object', + }); + }, }; Object.keys(assertionsPerProperty).forEach(aPropertyName => { @@ -438,6 +461,11 @@ describe('Definition generation', () => { Object.keys(definition.properties!).length, `because the swagger spec (${currentSpec.specName}) should only produce property schemas for properties that live on the TypeScript interface.`, ); + + expect(Object.keys(assertionsPerProperty)).to.length( + Object.keys(definition.properties!).length, + `because the swagger spec (${currentSpec.specName}) should only produce property schemas for properties that live on the TypeScript interface.`, + ); }); }); diff --git a/tests/unit/swagger/schemaDetails3.spec.ts b/tests/unit/swagger/schemaDetails3.spec.ts index f481cde01..2e85b885e 100644 --- a/tests/unit/swagger/schemaDetails3.spec.ts +++ b/tests/unit/swagger/schemaDetails3.spec.ts @@ -214,6 +214,29 @@ describe('Definition generation for OpenAPI 3.0.0', () => { expect(propertySchema.description).to.eq(undefined, `for property ${propertyName}.description`); expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); }, + objLiteral: (propertyName, propertySchema) => { + expect(propertySchema).to.deep.include({ + properties: { + name: { + type: 'string', + }, + nested: { + properties: { + allOptional: { + properties: { one: { type: 'string' }, two: { type: 'string' } }, + type: 'object', + }, + bool: { type: 'boolean' }, + optional: { format: 'double', type: 'number' }, + }, + required: ['allOptional', 'bool'], + type: 'object', + }, + }, + required: ['name'], + type: 'object', + }); + }, object: (propertyName, propertySchema) => { expect(propertySchema.type).to.eq('object', `for property ${propertyName}`); if (currentSpec.specName === 'specWithNoImplicitExtras') { @@ -481,6 +504,13 @@ describe('Definition generation for OpenAPI 3.0.0', () => { `because the swagger spec (${currentSpec.specName}) should only produce property schemas for properties that live on the TypeScript interface.`, ); }); + + it('should have only created schemas for properties on the TypeScript interface', () => { + expect(Object.keys(assertionsPerProperty)).to.length( + Object.keys(testModel.properties!).length, + `because the swagger spec (${currentSpec.specName}) should only produce property schemas for properties that live on the TypeScript interface.`, + ); + }); }); }); }); From d1fab03fb426134af641b147848791c566a691fb Mon Sep 17 00:00:00 2001 From: WoH Date: Sat, 17 Aug 2019 23:11:56 +0200 Subject: [PATCH 2/9] nOL: Validation --- src/metadataGeneration/tsoa.ts | 7 +-- src/metadataGeneration/typeResolver.ts | 32 +++++++++++-- src/routeGeneration/routeGenerator.ts | 10 ++++ src/routeGeneration/templateHelpers.ts | 48 +++++++++++++++++++ src/routeGeneration/tsoa-route.ts | 2 + src/swagger/specGenerator.ts | 2 +- src/utils/validatorUtils.ts | 2 +- tests/fixtures/testModel.ts | 5 +- tests/integration/express-server.spec.ts | 20 ++++++++ .../koa-server-no-additional-allowed.spec.ts | 45 +++++++++++++++++ .../definitionsGeneration/definitions.spec.ts | 17 +++---- tests/unit/swagger/schemaDetails3.spec.ts | 15 +++--- 12 files changed, 177 insertions(+), 28 deletions(-) diff --git a/src/metadataGeneration/tsoa.ts b/src/metadataGeneration/tsoa.ts index 07ee7ac2c..31c90c569 100644 --- a/src/metadataGeneration/tsoa.ts +++ b/src/metadataGeneration/tsoa.ts @@ -87,13 +87,13 @@ export namespace Tsoa { | 'any' | 'refEnum' | 'refObject' - | 'objectLiteral' + | 'nestedObjectLiteral' | 'union' | 'intersection'; export type RefTypeLiteral = 'refObject' | 'refEnum'; - export type PrimitiveTypeLiteral = Exclude; + export type PrimitiveTypeLiteral = Exclude; export interface Type { dataType: TypeStringLiteral; @@ -109,8 +109,9 @@ export namespace Tsoa { } export interface ObjectLiteralType extends Type { - dataType: 'objectLiteral'; + dataType: 'nestedObjectLiteral'; properties: Property[]; + additionalProperties?: Type; } export interface ArrayType extends Type { diff --git a/src/metadataGeneration/typeResolver.ts b/src/metadataGeneration/typeResolver.ts index a41c6b959..01c5cd020 100644 --- a/src/metadataGeneration/typeResolver.ts +++ b/src/metadataGeneration/typeResolver.ts @@ -17,7 +17,7 @@ syntaxKindMap[ts.SyntaxKind.VoidKeyword] = 'void'; const localReferenceTypeCache: { [typeName: string]: Tsoa.ReferenceType } = {}; const inProgressTypes: { [typeName: string]: boolean } = {}; -type UsableDeclaration = ts.InterfaceDeclaration | ts.ClassDeclaration | ts.TypeAliasDeclaration; +type UsableDeclaration = ts.InterfaceDeclaration | ts.ClassDeclaration | ts.TypeAliasDeclaration | ts.PropertySignature; export class TypeResolver { constructor(private readonly typeNode: ts.TypeNode, private readonly current: MetadataGenerator, private readonly parentNode?: ts.Node, private readonly extractEnum = true) {} @@ -90,22 +90,44 @@ export class TypeResolver { } if (this.typeNode.kind === ts.SyntaxKind.TypeLiteral) { - const properties = (this.typeNode as ts.TypeLiteralNode).members + const typeLiteralNode = this.typeNode as ts.TypeLiteralNode; + const properties = typeLiteralNode.members .filter(member => member.kind === ts.SyntaxKind.PropertySignature) .reduce((res, propertySignature: ts.PropertySignature) => { const type = new TypeResolver(propertySignature.type as ts.TypeNode, this.current, this.typeNode).resolve(); return [ { + default: getJSDocComment(propertySignature, 'default'), + description: this.getNodeDescription(propertySignature), + format: this.getNodeFormat(propertySignature), name: (propertySignature.name as ts.Identifier).text, required: !propertySignature.questionToken, type, + validators: getPropertyValidators(propertySignature), } as Tsoa.Property, ...res, ]; }, []); - return { dataType: 'objectLiteral', properties } as Tsoa.ObjectLiteralType; + const indexMember = typeLiteralNode.members.find(member => member.kind === ts.SyntaxKind.IndexSignature); + let additionalType: Tsoa.Type | undefined; + + if (indexMember) { + const indexSignatureDeclaration = indexMember as ts.IndexSignatureDeclaration; + const indexType = new TypeResolver(indexSignatureDeclaration.parameters[0].type as ts.TypeNode, this.current).resolve(); + if (indexType.dataType !== 'string') { + throw new GenerateMetadataError(`Only string indexers are supported.`); + } + + additionalType = new TypeResolver(indexSignatureDeclaration.type as ts.TypeNode, this.current).resolve(); + } + + return { + additionalProperties: indexMember && additionalType, + dataType: 'nestedObjectLiteral', + properties, + } as Tsoa.ObjectLiteralType; } if (this.typeNode.kind === ts.SyntaxKind.ObjectKeyword) { @@ -507,7 +529,7 @@ export class TypeResolver { const modelTypeDeclaration = node as UsableDeclaration; return (modelTypeDeclaration.name as ts.Identifier).text === typeName; - }) as UsableDeclaration[]; + }) as Array>; if (!modelTypes.length) { throw new GenerateMetadataError( @@ -709,7 +731,7 @@ export class TypeResolver { return undefined; } - private getModelInheritedProperties(modelTypeDeclaration: UsableDeclaration): Tsoa.Property[] { + private getModelInheritedProperties(modelTypeDeclaration: Exclude): Tsoa.Property[] { const properties = [] as Tsoa.Property[]; if (modelTypeDeclaration.kind === ts.SyntaxKind.TypeAliasDeclaration) { return []; diff --git a/src/routeGeneration/routeGenerator.ts b/src/routeGeneration/routeGenerator.ts index 9cd3c1504..095e91fd6 100644 --- a/src/routeGeneration/routeGenerator.ts +++ b/src/routeGeneration/routeGenerator.ts @@ -228,6 +228,16 @@ export class RouteGenerator { schema.subSchemas = (type as Tsoa.IntersectionType | Tsoa.UnionType).types.map(type => this.buildProperty(type)); } + if (type.dataType === 'nestedObjectLiteral') { + const objLiteral = type as Tsoa.ObjectLiteralType; + + schema.nestedProperties = objLiteral.properties.reduce((acc, prop) => { + return { ...acc, [prop.name]: this.buildPropertySchema(prop) }; + }, {}); + + schema.additionalProperties = objLiteral.additionalProperties && this.buildProperty(objLiteral.additionalProperties); + } + return schema; } } diff --git a/src/routeGeneration/templateHelpers.ts b/src/routeGeneration/templateHelpers.ts index 7002024e4..4f5b287c2 100644 --- a/src/routeGeneration/templateHelpers.ts +++ b/src/routeGeneration/templateHelpers.ts @@ -71,6 +71,8 @@ export class ValidationService { return this.validateIntersection(name, value, fieldErrors, minimalSwaggerConfig, property.subSchemas, parent); case 'any': return value; + case 'nestedObjectLiteral': + return this.validateNestedObjectLiteral(name, value, fieldErrors, minimalSwaggerConfig, property.nestedProperties, property.additionalProperties, parent); default: if (property.ref) { return this.validateModel({ name, value, refName: property.ref, fieldErrors, parent, minimalSwaggerConfig }); @@ -79,6 +81,52 @@ export class ValidationService { } } + public validateNestedObjectLiteral( + name: string, + value: any, + fieldErrors: FieldErrors, + minimalSwaggerConfig: SwaggerConfigRelatedToRoutes, + nestedProperties: { [name: string]: TsoaRoute.PropertySchema } | undefined, + additionalProperties: TsoaRoute.PropertySchema | boolean | undefined, + parent: string, + ) { + if (!nestedProperties) { + return; + } + + const propHandling = minimalSwaggerConfig.noImplicitAdditionalProperties; + if (propHandling) { + const excessProps = this.getExcessPropertiesFor({ properties: nestedProperties, additionalProperties }, Object.keys(value), minimalSwaggerConfig); + if (excessProps.length > 0) { + if (propHandling) { + excessProps.forEach(excessProp => { + delete value[excessProp]; + }); + if (propHandling === 'throw-on-extras') { + fieldErrors[parent + name] = { + message: `"${excessProps}" is an excess property and therefore is not allowed`, + value: excessProps, + }; + } + } + } + } + + Object.keys(value).forEach(key => { + if (!nestedProperties[key]) { + if (additionalProperties && additionalProperties !== true) { + return this.ValidateParam(additionalProperties, value[key], key, fieldErrors, parent + name + '.', minimalSwaggerConfig); + } else { + return key; + } + } + + return this.ValidateParam(nestedProperties[key], value[key], key, fieldErrors, parent + name + '.', minimalSwaggerConfig); + }); + + return value; + } + public validateInt(name: string, value: any, fieldErrors: FieldErrors, validators?: IntegerValidator, parent = '') { if (!validator.isInt(String(value))) { let message = `invalid integer number`; diff --git a/src/routeGeneration/tsoa-route.ts b/src/routeGeneration/tsoa-route.ts index 8b94986ce..c124c0dfa 100644 --- a/src/routeGeneration/tsoa-route.ts +++ b/src/routeGeneration/tsoa-route.ts @@ -30,6 +30,8 @@ export namespace TsoaRoute { subSchemas?: PropertySchema[]; validators?: ValidatorSchema; default?: any; + additionalProperties?: boolean | PropertySchema; + nestedProperties?: { [name: string]: PropertySchema }; } export interface ParameterSchema extends PropertySchema { diff --git a/src/swagger/specGenerator.ts b/src/swagger/specGenerator.ts index 9484d1705..65175d001 100644 --- a/src/swagger/specGenerator.ts +++ b/src/swagger/specGenerator.ts @@ -79,7 +79,7 @@ export abstract class SpecGenerator { return this.getSwaggerTypeForUnionType(type as Tsoa.UnionType); } else if (type.dataType === 'intersection') { return this.getSwaggerTypeForIntersectionType(type as Tsoa.IntersectionType); - } else if (type.dataType === 'objectLiteral') { + } else if (type.dataType === 'nestedObjectLiteral') { return this.getSwaggerTypeForObjectLiteral(type as Tsoa.ObjectLiteralType); } else { return assertNever(type.dataType); diff --git a/src/utils/validatorUtils.ts b/src/utils/validatorUtils.ts index 2db18fdf7..ecfb0e51f 100644 --- a/src/utils/validatorUtils.ts +++ b/src/utils/validatorUtils.ts @@ -108,7 +108,7 @@ export function getParameterValidators(parameter: ts.ParameterDeclaration, param ); } -export function getPropertyValidators(property: ts.PropertyDeclaration): Tsoa.Validators | undefined { +export function getPropertyValidators(property: ts.PropertyDeclaration | ts.PropertySignature): Tsoa.Validators | undefined { const tags = getJSDocTags(property, tag => { return getParameterTagSupport().some(value => value === tag.tagName.text); }); diff --git a/tests/fixtures/testModel.ts b/tests/fixtures/testModel.ts index 0d826a228..cdc5d70de 100644 --- a/tests/fixtures/testModel.ts +++ b/tests/fixtures/testModel.ts @@ -81,10 +81,13 @@ export interface TestModel extends Model { nested?: { bool: boolean; optional?: number; - allOptional: { + allNestedOptional: { one?: string; two?: string; }; + additionals?: { + [name: string]: TypeAliasModel1; + }; }; }; } diff --git a/tests/integration/express-server.spec.ts b/tests/integration/express-server.spec.ts index 3601bdf3d..3a8ba38f0 100644 --- a/tests/integration/express-server.spec.ts +++ b/tests/integration/express-server.spec.ts @@ -132,6 +132,26 @@ describe('Express Server', () => { }); }); + it('removes additional properties', () => { + const model = getFakeModel(); + const data = { + ...model, + objLiteral: { + ...model.objLiteral, + extra: 123, + nested: { + anotherExtra: 123, + }, + }, + }; + + return verifyPostRequest(basePath + '/PostTest', data, (err: any, res: any) => { + const resModel = res.body as TestModel; + expect(resModel).to.deep.equal({ ...model, objLiteral: { ...model.objLiteral, nested: {} } }); + expect(res.status).to.eq(200); + }); + }); + it('correctly returns status code', () => { const data = getFakeModel(); const path = basePath + '/PostTest/WithDifferentReturnCode'; diff --git a/tests/integration/koa-server-no-additional-allowed.spec.ts b/tests/integration/koa-server-no-additional-allowed.spec.ts index 43771871a..8fdd4a7bb 100644 --- a/tests/integration/koa-server-no-additional-allowed.spec.ts +++ b/tests/integration/koa-server-no-additional-allowed.spec.ts @@ -48,6 +48,51 @@ describe('Koa Server (with noImplicitAdditionalProperties turned on)', () => { ); }); + it('should call out any additionalProperties', () => { + const data = Object.assign({}, getFakeModel(), { + objLiteral: { + extra: 123, + nested: { + anotherExtra: 123, + }, + }, + }); + + return verifyPostRequest( + basePath + '/PostTest', + data, + (err: any, res: any) => { + const body = JSON.parse(err.text); + expect(body.fields['model.objLiteral'].message).to.eql('"extra" is an excess property and therefore is not allowed'); + expect(body.fields['model.objLiteral.nested'].message).to.eql('"anotherExtra" is an excess property and therefore is not allowed'); + }, + 400, + ); + }); + + it('should respect additional props', () => { + const fakeModel = getFakeModel(); + const data = { + ...fakeModel, + objLiteral: { + name: 'hello', + nested: { + additionals: { + one: { value1: '' }, + }, + allNestedOptional: {}, + bool: true, + }, + }, + } as TestModel; + + return verifyPostRequest(basePath + '/PostTest', data, (err: any, res: any) => { + expect(err).to.equal(false); + const model = res.body as TestModel; + expect(model).to.deep.equal(data); + }); + }); + it('should be okay if there are no additionalProperties', () => { const data = getFakeModel(); diff --git a/tests/unit/swagger/definitionsGeneration/definitions.spec.ts b/tests/unit/swagger/definitionsGeneration/definitions.spec.ts index 20c7b4b88..9087e3685 100644 --- a/tests/unit/swagger/definitionsGeneration/definitions.spec.ts +++ b/tests/unit/swagger/definitionsGeneration/definitions.spec.ts @@ -146,7 +146,6 @@ describe('Definition generation', () => { expect(definition.additionalProperties).to.eq(true, forSpec(currentSpec)); } }); - /** * By creating a record of "keyof T" we ensure that contributors will need add a test for any new property that is added to the model */ @@ -423,21 +422,23 @@ describe('Definition generation', () => { expect(propertySchema).to.not.haveOwnProperty('additionalProperties', `for property ${propertyName}`); }, objLiteral: (propertyName, propertySchema) => { - expect(propertySchema).to.deep.include({ + expect(propertySchema).to.deep.equal({ + default: undefined, + description: undefined, + format: undefined, properties: { - name: { - type: 'string', - }, + name: { type: 'string' }, nested: { properties: { - allOptional: { + additionals: { properties: {}, type: 'object' }, + allNestedOptional: { properties: { one: { type: 'string' }, two: { type: 'string' } }, type: 'object', }, bool: { type: 'boolean' }, - optional: { format: 'double', type: 'number' }, + optional: { type: 'number', format: 'double' }, }, - required: ['allOptional', 'bool'], + required: ['allNestedOptional', 'bool'], type: 'object', }, }, diff --git a/tests/unit/swagger/schemaDetails3.spec.ts b/tests/unit/swagger/schemaDetails3.spec.ts index 2e85b885e..6a079e601 100644 --- a/tests/unit/swagger/schemaDetails3.spec.ts +++ b/tests/unit/swagger/schemaDetails3.spec.ts @@ -222,14 +222,18 @@ describe('Definition generation for OpenAPI 3.0.0', () => { }, nested: { properties: { - allOptional: { + additionals: { + properties: {}, + type: 'object', + }, + allNestedOptional: { properties: { one: { type: 'string' }, two: { type: 'string' } }, type: 'object', }, bool: { type: 'boolean' }, optional: { format: 'double', type: 'number' }, }, - required: ['allOptional', 'bool'], + required: ['allNestedOptional', 'bool'], type: 'object', }, }, @@ -504,13 +508,6 @@ describe('Definition generation for OpenAPI 3.0.0', () => { `because the swagger spec (${currentSpec.specName}) should only produce property schemas for properties that live on the TypeScript interface.`, ); }); - - it('should have only created schemas for properties on the TypeScript interface', () => { - expect(Object.keys(assertionsPerProperty)).to.length( - Object.keys(testModel.properties!).length, - `because the swagger spec (${currentSpec.specName}) should only produce property schemas for properties that live on the TypeScript interface.`, - ); - }); }); }); }); From ffddb29ebf99cb84afe91a264b3b142c18a0529c Mon Sep 17 00:00:00 2001 From: WoH Date: Sat, 24 Aug 2019 14:24:01 +0200 Subject: [PATCH 3/9] nOL: refactor --- src/metadataGeneration/typeResolver.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/metadataGeneration/typeResolver.ts b/src/metadataGeneration/typeResolver.ts index 01c5cd020..c968c4ba3 100644 --- a/src/metadataGeneration/typeResolver.ts +++ b/src/metadataGeneration/typeResolver.ts @@ -89,12 +89,11 @@ export class TypeResolver { return { dataType: 'any' } as Tsoa.Type; } - if (this.typeNode.kind === ts.SyntaxKind.TypeLiteral) { - const typeLiteralNode = this.typeNode as ts.TypeLiteralNode; - const properties = typeLiteralNode.members - .filter(member => member.kind === ts.SyntaxKind.PropertySignature) + if (ts.isTypeLiteralNode(this.typeNode)) { + const properties = this.typeNode.members + .filter(member => ts.isPropertySignature(member)) .reduce((res, propertySignature: ts.PropertySignature) => { - const type = new TypeResolver(propertySignature.type as ts.TypeNode, this.current, this.typeNode).resolve(); + const type = new TypeResolver(propertySignature.type as ts.TypeNode, this.current, propertySignature).resolve(); return [ { @@ -110,7 +109,7 @@ export class TypeResolver { ]; }, []); - const indexMember = typeLiteralNode.members.find(member => member.kind === ts.SyntaxKind.IndexSignature); + const indexMember = this.typeNode.members.find(member => ts.isIndexSignatureDeclaration(member)); let additionalType: Tsoa.Type | undefined; if (indexMember) { From 6521cf95d5dec9bd444c6e3a6a1d01d470525747 Mon Sep 17 00:00:00 2001 From: WoH Date: Sat, 24 Aug 2019 15:37:51 +0200 Subject: [PATCH 4/9] nOL Validation: JSDoc validation tests --- tests/fixtures/testModel.ts | 74 ++++++++++++ ...dynamic-controllers-express-server.spec.ts | 108 ++++++++++++++++++ tests/integration/express-server.spec.ts | 108 ++++++++++++++++++ tests/integration/hapi-server.spec.ts | 107 +++++++++++++++++ .../koa-server-no-additional-allowed.spec.ts | 45 ++++++++ tests/integration/koa-server.spec.ts | 108 ++++++++++++++++++ tests/integration/openapi3-express.spec.ts | 108 ++++++++++++++++++ 7 files changed, 658 insertions(+) diff --git a/tests/fixtures/testModel.ts b/tests/fixtures/testModel.ts index cdc5d70de..30e09173f 100644 --- a/tests/fixtures/testModel.ts +++ b/tests/fixtures/testModel.ts @@ -283,6 +283,80 @@ export class ValidateModel { public intersection?: TypeAliasModel1 & TypeAliasModel2; public intersectionNoAdditional?: TypeAliasModel1 & TypeAliasModel2; public mixedUnion?: string | TypeAliasModel1; + + public nestedObject: { + /** + * @isFloat Invalid float error message. + */ + floatValue: number; + /** + * @isDouble Invalid double error message. + */ + doubleValue: number; + /** + * @isInt invalid integer number + */ + intValue: number; + /** + * @isLong Custom Required long number. + */ + longValue: number; + /** + * @isBoolean + */ + booleanValue: boolean; + /** + * @isArray + */ + arrayValue: number[]; + /** + * @isDate invalid ISO 8601 date format, i.e. YYYY-MM-DD + */ + dateValue: Date; + /** + * @isDateTime + */ + datetimeValue: Date; + + /** + * @maximum 10 + */ + numberMax10: number; + /** + * @minimum 5 + */ + numberMin5: number; + /** + * @maxLength 10 + */ + stringMax10Lenght: string; + /** + * @minLength 5 + */ + stringMin5Lenght: string; + /** + * @pattern ^[a-zA-Z]+$ + */ + stringPatternAZaz: string; + + /** + * @maxItems 5 + */ + arrayMax5Item: number[]; + /** + * @minItems 2 + */ + arrayMin2Item: number[]; + /** + * @uniqueItems + */ + arrayUniqueItem: number[]; + + model: TypeAliasModel1; + intersection?: TypeAliasModel1 & TypeAliasModel2; + intersectionNoAdditional?: TypeAliasModel1 & TypeAliasModel2; + mixedUnion?: string | TypeAliasModel1; + }; } export interface ValidateMapStringToNumber { diff --git a/tests/integration/dynamic-controllers-express-server.spec.ts b/tests/integration/dynamic-controllers-express-server.spec.ts index eb77b3726..1455b2ef6 100644 --- a/tests/integration/dynamic-controllers-express-server.spec.ts +++ b/tests/integration/dynamic-controllers-express-server.spec.ts @@ -500,6 +500,29 @@ describe('Express Server', () => { bodyModel.model = { value1: 'abcdef' }; bodyModel.mixedUnion = { value1: '' }; + bodyModel.nestedObject = { + floatValue: 1.2, + doubleValue: 1.2, + intValue: 120, + longValue: 120, + booleanValue: true, + arrayValue: [0, 2], + dateValue: new Date('2017-01-01'), + datetimeValue: new Date('2017-01-01T00:00:00'), + + numberMax10: 10, + numberMin5: 5, + stringMax10Lenght: 'abcdef', + stringMin5Lenght: 'abcdef', + stringPatternAZaz: 'aBcD', + + arrayMax5Item: [0, 1, 2, 3], + arrayMin2Item: [0, 1], + arrayUniqueItem: [0, 1, 2, 3], + model: { value1: 'abcdef' }, + mixedUnion: { value1: '' }, + }; + return verifyPostRequest( basePath + `/Validate/body`, bodyModel, @@ -527,6 +550,28 @@ describe('Express Server', () => { expect(body.arrayUniqueItem).to.deep.equal(bodyModel.arrayUniqueItem); expect(body.model).to.deep.equal(bodyModel.model); expect(body.mixedUnion).to.deep.equal(bodyModel.mixedUnion); + + expect(body.nestedObject.floatValue).to.equal(bodyModel.nestedObject.floatValue); + expect(body.nestedObject.doubleValue).to.equal(bodyModel.nestedObject.doubleValue); + expect(body.nestedObject.intValue).to.equal(bodyModel.nestedObject.intValue); + expect(body.nestedObject.longValue).to.equal(bodyModel.nestedObject.longValue); + expect(body.nestedObject.booleanValue).to.equal(bodyModel.nestedObject.booleanValue); + expect(body.nestedObject.arrayValue).to.deep.equal(bodyModel.nestedObject.arrayValue); + + expect(new Date(body.nestedObject.dateValue)).to.deep.equal(new Date(bodyModel.nestedObject.dateValue)); + expect(new Date(body.nestedObject.datetimeValue)).to.deep.equal(new Date(bodyModel.nestedObject.datetimeValue)); + + expect(body.nestedObject.numberMax10).to.equal(bodyModel.nestedObject.numberMax10); + expect(body.nestedObject.numberMin5).to.equal(bodyModel.nestedObject.numberMin5); + 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); + + expect(body.nestedObject.arrayMax5Item).to.deep.equal(bodyModel.nestedObject.arrayMax5Item); + expect(body.nestedObject.arrayMin2Item).to.deep.equal(bodyModel.nestedObject.arrayMin2Item); + expect(body.nestedObject.arrayUniqueItem).to.deep.equal(bodyModel.nestedObject.arrayUniqueItem); + expect(body.nestedObject.model).to.deep.equal(bodyModel.nestedObject.model); + expect(body.nestedObject.mixedUnion).to.deep.equal(bodyModel.nestedObject.mixedUnion); }, 200, ); @@ -554,6 +599,28 @@ describe('Express Server', () => { bodyModel.model = 1 as any; bodyModel.mixedUnion = 123 as any; + bodyModel.nestedObject = { + floatValue: '120a' as any, + doubleValue: '120a' as any, + intValue: 1.2, + longValue: 1.2, + booleanValue: 'abc' as any, + dateValue: 'abc' as any, + datetimeValue: 'abc' as any, + + numberMax10: 20, + numberMin5: 0, + stringMax10Lenght: 'abcdefghijk', + stringMin5Lenght: 'abcd', + stringPatternAZaz: 'ab01234', + + arrayMax5Item: [0, 1, 2, 3, 4, 6, 7, 8, 9], + arrayMin2Item: [0], + arrayUniqueItem: [0, 0, 1, 1], + model: 1 as any, + mixedUnion: 123 as any, + } as any; + return verifyPostRequest( basePath + `/Validate/body`, bodyModel, @@ -600,6 +667,47 @@ describe('Express Server', () => { 'Issues: [{"body.mixedUnion":{"message":"invalid string value","value":123}},' + '{"body.mixedUnion":{"message":"invalid object","value":123}}]', ); + + expect(body.fields['body.nestedObject.floatValue'].message).to.equal('Invalid float error message.'); + expect(body.fields['body.nestedObject.floatValue'].value).to.equal(bodyModel.floatValue); + expect(body.fields['body.nestedObject.doubleValue'].message).to.equal('Invalid double error message.'); + expect(body.fields['body.nestedObject.doubleValue'].value).to.equal(bodyModel.doubleValue); + expect(body.fields['body.nestedObject.intValue'].message).to.equal('invalid integer number'); + expect(body.fields['body.nestedObject.intValue'].value).to.equal(bodyModel.intValue); + expect(body.fields['body.nestedObject.longValue'].message).to.equal('Custom Required long number.'); + expect(body.fields['body.nestedObject.longValue'].value).to.equal(bodyModel.longValue); + expect(body.fields['body.nestedObject.booleanValue'].message).to.equal('invalid boolean value'); + expect(body.fields['body.nestedObject.booleanValue'].value).to.equal(bodyModel.booleanValue); + + expect(body.fields['body.nestedObject.dateValue'].message).to.equal('invalid ISO 8601 date format, i.e. YYYY-MM-DD'); + expect(body.fields['body.nestedObject.dateValue'].value).to.equal(bodyModel.dateValue); + expect(body.fields['body.nestedObject.datetimeValue'].message).to.equal('invalid ISO 8601 datetime format, i.e. YYYY-MM-DDTHH:mm:ss'); + expect(body.fields['body.nestedObject.datetimeValue'].value).to.equal(bodyModel.datetimeValue); + + expect(body.fields['body.nestedObject.numberMax10'].message).to.equal('max 10'); + expect(body.fields['body.nestedObject.numberMax10'].value).to.equal(bodyModel.numberMax10); + expect(body.fields['body.nestedObject.numberMin5'].message).to.equal('min 5'); + expect(body.fields['body.nestedObject.numberMin5'].value).to.equal(bodyModel.numberMin5); + expect(body.fields['body.nestedObject.stringMax10Lenght'].message).to.equal('maxLength 10'); + expect(body.fields['body.nestedObject.stringMax10Lenght'].value).to.equal(bodyModel.stringMax10Lenght); + expect(body.fields['body.nestedObject.stringMin5Lenght'].message).to.equal('minLength 5'); + expect(body.fields['body.nestedObject.stringMin5Lenght'].value).to.equal(bodyModel.stringMin5Lenght); + expect(body.fields['body.nestedObject.stringPatternAZaz'].message).to.equal("Not match in '^[a-zA-Z]+$'"); + expect(body.fields['body.nestedObject.stringPatternAZaz'].value).to.equal(bodyModel.stringPatternAZaz); + + expect(body.fields['body.nestedObject.arrayMax5Item'].message).to.equal('maxItems 5'); + expect(body.fields['body.nestedObject.arrayMax5Item'].value).to.deep.equal(bodyModel.arrayMax5Item); + expect(body.fields['body.nestedObject.arrayMin2Item'].message).to.equal('minItems 2'); + expect(body.fields['body.nestedObject.arrayMin2Item'].value).to.deep.equal(bodyModel.arrayMin2Item); + expect(body.fields['body.nestedObject.arrayUniqueItem'].message).to.equal('required unique array'); + expect(body.fields['body.nestedObject.arrayUniqueItem'].value).to.deep.equal(bodyModel.arrayUniqueItem); + expect(body.fields['body.nestedObject.model'].message).to.equal('invalid object'); + expect(body.fields['body.nestedObject.model'].value).to.deep.equal(bodyModel.model); + expect(body.fields['body.nestedObject.mixedUnion'].message).to.equal( + 'Could not match the union against any of the items. ' + + 'Issues: [{"body.nestedObject.mixedUnion":{"message":"invalid string value","value":123}},' + + '{"body.nestedObject.mixedUnion":{"message":"invalid object","value":123}}]', + ); }, 400, ); diff --git a/tests/integration/express-server.spec.ts b/tests/integration/express-server.spec.ts index 3a8ba38f0..219c02b6d 100644 --- a/tests/integration/express-server.spec.ts +++ b/tests/integration/express-server.spec.ts @@ -520,6 +520,29 @@ describe('Express Server', () => { bodyModel.model = { value1: 'abcdef' }; bodyModel.mixedUnion = { value1: '' }; + bodyModel.nestedObject = { + floatValue: 1.2, + doubleValue: 1.2, + intValue: 120, + longValue: 120, + booleanValue: true, + arrayValue: [0, 2], + dateValue: new Date('2017-01-01'), + datetimeValue: new Date('2017-01-01T00:00:00'), + + numberMax10: 10, + numberMin5: 5, + stringMax10Lenght: 'abcdef', + stringMin5Lenght: 'abcdef', + stringPatternAZaz: 'aBcD', + + arrayMax5Item: [0, 1, 2, 3], + arrayMin2Item: [0, 1], + arrayUniqueItem: [0, 1, 2, 3], + model: { value1: 'abcdef' }, + mixedUnion: { value1: '' }, + }; + return verifyPostRequest( basePath + `/Validate/body`, bodyModel, @@ -547,6 +570,28 @@ describe('Express Server', () => { expect(body.arrayUniqueItem).to.deep.equal(bodyModel.arrayUniqueItem); expect(body.model).to.deep.equal(bodyModel.model); expect(body.mixedUnion).to.deep.equal(bodyModel.mixedUnion); + + expect(body.nestedObject.floatValue).to.equal(bodyModel.nestedObject.floatValue); + expect(body.nestedObject.doubleValue).to.equal(bodyModel.nestedObject.doubleValue); + expect(body.nestedObject.intValue).to.equal(bodyModel.nestedObject.intValue); + expect(body.nestedObject.longValue).to.equal(bodyModel.nestedObject.longValue); + expect(body.nestedObject.booleanValue).to.equal(bodyModel.nestedObject.booleanValue); + expect(body.nestedObject.arrayValue).to.deep.equal(bodyModel.nestedObject.arrayValue); + + expect(new Date(body.nestedObject.dateValue)).to.deep.equal(new Date(bodyModel.nestedObject.dateValue)); + expect(new Date(body.nestedObject.datetimeValue)).to.deep.equal(new Date(bodyModel.nestedObject.datetimeValue)); + + expect(body.nestedObject.numberMax10).to.equal(bodyModel.nestedObject.numberMax10); + expect(body.nestedObject.numberMin5).to.equal(bodyModel.nestedObject.numberMin5); + 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); + + expect(body.nestedObject.arrayMax5Item).to.deep.equal(bodyModel.nestedObject.arrayMax5Item); + expect(body.nestedObject.arrayMin2Item).to.deep.equal(bodyModel.nestedObject.arrayMin2Item); + expect(body.nestedObject.arrayUniqueItem).to.deep.equal(bodyModel.nestedObject.arrayUniqueItem); + expect(body.nestedObject.model).to.deep.equal(bodyModel.nestedObject.model); + expect(body.nestedObject.mixedUnion).to.deep.equal(bodyModel.nestedObject.mixedUnion); }, 200, ); @@ -574,6 +619,28 @@ describe('Express Server', () => { bodyModel.model = 1 as any; bodyModel.mixedUnion = 123 as any; + bodyModel.nestedObject = { + floatValue: '120a' as any, + doubleValue: '120a' as any, + intValue: 1.2, + longValue: 1.2, + booleanValue: 'abc' as any, + dateValue: 'abc' as any, + datetimeValue: 'abc' as any, + + numberMax10: 20, + numberMin5: 0, + stringMax10Lenght: 'abcdefghijk', + stringMin5Lenght: 'abcd', + stringPatternAZaz: 'ab01234', + + arrayMax5Item: [0, 1, 2, 3, 4, 6, 7, 8, 9], + arrayMin2Item: [0], + arrayUniqueItem: [0, 0, 1, 1], + model: 1 as any, + mixedUnion: 123 as any, + } as any; + return verifyPostRequest( basePath + `/Validate/body`, bodyModel, @@ -620,6 +687,47 @@ describe('Express Server', () => { 'Issues: [{"body.mixedUnion":{"message":"invalid string value","value":123}},' + '{"body.mixedUnion":{"message":"invalid object","value":123}}]', ); + + expect(body.fields['body.nestedObject.floatValue'].message).to.equal('Invalid float error message.'); + expect(body.fields['body.nestedObject.floatValue'].value).to.equal(bodyModel.floatValue); + expect(body.fields['body.nestedObject.doubleValue'].message).to.equal('Invalid double error message.'); + expect(body.fields['body.nestedObject.doubleValue'].value).to.equal(bodyModel.doubleValue); + expect(body.fields['body.nestedObject.intValue'].message).to.equal('invalid integer number'); + expect(body.fields['body.nestedObject.intValue'].value).to.equal(bodyModel.intValue); + expect(body.fields['body.nestedObject.longValue'].message).to.equal('Custom Required long number.'); + expect(body.fields['body.nestedObject.longValue'].value).to.equal(bodyModel.longValue); + expect(body.fields['body.nestedObject.booleanValue'].message).to.equal('invalid boolean value'); + expect(body.fields['body.nestedObject.booleanValue'].value).to.equal(bodyModel.booleanValue); + + expect(body.fields['body.nestedObject.dateValue'].message).to.equal('invalid ISO 8601 date format, i.e. YYYY-MM-DD'); + expect(body.fields['body.nestedObject.dateValue'].value).to.equal(bodyModel.dateValue); + expect(body.fields['body.nestedObject.datetimeValue'].message).to.equal('invalid ISO 8601 datetime format, i.e. YYYY-MM-DDTHH:mm:ss'); + expect(body.fields['body.nestedObject.datetimeValue'].value).to.equal(bodyModel.datetimeValue); + + expect(body.fields['body.nestedObject.numberMax10'].message).to.equal('max 10'); + expect(body.fields['body.nestedObject.numberMax10'].value).to.equal(bodyModel.numberMax10); + expect(body.fields['body.nestedObject.numberMin5'].message).to.equal('min 5'); + expect(body.fields['body.nestedObject.numberMin5'].value).to.equal(bodyModel.numberMin5); + expect(body.fields['body.nestedObject.stringMax10Lenght'].message).to.equal('maxLength 10'); + expect(body.fields['body.nestedObject.stringMax10Lenght'].value).to.equal(bodyModel.stringMax10Lenght); + expect(body.fields['body.nestedObject.stringMin5Lenght'].message).to.equal('minLength 5'); + expect(body.fields['body.nestedObject.stringMin5Lenght'].value).to.equal(bodyModel.stringMin5Lenght); + expect(body.fields['body.nestedObject.stringPatternAZaz'].message).to.equal("Not match in '^[a-zA-Z]+$'"); + expect(body.fields['body.nestedObject.stringPatternAZaz'].value).to.equal(bodyModel.stringPatternAZaz); + + expect(body.fields['body.nestedObject.arrayMax5Item'].message).to.equal('maxItems 5'); + expect(body.fields['body.nestedObject.arrayMax5Item'].value).to.deep.equal(bodyModel.arrayMax5Item); + expect(body.fields['body.nestedObject.arrayMin2Item'].message).to.equal('minItems 2'); + expect(body.fields['body.nestedObject.arrayMin2Item'].value).to.deep.equal(bodyModel.arrayMin2Item); + expect(body.fields['body.nestedObject.arrayUniqueItem'].message).to.equal('required unique array'); + expect(body.fields['body.nestedObject.arrayUniqueItem'].value).to.deep.equal(bodyModel.arrayUniqueItem); + expect(body.fields['body.nestedObject.model'].message).to.equal('invalid object'); + expect(body.fields['body.nestedObject.model'].value).to.deep.equal(bodyModel.model); + expect(body.fields['body.nestedObject.mixedUnion'].message).to.equal( + 'Could not match the union against any of the items. ' + + 'Issues: [{"body.nestedObject.mixedUnion":{"message":"invalid string value","value":123}},' + + '{"body.nestedObject.mixedUnion":{"message":"invalid object","value":123}}]', + ); }, 400, ); diff --git a/tests/integration/hapi-server.spec.ts b/tests/integration/hapi-server.spec.ts index 9b99bc07d..043e02269 100644 --- a/tests/integration/hapi-server.spec.ts +++ b/tests/integration/hapi-server.spec.ts @@ -473,6 +473,29 @@ describe('Hapi Server', () => { bodyModel.model = { value1: 'abcdef' }; bodyModel.mixedUnion = { value1: '' }; + bodyModel.nestedObject = { + floatValue: 1.2, + doubleValue: 1.2, + intValue: 120, + longValue: 120, + booleanValue: true, + arrayValue: [0, 2], + dateValue: new Date('2017-01-01'), + datetimeValue: new Date('2017-01-01T00:00:00'), + + numberMax10: 10, + numberMin5: 5, + stringMax10Lenght: 'abcdef', + stringMin5Lenght: 'abcdef', + stringPatternAZaz: 'aBcD', + + arrayMax5Item: [0, 1, 2, 3], + arrayMin2Item: [0, 1], + arrayUniqueItem: [0, 1, 2, 3], + model: { value1: 'abcdef' }, + mixedUnion: { value1: '' }, + }; + return verifyPostRequest( basePath + `/Validate/body`, bodyModel, @@ -500,6 +523,28 @@ describe('Hapi Server', () => { expect(body.arrayUniqueItem).to.deep.equal(bodyModel.arrayUniqueItem); expect(body.model).to.deep.equal(bodyModel.model); expect(body.mixedUnion).to.deep.equal(bodyModel.mixedUnion); + + expect(body.nestedObject.floatValue).to.equal(bodyModel.nestedObject.floatValue); + expect(body.nestedObject.doubleValue).to.equal(bodyModel.nestedObject.doubleValue); + expect(body.nestedObject.intValue).to.equal(bodyModel.nestedObject.intValue); + expect(body.nestedObject.longValue).to.equal(bodyModel.nestedObject.longValue); + expect(body.nestedObject.booleanValue).to.equal(bodyModel.nestedObject.booleanValue); + expect(body.nestedObject.arrayValue).to.deep.equal(bodyModel.nestedObject.arrayValue); + + expect(new Date(body.nestedObject.dateValue)).to.deep.equal(new Date(bodyModel.nestedObject.dateValue)); + expect(new Date(body.nestedObject.datetimeValue)).to.deep.equal(new Date(bodyModel.nestedObject.datetimeValue)); + + expect(body.nestedObject.numberMax10).to.equal(bodyModel.nestedObject.numberMax10); + expect(body.nestedObject.numberMin5).to.equal(bodyModel.nestedObject.numberMin5); + 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); + + expect(body.nestedObject.arrayMax5Item).to.deep.equal(bodyModel.nestedObject.arrayMax5Item); + expect(body.nestedObject.arrayMin2Item).to.deep.equal(bodyModel.nestedObject.arrayMin2Item); + expect(body.nestedObject.arrayUniqueItem).to.deep.equal(bodyModel.nestedObject.arrayUniqueItem); + expect(body.nestedObject.model).to.deep.equal(bodyModel.nestedObject.model); + expect(body.nestedObject.mixedUnion).to.deep.equal(bodyModel.nestedObject.mixedUnion); }, 200, ); @@ -527,6 +572,28 @@ describe('Hapi Server', () => { bodyModel.model = 1 as any; bodyModel.mixedUnion = 123 as any; + bodyModel.nestedObject = { + floatValue: '120a' as any, + doubleValue: '120a' as any, + intValue: 1.2, + longValue: 1.2, + booleanValue: 'abc' as any, + dateValue: 'abc' as any, + datetimeValue: 'abc' as any, + + numberMax10: 20, + numberMin5: 0, + stringMax10Lenght: 'abcdefghijk', + stringMin5Lenght: 'abcd', + stringPatternAZaz: 'ab01234', + + arrayMax5Item: [0, 1, 2, 3, 4, 6, 7, 8, 9], + arrayMin2Item: [0], + arrayUniqueItem: [0, 0, 1, 1], + model: 1 as any, + mixedUnion: 123 as any, + } as any; + return verifyPostRequest( basePath + `/Validate/body`, bodyModel, @@ -573,6 +640,46 @@ describe('Hapi Server', () => { 'Issues: [{"body.mixedUnion":{"message":"invalid string value","value":123}},' + '{"body.mixedUnion":{"message":"invalid object","value":123}}]', ); + expect(body.fields['body.nestedObject.floatValue'].message).to.equal('Invalid float error message.'); + expect(body.fields['body.nestedObject.floatValue'].value).to.equal(bodyModel.floatValue); + expect(body.fields['body.nestedObject.doubleValue'].message).to.equal('Invalid double error message.'); + expect(body.fields['body.nestedObject.doubleValue'].value).to.equal(bodyModel.doubleValue); + expect(body.fields['body.nestedObject.intValue'].message).to.equal('invalid integer number'); + expect(body.fields['body.nestedObject.intValue'].value).to.equal(bodyModel.intValue); + expect(body.fields['body.nestedObject.longValue'].message).to.equal('Custom Required long number.'); + expect(body.fields['body.nestedObject.longValue'].value).to.equal(bodyModel.longValue); + expect(body.fields['body.nestedObject.booleanValue'].message).to.equal('invalid boolean value'); + expect(body.fields['body.nestedObject.booleanValue'].value).to.equal(bodyModel.booleanValue); + + expect(body.fields['body.nestedObject.dateValue'].message).to.equal('invalid ISO 8601 date format, i.e. YYYY-MM-DD'); + expect(body.fields['body.nestedObject.dateValue'].value).to.equal(bodyModel.dateValue); + expect(body.fields['body.nestedObject.datetimeValue'].message).to.equal('invalid ISO 8601 datetime format, i.e. YYYY-MM-DDTHH:mm:ss'); + expect(body.fields['body.nestedObject.datetimeValue'].value).to.equal(bodyModel.datetimeValue); + + expect(body.fields['body.nestedObject.numberMax10'].message).to.equal('max 10'); + expect(body.fields['body.nestedObject.numberMax10'].value).to.equal(bodyModel.numberMax10); + expect(body.fields['body.nestedObject.numberMin5'].message).to.equal('min 5'); + expect(body.fields['body.nestedObject.numberMin5'].value).to.equal(bodyModel.numberMin5); + expect(body.fields['body.nestedObject.stringMax10Lenght'].message).to.equal('maxLength 10'); + expect(body.fields['body.nestedObject.stringMax10Lenght'].value).to.equal(bodyModel.stringMax10Lenght); + expect(body.fields['body.nestedObject.stringMin5Lenght'].message).to.equal('minLength 5'); + expect(body.fields['body.nestedObject.stringMin5Lenght'].value).to.equal(bodyModel.stringMin5Lenght); + expect(body.fields['body.nestedObject.stringPatternAZaz'].message).to.equal("Not match in '^[a-zA-Z]+$'"); + expect(body.fields['body.nestedObject.stringPatternAZaz'].value).to.equal(bodyModel.stringPatternAZaz); + + expect(body.fields['body.nestedObject.arrayMax5Item'].message).to.equal('maxItems 5'); + expect(body.fields['body.nestedObject.arrayMax5Item'].value).to.deep.equal(bodyModel.arrayMax5Item); + expect(body.fields['body.nestedObject.arrayMin2Item'].message).to.equal('minItems 2'); + expect(body.fields['body.nestedObject.arrayMin2Item'].value).to.deep.equal(bodyModel.arrayMin2Item); + expect(body.fields['body.nestedObject.arrayUniqueItem'].message).to.equal('required unique array'); + expect(body.fields['body.nestedObject.arrayUniqueItem'].value).to.deep.equal(bodyModel.arrayUniqueItem); + expect(body.fields['body.nestedObject.model'].message).to.equal('invalid object'); + expect(body.fields['body.nestedObject.model'].value).to.deep.equal(bodyModel.model); + expect(body.fields['body.nestedObject.mixedUnion'].message).to.equal( + 'Could not match the union against any of the items. ' + + 'Issues: [{"body.nestedObject.mixedUnion":{"message":"invalid string value","value":123}},' + + '{"body.nestedObject.mixedUnion":{"message":"invalid object","value":123}}]', + ); }, 400, ); diff --git a/tests/integration/koa-server-no-additional-allowed.spec.ts b/tests/integration/koa-server-no-additional-allowed.spec.ts index 8fdd4a7bb..429686e86 100644 --- a/tests/integration/koa-server-no-additional-allowed.spec.ts +++ b/tests/integration/koa-server-no-additional-allowed.spec.ts @@ -195,6 +195,29 @@ describe('Koa Server (with noImplicitAdditionalProperties turned on)', () => { bodyModel.model = { value1: 'abcdef' }; bodyModel.mixedUnion = { value1: '' }; + bodyModel.nestedObject = { + floatValue: 1.2, + doubleValue: 1.2, + intValue: 120, + longValue: 120, + booleanValue: true, + arrayValue: [0, 2], + dateValue: new Date('2017-01-01'), + datetimeValue: new Date('2017-01-01T00:00:00'), + + numberMax10: 10, + numberMin5: 5, + stringMax10Lenght: 'abcdef', + stringMin5Lenght: 'abcdef', + stringPatternAZaz: 'aBcD', + + arrayMax5Item: [0, 1, 2, 3], + arrayMin2Item: [0, 1], + arrayUniqueItem: [0, 1, 2, 3], + model: { value1: 'abcdef' }, + mixedUnion: { value1: '' }, + }; + return verifyPostRequest( basePath + `/Validate/body`, bodyModel, @@ -222,6 +245,28 @@ describe('Koa Server (with noImplicitAdditionalProperties turned on)', () => { expect(body.arrayUniqueItem).to.deep.equal(bodyModel.arrayUniqueItem); expect(body.model).to.deep.equal(bodyModel.model); expect(body.mixedUnion).to.deep.equal(bodyModel.mixedUnion); + + expect(body.nestedObject.floatValue).to.equal(bodyModel.nestedObject.floatValue); + expect(body.nestedObject.doubleValue).to.equal(bodyModel.nestedObject.doubleValue); + expect(body.nestedObject.intValue).to.equal(bodyModel.nestedObject.intValue); + expect(body.nestedObject.longValue).to.equal(bodyModel.nestedObject.longValue); + expect(body.nestedObject.booleanValue).to.equal(bodyModel.nestedObject.booleanValue); + expect(body.nestedObject.arrayValue).to.deep.equal(bodyModel.nestedObject.arrayValue); + + expect(new Date(body.nestedObject.dateValue)).to.deep.equal(new Date(bodyModel.nestedObject.dateValue)); + expect(new Date(body.nestedObject.datetimeValue)).to.deep.equal(new Date(bodyModel.nestedObject.datetimeValue)); + + expect(body.nestedObject.numberMax10).to.equal(bodyModel.nestedObject.numberMax10); + expect(body.nestedObject.numberMin5).to.equal(bodyModel.nestedObject.numberMin5); + 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); + + expect(body.nestedObject.arrayMax5Item).to.deep.equal(bodyModel.nestedObject.arrayMax5Item); + expect(body.nestedObject.arrayMin2Item).to.deep.equal(bodyModel.nestedObject.arrayMin2Item); + expect(body.nestedObject.arrayUniqueItem).to.deep.equal(bodyModel.nestedObject.arrayUniqueItem); + expect(body.nestedObject.model).to.deep.equal(bodyModel.nestedObject.model); + expect(body.nestedObject.mixedUnion).to.deep.equal(bodyModel.nestedObject.mixedUnion); }, 200, ); diff --git a/tests/integration/koa-server.spec.ts b/tests/integration/koa-server.spec.ts index 5947df7b0..734c201e4 100644 --- a/tests/integration/koa-server.spec.ts +++ b/tests/integration/koa-server.spec.ts @@ -451,6 +451,29 @@ describe('Koa Server', () => { bodyModel.model = { value1: 'abcdef' }; bodyModel.mixedUnion = { value1: '' }; + bodyModel.nestedObject = { + floatValue: 1.2, + doubleValue: 1.2, + intValue: 120, + longValue: 120, + booleanValue: true, + arrayValue: [0, 2], + dateValue: new Date('2017-01-01'), + datetimeValue: new Date('2017-01-01T00:00:00'), + + numberMax10: 10, + numberMin5: 5, + stringMax10Lenght: 'abcdef', + stringMin5Lenght: 'abcdef', + stringPatternAZaz: 'aBcD', + + arrayMax5Item: [0, 1, 2, 3], + arrayMin2Item: [0, 1], + arrayUniqueItem: [0, 1, 2, 3], + model: { value1: 'abcdef' }, + mixedUnion: { value1: '' }, + }; + return verifyPostRequest( basePath + `/Validate/body`, bodyModel, @@ -478,6 +501,28 @@ describe('Koa Server', () => { expect(body.arrayUniqueItem).to.deep.equal(bodyModel.arrayUniqueItem); expect(body.model).to.deep.equal(bodyModel.model); expect(body.mixedUnion).to.deep.equal(bodyModel.mixedUnion); + + expect(body.nestedObject.floatValue).to.equal(bodyModel.nestedObject.floatValue); + expect(body.nestedObject.doubleValue).to.equal(bodyModel.nestedObject.doubleValue); + expect(body.nestedObject.intValue).to.equal(bodyModel.nestedObject.intValue); + expect(body.nestedObject.longValue).to.equal(bodyModel.nestedObject.longValue); + expect(body.nestedObject.booleanValue).to.equal(bodyModel.nestedObject.booleanValue); + expect(body.nestedObject.arrayValue).to.deep.equal(bodyModel.nestedObject.arrayValue); + + expect(new Date(body.nestedObject.dateValue)).to.deep.equal(new Date(bodyModel.nestedObject.dateValue)); + expect(new Date(body.nestedObject.datetimeValue)).to.deep.equal(new Date(bodyModel.nestedObject.datetimeValue)); + + expect(body.nestedObject.numberMax10).to.equal(bodyModel.nestedObject.numberMax10); + expect(body.nestedObject.numberMin5).to.equal(bodyModel.nestedObject.numberMin5); + 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); + + expect(body.nestedObject.arrayMax5Item).to.deep.equal(bodyModel.nestedObject.arrayMax5Item); + expect(body.nestedObject.arrayMin2Item).to.deep.equal(bodyModel.nestedObject.arrayMin2Item); + expect(body.nestedObject.arrayUniqueItem).to.deep.equal(bodyModel.nestedObject.arrayUniqueItem); + expect(body.nestedObject.model).to.deep.equal(bodyModel.nestedObject.model); + expect(body.nestedObject.mixedUnion).to.deep.equal(bodyModel.nestedObject.mixedUnion); }, 200, ); @@ -505,6 +550,28 @@ describe('Koa Server', () => { bodyModel.model = 1 as any; bodyModel.mixedUnion = 123 as any; + bodyModel.nestedObject = { + floatValue: '120a' as any, + doubleValue: '120a' as any, + intValue: 1.2, + longValue: 1.2, + booleanValue: 'abc' as any, + dateValue: 'abc' as any, + datetimeValue: 'abc' as any, + + numberMax10: 20, + numberMin5: 0, + stringMax10Lenght: 'abcdefghijk', + stringMin5Lenght: 'abcd', + stringPatternAZaz: 'ab01234', + + arrayMax5Item: [0, 1, 2, 3, 4, 6, 7, 8, 9], + arrayMin2Item: [0], + arrayUniqueItem: [0, 0, 1, 1], + model: 1 as any, + mixedUnion: 123 as any, + } as any; + return verifyPostRequest( basePath + `/Validate/body`, bodyModel, @@ -551,6 +618,47 @@ describe('Koa Server', () => { 'Issues: [{"body.mixedUnion":{"message":"invalid string value","value":123}},' + '{"body.mixedUnion":{"message":"invalid object","value":123}}]', ); + + expect(body.fields['body.nestedObject.floatValue'].message).to.equal('Invalid float error message.'); + expect(body.fields['body.nestedObject.floatValue'].value).to.equal(bodyModel.floatValue); + expect(body.fields['body.nestedObject.doubleValue'].message).to.equal('Invalid double error message.'); + expect(body.fields['body.nestedObject.doubleValue'].value).to.equal(bodyModel.doubleValue); + expect(body.fields['body.nestedObject.intValue'].message).to.equal('invalid integer number'); + expect(body.fields['body.nestedObject.intValue'].value).to.equal(bodyModel.intValue); + expect(body.fields['body.nestedObject.longValue'].message).to.equal('Custom Required long number.'); + expect(body.fields['body.nestedObject.longValue'].value).to.equal(bodyModel.longValue); + expect(body.fields['body.nestedObject.booleanValue'].message).to.equal('invalid boolean value'); + expect(body.fields['body.nestedObject.booleanValue'].value).to.equal(bodyModel.booleanValue); + + expect(body.fields['body.nestedObject.dateValue'].message).to.equal('invalid ISO 8601 date format, i.e. YYYY-MM-DD'); + expect(body.fields['body.nestedObject.dateValue'].value).to.equal(bodyModel.dateValue); + expect(body.fields['body.nestedObject.datetimeValue'].message).to.equal('invalid ISO 8601 datetime format, i.e. YYYY-MM-DDTHH:mm:ss'); + expect(body.fields['body.nestedObject.datetimeValue'].value).to.equal(bodyModel.datetimeValue); + + expect(body.fields['body.nestedObject.numberMax10'].message).to.equal('max 10'); + expect(body.fields['body.nestedObject.numberMax10'].value).to.equal(bodyModel.numberMax10); + expect(body.fields['body.nestedObject.numberMin5'].message).to.equal('min 5'); + expect(body.fields['body.nestedObject.numberMin5'].value).to.equal(bodyModel.numberMin5); + expect(body.fields['body.nestedObject.stringMax10Lenght'].message).to.equal('maxLength 10'); + expect(body.fields['body.nestedObject.stringMax10Lenght'].value).to.equal(bodyModel.stringMax10Lenght); + expect(body.fields['body.nestedObject.stringMin5Lenght'].message).to.equal('minLength 5'); + expect(body.fields['body.nestedObject.stringMin5Lenght'].value).to.equal(bodyModel.stringMin5Lenght); + expect(body.fields['body.nestedObject.stringPatternAZaz'].message).to.equal("Not match in '^[a-zA-Z]+$'"); + expect(body.fields['body.nestedObject.stringPatternAZaz'].value).to.equal(bodyModel.stringPatternAZaz); + + expect(body.fields['body.nestedObject.arrayMax5Item'].message).to.equal('maxItems 5'); + expect(body.fields['body.nestedObject.arrayMax5Item'].value).to.deep.equal(bodyModel.arrayMax5Item); + expect(body.fields['body.nestedObject.arrayMin2Item'].message).to.equal('minItems 2'); + expect(body.fields['body.nestedObject.arrayMin2Item'].value).to.deep.equal(bodyModel.arrayMin2Item); + expect(body.fields['body.nestedObject.arrayUniqueItem'].message).to.equal('required unique array'); + expect(body.fields['body.nestedObject.arrayUniqueItem'].value).to.deep.equal(bodyModel.arrayUniqueItem); + expect(body.fields['body.nestedObject.model'].message).to.equal('invalid object'); + expect(body.fields['body.nestedObject.model'].value).to.deep.equal(bodyModel.model); + expect(body.fields['body.nestedObject.mixedUnion'].message).to.equal( + 'Could not match the union against any of the items. ' + + 'Issues: [{"body.nestedObject.mixedUnion":{"message":"invalid string value","value":123}},' + + '{"body.nestedObject.mixedUnion":{"message":"invalid object","value":123}}]', + ); }, 400, ); diff --git a/tests/integration/openapi3-express.spec.ts b/tests/integration/openapi3-express.spec.ts index 56ea93b63..2704ca961 100644 --- a/tests/integration/openapi3-express.spec.ts +++ b/tests/integration/openapi3-express.spec.ts @@ -30,6 +30,29 @@ describe('OpenAPI3 Express Server', () => { bodyModel.model = { value1: 'abcdef' }; bodyModel.mixedUnion = { value1: '' }; + bodyModel.nestedObject = { + floatValue: 1.2, + doubleValue: 1.2, + intValue: 120, + longValue: 120, + booleanValue: true, + arrayValue: [0, 2], + dateValue: new Date('2017-01-01'), + datetimeValue: new Date('2017-01-01T00:00:00'), + + numberMax10: 10, + numberMin5: 5, + stringMax10Lenght: 'abcdef', + stringMin5Lenght: 'abcdef', + stringPatternAZaz: 'aBcD', + + arrayMax5Item: [0, 1, 2, 3], + arrayMin2Item: [0, 1], + arrayUniqueItem: [0, 1, 2, 3], + model: { value1: 'abcdef' }, + mixedUnion: { value1: '' }, + }; + return verifyPostRequest( basePath + `/Validate/body`, bodyModel, @@ -57,6 +80,28 @@ describe('OpenAPI3 Express Server', () => { expect(body.arrayUniqueItem).to.deep.equal(bodyModel.arrayUniqueItem); expect(body.model).to.deep.equal(bodyModel.model); expect(body.mixedUnion).to.deep.equal(bodyModel.mixedUnion); + + expect(body.nestedObject.floatValue).to.equal(bodyModel.nestedObject.floatValue); + expect(body.nestedObject.doubleValue).to.equal(bodyModel.nestedObject.doubleValue); + expect(body.nestedObject.intValue).to.equal(bodyModel.nestedObject.intValue); + expect(body.nestedObject.longValue).to.equal(bodyModel.nestedObject.longValue); + expect(body.nestedObject.booleanValue).to.equal(bodyModel.nestedObject.booleanValue); + expect(body.nestedObject.arrayValue).to.deep.equal(bodyModel.nestedObject.arrayValue); + + expect(new Date(body.nestedObject.dateValue)).to.deep.equal(new Date(bodyModel.nestedObject.dateValue)); + expect(new Date(body.nestedObject.datetimeValue)).to.deep.equal(new Date(bodyModel.nestedObject.datetimeValue)); + + expect(body.nestedObject.numberMax10).to.equal(bodyModel.nestedObject.numberMax10); + expect(body.nestedObject.numberMin5).to.equal(bodyModel.nestedObject.numberMin5); + 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); + + expect(body.nestedObject.arrayMax5Item).to.deep.equal(bodyModel.nestedObject.arrayMax5Item); + expect(body.nestedObject.arrayMin2Item).to.deep.equal(bodyModel.nestedObject.arrayMin2Item); + expect(body.nestedObject.arrayUniqueItem).to.deep.equal(bodyModel.nestedObject.arrayUniqueItem); + expect(body.nestedObject.model).to.deep.equal(bodyModel.nestedObject.model); + expect(body.nestedObject.mixedUnion).to.deep.equal(bodyModel.nestedObject.mixedUnion); }, 200, ); @@ -86,6 +131,28 @@ describe('OpenAPI3 Express Server', () => { bodyModel.model = 1 as any; bodyModel.mixedUnion = 123 as any; + bodyModel.nestedObject = { + floatValue: '120a' as any, + doubleValue: '120a' as any, + intValue: 1.2, + longValue: 1.2, + booleanValue: 'abc' as any, + dateValue: 'abc' as any, + datetimeValue: 'abc' as any, + + numberMax10: 20, + numberMin5: 0, + stringMax10Lenght: 'abcdefghijk', + stringMin5Lenght: 'abcd', + stringPatternAZaz: 'ab01234', + + arrayMax5Item: [0, 1, 2, 3, 4, 6, 7, 8, 9], + arrayMin2Item: [0], + arrayUniqueItem: [0, 0, 1, 1], + model: 1 as any, + mixedUnion: 123 as any, + } as any; + return verifyPostRequest( basePath + `/Validate/body`, bodyModel, @@ -134,6 +201,47 @@ describe('OpenAPI3 Express Server', () => { 'Issues: [{"body.mixedUnion":{"message":"invalid string value","value":123}},' + '{"body.mixedUnion":{"message":"invalid object","value":123}}]', ); + + expect(body.fields['body.nestedObject.floatValue'].message).to.equal('Invalid float error message.'); + expect(body.fields['body.nestedObject.floatValue'].value).to.equal(bodyModel.floatValue); + expect(body.fields['body.nestedObject.doubleValue'].message).to.equal('Invalid double error message.'); + expect(body.fields['body.nestedObject.doubleValue'].value).to.equal(bodyModel.doubleValue); + expect(body.fields['body.nestedObject.intValue'].message).to.equal('invalid integer number'); + expect(body.fields['body.nestedObject.intValue'].value).to.equal(bodyModel.intValue); + expect(body.fields['body.nestedObject.longValue'].message).to.equal('Custom Required long number.'); + expect(body.fields['body.nestedObject.longValue'].value).to.equal(bodyModel.longValue); + expect(body.fields['body.nestedObject.booleanValue'].message).to.equal('invalid boolean value'); + expect(body.fields['body.nestedObject.booleanValue'].value).to.equal(bodyModel.booleanValue); + + expect(body.fields['body.nestedObject.dateValue'].message).to.equal('invalid ISO 8601 date format, i.e. YYYY-MM-DD'); + expect(body.fields['body.nestedObject.dateValue'].value).to.equal(bodyModel.dateValue); + expect(body.fields['body.nestedObject.datetimeValue'].message).to.equal('invalid ISO 8601 datetime format, i.e. YYYY-MM-DDTHH:mm:ss'); + expect(body.fields['body.nestedObject.datetimeValue'].value).to.equal(bodyModel.datetimeValue); + + expect(body.fields['body.nestedObject.numberMax10'].message).to.equal('max 10'); + expect(body.fields['body.nestedObject.numberMax10'].value).to.equal(bodyModel.numberMax10); + expect(body.fields['body.nestedObject.numberMin5'].message).to.equal('min 5'); + expect(body.fields['body.nestedObject.numberMin5'].value).to.equal(bodyModel.numberMin5); + expect(body.fields['body.nestedObject.stringMax10Lenght'].message).to.equal('maxLength 10'); + expect(body.fields['body.nestedObject.stringMax10Lenght'].value).to.equal(bodyModel.stringMax10Lenght); + expect(body.fields['body.nestedObject.stringMin5Lenght'].message).to.equal('minLength 5'); + expect(body.fields['body.nestedObject.stringMin5Lenght'].value).to.equal(bodyModel.stringMin5Lenght); + expect(body.fields['body.nestedObject.stringPatternAZaz'].message).to.equal("Not match in '^[a-zA-Z]+$'"); + expect(body.fields['body.nestedObject.stringPatternAZaz'].value).to.equal(bodyModel.stringPatternAZaz); + + expect(body.fields['body.nestedObject.arrayMax5Item'].message).to.equal('maxItems 5'); + expect(body.fields['body.nestedObject.arrayMax5Item'].value).to.deep.equal(bodyModel.arrayMax5Item); + expect(body.fields['body.nestedObject.arrayMin2Item'].message).to.equal('minItems 2'); + expect(body.fields['body.nestedObject.arrayMin2Item'].value).to.deep.equal(bodyModel.arrayMin2Item); + expect(body.fields['body.nestedObject.arrayUniqueItem'].message).to.equal('required unique array'); + expect(body.fields['body.nestedObject.arrayUniqueItem'].value).to.deep.equal(bodyModel.arrayUniqueItem); + expect(body.fields['body.nestedObject.model'].message).to.equal('invalid object'); + expect(body.fields['body.nestedObject.model'].value).to.deep.equal(bodyModel.model); + expect(body.fields['body.nestedObject.mixedUnion'].message).to.equal( + 'Could not match the union against any of the items. ' + + 'Issues: [{"body.nestedObject.mixedUnion":{"message":"invalid string value","value":123}},' + + '{"body.nestedObject.mixedUnion":{"message":"invalid object","value":123}}]', + ); }, 400, ); From f289aa2a9b1cf60417a91b05ac921218b05f4728 Mon Sep 17 00:00:00 2001 From: WoH Date: Sat, 24 Aug 2019 16:48:54 +0200 Subject: [PATCH 5/9] nOL Validation: Reuse prop helper --- src/routeGeneration/templateHelpers.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/routeGeneration/templateHelpers.ts b/src/routeGeneration/templateHelpers.ts index 4f5b287c2..b55ec41f9 100644 --- a/src/routeGeneration/templateHelpers.ts +++ b/src/routeGeneration/templateHelpers.ts @@ -85,7 +85,7 @@ export class ValidationService { name: string, value: any, fieldErrors: FieldErrors, - minimalSwaggerConfig: SwaggerConfigRelatedToRoutes, + swaggerConfig: SwaggerConfigRelatedToRoutes, nestedProperties: { [name: string]: TsoaRoute.PropertySchema } | undefined, additionalProperties: TsoaRoute.PropertySchema | boolean | undefined, parent: string, @@ -94,20 +94,20 @@ export class ValidationService { return; } - const propHandling = minimalSwaggerConfig.noImplicitAdditionalProperties; - if (propHandling) { - const excessProps = this.getExcessPropertiesFor({ properties: nestedProperties, additionalProperties }, Object.keys(value), minimalSwaggerConfig); + const propHandling = this.resolveAdditionalPropSetting(swaggerConfig); + if (propHandling !== 'ignore') { + const excessProps = this.getExcessPropertiesFor({ properties: nestedProperties, additionalProperties }, Object.keys(value), swaggerConfig); if (excessProps.length > 0) { - if (propHandling) { + if (propHandling === 'silently-remove-extras') { excessProps.forEach(excessProp => { delete value[excessProp]; }); - if (propHandling === 'throw-on-extras') { - fieldErrors[parent + name] = { - message: `"${excessProps}" is an excess property and therefore is not allowed`, - value: excessProps, - }; - } + } + if (propHandling === 'throw-on-extras') { + fieldErrors[parent + name] = { + message: `"${excessProps}" is an excess property and therefore is not allowed`, + value: excessProps, + }; } } } @@ -115,13 +115,13 @@ export class ValidationService { Object.keys(value).forEach(key => { if (!nestedProperties[key]) { if (additionalProperties && additionalProperties !== true) { - return this.ValidateParam(additionalProperties, value[key], key, fieldErrors, parent + name + '.', minimalSwaggerConfig); + return this.ValidateParam(additionalProperties, value[key], key, fieldErrors, parent + name + '.', swaggerConfig); } else { return key; } } - return this.ValidateParam(nestedProperties[key], value[key], key, fieldErrors, parent + name + '.', minimalSwaggerConfig); + return this.ValidateParam(nestedProperties[key], value[key], key, fieldErrors, parent + name + '.', swaggerConfig); }); return value; From 199a864316871f1fbf41f5fb3fba296dcdc391eb Mon Sep 17 00:00:00 2001 From: WoH Date: Sun, 25 Aug 2019 11:38:23 +0200 Subject: [PATCH 6/9] nOL: assertions, throws, more info - Assert nOL is object - Throw on missing nested properties - Print excess prop value --- src/routeGeneration/templateHelpers.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/routeGeneration/templateHelpers.ts b/src/routeGeneration/templateHelpers.ts index b55ec41f9..43716343b 100644 --- a/src/routeGeneration/templateHelpers.ts +++ b/src/routeGeneration/templateHelpers.ts @@ -90,10 +90,23 @@ export class ValidationService { additionalProperties: TsoaRoute.PropertySchema | boolean | undefined, parent: string, ) { - if (!nestedProperties) { + if (!(value instanceof Object)) { + fieldErrors[parent + name] = { + message: `invalid object`, + value, + }; return; } + if (!nestedProperties) { + throw new Error( + 'internal tsoa error: ' + + 'the metadata that was generated should have had nested property schemas since it’s for a nested object,' + + 'however it did not. ' + + 'Please file an issue with tsoa at https://github.com/lukeautry/tsoa/issues', + ); + } + const propHandling = this.resolveAdditionalPropSetting(swaggerConfig); if (propHandling !== 'ignore') { const excessProps = this.getExcessPropertiesFor({ properties: nestedProperties, additionalProperties }, Object.keys(value), swaggerConfig); @@ -106,7 +119,7 @@ export class ValidationService { if (propHandling === 'throw-on-extras') { fieldErrors[parent + name] = { message: `"${excessProps}" is an excess property and therefore is not allowed`, - value: excessProps, + value: excessProps.reduce((acc, propName) => ({ [propName]: value[propName], ...acc }), {}), }; } } From 8ff14c488aaf192e2548ee044b1449cce00477a2 Mon Sep 17 00:00:00 2001 From: WoH Date: Sun, 25 Aug 2019 11:41:31 +0200 Subject: [PATCH 7/9] nOL: Excess prop validation specs --- tests/unit/templating/templateHelpers.spec.ts | 291 ++++++++++++++++++ 1 file changed, 291 insertions(+) diff --git a/tests/unit/templating/templateHelpers.spec.ts b/tests/unit/templating/templateHelpers.spec.ts index 7f145ad0a..7e558cb35 100644 --- a/tests/unit/templating/templateHelpers.spec.ts +++ b/tests/unit/templating/templateHelpers.spec.ts @@ -169,6 +169,113 @@ it('should throw if the data has additionalProperties (on a intersection) if noI } }); +it('should throw if the data has additionalProperties (on a nested Object) if noImplicitAdditionalProperties is set to throw-on-extras', () => { + // Arrange + const refName = 'ExampleModel'; + const models: TsoaRoute.Models = { + [refName]: { + properties: { + objLiteral: { + dataType: 'nestedObjectLiteral', + nestedProperties: { + nested: { + dataType: 'nestedObjectLiteral', + nestedProperties: { + additionals: { + dataType: 'nestedObjectLiteral', + nestedProperties: {}, + additionalProperties: { + ref: 'TypeAliasModel1', + }, + }, + allNestedOptional: { + dataType: 'nestedObjectLiteral', + nestedProperties: { + two: { + dataType: 'string', + }, + one: { + dataType: 'string', + }, + }, + required: true, + }, + optional: { + dataType: 'double', + }, + bool: { + dataType: 'boolean', + required: true, + }, + }, + }, + name: { dataType: 'string', required: true }, + }, + required: true, + }, + }, + }, + TypeAliasModel1: { + properties: { + value1: { dataType: 'string', required: true }, + }, + additionalProperties: false, + }, + }; + const v = new ValidationService(models); + const minimalSwaggerConfig: SwaggerConfigRelatedToRoutes = { + noImplicitAdditionalProperties: true, + }; + const errorDictionary: FieldErrors = {}; + const dataToValidate = { + name: '', + // extra + extra: 123, + nested: { + bool: true, + allNestedOptional: { + // extra + removed: '123', + }, + additionals: { + one: { value1: 'one' }, + two: { value1: 'two' }, + }, + }, + }; + + // Act + const result = v.validateNestedObjectLiteral('objLiteral', dataToValidate, errorDictionary, minimalSwaggerConfig, models[refName].properties!.objLiteral.nestedProperties, false, refName + '.'); + + // Assert + expect(errorDictionary).to.deep.eq({ + 'ExampleModel.objLiteral': { + message: '"extra" is an excess property and therefore is not allowed', + value: { extra: 123 }, + }, + 'ExampleModel.objLiteral.nested.allNestedOptional': { + message: '"removed" is an excess property and therefore is not allowed', + value: { removed: '123' }, + }, + }); + expect(result).to.eql({ + name: '', + // extra + extra: 123, + nested: { + bool: true, + allNestedOptional: { + // extra + removed: '123', + }, + additionals: { + one: { value1: 'one' }, + two: { value1: 'two' }, + }, + }, + }); +}); + it('should not throw if the data has additionalProperties (on a intersection) if noImplicitAdditionalProperties is set to silently-remove-extras', () => { // Arrange const refName = 'ExampleModel'; @@ -274,3 +381,187 @@ it('should not throw if the data has additionalProperties (on a intersection) if [nameOfAdditionalProperty]: 'something extra', }); }); + +it('should not throw if the data has additionalProperties (on a nested Object) if noImplicitAdditionalProperties is set to silently-remove-extras', () => { + // Arrange + const refName = 'ExampleModel'; + const models: TsoaRoute.Models = { + [refName]: { + properties: { + objLiteral: { + dataType: 'nestedObjectLiteral', + nestedProperties: { + nested: { + dataType: 'nestedObjectLiteral', + nestedProperties: { + additionals: { + dataType: 'nestedObjectLiteral', + nestedProperties: {}, + additionalProperties: { + ref: 'TypeAliasModel1', + }, + }, + allNestedOptional: { + dataType: 'nestedObjectLiteral', + nestedProperties: { + two: { dataType: 'string' }, + one: { + dataType: 'string', + }, + }, + required: true, + }, + optional: { dataType: 'double' }, + bool: { dataType: 'boolean', required: true }, + }, + }, + name: { dataType: 'string', required: true }, + }, + required: true, + }, + }, + }, + TypeAliasModel1: { + properties: { + value1: { dataType: 'string', required: true }, + }, + additionalProperties: false, + }, + }; + const v = new ValidationService(models); + const minimalSwaggerConfig: SwaggerConfigRelatedToRoutes = { + noImplicitAdditionalProperties: 'silently-remove-extras', + }; + const errorDictionary: FieldErrors = {}; + const dataToValidate = { + name: '', + // extra + extra: 123, + nested: { + bool: true, + allNestedOptional: { + // extra + removed: '123', + }, + additionals: { + one: { value1: 'one' }, + two: { value1: 'two' }, + }, + }, + }; + + // Act + const result = v.validateNestedObjectLiteral('objLiteral', dataToValidate, errorDictionary, minimalSwaggerConfig, models[refName].properties!.objLiteral.nestedProperties, false, refName + '.'); + + // Assert + expect(errorDictionary).to.deep.eq({}); + expect(result).to.eql({ + name: '', + nested: { + bool: true, + allNestedOptional: {}, + additionals: { + one: { value1: 'one' }, + two: { value1: 'two' }, + }, + }, + }); +}); + +it('should not throw if the data has additionalProperties (on a nested Object) if noImplicitAdditionalProperties is set to false', () => { + // Arrange + const refName = 'ExampleModel'; + const models: TsoaRoute.Models = { + [refName]: { + properties: { + objLiteral: { + dataType: 'nestedObjectLiteral', + nestedProperties: { + nested: { + dataType: 'nestedObjectLiteral', + nestedProperties: { + additionals: { + dataType: 'nestedObjectLiteral', + nestedProperties: {}, + additionalProperties: { + ref: 'TypeAliasModel1', + }, + }, + allNestedOptional: { + dataType: 'nestedObjectLiteral', + nestedProperties: { + two: { + dataType: 'string', + }, + one: { + dataType: 'string', + }, + }, + required: true, + }, + optional: { + dataType: 'double', + }, + bool: { + dataType: 'boolean', + required: true, + }, + }, + }, + name: { dataType: 'string', required: true }, + }, + required: true, + }, + }, + }, + TypeAliasModel1: { + properties: { + value1: { dataType: 'string', required: true }, + }, + additionalProperties: false, + }, + }; + const v = new ValidationService(models); + const minimalSwaggerConfig: SwaggerConfigRelatedToRoutes = { + noImplicitAdditionalProperties: false, + }; + const errorDictionary: FieldErrors = {}; + const dataToValidate = { + name: '', + // extra + extra: 123, + nested: { + bool: true, + allNestedOptional: { + // extra + removed: '123', + }, + additionals: { + one: { value1: 'one' }, + two: { value1: 'two' }, + }, + }, + }; + + // Act + const result = v.validateNestedObjectLiteral('objLiteral', dataToValidate, errorDictionary, minimalSwaggerConfig, models[refName].properties!.objLiteral.nestedProperties, false, refName + '.'); + + // Assert + expect(errorDictionary).to.deep.eq({}); + expect(result).to.eql({ + name: '', + // extra + extra: 123, + nested: { + bool: true, + allNestedOptional: { + // extra + removed: '123', + }, + additionals: { + one: { value1: 'one' }, + two: { value1: 'two' }, + }, + }, + }); +}); From 852bf0966599f0231731cae6f8a2ce104e03299d Mon Sep 17 00:00:00 2001 From: WoH Date: Sun, 1 Sep 2019 17:45:46 +0200 Subject: [PATCH 8/9] nOL: Don't cast --- src/metadataGeneration/typeResolver.ts | 29 +++++++++++++------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/src/metadataGeneration/typeResolver.ts b/src/metadataGeneration/typeResolver.ts index c968c4ba3..ed0e70044 100644 --- a/src/metadataGeneration/typeResolver.ts +++ b/src/metadataGeneration/typeResolver.ts @@ -94,19 +94,17 @@ export class TypeResolver { .filter(member => ts.isPropertySignature(member)) .reduce((res, propertySignature: ts.PropertySignature) => { const type = new TypeResolver(propertySignature.type as ts.TypeNode, this.current, propertySignature).resolve(); - - return [ - { - default: getJSDocComment(propertySignature, 'default'), - description: this.getNodeDescription(propertySignature), - format: this.getNodeFormat(propertySignature), - name: (propertySignature.name as ts.Identifier).text, - required: !propertySignature.questionToken, - type, - validators: getPropertyValidators(propertySignature), - } as Tsoa.Property, - ...res, - ]; + const property: Tsoa.Property = { + default: getJSDocComment(propertySignature, 'default'), + description: this.getNodeDescription(propertySignature), + format: this.getNodeFormat(propertySignature), + name: (propertySignature.name as ts.Identifier).text, + required: !propertySignature.questionToken, + type, + validators: getPropertyValidators(propertySignature) || {}, + }; + + return [property, ...res]; }, []); const indexMember = this.typeNode.members.find(member => ts.isIndexSignatureDeclaration(member)); @@ -122,11 +120,12 @@ export class TypeResolver { additionalType = new TypeResolver(indexSignatureDeclaration.type as ts.TypeNode, this.current).resolve(); } - return { + const objLiteral: Tsoa.ObjectLiteralType = { additionalProperties: indexMember && additionalType, dataType: 'nestedObjectLiteral', properties, - } as Tsoa.ObjectLiteralType; + }; + return objLiteral; } if (this.typeNode.kind === ts.SyntaxKind.ObjectKeyword) { From d0fe540cc6734d10a10b189fe3c24c4722979505 Mon Sep 17 00:00:00 2001 From: WoH Date: Sun, 1 Sep 2019 18:38:56 +0200 Subject: [PATCH 9/9] nOL: (Nested) Intersection validation specs --- .../dynamic-controllers-express-server.spec.ts | 10 ++++++++++ tests/integration/express-server.spec.ts | 10 ++++++++++ tests/integration/hapi-server.spec.ts | 11 +++++++++++ .../koa-server-no-additional-allowed.spec.ts | 4 ++++ tests/integration/koa-server.spec.ts | 10 ++++++++++ tests/integration/openapi3-express.spec.ts | 10 ++++++++++ 6 files changed, 55 insertions(+) diff --git a/tests/integration/dynamic-controllers-express-server.spec.ts b/tests/integration/dynamic-controllers-express-server.spec.ts index 1455b2ef6..844c88b46 100644 --- a/tests/integration/dynamic-controllers-express-server.spec.ts +++ b/tests/integration/dynamic-controllers-express-server.spec.ts @@ -499,6 +499,7 @@ describe('Express Server', () => { bodyModel.arrayUniqueItem = [0, 1, 2, 3]; bodyModel.model = { value1: 'abcdef' }; bodyModel.mixedUnion = { value1: '' }; + bodyModel.intersection = { value1: 'one', value2: 'two' }; bodyModel.nestedObject = { floatValue: 1.2, @@ -521,6 +522,7 @@ describe('Express Server', () => { arrayUniqueItem: [0, 1, 2, 3], model: { value1: 'abcdef' }, mixedUnion: { value1: '' }, + intersection: { value1: 'one', value2: 'two' }, }; return verifyPostRequest( @@ -550,6 +552,7 @@ describe('Express Server', () => { expect(body.arrayUniqueItem).to.deep.equal(bodyModel.arrayUniqueItem); expect(body.model).to.deep.equal(bodyModel.model); expect(body.mixedUnion).to.deep.equal(bodyModel.mixedUnion); + expect(body.intersection).to.deep.equal(bodyModel.intersection); expect(body.nestedObject.floatValue).to.equal(bodyModel.nestedObject.floatValue); expect(body.nestedObject.doubleValue).to.equal(bodyModel.nestedObject.doubleValue); @@ -572,6 +575,7 @@ describe('Express Server', () => { expect(body.nestedObject.arrayUniqueItem).to.deep.equal(bodyModel.nestedObject.arrayUniqueItem); expect(body.nestedObject.model).to.deep.equal(bodyModel.nestedObject.model); expect(body.nestedObject.mixedUnion).to.deep.equal(bodyModel.nestedObject.mixedUnion); + expect(body.nestedObject.intersection).to.deep.equal(bodyModel.nestedObject.intersection); }, 200, ); @@ -598,6 +602,7 @@ describe('Express Server', () => { bodyModel.arrayUniqueItem = [0, 0, 1, 1]; bodyModel.model = 1 as any; bodyModel.mixedUnion = 123 as any; + bodyModel.intersection = { value1: 'one' } as any; bodyModel.nestedObject = { floatValue: '120a' as any, @@ -619,6 +624,7 @@ describe('Express Server', () => { arrayUniqueItem: [0, 0, 1, 1], model: 1 as any, mixedUnion: 123 as any, + intersection: { value1: 'one' } as any, } as any; return verifyPostRequest( @@ -667,6 +673,7 @@ describe('Express Server', () => { 'Issues: [{"body.mixedUnion":{"message":"invalid string value","value":123}},' + '{"body.mixedUnion":{"message":"invalid object","value":123}}]', ); + expect(body.fields['body.intersection'].message).to.equal('Could not match the intersection against every type. Issues: [{"body.value2":{"message":"\'value2\' is required"}}]'); expect(body.fields['body.nestedObject.floatValue'].message).to.equal('Invalid float error message.'); expect(body.fields['body.nestedObject.floatValue'].value).to.equal(bodyModel.floatValue); @@ -708,6 +715,9 @@ describe('Express Server', () => { 'Issues: [{"body.nestedObject.mixedUnion":{"message":"invalid string value","value":123}},' + '{"body.nestedObject.mixedUnion":{"message":"invalid object","value":123}}]', ); + expect(body.fields['body.nestedObject.intersection'].message).to.equal( + 'Could not match the intersection against every type. Issues: [{"body.nestedObject.value2":{"message":"\'value2\' is required"}}]', + ); }, 400, ); diff --git a/tests/integration/express-server.spec.ts b/tests/integration/express-server.spec.ts index 219c02b6d..1d1de3609 100644 --- a/tests/integration/express-server.spec.ts +++ b/tests/integration/express-server.spec.ts @@ -519,6 +519,7 @@ describe('Express Server', () => { bodyModel.arrayUniqueItem = [0, 1, 2, 3]; bodyModel.model = { value1: 'abcdef' }; bodyModel.mixedUnion = { value1: '' }; + bodyModel.intersection = { value1: 'one', value2: 'two' }; bodyModel.nestedObject = { floatValue: 1.2, @@ -541,6 +542,7 @@ describe('Express Server', () => { arrayUniqueItem: [0, 1, 2, 3], model: { value1: 'abcdef' }, mixedUnion: { value1: '' }, + intersection: { value1: 'one', value2: 'two' }, }; return verifyPostRequest( @@ -570,6 +572,7 @@ describe('Express Server', () => { expect(body.arrayUniqueItem).to.deep.equal(bodyModel.arrayUniqueItem); expect(body.model).to.deep.equal(bodyModel.model); expect(body.mixedUnion).to.deep.equal(bodyModel.mixedUnion); + expect(body.intersection).to.deep.equal(bodyModel.intersection); expect(body.nestedObject.floatValue).to.equal(bodyModel.nestedObject.floatValue); expect(body.nestedObject.doubleValue).to.equal(bodyModel.nestedObject.doubleValue); @@ -592,6 +595,7 @@ describe('Express Server', () => { expect(body.nestedObject.arrayUniqueItem).to.deep.equal(bodyModel.nestedObject.arrayUniqueItem); expect(body.nestedObject.model).to.deep.equal(bodyModel.nestedObject.model); expect(body.nestedObject.mixedUnion).to.deep.equal(bodyModel.nestedObject.mixedUnion); + expect(body.nestedObject.intersection).to.deep.equal(bodyModel.nestedObject.intersection); }, 200, ); @@ -618,6 +622,7 @@ describe('Express Server', () => { bodyModel.arrayUniqueItem = [0, 0, 1, 1]; bodyModel.model = 1 as any; bodyModel.mixedUnion = 123 as any; + bodyModel.intersection = { value1: 'one' } as any; bodyModel.nestedObject = { floatValue: '120a' as any, @@ -639,6 +644,7 @@ describe('Express Server', () => { arrayUniqueItem: [0, 0, 1, 1], model: 1 as any, mixedUnion: 123 as any, + intersection: { value1: 'one' } as any, } as any; return verifyPostRequest( @@ -687,6 +693,7 @@ describe('Express Server', () => { 'Issues: [{"body.mixedUnion":{"message":"invalid string value","value":123}},' + '{"body.mixedUnion":{"message":"invalid object","value":123}}]', ); + expect(body.fields['body.intersection'].message).to.equal('Could not match the intersection against every type. Issues: [{"body.value2":{"message":"\'value2\' is required"}}]'); expect(body.fields['body.nestedObject.floatValue'].message).to.equal('Invalid float error message.'); expect(body.fields['body.nestedObject.floatValue'].value).to.equal(bodyModel.floatValue); @@ -728,6 +735,9 @@ describe('Express Server', () => { 'Issues: [{"body.nestedObject.mixedUnion":{"message":"invalid string value","value":123}},' + '{"body.nestedObject.mixedUnion":{"message":"invalid object","value":123}}]', ); + expect(body.fields['body.nestedObject.intersection'].message).to.equal( + 'Could not match the intersection against every type. Issues: [{"body.nestedObject.value2":{"message":"\'value2\' is required"}}]', + ); }, 400, ); diff --git a/tests/integration/hapi-server.spec.ts b/tests/integration/hapi-server.spec.ts index 043e02269..052edb0e5 100644 --- a/tests/integration/hapi-server.spec.ts +++ b/tests/integration/hapi-server.spec.ts @@ -472,6 +472,7 @@ describe('Hapi Server', () => { bodyModel.arrayUniqueItem = [0, 1, 2, 3]; bodyModel.model = { value1: 'abcdef' }; bodyModel.mixedUnion = { value1: '' }; + bodyModel.intersection = { value1: 'one', value2: 'two' }; bodyModel.nestedObject = { floatValue: 1.2, @@ -494,6 +495,7 @@ describe('Hapi Server', () => { arrayUniqueItem: [0, 1, 2, 3], model: { value1: 'abcdef' }, mixedUnion: { value1: '' }, + intersection: { value1: 'one', value2: 'two' }, }; return verifyPostRequest( @@ -523,6 +525,7 @@ describe('Hapi Server', () => { expect(body.arrayUniqueItem).to.deep.equal(bodyModel.arrayUniqueItem); expect(body.model).to.deep.equal(bodyModel.model); expect(body.mixedUnion).to.deep.equal(bodyModel.mixedUnion); + expect(body.intersection).to.deep.equal(bodyModel.intersection); expect(body.nestedObject.floatValue).to.equal(bodyModel.nestedObject.floatValue); expect(body.nestedObject.doubleValue).to.equal(bodyModel.nestedObject.doubleValue); @@ -545,6 +548,7 @@ describe('Hapi Server', () => { expect(body.nestedObject.arrayUniqueItem).to.deep.equal(bodyModel.nestedObject.arrayUniqueItem); expect(body.nestedObject.model).to.deep.equal(bodyModel.nestedObject.model); expect(body.nestedObject.mixedUnion).to.deep.equal(bodyModel.nestedObject.mixedUnion); + expect(body.nestedObject.intersection).to.deep.equal(bodyModel.nestedObject.intersection); }, 200, ); @@ -571,6 +575,7 @@ describe('Hapi Server', () => { bodyModel.arrayUniqueItem = [0, 0, 1, 1]; bodyModel.model = 1 as any; bodyModel.mixedUnion = 123 as any; + bodyModel.intersection = { value1: 'one' } as any; bodyModel.nestedObject = { floatValue: '120a' as any, @@ -592,6 +597,7 @@ describe('Hapi Server', () => { arrayUniqueItem: [0, 0, 1, 1], model: 1 as any, mixedUnion: 123 as any, + intersection: { value1: 'one' } as any, } as any; return verifyPostRequest( @@ -640,6 +646,8 @@ describe('Hapi Server', () => { 'Issues: [{"body.mixedUnion":{"message":"invalid string value","value":123}},' + '{"body.mixedUnion":{"message":"invalid object","value":123}}]', ); + expect(body.fields['body.intersection'].message).to.equal('Could not match the intersection against every type. Issues: [{"body.value2":{"message":"\'value2\' is required"}}]'); + expect(body.fields['body.nestedObject.floatValue'].message).to.equal('Invalid float error message.'); expect(body.fields['body.nestedObject.floatValue'].value).to.equal(bodyModel.floatValue); expect(body.fields['body.nestedObject.doubleValue'].message).to.equal('Invalid double error message.'); @@ -680,6 +688,9 @@ describe('Hapi Server', () => { 'Issues: [{"body.nestedObject.mixedUnion":{"message":"invalid string value","value":123}},' + '{"body.nestedObject.mixedUnion":{"message":"invalid object","value":123}}]', ); + expect(body.fields['body.nestedObject.intersection'].message).to.equal( + 'Could not match the intersection against every type. Issues: [{"body.nestedObject.value2":{"message":"\'value2\' is required"}}]', + ); }, 400, ); diff --git a/tests/integration/koa-server-no-additional-allowed.spec.ts b/tests/integration/koa-server-no-additional-allowed.spec.ts index 429686e86..7eefc78da 100644 --- a/tests/integration/koa-server-no-additional-allowed.spec.ts +++ b/tests/integration/koa-server-no-additional-allowed.spec.ts @@ -194,6 +194,7 @@ describe('Koa Server (with noImplicitAdditionalProperties turned on)', () => { bodyModel.arrayUniqueItem = [0, 1, 2, 3]; bodyModel.model = { value1: 'abcdef' }; bodyModel.mixedUnion = { value1: '' }; + bodyModel.intersection = { value1: 'one', value2: 'two' }; bodyModel.nestedObject = { floatValue: 1.2, @@ -216,6 +217,7 @@ describe('Koa Server (with noImplicitAdditionalProperties turned on)', () => { arrayUniqueItem: [0, 1, 2, 3], model: { value1: 'abcdef' }, mixedUnion: { value1: '' }, + intersection: { value1: 'one', value2: 'two' }, }; return verifyPostRequest( @@ -245,6 +247,7 @@ describe('Koa Server (with noImplicitAdditionalProperties turned on)', () => { expect(body.arrayUniqueItem).to.deep.equal(bodyModel.arrayUniqueItem); expect(body.model).to.deep.equal(bodyModel.model); expect(body.mixedUnion).to.deep.equal(bodyModel.mixedUnion); + expect(body.intersection).to.deep.equal(bodyModel.intersection); expect(body.nestedObject.floatValue).to.equal(bodyModel.nestedObject.floatValue); expect(body.nestedObject.doubleValue).to.equal(bodyModel.nestedObject.doubleValue); @@ -267,6 +270,7 @@ describe('Koa Server (with noImplicitAdditionalProperties turned on)', () => { expect(body.nestedObject.arrayUniqueItem).to.deep.equal(bodyModel.nestedObject.arrayUniqueItem); expect(body.nestedObject.model).to.deep.equal(bodyModel.nestedObject.model); expect(body.nestedObject.mixedUnion).to.deep.equal(bodyModel.nestedObject.mixedUnion); + expect(body.nestedObject.intersection).to.deep.equal(bodyModel.nestedObject.intersection); }, 200, ); diff --git a/tests/integration/koa-server.spec.ts b/tests/integration/koa-server.spec.ts index 734c201e4..86b67a5c0 100644 --- a/tests/integration/koa-server.spec.ts +++ b/tests/integration/koa-server.spec.ts @@ -450,6 +450,7 @@ describe('Koa Server', () => { bodyModel.arrayUniqueItem = [0, 1, 2, 3]; bodyModel.model = { value1: 'abcdef' }; bodyModel.mixedUnion = { value1: '' }; + bodyModel.intersection = { value1: 'one', value2: 'two' }; bodyModel.nestedObject = { floatValue: 1.2, @@ -472,6 +473,7 @@ describe('Koa Server', () => { arrayUniqueItem: [0, 1, 2, 3], model: { value1: 'abcdef' }, mixedUnion: { value1: '' }, + intersection: { value1: 'one', value2: 'two' }, }; return verifyPostRequest( @@ -501,6 +503,7 @@ describe('Koa Server', () => { expect(body.arrayUniqueItem).to.deep.equal(bodyModel.arrayUniqueItem); expect(body.model).to.deep.equal(bodyModel.model); expect(body.mixedUnion).to.deep.equal(bodyModel.mixedUnion); + expect(body.intersection).to.deep.equal(bodyModel.intersection); expect(body.nestedObject.floatValue).to.equal(bodyModel.nestedObject.floatValue); expect(body.nestedObject.doubleValue).to.equal(bodyModel.nestedObject.doubleValue); @@ -523,6 +526,7 @@ describe('Koa Server', () => { expect(body.nestedObject.arrayUniqueItem).to.deep.equal(bodyModel.nestedObject.arrayUniqueItem); expect(body.nestedObject.model).to.deep.equal(bodyModel.nestedObject.model); expect(body.nestedObject.mixedUnion).to.deep.equal(bodyModel.nestedObject.mixedUnion); + expect(body.nestedObject.intersection).to.deep.equal(bodyModel.nestedObject.intersection); }, 200, ); @@ -549,6 +553,7 @@ describe('Koa Server', () => { bodyModel.arrayUniqueItem = [0, 0, 1, 1]; bodyModel.model = 1 as any; bodyModel.mixedUnion = 123 as any; + bodyModel.intersection = { value1: 'one' } as any; bodyModel.nestedObject = { floatValue: '120a' as any, @@ -570,6 +575,7 @@ describe('Koa Server', () => { arrayUniqueItem: [0, 0, 1, 1], model: 1 as any, mixedUnion: 123 as any, + intersection: { value1: 'one' } as any, } as any; return verifyPostRequest( @@ -618,6 +624,7 @@ describe('Koa Server', () => { 'Issues: [{"body.mixedUnion":{"message":"invalid string value","value":123}},' + '{"body.mixedUnion":{"message":"invalid object","value":123}}]', ); + expect(body.fields['body.intersection'].message).to.equal('Could not match the intersection against every type. Issues: [{"body.value2":{"message":"\'value2\' is required"}}]'); expect(body.fields['body.nestedObject.floatValue'].message).to.equal('Invalid float error message.'); expect(body.fields['body.nestedObject.floatValue'].value).to.equal(bodyModel.floatValue); @@ -659,6 +666,9 @@ describe('Koa Server', () => { 'Issues: [{"body.nestedObject.mixedUnion":{"message":"invalid string value","value":123}},' + '{"body.nestedObject.mixedUnion":{"message":"invalid object","value":123}}]', ); + expect(body.fields['body.nestedObject.intersection'].message).to.equal( + 'Could not match the intersection against every type. Issues: [{"body.nestedObject.value2":{"message":"\'value2\' is required"}}]', + ); }, 400, ); diff --git a/tests/integration/openapi3-express.spec.ts b/tests/integration/openapi3-express.spec.ts index 2704ca961..c0b9b63fb 100644 --- a/tests/integration/openapi3-express.spec.ts +++ b/tests/integration/openapi3-express.spec.ts @@ -29,6 +29,7 @@ describe('OpenAPI3 Express Server', () => { bodyModel.arrayUniqueItem = [0, 1, 2, 3]; bodyModel.model = { value1: 'abcdef' }; bodyModel.mixedUnion = { value1: '' }; + bodyModel.intersection = { value1: 'one', value2: 'two' }; bodyModel.nestedObject = { floatValue: 1.2, @@ -51,6 +52,7 @@ describe('OpenAPI3 Express Server', () => { arrayUniqueItem: [0, 1, 2, 3], model: { value1: 'abcdef' }, mixedUnion: { value1: '' }, + intersection: { value1: 'one', value2: 'two' }, }; return verifyPostRequest( @@ -80,6 +82,7 @@ describe('OpenAPI3 Express Server', () => { expect(body.arrayUniqueItem).to.deep.equal(bodyModel.arrayUniqueItem); expect(body.model).to.deep.equal(bodyModel.model); expect(body.mixedUnion).to.deep.equal(bodyModel.mixedUnion); + expect(body.intersection).to.deep.equal(bodyModel.intersection); expect(body.nestedObject.floatValue).to.equal(bodyModel.nestedObject.floatValue); expect(body.nestedObject.doubleValue).to.equal(bodyModel.nestedObject.doubleValue); @@ -102,6 +105,7 @@ describe('OpenAPI3 Express Server', () => { expect(body.nestedObject.arrayUniqueItem).to.deep.equal(bodyModel.nestedObject.arrayUniqueItem); expect(body.nestedObject.model).to.deep.equal(bodyModel.nestedObject.model); expect(body.nestedObject.mixedUnion).to.deep.equal(bodyModel.nestedObject.mixedUnion); + expect(body.nestedObject.intersection).to.deep.equal(bodyModel.nestedObject.intersection); }, 200, ); @@ -130,6 +134,7 @@ describe('OpenAPI3 Express Server', () => { bodyModel.intersectionNoAdditional = { value1: '', value2: '', value3: 123, value4: 123 } as any; bodyModel.model = 1 as any; bodyModel.mixedUnion = 123 as any; + bodyModel.intersection = { value1: 'one' } as any; bodyModel.nestedObject = { floatValue: '120a' as any, @@ -151,6 +156,7 @@ describe('OpenAPI3 Express Server', () => { arrayUniqueItem: [0, 0, 1, 1], model: 1 as any, mixedUnion: 123 as any, + intersection: { value1: 'one' } as any, } as any; return verifyPostRequest( @@ -201,6 +207,7 @@ describe('OpenAPI3 Express Server', () => { 'Issues: [{"body.mixedUnion":{"message":"invalid string value","value":123}},' + '{"body.mixedUnion":{"message":"invalid object","value":123}}]', ); + expect(body.fields['body.intersection'].message).to.equal('Could not match the intersection against every type. Issues: [{"body.value2":{"message":"\'value2\' is required"}}]'); expect(body.fields['body.nestedObject.floatValue'].message).to.equal('Invalid float error message.'); expect(body.fields['body.nestedObject.floatValue'].value).to.equal(bodyModel.floatValue); @@ -242,6 +249,9 @@ describe('OpenAPI3 Express Server', () => { 'Issues: [{"body.nestedObject.mixedUnion":{"message":"invalid string value","value":123}},' + '{"body.nestedObject.mixedUnion":{"message":"invalid object","value":123}}]', ); + expect(body.fields['body.nestedObject.intersection'].message).to.equal( + 'Could not match the intersection against every type. Issues: [{"body.nestedObject.value2":{"message":"\'value2\' is required"}}]', + ); }, 400, );