From ff3ac901a8e151a1f47a5fb7f87c96dff0eec93d Mon Sep 17 00:00:00 2001 From: Sebastian <> Date: Sat, 16 Dec 2017 00:05:35 +0100 Subject: [PATCH] refactor(appointment-helper): split getNow() into two smaller functions and add unit tests --- src/app/helper/appointment.js | 142 ++++++++++---------- src/app/helper/appointment.spec.js | 209 +++++++++++++++++++++++++++++ src/sockets/appointment.js | 2 +- 3 files changed, 284 insertions(+), 69 deletions(-) diff --git a/src/app/helper/appointment.js b/src/app/helper/appointment.js index f1111d7..6decaf4 100644 --- a/src/app/helper/appointment.js +++ b/src/app/helper/appointment.js @@ -37,6 +37,10 @@ let appointmentHelper = { * @return {Object} Modified appointment object */ addIncrement: (appointment, increment) => { + if (!appointment) { + return null; + } + // add increment appointment.hour = (appointment.hour + 100 * increment); @@ -140,6 +144,72 @@ appointmentHelper.notify = () => new Promise(async (resolve) => { resolve('Notifications were send. Exiting.'); }); +/** + * Find an appointment for the given date/time. + * + * @param {'Datable'} datetime The date, moment, date-string or 'now' + * @param {number} toleranceBefore The minutes of tolerance before + * @param {number} toleranceAfter The minutes tolerance after + * @return The appointment object + */ +appointmentHelper.getAt = (datetime, toleranceBefore = 0, toleranceAfter = 0) => new Promise(async resolve => { + // load settings entity + const settings = await Settings.findOne(); + let at = datetime === 'now' + ? moment.tz(settings.appointmentsTimezone) + : moment(datetime); + + if (!at.isValid()) { + return resolve(null); + } + + // apply increment + const increment = settings && settings.appointmentsIncrement + ? settings.appointmentsIncrement + : 0; + at = at.subtract(increment, 'hours'); + + const minDate = at.clone().subtract(toleranceAfter, 'minutes'); + const maxDate = at.clone().add(toleranceBefore, 'minutes'); + + let doc = null; + if (minDate.weekday() !== maxDate.weekday()) { + // handle the case where the tolerance range + // includes two days + doc = await Appointment + .findOne({ + $or: [ + { + weekDay: minDate.weekday(), + hour: { + $gte: timeToNumber(minDate) + } + }, + { + weekDay: maxDate.weekday(), + hour: { + $lte: timeToNumber(maxDate) + } + } + ] + }) + .populate('user', 'name username gravatarHash') + .exec(); + } else { + doc = await Appointment + .findOne({ + weekDay: minDate.weekday(), + hour: { + $gte: timeToNumber(minDate), + $lte: timeToNumber(maxDate) + } + }) + .populate('user', 'name username gravatarHash') + .exec(); + } + + resolve(appointmentHelper.addIncrement(doc, increment)); +}); /** * Searches for appointments the user is allowed to @@ -147,7 +217,7 @@ appointmentHelper.notify = () => new Promise(async (resolve) => { * the time of their booked appointment (with a 5 minute * tolerance). For admins marked as teacher, it means * within the timespan of 5 minutes before any appointment - * starts and 25 minutes after it was scheduled. + * starts and 20 minutes after it was scheduled. * * @param {Object} user User that needs authorization * @param {Boolean} reconnect If set to true, the time tolarance @@ -156,72 +226,8 @@ appointmentHelper.notify = () => new Promise(async (resolve) => { * * @return the appointment object */ -appointmentHelper.getNow = (user, reconnect = false) => new Promise(async (resolve) => { - if (!user) { - return resolve(null); - } - - // load settings entity - const settings = await Settings.findOne(); - - // appointments are formatted in this timezone - const now = moment.tz(settings.appointmentsTimezone); - - const increment = settings && settings.appointmentsIncrement - ? settings.appointmentsIncrement - : 0; - - // find any appointment for right now - const doc = await Appointment - .findOne({ - weekDay: now.weekday(), - hour: { - $lte: timeToNumber( - // 5 minutes before appointment - now.clone().add(5 + 60 * increment , 'minutes') - ), - $gte: timeToNumber( - // 25 minutes after appointment - now.clone().subtract(25 + 60 * increment, 'minutes') - ) - }, - user: { $exists: true, $ne: null } - }) - .populate('user', 'name username gravatarHash') - .exec(); - - if (!doc) { - return resolve(null); - } - - // check if user is admin and marked as teacher - const isCallee = user.role === 'ROLE_ADMIN' && user.appointmentsCallee === true; - - // check if appointment is the one of requested user - const isOwnAppointment = doc.user._id.toString() === user._id.toString(); - // check whether the time now is before 10 minutes after the appointment starts - const isAppOnTime = doc.hour >= timeToNumber(now.clone().subtract(10 + 60 * increment, 'minutes')); - - - if (doc && (isCallee || isOwnAppointment && (reconnect || isAppOnTime))) { - - if (!isCallee && !reconnect) { - // register the user's request to initiate an appointment. - // Use 'await' in order to not confuse the count of - // the appointments before this update with the count - // afterwards. - await User - .findOneAndUpdate({ - _id: user._id - }, { - $addToSet: { appointments: moment.utc().startOf('day').toDate() } - }); - } - - return resolve(doc); - } - - resolve(null); -}); +appointmentHelper.getNowAuthorized = (user, reconnect = false) => appointmentHelper.getAt('now', 5, + reconnect || (user && user.role === 'ROLE_ADMIN' && user.appointmentsCallee === true) ? 25 : 8 +); export default appointmentHelper; diff --git a/src/app/helper/appointment.spec.js b/src/app/helper/appointment.spec.js index 9335cbe..5cc02ac 100644 --- a/src/app/helper/appointment.spec.js +++ b/src/app/helper/appointment.spec.js @@ -1,7 +1,13 @@ +// eslint-disable-next-line no-unused-vars +import { app } from '../../server.conf.js'; +import Appointment from '../models/appointment.model.js'; +import Settings from '../models/settings.model.js'; import appointHelper from './appointment.js'; import { expect } from 'chai'; +import moment from 'moment-timezone'; describe('Appointment Helper', () => { + it('should format hour correctly', () => { expect(appointHelper.printHour(6)).to.equal('00:06'); expect(appointHelper.printHour(59)).to.equal('00:59'); @@ -36,4 +42,207 @@ describe('Appointment Helper', () => { expect(appointHelper.addIncrement(dummy3, 0).weekDay).to.equal(0); expect(appointHelper.addIncrement(dummy3, -1).weekDay).to.equal(6); }); + + describe('Find appointment at certain time', () => { + before(done => { + Appointment.remove(() => Appointment + .insertMany([ + { weekDay: 0, hour: 0 }, + { weekDay: 1, hour: 10 }, + { weekDay: 2, hour: 700 }, + { weekDay: 3, hour: 1400 }, + { weekDay: 4, hour: 1800 }, + { weekDay: 5, hour: 1730 }, + { weekDay: 6, hour: 2300 } + ]) + .then((docs, err) => { + if (err) return done(err); + done(); + }) + ); + }); + + describe('Without increment', () => { + before(done => { + Settings.remove(() => { + Settings + .create({ + appointmentsIncrement: 0, + appointmentsTimezone: 'America/Toronto' + }) + .then((docs, err) => { + if (err) return done(err); + done(); + }); + }); + }); + + it('should find appointments correctly', async () => { + const dt = moment(); + + const app1 = await appointHelper.getAt(dt.weekday(0).hours(0).minutes(0)); + + expect(app1).to.not.be.null; + expect(app1.weekDay).to.equal(0); + expect(app1.hour).to.equal(0); + + const app2 = await appointHelper.getAt(dt.weekday(4).hours(18).minutes(0)); + expect(app2).to.not.be.null; + expect(app2.weekDay).to.equal(4); + expect(app2.hour).to.equal(1800); + + const app3 = await appointHelper.getAt(dt.weekday(5).hours(17).minutes(30)); + expect(app3).to.not.be.null; + expect(app3.weekDay).to.equal(5); + expect(app3.hour).to.equal(1730); + }); + + it('should find within range correctly', async () => { + const dt = moment(); + + let app1 = await appointHelper.getAt(dt.weekday(0).hours(0).minutes(10).seconds(0), 0, 10); + + expect(app1).to.not.be.null; + expect(app1.weekDay).to.equal(0); + expect(app1.hour).to.equal(0); + + app1 = await appointHelper.getAt(dt.weekday(0).hours(0).minutes(10), 0, 15); + expect(app1).to.not.be.null; + expect(app1.weekDay).to.equal(0); + expect(app1.hour).to.equal(0); + + app1 = await appointHelper.getAt(dt.weekday(0).hours(0).minutes(10), 0, 9); + expect(app1).to.be.null; + + app1 = await appointHelper.getAt(dt.weekday(6).hours(23).minutes(55), 5, 0); + expect(app1).to.not.be.null; + expect(app1.weekDay).to.equal(0); + expect(app1.hour).to.equal(0); + + app1 = await appointHelper.getAt(dt.weekday(6).hours(23).minutes(52), 15, 0); + expect(app1).to.not.be.null; + expect(app1.weekDay).to.equal(0); + expect(app1.hour).to.equal(0); + + app1 = await appointHelper.getAt(dt.weekday(6).hours(23).minutes(52), 3, 0); + expect(app1).to.be.null; + + let app2 = await appointHelper.getAt(dt.weekday(5).hours(18).minutes(25), 0, 65); + expect(app2).to.not.be.null; + expect(app2.weekDay).to.equal(5); + expect(app2.hour).to.equal(1730); + + app2 = await appointHelper.getAt(dt.weekday(5).hours(18).minutes(0), 0, 30); + expect(app2).to.not.be.null; + expect(app2.weekDay).to.equal(5); + expect(app2.hour).to.equal(1730); + + let app3 = await appointHelper.getAt(dt.weekday(4).hours(0).minutes(0), 18 * 60, 0); + expect(app3).to.not.be.null; + expect(app3.weekDay).to.equal(4); + expect(app3.hour).to.equal(1800); + + app3 = await appointHelper.getAt(dt.weekday(5).hours(0).minutes(5), 0, 6 * 60 + 6); + expect(app3).to.not.be.null; + expect(app3.weekDay).to.equal(4); + expect(app3.hour).to.equal(1800); + }); + }); + + describe('With increment', () => { + before(done => { + Settings.remove(() => { + Settings + .create({ + appointmentsIncrement: 2, + appointmentsTimezone: 'America/Toronto' + }) + .then((docs, err) => { + if (err) return done(err); + done(); + }); + }); + }); + + it('should find appointments correctly', async () => { + const dt = moment(); + + const app1 = await appointHelper.getAt(dt.weekday(0).hours(2).minutes(0)); + + expect(app1).to.not.be.null; + expect(app1.weekDay).to.equal(0); + expect(app1.hour).to.equal(200); + + const app2 = await appointHelper.getAt(dt.weekday(4).hours(20).minutes(0)); + expect(app2).to.not.be.null; + expect(app2.weekDay).to.equal(4); + expect(app2.hour).to.equal(2000); + + const app3 = await appointHelper.getAt(dt.weekday(5).hours(19).minutes(30)); + expect(app3).to.not.be.null; + expect(app3.weekDay).to.equal(5); + expect(app3.hour).to.equal(1930); + }); + + it('should find within range correctly', async () => { + const dt = moment(); + + let app1 = await appointHelper.getAt(dt.weekday(0).hours(2).minutes(10).seconds(0), 0, 10); + + expect(app1).to.not.be.null; + expect(app1.weekDay).to.equal(0); + expect(app1.hour).to.equal(200); + + app1 = await appointHelper.getAt(dt.weekday(0).hours(2).minutes(10), 0, 15); + expect(app1).to.not.be.null; + expect(app1.weekDay).to.equal(0); + expect(app1.hour).to.equal(200); + + app1 = await appointHelper.getAt(dt.weekday(0).hours(2).minutes(10), 0, 9); + expect(app1).to.be.null; + + app1 = await appointHelper.getAt(dt.weekday(0).hours(1).minutes(55), 5, 0); + expect(app1).to.not.be.null; + expect(app1.weekDay).to.equal(0); + expect(app1.hour).to.equal(200); + + app1 = await appointHelper.getAt(dt.weekday(0).hours(1).minutes(52), 15, 0); + expect(app1).to.not.be.null; + expect(app1.weekDay).to.equal(0); + expect(app1.hour).to.equal(200); + + app1 = await appointHelper.getAt(dt.weekday(6).hours(23).minutes(52), 3, 0); + expect(app1).to.be.null; + + let app2 = await appointHelper.getAt(dt.weekday(5).hours(20).minutes(25), 0, 65); + expect(app2).to.not.be.null; + expect(app2.weekDay).to.equal(5); + expect(app2.hour).to.equal(1930); + + app2 = await appointHelper.getAt(dt.weekday(5).hours(20).minutes(0), 0, 30); + expect(app2).to.not.be.null; + expect(app2.weekDay).to.equal(5); + expect(app2.hour).to.equal(1930); + + let app3 = await appointHelper.getAt(dt.weekday(4).hours(2).minutes(0), 18 * 60, 0); + expect(app3).to.not.be.null; + expect(app3.weekDay).to.equal(4); + expect(app3.hour).to.equal(2000); + + app3 = await appointHelper.getAt(dt.weekday(5).hours(2).minutes(5), 0, 6 * 60 + 6); + expect(app3).to.not.be.null; + expect(app3.weekDay).to.equal(4); + expect(app3.hour).to.equal(2000); + + let app4 = await appointHelper.getAt(dt.weekday(1).hours(23).minutes(55), 9 * 60 + 5, 0); + expect(app4).to.not.be.null; + expect(app4.weekDay).to.equal(2); + expect(app4.hour).to.equal(900); + + app4 = await appointHelper.getAt(dt.weekday(1).hours(23).minutes(55), 9 * 60 + 4, 0); + expect(app4).to.be.null; + }); + }); + }); + }); diff --git a/src/sockets/appointment.js b/src/sockets/appointment.js index 1b3c16d..48b0038 100644 --- a/src/sockets/appointment.js +++ b/src/sockets/appointment.js @@ -58,7 +58,7 @@ export default (socket, io) => { // if an authorized user is already in the room 'Appointment', then this means that a // reconnect is given (-> second parameter). - const appointment = await appointHelper.getNow(userNow, roomLen === 1 && !inRoom()); + const appointment = await appointHelper.getNowAuthorized(userNow, roomLen === 1 && !inRoom()); // send appointment and status to client socket.emit('appointment', {