Fix: Current challenge timeout (#35352)
<!-- Please follow this checklist and put an x in each of the boxes, like this: [x]. It will ensure that our team takes your pull request seriously. --> - [x] I have read [freeCodeCamp's contribution guidelines](https://github.com/freeCodeCamp/freeCodeCamp/blob/master/CONTRIBUTING.md). - [x] My pull request has a descriptive title (not a vague title like `Update index.md`) - [x] My pull request targets the `master` branch of freeCodeCamp. Closes #35345 This PR resolves an issue where the `/challenges/current-challenge` request times out due to an unresolved promise.
This commit is contained in:
committed by
mrugesh mohapatra
parent
ce2b46926f
commit
675d6a76e4
@ -18,14 +18,12 @@ before_install:
|
|||||||
install: npm ci
|
install: npm ci
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
- git config --global user.email team+camperbot@freeCodeCamp.org
|
- npm run ensure-env
|
||||||
- git config --global user.name "CamperBot"
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
include:
|
include:
|
||||||
- stage: Lint javaScript
|
- stage: Lint javaScript
|
||||||
script:
|
script:
|
||||||
- npm run ensure-env
|
|
||||||
- npm run lint
|
- npm run lint
|
||||||
|
|
||||||
- stage: Unit and Integration tests
|
- stage: Unit and Integration tests
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
import { Observable } from 'rx';
|
import { Observable } from 'rx';
|
||||||
import _ from 'lodash';
|
import { isEmpty, pick, omit, find, uniqBy, last } from 'lodash';
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
import accepts from 'accepts';
|
import accepts from 'accepts';
|
||||||
import dedent from 'dedent';
|
import dedent from 'dedent';
|
||||||
@ -19,129 +19,14 @@ import { fixCompletedChallengeItem } from '../../common/utils';
|
|||||||
|
|
||||||
const log = debug('fcc:boot:challenges');
|
const log = debug('fcc:boot:challenges');
|
||||||
|
|
||||||
const learnURL = `${homeLocation}/learn`;
|
|
||||||
|
|
||||||
const jsProjects = [
|
|
||||||
'aaa48de84e1ecc7c742e1124',
|
|
||||||
'a7f4d8f2483413a6ce226cac',
|
|
||||||
'56533eb9ac21ba0edf2244e2',
|
|
||||||
'aff0395860f5d3034dc0bfc9',
|
|
||||||
'aa2e6f85cab2ab736c9a9b24'
|
|
||||||
];
|
|
||||||
|
|
||||||
function buildUserUpdate(user, challengeId, _completedChallenge, timezone) {
|
|
||||||
const { files } = _completedChallenge;
|
|
||||||
let completedChallenge = {};
|
|
||||||
|
|
||||||
if (jsProjects.includes(challengeId)) {
|
|
||||||
completedChallenge = {
|
|
||||||
..._completedChallenge,
|
|
||||||
files: Object.keys(files)
|
|
||||||
.map(key => files[key])
|
|
||||||
.map(file =>
|
|
||||||
_.pick(file, ['contents', 'key', 'index', 'name', 'path', 'ext'])
|
|
||||||
)
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
completedChallenge = _.omit(_completedChallenge, ['files']);
|
|
||||||
}
|
|
||||||
let finalChallenge;
|
|
||||||
const updateData = {};
|
|
||||||
const { timezone: userTimezone, completedChallenges = [] } = user;
|
|
||||||
|
|
||||||
const oldChallenge = _.find(
|
|
||||||
completedChallenges,
|
|
||||||
({ id }) => challengeId === id
|
|
||||||
);
|
|
||||||
const alreadyCompleted = !!oldChallenge;
|
|
||||||
|
|
||||||
if (alreadyCompleted) {
|
|
||||||
finalChallenge = {
|
|
||||||
...completedChallenge,
|
|
||||||
completedDate: oldChallenge.completedDate
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
updateData.$push = {
|
|
||||||
...updateData.$push,
|
|
||||||
progressTimestamps: Date.now()
|
|
||||||
};
|
|
||||||
finalChallenge = {
|
|
||||||
...completedChallenge
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
updateData.$set = {
|
|
||||||
completedChallenges: _.uniqBy(
|
|
||||||
[finalChallenge, ...completedChallenges.map(fixCompletedChallengeItem)],
|
|
||||||
'id'
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
if (
|
|
||||||
timezone &&
|
|
||||||
timezone !== 'UTC' &&
|
|
||||||
(!userTimezone || userTimezone === 'UTC')
|
|
||||||
) {
|
|
||||||
updateData.$set = {
|
|
||||||
...updateData.$set,
|
|
||||||
timezone: userTimezone
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
alreadyCompleted,
|
|
||||||
updateData,
|
|
||||||
completedDate: finalChallenge.completedDate
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildChallengeUrl(challenge) {
|
|
||||||
const { superBlock, block, dashedName } = challenge;
|
|
||||||
return `/learn/${dasherize(superBlock)}/${dasherize(block)}/${dashedName}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getFirstChallenge(Challenge) {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
Challenge.find(
|
|
||||||
{ where: { challengeOrder: 0, superOrder: 1, order: 0 } },
|
|
||||||
(err, challenge) => {
|
|
||||||
if (err) {
|
|
||||||
console.log(err);
|
|
||||||
return resolve('/learn');
|
|
||||||
}
|
|
||||||
return resolve(buildChallengeUrl(challenge));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createChallengeUrlResolver(app) {
|
|
||||||
const { Challenge } = app.models;
|
|
||||||
const cache = new Map();
|
|
||||||
const firstChallenge = await getFirstChallenge(Challenge);
|
|
||||||
|
|
||||||
return function resolveChallengeUrl(id) {
|
|
||||||
return new Promise(resolve => {
|
|
||||||
if (cache.has(id)) {
|
|
||||||
return resolve(cache.get(id));
|
|
||||||
}
|
|
||||||
return Challenge.findById(id, (err, challenge) => {
|
|
||||||
if (err) {
|
|
||||||
console.log(err);
|
|
||||||
return firstChallenge;
|
|
||||||
}
|
|
||||||
const challengeUrl = buildChallengeUrl(challenge);
|
|
||||||
cache.set(id, challengeUrl);
|
|
||||||
return resolve(challengeUrl);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function bootChallenge(app, done) {
|
export default async function bootChallenge(app, done) {
|
||||||
const send200toNonUser = ifNoUserSend(true);
|
const send200toNonUser = ifNoUserSend(true);
|
||||||
const api = app.loopback.Router();
|
const api = app.loopback.Router();
|
||||||
const router = app.loopback.Router();
|
const router = app.loopback.Router();
|
||||||
const challengeUrlResolver = await createChallengeUrlResolver(app);
|
const challengeUrlResolver = await createChallengeUrlResolver(app);
|
||||||
|
const redirectToCurrentChallenge = createRedirectToCurrentChallenge(
|
||||||
|
challengeUrlResolver
|
||||||
|
);
|
||||||
|
|
||||||
api.post(
|
api.post(
|
||||||
'/modern-challenge-completed',
|
'/modern-challenge-completed',
|
||||||
@ -183,259 +68,381 @@ export default async function bootChallenge(app, done) {
|
|||||||
app.use('/external', api);
|
app.use('/external', api);
|
||||||
app.use('/internal', api);
|
app.use('/internal', api);
|
||||||
app.use(router);
|
app.use(router);
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
const learnURL = `${homeLocation}/learn`;
|
||||||
|
|
||||||
function modernChallengeCompleted(req, res, next) {
|
const jsProjects = [
|
||||||
const type = accepts(req).type('html', 'json', 'text');
|
'aaa48de84e1ecc7c742e1124',
|
||||||
req.checkBody('id', 'id must be an ObjectId').isMongoId();
|
'a7f4d8f2483413a6ce226cac',
|
||||||
|
'56533eb9ac21ba0edf2244e2',
|
||||||
|
'aff0395860f5d3034dc0bfc9',
|
||||||
|
'aa2e6f85cab2ab736c9a9b24'
|
||||||
|
];
|
||||||
|
|
||||||
const errors = req.validationErrors(true);
|
function buildUserUpdate(user, challengeId, _completedChallenge, timezone) {
|
||||||
if (errors) {
|
const { files } = _completedChallenge;
|
||||||
if (type === 'json') {
|
let completedChallenge = {};
|
||||||
return res.status(403).send({ errors });
|
|
||||||
}
|
|
||||||
|
|
||||||
log('errors', errors);
|
if (jsProjects.includes(challengeId)) {
|
||||||
return res.sendStatus(403);
|
completedChallenge = {
|
||||||
}
|
..._completedChallenge,
|
||||||
|
files: Object.keys(files)
|
||||||
|
.map(key => files[key])
|
||||||
|
.map(file =>
|
||||||
|
pick(file, ['contents', 'key', 'index', 'name', 'path', 'ext'])
|
||||||
|
)
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
completedChallenge = omit(_completedChallenge, ['files']);
|
||||||
|
}
|
||||||
|
let finalChallenge;
|
||||||
|
const updateData = {};
|
||||||
|
const { timezone: userTimezone, completedChallenges = [] } = user;
|
||||||
|
|
||||||
const user = req.user;
|
const oldChallenge = find(
|
||||||
return user
|
completedChallenges,
|
||||||
.getCompletedChallenges$()
|
({ id }) => challengeId === id
|
||||||
.flatMap(() => {
|
);
|
||||||
const completedDate = Date.now();
|
const alreadyCompleted = !!oldChallenge;
|
||||||
const { id, files } = req.body;
|
|
||||||
|
|
||||||
const { alreadyCompleted, updateData } = buildUserUpdate(user, id, {
|
if (alreadyCompleted) {
|
||||||
id,
|
finalChallenge = {
|
||||||
files,
|
...completedChallenge,
|
||||||
completedDate
|
completedDate: oldChallenge.completedDate
|
||||||
});
|
};
|
||||||
|
} else {
|
||||||
const points = alreadyCompleted ? user.points : user.points + 1;
|
updateData.$push = {
|
||||||
const updatePromise = new Promise((resolve, reject) =>
|
...updateData.$push,
|
||||||
user.updateAttributes(updateData, err => {
|
progressTimestamps: Date.now()
|
||||||
if (err) {
|
};
|
||||||
return reject(err);
|
finalChallenge = {
|
||||||
}
|
...completedChallenge
|
||||||
return resolve();
|
};
|
||||||
})
|
|
||||||
);
|
|
||||||
return Observable.fromPromise(updatePromise).map(() => {
|
|
||||||
if (type === 'json') {
|
|
||||||
return res.json({
|
|
||||||
points,
|
|
||||||
alreadyCompleted,
|
|
||||||
completedDate
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return res.sendStatus(200);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.subscribe(() => {}, next);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function completedChallenge(req, res, next) {
|
updateData.$set = {
|
||||||
req.checkBody('id', 'id must be an ObjectId').isMongoId();
|
completedChallenges: uniqBy(
|
||||||
const type = accepts(req).type('html', 'json', 'text');
|
[finalChallenge, ...completedChallenges.map(fixCompletedChallengeItem)],
|
||||||
const errors = req.validationErrors(true);
|
'id'
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
const { user } = req;
|
if (
|
||||||
|
timezone &&
|
||||||
|
timezone !== 'UTC' &&
|
||||||
|
(!userTimezone || userTimezone === 'UTC')
|
||||||
|
) {
|
||||||
|
updateData.$set = {
|
||||||
|
...updateData.$set,
|
||||||
|
timezone: userTimezone
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
alreadyCompleted,
|
||||||
|
updateData,
|
||||||
|
completedDate: finalChallenge.completedDate
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (errors) {
|
export function buildChallengeUrl(challenge) {
|
||||||
if (type === 'json') {
|
const { superBlock, block, dashedName } = challenge;
|
||||||
return res.status(403).send({ errors });
|
return `/learn/${dasherize(superBlock)}/${dasherize(block)}/${dashedName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFirstChallenge(Challenge) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
Challenge.findOne(
|
||||||
|
{ where: { challengeOrder: 0, superOrder: 1, order: 0 } },
|
||||||
|
(err, challenge) => {
|
||||||
|
if (err || isEmpty(challenge)) {
|
||||||
|
return resolve('/learn');
|
||||||
|
}
|
||||||
|
return resolve(buildChallengeUrl(challenge));
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
log('errors', errors);
|
export async function createChallengeUrlResolver(
|
||||||
return res.sendStatus(403);
|
app,
|
||||||
|
{ _getFirstChallenge = getFirstChallenge } = {}
|
||||||
|
) {
|
||||||
|
const { Challenge } = app.models;
|
||||||
|
const cache = new Map();
|
||||||
|
const firstChallenge = await _getFirstChallenge(Challenge);
|
||||||
|
return function resolveChallengeUrl(id) {
|
||||||
|
if (isEmpty(id)) {
|
||||||
|
return Promise.resolve(firstChallenge);
|
||||||
|
}
|
||||||
|
return new Promise(resolve => {
|
||||||
|
if (cache.has(id)) {
|
||||||
|
return resolve(cache.get(id));
|
||||||
|
}
|
||||||
|
return Challenge.findById(id, (err, challenge) => {
|
||||||
|
if (err || isEmpty(challenge)) {
|
||||||
|
return resolve(firstChallenge);
|
||||||
|
}
|
||||||
|
const challengeUrl = buildChallengeUrl(challenge);
|
||||||
|
cache.set(id, challengeUrl);
|
||||||
|
return resolve(challengeUrl);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function modernChallengeCompleted(req, res, next) {
|
||||||
|
const type = accepts(req).type('html', 'json', 'text');
|
||||||
|
req.checkBody('id', 'id must be an ObjectId').isMongoId();
|
||||||
|
|
||||||
|
const errors = req.validationErrors(true);
|
||||||
|
if (errors) {
|
||||||
|
if (type === 'json') {
|
||||||
|
return res.status(403).send({ errors });
|
||||||
}
|
}
|
||||||
|
|
||||||
return user
|
log('errors', errors);
|
||||||
.getCompletedChallenges$()
|
return res.sendStatus(403);
|
||||||
.flatMap(() => {
|
|
||||||
const completedDate = Date.now();
|
|
||||||
const { id, solution, timezone, files } = req.body;
|
|
||||||
|
|
||||||
const { alreadyCompleted, updateData } = buildUserUpdate(
|
|
||||||
user,
|
|
||||||
id,
|
|
||||||
{ id, solution, completedDate, files },
|
|
||||||
timezone
|
|
||||||
);
|
|
||||||
|
|
||||||
const points = alreadyCompleted ? user.points : user.points + 1;
|
|
||||||
|
|
||||||
const updatePromise = new Promise((resolve, reject) =>
|
|
||||||
user.updateAttributes(updateData, err => {
|
|
||||||
if (err) {
|
|
||||||
return reject(err);
|
|
||||||
}
|
|
||||||
return resolve();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return Observable.fromPromise(updatePromise).map(() => {
|
|
||||||
if (type === 'json') {
|
|
||||||
return res.json({
|
|
||||||
points,
|
|
||||||
alreadyCompleted,
|
|
||||||
completedDate
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return res.sendStatus(200);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.subscribe(() => {}, next);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function projectCompleted(req, res, next) {
|
const user = req.user;
|
||||||
const type = accepts(req).type('html', 'json', 'text');
|
return user
|
||||||
req.checkBody('id', 'id must be an ObjectId').isMongoId();
|
.getCompletedChallenges$()
|
||||||
req.checkBody('challengeType', 'must be a number').isNumber();
|
.flatMap(() => {
|
||||||
req.checkBody('solution', 'solution must be a URL').isURL();
|
const completedDate = Date.now();
|
||||||
|
const { id, files } = req.body;
|
||||||
|
|
||||||
const errors = req.validationErrors(true);
|
const { alreadyCompleted, updateData } = buildUserUpdate(user, id, {
|
||||||
|
id,
|
||||||
|
files,
|
||||||
|
completedDate
|
||||||
|
});
|
||||||
|
|
||||||
if (errors) {
|
const points = alreadyCompleted ? user.points : user.points + 1;
|
||||||
if (type === 'json') {
|
const updatePromise = new Promise((resolve, reject) =>
|
||||||
return res.status(403).send({ errors });
|
user.updateAttributes(updateData, err => {
|
||||||
}
|
if (err) {
|
||||||
log('errors', errors);
|
return reject(err);
|
||||||
return res.sendStatus(403);
|
}
|
||||||
}
|
return resolve();
|
||||||
|
})
|
||||||
const { user, body = {} } = req;
|
|
||||||
|
|
||||||
const completedChallenge = _.pick(body, [
|
|
||||||
'id',
|
|
||||||
'solution',
|
|
||||||
'githubLink',
|
|
||||||
'challengeType',
|
|
||||||
'files'
|
|
||||||
]);
|
|
||||||
completedChallenge.completedDate = Date.now();
|
|
||||||
|
|
||||||
if (
|
|
||||||
!completedChallenge.solution ||
|
|
||||||
// only basejumps require github links
|
|
||||||
(completedChallenge.challengeType === 4 && !completedChallenge.githubLink)
|
|
||||||
) {
|
|
||||||
req.flash(
|
|
||||||
'danger',
|
|
||||||
"You haven't supplied the necessary URLs for us to inspect your work."
|
|
||||||
);
|
);
|
||||||
return res.sendStatus(403);
|
return Observable.fromPromise(updatePromise).map(() => {
|
||||||
|
if (type === 'json') {
|
||||||
|
return res.json({
|
||||||
|
points,
|
||||||
|
alreadyCompleted,
|
||||||
|
completedDate
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return res.sendStatus(200);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.subscribe(() => {}, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
function completedChallenge(req, res, next) {
|
||||||
|
req.checkBody('id', 'id must be an ObjectId').isMongoId();
|
||||||
|
const type = accepts(req).type('html', 'json', 'text');
|
||||||
|
const errors = req.validationErrors(true);
|
||||||
|
|
||||||
|
const { user } = req;
|
||||||
|
|
||||||
|
if (errors) {
|
||||||
|
if (type === 'json') {
|
||||||
|
return res.status(403).send({ errors });
|
||||||
}
|
}
|
||||||
|
|
||||||
return user
|
log('errors', errors);
|
||||||
.getCompletedChallenges$()
|
return res.sendStatus(403);
|
||||||
.flatMap(() => {
|
|
||||||
const { alreadyCompleted, updateData } = buildUserUpdate(
|
|
||||||
user,
|
|
||||||
completedChallenge.id,
|
|
||||||
completedChallenge
|
|
||||||
);
|
|
||||||
|
|
||||||
const updatePromise = new Promise((resolve, reject) =>
|
|
||||||
user.updateAttributes(updateData, err => {
|
|
||||||
if (err) {
|
|
||||||
return reject(err);
|
|
||||||
}
|
|
||||||
return resolve();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return Observable.fromPromise(updatePromise).doOnNext(() => {
|
|
||||||
if (type === 'json') {
|
|
||||||
return res.send({
|
|
||||||
alreadyCompleted,
|
|
||||||
points: alreadyCompleted ? user.points : user.points + 1,
|
|
||||||
completedDate: completedChallenge.completedDate
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return res.status(200).send(true);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.subscribe(() => {}, next);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function backendChallengeCompleted(req, res, next) {
|
return user
|
||||||
const type = accepts(req).type('html', 'json', 'text');
|
.getCompletedChallenges$()
|
||||||
req.checkBody('id', 'id must be an ObjectId').isMongoId();
|
.flatMap(() => {
|
||||||
req.checkBody('solution', 'solution must be a URL').isURL();
|
const completedDate = Date.now();
|
||||||
|
const { id, solution, timezone, files } = req.body;
|
||||||
|
|
||||||
const errors = req.validationErrors(true);
|
const { alreadyCompleted, updateData } = buildUserUpdate(
|
||||||
|
user,
|
||||||
|
id,
|
||||||
|
{ id, solution, completedDate, files },
|
||||||
|
timezone
|
||||||
|
);
|
||||||
|
|
||||||
if (errors) {
|
const points = alreadyCompleted ? user.points : user.points + 1;
|
||||||
if (type === 'json') {
|
|
||||||
return res.status(403).send({ errors });
|
const updatePromise = new Promise((resolve, reject) =>
|
||||||
}
|
user.updateAttributes(updateData, err => {
|
||||||
log('errors', errors);
|
if (err) {
|
||||||
return res.sendStatus(403);
|
return reject(err);
|
||||||
|
}
|
||||||
|
return resolve();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return Observable.fromPromise(updatePromise).map(() => {
|
||||||
|
if (type === 'json') {
|
||||||
|
return res.json({
|
||||||
|
points,
|
||||||
|
alreadyCompleted,
|
||||||
|
completedDate
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return res.sendStatus(200);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.subscribe(() => {}, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
function projectCompleted(req, res, next) {
|
||||||
|
const type = accepts(req).type('html', 'json', 'text');
|
||||||
|
req.checkBody('id', 'id must be an ObjectId').isMongoId();
|
||||||
|
req.checkBody('challengeType', 'must be a number').isNumber();
|
||||||
|
req.checkBody('solution', 'solution must be a URL').isURL();
|
||||||
|
|
||||||
|
const errors = req.validationErrors(true);
|
||||||
|
|
||||||
|
if (errors) {
|
||||||
|
if (type === 'json') {
|
||||||
|
return res.status(403).send({ errors });
|
||||||
}
|
}
|
||||||
|
log('errors', errors);
|
||||||
const { user, body = {} } = req;
|
return res.sendStatus(403);
|
||||||
|
|
||||||
const completedChallenge = _.pick(body, ['id', 'solution']);
|
|
||||||
completedChallenge.completedDate = Date.now();
|
|
||||||
|
|
||||||
return user
|
|
||||||
.getCompletedChallenges$()
|
|
||||||
.flatMap(() => {
|
|
||||||
const { alreadyCompleted, updateData } = buildUserUpdate(
|
|
||||||
user,
|
|
||||||
completedChallenge.id,
|
|
||||||
completedChallenge
|
|
||||||
);
|
|
||||||
|
|
||||||
const updatePromise = new Promise((resolve, reject) =>
|
|
||||||
user.updateAttributes(updateData, err => {
|
|
||||||
if (err) {
|
|
||||||
return reject(err);
|
|
||||||
}
|
|
||||||
return resolve();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return Observable.fromPromise(updatePromise).doOnNext(() => {
|
|
||||||
if (type === 'json') {
|
|
||||||
return res.send({
|
|
||||||
alreadyCompleted,
|
|
||||||
points: alreadyCompleted ? user.points : user.points + 1,
|
|
||||||
completedDate: completedChallenge.completedDate
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return res.status(200).send(true);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.subscribe(() => {}, next);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function redirectToCurrentChallenge(req, res, next) {
|
const { user, body = {} } = req;
|
||||||
|
|
||||||
|
const completedChallenge = pick(body, [
|
||||||
|
'id',
|
||||||
|
'solution',
|
||||||
|
'githubLink',
|
||||||
|
'challengeType',
|
||||||
|
'files'
|
||||||
|
]);
|
||||||
|
completedChallenge.completedDate = Date.now();
|
||||||
|
|
||||||
|
if (
|
||||||
|
!completedChallenge.solution ||
|
||||||
|
// only basejumps require github links
|
||||||
|
(completedChallenge.challengeType === 4 && !completedChallenge.githubLink)
|
||||||
|
) {
|
||||||
|
req.flash(
|
||||||
|
'danger',
|
||||||
|
"You haven't supplied the necessary URLs for us to inspect your work."
|
||||||
|
);
|
||||||
|
return res.sendStatus(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
return user
|
||||||
|
.getCompletedChallenges$()
|
||||||
|
.flatMap(() => {
|
||||||
|
const { alreadyCompleted, updateData } = buildUserUpdate(
|
||||||
|
user,
|
||||||
|
completedChallenge.id,
|
||||||
|
completedChallenge
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatePromise = new Promise((resolve, reject) =>
|
||||||
|
user.updateAttributes(updateData, err => {
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
return resolve();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return Observable.fromPromise(updatePromise).doOnNext(() => {
|
||||||
|
if (type === 'json') {
|
||||||
|
return res.send({
|
||||||
|
alreadyCompleted,
|
||||||
|
points: alreadyCompleted ? user.points : user.points + 1,
|
||||||
|
completedDate: completedChallenge.completedDate
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return res.status(200).send(true);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.subscribe(() => {}, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
function backendChallengeCompleted(req, res, next) {
|
||||||
|
const type = accepts(req).type('html', 'json', 'text');
|
||||||
|
req.checkBody('id', 'id must be an ObjectId').isMongoId();
|
||||||
|
req.checkBody('solution', 'solution must be a URL').isURL();
|
||||||
|
|
||||||
|
const errors = req.validationErrors(true);
|
||||||
|
|
||||||
|
if (errors) {
|
||||||
|
if (type === 'json') {
|
||||||
|
return res.status(403).send({ errors });
|
||||||
|
}
|
||||||
|
log('errors', errors);
|
||||||
|
return res.sendStatus(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { user, body = {} } = req;
|
||||||
|
|
||||||
|
const completedChallenge = pick(body, ['id', 'solution']);
|
||||||
|
completedChallenge.completedDate = Date.now();
|
||||||
|
|
||||||
|
return user
|
||||||
|
.getCompletedChallenges$()
|
||||||
|
.flatMap(() => {
|
||||||
|
const { alreadyCompleted, updateData } = buildUserUpdate(
|
||||||
|
user,
|
||||||
|
completedChallenge.id,
|
||||||
|
completedChallenge
|
||||||
|
);
|
||||||
|
|
||||||
|
const updatePromise = new Promise((resolve, reject) =>
|
||||||
|
user.updateAttributes(updateData, err => {
|
||||||
|
if (err) {
|
||||||
|
return reject(err);
|
||||||
|
}
|
||||||
|
return resolve();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return Observable.fromPromise(updatePromise).doOnNext(() => {
|
||||||
|
if (type === 'json') {
|
||||||
|
return res.send({
|
||||||
|
alreadyCompleted,
|
||||||
|
points: alreadyCompleted ? user.points : user.points + 1,
|
||||||
|
completedDate: completedChallenge.completedDate
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return res.status(200).send(true);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.subscribe(() => {}, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createRedirectToCurrentChallenge(
|
||||||
|
challengeUrlResolver,
|
||||||
|
{ _homeLocation = homeLocation, _learnUrl = learnURL } = {}
|
||||||
|
) {
|
||||||
|
return async function redirectToCurrentChallenge(req, res, next) {
|
||||||
const { user } = req;
|
const { user } = req;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return res.redirect(learnURL);
|
return res.redirect(_learnUrl);
|
||||||
}
|
}
|
||||||
const challengeId = user && user.currentChallengeId;
|
const challengeId = user && user.currentChallengeId;
|
||||||
log(req.user.username);
|
|
||||||
log(challengeId);
|
|
||||||
const challengeUrl = await challengeUrlResolver(challengeId).catch(next);
|
const challengeUrl = await challengeUrlResolver(challengeId).catch(next);
|
||||||
log(challengeUrl);
|
|
||||||
if (challengeUrl === '/learn') {
|
if (challengeUrl === '/learn') {
|
||||||
// this should normally not be hit if database is properly seeded
|
// this should normally not be hit if database is properly seeded
|
||||||
throw new Error(dedent`
|
throw new Error(dedent`
|
||||||
Attempted to find the url for ${challengeId}'
|
Attempted to find the url for ${challengeId || 'Unknown ID'}'
|
||||||
but came up empty.
|
but came up empty.
|
||||||
db may not be properly seeded.
|
db may not be properly seeded.
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
return res.redirect(`${homeLocation}${challengeUrl}`);
|
return res.redirect(`${_homeLocation}${challengeUrl}`);
|
||||||
}
|
};
|
||||||
|
}
|
||||||
function redirectToLearn(req, res) {
|
|
||||||
const maybeChallenge = _.last(req.path.split('/'));
|
function redirectToLearn(req, res) {
|
||||||
if (maybeChallenge in pathMigrations) {
|
const maybeChallenge = last(req.path.split('/'));
|
||||||
const redirectPath = pathMigrations[maybeChallenge];
|
if (maybeChallenge in pathMigrations) {
|
||||||
return res.status(302).redirect(`${learnURL}${redirectPath}`);
|
const redirectPath = pathMigrations[maybeChallenge];
|
||||||
}
|
return res.status(302).redirect(`${learnURL}${redirectPath}`);
|
||||||
return res.status(302).redirect(learnURL);
|
}
|
||||||
}
|
return res.status(302).redirect(learnURL);
|
||||||
done();
|
|
||||||
}
|
}
|
||||||
|
195
api-server/server/boot_tests/challenge.test.js
Normal file
195
api-server/server/boot_tests/challenge.test.js
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
/* global describe xdescribe it expect */
|
||||||
|
import { isEqual } from 'lodash';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
import { mockReq, mockRes } from 'sinon-express-mock';
|
||||||
|
|
||||||
|
import {
|
||||||
|
buildChallengeUrl,
|
||||||
|
createChallengeUrlResolver,
|
||||||
|
createRedirectToCurrentChallenge,
|
||||||
|
getFirstChallenge
|
||||||
|
} from '../boot/challenge';
|
||||||
|
|
||||||
|
const firstChallengeUrl = '/learn/the/first/challenge';
|
||||||
|
const requestedChallengeUrl = '/learn/my/actual/challenge';
|
||||||
|
const mockChallenge = {
|
||||||
|
id: '123abc',
|
||||||
|
block: 'actual',
|
||||||
|
superBlock: 'my',
|
||||||
|
dashedName: 'challenge'
|
||||||
|
};
|
||||||
|
const mockFirstChallenge = {
|
||||||
|
id: '456def',
|
||||||
|
block: 'first',
|
||||||
|
superBlock: 'the',
|
||||||
|
dashedName: 'challenge'
|
||||||
|
};
|
||||||
|
const mockUser = {
|
||||||
|
username: 'camperbot',
|
||||||
|
currentChallengeId: '123abc'
|
||||||
|
};
|
||||||
|
const mockApp = {
|
||||||
|
models: {
|
||||||
|
Challenge: {
|
||||||
|
find() {
|
||||||
|
return firstChallengeUrl;
|
||||||
|
},
|
||||||
|
findById(id, cb) {
|
||||||
|
return id === mockChallenge.id
|
||||||
|
? cb(null, mockChallenge)
|
||||||
|
: cb(new Error('challenge not found'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const mockGetFirstChallenge = () => firstChallengeUrl;
|
||||||
|
const firstChallengeQuery = {
|
||||||
|
where: { challengeOrder: 0, superOrder: 1, order: 0 }
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('boot/challenge', () => {
|
||||||
|
xdescribe('backendChallengeCompleted');
|
||||||
|
|
||||||
|
xdescribe('buildUserUpdate');
|
||||||
|
|
||||||
|
describe('buildChallengeUrl', () => {
|
||||||
|
it('resolves the correct Url for the provided challenge', () => {
|
||||||
|
const result = buildChallengeUrl(mockChallenge);
|
||||||
|
|
||||||
|
expect(result).toEqual(requestedChallengeUrl);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can handle non-url-complient challenge names', () => {
|
||||||
|
const challenge = { ...mockChallenge, superBlock: 'my awesome' };
|
||||||
|
const expected = '/learn/my-awesome/actual/challenge';
|
||||||
|
const result = buildChallengeUrl(challenge);
|
||||||
|
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('challengeUrlResolver', () => {
|
||||||
|
it('resolves to the first challenge url by default', async () => {
|
||||||
|
const challengeUrlResolver = await createChallengeUrlResolver(mockApp, {
|
||||||
|
_getFirstChallenge: mockGetFirstChallenge
|
||||||
|
});
|
||||||
|
|
||||||
|
return challengeUrlResolver().then(url => {
|
||||||
|
expect(url).toEqual(firstChallengeUrl);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the first challenge url if the provided id does not relate to a challenge', async () => {
|
||||||
|
const challengeUrlResolver = await createChallengeUrlResolver(mockApp, {
|
||||||
|
_getFirstChallenge: mockGetFirstChallenge
|
||||||
|
});
|
||||||
|
|
||||||
|
return challengeUrlResolver('not-a-real-challenge').then(url => {
|
||||||
|
expect(url).toEqual(firstChallengeUrl);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves the correct url for the requested challenge', async () => {
|
||||||
|
const challengeUrlResolver = await createChallengeUrlResolver(mockApp, {
|
||||||
|
_getFirstChallenge: mockGetFirstChallenge
|
||||||
|
});
|
||||||
|
|
||||||
|
return challengeUrlResolver('123abc').then(url => {
|
||||||
|
expect(url).toEqual(requestedChallengeUrl);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
xdescribe('completedChallenge');
|
||||||
|
|
||||||
|
describe('getFirstChallenge', () => {
|
||||||
|
const createMockChallengeModel = success =>
|
||||||
|
success
|
||||||
|
? {
|
||||||
|
findOne(query, cb) {
|
||||||
|
return isEqual(query, firstChallengeQuery)
|
||||||
|
? cb(null, mockFirstChallenge)
|
||||||
|
: cb(new Error('no challenge found'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
findOne(_, cb) {
|
||||||
|
return cb(new Error('no challenge found'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
it('returns the correct challenge url from the model', async () => {
|
||||||
|
const result = await getFirstChallenge(createMockChallengeModel(true));
|
||||||
|
|
||||||
|
expect(result).toEqual(firstChallengeUrl);
|
||||||
|
});
|
||||||
|
it('returns the learn base if no challenges found', async () => {
|
||||||
|
const result = await getFirstChallenge(createMockChallengeModel(false));
|
||||||
|
|
||||||
|
expect(result).toEqual('/learn');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
xdescribe('modernChallengeCompleted');
|
||||||
|
|
||||||
|
xdescribe('projectcompleted');
|
||||||
|
|
||||||
|
describe('redirectToCurrentChallenge', () => {
|
||||||
|
const mockHomeLocation = 'https://www.example.com';
|
||||||
|
const mockLearnUrl = `${mockHomeLocation}/learn`;
|
||||||
|
|
||||||
|
it('redircts to the learn base url for non-users', async done => {
|
||||||
|
const redirectToCurrentChallenge = createRedirectToCurrentChallenge(
|
||||||
|
() => {},
|
||||||
|
{ _homeLocation: mockHomeLocation, _learnUrl: mockLearnUrl }
|
||||||
|
);
|
||||||
|
const req = mockReq();
|
||||||
|
const res = mockRes();
|
||||||
|
const next = sinon.spy();
|
||||||
|
await redirectToCurrentChallenge(req, res, next);
|
||||||
|
|
||||||
|
expect(res.redirect.calledWith(mockLearnUrl));
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redirects to the url provided by the challengeUrlResolver', async done => {
|
||||||
|
const challengeUrlResolver = await createChallengeUrlResolver(mockApp, {
|
||||||
|
_getFirstChallenge: mockGetFirstChallenge
|
||||||
|
});
|
||||||
|
const expectedUrl = `${mockHomeLocation}${requestedChallengeUrl}`;
|
||||||
|
const redirectToCurrentChallenge = createRedirectToCurrentChallenge(
|
||||||
|
challengeUrlResolver,
|
||||||
|
{ _homeLocation: mockHomeLocation, _learnUrl: mockLearnUrl }
|
||||||
|
);
|
||||||
|
const req = mockReq({
|
||||||
|
user: mockUser
|
||||||
|
});
|
||||||
|
const res = mockRes();
|
||||||
|
const next = sinon.spy();
|
||||||
|
await redirectToCurrentChallenge(req, res, next);
|
||||||
|
|
||||||
|
expect(res.redirect.calledWith(expectedUrl)).toBe(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('redirects to the first challenge for users without a currentChallengeId', async done => {
|
||||||
|
const challengeUrlResolver = await createChallengeUrlResolver(mockApp, {
|
||||||
|
_getFirstChallenge: mockGetFirstChallenge
|
||||||
|
});
|
||||||
|
const redirectToCurrentChallenge = createRedirectToCurrentChallenge(
|
||||||
|
challengeUrlResolver,
|
||||||
|
{ _homeLocation: mockHomeLocation, _learnUrl: mockLearnUrl }
|
||||||
|
);
|
||||||
|
const req = mockReq({
|
||||||
|
user: { ...mockUser, currentChallengeId: '' }
|
||||||
|
});
|
||||||
|
const res = mockRes();
|
||||||
|
const next = sinon.spy();
|
||||||
|
await redirectToCurrentChallenge(req, res, next);
|
||||||
|
const expectedUrl = `${mockHomeLocation}${firstChallengeUrl}`;
|
||||||
|
expect(res.redirect.calledWith(expectedUrl)).toBe(true);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
xdescribe('redirectToLearn');
|
||||||
|
});
|
Reference in New Issue
Block a user