diff --git a/app.js b/app.js index 9cfb46a..da6feaa 100644 --- a/app.js +++ b/app.js @@ -1,10 +1,13 @@ +let dictionary = {}; + const express = require("express"); 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")(dictionary); const routes = require("./routes"); +const request = require("request"); require("./models"); const app = express(); @@ -18,19 +21,19 @@ app.use(bodyParser.urlencoded({extended: true})); app.use(bodyParser.json()); app.use(morgan("dev")); +// Add cache middleware +app.use(cacheMiddleware); + // Add response helpers app.use(responseHelpers); -// Add cache middleware -// app.use(cacheMiddleware); - // Setup mongoose and load models mongoose.Promise = global.Promise; mongoose.connect(config.db, {useNewUrlParser: true}); // models(mongoose); // Register the routes and mount them all at /api -app.use("/api", routes(app, express.Router())); +app.use("/api", routes(app, express.Router(), request, config)); // default route handler app.use((req, res) => { diff --git a/config/test.js b/config/test.js index 736f065..d2a6bb7 100644 --- a/config/test.js +++ b/config/test.js @@ -1,5 +1,5 @@ module.exports = { env: "test", db: "mongodb://localhost:27017/courses_test", - port: process.env.PORT || 3000 + port: 3000 }; diff --git a/controllers/billing.js b/controllers/billing.js index e4c98b8..55f4586 100644 --- a/controllers/billing.js +++ b/controllers/billing.js @@ -1,7 +1,8 @@ -module.exports = (mongoose) => { +module.exports = (mongoose, request, config) => { const Course = mongoose.model("Course"); const Evaluation = mongoose.model("Evaluation"); const Student = mongoose.model("Student"); + const afipUrl = 'http://localhost:'+config.port+'/api/afip'; // para cada curso dame la evaluacion // para cada evaluacion dame los aprobados @@ -10,6 +11,70 @@ module.exports = (mongoose) => { // agrupar por student y calcular precio final // devolver precio, nombre, apellido y billing address + function getChargeables(){ + return new Promise((resolve, reject) => { + let courses = {}; + Course.find({status: "finished"}) + .then((_courses) => { + courses = _courses.reduce((coursesDict, course) => { + // eslint-disable-next-line + coursesDict[course._id] = course.price; + return coursesDict; + }, {}); + const promiseArray = _courses.map((course) => { + return Evaluation.findOne({courseId: course._id}); + }); + return Promise.all(promiseArray); + }) + .then((evaluations) => { + return Promise.resolve(evaluations.map((evaluation) => { + const evaluationStudents = evaluation.notes + .filter((note) => { + return note.status === "passed"; + }) + .map((student) => { + return student.studentId; + }); + return { + courseId: evaluation.courseId, + students: evaluationStudents + }; + })); + }) + .then((approvedStudentsToBill) => { + const studentsWithPrice = {}; + approvedStudentsToBill.forEach((course) => { + course.students.forEach((student) => { + if (studentsWithPrice[student]) { + studentsWithPrice[student] += courses[course.courseId]; + } else { + studentsWithPrice[student] = courses[course.courseId]; + } + }); + }); + return Promise.resolve(studentsWithPrice); + }) + .then((studentsWithPrice) => { + const studentsBillingInfo = Object.keys(studentsWithPrice).map((id) => { + return new Promise((resolve) => { + Student.findById(id).then((student) => { + return resolve({ + firstName: student.firstName, + lastName: student.lastName, + address: student.billingAddress, + price: studentsWithPrice[id] + }); + }); + }); + }); + resolve(Promise.all(studentsBillingInfo)); + }) + .catch((err) => { + reject(err); + }); + }) + } + /** * getChargeableStudents - get the students which passed the evaluation for a finished course * @@ -18,62 +83,7 @@ module.exports = (mongoose) => { * @return {Array} studentsBillingInfo */ function getChargeableStudents(req, res) { - let courses = {}; - Course.find({status: "finished"}) - .then((_courses) => { - courses = _courses.reduce((coursesDict, course) => { - // eslint-disable-next-line - coursesDict[course._id] = course.price; - return coursesDict; - }, {}); - const promiseArray = _courses.map((course) => { - return Evaluation.findOne({courseId: course._id}); - }); - return Promise.all(promiseArray); - }) - .then((evaluations) => { - return Promise.resolve(evaluations.map((evaluation) => { - const evaluationStudents = evaluation.notes - .filter((note) => { - return note.status === "passed"; - }) - .map((student) => { - return student.studentId; - }); - return { - courseId: evaluation.courseId, - students: evaluationStudents - }; - })); - }) - .then((approvedStudentsToBill) => { - const studentsWithPrice = {}; - approvedStudentsToBill.forEach((course) => { - course.students.forEach((student) => { - if (studentsWithPrice[student]) { - studentsWithPrice[student] += courses[course.courseId]; - } else { - studentsWithPrice[student] = courses[course.courseId]; - } - }); - }); - return Promise.resolve(studentsWithPrice); - }) - .then((studentsWithPrice) => { - const studentsBillingInfo = Object.keys(studentsWithPrice).map((id) => { - return new Promise((resolve) => { - Student.findById(id).then((student) => { - return resolve({ - firstName: student.firstName, - lastName: student.lastName, - address: student.billingAddress, - price: studentsWithPrice[id] - }); - }); - }); - }); - return Promise.all(studentsBillingInfo); - }) + getChargeables() .then((studentsBillingInfo) => { res.response200({studentsBillingInfo}, `Found '${studentsBillingInfo.length}' Students.`); }) @@ -82,7 +92,57 @@ module.exports = (mongoose) => { }); } + function getInvoices(req, res) { + getChargeables() + .then(students => { + return students.map(item => { + return { + nomYAp: `${item.firstName} ${item.lastName}`, + dir: `${item.address.street1}, ${item.address.city}, ${item.address.state}, ${item.address.country}`, + importe: item.price / 100 + } + }) + }) + .then(async (toInvoice) => { + let data = []; + let initLength = toInvoice.length; + while (data.length < initLength) { + let elem = toInvoice.shift(); + await callAFIP(elem) + .then(info => { + data.push({ + BillingNumber: info.data.id+"", + FirstAndLastName: elem.nomYAp, + Address: elem.dir , + price: elem.importe+"" + }); + }) + .catch(err => { + toInvoice.push(elem); + }) + } + res.response200(data, `Found '${data.length}' Students to invoice.`); + }) + .catch(err => { + res.response500(err, "Courses couldn't be found!"); + }) + } + + function callAFIP(obj){ + return new Promise((resolve, reject) => { + request.post(afipUrl, {json: obj}, (err, res, info) => { + if(err) { + reject(err); + } else if(info.status == "error") + reject(info.message); + else + resolve(info); + }) + }) + } + return { - getChargeableStudents + getChargeableStudents, + getInvoices }; }; diff --git a/controllers/courses.js b/controllers/courses.js index 4de1399..6884712 100644 --- a/controllers/courses.js +++ b/controllers/courses.js @@ -1,6 +1,6 @@ module.exports = (mongoose) => { var Course = mongoose.model("Course"), - filterFields = ["status"], + filterFields = ["status", "technologyId"], sortFields = ["status"]; var buildFilterQuery = function(params) { diff --git a/controllers/evaluations.js b/controllers/evaluations.js index f31eacd..2fc5e81 100644 --- a/controllers/evaluations.js +++ b/controllers/evaluations.js @@ -1,6 +1,9 @@ const GenericController = require("./genericController"); module.exports = (mongoose) => { + const Evaluation = mongoose.model("Evaluation"); + const Student = mongoose.model("Student"); + const listOptions = { filterBy: [], sortBy: [] @@ -18,8 +21,50 @@ module.exports = (mongoose) => { return controller.create(req, res, {normalize: getEvaluationDTO}); } + function failuresByStates(req, res) { + Evaluation.find({},{notes: 1}) + .then(evaluations => { + let failed = {}; + for(var i=0; i { + let promises = [] + Object.keys(evalsFailed).forEach(item => { + promises.push(Student.find({"_id": { $eq: item}}, {"billingAddress.state": 1})) + }) + return Promise.all(promises) + }) + .then(states => { + let _states_ = {} + for(var i=0; i { + res.response200(evalsFailed, `Found ${Object.keys(evalsFailed).length} State.`); + }) + .catch((err) => { + res.response500(err, "evaluations couldn't be found!"); + }); + } + return { ...controller, - create: createEvaluation + create: createEvaluation, + failuresByStates }; }; diff --git a/middleware/cacheMiddleware.js b/middleware/cacheMiddleware.js new file mode 100644 index 0000000..b7f866d --- /dev/null +++ b/middleware/cacheMiddleware.js @@ -0,0 +1,61 @@ +const notStored = [ + "/api/admin/billing/getChargeableStudents" +]; + +function isNotCacheable(url){ + return notStored.find(item => { + return url.includes(item); + }) +} + +function findResponse(dictionary, url){ + return dictionary[url]; +} + +let postCondition = (key, url) => { + return key == url +} + +function getParentPath(url){ + let parts = url.split("/"); + return (parts.slice(0, parts.length -1)).join("/"); +} + +let putCondition = (key, url) => { + let parentUrl = getParentPath(url); + return key == url || key == parentUrl +} + +function deleteFromDictionary(dictionary, method, url){ + let condition; + if(method === 'POST') + condition = postCondition + else //method PUT + condition = putCondition + for(key in dictionary){ + if(condition(key, url)){ + delete dictionary[key]; + } + } +} + +module.exports = (dictionary) => { + return (req, res, next) => { + if(isNotCacheable(req.originalUrl)){ + //console.log("Ruta no cacheable") + next() + } else if(req.method === "GET") { + let elem = findResponse(dictionary, req.originalUrl); + if(elem){ + //console.log("Respuesta cacheada") + res.json(elem) + } else { + //console.log("Respuesta sin cachear") + next() + } + } else if(req.method === "POST" || req.method === "PUT") { + deleteFromDictionary(dictionary, req.method, req.originalUrl) + next() + } + } +}; \ No newline at end of file diff --git a/middleware/index.js b/middleware/index.js index f7c2174..3265dad 100644 --- a/middleware/index.js +++ b/middleware/index.js @@ -1,3 +1,10 @@ -const responseHelpers = require("./responseHelpers"); -module.exports = {responseHelpers}; +module.exports = (dictionary) => { + const responseHelpers = require("./responseHelpers")(dictionary); + const cacheMiddleware = require("./cacheMiddleware")(dictionary); + + return { + cacheMiddleware, + responseHelpers + } +}; diff --git a/middleware/responseHelpers.js b/middleware/responseHelpers.js index 69c955b..0f6cb0d 100644 --- a/middleware/responseHelpers.js +++ b/middleware/responseHelpers.js @@ -1,23 +1,27 @@ -module.exports = (req, res, next) => { - res.response200 = (data = {}, message = "ok") => { - res.json({data, status: "success", message}); - }; +module.exports = (dictionary) => { + return (req, res, next) => { + res.response200 = (data = {}, message = "ok") => { + res.json({data, status: "success", message}); + if(req.method === 'GET') + dictionary[req.originalUrl] = {data, status: "success", message} + }; - res.response404 = (message = "Not found!") => { - res.status(404).json({status: "error", message}); - }; + res.response404 = (message = "Not found!") => { + res.status(404).json({status: "error", message}); + }; - res.response500 = (err, msg = "Unknown error!") => { - const error = typeof err === "string" ? {message: err} : (err || {}); - if (msg && error.message) { - error.message = `${msg} :: ${error.message}`; - } else if (msg) { - error.message = msg; - } + res.response500 = (err, msg = "Unknown error!") => { + const error = typeof err === "string" ? {message: err} : (err || {}); + if (msg && error.message) { + error.message = `${msg} :: ${error.message}`; + } else if (msg) { + error.message = msg; + } - console.error(error); - res.status(500).json({status: "error", message: error.message}); - }; + console.error(error); + res.status(500).json({status: "error", message: error.message}); + }; - next(); + next(); + } }; diff --git a/routes.js b/routes.js index 5b0d932..4bb9256 100644 --- a/routes.js +++ b/routes.js @@ -20,13 +20,13 @@ function mapGenericControllerRoutes(controllers, router) { }); } -module.exports = (app, router) => { +module.exports = (app, router, request, config) => { const mongoose = app.get("mongoose"); const courseController = CoursesController(mongoose); const studentController = StudentsController(mongoose); const evaluationController = EvaluationsController(mongoose); const technologyController = TechnologiesController(mongoose); - const billingController = BillingController(mongoose); + const billingController = BillingController(mongoose, request, config); const controllers = [ {basePath: "/evaluations", controller: evaluationController}, @@ -40,6 +40,12 @@ 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(evaluationController.failuresByStates); + router.route("/afip") .post(afipAPI.getInvoice); diff --git a/test/billing_test.js b/test/billing_test.js new file mode 100644 index 0000000..e8939b7 --- /dev/null +++ b/test/billing_test.js @@ -0,0 +1,156 @@ +const app = require("../app.js"), + expect = require("chai").expect, + request = require("supertest"), + mongoose = app.get("mongoose"), + path = "/api/admin/billing/getInvoices", + Student = mongoose.model("Student"), + Course = mongoose.model("Course") + Evaluation = mongoose.model("Evaluation"); + +describe("Billing controller tests", () => { + + beforeEach(() => { + return Promise.all([ + Student.deleteMany({}), + Course.deleteMany({}), + Evaluation.deleteMany({}) + ]) + .then(async () => { + console.log("Students, courses and evaluations cleaned!"); + let students = await Promise.all([ + Student.create({ + "firstName" : "Kris", + "lastName" : "Breitenberg", + "billingAddress" : { + "street1" : "01285 Glover Spur", + "city" : "Fritschville", + "state" : "Delaware", + "zipCode" : "34216", + "country" : "Somalia" + }, + "creditCards" : [] + }), + Student.create({ + "firstName" : "Westley", + "lastName" : "Mann", + "billingAddress" : { + "street1" : "474 Mraz Glen", + "city" : "Dariusfort", + "state" : "Kentucky", + "zipCode" : "67720-1349", + "country" : "Cameroon" + }, + "creditCards" : [ + { + "firstName" : "Cecil", + "lastName" : "Shields", + "last4Numbers" : 5175, + "creditCardAPIToken" : "560e28e7-91c1-4a3b-acb9-dcbedb42328a", + "isDefault" : false + } + ] + }), + Student.create({ + "firstName" : "Garrison", + "lastName" : "Cormier", + "billingAddress" : { + "street1" : "265 Daryl Station", + "city" : "West Royceview", + "state" : "Wisconsin", + "zipCode" : "01231", + "country" : "Holy See (Vatican City State)" + }, + "creditCards" : [ + { + "firstName" : "Berneice", + "lastName" : "Von", + "last4Numbers" : 7046, + "creditCardAPIToken" : "7ae905f8-7539-4952-8084-7a7a2b4155c6", + "isDefault" : false + }, + { + "firstName" : "Bernadine", + "lastName" : "Pollich", + "last4Numbers" : 1034, + "creditCardAPIToken" : "9d4d0af5-ae1e-4aa7-8ab0-08c85ae94fa2", + "isDefault" : true + }, + { + "firstName" : "Casimer", + "lastName" : "Koch", + "last4Numbers" : 1808, + "creditCardAPIToken" : "3c2fef30-8102-4fd4-b2fe-ec668bcd1d8a", + "isDefault" : false + } + ] + }) + ]); + let courses = await Promise.all([ + Course.create({ + "technologyId" : "JS-000", + "description" : "Node Js course", + "date" : { + "from" : new Date("2019-05-01T00:00:00.000Z"), + "to" : new Date("2019-05-15T00:00:00.000Z") + }, + "status" : "new", + "price" : 123409, + "students" : [students[0]._id.toString()] + }), + Course.create({ + "technologyId" : "JS-001", + "description" : "VUE Js course", + "date" : { + "from" : new Date("2019-02-01T00:00:00.000Z"), + "to" : new Date("2019-02-28T00:00:00.000Z") + }, + "status" : "finished", + "price" : 11111, + "students" : [students[1]._id.toString(), students[2]._id.toString()] + }) + ]); + let evaluations = await Promise.all([ + Evaluation.create({ + "courseId" : courses[1]._id.toString(), + "date" : { + "from" : new Date("2019-03-01T00:00:00.000Z"), + "to" : new Date("2019-03-02T00:00:00.000Z") + }, + "abstract" : "Written evaluation", + "notes" : [ + { + "studentId" : students[1]._id.toString(), + "qualification" : 10, + "status" : "passed" + }, + { + "studentId" : students[2]._id.toString(), + "qualification" : 9, + "status" : "passed" + } + ] + }) + ]) + }) //fin then + .catch(err => { + console.log(`ERROR inserting data for test`) + }) + }) + + describe("#GET /api/admin/billing/getInvoices", () => { + + it("should get all invoices of students", function() { + this.timeout(10000) + return request(app) + .get(path) + .expect(200) + .then((res) => { + expect(res.body.status).to.eql("success"); + expect(res.body.data).to.be.an("array"); + expect(res.body.data.length).to.eql(2); + }); + }); + }); + + +}); diff --git a/test/courses_test.js b/test/courses_test.js new file mode 100644 index 0000000..ba5cee8 --- /dev/null +++ b/test/courses_test.js @@ -0,0 +1,116 @@ +const app = require("../app.js"), + expect = require("chai").expect, + request = require("supertest"), + mongoose = app.get("mongoose"), + path = "/api/courses", + Course = mongoose.model("Course"); + +describe("Courses controller tests", () => { + + let course1 = { + "technologyId" : "JS-000", + "description" : "Node Js course", + "date" : { + "from" : new Date("2019-05-01T00:00:00.000Z"), + "to" : new Date("2019-05-15T00:00:00.000Z") + }, + "status" : "new", + "price" : 123400, + "students" : [] + } + + beforeEach(() => { + return Course.deleteMany({}) + .then(() => { + console.log("courses collection cleaned!"); + return Promise.all([ + Course.create({ + "technologyId" : "JS-000", + "description" : "Node Js course", + "date" : { + "from" : new Date("2019-05-01T00:00:00.000Z"), + "to" : new Date("2019-05-15T00:00:00.000Z") + }, + "status" : "new", + "price" : 123400, + "students" : [] + }), + Course.create({ + "technologyId" : "JS-001", + "description" : "VUE Js course", + "date" : { + "from" : new Date("2019-02-01T00:00:00.000Z"), + "to" : new Date("2019-02-28T00:00:00.000Z") + }, + "status" : "finished", + "price" : 123456, + "students" : [] + }) + ]); + }) + .then((data) => { + course1 = data[0]; + }); + }); + + context("#GET /courses", () => { + + it("should get all courses", () => { + return request(app) + .get(path) + .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); + }); + }); + + describe("filter", () => { + + it("should filter courses by technologyId", () => { + return request(app) + .get(`${path}?technologyId=${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].technologyId).to.eql(course1.technologyId); + }); + }); + + it("should filter courses by status", () => { + return request(app) + .get(`${path}?status=${course1.status}`) + .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].status).to.eql(course1.status); + }); + }); + + }); + + describe("execution Time", () => { + it("should get all courses twice and compare the response time", async () => { + let init = new Date(); + let resOne = await request(app) + .get(path) + .expect(200); + let stopOne = new Date(); + let resTwo = await request(app) + .get(path) + .expect(200); + let stopTwo = new Date(); + let firstExec = stopOne.getTime() - init.getTime(); + let secondExec = stopTwo.getTime() - stopOne.getTime(); + expect(secondExec <= firstExec).to.be.true; + }); + }); + + }); + +}); diff --git a/test/stats_test.js b/test/stats_test.js new file mode 100644 index 0000000..3544a42 --- /dev/null +++ b/test/stats_test.js @@ -0,0 +1,157 @@ +const app = require("../app.js"), + expect = require("chai").expect, + request = require("supertest"), + mongoose = app.get("mongoose"), + path = "/api/stats/failuresByStates", + Student = mongoose.model("Student"), + Course = mongoose.model("Course") + Evaluation = mongoose.model("Evaluation"); + +describe("Courses controller tests", () => { + + beforeEach(() => { + return Promise.all([ + Student.deleteMany({}), + Course.deleteMany({}), + Evaluation.deleteMany({}) + ]) + .then(async () => { + console.log("Students, courses and evaluations cleaned!"); + let students = await Promise.all([ + Student.create({ + "firstName" : "Kris", + "lastName" : "Breitenberg", + "billingAddress" : { + "street1" : "01285 Glover Spur", + "city" : "Fritschville", + "state" : "Delaware", + "zipCode" : "34216", + "country" : "Somalia" + }, + "creditCards" : [] + }), + Student.create({ + "firstName" : "Westley", + "lastName" : "Mann", + "billingAddress" : { + "street1" : "474 Mraz Glen", + "city" : "Dariusfort", + "state" : "Kentucky", + "zipCode" : "67720-1349", + "country" : "Cameroon" + }, + "creditCards" : [ + { + "firstName" : "Cecil", + "lastName" : "Shields", + "last4Numbers" : 5175, + "creditCardAPIToken" : "560e28e7-91c1-4a3b-acb9-dcbedb42328a", + "isDefault" : false + } + ] + }), + Student.create({ + "firstName" : "Garrison", + "lastName" : "Cormier", + "billingAddress" : { + "street1" : "265 Daryl Station", + "city" : "West Royceview", + "state" : "Wisconsin", + "zipCode" : "01231", + "country" : "Holy See (Vatican City State)" + }, + "creditCards" : [ + { + "firstName" : "Berneice", + "lastName" : "Von", + "last4Numbers" : 7046, + "creditCardAPIToken" : "7ae905f8-7539-4952-8084-7a7a2b4155c6", + "isDefault" : false + }, + { + "firstName" : "Bernadine", + "lastName" : "Pollich", + "last4Numbers" : 1034, + "creditCardAPIToken" : "9d4d0af5-ae1e-4aa7-8ab0-08c85ae94fa2", + "isDefault" : true + }, + { + "firstName" : "Casimer", + "lastName" : "Koch", + "last4Numbers" : 1808, + "creditCardAPIToken" : "3c2fef30-8102-4fd4-b2fe-ec668bcd1d8a", + "isDefault" : false + } + ] + }) + ]); + let courses = await Promise.all([ + Course.create({ + "technologyId" : "JS-000", + "description" : "Node Js course", + "date" : { + "from" : new Date("2019-05-01T00:00:00.000Z"), + "to" : new Date("2019-05-15T00:00:00.000Z") + }, + "status" : "new", + "price" : 123409, + "students" : [students[0]._id.toString()] + }), + Course.create({ + "technologyId" : "JS-001", + "description" : "VUE Js course", + "date" : { + "from" : new Date("2019-02-01T00:00:00.000Z"), + "to" : new Date("2019-02-28T00:00:00.000Z") + }, + "status" : "finished", + "price" : 11111, + "students" : [students[1]._id.toString(), students[2]._id.toString()] + }) + ]); + let evaluations = await Promise.all([ + Evaluation.create({ + "courseId" : courses[1]._id.toString(), + "date" : { + "from" : new Date("2019-03-01T00:00:00.000Z"), + "to" : new Date("2019-03-02T00:00:00.000Z") + }, + "abstract" : "Written evaluation", + "notes" : [ + { + "studentId" : students[1]._id.toString(), + "qualification" : 10, + "status" : "failed" + }, + { + "studentId" : students[2]._id.toString(), + "qualification" : 9, + "status" : "failed" + } + ] + }) + ]) + }) //fin then + .catch(err => { + console.log(`ERROR inserting data for test`) + }) + }) + + context("#GET /stats/failuresByStates", () => { + + it("should get all students that failed exams grouped by state", () => { + return request(app) + .get(path) + .expect(200) + .then((res) => { + expect(res.body.status).to.eql("success"); + expect(res.body.data).to.be.an("object"); + expect(Object.keys(res.body.data).length).to.eql(2); + expect(res.body.data).to.have.property("Wisconsin"); + expect(res.body.data["Wisconsin"]).to.equal(1); // + }); + }); + + }); + +});