diff --git a/test/common/ops/buy/buySpell.js b/test/common/ops/buy/buySpell.js deleted file mode 100644 index f752e6c5a8..0000000000 --- a/test/common/ops/buy/buySpell.js +++ /dev/null @@ -1,89 +0,0 @@ -import { BuySpellOperation } from '../../../../website/common/script/ops/buy/buySpell'; -import { - BadRequest, - NotFound, - NotAuthorized, -} from '../../../../website/common/script/libs/errors'; -import i18n from '../../../../website/common/script/i18n'; -import { - generateUser, -} from '../../../helpers/common.helper'; -import content from '../../../../website/common/script/content/index'; -import { errorMessage } from '../../../../website/common/script/libs/errorMessage'; - -describe('shared.ops.buySpecialSpell', () => { - let user; - const analytics = { track () {} }; - - async function buySpecialSpell (_user, _req, _analytics) { - const buyOp = new BuySpellOperation(_user, _req, _analytics); - - return buyOp.purchase(); - } - beforeEach(() => { - user = generateUser(); - sinon.stub(analytics, 'track'); - }); - - afterEach(() => { - analytics.track.restore(); - }); - - it('throws an error if params.key is missing', async () => { - try { - await buySpecialSpell(user); - } catch (err) { - expect(err).to.be.an.instanceof(BadRequest); - expect(err.message).to.equal(errorMessage('missingKeyParam')); - } - }); - - it('throws an error if the spell doesn\'t exists', async () => { - try { - await buySpecialSpell(user, { - params: { - key: 'notExisting', - }, - }); - } catch (err) { - expect(err).to.be.an.instanceof(NotFound); - expect(err.message).to.equal(errorMessage('spellNotFound', { spellId: 'notExisting' })); - } - }); - - it('throws an error if the user doesn\'t have enough gold', async () => { - user.stats.gp = 1; - try { - await buySpecialSpell(user, { - params: { - key: 'thankyou', - }, - }); - } catch (err) { - expect(err).to.be.an.instanceof(NotAuthorized); - expect(err.message).to.equal(i18n.t('messageNotEnoughGold')); - } - }); - - it('buys an item', async () => { - user.stats.gp = 11; - const item = content.special.thankyou; - - const [data, message] = await buySpecialSpell(user, { - params: { - key: 'thankyou', - }, - }, analytics); - - expect(user.stats.gp).to.equal(1); - expect(user.items.special.thankyou).to.equal(1); - expect(data).to.eql({ - items: user.items, - stats: user.stats, - }); - expect(message).to.equal(i18n.t('messageBought', { - itemText: item.text(), - })); - expect(analytics.track).to.be.calledOnce; - }); -}); diff --git a/test/common/ops/buy/buySpell.test.js b/test/common/ops/buy/buySpell.test.js new file mode 100644 index 0000000000..f914ca8c6f --- /dev/null +++ b/test/common/ops/buy/buySpell.test.js @@ -0,0 +1,172 @@ +import { BuySpellOperation } from '../../../../website/common/script/ops/buy/buySpell'; +import { + BadRequest, + NotFound, + NotAuthorized, +} from '../../../../website/common/script/libs/errors'; +import i18n from '../../../../website/common/script/i18n'; +import { + generateUser, +} from '../../../helpers/common.helper'; +import content from '../../../../website/common/script/content/index'; +import { errorMessage } from '../../../../website/common/script/libs/errorMessage'; + +describe('shared.ops.buySpecialSpell', () => { + let user; + let clock; + const analytics = { track () {} }; + + async function buySpecialSpell (_user, _req, _analytics) { + const buyOp = new BuySpellOperation(_user, _req, _analytics); + + return buyOp.purchase(); + } + beforeEach(() => { + user = generateUser(); + sinon.stub(analytics, 'track'); + }); + + afterEach(() => { + analytics.track.restore(); + if (clock) { + clock.restore(); + } + }); + + it('throws an error if params.key is missing', async () => { + try { + await buySpecialSpell(user); + } catch (err) { + expect(err).to.be.an.instanceof(BadRequest); + expect(err.message).to.equal(errorMessage('missingKeyParam')); + } + }); + + it('throws an error if the item doesn\'t exists', async () => { + try { + await buySpecialSpell(user, { + params: { + key: 'notExisting', + }, + }); + } catch (err) { + expect(err).to.be.an.instanceof(NotFound); + expect(err.message).to.equal(errorMessage('spellNotFound', { spellId: 'notExisting' })); + } + }); + + it('throws an error if the user doesn\'t have enough gold', async () => { + user.stats.gp = 1; + try { + await buySpecialSpell(user, { + params: { + key: 'thankyou', + }, + }); + } catch (err) { + expect(err).to.be.an.instanceof(NotAuthorized); + expect(err.message).to.equal(i18n.t('messageNotEnoughGold')); + } + }); + + describe('buying cards', () => { + it('buys a card that is always available', async () => { + user.stats.gp = 11; + const item = content.special.thankyou; + + const [data, message] = await buySpecialSpell(user, { + params: { + key: 'thankyou', + }, + }, analytics); + + expect(user.stats.gp).to.equal(1); + expect(user.items.special.thankyou).to.equal(1); + expect(data).to.eql({ + items: user.items, + stats: user.stats, + }); + expect(message).to.equal(i18n.t('messageBought', { + itemText: item.text(), + })); + expect(analytics.track).to.be.calledOnce; + }); + + it('buys a limited card when it is available', async () => { + user.stats.gp = 11; + const item = content.special.nye; + clock = sinon.useFakeTimers(new Date('2024-01-01')); + + const [data, message] = await buySpecialSpell(user, { + params: { + key: 'nye', + }, + }, analytics); + + expect(user.stats.gp).to.equal(1); + expect(user.items.special.nye).to.equal(1); + expect(data).to.eql({ + items: user.items, + stats: user.stats, + }); + expect(message).to.equal(i18n.t('messageBought', { + itemText: item.text(), + })); + expect(analytics.track).to.be.calledOnce; + }); + + it('throws an error if the card is not currently available', async () => { + user.stats.gp = 11; + clock = sinon.useFakeTimers(new Date('2024-06-01')); + try { + await buySpecialSpell(user, { + params: { + key: 'nye', + }, + }); + } catch (err) { + expect(err).to.be.an.instanceof(NotAuthorized); + expect(err.message).to.equal(i18n.t('cannotBuyItem')); + } + }); + }); + + describe('buying spells', () => { + it('buys a spell if it is currently available', async () => { + user.stats.gp = 16; + clock = sinon.useFakeTimers(new Date('2024-06-22')); + const item = content.special.seafoam; + const [data, message] = await buySpecialSpell(user, { + params: { + key: 'seafoam', + }, + }, analytics); + + expect(user.stats.gp).to.equal(1); + expect(user.items.special.seafoam).to.equal(1); + expect(data).to.eql({ + items: user.items, + stats: user.stats, + }); + expect(message).to.equal(i18n.t('messageBought', { + itemText: item.text(), + })); + expect(analytics.track).to.be.calledOnce; + }); + + it('throws an error if the spell is not currently available', async () => { + user.stats.gp = 50; + clock = sinon.useFakeTimers(new Date('2024-01-22')); + try { + await buySpecialSpell(user, { + params: { + key: 'seafoam', + }, + }); + } catch (err) { + expect(err).to.be.an.instanceof(NotAuthorized); + expect(err.message).to.equal(i18n.t('cannotBuyItem')); + } + }); + }); +}); diff --git a/website/common/script/content/spells.js b/website/common/script/content/spells.js index 7a4f7a6451..72f379131c 100644 --- a/website/common/script/content/spells.js +++ b/website/common/script/content/spells.js @@ -280,9 +280,7 @@ spells.special = { previousPurchase: true, target: 'user', notes: t('spellSpecialSnowballAuraNotes'), - canOwn () { - return moment().isBetween('2024-01-09T08:00-05:00', EVENTS.winter2024.end); - }, + limited: true, cast (user, target, req) { if (!user.items.special.snowball) throw new NotAuthorized(t('spellNotOwned')(req.language)); target.stats.buffs.snowball = true; @@ -316,9 +314,7 @@ spells.special = { previousPurchase: true, target: 'user', notes: t('spellSpecialSpookySparklesNotes'), - canOwn () { - return moment().isBetween('2023-10-03T08:00-04:00', EVENTS.fall2023.end); - }, + limited: true, cast (user, target, req) { if (!user.items.special.spookySparkles) throw new NotAuthorized(t('spellNotOwned')(req.language)); target.stats.buffs.snowball = false; @@ -352,10 +348,7 @@ spells.special = { previousPurchase: true, target: 'user', notes: t('spellSpecialShinySeedNotes'), - event: EVENTS.spring2024, - canOwn () { - return moment().isBetween('2024-04-18T08:00-05:00', EVENTS.spring2024.end); - }, + limited: true, cast (user, target, req) { if (!user.items.special.shinySeed) throw new NotAuthorized(t('spellNotOwned')(req.language)); target.stats.buffs.snowball = false; @@ -389,9 +382,7 @@ spells.special = { previousPurchase: true, target: 'user', notes: t('spellSpecialSeafoamNotes'), - canOwn () { - return moment().isBetween('2023-07-11T08:00-04:00', EVENTS.summer2023.end); - }, + limited: true, cast (user, target, req) { if (!user.items.special.seafoam) throw new NotAuthorized(t('spellNotOwned')(req.language)); target.stats.buffs.snowball = false; @@ -426,9 +417,7 @@ spells.special = { silent: true, target: 'user', notes: t('nyeCardNotes'), - canOwn () { - return moment().isBetween(EVENTS.nye2023.start, EVENTS.nye2023.end); - }, + limited: true, cast (user, target) { if (user === target) { if (!user.achievements.nye) user.achievements.nye = 0; @@ -466,9 +455,7 @@ spells.special = { silent: true, target: 'user', notes: t('valentineCardNotes'), - canOwn () { - return moment().isBetween(EVENTS.valentine2024.start, EVENTS.valentine2024.end); - }, + limited: true, cast (user, target) { if (user === target) { if (!user.achievements.valentine) user.achievements.valentine = 0; diff --git a/website/common/script/ops/buy/buySpell.js b/website/common/script/ops/buy/buySpell.js index 5d60b5a1a5..9baead6f4e 100644 --- a/website/common/script/ops/buy/buySpell.js +++ b/website/common/script/ops/buy/buySpell.js @@ -8,6 +8,8 @@ import { } from '../../libs/errors'; import { AbstractGoldItemOperation } from './abstractBuyOperation'; import { errorMessage } from '../../libs/errorMessage'; +import { NotAuthorized } from '../../../../server/libs/errors'; +import { getScheduleMatchingGroup } from '../../content/constants/schedule'; export class BuySpellOperation extends AbstractGoldItemOperation { // eslint-disable-line import/prefer-default-export, max-len getItemKey () { @@ -22,6 +24,23 @@ export class BuySpellOperation extends AbstractGoldItemOperation { // eslint-dis return true; } + canUserPurchase (user, item) { + super.canUserPurchase(user, item); + + if (item.limited) { + let matcherGroup; + if (content.cardTypes[item.key]) { + matcherGroup = 'card'; + } else { + matcherGroup = 'seasonalSpells'; + } + const matcher = getScheduleMatchingGroup(matcherGroup); + if (!matcher.match(item.key)) { + throw new NotAuthorized(this.i18n('cannotBuyItem')); + } + } + } + extractAndValidateParams (user, req) { this.key = get(req, 'params.key'); const { key } = this;