API v3 [WIP] (#6144)

* Fixed more tests

* Added tags into user service

* Added api-v3 auth urls

* v3: fix package.json

* v3: fix package.json

* Fixed auth tests. Updated Authctrl response

* v3: remove newrelic config file in favour of env variables

* v3: upgrade some deps

* switch from Q to Bluebird

* v3 fix tests with deferred

* Removed extra consoles.log. Changed data.data to res.data

* v3 fix tests and use coroutines instead of regenerator

* v3: fix tests

* v3: do not await a non promise

* v3: q -> bluebird

* Changed id param for registration response

* Updated party query and create

* Ensured login callback happens after user sync

* Add challenges to groups. Fixed isMemberOfGuild check

* Updated party and group tests

* Fixed cron test

* return user.id and send analytics event before changing page

* fix trailing spaces

* disable redirects

* Api v3 party tavern fixes (#7191)

* Added check if user is in party before query

* Cached party query. Prevented party request when user is not in party. Updated Party create with no invites

* Update tavern ctrl to use new promise

* v3: misc fixes

* Api v3 task fixes (#7193)

* Update task view to use _id

* Added try catch to user service ops calls

* v3 client: saving after syncing is complete

* Fixed test broken by part sync change (#7195)

* v3: fix todo scoring and try to fix production testing problem

* revert changes to mongoose config

* mongoose: increase keepAlive

* test mongoose fix

* fix: Only apply captureStackTrace if it exists on the error object

* v3: fix reminders with no startDate

* mongoose: use options

* chore(): rename website/src -> website/server and website/public -> website/client (#7199)

* v3 fix GET /groups: return an error only if an invalid type is supplied not when there are 0 results (#7203)

* [API v3] Fix calls to user.ops and deleting tags (#7204)

* v3: fixes calls to user.ops from views and deleting tags

* v3: fix tests that use user._statsComputed

* Api v3 fixes continued (#7205)

* Added timzeone offset back

* Added APIToken back to settings page

* Fixed fetch recent messages for party

* Fixed returning group description

* Fixed check if user is member of challenge

* Fixed party members appearing in header

* Updated get myGroups param to include public groups. Fixed isMemberOf group

* Fixed hourglass purchase

* Fixed challenge addding tasks on first creating

* Updated tests to accomidate new changes

* fix: Correct checklist on client

Closes #7207

* fix: Pin eslint to 2.9

* minor improvements to cron code for clarity; fix inaccurate comments; add TODOs for rest-in-inn actions

* fix: Add missing type param to equip call

closes #7212

* rename and reword pubChalsMinPrize to reflect that it's only for Tavern challenges

* allows players to send gems to each other; other minor related changes - fixes https://github.com/HabitRPG/habitrpg/issues/7227

* fix tests for /members/transfer-gems

* fix: Set gems sent notification as translatable string

* chore: Remove unusued variable

* fix: Remove requirement on message paramter in transfer-gems

* add a missing variable declaration

* chore: clarify comments on cron code

* fix: Correct client request from habitrpg -> tavern

* update apidoc URL in package.json

Closes #7222

* Fixed start party by invites

* Updated spell casting to v3

* Fixed adding and removing tags on tasks

* Fixed page reload on settings change

* Fixed battle monsters with friends button

* Loaded completed todos when done is clicked

* chore: Reinstate floating version number for eslint

babel-eslint regression fixed

* Fixed reload tests

* change "an user" to "a user" in comments and text (no code changes) (#7257)

* fix: Alert user that drops were recieved

* remove userServices.js from karma.conf - it's been moved to website/client/js/services

* feat: Create debug update user route

* fix: Correct set cron debug function

* feat: Add make admin button to debug menu

* lint: Add missing semicolons in test

* fix: Temporarilly comment out udpate user debug route

* v3: fix _tmp for crit and streakBonus

* v3: execute all actions when leaving a solo party

* v3 client: fix group not found when leaving party

* v3 migration: fix challenge prize

* v3 cron: only save modified tasks

* v3: add CHALLENGE_TASK_NOT_FOUND to valid broken reasons

* v3: fix tasks chart

* v3 client: fix ability to leave challenge

* v3 client: fix filtering by tag and correctly show tag tooltip

* v3 common: fix tags tests

* v3 client: support unlinking not found challenges tasks

* v3: disable Bluebird warning for missing return, fixes #7269

* feat: Separate out update-user into set-cron and make-admin debug routes

* chore: Disable make admin debug route for v3 prod testing

* v3: misc fixes

* v3: misc fixes

* v3: fix adding multiple tasks

* Fixed join/leave button updates

* Queried only user groups to be available when creating challenges

* Fixed bulk add tasks to challenge

* Synced challenge tasks after leave and join.

* Fixed default selected group

* Fixed challenge member info. Fixed challenge winner selection

* Fixed deleting challenge tasks

* Fixed particiapting filter

* v3 client: fix casting spells

* v3: do not log sensitive data

* v3: always save user when casting spell

* v3: always save user when casting spell

* v3: more fixes for spells

* fix typos and missing information in apidocs - fixes https://github.com/HabitRPG/habitrpg/issues/7277 (#7282)

* v3: add TODO for client side spells

* feat: Add modify inventory debug menu

* Fixed viewing user progress on challenge

* Updated tests

* fix: Fix quest progress button

* fix incorrect Armoire test; remove unneeded param details from apidocs; disambiguate health potion

* v3: fix stealth casting

* v3: fix tasks saving and selection for rebirth reroll and reset (server-only)

* v3: fix auto allocation

* v3 client: misc fixes

* rename buyPotion and buy-potion to buyHealthPotion and buy-health-potion; fix apidoc param error

* Added delete for saved challenge task

* Fixed member modal on front page

* adjust text in apidocs for errors / clarity / consistency / standard terminology (no code changes) (#7298)

* fix bug in Rebirth test, add new tests, adjust apidocs (#7293)

* Updated task model to allow setting streak (#7306)

* fix: Correct missing * in apidoc comments

* Api v3 challenge fixes (#7287)

* Fixed join/leave button updates

* Queried only user groups to be available when creating challenges

* Fixed bulk add tasks to challenge

* Synced challenge tasks after leave and join.

* Fixed default selected group

* Fixed challenge member info. Fixed challenge winner selection

* Fixed deleting challenge tasks

* Fixed particiapting filter

* Fixed viewing user progress on challenge

* Updated tests

* Added delete for saved challenge task

* v3: fix sorting

* [API v3] add CRON_SAFE_MODE (#7286)

* add CRON_SAFE_MODE to example config file, fix some bugs, add an unrelated low-priority TODO

* create CRON_SAFE_MODE to disable parts of cron for use after extended outage - fixes https://github.com/HabitRPG/habitrpg/issues/7161

* fix a bug with CRON_SAFE_MODE, remove duplicated code, remove completed TODO comment

* fix check for CRON_SAFE_MODE

* v3 client: fix typo

* adjust debug menu Modify Inventory: hungrier pets, fewer Special items, "Hide" buttons

* completed To-Dos: return the 30 most recent instead of 30 oldest (#7318)

* v3 migration: fix createdAt date

* adjust locales text, key names, and files for Rebirth, Reset, and Fortify / ReRoll for consistency with existing strings (#7321)

* v3: fix unlinking multiple tasks

* v3 fix releasing pets

* v3: fix authenticating with apiUrl

* v3: fix typo

* v3 fix client tests for unlinking

* v3 client: do not show start quest button when quest is active

* v3 client: fix ability to send cards

* v3 client: fix misc challenge issues

* v3: fix notifications

* v3 client: more user friendly errors

* v3 client: only load completed todos once

* v3 client: fix tests

* v3: move TAVERN_ID to common code

* fix: Provide default type and text for new task creation in score route

* fix: Provide default history [] for habit in score route

* fix: Add _legacyId prop to tasks to support non-uuid identifiers

* chore: Change v3 migration to use _legacyId instead of legacyId

* fix: check for _legacyId in tasks if id does not exist

* refactor: Extract out finding task by id or _legacyId into a function

* Api v3 party quest fixes (#7341)

* Fix display of add challenge message when group challenges are empty

* Fixed forced quest start to update quest without reload

* Fixed needing to reload when accepting party invite

* Fix group leave and join reload

* Fixed leave current party and join another

* Updated party tests

* v3 client: remove console.log statement

* v3: misc fixes

* v3 client: fix predicatbale random

* v3: info about API v3

* v3: update footer with links to developer resources

* v3: support party invitation from email

* v3 client: fix chat flagging

* fix: Correct get tasks route to properly get todos (#7349)

* move locales strings from api-v3.json to other locales files (#7347)

* move locales strings from api-v3.json: authentication strings -> front.json

* move locales strings from api-v3.json: authentication strings -> tasks.json

* move locales strings from api-v3.json: authentication strings -> groups.json

* move locales strings from api-v3.json: authentication strings -> challenge.json

* move locales strings from api-v3.json: authentication strings -> groups.json (again)

* move locales strings from api-v3.json: authentication strings -> quests.json

* move locales strings from api-v3.json: authentication strings -> subscriber.json

* move locales strings from api-v3.json: authentication strings -> spells.json

* move locales strings from api-v3.json: authentication strings -> character.json

* move locales strings from api-v3.json: authentication strings -> groups.json (PMs)

* move locales strings from api-v3.json: authentication strings -> npc.json

* move locales strings from api-v3.json: authentication strings -> pets.json

* move locales strings from api-v3.json: authentication strings -> miscellaneous

* move locales strings from api-v3.json: authentication strings -> contrib.json and settings.json

* move locales strings from api-v3.json: delete unused string (invalidTasksOwner), delete api-v3.json, whitespace cleanup

* v3 client: fix sticky header

* v3: remove unused code

* v3 client: correctly redirect after inviting

* Removed v2 calls from views (#7351)

* v3: fix tests for challenge export

* v3: fallbackto authWithHeaders if wuthWithSession or authWithUrl fails

* Added force cache update when fetching new messages (#7360)

* v3: fetch whole user when booting from group tto avoid issues with pre save hook expecting all data

* v3: misc fixes for payments

* v3: limit fields of challenge tasks that can be updated

* fix(tests): never connect to NODE_DB_URI for tests

* Added new route for setting last cron and updated front end

* v3: fix iap url

* v3: fix build and ios IAP

* Changed route to user set custom day start

* v3: iap accessible under /api/v3, fixes to spells and groups invitations

* v3: correctly use v3 routes in client

* remove XP, GP when unticking a Daily with a completed checklist - fixes https://github.com/HabitRPG/habitrpg/issues/7246

* use natural language for error message about skills on challenge tasks (#7336), fix other gramatical error

* Updated ui when user rejects a guild invite (#7368)

* feat: complete custom day start route

Closes #7363

* fix: Correct spelling of healAll skill

fix: Correct sprite name of healAll skill

* fix: Change all instances of spookDust -> spookySparkles

* add dateCreated to all tasks; add empty challenge object to tasks that don't have one (#7386)

* add plumilla to artists for Tangle Tree in Bailey message

* Fixed quest drop modal (#7377)

* Fixed quest drop modal

* Fixed broken party test

* [API v3] Maintenance Mode (#7367)

* WIP(maintenance): maintenance

* WIP(maintenance): working locale features

* fix(maintenance): don't translate info page target

* WIP(maintenance): start adding info page

* fix(maintenance): linting

* feat: Add container to maintenance info page

* fix(maintenance): add config.json edits
Also DRY variables for main vs info pages

* fix(maintenance): linting

* refactor(maintenance): further slim down variables

* refactor: Remove unnecessary variables

* fix: Correct string interpolation in maintenace view

* feat: Dynamically add time to maintenance pages

* maintenance mode: do not connect to mongodb

* fix(maintenance): clean up timezones etc.

* fix(maintenance): remove unneeded sprite

* Tavern party challenges invites fix (#7394)

* Added challenges and invitations to party

* Loaded tavern challenges

* Updated group and quest services tests

* v3: implement automatic syncing if user is not up to date

* Removed unnecessary fields when updating groups and challenges (#7395)

* v3: do not saved populated user

* v3: correctly return user subset

* Chained party promises together (#7396)

* v3: $w -> splitWhitespace

* use bluebird

* use babel polyfill

* migration: fix items

* update links for v3

* Updated shortname validation to support multiple browsers

* Docs changes (#7401)

* chore: Clarify transfer-gems documentation

* chore: Clarify api status route documentation

* chore: Mark webhooks as BETA

* Added tags update route. Added sort to user service (#7381)

* Added tags update route. Added sort to user service

* Change update tasks route to reorder tasks

* Fixed linting issue

* Changed params for reorder tags route

* Fixed not found tag and added test

* Added password confirmation when deleteing account (#7402)

* fix production logging

* feat(commit): push

* empty commit

* feat(maintenance): post-downtime news & awards (#7406)

* fix exporting avatar

* second attempt at fixing exporting avatar

* fix production logging

* s3: convert moment to date instance

* fix avatar sharing and caching (30 minutes)

* fix: Correct missing parameter

Closes #7433

* fix: Validate challenge shortname on server

* adjust text strings - fixes https://github.com/HabitRPG/habitrpg/issues/5631 and also Short Name -> Tag Name
This commit is contained in:
Matteo Pagliazzi
2016-05-23 13:58:31 +02:00
parent ef3a2fc286
commit 28f2e9c356
993 changed files with 44888 additions and 12883 deletions
@@ -0,0 +1,49 @@
/* eslint-disable global-require */
import {
generateRes,
generateReq,
generateNext,
} from '../../../../helpers/api-unit.helper';
import analyticsService from '../../../../../website/server/libs/api-v3/analyticsService';
import nconf from 'nconf';
import requireAgain from 'require-again';
describe('analytics middleware', () => {
let res, req, next;
let pathToAnalyticsMiddleware = '../../../../../website/server/middlewares/api-v3/analytics';
beforeEach(() => {
res = generateRes();
req = generateReq();
next = generateNext();
});
it('attaches analytics object res.locals', () => {
let attachAnalytics = requireAgain(pathToAnalyticsMiddleware);
attachAnalytics(req, res, next);
expect(res.analytics).to.exist;
});
it('attaches stubbed methods for non-prod environments', () => {
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(false);
let attachAnalytics = requireAgain(pathToAnalyticsMiddleware);
attachAnalytics(req, res, next);
expect(res.analytics.track).to.eql(analyticsService.mockAnalyticsService.track);
expect(res.analytics.trackPurchase).to.eql(analyticsService.mockAnalyticsService.trackPurchase);
});
it('attaches real methods for prod environments', () => {
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
let attachAnalytics = requireAgain(pathToAnalyticsMiddleware);
attachAnalytics(req, res, next);
expect(res.analytics.track).to.eql(analyticsService.track);
expect(res.analytics.trackPurchase).to.eql(analyticsService.trackPurchase);
});
});
+40
View File
@@ -0,0 +1,40 @@
/* eslint-disable global-require */
import {
generateRes,
generateReq,
generateNext,
} from '../../../../helpers/api-unit.helper';
import cors from '../../../../../website/server/middlewares/api-v3/cors';
describe('cors middleware', () => {
let res, req, next;
beforeEach(() => {
req = generateReq();
res = generateRes();
next = generateNext();
});
it('sets the correct headers', () => {
cors(req, res, next);
expect(res.set).to.have.been.calledWith({
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'OPTIONS,GET,POST,PUT,HEAD,DELETE',
'Access-Control-Allow-Headers': 'Content-Type,Accept,Content-Encoding,X-Requested-With,x-api-user,x-api-key',
});
expect(res.sendStatus).to.not.have.been.called;
expect(next).to.have.been.called.once;
});
it('responds immediately if method is OPTIONS', () => {
req.method = 'OPTIONS';
cors(req, res, next);
expect(res.set).to.have.been.calledWith({
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'OPTIONS,GET,POST,PUT,HEAD,DELETE',
'Access-Control-Allow-Headers': 'Content-Type,Accept,Content-Encoding,X-Requested-With,x-api-user,x-api-key',
});
expect(res.sendStatus).to.have.been.calledWith(200);
expect(next).to.not.have.been.called;
});
});
@@ -0,0 +1,177 @@
import {
generateRes,
generateReq,
generateNext,
generateTodo,
generateDaily,
} from '../../../../helpers/api-unit.helper';
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 { v4 as generateUUID } from 'uuid';
describe('cron middleware', () => {
let res, req, next;
let user;
beforeEach(() => {
res = generateRes();
req = generateReq();
next = generateNext();
user = new User({
auth: {
local: {
username: 'username',
lowerCaseUsername: 'username',
email: 'email@email.email',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
},
},
});
user._statsComputed = {
mp: 10,
maxMP: 100,
};
res.locals.user = user;
res.analytics = analyticsService;
});
it('calls next when user is not attached', () => {
res.locals.user = null;
cronMiddleware(req, res, next);
expect(next).to.be.calledOnce;
});
it('calls next when days have not been missed', () => {
cronMiddleware(req, res, next);
expect(next).to.be.calledOnce;
});
it('should clear todos older than 30 days for free users', async (done) => {
user.lastCron = moment(new Date()).subtract({days: 2});
let task = generateTodo(user);
task.dateCompleted = moment(new Date()).subtract({days: 31});
task.completed = true;
await task.save();
cronMiddleware(req, res, () => {
Tasks.Task.findOne({_id: task}, function (err, taskFound) {
expect(err).to.not.exist;
expect(taskFound).to.not.exist;
done();
});
});
});
it('should not clear todos older than 30 days for subscribed users', (done) => {
user.purchased.plan.customerId = 'subscribedId';
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
user.lastCron = moment(new Date()).subtract({days: 2});
let task = generateTodo(user);
task.dateCompleted = moment(new Date()).subtract({days: 31});
task.completed = true;
task.save();
cronMiddleware(req, res, () => {
Tasks.Task.findOne({_id: task}, function (err, taskFound) {
expect(err).to.not.exist;
expect(taskFound).to.exist;
done();
});
});
});
it('should clear todos older than 90 days for subscribed users', (done) => {
user.purchased.plan.customerId = 'subscribedId';
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
user.lastCron = moment(new Date()).subtract({days: 2});
let task = generateTodo(user);
task.dateCompleted = moment(new Date()).subtract({days: 91});
task.completed = true;
task.save();
cronMiddleware(req, res, () => {
Tasks.Task.findOne({_id: task}, function (err, taskFound) {
expect(err).to.not.exist;
expect(taskFound).to.not.exist;
done();
});
});
});
it('should call next is user was not modified after cron', (done) => {
let hpBefore = user.stats.hp;
user.lastCron = moment(new Date()).subtract({days: 2});
user.save().then(function () {
cronMiddleware(req, res, function () {
expect(hpBefore).to.equal(user.stats.hp);
done();
});
});
});
it('does damage for missing dailies', (done) => {
let hpBefore = user.stats.hp;
user.lastCron = moment(new Date()).subtract({days: 2});
let daily = generateDaily(user);
daily.startDate = moment(new Date()).subtract({days: 2});
daily.save();
cronMiddleware(req, res, () => {
expect(user.stats.hp).to.be.lessThan(hpBefore);
done();
});
});
it('updates tasks', (done) => {
user.lastCron = moment(new Date()).subtract({days: 2});
let todo = generateTodo(user);
let todoValueBefore = todo.value;
cronMiddleware(req, res, () => {
Tasks.Task.findOne({_id: todo._id}, function (err, todoFound) {
expect(err).to.not.exist;
expect(todoFound.value).to.be.lessThan(todoValueBefore);
done();
});
});
});
it('applies quest progress', async (done) => {
let hpBefore = user.stats.hp;
user.lastCron = moment(new Date()).subtract({days: 2});
let daily = generateDaily(user);
daily.startDate = moment(new Date()).subtract({days: 2});
daily.save();
let questKey = 'dilatory';
user.party.quest.key = questKey;
let party = new Group({
type: 'party',
name: generateUUID(),
leader: user._id,
});
party.quest.members[user._id] = true;
party.quest.key = questKey;
await party.save();
user.party._id = party._id;
await user.save();
party.startQuest(user);
cronMiddleware(req, res, () => {
expect(user.stats.hp).to.be.lessThan(hpBefore);
done();
});
});
});
@@ -0,0 +1,57 @@
/* eslint-disable global-require */
import {
generateRes,
generateReq,
generateNext,
} from '../../../../helpers/api-unit.helper';
import i18n from '../../../../../common/script/i18n';
import { ensureAdmin, ensureSudo } from '../../../../../website/server/middlewares/api-v3/ensureAccessRight';
import { NotAuthorized } from '../../../../../website/server/libs/api-v3/errors';
describe('ensure access middlewares', () => {
let res, req, next;
beforeEach(() => {
res = generateRes();
req = generateReq();
next = generateNext();
});
context('ensure admin', () => {
it('returns not authorized when user is not an admin', () => {
res.locals = {user: {contributor: {admin: false}}};
ensureAdmin(req, res, next);
expect(next).to.be.calledWith(new NotAuthorized(i18n.t('noAdminAccess')));
});
it('passes when user is an admin', () => {
res.locals = {user: {contributor: {admin: true}}};
ensureAdmin(req, res, next);
expect(next).to.be.calledOnce;
expect(next.args[0]).to.be.empty;
});
});
context('ensure sudo', () => {
it('returns not authorized when user is not a sudo user', () => {
res.locals = {user: {contributor: {sudo: false}}};
ensureSudo(req, res, next);
expect(next).to.be.calledWith(new NotAuthorized(i18n.t('noSudoAccess')));
});
it('passes when user is a sudo user', () => {
res.locals = {user: {contributor: {sudo: true}}};
ensureSudo(req, res, next);
expect(next).to.be.calledOnce;
expect(next.args[0]).to.be.empty;
});
});
});
@@ -0,0 +1,36 @@
/* eslint-disable global-require */
import {
generateRes,
generateReq,
generateNext,
} from '../../../../helpers/api-unit.helper';
import ensureDevelpmentMode from '../../../../../website/server/middlewares/api-v3/ensureDevelpmentMode';
import { NotFound } from '../../../../../website/server/libs/api-v3/errors';
import nconf from 'nconf';
describe('developmentMode middleware', () => {
let res, req, next;
beforeEach(() => {
res = generateRes();
req = generateReq();
next = generateNext();
});
it('returns not found when in production mode', () => {
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
ensureDevelpmentMode(req, res, next);
expect(next).to.be.calledWith(new NotFound());
});
it('passes when not in production', () => {
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(false);
ensureDevelpmentMode(req, res, next);
expect(next).to.be.calledOnce;
expect(next.args[0]).to.be.empty;
});
});
@@ -0,0 +1,173 @@
import {
generateRes,
generateReq,
generateNext,
} from '../../../../helpers/api-unit.helper';
import errorHandler from '../../../../../website/server/middlewares/api-v3/errorHandler';
import responseMiddleware from '../../../../../website/server/middlewares/api-v3/response';
import {
getUserLanguage,
attachTranslateFunction,
} from '../../../../../website/server/middlewares/api-v3/language';
import { BadRequest } from '../../../../../website/server/libs/api-v3/errors';
import logger from '../../../../../website/server/libs/api-v3/logger';
describe('errorHandler', () => {
let res, req, next;
beforeEach(() => {
res = generateRes();
req = generateReq();
next = generateNext();
responseMiddleware(req, res, next);
getUserLanguage(req, res, next);
attachTranslateFunction(req, res, next);
sandbox.stub(logger, 'error');
});
it('sends internal server error if error is not a CustomError and is not identified', () => {
let error = new Error();
errorHandler(error, req, res, next);
expect(res.status).to.be.calledOnce;
expect(res.json).to.be.calledOnce;
expect(res.status).to.be.calledWith(500);
expect(res.json).to.be.calledWith({
success: false,
error: 'InternalServerError',
message: 'An unexpected error occurred.',
});
});
it('identifies errors with statusCode property and format them correctly', () => {
let error = new Error('Error message');
error.statusCode = 400;
errorHandler(error, req, res, next);
expect(res.status).to.be.calledOnce;
expect(res.json).to.be.calledOnce;
expect(res.status).to.be.calledWith(400);
expect(res.json).to.be.calledWith({
success: false,
error: 'Error',
message: 'Error message',
});
});
it('doesn\'t leak info about 500 errors', () => {
let error = new Error('Some secret error message');
error.statusCode = 500;
errorHandler(error, req, res, next);
expect(res.status).to.be.calledOnce;
expect(res.json).to.be.calledOnce;
expect(res.status).to.be.calledWith(500);
expect(res.json).to.be.calledWith({
success: false,
error: 'InternalServerError',
message: 'An unexpected error occurred.',
});
});
it('sends CustomError', () => {
let error = new BadRequest();
errorHandler(error, req, res, next);
expect(res.status).to.be.calledOnce;
expect(res.json).to.be.calledOnce;
expect(res.status).to.be.calledWith(400);
expect(res.json).to.be.calledWith({
success: false,
error: 'BadRequest',
message: 'Bad request.',
});
});
it('handle http-errors errors', () => {
let error = new Error('custom message');
error.statusCode = 422;
errorHandler(error, req, res, next);
expect(res.status).to.be.calledOnce;
expect(res.json).to.be.calledOnce;
expect(res.status).to.be.calledWith(error.statusCode);
expect(res.json).to.be.calledWith({
success: false,
error: error.name,
message: error.message,
});
});
it('handle express-validator errors', () => {
let error = [{param: 'param', msg: 'invalid param', value: 123}];
errorHandler(error, req, res, next);
expect(res.status).to.be.calledOnce;
expect(res.json).to.be.calledOnce;
expect(res.status).to.be.calledWith(400);
expect(res.json).to.be.calledWith({
success: false,
error: 'BadRequest',
message: 'Invalid request parameters.',
errors: [
{ param: error[0].param, value: error[0].value, message: error[0].msg },
],
});
});
it('handle Mongoose Validation errors', () => {
let error = new Error('User validation failed.');
error.name = 'ValidationError';
error.errors = {
'auth.local.email': {
path: 'auth.local.email',
message: 'Invalid email.',
value: 'not an email',
},
};
errorHandler(error, req, res, next);
expect(res.status).to.be.calledOnce;
expect(res.json).to.be.calledOnce;
expect(res.status).to.be.calledWith(400);
expect(res.json).to.be.calledWith({
success: false,
error: 'BadRequest',
message: 'User validation failed.',
errors: [
{ path: 'auth.local.email', message: 'Invalid email.', value: 'not an email' },
],
});
});
it('logs error', () => {
let error = new BadRequest();
errorHandler(error, req, res, next);
expect(logger.error).to.be.calledOnce;
expect(logger.error).to.be.calledWithExactly(error, {
originalUrl: req.originalUrl,
headers: req.headers,
body: req.body,
});
});
});
@@ -0,0 +1,307 @@
import {
generateRes,
generateReq,
generateNext,
} from '../../../../helpers/api-unit.helper';
import {
getUserLanguage,
attachTranslateFunction,
} from '../../../../../website/server/middlewares/api-v3/language';
import common from '../../../../../common';
import Bluebird from 'bluebird';
import { model as User } from '../../../../../website/server/models/user';
const i18n = common.i18n;
describe('language middleware', () => {
describe('res.t', () => {
let res, req, next;
beforeEach(() => {
res = generateRes();
req = generateReq();
next = generateNext();
sinon.stub(i18n, 't');
});
afterEach(() => {
i18n.t.restore();
});
it('attaches t method to res', () => {
attachTranslateFunction(req, res, next);
expect(res.t).to.exist;
});
it('uses the language specified in req.language', () => {
req.language = 'de';
attachTranslateFunction(req, res, next);
res.t(1, 2);
expect(i18n.t).to.be.calledOnce;
expect(i18n.t).to.be.calledWith(1, 2);
});
});
describe('getUserLanguage', () => {
let res, req, next;
let checkResT = (resToCheck) => {
expect(resToCheck.t).to.be.a('function');
expect(resToCheck.t('help')).to.equal(i18n.t('help', req.language));
};
beforeEach(() => {
res = generateRes();
req = generateReq();
next = generateNext();
attachTranslateFunction(req, res, next);
});
context('query parameter', () => {
it('uses the language in the query parameter if avalaible', () => {
req.query = {
lang: 'es',
};
getUserLanguage(req, res, next);
expect(req.language).to.equal('es');
checkResT(res);
});
it('falls back to english if the query parameter language does not exists', () => {
req.query = {
lang: 'bla',
};
getUserLanguage(req, res, next);
expect(req.language).to.equal('en');
checkResT(res);
});
it('uses query even if the request includes a user and session', () => {
req.query = {
lang: 'es',
};
req.locals = {
user: {
preferences: {
language: 'it',
},
},
};
req.session = {
userId: 123,
};
getUserLanguage(req, res, next);
expect(req.language).to.equal('es');
checkResT(res);
});
});
context('authorized request', () => {
it('uses the user preferred language if avalaible', () => {
req.locals = {
user: {
preferences: {
language: 'it',
},
},
};
getUserLanguage(req, res, next);
expect(req.language).to.equal('it');
checkResT(res);
});
it('falls back to english if the user preferred language is not avalaible', (done) => {
req.locals = {
user: {
preferences: {
language: 'bla',
},
},
};
getUserLanguage(req, res, () => {
expect(req.language).to.equal('en');
checkResT(res);
done();
});
});
it('uses the user preferred language even if a session is included in request', () => {
req.locals = {
user: {
preferences: {
language: 'it',
},
},
};
req.session = {
userId: 123,
};
getUserLanguage(req, res, next);
expect(req.language).to.equal('it');
checkResT(res);
});
});
context('request with session', () => {
it('uses the user preferred language if avalaible', (done) => {
sandbox.stub(User, 'findOne').returns({
lean () {
return this;
},
exec () {
return Bluebird.resolve({
preferences: {
language: 'it',
},
});
},
});
req.session = {
userId: 123,
};
getUserLanguage(req, res, () => {
expect(req.language).to.equal('it');
checkResT(res);
done();
});
});
});
context('browser fallback', () => {
it('uses browser specificed language', (done) => {
req.headers['accept-language'] = 'pt';
getUserLanguage(req, res, () => {
expect(req.language).to.equal('pt');
checkResT(res);
done();
});
});
it('uses first language in series if browser specifies multiple', (done) => {
req.headers['accept-language'] = 'he, pt, it';
getUserLanguage(req, res, () => {
expect(req.language).to.equal('he');
checkResT(res);
done();
});
});
it('skips invalid lanaguages and uses first language in series if browser specifies multiple', (done) => {
req.headers['accept-language'] = 'blah, he, pt, it';
getUserLanguage(req, res, () => {
expect(req.language).to.equal('he');
checkResT(res);
done();
});
});
it('uses normal version of language if specialized locale is passed in', (done) => {
req.headers['accept-language'] = 'fr-CA';
getUserLanguage(req, res, () => {
expect(req.language).to.equal('fr');
checkResT(res);
done();
});
});
it('uses normal version of language if specialized locale is passed in', (done) => {
req.headers['accept-language'] = 'fr-CA';
getUserLanguage(req, res, () => {
expect(req.language).to.equal('fr');
checkResT(res);
done();
});
});
it('uses es if es is passed in', (done) => {
req.headers['accept-language'] = 'es';
getUserLanguage(req, res, () => {
expect(req.language).to.equal('es');
checkResT(res);
done();
});
});
it('uses es_419 if applicable es-languages are passed in', (done) => {
req.headers['accept-language'] = 'es-mx';
getUserLanguage(req, res, () => {
expect(req.language).to.equal('es_419');
checkResT(res);
done();
});
});
it('uses es_419 if multiple es languages are passed in', (done) => {
req.headers['accept-language'] = 'es-GT, es-MX, es-CR';
getUserLanguage(req, res, () => {
expect(req.language).to.equal('es_419');
checkResT(res);
done();
});
});
it('zh', (done) => {
req.headers['accept-language'] = 'zh-TW';
getUserLanguage(req, res, () => {
expect(req.language).to.equal('zh_TW');
checkResT(res);
done();
});
});
it('uses english if browser specified language is not compatible', (done) => {
req.headers['accept-language'] = 'blah';
getUserLanguage(req, res, () => {
expect(req.language).to.equal('en');
checkResT(res);
done();
});
});
it('uses english if browser does not specify', (done) => {
req.headers['accept-language'] = '';
getUserLanguage(req, res, () => {
expect(req.language).to.equal('en');
checkResT(res);
done();
});
});
it('uses english if browser does not supply an accept-language header', (done) => {
delete req.headers['accept-language'];
getUserLanguage(req, res, () => {
expect(req.language).to.equal('en');
checkResT(res);
done();
});
});
});
});
});
@@ -0,0 +1,58 @@
import {
generateRes,
generateReq,
generateNext,
} from '../../../../helpers/api-unit.helper';
import nconf from 'nconf';
import requireAgain from 'require-again';
describe('maintenance mode middleware', () => {
let res, req, next;
let pathToMaintenanceModeMiddleware = '../../../../../website/server/middlewares/api-v3/maintenanceMode';
beforeEach(() => {
res = generateRes();
next = generateNext();
});
it('does not return 503 error when maintenance mode is off', () => {
req = generateReq();
sandbox.stub(nconf, 'get').withArgs('MAINTENANCE_MODE').returns('false');
let attachMaintenanceMode = requireAgain(pathToMaintenanceModeMiddleware);
attachMaintenanceMode(req, res, next);
expect(next).to.have.been.called.once;
expect(res.status).to.not.have.been.called;
});
it('returns 503 error when maintenance mode is on', () => {
req = generateReq();
sandbox.stub(nconf, 'get').withArgs('MAINTENANCE_MODE').returns('true');
let attachMaintenanceMode = requireAgain(pathToMaintenanceModeMiddleware);
attachMaintenanceMode(req, res, next);
expect(next).to.not.have.been.called;
expect(res.status).to.have.been.calledOnce;
expect(res.status).to.have.been.calledWith(503);
});
it('renders maintenance page when request type is HTML', () => {
req = generateReq({headers: {accept: 'text/html'}});
sandbox.stub(nconf, 'get').withArgs('MAINTENANCE_MODE').returns('true');
let attachMaintenanceMode = requireAgain(pathToMaintenanceModeMiddleware);
attachMaintenanceMode(req, res, next);
expect(res.render).to.have.been.calledOnce;
});
it('sends error message when request type is JSON', () => {
req = generateReq({headers: {accept: 'application/json'}});
sandbox.stub(nconf, 'get').withArgs('MAINTENANCE_MODE').returns('true');
let attachMaintenanceMode = requireAgain(pathToMaintenanceModeMiddleware);
attachMaintenanceMode(req, res, next);
expect(res.send).to.have.been.calledOnce;
});
});
+66
View File
@@ -0,0 +1,66 @@
import {
generateRes,
generateReq,
generateNext,
} from '../../../../helpers/api-unit.helper';
import responseMiddleware from '../../../../../website/server/middlewares/api-v3/response';
describe('response middleware', () => {
let res, req, next;
beforeEach(() => {
res = generateRes();
req = generateReq();
next = generateNext();
});
it('attaches respond method to res', () => {
responseMiddleware(req, res, next);
expect(res.respond).to.exist;
});
it('can be used to respond to requests', () => {
responseMiddleware(req, res, next);
res.respond(200, {field: 1});
expect(res.status).to.be.calledOnce;
expect(res.json).to.be.calledOnce;
expect(res.status).to.be.calledWith(200);
expect(res.json).to.be.calledWith({
success: true,
data: {field: 1},
});
});
it('can be passed a third parameter to be used as optional message', () => {
responseMiddleware(req, res, next);
res.respond(200, {field: 1}, 'hello');
expect(res.status).to.be.calledOnce;
expect(res.json).to.be.calledOnce;
expect(res.status).to.be.calledWith(200);
expect(res.json).to.be.calledWith({
success: true,
data: {field: 1},
message: 'hello',
});
});
it('treats status >= 400 as failures', () => {
responseMiddleware(req, res, next);
res.respond(403, {field: 1});
expect(res.status).to.be.calledOnce;
expect(res.json).to.be.calledOnce;
expect(res.status).to.be.calledWith(403);
expect(res.json).to.be.calledWith({
success: false,
data: {field: 1},
});
});
});