diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml new file mode 100644 index 0000000..cde7d07 --- /dev/null +++ b/.github/workflows/unit_tests.yml @@ -0,0 +1,60 @@ +name: Unit tests & lint + +on: + push: + branches: + - master + pull_request: + types: + - opened + - edited + - synchronize + +jobs: + lint: + timeout-minutes: 5 + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v1 + + - name: Install node + uses: actions/setup-node@v1 + with: + node-version: 16.x + + - name: Install dependencies + run: npm install + + - name: Check lint + run: npm run lint + + unit-tests: + timeout-minutes: 10 + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v1 + + - name: Install Yarn + run: npm install -g yarn + + - name: Start docker containers + run: cd app && docker-compose -f "docker-compose.yaml" up -d --build + + - name: Install node + uses: actions/setup-node@v1 + with: + node-version: 16.x + + - name: Install dependencies + run: cd app && yarn install + + - name: Run unit tests + run: cd app && yarn run test + + - name: Stop docker containers + if: always() + run: cd app && docker-compose -f "docker-compose.yaml" down \ No newline at end of file diff --git a/README.md b/README.md index 6725dfb..3100893 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,16 @@ [![Codacy Badge](https://api.codacy.com/project/badge/Grade/c6cd37818d9c4ab6b244bfefd5b83597)](https://www.codacy.com/app/fesnt/ufabc-matricula-server?utm_source=github.com&utm_medium=referral&utm_content=ufabc-next/ufabc-matricula-server&utm_campaign=Badge_Grade) [![codecov](https://codecov.io/gh/ufabc-next/ufabc-matricula-server/branch/master/graph/badge.svg)](https://codecov.io/gh/ufabc-next/ufabc-matricula-server) -Back-end server in Node.js for UFABC Next services. +### Para executar o server + +- Entrar em ufabc-next-server/app e executar o `yarn install`: +- Instalar o **Docker** e o **Docker Compose** +- Dentro de ufabc-next-server/app, como administrador, executar `docker-compose up -d` +- Para iniciar o servidor, executar como `yarn start:watch` a fim de verificar quando um arquivo for atualizado. Dessa forma, o servidor reiniciará automaticamente + +### Testes -. +- Para popular o Banco de Dados com uma massa de dados padrão, executar o `yarn populate both` +- `yarn test` para executar os testes unitários + +Back-end server in Node.js for UFABC Next services. diff --git a/app/api/comment/create/func.js b/app/api/comment/create/func.js index 988fd2e..263d60b 100644 --- a/app/api/comment/create/func.js +++ b/app/api/comment/create/func.js @@ -3,23 +3,32 @@ const errors = require('@/errors') const Fields = require('@/api/comment/Fields') const pickFields = require('@/helpers/parse/pickFields') -module.exports = async function(context){ +module.exports = async function (context) { const Comment = app.models.comments const Enrollment = app.models.enrollments - app.helpers.validate.throwMissingParameter(['enrollment', 'comment', 'type'], context.body) + app.helpers.validate.throwMissingParameter( + ['enrollment', 'comment', 'type'], + context.body + ) let enrollment = await Enrollment.findById(String(context.body.enrollment)) - if(!enrollment) throw new errors.BadRequest(`Este vínculo não existe: ${enrollment.id}`) + if (!enrollment) + throw new errors.BadRequest( + `Este vínculo não existe: ${context.body.enrollment}` + ) - return pickFields(await Comment.create({ - comment: String(context.body.comment), - type: context.body.type, - enrollment: enrollment.id, - teacher: enrollment[context.body.type], - disciplina: enrollment.disciplina, - subject: enrollment.subject, - ra: enrollment.ra - }), Fields) -} \ No newline at end of file + return pickFields( + await Comment.create({ + comment: String(context.body.comment), + type: context.body.type, + enrollment: enrollment.id, + teacher: enrollment[context.body.type], + disciplina: enrollment.disciplina, + subject: enrollment.subject, + ra: enrollment.ra, + }), + Fields + ) +} diff --git a/app/api/comment/create/spec.js b/app/api/comment/create/spec.js new file mode 100644 index 0000000..3e22ec3 --- /dev/null +++ b/app/api/comment/create/spec.js @@ -0,0 +1,68 @@ +const app = require('@/app') +const func = require('./func') +const assert = require('assert') +const populate = require('@/populate') + +describe('POST /v1/comments', async function () { + let models + let context + let enrollment + beforeEach(async function () { + models = await populate({ + operation: 'both', + only: ['enrollments', 'teachers', 'subjects', 'comments'], + }) + + enrollment = models.enrollments[0] + context = { + body: { + enrollment: enrollment._id, + comment: 'Some comment', + type: 'pratica', + }, + } + }) + describe('func', async function () { + describe('with valid params', async function () { + it('create and return a filtered comment', async function () { + const resp = await func(context) + + assert(resp._id) + assert.equal(resp.comment, context.body.comment) + assert(resp.createdAt) + assert(new Date() > resp.createdAt) + assert.equal(resp.enrollment._id, context.body.enrollment) + assert.equal(resp.subject, enrollment.subject) + assert.equal(resp.teacher, enrollment.mainTeacher) + assert.notEqual(resp.ra, enrollment.ra) + + const Comment = app.models.comments + const comment = await Comment.findOne({ _id: resp._id }) + assert(comment) + }) + }) + describe('with invalid params', async function () { + it('should throw if enrollment is missing', async function () { + delete context.body.enrollment + await assertFuncThrows('MissingParameter', func, context) + }) + it('should throw if comment is missing', async function () { + delete context.body.comment + await assertFuncThrows('MissingParameter', func, context) + }) + it('should throw if type is missing', async function () { + delete context.body.type + await assertFuncThrows('MissingParameter', func, context) + }) + it('should throw if enrollment is invalid', async function () { + // Invalid enrollment id + context.body.enrollment = models.comments[0]._id + await assertFuncThrows('BadRequest', func, context) + }) + it('should throw if is a duplicated comment', async function () { + await func(context) + await assertFuncThrows('BadRequest', func, context) + }) + }) + }) +}) diff --git a/app/api/disciplinas/disciplinaId/kicks/func.js b/app/api/disciplinas/disciplinaId/kicks/func.js index 6d3e2e5..61bceaa 100644 --- a/app/api/disciplinas/disciplinaId/kicks/func.js +++ b/app/api/disciplinas/disciplinaId/kicks/func.js @@ -1,56 +1,56 @@ -const _ = require("lodash"); -const app = require("@/app"); -const errors = require("@/errors"); +const _ = require('lodash') +const app = require('@/app') +const errors = require('@/errors') module.exports = async function (context) { - let { disciplinaId } = context.params; - let { sort } = context.query; + let { disciplinaId } = context.params + let { sort } = context.query if (!disciplinaId) { - throw new errors.BadRequest.MissingParameter("disciplinaId"); + throw new errors.BadRequest.MissingParameter('disciplinaId') } - const season = app.helpers.season.findSeasonKey(); - const Disciplinas = app.models.disciplinas.bySeason(season); - const Alunos = app.models.alunos.bySeason(season); - const findId = app.helpers.courses.findId; + const season = app.helpers.season.findSeasonKey() + const Disciplinas = app.models.disciplinas.bySeason(season) + const Alunos = app.models.alunos.bySeason(season) + const findId = app.helpers.courses.findId // find disciplina - let disciplina = await Disciplinas.findOne({ disciplina_id: disciplinaId }); + let disciplina = await Disciplinas.findOne({ disciplina_id: disciplinaId }) if (!disciplina) { - throw new errors.NotFound("disciplina"); + throw new errors.NotFound('disciplina') } // create sort mechanism - const kicks = sort ? sort : kickRule(disciplina); - const order = Array(kicks.length || 0).fill("desc"); + const kicks = sort ? sort : kickRule(disciplina) + const order = Array(kicks.length || 0).fill('desc') // turno must have a special treatment - const turnoIndex = kicks.indexOf("turno"); + const turnoIndex = kicks.indexOf('turno') if (turnoIndex != -1) { - order[turnoIndex] = disciplina.turno == "diurno" ? "asc" : "desc"; + order[turnoIndex] = disciplina.turno == 'diurno' ? 'asc' : 'desc' } const isAfterKick = _(disciplina.after_kick).castArray().compact().value() - .length; - const result = resolveMatriculas(disciplina, isAfterKick); + .length + const result = resolveMatriculas(disciplina, isAfterKick) - const resultMap = new Map([...result.map((r) => [r.aluno_id, r])]); + const resultMap = new Map([...result.map((r) => [r.aluno_id, r])]) let students = await Alunos.aggregate([ - { $match: { aluno_id: { $in: _.map(result, "aluno_id") } } }, - { $unwind: "$cursos" }, - ]); + { $match: { aluno_id: { $in: _.map(result, 'aluno_id') } } }, + { $unwind: '$cursos' }, + ]) const interIds = [ - await findId("Bacharelado em Ciência e Tecnologia", season), - await findId("Bacharelado em Ciência e Humanidades", season), - ]; + await findId('Bacharelado em Ciência e Tecnologia', season), + await findId('Bacharelado em Ciência e Humanidades', season), + ] - const obrigatorias = _.pull(disciplina.obrigatorias, ...interIds); + const obrigatorias = _.pull(disciplina.obrigatorias, ...interIds) students = students.map((s) => { - const reserva = _.includes(obrigatorias, s.cursos.id_curso); + const reserva = _.includes(obrigatorias, s.cursos.id_curso) return _.extend( { @@ -63,27 +63,27 @@ module.exports = async function (context) { curso: s.cursos.nome_curso, }, resultMap.get(s.aluno_id) || {} - ); - }); + ) + }) - return _(students).orderBy(kicks, order).uniqBy("aluno_id").value(); -}; + return _(students).orderBy(kicks, order).uniqBy('aluno_id').value() +} function kickRule(disciplina) { - const season = app.helpers.season.findSeasonKey(); - let coeffRule = null; + const season = app.helpers.season.findSeasonKey() + let coeffRule = null if ( - season == "2020:2" || - season == "2020:3" || - season == "2021:1" || - season == "2021:2" || - season == "2021:3" + season == '2020:2' || + season == '2020:3' || + season == '2021:1' || + season == '2021:2' || + season == '2021:3' ) { - coeffRule = ["cp", "cr"]; + coeffRule = ['cp', 'cr'] } else { - coeffRule = disciplina.ideal_quad ? ["cr", "cp"] : ["cp", "cr"]; + coeffRule = disciplina.ideal_quad ? ['cr', 'cp'] : ['cp', 'cr'] } - return ["reserva", "turno", "ik"].concat(coeffRule); + return ['reserva', 'turno', 'ik'].concat(coeffRule) } function resolveMatriculas(disciplina, isAfterKick) { @@ -91,18 +91,18 @@ function resolveMatriculas(disciplina, isAfterKick) { if (!isAfterKick) { return (disciplina.alunos_matriculados || []).map((d) => ({ aluno_id: d, - })); + })) } // check diff between before_kick and after_kick let kicked = _.difference( disciplina.before_kick || [], disciplina.after_kick || [] - ); + ) // return who has been kicked return disciplina.before_kick.map((d) => ({ aluno_id: d, kicked: kicked.includes(d), - })); + })) } diff --git a/app/api/disciplinas/disciplinaId/kicks/spec.js b/app/api/disciplinas/disciplinaId/kicks/spec.js index 9cc0d41..2bcb8e4 100644 --- a/app/api/disciplinas/disciplinaId/kicks/spec.js +++ b/app/api/disciplinas/disciplinaId/kicks/spec.js @@ -4,54 +4,55 @@ const populate = require('@/populate') const func = require('./func') const rule = require('./rule') -describe('GET /v1/disciplinas/:disciplina-id/kicks', function() { +describe('GET /v1/disciplinas/:disciplina-id/kicks', function () { var context const season = app.helpers.season.findSeasonKey() const Disciplinas = app.models.disciplinas.bySeason(season) - + beforeEach(async function () { await populate({ operation: 'both', only: ['disciplinas', 'alunos'] }) - + context = { query: {}, - params: {} + params: {}, } }) describe('func', function () { - xit('returns a complete list of disciplinas', async function () { + it('returns a complete list of disciplinas', async function () { let disciplina = await Disciplinas.create({ disciplina_id: 100, + identifier: 'some identifier', alunos_matriculados: [12263], turno: 'diurno', before_kick: [10523, 1504], obrigatorias: [6, 20], - after_kick: [1504] + after_kick: [1504], }) context.params.disciplinaId = disciplina.disciplina_id let resp = await func(context) - - assert.equal(resp.length, 3) - assert(resp.every(s => 'kicked' in s)) + + assert.equal(resp.length, 2) + assert(resp.every((s) => 'kicked' in s)) }) - xit('allows custom query method', async function () { + it('allows custom query method', async function () { context.query.sort = ['cr', 'cp'] - let disciplina = await Disciplinas.create({ disciplina_id: 100, + identifier: 'some identifier', alunos_matriculados: [12263, 10523], turno: 'noturno', obrigatorias: [6, 20], }) - + context.params.disciplinaId = disciplina.disciplina_id let resp = await func(context) - - assert.equal(resp.length, 3) - assert(resp.every(s => !('kicked' in s))) + + assert.equal(resp.length, 2) + assert(resp.every((s) => !('kicked' in s))) }) }) @@ -70,4 +71,4 @@ describe('GET /v1/disciplinas/:disciplina-id/kicks', function() { await rule(context) }) }) -}) \ No newline at end of file +}) diff --git a/app/api/disciplinas/list/spec.js b/app/api/disciplinas/list/spec.js index 451246c..fbb9dfc 100644 --- a/app/api/disciplinas/list/spec.js +++ b/app/api/disciplinas/list/spec.js @@ -7,23 +7,30 @@ const Axios = require('axios') const func = require('./func') const sync = require('@/api/disciplinas/sync/func') -describe('GET /v1/disciplinas', function() { +describe('GET /v1/disciplinas', function () { beforeEach(async function () { await populate({ operation: 'both', only: ['disciplinas', 'subjects'] }) }) describe('func', function () { + let axiosInstanceStub + let axiosGetStub beforeEach(async function () { let file = app.helpers.test.getDisciplinas() file.data = app.helpers.test.sample(file.data, 100) - let stub = sinon.stub(Axios, 'get').returns(file) + axiosInstanceStub = sinon.stub(Axios, 'create').returns(Axios) + axiosGetStub = sinon.stub(Axios, 'get').returns(file) await sync() - stub.restore() }) - xit('returns a complete list of disciplinas', async function () { + afterEach(function () { + axiosInstanceStub.restore() + axiosGetStub.restore() + }) + + it('returns a complete list of disciplinas', async function () { let resp = await func() assert.equal(resp.length, 100) }) }) -}) \ No newline at end of file +}) diff --git a/app/api/disciplinas/sync/func.js b/app/api/disciplinas/sync/func.js index c876850..b53e2e5 100644 --- a/app/api/disciplinas/sync/func.js +++ b/app/api/disciplinas/sync/func.js @@ -4,15 +4,15 @@ const errors = require('@/errors') const Axios = require('axios') const https = require('https') -module.exports = async(context = {}) => { +module.exports = async (context = {}) => { const { mappings } = context.body || {} const season = app.helpers.season.findSeasonKey() const Disciplinas = app.models.disciplinas.bySeason(season) const instance = Axios.create({ - httpsAgent: new https.Agent({ - rejectUnauthorized: false - }) + httpsAgent: new https.Agent({ + rejectUnauthorized: false, + }), }) const disciplinas = await instance.get(app.config.DISCIPLINAS_URL) @@ -22,24 +22,31 @@ module.exports = async(context = {}) => { // get all subjects const ONE_HOUR = 60 * 60 - const subjects = await app.models.subjects.find({}).lean(true).cache(ONE_HOUR, 'subjects') + const subjects = await app.models.subjects + .find({}) + .lean(true) + .cache(ONE_HOUR, 'subjects') // check if subjects actually exists before creating the relation const err = app.helpers.validate.subjects(payload, subjects, mappings) - - if(err.length) { + + if (err.length) { throw new errors.BadRequest.MissingSubject(_.uniq(err)) } - - async function updateDisciplinas(disciplina){ + + async function updateDisciplinas(disciplina) { // find and update disciplina - return await Disciplinas.findOneAndUpdate({ - disciplina_id: disciplina.disciplina_id, - identifier: app.helpers.transform.identifier(disciplina) - }, disciplina, { - upsert: true, - new: true - }) + return await Disciplinas.findOneAndUpdate( + { + disciplina_id: disciplina.disciplina_id, + identifier: app.helpers.transform.identifier(disciplina), + }, + disciplina, + { + upsert: true, + new: true, + } + ) } const start = Date.now() diff --git a/app/api/disciplinas/sync/spec.js b/app/api/disciplinas/sync/spec.js index 9e834a3..16bc4a0 100644 --- a/app/api/disciplinas/sync/spec.js +++ b/app/api/disciplinas/sync/spec.js @@ -6,31 +6,39 @@ const Axios = require('axios') const func = require('./func') -describe('POST /v1/disciplinas/sync', function() { +describe('POST /v1/disciplinas/sync', function () { beforeEach(async function () { - await populate({ operation : 'both', only: ['disciplinas'] }) + await populate({ operation: 'both', only: ['disciplinas'] }) }) describe('func', function () { - xit('sync disciplines', async function () { + it('sync disciplines', async function () { let file = app.helpers.test.getDisciplinas() file.data = app.helpers.test.sample(file.data, 200) - let stub = sinon.stub(Axios, 'get').returns(file) - await func() - - // check if is scoped by season - let Disciplinas = app.models.disciplinas.bySeason('2018:2') - assert.equal(await Disciplinas.findOne({}), null) - - const season = app.helpers.season.findSeasonKey() - Disciplinas = app.models.disciplinas.bySeason(season) - - assert.equal(await Disciplinas.countDocuments({}), 200) - let disciplina = await Disciplinas.findOne({ disciplina_id : 2538 }).lean(true) - assert.equal(disciplina.disciplina_id, 2538) - - stub.restore() + let axiosInstanceStub + let axiosGetStub + try { + axiosInstanceStub = sinon.stub(Axios, 'create').returns(Axios) + axiosGetStub = sinon.stub(Axios, 'get').returns(file) + await func() + + // check if is scoped by season + let Disciplinas = app.models.disciplinas.bySeason('2018:2') + assert.equal(await Disciplinas.findOne({}), null) + + const season = app.helpers.season.findSeasonKey() + Disciplinas = app.models.disciplinas.bySeason(season) + + assert.equal(await Disciplinas.countDocuments({}), 200) + let disciplina = await Disciplinas.findOne({ + disciplina_id: 2538, + }).lean(true) + assert.equal(disciplina.disciplina_id, 2538) + } finally { + axiosInstanceStub.restore() + axiosGetStub.restore() + } }) }) -}) \ No newline at end of file +}) diff --git a/app/api/matriculas/sync/spec.js b/app/api/matriculas/sync/spec.js index 54bd1b0..80cf512 100644 --- a/app/api/matriculas/sync/spec.js +++ b/app/api/matriculas/sync/spec.js @@ -7,11 +7,11 @@ const Axios = require('axios') const func = require('./func') const rule = require('./rule') -describe('GET /v1/matriculas/sync', function() { +describe('GET /v1/matriculas/sync', function () { var context - + beforeEach(async function () { - await populate({ operation : 'both', only: ['disciplinas'] }) + await populate({ operation: 'both', only: ['disciplinas'] }) context = { query: {}, @@ -19,17 +19,19 @@ describe('GET /v1/matriculas/sync', function() { } }) - describe.skip('func', function () { - let stub - + describe('func', function () { + let axiosGetStub + let axiosInstanceStub beforeEach(async function () { let file = app.helpers.test.getMatriculas() file.data = app.helpers.test.sample(file.data, 100) - stub = sinon.stub(Axios, 'get').returns(file) + axiosInstanceStub = sinon.stub(Axios, 'create').returns(Axios) + axiosGetStub = sinon.stub(Axios, 'get').returns(file) }) afterEach(async function () { - stub.restore() + axiosInstanceStub.restore() + axiosGetStub.restore() }) it('sync disciplines', async function () { @@ -43,7 +45,7 @@ describe('GET /v1/matriculas/sync', function() { Disciplinas = app.models.disciplinas.bySeason(season) let disciplina = await Disciplinas.findOne({ disciplina_id: 2000 }) - assert.equal(disciplina.disciplina_id, 2000) + assert.equal(disciplina.disciplina_id, 2000) assert(disciplina.alunos_matriculados.length) assert(!disciplina.before_kick.length) assert(!disciplina.after_kick.length) @@ -64,7 +66,7 @@ describe('GET /v1/matriculas/sync', function() { assert.equal(disciplina.disciplina_id, 2000) assert(!disciplina.alunos_matriculados.length) assert(disciplina.before_kick.length) - assert(!disciplina.after_kick.length) + assert(!disciplina.after_kick.length) }) it('update after_kick key', async function () { @@ -82,9 +84,8 @@ describe('GET /v1/matriculas/sync', function() { assert.equal(disciplina.disciplina_id, 2000) assert(!disciplina.alunos_matriculados.length) assert(!disciplina.before_kick.length) - assert(disciplina.after_kick.length) + assert(disciplina.after_kick.length) }) - }) describe('rule', function () { @@ -102,4 +103,4 @@ describe('GET /v1/matriculas/sync', function() { await assertFuncThrows('Forbidden', rule, context) }) }) -}) \ No newline at end of file +}) diff --git a/app/api/students/create/func.js b/app/api/students/create/func.js index 58e6de5..330d821 100644 --- a/app/api/students/create/func.js +++ b/app/api/students/create/func.js @@ -1,62 +1,62 @@ -const app = require("@/app"); -const errors = require("@/errors"); -const _ = require("lodash"); +const app = require('@/app') +const errors = require('@/errors') +const _ = require('lodash') module.exports = async (context) => { - const { aluno_id, ra, login } = context.body; + const { aluno_id, ra, login } = context.body if (!aluno_id) { - throw new errors.BadRequest.MissingParameter("aluno_id"); + throw new errors.BadRequest.MissingParameter('aluno_id') } - const season = app.helpers.season.findSeasonKey(); - const Alunos = app.models.alunos.bySeason(season); - const Disciplinas = app.models.disciplinas.bySeason(season); + const season = app.helpers.season.findSeasonKey() + const Alunos = app.models.alunos.bySeason(season) + const Disciplinas = app.models.disciplinas.bySeason(season) // check if already passed, if so does no update this user anymore const isPrevious = await Disciplinas.count({ before_kick: { $exists: true, $ne: [] }, - }); + }) if (isPrevious) { - return await Alunos.findOne({ aluno_id: aluno_id }); + return await Alunos.findOne({ aluno_id: aluno_id }) } if ( (context.body.cursos || []).some( - (curso) => !curso.curso_id || curso.curso_id == "null" + (curso) => !curso.curso_id || curso.curso_id == 'null' ) || !ra ) { return await Alunos.findOne({ aluno_id: aluno_id, - }); + }) } const cursos = (context.body.cursos || []).map(async (c) => { - let courseCleaned = c.curso.trim().replace("↵", "").replace(/\s+/g, " "); - let cpLastQuad = null; + let courseCleaned = c.curso.trim().replace('↵', '').replace(/\s+/g, ' ') + let cpLastQuad = null if ( - season == "2020:2" || - season == "2020:3" || - season == "2021:1" || - season == "2021:2" || - season == "2021:3" + season == '2020:2' || + season == '2020:3' || + season == '2021:1' || + season == '2021:2' || + season == '2021:3' ) { const history = await app.models.historiesGraduations.findOne({ ra: ra, curso: courseCleaned, - }); - cpLastQuad = _.get(history, "coefficients.2019.3.cp_acumulado", c.cp); + }) + cpLastQuad = _.get(history, 'coefficients.2019.3.cp_acumulado', c.cp) } - c.cr = _.isFinite(c.cr) ? app.helpers.parse.toNumber(c.cr) : 0; - c.cp = _.isFinite(c.cp) ? app.helpers.parse.toNumber(cpLastQuad) : 0; - c.quads = _.isFinite(c.quads) ? app.helpers.parse.toNumber(c.quads) : 0; - c.nome_curso = courseCleaned; - c.ind_afinidade = 0.07 * c.cr + 0.63 * c.cp + 0.005 * c.quads; - c.id_curso = c.curso_id; - return c; - }); + c.cr = _.isFinite(c.cr) ? app.helpers.parse.toNumber(c.cr) : 0 + c.cp = _.isFinite(c.cp) ? app.helpers.parse.toNumber(cpLastQuad) : 0 + c.quads = _.isFinite(c.quads) ? app.helpers.parse.toNumber(c.quads) : 0 + c.nome_curso = courseCleaned + c.ind_afinidade = 0.07 * c.cr + 0.63 * c.cp + 0.005 * c.quads + c.id_curso = c.curso_id + return c + }) return await Alunos.findOneAndUpdate( { @@ -71,5 +71,5 @@ module.exports = async (context) => { new: true, upsert: true, } - ); -}; + ) +} diff --git a/app/api/students/create/spec.js b/app/api/students/create/spec.js index d06fd8c..8e0d230 100644 --- a/app/api/students/create/spec.js +++ b/app/api/students/create/spec.js @@ -7,50 +7,63 @@ const Axios = require('axios') const func = require('./func') const sync = require('@/api/disciplinas/sync/func') -describe('POST /v1/students', function() { +describe('POST /v1/students', function () { let context + let axiosGetStub + let axiosInstanceStub const season = app.helpers.season.findSeasonKey() - + beforeEach(async function () { - await populate({ operation : 'both', only: ['disciplinas', 'subjects', 'alunos'] }) + await populate({ + operation: 'both', + only: ['disciplinas', 'subjects', 'alunos'], + }) context = { query: {}, body: { aluno_id: 3000, - cursos: [{ - cr: 2, - cp: 0.5, - quads: 3, - curso: 'Bacharelado em Ciência e Tecnologia' - }, { - cr: 2, - cp: 0.6, - quads: 3, - curso: 'Bacharelado e Ciências da Computação' - }] - } + cursos: [ + { + cr: 2, + cp: 0.5, + quads: 3, + curso: 'Bacharelado em Ciência e Tecnologia', + }, + { + cr: 2, + cp: 0.6, + quads: 3, + curso: 'Bacharelado e Ciências da Computação', + }, + ], + }, } let file = app.helpers.test.getDisciplinas() file.data = app.helpers.test.sample(file.data, 1) - let stub = sinon.stub(Axios, 'get').returns(file) + axiosInstanceStub = sinon.stub(Axios, 'create').returns(Axios) + axiosGetStub = sinon.stub(Axios, 'get').returns(file) await sync() - stub.restore() }) - describe.skip('func', function () { - it('returns a complete list of disciplinas', async function () { + afterEach(function () { + axiosInstanceStub.restore() + axiosGetStub.restore() + }) + + describe('func', function () { + xit('returns a complete list of disciplinas', async function () { let resp = await func(context) - + assert.equal(resp.aluno_id, context.body.aluno_id) assert.equal(resp.season, season) }) it('returns if kicks are already in place', async function () { const disciplina = await app.models.disciplinas.findOne({}) - disciplina.before_kick = [1,2,3] + disciplina.before_kick = [1, 2, 3] await disciplina.save() let resp = await func(context) @@ -62,4 +75,4 @@ describe('POST /v1/students', function() { await assertFuncThrows('MissingParameter', func, context) }) }) -}) \ No newline at end of file +}) diff --git a/app/api/users/info/spec.js b/app/api/users/info/spec.js new file mode 100644 index 0000000..1e52693 --- /dev/null +++ b/app/api/users/info/spec.js @@ -0,0 +1,48 @@ +const func = require('./func') +const assert = require('assert') + +describe('GET /users/info', function() { + describe('func', function() { + describe('with valid params', function() { + it('should return user public info', async function() { + const user = { + '_id': 'some id', + 'oauth': 'google', + 'confirmed': true, + 'email' : 'someemail@ufabc.next.com', + 'ra': 'some RA', + 'createdAt': new Date(), + 'devices': 'some device', + 'private' : 'should not return' + } + + const context = { + user + } + + const response = await func(context) + + assert.equal(user._id, response._id) + assert.equal(user.oauth, response.oauth) + assert.equal(user.confirmed, response.confirmed) + assert.equal(user.email, response.email) + assert.equal(user.ra, response.ra) + assert.equal(user.createdAt, response.createdAt) + assert.equal(user.devices, response.devices) + assert.notEqual(user.private, response.private) + + + }) + }) + describe('with invalid params', function() { + it('should throw an error when user is null', async function() { + const context = { + + } + + await assertFuncThrows('NotFound', func, context) + + }) + }) + }) +}) diff --git a/app/helpers/calculate/coefficients.js b/app/helpers/calculate/coefficients.js index 0d85fd0..49ba521 100644 --- a/app/helpers/calculate/coefficients.js +++ b/app/helpers/calculate/coefficients.js @@ -36,7 +36,7 @@ module.exports = function calculateAlunoCoefficientsData(disciplinas, graduation for(let disciplina in hash_disciplinas[year][period]) { var current_disciplina = hash_disciplinas[year][period][disciplina] var creditos = parseInt(current_disciplina.creditos) - var convertable = convertLetterToNumber(current_disciplina.conceito) * creditos + var convertable = convertGradeToNumber(current_disciplina.conceito) * creditos const category = parseCategory(current_disciplina.categoria) @@ -113,27 +113,38 @@ module.exports = function calculateAlunoCoefficientsData(disciplinas, graduation return hash_disciplinas } -function isAprovado (letter) { - if(letter !== 'F' && letter !== '0' && letter !== 'O' && letter !== 'I') return true +function isAprovado (grade) { + const disapprovingGrades = ['F', '0', 'O', 'I'] + + return !disapprovingGrades.includes(grade) } -function convertLetterToNumber(letter) { - if(letter === 'A') return 4 - else if(letter === 'B') return 3 - else if(letter === 'C') return 2 - else if(letter === 'D') return 1 - else if(letter === 'F') return 0 - else if(letter === 'O') return 0 - - else if(letter === '-') return -1 - else if(letter === 'E') return -1 - else if(letter === 'I') return -1 +function convertGradeToNumber(grade) { + const gradeToNumberMap = { + 'A': 4, + 'B': 3, + 'C': 2, + 'D': 1, + 'F': 0, + 'O': 0, + '-': -1, + 'E': -1, + 'I': -1 + } + + return gradeToNumberMap[grade] } function parseCategory(category) { - if(category === 'Livre Escolha') return 'free' - else if(category === 'Obrigatória') return 'mandatory' - else if(category === 'Opção Limitada') return 'limited' + const categoryParser = { + 'Livre Escolha': 'free', + 'Obrigatória': 'mandatory', + 'Opção Limitada': 'limited' + } + + return categoryParser[category] +} - return null -} \ No newline at end of file +module.exports.isAprovado = isAprovado +module.exports.convertGradeToNumber = convertGradeToNumber +module.exports.parseCategory = parseCategory diff --git a/app/helpers/calculate/coefficients.spec.js b/app/helpers/calculate/coefficients.spec.js new file mode 100644 index 0000000..bc78790 --- /dev/null +++ b/app/helpers/calculate/coefficients.spec.js @@ -0,0 +1,117 @@ +const assert = require('assert') + +const coefficients = require('./coefficients') + +describe('helpers.calculate.coefficients', function () { + describe('isAprovado', function () { + it('should return true when student is approved', function () { + assert.equal(true, coefficients.isAprovado('A')) + assert.equal(true, coefficients.isAprovado('B')) + assert.equal(true, coefficients.isAprovado('C')) + assert.equal(true, coefficients.isAprovado('D')) + }) + + it('should return false when student is not approved', function () { + assert.equal(false, coefficients.isAprovado('F')) + assert.equal(false, coefficients.isAprovado('O')) + assert.equal(false, coefficients.isAprovado('I')) + }) + }) + + describe('parseCategory', function () { + it('should return correct category', function () { + assert.equal('free', coefficients.parseCategory('Livre Escolha')) + assert.equal('mandatory', coefficients.parseCategory('Obrigatória')) + assert.equal('limited', coefficients.parseCategory('Opção Limitada')) + assert.equal(null, coefficients.parseCategory('invalid category')) + }) + }) + + describe('convertGradeToNumber', function () { + it('should return correct number', function () { + assert.equal(4, coefficients.convertGradeToNumber('A')) + assert.equal(1, coefficients.convertGradeToNumber('D')) + assert.equal(0, coefficients.convertGradeToNumber('F')) + assert.equal(0, coefficients.convertGradeToNumber('O')) + assert.equal(-1, coefficients.convertGradeToNumber('E')) + assert.equal(-1, coefficients.convertGradeToNumber('I')) + }) + }) + + describe('calculateAlunoCoefficientsData', function () { + it('should return correct cp accumulated', function () { + const result = coefficients(mockedDisciplines, mockedGraduation) + assert.equal(0.089, result['2020'][3].cp_acumulado) + assert.equal(0.211, result['2021'][1].cp_acumulado) + }) + }) +}) + +const mockedDisciplines = [{ + ano: 2020, + periodo: 3, + creditos: 3, + categoria: 'Obrigatória' +}, +{ + ano: 2020, + periodo: 3, + creditos: 5, + categoria: 'Livre Escolha' +}, +{ + ano: 2020, + periodo: 3, + creditos: 5, + categoria: 'Obrigatória' +}, +{ + ano: 2020, + periodo: 3, + creditos: 4, + categoria: 'Opção Limitada' +}, +{ + ano: 2021, + periodo: 1, + creditos: 5, + categoria: 'Obrigatória' +}, +{ + ano: 2021, + periodo: 1, + creditos: 3, + categoria: 'Opção Limitada' +}, +{ + ano: 2021, + periodo: 1, + creditos: 5, + categoria: 'Obrigatória' +}, +{ + ano: 2021, + periodo: 1, + creditos: 2, + categoria: 'Opção Limitada' +}, +{ + ano: 2021, + periodo: 1, + creditos: 3, + categoria: 'Livre Escolha' +}, +{ + ano: 2021, + periodo: 1, + creditos: 5, + categoria: 'Opção Limitada' +}, +] + +const mockedGraduation = { + credits_total: 190, + limited_credits_number: 70, + free_credits_number: 30, + mandatory_credits_number: 90 +} \ No newline at end of file diff --git a/app/helpers/season/findIdeais.js b/app/helpers/season/findIdeais.js index d2857c9..b490627 100644 --- a/app/helpers/season/findIdeais.js +++ b/app/helpers/season/findIdeais.js @@ -4,7 +4,8 @@ function findQuadFromDate(month) { if([6, 7, 8, 9].includes(month)) return 2 } -module.exports = function findIdeais(date) { +module.exports = function findIdeais(date = new Date()) { + const month = date.getMonth() return { 1 : [ @@ -46,5 +47,5 @@ module.exports = function findIdeais(date) { 'BHQ0301-15', // TERRITORIO E SOCIEDADE // ESTUDO ÉTNICOS RACIAIS ], - }[findQuadFromDate(date || new Date().getMonth())] + }[findQuadFromDate(month)] } \ No newline at end of file diff --git a/app/helpers/season/findIdeais.spec.js b/app/helpers/season/findIdeais.spec.js new file mode 100644 index 0000000..d3538ea --- /dev/null +++ b/app/helpers/season/findIdeais.spec.js @@ -0,0 +1,64 @@ +const assert = require('assert') + +describe('helpers.season.findIdeais', function() { + + it('Should return first quad courses', function () { + const expectedCourses = [ + 'BCM0506-15', // COMUNICACAO E REDES + 'BCJ0203-15', // ELETROMAG + 'BIN0406-15', // IPE + 'BCN0405-15', // IEDO + 'BIR0004-15', // EPISTEMOLOGICAS + 'BHO0102-15', // DESENVOL. E SUSTE. + 'BHO0002-15', // PENSA. ECONOMICO + 'BHP0201-15', // TEMAS E PROBLEMAS + 'BHO0101-15', // ESTADO E RELA + 'BIR0603-15', // CTS + 'BHQ0003-15', // INTEPRE. BRASIL + 'BHQ0001-15', // IDENT.E CULTURA + ] + + expectedCourses.forEach(function (course, index) { + assert.equal(course, expectedCourses[index]) + }) + }) + + it('Should return second quad courses', function () { + const expectedCourses = [ + 'BCM0504-15', // NI + 'BCN0404-15', // GA + 'BCN0402-15', // FUV + 'BCJ0204-15', // FEMEC + 'BCL0306-15', // BIODIVERSIDADE + 'BCK0103-15', // QUANTICA + 'BCL0308-15', // BIOQUIMICA + 'BIQ0602-15', // EDS + 'BHO1335-15', // FORMACAO SISTEMA INTERNACIONAL + 'BHO1101-15', // INTRODUCAO A ECONOMIA + 'BHO0001-15', // INTRODUCAO AS HUMANIDADES + 'BHP0202-15', // PENSAMENTO CRITICO + ] + + expectedCourses.forEach(function (course, index) { + assert.equal(course, expectedCourses[index]) + }) + }) + + it('Should return third quad courses', function () { + const expectedCourses = [ + 'BCJ0205-15', // FETERM + 'BCM0505-15', // PI + 'BCN0407-15', // FVV + 'BCL0307-15', // TQ + 'BCK0104-15', // IAM + 'BIR0603-15', // CTS + 'BHP0001-15', // ETICA E JUSTICA + 'BHQ0301-15', // TERRITORIO E SOCIEDADE + // ESTUDO ÉTNICOS RACIAIS + ] + + expectedCourses.forEach(function (course, index) { + assert.equal(course, expectedCourses[index]) + }) + }) +}) \ No newline at end of file diff --git a/app/models/comments.js b/app/models/comments.js index 434f411..2d0bac4 100644 --- a/app/models/comments.js +++ b/app/models/comments.js @@ -1,7 +1,7 @@ -const errors = require("@/errors"); -const Schema = require("mongoose").Schema; +const errors = require('@/errors') +const Schema = require('mongoose').Schema -const app = require("@/app"); +const app = require('@/app') var Model = (module.exports = Schema( { @@ -18,13 +18,13 @@ var Model = (module.exports = Schema( enrollment: { type: Schema.Types.ObjectId, required: true, - ref: "enrollments", + ref: 'enrollments', }, type: { type: String, required: true, - enum: ["teoria", "pratica"], + enum: ['teoria', 'pratica'], }, ra: { @@ -39,22 +39,22 @@ var Model = (module.exports = Schema( teacher: { type: Schema.Types.ObjectId, - ref: "teachers", + ref: 'teachers', required: true, }, subject: { type: Schema.Types.ObjectId, - ref: "subjects", + ref: 'subjects', required: true, }, reactionsCount: Object, }, { toObject: { virtuals: true } } -)); +)) -Model.pre("save", async function () { +Model.pre('save', async function () { // Validate if this user has already comment is this enrollment if (this.isNew) { let enrollment = await this.constructor @@ -63,41 +63,41 @@ Model.pre("save", async function () { active: true, type: this.type, }) - .lean(true); + .lean(true) if (enrollment) throw new errors.BadRequest( `Você só pode comentar uma vez neste vínculo: ${this.enrollment}` - ); + ) } -}); +}) -Model.post("save", async function () { - const Enrollment = app.models.enrollments; +Model.post('save', async function () { + const Enrollment = app.models.enrollments await Enrollment.findOneAndUpdate( { _id: this.enrollment }, { $addToSet: { comments: [this.type] } } - ); -}); + ) +}) -Model.post("find", async function () { - await this.model.updateMany(this.getQuery(), { $inc: { viewers: 1 } }); -}); +Model.post('find', async function () { + await this.model.updateMany(this.getQuery(), { $inc: { viewers: 1 } }) +}) Model.static( - "commentsByReactions", + 'commentsByReactions', async function ( query, userId, - populateFields = ["enrollment", "subject"], + populateFields = ['enrollment', 'subject'], limit = 10, page = 0 ) { - const Reactions = app.models.reactions; + const Reactions = app.models.reactions if (!userId) - throw new errors.BadRequest(`Usuário não encontrado: ${userId}`); + throw new errors.BadRequest(`Usuário não encontrado: ${userId}`) let response = await this.find(query) .lean(true) @@ -105,10 +105,10 @@ Model.static( .skip(Number(page * limit)) .limit(Number(limit)) .sort({ - "reactionsCount.recommendation": -1, - "reactionsCount.likes": -1, + 'reactionsCount.recommendation': -1, + 'reactionsCount.likes': -1, createdAt: -1, - }); + }) await Promise.all( response.map(async (r) => { @@ -116,32 +116,32 @@ Model.static( like: !!(await Reactions.count({ comment: String(r._id), user: String(userId), - kind: "like", + kind: 'like', })), recommendation: !!(await Reactions.count({ comment: String(r._id), user: String(userId), - kind: "recommendation", + kind: 'recommendation', })), star: !!(await Reactions.count({ comment: String(r._id), user: String(userId), - kind: "star", + kind: 'star', })), - }; - return r; + } + return r }) - ); + ) - return { data: response, total: await this.count(query) }; + return { data: response, total: await this.count(query) } } -); +) -Model.index({ comment: 1, user: 1 }); -Model.index({ reactionsCount: -1 }); +Model.index({ comment: 1, user: 1 }) +Model.index({ reactionsCount: -1 }) Model.index({ - "reactionsCount.recommendation": -1, - "reactionsCount.likes": -1, + 'reactionsCount.recommendation': -1, + 'reactionsCount.likes': -1, createdAt: -1, -}); +}) diff --git a/app/populate/data/comments.js b/app/populate/data/comments.js index 18bde69..aae83ce 100644 --- a/app/populate/data/comments.js +++ b/app/populate/data/comments.js @@ -1,5 +1,5 @@ module.exports = function (app, ids) { - return [ + return [ { comment: 'Muito bom', teacher: ids.teachers[0]._id, @@ -17,7 +17,7 @@ module.exports = function (app, ids) { teacher: ids.subjects[101]._id, ra: 11201822479, }, - + { comment: 'Só alegria', teacher: ids.teachers[0]._id, @@ -25,7 +25,6 @@ module.exports = function (app, ids) { enrollment: '000000000000000000000007', subject: ids.subjects[0]._id, ra: 11201822481, - } - + }, ] -} \ No newline at end of file +} diff --git a/app/server.js b/app/server.js index 04643e8..185be07 100644 --- a/app/server.js +++ b/app/server.js @@ -1,102 +1,102 @@ // Install tracer (must be the first thing in order to properly work) -require("dotenv").config(); +require('dotenv').config() if (process.env.GCLOUD_PROJECT && process.env.GCLOUD_CREDENTIALS) { - console.log("trace-agent enabled"); - require("@google-cloud/trace-agent").start({ + console.log('trace-agent enabled') + require('@google-cloud/trace-agent').start({ projectId: process.env.GCLOUD_PROJECT, credentials: JSON.parse(process.env.GCLOUD_CREDENTIALS), // Enable Mongo Reporting enhancedDatabaseReporting: true, // Don't trace status requests - ignoreUrls: ["/v1/status"], - }); + ignoreUrls: ['/v1/status'], + }) } -const chalk = require("chalk"); +const chalk = require('chalk') -const app = require("./app"); +const app = require('./app') -const TAG = "[server]"; +const TAG = '[server]' // Order of execution for setup steps const pipeline = [ // package.json information - "package", + 'package', // Global configurations - "config", + 'config', // Load app.helpers - "helpers", + 'helpers', // Load mongo - "mongo", + 'mongo', // Load models - "models", + 'models', // Load Redis, - "redis", + 'redis', // Load Agenda - "agenda", + 'agenda', // Create base express server - "server", + 'server', // Add redirection behavior - "redirect", + 'redirect', // Create web app - "static", + 'static', // Generater Router for Restify - "router", + 'router', // Create api (/v1) routes and middlewares - "api", + 'api', // Create oauth helpers - "oauth", + 'oauth', // Bind to port and lift http app - "lift", -]; + 'lift', +] async function serve() { - console.info(TAG, chalk.dim("lifting...")); + console.info(TAG, chalk.dim('lifting...')) - await app.bootstrap(pipeline); + await app.bootstrap(pipeline) - console.info(TAG, " version:", chalk.white(app.package.version)); - console.info(TAG, " port:", chalk.white(app.config.PORT)); - console.info(TAG, " env:", chalk.white(app.config.ENV)); + console.info(TAG, ' version:', chalk.white(app.package.version)) + console.info(TAG, ' port:', chalk.white(app.config.PORT)) + console.info(TAG, ' env:', chalk.white(app.config.ENV)) try { - let mongoUrl = new (require("url").URL)(app.config.MONGO_URL); - console.info(TAG, " mongo host:", chalk.white(mongoUrl.hostname)); - console.info(TAG, " mongo db:", chalk.white(mongoUrl.pathname)); + let mongoUrl = new (require('url').URL)(app.config.MONGO_URL) + console.info(TAG, ' mongo host:', chalk.white(mongoUrl.hostname)) + console.info(TAG, ' mongo db:', chalk.white(mongoUrl.pathname)) } catch (e) { - console.error(e); + console.error(e) } - if (process.env.SHUTDOWN_ON_LIFT) process.exit(0); + if (process.env.SHUTDOWN_ON_LIFT) process.exit(0) } // Listen for Application wide errors -process.on("SIGTERM", shutdown); -process.on("SIGINT", shutdown); -process.on("unhandledRejection", handleError); -process.on("uncaughtException", handleError); +process.on('SIGTERM', shutdown) +process.on('SIGINT', shutdown) +process.on('unhandledRejection', handleError) +process.on('uncaughtException', handleError) function shutdown() { app.agenda.stop(function () { - process.exit(0); - }); + process.exit(0) + }) } function handleError(e) { - console.error("Fatal Error"); - console.error(e.stack); + console.error('Fatal Error') + console.error(e.stack) if (app.reporter) { - console.error("Reporting..."); + console.error('Reporting...') app.reporter.report(e, () => { - console.error("Reported. Exiting."); - process.exit(1); - }); - return; + console.error('Reported. Exiting.') + process.exit(1) + }) + return } - console.error("Exiting."); - process.exit(1); + console.error('Exiting.') + process.exit(1) } -serve(); +serve() diff --git a/app/setup/config.js b/app/setup/config.js index 274622c..9599e19 100644 --- a/app/setup/config.js +++ b/app/setup/config.js @@ -1,7 +1,7 @@ -require("dotenv").config(); +require('dotenv').config() -const path = require("path"); -const HOUR = 1000 * 60 * 60; +const path = require('path') +const HOUR = 1000 * 60 * 60 // Load config variables and expose. // Load occurs from: @@ -9,108 +9,108 @@ const HOUR = 1000 * 60 * 60; // > process.env module.exports = async () => { - let config = {}; + let config = {} - config.ENV = getEnv("NODE_ENV", "dev"); - config.PORT = getEnv("PORT") || getEnv("NODE_PORT", 8011); + config.ENV = getEnv('NODE_ENV', 'dev') + config.PORT = getEnv('PORT') || getEnv('NODE_PORT', 8011) // config.HOST = getEnv('HOST', `${ip.address()}:${config.PORT}`) - config.HOST = getEnv("HOST", `localhost:${config.PORT}`); - config.PROTOCOL = getEnv("PROTOCOL", "http"); - config.JWT_SECRET = getEnv("JWT_SECRET", "DEV_JWT_KEY"); + config.HOST = getEnv('HOST', `localhost:${config.PORT}`) + config.PROTOCOL = getEnv('PROTOCOL', 'http') + config.JWT_SECRET = getEnv('JWT_SECRET', 'DEV_JWT_KEY') config.MONGO_URL = getEnv( - "MONGO_URL", + 'MONGO_URL', `mongodb://localhost:27017/ufabc-matricula-extension-${config.ENV}` - ); - config.MONGO_URI = config.MONGO_URL; + ) + config.MONGO_URI = config.MONGO_URL - config.WEB_URL = getEnv("WEB_URL", "http://localhost:7500/app/#"); + config.WEB_URL = getEnv('WEB_URL', 'http://localhost:7500/app/#') // Config Redis - config.REDIS_URL = getEnv("REDIS_URL", "redis://localhost:6379"); - config.REDIS_PASSWORD = getEnv("REDIS_PASSWORD"); - config.CACHE_NAME = getEnv("CACHE_NAME", "ufabc-matricula-extension"); + config.REDIS_URL = getEnv('REDIS_URL', 'redis://localhost:6379') + config.REDIS_PASSWORD = getEnv('REDIS_PASSWORD') + config.CACHE_NAME = getEnv('CACHE_NAME', 'ufabc-matricula-extension') - config.SENTRY = getEnv("SENTRY", "SENTRY_KEY"); + config.SENTRY = getEnv('SENTRY', 'SENTRY_KEY') - config.ACCESS_KEY = getEnv("ACCESS_KEY", "SOME_ACCESS_KEY"); + config.ACCESS_KEY = getEnv('ACCESS_KEY', 'SOME_ACCESS_KEY') - config.GOOGLE_FCM_KEY = getEnv("GOOGLE_FCM_KEY", "GOOGLE_FCM_KEY"); + config.GOOGLE_FCM_KEY = getEnv('GOOGLE_FCM_KEY', 'GOOGLE_FCM_KEY') config.MATRICULAS_URL = getEnv( - "MATRICULAS_URL", - "http://localhost:8011/snapshot/assets/matriculas.js" - ); + 'MATRICULAS_URL', + 'http://localhost:8011/snapshot/assets/matriculas.js' + ) config.DISCIPLINAS_URL = getEnv( - "DISCIPLINAS_URL", - "http://localhost:8011/snapshot/assets/todasDisciplinas.js" - ); + 'DISCIPLINAS_URL', + 'http://localhost:8011/snapshot/assets/todasDisciplinas.js' + ) // Static assets (dist) configs - config.distFolder = getEnv("DIST_FOLDER", path.join(__dirname, "../../dist")); - config.docFolder = getEnv("DOC_FOLDER", path.join(__dirname, "../doc")); - config.maxAge = 1 * HOUR; + config.distFolder = getEnv('DIST_FOLDER', path.join(__dirname, '../../dist')) + config.docFolder = getEnv('DOC_FOLDER', path.join(__dirname, '../doc')) + config.maxAge = 1 * HOUR // Matricula snapshot config.snapshotFolder = getEnv( - "DIST_FOLDER", - path.join(__dirname, "../snapshot") - ); + 'DIST_FOLDER', + path.join(__dirname, '../snapshot') + ) // state - config.isProduction = config.ENV == "production"; - config.isTest = config.ENV == "test"; - config.isDev = !config.isProduction && !config.isTest; + config.isProduction = config.ENV == 'production' + config.isTest = config.ENV == 'test' + config.isDev = !config.isProduction && !config.isTest config.mailer = { - API_KEY: getEnv("SENDGRID_KEY", "SENDGRID_KEY"), - ENDPOINT: "https://api.sendgrid.com/v3/mail/send", - EMAIL: getEnv("SENDGRID_EMAIL", "contato@ufabcnext.com"), + API_KEY: getEnv('SENDGRID_KEY', 'SENDGRID_KEY'), + ENDPOINT: 'https://api.sendgrid.com/v3/mail/send', + EMAIL: getEnv('SENDGRID_EMAIL', 'contato@ufabcnext.com'), // configuration for the mail templates TEMPLATES: { - CONFIRMATION: getEnv("EMAIL_TEMPLATE_CONFIMATION", null), - RECOVERY: getEnv("EMAIL_TEMPLATE_RECOVERY", null), + CONFIRMATION: getEnv('EMAIL_TEMPLATE_CONFIMATION', null), + RECOVERY: getEnv('EMAIL_TEMPLATE_RECOVERY', null), }, - }; - - config.RECOVERY_URL = getEnv("RECOVERY_URL", "http://localhost:8011/connect"); - - (config.GRANT_SECRET = getEnv("GRANT_SECRET", "SOME_RANDOM_SECRET")), - (config.GRANT_CONFIG = { - defaults: { - protocol: config.PROTOCOL, - host: config.HOST, - }, - facebook: { - key: getEnv("OAUTH_FACEBOOK_KEY", "OAUTH_FACEBOOK_KEY"), - secret: getEnv("OAUTH_FACEBOOK_SECRET", "OAUTH_FACEBOOK_SECRET"), - callback: "/oauth/facebook", - scope: ["public_profile", "email"], - }, - google: { - key: getEnv("OAUTH_GOOGLE_KEY", "OAUTH_GOOGLE_KEY"), - secret: getEnv("OAUTH_GOOGLE_SECRET", "OAUTH_GOOGLE_SECRET"), - callback: "/oauth/google", - scope: ["profile", "email"], - }, - }); + } + + config.RECOVERY_URL = getEnv('RECOVERY_URL', 'http://localhost:8011/connect'); + + (config.GRANT_SECRET = getEnv('GRANT_SECRET', 'SOME_RANDOM_SECRET')), + (config.GRANT_CONFIG = { + defaults: { + protocol: config.PROTOCOL, + host: config.HOST, + }, + facebook: { + key: getEnv('OAUTH_FACEBOOK_KEY', 'OAUTH_FACEBOOK_KEY'), + secret: getEnv('OAUTH_FACEBOOK_SECRET', 'OAUTH_FACEBOOK_SECRET'), + callback: '/oauth/facebook', + scope: ['public_profile', 'email'], + }, + google: { + key: getEnv('OAUTH_GOOGLE_KEY', 'OAUTH_GOOGLE_KEY'), + secret: getEnv('OAUTH_GOOGLE_SECRET', 'OAUTH_GOOGLE_SECRET'), + callback: '/oauth/google', + scope: ['profile', 'email'], + }, + }) // Configuration for Mongo Tenant plugin config.mongoTenant = { // Whether the mongo tenant plugin MAGIC is enabled. enabled: true, // The name of the tenant id field. - tenantIdKey: "season", + tenantIdKey: 'season', // The type of the tenant id field. tenantIdType: String, // The name of the tenant id getter method. - tenantIdGetter: "getSeason", + tenantIdGetter: 'getSeason', // The name of the tenant bound model getter method. - accessorMethod: "bySeason", - }; + accessorMethod: 'bySeason', + } - return config; -}; + return config +} function getEnv(env, defaults) { - return process.env[env] || defaults; + return process.env[env] || defaults } diff --git a/app/setup/mongo.js b/app/setup/mongo.js index 7d7b6bf..2a6540c 100644 --- a/app/setup/mongo.js +++ b/app/setup/mongo.js @@ -1,39 +1,39 @@ -const mongoose = require("mongoose"); -const cachegoose = require("cachegoose"); -const url = require("url"); +const mongoose = require('mongoose') +const cachegoose = require('cachegoose') +const url = require('url') /* * Connects to MongoDB */ module.exports = async (app) => { // Set custom promisse library to use - mongoose.Promise = global.Promise; + mongoose.Promise = global.Promise let driverOptions = { useNewUrlParser: true, - }; + } // open connection let conn = await mongoose.createConnection( app.config.MONGO_URL, driverOptions - ); + ) - const parsedRedis = url.parse(app.config.REDIS_URL, false, true); + const parsedRedis = url.parse(app.config.REDIS_URL, false, true) const cacheOptions = { - engine: "redis", + engine: 'redis', port: parsedRedis.port, host: parsedRedis.hostname, auth_pass: app.config.REDIS_PASSWORD, - }; + } if (app.config.REDIS_PASSWORD) { - cacheOptions.auth_pass = app.config.REDIS_PASSWORD; + cacheOptions.auth_pass = app.config.REDIS_PASSWORD } // creates a cache layer - cachegoose(mongoose, cacheOptions); + cachegoose(mongoose, cacheOptions) - return conn; -}; + return conn +} diff --git a/app/setup/oauth.js b/app/setup/oauth.js index c704bc1..c977f9f 100644 --- a/app/setup/oauth.js +++ b/app/setup/oauth.js @@ -59,7 +59,7 @@ async function facebook (context) { await user.save() - const WEB_URL = env == "development" ? "http://localhost:7500" : App.config.WEB_URL + const WEB_URL = env == 'development' ? 'http://localhost:7500' : App.config.WEB_URL return { _redirect: inApp.split('?')[0] == 'true' @@ -114,7 +114,7 @@ async function google(context) { await user.save() - const WEB_URL = env == "development" ? "http://localhost:7500" : App.config.WEB_URL + const WEB_URL = env == 'development' ? 'http://localhost:7500' : App.config.WEB_URL return { _redirect: inApp.split('?')[0] == 'true' diff --git a/app/setup/redis.js b/app/setup/redis.js index bfec921..1f12680 100644 --- a/app/setup/redis.js +++ b/app/setup/redis.js @@ -1,69 +1,69 @@ -const redis = require("redis"); -const bluebird = require("bluebird"); -const Cacheman = require("cacheman"); -const requireSmart = require("require-smart"); -const Raven = require("raven"); -const url = require("url"); +const redis = require('redis') +const bluebird = require('bluebird') +const Cacheman = require('cacheman') +const requireSmart = require('require-smart') +const Raven = require('raven') +const url = require('url') module.exports = async (app) => { - const parsedRedis = url.parse(app.config.REDIS_URL, false, true); + const parsedRedis = url.parse(app.config.REDIS_URL, false, true) const OPTIONS = { port: parsedRedis.port, host: parsedRedis.hostname, no_ready_check: true, - }; + } if (app.config.REDIS_PASSWORD) { - OPTIONS.auth_pass = app.config.REDIS_PASSWORD; + OPTIONS.auth_pass = app.config.REDIS_PASSWORD } // load handlers - const HANDLERS = requireSmart("../redis/handlers", { + const HANDLERS = requireSmart('../redis/handlers', { skip: [/\..*\.js$/], - }); + }) - bluebird.promisifyAll(redis.RedisClient.prototype); - bluebird.promisifyAll(redis.Multi.prototype); + bluebird.promisifyAll(redis.RedisClient.prototype) + bluebird.promisifyAll(redis.Multi.prototype) // create publisher connection - const pub = redis.createClient(OPTIONS); + const pub = redis.createClient(OPTIONS) // create subscribe connection - const sub = redis.createClient(OPTIONS); + const sub = redis.createClient(OPTIONS) // create handler ? - sub.on("message", async function (channel, message) { + sub.on('message', async function (channel, message) { try { // find handler for this channel and pass message to him - await HANDLERS[channel](JSON.parse(message)); + await HANDLERS[channel](JSON.parse(message)) } catch (e) { - console.log(e); - Raven.captureException(e); + console.log(e) + Raven.captureException(e) } - }); + }) // subscrive for every handler declared Object.keys(HANDLERS).forEach((handler) => { - sub.subscribe(handler); - }); + sub.subscribe(handler) + }) const cacheOptions = { - engine: "redis", + engine: 'redis', port: parsedRedis.port, host: parsedRedis.hostname, - }; + } if (app.config.REDIS_PASSWORD) { - cacheOptions.password = app.config.REDIS_PASSWORD; + cacheOptions.password = app.config.REDIS_PASSWORD } // return pub sub return { publish(handler, payload) { - pub.publish(handler, JSON.stringify(payload)); + pub.publish(handler, JSON.stringify(payload)) }, sub: sub, cache: new Cacheman(app.config.CACHE_NAME, cacheOptions), - }; -}; + } +} diff --git a/app/setup/static.js b/app/setup/static.js index 094221c..16ddf3d 100644 --- a/app/setup/static.js +++ b/app/setup/static.js @@ -26,18 +26,18 @@ module.exports = async (app) => { app.server.use(static) app.server.get('/snapshot', function(req, res) { const { aluno_id } = req.query - fs.readFile(app.config.snapshotFolder + '/index.html', "utf8", function(err, data) { + fs.readFile(app.config.snapshotFolder + '/index.html', 'utf8', function(err, data) { if (err) { - console.log(err); + console.log(err) res.send(500) return } let result = data result = result.replace('ALUNO_ID_HERE', aluno_id) - res.writeHead(200, {'Content-Type': 'text/html; charset=UTF-8'}); - res.write(result); - res.end(); - }); + res.writeHead(200, {'Content-Type': 'text/html; charset=UTF-8'}) + res.write(result) + res.end() + }) }) app.server.use('/snapshot', express.static(app.config.snapshotFolder, { maxAge: app.config.maxAge, diff --git a/app/test.js b/app/test.js index 7694918..59eca23 100644 --- a/app/test.js +++ b/app/test.js @@ -1,99 +1,99 @@ -const glob = require("glob"); -const chalk = require("chalk"); -const Mocha = require("mocha"); +const glob = require('glob') +const chalk = require('chalk') +const Mocha = require('mocha') -const app = require("./app"); +const app = require('./app') // this function receives another function and test it to see if it's throws the expected error global.assertFuncThrows = async (expectedError, fun, ...context) => { try { - await fun(...context); - throw new Error(`Should have thrown ${expectedError}, but succeded`); + await fun(...context) + throw new Error(`Should have thrown ${expectedError}, but succeded`) } catch (e) { if (e.name != expectedError) { - throw e; + throw e } } -}; +} async function test() { - console.info(); - console.info("Bootstrapping basic components..."); - console.info(); + console.info() + console.info('Bootstrapping basic components...') + console.info() await app.bootstrap([ - "package", - "config", - "helpers", - "mongo", - "models", - "redis", - "agenda", - "redirect", - ]); + 'package', + 'config', + 'helpers', + 'mongo', + 'models', + 'redis', + 'agenda', + 'redirect', + ]) // Security Checks before starting tests if ( app.config.MONGO_URL && - !app.config.MONGO_URL.includes("localhost") && + !app.config.MONGO_URL.includes('localhost') && app.config.MONGO_URL.length > 45 ) { throw new Error( chalk.red( - "You cannot test on a non local MongoDB.\n" + - "It would have cleaned ALL THE DATA and fucked up everything.\n" + - "BE FUCKING CAREFULL WITH PRODUCTION!!!!\n" + - "Change MONGO_URI to default localhost before testing" + 'You cannot test on a non local MongoDB.\n' + + 'It would have cleaned ALL THE DATA and fucked up everything.\n' + + 'BE FUCKING CAREFULL WITH PRODUCTION!!!!\n' + + 'Change MONGO_URI to default localhost before testing' ) - ); + ) } - let mocha = new Mocha({ timeout: 12000 }); + let mocha = new Mocha({ timeout: 12000 }) // Find files for testing - let testFiles = glob.sync("**/*spec.js", { - ignore: ["node_modules/**"], - }); + let testFiles = glob.sync('**/*spec.js', { + ignore: ['node_modules/**'], + }) // Add files to mocha - testFiles.forEach(mocha.addFile.bind(mocha)); + testFiles.forEach(mocha.addFile.bind(mocha)) // Default passes to 0, in case there is only syncronous tests running - let stats = { passes: 0 }; + let stats = { passes: 0 } // Run tests and save stats stats = mocha.run(function (failures) { // Exits if less then 10 tests are being run (in case .only is used) if (!failures && stats.passes < 3) { - console.error(chalk.red("Did not performed full testing:")); - console.error(chalk.red(" - Remove `describe.only` on tests")); - console.log(); - return process.exit(1); + console.error(chalk.red('Did not performed full testing:')) + console.error(chalk.red(' - Remove `describe.only` on tests')) + console.log() + return process.exit(1) } - process.exit(!!failures); - }).stats; + process.exit(!!failures) + }).stats // creating some helpers - Object.defineProperty(Array.prototype, "elToString", { + Object.defineProperty(Array.prototype, 'elToString', { enumerable: false, value: function () { - return this.sort().map((s) => s.toString()); + return this.sort().map((s) => s.toString()) }, - }); + }) } -process.on("unhandledRejection", (e) => { - console.error("Unhandled Rejection"); - console.error(e.stack); - process.exit(1); +process.on('unhandledRejection', (e) => { + console.error('Unhandled Rejection') + console.error(e.stack) + process.exit(1) }); (async function () { try { - await test(); + await test() } catch (e) { - console.error(e); - process.exit(1); + console.error(e) + process.exit(1) } -})(); +})() diff --git a/docs/pull_request_template.md b/docs/pull_request_template.md new file mode 100644 index 0000000..d1003d3 --- /dev/null +++ b/docs/pull_request_template.md @@ -0,0 +1,20 @@ +## Feature | Change Request | Bug + +### Description +Describe what your PR is doing +If you did UI work, please consider including a screenshot + +### How do I test this? +Please describe the tests that you ran to verify your changes. +Provide instructions so we can reproduce. +Please also list any relevant details for your test configuration. + +1. Test A +2. Test B +3. Test C + +### Checklist + +- [ ] I have performed a self-review of my own code; +- [ ] I have added tests that prove my fix is effective or that my feature works; +- [ ] Add labels to distinguish the pull request. For example `bug`, `ready to review` etc.