diff --git a/ghost/limit-service/lib/limit.js b/ghost/limit-service/lib/limit.js index 0bd96de839..15af078840 100644 --- a/ghost/limit-service/lib/limit.js +++ b/ghost/limit-service/lib/limit.js @@ -1,6 +1,6 @@ // run in context allows us to change the templateSettings without causing havoc const _ = require('lodash').runInContext(); -const {SUPPORTED_INTERVALS} = require('./date-utils'); +const {lastPeriodStart, SUPPORTED_INTERVALS} = require('./date-utils'); _.templateSettings.interpolate = /{{([\s\S]+?)}}/g; @@ -155,6 +155,63 @@ class MaxPeriodicLimit extends Limit { this.startDate = config.startDate; this.fallbackMessage = `This action would exceed the ${_.lowerCase(this.name)} limit on your current plan.`; } + + generateError(count) { + let errorObj = super.generateError(); + + errorObj.message = this.fallbackMessage; + + if (this.error) { + try { + errorObj.message = _.template(this.error)( + { + max: Intl.NumberFormat().format(this.maxPeriodic), + count: Intl.NumberFormat().format(count) + }); + } catch (e) { + errorObj.message = this.fallbackMessage; + } + } + + errorObj.errorDetails.limit = this.maxPeriodic; + errorObj.errorDetails.total = count; + + return new this.errors.HostLimitError(errorObj); + } + + async currentCountQuery() { + const lastPeriodStartDate = lastPeriodStart(this.startDate, this.interval); + + return await this.currentCountQueryFn(this.db, lastPeriodStartDate); + } + + /** + * Throws a HostLimitError if the configured or passed max limit is ecceded by currentCountQuery + * + * @param {Object} options + * @param {Number} [options.max] - overrides configured default maxPeriodic value to perform checks against + */ + async errorIfWouldGoOverLimit({max} = {}) { + let currentCount = await this.currentCountQuery(this.db); + + if ((currentCount + 1) > (max || this.maxPeriodic)) { + throw this.generateError(currentCount); + } + } + + /** + * Throws a HostLimitError if the configured or passed max limit is ecceded by currentCountQuery + * + * @param {Object} options + * @param {Number} [options.max] - overrides configured default maxPeriodic value to perform checks against + */ + async errorIfIsOverLimit({max} = {}) { + let currentCount = await this.currentCountQuery(this.db); + + if (currentCount > (max || this.maxPeriodic)) { + throw this.generateError(currentCount); + } + } } class FlagLimit extends Limit { diff --git a/ghost/limit-service/test/limit.test.js b/ghost/limit-service/test/limit.test.js index 0214291143..0b3b4d20c8 100644 --- a/ghost/limit-service/test/limit.test.js +++ b/ghost/limit-service/test/limit.test.js @@ -294,6 +294,42 @@ describe('Limit Service', function () { } }); }); + + describe('Is over limit', function () { + it('throws if is over the limit', async function () { + const currentCountyQueryMock = sinon.mock().returns(11); + + const config = { + maxPeriodic: 3, + error: 'You have exceeded the number of emails you can send within your billing period.', + interval: 'month', + startDate: '2021-01-01T00:00:00Z', + currentCountQuery: currentCountyQueryMock + }; + + try { + const limit = new MaxPeriodicLimit({name: 'mailguard', config, errors}); + await limit.errorIfIsOverLimit(); + } catch (error) { + error.errorType.should.equal('HostLimitError'); + error.errorDetails.name.should.equal('mailguard'); + error.errorDetails.limit.should.equal(3); + error.errorDetails.total.should.equal(11); + + currentCountyQueryMock.callCount.should.equal(1); + should(currentCountyQueryMock.args).not.be.undefined(); + should(currentCountyQueryMock.args[0][0]).be.undefined(); //knex db connection + + const nowDate = new Date(); + const startOfTheMonthDate = new Date(Date.UTC( + nowDate.getUTCFullYear(), + nowDate.getUTCMonth() + )).toISOString(); + + currentCountyQueryMock.args[0][1].should.equal(startOfTheMonthDate); + } + }); + }); }); describe('Allowlist limit', function () {