diff --git a/src/metadataGeneration/tsoa.ts b/src/metadataGeneration/tsoa.ts index a9fbb8c60..31c90c569 100644 --- a/src/metadataGeneration/tsoa.ts +++ b/src/metadataGeneration/tsoa.ts @@ -87,12 +87,13 @@ export namespace Tsoa { | 'any' | 'refEnum' | 'refObject' + | 'nestedObjectLiteral' | 'union' | 'intersection'; export type RefTypeLiteral = 'refObject' | 'refEnum'; - export type PrimitiveTypeLiteral = Exclude; + export type PrimitiveTypeLiteral = Exclude; export interface Type { dataType: TypeStringLiteral; @@ -107,6 +108,12 @@ export namespace Tsoa { enums: string[]; } + export interface ObjectLiteralType extends Type { + dataType: 'nestedObjectLiteral'; + properties: Property[]; + additionalProperties?: Type; + } + export interface ArrayType extends Type { dataType: 'array'; elementType: Type; diff --git a/src/metadataGeneration/typeResolver.ts b/src/metadataGeneration/typeResolver.ts index 6c0d22322..ed0e70044 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) {} @@ -89,8 +89,43 @@ export class TypeResolver { return { dataType: 'any' } as Tsoa.Type; } - if (this.typeNode.kind === ts.SyntaxKind.TypeLiteral) { - return { dataType: 'any' } as Tsoa.Type; + 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, propertySignature).resolve(); + 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)); + 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(); + } + + const objLiteral: Tsoa.ObjectLiteralType = { + additionalProperties: indexMember && additionalType, + dataType: 'nestedObjectLiteral', + properties, + }; + return objLiteral; } if (this.typeNode.kind === ts.SyntaxKind.ObjectKeyword) { @@ -492,7 +527,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( @@ -694,7 +729,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..43716343b 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,65 @@ export class ValidationService { } } + public validateNestedObjectLiteral( + name: string, + value: any, + fieldErrors: FieldErrors, + swaggerConfig: SwaggerConfigRelatedToRoutes, + nestedProperties: { [name: string]: TsoaRoute.PropertySchema } | undefined, + additionalProperties: TsoaRoute.PropertySchema | boolean | undefined, + parent: string, + ) { + 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); + if (excessProps.length > 0) { + 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.reduce((acc, propName) => ({ [propName]: value[propName], ...acc }), {}), + }; + } + } + } + + Object.keys(value).forEach(key => { + if (!nestedProperties[key]) { + if (additionalProperties && additionalProperties !== true) { + 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 + '.', swaggerConfig); + }); + + 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 9fbfdbe62..65175d001 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 === 'nestedObjectLiteral') { + 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/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/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..30e09173f 100644 --- a/tests/fixtures/testModel.ts +++ b/tests/fixtures/testModel.ts @@ -75,6 +75,21 @@ export interface TestModel extends Model { genericNestedArrayKeyword2?: GenericRequest>; genericNestedArrayCharacter2?: GenericRequest; mixedUnion?: string | TypeAliasModel1; + + objLiteral: { + name: string; + nested?: { + bool: boolean; + optional?: number; + allNestedOptional: { + one?: string; + two?: string; + }; + additionals?: { + [name: string]: TypeAliasModel1; + }; + }; + }; } export interface TypeAliasModel1 { @@ -268,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 { @@ -333,12 +422,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..844c88b46 100644 --- a/tests/integration/dynamic-controllers-express-server.spec.ts +++ b/tests/integration/dynamic-controllers-express-server.spec.ts @@ -499,6 +499,31 @@ 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, + 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: '' }, + intersection: { value1: 'one', value2: 'two' }, + }; return verifyPostRequest( basePath + `/Validate/body`, @@ -527,6 +552,30 @@ 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); + 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); + expect(body.nestedObject.intersection).to.deep.equal(bodyModel.nestedObject.intersection); }, 200, ); @@ -553,6 +602,30 @@ 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, + 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, + intersection: { value1: 'one' } as any, + } as any; return verifyPostRequest( basePath + `/Validate/body`, @@ -600,6 +673,51 @@ 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); + 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}}]', + ); + 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, ); @@ -937,6 +1055,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..1d1de3609 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'; @@ -499,6 +519,31 @@ 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, + 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: '' }, + intersection: { value1: 'one', value2: 'two' }, + }; return verifyPostRequest( basePath + `/Validate/body`, @@ -527,6 +572,30 @@ 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); + 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); + expect(body.nestedObject.intersection).to.deep.equal(bodyModel.nestedObject.intersection); }, 200, ); @@ -553,6 +622,30 @@ 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, + 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, + intersection: { value1: 'one' } as any, + } as any; return verifyPostRequest( basePath + `/Validate/body`, @@ -600,6 +693,51 @@ 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); + 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}}]', + ); + 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, ); @@ -937,6 +1075,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..052edb0e5 100644 --- a/tests/integration/hapi-server.spec.ts +++ b/tests/integration/hapi-server.spec.ts @@ -472,6 +472,31 @@ 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, + 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: '' }, + intersection: { value1: 'one', value2: 'two' }, + }; return verifyPostRequest( basePath + `/Validate/body`, @@ -500,6 +525,30 @@ 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); + 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); + expect(body.nestedObject.intersection).to.deep.equal(bodyModel.nestedObject.intersection); }, 200, ); @@ -526,6 +575,30 @@ 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, + 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, + intersection: { value1: 'one' } as any, + } as any; return verifyPostRequest( basePath + `/Validate/body`, @@ -573,6 +646,51 @@ 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.'); + 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}}]', + ); + 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, ); @@ -866,6 +984,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..7eefc78da 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(); @@ -149,6 +194,31 @@ 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, + 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: '' }, + intersection: { value1: 'one', value2: 'two' }, + }; return verifyPostRequest( basePath + `/Validate/body`, @@ -177,6 +247,30 @@ 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); + 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); + expect(body.nestedObject.intersection).to.deep.equal(bodyModel.nestedObject.intersection); }, 200, ); @@ -355,6 +449,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..86b67a5c0 100644 --- a/tests/integration/koa-server.spec.ts +++ b/tests/integration/koa-server.spec.ts @@ -450,6 +450,31 @@ 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, + 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: '' }, + intersection: { value1: 'one', value2: 'two' }, + }; return verifyPostRequest( basePath + `/Validate/body`, @@ -478,6 +503,30 @@ 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); + 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); + expect(body.nestedObject.intersection).to.deep.equal(bodyModel.nestedObject.intersection); }, 200, ); @@ -504,6 +553,30 @@ 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, + 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, + intersection: { value1: 'one' } as any, + } as any; return verifyPostRequest( basePath + `/Validate/body`, @@ -551,6 +624,51 @@ 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); + 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}}]', + ); + 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, ); @@ -851,6 +969,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/integration/openapi3-express.spec.ts b/tests/integration/openapi3-express.spec.ts index 56ea93b63..c0b9b63fb 100644 --- a/tests/integration/openapi3-express.spec.ts +++ b/tests/integration/openapi3-express.spec.ts @@ -29,6 +29,31 @@ 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, + 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: '' }, + intersection: { value1: 'one', value2: 'two' }, + }; return verifyPostRequest( basePath + `/Validate/body`, @@ -57,6 +82,30 @@ 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); + 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); + expect(body.nestedObject.intersection).to.deep.equal(bodyModel.nestedObject.intersection); }, 200, ); @@ -85,6 +134,30 @@ 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, + 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, + intersection: { value1: 'one' } as any, + } as any; return verifyPostRequest( basePath + `/Validate/body`, @@ -134,6 +207,51 @@ 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); + 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}}]', + ); + 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/unit/swagger/definitionsGeneration/definitions.spec.ts b/tests/unit/swagger/definitionsGeneration/definitions.spec.ts index 7cedc6c71..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 */ @@ -422,6 +421,31 @@ 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.equal({ + default: undefined, + description: undefined, + format: undefined, + properties: { + name: { type: 'string' }, + nested: { + properties: { + additionals: { properties: {}, type: 'object' }, + allNestedOptional: { + properties: { one: { type: 'string' }, two: { type: 'string' } }, + type: 'object', + }, + bool: { type: 'boolean' }, + optional: { type: 'number', format: 'double' }, + }, + required: ['allNestedOptional', 'bool'], + type: 'object', + }, + }, + required: ['name'], + type: 'object', + }); + }, }; Object.keys(assertionsPerProperty).forEach(aPropertyName => { @@ -438,6 +462,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..6a079e601 100644 --- a/tests/unit/swagger/schemaDetails3.spec.ts +++ b/tests/unit/swagger/schemaDetails3.spec.ts @@ -214,6 +214,33 @@ 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: { + additionals: { + properties: {}, + type: 'object', + }, + allNestedOptional: { + properties: { one: { type: 'string' }, two: { type: 'string' } }, + type: 'object', + }, + bool: { type: 'boolean' }, + optional: { format: 'double', type: 'number' }, + }, + required: ['allNestedOptional', 'bool'], + type: 'object', + }, + }, + required: ['name'], + type: 'object', + }); + }, object: (propertyName, propertySchema) => { expect(propertySchema.type).to.eq('object', `for property ${propertyName}`); if (currentSpec.specName === 'specWithNoImplicitExtras') { 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' }, + }, + }, + }); +});