Repository: glennjones/hapi-swagger
Pull request #676 (2020-08-05 23:39:27)

Override server-level validation in plugin registration (via options)
should close: https://github.com/glennjones/hapi-swagger/issues/675

* [x] code changes
* [x] integration tests
* [x] docs

**Note:**
when running tests I keps receiving: `The following leaks were detected:CSS` regardless of the -I setting in package.json. Hopefully it's just a misconfiguration



Patch:
diff --git a/lib/builder.js b/lib/builder.js
index b4ce59c..4a0a703 100644
--- a/lib/builder.js
+++ b/lib/builder.js
@@ -245,7 +245,8 @@ internals.removeNoneSchemaOptions = function(options) {
     'pathReplacements',
     'log',
     'cors',
-    'routeTag'
+    'routeTag',
+    'validate'
   ].forEach(element => {
     delete out[element];
   });
diff --git a/lib/index.js b/lib/index.js
index 040f5b8..32a2bbc 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -48,7 +48,23 @@ const schema = Joi.object({
       replacement: Joi.string().allow('')
     })
   ),
-  routeTag: Joi.string()
+  routeTag: Joi.string(),
+  // validate as declared in @hapi/hapi
+  // https://github.com/hapijs/hapi/blob/5c0850989f2b7270fe7a7b6a7d4ebdc9a7fecd79/lib/config.js#L220
+  validate: Joi.object({
+    headers: Joi.alternatives(Joi.object(), Joi.array(), Joi.function()).allow(null, true),
+    params: Joi.alternatives(Joi.object(), Joi.array(), Joi.function()).allow(null, true),
+    query: Joi.alternatives(Joi.object(), Joi.array(), Joi.function()).allow(null, false, true),
+    payload: Joi.alternatives(Joi.object(), Joi.array(), Joi.function()).allow(null, false, true),
+    state: Joi.alternatives(Joi.object(), Joi.array(), Joi.function()).allow(null, false, true),
+    failAction: Joi.alternatives([
+      Joi.valid('error', 'log', 'ignore'),
+      Joi.function()
+    ]),
+    errorFields: Joi.object(),
+    options: Joi.object(),
+    validator: Joi.object()
+  })
 }).unknown();
 
 /**
@@ -64,6 +80,10 @@ exports.plugin = {
   multiple: true,
 
   register: (server, options) => {
+    // `options.validate` might not be present but it should not be set in the
+    // `Defaults` since it would always override the server-level defaults.
+    // It should be overwritten only if explicitly passed to the plugin options.
+    const validateOption = { validate: options.validate }
     let settings = Hoek.applyToDefaults(Defaults, options, { nullOverride: true });
     const publicDirPath = Path.resolve(__dirname, '..', 'public');
 
@@ -111,7 +131,7 @@ exports.plugin = {
       {
         method: 'GET',
         path: settings.jsonRoutePath,
-        options: {
+        options: Hoek.applyToDefaults({
           auth: settings.auth,
           cors: settings.cors,
           tags: settings.documentationRouteTags,
@@ -128,7 +148,7 @@ exports.plugin = {
           plugins: {
             'hapi-swagger': false
           }
-        }
+        }, validateOption)
       }
     ]);
 
@@ -149,14 +169,14 @@ exports.plugin = {
             {
               method: 'GET',
               path: settings.documentationPath,
-              options: {
+              options: Hoek.applyToDefaults({
                 auth: settings.auth,
                 tags: settings.documentationRouteTags,
                 handler: (request, h) => {
                   return h.view('index', {});
                 },
                 plugins: settings.documentationRoutePlugins
-              }
+              }, validateOption)
             }
           ]);
         }
@@ -181,13 +201,13 @@ exports.plugin = {
             server.route({
               method: 'GET',
               path: `${settings.routesBasePath}${filename}`,
-              options: {
+              options: Hoek.applyToDefaults({
                 auth: settings.auth,
                 tags: settings.documentationRouteTags,
                 files: {
                   relativeTo: swaggerUiAssetPath
                 }
-              },
+              }, validateOption),
               handler: {
                 file: `${filename}`
               }
@@ -197,7 +217,7 @@ exports.plugin = {
           server.route({
             method: 'GET',
             path: settings.routesBasePath + 'extend.js',
-            options: {
+            options: Hoek.applyToDefaults({
               tags: settings.documentationRouteTags,
               auth: settings.auth,
               files: {
@@ -206,7 +226,7 @@ exports.plugin = {
               handler: {
                 file: 'extend.js'
               }
-            }
+            }, validateOption)
           });
         }
 
@@ -218,14 +238,14 @@ exports.plugin = {
               path: join(settings.documentationPath, sep, 'debug')
                 .split(sep)
                 .join('/'),
-              options: {
+              options: Hoek.applyToDefaults({
                 auth: settings.auth,
                 tags: settings.documentationRouteTags,
                 handler: (request, h) => {
                   return h.view('debug.html', {}).type('application/json');
                 },
                 plugins: settings.documentationRoutePlugins
-              }
+              }, validateOption)
             }
           ]);
         }
@@ -259,7 +279,7 @@ exports.plugin = {
  * @param  {Object} settings
  * @return {Object}
  */
