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:
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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'),
|
||||
},
|
||||
},
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
@@ -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},
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user