Merge pull request #6565 from HabitRPG/sabrecat/quest-accept

WIP: Quests for API v3
This commit is contained in:
Matteo Pagliazzi
2016-02-12 20:56:19 +01:00
23 changed files with 2260 additions and 451 deletions

View File

@@ -0,0 +1,119 @@
import {
createAndPopulateGroup,
translate as t,
generateUser,
} from '../../../../helpers/api-v3-integration.helper';
describe('POST /groups/:groupId/quests/accept', () => {
const PET_QUEST = 'whale';
let questingGroup;
let leader;
let partyMembers;
let user;
beforeEach(async () => {
user = await generateUser();
let { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
members: 2,
});
questingGroup = group;
leader = groupLeader;
partyMembers = members;
await leader.update({
[`items.quests.${PET_QUEST}`]: 1,
});
});
context('failure conditions', () => {
it('does not accept quest without an invite', async () => {
await expect(leader.post(`/groups/${questingGroup._id}/quests/accept`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('questInviteNotFound'),
});
});
it('does not accept quest for a group in which user is not a member', async () => {
await expect(user.post(`/groups/${questingGroup._id}/quests/accept`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('groupNotFound'),
});
});
it('does not accept quest for a guild', async () => {
let { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
});
await expect(guildLeader.post(`/groups/${guild._id}/quests/accept`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('guildQuestsNotSupported'),
});
});
it('does not accept invite twice', async () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
await expect(partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('questAlreadyAccepted'),
});
});
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`);
// quest will start after everyone has accepted
await partyMembers[1].post(`/groups/${questingGroup._id}/quests/accept`);
await expect(partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('questAlreadyUnderway'),
});
});
});
context('successfully accepting a quest invitation', () => {
it('joins a quest from an invitation', async () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
await Promise.all([partyMembers[0].sync(), questingGroup.sync()]);
expect(leader.party.quest.RSVPNeeded).to.equal(false);
expect(questingGroup.quest.members[partyMembers[0]._id]);
});
it('does not begin the quest if pending invitations remain', async () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
await questingGroup.sync();
expect(questingGroup.quest.active).to.equal(false);
});
it('begins the quest if accepting the last pending invite', async () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
// quest will start after everyone has accepted
await partyMembers[1].post(`/groups/${questingGroup._id}/quests/accept`);
await questingGroup.sync();
expect(questingGroup.quest.active).to.equal(true);
});
});
});

View File

@@ -0,0 +1,126 @@
import {
createAndPopulateGroup,
translate as t,
generateUser,
} from '../../../../helpers/api-v3-integration.helper';
describe('POST /groups/:groupId/quests/force-start', () => {
const PET_QUEST = 'whale';
let questingGroup;
let leader;
let partyMembers;
beforeEach(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
members: 2,
});
questingGroup = group;
leader = groupLeader;
partyMembers = members;
await leader.update({
[`items.quests.${PET_QUEST}`]: 1,
});
});
context('failure conditions', () => {
it('does not force start a quest for a group in which user is not a member', async () => {
let nonMember = await generateUser();
await expect(nonMember.post(`/groups/${questingGroup._id}/quests/force-start`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('groupNotFound'),
});
});
it('does not force start quest for a guild', async () => {
let { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
});
await expect(guildLeader.post(`/groups/${guild._id}/quests/force-start`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('guildQuestsNotSupported'),
});
});
it('does not force start for a party without a pending quest', async () => {
await expect(leader.post(`/groups/${questingGroup._id}/quests/force-start`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('questNotPending'),
});
});
it('does not force start 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`);
// quest will start after everyone has accepted
await partyMembers[1].post(`/groups/${questingGroup._id}/quests/accept`);
await expect(leader.post(`/groups/${questingGroup._id}/quests/force-start`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('questAlreadyUnderway'),
});
});
it('does not allow non-quest leader or non-group leader to force start a quest', async () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await expect(partyMembers[0].post(`/groups/${questingGroup._id}/quests/force-start`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('questOrGroupLeaderOnlyStartQuest'),
});
});
});
context('successfully force starting a quest', () => {
it('allows quest leader to force start quest', async () => {
let questLeader = partyMembers[0];
await questLeader.update({[`items.quests.${PET_QUEST}`]: 1});
await questLeader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await questLeader.post(`/groups/${questingGroup._id}/quests/force-start`);
await questingGroup.sync();
expect(questingGroup.quest.active).to.eql(true);
});
it('allows group leader to force start quest', async () => {
let questLeader = partyMembers[0];
await questLeader.update({[`items.quests.${PET_QUEST}`]: 1});
await questLeader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await leader.post(`/groups/${questingGroup._id}/quests/force-start`);
await questingGroup.sync();
expect(questingGroup.quest.active).to.eql(true);
});
it('sends back the quest object', async () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
let quest = await leader.post(`/groups/${questingGroup._id}/quests/force-start`);
expect(quest.active).to.eql(true);
expect(quest.key).to.eql(PET_QUEST);
expect(quest.members).to.eql({
[`${leader._id}`]: true,
});
});
});
});

