mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 22:57:21 +01:00
Improved number validation (#11131)
* fix(purchasing): more number validation * test(purchasing): add error cases Also refactor NaN check and create client mixin * fix(purchasing): cover "purchase" cases
This commit is contained in:
@@ -142,4 +142,52 @@ describe('shared.ops.buy', () => {
|
|||||||
buy(user, {params: {key: 'potion'}, quantity: 2});
|
buy(user, {params: {key: 'potion'}, quantity: 2});
|
||||||
expect(user.stats.hp).to.eql(50);
|
expect(user.stats.hp).to.eql(50);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('errors if user supplies a non-numeric quantity', (done) => {
|
||||||
|
try {
|
||||||
|
buy(user, {
|
||||||
|
params: {
|
||||||
|
key: 'dilatoryDistress1',
|
||||||
|
},
|
||||||
|
type: 'quest',
|
||||||
|
quantity: 'bogle',
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(BadRequest);
|
||||||
|
expect(err.message).to.equal(errorMessage('invalidQuantity'));
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('errors if user supplies a negative quantity', (done) => {
|
||||||
|
try {
|
||||||
|
buy(user, {
|
||||||
|
params: {
|
||||||
|
key: 'dilatoryDistress1',
|
||||||
|
},
|
||||||
|
type: 'quest',
|
||||||
|
quantity: -3,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(BadRequest);
|
||||||
|
expect(err.message).to.equal(errorMessage('invalidQuantity'));
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('errors if user supplies a decimal quantity', (done) => {
|
||||||
|
try {
|
||||||
|
buy(user, {
|
||||||
|
params: {
|
||||||
|
key: 'dilatoryDistress1',
|
||||||
|
},
|
||||||
|
type: 'quest',
|
||||||
|
quantity: 1.83,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(BadRequest);
|
||||||
|
expect(err.message).to.equal(errorMessage('invalidQuantity'));
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -108,6 +108,47 @@ describe('shared.ops.purchase', () => {
|
|||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns error when user supplies a non-numeric quantity', (done) => {
|
||||||
|
let type = 'eggs';
|
||||||
|
let key = 'Wolf';
|
||||||
|
|
||||||
|
try {
|
||||||
|
purchase(user, {params: {type, key}, quantity: 'jamboree'}, analytics);
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(BadRequest);
|
||||||
|
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns error when user supplies a negative quantity', (done) => {
|
||||||
|
let type = 'eggs';
|
||||||
|
let key = 'Wolf';
|
||||||
|
user.balance = 10;
|
||||||
|
|
||||||
|
try {
|
||||||
|
purchase(user, {params: {type, key}, quantity: -2}, analytics);
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(BadRequest);
|
||||||
|
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns error when user supplies a decimal quantity', (done) => {
|
||||||
|
let type = 'eggs';
|
||||||
|
let key = 'Wolf';
|
||||||
|
user.balance = 10;
|
||||||
|
|
||||||
|
try {
|
||||||
|
purchase(user, {params: {type, key}, quantity: 2.9}, analytics);
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(BadRequest);
|
||||||
|
expect(err.message).to.equal(i18n.t('invalidQuantity'));
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
context('successful purchase', () => {
|
context('successful purchase', () => {
|
||||||
|
|||||||
@@ -48,7 +48,7 @@
|
|||||||
strong {{ $t('howManyToBuy') }}
|
strong {{ $t('howManyToBuy') }}
|
||||||
div(v-if='showAmountToBuy(item)')
|
div(v-if='showAmountToBuy(item)')
|
||||||
.box
|
.box
|
||||||
input(type='number', min='0', v-model.number='selectedAmountToBuy')
|
input(type='number', min='0', step='1', v-model.number='selectedAmountToBuy')
|
||||||
span(:class="{'notEnough': notEnoughCurrency}")
|
span(:class="{'notEnough': notEnoughCurrency}")
|
||||||
span.svg-icon.inline.icon-32(aria-hidden="true", v-html="icons[getPriceClass()]")
|
span.svg-icon.inline.icon-32(aria-hidden="true", v-html="icons[getPriceClass()]")
|
||||||
span.cost(:class="getPriceClass()") {{ item.value }}
|
span.cost(:class="getPriceClass()") {{ item.value }}
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
button.btn.btn-primary(
|
button.btn.btn-primary(
|
||||||
@click="buyItem()",
|
@click="buyItem()",
|
||||||
v-else,
|
v-else,
|
||||||
:disabled='item.key === "gem" && gemsLeft === 0 || attemptingToPurchaseMoreGemsThanAreLeft',
|
:disabled='item.key === "gem" && gemsLeft === 0 || attemptingToPurchaseMoreGemsThanAreLeft || numberInvalid',
|
||||||
:class="{'notEnough': !preventHealthPotion || !this.enoughCurrency(getPriceClass(), item.value * selectedAmountToBuy)}"
|
:class="{'notEnough': !preventHealthPotion || !this.enoughCurrency(getPriceClass(), item.value * selectedAmountToBuy)}"
|
||||||
) {{ $t('buyNow') }}
|
) {{ $t('buyNow') }}
|
||||||
|
|
||||||
@@ -260,6 +260,7 @@
|
|||||||
import * as Analytics from 'client/libs/analytics';
|
import * as Analytics from 'client/libs/analytics';
|
||||||
import spellsMixin from 'client/mixins/spells';
|
import spellsMixin from 'client/mixins/spells';
|
||||||
import planGemLimits from 'common/script/libs/planGemLimits';
|
import planGemLimits from 'common/script/libs/planGemLimits';
|
||||||
|
import numberInvalid from 'client/mixins/numberInvalid';
|
||||||
|
|
||||||
import svgClose from 'assets/svg/close.svg';
|
import svgClose from 'assets/svg/close.svg';
|
||||||
import svgGold from 'assets/svg/gold.svg';
|
import svgGold from 'assets/svg/gold.svg';
|
||||||
@@ -291,7 +292,7 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [currencyMixin, notifications, spellsMixin, buyMixin],
|
mixins: [buyMixin, currencyMixin, notifications, numberInvalid, spellsMixin],
|
||||||
components: {
|
components: {
|
||||||
BalanceInfo,
|
BalanceInfo,
|
||||||
EquipmentAttributesGrid,
|
EquipmentAttributesGrid,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
.how-many-to-buy
|
.how-many-to-buy
|
||||||
strong {{ $t('howManyToBuy') }}
|
strong {{ $t('howManyToBuy') }}
|
||||||
.box
|
.box
|
||||||
input(type='number', min='0', v-model.number='selectedAmountToBuy')
|
input(type='number', min='0', step='1', v-model.number='selectedAmountToBuy')
|
||||||
span.svg-icon.inline.icon-32(aria-hidden="true", v-html="(priceType === 'gems') ? icons.gem : icons.gold")
|
span.svg-icon.inline.icon-32(aria-hidden="true", v-html="(priceType === 'gems') ? icons.gem : icons.gold")
|
||||||
span.value(:class="priceType") {{ item.value }}
|
span.value(:class="priceType") {{ item.value }}
|
||||||
|
|
||||||
@@ -34,7 +34,8 @@
|
|||||||
button.btn.btn-primary(
|
button.btn.btn-primary(
|
||||||
@click="buyItem()",
|
@click="buyItem()",
|
||||||
v-else,
|
v-else,
|
||||||
:class="{'notEnough': !this.enoughCurrency(priceType, item.value * selectedAmountToBuy)}"
|
:class="{'notEnough': !this.enoughCurrency(priceType, item.value * selectedAmountToBuy)}",
|
||||||
|
:disabled='numberInvalid',
|
||||||
) {{ $t('buyNow') }}
|
) {{ $t('buyNow') }}
|
||||||
|
|
||||||
div.right-sidebar(v-if="item.drop")
|
div.right-sidebar(v-if="item.drop")
|
||||||
@@ -207,12 +208,13 @@
|
|||||||
import QuestInfo from './questInfo.vue';
|
import QuestInfo from './questInfo.vue';
|
||||||
import notifications from 'client/mixins/notifications';
|
import notifications from 'client/mixins/notifications';
|
||||||
import buyMixin from 'client/mixins/buy';
|
import buyMixin from 'client/mixins/buy';
|
||||||
|
import numberInvalid from 'client/mixins/numberInvalid';
|
||||||
|
|
||||||
import questDialogDrops from './questDialogDrops';
|
import questDialogDrops from './questDialogDrops';
|
||||||
import questDialogContent from './questDialogContent';
|
import questDialogContent from './questDialogContent';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [currencyMixin, notifications, buyMixin],
|
mixins: [buyMixin, currencyMixin, notifications, numberInvalid],
|
||||||
components: {
|
components: {
|
||||||
BalanceInfo,
|
BalanceInfo,
|
||||||
QuestInfo,
|
QuestInfo,
|
||||||
@@ -309,7 +311,6 @@
|
|||||||
return `Unknown type: ${drop.type}`;
|
return `Unknown type: ${drop.type}`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
purchaseGems () {
|
purchaseGems () {
|
||||||
this.$root.$emit('bv::show::modal', 'buy-gems');
|
this.$root.$emit('bv::show::modal', 'buy-gems');
|
||||||
},
|
},
|
||||||
|
|||||||
7
website/client/mixins/numberInvalid.js
Normal file
7
website/client/mixins/numberInvalid.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export default {
|
||||||
|
computed: {
|
||||||
|
numberInvalid () {
|
||||||
|
return this.selectedAmountToBuy < 1 || !Number.isInteger(this.selectedAmountToBuy);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -9,6 +9,7 @@ module.exports = {
|
|||||||
itemNotFound: 'Item "<%= key %>" not found.',
|
itemNotFound: 'Item "<%= key %>" not found.',
|
||||||
questNotFound: 'Quest "<%= key %>" not found.',
|
questNotFound: 'Quest "<%= key %>" not found.',
|
||||||
spellNotFound: 'Skill "<%= spellId %>" not found.',
|
spellNotFound: 'Skill "<%= spellId %>" not found.',
|
||||||
|
invalidQuantity: 'Quantity to purchase must be a positive whole number.',
|
||||||
invalidTypeEquip: '"type" must be one of "equipped", "pet", "mount", "costume"',
|
invalidTypeEquip: '"type" must be one of "equipped", "pet", "mount", "costume"',
|
||||||
missingPetFoodFeed: '"pet" and "food" are required parameters.',
|
missingPetFoodFeed: '"pet" and "food" are required parameters.',
|
||||||
missingEggHatchingPotion: '"egg" and "hatchingPotion" are required parameters.',
|
missingEggHatchingPotion: '"egg" and "hatchingPotion" are required parameters.',
|
||||||
|
|||||||
@@ -99,7 +99,7 @@
|
|||||||
"unlocked": "Items have been unlocked",
|
"unlocked": "Items have been unlocked",
|
||||||
"alreadyUnlocked": "Full set already unlocked.",
|
"alreadyUnlocked": "Full set already unlocked.",
|
||||||
"alreadyUnlockedPart": "Full set already partially unlocked.",
|
"alreadyUnlockedPart": "Full set already partially unlocked.",
|
||||||
"invalidQuantity": "Quantity to purchase must be a number.",
|
"invalidQuantity": "Quantity to purchase must be a positive whole number.",
|
||||||
|
|
||||||
"USD": "(USD)",
|
"USD": "(USD)",
|
||||||
"newStuff": "New Stuff by Bailey",
|
"newStuff": "New Stuff by Bailey",
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export class AbstractBuyOperation {
|
|||||||
let quantity = _get(req, 'quantity');
|
let quantity = _get(req, 'quantity');
|
||||||
|
|
||||||
this.quantity = quantity ? Number(quantity) : 1;
|
this.quantity = quantity ? Number(quantity) : 1;
|
||||||
if (isNaN(this.quantity)) throw new BadRequest(this.i18n('invalidQuantity'));
|
if (this.quantity < 1 || !Number.isInteger(this.quantity)) throw new BadRequest(this.i18n('invalidQuantity'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ module.exports = function purchase (user, req = {}, analytics) {
|
|||||||
let key = get(req.params, 'key');
|
let key = get(req.params, 'key');
|
||||||
|
|
||||||
let quantity = req.quantity ? Number(req.quantity) : 1;
|
let quantity = req.quantity ? Number(req.quantity) : 1;
|
||||||
if (isNaN(quantity)) throw new BadRequest(i18n.t('invalidQuantity', req.language));
|
if (quantity < 1 || !Number.isInteger(quantity)) throw new BadRequest(i18n.t('invalidQuantity', req.language));
|
||||||
|
|
||||||
if (!type) {
|
if (!type) {
|
||||||
throw new BadRequest(i18n.t('typeRequired', req.language));
|
throw new BadRequest(i18n.t('typeRequired', req.language));
|
||||||
|
|||||||
Reference in New Issue
Block a user