Ported user delete route and added initial tests

This commit is contained in:
Keith Holliday
2016-03-20 14:08:08 -05:00
parent be9312deb0
commit 7f65707ee4
5 changed files with 226 additions and 36 deletions

View File

@@ -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."
} }

View 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;
});
});
});

View File

@@ -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);
}, },

View File

@@ -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';
/** /**

View File

@@ -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