diff --git a/common/locales/en/api-v3.json b/common/locales/en/api-v3.json index 3f541da96a..3bd4f194cd 100644 --- a/common/locales/en/api-v3.json +++ b/common/locales/en/api-v3.json @@ -168,5 +168,6 @@ "resetComplete": "Reset has completed", "regIdRequired": "RegId is required", "pushDeviceAdded": "Push device added successfully", - "pushDeviceAlreadyAdded": "The user already has the push device" + "pushDeviceAlreadyAdded": "The user already has the push device", + "resetComplete": "Reset has completed" } diff --git a/common/script/ops/resetGear.js b/common/script/fns/resetGear.js similarity index 100% rename from common/script/ops/resetGear.js rename to common/script/fns/resetGear.js diff --git a/common/script/index.js b/common/script/index.js index 050e29d8ee..0d734f8ba2 100644 --- a/common/script/index.js +++ b/common/script/index.js @@ -120,6 +120,7 @@ import clearPMs from './ops/clearPMs'; import deletePM from './ops/deletePM'; import reroll from './ops/reroll'; import addPushDevice from './ops/addPushDevice'; +import reset from './ops/reset'; api.ops = { scoreTask, @@ -156,6 +157,7 @@ api.ops = { deletePM, reroll, addPushDevice, + reset, }; import handleTwoHanded from './fns/handleTwoHanded'; diff --git a/common/script/ops/rebirth.js b/common/script/ops/rebirth.js index e287323d48..bf29e64d67 100644 --- a/common/script/ops/rebirth.js +++ b/common/script/ops/rebirth.js @@ -5,7 +5,7 @@ import { MAX_LEVEL } from '../constants'; import { NotAuthorized, } from '../libs/errors'; -import resetGear from './resetGear'; +import resetGear from '../fns/resetGear'; import equip from './equip'; const USERSTATSLIST = ['per', 'int', 'con', 'str', 'points', 'gp', 'exp', 'mp']; diff --git a/common/script/ops/reset.js b/common/script/ops/reset.js index fbe87729e3..525127087d 100644 --- a/common/script/ops/reset.js +++ b/common/script/ops/reset.js @@ -1,35 +1,28 @@ -import _ from 'lodash'; +import resetGear from '../fns/resetGear'; +import i18n from '../i18n'; -module.exports = function(user, req, cb) { - var gear; - user.habits = []; - user.dailys = []; - user.todos = []; - user.rewards = []; +module.exports = function reset (user, tasks = []) { user.stats.hp = 50; user.stats.lvl = 1; user.stats.gp = 0; user.stats.exp = 0; - gear = user.items.gear; - _.each(['equipped', 'costume'], function(type) { - gear[type].armor = 'armor_base_0'; - gear[type].weapon = 'weapon_base_0'; - gear[type].head = 'head_base_0'; - return gear[type].shield = 'shield_base_0'; - }); - if (typeof gear.owned === 'undefined') { - gear.owned = {}; - } - _.each(gear.owned, function(v, k) { - if (gear.owned[k]) { - gear.owned[k] = false; + + let tasksToRemove = []; + tasks.forEach(task => { + if (!task.challenge || !task.challenge.id || task.challenge.broken) { + tasksToRemove.push(task._id); + let i = user.tasksOrder[`${task.type}s`].indexOf(task._id); + if (i !== -1) user.tasksOrder[`${task.type}s`].splice(i, 1); + tasksToRemove.push(task._id); } - return true; }); - gear.owned.weapon_warrior_0 = true; - if (typeof user.markModified === "function") { - user.markModified('items.gear.owned'); - } - user.preferences.costume = false; - return typeof cb === "function" ? cb(null, user) : void 0; + + resetGear(user); + + let response = { + data: {user, tasksToRemove}, + message: i18n.t('resetComplete'), + }; + + return response; }; diff --git a/tasks/gulp-eslint.js b/tasks/gulp-eslint.js index 30ec320fb8..624240b406 100644 --- a/tasks/gulp-eslint.js +++ b/tasks/gulp-eslint.js @@ -10,7 +10,6 @@ const COMMON_FILES = [ './common/script/**/*.js', // @TODO remove these negations as the files are converted over. '!./common/script/content/index.js', - '!./common/script/ops/reset.js', '!./common/script/fns/randomDrop.js', '!./common/script/libs/countExists.js', '!./common/script/libs/encodeiCalLink.js', diff --git a/test/api/v3/integration/user/POST-user_reset.test.js b/test/api/v3/integration/user/POST-user_reset.test.js new file mode 100644 index 0000000000..2baf7bd083 --- /dev/null +++ b/test/api/v3/integration/user/POST-user_reset.test.js @@ -0,0 +1,104 @@ +import { + generateUser, + generateGroup, + generateChallenge, + translate as t, +} from '../../../../helpers/api-integration/v3'; + +describe('POST /user/reset', () => { + let user; + + beforeEach(async () => { + user = await generateUser(); + }); + + // More tests in common code unit tests + + it('resets user\'s habits', async () => { + let task = await user.post('/tasks/user', { + text: 'test habit', + type: 'habit', + }); + + await user.post('/user/reset'); + await user.sync(); + + await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: t('taskNotFound'), + }); + + expect(user.tasksOrder.habits).to.be.empty; + }); + + it('resets user\'s dailys', async () => { + let task = await user.post('/tasks/user', { + text: 'test daily', + type: 'daily', + }); + + await user.post('/user/reset'); + await user.sync(); + + await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: t('taskNotFound'), + }); + + expect(user.tasksOrder.dailys).to.be.empty; + }); + + it('resets user\'s todos', async () => { + let task = await user.post('/tasks/user', { + text: 'test todo', + type: 'todo', + }); + + await user.post('/user/reset'); + await user.sync(); + + await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: t('taskNotFound'), + }); + + expect(user.tasksOrder.todos).to.be.empty; + }); + + it('resets user\'s rewards', async () => { + let task = await user.post('/tasks/user', { + text: 'test reward', + type: 'reward', + }); + + await user.post('/user/reset'); + await user.sync(); + + await expect(user.get(`/tasks/${task._id}`)).to.eventually.be.rejected.and.eql({ + code: 404, + error: 'NotFound', + message: t('taskNotFound'), + }); + + expect(user.tasksOrder.rewards).to.be.empty; + }); + + it('does not delete challenge tasks', async () => { + let guild = await generateGroup(user); + let challenge = await generateChallenge(user, guild); + let task = await user.post(`/tasks/challenge/${challenge._id}`, { + text: 'test challenge habit', + type: 'habit', + }); + + await user.post('/user/reset'); + await user.sync(); + + let userChallengeTask = await user.get(`/tasks/${task._id}`); + + expect(userChallengeTask).to.eql(task); + }); +}); diff --git a/test/common/ops/reset.js b/test/common/ops/reset.js new file mode 100644 index 0000000000..2243cfa127 --- /dev/null +++ b/test/common/ops/reset.js @@ -0,0 +1,79 @@ +import reset from '../../../common/script/ops/reset'; +import i18n from '../../../common/script/i18n'; +import { + generateUser, + generateDaily, + generateHabit, + generateReward, + generateTodo, +} from '../../helpers/common.helper'; + +describe('shared.ops.reset', () => { + let user; + let tasksToRemove; + + beforeEach(() => { + user = generateUser(); + user.balance = 2; + + let habit = generateHabit(); + let todo = generateTodo(); + let daily = generateDaily(); + let reward = generateReward(); + + user.tasksOrder.habits = [habit._id]; + user.tasksOrder.todos = [todo._id]; + user.tasksOrder.dailys = [daily._id]; + user.tasksOrder.rewards = [reward._id]; + + tasksToRemove = [habit, todo, daily, reward]; + }); + + + it('resets a user', () => { + let response = reset(user); + + expect(response.message).to.equal(i18n.t('resetComplete')); + }); + + it('resets user\'s health', () => { + user.stats.hp = 40; + + reset(user); + + expect(user.stats.hp).to.equal(50); + }); + + it('resets user\'s level', () => { + user.stats.lvl = 2; + + reset(user); + + expect(user.stats.lvl).to.equal(1); + }); + + it('resets user\'s gold', () => { + user.stats.gp = 20; + + reset(user); + + expect(user.stats.gp).to.equal(0); + }); + + it('resets user\'s exp', () => { + user.stats.exp = 20; + + reset(user); + + expect(user.stats.exp).to.equal(0); + }); + + it('resets user\'s tasksOrder', () => { + reset(user, tasksToRemove); + + expect(user.tasksOrder.habits).to.be.empty; + expect(user.tasksOrder.todos).to.be.empty; + expect(user.tasksOrder.dailys).to.be.empty; + expect(user.tasksOrder.rewards).to.be.empty; + }); +}); diff --git a/website/src/controllers/api-v3/user.js b/website/src/controllers/api-v3/user.js index ab2b0fa99e..af1fb94ee4 100644 --- a/website/src/controllers/api-v3/user.js +++ b/website/src/controllers/api-v3/user.js @@ -1083,4 +1083,29 @@ api.userAddPushDevice = { }, }; +/* +* @api {post} /user/reset Resets a user. +* @apiVersion 3.0.0 +* @apiName UserReset +* @apiGroup User +* +* @apiSuccess {Object} data `user` +*/ +api.userReset = { + method: 'POST', + middlewares: [authWithHeaders(), cron], + url: '/user/reset', + async handler (req, res) { + let user = res.locals.user; + + let tasks = await Tasks.Task.find({userId: user._id}).select('_id type challenge').exec(); + + let resetResponse = common.ops.reset(user, tasks); + + await Q.all([Tasks.Task.remove({_id: {$in: resetResponse.data.tasksToRemove}, userId: user._id}), user.save()]); + + res.respond(200, resetResponse); + }, +}; + module.exports = api; diff --git a/website/src/models/user.js b/website/src/models/user.js index 403134bfd4..cd420fd5b7 100644 --- a/website/src/models/user.js +++ b/website/src/models/user.js @@ -808,7 +808,6 @@ schema.methods.getTransformedData = function getTransformedData (cb) { }; // END of API v2 methods - export let model = mongoose.model('User', schema); // Initially export an empty object so external requires will get