Skip to content

Commit ace6831

Browse files
authored
Merge pull request #31 from Zooz/fix/30-text-field-validation
refs: #30 Clarify readme * Test for a combination of files and fields * Add optional param for test * Extract logic into an internal function. Add comments
2 parents 7c2cb34 + d7365c0 commit ace6831

7 files changed

Lines changed: 162 additions & 22 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ out/
102102

103103
# mpeltonen/sbt-idea plugin
104104
.idea_modules/
105+
.idea
105106

106107
# JIRA plugin
107108
atlassian-ide-plugin.xml
@@ -116,4 +117,4 @@ crashlytics-build.properties
116117
fabric.properties
117118

118119
# Mocha Awsome
119-
mochawesome-report/*
120+
mochawesome-report/*

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ Options currently supports:
7878
- `ajvConfigBody` - Object that will be passed as config to new Ajv instance which will be used for validating request body. Can be useful to e. g. enable type coercion (to automatically convert strings to numbers etc). See Ajv documentation for supported values.
7979
- `ajvConfigParams` - Object that will be passed as config to new Ajv instance which will be used for validating request body. See Ajv documentation for supported values.
8080
- `contentTypeValidation` - Boolean that indicates if to perform content type validation in case `consume` field is specified and the request body is not empty.
81+
- `expectFormFieldsInBody` - Boolean that indicates whether form fields of non-file type that are specified in the schema should be validated against request body (e. g. Multer is copying text form fields to body)
8182

8283
```js
8384
formats: [

src/middleware.js

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ var SwaggerParser = require('swagger-parser'),
66
filesKeyword = require('./customKeywords/files'),
77
contentKeyword = require('./customKeywords/contentTypeValidation'),
88
InputValidationError = require('./inputValidationError'),
9-
schemaPreprocessor = require('./utils/schema-preprocessor');
9+
schemaPreprocessor = require('./utils/schema-preprocessor'),
10+
sourceResolver = require('./utils/sourceResolver');
1011

1112
var schemas = {};
1213
var middlewareOptions;
@@ -16,7 +17,7 @@ var ajvConfigParams;
1617
/**
1718
* Initialize the input validation middleware
1819
* @param {string} swaggerPath - the path for the swagger file
19-
* @param {Object} options - options.formats to add formats to ajv, options.beautifyErrors, options.firstError, options.fileNameField (default is 'fieldname' - multer package), options.ajvConfigBody and options.ajvConfigParams for config object that will be passed for creation of Ajv instance used for validation of body and parameters appropriately
20+
* @param {Object} options - options.formats to add formats to ajv, options.beautifyErrors, options.firstError, options.expectFormFieldsInBody, options.fileNameField (default is 'fieldname' - multer package), options.ajvConfigBody and options.ajvConfigParams for config object that will be passed for creation of Ajv instance used for validation of body and parameters appropriately
2021
*/
2122
function init(swaggerPath, options) {
2223
middlewareOptions = options || {};
@@ -28,7 +29,7 @@ function init(swaggerPath, options) {
2829
SwaggerParser.dereference(swaggerPath),
2930
SwaggerParser.parse(swaggerPath)
3031
]).then(function (swaggers) {
31-
var dereferenced = swaggers[0];
32+
const dereferenced = swaggers[0];
3233
Object.keys(dereferenced.paths).forEach(function (currentPath) {
3334
let pathParameters = dereferenced.paths[currentPath].parameters || [];
3435
let parsedPath = dereferenced.basePath && dereferenced.basePath !== '/' ? dereferenced.basePath.concat(currentPath.replace(/{/g, ':').replace(/}/g, '')) : currentPath.replace(/{/g, ':').replace(/}/g, '');
@@ -38,22 +39,23 @@ function init(swaggerPath, options) {
3839
schemas[parsedPath][currentMethod.toLowerCase()] = {};
3940

4041
const parameters = dereferenced.paths[currentPath][currentMethod].parameters || [];
41-
let bodySchema = parameters.filter(function (parameter) { return parameter.in === 'body' });
42+
43+
let bodySchema = middlewareOptions.expectFormFieldsInBody
44+
? parameters.filter(function (parameter) { return (parameter.in === 'body' || (parameter.in === 'formData' && parameter.type !== 'file')) })
45+
: parameters.filter(function (parameter) { return parameter.in === 'body' });
46+
4247
if (makeOptionalAttributesNullable) {
4348
schemaPreprocessor.makeOptionalAttributesNullable(bodySchema);
4449
}
4550
if (bodySchema.length > 0) {
46-
schemas[parsedPath][currentMethod].body = buildBodyValidation(bodySchema[0].schema, dereferenced.definitions, swaggers[1], currentPath, currentMethod, parsedPath);
51+
const validatedBodySchema = _getValidatedBodySchema(bodySchema);
52+
schemas[parsedPath][currentMethod].body = buildBodyValidation(validatedBodySchema, dereferenced.definitions, swaggers[1], currentPath, currentMethod, parsedPath);
4753
}
4854

4955
let localParameters = parameters.filter(function (parameter) {
5056
return parameter.in !== 'body';
5157
}).concat(pathParameters);
5258

53-
if (bodySchema.length > 0) {
54-
schemas[parsedPath][currentMethod].body = buildBodyValidation(bodySchema[0].schema, dereferenced.definitions, swaggers[1], currentPath, currentMethod, parsedPath);
55-
}
56-
5759
if (localParameters.length > 0 || middlewareOptions.contentTypeValidation) {
5860
schemas[parsedPath][currentMethod].parameters = buildParametersValidation(localParameters,
5961
dereferenced.paths[currentPath][currentMethod].consumes || dereferenced.paths[currentPath].consumes || dereferenced.consumes);
@@ -66,6 +68,31 @@ function init(swaggerPath, options) {
6668
});
6769
}
6870

71+
function _getValidatedBodySchema(bodySchema) {
72+
let validatedBodySchema;
73+
if (bodySchema[0].in === 'body') {
74+
// if we are processing schema for a simple JSON payload, no additional processing needed
75+
validatedBodySchema = bodySchema[0].schema;
76+
} else if (bodySchema[0].in === 'formData') {
77+
// if we are processing multipart form, assemble body schema from form field schemas
78+
validatedBodySchema = {
79+
required: [],
80+
properties: {}
81+
};
82+
bodySchema.forEach((formField) => {
83+
if (formField.type !== 'file') {
84+
validatedBodySchema.properties[formField.name] = {
85+
type: formField.type
86+
};
87+
if (formField.required) {
88+
validatedBodySchema.required.push(formField.name);
89+
}
90+
}
91+
});
92+
}
93+
return validatedBodySchema;
94+
}
95+
6996
/**
7097
* The middleware - should be called for each express route
7198
* @param {any} req
@@ -90,7 +117,7 @@ function validate(req, res, next) {
90117
firstError: middlewareOptions.firstError });
91118
return next(error);
92119
});
93-
};
120+
}
94121

95122
function _validateBody(body, path, method) {
96123
return new Promise(function (resolve, reject) {
@@ -209,7 +236,7 @@ function buildParametersValidation(parameters, contentTypes) {
209236
additionalProperties: false
210237
},
211238
files: {
212-
title: 'HTTP form',
239+
title: 'HTTP form files',
213240
files: {
214241
required: [],
215242
optional: []
@@ -222,7 +249,7 @@ function buildParametersValidation(parameters, contentTypes) {
222249
var data = Object.assign({}, parameter);
223250

224251
const required = parameter.required;
225-
const source = typeNameConversion[parameter.in] || parameter.in;
252+
const source = sourceResolver.resolveParameterSource(parameter);
226253
const key = parameter.in === 'header' ? parameter.name.toLowerCase() : parameter.name;
227254

228255
var destination = ajvParametersSchema.properties[source];
@@ -233,7 +260,7 @@ function buildParametersValidation(parameters, contentTypes) {
233260

234261
if (data.type === 'file') {
235262
required ? destination.files.required.push(key) : destination.files.optional.push(key);
236-
} else {
263+
} else if (source !== 'fields') {
237264
if (required) {
238265
destination.required = destination.required || [];
239266
destination.required.push(key);
@@ -252,8 +279,3 @@ module.exports = {
252279
validate: validate,
253280
InputValidationError: InputValidationError
254281
};
255-
256-
var typeNameConversion = {
257-
header: 'headers',
258-
formData: 'files'
259-
};

src/utils/sourceResolver.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* Resolve value source for a given schema parameter
3+
* @param {Object} parameter from Swagger schema
4+
* @returns {string}
5+
*/
6+
function resolveParameterSource(parameter) {
7+
if (parameter.in === 'formData') {
8+
if (parameter.type === 'file') {
9+
return 'files';
10+
} else {
11+
return 'fields';
12+
}
13+
} else if (parameter.in === 'header') {
14+
return 'headers';
15+
}
16+
17+
return parameter.in;
18+
}
19+
20+
module.exports = {
21+
resolveParameterSource
22+
};

test/form-data-swagger.yaml

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,52 @@ paths:
2929
responses:
3030
'200':
3131
description: Import result
32+
/login:
33+
post:
34+
description: Login operation
35+
produces:
36+
- application/json
37+
consumes:
38+
- multipart/form-data
39+
parameters:
40+
- name: username
41+
in: formData
42+
required: true
43+
type: string
44+
description: Authentication username.
45+
- name: password
46+
in: formData
47+
required: true
48+
type: string
49+
description: Authentication password.
50+
- name: params
51+
in: formData
52+
type: string
53+
description: Optional params.
54+
responses:
55+
'200':
56+
description: Auth result
57+
/kennels/import:
58+
post:
59+
description: Login operation
60+
produces:
61+
- application/json
62+
consumes:
63+
- multipart/form-data
64+
parameters:
65+
- name: name
66+
in: formData
67+
required: true
68+
type: string
69+
description: Kennel name.
70+
- name: blueprintFile
71+
in: formData
72+
required: true
73+
type: file
74+
description: File to import from.
75+
responses:
76+
'200':
77+
description: Import result
3278
definitions:
3379

3480
parameters:
@@ -47,4 +93,4 @@ parameters:
4793
description: 'global request id through the system.'
4894
type: string
4995
minLength: 1
50-
x-example: '123456'
96+
x-example: '123456'

test/middleware-test.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2275,5 +2275,46 @@ describe('input-validation middleware tests', function () {
22752275
done();
22762276
});
22772277
});
2278+
it('supports string formData', function (done) {
2279+
request(app)
2280+
.post('/login')
2281+
.set('api-version', '1.0')
2282+
.field('username', 'user')
2283+
.field('password', 'pass')
2284+
.expect(200, function (err, res) {
2285+
if (err) {
2286+
throw err;
2287+
}
2288+
expect(res.body.result).to.equal('OK');
2289+
done();
2290+
});
2291+
});
2292+
it('supports mix of files and fields', function (done) {
2293+
request(app)
2294+
.post('/kennels/import')
2295+
.set('api-version', '1.0')
2296+
.field('name', 'kennel 1 ')
2297+
.attach('blueprintFile', 'LICENSE')
2298+
.expect(200, function (err, res) {
2299+
if (err) {
2300+
throw err;
2301+
}
2302+
expect(res.body.result).to.equal('OK');
2303+
done();
2304+
});
2305+
});
2306+
it('validates string formData', function (done) {
2307+
request(app)
2308+
.post('/login')
2309+
.set('api-version', '1.0')
2310+
.field('username', 'user')
2311+
.expect(400, function (err, res) {
2312+
if (err) {
2313+
throw err;
2314+
}
2315+
expect(res.body.more_info).to.includes('body should have required property \'password\'');
2316+
done();
2317+
});
2318+
});
22782319
});
22792320
});

