diff --git a/common/locales/en/api-v3.json b/common/locales/en/api-v3.json index 5ab3b3399e..41fca419f9 100644 --- a/common/locales/en/api-v3.json +++ b/common/locales/en/api-v3.json @@ -149,7 +149,18 @@ "couponCodeRequired": "The coupon code is required.", "eventRequired": "\"req.params.event\" is required.", "countRequired": "\"req.query.count\" is required.", + "invalidUrl": "invalid url", + "invalidEnabled": "the \"enabled\" parameter should be a boolean", "petsReleased": "Pets released.", "mountsAndPetsReleased": "Mounts and pets released", - "mountsReleased": "Mounts released" + "mountsReleased": "Mounts released", + "typeNotSellable": "Type is not sellable. Must be one of the following <%= acceptedTypes %>", + "userItemsKeyNotFound": "Key not found for user.items <%= type %>", + "sold": "You sold a <%= key %> <%= type %>", + "pathRequired": "Path string is required", + "unlocked": "Items have been unlocked", + "alreadyUnlocked": "Item already unlocked", + "cannotRevive": "Cannot revive if not dead", + "rebirthComplete": "You have been reborn!", + "petNotOwned": "You do not own this pet." } diff --git a/common/script/index.js b/common/script/index.js index 0d376bd4f2..182e356a03 100644 --- a/common/script/index.js +++ b/common/script/index.js @@ -103,11 +103,18 @@ import purchase from './ops/purchase'; import purchaseHourglass from './ops/hourglassPurchase'; import readCard from './ops/readCard'; import openMysteryItem from './ops/openMysteryItem'; +import addWebhook from './ops/addWebhook'; +import updateWebhook from './ops/updateWebhook'; +import deleteWebhook from './ops/deleteWebhook'; import releasePets from './ops/releasePets'; import releaseBoth from './ops/releaseBoth'; import releaseMounts from './ops/releaseMounts'; import updateTask from './ops/updateTask'; import clearCompleted from './ops/clearCompleted'; +import sell from './ops/sell'; +import unlock from './ops/unlock'; +import revive from './ops/revive'; +import rebirth from './ops/rebirth'; api.ops = { scoreTask, @@ -127,11 +134,18 @@ api.ops = { purchaseHourglass, readCard, openMysteryItem, + addWebhook, + updateWebhook, + deleteWebhook, releasePets, releaseBoth, releaseMounts, updateTask, clearCompleted, + sell, + unlock, + revive, + rebirth, }; import handleTwoHanded from './fns/handleTwoHanded'; diff --git a/common/script/ops/addWebhook.js b/common/script/ops/addWebhook.js index 99eaf49bba..ea6b32ed68 100644 --- a/common/script/ops/addWebhook.js +++ b/common/script/ops/addWebhook.js @@ -1,15 +1,22 @@ import refPush from '../libs/refPush'; +import validator from 'validator'; +import i18n from '../i18n'; +import { + BadRequest, +} from '../libs/errors'; +import _ from 'lodash'; -module.exports = function(user, req, cb) { - var wh; +module.exports = function addWebhook (user, req = {}) { + let wh; wh = user.preferences.webhooks; - refPush(wh, { + + if (!validator.isURL(_.get(req, 'body.url'))) throw new BadRequest(i18n.t('invalidUrl', req.language)); + if (!validator.isBoolean(_.get(req, 'body.enabled'))) throw new BadRequest(i18n.t('invalidEnabled', req.language)); + + user.markModified('preferences.webhooks'); + + return refPush(wh, { url: req.body.url, - enabled: req.body.enabled || true, - id: req.body.id + enabled: req.body.enabled, }); - if (typeof user.markModified === "function") { - user.markModified('preferences.webhooks'); - } - return typeof cb === "function" ? cb(null, user.preferences.webhooks) : void 0; }; diff --git a/common/script/ops/deleteWebhook.js b/common/script/ops/deleteWebhook.js index a187f08770..03eb622892 100644 --- a/common/script/ops/deleteWebhook.js +++ b/common/script/ops/deleteWebhook.js @@ -1,7 +1,6 @@ -module.exports = function(user, req, cb) { - delete user.preferences.webhooks[req.params.id]; - if (typeof user.markModified === "function") { - user.markModified('preferences.webhooks'); - } - return typeof cb === "function" ? cb(null, user.preferences.webhooks) : void 0; +import _ from 'lodash'; + +module.exports = function deleteWebhook (user, req) { + delete user.preferences.webhooks[_.get(req, 'params.id')]; + user.markModified('preferences.webhooks'); }; diff --git a/common/script/ops/rebirth.js b/common/script/ops/rebirth.js index 1ddb75aba1..3739f2c80b 100644 --- a/common/script/ops/rebirth.js +++ b/common/script/ops/rebirth.js @@ -1,21 +1,30 @@ -import content from '../content/index'; import i18n from '../i18n'; import _ from 'lodash'; import { capByLevel } from '../statHelpers'; import { MAX_LEVEL } from '../constants'; +import { + NotAuthorized, +} from '../libs/errors'; +import resetGear from './resetGear'; +import equip from './equip'; + +const USERSTATSLIST = ['per', 'int', 'con', 'str', 'points', 'gp', 'exp', 'mp']; + +module.exports = function rebirth (user, tasks = [], req = {}, analytics) { + let analyticsData; + let flags; + let lvl; + let stats; -module.exports = function(user, req, cb, analytics) { - var analyticsData, flags, gear, lvl, stats; if (user.balance < 2 && user.stats.lvl < MAX_LEVEL) { - return typeof cb === "function" ? cb({ - code: 401, - message: i18n.t('notEnoughGems', req.language) - }) : void 0; + throw new NotAuthorized(i18n.t('notEnoughGems', req.language)); } + analyticsData = { uuid: user._id, - category: 'behavior' + category: 'behavior', }; + if (user.stats.lvl < MAX_LEVEL) { user.balance -= 2; analyticsData.acquireMethod = 'Gems'; @@ -24,62 +33,52 @@ module.exports = function(user, req, cb, analytics) { analyticsData.gemCost = 0; analyticsData.acquireMethod = '> 100'; } - if (analytics != null) { + + if (analytics) { analytics.track('Rebirth', analyticsData); } + lvl = capByLevel(user.stats.lvl); - _.each(user.tasks, function(task) { + + _.each(tasks, function resetTasks (task) { if (task.type !== 'reward') { task.value = 0; } if (task.type === 'daily') { - return task.streak = 0; + task.streak = 0; } }); + stats = user.stats; stats.buffs = {}; stats.hp = 50; stats.lvl = 1; - stats["class"] = 'warrior'; - _.each(['per', 'int', 'con', 'str', 'points', 'gp', 'exp', 'mp'], function(value) { - return stats[value] = 0; - }); - // TODO during refactoring: move all gear code from rebirth() to its own function and then call it in reset() as well - gear = user.items.gear; - _.each(['equipped', 'costume'], function(type) { - gear[type] = {}; - gear[type].armor = 'armor_base_0'; - gear[type].weapon = 'weapon_warrior_0'; - gear[type].head = 'head_base_0'; - return gear[type].shield = 'shield_base_0'; + stats.class = 'warrior'; + + _.each(USERSTATSLIST, function resetStats (value) { + stats[value] = 0; }); + + resetGear(user); + if (user.items.currentPet) { - user.ops.equip({ + equip(user, { params: { type: 'pet', - key: user.items.currentPet - } + key: user.items.currentPet, + }, }); } + if (user.items.currentMount) { - user.ops.equip({ + equip(user, { params: { type: 'mount', - key: user.items.currentMount - } + key: user.items.currentMount, + }, }); } - _.each(gear.owned, function(v, k) { - if (gear.owned[k] && content.gear.flat[k].value) { - gear.owned[k] = false; - return true; - } - }); - gear.owned.weapon_warrior_0 = true; - if (typeof user.markModified === "function") { - user.markModified('items.gear.owned'); - } - user.preferences.costume = false; + flags = user.flags; if (!user.achievements.beastMaster) { flags.rebirthEnabled = false; @@ -88,13 +87,21 @@ module.exports = function(user, req, cb, analytics) { flags.dropsEnabled = false; flags.classSelected = false; flags.levelDrops = {}; + if (!user.achievements.rebirths) { user.achievements.rebirths = 1; user.achievements.rebirthLevel = lvl; - } else if (lvl > user.achievements.rebirthLevel || lvl === 100) { + } else if (lvl > user.achievements.rebirthLevel || lvl === MAX_LEVEL) { user.achievements.rebirths++; user.achievements.rebirthLevel = lvl; } + user.stats.buffs = {}; - return typeof cb === "function" ? cb(null, user) : void 0; + + let response = { + data: user, + message: i18n.t('rebirthComplete'), + }; + + return response; }; diff --git a/common/script/ops/resetGear.js b/common/script/ops/resetGear.js new file mode 100644 index 0000000000..2625f5c9b8 --- /dev/null +++ b/common/script/ops/resetGear.js @@ -0,0 +1,25 @@ +import _ from 'lodash'; +import content from '../content/index'; + +module.exports = function resetGear (user) { + let gear = user.items.gear; + + _.each(['equipped', 'costume'], function resetUserGear (type) { + gear[type] = {}; + gear[type].armor = 'armor_base_0'; + gear[type].weapon = 'weapon_warrior_0'; + gear[type].head = 'head_base_0'; + gear[type].shield = 'shield_base_0'; + }); + + // Gear.owned is a Mongo object so the _.each function iterates over hidden properties. + // The content.gear.flat[k] check should prevent this causing an error + _.each(gear.owned, function resetOwnedGear (v, k) { + if (gear.owned[k] && content.gear.flat[k] && content.gear.flat[k].value) { + gear.owned[k] = false; + } + }); + + gear.owned.weapon_warrior_0 = true; // eslint-disable-line camelcase + user.preferences.costume = false; +}; diff --git a/common/script/ops/revive.js b/common/script/ops/revive.js index 7b6fe8445f..fa33946943 100644 --- a/common/script/ops/revive.js +++ b/common/script/ops/revive.js @@ -1,72 +1,106 @@ import content from '../content/index'; import i18n from '../i18n'; import _ from 'lodash'; +import { + NotAuthorized, +} from '../libs/errors'; +import splitWhitespace from '../libs/splitWhitespace'; +import randomVal from '../fns/randomVal'; -module.exports = function(user, req, cb, analytics) { - var analyticsData, base, cl, gearOwned, item, losableItems, lostItem, lostStat; - if (!(user.stats.hp <= 0)) { - return typeof cb === "function" ? cb({ - code: 400, - message: "Cannot revive if not dead" - }) : void 0; +module.exports = function revive (user, req = {}, analytics) { + if (user.stats.hp > 0) { + throw new NotAuthorized(i18n.t('cannotRevive', req.language)); } + _.merge(user.stats, { hp: 50, exp: 0, - gp: 0 + gp: 0, }); + if (user.stats.lvl > 1) { user.stats.lvl--; } - lostStat = user.fns.randomVal(_.reduce(['str', 'con', 'per', 'int'], (function(m, k) { + + let lostStat = randomVal(user, _.reduce(['str', 'con', 'per', 'int'], function findRandomStat (m, k) { if (user.stats[k]) { m[k] = k; } return m; - }), {})); + }, {})); + if (lostStat) { user.stats[lostStat]--; } - cl = user.stats["class"]; - gearOwned = (typeof (base = user.items.gear.owned).toObject === "function" ? base.toObject() : void 0) || user.items.gear.owned; - losableItems = {}; - _.each(gearOwned, function(v, k) { - var itm; - if (v) { - itm = content.gear.flat['' + k]; + + let base = user.items.gear.owned; + let gearOwned; + + if (typeof base.toObject === 'function') { + gearOwned = base.toObject(); + } else { + gearOwned = user.items.gear.owned; + } + + let losableItems = {}; + let userClass = user.stats.class; + + _.each(gearOwned, function findLosableItems (value, key) { + let itm; + if (value) { + itm = content.gear.flat[key]; + if (itm) { - if ((itm.value > 0 || k === 'weapon_warrior_0') && (itm.klass === cl || (itm.klass === 'special' && (!itm.specialClass || itm.specialClass === cl)) || itm.klass === 'armoire')) { - return losableItems['' + k] = '' + k; + let itemHasValueOrWarrior0 = itm.value > 0 || key === 'weapon_warrior_0'; + + let itemClassEqualsUserClass = itm.klass === userClass; + + let itemClassSpecial = itm.klass === 'special'; + let itemNotSpecialOrUserClassIsSpecial = !itm.specialClass || itm.specialClass === userClass; + let itemIsSpecial = itemNotSpecialOrUserClassIsSpecial && itemClassSpecial; + + let itemIsArmoire = itm.klass === 'armoire'; + + if (itemHasValueOrWarrior0 && (itemClassEqualsUserClass || itemIsSpecial || itemIsArmoire)) { + losableItems[key] = key; + return losableItems[key]; } } } }); - lostItem = user.fns.randomVal(losableItems); - if (item = content.gear.flat[lostItem]) { + + let lostItem = randomVal(user, losableItems); + + let message = ''; + let item = content.gear.flat[lostItem]; + + if (item) { user.items.gear.owned[lostItem] = false; + if (user.items.gear.equipped[item.type] === lostItem) { - user.items.gear.equipped[item.type] = item.type + "_base_0"; + user.items.gear.equipped[item.type] = `${item.type}_base_0`; } + if (user.items.gear.costume[item.type] === lostItem) { - user.items.gear.costume[item.type] = item.type + "_base_0"; + user.items.gear.costume[item.type] = `${item.type}_base_0`; } + + message = i18n.t('messageLostItem', { itemText: item.text(req.language)}, req.language); } - if (typeof user.markModified === "function") { - user.markModified('items.gear'); + + if (analytics) { + analytics.track('Death', { + uuid: user._id, + lostItem, + gaLabel: lostItem, + category: 'behavior', + }); } - analyticsData = { - uuid: user._id, - lostItem: lostItem, - gaLabel: lostItem, - category: 'behavior' + + let response = { + data: _.pick(user, splitWhitespace('user.items')), + message, }; - if (analytics != null) { - analytics.track('Death', analyticsData); - } - return typeof cb === "function" ? cb((item ? { - code: 200, - message: i18n.t('messageLostItem', { - itemText: item.text(req.language) - }, req.language) - } : null), user) : void 0; + + return response; }; diff --git a/common/script/ops/sell.js b/common/script/ops/sell.js index 33e5f7d673..d5dfa9fbbb 100644 --- a/common/script/ops/sell.js +++ b/common/script/ops/sell.js @@ -1,23 +1,42 @@ import content from '../content/index'; +import i18n from '../../../common/script/i18n'; import _ from 'lodash'; import splitWhitespace from '../libs/splitWhitespace'; +import { + NotFound, + NotAuthorized, + BadRequest, +} from '../libs/errors'; -module.exports = function(user, req, cb) { - var key, ref, type; - ref = req.params, key = ref.key, type = ref.type; - if (type !== 'eggs' && type !== 'hatchingPotions' && type !== 'food') { - return typeof cb === "function" ? cb({ - code: 404, - message: ":type not found. Must bes in [eggs, hatchingPotions, food]" - }) : void 0; +const ACCEPTEDTYPES = ['eggs', 'hatchingPotions', 'food']; + +module.exports = function sell (user, req = {}) { + let key = _.get(req.params, 'key'); + let type = _.get(req.params, 'type'); + + if (!type) { + throw new BadRequest(i18n.t('typeRequired', req.language)); } + + if (!key) { + throw new BadRequest(i18n.t('keyRequired', req.language)); + } + + if (ACCEPTEDTYPES.indexOf(type) === -1) { + throw new NotAuthorized(i18n.t('typeNotSellable', {acceptedTypes: ACCEPTEDTYPES.join(', ')}, req.language)); + } + if (!user.items[type][key]) { - return typeof cb === "function" ? cb({ - code: 404, - message: ":key not found for user.items." + type - }) : void 0; + throw new NotFound(i18n.t('userItemsKeyNotFound', {type}, req.language)); } + user.items[type][key]--; user.stats.gp += content[type][key].value; - return typeof cb === "function" ? cb(null, _.pick(user, splitWhitespace('stats items'))) : void 0; + + let response = { + data: _.pick(user, splitWhitespace('stats items')), + message: i18n.t('sold', {type, key}), + }; + + return response; }; diff --git a/common/script/ops/unlock.js b/common/script/ops/unlock.js index c53a5997ec..bbd3cb0e39 100644 --- a/common/script/ops/unlock.js +++ b/common/script/ops/unlock.js @@ -1,63 +1,85 @@ import i18n from '../i18n'; import _ from 'lodash'; import splitWhitespace from '../libs/splitWhitespace'; +import dotSet from '../libs/dotSet'; +import { + NotAuthorized, + BadRequest, +} from '../libs/errors'; -module.exports = function(user, req, cb, analytics) { - var alreadyOwns, analyticsData, cost, fullSet, k, path, split, v; - path = req.query.path; - fullSet = ~path.indexOf(","); - cost = ~path.indexOf('background.') ? fullSet ? 3.75 : 1.75 : fullSet ? 1.25 : 0.5; - alreadyOwns = !fullSet && user.fns.dotGet("purchased." + path) === true; - if ((user.balance < cost || !user.balance) && !alreadyOwns) { - return typeof cb === "function" ? cb({ - code: 401, - message: i18n.t('notEnoughGems', req.language) - }) : void 0; +module.exports = function unlock (user, req = {}, analytics) { + let path = _.get(req.query, 'path'); + + if (!path) { + throw new BadRequest(i18n.t('pathRequired', req.language)); } - if (fullSet) { - _.each(path.split(","), function(p) { - if (~path.indexOf('gear.')) { - user.fns.dotSet("" + p, true); - true; - } else { + let isFullSet = path.indexOf(',') !== -1; + let cost; + let isBackground = path.indexOf('background.') !== -1; + + if (isBackground && isFullSet) { + cost = 3.75; + } else if (isBackground) { + cost = 1.75; + } else if (isFullSet) { + cost = 1.25; + } else { + cost = 0.5; + } + + let alreadyOwns = !isFullSet && user.fns.dotGet(`purchased.${path}`) === true; + + if ((!user.balance || user.balance < cost) && !alreadyOwns) { + throw new NotAuthorized(i18n.t('notEnoughGems', req.language)); + } + + if (isFullSet) { + _.each(path.split(','), function markItemsAsPurchased (pathPart) { + if (path.indexOf('gear.') !== -1) { + dotSet(user, pathPart, true); + return true; } - user.fns.dotSet("purchased." + p, true); + + dotSet(user, `purchased.${pathPart}`, true); return true; }); } else { if (alreadyOwns) { - split = path.split('.'); - v = split.pop(); - k = split.join('.'); - if (k === 'background' && v === user.preferences.background) { - v = ''; + let split = path.split('.'); + let value = split.pop(); + let key = split.join('.'); + if (key === 'background' && value === user.preferences.background) { + value = ''; } - user.fns.dotSet("preferences." + k, v); - return typeof cb === "function" ? cb(null, req) : void 0; + dotSet(user, `preferences.${key}`, value); + + throw new NotAuthorized(i18n.t('alreadyUnlocked', req.language)); } - user.fns.dotSet("purchased." + path, true); + dotSet(user, `purchased.${path}`, true); } + + if (path.indexOf('gear.') === -1) { + user.markModified('purchased'); + } + user.balance -= cost; - if (~path.indexOf('gear.')) { - if (typeof user.markModified === "function") { - user.markModified('gear.owned'); - } - } else { - if (typeof user.markModified === "function") { - user.markModified('purchased'); - } + + if (analytics) { + analytics.track('acquire item', { + uuid: user._id, + itemKey: path, + itemType: 'customization', + acquireMethod: 'Gems', + gemCost: cost / 0.25, + category: 'behavior', + }); } - analyticsData = { - uuid: user._id, - itemKey: path, - itemType: 'customization', - acquireMethod: 'Gems', - gemCost: cost / .25, - category: 'behavior' + + let response = { + data: _.pick(user, splitWhitespace('purchased preferences items')), + message: i18n.t('unlocked'), }; - if (analytics != null) { - analytics.track('acquire item', analyticsData); - } - return typeof cb === "function" ? cb(null, _.pick(user, splitWhitespace('purchased preferences items'))) : void 0; + + return response; }; diff --git a/common/script/ops/updateWebhook.js b/common/script/ops/updateWebhook.js index e2775a40b4..0de2f5fa47 100644 --- a/common/script/ops/updateWebhook.js +++ b/common/script/ops/updateWebhook.js @@ -1,9 +1,16 @@ -import _ from 'lodash'; +import validator from 'validator'; +import i18n from '../i18n'; +import { + BadRequest, +} from '../libs/errors'; -module.exports = function(user, req, cb) { - _.merge(user.preferences.webhooks[req.params.id], req.body); - if (typeof user.markModified === "function") { - user.markModified('preferences.webhooks'); - } - return typeof cb === "function" ? cb(null, user.preferences.webhooks) : void 0; +module.exports = function updateWebhook (user, req) { + if (!validator.isURL(req.body.url)) throw new BadRequest(i18n.t('invalidUrl', req.language)); + if (!validator.isBoolean(req.body.enabled)) throw new BadRequest(i18n.t('invalidEnabled', req.language)); + + user.markModified('preferences.webhooks'); + user.preferences.webhooks[req.params.id].url = req.body.url; + user.preferences.webhooks[req.params.id].enabled = req.body.enabled; + + return user.preferences.webhooks[req.params.id]; }; diff --git a/tasks/gulp-eslint.js b/tasks/gulp-eslint.js index 90003ac77f..10d953ab28 100644 --- a/tasks/gulp-eslint.js +++ b/tasks/gulp-eslint.js @@ -11,22 +11,14 @@ const COMMON_FILES = [ // @TODO remove these negations as the files are converted over. '!./common/script/content/index.js', '!./common/script/ops/addPushDevice.js', - '!./common/script/ops/addWebhook.js', '!./common/script/ops/blockUser.js', '!./common/script/ops/clearPMs.js', '!./common/script/ops/deletePM.js', - '!./common/script/ops/deleteWebhook.js', - '!./common/script/ops/rebirth.js', '!./common/script/ops/releaseBoth.js', '!./common/script/ops/releaseMounts.js', '!./common/script/ops/releasePets.js', '!./common/script/ops/reroll.js', '!./common/script/ops/reset.js', - '!./common/script/ops/revive.js', - '!./common/script/ops/sell.js', - '!./common/script/ops/unlock.js', - '!./common/script/ops/update.js', - '!./common/script/ops/updateWebhook.js', '!./common/script/fns/crit.js', '!./common/script/fns/randomDrop.js', '!./common/script/libs/appliedTags.js', diff --git a/tasks/gulp-tests.js b/tasks/gulp-tests.js index 93f92ca5a2..9cb654ce21 100644 --- a/tasks/gulp-tests.js +++ b/tasks/gulp-tests.js @@ -368,6 +368,10 @@ gulp.task('test:api-v3:integration', (done) => { pipe(runner); }); +gulp.task('test:api-v3:integration:watch', () => { + gulp.watch(['website/src/controllers/api-v3/**/*', 'test/api/v3/integration/**/*', 'common/script/ops/*'], ['test:api-v3:integration']); +}); + gulp.task('test:api-v3:integration:separate-server', (done) => { let runner = exec( testBin('mocha test/api/v3/integration --recursive', 'LOAD_SERVER=0'), diff --git a/test/api/v3/integration/user/DELETE-user_delete_webhook.test.js b/test/api/v3/integration/user/DELETE-user_delete_webhook.test.js new file mode 100644 index 0000000000..46844dd855 --- /dev/null +++ b/test/api/v3/integration/user/DELETE-user_delete_webhook.test.js @@ -0,0 +1,23 @@ +import { + generateUser, +} from '../../../../helpers/api-integration/v3'; + +let user; +let endpoint = '/user/webhook'; + +describe('DELETE /user/webhook', () => { + beforeEach(async () => { + user = await generateUser(); + }); + + it('succeeds', async () => { + let id = 'some-id'; + user.preferences.webhooks[id] = { url: 'http://some-url.com', enabled: true }; + await user.sync(); + expect(user.preferences.webhooks).to.eql({}); + let response = await user.del(`${endpoint}/${id}`); + expect(response).to.eql({}); + await user.sync(); + expect(user.preferences.webhooks).to.eql({}); + }); +}); diff --git a/test/api/v3/integration/user/POST-user_add_webhook.test.js b/test/api/v3/integration/user/POST-user_add_webhook.test.js new file mode 100644 index 0000000000..d13f15baa4 --- /dev/null +++ b/test/api/v3/integration/user/POST-user_add_webhook.test.js @@ -0,0 +1,29 @@ +import { + generateUser, + translate as t, +} from '../../../../helpers/api-integration/v3'; + +let user; +let endpoint = '/user/webhook'; + +describe('POST /user/webhook', () => { + beforeEach(async () => { + user = await generateUser(); + }); + + it('validates', async () => { + await expect(user.post(endpoint, { enabled: true })).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('invalidUrl'), + }); + }); + + it('successfully adds the webhook', async () => { + expect(user.preferences.webhooks).to.eql({}); + let response = await user.post(endpoint, { enabled: true, url: 'http://some-url.com'}); + expect(response.id).to.exist; + await user.sync(); + expect(user.preferences.webhooks).to.not.eql({}); + }); +}); diff --git a/test/api/v3/integration/user/POST-user_rebirth.test.js b/test/api/v3/integration/user/POST-user_rebirth.test.js new file mode 100644 index 0000000000..3591844c3c --- /dev/null +++ b/test/api/v3/integration/user/POST-user_rebirth.test.js @@ -0,0 +1,56 @@ +import { + generateUser, + generateDaily, + generateReward, + translate as t, +} from '../../../../helpers/api-integration/v3'; + +describe('POST /user/rebirth', () => { + let user; + + beforeEach(async () => { + user = await generateUser(); + }); + + it('returns an error when user balance is too low', async () => { + await expect(user.post('/user/rebirth')) + .to.eventually.be.rejected.and.to.eql({ + code: 401, + error: 'NotAuthorized', + message: t('notEnoughGems'), + }); + }); + + // More tests in common code unit tests + + it('resets user\'s tasks', async () => { + await user.update({ + balance: 2, + }); + + let daily = await generateDaily({ + text: 'test habit', + type: 'daily', + streak: 1, + userId: user._id, + }); + + let reward = await generateReward({ + text: 'test reward', + type: 'reward', + value: 1, + userId: user._id, + }); + + let response = await user.post('/user/rebirth'); + await user.sync(); + + let updatedDaily = await user.get(`/tasks/${daily._id}`); + let updatedReward = await user.get(`/tasks/${reward._id}`); + + expect(response.message).to.equal(t('rebirthComplete')); + expect(updatedDaily.streak).to.equal(0); + expect(updatedDaily.value).to.equal(0); + expect(updatedReward.value).to.equal(1); + }); +}); diff --git a/test/api/v3/integration/user/POST-user_revive.test.js b/test/api/v3/integration/user/POST-user_revive.test.js new file mode 100644 index 0000000000..6ba85ac87f --- /dev/null +++ b/test/api/v3/integration/user/POST-user_revive.test.js @@ -0,0 +1,37 @@ +import { + generateUser, + translate as t, +} from '../../../../helpers/api-integration/v3'; + +describe('POST /user/revive', () => { + let user; + + beforeEach(async () => { + user = await generateUser({ + 'user.items.gear.owned': {weaponKey: true}, + }); + }); + + it('returns an error when user is not dead', async () => { + await expect(user.post('/user/revive')) + .to.eventually.be.rejected.and.to.eql({ + code: 401, + error: 'NotAuthorized', + message: t('cannotRevive'), + }); + }); + + // More tests in common code unit tests + + it('decreases a stat', async () => { + await user.update({ + 'stats.str': 2, + 'stats.hp': 0, + }); + + await user.post('/user/revive'); + await user.sync(); + + expect(user.stats.str).to.equal(1); + }); +}); diff --git a/test/api/v3/integration/user/POST-user_sell.test.js b/test/api/v3/integration/user/POST-user_sell.test.js new file mode 100644 index 0000000000..be8d55c932 --- /dev/null +++ b/test/api/v3/integration/user/POST-user_sell.test.js @@ -0,0 +1,42 @@ +import { + generateUser, + translate as t, +} from '../../../../helpers/api-integration/v3'; +import content from '../../../../../common/script/content'; + +describe('POST /user/sell/:type/:key', () => { + let user; + let type = 'eggs'; + let key = 'Wolf'; + + beforeEach(async () => { + user = await generateUser(); + }); + + // More tests in common code unit tests + + it('returns an error when user does not have item', async () => { + await expect(user.post(`/user/sell/${type}/${key}`)) + .to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: t('userItemsKeyNotFound', {type}), + }); + }); + + it('sells an item', async () => { + await user.update({ + items: { + eggs: { + Wolf: 1, + }, + }, + }); + + let response = await user.post(`/user/sell/${type}/${key}`); + await user.sync(); + + expect(response.message).to.equal(t('sold', {type, key})); + expect(user.stats.gp).to.equal(content[type][key].value); + }); +}); diff --git a/test/api/v3/integration/user/POST-user_unlock.js b/test/api/v3/integration/user/POST-user_unlock.js new file mode 100644 index 0000000000..6dbdb3c1b1 --- /dev/null +++ b/test/api/v3/integration/user/POST-user_unlock.js @@ -0,0 +1,37 @@ +import { + generateUser, + translate as t, +} from '../../../../helpers/api-integration/v3'; + +describe('POST /user/unlock', () => { + let user; + let unlockPath = 'shirt.convict,shirt.cross,shirt.fire,shirt.horizon,shirt.ocean,shirt.purple,shirt.rainbow,shirt.redblue,shirt.thunder,shirt.tropical,shirt.zombie'; + let unlockCost = 1.25; + let usersStartingGems = 5; + + beforeEach(async () => { + user = await generateUser(); + }); + + it('returns an error when user balance is too low', async () => { + await expect(user.post(`/user/unlock?path=${unlockPath}`)) + .to.eventually.be.rejected.and.to.eql({ + code: 401, + error: 'NotAuthorized', + message: t('notEnoughGems'), + }); + }); + + // More tests in common code unit tests + + it('reduces a user\'s balance', async () => { + await user.update({ + balance: usersStartingGems, + }); + let response = await user.post(`/user/unlock?path=${unlockPath}`); + await user.sync(); + + expect(response.message).to.equal(t('unlocked')); + expect(user.balance).to.equal(usersStartingGems - unlockCost); + }); +}); diff --git a/test/api/v3/integration/user/PUT-user_update_webhook.test.js b/test/api/v3/integration/user/PUT-user_update_webhook.test.js new file mode 100644 index 0000000000..13ca9ff00c --- /dev/null +++ b/test/api/v3/integration/user/PUT-user_update_webhook.test.js @@ -0,0 +1,32 @@ +import { + generateUser, + translate as t, +} from '../../../../helpers/api-integration/v3'; + +let user; +let url = 'http://new-url.com'; +let enabled = true; + +describe('PUT /user/webhook/:id', () => { + beforeEach(async () => { + user = await generateUser(); + }); + + it('validation fails', async () => { + await expect(user.put('/user/webhook/some-id'), { enabled: true }).to.eventually.be.rejected.and.eql({ + code: 400, + error: 'BadRequest', + message: t('invalidUrl'), + }); + }); + + it('succeeds', async () => { + let response = await user.post('/user/webhook', { enabled: true, url: 'http://some-url.com'}); + await user.sync(); + expect(user.preferences.webhooks[response.id].url).to.not.eql(url); + let response2 = await user.put(`/user/webhook/${response.id}`, {url, enabled}); + expect(response2.url).to.eql(url); + await user.sync(); + expect(user.preferences.webhooks[response.id].url).to.eql(url); + }); +}); diff --git a/test/common/ops/addWebhook.test.js b/test/common/ops/addWebhook.test.js new file mode 100644 index 0000000000..11d26e622b --- /dev/null +++ b/test/common/ops/addWebhook.test.js @@ -0,0 +1,57 @@ +import addWebhook from '../../../common/script/ops/addWebhook'; +import { + BadRequest, +} from '../../../common/script/libs/errors'; +import i18n from '../../../common/script/i18n'; +import { + generateUser, +} from '../../helpers/common.helper'; + +describe('shared.ops.addWebhook', () => { + let user; + let req; + + beforeEach(() => { + user = generateUser(); + req = { body: { + enabled: true, + url: 'http://some-url.com', + } }; + }); + + context('adds webhook', () => { + it('validates req.body.url', (done) => { + delete req.body.url; + try { + addWebhook(user, req); + } catch (err) { + expect(err).to.be.an.instanceof(BadRequest); + expect(err.message).to.equal(i18n.t('invalidUrl')); + done(); + } + }); + + it('validates req.body.enabled', (done) => { + delete req.body.enabled; + try { + addWebhook(user, req); + } catch (err) { + expect(err).to.be.an.instanceof(BadRequest); + expect(err.message).to.equal(i18n.t('invalidEnabled')); + done(); + } + }); + + it('calls marksModified()', () => { + user.markModified = sinon.spy(); + addWebhook(user, req); + expect(user.markModified.called).to.eql(true); + }); + + it('succeeds', () => { + expect(user.preferences.webhooks).to.eql({}); + addWebhook(user, req); + expect(user.preferences.webhooks).to.not.eql({}); + }); + }); +}); diff --git a/test/common/ops/deleteWebhook.test.js b/test/common/ops/deleteWebhook.test.js new file mode 100644 index 0000000000..0a3178007a --- /dev/null +++ b/test/common/ops/deleteWebhook.test.js @@ -0,0 +1,20 @@ +import deleteWebhook from '../../../common/script/ops/deleteWebhook'; +import { + generateUser, +} from '../../helpers/common.helper'; + +describe('shared.ops.deleteWebhook', () => { + let user; + let req; + + beforeEach(() => { + user = generateUser(); + req = { params: { id: 'some-id' } }; + }); + + it('succeeds', () => { + user.preferences.webhooks = { 'some-id': {} }; + deleteWebhook(user, req); + expect(user.preferences.webhooks).to.eql({}); + }); +}); diff --git a/test/common/ops/rebirth.js b/test/common/ops/rebirth.js new file mode 100644 index 0000000000..61d1334569 --- /dev/null +++ b/test/common/ops/rebirth.js @@ -0,0 +1,188 @@ +import rebirth from '../../../common/script/ops/rebirth'; +import i18n from '../../../common/script/i18n'; +import { MAX_LEVEL } from '../../../common/script/constants'; +import { + generateUser, + generateDaily, + generateReward, +} from '../../helpers/common.helper'; +import { + NotAuthorized, +} from '../../../common/script/libs/errors'; + +describe('shared.ops.rebirth', () => { + let user; + let animal = 'Wolf-Base'; + let userStats = ['per', 'int', 'con', 'str', 'points', 'gp', 'exp', 'mp']; + let tasks = []; + + beforeEach(() => { + user = generateUser(); + user.balance = 2; + tasks = [generateDaily(), generateReward()]; + }); + + it('returns an error when user balance is too low and user is less than max level', (done) => { + user.balance = 0; + + try { + rebirth(user); + } catch (err) { + expect(err).to.be.an.instanceof(NotAuthorized); + expect(err.message).to.equal(i18n.t('notEnoughGems')); + done(); + } + }); + + it('rebirths a user with enough gems', () => { + let response = rebirth(user); + + expect(response.message).to.equal(i18n.t('rebirthComplete')); + }); + + it('rebirths a user with not enough gems but max level', () => { + user.balance = 0; + user.stats.lvl = MAX_LEVEL; + + let response = rebirth(user); + + expect(response.message).to.equal(i18n.t('rebirthComplete')); + }); + + it('resets user\'s taks values except for rewards to 0', () => { + tasks[0].value = 1; + tasks[1].value = 1; + + rebirth(user, tasks); + + expect(tasks[0].value).to.equal(0); + expect(tasks[1].value).to.equal(1); + }); + + it('resets user\'s daily streaks to 0', () => { + tasks[0].streak = 1; + + rebirth(user, tasks); + + expect(tasks[0].streak).to.equal(0); + }); + + it('resets a user\'s buffs', () => { + user.stats.buffs = {test: 'test'}; + + rebirth(user); + + expect(user.stats.buffs).to.be.empty; + }); + + it('resets a user\'s health points', () => { + user.stats.hp = 40; + + rebirth(user); + + expect(user.stats.hp).to.equal(50); + }); + + it('resets a user\'s class', () => { + user.stats.class = 'rouge'; + + rebirth(user); + + expect(user.stats.class).to.equal('warrior'); + }); + + it('resets a user\'s stats', () => { + user.stats.class = 'rouge'; + _.each(userStats, function setUsersStats (value) { + user.stats[value] = 10; + }); + + rebirth(user); + + _.each(userStats, function resetUserStats (value) { + user.stats[value] = 0; + }); + }); + + it('resets a user\'s gear', () => { + let gearReset = { + armor: 'armor_base_0', + weapon: 'weapon_warrior_0', + head: 'head_base_0', + shield: 'shield_base_0', + }; + + rebirth(user); + + expect(user.items.gear.equipped).to.deep.equal(gearReset); + expect(user.items.gear.costume).to.deep.equal(gearReset); + expect(user.preferences.costume).to.be.false; + }); + + it('resets a user\'s gear owned', () => { + user.items.gear.owned.weapon_warrior_1 = true; // eslint-disable-line camelcase + rebirth(user); + + expect(user.items.gear.owned.weapon_warrior_1).to.be.false; + expect(user.items.gear.owned.weapon_warrior_0).to.be.true; + }); + + it('resets a user\'s current pet', () => { + user.items.pets[animal] = true; + user.items.currentPet = animal; + rebirth(user); + + expect(user.items.currentPet).to.be.empty; + }); + + it('resets a user\'s current mount', () => { + user.items.mounts[animal] = true; + user.items.currentMount = animal; + rebirth(user); + + expect(user.items.currentMount).to.be.empty; + }); + + it('resets a user\'s flags', () => { + user.flags.itemsEnabled = true; + user.flags.dropsEnabled = true; + user.flags.classSelected = true; + user.flags.rebirthEnabled = true; + user.flags.levelDrops = {test: 'test'}; + + rebirth(user); + + expect(user.flags.itemsEnabled).to.be.false; + expect(user.flags.dropsEnabled).to.be.false; + expect(user.flags.classSelected).to.be.false; + expect(user.flags.rebirthEnabled).to.be.false; + expect(user.flags.levelDrops).to.be.emtpy; + }); + + it('does not reset rebirthEnabled if user has beastMaster', () => { + user.achievements.beastMaster = 1; + user.flags.rebirthEnabled = true; + + rebirth(user); + + expect(user.flags.rebirthEnabled).to.be.true; + }); + + it('sets rebirth achievement', () => { + rebirth(user); + + expect(user.achievements.rebirths).to.equal(1); + expect(user.achievements.rebirthLevel).to.equal(user.stats.lvl); + }); + + it('increments rebirth achievemnts', () => { + user.stats.lvl = 2; + user.achievements.rebirths = 1; + user.achievements.rebirthLevel = 1; + + rebirth(user); + + expect(user.achievements.rebirths).to.equal(2); + expect(user.achievements.rebirthLevel).to.equal(2); + }); +}); diff --git a/test/common/ops/revive.js b/test/common/ops/revive.js new file mode 100644 index 0000000000..42e1915b07 --- /dev/null +++ b/test/common/ops/revive.js @@ -0,0 +1,91 @@ +import revive from '../../../common/script/ops/revive'; +import i18n from '../../../common/script/i18n'; +import { + generateUser, +} from '../../helpers/common.helper'; +import { + NotAuthorized, +} from '../../../common/script/libs/errors'; +import content from '../../../common/script/content/index'; + +describe('shared.ops.revive', () => { + let user; + + beforeEach(() => { + user = generateUser(); + user.stats.hp = 0; + }); + + it('returns an error when user is not dead', (done) => { + user.stats.hp = 10; + + try { + revive(user); + } catch (err) { + expect(err).to.be.an.instanceof(NotAuthorized); + expect(err.message).to.equal(i18n.t('cannotRevive')); + done(); + } + }); + + it('resets user\'s hp, exp and gp', () => { + user.stats.exp = 100; + user.stats.gp = 100; + + revive(user); + + expect(user.stats.hp).to.equal(50); + expect(user.stats.exp).to.equal(0); + expect(user.stats.gp).to.equal(0); + }); + + it('decreases user\'s level', () => { + user.stats.lvl = 2; + revive(user); + + expect(user.stats.lvl).to.equal(1); + }); + + it('decreases a stat', () => { + user.stats.str = 2; + revive(user); + + expect(user.stats.str).to.equal(1); + }); + + it('removes a random item from user gear owned', () => { + let weaponKey = 'weapon_warrior_0'; + user.items.gear.owned[weaponKey] = true; + + let reviveRequest = revive(user); + + expect(reviveRequest.message).to.equal(i18n.t('messageLostItem', { itemText: content.gear.flat[weaponKey].text()})); + expect(user.items.gear.owned[weaponKey]).to.be.false; + }); + + it('removes a random item from user gear equipped', () => { + let weaponKey = 'weapon_warrior_0'; + let itemToLose = content.gear.flat[weaponKey]; + + user.items.gear.owned[weaponKey] = true; + user.items.gear.equipped[itemToLose.type] = itemToLose.key; + + let reviveRequest = revive(user); + + expect(reviveRequest.message).to.equal(i18n.t('messageLostItem', { itemText: itemToLose.text()})); + expect(user.items.gear.equipped[itemToLose.type]).to.equal(`${itemToLose.type}_base_0`); + }); + + it('removes a random item from user gear costume', () => { + let weaponKey = 'weapon_warrior_0'; + let itemToLose = content.gear.flat[weaponKey]; + + user.items.gear.owned[weaponKey] = true; + user.items.gear.costume[itemToLose.type] = itemToLose.key; + + let reviveRequest = revive(user); + + expect(reviveRequest.message).to.equal(i18n.t('messageLostItem', { itemText: itemToLose.text()})); + expect(user.items.gear.costume[itemToLose.type]).to.equal(`${itemToLose.type}_base_0`); + }); +}); diff --git a/test/common/ops/sell.js b/test/common/ops/sell.js new file mode 100644 index 0000000000..c5499b00d2 --- /dev/null +++ b/test/common/ops/sell.js @@ -0,0 +1,81 @@ +import sell from '../../../common/script/ops/sell'; +import i18n from '../../../common/script/i18n'; +import { + generateUser, +} from '../../helpers/common.helper'; +import { + NotAuthorized, + BadRequest, + NotFound, +} from '../../../common/script/libs/errors'; +import content from '../../../common/script/content/index'; + +describe('shared.ops.sell', () => { + let user; + let type = 'eggs'; + let key = 'Wolf'; + let acceptedTypes = ['eggs', 'hatchingPotions', 'food']; + + beforeEach(() => { + user = generateUser(); + user.items[type][key] = 1; + }); + + it('returns an error when type is not provided', (done) => { + try { + sell(user); + } catch (err) { + expect(err).to.be.an.instanceof(BadRequest); + expect(err.message).to.equal(i18n.t('typeRequired')); + done(); + } + }); + + it('returns an error when key is not provided', (done) => { + try { + sell(user, {params: { type } }); + } catch (err) { + expect(err).to.be.an.instanceof(BadRequest); + expect(err.message).to.equal(i18n.t('keyRequired')); + done(); + } + }); + + it('returns an error when non-sellable type is provided', (done) => { + let nonSellableType = 'nonSellableType'; + + try { + sell(user, {params: { type: nonSellableType, key } }); + } catch (err) { + expect(err).to.be.an.instanceof(NotAuthorized); + expect(err.message).to.equal(i18n.t('typeNotSellable', {acceptedTypes: acceptedTypes.join(', ')})); + done(); + } + }); + + it('returns an error when key is not found with type provided', (done) => { + let fakeKey = 'fakeKey'; + + try { + sell(user, {params: { type, key: fakeKey } }); + } catch (err) { + expect(err).to.be.an.instanceof(NotFound); + expect(err.message).to.equal(i18n.t('userItemsKeyNotFound', {type})); + done(); + } + }); + + it('reduces item count from user', () => { + let response = sell(user, {params: { type, key } }); + + expect(response.message).to.equal(i18n.t('sold', {type, key})); + expect(user.items[type][key]).to.equal(0); + }); + + it('increases user\'s gold', () => { + let response = sell(user, {params: { type, key } }); + + expect(response.message).to.equal(i18n.t('sold', {type, key})); + expect(user.stats.gp).to.equal(content[type][key].value); + }); +}); diff --git a/test/common/ops/unlock.js b/test/common/ops/unlock.js new file mode 100644 index 0000000000..b7d7bda370 --- /dev/null +++ b/test/common/ops/unlock.js @@ -0,0 +1,84 @@ +import unlock from '../../../common/script/ops/unlock'; +import i18n from '../../../common/script/i18n'; +import { + generateUser, +} from '../../helpers/common.helper'; +import { + NotAuthorized, + BadRequest, +} from '../../../common/script/libs/errors'; + +describe('shared.ops.unlock', () => { + let user; + let unlockPath = 'shirt.convict,shirt.cross,shirt.fire,shirt.horizon,shirt.ocean,shirt.purple,shirt.rainbow,shirt.redblue,shirt.thunder,shirt.tropical,shirt.zombie'; + let unlockGearSetPath = 'items.gear.owned.headAccessory_special_bearEars,items.gear.owned.headAccessory_special_cactusEars,items.gear.owned.headAccessory_special_foxEars,items.gear.owned.headAccessory_special_lionEars,items.gear.owned.headAccessory_special_pandaEars,items.gear.owned.headAccessory_special_pigEars,items.gear.owned.headAccessory_special_tigerEars,items.gear.owned.headAccessory_special_wolfEars'; + let backgroundUnlockPath = 'background.giant_florals'; + let unlockCost = 1.25; + let usersStartingGems = 5; + + beforeEach(() => { + user = generateUser(); + user.balance = usersStartingGems; + }); + + it('returns an error when path is not provided', (done) => { + try { + unlock(user); + } catch (err) { + expect(err).to.be.an.instanceof(BadRequest); + expect(err.message).to.equal(i18n.t('pathRequired')); + done(); + } + }); + + it('returns an error when user balance is too low', (done) => { + user.balance = 0; + + try { + unlock(user, {query: {path: unlockPath}}); + } catch (err) { + expect(err).to.be.an.instanceof(NotAuthorized); + expect(err.message).to.equal(i18n.t('notEnoughGems')); + done(); + } + }); + + it('returns an error when user already owns an item', (done) => { + try { + unlock(user, {query: {path: backgroundUnlockPath}}); + unlock(user, {query: {path: backgroundUnlockPath}}); + } catch (err) { + expect(err).to.be.an.instanceof(NotAuthorized); + expect(err.message).to.equal(i18n.t('alreadyUnlocked')); + done(); + } + }); + + it('unlocks a full set', () => { + let response = unlock(user, {query: {path: unlockPath}}); + + expect(response.message).to.equal(i18n.t('unlocked')); + expect(user.purchased.shirt.convict).to.be.true; + }); + + it('unlocks a full set of gear', () => { + let response = unlock(user, {query: {path: unlockGearSetPath}}); + + expect(response.message).to.equal(i18n.t('unlocked')); + expect(user.items.gear.owned.headAccessory_special_wolfEars).to.be.true; + }); + + it('unlocks a an item', () => { + let response = unlock(user, {query: {path: backgroundUnlockPath}}); + + expect(response.message).to.equal(i18n.t('unlocked')); + expect(user.purchased.background.giant_florals).to.be.true; + }); + + it('reduces a user\'s balance', () => { + let response = unlock(user, {query: {path: unlockPath}}); + + expect(response.message).to.equal(i18n.t('unlocked')); + expect(user.balance).to.equal(usersStartingGems - unlockCost); + }); +}); diff --git a/test/common/ops/updateWebhook.test.js b/test/common/ops/updateWebhook.test.js new file mode 100644 index 0000000000..43c353626e --- /dev/null +++ b/test/common/ops/updateWebhook.test.js @@ -0,0 +1,42 @@ +import updateWebhook from '../../../common/script/ops/updateWebhook'; +import { + BadRequest, +} from '../../../common/script/libs/errors'; +import i18n from '../../../common/script/i18n'; +import { + generateUser, +} from '../../helpers/common.helper'; + +describe('shared.ops.updateWebhook', () => { + let user; + let req; + let newUrl = 'http://new-url.com'; + + beforeEach(() => { + user = generateUser(); + req = { params: { + id: 'this-id', + }, body: { + url: newUrl, + enabled: true, + } }; + }); + + it('validates body', (done) => { + delete req.body.url; + try { + updateWebhook(user, req); + } catch (err) { + expect(err).to.be.an.instanceof(BadRequest); + expect(err.message).to.equal(i18n.t('invalidUrl')); + done(); + } + }); + + it('succeeds', () => { + let url = 'http://existing-url.com'; + user.preferences.webhooks = { 'this-id': { url } }; + updateWebhook(user, req); + expect(user.preferences.webhooks['this-id'].url).to.eql(newUrl); + }); +}); diff --git a/website/src/controllers/api-v3/user.js b/website/src/controllers/api-v3/user.js index 1f3ec4e0a0..d3a1d8e1c5 100644 --- a/website/src/controllers/api-v3/user.js +++ b/website/src/controllers/api-v3/user.js @@ -769,7 +769,64 @@ api.userOpenMysteryItem = { }; /** -* @api {post} /user/release-pets Releases pets. + * @api {post} /user/webhook + * @apiVersion 3.0.0 + * @apiName UserAddWebhook + * @apiGroup User + * @apiSuccess {} + **/ +api.addWebhook = { + method: 'POST', + middlewares: [authWithHeaders()], + url: '/user/webhook', + async handler (req, res) { + let user = res.locals.user; + let result = common.ops.addWebhook(user, req); + await user.save(); + res.respond(200, result); + }, +}; + +/** + * @api {put} /user/webhook/:id + * @apiVersion 3.0.0 + * @apiName UserUpdateWebhook + * @apiGroup User + * @apiSuccess {} + **/ +api.updateWebhook = { + method: 'PUT', + middlewares: [authWithHeaders()], + url: '/user/webhook/:id', + async handler (req, res) { + let user = res.locals.user; + let result = common.ops.updateWebhook(user, req); + await user.save(); + res.respond(200, result); + }, +}; + +/** + * @api {delete} /user/webhook/:id + * @apiVersion 3.0.0 + * @apiName UserDeleteWebhook + * @apiGroup User + * @apiSuccess {} + **/ +api.deleteWebhook = { + method: 'DELETE', + middlewares: [authWithHeaders()], + url: '/user/webhook/:id', + async handler (req, res) { + let user = res.locals.user; + common.ops.deleteWebhook(user, req); + await user.save(); + res.respond(200, {}); + }, +}; + + +/* @api {post} /user/release-pets Releases pets. * @apiVersion 3.0.0 * @apiName UserReleasePets * @apiGroup User @@ -828,4 +885,93 @@ api.userReleaseMounts = { }, }; +/* +* @api {post} /user/sell/:type/:key Sells user's items. +* @apiVersion 3.0.0 +* @apiName UserSell +* @apiGroup User +* +* @apiSuccess {Object} data `stats items` +*/ +api.userSell = { + method: 'POST', + middlewares: [authWithHeaders(), cron], + url: '/user/sell/:type/:key', + async handler (req, res) { + let user = res.locals.user; + let sellResponse = common.ops.sell(user, req); + await user.save(); + res.respond(200, sellResponse); + }, +}; + +/* +* @api {post} /user/unlock Unlocks items by purchase. +* @apiVersion 3.0.0 +* @apiName UserUnlock +* @apiGroup User +* +* @apiSuccess {Object} data `purchased preferences items` +*/ +api.userUnlock = { + method: 'POST', + middlewares: [authWithHeaders(), cron], + url: '/user/unlock', + async handler (req, res) { + let user = res.locals.user; + let unlockResponse = common.ops.unlock(user, req); + await user.save(); + res.respond(200, unlockResponse); + }, +}; + +/** +* @api {post} /user/revive Revives user from death. +* @apiVersion 3.0.0 +* @apiName UserRevive +* @apiGroup User +* +* @apiSuccess {Object} data `user.items` +*/ +api.userRevive = { + method: 'POST', + middlewares: [authWithHeaders(), cron], + url: '/user/revive', + async handler (req, res) { + let user = res.locals.user; + let reviveResponse = common.ops.revive(user, req, res.analytics); + await user.save(); + res.respond(200, reviveResponse); + }, +}; + +/* +* @api {post} /user/rebirth Resets a user. +* @apiVersion 3.0.0 +* @apiName UserRebirth +* @apiGroup User +* +* @apiSuccess {Object} data `user` +*/ +api.userRebirth = { + method: 'POST', + middlewares: [authWithHeaders(), cron], + url: '/user/rebirth', + async handler (req, res) { + let user = res.locals.user; + let query = { + userId: user._id, + type: {$in: ['daily', 'habit', 'todo']}, + }; + let tasks = await Tasks.Task.find(query).exec(); + let rebirthResponse = common.ops.rebirth(user, tasks, req, res.analytics); + + await user.save(); + + await Q.all(tasks.map(task => task.save())); + + res.respond(200, rebirthResponse); + }, +}; + module.exports = api;