diff --git a/.gitignore b/.gitignore index 7d76dfea20..30acf874b2 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ yarn.lock .elasticbeanstalk/* !.elasticbeanstalk/*.cfg.yml !.elasticbeanstalk/*.global.yml + /.vscode # webstorm fake webpack for path intellisense diff --git a/gulp/gulp-build.js b/gulp/gulp-build.js index 894dd1e234..3aa106b8fc 100644 --- a/gulp/gulp-build.js +++ b/gulp/gulp-build.js @@ -48,17 +48,17 @@ gulp.task('build:prepare-mongo', async () => { return; } - console.log('MongoDB data folder is missing, setting up.'); + console.log('MongoDB data folder is missing, setting up.'); // eslint-disable-line no-console // use run-rs without --keep, kill it as soon as the replica set starts const runRsProcess = spawn('run-rs', ['-v', '4.2.8', '-l', 'ubuntu1804', '--dbpath', 'mongodb-data', '--number', '1', '--quiet']); for await (const chunk of runRsProcess.stdout) { const stringChunk = chunk.toString(); - console.log(stringChunk); + console.log(stringChunk); // eslint-disable-line no-console // kills the process after the replica set is setup if (stringChunk.includes('Started replica set')) { - console.log('MongoDB setup correctly.'); + console.log('MongoDB setup correctly.'); // eslint-disable-line no-console runRsProcess.kill(); } } diff --git a/test/api/unit/models/task.test.js b/test/api/unit/models/task.test.js index 006efd0a07..5921a4e2c2 100644 --- a/test/api/unit/models/task.test.js +++ b/test/api/unit/models/task.test.js @@ -3,7 +3,6 @@ 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 { InternalServerError } from '../../../../website/server/libs/errors'; import { generateHistory } from '../../../helpers/api-unit.helper'; describe('Task Model', () => { @@ -99,7 +98,8 @@ describe('Task Model', () => { throw new Error('No exception when Id is None'); } catch (err) { expect(err).to.exist; - expect(err).to.eql(new InternalServerError('Task identifier is a required argument')); + expect(err).to.be.an.instanceOf(Error); + expect(err.message).to.eql('Task identifier is a required argument'); } }); @@ -109,7 +109,8 @@ describe('Task Model', () => { throw new Error('No exception when user_id is undefined'); } catch (err) { expect(err).to.exist; - expect(err).to.eql(new InternalServerError('User identifier is a required argument')); + expect(err).to.be.an.instanceOf(Error); + expect(err.message).to.eql('User identifier is a required argument'); } }); @@ -153,6 +154,132 @@ describe('Task Model', () => { }); }); + describe('findMultipleByIdOrAlias', () => { + let taskWithAlias; + let secondTask; + let user; + + beforeEach(async () => { + user = new User(); + await user.save(); + + taskWithAlias = new Tasks.todo({ // eslint-disable-line new-cap + text: 'some text', + alias: 'short-name', + userId: user.id, + }); + await taskWithAlias.save(); + + secondTask = new Tasks.habit({ // eslint-disable-line new-cap + text: 'second task', + alias: 'second-short-name', + userId: user.id, + }); + await secondTask.save(); + + sandbox.spy(Tasks.Task, 'find'); + }); + + it('throws an error if task identifiers is not passed in', async () => { + try { + await Tasks.Task.findMultipleByIdOrAlias(null, user._id); + throw new Error('No exception when Id is None'); + } catch (err) { + expect(err).to.exist; + expect(err).to.be.an.instanceOf(Error); + expect(err.message).to.eql('Task identifiers is a required array argument'); + } + }); + + it('throws an error if task identifiers is not an array', async () => { + try { + await Tasks.Task.findMultipleByIdOrAlias('string', user._id); + throw new Error('No exception when Id is None'); + } catch (err) { + expect(err).to.exist; + expect(err).to.be.an.instanceOf(Error); + expect(err.message).to.eql('Task identifiers is a required array argument'); + } + }); + + it('throws an error if user identifier is not passed in', async () => { + try { + await Tasks.Task.findMultipleByIdOrAlias([taskWithAlias._id]); + throw new Error('No exception when user_id is undefined'); + } catch (err) { + expect(err).to.exist; + expect(err).to.be.an.instanceOf(Error); + expect(err.message).to.eql('User identifier is a required argument'); + } + }); + + it('returns task by id', async () => { + const foundTasks = await Tasks.Task.findMultipleByIdOrAlias([taskWithAlias._id], user._id); + + expect(foundTasks[0].text).to.eql(taskWithAlias.text); + }); + + it('returns task by alias', async () => { + const foundTasks = await Tasks.Task.findMultipleByIdOrAlias( + [taskWithAlias.alias], user._id, + ); + + expect(foundTasks[0].text).to.eql(taskWithAlias.text); + }); + + it('returns multiple tasks', async () => { + const foundTasks = await Tasks.Task.findMultipleByIdOrAlias( + [taskWithAlias.alias, secondTask._id], user._id, + ); + + expect(foundTasks.length).to.eql(2); + expect(foundTasks[0]._id).to.eql(taskWithAlias._id); + expect(foundTasks[1]._id).to.eql(secondTask._id); + }); + + it('returns a task only once if searched by both id and alias', async () => { + const foundTasks = await Tasks.Task.findMultipleByIdOrAlias( + [taskWithAlias.alias, taskWithAlias._id], user._id, + ); + + expect(foundTasks.length).to.eql(1); + expect(foundTasks[0].text).to.eql(taskWithAlias.text); + }); + + it('scopes alias lookup to user', async () => { + await Tasks.Task.findMultipleByIdOrAlias([taskWithAlias.alias], user._id); + + expect(Tasks.Task.find).to.be.calledOnce; + expect(Tasks.Task.find).to.be.calledWithMatch({ + $or: [ + { _id: { $in: [] } }, + { alias: { $in: [taskWithAlias.alias] } }, + ], + userId: user._id, + }); + }); + + it('returns empty array if tasks cannot be found', async () => { + const foundTasks = await Tasks.Task.findMultipleByIdOrAlias(['not-found'], user._id); + + expect(foundTasks).to.eql([]); + }); + + it('accepts additional query parameters', async () => { + await Tasks.Task.findMultipleByIdOrAlias([taskWithAlias.alias], user._id, { foo: 'bar' }); + + expect(Tasks.Task.find).to.be.calledOnce; + expect(Tasks.Task.find).to.be.calledWithMatch({ + $or: [ + { _id: { $in: [] } }, + { alias: { $in: [taskWithAlias.alias] } }, + ], + userId: user._id, + foo: 'bar', + }); + }); + }); + describe('sanitizeUserChallengeTask ', () => { }); diff --git a/test/api/v3/integration/tasks/POST-tasks_id_score_direction.test.js b/test/api/v3/integration/tasks/POST-tasks_id_score_direction.test.js index bdfc00fce0..864a12cba6 100644 --- a/test/api/v3/integration/tasks/POST-tasks_id_score_direction.test.js +++ b/test/api/v3/integration/tasks/POST-tasks_id_score_direction.test.js @@ -1,4 +1,5 @@ import { v4 as generateUUID } from 'uuid'; +import apiError from '../../../../../website/server/libs/apiError'; import { generateUser, sleep, @@ -44,7 +45,7 @@ describe('POST /tasks/:id/score/:direction', () => { await expect(user.post(`/tasks/${generateUUID()}/score/tt`)).to.eventually.be.rejected.and.eql({ code: 400, error: 'BadRequest', - message: t('invalidReqParams'), + message: apiError('directionUpDown'), }); }); @@ -261,6 +262,7 @@ describe('POST /tasks/:id/score/:direction', () => { const task = await user.get(`/tasks/${daily._id}`); expect(task.completed).to.equal(true); + expect(task.value).to.be.greaterThan(daily.value); }); it('uncompletes daily when direction is down', async () => { diff --git a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_score_direction.test.js b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_score_direction.test.js index 810ad517d1..a2ca7f8e42 100644 --- a/test/api/v3/integration/tasks/groups/POST-group_tasks_id_score_direction.test.js +++ b/test/api/v3/integration/tasks/groups/POST-group_tasks_id_score_direction.test.js @@ -46,7 +46,7 @@ describe('POST /tasks/:id/score/:direction', () => { const response = await member.post(`/tasks/${syncedTask._id}/score/${direction}`); - expect(response.data.approvalRequested).to.equal(true); + expect(response.data.requiresApproval).to.equal(true); expect(response.message).to.equal(t('taskApprovalHasBeenRequested')); const updatedTask = await member.get(`/tasks/${syncedTask._id}`); @@ -107,12 +107,9 @@ describe('POST /tasks/:id/score/:direction', () => { await member.post(`/tasks/${syncedTask._id}/score/up`); - await expect(member.post(`/tasks/${syncedTask._id}/score/up`)) - .to.eventually.be.rejected.and.eql({ - code: 401, - error: 'NotAuthorized', - message: t('taskRequiresApproval'), - }); + const response = await member.post(`/tasks/${syncedTask._id}/score/up`); + expect(response.data.requiresApproval).to.equal(true); + expect(response.message).to.equal(t('taskRequiresApproval')); }); it('allows a user to score an approved task', async () => { diff --git a/test/api/v3/integration/tasks/groups/PUT-group_task_id.test.js b/test/api/v3/integration/tasks/groups/PUT-group_task_id.test.js index 74c76475a2..44dc4fb162 100644 --- a/test/api/v3/integration/tasks/groups/PUT-group_task_id.test.js +++ b/test/api/v3/integration/tasks/groups/PUT-group_task_id.test.js @@ -73,7 +73,7 @@ describe('PUT /tasks/:id', () => { // score up to trigger approval const response = await member2.post(`/tasks/${syncedTask._id}/score/up`); - expect(response.data.approvalRequested).to.equal(true); + expect(response.data.requiresApproval).to.equal(true); expect(response.message).to.equal(t('taskApprovalHasBeenRequested')); }); diff --git a/test/api/v4/tasks/POST-tasks-bulk-score.test.js b/test/api/v4/tasks/POST-tasks-bulk-score.test.js new file mode 100644 index 0000000000..52b4bd61f5 --- /dev/null +++ b/test/api/v4/tasks/POST-tasks-bulk-score.test.js @@ -0,0 +1,583 @@ +import { v4 as generateUUID } from 'uuid'; +import { + generateUser, + sleep, + translate as t, + server, +} from '../../../helpers/api-integration/v4'; + +describe('POST /tasks/bulk-score', () => { + let user; + + beforeEach(async () => { + user = await generateUser({ + 'stats.gp': 100, + }); + }); + + context('all', () => { + it('can use id to identify the task', async () => { + const todo = await user.post('/tasks/user', { + text: 'test todo', + type: 'todo', + alias: 'alias', + }); + + const res = await user.post('/tasks/bulk-score', [{ id: todo.id, direction: 'up' }]); + + expect(res).to.be.ok; + expect(res.tasks.length).to.equal(1); + expect(res.tasks[0].id).to.equal(todo._id); + expect(res.tasks[0].delta).to.be.greaterThan(0); + }); + + it('can use a alias in place of the id', async () => { + const todo = await user.post('/tasks/user', { + text: 'test todo', + type: 'todo', + alias: 'alias', + }); + + const res = await user.post('/tasks/bulk-score', [{ id: todo.alias, direction: 'up' }]); + + expect(res).to.be.ok; + expect(res.tasks.length).to.equal(1); + expect(res.tasks[0].id).to.equal(todo._id); + expect(res.tasks[0].delta).to.be.greaterThan(0); + }); + + it('sends task scored webhooks', async () => { + const uuid = generateUUID(); + await server.start(); + + await user.post('/user/webhook', { + url: `http://localhost:${server.port}/webhooks/${uuid}`, + type: 'taskActivity', + enabled: true, + options: { + created: false, + scored: true, + }, + }); + + const task = await user.post('/tasks/user', { + text: 'test habit', + type: 'habit', + }); + + await user.post('/tasks/bulk-score', [{ id: task.id, direction: 'up' }]); + + await sleep(); + + await server.close(); + + const body = server.getWebhookData(uuid); + + expect(body.user).to.have.all.keys('_id', '_tmp', 'stats'); + expect(body.user.stats).to.have.all.keys('hp', 'mp', 'exp', 'gp', 'lvl', 'class', 'points', 'str', 'con', 'int', 'per', 'buffs', 'training', 'maxHealth', 'maxMP', 'toNextLevel'); + expect(body.task.id).to.eql(task.id); + expect(body.direction).to.eql('up'); + expect(body.delta).to.be.greaterThan(0); + }); + + context('sending user activity webhooks', () => { + before(async () => { + await server.start(); + }); + + after(async () => { + await server.close(); + }); + + it('sends user activity webhook when the user levels up', async () => { + const uuid = generateUUID(); + + await user.post('/user/webhook', { + url: `http://localhost:${server.port}/webhooks/${uuid}`, + type: 'userActivity', + enabled: true, + options: { + leveledUp: true, + }, + }); + + const initialLvl = user.stats.lvl; + + await user.update({ + 'stats.exp': 3000, + }); + const task = await user.post('/tasks/user', { + text: 'test habit', + type: 'habit', + }); + + await user.post('/tasks/bulk-score', [{ id: task.id, direction: 'up' }]); + await user.sync(); + await sleep(); + + const body = server.getWebhookData(uuid); + + expect(body.type).to.eql('leveledUp'); + expect(body.initialLvl).to.eql(initialLvl); + expect(body.finalLvl).to.eql(user.stats.lvl); + }); + }); + + it('fails the entire op if one task scoring fails', async () => { + const todo = await user.post('/tasks/user', { + text: 'test todo', + type: 'todo', + }); + const habit = await user.post('/tasks/user', { + text: 'test habit', + type: 'habit', + }); + + await expect(user.post('/tasks/bulk-score', [ + { id: todo.id, direction: 'down' }, + { id: habit.id, direction: 'down' }, + ])).to.eventually.be.rejected.and.eql({ + code: 401, + error: 'NotAuthorized', + message: t('sessionOutdated'), + }); + + const updatedHabit = await user.get(`/tasks/${habit._id}`); + expect(updatedHabit.history.length).to.equal(0); + expect(updatedHabit.value).to.equal(0); + + const updatedTodo = await user.get(`/tasks/${todo._id}`); + expect(updatedTodo.value).to.equal(0); + }); + + it('sends _tmp for each task', async () => { + const habit1 = await user.post('/tasks/user', { + text: 'test habit 1', + type: 'habit', + }); + const habit2 = await user.post('/tasks/user', { + text: 'test habit 2', + type: 'habit', + }); + + await user.update({ + 'party.quest.key': 'gryphon', + }); + + const res = await user.post('/tasks/bulk-score', [ + { id: habit1._id, direction: 'up' }, + { id: habit2._id, direction: 'up' }, + ]); + + await user.sync(); + + expect(res.tasks[0]._tmp.quest.progressDelta).to.be.greaterThan(0); + expect(res.tasks[1]._tmp.quest.progressDelta).to.be.greaterThan(0); + expect(user.party.quest.progress.up).to + .eql(res.tasks[0]._tmp.quest.progressDelta + res.tasks[1]._tmp.quest.progressDelta); + }); + }); + + context('todos', () => { + let todo; + + beforeEach(async () => { + todo = await user.post('/tasks/user', { + text: 'test todo', + type: 'todo', + }); + }); + + it('completes todo when direction is up', async () => { + await user.post('/tasks/bulk-score', [{ id: todo.id, direction: 'up' }]); + const task = await user.get(`/tasks/${todo._id}`); + + expect(task.completed).to.equal(true); + expect(task.dateCompleted).to.be.a('string'); // date gets converted to a string as json doesn't have a Date type + }); + + it('moves completed todos out of user.tasksOrder.todos', async () => { + const getUser = await user.get('/user'); + expect(getUser.tasksOrder.todos.indexOf(todo._id)).to.not.equal(-1); + + await user.post('/tasks/bulk-score', [{ id: todo.id, direction: 'up' }]); + const updatedTask = await user.get(`/tasks/${todo._id}`); + expect(updatedTask.completed).to.equal(true); + + const updatedUser = await user.get('/user'); + expect(updatedUser.tasksOrder.todos.indexOf(todo._id)).to.equal(-1); + }); + + it('moves un-completed todos back into user.tasksOrder.todos', async () => { + const getUser = await user.get('/user'); + expect(getUser.tasksOrder.todos.indexOf(todo._id)).to.not.equal(-1); + + await user.post('/tasks/bulk-score', [{ id: todo.id, direction: 'up' }]); + await user.post('/tasks/bulk-score', [{ id: todo.id, direction: 'down' }]); + + const updatedTask = await user.get(`/tasks/${todo._id}`); + expect(updatedTask.completed).to.equal(false); + + const updatedUser = await user.get('/user'); + const l = updatedUser.tasksOrder.todos.length; + expect(updatedUser.tasksOrder.todos.indexOf(todo._id)).not.to.equal(-1); + // Check that it was pushed at the bottom + expect(updatedUser.tasksOrder.todos.indexOf(todo._id)).to.equal(l - 1); + }); + + it('uncompletes todo when direction is down', async () => { + await user.post('/tasks/bulk-score', [{ id: todo.id, direction: 'up' }, { id: todo.id, direction: 'down' }]); + const updatedTask = await user.get(`/tasks/${todo._id}`); + + expect(updatedTask.completed).to.equal(false); + expect(updatedTask.dateCompleted).to.be.a('undefined'); + }); + + it('doesn\'t let a todo be uncompleted twice', async () => { + await expect(user.post('/tasks/bulk-score', [{ id: todo.id, direction: 'down' }])).to.eventually.be.rejected.and.eql({ + code: 401, + error: 'NotAuthorized', + message: t('sessionOutdated'), + }); + }); + + context('user stats when direction is up', () => { + let updatedUser; let res; + + beforeEach(async () => { + res = await user.post('/tasks/bulk-score', [{ id: todo.id, direction: 'up' }]); + updatedUser = await user.get('/user'); + }); + + it('increases user\'s mp', () => { + expect(updatedUser.stats.mp).to.be.greaterThan(user.stats.mp); + expect(res.mp).to.equal(updatedUser.stats.mp); + }); + + it('increases user\'s exp', () => { + expect(updatedUser.stats.exp).to.be.greaterThan(user.stats.exp); + expect(res.exp).to.equal(updatedUser.stats.exp); + }); + + it('increases user\'s gold', () => { + expect(updatedUser.stats.gp).to.be.greaterThan(user.stats.gp); + expect(res.gp).to.equal(updatedUser.stats.gp); + }); + }); + + context('user stats when direction is down', () => { + let updatedUser; let initialUser; let res; + + beforeEach(async () => { + await user.post('/tasks/bulk-score', [{ id: todo.id, direction: 'up' }]); + initialUser = await user.get('/user'); + res = await user.post('/tasks/bulk-score', [{ id: todo.id, direction: 'down' }]); + updatedUser = await user.get('/user'); + }); + + it('decreases user\'s mp', () => { + expect(updatedUser.stats.mp).to.be.lessThan(initialUser.stats.mp); + }); + + it('decreases user\'s exp', () => { + expect(updatedUser.stats.exp).to.be.lessThan(initialUser.stats.exp); + expect(res.exp).to.equal(updatedUser.stats.exp); + }); + + it('decreases user\'s gold', () => { + expect(updatedUser.stats.gp).to.be.lessThan(initialUser.stats.gp); + expect(res.gp).to.equal(updatedUser.stats.gp); + }); + }); + }); + + context('dailys', () => { + let daily; + + beforeEach(async () => { + daily = await user.post('/tasks/user', { + text: 'test daily', + type: 'daily', + }); + }); + + it('completes daily when direction is up', async () => { + await user.post('/tasks/bulk-score', [{ id: daily.id, direction: 'up' }]); + const task = await user.get(`/tasks/${daily._id}`); + + expect(task.completed).to.equal(true); + }); + + it('uncompletes daily when direction is down', async () => { + await user.post('/tasks/bulk-score', [{ id: daily.id, direction: 'up' }, { id: daily.id, direction: 'down' }]); + const task = await user.get(`/tasks/${daily._id}`); + + expect(task.completed).to.equal(false); + }); + + it('computes isDue', async () => { + await user.post('/tasks/bulk-score', [{ id: daily.id, direction: 'up' }]); + const task = await user.get(`/tasks/${daily._id}`); + + expect(task.isDue).to.equal(true); + }); + + it('computes nextDue', async () => { + await user.post('/tasks/bulk-score', [{ id: daily.id, direction: 'up' }]); + const task = await user.get(`/tasks/${daily._id}`); + + expect(task.nextDue.length).to.eql(6); + }); + + context('user stats when direction is up', () => { + let updatedUser; let res; + + beforeEach(async () => { + res = await user.post('/tasks/bulk-score', [{ id: daily.id, direction: 'up' }]); + updatedUser = await user.get('/user'); + }); + + it('increases user\'s mp', () => { + expect(updatedUser.stats.mp).to.be.greaterThan(user.stats.mp); + expect(res.mp).to.equal(updatedUser.stats.mp); + }); + + it('increases user\'s exp', () => { + expect(updatedUser.stats.exp).to.be.greaterThan(user.stats.exp); + expect(res.exp).to.equal(updatedUser.stats.exp); + }); + + it('increases user\'s gold', () => { + expect(updatedUser.stats.gp).to.be.greaterThan(user.stats.gp); + expect(res.gp).to.equal(updatedUser.stats.gp); + }); + }); + + context('user stats when direction is down', () => { + let updatedUser; let initialUser; let res; + + beforeEach(async () => { + await user.post('/tasks/bulk-score', [{ id: daily.id, direction: 'up' }]); + initialUser = await user.get('/user'); + res = await user.post('/tasks/bulk-score', [{ id: daily.id, direction: 'down' }]); + updatedUser = await user.get('/user'); + }); + + it('decreases user\'s mp', () => { + expect(updatedUser.stats.mp).to.be.lessThan(initialUser.stats.mp); + expect(res.mp).to.equal(updatedUser.stats.mp); + }); + + it('decreases user\'s exp', () => { + expect(updatedUser.stats.exp).to.be.lessThan(initialUser.stats.exp); + expect(res.exp).to.equal(updatedUser.stats.exp); + }); + + it('decreases user\'s gold', () => { + expect(updatedUser.stats.gp).to.be.lessThan(initialUser.stats.gp); + expect(res.gp).to.equal(updatedUser.stats.gp); + }); + }); + }); + + context('habits', () => { + let habit; let minusHabit; let plusHabit; let + neitherHabit; // eslint-disable-line no-unused-vars + + beforeEach(async () => { + habit = await user.post('/tasks/user', { + text: 'test habit', + type: 'habit', + }); + + minusHabit = await user.post('/tasks/user', { + text: 'test min habit', + type: 'habit', + up: false, + }); + + plusHabit = await user.post('/tasks/user', { + text: 'test plus habit', + type: 'habit', + down: false, + }); + + neitherHabit = await user.post('/tasks/user', { + text: 'test neither habit', + type: 'habit', + up: false, + down: false, + }); + }); + + it('increases user\'s mp when direction is up', async () => { + const res = await user.post('/tasks/bulk-score', [{ id: habit.id, direction: 'up' }, { + id: plusHabit.id, + direction: 'up', + }]); + const updatedUser = await user.get('/user'); + + expect(updatedUser.stats.mp).to.be.greaterThan(user.stats.mp); + expect(res.mp).to.equal(updatedUser.stats.mp); + }); + + it('decreases user\'s mp when direction is down', async () => { + const res = await user.post('/tasks/bulk-score', [{ + id: habit.id, + direction: 'down', + }, { + id: minusHabit.id, + direction: 'down', + }]); + const updatedUser = await user.get('/user'); + + expect(updatedUser.stats.mp).to.be.lessThan(user.stats.mp); + expect(res.mp).to.equal(updatedUser.stats.mp); + }); + + it('increases user\'s exp when direction is up', async () => { + const res = await user.post('/tasks/bulk-score', [{ + id: habit.id, + direction: 'up', + }, { + id: plusHabit.id, + direction: 'up', + }]); + const updatedUser = await user.get('/user'); + + expect(updatedUser.stats.exp).to.be.greaterThan(user.stats.exp); + expect(res.exp).to.equal(updatedUser.stats.exp); + }); + + it('increases user\'s gold when direction is up', async () => { + const res = await user.post('/tasks/bulk-score', [{ + id: habit.id, + direction: 'up', + }, { + id: plusHabit.id, + direction: 'up', + }]); + const updatedUser = await user.get('/user'); + + expect(updatedUser.stats.gp).to.be.greaterThan(user.stats.gp); + expect(res.gp).to.equal(updatedUser.stats.gp); + }); + + it('records only one history entry per day', async () => { + const initialHistoryLength = habit.history.length; + await user.post('/tasks/bulk-score', [{ + id: habit.id, + direction: 'up', + }, { + id: habit.id, + direction: 'up', + }, { + id: habit.id, + direction: 'down', + }, { + id: habit.id, + direction: 'up', + }]); + + const updatedTask = await user.get(`/tasks/${habit._id}`); + + expect(updatedTask.history.length).to.eql(initialHistoryLength + 1); + + const lastHistoryEntry = updatedTask.history[updatedTask.history.length - 1]; + expect(lastHistoryEntry.scoredUp).to.equal(3); + expect(lastHistoryEntry.scoredDown).to.equal(1); + }); + }); + + context('mixed', () => { + let habit; let daily; let todo; + + beforeEach(async () => { + habit = await user.post('/tasks/user', { + text: 'test habit', + type: 'habit', + }); + daily = await user.post('/tasks/user', { + text: 'test habit', + type: 'habit', + }); + todo = await user.post('/tasks/user', { + text: 'test habit', + type: 'habit', + }); + }); + + it('scores habits, dailies, todos', async () => { + const res = await user.post('/tasks/bulk-score', [ + { id: habit.id, direction: 'down' }, + { id: daily.id, direction: 'up' }, + { id: todo.id, direction: 'up' }, + ]); + + expect(res.tasks[0].id).to.eql(habit.id); + expect(res.tasks[0].delta).to.be.below(0); + expect(res.tasks[0]._tmp).to.exist; + + expect(res.tasks[1].id).to.eql(daily.id); + expect(res.tasks[1].delta).to.be.greaterThan(0); + expect(res.tasks[1]._tmp).to.exist; + + expect(res.tasks[2].id).to.eql(todo.id); + expect(res.tasks[2].delta).to.be.greaterThan(0); + expect(res.tasks[2]._tmp).to.exist; + + const updatedHabit = await user.get(`/tasks/${habit._id}`); + const updatedDaily = await user.get(`/tasks/${daily._id}`); + const updatedTodo = await user.get(`/tasks/${todo._id}`); + + expect(habit.value).to.be.greaterThan(updatedHabit.value); + expect(updatedHabit.counterDown).to.equal(1); + expect(updatedDaily.value).to.be.greaterThan(daily.value); + expect(updatedTodo.value).to.be.greaterThan(todo.value); + }); + }); + + context('reward', () => { + it('correctly handles rewards', async () => { + const reward = await user.post('/tasks/user', { + text: 'test reward', + type: 'reward', + value: 5, + }); + + const res = await user.post('/tasks/bulk-score', [{ id: reward.id, direction: 'up' }]); + const updatedUser = await user.get('/user'); + + // purchases reward + expect(user.stats.gp).to.equal(updatedUser.stats.gp + 5); + expect(res.gp).to.equal(updatedUser.stats.gp); + + // does not change user\'s mp + expect(user.stats.mp).to.equal(updatedUser.stats.mp); + expect(res.mp).to.equal(updatedUser.stats.mp); + + // does not change user\'s exp + expect(user.stats.exp).to.equal(updatedUser.stats.exp); + expect(res.exp).to.equal(updatedUser.stats.exp); + }); + + it('fails if the user does not have enough gold', async () => { + const reward = await user.post('/tasks/user', { + text: 'test reward', + type: 'reward', + value: 500, + }); + + await expect(user.post('/tasks/bulk-score', [{ id: reward.id, direction: 'up' }])).to.eventually.be.rejected.and.eql({ + code: 401, + error: 'NotAuthorized', + message: t('messageNotEnoughGold'), + }); + + const updatedUser = await user.get('/user'); + + // does not purchase reward + expect(user.stats.gp).to.equal(updatedUser.stats.gp); + }); + }); +}); diff --git a/website/client/src/assets/scss/button.scss b/website/client/src/assets/scss/button.scss index a19e96acf4..f4ceeee397 100644 --- a/website/client/src/assets/scss/button.scss +++ b/website/client/src/assets/scss/button.scss @@ -5,7 +5,7 @@ font-weight: bold; line-height: 1.71; border: 1px solid transparent; - padding: 0.25rem 1rem; + padding: 0.219rem 1rem; border-radius: 2px; box-shadow: 0 1px 3px 0 rgba($black, 0.12), 0 1px 2px 0 rgba($black, 0.24); color: $white; diff --git a/website/client/src/components/group-plans/index.vue b/website/client/src/components/group-plans/index.vue index 25b597c2e7..53da4f72d2 100644 --- a/website/client/src/components/group-plans/index.vue +++ b/website/client/src/components/group-plans/index.vue @@ -48,7 +48,7 @@ export default { computed: { ...mapState({ user: 'user.data', - groupPlans: 'groupPlans', + groupPlans: 'groupPlans.data', }), currentGroup () { const groupFound = this.groupPlans.find(group => group._id === this.groupId); diff --git a/website/client/src/components/header/menu.vue b/website/client/src/components/header/menu.vue index 963fb54fe2..13be2ebe44 100644 --- a/website/client/src/components/header/menu.vue +++ b/website/client/src/components/header/menu.vue @@ -212,7 +212,7 @@ 'active': $route.path.startsWith('/group-plans')}" >
+ {{ $t('checkOffYesterDailies') }} +
+- {{ $t('checkOffYesterDailies') }} -
-