test/test-server-formdata.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ var inputValidationOptions = {
1414
{ name: 'file', validate: () => { return true } }
1515
],
1616
beautifyErrors: true,
17-
firstError: true
17+
firstError: true,
18+
expectFormFieldsInBody: true
1819
};
1920

2021
module.exports = inputValidation.init('test/form-data-swagger.yaml', inputValidationOptions)
@@ -24,6 +25,12 @@ module.exports = inputValidation.init('test/form-data-swagger.yaml', inputValida
2425
app.post('/pets/import', upload.any(), inputValidation.validate, function (req, res, next) {
2526
res.json({ result: 'OK' });
2627
});
28+
app.post('/kennels/import', upload.any(), inputValidation.validate, function (req, res, next) {
29+
res.json({ result: 'OK' });
30+
});
31+
app.post('/login', upload.any(), inputValidation.validate, function (req, res, next) {
32+
res.json({ result: 'OK' });
33+
});
2734
app.use(function (err, req, res, next) {
2835
if (err instanceof inputValidation.InputValidationError) {
2936
res.status(400).json({ more_info: err.errors });
@@ -33,4 +40,4 @@ module.exports = inputValidation.init('test/form-data-swagger.yaml', inputValida
3340
module.exports = app;
3441

3542
return Promise.resolve(app);
36-
});
43+
});

0 commit comments

Comments
 (0)