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,309 @@
import analyticsService from '../../../../../website/server/libs/api-v3/analyticsService';
import nock from 'nock';
describe('analyticsService', () => {
let amplitudeNock, gaNock;
beforeEach(() => {
amplitudeNock = nock('https://api.amplitude.com')
.filteringPath(/httpapi.*/g, '')
.post('/')
.reply(200, {status: 'OK'});
gaNock = nock('http://www.google-analytics.com');
});
describe('#track', () => {
let eventType, data;
beforeEach(() => {
eventType = 'Cron';
data = {
category: 'behavior',
uuid: 'unique-user-id',
resting: true,
cronCount: 5,
};
});
context('Amplitude', () => {
it('calls out to amplitude', () => {
return analyticsService.track(eventType, data)
.then(() => {
amplitudeNock.done();
});
});
it('uses a dummy user id if none is provided', () => {
delete data.uuid;
amplitudeNock
.filteringPath(/httpapi.*user_id.*no-user-id-was-provided.*/g, '');
return analyticsService.track(eventType, data)
.then(() => {
amplitudeNock.done();
});
});
it('sets platform as server', () => {
amplitudeNock
.filteringPath(/httpapi.*platform.*server.*/g, '');
return analyticsService.track(eventType, data)
.then(() => {
amplitudeNock.done();
});
});
it('sends details about event', () => {
amplitudeNock
.filteringPath(/httpapi.*event_properties%22%3A%7B%22category%22%3A%22behavior%22%2C%22resting%22%3Atrue%2C%22cronCount%22%3A5%7D%2C%22.*/g, '');
return analyticsService.track(eventType, data)
.then(() => {
amplitudeNock.done();
});
});
it('sends english item name for gear if itemKey is provided', () => {
data.itemKey = 'headAccessory_special_foxEars';
amplitudeNock
.filteringPath(/httpapi.*itemName.*Fox%20Ears.*/g, '');
return analyticsService.track(eventType, data)
.then(() => {
amplitudeNock.done();
});
});
it('sends english item name for egg if itemKey is provided', () => {
data.itemKey = 'Wolf';
amplitudeNock
.filteringPath(/httpapi.*itemName.*Wolf%20Egg.*/g, '');
return analyticsService.track(eventType, data)
.then(() => {
amplitudeNock.done();
});
});
it('sends english item name for food if itemKey is provided', () => {
data.itemKey = 'Cake_Skeleton';
amplitudeNock
.filteringPath(/httpapi.*itemName.*Bare%20Bones%20Cake.*/g, '');
return analyticsService.track(eventType, data)
.then(() => {
amplitudeNock.done();
});
});
it('sends english item name for hatching potion if itemKey is provided', () => {
data.itemKey = 'Golden';
amplitudeNock
.filteringPath(/httpapi.*itemName.*Golden%20Hatching%20Potion.*/g, '');
return analyticsService.track(eventType, data)
.then(() => {
amplitudeNock.done();
});
});
it('sends english item name for quest if itemKey is provided', () => {
data.itemKey = 'atom1';
amplitudeNock
.filteringPath(/httpapi.*itemName.*Attack%20of%20the%20Mundane%2C%20Part%201%3A%20Dish%20Disaster!.*/g, '');
return analyticsService.track(eventType, data)
.then(() => {
amplitudeNock.done();
});
});
it('sends english item name for purchased spell if itemKey is provided', () => {
data.itemKey = 'seafoam';
amplitudeNock
.filteringPath(/httpapi.*itemName.*Seafoam.*/g, '');
return analyticsService.track(eventType, data)
.then(() => {
amplitudeNock.done();
});
});
it('sends user data if provided', () => {
let stats = { class: 'wizard', exp: 5, gp: 23, hp: 10, lvl: 4, mp: 30 };
let user = {
stats,
contributor: { level: 1 },
purchased: { plan: { planId: 'foo-plan' } },
flags: {tour: {intro: -2}},
habits: [{_id: 'habit'}],
dailys: [{_id: 'daily'}],
todos: [{_id: 'todo'}],
rewards: [{_id: 'reward'}],
};
data.user = user;
amplitudeNock
.filteringPath(/httpapi.*user_properties%22%3A%7B%22Class%22%3A%22wizard%22%2C%22Experience%22%3A5%2C%22Gold%22%3A23%2C%22Health%22%3A10%2C%22Level%22%3A4%2C%22Mana%22%3A30%2C%22tutorialComplete%22%3Atrue%2C%22Number%20Of%20Tasks%22%3A%7B%22habits%22%3A1%2C%22dailys%22%3A1%2C%22todos%22%3A1%2C%22rewards%22%3A1%7D%2C%22contributorLevel%22%3A1%2C%22subscription%22%3A%22foo-plan%22%7D%2C%22.*/g, '');
return analyticsService.track(eventType, data)
.then(() => {
amplitudeNock.done();
});
});
});
context('GA', () => {
it('calls out to GA', () => {
gaNock
.post('/collect')
.reply(200, {status: 'OK'});
return analyticsService.track(eventType, data)
.then(() => {
gaNock.done();
});
});
it('sends details about event', () => {
gaNock
.post('/collect', /ec=behavior&ea=Cron&v=1&tid=GA_ID&cid=.*&t=event/)
.reply(200, {status: 'OK'});
return analyticsService.track(eventType, data)
.then(() => {
gaNock.done();
});
});
});
});
describe('#trackPurchase', () => {
let data;
beforeEach(() => {
data = {
uuid: 'user-id',
sku: 'paypal-checkout',
paymentMethod: 'PayPal',
itemPurchased: 'Gems',
purchaseValue: 8,
purchaseType: 'checkout',
gift: false,
quantity: 1,
};
});
context('Amplitude', () => {
it('calls out to amplitude', () => {
return analyticsService.trackPurchase(data)
.then(() => {
amplitudeNock.done();
});
});
it('uses a dummy user id if none is provided', () => {
delete data.uuid;
amplitudeNock
.filteringPath(/httpapi.*user_id.*no-user-id-was-provided.*/g, '');
return analyticsService.trackPurchase(data)
.then(() => {
amplitudeNock.done();
});
});
it('sets platform as server', () => {
amplitudeNock
.filteringPath(/httpapi.*platform.*server.*/g, '');
return analyticsService.trackPurchase(data)
.then(() => {
amplitudeNock.done();
});
});
it('sends details about purchase', () => {
amplitudeNock
.filteringPath(/httpapi.*aypal-checkout%22%2C%22paymentMethod%22%3A%22PayPal%22%2C%22itemPurchased%22%3A%22Gems%22%2C%22purchaseType%22%3A%22checkout%22%2C%22gift%22%3Afalse%2C%22quantity%22%3A1%7D%2C%22event_type%22%3A%22purchase%22%2C%22revenue.*/g, '');
return analyticsService.trackPurchase(data)
.then(() => {
amplitudeNock.done();
});
});
it('sends user data if provided', () => {
let stats = { class: 'wizard', exp: 5, gp: 23, hp: 10, lvl: 4, mp: 30 };
let user = {
stats,
contributor: { level: 1 },
purchased: { plan: { planId: 'foo-plan' } },
flags: {tour: {intro: -2}},
habits: [{_id: 'habit'}],
dailys: [{_id: 'daily'}],
todos: [{_id: 'todo'}],
rewards: [{_id: 'reward'}],
};
data.user = user;
amplitudeNock
.filteringPath(/httpapi.*user_properties%22%3A%7B%22Class%22%3A%22wizard%22%2C%22Experience%22%3A5%2C%22Gold%22%3A23%2C%22Health%22%3A10%2C%22Level%22%3A4%2C%22Mana%22%3A30%2C%22tutorialComplete%22%3Atrue%2C%22Number%20Of%20Tasks%22%3A%7B%22habits%22%3A1%2C%22dailys%22%3A1%2C%22todos%22%3A1%2C%22rewards%22%3A1%7D%2C%22contributorLevel%22%3A1%2C%22subscription%22%3A%22foo-plan%22%7D%2C%22.*/g, '');
return analyticsService.trackPurchase(data)
.then(() => {
amplitudeNock.done();
});
});
});
context('GA', () => {
it('calls out to GA', () => {
gaNock
.post('/collect')
.reply(200, {status: 'OK'});
return analyticsService.trackPurchase(data)
.then(() => {
gaNock.done();
});
});
it('sends details about purchase', () => {
gaNock
.post('/collect', /ti=user-id&tr=8&v=1&tid=GA_ID&cid=.*&t=transaction/)
.reply(200, {status: 'OK'})
.post('/collect', /ec=commerce&ea=checkout&el=PayPal&ev=8&v=1&tid=GA_ID&cid=.*&t=event/)
.reply(200, {status: 'OK'});
return analyticsService.trackPurchase(data)
.then(() => {
gaNock.done();
});
});
});
});
describe('mockAnalyticsService', () => {
it('has stubbed track method', () => {
expect(analyticsService.mockAnalyticsService).to.respondTo('track');
});
it('has stubbed trackPurchase method', () => {
expect(analyticsService.mockAnalyticsService).to.respondTo('trackPurchase');
});
});
});
+97
View File
@@ -0,0 +1,97 @@
import baseModel from '../../../../../website/server/libs/api-v3/baseModel';
import mongoose from 'mongoose';
describe('Base model plugin', () => {
let schema;
beforeEach(() => {
schema = new mongoose.Schema();
sandbox.stub(schema, 'add');
});
it('adds a _id field to the schema', () => {
schema.plugin(baseModel);
expect(schema.add).to.be.calledWith(sinon.match({
_id: sinon.match.object,
}));
});
it('can add timestamps fields', () => {
schema.plugin(baseModel, {timestamps: true});
expect(schema.add).to.be.calledTwice;
});
it('can sanitize input objects', () => {
schema.plugin(baseModel, {
noSet: ['noUpdateForMe'],
});
expect(schema.statics.sanitize).to.exist;
let sanitized = schema.statics.sanitize({ok: true, noUpdateForMe: true});
expect(sanitized).to.have.property('ok');
expect(sanitized).not.to.have.property('noUpdateForMe');
expect(sanitized.noUpdateForMe).to.equal(undefined);
});
it('accepts an array of additional fields to sanitize at runtime', () => {
schema.plugin(baseModel, {
noSet: ['noUpdateForMe'],
});
expect(schema.statics.sanitize).to.exist;
let sanitized = schema.statics.sanitize({ok: true, noUpdateForMe: true, usuallySettable: true}, ['usuallySettable']);
expect(sanitized).to.have.property('ok');
expect(sanitized).not.to.have.property('noUpdateForMe');
expect(sanitized).not.to.have.property('usuallySettable');
});
it('can make fields private', () => {
schema.plugin(baseModel, {
private: ['amPrivate'],
});
expect(schema.options.toJSON.transform).to.exist;
let objToTransform = {ok: true, amPrivate: true};
let privatized = schema.options.toJSON.transform({}, objToTransform);
expect(privatized).to.have.property('ok');
expect(privatized).not.to.have.property('amPrivate');
});
it('accepts a further transform function for toJSON', () => {
let options = {
private: ['amPrivate'],
toJSONTransform: sandbox.stub().returns(true),
};
schema.plugin(baseModel, options);
let objToTransform = {ok: true, amPrivate: true};
let doc = {doc: true};
let privatized = schema.options.toJSON.transform(doc, objToTransform);
expect(privatized).to.equals(true);
expect(options.toJSONTransform).to.be.calledWith(objToTransform, doc);
});
it('accepts a transform function for sanitize', () => {
let options = {
private: ['amPrivate'],
sanitizeTransform: sandbox.stub().returns(true),
};
schema.plugin(baseModel, options);
expect(schema.options.toJSON.transform).to.exist;
let objToSanitize = {ok: true, noUpdateForMe: true};
let sanitized = schema.statics.sanitize(objToSanitize);
expect(sanitized).to.equals(true);
expect(options.sanitizeTransform).to.be.calledWith(objToSanitize);
});
});
@@ -0,0 +1,19 @@
import {
getManifestFiles,
} from '../../../../../website/server/libs/api-v3/buildManifest';
describe('Build Manifest', () => {
describe('getManifestFiles', () => {
it('returns an html string', () => {
let htmlCode = getManifestFiles('app');
expect(htmlCode.startsWith('<script') || htmlCode.startsWith('<link')).to.be.true;
});
it('throws an error in case the page does not exist', () => {
expect(() => {
getManifestFiles('strange name here');
}).to.throw(Error);
});
});
});
@@ -0,0 +1,88 @@
import mongoose from 'mongoose';
import {
removeFromArray,
} from '../../../../../website/server/libs/api-v3/collectionManipulators';
describe('Collection Manipulators', () => {
describe('removeFromArray', () => {
it('removes element from array', () => {
let array = ['a', 'b', 'c', 'd'];
removeFromArray(array, 'c');
expect(array).to.not.include('c');
});
it('removes object from array', () => {
let array = [
{ id: 'a', foo: 'bar' },
{ id: 'b', foo: 'bar' },
{ id: 'c', foo: 'bar' },
{ id: 'd', foo: 'bar' },
{ id: 'e', foo: 'bar' },
];
removeFromArray(array, { id: 'c' });
expect(array).to.not.include({ id: 'c', foo: 'bar' });
});
it('does not change array if value is not found', () => {
let array = ['a', 'b', 'c', 'd'];
removeFromArray(array, 'z');
expect(array).to.have.a.lengthOf(4);
expect(array[0]).to.eql('a');
expect(array[1]).to.eql('b');
expect(array[2]).to.eql('c');
expect(array[3]).to.eql('d');
});
it('returns the removed element', () => {
let array = ['a', 'b', 'c'];
let result = removeFromArray(array, 'b');
expect(result).to.eql('b');
});
it('returns the removed object element', () => {
let array = [
{ id: 'a', foo: 'bar' },
{ id: 'b', foo: 'bar' },
{ id: 'c', foo: 'bar' },
{ id: 'd', foo: 'bar' },
{ id: 'e', foo: 'bar' },
];
let result = removeFromArray(array, { id: 'c' });
expect(result).to.eql({ id: 'c', foo: 'bar' });
});
it('returns false if item is not found', () => {
let array = ['a', 'b', 'c'];
let result = removeFromArray(array, 'z');
expect(result).to.eql(false);
});
it('persists removal of element when mongoose document is saved', async () => {
let schema = new mongoose.Schema({
array: Array,
});
let Model = mongoose.model('ModelToTestRemoveFromArray', schema);
let model = await new Model({
array: ['a', 'b', 'c'],
}).save(); // Initial creation
removeFromArray(model.array, 'b');
let savedModel = await model.save();
expect(savedModel.array).to.not.include('b');
});
});
});
+573
View File
@@ -0,0 +1,573 @@
/* eslint-disable global-require */
import moment from 'moment';
import { cron } from '../../../../../website/server/libs/api-v3/cron';
import { model as User } from '../../../../../website/server/models/user';
import * as Tasks from '../../../../../website/server/models/task';
import { clone } from 'lodash';
import common from '../../../../../common';
// const scoreTask = common.ops.scoreTask;
describe('cron', () => {
let user;
let tasksByType = {habits: [], dailys: [], todos: [], rewards: []};
let daysMissed = 0;
let analytics = {
track: sinon.spy(),
};
beforeEach(() => {
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,
};
});
it('updates user.auth.timestamps.loggedin and lastCron', () => {
let now = new Date();
cron({user, tasksByType, daysMissed, analytics, now});
expect(user.auth.timestamps.loggedin).to.equal(now);
expect(user.lastCron).to.equal(now);
});
it('updates user.preferences.timezoneOffsetAtLastCron', () => {
let timezoneOffsetFromUserPrefs = 1;
cron({user, tasksByType, daysMissed, analytics, timezoneOffsetFromUserPrefs});
expect(user.preferences.timezoneOffsetAtLastCron).to.equal(timezoneOffsetFromUserPrefs);
});
it('resets user.items.lastDrop.count', () => {
user.items.lastDrop.count = 4;
cron({user, tasksByType, daysMissed, analytics});
expect(user.items.lastDrop.count).to.equal(0);
});
it('increments user cron count', () => {
let cronCountBefore = user.flags.cronCount;
cron({user, tasksByType, daysMissed, analytics});
expect(user.flags.cronCount).to.be.greaterThan(cronCountBefore);
});
describe('end of the month perks', () => {
beforeEach(() => {
user.purchased.plan.customerId = 'subscribedId';
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
});
it('resets plan.gemsBought on a new month', () => {
user.purchased.plan.gemsBought = 10;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.gemsBought).to.equal(0);
});
it('resets plan.dateUpdated on a new month', () => {
let currentMonth = moment().format('MMYYYY');
cron({user, tasksByType, daysMissed, analytics});
expect(moment(user.purchased.plan.dateUpdated).format('MMYYYY')).to.equal(currentMonth);
});
it('increments plan.consecutive.count', () => {
user.purchased.plan.consecutive.count = 0;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.count).to.equal(1);
});
it('decrements plan.consecutive.offset when offset is greater than 0', () => {
user.purchased.plan.consecutive.offset = 1;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.offset).to.equal(0);
});
it('increments plan.consecutive.trinkets when user has reached a month that is a multiple of 3', () => {
user.purchased.plan.consecutive.count = 5;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.trinkets).to.equal(1);
});
it('increments plan.consecutive.gemCapExtra when user has reached a month that is a multiple of 3', () => {
user.purchased.plan.consecutive.count = 5;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(5);
});
it('does not increment plan.consecutive.gemCapExtra when user has reached the gemCap limit', () => {
user.purchased.plan.consecutive.gemCapExtra = 25;
user.purchased.plan.consecutive.count = 5;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(25);
});
it('does not reset plan stats if we are before the last day of the cancelled month', () => {
user.purchased.plan.dateTerminated = moment(new Date()).add({days: 1});
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.customerId).to.exist;
});
it('does reset plan stats until we are after the last day of the cancelled month', () => {
user.purchased.plan.dateTerminated = moment(new Date()).subtract({days: 1});
user.purchased.plan.consecutive.gemCapExtra = 20;
user.purchased.plan.consecutive.count = 5;
user.purchased.plan.consecutive.offset = 1;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.customerId).to.not.exist;
expect(user.purchased.plan.consecutive.gemCapExtra).to.be.empty;
expect(user.purchased.plan.consecutive.count).to.be.empty;
expect(user.purchased.plan.consecutive.offset).to.be.empty;
});
});
describe('end of the month perks when user is not subscribed', () => {
it('does not reset plan.gemsBought on a new month', () => {
user.purchased.plan.gemsBought = 10;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.gemsBought).to.equal(10);
});
it('does not reset plan.dateUpdated on a new month', () => {
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.dateUpdated).to.be.empty;
});
it('does not increment plan.consecutive.count', () => {
user.purchased.plan.consecutive.count = 0;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.count).to.equal(0);
});
it('does not decrement plan.consecutive.offset when offset is greater than 0', () => {
user.purchased.plan.consecutive.offset = 1;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.offset).to.equal(1);
});
it('does not increment plan.consecutive.trinkets when user has reached a month that is a multiple of 3', () => {
user.purchased.plan.consecutive.count = 5;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.trinkets).to.equal(0);
});
it('doest not increment plan.consecutive.gemCapExtra when user has reached a month that is a multiple of 3', () => {
user.purchased.plan.consecutive.count = 5;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(0);
});
it('does not increment plan.consecutive.gemCapExtra when user has reached the gemCap limit', () => {
user.purchased.plan.consecutive.gemCapExtra = 25;
user.purchased.plan.consecutive.count = 5;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(25);
});
it('does nothing to plan stats if we are before the last day of the cancelled month', () => {
user.purchased.plan.dateTerminated = moment(new Date()).add({days: 1});
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.customerId).to.not.exist;
});
xit('does nothing to plan stats when we are after the last day of the cancelled month', () => {
user.purchased.plan.dateTerminated = moment(new Date()).subtract({days: 1});
user.purchased.plan.consecutive.gemCapExtra = 20;
user.purchased.plan.consecutive.count = 5;
user.purchased.plan.consecutive.offset = 1;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.customerId).to.exist;
expect(user.purchased.plan.consecutive.gemCapExtra).to.exist;
expect(user.purchased.plan.consecutive.count).to.exist;
expect(user.purchased.plan.consecutive.offset).to.exist;
});
});
describe('user is sleeping', () => {
beforeEach(() => {
user.preferences.sleep = true;
});
it('clears user buffs', () => {
user.stats.buffs = {
str: 1,
int: 1,
per: 1,
con: 1,
stealth: 1,
streaks: true,
};
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.buffs.str).to.equal(0);
expect(user.stats.buffs.int).to.equal(0);
expect(user.stats.buffs.per).to.equal(0);
expect(user.stats.buffs.con).to.equal(0);
expect(user.stats.buffs.stealth).to.equal(0);
expect(user.stats.buffs.streaks).to.be.false;
});
it('resets all dailies without damaging user', () => {
let daily = {
text: 'test daily',
type: 'daily',
frequency: 'daily',
everyX: 5,
startDate: new Date(),
};
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line babel/new-cap
tasksByType.dailys.push(task);
tasksByType.dailys[0].completed = true;
let healthBefore = user.stats.hp;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].completed).to.be.false;
expect(user.stats.hp).to.equal(healthBefore);
});
});
describe('todos', () => {
beforeEach(() => {
let todo = {
text: 'test todo',
type: 'todo',
value: 0,
};
let task = new Tasks.todo(Tasks.Task.sanitize(todo)); // eslint-disable-line babel/new-cap
tasksByType.todos.push(task);
});
it('should make uncompleted todos redder', () => {
let valueBefore = tasksByType.todos[0].value;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.todos[0].value).to.be.lessThan(valueBefore);
});
it('should add history of completed todos to user history', () => {
tasksByType.todos[0].completed = true;
cron({user, tasksByType, daysMissed, analytics});
expect(user.history.todos).to.be.lengthOf(1);
});
});
describe('dailys', () => {
beforeEach(() => {
let daily = {
text: 'test daily',
type: 'daily',
};
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line babel/new-cap
tasksByType.dailys = [];
tasksByType.dailys.push(task);
user._statsComputed = {
con: 1,
};
});
it('should add history', () => {
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].history).to.be.lengthOf(1);
});
it('should set tasks completed to false', () => {
tasksByType.dailys[0].completed = true;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].completed).to.be.false;
});
it('should reset task checklist for completed dailys', () => {
tasksByType.dailys[0].checklist.push({title: 'test', completed: false});
tasksByType.dailys[0].completed = true;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
});
it('should reset task checklist for dailys with scheduled misses', () => {
daysMissed = 10;
tasksByType.dailys[0].checklist.push({title: 'test', completed: false});
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
});
it('should do damage for missing a daily', () => {
daysMissed = 1;
let hpBefore = user.stats.hp;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.hp).to.be.lessThan(hpBefore);
});
it('should not do damage for missing a daily if user stealth buff is greater than or equal to days missed', () => {
daysMissed = 1;
let hpBefore = user.stats.hp;
user.stats.buffs.stealth = 2;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.hp).to.equal(hpBefore);
});
it('should do less damage for missing a daily with partial completion', () => {
daysMissed = 1;
let hpBefore = user.stats.hp;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
cron({user, tasksByType, daysMissed, analytics});
let hpDifferenceOfFullyIncompleteDaily = hpBefore - user.stats.hp;
hpBefore = user.stats.hp;
tasksByType.dailys[0].checklist.push({title: 'test', completed: true});
tasksByType.dailys[0].checklist.push({title: 'test2', completed: false});
cron({user, tasksByType, daysMissed, analytics});
let hpDifferenceOfPartiallyIncompleteDaily = hpBefore - user.stats.hp;
expect(hpDifferenceOfPartiallyIncompleteDaily).to.be.lessThan(hpDifferenceOfFullyIncompleteDaily);
});
it('should decrement quest progress down for missing a daily', () => {
daysMissed = 1;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
let progress = cron({user, tasksByType, daysMissed, analytics});
expect(progress.down).to.equal(-1);
});
});
describe('habits', () => {
beforeEach(() => {
let habit = {
text: 'test habit',
type: 'habit',
};
let task = new Tasks.habit(Tasks.Task.sanitize(habit)); // eslint-disable-line babel/new-cap
tasksByType.habits = [];
tasksByType.habits.push(task);
});
it('should decrement only up value', () => {
tasksByType.habits[0].value = 1;
tasksByType.habits[0].down = false;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.habits[0].value).to.be.lessThan(1);
});
it('should decrement only down value', () => {
tasksByType.habits[0].value = 1;
tasksByType.habits[0].up = false;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.habits[0].value).to.be.lessThan(1);
});
it('should do nothing to habits with both up and down', () => {
tasksByType.habits[0].value = 1;
tasksByType.habits[0].up = true;
tasksByType.habits[0].down = true;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.habits[0].value).to.equal(1);
});
});
describe('perfect day', () => {
beforeEach(() => {
let daily = {
text: 'test daily',
type: 'daily',
};
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line babel/new-cap
tasksByType.dailys = [];
tasksByType.dailys.push(task);
user._statsComputed = {
con: 1,
};
});
it('stores a new entry in user.history.exp', () => {
user.stats.lvl = 2;
cron({user, tasksByType, daysMissed, analytics});
expect(user.history.exp).to.have.lengthOf(1);
expect(user.history.exp[0].value).to.equal(150);
});
it('increments perfect day achievement', () => {
tasksByType.dailys[0].completed = true;
cron({user, tasksByType, daysMissed, analytics});
expect(user.achievements.perfect).to.equal(1);
});
it('increments user buffs if they have a perfect day', () => {
tasksByType.dailys[0].completed = true;
let previousBuffs = clone(user.stats.buffs);
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
expect(user.stats.buffs.int).to.be.greaterThan(previousBuffs.int);
expect(user.stats.buffs.per).to.be.greaterThan(previousBuffs.per);
expect(user.stats.buffs.con).to.be.greaterThan(previousBuffs.con);
});
it('clears buffs if user does not have a perfect day', () => {
daysMissed = 1;
tasksByType.dailys[0].completed = false;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
user.stats.buffs = {
str: 1,
int: 1,
per: 1,
con: 1,
stealth: 0,
streaks: true,
};
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.buffs.str).to.equal(0);
expect(user.stats.buffs.int).to.equal(0);
expect(user.stats.buffs.per).to.equal(0);
expect(user.stats.buffs.con).to.equal(0);
expect(user.stats.buffs.stealth).to.equal(0);
expect(user.stats.buffs.streaks).to.be.false;
});
});
describe('adding mp', () => {
it('should add mp to user', () => {
let mpBefore = user.stats.mp;
tasksByType.dailys[0].completed = true;
user._statsComputed.maxMP = 100;
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.mp).to.be.greaterThan(mpBefore);
});
it('set user\'s mp to user._statsComputed.maxMP when user.stats.mp is greater', () => {
user.stats.mp = 120;
user._statsComputed.maxMP = 100;
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.mp).to.equal(user._statsComputed.maxMP);
});
});
describe('quest progress', () => {
beforeEach(() => {
let daily = {
text: 'test daily',
type: 'daily',
};
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line babel/new-cap
tasksByType.dailys = [];
tasksByType.dailys.push(task);
user._statsComputed = {
con: 1,
};
daysMissed = 1;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
});
it('resets user progress', () => {
cron({user, tasksByType, daysMissed, analytics});
expect(user.party.quest.progress.up).to.equal(0);
expect(user.party.quest.progress.down).to.equal(0);
expect(user.party.quest.progress.collect).to.be.empty;
});
it('applies the user progress', () => {
let progress = cron({user, tasksByType, daysMissed, analytics});
expect(progress.down).to.equal(-1);
});
});
describe('private messages', () => {
let lastMessageId;
beforeEach(() => {
let maxPMs = 200;
for (let index = 0; index < maxPMs - 1; index += 1) {
let messageId = common.uuid();
user.inbox.messages[messageId] = {
id: messageId,
text: `test ${index}`,
timestamp: Number(new Date()),
likes: {},
flags: {},
flagCount: 0,
};
}
lastMessageId = common.uuid();
user.inbox.messages[lastMessageId] = {
id: lastMessageId,
text: `test ${lastMessageId}`,
timestamp: Number(new Date()),
likes: {},
flags: {},
flagCount: 0,
};
});
xit('does not clear pms under 200', () => {
cron({user, tasksByType, daysMissed, analytics});
expect(user.inbox.messages[lastMessageId]).to.exist;
});
xit('clears pms over 200', () => {
let messageId = common.uuid();
user.inbox.messages[messageId] = {
id: messageId,
text: `test ${messageId}`,
timestamp: Number(new Date()),
likes: {},
flags: {},
flagCount: 0,
};
cron({user, tasksByType, daysMissed, analytics});
expect(user.inbox.messages[messageId]).to.not.exist;
});
});
});
+232
View File
@@ -0,0 +1,232 @@
/* eslint-disable global-require */
import request from 'request';
import nconf from 'nconf';
import nodemailer from 'nodemailer';
import Bluebird from 'bluebird';
import requireAgain from 'require-again';
import logger from '../../../../../website/server/libs/api-v3/logger';
function defer () {
let resolve;
let reject;
let promise = new Bluebird((resolveParam, rejectParam) => {
resolve = resolveParam;
reject = rejectParam;
});
return {
resolve,
reject,
promise,
};
}
function getUser () {
return {
_id: 'random _id',
auth: {
local: {
username: 'username',
email: 'email@email',
},
facebook: {
emails: [{
value: 'email@facebook',
}],
displayName: 'fb display name',
},
},
profile: {
name: 'profile name',
},
preferences: {
emailNotifications: {
unsubscribeFromAll: false,
},
},
};
}
describe('emails', () => {
let pathToEmailLib = '../../../../../website/server/libs/api-v3/email';
describe('sendEmail', () => {
it('can send an email using the default transport', () => {
let sendMailSpy = sandbox.stub().returns(defer().promise);
sandbox.stub(nodemailer, 'createTransport').returns({
sendMail: sendMailSpy,
});
let attachEmail = requireAgain(pathToEmailLib);
attachEmail.send();
expect(sendMailSpy).to.be.calledOnce;
});
it('logs errors', (done) => {
let deferred = defer();
let sendMailSpy = sandbox.stub().returns(deferred.promise);
sandbox.stub(nodemailer, 'createTransport').returns({
sendMail: sendMailSpy,
});
sandbox.stub(logger, 'error');
let attachEmail = requireAgain(pathToEmailLib);
attachEmail.send();
expect(sendMailSpy).to.be.calledOnce;
deferred.reject();
// wait for unhandledRejection event to fire
setTimeout(() => {
expect(logger.error).to.be.calledOnce;
done();
}, 20);
});
});
describe('getUserInfo', () => {
it('returns an empty object if no field request', () => {
let attachEmail = requireAgain(pathToEmailLib);
let getUserInfo = attachEmail.getUserInfo;
expect(getUserInfo({}, [])).to.be.empty;
});
it('returns correct user data', () => {
let attachEmail = requireAgain(pathToEmailLib);
let getUserInfo = attachEmail.getUserInfo;
let user = getUser();
let data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
expect(data).to.have.property('name', user.profile.name);
expect(data).to.have.property('email', user.auth.local.email);
expect(data).to.have.property('_id', user._id);
expect(data).to.have.property('canSend', true);
});
it('returns correct user data [facebook users]', () => {
let attachEmail = requireAgain(pathToEmailLib);
let getUserInfo = attachEmail.getUserInfo;
let user = getUser();
delete user.profile.name;
delete user.auth.local;
let data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
expect(data).to.have.property('name', user.auth.facebook.displayName);
expect(data).to.have.property('email', user.auth.facebook.emails[0].value);
expect(data).to.have.property('_id', user._id);
expect(data).to.have.property('canSend', true);
});
it('has fallbacks for missing data', () => {
let attachEmail = requireAgain(pathToEmailLib);
let getUserInfo = attachEmail.getUserInfo;
let user = getUser();
delete user.profile.name;
delete user.auth.local.email;
delete user.auth.facebook;
let data = getUserInfo(user, ['name', 'email', '_id', 'canSend']);
expect(data).to.have.property('name', user.auth.local.username);
expect(data).not.to.have.property('email');
expect(data).to.have.property('_id', user._id);
expect(data).to.have.property('canSend', true);
});
});
describe('sendTxnEmail', () => {
beforeEach(() => {
sandbox.stub(request, 'post');
});
afterEach(() => {
sandbox.restore();
});
it('can send a txn email to one recipient', () => {
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
let attachEmail = requireAgain(pathToEmailLib);
let sendTxnEmail = attachEmail.sendTxn;
let emailType = 'an email type';
let mailingInfo = {
name: 'my name',
email: 'my@email',
};
sendTxnEmail(mailingInfo, emailType);
expect(request.post).to.be.calledWith(sinon.match({
json: {
data: {
emailType: sinon.match.same(emailType),
to: sinon.match((value) => {
return Array.isArray(value) && value[0].name === mailingInfo.name;
}, 'matches mailing info array'),
},
},
}));
});
it('does not send email if address is missing', () => {
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
let attachEmail = requireAgain(pathToEmailLib);
let sendTxnEmail = attachEmail.sendTxn;
let emailType = 'an email type';
let mailingInfo = {
name: 'my name',
// email: 'my@email',
};
sendTxnEmail(mailingInfo, emailType);
expect(request.post).not.to.be.called;
});
it('uses getUserInfo in case of user data', () => {
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
let attachEmail = requireAgain(pathToEmailLib);
let sendTxnEmail = attachEmail.sendTxn;
let emailType = 'an email type';
let mailingInfo = getUser();
sendTxnEmail(mailingInfo, emailType);
expect(request.post).to.be.calledWith(sinon.match({
json: {
data: {
emailType: sinon.match.same(emailType),
to: sinon.match(val => val[0]._id === mailingInfo._id),
},
},
}));
});
it('sends email with some default variables', () => {
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
let attachEmail = requireAgain(pathToEmailLib);
let sendTxnEmail = attachEmail.sendTxn;
let emailType = 'an email type';
let mailingInfo = {
name: 'my name',
email: 'my@email',
};
let variables = [1, 2, 3];
sendTxnEmail(mailingInfo, emailType, variables);
expect(request.post).to.be.calledWith(sinon.match({
json: {
data: {
variables: sinon.match((value) => {
return value[0].name === 'BASE_URL';
}, 'matches variables'),
personalVariables: sinon.match((value) => {
return value[0].rcpt === mailingInfo.email &&
value[0].vars[0].name === 'RECIPIENT_NAME' &&
value[0].vars[1].name === 'RECIPIENT_UNSUB_URL';
}, 'matches personal variables'),
},
},
}));
});
});
});
+15
View File
@@ -0,0 +1,15 @@
import {
encrypt,
decrypt,
} from '../../../../../website/server/libs/api-v3/encryption';
describe('encryption', () => {
it('can encrypt and decrypt', () => {
let data = 'some secret text';
let encrypted = encrypt(data);
let decrypted = decrypt(encrypted);
expect(encrypted).not.to.equal(data);
expect(data).to.equal(decrypted);
});
});
+122
View File
@@ -0,0 +1,122 @@
// TODO move to shared tests
import {
CustomError,
NotAuthorized,
BadRequest,
InternalServerError,
NotFound,
} from '../../../../../website/server/libs/api-v3/errors';
describe('Custom Errors', () => {
describe('CustomError', () => {
it('is an instance of Error', () => {
let customError = new CustomError();
expect(customError).to.be.an.instanceOf(Error);
});
});
describe('NotAuthorized', () => {
it('is an instance of CustomError', () => {
let notAuthorizedError = new NotAuthorized();
expect(notAuthorizedError).to.be.an.instanceOf(CustomError);
});
it('it returns an http code of 401', () => {
let notAuthorizedError = new NotAuthorized();
expect(notAuthorizedError.httpCode).to.eql(401);
});
it('returns a default message', () => {
let notAuthorizedError = new NotAuthorized();
expect(notAuthorizedError.message).to.eql('Not authorized.');
});
it('allows a custom message', () => {
let notAuthorizedError = new NotAuthorized('Custom Error Message');
expect(notAuthorizedError.message).to.eql('Custom Error Message');
});
});
describe('NotFound', () => {
it('is an instance of CustomError', () => {
let notAuthorizedError = new NotFound();
expect(notAuthorizedError).to.be.an.instanceOf(CustomError);
});
it('it returns an http code of 404', () => {
let notAuthorizedError = new NotFound();
expect(notAuthorizedError.httpCode).to.eql(404);
});
it('returns a default message', () => {
let notAuthorizedError = new NotFound();
expect(notAuthorizedError.message).to.eql('Not found.');
});
it('allows a custom message', () => {
let notAuthorizedError = new NotFound('Custom Error Message');
expect(notAuthorizedError.message).to.eql('Custom Error Message');
});
});
describe('BadRequest', () => {
it('is an instance of CustomError', () => {
let badRequestError = new BadRequest();
expect(badRequestError).to.be.an.instanceOf(CustomError);
});
it('it returns an http code of 401', () => {
let badRequestError = new BadRequest();
expect(badRequestError.httpCode).to.eql(400);
});
it('returns a default message', () => {
let badRequestError = new BadRequest();
expect(badRequestError.message).to.eql('Bad request.');
});
it('allows a custom message', () => {
let badRequestError = new BadRequest('Custom Error Message');
expect(badRequestError.message).to.eql('Custom Error Message');
});
});
describe('InternalServerError', () => {
it('is an instance of CustomError', () => {
let internalServerError = new InternalServerError();
expect(internalServerError).to.be.an.instanceOf(CustomError);
});
it('it returns an http code of 500', () => {
let internalServerError = new InternalServerError();
expect(internalServerError.httpCode).to.eql(500);
});
it('returns a default message', () => {
let internalServerError = new InternalServerError();
expect(internalServerError.message).to.eql('An unexpected error occurred.');
});
it('allows a custom message', () => {
let internalServerError = new InternalServerError('Custom Error Message');
expect(internalServerError.message).to.eql('Custom Error Message');
});
});
});
+39
View File
@@ -0,0 +1,39 @@
import {
translations,
localePath,
langCodes,
} from '../../../../../website/server/libs/api-v3/i18n';
import fs from 'fs';
import path from 'path';
describe('i18n', () => {
let listOfLocales = [];
before((done) => {
fs.readdir(localePath, (err, files) => {
if (err) return done(err);
files.forEach((file) => {
if (fs.statSync(path.join(localePath, file)).isDirectory() === false) return;
listOfLocales.push(file);
});
listOfLocales = listOfLocales.sort();
done();
});
});
describe('translations', () => {
it('includes a translation object for each locale', () => {
listOfLocales.forEach((locale) => {
expect(translations[locale]).to.be.an('object');
});
});
});
describe('langCodes', () => {
it('is a list of all the language codes', () => {
expect(langCodes.sort()).to.eql(listOfLocales);
});
});
});
+57
View File
@@ -0,0 +1,57 @@
import winston from 'winston';
import requireAgain from 'require-again';
/* eslint-disable global-require */
describe('logger', () => {
let pathToLoggerLib = '../../../../../website/server/libs/api-v3/logger';
let infoSpy;
let errorSpy;
beforeEach(() => {
infoSpy = sandbox.stub();
errorSpy = sandbox.stub();
sandbox.stub(winston, 'Logger').returns({
info: infoSpy,
error: errorSpy,
});
});
afterEach(() => {
sandbox.restore();
});
it('info', () => {
let attachLogger = requireAgain(pathToLoggerLib);
attachLogger.info(1, 2, 3);
expect(infoSpy).to.be.calledOnce;
expect(infoSpy).to.be.calledWith(1, 2, 3);
});
describe('error', () => {
it('with custom arguments', () => {
let attachLogger = requireAgain(pathToLoggerLib);
attachLogger.error(1, 2, 3, 4);
expect(errorSpy).to.be.calledOnce;
expect(errorSpy).to.be.calledWith(1, 2, 3, 4);
});
it('with error', () => {
let attachLogger = requireAgain(pathToLoggerLib);
let errInstance = new Error('An error.');
attachLogger.error(errInstance, {
data: 1,
}, 2, 3);
expect(errorSpy).to.be.calledOnce;
// using calledWith doesn't work
let lastCallArgs = errorSpy.lastCall.args;
expect(lastCallArgs[3]).to.equal(3);
expect(lastCallArgs[2]).to.equal(2);
expect(lastCallArgs[1]).to.eql({
data: 1,
fullError: errInstance,
});
expect(lastCallArgs[0]).to.eql(errInstance.stack);
});
});
});
+41
View File
@@ -0,0 +1,41 @@
import {
encrypt as encryptPassword,
makeSalt,
} from '../../../../../website/server/libs/api-v3/password';
describe('Password Utilities', () => {
describe('Encrypt', () => {
it('always encrypt the same password to the same value when using the same salt', () => {
let textPassword = 'mySecretPassword';
let salt = makeSalt();
let encryptedPassword = encryptPassword(textPassword, salt);
expect(encryptPassword(textPassword, salt)).to.eql(encryptedPassword);
});
it('never encrypt the same password to the same value when using a different salt', () => {
let textPassword = 'mySecretPassword';
let aSalt = makeSalt();
let anotherSalt = makeSalt();
let anEncryptedPassword = encryptPassword(textPassword, aSalt);
let anotherEncryptedPassword = encryptPassword(textPassword, anotherSalt);
expect(anEncryptedPassword).not.to.eql(anotherEncryptedPassword);
});
});
describe('Make Salt', () => {
it('creates a salt with length 10 by default', () => {
let salt = makeSalt();
expect(salt.length).to.eql(10);
});
it('can create a salt of any length', () => {
let length = 24;
let salt = makeSalt(length);
expect(salt.length).to.eql(length);
});
});
});
+72
View File
@@ -0,0 +1,72 @@
import * as sender from '../../../../../website/server/libs/api-v3/email';
import * as api from '../../../../../website/server/libs/api-v3/payments';
import { model as User } from '../../../../../website/server/models/user';
import moment from 'moment';
describe('payments/index', () => {
let fakeSend;
let data;
let user;
describe('#createSubscription', () => {
beforeEach(async () => {
user = new User();
});
it('succeeds', async () => {
data = { user, sub: { key: 'basic_3mo' } };
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
expect(user.purchased.plan.planId).to.exist;
});
});
describe('#cancelSubscription', () => {
beforeEach(() => {
fakeSend = sinon.spy(sender, 'sendTxn');
data = { user: new User() };
});
afterEach(() => {
fakeSend.restore();
});
it('plan.extraMonths is defined', () => {
api.cancelSubscription(data);
let terminated = data.user.purchased.plan.dateTerminated;
data.user.purchased.plan.extraMonths = 2;
api.cancelSubscription(data);
let difference = Math.abs(moment(terminated).diff(data.user.purchased.plan.dateTerminated, 'days'));
expect(difference - 60).to.be.lessThan(3); // the difference is approximately two months, +/- 2 days
});
it('plan.extraMonth is a fraction', () => {
api.cancelSubscription(data);
let terminated = data.user.purchased.plan.dateTerminated;
data.user.purchased.plan.extraMonths = 0.3;
api.cancelSubscription(data);
let difference = Math.abs(moment(terminated).diff(data.user.purchased.plan.dateTerminated, 'days'));
expect(difference - 10).to.be.lessThan(3); // the difference should be 10 days.
});
it('nextBill is defined', () => {
api.cancelSubscription(data);
let terminated = data.user.purchased.plan.dateTerminated;
data.nextBill = moment().add({ days: 25 });
api.cancelSubscription(data);
let difference = Math.abs(moment(terminated).diff(data.user.purchased.plan.dateTerminated, 'days'));
expect(difference - 5).to.be.lessThan(2); // the difference should be 5 days, +/- 1 day
});
it('saves the canceled subscription for the user', () => {
expect(data.user.purchased.plan.dateTerminated).to.not.exist;
api.cancelSubscription(data);
expect(data.user.purchased.plan.dateTerminated).to.exist;
});
it('sends a text', async () => {
await api.cancelSubscription(data);
sinon.assert.called(fakeSend);
});
});
});
+56
View File
@@ -0,0 +1,56 @@
import { preenHistory } from '../../../../../website/server/libs/api-v3/preening';
import moment from 'moment';
import sinon from 'sinon'; // eslint-disable-line no-shadow
import { generateHistory } from '../../../../helpers/api-unit.helper.js';
describe('preenHistory', () => {
let clock;
beforeEach(() => {
// Replace system clocks so we can get predictable results
clock = sinon.useFakeTimers(Number(moment('2013-10-20').zone(0).startOf('day').toDate()), 'Date');
});
afterEach(() => {
return clock.restore();
});
it('does not modify history if all entries are more recent than cutoff (free users)', () => {
let h = generateHistory(60);
expect(preenHistory(_.cloneDeep(h), false, 0)).to.eql(h);
});
it('does not modify history if all entries are more recent than cutoff (subscribers)', () => {
let h = generateHistory(365);
expect(preenHistory(_.cloneDeep(h), true, 0)).to.eql(h);
});
it('does aggregate data in monthly entries before cutoff (free users)', () => {
let h = generateHistory(81); // Jumps to July
let preened = preenHistory(_.cloneDeep(h), false, 0);
expect(preened.length).to.eql(62); // Keeps 60 days + 2 entries per august and july
});
it('does aggregate data in monthly entries before cutoff (subscribers)', () => {
let h = generateHistory(396); // Jumps to September 2012
let preened = preenHistory(_.cloneDeep(h), true, 0);
expect(preened.length).to.eql(367); // Keeps 365 days + 2 entries per october and september
});
it('does aggregate data in monthly and yearly entries before cutoff (free users)', () => {
let h = generateHistory(731); // Jumps to October 21 2012
let preened = preenHistory(_.cloneDeep(h), false, 0);
expect(preened.length).to.eql(73); // Keeps 60 days + 11 montly entries and 2 yearly entry for 2011 and 2012
});
it('does aggregate data in monthly and yearly entries before cutoff (subscribers)', () => {
let h = generateHistory(1031); // Jumps to October 21 2012
let preened = preenHistory(_.cloneDeep(h), true, 0);
expect(preened.length).to.eql(380); // Keeps 365 days + 13 montly entries and 2 yearly entries for 2011 and 2010
});
it('correctly aggregates values', () => {
let h = generateHistory(63); // Compress last 3 days
let preened = preenHistory(_.cloneDeep(h), false, 0);
expect(preened[0].value).to.eql((61 + 62 + 63) / 3);
});
});
+44
View File
@@ -0,0 +1,44 @@
import setupNconf from '../../../../../website/server/libs/api-v3/setupNconf';
import path from 'path';
import nconf from 'nconf';
describe('setupNconf', () => {
beforeEach(() => {
sandbox.stub(nconf, 'argv').returnsThis();
sandbox.stub(nconf, 'env').returnsThis();
sandbox.stub(nconf, 'file').returnsThis();
});
afterEach(() => {
sandbox.restore();
});
it('sets up nconf', () => {
setupNconf();
expect(nconf.argv).to.be.calledOnce;
expect(nconf.env).to.be.calledOnce;
expect(nconf.file).to.be.calledOnce;
let regexString = `\\${path.sep}config.json$`;
expect(nconf.file).to.be.calledWithMatch('user', new RegExp(regexString));
});
it('sets IS_PROD variable', () => {
setupNconf();
expect(nconf.get('IS_PROD')).to.exist;
});
it('sets IS_DEV variable', () => {
setupNconf();
expect(nconf.get('IS_DEV')).to.exist;
});
it('allows a custom config.json file to be passed in', () => {
setupNconf('customfile.json');
expect(nconf.file).to.be.calledOnce;
expect(nconf.file).to.be.calledWithMatch('user', 'customfile.json');
});
});
+135
View File
@@ -0,0 +1,135 @@
import request from 'request';
import { sendTaskWebhook } from '../../../../../website/server/libs/api-v3/webhook';
describe('webhooks', () => {
beforeEach(() => {
sandbox.stub(request, 'post');
});
afterEach(() => {
sandbox.restore();
});
describe('sendTaskWebhook', () => {
let task = {
details: { _id: 'task-id' },
delta: 1.4,
direction: 'up',
};
let data = {
task,
user: { _id: 'user-id' },
};
it('does not send if no webhook endpoints exist', () => {
let webhooks = { };
sendTaskWebhook(webhooks, data);
expect(request.post).to.not.be.called;
});
it('does not send if no webhooks are enabled', () => {
let webhooks = {
'some-id': {
sort: 0,
id: 'some-id',
enabled: false,
url: 'http://example.org/endpoint',
},
};
sendTaskWebhook(webhooks, data);
expect(request.post).to.not.be.called;
});
it('does not send if webhook url is not valid', () => {
let webhooks = {
'some-id': {
sort: 0,
id: 'some-id',
enabled: true,
url: 'http://malformedurl/endpoint',
},
};
sendTaskWebhook(webhooks, data);
expect(request.post).to.not.be.called;
});
it('sends task direction, task, task delta, and abridged user data', () => {
let webhooks = {
'some-id': {
sort: 0,
id: 'some-id',
enabled: true,
url: 'http://example.org/endpoint',
},
};
sendTaskWebhook(webhooks, data);
expect(request.post).to.be.calledOnce;
expect(request.post).to.be.calledWith({
url: 'http://example.org/endpoint',
body: {
direction: 'up',
task: { _id: 'task-id' },
delta: 1.4,
user: {
_id: 'user-id',
},
},
json: true,
});
});
it('sends a post request for each webhook endpoint', () => {
let webhooks = {
'some-id': {
sort: 0,
id: 'some-id',
enabled: true,
url: 'http://example.org/endpoint',
},
'second-webhook': {
sort: 1,
id: 'second-webhook',
enabled: true,
url: 'http://example.com/2/endpoint',
},
};
sendTaskWebhook(webhooks, data);
expect(request.post).to.be.calledTwice;
expect(request.post).to.be.calledWith({
url: 'http://example.org/endpoint',
body: {
direction: 'up',
task: { _id: 'task-id' },
delta: 1.4,
user: {
_id: 'user-id',
},
},
json: true,
});
expect(request.post).to.be.calledWith({
url: 'http://example.com/2/endpoint',
body: {
direction: 'up',
task: { _id: 'task-id' },
delta: 1.4,
user: {
_id: 'user-id',
},
},
json: true,
});
});
});
});
@@ -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},
});
});
});
+161
View File
@@ -0,0 +1,161 @@
import { model as Challenge } from '../../../../../website/server/models/challenge';
import { model as Group } from '../../../../../website/server/models/group';
import { model as User } from '../../../../../website/server/models/user';
import * as Tasks from '../../../../../website/server/models/task';
import common from '../../../../../common/';
import { each, find } from 'lodash';
describe('Challenge Model', () => {
let guild, leader, challenge, task;
let tasksToTest = {
habit: {
text: 'test habit',
type: 'habit',
up: false,
down: true,
},
todo: {
text: 'test todo',
type: 'todo',
},
daily: {
text: 'test daily',
type: 'daily',
frequency: 'daily',
everyX: 5,
startDate: new Date(),
},
reward: {
text: 'test reward',
type: 'reward',
},
};
beforeEach(async () => {
guild = new Group({
name: 'test party',
type: 'guild',
});
leader = new User({
guilds: [guild._id],
});
guild.leader = leader._id;
challenge = new Challenge({
name: 'Test Challenge',
shortName: 'Test',
leader: leader._id,
group: guild._id,
});
leader.challenges = [challenge._id];
await Promise.all([
guild.save(),
leader.save(),
challenge.save(),
]);
});
each(tasksToTest, (taskValue, taskType) => {
context(`${taskType}`, () => {
beforeEach(async() => {
task = new Tasks[`${taskType}`](Tasks.Task.sanitize(taskValue));
task.challenge.id = challenge._id;
await task.save();
});
it('adds tasks to challenge and challenge members', async () => {
await challenge.addTasks([task]);
let updatedLeader = await User.findOne({_id: leader._id});
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
let syncedTask = find(updatedLeadersTasks, function findNewTask (updatedLeadersTask) {
return updatedLeadersTask.type === taskValue.type && updatedLeadersTask.text === taskValue.text;
});
expect(syncedTask).to.exist;
});
it('syncs a challenge to a user', async () => {
await challenge.addTasks([task]);
let newMember = new User({
guilds: [guild._id],
});
await newMember.save();
await challenge.syncToUser(newMember);
let updatedNewMember = await User.findById(newMember._id);
let updatedNewMemberTasks = await Tasks.Task.find({_id: { $in: updatedNewMember.tasksOrder[`${taskType}s`]}});
let syncedTask = find(updatedNewMemberTasks, function findNewTask (updatedNewMemberTask) {
return updatedNewMemberTask.type === taskValue.type && updatedNewMemberTask.text === taskValue.text;
});
expect(updatedNewMember.challenges).to.contain(challenge._id);
expect(updatedNewMember.tags[3].id).to.equal(challenge._id);
expect(updatedNewMember.tags[3].name).to.equal(challenge.shortName);
expect(syncedTask).to.exist;
});
it('updates tasks to challenge and challenge members', async () => {
let updatedTaskName = 'Updated Test Habit';
await challenge.addTasks([task]);
let req = {
body: { text: updatedTaskName },
};
Tasks.Task.sanitize(req.body);
_.assign(task, common.ops.updateTask(task.toObject(), req)[0]);
await challenge.updateTask(task);
let updatedLeader = await User.findOne({_id: leader._id});
let updatedUserTask = await Tasks.Task.findById(updatedLeader.tasksOrder[`${taskType}s`][0]);
expect(updatedUserTask.text).to.equal(updatedTaskName);
});
it('removes a tasks to challenge and challenge members', async () => {
await challenge.addTasks([task]);
await challenge.removeTask(task);
let updatedLeader = await User.findOne({_id: leader._id});
let updatedUserTask = await Tasks.Task.findOne({_id: updatedLeader.tasksOrder[`${taskType}s`][0]}).exec();
expect(updatedUserTask.challenge.broken).to.equal('TASK_DELETED');
});
it('unlinks and deletes challenge tasks for a user when remove-all is specified', async () => {
await challenge.addTasks([task]);
await challenge.unlinkTasks(leader, 'remove-all');
let updatedLeader = await User.findOne({_id: leader._id});
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
let syncedTask = find(updatedLeadersTasks, function findNewTask (updatedLeadersTask) {
return updatedLeadersTask.type === taskValue.type && updatedLeadersTask.text === taskValue.text;
});
expect(syncedTask).to.not.exist;
});
it('unlinks and keeps challenge tasks for a user when keep-all is specified', async () => {
await challenge.addTasks([task]);
await challenge.unlinkTasks(leader, 'keep-all');
let updatedLeader = await User.findOne({_id: leader._id});
let updatedLeadersTasks = await Tasks.Task.find({_id: { $in: updatedLeader.tasksOrder[`${taskType}s`]}});
let syncedTask = find(updatedLeadersTasks, function findNewTask (updatedLeadersTask) {
return updatedLeadersTask.type === taskValue.type && updatedLeadersTask.text === taskValue.text;
});
expect(syncedTask).to.exist;
expect(syncedTask.challenge._id).to.be.empty;
});
});
});
});
+300
View File
@@ -0,0 +1,300 @@
import { sleep } from '../../../../helpers/api-unit.helper';
import { model as Group } from '../../../../../website/server/models/group';
import { model as User } from '../../../../../website/server/models/user';
import { quests as questScrolls } from '../../../../../common/script/content';
import * as email from '../../../../../website/server/libs/api-v3/email';
describe('Group Model', () => {
context('Instance Methods', () => {
describe('#startQuest', () => {
let party, questLeader, participatingMember, nonParticipatingMember, undecidedMember;
beforeEach(async () => {
sandbox.stub(email, 'sendTxn');
party = new Group({
name: 'test party',
type: 'party',
privacy: 'private',
});
questLeader = new User({
party: { _id: party._id },
items: {
quests: {
whale: 1,
},
},
});
party.leader = questLeader._id;
participatingMember = new User({
party: { _id: party._id },
});
nonParticipatingMember = new User({
party: { _id: party._id },
});
undecidedMember = new User({
party: { _id: party._id },
});
await Promise.all([
party.save(),
questLeader.save(),
participatingMember.save(),
nonParticipatingMember.save(),
undecidedMember.save(),
]);
});
context('Failure Conditions', () => {
it('throws an error if group is not a party', async () => {
let guild = new Group({
type: 'guild',
});
await expect(guild.startQuest(participatingMember)).to.eventually.be.rejected;
});
it('throws an error if party is not on a quest', async () => {
await expect(party.startQuest(participatingMember)).to.eventually.be.rejected;
});
it('throws an error if quest is already active', async () => {
party.quest.key = 'whale';
party.quest.active = true;
await expect(party.startQuest(participatingMember)).to.eventually.be.rejected;
});
});
context('Successes', () => {
beforeEach(() => {
party.quest.key = 'whale';
party.quest.active = false;
party.quest.leader = questLeader._id;
party.quest.members = { };
party.quest.members[questLeader._id] = true;
party.quest.members[participatingMember._id] = true;
party.quest.members[nonParticipatingMember._id] = false;
party.quest.members[undecidedMember._id] = null;
});
it('activates quest', () => {
party.startQuest(participatingMember);
expect(party.quest.active).to.eql(true);
});
it('sets up boss quest', () => {
let bossQuest = questScrolls.whale;
party.quest.key = bossQuest.key;
party.startQuest(participatingMember);
expect(party.quest.progress.hp).to.eql(bossQuest.boss.hp);
});
it('sets up rage meter for rage boss quest', () => {
let rageBossQuest = questScrolls.trex_undead;
party.quest.key = rageBossQuest.key;
party.startQuest(participatingMember);
expect(party.quest.progress.rage).to.eql(0);
});
it('sets up collection quest', () => {
let collectionQuest = questScrolls.vice2;
party.quest.key = collectionQuest.key;
party.startQuest(participatingMember);
expect(party.quest.progress.collect).to.eql({
lightCrystal: 0,
});
});
it('sets up collection quest with multiple items', () => {
let collectionQuest = questScrolls.evilsanta2;
party.quest.key = collectionQuest.key;
party.startQuest(participatingMember);
expect(party.quest.progress.collect).to.eql({
tracks: 0,
branches: 0,
});
});
it('prunes non-participating members from quest members object', () => {
party.startQuest(participatingMember);
let expectedQuestMembers = {};
expectedQuestMembers[questLeader._id] = true;
expectedQuestMembers[participatingMember._id] = true;
expect(party.quest.members).to.eql(expectedQuestMembers);
});
it('applies updates to user object directly if user is participating', async () => {
await party.startQuest(participatingMember);
expect(participatingMember.party.quest.key).to.eql('whale');
expect(participatingMember.party.quest.progress.down).to.eql(0);
expect(participatingMember.party.quest.progress.collect).to.eql({});
expect(participatingMember.party.quest.completed).to.eql(null);
});
it('applies updates to other participating members', async () => {
await party.startQuest(nonParticipatingMember);
questLeader = await User.findById(questLeader._id);
participatingMember = await User.findById(participatingMember._id);
expect(participatingMember.party.quest.key).to.eql('whale');
expect(participatingMember.party.quest.progress.down).to.eql(0);
expect(participatingMember.party.quest.progress.collect).to.eql({});
expect(participatingMember.party.quest.completed).to.eql(null);
expect(questLeader.party.quest.key).to.eql('whale');
expect(questLeader.party.quest.progress.down).to.eql(0);
expect(questLeader.party.quest.progress.collect).to.eql({});
expect(questLeader.party.quest.completed).to.eql(null);
});
it('does not apply updates to nonparticipating members', async () => {
await party.startQuest(participatingMember);
nonParticipatingMember = await User.findById(nonParticipatingMember ._id);
undecidedMember = await User.findById(undecidedMember._id);
expect(nonParticipatingMember.party.quest.key).to.not.eql('whale');
expect(undecidedMember.party.quest.key).to.not.eql('whale');
});
it('sends email to participating members that quest has started', async () => {
participatingMember.preferences.emailNotifications.questStarted = true;
questLeader.preferences.emailNotifications.questStarted = true;
await Promise.all([
participatingMember.save(),
questLeader.save(),
]);
await party.startQuest(nonParticipatingMember);
await sleep(0.5);
expect(email.sendTxn).to.be.calledOnce;
let memberIds = _.pluck(email.sendTxn.args[0][0], '_id');
let typeOfEmail = email.sendTxn.args[0][1];
expect(memberIds).to.have.a.lengthOf(2);
expect(memberIds).to.include(participatingMember._id);
expect(memberIds).to.include(questLeader._id);
expect(typeOfEmail).to.eql('quest-started');
});
it('sends email only to members who have not opted out', async () => {
participatingMember.preferences.emailNotifications.questStarted = false;
questLeader.preferences.emailNotifications.questStarted = true;
await Promise.all([
participatingMember.save(),
questLeader.save(),
]);
await party.startQuest(nonParticipatingMember);
await sleep(0.5);
expect(email.sendTxn).to.be.calledOnce;
let memberIds = _.pluck(email.sendTxn.args[0][0], '_id');
expect(memberIds).to.have.a.lengthOf(1);
expect(memberIds).to.not.include(participatingMember._id);
expect(memberIds).to.include(questLeader._id);
});
it('does not send email to initiating member', async () => {
participatingMember.preferences.emailNotifications.questStarted = true;
questLeader.preferences.emailNotifications.questStarted = true;
await Promise.all([
participatingMember.save(),
questLeader.save(),
]);
await party.startQuest(participatingMember);
await sleep(0.5);
expect(email.sendTxn).to.be.calledOnce;
let memberIds = _.pluck(email.sendTxn.args[0][0], '_id');
expect(memberIds).to.have.a.lengthOf(1);
expect(memberIds).to.not.include(participatingMember._id);
expect(memberIds).to.include(questLeader._id);
});
it('updates participting members (not including user)', async () => {
sandbox.spy(User, 'update');
await party.startQuest(nonParticipatingMember);
let members = [questLeader._id, participatingMember._id];
expect(User.update).to.be.calledWith(
{ _id: { $in: members } },
{
$set: {
'party.quest.key': 'whale',
'party.quest.progress.down': 0,
'party.quest.progress.collect': {},
'party.quest.completed': null,
},
}
);
});
it('updates non-user quest leader and decrements quest scroll', async () => {
sandbox.spy(User, 'update');
await party.startQuest(participatingMember);
expect(User.update).to.be.calledWith(
{ _id: questLeader._id },
{
$inc: {
'items.quests.whale': -1,
},
}
);
});
it('modifies the participating initiating user directly', async () => {
await party.startQuest(participatingMember);
let userQuest = participatingMember.party.quest;
expect(userQuest.key).to.eql('whale');
expect(userQuest.progress.down).to.eql(0);
expect(userQuest.progress.collect).to.eql({});
expect(userQuest.completed).to.eql(null);
});
it('does not modify user if not participating', async () => {
await party.startQuest(nonParticipatingMember);
expect(nonParticipatingMember.party.quest.key).to.not.eql('whale');
});
it('removes the quest directly if initiating user is the quest leader', async () => {
await party.startQuest(questLeader);
expect(questLeader.items.quests.whale).to.eql(0);
});
});
});
});
});
+74
View File
@@ -0,0 +1,74 @@
import { model as Challenge } from '../../../../../website/server/models/challenge';
import { model as Group } from '../../../../../website/server/models/group';
import { model as User } from '../../../../../website/server/models/user';
import * as Tasks from '../../../../../website/server/models/task';
import { each } from 'lodash';
import { generateHistory } from '../../../../helpers/api-unit.helper.js';
describe('Task Model', () => {
let guild, leader, challenge, task;
let tasksToTest = {
habit: {
text: 'test habit',
type: 'habit',
up: false,
down: true,
},
daily: {
text: 'test daily',
type: 'daily',
frequency: 'daily',
everyX: 5,
startDate: new Date(),
},
};
beforeEach(async () => {
guild = new Group({
name: 'test guild',
type: 'guild',
});
leader = new User({
guilds: [guild._id],
});
guild.leader = leader._id;
challenge = new Challenge({
name: 'Test Challenge',
shortName: 'Test',
leader: leader._id,
group: guild._id,
});
leader.challenges = [challenge._id];
await Promise.all([
guild.save(),
leader.save(),
challenge.save(),
]);
});
each(tasksToTest, (taskValue, taskType) => {
context(`${taskType}`, () => {
beforeEach(async() => {
task = new Tasks[`${taskType}`](Tasks.Task.sanitize(taskValue));
task.challenge.id = challenge._id;
task.history = generateHistory(396);
await task.save();
});
it('preens challenge tasks history when scored', async () => {
let historyLengthBeforePreen = task.history.length;
await task.scoreChallengeTask(1.2);
let updatedTask = await Tasks.Task.findOne({_id: task._id});
expect(historyLengthBeforePreen).to.be.greaterThan(updatedTask.history.length);
});
});
});
});
+33
View File
@@ -0,0 +1,33 @@
import { model as User } from '../../../../../website/server/models/user';
describe('User Model', () => {
it('keeps user._tmp when calling .toJSON', () => {
let user = new User({
auth: {
local: {
username: 'username',
lowerCaseUsername: 'username',
email: 'email@email.email',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
},
},
});
user._tmp = {ok: true};
user._nonTmp = {ok: true};
expect(user._tmp).to.eql({ok: true});
expect(user._nonTmp).to.eql({ok: true});
let toObject = user.toObject();
let toJSON = user.toJSON();
expect(toObject).to.not.have.keys('_tmp');
expect(toObject).to.not.have.keys('_nonTmp');
expect(toJSON).to.have.any.key('_tmp');
expect(toJSON._tmp).to.eql({ok: true});
expect(toJSON).to.not.have.keys('_nonTmp');
});
});