View File

@@ -0,0 +1,192 @@
import {
createAndPopulateGroup,
translate as t,
sleep,
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
import { quests as questScrolls } from '../../../../../common/script/content';
describe('POST /groups/:groupId/quests/invite/:questKey', () => {
let questingGroup;
let leader;
let member;
const PET_QUEST = 'whale';
beforeEach(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
members: 1,
});
questingGroup = group;
leader = groupLeader;
member = members[0];
});
context('failure conditions', () => {
it('does not issue invites with an invalid group ID', async () => {
await expect(leader.post(`/groups/${generateUUID()}/quests/invite/${PET_QUEST}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('groupNotFound'),
});
});
it('does not issue invites for a group in which user is not a member', async () => {
let { group } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
members: 1,
});
let alternateGroup = group;
await expect(leader.post(`/groups/${alternateGroup._id}/quests/invite/${PET_QUEST}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('groupNotFound'),
});
});
it('does not issue invites for Guilds', async () => {
let { group } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'public' },
members: 1,
});
let alternateGroup = group;
await expect(leader.post(`/groups/${alternateGroup._id}/quests/invite/${PET_QUEST}`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('guildQuestsNotSupported'),
});
});
it('does not issue invites with an invalid quest key', async () => {
const FAKE_QUEST = 'herkimer';
await expect(leader.post(`/groups/${questingGroup._id}/quests/invite/${FAKE_QUEST}`)).to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('questNotFound', {key: FAKE_QUEST}),
});
});
it('does not issue invites for a quest the user does not own', async () => {
await expect(leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('questNotOwned'),
});
});
it('does not issue invites if the user is of insufficient Level', async () => {
const LEVELED_QUEST = 'atom1';
const LEVELED_QUEST_REQ = questScrolls[LEVELED_QUEST].lvl;
const leaderUpdate = {};
leaderUpdate[`items.quests.${LEVELED_QUEST}`] = 1;
leaderUpdate['stats.lvl'] = LEVELED_QUEST_REQ - 1;
await leader.update(leaderUpdate);
await expect(leader.post(`/groups/${questingGroup._id}/quests/invite/${LEVELED_QUEST}`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('questLevelTooHigh', {level: LEVELED_QUEST_REQ}),
});
});
it('does not issue invites if a quest is already underway', async () => {
const QUEST_IN_PROGRESS = 'atom1';
const leaderUpdate = {};
leaderUpdate[`items.quests.${PET_QUEST}`] = 1;
await leader.update(leaderUpdate);
await questingGroup.update({ 'quest.key': QUEST_IN_PROGRESS });
await expect(leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`)).to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('questAlreadyUnderway'),
});
});
});
context('successfully issuing a quest invitation', () => {
beforeEach(async () => {
const memberUpdate = {};
memberUpdate[`items.quests.${PET_QUEST}`] = 1;
await Promise.all([
leader.update(memberUpdate),
member.update(memberUpdate),
]);
});
it('adds quest details to group object', async () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await questingGroup.sync();
let quest = questingGroup.quest;
expect(quest.key).to.eql(PET_QUEST);
expect(quest.active).to.eql(false);
expect(quest.leader).to.eql(leader._id);
expect(quest.members).to.have.property(leader._id, true);
expect(quest.members).to.have.property(member._id, null);
expect(quest).to.have.property('progress');
});
it('adds quest details to user objects', async () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await sleep(0.1); // member updates happen in the background
await Promise.all([
leader.sync(),
member.sync(),
]);
expect(leader.party.quest.key).to.eql(PET_QUEST);
expect(member.party.quest.key).to.eql(PET_QUEST);
expect(leader.party.quest.RSVPNeeded).to.eql(false);
expect(member.party.quest.RSVPNeeded).to.eql(true);
});
it('sends back the quest object', async () => {
let inviteResponse = await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
expect(inviteResponse.key).to.eql(PET_QUEST);
expect(inviteResponse.active).to.eql(false);
expect(inviteResponse.leader).to.eql(leader._id);
expect(inviteResponse.members).to.have.property(leader._id, true);
expect(inviteResponse.members).to.have.property(member._id, null);
expect(inviteResponse).to.have.property('progress');
});
it('allows non-party-leader party members to send invites', async () => {
let inviteResponse = await member.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await questingGroup.sync();
expect(inviteResponse.key).to.eql(PET_QUEST);
expect(questingGroup.quest.key).to.eql(PET_QUEST);
});
it('starts quest automatically if user is in a solo party', async () => {
let leaderDetails = { balance: 10 };
leaderDetails[`items.quests.${PET_QUEST}`] = 1;
let { group, groupLeader } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
leaderDetails,
});
await groupLeader.post(`/groups/${group._id}/quests/invite/${PET_QUEST}`);
await group.sync();
expect(group.quest.active).to.eql(true);
});
});
});

View File

@@ -0,0 +1,126 @@
import {
createAndPopulateGroup,
translate as t,
generateUser,
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
describe('POST /groups/:groupId/quests/abort', () => {
let questingGroup;
let partyMembers;
let user;
let leader;
const PET_QUEST = 'whale';
beforeEach(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
members: 2,
});
questingGroup = group;
leader = groupLeader;
partyMembers = members;
await leader.update({
[`items.quests.${PET_QUEST}`]: 1,
});
user = await generateUser();
});
context('failure conditions', () => {
it('returns an error when group is not found', async () => {
await expect(partyMembers[0].post(`/groups/${generateUUID()}/quests/abort`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('groupNotFound'),
});
});
it('returns an error for a group in which user is not a member', async () => {
await expect(user.post(`/groups/${questingGroup._id}/quests/abort`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('groupNotFound'),
});
});
it('returns an error when group is a guild', async () => {
let { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
});
await expect(guildLeader.post(`/groups/${guild._id}/quests/abort`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('guildQuestsNotSupported'),
});
});
it('returns an error when quest is not active', async () => {
await expect(partyMembers[0].post(`/groups/${questingGroup._id}/quests/abort`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('noActiveQuestToAbort'),
});
});
it('returns an error when non quest leader attempts to abort', async () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
await partyMembers[1].post(`/groups/${questingGroup._id}/quests/accept`);
await expect(partyMembers[0].post(`/groups/${questingGroup._id}/quests/abort`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlyLeaderAbortQuest'),
});
});
});
it('aborts a quest', async () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
await partyMembers[1].post(`/groups/${questingGroup._id}/quests/accept`);
let res = await leader.post(`/groups/${questingGroup._id}/quests/abort`);
await Promise.all([
leader.sync(),
questingGroup.sync(),
partyMembers[0].sync(),
partyMembers[1].sync(),
]);
let cleanUserQuestObj = {
key: null,
progress: {
up: 0,
down: 0,
collect: {},
},
completed: null,
RSVPNeeded: false,
};
expect(leader.party.quest).to.eql(cleanUserQuestObj);
expect(partyMembers[0].party.quest).to.eql(cleanUserQuestObj);
expect(partyMembers[1].party.quest).to.eql(cleanUserQuestObj);
expect(leader.items.quests[PET_QUEST]).to.equal(1);
expect(questingGroup.quest).to.deep.equal(res);
expect(questingGroup.quest).to.eql({
key: null,
active: false,
leader: null,
progress: {
collect: {},
},
members: {},
});
});
});

View File

@@ -0,0 +1,138 @@
import {
createAndPopulateGroup,
translate as t,
generateUser,
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
describe('POST /groups/:groupId/quests/cancel', () => {
let questingGroup;
let partyMembers;
let user;
let leader;
const PET_QUEST = 'whale';
beforeEach(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
members: 2,
});
questingGroup = group;
leader = groupLeader;
partyMembers = members;
await leader.update({
[`items.quests.${PET_QUEST}`]: 1,
});
user = await generateUser();
});
context('failure conditions', () => {
it('returns an error when group is not found', async () => {
await expect(partyMembers[0].post(`/groups/${generateUUID()}/quests/cancel`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('groupNotFound'),
});
});
it('does not reject quest for a group in which user is not a member', async () => {
await expect(user.post(`/groups/${questingGroup._id}/quests/cancel`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('groupNotFound'),
});
});
it('returns an error when group is a guild', async () => {
let { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
});
await expect(guildLeader.post(`/groups/${guild._id}/quests/cancel`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('guildQuestsNotSupported'),
});
});
it('returns an error when group is not on a quest', async () => {
await expect(partyMembers[0].post(`/groups/${questingGroup._id}/quests/cancel`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('questInvitationDoesNotExist'),
});
});
it('only the leader can cancel the quest', async () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await expect(partyMembers[0].post(`/groups/${questingGroup._id}/quests/cancel`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('onlyLeaderCancelQuest'),
});
});
it('does not cancel a quest already underway', async () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
// quest will start after everyone has accepted
await partyMembers[1].post(`/groups/${questingGroup._id}/quests/accept`);
await expect(leader.post(`/groups/${questingGroup._id}/quests/cancel`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('cantCancelActiveQuest'),
});
});
});
it('cancels a quest', async () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
let res = await leader.post(`/groups/${questingGroup._id}/quests/cancel`);
await Promise.all([
leader.sync(),
partyMembers[0].sync(),
partyMembers[1].sync(),
questingGroup.sync(),
]);
let clean = {
key: null,
progress: {
up: 0,
down: 0,
collect: {},
},
completed: null,
RSVPNeeded: false,
};
expect(leader.party.quest).to.eql(clean);
expect(partyMembers[1].party.quest).to.eql(clean);
expect(partyMembers[0].party.quest).to.eql(clean);
expect(res).to.eql(questingGroup.quest);
expect(questingGroup.quest).to.eql({
key: null,
active: false,
leader: null,
progress: {
collect: {},
},
members: {},
});
});
});

View File

@@ -0,0 +1,124 @@
import {
createAndPopulateGroup,
translate as t,
generateUser,
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
describe('POST /groups/:groupId/quests/leave', () => {
let questingGroup;
let partyMembers;
let user;
let leader;
const PET_QUEST = 'whale';
beforeEach(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
members: 2,
});
questingGroup = group;
leader = groupLeader;
partyMembers = members;
await leader.update({
[`items.quests.${PET_QUEST}`]: 1,
});
user = await generateUser();
});
context('failure conditions', () => {
it('returns an error when group is not found', async () => {
await expect(partyMembers[0].post(`/groups/${generateUUID()}/quests/leave`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('groupNotFound'),
});
});
it('returns an error for a group in which user is not a member', async () => {
await expect(user.post(`/groups/${questingGroup._id}/quests/leave`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('groupNotFound'),
});
});
it('returns an error when group is a guild', async () => {
let { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
});
await expect(guildLeader.post(`/groups/${guild._id}/quests/leave`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('guildQuestsNotSupported'),
});
});
it('returns an error when quest is not active', async () => {
await expect(partyMembers[0].post(`/groups/${questingGroup._id}/quests/leave`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('noActiveQuestToLeave'),
});
});
it('returns an error when quest leader attempts to leave', async () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
await partyMembers[1].post(`/groups/${questingGroup._id}/quests/accept`);
await expect(leader.post(`/groups/${questingGroup._id}/quests/leave`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('questLeaderCannotLeaveQuest'),
});
});
it('returns an error when non quest member attempts to leave', async () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
await partyMembers[1].post(`/groups/${questingGroup._id}/quests/reject`);
await expect(partyMembers[1].post(`/groups/${questingGroup._id}/quests/leave`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('notPartOfQuest'),
});
});
});
it('leaves a quest', async () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
await partyMembers[1].post(`/groups/${questingGroup._id}/quests/accept`);
let leaveResult = await partyMembers[0].post(`/groups/${questingGroup._id}/quests/leave`);
await Promise.all([
partyMembers[0].sync(),
questingGroup.sync(),
]);
expect(partyMembers[0].party.quest).to.eql({
key: null,
progress: {
up: 0,
down: 0,
collect: {},
},
completed: null,
RSVPNeeded: false,
});
expect(questingGroup.quest).to.deep.equal(leaveResult);
expect(questingGroup.quest.members[partyMembers[0]._id]).to.be.false;
});
});

View File

@@ -0,0 +1,146 @@
import {
createAndPopulateGroup,
translate as t,
generateUser,
} from '../../../../helpers/api-v3-integration.helper';
import { v4 as generateUUID } from 'uuid';
describe('POST /groups/:groupId/quests/reject', () => {
let questingGroup;
let partyMembers;
let user;
let leader;
const PET_QUEST = 'whale';
beforeEach(async () => {
let { group, groupLeader, members } = await createAndPopulateGroup({
groupDetails: { type: 'party', privacy: 'private' },
members: 2,
});
questingGroup = group;
leader = groupLeader;
partyMembers = members;
await leader.update({
[`items.quests.${PET_QUEST}`]: 1,
});
user = await generateUser();
});
context('failure conditions', () => {
it('returns an error when group is not found', async () => {
await expect(partyMembers[0].post(`/groups/${generateUUID()}/quests/reject`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('groupNotFound'),
});
});
it('does not accept quest for a group in which user is not a member', async () => {
await expect(user.post(`/groups/${questingGroup._id}/quests/accept`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('groupNotFound'),
});
});
it('returns an error when group is a guild', async () => {
let { group: guild, groupLeader: guildLeader } = await createAndPopulateGroup({
groupDetails: { type: 'guild', privacy: 'private' },
});
await expect(guildLeader.post(`/groups/${guild._id}/quests/reject`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('guildQuestsNotSupported'),
});
});
it('returns an error when group is not on a quest', async () => {
await expect(partyMembers[0].post(`/groups/${questingGroup._id}/quests/reject`))
.to.eventually.be.rejected.and.eql({
code: 404,
error: 'NotFound',
message: t('questInvitationDoesNotExist'),
});
});
it('return an error when an user rejects an invite twice', async () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/reject`);
await expect(partyMembers[0].post(`/groups/${questingGroup._id}/quests/reject`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('questAlreadyRejected'),
});
});
it('return an error when an 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`);
await expect(partyMembers[0].post(`/groups/${questingGroup._id}/quests/reject`))
.to.eventually.be.rejected.and.eql({
code: 400,
error: 'BadRequest',
message: t('questAlreadyAccepted'),
});
});
it('does not reject 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`);
// quest will start after everyone has accepted
await partyMembers[1].post(`/groups/${questingGroup._id}/quests/accept`);
await expect(partyMembers[0].post(`/groups/${questingGroup._id}/quests/reject`))
.to.eventually.be.rejected.and.eql({
code: 401,
error: 'NotAuthorized',
message: t('questAlreadyUnderway'),
});
});
});
context('successfully quest rejection', () => {
let cleanUserQuestObj = {
key: null,
progress: {
up: 0,
down: 0,
collect: {},
},
completed: null,
RSVPNeeded: false,
};
it('rejects a quest invitation', async () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
let res = await partyMembers[0].post(`/groups/${questingGroup._id}/quests/reject`);
await partyMembers[0].sync();
await questingGroup.sync();
expect(partyMembers[0].party.quest).to.eql(cleanUserQuestObj);
expect(questingGroup.quest.members[partyMembers[0]._id]).to.be.false;
expect(questingGroup.quest.active).to.be.false;
expect(res).to.eql(questingGroup.quest);
});
it('starts the quest when the last user reject', async () => {
await leader.post(`/groups/${questingGroup._id}/quests/invite/${PET_QUEST}`);
await partyMembers[0].post(`/groups/${questingGroup._id}/quests/accept`);
await partyMembers[1].post(`/groups/${questingGroup._id}/quests/reject`);
await questingGroup.sync();
expect(questingGroup.quest.active).to.be.true;
});
});
});

