diff --git a/test/api/v3/unit/models/challenge.test.js b/test/api/v3/unit/models/challenge.test.js new file mode 100644 index 0000000000..d028933e33 --- /dev/null +++ b/test/api/v3/unit/models/challenge.test.js @@ -0,0 +1,154 @@ +import { model as Challenge } from '../../../../../website/src/models/challenge'; +import { model as Group } from '../../../../../website/src/models/group'; +import { model as User } from '../../../../../website/src/models/user'; +import * as Tasks from '../../../../../website/src/models/task'; +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.sanitizeCreate(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]); + + _.assign(task, _.merge(task.toObject(), Tasks.Task.sanitizeUpdate({ text: updatedTaskName }))); + 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; + }); + }); + }); +}); diff --git a/website/src/controllers/api-v3/challenges.js b/website/src/controllers/api-v3/challenges.js index 632c36bd3f..cf5d44546f 100644 --- a/website/src/controllers/api-v3/challenges.js +++ b/website/src/controllers/api-v3/challenges.js @@ -191,7 +191,7 @@ api.leaveChallenge = { challenge.memberCount -= 1; // Unlink challenge's tasks from user's tasks and save the challenge - await Q.all([user.unlinkChallengeTasks(challenge._id, keep), challenge.save()]); + await Q.all([challenge.unlinkTasks(user, keep), challenge.save()]); res.respond(200, {}); }, }; diff --git a/website/src/models/challenge.js b/website/src/models/challenge.js index b67e8b7832..e3c2f3ee97 100644 --- a/website/src/models/challenge.js +++ b/website/src/models/challenge.js @@ -5,6 +5,7 @@ import baseModel from '../libs/api-v3/baseModel'; import _ from 'lodash'; import * as Tasks from './task'; import { model as User } from './user'; +import { removeFromArray } from '../libs/api-v3/collectionManipulators'; let Schema = mongoose.Schema; @@ -186,9 +187,10 @@ schema.methods.updateTask = async function challengeUpdateTask (task) { let updateCmd = {$set: {}}; - _syncableAttrs(task).forEach((value, key) => { - updateCmd.$set[key] = value; - }); + let syncableAttrs = _syncableAttrs(task); + for (let key in syncableAttrs) { + updateCmd.$set[key] = syncableAttrs[key]; + } // TODO reveiw // Updating instead of loading and saving for performances, risks becoming a problem if we introduce more complexity in tasks @@ -213,4 +215,37 @@ schema.methods.removeTask = async function challengeRemoveTask (task) { }, {multi: true}).exec(); }; +// Unlink challenges tasks (and the challenge itself) from user +schema.methods.unlinkTasks = async function challengeUnlinkTasks (user, keep) { + let challengeId = this._id; + let findQuery = { + userId: user._id, + 'challenge.id': challengeId, + }; + + removeFromArray(user.challenges, challengeId); + + if (keep === 'keep-all') { + await Tasks.Task.update(findQuery, { + $set: {challenge: {}}, // TODO what about updatedAt? + }, {multi: true}).exec(); + + await user.save(); + } else { // keep = 'remove-all' + let tasks = await Tasks.Task.find(findQuery).select('_id type completed').exec(); + let taskPromises = tasks.map(task => { + // Remove task from user.tasksOrder and delete them + if (task.type !== 'todo' || !task.completed) { + removeFromArray(user.tasksOrder[`${task.type}s`], task._id); + } + + return task.remove(); + }); + user.markModified('tasksOrder'); + taskPromises.push(user.save()); + return Q.all(taskPromises); + } +}; + + export let model = mongoose.model('Challenge', schema); diff --git a/website/src/models/group.js b/website/src/models/group.js index cdc1ced495..9cf30e582e 100644 --- a/website/src/models/group.js +++ b/website/src/models/group.js @@ -587,7 +587,7 @@ schema.methods.leave = async function leaveGroup (user, keep = 'keep-all') { }); let challengesToRemoveUserFrom = challenges.map(chal => { - return user.unlinkChallengeTasks(chal._id, keep); + return chal.unlinkTasks(user, keep); }); await Q.all(challengesToRemoveUserFrom); diff --git a/website/src/models/user.js b/website/src/models/user.js index ea57a7a5cb..e6bcad4261 100644 --- a/website/src/models/user.js +++ b/website/src/models/user.js @@ -6,7 +6,6 @@ import moment from 'moment'; import * as Tasks from './task'; import Q from 'q'; import { schema as TagSchema } from './tag'; -import { removeFromArray } from '../libs/api-v3/collectionManipulators'; import baseModel from '../libs/api-v3/baseModel'; // import {model as Challenge} from './challenge'; @@ -703,38 +702,6 @@ schema.methods.getGroups = function getUserGroups () { return userGroups; }; -// Unlink challenges tasks (and the challenge itself) from user -schema.methods.unlinkChallengeTasks = async function unlinkChallengeTasks (challengeId, keep) { - let user = this; - let findQuery = { - userId: user._id, - 'challenge.id': challengeId, - }; - - removeFromArray(user.challenges, challengeId); - - if (keep === 'keep-all') { - await Tasks.Task.update(findQuery, { - $set: {challenge: {}}, // TODO what about updatedAt? - }, {multi: true}).exec(); - - await user.save(); - } else { // keep = 'remove-all' - let tasks = await Tasks.Task.find(findQuery).select('_id type completed').exec(); - let taskPromises = tasks.map(task => { - // Remove task from user.tasksOrder and delete them - if (task.type !== 'todo' || !task.completed) { - removeFromArray(user.tasksOrder[`${task.type}s`], task._id); - } - - return task.remove(); - }); - - taskPromises.push(user.save()); - return Q.all(taskPromises); - } -}; - export let model = mongoose.model('User', schema); // Initially export an empty object so external requires will get