diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b3949c4f704e19351c9126df4c326bdfaa229998..04cdb1f9f46441a3617636eea51bf6403f1e2ab9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,13 +1,27 @@ stages: - test +services: + - mongo:latest + +variables: + MONGO_URI: 'mongodb://mongo/app_name' + NODE_ENV: 'test' + before_script: - - npm install --global gulp gulp-cli babel babel-cli babel-core babel-register mocha + - npm install --global gulp gulp-cli babel babel-cli babel-core babel-register mocha gulp-mocha gulp-eslint - npm install run_tests: stage: test script: + - ping -W1 -c1 mongo + - mv config.json.example config.json + - sed -i -e 's/false/true/g' config.json + - gulp build - gulp test tags: - node + cache: + paths: + - node_modules/ diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000000000000000000000000000000000..7733f133f247a71bbb99a09dacd3a92b4963d6a4 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,11 @@ +# Contributor code of conduct + +As contributors and maintainers of the SimCAQ project, we pledge to respect everyone who contributes by posting issues, updating documentation, submitting merge requests, providing feedback in comments, and any other activities. + +Communication through any of SimCAQ's channels (GitLab, Mattermos, mailing lists, etc.) must be constructive and never resort to personal attacks, trolling, public or private harrassment, insults, or other unprofessional conduct. + +We promise to extend courtesy and respect to everyone involved in this project regardless of gender, gender identity, sexual orientation, disability, age, race, ethnicity, religion, or level of experience. We expect anyone contributing to the SimCAQ project to do the same. + +If any member of the community violates this code of conduct, the maintainers of the SimCAQ project may take action, removing issues, comments, and MRs or blocking accounts as deemed appropriate. + +If you are subject to or witness unacceptable behavior, or have any other concerns, please contact us. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000000000000000000000000000000000..6991bd19d1d11e8b87bcc4e38506259747f28ab6 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,47 @@ +# Contributing to SimCAQ API + +## Before you get started + +### Code of Conduct + +By participating of this project you are expected to hold the [Code of Conduct](https://gitlab.c3sl.ufpr.br/simcaq/simcaq-node/blob/development/CODE_OF_CONDUCT.md). + +## How Can I Contribute? + +### Reporting Bugs +Bugs are reported and tracked at [simcaq/SCRUM](https://gitlab.c3sl.ufpr.br/simcaq/SCRUM/board) repository. As you determinate which repository your bug is related to, create an issue and label it with the appropriate tags. + +## Branch structure + +* **master**: protected branch and contains the stable version +* **development**: default branch +* **issue/??**: issue branch - a branch created to solve a issue +* **feature_????**: feature branch - a branch created to add a feature + +## Styleguide + +### Commit messages + +* Use the present tense ("Add feature" not "Added feature") +* Use the imperative mood ("Move cursor to..." not "Moves cursor to...") +* Limit the first line to 72 characters or less +* Reference issues and pull requests liberally +* When only changing documentation, include [ci skip] in the commit description +* Consider starting the commit message with an applicable emoji: + * :art: `:art:` when improving the format/structure of the code + * :racehorse: `:racehorse:` when improving performance + * :memo: `:memo:` when writing docs + * :bug: `:bug:` when fixing a bug + * :fire: `:fire:` when removing code or files + * :green_heart: `:green_heart:` when fixing the CI build + * :white_check_mark: `:white_check_mark:` when adding tests + * :lock: `:lock:` when dealing with security + * :arrow_up: `:arrow_up:` when upgrading dependencies + * :arrow_down: `arrow_down` when downgrading dependencies + +### JavaScript Styleguide + +All JavaScript follows the [AirBnB styleguide](https://github.com/airbnb/javascript) with the following modifications: +* 4 spaces of identation +* No unused vars +* Allow param reassign diff --git a/README.md b/README.md index 12530fa13c5edcffdd2b9e68892950e68b9ef71a..74ecd6234f14689c66dc397dbf7af2845ca1e76a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ # SIMCAQ +[](https://gitlab.c3sl.ufpr.br/simcaq/simcaq-node/commits/development) +[](https://gitlab.c3sl.ufpr.br/simcaq/simcaq-node/commits/development) # Dependencies @@ -16,9 +18,9 @@ Previous versions of Node.js do not support ECMAScript6, it is recommended to us > nvm use v4.5.0 -4) Install babel and gulp globally +4) Install the global dependencies -> npm install -g gulp gulp-cli babel babel-cli babel-core babel-register mocha +> npm install --global gulp gulp-cli babel babel-cli babel-core babel-register mocha gulp-mocha gulp-eslint istanbul 5) Install project dependencies diff --git a/gulpfile.babel.js b/gulpfile.babel.js index 1f23dd9ee0b5c083c7f66d6d6650982a34a748a6..b2758b0a6abe55329d30fb9658e0547e5fcb150d 100644 --- a/gulpfile.babel.js +++ b/gulpfile.babel.js @@ -1,3 +1,5 @@ +require('babel-core/register'); + const fs = require('fs'); const gulp = require('gulp'); @@ -8,46 +10,49 @@ const eslint = require('gulp-eslint'); const mocha = require('gulp-mocha'); +const istanbul = require('gulp-istanbul'); + const nodemon = require('gulp-nodemon'); const Cache = require('gulp-file-cache'); const docco = require('gulp-docco'); +const mkdirp = require('mkdirp'); + const cache = new Cache(); function createLogDir() { const logDirPath = 'build/logs'; - fs.access(logDirPath, fs.F_OK, (err) => { - if (err) { - console.info(`Logs directory not found, creating it.`); - fs.mkdir(logDirPath, 0o700, (dirErr) => { - console.error(`Failed to create logs directory.\n\tError: ${dirErr}`); - }); - } + mkdirp(logDirPath, (err) => { + if(err) console.error(err); }); } -/** - * Compile source files - */ -gulp.task('compile', () => { - createLogDir(); +gulp.task('lint', () => { // run ESLint gulp.src('src/**/*.js') - .pipe(eslint()) - .pipe(eslint.format()); + .pipe(eslint()) + .pipe(eslint.format()); +}) + +/** +* Compile source files +*/ +gulp.task('compile', ['lint'], () => { // compile source to ES5 gulp.src('src/**/*.js') - .pipe(cache.filter()) // cache source files - .pipe(babel()) // compile only modified files - .pipe(cache.cache()) // cache compiled files - .pipe(gulp.dest('build')); // move compiled files to build directory + .pipe(cache.filter()) // cache source files + .pipe(babel()) // compile only modified files + // .pipe(cache.cache()) // cache compiled files + .pipe(gulp.dest('build')); // move compiled files to build directory // copy configuration file to build directory gulp.src('config.json') - .pipe(gulp.dest('build')); + .pipe(gulp.dest('build')); + + createLogDir(); }); gulp.task('build', ['compile']); @@ -60,15 +65,28 @@ gulp.task('docco', () => { gulp.task('doc', ['docco']); -gulp.task('test', ['build'], () => { +gulp.task('pre-test', () => { + return gulp.src(['build/**/*.js', '!build/{test,test/**}']) + .pipe(istanbul()) + .pipe(istanbul.hookRequire()); +}); + +gulp.task('test', ['pre-test'], () => { + process.chdir('build'); gulp.src('test/test.js', {read: false}) - .pipe(mocha()) - .once('error', () => { - process.exit(1); - }) - .once('end', () => { - process.exit(); - }); + .pipe(mocha()) + .pipe(istanbul.writeReports()) + .pipe(istanbul.enforceThresholds({ + thresholds: { + global: 80 + } + })) + .on('error', () => { + process.exit(1); + }) + .on('end', () => { + process.exit(); + }); }); gulp.task('watch', ['compile'], () => { diff --git a/package.json b/package.json index 5973ed2a9f2897def179077fada4c2075032f841..acc29cd2e3d6ff9a605fde13cfaa6774243b6307 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "babel-register": "^6.14.0", "babelify": "^7.3.0", "browserify": "^13.1.0", + "chai-xml": "^0.3.1", "docdash": "^0.4.0", "eslint": "^3.3.1", "eslint-config-airbnb": "^10.0.1", @@ -51,12 +52,15 @@ "gulp-docco": "0.0.4", "gulp-eslint": "^3.0.1", "gulp-file-cache": "0.0.1", + "gulp-function": "^1.3.6", + "gulp-istanbul": "^1.1.1", "gulp-jsdoc3": "^0.3.0", "gulp-mocha": "^3.0.1", "gulp-nodemon": "^2.1.0", "gulp-plumber": "^1.1.0", "gulp-rename": "^1.2.2", "gulp-uglify": "^2.0.0", - "jsdoc": "^3.4.1" + "jsdoc": "^3.4.1", + "mkdirp": "^0.5.1" } } diff --git a/src/libs/app.js b/src/libs/app.js index a1b3ff01248a70c6a647a63016ddc605a1dbb3d5..5133df46dc0ebe60b00718ae57e9aea0dd6cbaf3 100644 --- a/src/libs/app.js +++ b/src/libs/app.js @@ -22,9 +22,6 @@ const mongoose = require(`${libs}/db/mongoose`); const db = mongoose(); -// Set default node environment - -// Parse json received in requests app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: false })); app.use(cookieParser()); diff --git a/src/libs/db/mongoose.js b/src/libs/db/mongoose.js index 8571b993b009aa644bc3bcb15cb846494b076a9c..5be072989a67d56b69a9ce0cb184581ae4ecc42c 100644 --- a/src/libs/db/mongoose.js +++ b/src/libs/db/mongoose.js @@ -8,7 +8,7 @@ const mongoose = require('mongoose'); module.exports = () => { // Get mongodb URI (ip and port) in config file - const mongoUri = config.mongodb.uri; + const mongoUri = process.env.MONGO_URI || config.mongodb.uri; log.debug(`Connecting to MongDB on URI ${mongoUri}`); // Connection singleton const db = mongoose.connect(mongoUri); diff --git a/src/libs/log.js b/src/libs/log.js index 1cf59931babf02f74baac17e4e4fd775f828076a..ddf0303f8481514b047aa6678894316500d25a96 100644 --- a/src/libs/log.js +++ b/src/libs/log.js @@ -38,7 +38,7 @@ function logger(module) { }), new winston.transports.Console({ name: 'debug-log', - level: 'debug', + level: (process.env.NODE_ENV === 'development') ? 'debug' : 'error', label: getFilePath(module), handleException: true, json: false, diff --git a/src/libs/routes/enrollment.js b/src/libs/routes/enrollment.js index a73f203fe0cdbf8c5cbd4300541f3d2383cfec9d..9fe6e903bdbe9bc10b89181866a920792b832533 100644 --- a/src/libs/routes/enrollment.js +++ b/src/libs/routes/enrollment.js @@ -48,11 +48,6 @@ enrollmentApp.get('/adm_dependency', (req, res, next) => { next(); }, query, response('adm_dependency')); -enrollmentApp.get('/data', (req, res, next) => { - req.sql = squel.select().from('turmas'); - next(); -}, query, response('data')); - // Parse the filters and dimensions parameter in the query enrollmentApp.use('/', parseParams('filter', [ 'min_year', @@ -161,7 +156,8 @@ enrollmentApp.use('/', parseParams('filter', [ if(typeof req.dims.region === 'undefined' && typeof req.dims.state === 'undefined' - && typeof req.dims.city === 'undefined') { + && typeof req.dims.city === 'undefined' + && typeof req.dims.school === 'undefined') { req.sql.field("'Brasil'", 'name'); } diff --git a/src/libs/routes/school.js b/src/libs/routes/school.js index 8662d945f4f39989bc318709e7beab9802a3bfb8..6c92430b425d3b9fde8c682216c8f6a10f07b75f 100644 --- a/src/libs/routes/school.js +++ b/src/libs/routes/school.js @@ -37,8 +37,8 @@ schoolApp.get('/:id', (req, res, next) => { schoolApp.get('/state/:id', (req, res, next) => { req.sql.from('escolas') .field('pk_escola_id') - .field('nome_entidade', 'name') - .field('ano_censo', 'year') + .field('nome_entidade') + .field('ano_censo') .field('fk_cod_estado') .field('fk_cod_municipio') .where('fk_cod_estado = ?', parseInt(req.params.id, 10)); @@ -49,8 +49,8 @@ schoolApp.get('/state/:id', (req, res, next) => { schoolApp.get('/city/:id', (req, res, next) => { req.sql.from('escolas') .field('pk_escola_id') - .field('nome_entidade', 'name') - .field('ano_censo', 'year') + .field('nome_entidade') + .field('ano_censo') .field('fk_cod_estado') .field('fk_cod_municipio') .where('fk_cod_municipio = ?', parseInt(req.params.id, 10)); diff --git a/src/server.js b/src/server.js index ea8de70ee6929c2105ff1099d96ced244e84787d..4e097f28a4053050757c6329e19e225e0424b961 100644 --- a/src/server.js +++ b/src/server.js @@ -9,6 +9,9 @@ const log = require(`${libs}/log`)(module); const app = require(`${libs}/app`); // Set default port: first environment variable PORT, then configuration and last 3000 +app.set('port', process.env.PORT || config.port || 3000); +process.env.NODE_ENV = process.env.NODE_ENV || 'development'; + app.set('port', process.env.PORT || config.port || 3000); // Set default ip: first environment variable IOP, then configuration and last '127.0.0.1' @@ -17,3 +20,6 @@ app.set('ip', process.env.IP || config.ip || '127.0.0.1'); const server = app.listen(app.get('port'), () => { log.info(`Express server listening on port ${server.address().port}`); }); + +// For testing +module.exports = server; diff --git a/src/test/test.js b/src/test/test.js index e3f03fc2ef4aecc600c4a06fa6607e51119945bd..c25c218b0ecdb168196835520de3846b9aae111d 100644 --- a/src/test/test.js +++ b/src/test/test.js @@ -6,6 +6,10 @@ const dirtyChai = require('dirty-chai'); chai.use(dirtyChai); +const chaiXml = require('chai-xml'); + +chai.use(chaiXml); + const chaiHttp = require('chai-http'); const assert = chai.assert; @@ -14,14 +18,71 @@ const expect = chai.expect; const should = chai.should(); // actually call the function -const server = require('../libs/app'); +const libs = `${process.cwd()}/libs`; + +const server = require(`${libs}/app`); const mongoose = require('../libs/db/mongoose'); const Simulation = require('../libs/models/simulation'); chai.use(chaiHttp); +describe('API is running', () => { + it('should respond it\'s running', (done) => { + chai.request(server) + .get('/api/v1') + .end((err, res) => { + res.should.have.status(200); + res.should.be.json; + res.body.should.have.property('msg'); + done(); + }) + }); +}); + describe('request enrollments', () => { + it('should list the year range', (done) => { + chai.request(server) + .get('/api/v1/enrollment/year_range') + .end((err, res) => { + res.should.have.status(200); + res.should.be.json; + res.body.should.have.property('result'); + res.body.result.should.be.a('array'); + res.body.result[0].should.have.property('start_year'); + res.body.result[0].should.have.property('end_year'); + done(); + }); + }); + + it('should list the education level', (done) => { + chai.request(server) + .get('/api/v1/enrollment/education_level') + .end((err, res) => { + res.should.have.status(200); + res.should.be.json; + res.body.should.have.property('result'); + res.body.result.should.be.a('array'); + res.body.result[0].should.have.property('id'); + res.body.result[0].should.have.property('name'); + done(); + }); + }); + + it('should list the administrative dependencies', (done) => { + chai.request(server) + .get('/api/v1/enrollment/adm_dependency ') + .end((err, res) => { + res.should.have.status(200); + res.should.be.json; + res.body.should.have.property('result'); + res.body.result.should.be.a('array'); + res.body.result[0].should.have.property('id'); + res.body.result[0].should.have.property('name'); + done(); + }); + }); + it('should list enrollments', (done) => { chai.request(server) .get('/api/v1/enrollment') @@ -35,6 +96,84 @@ describe('request enrollments', () => { done(); }); }); + + it('should list enrollments with valid filters', (done) => { + chai.request(server) + .get('/api/v1/enrollment?filter=min_year:2010,state:41') + .end((err, res) => { + res.should.have.status(200); + res.should.be.json; + res.body.should.have.property('result'); + res.body.result.should.be.a('array'); + res.body.result[0].should.have.property('name'); + res.body.result[0].should.have.property('total'); + done(); + }); + }); + + it('should list enrollments with invalid filters', (done) => { + chai.request(server) + .get('/api/v1/enrollment?filter=foo:2010,bar:41') + .end((err, res) => { + res.should.have.status(200); + res.should.be.json; + res.body.should.have.property('result'); + res.body.result.should.be.a('array'); + res.body.result[0].should.have.property('name'); + res.body.result[0].should.have.property('total'); + done(); + }); + }); + + it('should list enrollments with valid dimensions', (done) => { + chai.request(server) + .get('/api/v1/enrollment?dims=region,state,adm_dependency_id,location_id&filter=min_year:2014,region:4') + .end((err, res) => { + res.should.have.status(200); + res.should.be.json; + res.body.should.have.property('result'); + res.body.result.should.be.a('array'); + res.body.result[0].should.have.property('region_name'); + res.body.result[0].should.have.property('state_name'); + res.body.result[0].should.have.property('adm_dependency_name'); + res.body.result[0].should.have.property('total'); + done(); + }); + }); + + it('should list enrollments with invalid dimensions', (done) => { + chai.request(server) + .get('/api/v1/enrollment?dims=foo,bar') + .end((err, res) => { + res.should.have.status(200); + res.should.be.json; + res.body.should.have.property('result'); + res.body.result.should.be.a('array'); + res.body.result[0].should.have.property('name'); + res.body.result[0].should.have.property('total'); + done(); + }); + }); + + it('should list enrollments with valid dimensions and filters', (done) => { + chai.request(server) + .get('/api/v1/enrollment?dims=region,state,education_level_id,school&filter=min_year:2013,max_year:2014,city:3287') + .end((err, res) => { + res.should.have.status(200); + res.should.be.json; + res.body.should.have.property('result'); + res.body.result.should.be.a('array'); + res.body.result[0].should.have.property('region_name'); + res.body.result[0].should.have.property('state_name'); + res.body.result[0].should.have.property('school_name'); + res.body.result[0].should.have.property('education_level'); + res.body.result[0].should.have.property('total'); + res.body.result[0].should.have.property('year'); + done(); + }); + }); + + }); describe('request regions', () => { @@ -164,6 +303,101 @@ describe('request cities', () => { done(); }); }); + + it('should list all cities from a state', (done) => { + chai.request(server) + .get('/api/v1/city/state/41') + .end((err, res) => { + res.should.have.status(200); + res.should.be.json; + res.body.should.have.property('result'); + res.body.result.should.be.a('array'); + res.body.result[0].should.have.property('pk_municipio_id'); + res.body.result[0].should.have.property('fk_estado_id'); + res.body.result[0].should.have.property('nome'); + res.body.result[0].should.have.property('codigo_ibge'); + done(); + }) + }) +}); + +describe('request schools', () => { + it('should list a school by id', (done) => { + chai.request(server) + .get('/api/v1/school/185588') + .end((err, res) => { + res.should.have.status(200); + res.should.be.json; + res.body.should.have.property('result'); + res.body.result.should.be.a('array'); + res.body.result[0].should.have.property('pk_escola_id'); + res.body.result[0].should.have.property('ano_censo'); + res.body.result[0].should.have.property('cod_entidade'); + res.body.result[0].should.have.property('nome_entidade'); + done(); + }); + }); + + it('should list all schools from a state', (done) => { + chai.request(server) + .get('/api/v1/school/state/41') + .end((err, res) => { + res.should.have.status(200); + res.should.be.json; + res.body.should.have.property('result'); + res.body.result.should.be.a('array'); + res.body.result[0].should.have.property('pk_escola_id'); + res.body.result[0].should.have.property('ano_censo'); + res.body.result[0].should.have.property('nome_entidade'); + done(); + }); + }); + + it('should list all schools from a city', (done) => { + chai.request(server) + .get('/api/v1/school/city/3287') + .end((err, res) => { + res.should.have.status(200); + res.should.be.json; + res.body.should.have.property('result'); + res.body.result.should.be.a('array'); + res.body.result[0].should.have.property('pk_escola_id'); + res.body.result[0].should.have.property('ano_censo'); + res.body.result[0].should.have.property('nome_entidade'); + done(); + }) + }) +}); + +describe('test response', () => { + it('should list all regions in json', (done) => { + chai.request(server) + .get('/api/v1/region') + .end((err, res) => { + res.should.have.status(200); + res.should.be.json; + done(); + }); + }); + + it('should list all regions in xml', (done) => { + chai.request(server) + .get('/api/v1/region?format=xml') + .end((err, res) => { + res.should.have.status(200); + res.should.be.xml; + done(); + }); + }); + + it('should list all regions in csv', (done) => { + chai.request(server) + .get('/api/v1/region?format=csv') + .end((err, res) => { + res.should.have.status(200); + done(); + }); + }); }); describe('Requires a simulation', () => {