View File

@@ -0,0 +1,300 @@
import { sleep } from '../../../../helpers/api-unit.helper';
import { model as Group } from '../../../../../website/src/models/group';
import { model as User } from '../../../../../website/src/models/user';
import { quests as questScrolls } from '../../../../../common/script/content';
import * as email from '../../../../../website/src/libs/api-v3/email';
describe('Group Model', () => {
context('Instance Methods', () => {
describe('#startQuest', () => {
let party, questLeader, participatingMember, nonParticipatingMember, undecidedMember;
beforeEach(async () => {
sandbox.stub(email, 'sendTxn');
party = new Group({
name: 'test party',
type: 'party',
privacy: 'private',
});
questLeader = new User({
party: { _id: party._id },
items: {
quests: {
whale: 1,
},
},
});
party.leader = questLeader._id;
participatingMember = new User({
party: { _id: party._id },
});
nonParticipatingMember = new User({
party: { _id: party._id },
});
undecidedMember = new User({
party: { _id: party._id },
});
await Promise.all([
party.save(),
questLeader.save(),
participatingMember.save(),
nonParticipatingMember.save(),
undecidedMember.save(),
]);
});
context('Failure Conditions', () => {
it('throws an error if group is not a party', async () => {
let guild = new Group({
type: 'guild',
});
await expect(guild.startQuest(participatingMember)).to.eventually.be.rejected;
});
it('throws an error if party is not on a quest', async () => {
await expect(party.startQuest(participatingMember)).to.eventually.be.rejected;
});
it('throws an error if quest is already active', async () => {
party.quest.key = 'whale';
party.quest.active = true;
await expect(party.startQuest(participatingMember)).to.eventually.be.rejected;
});
});
context('Successes', () => {
beforeEach(() => {
party.quest.key = 'whale';
party.quest.active = false;
party.quest.leader = questLeader._id;
party.quest.members = { };
party.quest.members[questLeader._id] = true;
party.quest.members[participatingMember._id] = true;
party.quest.members[nonParticipatingMember._id] = false;
party.quest.members[undecidedMember._id] = null;
});
it('activates quest', () => {
party.startQuest(participatingMember);
expect(party.quest.active).to.eql(true);
});
it('sets up boss quest', () => {
let bossQuest = questScrolls.whale;
party.quest.key = bossQuest.key;
party.startQuest(participatingMember);
expect(party.quest.progress.hp).to.eql(bossQuest.boss.hp);
});
it('sets up rage meter for rage boss quest', () => {
let rageBossQuest = questScrolls.trex_undead;
party.quest.key = rageBossQuest.key;
party.startQuest(participatingMember);
expect(party.quest.progress.rage).to.eql(0);
});
it('sets up collection quest', () => {
let collectionQuest = questScrolls.vice2;
party.quest.key = collectionQuest.key;
party.startQuest(participatingMember);
expect(party.quest.progress.collect).to.eql({
lightCrystal: 0,
});
});
it('sets up collection quest with multiple items', () => {
let collectionQuest = questScrolls.evilsanta2;
party.quest.key = collectionQuest.key;
party.startQuest(participatingMember);
expect(party.quest.progress.collect).to.eql({
tracks: 0,
branches: 0,
});
});
it('prunes non-participating members from quest members object', () => {
party.startQuest(participatingMember);
let expectedQuestMembers = {};
expectedQuestMembers[questLeader._id] = true;
expectedQuestMembers[participatingMember._id] = true;
expect(party.quest.members).to.eql(expectedQuestMembers);
});
it('applies updates to user object directly if user is participating', async () => {
await party.startQuest(participatingMember);
expect(participatingMember.party.quest.key).to.eql('whale');
expect(participatingMember.party.quest.progress.down).to.eql(0);
expect(participatingMember.party.quest.collect).to.eql({});
expect(participatingMember.party.quest.completed).to.eql(null);
});
it('applies updates to other participating members', async () => {
await party.startQuest(nonParticipatingMember);
questLeader = await User.findById(questLeader._id);
participatingMember = await User.findById(participatingMember._id);
expect(participatingMember.party.quest.key).to.eql('whale');
expect(participatingMember.party.quest.progress.down).to.eql(0);
expect(participatingMember.party.quest.progress.collect).to.eql({});
expect(participatingMember.party.quest.completed).to.eql(null);
expect(questLeader.party.quest.key).to.eql('whale');
expect(questLeader.party.quest.progress.down).to.eql(0);
expect(questLeader.party.quest.progress.collect).to.eql({});
expect(questLeader.party.quest.completed).to.eql(null);
});
it('does not apply updates to nonparticipating members', async () => {
await party.startQuest(participatingMember);
nonParticipatingMember = await User.findById(nonParticipatingMember ._id);
undecidedMember = await User.findById(undecidedMember._id);
expect(nonParticipatingMember.party.quest.key).to.not.eql('whale');
expect(undecidedMember.party.quest.key).to.not.eql('whale');
});
it('sends email to participating members that quest has started', async () => {
participatingMember.preferences.emailNotifications.questStarted = true;
questLeader.preferences.emailNotifications.questStarted = true;
await Promise.all([
participatingMember.save(),
questLeader.save(),
]);
await party.startQuest(nonParticipatingMember);
await sleep(0.5);
expect(email.sendTxn).to.be.calledOnce;
let memberIds = _.pluck(email.sendTxn.args[0][0], '_id');
let typeOfEmail = email.sendTxn.args[0][1];
expect(memberIds).to.have.a.lengthOf(2);
expect(memberIds).to.include(participatingMember._id);
expect(memberIds).to.include(questLeader._id);
expect(typeOfEmail).to.eql('quest-started');
});
it('sends email only to members who have not opted out', async () => {
participatingMember.preferences.emailNotifications.questStarted = false;
questLeader.preferences.emailNotifications.questStarted = true;
await Promise.all([
participatingMember.save(),
questLeader.save(),
]);
await party.startQuest(nonParticipatingMember);
await sleep(0.5);
expect(email.sendTxn).to.be.calledOnce;
let memberIds = _.pluck(email.sendTxn.args[0][0], '_id');
expect(memberIds).to.have.a.lengthOf(1);
expect(memberIds).to.not.include(participatingMember._id);
expect(memberIds).to.include(questLeader._id);
});
it('does not send email to initiating member', async () => {
participatingMember.preferences.emailNotifications.questStarted = true;
questLeader.preferences.emailNotifications.questStarted = true;
await Promise.all([
participatingMember.save(),
questLeader.save(),
]);
await party.startQuest(participatingMember);
await sleep(0.5);
expect(email.sendTxn).to.be.calledOnce;
let memberIds = _.pluck(email.sendTxn.args[0][0], '_id');
expect(memberIds).to.have.a.lengthOf(1);
expect(memberIds).to.not.include(participatingMember._id);
expect(memberIds).to.include(questLeader._id);
});
it('updates participting members (not including user)', async () => {
sandbox.spy(User, 'update');
await party.startQuest(nonParticipatingMember);
let members = [questLeader._id, participatingMember._id];
expect(User.update).to.be.calledWith(
{ _id: { $in: members } },
{
$set: {
'party.quest.key': 'whale',
'party.quest.progress.down': 0,
'party.quest.collect': {},
'party.quest.completed': null,
},
}
);
});
it('updates non-user quest leader and decrements quest scroll', async () => {
sandbox.spy(User, 'update');
await party.startQuest(participatingMember);
expect(User.update).to.be.calledWith(
{ _id: questLeader._id },
{
$inc: {
'items.quests.whale': -1,
},
}
);
});
it('modifies the participating initiating user directly', async () => {
await party.startQuest(participatingMember);
let userQuest = participatingMember.party.quest;
expect(userQuest.key).to.eql('whale');
expect(userQuest.progress.down).to.eql(0);
expect(userQuest.collect).to.eql({});
expect(userQuest.completed).to.eql(null);
});
it('does not modify user if not participating', async () => {
await party.startQuest(nonParticipatingMember);
expect(nonParticipatingMember.party.quest.key).to.not.eql('whale');
});
it('removes the quest directly if initiating user is the quest leader', async () => {
await party.startQuest(questLeader);
expect(questLeader.items.quests.whale).to.eql(0);
});
});
});
});
});