mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-18 07:07:35 +01:00
add tests for getChallengeMemberProgress route and several bug fixes
This commit is contained in:
@@ -0,0 +1,126 @@
|
|||||||
|
import {
|
||||||
|
generateUser,
|
||||||
|
generateGroup,
|
||||||
|
translate as t,
|
||||||
|
} from '../../../../helpers/api-v3-integration.helper';
|
||||||
|
import { v4 as generateUUID } from 'uuid';
|
||||||
|
|
||||||
|
describe('GET /challenges/:challengeId/members/:memberId', () => {
|
||||||
|
let user;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
user = await generateUser();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates req.params.memberId to be an UUID', async () => {
|
||||||
|
await expect(user.get(`/challenges/invalidUUID/members/${generateUUID()}`)).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: t('invalidReqParams'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('validates req.params.memberId to be an UUID', async () => {
|
||||||
|
await expect(user.get(`/challenges/${generateUUID()}/members/invalidUUID`)).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 400,
|
||||||
|
error: 'BadRequest',
|
||||||
|
message: t('invalidReqParams'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails if member doesn\'t exists', async () => {
|
||||||
|
let userId = generateUUID();
|
||||||
|
await expect(user.get(`/challenges/${generateUUID()}/members/${userId}`)).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 404,
|
||||||
|
error: 'NotFound',
|
||||||
|
message: t('userWithIDNotFound', {userId}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails if challenge doesn\'t exists', async () => {
|
||||||
|
let member = await generateUser();
|
||||||
|
await expect(user.get(`/challenges/${generateUUID()}/members/${member._id}`)).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 404,
|
||||||
|
error: 'NotFound',
|
||||||
|
message: t('challengeNotFound'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails if user doesn\'t have access to the challenge', async () => {
|
||||||
|
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
|
||||||
|
let challenge = await user.post('/challenges', {
|
||||||
|
name: 'test chal',
|
||||||
|
shortName: 'test-chal',
|
||||||
|
groupId: group._id,
|
||||||
|
});
|
||||||
|
let anotherUser = await generateUser();
|
||||||
|
let member = await generateUser();
|
||||||
|
await expect(anotherUser.get(`/challenges/${challenge._id}/members/${member._id}`)).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 404,
|
||||||
|
error: 'NotFound',
|
||||||
|
message: t('challengeNotFound'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails if member is not part of the challenge', async () => {
|
||||||
|
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
|
||||||
|
let challenge = await user.post('/challenges', {
|
||||||
|
name: 'test chal',
|
||||||
|
shortName: 'test-chal',
|
||||||
|
groupId: group._id,
|
||||||
|
});
|
||||||
|
let member = await generateUser();
|
||||||
|
await expect(user.get(`/challenges/${challenge._id}/members/${member._id}`)).to.eventually.be.rejected.and.eql({
|
||||||
|
code: 404,
|
||||||
|
error: 'NotFound',
|
||||||
|
message: t('challengeMemberNotFound'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('works with challenges belonging to a public guild', async () => {
|
||||||
|
let groupLeader = await generateUser({balance: 4});
|
||||||
|
let group = await generateGroup(groupLeader, {type: 'guild', privacy: 'public', name: generateUUID()});
|
||||||
|
let challenge = await groupLeader.post('/challenges', {
|
||||||
|
name: 'test chal',
|
||||||
|
shortName: 'test-chal',
|
||||||
|
groupId: group._id,
|
||||||
|
});
|
||||||
|
let taskText = 'Test Text';
|
||||||
|
await groupLeader.post(`/tasks/challenge/${challenge._id}`, [{type: 'habit', text: taskText}]);
|
||||||
|
|
||||||
|
let memberProgress = await user.get(`/challenges/${challenge._id}/members/${groupLeader._id}`);
|
||||||
|
expect(memberProgress).to.have.all.keys(['_id', 'profile', 'tasks']);
|
||||||
|
expect(memberProgress.profile).to.have.all.keys(['name']);
|
||||||
|
expect(memberProgress.tasks.length).to.equal(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the member tasks for the challenges', async () => {
|
||||||
|
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
|
||||||
|
let challenge = await user.post('/challenges', {
|
||||||
|
name: 'test chal',
|
||||||
|
shortName: 'test-chal',
|
||||||
|
groupId: group._id,
|
||||||
|
});
|
||||||
|
await user.post(`/tasks/challenge/${challenge._id}`, [{type: 'habit', text: 'Test Text'}]);
|
||||||
|
|
||||||
|
let memberProgress = await user.get(`/challenges/${challenge._id}/members/${user._id}`);
|
||||||
|
let chalTasks = await user.get(`/tasks/challenge/${challenge._id}`);
|
||||||
|
expect(memberProgress.tasks.length).to.equal(chalTasks.length);
|
||||||
|
expect(memberProgress.tasks[0].challenge.id).to.equal(challenge._id);
|
||||||
|
expect(memberProgress.tasks[0].challenge.taskId).to.equal(chalTasks[0]._id);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the tasks without the tags', async () => {
|
||||||
|
let group = await generateGroup(user, {type: 'party', name: generateUUID()});
|
||||||
|
let challenge = await user.post('/challenges', {
|
||||||
|
name: 'test chal',
|
||||||
|
shortName: 'test-chal',
|
||||||
|
groupId: group._id,
|
||||||
|
});
|
||||||
|
let taskText = 'Test Text';
|
||||||
|
await user.post(`/tasks/challenge/${challenge._id}`, [{type: 'habit', text: taskText}]);
|
||||||
|
|
||||||
|
let memberProgress = await user.get(`/challenges/${challenge._id}/members/${user._id}`);
|
||||||
|
expect(memberProgress.tasks[0]).not.to.have.key('tags');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,10 +2,7 @@ import { authWithHeaders } from '../../middlewares/api-v3/auth';
|
|||||||
import cron from '../../middlewares/api-v3/cron';
|
import cron from '../../middlewares/api-v3/cron';
|
||||||
import { model as Challenge } from '../../models/challenge';
|
import { model as Challenge } from '../../models/challenge';
|
||||||
import { model as Group } from '../../models/group';
|
import { model as Group } from '../../models/group';
|
||||||
import {
|
import { model as User } from '../../models/user';
|
||||||
model as User,
|
|
||||||
nameFields,
|
|
||||||
} from '../../models/user';
|
|
||||||
import {
|
import {
|
||||||
NotFound,
|
NotFound,
|
||||||
NotAuthorized,
|
NotAuthorized,
|
||||||
@@ -165,56 +162,6 @@ api.getChallenge = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* @api {get} /challenges/:challengeId/members/:memberId Get a challenge member progress
|
|
||||||
* @apiVersion 3.0.0
|
|
||||||
* @apiName GetChallenge
|
|
||||||
* @apiGroup Challenge
|
|
||||||
*
|
|
||||||
* @apiParam {UUID} challengeId The challenge _id
|
|
||||||
* @apiParam {UUID} member The member _id
|
|
||||||
*
|
|
||||||
* @apiSuccess {object} member Return an object with member _id, profile.name and a tasks object with the challenge tasks for the member
|
|
||||||
*/
|
|
||||||
api.getChallengeMemberProgress = {
|
|
||||||
method: 'GET',
|
|
||||||
url: '/challenges/:challengeId/members/:memberId',
|
|
||||||
middlewares: [authWithHeaders(), cron],
|
|
||||||
async handler (req, res) {
|
|
||||||
req.checkQuery('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
|
||||||
req.checkQuery('memberId', res.t('memberIdRequired')).notEmpty().isUUID();
|
|
||||||
|
|
||||||
let validationErrors = req.validationErrors();
|
|
||||||
if (validationErrors) throw validationErrors;
|
|
||||||
|
|
||||||
let user = res.locals.user;
|
|
||||||
let challengeId = req.params.challengeId;
|
|
||||||
let memberId = req.params.memberId;
|
|
||||||
|
|
||||||
let member = await User.findById(memberId).select(`${nameFields} challenges`).exec();
|
|
||||||
if (!member) throw new NotFound(res.t('userWithIDNotFound', {userId: memberId}));
|
|
||||||
|
|
||||||
let challenge = await Challenge.findById(challengeId).exec();
|
|
||||||
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
|
|
||||||
|
|
||||||
let group = await Group.getGroup({user, groupId: challenge.groupId, fields: '_id type privacy'});
|
|
||||||
if (!group || !challenge.canView(user, group)) throw new NotFound(res.t('challengeNotFound'));
|
|
||||||
if (!challenge.isMember(member)) throw new NotFound(res.t('challengeMemberNotFound'));
|
|
||||||
|
|
||||||
let chalTasks = Tasks.Task.find({
|
|
||||||
userId: memberId,
|
|
||||||
'challenge.id': challengeId,
|
|
||||||
})
|
|
||||||
.select('-tags') // We don't want to return the tags publicly TODO same for other data?
|
|
||||||
.exec();
|
|
||||||
|
|
||||||
// manually call toJSON with minimize: true so empty paths aren't returned
|
|
||||||
let response = member.toJSON({minimize: true});
|
|
||||||
response.tasks = chalTasks.map(chalTask => chalTask.toJSON({minimize: true}));
|
|
||||||
res.respond(200, response);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO everything here should be moved to a worker
|
// TODO everything here should be moved to a worker
|
||||||
// actually even for a worker it's probably just to big and will kill mongo
|
// actually even for a worker it's probably just to big and will kill mongo
|
||||||
function _closeChal (challenge, broken = {}) {
|
function _closeChal (challenge, broken = {}) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { model as Challenge } from '../../models/challenge';
|
|||||||
import {
|
import {
|
||||||
NotFound,
|
NotFound,
|
||||||
} from '../../libs/api-v3/errors';
|
} from '../../libs/api-v3/errors';
|
||||||
|
import * as Tasks from '../../models/task';
|
||||||
|
|
||||||
let api = {};
|
let api = {};
|
||||||
|
|
||||||
@@ -174,4 +175,55 @@ api.getMembersForChallenge = {
|
|||||||
handler: _getMembersForItem('challenge-members'),
|
handler: _getMembersForItem('challenge-members'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} /challenges/:challengeId/members/:memberId Get a challenge member progress
|
||||||
|
* @apiVersion 3.0.0
|
||||||
|
* @apiName GetChallenge
|
||||||
|
* @apiGroup Challenge
|
||||||
|
*
|
||||||
|
* @apiParam {UUID} challengeId The challenge _id
|
||||||
|
* @apiParam {UUID} member The member _id
|
||||||
|
*
|
||||||
|
* @apiSuccess {object} member Return an object with member _id, profile.name and a tasks object with the challenge tasks for the member
|
||||||
|
*/
|
||||||
|
api.getChallengeMemberProgress = {
|
||||||
|
method: 'GET',
|
||||||
|
url: '/challenges/:challengeId/members/:memberId',
|
||||||
|
middlewares: [authWithHeaders(), cron],
|
||||||
|
async handler (req, res) {
|
||||||
|
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||||
|
req.checkParams('memberId', res.t('memberIdRequired')).notEmpty().isUUID();
|
||||||
|
|
||||||
|
let validationErrors = req.validationErrors();
|
||||||
|
if (validationErrors) throw validationErrors;
|
||||||
|
|
||||||
|
let user = res.locals.user;
|
||||||
|
let challengeId = req.params.challengeId;
|
||||||
|
let memberId = req.params.memberId;
|
||||||
|
|
||||||
|
let member = await User.findById(memberId).select(`${nameFields} challenges`).exec();
|
||||||
|
if (!member) throw new NotFound(res.t('userWithIDNotFound', {userId: memberId}));
|
||||||
|
|
||||||
|
let challenge = await Challenge.findById(challengeId).exec();
|
||||||
|
if (!challenge) throw new NotFound(res.t('challengeNotFound'));
|
||||||
|
|
||||||
|
let group = await Group.getGroup({user, groupId: challenge.groupId, fields: '_id type privacy'});
|
||||||
|
if (!group || !challenge.canView(user, group)) throw new NotFound(res.t('challengeNotFound'));
|
||||||
|
if (!challenge.isMember(member)) throw new NotFound(res.t('challengeMemberNotFound'));
|
||||||
|
|
||||||
|
let chalTasks = await Tasks.Task.find({
|
||||||
|
userId: memberId,
|
||||||
|
'challenge.id': challengeId,
|
||||||
|
})
|
||||||
|
.select('-tags') // We don't want to return the tags publicly TODO same for other data?
|
||||||
|
.exec();
|
||||||
|
|
||||||
|
// manually call toJSON with minimize: true so empty paths aren't returned
|
||||||
|
let response = member.toJSON({minimize: true});
|
||||||
|
delete response.challenges;
|
||||||
|
response.tasks = chalTasks.map(chalTask => chalTask.toJSON({minimize: true}));
|
||||||
|
res.respond(200, response);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default api;
|
export default api;
|
||||||
|
|||||||
@@ -84,10 +84,10 @@ api.createUserTasks = {
|
|||||||
*/
|
*/
|
||||||
api.createChallengeTasks = {
|
api.createChallengeTasks = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: '/tasks/challenge/:challengeId',
|
url: '/tasks/challenge/:challengeId', // TODO should be /tasks/challengeS/:challengeId ? plural?
|
||||||
middlewares: [authWithHeaders(), cron],
|
middlewares: [authWithHeaders(), cron],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
req.checkQuery('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||||
|
|
||||||
let reqValidationErrors = req.validationErrors();
|
let reqValidationErrors = req.validationErrors();
|
||||||
if (reqValidationErrors) throw reqValidationErrors;
|
if (reqValidationErrors) throw reqValidationErrors;
|
||||||
@@ -188,7 +188,7 @@ api.getChallengeTasks = {
|
|||||||
url: '/tasks/challenge/:challengeId',
|
url: '/tasks/challenge/:challengeId',
|
||||||
middlewares: [authWithHeaders(), cron],
|
middlewares: [authWithHeaders(), cron],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
req.checkQuery('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
req.checkParams('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
|
||||||
req.checkQuery('type', res.t('invalidTaskType')).optional().isIn(Tasks.tasksTypes);
|
req.checkQuery('type', res.t('invalidTaskType')).optional().isIn(Tasks.tasksTypes);
|
||||||
|
|
||||||
let validationErrors = req.validationErrors();
|
let validationErrors = req.validationErrors();
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ schema.methods.canModify = function canModifyChallenge (user) {
|
|||||||
function _syncableAttrs (task) {
|
function _syncableAttrs (task) {
|
||||||
let t = task.toObject(); // lodash doesn't seem to like _.omit on Document
|
let t = task.toObject(); // lodash doesn't seem to like _.omit on Document
|
||||||
// only sync/compare important attrs
|
// only sync/compare important attrs
|
||||||
let omitAttrs = ['userId', 'challenge', 'history', 'tags', 'completed', 'streak', 'notes']; // TODO what to do with updatedAt?
|
let omitAttrs = ['_id', 'userId', 'challenge', 'history', 'tags', 'completed', 'streak', 'notes']; // TODO what to do with updatedAt?
|
||||||
if (t.type !== 'reward') omitAttrs.push('value');
|
if (t.type !== 'reward') omitAttrs.push('value');
|
||||||
return _.omit(t, omitAttrs);
|
return _.omit(t, omitAttrs);
|
||||||
}
|
}
|
||||||
@@ -168,7 +168,7 @@ schema.methods.addTasks = async function challengeAddTasks (tasks) {
|
|||||||
tasksOrderList.$each.unshift(userTask._id);
|
tasksOrderList.$each.unshift(userTask._id);
|
||||||
}
|
}
|
||||||
|
|
||||||
toSave.push(userTask);
|
toSave.push(userTask.save());
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update the user
|
// Update the user
|
||||||
|
|||||||
Reference in New Issue
Block a user