-const appendDataContext = function(plugin, settings) {
+const appendDataContext = function (plugin, settings) {
   plugin.ext('onPostHandler', (request, h) => {
     let response = request.response;
     // if the reply is a view add settings data into template system
@@ -308,7 +328,7 @@ const appendDataContext = function(plugin, settings) {
  * @param  {String} qsValue
  * @return {String}
  */
-const appendQueryString = function(url, qsName, qsValue) {
+const appendQueryString = function (url, qsName, qsValue) {
   let urlObj = Url.parse(url);
   if (qsName && qsValue) {
     urlObj.query = Querystring.parse(qsName + '=' + qsValue);
@@ -325,7 +345,7 @@ const appendQueryString = function(url, qsName, qsValue) {
  * @param  {Object} settings
  * @return {String}
  */
-const findAPIKeyPrefix = function(settings) {
+const findAPIKeyPrefix = function (settings) {
   // Need JWT plugin to work with Hapi v17+ to test this again
   /* $lab:coverage:off$ */
   let out = '';
diff --git a/optionsreference.md b/optionsreference.md
index 379f752..d4b347a 100644
--- a/optionsreference.md
+++ b/optionsreference.md
@@ -8,6 +8,7 @@
 -   `host`: (string) The host (name or IP) serving the API including port if any i.e. `localhost:8080`
 -   `auth`: (boolean, string or object) defines security strategy to use for plugin resources - default: `false`,
 -   `cors`: (boolean) whether the swagger.json routes is served with cors support - default: `false`,
+-   `validate`: (object) sets validation rules for all routes created by the plugin - not set by default
 
 #### JSON (JSON endpoint needed to create UI)
 


Test patch:
diff --git a/test/Integration/plugin-test.js b/test/Integration/plugin-test.js
index 8181393..a6b4d82 100644
--- a/test/Integration/plugin-test.js
+++ b/test/Integration/plugin-test.js
@@ -1,5 +1,6 @@
 const Code = require('@hapi/code');
 const Hapi = require('@hapi/hapi');
+const Hoek = require('@hapi/hoek');
 const Inert = require('@hapi/inert');
 const Joi = require('@hapi/joi');
 const Lab = require('@hapi/lab');
@@ -66,6 +67,20 @@ lab.experiment('plugin', () => {
     }
   });
 
+  lab.test('fail plug-in register with bad options', async () => {
+    const badOptions = {
+      validate: 42
+    };
+    try {
+      await Helper.createServer(badOptions, routes);
+    } catch (err) {
+      expect(err).to.exist();
+      expect(err.name).to.equal('ValidationError');
+      expect(err.details).to.have.length(1)
+      expect(err.details[0].message).to.equal('"validate" must be of type object')
+    }
+  });
+
   lab.test('plug-in register test', async () => {
     const server = await Helper.createServer({}, routes);
     const response = await server.inject({ method: 'GET', url: '/swagger.json' });
@@ -73,6 +88,68 @@ lab.experiment('plugin', () => {
     expect(response.result.paths).to.have.length(1);
   });
 
+  lab.test('plug-in register with validate option', async () => {
+    const goodOptions = {
+      validate: {
+        headers: true
+      }
+    };
+    const server = await Helper.createServer(goodOptions, routes);
+    const response = await server.inject({ method: 'GET', url: '/swagger.json' });
+    expect(response.statusCode).to.equal(200);
+    expect(response.result.paths).to.have.length(1);
+  });
+
+  lab.test('plug-in register with server validation options', async () => {
+    const serverOptions = {
+      routes: {
+        validate: {
+          failAction: (request, h, err) => {
+            Hoek.ignore(request, h)
+            throw err
+          },
+          headers: Joi.object({
+            authorization: Joi.string().required()
+          })
+            .required()
+        }
+      }
+    };
+    const server = await Helper.createServer(undefined, routes, serverOptions);
+    const response = await server.inject({ method: 'GET', url: '/swagger.json' });
+    expect(response.statusCode).to.equal(400);
+    expect(response.statusMessage).to.equal('Bad Request');
+    expect(response.result.statusCode).to.equal(400);
+    expect(response.result.error).to.equal('Bad Request');
+    expect(response.result.message).to.equal('"authorization" is required');
+  });
+
+  lab.test('plug-in register and overrride server validation options', async () => {
+    const serverOptions = {
+      routes: {
+        validate: {
+          failAction: (request, h, err) => {
+            Hoek.ignore(request, h)
+            throw err
+          },
+          headers: Joi.object({
+            authorization: Joi.string().required()
+          })
+            .required()
+        }
+      }
+    };
+    const pluginOptions = {
+      validate: {
+        headers: true
+      }
+    }
+    const server = await Helper.createServer(pluginOptions, routes, serverOptions);
+    const response = await server.inject({ method: 'GET', url: '/swagger.json' });
+    expect(response.statusCode).to.equal(200);
+    expect(response.result.paths).to.have.length(1);
+  });
+
   lab.test('default jsonPath url', async () => {
     const server = await Helper.createServer({}, routes);
     const response = await server.inject({ method: 'GET', url: '/swagger.json' });