tests: Add tests for recoverCron

This commit is contained in:
Blade Barringer
2016-05-26 22:08:31 -05:00
parent 8292903444
commit 91aba965b0
4 changed files with 124 additions and 25 deletions

View File

@@ -1,6 +1,7 @@
/* eslint-disable global-require */ /* eslint-disable global-require */
import moment from 'moment'; 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 { model as User } from '../../../../../website/server/models/user';
import * as Tasks from '../../../../../website/server/models/task'; import * as Tasks from '../../../../../website/server/models/task';
import { clone } from 'lodash'; 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();
}
});
});

View File

@@ -4,12 +4,14 @@ import {
generateTodo, generateTodo,
generateDaily, generateDaily,
} from '../../../../helpers/api-unit.helper'; } from '../../../../helpers/api-unit.helper';
import { cloneDeep } from 'lodash';
import cronMiddleware from '../../../../../website/server/middlewares/api-v3/cron'; import cronMiddleware from '../../../../../website/server/middlewares/api-v3/cron';
import moment from 'moment'; import moment from 'moment';
import { model as User } from '../../../../../website/server/models/user'; import { model as User } from '../../../../../website/server/models/user';
import { model as Group } from '../../../../../website/server/models/group'; import { model as Group } from '../../../../../website/server/models/group';
import * as Tasks from '../../../../../website/server/models/task'; import * as Tasks from '../../../../../website/server/models/task';
import analyticsService from '../../../../../website/server/libs/api-v3/analyticsService'; 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'; import { v4 as generateUUID } from 'uuid';
describe('cron middleware', () => { describe('cron middleware', () => {
@@ -45,6 +47,10 @@ describe('cron middleware', () => {
.catch(done); .catch(done);
}); });
afterEach(() => {
sandbox.restore();
});
it('calls next when user is not attached', (done) => { it('calls next when user is not attached', (done) => {
res.locals.user = null; res.locals.user = null;
cronMiddleware(req, res, (err) => done(err)); cronMiddleware(req, res, (err) => done(err));
@@ -191,4 +197,28 @@ describe('cron middleware', () => {
done(); 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();
});
});
}); });

View File

@@ -1,4 +1,6 @@
import moment from 'moment'; import moment from 'moment';
import Bluebird from 'bluebird';
import { model as User } from '../../models/user';
import common from '../../../../common/'; import common from '../../../../common/';
import { preenUserHistory } from '../../libs/api-v3/preening'; import { preenUserHistory } from '../../libs/api-v3/preening';
import _ from 'lodash'; 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. // Perform various beginning-of-day reset actions.
export function cron (options = {}) { export function cron (options = {}) {
let {user, tasksByType, analytics, now = new Date(), daysMissed, timezoneOffsetFromUserPrefs} = options; let {user, tasksByType, analytics, now = new Date(), daysMissed, timezoneOffsetFromUserPrefs} = options;

View File

@@ -5,33 +5,11 @@ import * as Tasks from '../../models/task';
import Bluebird from 'bluebird'; import Bluebird from 'bluebird';
import { model as Group } from '../../models/group'; import { model as Group } from '../../models/group';
import { model as User } from '../../models/user'; 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'; import { v4 as uuid } from 'uuid';
const daysSince = common.daysSince; 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) { async function cronAsync (req, res) {
let user = res.locals.user; let user = res.locals.user;
if (!user) return null; // User might not be available when authentication is not mandatory 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, times: 0,
}; };
recoverCron(recoveryStatus, req, res); recoverCron(recoveryStatus, res.locals);
} else { } else {
// For any other error make sure to reset _cronSignature so that it doesn't prevent cron from running // For any other error make sure to reset _cronSignature so that it doesn't prevent cron from running
// at the next request // at the next request