From 2da8bf48937883f1ea0cf6f4c059828fba634fd4 Mon Sep 17 00:00:00 2001 From: bogui Date: Thu, 21 Mar 2019 09:47:18 -0300 Subject: [PATCH 1/3] Exam solved --- README.md | 66 +++++ app.js | 4 +- controllers/billing.js | 90 ++++++- controllers/courses.js | 26 +- controllers/index.js | 2 + controllers/stats.js | 63 +++++ middleware/cacheMiddleware.js | 36 +++ middleware/index.js | 3 +- package.json | 2 +- routes.js | 7 + test/dummy_tests.js | 442 ++++++++++++++++++++++++++++++++++ 11 files changed, 722 insertions(+), 19 deletions(-) create mode 100644 controllers/stats.js create mode 100644 middleware/cacheMiddleware.js diff --git a/README.md b/README.md index 4b4d635..38d2139 100644 --- a/README.md +++ b/README.md @@ -26,3 +26,69 @@ mongorestore ./dump To pass this exam all excercises must be resolved, every feature must be tested (with all tests passing) and there must be no linting errors. Summission must be done through a single Pull Request. + +## Responses + +## Models + + As a general observation, I considera that the model of the app seems + very well dessigned. + As a suggestion of what I may change, taking into account that one + of the exercises asks for the amount of students that failed the exam + by each state, and the posible way to get it is by looking for the + state as a string, I think it would be better to create other entity (schema) + that has the country, the state, postal code and the id. So that, + we could have filter by state in a neatter way. + On the other hand, I may add some relationships to all the squema + that I have done on mongoose. For example: to relate the student + to his/her exam. + + +## 1) Issue 1 - Implementation plan: + +1 - I solved this issue just adding the parameter "technologyId" + to the filter parameters Array that already implemented. + It was simple due to the structure of the implemetation + and the filtering query mode. + +## 2) Issue 2 - Implementation plan: + + 1- I decided to create a new endpoint that calls one which I developed previously, + by using a request to avoid to modify it. + From the response I can get the students that have to pay the course. + + 2- This data is used to make a call to the AFIP API in order + to get the corresponding id for each student. Then I used destructuring, + which allowed me to made it easier, to give format to the object that + will be send as the response required on this exercise. + + +## 3) Issue 3 - Implementation plan: + + 1 - To solve this issue, firstly I create an array with URLs that are excluded from the middleware. + This is a scalable solution like the Issue 1, due to exclude other URL just need to add it + to the array that I mention before. + + 2 - Secondly, I ask who request method are using and depend if is a GET, + PUT or POST I do different things. Always I ask if that request is + in the local cache, and depending if is a POST or a PUT y delete from + cache with a different form. + + 3 - Also I redefined the response200 , in this way I able to get the data + in the callback to save it into cache and added to the new response. + + +## 4) Issue 4 - Implementation plan: + + 1- Since there is an exams list and each exam has a list of marks, + I decided to create a new list with all those marks that represent a failed exam. + + 2- With this new list of failed marks, + I looked for each student that was related with each mark creating a new list. + Taking into account this can result on duplicated students, + due to the fact that they could failed more than one exam, + I decided to use a dictionary structure to map each student + with his/her city with a function that verify that the student wasnt + added before. + +## Tests \ No newline at end of file diff --git a/app.js b/app.js index 9cfb46a..533129a 100644 --- a/app.js +++ b/app.js @@ -3,7 +3,7 @@ const bodyParser = require("body-parser"); const morgan = require("morgan"); const mongoose = require("mongoose"); const config = require("./config"); -const {responseHelpers} = require("./middleware"); +const {responseHelpers, cacheMiddleware} = require("./middleware"); const routes = require("./routes"); require("./models"); @@ -22,7 +22,7 @@ app.use(morgan("dev")); app.use(responseHelpers); // Add cache middleware -// app.use(cacheMiddleware); +app.use(cacheMiddleware); // Setup mongoose and load models mongoose.Promise = global.Promise; diff --git a/controllers/billing.js b/controllers/billing.js index e4c98b8..e9f2b72 100644 --- a/controllers/billing.js +++ b/controllers/billing.js @@ -2,6 +2,7 @@ module.exports = (mongoose) => { const Course = mongoose.model("Course"); const Evaluation = mongoose.model("Evaluation"); const Student = mongoose.model("Student"); + const request = require("request"); // para cada curso dame la evaluacion // para cada evaluacion dame los aprobados @@ -67,7 +68,7 @@ module.exports = (mongoose) => { firstName: student.firstName, lastName: student.lastName, address: student.billingAddress, - price: studentsWithPrice[id] + price: studentsWithPrice[id] / 100 }); }); }); @@ -81,8 +82,93 @@ module.exports = (mongoose) => { res.response500(err, "Courses couldn't be found!"); }); } + function getStudentsBillingInfo() { + const options = { + url: "http://localhost:8000/api/admin/billing/getChargeableStudents", + headers: { + "user-agent": "node-exam" + } + }; + + return new Promise((resolve, reject) => { + request.get(options, (err, response, body) => { + if (err) { + reject(err); + } else if (response.statusCode === 200) { + try { + const studentsBillingInfo = JSON.parse(body).data.studentsBillingInfo; + resolve(studentsBillingInfo); + } catch (ex) { + // console.error(ex); + // Log the exception but returns an error without any sensitive data! + reject(new Error("Couldn't get node repos!")); + } + } else { + console.error(response); + // Log the conflictive response but returns an error without any sensitive data! + reject(new Error("Couldn't get billing student!")); + } + }); + }); + } + function formatObject(infoAfip, studentInfo) { + return ({ + "BillingNumber": infoAfip.id, + "FirstAndLastName": studentInfo.nomYAp, + "Address": studentInfo.dir, + "price": studentInfo.importe + }); + } + function getAfipInfo(studentInfo) { + const options = { + url: "http://localhost:8000/api/afip", + headers: { + "user-agent": "node-exam" + } + }; + return new Promise((resolve, reject) => { + request.post(options.url, {json: studentInfo}, (_, response, body) => { + if (response.statusCode === 404) { + resolve(getAfipInfo(studentInfo)); + } else if (response.statusCode === 200) { + try { + resolve(formatObject(body.data, studentInfo)); + } catch (ex) { + console.error(ex); + // Log the exception but returns an error without any sensitive data! + reject(new Error("Couldn't get node repos!")); + } + } else { + console.error(response); + // Log the conflictive response but returns an error without any sensitive data! + reject(new Error("Couldn't get billing student!")); + } + }); + }); + } + function formatStudentForAfip(student) { + const {firstName, lastName, address, price} = student; + return {"nomYAp": `${firstName} ${lastName}`, "dir": address.street1, "importe": price}; + } + function getAfipStudensInfo(students) { + const promiseArray = students.map((student) => { + const studentInfo = formatStudentForAfip(student); + return getAfipInfo(studentInfo); + }); + return Promise.all(promiseArray); + } + async function getInvoices(req, res) { + try { + const students = await getStudentsBillingInfo(req, res); + const afipFinalInfo = await getAfipStudensInfo(students); + res.response200(afipFinalInfo, "Success"); + } catch (error) { + res.response500(error, "Error"); + } + } return { - getChargeableStudents + getChargeableStudents, + getInvoices }; }; diff --git a/controllers/courses.js b/controllers/courses.js index 4de1399..833d8e5 100644 --- a/controllers/courses.js +++ b/controllers/courses.js @@ -1,9 +1,9 @@ +/* eslint-disable no-param-reassign */ module.exports = (mongoose) => { - var Course = mongoose.model("Course"), - filterFields = ["status"], - sortFields = ["status"]; + const Course = mongoose.model("Course"); + const filterFields = ["status", "technologyId"]; - var buildFilterQuery = function(params) { + function buildFilterQuery(params) { return filterFields.reduce((query, prop) => { if (params[prop]) { query[prop] = params[prop]; @@ -20,32 +20,32 @@ module.exports = (mongoose) => { function list(req, res) { Course.find(buildFilterQuery(req.query)).sort() - .then(function (courses) { - res.response200({courses}, "Found '" + courses.length + "' Courses."); + .then((courses) => { + res.response200({courses}, `Found '${courses.length}' Courses.`); }) - .catch(function (err) { + .catch((err) => { res.response500(err, "Courses couldn't be found!"); }); } function create(req, res) { - let courseDTO = getCourseDTO(req.body); + const courseDTO = getCourseDTO(req.body); Course.create(courseDTO) .then((course) => { res.response200(course, `Course '${course._id}' successfully created.`); }) .catch((err) => { - res.response500(err, "Course couldn't be created!"); + res.response500(err, "Course couldn't be created!"); }); } function read(req, res) { - Course.findById({ _id: req.params.id }) + Course.findById({_id: req.params.id}) .then((course) => { if (course) { - res.response200({course}, `Course '${course._id}' found.`); + res.response200({course}, `Course '${course._id}' found.`); } else { - res.response404("Course not found!"); + res.response404("Course not found!"); } }) .catch((err) => { @@ -59,7 +59,7 @@ module.exports = (mongoose) => { if (course) { res.response200({course}, `Course '${course._id}' successfully updated.`); } else { - res.response404('Course not found!'); + res.response404("Course not found!"); } }) .catch((err) => { diff --git a/controllers/index.js b/controllers/index.js index 85840f2..8e2423f 100644 --- a/controllers/index.js +++ b/controllers/index.js @@ -3,11 +3,13 @@ const CoursesController = require("./courses"); const StudentsController = require("./students"); const EvaluationsController = require("./evaluations"); const TechnologiesController = require("./technologies"); +const StatsController = require("./stats"); module.exports = { BillingController, EvaluationsController, CoursesController, StudentsController, + StatsController, TechnologiesController }; diff --git a/controllers/stats.js b/controllers/stats.js new file mode 100644 index 0000000..b84460c --- /dev/null +++ b/controllers/stats.js @@ -0,0 +1,63 @@ +module.exports = (mongoose) => { + const Evaluation = mongoose.model("Evaluation"); + const Student = mongoose.model("Student"); + const stateArray = {}; + const addedStudents = []; + function verifyExistentStudent(studentId, array) { + let exist = false; + array.forEach((student) => { + if (studentId.toString() === student._id) { + exist = true; + } + }); + + return exist; + } + + function incrementState(state) { + if (stateArray[state]) { + stateArray[state]++; + } else { + stateArray[state] = 1; + } + } + + function addStateByStudent(state, studentId) { + if (!verifyExistentStudent(studentId, addedStudents)) { + addedStudents.push(studentId); + incrementState(state); + } + } + + function addStudentsToList(array) { + array.forEach((student) => { + addStateByStudent(student.billingAddress.state, student._id); + }); + } + + function getFailuresByState(req, res) { + Evaluation.find() + .then((failedEvals) => { + const notes = failedEvals.reduce((notesFiltered, toConcat) => { + return notesFiltered.concat(toConcat.notes); + }, []).filter((note) => { + return note.status === "failed"; + }); + + return Promise.all(notes); + }) + .then((notes) => { + const studentsFiltered = notes.map((note) => { + return Student.findOne({_id: note.studentId}); + }); + + return Promise.all(studentsFiltered); + }) + .then((students) => { + addStudentsToList(students); + res.response200(stateArray); + }); + } + + return {getFailuresByState}; +}; diff --git a/middleware/cacheMiddleware.js b/middleware/cacheMiddleware.js new file mode 100644 index 0000000..5cebca7 --- /dev/null +++ b/middleware/cacheMiddleware.js @@ -0,0 +1,36 @@ +const memCache = {}; + +function isInCache({url}) { + return memCache[url]; +} + +function getNewUrl(oldUrl) { + const splitted = oldUrl.split("/").slice(1, -1); + return splitted.reduce((oldValue, newValur) => { + return (`${oldValue}/${newValur}`); + }, ""); +} + +module.exports = (req, res, next) => { + const forbiddenPaths = ["/admin/billing/getChargeableStudents"]; + const dontAuth = forbiddenPaths.find((path) => { + return path === `${req.method} ${req.url}`; + }); + const isCached = isInCache(req); + if (req.method === "GET" && !dontAuth) { + if (isCached) { + return res.response200((isCached), "Was cached!"); + } + res.response200 = (data = {}, message = "ok") => { + memCache[req.originalUrl] = data; + res.json({data, status: "success", message}); + }; + } else if (req.method === "POST" && isCached) { + Reflect.deleteProperty(memCache, req.url); + return next(); + } else if (req.method === "PUT" && isCached) { + Reflect.deleteProperty(memCache, getNewUrl(req.originalUrl)); + return next(); + } + return next(); +}; diff --git a/middleware/index.js b/middleware/index.js index f7c2174..2b53879 100644 --- a/middleware/index.js +++ b/middleware/index.js @@ -1,3 +1,4 @@ const responseHelpers = require("./responseHelpers"); +const cacheMiddleware = require("./cacheMiddleware"); -module.exports = {responseHelpers}; +module.exports = {responseHelpers, cacheMiddleware}; diff --git a/package.json b/package.json index ac05951..77b2baf 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "app.js", "scripts": { "start": "node ./app.js", - "test": "NODE_ENV=test ./node_modules/mocha/bin/_mocha --timeout 1000 --exit" + "test": "NODE_ENV=test ./node_modules/mocha/bin/_mocha --timeout 10000 --exit" }, "keywords": [ "exam", diff --git a/routes.js b/routes.js index 5b0d932..dd5bb52 100644 --- a/routes.js +++ b/routes.js @@ -3,6 +3,7 @@ const { EvaluationsController, CoursesController, StudentsController, + StatsController, TechnologiesController } = require("./controllers"); const afipAPI = require("./services/afip-mock-api"); @@ -27,6 +28,7 @@ module.exports = (app, router) => { const evaluationController = EvaluationsController(mongoose); const technologyController = TechnologiesController(mongoose); const billingController = BillingController(mongoose); + const statsController = StatsController(mongoose); const controllers = [ {basePath: "/evaluations", controller: evaluationController}, @@ -40,6 +42,11 @@ module.exports = (app, router) => { router.route("/admin/billing/getChargeableStudents") .get(billingController.getChargeableStudents); + router.route("/admin/billing/getInvoices") + .get(billingController.getInvoices); + router.route("/stats/failuresByStates") + .get(statsController.getFailuresByState); + router.route("/afip") .post(afipAPI.getInvoice); diff --git a/test/dummy_tests.js b/test/dummy_tests.js index e69de29..46dde39 100644 --- a/test/dummy_tests.js +++ b/test/dummy_tests.js @@ -0,0 +1,442 @@ +const app = require("../app.js"); +const {expect} = require("chai"); +const request = require("supertest"); +const mongoose = app.get("mongoose"); +const Course = mongoose.model("Course"); +const basePath = "/api/courses"; +const invoicePath = "/api/admin/billing/getInvoices"; +const validFilterParam = "?technologyId="; +const invalidFilterParam = "?techId=JS-000"; +const Student = mongoose.model("Student"); +const Evaluation = mongoose.model("Evaluation"); + +describe("Node Test", () => { + let course1 = null; + let course2 = null; + + before(() => { + return Course.deleteMany({}) + .then(() => { + console.log("Tasks collection cleaned!"); + return Promise.all([ + Course.create({ + "technologyId": "JS-000", + "date": { + "from": "2019-05-01T00:00:00.000Z", + "to": "2019-05-15T00:00:00.000Z" + }, + "description": "Node Js course", + "status": "new", + "classes": [ + { + "name": "Programming basics with Node JS", + "description": "A class to start programming with Node JS", + "content": "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", + "tags": "" + }, + { + "name": "Programming with Node JS", + "description": "A class to continue programming with Node JS", + "content": "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", + "tags": "" + }, + { + "name": "Programming advanced with Node JS", + "description": "A class to program with Node JS", + "content": "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", + "tags": "" + } + ], + "price": 123400, + "students": [] + }), + Course.create({ + "technologyId": "JS-001", + "date": { + "from": "2019-02-01T00:00:00.000Z", + "to": "2019-02-28T00:00:00.000Z" + }, + "description": "Vue Js course", + "status": "finished", + "classes": [ + { + "name": "Programming basics with Vue JS", + "description": "A class to start programming with Vue JS", + "content": "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", + "tags": "" + }, + { + "name": "Programming with Vue JS", + "description": "A class to continue programming with Vue JS", + "content": "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", + "tags": "" + }, + { + "name": "Programming advanced with Vue JS", + "description": "A class to program with Vue JS", + "content": "Lorem Ipsum is simply dummy text of the printing and typesetting industry.", + "tags": "" + } + ], + "price": 432100, + "students": [ + "5c8be6ec89a91670892d731a", + "5c8be6ec89a91670892d731b", + "5c8be6ec89a91670892d731c", + "5c8be6ec89a91670892d731d", + "5c8be6ec89a91670892d731e" + ] + }) + ]); + }) + .then((data) => { + course1 = data[0]; + course2 = data[1]; + }); + }); + + context("Issue #1 - Testing Filter", () => { + it("should filter courses by technologyId and return just one with the id JS-000", () => { + return request(app) + .get(`${basePath}${validFilterParam}${course1.technologyId}`) + .expect(200) + .then((res) => { + expect(res.body.status).to.eql("success"); + expect(res.body.data.courses).to.be.an("array"); + expect(res.body.data.courses.length).to.eql(1); + expect(res.body.data.courses[0]._id.toString()).to.eql(course1._id.toString()); + }); + }); + + it("should filter courses by technologyId and return just one with the id JS-001", () => { + return request(app) + .get(`${basePath}${validFilterParam}${course2.technologyId}`) + .expect(200) + .then((res) => { + expect(res.body.status).to.eql("success"); + expect(res.body.data.courses).to.be.an("array"); + expect(res.body.data.courses.length).to.eql(1); + expect(res.body.data.courses[0]._id.toString()).to.eql(course2._id.toString()); + }); + }); + + it("should return all courses due to invalid filter with an invalid param", () => { + return request(app) + .get(`${basePath}${invalidFilterParam}`) + .expect(200) + .then((res) => { + expect(res.body.status).to.eql("success"); + expect(res.body.data.courses).to.be.an("array"); + expect(res.body.data.courses.length).to.eql(2); + }); + }); + + it("Should return all courses without passing any filtering params", () => { + return request(app) + .get(`${basePath}`) + .expect(200) + .then((res) => { + expect(res.body.status).to.eql("success"); + expect(res.body.data.courses).to.be.an("array"); + expect(res.body.data.courses.length).to.eql(2); + }); + }); + }); + + context("Issue #2 - Testing getInvoice with AFIP", () => { + // This test works with the database that not from test. Rest to mock de necessary data + it.skip("Should return an array of student with their respective invoices", () => { + return request(app) + .get(`${invoicePath}`) + .expect(200) + .then((res) => { + expect(res.body.status).to.eql("success"); + expect(res.body.data).to.be.an("array"); + }); + }); + + it.skip("Should returns the list of students with their respective invoice", () => { + return request(app) + .get(`${invoicePath}`) + .expect(200) + .then((res) => { + expect(res.body.data.data); + expect(res.body.data.length).to.be.eql(7); + const invoice = res.body.data[0]; + expect(invoice.BillingNumber).to.be.a("number"); + expect(invoice.FirstAndLastName).to.be.eql("Everette Lehner"); + expect(invoice.address).to.be.eql("126 Schuppe Shore"); + expect(invoice.price).to.be.eql(932100); + }); + }); + }); + context("Issue #3 - Testing cache MiddleWare", () => { + it("Should return message 'Was cached!'", () => { + return request(app) + .get("/api/courses") + .expect(200) + .then(() => { + return request(app) + .get(`${basePath}`) + .expect(200); + }) + .then((res) => { + expect(res.body.status).to.eql("success"); + expect(res.body.message).to.be.eql("Was cached!"); + }); + }); + + it("Should not return message 'Was cached!' When Posting new Course", () => { + return request(app) + .get("/api/courses") + .expect(200) + .then(() => { + return request(app) + .post("/api/courses") + .send({ + "technologyId": "Ruby", + "date": { + "from": "2018-10-20T00:00:00Z", + "to": "2018-10-21T00:00:00Z" + }, + "description": "A Ruby starter course", + "classes": [{"name": "Class 1", "description": "zaraza"}], + "price": 3000, + "students": ["pepe"] + }) + .expect(200); + }) + .then(() => { + return request(app) + .get("/api/courses") + .expect(200); + }) + .then((res) => { + expect(res.body.status).to.eql("success"); + expect(res.body.message).not.to.be.eql("Was cached!"); + }); + }); + }); + + context("Issue #4 - Testing States who Students Fail the", () => { + before(async () => { + await Student.deleteMany({}); + await Evaluation.deleteMany({}); + const students = [ + { + "_id": "5c92b9f262e5287d7db5602f", + "firstName": "Pupa", + "lastName": "Polainas", + "billingAddress": { + "_id": "5c92b9f262e5287d7db56030", + "street1": "Av. De Mayo 776", + "city": "Buenos Aires", + "state": "Buenos Aires", + "zipCode": "1084", + "country": "Argentina" + }, + "creditCards": [ + ] + }, + { + "_id": "5c92b9f262e5287d7db56fff", + "firstName": "Pupa", + "lastName": "Polainas", + "billingAddress": { + "_id": "5c92b9f262e5287d7db56030", + "street1": "Av. De Mayo 776", + "city": "Buenos Aires", + "state": "Catamarca", + "zipCode": "1084", + "country": "Argentina" + }, + "creditCards": [ + ] + }, + { + "_id": "5c92b9f262e5287d7db5ffff", + "firstName": "Pupa", + "lastName": "Polainas", + "billingAddress": { + "_id": "5c92b9f262e5287d7db56030", + "street1": "Av. De Mayo 776", + "city": "Buenos Aires", + "state": "Santiago", + "zipCode": "1084", + "country": "Argentina" + }, + "creditCards": [ + ] + }, + { + "_id": "5c92b9f262e5287d7db56aaa", + "firstName": "Pupa", + "lastName": "Polainas", + "billingAddress": { + "_id": "5c92b9f262e5287d7db56030", + "street1": "Av. De Mayo 776", + "city": "Buenos Aires", + "state": "CABA", + "zipCode": "1084", + "country": "Argentina" + }, + "creditCards": [ + ] + }, + { + "_id": "5c92b9f262e5287d7db56aab", + "firstName": "Pupa", + "lastName": "Polainas", + "billingAddress": { + "_id": "5c92b9f262e5287d7db56030", + "street1": "Av. De Mayo 776", + "city": "Buenos Aires", + "state": "CABA", + "zipCode": "1084", + "country": "Argentina" + }, + "creditCards": [ + ] + } + ]; + const evaluations = [ + { + "_id": "5c92d654e34b581661a5779a", + "courseId": "5c8c09ff444bfa0dfa450066", + "date": { + "from": "2019-03-21T00:00:00.000Z", + "to": "2019-03-22T00:00:00.000Z" + }, + "abstract": "The evaluation for the Ruby course. Write JS stuff to pass", + "notes": [ + { + "_id": "5c92d654e34b581661a5779b", + "studentId": "5c92b9f262e5287d7db5602f", + "qualification": 2, + "status": "failed" + } + ] + }, + { + "_id": "5c92d654e34b581661a577dd", + "courseId": "5c8c09ff444bfa0dfa450066", + "date": { + "from": "2019-03-21T00:00:00.000Z", + "to": "2019-03-22T00:00:00.000Z" + }, + "abstract": "The evaluation for the Ruby course. Write JS stuff to pass", + "notes": [ + { + "_id": "5c92d654e34b581661a5779b", + "studentId": "5c92b9f262e5287d7db5602f", + "qualification": 2, + "status": "failed" + } + ] + }, + { + "_id": "5c92d654e34b581661a577de", + "courseId": "5c8c09ff444bfa0dfa450066", + "date": { + "from": "2019-03-21T00:00:00.000Z", + "to": "2019-03-22T00:00:00.000Z" + }, + "abstract": "The evaluation for the Ruby course. Write JS stuff to pass", + "notes": [ + { + "_id": "5c92d654e34b581661a5779b", + "studentId": "5c92b9f262e5287d7db56fff", + "qualification": 1, + "status": "failed" + } + ] + }, + { + "_id": "5c92d654e34b581661a577ee", + "courseId": "5c8c09ff444bfa0dfa450066", + "date": { + "from": "2019-03-21T00:00:00.000Z", + "to": "2019-03-22T00:00:00.000Z" + }, + "abstract": "The evaluation for the Ruby course. Write JS stuff to pass", + + "notes": [ + { + "_id": "5c92d654e34b581661a5779b", + "studentId": "5c92b9f262e5287d7db56aaa", + "qualification": 1, + "status": "failed" + } + ] + }, + { + "_id": "5c92d654e34b581661a577e2", + "courseId": "5c8c09ff444bfa0dfa450066", + "date": { + "from": "2019-03-21T00:00:00.000Z", + "to": "2019-03-22T00:00:00.000Z" + }, + "abstract": "The evaluation for the Ruby course. Write JS stuff to pass", + "notes": [ + { + "_id": "5c92d654e34b581661a5779b", + "studentId": "5c92b9f262e5287d7db56aab", + "qualification": 1, + "status": "failed" + } + ] + }, + { + "_id": "5c92d654e34b581661a5ffe2", + "courseId": "5c8c09ff444bfa0dfa450066", + "date": { + "from": "2019-03-21T00:00:00.000Z", + "to": "2019-03-22T00:00:00.000Z" + }, + "abstract": "The evaluation for the Ruby course. Write JS stuff to pass", + "notes": [ + { + "_id": "5c92d654e34b581661a5779b", + "studentId": "5c92b9f262e5287d7db5ffff", + "qualification": 9, + "status": "passed" + } + ] + } + ]; + await Student.insertMany(students); + await Evaluation.insertMany(evaluations); + }); + + it("Should returns the Number of students by city that have failed one evaluationn", () => { + return request(app) + .get("/api/stats/failuresByStates") + .expect(200) + .then((res) => { + expect(res.body.data.data); + expect(res.body.data.Catamarca).to.be.eql(1); + }); + }); + + it("Should returns the Number of students by city that have failed two evaluation", () => { + return request(app) + .get("/api/stats/failuresByStates") + .expect(200) + .then((res) => { + expect(res.body.data.data); + expect(res.body.data.CABA).to.be.eql(2); + }); + }); + + it("Should returns the Number of students by city that have no failed any evaluations", () => { + return request(app) + .get("/api/stats/failuresByStates") + .expect(200) + .then((res) => { + expect(res.body.data.data); + // eslint-disable-next-line no-unused-expressions + expect(res.body.data.Santiago).to.be.undefined; + }); + }); + }); +}); From bd960ee23221aed5494e8c249b70213d8a784ded Mon Sep 17 00:00:00 2001 From: bogui Date: Thu, 21 Mar 2019 10:08:27 -0300 Subject: [PATCH 2/3] update readme with exercises explination --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 38d2139..7d8d9ae 100644 --- a/README.md +++ b/README.md @@ -91,4 +91,7 @@ Summission must be done through a single Pull Request. with his/her city with a function that verify that the student wasnt added before. -## Tests \ No newline at end of file +## Tests + +According to the test, I would have prefer to developed some better tests for the app, +I think I could not dedicate enough time to do it, but I'm aware about the need to develop some more of them and on a neatter way. \ No newline at end of file From 767ed4381f276b0e0fafe09765175fa3ae5cfdf8 Mon Sep 17 00:00:00 2001 From: bogui Date: Thu, 21 Mar 2019 10:14:15 -0300 Subject: [PATCH 3/3] readme update --- README.md | 68 ------------------------------------------------------- 1 file changed, 68 deletions(-) diff --git a/README.md b/README.md index 7d8d9ae..5e8751a 100644 --- a/README.md +++ b/README.md @@ -27,71 +27,3 @@ To pass this exam all excercises must be resolved, every feature must be tested Summission must be done through a single Pull Request. -## Responses - -## Models - - As a general observation, I considera that the model of the app seems - very well dessigned. - As a suggestion of what I may change, taking into account that one - of the exercises asks for the amount of students that failed the exam - by each state, and the posible way to get it is by looking for the - state as a string, I think it would be better to create other entity (schema) - that has the country, the state, postal code and the id. So that, - we could have filter by state in a neatter way. - On the other hand, I may add some relationships to all the squema - that I have done on mongoose. For example: to relate the student - to his/her exam. - - -## 1) Issue 1 - Implementation plan: - -1 - I solved this issue just adding the parameter "technologyId" - to the filter parameters Array that already implemented. - It was simple due to the structure of the implemetation - and the filtering query mode. - -## 2) Issue 2 - Implementation plan: - - 1- I decided to create a new endpoint that calls one which I developed previously, - by using a request to avoid to modify it. - From the response I can get the students that have to pay the course. - - 2- This data is used to make a call to the AFIP API in order - to get the corresponding id for each student. Then I used destructuring, - which allowed me to made it easier, to give format to the object that - will be send as the response required on this exercise. - - -## 3) Issue 3 - Implementation plan: - - 1 - To solve this issue, firstly I create an array with URLs that are excluded from the middleware. - This is a scalable solution like the Issue 1, due to exclude other URL just need to add it - to the array that I mention before. - - 2 - Secondly, I ask who request method are using and depend if is a GET, - PUT or POST I do different things. Always I ask if that request is - in the local cache, and depending if is a POST or a PUT y delete from - cache with a different form. - - 3 - Also I redefined the response200 , in this way I able to get the data - in the callback to save it into cache and added to the new response. - - -## 4) Issue 4 - Implementation plan: - - 1- Since there is an exams list and each exam has a list of marks, - I decided to create a new list with all those marks that represent a failed exam. - - 2- With this new list of failed marks, - I looked for each student that was related with each mark creating a new list. - Taking into account this can result on duplicated students, - due to the fact that they could failed more than one exam, - I decided to use a dictionary structure to map each student - with his/her city with a function that verify that the student wasnt - added before. - -## Tests - -According to the test, I would have prefer to developed some better tests for the app, -I think I could not dedicate enough time to do it, but I'm aware about the need to develop some more of them and on a neatter way. \ No newline at end of file