fix(quests): stuck "you were invited" banner after accept/reject (#15647)
wrap the conditional group update and the user save in a single transaction so a failure between them can't leave members[uid] and RSVPNeeded out of sync
This commit is contained in:
@@ -91,6 +91,23 @@ describe('POST /groups/:groupId/quests/accept', () => {
|
||||
expect(partyMembers[0].party.quest.RSVPNeeded).to.be.false;
|
||||
});
|
||||
|
||||
it('heals stuck RSVPNeeded when group already has the user accepted', async () => {
|
||||
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
|
||||
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
|
||||
|
||||
await partyMembers[0].updateOne({ 'party.quest.RSVPNeeded': true });
|
||||
await partyMembers[0].sync();
|
||||
expect(partyMembers[0].party.quest.RSVPNeeded).to.be.true;
|
||||
|
||||
const res = await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
|
||||
expect(res).to.exist;
|
||||
|
||||
await partyMembers[0].sync();
|
||||
await questingGroup.sync();
|
||||
expect(partyMembers[0].party.quest.RSVPNeeded).to.equal(false);
|
||||
expect(questingGroup.quest.members[partyMembers[0]._id]).to.equal(true);
|
||||
});
|
||||
|
||||
it('does not accept invite for a quest already underway', async () => {
|
||||
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
|
||||
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
|
||||
|
||||
@@ -100,6 +100,23 @@ describe('POST /groups/:groupId/quests/reject', () => {
|
||||
expect(partyMembers[0].party.quest.RSVPNeeded).to.be.false;
|
||||
});
|
||||
|
||||
it('heals stuck RSVPNeeded when group already has the user rejected', async () => {
|
||||
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
|
||||
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/reject`);
|
||||
|
||||
await partyMembers[0].updateOne({ 'party.quest.RSVPNeeded': true });
|
||||
await partyMembers[0].sync();
|
||||
expect(partyMembers[0].party.quest.RSVPNeeded).to.be.true;
|
||||
|
||||
const res = await partyMembers[0].post(`/groups/${questingGroup._id}/quests/reject`);
|
||||
expect(res).to.exist;
|
||||
|
||||
await partyMembers[0].sync();
|
||||
await questingGroup.sync();
|
||||
expect(partyMembers[0].party.quest.RSVPNeeded).to.equal(false);
|
||||
expect(questingGroup.quest.members[partyMembers[0]._id]).to.equal(false);
|
||||
});
|
||||
|
||||
it('return an error when a user rejects an invite already accepted', async () => {
|
||||
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
|
||||
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
|
||||
|
||||
@@ -198,16 +198,32 @@ api.acceptQuest = {
|
||||
if (group.type !== 'party') throw new NotAuthorized(res.t('guildQuestsNotSupported'));
|
||||
if (!group.quest.key) throw new NotFound(res.t('questInviteNotFound'));
|
||||
if (group.quest.active) throw new NotAuthorized(res.t('questAlreadyStartedFriendly'));
|
||||
if (group.quest.members[user._id]) throw new BadRequest(res.t('questAlreadyAccepted'));
|
||||
|
||||
const acceptedSuccessfully = await group.handleQuestInvitation(user, true);
|
||||
if (group.quest.members[user._id] === true) {
|
||||
if (user.party.quest.RSVPNeeded) {
|
||||
user.party.quest.RSVPNeeded = false;
|
||||
await user.save();
|
||||
res.respond(200, group.quest);
|
||||
return;
|
||||
}
|
||||
throw new BadRequest(res.t('questAlreadyAccepted'));
|
||||
}
|
||||
if (group.quest.members[user._id] === false) {
|
||||
throw new BadRequest(res.t('questAlreadyAccepted'));
|
||||
}
|
||||
|
||||
let acceptedSuccessfully = false;
|
||||
await Group.db.transaction(async session => {
|
||||
acceptedSuccessfully = await group.handleQuestInvitation(user, true, session);
|
||||
if (!acceptedSuccessfully) return;
|
||||
user.party.quest.RSVPNeeded = false;
|
||||
await user.save({ session });
|
||||
});
|
||||
|
||||
if (!acceptedSuccessfully) {
|
||||
throw new NotAuthorized(res.t('questAlreadyAccepted'));
|
||||
}
|
||||
|
||||
user.party.quest.RSVPNeeded = false;
|
||||
await user.save();
|
||||
|
||||
if (canStartQuestAutomatically(group)) {
|
||||
await group.startQuest(user);
|
||||
}
|
||||
@@ -251,18 +267,34 @@ api.rejectQuest = {
|
||||
if (group.type !== 'party') throw new NotAuthorized(res.t('guildQuestsNotSupported'));
|
||||
if (!group.quest.key) throw new NotFound(res.t('questInvitationDoesNotExist'));
|
||||
if (group.quest.active) throw new NotAuthorized(res.t('questAlreadyStartedFriendly'));
|
||||
if (group.quest.members[user._id]) throw new BadRequest(res.t('questAlreadyAccepted'));
|
||||
if (group.quest.members[user._id] === false) throw new BadRequest(res.t('questAlreadyRejected'));
|
||||
|
||||
const rejectedSuccessfully = await group.handleQuestInvitation(user, false);
|
||||
if (group.quest.members[user._id] === true) {
|
||||
throw new BadRequest(res.t('questAlreadyAccepted'));
|
||||
}
|
||||
if (group.quest.members[user._id] === false) {
|
||||
if (user.party.quest.RSVPNeeded) {
|
||||
user.party.quest = Group.cleanQuestUser(user.party.quest.progress);
|
||||
user.markModified('party.quest');
|
||||
await user.save();
|
||||
res.respond(200, group.quest);
|
||||
return;
|
||||
}
|
||||
throw new BadRequest(res.t('questAlreadyRejected'));
|
||||
}
|
||||
|
||||
let rejectedSuccessfully = false;
|
||||
await Group.db.transaction(async session => {
|
||||
rejectedSuccessfully = await group.handleQuestInvitation(user, false, session);
|
||||
if (!rejectedSuccessfully) return;
|
||||
user.party.quest = Group.cleanQuestUser(user.party.quest.progress);
|
||||
user.markModified('party.quest');
|
||||
await user.save({ session });
|
||||
});
|
||||
|
||||
if (!rejectedSuccessfully) {
|
||||
throw new NotAuthorized(res.t('questAlreadyRejected'));
|
||||
}
|
||||
|
||||
user.party.quest = Group.cleanQuestUser(user.party.quest.progress);
|
||||
user.markModified('party.quest');
|
||||
await user.save();
|
||||
|
||||
if (canStartQuestAutomatically(group)) {
|
||||
await group.startQuest(user);
|
||||
}
|
||||
|
||||
@@ -654,7 +654,8 @@ schema.methods.sendChat = async function sendChat (options = {}) {
|
||||
return newChatMessage;
|
||||
};
|
||||
|
||||
schema.methods.handleQuestInvitation = async function handleQuestInvitation (user, accept) {
|
||||
// eslint-disable-next-line max-len
|
||||
schema.methods.handleQuestInvitation = async function handleQuestInvitation (user, accept, session) {
|
||||
if (!user) throw new InternalServerError('Must provide user to handle quest invitation');
|
||||
if (accept !== true && accept !== false) throw new InternalServerError('Must provide accept param handle quest invitation');
|
||||
|
||||
@@ -662,12 +663,14 @@ schema.methods.handleQuestInvitation = async function handleQuestInvitation (use
|
||||
// to prevent multiple concurrent requests overriding updates
|
||||
// see https://github.com/HabitRPG/habitica/issues/11398
|
||||
const Group = this.constructor;
|
||||
const options = session ? { session } : {};
|
||||
const result = await Group.updateOne(
|
||||
{
|
||||
_id: this._id,
|
||||
[`quest.members.${user._id}`]: { $type: 10 }, // match BSON Type Null (type number 10)
|
||||
},
|
||||
{ $set: { [`quest.members.${user._id}`]: accept } },
|
||||
options,
|
||||
).exec();
|
||||
|
||||
if (result.modifiedCount) {
|
||||
|
||||
Reference in New Issue
Block a user