mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-14 21:27:23 +01:00
unlock: minor fixes and increase tests coverage
This commit is contained in:
@@ -2,13 +2,14 @@ import unlock from '../../../website/common/script/ops/unlock';
|
|||||||
import i18n from '../../../website/common/script/i18n';
|
import i18n from '../../../website/common/script/i18n';
|
||||||
import { generateUser } from '../../helpers/common.helper';
|
import { generateUser } from '../../helpers/common.helper';
|
||||||
import { NotAuthorized, BadRequest } from '../../../website/common/script/libs/errors';
|
import { NotAuthorized, BadRequest } from '../../../website/common/script/libs/errors';
|
||||||
|
import get from 'lodash/get';
|
||||||
|
|
||||||
describe('shared.ops.unlock', () => {
|
describe.only('shared.ops.unlock', () => {
|
||||||
let user;
|
let user;
|
||||||
const unlockPath = 'shirt.convict,shirt.cross,shirt.fire,shirt.horizon,shirt.ocean,shirt.purple,shirt.rainbow,shirt.redblue,shirt.thunder,shirt.tropical,shirt.zombie';
|
const unlockPath = 'shirt.convict,shirt.cross,shirt.fire,shirt.horizon,shirt.ocean,shirt.purple,shirt.rainbow,shirt.redblue,shirt.thunder,shirt.tropical,shirt.zombie';
|
||||||
const unlockGearSetPath = 'items.gear.owned.headAccessory_special_bearEars,items.gear.owned.headAccessory_special_cactusEars,items.gear.owned.headAccessory_special_foxEars,items.gear.owned.headAccessory_special_lionEars,items.gear.owned.headAccessory_special_pandaEars,items.gear.owned.headAccessory_special_pigEars,items.gear.owned.headAccessory_special_tigerEars,items.gear.owned.headAccessory_special_wolfEars';
|
const unlockGearSetPath = 'items.gear.owned.headAccessory_special_bearEars,items.gear.owned.headAccessory_special_cactusEars,items.gear.owned.headAccessory_special_foxEars,items.gear.owned.headAccessory_special_lionEars,items.gear.owned.headAccessory_special_pandaEars,items.gear.owned.headAccessory_special_pigEars,items.gear.owned.headAccessory_special_tigerEars,items.gear.owned.headAccessory_special_wolfEars';
|
||||||
const backgroundUnlockPath = 'background.giant_florals';
|
const backgroundUnlockPath = 'background.giant_florals';
|
||||||
const unlockCost = 1.25;
|
const backgroundSetUnlockPath = 'background.archery_range,background.giant_florals,background.rainbows_end';
|
||||||
const usersStartingGems = 50 / 4;
|
const usersStartingGems = 50 / 4;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -77,6 +78,56 @@ describe('shared.ops.unlock', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns an error if an item does not exists', done => {
|
||||||
|
try {
|
||||||
|
unlock(user, { query: { path: 'background.invalid_background' } });
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(BadRequest);
|
||||||
|
expect(err.message).to.equal(i18n.t('invalidUnlockSet'));
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error if there are items from multiple sets', done => {
|
||||||
|
try {
|
||||||
|
unlock(user, { query: { path: 'shirt.convict,skin.0ff591' } });
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(BadRequest);
|
||||||
|
expect(err.message).to.equal(i18n.t('invalidUnlockSet'));
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error if gear is not from the animal set', done => {
|
||||||
|
try {
|
||||||
|
unlock(user, { query: { path: 'items.gear.owned.back_mystery_202004' } });
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(BadRequest);
|
||||||
|
expect(err.message).to.equal(i18n.t('invalidUnlockSet'));
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error if the item is free', done => {
|
||||||
|
try {
|
||||||
|
unlock(user, { query: { path: 'shirt.black' } });
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(BadRequest);
|
||||||
|
expect(err.message).to.equal(i18n.t('invalidUnlockSet'));
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns an error if an item does not belong to a set (appearances)', done => {
|
||||||
|
try {
|
||||||
|
unlock(user, { query: { path: 'shirt.pink' } });
|
||||||
|
} catch (err) {
|
||||||
|
expect(err).to.be.an.instanceof(BadRequest);
|
||||||
|
expect(err.message).to.equal(i18n.t('invalidUnlockSet'));
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('returns an error when user already owns items in a full set and it would be more expensive to buy the entire set', done => {
|
it('returns an error when user already owns items in a full set and it would be more expensive to buy the entire set', done => {
|
||||||
try {
|
try {
|
||||||
// There are 11 shirts in the set, each cost 2 gems, the full set 5 gems
|
// There are 11 shirts in the set, each cost 2 gems, the full set 5 gems
|
||||||
@@ -141,31 +192,78 @@ describe('shared.ops.unlock', () => {
|
|||||||
expect(user.preferences.background).to.equal('');
|
expect(user.preferences.background).to.equal('');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('unlocks a full set', () => {
|
it('unlocks a full set of appearance items', () => {
|
||||||
|
const initialShirts = Object.keys(user.purchased.shirt).length;
|
||||||
const [, message] = unlock(user, { query: { path: unlockPath } });
|
const [, message] = unlock(user, { query: { path: unlockPath } });
|
||||||
|
|
||||||
expect(message).to.equal(i18n.t('unlocked'));
|
expect(message).to.equal(i18n.t('unlocked'));
|
||||||
expect(user.purchased.shirt.convict).to.be.true;
|
const individualPaths = unlockPath.split(',');
|
||||||
|
individualPaths.forEach(path => {
|
||||||
|
expect(get(user.purchased, path)).to.be.true;
|
||||||
|
});
|
||||||
|
expect(Object.keys(user.purchased.shirt).length)
|
||||||
|
.to.equal(initialShirts + individualPaths.length);
|
||||||
|
expect(user.balance).to.equal(usersStartingGems - 1.25);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('unlocks a full set of gear', () => {
|
it('unlocks a full set of gear', () => {
|
||||||
|
const initialGear = Object.keys(user.items.gear.owned).length;
|
||||||
const [, message] = unlock(user, { query: { path: unlockGearSetPath } });
|
const [, message] = unlock(user, { query: { path: unlockGearSetPath } });
|
||||||
|
|
||||||
expect(message).to.equal(i18n.t('unlocked'));
|
expect(message).to.equal(i18n.t('unlocked'));
|
||||||
expect(user.items.gear.owned.headAccessory_special_wolfEars).to.be.true;
|
|
||||||
|
const individualPaths = unlockGearSetPath.split(',');
|
||||||
|
individualPaths.forEach(path => {
|
||||||
|
expect(get(user, path)).to.be.true;
|
||||||
|
});
|
||||||
|
expect(Object.keys(user.items.gear.owned).length)
|
||||||
|
.to.equal(initialGear + individualPaths.length);
|
||||||
|
expect(user.balance).to.equal(usersStartingGems - 1.25);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('unlocks an item', () => {
|
it('unlocks a full set of backgrounds', () => {
|
||||||
|
const initialBackgrounds = Object.keys(user.purchased.background).length;
|
||||||
|
const [, message] = unlock(user, { query: { path: backgroundSetUnlockPath } });
|
||||||
|
|
||||||
|
expect(message).to.equal(i18n.t('unlocked'));
|
||||||
|
const individualPaths = backgroundSetUnlockPath.split(',');
|
||||||
|
individualPaths.forEach(path => {
|
||||||
|
expect(get(user.purchased, path)).to.be.true;
|
||||||
|
});
|
||||||
|
expect(Object.keys(user.purchased.background).length)
|
||||||
|
.to.equal(initialBackgrounds + individualPaths.length);
|
||||||
|
expect(user.balance).to.equal(usersStartingGems - 3.75);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('unlocks an item (appearance)', () => {
|
||||||
|
const path = unlockPath.split(',')[0];
|
||||||
|
const initialShirts = Object.keys(user.purchased.shirt).length;
|
||||||
|
const [, message] = unlock(user, { query: { path } });
|
||||||
|
|
||||||
|
expect(message).to.equal(i18n.t('unlocked'));
|
||||||
|
expect(Object.keys(user.purchased.shirt).length).to.equal(initialShirts + 1);
|
||||||
|
expect(get(user.purchased, path)).to.be.true;
|
||||||
|
expect(user.balance).to.equal(usersStartingGems - 0.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('unlocks an item (gear)', () => {
|
||||||
|
const path = unlockGearSetPath.split(',')[0];
|
||||||
|
const initialGear = Object.keys(user.items.gear.owned).length;
|
||||||
|
const [, message] = unlock(user, { query: { path } });
|
||||||
|
|
||||||
|
expect(message).to.equal(i18n.t('unlocked'));
|
||||||
|
expect(Object.keys(user.items.gear.owned).length).to.equal(initialGear + 1);
|
||||||
|
expect(get(user, path)).to.be.true;
|
||||||
|
expect(user.balance).to.equal(usersStartingGems - 0.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('unlocks an item (background)', () => {
|
||||||
|
const initialBackgrounds = Object.keys(user.purchased.background).length;
|
||||||
const [, message] = unlock(user, { query: { path: backgroundUnlockPath } });
|
const [, message] = unlock(user, { query: { path: backgroundUnlockPath } });
|
||||||
|
|
||||||
expect(message).to.equal(i18n.t('unlocked'));
|
expect(message).to.equal(i18n.t('unlocked'));
|
||||||
expect(user.purchased.background.giant_florals).to.be.true;
|
expect(Object.keys(user.purchased.background).length).to.equal(initialBackgrounds + 1);
|
||||||
});
|
expect(get(user.purchased, backgroundUnlockPath)).to.be.true;
|
||||||
|
expect(user.balance).to.equal(usersStartingGems - 1.75);
|
||||||
it('reduces a user\'s balance', () => {
|
|
||||||
const [, message] = unlock(user, { query: { path: unlockPath } });
|
|
||||||
|
|
||||||
expect(message).to.equal(i18n.t('unlocked'));
|
|
||||||
expect(user.balance).to.equal(usersStartingGems - unlockCost);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -99,6 +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.",
|
||||||
|
"invalidUnlockSet": "This set of items is invalid and cannot be unlocked.",
|
||||||
"invalidQuantity": "Quantity to purchase must be a positive whole number.",
|
"invalidQuantity": "Quantity to purchase must be a positive whole number.",
|
||||||
|
|
||||||
"USD": "(USD)",
|
"USD": "(USD)",
|
||||||
|
|||||||
@@ -20,15 +20,14 @@ function splitPathItem (path) {
|
|||||||
/**
|
/**
|
||||||
* Throw an error when the provided set isn't valid.
|
* Throw an error when the provided set isn't valid.
|
||||||
*/
|
*/
|
||||||
function invalidSet () {
|
function invalidSet (req) {
|
||||||
throw new BadRequest("invalid set string");
|
throw new BadRequest(i18n.t('invalidUnlockSet', req.language));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return an item given its path and the type of set
|
* Return an item given its path and the type of set
|
||||||
*/
|
*/
|
||||||
function getItemByPath (path, setType) {
|
function getItemByPath (path, setType) {
|
||||||
console.log('getting item by path', path, setType);
|
|
||||||
const itemKey = splitPathItem(path)[1];
|
const itemKey = splitPathItem(path)[1];
|
||||||
const item = setType === 'gear'
|
const item = setType === 'gear'
|
||||||
? content.gear.flat[itemKey]
|
? content.gear.flat[itemKey]
|
||||||
@@ -40,27 +39,25 @@ function getItemByPath (path, setType) {
|
|||||||
/**
|
/**
|
||||||
* Return the type of the set (gear or one of the appareance sets - see content.appearances).
|
* Return the type of the set (gear or one of the appareance sets - see content.appearances).
|
||||||
*/
|
*/
|
||||||
function getSetType (firstPath) {
|
function getSetType (firstPath, req) {
|
||||||
if (firstPath.includes('gear.')) return 'gear';
|
if (firstPath.includes('gear.')) return 'gear';
|
||||||
|
|
||||||
const type = firstPath.split('.')[0];
|
const type = firstPath.split('.')[0];
|
||||||
if (content.appearances[type]) return type;
|
if (content.appearances[type]) return type;
|
||||||
|
|
||||||
return invalidSet();
|
return invalidSet(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the set of items to unlock given the path of the first item in the set.
|
* Return the set of items to unlock given the path of the first item in the set.
|
||||||
*/
|
*/
|
||||||
function getSet (setType, firstPath) {
|
function getSet (setType, firstPath, req) {
|
||||||
console.log('getting set from type and firstpath', setType, firstPath);
|
|
||||||
const item = getItemByPath(firstPath, setType);
|
const item = getItemByPath(firstPath, setType);
|
||||||
console.log('item', item);
|
if (!item) return invalidSet(req);
|
||||||
if (!item) return invalidSet();
|
|
||||||
|
|
||||||
if (setType === 'gear') {
|
if (setType === 'gear') {
|
||||||
// Only animal gear sets are unlockable
|
// Only animal gear sets are unlockable
|
||||||
if (item.gearSet !== 'animal') return invalidSet();
|
if (item.gearSet !== 'animal') return invalidSet(req);
|
||||||
|
|
||||||
// Since each type of gear has only one purchasable set (the animal set)
|
// Since each type of gear has only one purchasable set (the animal set)
|
||||||
// we get all items with the same type and gearSet === 'animal'
|
// we get all items with the same type and gearSet === 'animal'
|
||||||
@@ -77,10 +74,9 @@ function getSet (setType, firstPath) {
|
|||||||
|
|
||||||
return { items, paths, set: { setPrice: 5 } };
|
return { items, paths, set: { setPrice: 5 } };
|
||||||
}
|
}
|
||||||
console.log('not a gear set');
|
|
||||||
|
|
||||||
const { set } = item;
|
const { set } = item;
|
||||||
if (!set || set.setPrice === 0) return invalidSet();
|
if (!set || set.setPrice === 0) return invalidSet(req);
|
||||||
|
|
||||||
const items = [];
|
const items = [];
|
||||||
const paths = [];
|
const paths = [];
|
||||||
@@ -119,6 +115,9 @@ function markModified (user, path) {
|
|||||||
if (user.markModified) user.markModified(path);
|
if (user.markModified) user.markModified(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Purchase an item from a set for a given user
|
||||||
|
*/
|
||||||
function purchaseItem (path, setType, user) {
|
function purchaseItem (path, setType, user) {
|
||||||
const isGear = setType === 'gear';
|
const isGear = setType === 'gear';
|
||||||
|
|
||||||
@@ -133,10 +132,13 @@ function purchaseItem (path, setType, user) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getIndividualItemPrice (setType, item) {
|
/**
|
||||||
|
* Return the price of a single item in a set
|
||||||
|
*/
|
||||||
|
function getIndividualItemPrice (setType, item, req) {
|
||||||
if (setType === 'gear') return 0.5;
|
if (setType === 'gear') return 0.5;
|
||||||
|
|
||||||
if (!item.price || item.price === 0) return invalidSet();
|
if (!item.price || item.price === 0) return invalidSet(req);
|
||||||
return item.price / 4;
|
return item.price / 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,29 +164,30 @@ export default function unlock (user, req = {}, analytics) {
|
|||||||
const isFullSet = setPaths.length > 1;
|
const isFullSet = setPaths.length > 1;
|
||||||
const firstPath = setPaths[0];
|
const firstPath = setPaths[0];
|
||||||
|
|
||||||
const setType = getSetType(firstPath);
|
const setType = getSetType(firstPath, req);
|
||||||
const isBackground = setType === 'background';
|
const isBackground = setType === 'background';
|
||||||
|
|
||||||
// We take the first path and use it to get the set,
|
// We take the first path and use it to get the set,
|
||||||
// The passed paths are not used anymore after this point for full sets
|
// The passed paths are not used anymore after this point for full sets
|
||||||
const { set, items, paths } = getSet(setType, firstPath);
|
const { set, items, paths } = getSet(setType, firstPath, req);
|
||||||
|
|
||||||
let cost;
|
let cost;
|
||||||
let unlockedAlready = false;
|
let unlockedAlready = false;
|
||||||
|
|
||||||
console.log('isFullSet', isFullSet, 'setType', setType);
|
|
||||||
|
|
||||||
if (isFullSet) {
|
if (isFullSet) {
|
||||||
console.log('fullset', {items}, {paths}, {set});
|
// Make sure the paths as parameters match the ones from the set
|
||||||
|
if (setPaths.length !== paths.length) return invalidSet(req);
|
||||||
|
if (!setPaths.every(setPath => paths.includes(setPath))) return invalidSet(req);
|
||||||
|
|
||||||
cost = set.setPrice / 4;
|
cost = set.setPrice / 4;
|
||||||
|
|
||||||
// all items in a set have the same price
|
// all items in a set have the same price
|
||||||
const individualPrice = getIndividualItemPrice(setType, items[0]);
|
const individualPrice = getIndividualItemPrice(setType, items[0], req);
|
||||||
|
|
||||||
const alreadyUnlockedItems = paths
|
const alreadyUnlockedItems = paths
|
||||||
.filter(itemPath => alreadyUnlocked(user, setType, itemPath)).length;
|
.filter(itemPath => alreadyUnlocked(user, setType, itemPath)).length;
|
||||||
const totalItems = items.length;
|
const totalItems = items.length;
|
||||||
console.log('totalItems', totalItems, 'alreadyUnlockedItems', alreadyUnlockedItems)
|
|
||||||
if (alreadyUnlockedItems === totalItems) {
|
if (alreadyUnlockedItems === totalItems) {
|
||||||
throw new NotAuthorized(i18n.t('alreadyUnlocked', req.language));
|
throw new NotAuthorized(i18n.t('alreadyUnlocked', req.language));
|
||||||
} else if ((totalItems - alreadyUnlockedItems) * individualPrice < cost) {
|
} else if ((totalItems - alreadyUnlockedItems) * individualPrice < cost) {
|
||||||
@@ -193,10 +196,10 @@ export default function unlock (user, req = {}, analytics) {
|
|||||||
} else {
|
} else {
|
||||||
const item = getItemByPath(firstPath, setType);
|
const item = getItemByPath(firstPath, setType);
|
||||||
if (!item || !items.includes(item) || !paths.includes(firstPath)) {
|
if (!item || !items.includes(item) || !paths.includes(firstPath)) {
|
||||||
return invalidSet();
|
return invalidSet(req);
|
||||||
}
|
}
|
||||||
|
|
||||||
cost = getIndividualItemPrice(setType, item);
|
cost = getIndividualItemPrice(setType, item, req);
|
||||||
|
|
||||||
unlockedAlready = alreadyUnlocked(user, setType, firstPath);
|
unlockedAlready = alreadyUnlocked(user, setType, firstPath);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user