test(common): add test for flattening children errors (validation)

This commit is contained in:
Kamil Myśliwiec
2020-03-26 17:52:39 +01:00
2 changed files with 92 additions and 11 deletions

View File

@@ -120,12 +120,7 @@ export class ValidationPipe implements PipeTransform<any> {
if (this.isDetailedOutputDisabled) {
return new HttpErrorByCode[this.errorHttpStatusCode]();
}
const errors = iterate(validationErrors)
.filter(item => !!item.constraints)
.map(item => Object.values(item.constraints))
.flatten()
.toArray();
const errors = this.flattenValidationErrors(validationErrors);
return new HttpErrorByCode[this.errorHttpStatusCode](errors);
};
}
@@ -172,4 +167,46 @@ export class ValidationPipe implements PipeTransform<any> {
private isPrimitive(value: unknown): boolean {
return ['number', 'boolean', 'string'].includes(typeof value);
}
private flattenValidationErrors(
validationErrors: ValidationError[],
): string[] {
return iterate(validationErrors)
.map(error => this.mapChildrenToValidationErrors(error))
.flatten()
.filter(item => !!item.constraints)
.map(item => Object.values(item.constraints))
.flatten()
.toArray();
}
private mapChildrenToValidationErrors(
error: ValidationError,
): ValidationError[] {
if (!(error.children && error.children.length)) {
return [error];
}
const validationErrors = [];
for (const item of error.children) {
if (item.children && item.children.length) {
validationErrors.push(...this.mapChildrenToValidationErrors(item));
}
validationErrors.push(this.prependConstraintsWithParentProp(error, item));
}
return validationErrors;
}
private prependConstraintsWithParentProp(
parentError: ValidationError,
error: ValidationError,
): ValidationError {
const constraints = {};
for (const key in error.constraints) {
constraints[key] = `${parentError.property}.${error.constraints[key]}`;
}
return {
...error,
constraints,
};
}
}

View File

@@ -1,8 +1,14 @@
import * as chai from 'chai';
import { expect } from 'chai';
import * as chaiAsPromised from 'chai-as-promised';
import { Exclude, Expose } from 'class-transformer';
import { IsOptional, IsString } from 'class-validator';
import { Exclude, Expose, Type } from 'class-transformer';
import {
IsBoolean,
IsDefined,
IsOptional,
IsString,
ValidateNested,
} from 'class-validator';
import { HttpStatus } from '../../enums';
import { UnprocessableEntityException } from '../../exceptions';
import { ArgumentMetadata } from '../../interfaces';
@@ -27,10 +33,11 @@ class TestModelInternal {
}
class TestModel {
constructor() {}
@IsString() public prop1: string;
@IsString()
public prop1: string;
@IsString() public prop2: string;
@IsString()
public prop2: string;
@IsOptional()
@IsString()
@@ -112,6 +119,43 @@ describe('ValidationPipe', () => {
const testObj = { prop1: 'value1' };
return expect(target.transform(testObj, metadata)).to.be.rejected;
});
class TestModel2 {
@IsString()
public prop1: string;
@IsBoolean()
public prop2: string;
@IsOptional()
@IsString()
public optionalProp: string;
}
class TestModelWithNested {
@IsString()
prop: string;
@IsDefined()
@Type(() => TestModel2)
@ValidateNested()
test: TestModel2;
}
it('should flatten nested errors', async () => {
try {
const model = new TestModelWithNested();
model.test = new TestModel2();
await target.transform(model, {
type: 'body',
metatype: TestModelWithNested,
});
} catch (err) {
expect(err.getResponse().message).to.be.eql([
'prop must be a string',
'test.prop1 must be a string',
'test.prop2 must be a boolean value',
]);
}
});
});
describe('when validation transforms', () => {
it('should return a TestModel instance', async () => {