diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/NodeJSExpressServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/NodeJSExpressServerCodegen.java index d89fb863fa8e..4efb64e61d18 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/NodeJSExpressServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/NodeJSExpressServerCodegen.java @@ -93,7 +93,6 @@ public NodeJSExpressServerCodegen() { supportingFiles.add(new SupportingFile("utils" + File.separator + "writer.mustache", "utils", "writer.js")); // controllers folder - supportingFiles.add(new SupportingFile("controllers" + File.separator + "test.mustache", "controllers", "TestController.js")); supportingFiles.add(new SupportingFile("controllers" + File.separator + "index.mustache", "controllers", "index.js")); supportingFiles.add(new SupportingFile("controllers" + File.separator + "Controller.mustache", "controllers", "Controller.js")); // service folder diff --git a/samples/server/petstore/nodejs-express-server/api/openapi.yaml b/samples/server/petstore/nodejs-express-server/api/openapi.yaml index 401e628f3bd1..1f42f2aa3f88 100644 --- a/samples/server/petstore/nodejs-express-server/api/openapi.yaml +++ b/samples/server/petstore/nodejs-express-server/api/openapi.yaml @@ -8,7 +8,15 @@ info: title: OpenAPI Petstore version: 1.0.0 servers: -- url: http://petstore.swagger.io/v2 +- url: http://localhost:{port}/api/{basePath} + variables: + port: + enum: + - '3000' + - '3009' + default: '3000' + basePath: + default: 'v2' tags: - description: Everything about your Pets name: pet diff --git a/samples/server/petstore/nodejs-express-server/controllers/TestController.js b/samples/server/petstore/nodejs-express-server/controllers/TestController.js deleted file mode 100644 index 516c135b45be..000000000000 --- a/samples/server/petstore/nodejs-express-server/controllers/TestController.js +++ /dev/null @@ -1,72 +0,0 @@ -const Service = require('../services/Service'); - -const testItems = require('../tests/testFiles/testItems.json'); - -class TestService { - static testGetController() { - return new Promise( - async (resolve, reject) => { - try { - resolve(Service.successResponse( - testItems, - 200, - )); - } catch (e) { - const message = e.getMessage() || 'Could not get items. Server error'; - reject(Service.rejectResponse(message, 500)); - } - }, - ); - - sendResponse(request, response) { - response.status(200); - const objectToReturn = {}; - Object.keys(request.swagger.paramValues).forEach((key) => { - const val = request.swagger.paramValues[key]; - if (val instanceof Object) { - objectToReturn[key] = val.originalname || val.name || val; - } else { - objectToReturn[key] = request.swagger.paramValues[key]; - } - }); - response.json(objectToReturn); - } - - confirmRouteGetSingle(request, response) { - this.sendResponse(request, response); - } - - confirmRouteGetMany(request, response) { - this.sendResponse(request, response); - } - - confirmRoutePost(request, response) { - this.sendResponse(request, response); - } - - confirmRoutePut(request, response) { - this.sendResponse(request, response); - } - - async testGetController(request, response) { - await Controller.handleRequest(request, response, this.service.testGetController); - } - - async testPostController(request, response) { - await Controller.handleRequest(request, response, this.service.testPostController); - } - - async testPutController(request, response) { - await Controller.handleRequest(request, response, this.service.testPutController); - } - - async testDeleteController(request, response) { - await Controller.handleRequest(request, response, this.service.testDeleteController); - } - - async testFindByIdController(request, response) { - await Controller.handleRequest(request, response, this.service.testFindByIdController); - } -} - -module.exports = TestController; diff --git a/samples/server/petstore/nodejs-express-server/controllers/index.js b/samples/server/petstore/nodejs-express-server/controllers/index.js index 3dc796c1e507..0ad912de2051 100644 --- a/samples/server/petstore/nodejs-express-server/controllers/index.js +++ b/samples/server/petstore/nodejs-express-server/controllers/index.js @@ -1,11 +1,9 @@ const PetController = require('./PetController'); const StoreController = require('./StoreController'); const UserController = require('./UserController'); -const TestController = require('./TestController'); module.exports = { PetController, StoreController, UserController, - TestController, }; diff --git a/samples/server/petstore/nodejs-express-server/models/Category.js b/samples/server/petstore/nodejs-express-server/models/Category.js new file mode 100644 index 000000000000..01d408c7512a --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/models/Category.js @@ -0,0 +1,21 @@ +const ono = require('ono'); +const Model = require('./Model'); + +class Category { + constructor(name, id) { + const validationErrors = (Model.validateModel(Category, {name, id})); + if (validationErrors.length === 0) { + this.id = id; + this.name = name; + } else { + throw ono('Tried to create an invalid Category instance', {errors: validationErrors}); + } + } +} + +Category.types = { + id: 'integer', + name: 'string', +}; + +module.exports = Category; diff --git a/samples/server/petstore/nodejs-express-server/models/Model.js b/samples/server/petstore/nodejs-express-server/models/Model.js new file mode 100644 index 000000000000..d60aabb4ece5 --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/models/Model.js @@ -0,0 +1,47 @@ +class Model { + static validateModel(modelClass, variables) { + const invalidArray = []; + Object.entries(variables).forEach(([key, value]) => { + const typeToCheck = modelClass.types[key]; + switch (typeToCheck) { + case 'string': + if (!(typeof value === 'string' || value instanceof String)) { + invalidArray.push({ key, expectedType: typeToCheck, value }); + } + break; + case 'number': + case 'integer': + if (!(typeof value === 'number' && !Number.isNaN(value))) { + invalidArray.push({ key, expectedType: typeToCheck, value }); + } + break; + case 'array': + if (!(value && typeof value === 'object' && value.constructor === Array)) { + invalidArray.push({ key, expectedType: typeToCheck, value }); + } + break; + case 'object': + if (!(value && typeof value === 'object' && value.constructor === Array)) { + invalidArray.push({ key, expectedType: typeToCheck, value }); + } + break; + case 'boolean': + if (!(typeof value === 'boolean')) { + invalidArray.push({ key, expectedType: typeToCheck, value }); + } + break; + default: + break; + } + }); + modelClass.required.forEach((requiredFieldName) => { + if (variables[requiredFieldName] === undefined || variables[requiredFieldName] === '') { + invalidArray.push( + { field: requiredFieldName, required: true, value: variables[requiredFieldName] }, + ); + } + }); + return invalidArray; + } +} +module.exports = Model; diff --git a/samples/server/petstore/nodejs-express-server/models/Pet.js b/samples/server/petstore/nodejs-express-server/models/Pet.js new file mode 100644 index 000000000000..cecf677196aa --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/models/Pet.js @@ -0,0 +1,35 @@ +const ono = require('ono'); +const Model = require('./Model'); +const Tag = require('./Tag'); +const Category = require('./Category'); + +class Pet { + constructor(photoUrls, name, id, tags, status, category) { + const validationErrors = Model.validateModel(Pet, { + photoUrls, name, id, tags, status, category, + }); + if (validationErrors.length === 0) { + this.photoUrls = photoUrls; + this.name = name; + this.id = id; + this.tags = tags.map(t => new Tag(...t)); + this.status = status; + this.category = new Category(category); + } else { + throw ono('Tried to create an invalid Pet instance', { errors: validationErrors }); + } + } +} + +Pet.types = { + photoUrls: 'array', + name: 'string', + id: 'string', + tags: 'array', + status: 'string', + category: 'object', +}; + +Pet.required = ['name', 'photoUrls']; + +module.exports = Pet; diff --git a/samples/server/petstore/nodejs-express-server/models/Tag.js b/samples/server/petstore/nodejs-express-server/models/Tag.js new file mode 100644 index 000000000000..64f2c3e78deb --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/models/Tag.js @@ -0,0 +1,17 @@ +const ono = require('ono'); +const Model = require('./Model'); + +class Tag { + constructor(name, id) { + const validationErrors = Model.validateModel(Tag, + { name, id }); + if (validationErrors.length === 0) { + this.name = name; + this.id = id; + } else { + throw ono('Tried to create an invalid Tag instance', { errors: validationErrors }); + } + } +} + +module.exports = Tag; diff --git a/samples/server/petstore/nodejs-express-server/models/index.js b/samples/server/petstore/nodejs-express-server/models/index.js new file mode 100644 index 000000000000..66963054a241 --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/models/index.js @@ -0,0 +1,9 @@ +const CategoryModel = require('./Category'); +const PetModel = require('./Pet'); +const TagModel = require('./Tag'); + +module.exports = { + CategoryModel, + PetModel, + TagModel, +}; diff --git a/samples/server/petstore/nodejs-express-server/package.json b/samples/server/petstore/nodejs-express-server/package.json index 18e7d92069f9..ed38f18c8d3e 100644 --- a/samples/server/petstore/nodejs-express-server/package.json +++ b/samples/server/petstore/nodejs-express-server/package.json @@ -8,7 +8,8 @@ "start": "node index.js" }, "keywords": [ - "openapi-generator", "openapi" + "openapi-generator", + "openapi" ], "license": "Unlicense", "private": true, @@ -20,6 +21,9 @@ "express": "^4.16.4", "express-openapi-validator": "^1.0.0", "js-yaml": "^3.3.0", + "jstoxml": "^1.5.0", + "ono": "^5.0.1", + "openapi-sampler": "^1.0.0-beta.15", "swagger-express-middleware": "^2.0.2", "swagger-tools": "^0.10.4", "swagger-ui-express": "^4.0.2", @@ -35,5 +39,10 @@ "eslint-plugin-import": "^2.17.2", "form-data": "^2.3.3", "mocha": "^6.1.4" + }, + "eslintConfig": { + "env": { + "node": true + } } } diff --git a/samples/server/petstore/nodejs-express-server/tests/SwaggerRouterTests.js b/samples/server/petstore/nodejs-express-server/tests/SwaggerRouterTests.js deleted file mode 100644 index efe39ae5ffb9..000000000000 --- a/samples/server/petstore/nodejs-express-server/tests/SwaggerRouterTests.js +++ /dev/null @@ -1,164 +0,0 @@ -const path = require('path'); -const fs = require('fs'); -const { AssertionError } = require('assert'); -const { - describe, before, after, it, -} = require('mocha'); -const chai = require('chai'); -const chaiAsPromised = require('chai-as-promised'); -const { get, post, put } = require('axios'); -const FormData = require('form-data'); - -const logger = require('./logger'); -const config = require('./config'); -const App = require('../app'); - -// IMPORTANT CHANGE: WORKING ON TEST SWAGGER FILE - -config.OPENAPI_YAML = path.join(__dirname, 'testFiles', 'swagger.yaml'); - -const app = new App(config); -chai.use(chaiAsPromised); -chai.should(); - -describe('Tests for confirming that the Swagger router works as expected', () => { - const pathVarValue = 123; - const urlPath = `${config.FULL_PATH}/test/${pathVarValue}?queryOptionalNumber=2&queryRequiredString=sss`; - const headerRequiredArray = [1, 2, 3]; - before(async () => { - try { - await app.launch(); - logger.info('express server launched\n'); - } catch (error) { - logger.info(error); - await app.close(); - throw (error); - } - }); - - after(async () => { - await app.close() - .catch(error => logger.error(error)); - logger.error('express server closed'); - }); - - it('Should handle variables sent in the header', - async () => { - try { - const headerOptionalBool = false; - const headers = { headerOptionalBool, headerRequiredArray }; - const allHeadersResponse = await get(urlPath, { headers }); - allHeadersResponse.status.should.equal(200, 'Expecting a successful allHeadersResponse'); - allHeadersResponse.data.should.have.property('headerOptionalBool').and.equal(headerOptionalBool); - allHeadersResponse.data.should.have.property('headerRequiredArray'); - allHeadersResponse.data.headerRequiredArray - .should.have.deep.members(headerRequiredArray); - const onlyRequiredResponse = await get(urlPath, { headers: { headerRequiredArray } }); - onlyRequiredResponse.status.should.equal(200, 'Expecting a successful onlyRequiredResponse'); - onlyRequiredResponse.data.should.have.property('headerOptionalBool').and.equal(headerOptionalBool); - onlyRequiredResponse.data.should.have.property('headerRequiredArray'); - onlyRequiredResponse.data.headerRequiredArray.should.have.deep.members(headerRequiredArray); - await get(urlPath, { headers: { headerOptionalBool } }) - .catch((error => error.response.status.should.equal(400))); - } catch (error) { - if (error.response && error.response.data) { - logger.error(JSON.stringify(error.response.data)); - } else { - logger.error(error); - } - error.should.have.property('response'); - throw new Error(error); - } - }); - - it('Should handle variables sent in the path', - async () => { - try { - const headers = { headerRequiredArray }; - const correctPathVarResponse = await get(urlPath, { headers }); - correctPathVarResponse.status.should.equal(200); - correctPathVarResponse.data.should.have.property('pathRequiredNumber').and.equal(pathVarValue); - const pathVarNotInt = `${config.FULL_PATH}/test/aaa?queryOptionalNumber=2&queryRequiredString=sss`; - await get(pathVarNotInt, { headers: { headers } }) - .catch((error => error.response.status.should.equal(400))); - } catch (error) { - logger.error(error); - error.should.have.property('response'); - throw new Error(error); - } - }); - - it('Should handle variables sent in the queryString', - async () => { - try { - const headers = { headerRequiredArray }; - // const allHeadersResponse = await get(urlPath, { headers }); - const queryRequiredString = 'sss'; - const noOptionalQueryUrl = `${config.FULL_PATH}/test/${pathVarValue}/?queryRequiredString=${queryRequiredString}`; - const noOptionalQueryResponse = await get(noOptionalQueryUrl, { headers }); - noOptionalQueryResponse.status.should.equal(200, 'Expecting a successful allHeadersResponse'); - noOptionalQueryResponse.data.should.have.property('queryRequiredString').and.equal(queryRequiredString); - noOptionalQueryResponse.data.should.have.property('queryOptionalNumber').and.equal(1); - - const noRequiredQueryUrl = `${config.FULL_PATH}/test/${pathVarValue}`; - await get(noRequiredQueryUrl, headers) - .catch((error => error.response.status.should.equal(400))); - } catch (error) { - logger.error(error); - error.should.have.property('response'); - throw new Error(error); - } - }); - - it('Should handle variables sent in the body', - async () => { - try { - const fullPathURL = `${config.FULL_PATH}/test/`; - const body = { firstName: 'Foo', lastName: 'Bar' }; - const fullBodyResponse = await post(fullPathURL, body, - { headers: { headerRequiredArray } }); - fullBodyResponse.status.should.equal(200); - await post(fullPathURL, { firstName: 'foo' }, - { headers: { headerRequiredArray } }) - .then(response => response.status.should.equal(200)); - await post(fullPathURL, { lastName: 'bar' }, - { headers: { headerRequiredArray } }) - .catch(error => error.response.status.should.equal(400)); - } catch (error) { - error.should.have.property('response'); - console.error(JSON.stringify(error.response.data)); - } - }); - - it('Should handle variables sent in the formData', - async () => { - try { - const fullPathURL = `${config.FULL_PATH}/test/123`; - const formData = new FormData(); - const person = { - firstName: 'Foo', - lastName: 'Bar', - image: fs.createReadStream(path.join(__dirname, 'testFiles', 'pet.json')), - }; - Object.keys(person).forEach(key => formData.append(key, person[key])); - const formResponse = await put(fullPathURL, formData, - { - headers: formData.getHeaders(), - }); - formResponse.status.should.equal(200); - Object.keys(person).forEach((nameKey) => { - formResponse.data.should.have.property(nameKey); - if (person[nameKey] instanceof String) { - formResponse.data[nameKey].should.equal(person[nameKey]); - } - }); - } catch (error) { - if (error instanceof AssertionError) { - throw error; - } - error.should.have.property('response'); - console.error(JSON.stringify(error.response.data)); - throw new Error(error); - } - }); -}); diff --git a/samples/server/petstore/nodejs-express-server/tests/routingTests.js b/samples/server/petstore/nodejs-express-server/tests/routingTests.js new file mode 100644 index 000000000000..7018c8fc5a86 --- /dev/null +++ b/samples/server/petstore/nodejs-express-server/tests/routingTests.js @@ -0,0 +1,147 @@ +/** + * The purpose of these tests is to confirm that every path in the openapi spec, if built properly, + * returns a valid 200, with a simple text response. + * The codeGen will generate a response string including the name of the operation that was called. + * These tests confirm that the codeGen worked as expected. + * Once we start adding our own business logic, these + * tests will fail. It is recommended to keep these tests updated with the code changes. + */ +const { + describe, before, after, it, +} = require('mocha'); +const assert = require('assert').strict; +const chai = require('chai'); +const chaiAsPromised = require('chai-as-promised'); +const axios = require('axios'); +const yamljs = require('yamljs'); +const openApiSampler = require('openapi-sampler'); +const jstoxml = require('jstoxml'); +const logger = require('./logger'); +const config = require('./config'); +const App = require('../app'); + +const app = new App(config); +chai.use(chaiAsPromised); +chai.should(); + + +const pathPrefix = `${config.URL_PATH}:${config.URL_PORT}/api/v2`; +const spec = yamljs.load(config.OPENAPI_YAML); + +const parseParameters = (originalPath, schemaParameters) => { + let path = originalPath; + const headers = {}; + const queryParams = []; + schemaParameters.forEach((parameter) => { + const parameterValue = parameter.example || openApiSampler.sample(parameter.schema); + switch (parameter.in) { + case 'header': + headers[parameter.name] = parameterValue; + break; + case 'path': + path = path.replace(`{${parameter.name}}`, parameterValue); + break; + case 'query': + queryParams.push(`${parameter.name}=${parameterValue}`); + break; + default: + break; + } + }); + return { path, headers, queryString: queryParams.join('&') }; +}; + +const buildRequestObject = (pathEndpoint, method, operationObject, requestsArray) => { + logger.info(`method: ${method}`); + let headers = {}; + let requestBody = {}; + let queryString = ''; + let path = pathEndpoint; + if (operationObject.parameters !== undefined) { + logger.info('this is a request with parameters'); + ({ path, headers, queryString } = parseParameters(pathEndpoint, operationObject.parameters)); + if (queryString.length > 0) { + path += `?${queryString}`; + } + Object.entries(headers).forEach(([headerName, headerValue]) => { + headers[headerName] = headerValue; + }); + } + if (operationObject.requestBody !== undefined) { + logger.info('This is a request with a body'); + const content = Object.entries(operationObject.requestBody.content); + content.forEach(([contentType, contentObject]) => { + requestBody = openApiSampler.sample(contentObject.schema, {}, spec); + let requestXML; + if (contentType === 'application/xml') { + requestXML = jstoxml.toXML(requestBody); + } + headers['Content-Type'] = contentType; + requestsArray.push({ + method, + path, + body: requestXML || requestBody, + headers, + }); + }); + } else { + requestsArray.push({ + method, + path, + headers, + }); + } +}; + +const getApiRequestsData = (apiSchema) => { + const requestsArray = []; + Object.entries(apiSchema.paths).forEach(([pathEndpoint, pathObject]) => { + logger.info(`adding path: ${pathPrefix}${pathEndpoint} to testing array`); + Object.entries(pathObject).forEach(([operationMethod, operationObject]) => { + buildRequestObject(pathEndpoint, operationMethod, operationObject, requestsArray); + }); + }); + return requestsArray; +}; + +describe('API tests, checking that the codegen generated code that allows all paths specified in schema to work', () => { + before(async () => { + try { + await app.launch(); + logger.info('express server launched\n'); + } catch (error) { + logger.info(error); + await app.close(); + throw (error); + } + }); + + after(async () => { + await app.close() + .catch(error => logger.error(error)); + logger.error('express server closed'); + }); + + const requestsArray = getApiRequestsData(spec); + requestsArray.forEach((requestObject) => { + it(`should run ${requestObject.method.toUpperCase()} request to ${requestObject.path} and return healthy 200`, async () => { + try { + const { + method, path, body, headers, + } = requestObject; + const url = `${pathPrefix}${path}`; + logger.info(`testing ${method.toUpperCase()} call to ${url}. encoding: ${headers['Content-Type']}`); + const response = await axios({ + method, + url, + data: body, + headers, + }); + response.should.have.property('status'); + response.status.should.equal(200); + } catch (e) { + assert.fail(e.message); + } + }); + }); +});