From 91aba965b0fbb74e13336f91cbc5639b316d4aea Mon Sep 17 00:00:00 2001 From: Blade Barringer Date: Thu, 26 May 2016 22:08:31 -0500 Subject: [PATCH] tests: Add tests for recoverCron --- test/api/v3/unit/libs/cron.test.js | 68 ++++++++++++++++++- .../api/v3/unit/middlewares/cronMiddleware.js | 30 ++++++++ website/server/libs/api-v3/cron.js | 25 +++++++ website/server/middlewares/api-v3/cron.js | 26 +------ 4 files changed, 124 insertions(+), 25 deletions(-) diff --git a/test/api/v3/unit/libs/cron.test.js b/test/api/v3/unit/libs/cron.test.js index 6f16f9973f..ef09868c27 100644 --- a/test/api/v3/unit/libs/cron.test.js +++ b/test/api/v3/unit/libs/cron.test.js @@ -1,6 +1,7 @@ /* eslint-disable global-require */ import moment from 'moment'; -import { cron } from '../../../../../website/server/libs/api-v3/cron'; +import Bluebird from 'bluebird'; +import { recoverCron, cron } from '../../../../../website/server/libs/api-v3/cron'; import { model as User } from '../../../../../website/server/models/user'; import * as Tasks from '../../../../../website/server/models/task'; import { clone } from 'lodash'; @@ -562,3 +563,68 @@ describe('cron', () => { }); }); }); + +describe('recoverCron', () => { + let locals, status, execStub; + + beforeEach(() => { + execStub = sandbox.stub(); + sandbox.stub(User, 'findOne').returns({ exec: execStub }); + + status = { times: 0 }; + locals = { + user: new User({ + auth: { + local: { + username: 'username', + lowerCaseUsername: 'username', + email: 'email@email.email', + salt: 'salt', + hashed_password: 'hashed_password', // eslint-disable-line camelcase + }, + }, + }), + }; + }); + + afterEach(() => { + sandbox.restore(); + }); + + it('throws an error if user cannot be found', async (done) => { + execStub.returns(Bluebird.resolve(null)); + + try { + await recoverCron(status, locals); + } catch (err) { + expect(err.message).to.eql(`User ${locals.user._id} not found while recovering.`); + + done(); + } + }); + + it('increases status.times count and reruns up to 3 times', async (done) => { + execStub.returns(Bluebird.resolve({_cronSignature: 'RUNNING_CRON'})); + execStub.onCall(3).returns(Bluebird.resolve({_cronSignature: 'NOT_RUNNING'})); + + await recoverCron(status, locals); + + expect(status.times).to.eql(3); + expect(locals.user).to.eql({_cronSignature: 'NOT_RUNNING'}); + + done(); + }); + + it('throws an error if recoverCron runs 4 times', async (done) => { + execStub.returns(Bluebird.resolve({_cronSignature: 'RUNNING_CRON'})); + + try { + await recoverCron(status, locals); + } catch (err) { + expect(status.times).to.eql(4); + expect(err.message).to.eql(`Impossible to recover from cron for user ${locals.user._id}.`); + + done(); + } + }); +}); diff --git a/test/api/v3/unit/middlewares/cronMiddleware.js b/test/api/v3/unit/middlewares/cronMiddleware.js index cba4cf83c1..2e4269e9fc 100644 --- a/test/api/v3/unit/middlewares/cronMiddleware.js +++ b/test/api/v3/unit/middlewares/cronMiddleware.js @@ -4,12 +4,14 @@ import { generateTodo, generateDaily, } from '../../../../helpers/api-unit.helper'; +import { cloneDeep } from 'lodash'; import cronMiddleware from '../../../../../website/server/middlewares/api-v3/cron'; import moment from 'moment'; import { model as User } from '../../../../../website/server/models/user'; import { model as Group } from '../../../../../website/server/models/group'; import * as Tasks from '../../../../../website/server/models/task'; import analyticsService from '../../../../../website/server/libs/api-v3/analyticsService'; +import * as cronLib from '../../../../../website/server/libs/api-v3/cron'; import { v4 as generateUUID } from 'uuid'; describe('cron middleware', () => { @@ -45,6 +47,10 @@ describe('cron middleware', () => { .catch(done); }); + afterEach(() => { + sandbox.restore(); + }); + it('calls next when user is not attached', (done) => { res.locals.user = null; cronMiddleware(req, res, (err) => done(err)); @@ -191,4 +197,28 @@ describe('cron middleware', () => { done(); }); }); + + it('recovers from failed cron and does not error when user is already cronning', async (done) => { + user.lastCron = moment(new Date()).subtract({days: 2}); + await user.save(); + + let updatedUser = cloneDeep(user); + updatedUser.nMatched = 0; + + sandbox.spy(cronLib, 'recoverCron'); + + sandbox.stub(User, 'update') + .withArgs({ _id: user._id, _cronSignature: 'NOT_RUNNING' }) + .returns({ + exec () { + return Promise.resolve(updatedUser); + }, + }); + + cronMiddleware(req, res, () => { + expect(cronLib.recoverCron).to.be.calledOnce; + + done(); + }); + }); }); diff --git a/website/server/libs/api-v3/cron.js b/website/server/libs/api-v3/cron.js index c59403d545..92ae202c12 100644 --- a/website/server/libs/api-v3/cron.js +++ b/website/server/libs/api-v3/cron.js @@ -1,4 +1,6 @@ import moment from 'moment'; +import Bluebird from 'bluebird'; +import { model as User } from '../../models/user'; import common from '../../../../common/'; import { preenUserHistory } from '../../libs/api-v3/preening'; import _ from 'lodash'; @@ -79,6 +81,29 @@ function performSleepTasks (user, tasksByType, now) { }); } +export async function recoverCron (status, locals) { + let {user} = locals; + + await Bluebird.delay(300); + + let reloadedUser = await User.findOne({_id: user._id}).exec(); + + if (!reloadedUser) { + throw new Error(`User ${user._id} not found while recovering.`); + } else if (reloadedUser._cronSignature !== 'NOT_RUNNING') { + status.times++; + + if (status.times < 4) { + await recoverCron(status, locals); + } else { + throw new Error(`Impossible to recover from cron for user ${user._id}.`); + } + } else { + locals.user = reloadedUser; + return null; + } +} + // Perform various beginning-of-day reset actions. export function cron (options = {}) { let {user, tasksByType, analytics, now = new Date(), daysMissed, timezoneOffsetFromUserPrefs} = options; diff --git a/website/server/middlewares/api-v3/cron.js b/website/server/middlewares/api-v3/cron.js index 75589e5af3..b6555cb682 100644 --- a/website/server/middlewares/api-v3/cron.js +++ b/website/server/middlewares/api-v3/cron.js @@ -5,33 +5,11 @@ import * as Tasks from '../../models/task'; import Bluebird from 'bluebird'; import { model as Group } from '../../models/group'; import { model as User } from '../../models/user'; -import { cron } from '../../libs/api-v3/cron'; +import { recoverCron, cron } from '../../libs/api-v3/cron'; import { v4 as uuid } from 'uuid'; const daysSince = common.daysSince; -async function recoverCron (status, req, res) { - let user = res.locals.user; - - await Bluebird.delay(300); - let reloadedUser = await User.findOne({_id: user._id}).exec(); - - if (!reloadedUser) { - throw new Error(`User ${user._id} not found while recovering.`); - } else if (reloadedUser._cronSignature !== 'NOT_RUNNING') { - status.times++; - - if (status.times < 4) { - await recoverCron(status, req, res); - } else { - throw new Error(`Impossible to recover from cron for user ${user._id}.`); - } - } else { - res.locals.user = reloadedUser; - return null; - } -} - async function cronAsync (req, res) { let user = res.locals.user; if (!user) return null; // User might not be available when authentication is not mandatory @@ -210,7 +188,7 @@ async function cronAsync (req, res) { times: 0, }; - recoverCron(recoveryStatus, req, res); + recoverCron(recoveryStatus, res.locals); } else { // For any other error make sure to reset _cronSignature so that it doesn't prevent cron from running // at the next request