diff --git a/.travis.yml b/.travis.yml index 90e7c628639..f5d8656a9f0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,14 +18,12 @@ before_install: install: npm ci before_script: - - git config --global user.email team+camperbot@freeCodeCamp.org - - git config --global user.name "CamperBot" + - npm run ensure-env jobs: include: - stage: Lint javaScript script: - - npm run ensure-env - npm run lint - stage: Unit and Integration tests diff --git a/api-server/server/boot/challenge.js b/api-server/server/boot/challenge.js index 7eb5f61cf41..edbca99d519 100644 --- a/api-server/server/boot/challenge.js +++ b/api-server/server/boot/challenge.js @@ -5,7 +5,7 @@ * */ import { Observable } from 'rx'; -import _ from 'lodash'; +import { isEmpty, pick, omit, find, uniqBy, last } from 'lodash'; import debug from 'debug'; import accepts from 'accepts'; import dedent from 'dedent'; @@ -19,129 +19,14 @@ import { fixCompletedChallengeItem } from '../../common/utils'; 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) { const send200toNonUser = ifNoUserSend(true); const api = app.loopback.Router(); const router = app.loopback.Router(); const challengeUrlResolver = await createChallengeUrlResolver(app); + const redirectToCurrentChallenge = createRedirectToCurrentChallenge( + challengeUrlResolver + ); api.post( '/modern-challenge-completed', @@ -183,259 +68,381 @@ export default async function bootChallenge(app, done) { app.use('/external', api); app.use('/internal', api); app.use(router); + done(); +} +const learnURL = `${homeLocation}/learn`; - function modernChallengeCompleted(req, res, next) { - const type = accepts(req).type('html', 'json', 'text'); - req.checkBody('id', 'id must be an ObjectId').isMongoId(); +const jsProjects = [ + 'aaa48de84e1ecc7c742e1124', + 'a7f4d8f2483413a6ce226cac', + '56533eb9ac21ba0edf2244e2', + 'aff0395860f5d3034dc0bfc9', + 'aa2e6f85cab2ab736c9a9b24' +]; - const errors = req.validationErrors(true); - if (errors) { - if (type === 'json') { - return res.status(403).send({ errors }); - } +function buildUserUpdate(user, challengeId, _completedChallenge, timezone) { + const { files } = _completedChallenge; + let completedChallenge = {}; - log('errors', errors); - return res.sendStatus(403); - } + 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 user = req.user; - return user - .getCompletedChallenges$() - .flatMap(() => { - const completedDate = Date.now(); - const { id, files } = req.body; + const oldChallenge = find( + completedChallenges, + ({ id }) => challengeId === id + ); + const alreadyCompleted = !!oldChallenge; - const { alreadyCompleted, updateData } = buildUserUpdate(user, id, { - id, - files, - completedDate - }); - - 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); + if (alreadyCompleted) { + finalChallenge = { + ...completedChallenge, + completedDate: oldChallenge.completedDate + }; + } else { + updateData.$push = { + ...updateData.$push, + progressTimestamps: Date.now() + }; + finalChallenge = { + ...completedChallenge + }; } - 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); + updateData.$set = { + completedChallenges: uniqBy( + [finalChallenge, ...completedChallenges.map(fixCompletedChallengeItem)], + '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) { - if (type === 'json') { - return res.status(403).send({ errors }); +export function buildChallengeUrl(challenge) { + const { superBlock, block, dashedName } = challenge; + 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); - return res.sendStatus(403); +export async function createChallengeUrlResolver( + 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 - .getCompletedChallenges$() - .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); + log('errors', errors); + return res.sendStatus(403); } - 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 user = req.user; + return user + .getCompletedChallenges$() + .flatMap(() => { + 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) { - 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', - '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." + 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 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 - .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); + log('errors', errors); + return res.sendStatus(403); } - 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(); + return user + .getCompletedChallenges$() + .flatMap(() => { + 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) { - if (type === 'json') { - return res.status(403).send({ errors }); - } - log('errors', errors); - return res.sendStatus(403); + 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 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 }); } - - 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); + log('errors', errors); + return res.sendStatus(403); } - 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; if (!user) { - return res.redirect(learnURL); + return res.redirect(_learnUrl); } const challengeId = user && user.currentChallengeId; - log(req.user.username); - log(challengeId); const challengeUrl = await challengeUrlResolver(challengeId).catch(next); - log(challengeUrl); if (challengeUrl === '/learn') { // this should normally not be hit if database is properly seeded throw new Error(dedent` - Attempted to find the url for ${challengeId}' + Attempted to find the url for ${challengeId || 'Unknown ID'}' but came up empty. db may not be properly seeded. `); } - return res.redirect(`${homeLocation}${challengeUrl}`); - } - - function redirectToLearn(req, res) { - const maybeChallenge = _.last(req.path.split('/')); - if (maybeChallenge in pathMigrations) { - const redirectPath = pathMigrations[maybeChallenge]; - return res.status(302).redirect(`${learnURL}${redirectPath}`); - } - return res.status(302).redirect(learnURL); - } - done(); + return res.redirect(`${_homeLocation}${challengeUrl}`); + }; +} + +function redirectToLearn(req, res) { + const maybeChallenge = last(req.path.split('/')); + if (maybeChallenge in pathMigrations) { + const redirectPath = pathMigrations[maybeChallenge]; + return res.status(302).redirect(`${learnURL}${redirectPath}`); + } + return res.status(302).redirect(learnURL); } diff --git a/api-server/server/boot_tests/challenge.test.js b/api-server/server/boot_tests/challenge.test.js new file mode 100644 index 00000000000..3b5c4193533 --- /dev/null +++ b/api-server/server/boot_tests/challenge.test.js @@ -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'); +});