-
-
Notifications
You must be signed in to change notification settings - Fork 33
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
8febc25
commit 81cd935
Showing
4 changed files
with
255 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import { Router, Request, Response, NextFunction } from 'express'; | ||
import { Controller } from '../interfaces'; | ||
import { ProblemException } from '../exceptions/problem.exception'; | ||
import { getAppOpenAPI } from '../utils/app-openapi'; | ||
|
||
const getCommandsFromRequest = (req: Request): string[] => { | ||
return req.params.command ? req.params.command.split('/').filter(cmd => cmd.trim()) : []; | ||
}; | ||
|
||
const isKeyValid = (key: string, obj: any): boolean => { | ||
return Object.keys(obj).includes(key); | ||
}; | ||
|
||
const getPathKeysMatchingCommands = (commands: string[], pathKeys: string[]): string | undefined => { | ||
if (!Array.isArray(pathKeys) || !pathKeys.every(key => typeof key === 'string')) { | ||
return undefined; | ||
} | ||
return pathKeys.find(pathKey => { | ||
const pathParts = pathKey.split('/').filter(part => part !== ''); | ||
return pathParts.every((pathPart, i) => { | ||
const command = commands[Number(i)]; | ||
return pathPart === command || pathPart.startsWith('{'); | ||
}); | ||
}); | ||
}; | ||
|
||
const getFullRequestBodySpec = (operationDetails: any) => { | ||
return isKeyValid('requestBody', operationDetails) ? operationDetails.requestBody.content['application/json'].schema : null; | ||
}; | ||
|
||
const buildResponseObject = (matchedPathKey: string, method: string, operationDetails: any, requestBodySchema: any) => { | ||
return { | ||
command: matchedPathKey, | ||
method: method.toUpperCase(), | ||
summary: operationDetails.summary || '', | ||
requestBody: requestBodySchema | ||
}; | ||
}; | ||
|
||
export class HelpController implements Controller { | ||
public basepath = '/help'; | ||
|
||
public async boot(): Promise<Router> { | ||
const router: Router = Router(); | ||
|
||
router.get('/help/:command*?', async (req: Request, res: Response, next: NextFunction) => { | ||
const commands = getCommandsFromRequest(req); | ||
let openapiSpec: any; | ||
|
||
try { | ||
openapiSpec = await getAppOpenAPI(); | ||
} catch (err) { | ||
return next(err); | ||
} | ||
|
||
if (commands.length === 0) { | ||
const routes = isKeyValid('paths', openapiSpec) ? Object.keys(openapiSpec.paths).map(path => ({ command: path.replace(/^\//, ''), url: `${this.basepath}${path}` })) : []; | ||
return res.json(routes); | ||
} | ||
|
||
const pathKeys = isKeyValid('paths', openapiSpec) ? Object.keys(openapiSpec.paths) : []; | ||
const matchedPathKey = getPathKeysMatchingCommands(commands, pathKeys); | ||
|
||
if (!matchedPathKey) { | ||
return next(new ProblemException({ | ||
type: 'invalid-asyncapi-command', | ||
title: 'Invalid AsyncAPI Command', | ||
status: 404, | ||
detail: 'The given AsyncAPI command is not valid.' | ||
})); | ||
} | ||
|
||
const pathInfo = isKeyValid(matchedPathKey, openapiSpec.paths) ? openapiSpec.paths[String(matchedPathKey)] : undefined; | ||
const method = commands.length > 1 ? 'get' : 'post'; | ||
const operationDetails = isKeyValid(method, pathInfo) ? pathInfo[String(method)] : undefined; | ||
if (!operationDetails) { | ||
return next(new ProblemException({ | ||
type: 'invalid-asyncapi-command', | ||
title: 'Invalid AsyncAPI Command', | ||
status: 404, | ||
detail: 'The given AsyncAPI command is not valid.' | ||
})); | ||
} | ||
|
||
const requestBodySchema = getFullRequestBodySpec(operationDetails); | ||
|
||
return res.json(buildResponseObject(matchedPathKey, method, operationDetails, requestBodySchema)); | ||
}); | ||
|
||
return router; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import request from 'supertest'; | ||
import { App } from '../../app'; | ||
import { HelpController } from '../help.controller'; | ||
import { getAppOpenAPI } from '../../utils/app-openapi'; | ||
|
||
jest.mock('../../utils/app-openapi', () => ({ | ||
getAppOpenAPI: jest.fn(), | ||
})); | ||
|
||
describe('HelpController', () => { | ||
let app; | ||
beforeAll(async () => { | ||
app = new App([new HelpController()]); | ||
await app.init(); | ||
}); | ||
|
||
describe('[GET] /help', () => { | ||
it('should return all commands', async () => { | ||
(getAppOpenAPI as jest.Mock).mockResolvedValue({ | ||
paths: { | ||
'/validate': {}, | ||
'/parse': {}, | ||
'/generate': {}, | ||
'/convert': {}, | ||
'/bundle': {}, | ||
'/help': {}, | ||
'/diff': {} | ||
} | ||
}); | ||
|
||
const response = await request(app.getServer()) | ||
.get('/v1/help') | ||
.expect(200); | ||
|
||
expect(response.body).toEqual([ | ||
{ | ||
command: 'validate', | ||
url: '/help/validate' | ||
}, | ||
{ | ||
command: 'parse', | ||
url: '/help/parse' | ||
}, | ||
{ | ||
command: 'generate', | ||
url: '/help/generate' | ||
}, | ||
{ | ||
command: 'convert', | ||
url: '/help/convert' | ||
}, | ||
{ | ||
command: 'bundle', | ||
url: '/help/bundle' | ||
}, | ||
{ | ||
command: 'help', | ||
url: '/help/help' | ||
}, | ||
{ | ||
command: 'diff', | ||
url: '/help/diff' | ||
} | ||
]); | ||
}); | ||
|
||
it('should return 404 error for an invalid command', async () => { | ||
const response = await request(app.getServer()) | ||
.get('/v1/help/invalidCommand') | ||
.expect(404); | ||
|
||
expect(response.body).toEqual({ | ||
type: 'https://api.asyncapi.com/problem/invalid-asyncapi-command', | ||
title: 'Invalid AsyncAPI Command', | ||
status: 404, | ||
detail: 'The given AsyncAPI command is not valid.' | ||
}); | ||
}); | ||
|
||
it('should return 404 error for a command without a method', async () => { | ||
(getAppOpenAPI as jest.Mock).mockResolvedValue({ | ||
paths: { | ||
'/someCommand': {} | ||
} | ||
}); | ||
|
||
const response = await request(app.getServer()) | ||
.get('/v1/help/someCommand') | ||
.expect(404); | ||
|
||
expect(response.body).toEqual({ | ||
type: 'https://api.asyncapi.com/problem/invalid-asyncapi-command', | ||
title: 'Invalid AsyncAPI Command', | ||
status: 404, | ||
detail: 'The given AsyncAPI command is not valid.' | ||
}); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters