mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-13 04:37:36 +01:00
Api quest restrictions - no purchase/start without fulfilling eligibility requirements (#10387)
* removing duplicate translation key * fixing typos * extracting quest prerequisite check. adding check for previous quest completion, if required * fixing (undoing) static change, adding tests * more typos * correcting test failures * honoring quest prerequisites in quest invite API call. updating format of il8n string replacement arg * no longer using apiError, use translate method instead (msg key was not defined) * adding @apiError to docblock as requested in issue * removing checks on quest invite method. small window of opportunity/low risk
This commit is contained in:
committed by
Matteo Pagliazzi
parent
8fb67e7944
commit
ac90a40be5
@@ -38,4 +38,32 @@ describe('POST /user/buy-quest/:key', () => {
|
||||
itemText: item.text(),
|
||||
}));
|
||||
});
|
||||
|
||||
it('returns an error if quest prerequisites are not met', async () => {
|
||||
let key = 'dilatoryDistress2';
|
||||
|
||||
await expect(user.post(`/user/buy-quest/${key}`))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 401,
|
||||
error: 'NotAuthorized',
|
||||
message: t('mustComplete', {quest: 'dilatoryDistress1'}),
|
||||
});
|
||||
});
|
||||
|
||||
it('allows purchase of a quest if prerequisites are met', async () => {
|
||||
const prerequisite = 'dilatoryDistress1';
|
||||
const key = 'dilatoryDistress2';
|
||||
const item = content.quests[key];
|
||||
|
||||
const achievementName = `achievements.quests.${prerequisite}`;
|
||||
|
||||
await user.update({[achievementName]: true, 'stats.gp': 9999});
|
||||
let res = await user.post(`/user/buy-quest/${key}`);
|
||||
await user.sync();
|
||||
|
||||
expect(res.data).to.eql(user.items.quests);
|
||||
expect(res.message).to.equal(t('messageBought', {
|
||||
itemText: item.text(),
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -156,4 +156,19 @@ describe('shared.ops.buyQuest', () => {
|
||||
done();
|
||||
}
|
||||
});
|
||||
|
||||
it('does not buy a quest without completing previous quests', (done) => {
|
||||
try {
|
||||
buyQuest(user, {
|
||||
params: {
|
||||
key: 'dilatoryDistress3',
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
expect(err).to.be.an.instanceof(NotAuthorized);
|
||||
expect(err.message).to.equal(i18n.t('mustComplete', {quest: 'dilatoryDistress2'}));
|
||||
expect(user.items.quests).to.eql({});
|
||||
done();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@ import * as Tasks from '../../../../website/server/models/task';
|
||||
// If you need the user to have specific requirements,
|
||||
// such as a balance > 0, just pass in the adjustment
|
||||
// to the update object. If you want to adjust a nested
|
||||
// paramter, such as the number of wolf eggs the user has,
|
||||
// parameter, such as the number of wolf eggs the user has,
|
||||
// , you can do so by passing in the full path as a string:
|
||||
// { 'items.eggs.Wolf': 10 }
|
||||
export async function generateUser (update = {}) {
|
||||
|
||||
@@ -110,7 +110,6 @@
|
||||
"questAlreadyRejected": "You already rejected the quest invitation.",
|
||||
"cantCancelActiveQuest": "You can not cancel an active quest, use the abort functionality.",
|
||||
"onlyLeaderCancelQuest": "Only the group or quest leader can cancel the quest.",
|
||||
"questInvitationDoesNotExist": "No quest invitation has been sent out yet.",
|
||||
"questNotPending": "There is no quest to start.",
|
||||
"questOrGroupLeaderOnlyStartQuest": "Only the quest leader or group leader can force start the quest",
|
||||
"createAccountReward": "Create Account",
|
||||
|
||||
@@ -23,7 +23,7 @@ module.exports = function buy (user, req = {}, analytics) {
|
||||
if (!key) throw new BadRequest(errorMessage('missingKeyParam'));
|
||||
|
||||
// @TODO: Slowly remove the need for key and use type instead
|
||||
// This should evenutally be the 'factory' function with vendor classes
|
||||
// This should eventually be the 'factory' function with vendor classes
|
||||
let type = get(req, 'type');
|
||||
if (!type) type = get(req, 'params.type');
|
||||
if (!type) type = key;
|
||||
|
||||
@@ -37,10 +37,6 @@ export class BuyQuestWithGoldOperation extends AbstractGoldItemOperation {
|
||||
let key = this.key = get(req, 'params.key');
|
||||
if (!key) throw new BadRequest(errorMessage('missingKeyParam'));
|
||||
|
||||
if (key === 'lostMasterclasser1' && !this.userAbleToStartMasterClasser(user)) {
|
||||
throw new NotAuthorized(this.i18n('questUnlockLostMasterclasser'));
|
||||
}
|
||||
|
||||
let item = content.quests[key];
|
||||
|
||||
if (!item) throw new NotFound(errorMessage('questNotFound', {key}));
|
||||
@@ -49,9 +45,22 @@ export class BuyQuestWithGoldOperation extends AbstractGoldItemOperation {
|
||||
throw new NotAuthorized(this.i18n('questNotGoldPurchasable', {key}));
|
||||
}
|
||||
|
||||
this.checkPrerequisites(user, key);
|
||||
|
||||
this.canUserPurchase(user, item);
|
||||
}
|
||||
|
||||
checkPrerequisites (user, questKey) {
|
||||
const item = content.quests[questKey];
|
||||
if (questKey === 'lostMasterclasser1' && !this.userAbleToStartMasterClasser(user)) {
|
||||
throw new NotAuthorized(this.i18n('questUnlockLostMasterclasser'));
|
||||
}
|
||||
|
||||
if (item && item.previous && !user.achievements.quests[item.previous]) {
|
||||
throw new NotAuthorized(this.i18n('mustComplete', {quest: item.previous}));
|
||||
}
|
||||
}
|
||||
|
||||
executeChanges (user, item, req) {
|
||||
user.items.quests[item.key] = user.items.quests[item.key] || 0;
|
||||
user.items.quests[item.key] += this.quantity;
|
||||
|
||||
@@ -830,7 +830,9 @@ api.buyMysterySet = {
|
||||
*
|
||||
* @apiErrorExample {json} Quest chosen does not exist
|
||||
* {"success":false,"error":"NotFound","message":"Quest \"dilatoryDistress99\" not found."}
|
||||
* @apiErrorExample {json} NotAuthorized Not enough gold
|
||||
* @apiErrorExample {json} You must first complete this quest's prerequisites
|
||||
* {"success":false,"error":"NotAuthorized","message":"You must first complete dilatoryDistress2."}
|
||||
* @apiErrorExample {json} NotAuthorized Not enough gold
|
||||
* {"success":false,"error":"NotAuthorized","message":"Not Enough Gold"}
|
||||
*
|
||||
*/
|
||||
@@ -912,7 +914,7 @@ api.buySpecialSpell = {
|
||||
* }
|
||||
*
|
||||
* @apiError {NotAuthorized} messageAlreadyPet Already have the specific pet combination
|
||||
* @apiError {NotFound} messageMissingEggPotion One or both of the ingrediants are missing.
|
||||
* @apiError {NotFound} messageMissingEggPotion One or both of the ingredients are missing.
|
||||
* @apiError {NotFound} messageInvalidEggPotionCombo Cannot use that combination of egg and potion.
|
||||
*
|
||||
* @apiErrorExample {json} Already have that pet.
|
||||
|
||||
@@ -35,7 +35,7 @@ export function sha1MakeSalt (len = 10) {
|
||||
}
|
||||
|
||||
// Compare the password for an user
|
||||
// Works with bcrypt and sha1 indipendently
|
||||
// Works with bcrypt and sha1 independently
|
||||
// An async function is used so that a promise is always returned
|
||||
// even for comparing sha1 hashed passwords that use a sync method
|
||||
export async function compare (user, passwordToCheck) {
|
||||
|
||||
@@ -20,7 +20,7 @@ const app = express();
|
||||
app.set('port', nconf.get('PORT'));
|
||||
|
||||
// Setup translations
|
||||
// Must come before attach middlwares so Mongoose validations can use translations
|
||||
// Must come before attach middlewares so Mongoose validations can use translations
|
||||
import './libs/i18n';
|
||||
|
||||
import attachMiddlewares from './middlewares/index';
|
||||
|
||||
Reference in New Issue
Block a user