Repository: BRIKEV/express-jsdoc-swagger (js)

Issue:
support multiple security annotate
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
I want to add multiple security.
for example of my case :
I use two request header strings for authentication. `x-access-token` and `x-refresh-token`.
in this case, I cannot implement both authentication in one request.


**Describe the solution you'd like**
A clear and concise description of what you want to happen.
according to official document of [swagger](https://swagger.io/docs/specification/2-0/authentication/)
we can add multiple authentication variable with `and` `or` operator.

I can define multiple of security objects in option.

it will be easy if I can add
```
/**
 * GET /test
 *
 * @summary 
 ...
 * @security accessToken | refreshToken
 ...
```
for `or` operator

and

```
/**
 * GET /test
 *
 * @summary 
 ...
 * @security accessToken & refreshToken
 ...
```
for `and` operator I guess.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
or at least availabie multiple annotation for security
```
/**
 * GET /test
 *
 * @summary 
 ...
 * @security accessToken
 * @security refreshToken
 ...
```
like this.

**Additional context**

this is my options settings for security.
```
{
    info: {
        version: '1.0.0',
        title: 'HR API',
        license: {
            name: 'MIT',
        },
    },
    security: {
        BasicAuth: {
            type: 'http',
            scheme: 'basic',
        },
        accessToken: {
            type: 'apiKey',
            name: 'x-access-token',
            in: 'header'
        },
        refreshToken: {
            type: 'apiKey',
            name: 'x-refresh-token',
            in: 'header'
        }
    },
    filesPattern: [ './routes/*.js' ], // Glob pattern to find your jsdoc files
    swaggerUIPath: `/swagger`, // SwaggerUI will be render in this url. Default: '/api-docs'
    baseDir: __dirname,
    exposeSwaggerUI: true, // Expose OpenAPI UI. Default true
    exposeApiDocs: true, // Expose Open API JSON Docs documentation in `apiDocsPath` path. Default false.
    apiDocsPath: '/v3/api-docs', // Open API JSON Docs endpoint. Default value '/v3/api-docs'.
}
```
for just in case
Add any other context or screenshots about the feature request here.

I am not sure this is already implemented or not, at least i cannot find any example or issues in here.


Pull request description:
Feat/multiple security config
### What kind of change does this PR introduce? (check at least one)
 - [X] Feature
 - [X] Test
 - [X] Docs

### Description:
This PR adds different ways to define security headers in the OpenApi Docs. This is based on this [doc](https://swagger.io/docs/specification/authentication/)

We defined multiple ways to define it.

1. Basic one

```
/**
 * GET /api/v1
 ....
 * @security BasicAuth
 */
```

2. Multiple (AND condition)

```
/**
 * GET /api/v2
 ....
 * @security BasicAuth & BearerAuth
 */
```

2. Multiple (OR condition)

```
/**
 * GET /api/v3
 ....
 * @security BasicAuth | BearerAuth
 */

/**
 * GET /api/v4
 ....
 * @security BearerAuth
 * @security BasicAuth
 */
```

This closes #157 


Patch:
diff --git a/examples/security/basic-auth.js b/examples/security/basic-auth.js
index 193d294..b307494 100644
--- a/examples/security/basic-auth.js
+++ b/examples/security/basic-auth.js
@@ -16,6 +16,10 @@ const options = {
       type: 'http',
       scheme: 'basic',
     },
+    BearerAuth: {
+      type: 'http',
+      scheme: 'bearer',
+    },
   },
   filesPattern: './basic-auth.js',
   baseDir: __dirname,
@@ -28,10 +32,26 @@ expressJSDocSwagger(app)(options);
 
 /**
  * GET /api/v1
- * @summary This is the summary of the endpoint
+ * @summary Endpoint with security info
  * @return {string} 200 - success response
  * @security BasicAuth
  */
 app.get('/api/v1', (req, res) => res.send('Hello World!'));
 
+/**
+ * GET /api/v2
+ * @summary Endpoint with multiple security configuration (AND logic)
+ * @return {string} 200 - success response
+ * @security BasicAuth & BearerAuth
+ */
+app.get('/api/v2', (req, res) => res.send('Hello World!'));
+
+/**
+ * GET /api/v3
+ * @summary Endpoint with multiple security configuration (OR logic)
+ * @return {string} 200 - success response
+ * @security BasicAuth | BearerAuth
+ */
+app.get('/api/v3', (req, res) => res.send('Hello World!'));
+
 app.listen(port, () => logger.info(`Example app listening at http://localhost:${port}`));
diff --git a/transforms/paths/index.js b/transforms/paths/index.js
index 20ab60f..dbc6106 100644
--- a/transforms/paths/index.js
+++ b/transforms/paths/index.js
@@ -7,16 +7,13 @@ const {
   validRequestBodyMethods: bodyMethods,
   validHTTPMethod,
 } = require('../utils/httpMethods');
+const formatSecurity = require('./security');
 
 const formatTags = (tags = []) => tags.map(({ description }) => {
   const { name } = formatDescriptionTag(description);
   return name;
 });
 
-const formatSecurity = (securityValues = []) => securityValues.map(({ description }) => ({
-  [description]: [],
-}));
-
 const formatSummary = summary => (summary || {}).description || '';
 
 const formatDescription = description => (description || {}).description || undefined;
diff --git a/transforms/paths/security.js b/transforms/paths/security.js
new file mode 100644
index 0000000..1128cd7
--- /dev/null
+++ b/transforms/paths/security.js
@@ -0,0 +1,29 @@
+const { flatArray } = require('../utils/arrays');
+
+const AND_SEPARATOR = ' & ';
+const OR_SEPARATOR = ' | ';
+
+const formatOrValues = ({ description }) => {
+  const securityNames = description.split(OR_SEPARATOR);
+  return securityNames.map(names => ({
+    description: names,
+  }));
+};
+
+const formatSecurity = (securityValues = []) => (
+  flatArray(securityValues
+    .map(formatOrValues))
+    .map(({ description }) => {
+      const securityNames = description.split(AND_SEPARATOR);
+      return {
+        ...securityNames.reduce((acum, names) => (
+          {
+            ...acum,
+            [names]: [],
+          }
+        ), {}),
+      };
+    })
+);
+
+module.exports = formatSecurity;


Test patch:
diff --git a/test/transforms/paths/security.test.js b/test/transforms/paths/security.test.js
index 0baf055..83df759 100644
--- a/test/transforms/paths/security.test.js
+++ b/test/transforms/paths/security.test.js
@@ -32,4 +32,106 @@ describe('Paths - security', () => {
     const result = setPaths({}, parsedJSDocs);
     expect(result).toEqual(expected);
   });
+
+  it('Should parse jsdoc multiple security params into security array with each security type', () => {
+    const jsodInput = [`
+      /**
+       * POST /api/v1/song
+       * @summary Create new song
+       * @security BasicAuth
+       * @security BearerAuth
+       */
+    `];
+    const expected = {
+      paths: {
+        '/api/v1/song': {
+          post: {
+            deprecated: false,
+            summary: 'Create new song',
+            security: [
+              {
+                BasicAuth: [],
+              },
+              {
+                BearerAuth: [],
+              },
+            ],
+            tags: [],
+            responses: {},
+            parameters: [],
+          },
+        },
+      },
+    };
+    const parsedJSDocs = jsdocInfo()(jsodInput);
+    const result = setPaths({}, parsedJSDocs);
+    expect(result).toEqual(expected);
+  });
+
+  it('Should parse jsdoc multiple security with "and" configuration', () => {
+    const jsodInput = [`
+      /**
+       * POST /api/v1/song
+       * @summary Create new song
+       * @security BasicAuth & BearerAuth
+       */
+    `];
+    const expected = {
+      paths: {
+        '/api/v1/song': {
+          post: {
+            deprecated: false,
+            summary: 'Create new song',
+            security: [
+              {
+                BasicAuth: [],
+                BearerAuth: [],
+              },
+            ],
+            tags: [],
+            responses: {},
+            parameters: [],
+          },
+        },
+      },
+    };
+    const parsedJSDocs = jsdocInfo()(jsodInput);
+    const result = setPaths({}, parsedJSDocs);
+    expect(result).toEqual(expected);
+  });
+
+  it('Should parse jsdoc multiple security with "or" configuration', () => {
+    const jsodInput = [`
+      /**
+       * POST /api/v1/song
+       * @summary Create new song
+       * @security BasicAuth & BearerAuth | Oauth2
+       */
+    `];
+    const expected = {
+      paths: {
+        '/api/v1/song': {
+          post: {
+            deprecated: false,
+            summary: 'Create new song',
+            security: [
+              {
+                BasicAuth: [],
+                BearerAuth: [],
+              },
+              {
+                Oauth2: [],
+              },
+            ],
+            tags: [],
+            responses: {},
+            parameters: [],
+          },
+        },
+      },
+    };
+    const parsedJSDocs = jsdocInfo()(jsodInput);
+    const result = setPaths({}, parsedJSDocs);
+    expect(result).toEqual(expected);
+  });
 });