mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-18 07:07:35 +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.",
|
||||
"invalidPetName": "Invalid pet name supplied.",
|
||||
"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 groupFields = basicGroupFields.concat('description memberCount balance');
|
||||
let sort = '-memberCount';
|
||||
let queries = [];
|
||||
|
||||
types.forEach(type => {
|
||||
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;
|
||||
}
|
||||
});
|
||||
let results = await Group.getGroups({user, types, groupFields, sort});
|
||||
|
||||
// If no valid value for type was supplied, return an error
|
||||
if (queries.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
|
||||
}, []);
|
||||
if (results.length === 0) throw new BadRequest(res.t('groupTypesRequired'));
|
||||
|
||||
res.respond(200, results);
|
||||
},
|
||||
@@ -170,7 +139,8 @@ api.getGroup = {
|
||||
|
||||
group = Group.toJSONCleanChat(group, user);
|
||||
// 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);
|
||||
},
|
||||
|
||||
@@ -7,10 +7,14 @@ import {
|
||||
NotAuthorized,
|
||||
} from '../../libs/api-v3/errors';
|
||||
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 Q from 'q';
|
||||
import _ from 'lodash';
|
||||
import * as firebase from '../../libs/api-v3/firebase';
|
||||
|
||||
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';
|
||||
|
||||
/**
|
||||
|
||||
@@ -151,6 +151,50 @@ schema.statics.getGroup = async function getGroup (options = {}) {
|
||||
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
|
||||
// unless the user is an admin
|
||||
// Not putting into toJSON because there we can't access user
|
||||
|
||||
Reference in New Issue
Block a user