mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-18 23:27:26 +01:00
Ported user delete route and added initial tests
This commit is contained in:
@@ -117,5 +117,6 @@
|
|||||||
"missingPetFoodFeed": "\"pet\" and \"food\" are required parameters.",
|
"missingPetFoodFeed": "\"pet\" and \"food\" are required parameters.",
|
||||||
"invalidPetName": "Invalid pet name supplied.",
|
"invalidPetName": "Invalid pet name supplied.",
|
||||||
"missingEggHatchingPotionHatch": "\"egg\" and \"hatchingPotion\" are required parameters.",
|
"missingEggHatchingPotionHatch": "\"egg\" and \"hatchingPotion\" are required parameters.",
|
||||||
"invalidTypeEquip": "\"type\" must be one of 'equipped', 'pet', 'mount', 'costume'."
|
"invalidTypeEquip": "\"type\" must be one of 'equipped', 'pet', 'mount', 'costume'.",
|
||||||
|
"cannotDeleteActiveAccount": "You have an active subscription, cancel your plan before deleting your account."
|
||||||
}
|
}
|
||||||
|
|||||||
130
test/api/v3/integration/user/DELETE-user.test.js
Normal file
130
test/api/v3/integration/user/DELETE-user.test.js
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import {
|
||||||
|
checkExistence,
|
||||||
|
createAndPopulateGroup,
|
||||||
|
generateGroup,
|
||||||
|
generateUser,
|
||||||
|
translate as t,
|
||||||
|
} from '../../../../helpers/api-integration/v3';
|
||||||
|
import { find } from 'lodash';
|
||||||
|
|
||||||
|
describe('DELETE /user', () => {
|
||||||
|
let user;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
user = await generateUser({balance: 10});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('user has active subscription', async () => {
|
||||||
|
let userWithSubscription = await generateUser({'purchased.plan.customerId': 'fake-customer-id'});
|
||||||
|
|
||||||
|
await expect(userWithSubscription.del('/user')).to.be.rejected.and.to.eventually.eql({
|
||||||
|
code: 401,
|
||||||
|
error: 'NotAuthorized',
|
||||||
|
message: t('cannotDeleteActiveAccount'),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes the user', async () => {
|
||||||
|
await user.del('/user');
|
||||||
|
await expect(checkExistence('users', user._id)).to.eventually.eql(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
context('last member of a party', () => {
|
||||||
|
let party;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
party = await generateGroup(user, {
|
||||||
|
type: 'party',
|
||||||
|
privacy: 'private',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes party when user is the only member', async () => {
|
||||||
|
await user.del('/user');
|
||||||
|
await expect(checkExistence('party', party._id)).to.eventually.eql(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('last member of a private guild', () => {
|
||||||
|
let privateGuild;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
privateGuild = await generateGroup(user, {
|
||||||
|
type: 'guild',
|
||||||
|
privacy: 'private',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deletes guild when user is the only member', async () => {
|
||||||
|
await user.del('/user');
|
||||||
|
await expect(checkExistence('groups', privateGuild._id)).to.eventually.eql(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('groups user is leader of', () => {
|
||||||
|
let guild, oldLeader, newLeader;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
let { group, groupLeader, members } = await createAndPopulateGroup({
|
||||||
|
groupDetails: {
|
||||||
|
type: 'guild',
|
||||||
|
privacy: 'public',
|
||||||
|
},
|
||||||
|
members: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
guild = group;
|
||||||
|
newLeader = members[0];
|
||||||
|
oldLeader = groupLeader;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('chooses new group leader for any group user was the leader of', async () => {
|
||||||
|
await oldLeader.del('/user');
|
||||||
|
|
||||||
|
let updatedGuild = await newLeader.get(`/groups/${guild._id}`);
|
||||||
|
|
||||||
|
expect(updatedGuild.leader).to.exist;
|
||||||
|
expect(updatedGuild.leader._id).to.not.eql(oldLeader._id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
context('groups user is a part of', () => {
|
||||||
|
let group1, group2, userToDelete, otherUser;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
userToDelete = await generateUser({balance: 10});
|
||||||
|
|
||||||
|
group1 = await generateGroup(userToDelete, {
|
||||||
|
type: 'guild',
|
||||||
|
privacy: 'public',
|
||||||
|
});
|
||||||
|
|
||||||
|
let {group, members} = await createAndPopulateGroup({
|
||||||
|
groupDetails: {
|
||||||
|
type: 'guild',
|
||||||
|
privacy: 'public',
|
||||||
|
},
|
||||||
|
members: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
group2 = group;
|
||||||
|
otherUser = members[0];
|
||||||
|
|
||||||
|
await userToDelete.post(`/groups/${group2._id}/join`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes user from all groups user was a part of', async () => {
|
||||||
|
await userToDelete.del('/user');
|
||||||
|
|
||||||
|
let updatedGroup1Members = await otherUser.get(`/groups/${group1._id}/members`);
|
||||||
|
let updatedGroup2Members = await otherUser.get(`/groups/${group2._id}/members`);
|
||||||
|
let userInGroup = find(updatedGroup2Members, (member) => {
|
||||||
|
return member._id === userToDelete._id;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updatedGroup1Members).to.be.empty;
|
||||||
|
expect(updatedGroup2Members).to.not.be.empty;
|
||||||
|
expect(userInGroup).to.not.exist;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -102,42 +102,11 @@ api.getGroups = {
|
|||||||
let types = req.query.type.split(',');
|
let types = req.query.type.split(',');
|
||||||
let groupFields = basicGroupFields.concat('description memberCount balance');
|
let groupFields = basicGroupFields.concat('description memberCount balance');
|
||||||
let sort = '-memberCount';
|
let sort = '-memberCount';
|
||||||
let queries = [];
|
|
||||||
|
|
||||||
types.forEach(type => {
|
let results = await Group.getGroups({user, types, groupFields, sort});
|
||||||
switch (type) {
|
|
||||||
case 'party':
|
|
||||||
queries.push(Group.getGroup({user, groupId: 'party', fields: groupFields}));
|
|
||||||
break;
|
|
||||||
case 'privateGuilds':
|
|
||||||
queries.push(Group.find({
|
|
||||||
type: 'guild',
|
|
||||||
privacy: 'private',
|
|
||||||
_id: {$in: user.guilds},
|
|
||||||
}).select(groupFields).sort(sort).exec());
|
|
||||||
break;
|
|
||||||
case 'publicGuilds':
|
|
||||||
queries.push(Group.find({
|
|
||||||
type: 'guild',
|
|
||||||
privacy: 'public',
|
|
||||||
}).select(groupFields).sort(sort).exec()); // TODO use lean?
|
|
||||||
break;
|
|
||||||
case 'tavern':
|
|
||||||
if (types.indexOf('publicGuilds') === -1) {
|
|
||||||
queries.push(Group.getGroup({user, groupId: 'habitrpg', fields: groupFields}));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// If no valid value for type was supplied, return an error
|
// If no valid value for type was supplied, return an error
|
||||||
if (queries.length === 0) throw new BadRequest(res.t('groupTypesRequired'));
|
if (results.length === 0) throw new BadRequest(res.t('groupTypesRequired'));
|
||||||
|
|
||||||
// TODO we would like not to return a single big array but Q doesn't support the funtionality https://github.com/kriskowal/q/issues/328
|
|
||||||
let results = _.reduce(await Q.all(queries), (previousValue, currentValue) => {
|
|
||||||
if (_.isEmpty(currentValue)) return previousValue; // don't add anything to the results if the query returned null or an empty array
|
|
||||||
return previousValue.concat(Array.isArray(currentValue) ? currentValue : [currentValue]); // otherwise concat the new results to the previousValue
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
res.respond(200, results);
|
res.respond(200, results);
|
||||||
},
|
},
|
||||||
@@ -170,7 +139,8 @@ api.getGroup = {
|
|||||||
|
|
||||||
group = Group.toJSONCleanChat(group, user);
|
group = Group.toJSONCleanChat(group, user);
|
||||||
// TODO Instead of populate we make a find call manually because of https://github.com/Automattic/mongoose/issues/3833
|
// TODO Instead of populate we make a find call manually because of https://github.com/Automattic/mongoose/issues/3833
|
||||||
group.leader = (await User.findById(group.leader).select(nameFields).exec()).toJSON({minimize: true});
|
let leader = await User.findById(group.leader).select(nameFields).exec();
|
||||||
|
if (leader) group.leader = leader.toJSON({minimize: true});
|
||||||
|
|
||||||
res.respond(200, group);
|
res.respond(200, group);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,10 +7,14 @@ import {
|
|||||||
NotAuthorized,
|
NotAuthorized,
|
||||||
} from '../../libs/api-v3/errors';
|
} from '../../libs/api-v3/errors';
|
||||||
import * as Tasks from '../../models/task';
|
import * as Tasks from '../../models/task';
|
||||||
import { model as Group } from '../../models/group';
|
import {
|
||||||
|
basicFields as basicGroupFields,
|
||||||
|
model as Group,
|
||||||
|
} from '../../models/group';
|
||||||
import { model as User } from '../../models/user';
|
import { model as User } from '../../models/user';
|
||||||
import Q from 'q';
|
import Q from 'q';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import * as firebase from '../../libs/api-v3/firebase';
|
||||||
|
|
||||||
let api = {};
|
let api = {};
|
||||||
|
|
||||||
@@ -41,6 +45,47 @@ api.getUser = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {delete} /user DELETE an authenticated user's profile
|
||||||
|
* @apiVersion 3.0.0
|
||||||
|
* @apiName UserDelete
|
||||||
|
* @apiGroup User
|
||||||
|
*
|
||||||
|
* @apiSuccess {} object An empty object
|
||||||
|
*/
|
||||||
|
api.deleteUser = {
|
||||||
|
method: 'DELETE',
|
||||||
|
middlewares: [authWithHeaders(), cron],
|
||||||
|
url: '/user',
|
||||||
|
async handler (req, res) {
|
||||||
|
let user = res.locals.user;
|
||||||
|
let plan = user.purchased.plan;
|
||||||
|
|
||||||
|
if (plan && plan.customerId && !plan.dateTerminated) {
|
||||||
|
throw new NotAuthorized(res.t('cannotDeleteActiveAccount'));
|
||||||
|
}
|
||||||
|
|
||||||
|
let types = ['party', 'publicGuilds', 'privateGuilds'];
|
||||||
|
// @TODO: The group leave route doesn't work unless it has these fields. We should probably force the group to get these
|
||||||
|
let groupFields = basicGroupFields.concat(' leader memberCount');
|
||||||
|
let populateLeader = true;
|
||||||
|
|
||||||
|
let groupsUserIsMemberOf = await Group.getGroups({user, types, groupFields, populateLeader});
|
||||||
|
|
||||||
|
let groupLeavePromises = groupsUserIsMemberOf.map((group) => {
|
||||||
|
return group.leave(user, 'remove-all');
|
||||||
|
});
|
||||||
|
|
||||||
|
await Q.all(groupLeavePromises);
|
||||||
|
|
||||||
|
await user.remove();
|
||||||
|
|
||||||
|
res.respond(200, {});
|
||||||
|
|
||||||
|
firebase.deleteUser(user._id);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const partyMembersFields = 'profile.name stats achievements items.special';
|
const partyMembersFields = 'profile.name stats achievements items.special';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -151,6 +151,50 @@ schema.statics.getGroup = async function getGroup (options = {}) {
|
|||||||
return group;
|
return group;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
schema.statics.getGroups = async function getGroups (options = {}) {
|
||||||
|
let {user, types, groupFields = basicFields, sort = '-memberCount', populateLeader = false} = options;
|
||||||
|
let queries = [];
|
||||||
|
|
||||||
|
types.forEach(type => {
|
||||||
|
switch (type) {
|
||||||
|
case 'party':
|
||||||
|
queries.push(this.getGroup({user, groupId: 'party', fields: groupFields, populateLeader}));
|
||||||
|
break;
|
||||||
|
case 'privateGuilds':
|
||||||
|
let privateGroupQuery = this.find({
|
||||||
|
type: 'guild',
|
||||||
|
privacy: 'private',
|
||||||
|
_id: {$in: user.guilds},
|
||||||
|
}).select(groupFields);
|
||||||
|
if (populateLeader === true) privateGroupQuery.populate('leader', nameFields);
|
||||||
|
privateGroupQuery.sort(sort).exec();
|
||||||
|
queries.push(privateGroupQuery);
|
||||||
|
break;
|
||||||
|
case 'publicGuilds':
|
||||||
|
let publicGroupQuery = this.find({
|
||||||
|
type: 'guild',
|
||||||
|
privacy: 'public',
|
||||||
|
}).select(groupFields);
|
||||||
|
if (populateLeader === true) publicGroupQuery.populate('leader', nameFields);
|
||||||
|
publicGroupQuery.sort(sort).exec();
|
||||||
|
queries.push(publicGroupQuery); // TODO use lean?
|
||||||
|
break;
|
||||||
|
case 'tavern':
|
||||||
|
if (types.indexOf('publicGuilds') === -1) {
|
||||||
|
queries.push(this.getGroup({user, groupId: 'habitrpg', fields: groupFields}));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let groupsArray = _.reduce(await Q.all(queries), (previousValue, currentValue) => {
|
||||||
|
if (_.isEmpty(currentValue)) return previousValue; // don't add anything to the results if the query returned null or an empty array
|
||||||
|
return previousValue.concat(Array.isArray(currentValue) ? currentValue : [currentValue]); // otherwise concat the new results to the previousValue
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return groupsArray;
|
||||||
|
};
|
||||||
|
|
||||||
// When converting to json remove chat messages with more than 1 flag and remove all flags info
|
// When converting to json remove chat messages with more than 1 flag and remove all flags info
|
||||||
// unless the user is an admin
|
// unless the user is an admin
|
||||||
// Not putting into toJSON because there we can't access user
|
// Not putting into toJSON because there we can't access user
|
||||||
|
|||||||
Reference in New Issue
Block a user