From 0b9c9968d03e58e888f6f444d13b2c3aa8306d33 Mon Sep 17 00:00:00 2001 From: Fabien O'Carroll Date: Thu, 4 Nov 2021 19:52:20 +0200 Subject: [PATCH] Added initial tests for Offers refs https://github.com/TryGhost/Team/issues/1198 --- ghost/offers/lib/domain/models/OfferAmount.js | 16 +- .../offers/lib/domain/models/OfferCadence.js | 2 + ghost/offers/lib/domain/models/OfferCode.js | 2 + .../offers/lib/domain/models/OfferCurrency.js | 10 +- .../lib/domain/models/OfferDescription.js | 2 + .../offers/lib/domain/models/OfferDuration.js | 9 +- ghost/offers/lib/domain/models/OfferName.js | 4 +- ghost/offers/lib/domain/models/OfferStatus.js | 2 + ghost/offers/lib/domain/models/OfferTitle.js | 2 + ghost/offers/lib/domain/models/OfferType.js | 2 + .../lib/application/UniqueChecker.test.js | 61 ++++ .../test/lib/domain/models/Offer.test.js | 330 ++++++++++++++++++ .../lib/domain/models/OfferAmount.test.js | 121 +++++++ .../lib/domain/models/OfferCadence.test.js | 46 +++ .../test/lib/domain/models/OfferCode.test.js | 55 +++ .../lib/domain/models/OfferCurrency.test.js | 74 ++++ .../domain/models/OfferDescription.test.js | 62 ++++ .../lib/domain/models/OfferDuration.test.js | 71 ++++ .../test/lib/domain/models/OfferName.test.js | 78 +++++ .../lib/domain/models/OfferStatus.test.js | 31 ++ .../test/lib/domain/models/OfferTitle.test.js | 62 ++++ .../test/lib/domain/models/OfferType.test.js | 44 +++ 22 files changed, 1079 insertions(+), 7 deletions(-) create mode 100644 ghost/offers/test/lib/application/UniqueChecker.test.js create mode 100644 ghost/offers/test/lib/domain/models/Offer.test.js create mode 100644 ghost/offers/test/lib/domain/models/OfferAmount.test.js create mode 100644 ghost/offers/test/lib/domain/models/OfferCadence.test.js create mode 100644 ghost/offers/test/lib/domain/models/OfferCode.test.js create mode 100644 ghost/offers/test/lib/domain/models/OfferCurrency.test.js create mode 100644 ghost/offers/test/lib/domain/models/OfferDescription.test.js create mode 100644 ghost/offers/test/lib/domain/models/OfferDuration.test.js create mode 100644 ghost/offers/test/lib/domain/models/OfferName.test.js create mode 100644 ghost/offers/test/lib/domain/models/OfferStatus.test.js create mode 100644 ghost/offers/test/lib/domain/models/OfferTitle.test.js create mode 100644 ghost/offers/test/lib/domain/models/OfferType.test.js diff --git a/ghost/offers/lib/domain/models/OfferAmount.js b/ghost/offers/lib/domain/models/OfferAmount.js index 0b5f5591c0..84454607fe 100644 --- a/ghost/offers/lib/domain/models/OfferAmount.js +++ b/ghost/offers/lib/domain/models/OfferAmount.js @@ -24,6 +24,8 @@ class OfferPercentageAmount extends OfferAmount { } return new OfferPercentageAmount(amount); } + + static InvalidOfferAmount = InvalidOfferAmount; } class OfferFixedAmount extends OfferAmount { @@ -31,17 +33,23 @@ class OfferFixedAmount extends OfferAmount { static create(amount) { if (typeof amount !== 'number') { throw new InvalidOfferAmount({ - message: 'Offer `amount` must be a number greater than 0.' + message: 'Offer `amount` must be an integer greater than 0.' + }); + } + if (!Number.isInteger(amount)) { + throw new InvalidOfferAmount({ + message: 'Offer `amount` must be a integer greater than 0.' }); } if (amount < 0) { throw new InvalidOfferAmount({ - message: 'Offer `amount` must be a number greater than 0.' + message: 'Offer `amount` must be a integer greater than 0.' }); } - const withTwoDecimalPlaces = +amount.toFixed(2); - return new OfferPercentageAmount(withTwoDecimalPlaces); + return new OfferPercentageAmount(amount); } + + static InvalidOfferAmount = InvalidOfferAmount; } module.exports = OfferAmount; diff --git a/ghost/offers/lib/domain/models/OfferCadence.js b/ghost/offers/lib/domain/models/OfferCadence.js index b72c2df61a..094e83f1ce 100644 --- a/ghost/offers/lib/domain/models/OfferCadence.js +++ b/ghost/offers/lib/domain/models/OfferCadence.js @@ -19,6 +19,8 @@ class OfferCadence extends ValueObject { } return new OfferCadence(cadence); } + + static InvalidOfferCadence = InvalidOfferCadence; } module.exports = OfferCadence; diff --git a/ghost/offers/lib/domain/models/OfferCode.js b/ghost/offers/lib/domain/models/OfferCode.js index 3125a4a98c..971c0d89d5 100644 --- a/ghost/offers/lib/domain/models/OfferCode.js +++ b/ghost/offers/lib/domain/models/OfferCode.js @@ -23,6 +23,8 @@ class OfferCode extends ValueObject { return new OfferCode(slugged); } + + static InvalidOfferCode = InvalidOfferCode; } module.exports = OfferCode; diff --git a/ghost/offers/lib/domain/models/OfferCurrency.js b/ghost/offers/lib/domain/models/OfferCurrency.js index 562d92711d..fefcc7b58d 100644 --- a/ghost/offers/lib/domain/models/OfferCurrency.js +++ b/ghost/offers/lib/domain/models/OfferCurrency.js @@ -10,9 +10,17 @@ class OfferCurrency extends ValueObject { message: 'Offer `currency` must be a string.' }); } + // Check currency is a 3 character string consisting of only letters (case insensitive) + if (!currency.match(/^[A-Z]{3}$/i)) { + throw new InvalidOfferCurrency({ + message: 'Offer `currency` must be an ISO currency code.' + }); + } // TODO: Validate it is a country code we support? - return new OfferCurrency(currency); + return new OfferCurrency(currency.toUpperCase()); } + + static InvalidOfferCurrency = InvalidOfferCurrency; } module.exports = OfferCurrency; diff --git a/ghost/offers/lib/domain/models/OfferDescription.js b/ghost/offers/lib/domain/models/OfferDescription.js index 64a3959314..5c7f4c665d 100644 --- a/ghost/offers/lib/domain/models/OfferDescription.js +++ b/ghost/offers/lib/domain/models/OfferDescription.js @@ -23,6 +23,8 @@ class OfferDescription extends ValueObject { return new OfferDescription(description.trim()); } + + static InvalidOfferDescription = InvalidOfferDescription; } module.exports = OfferDescription; diff --git a/ghost/offers/lib/domain/models/OfferDuration.js b/ghost/offers/lib/domain/models/OfferDuration.js index 12a4533a7d..92c552f336 100644 --- a/ghost/offers/lib/domain/models/OfferDuration.js +++ b/ghost/offers/lib/domain/models/OfferDuration.js @@ -41,11 +41,18 @@ class OfferDuration extends ValueObject { } if (!Number.isInteger(months)) { throw new InvalidOfferDuration({ - message: 'Offer `duration_in_months` must be an integer.' + message: 'Offer `duration_in_months` must be an integer greater than 0.' + }); + } + if (months < 1) { + throw new InvalidOfferDuration({ + message: 'Offer `duration_in_months` must be an integer greater than 0.' }); } return new OfferDuration({type, months}); } + + static InvalidOfferDuration = InvalidOfferDuration; } module.exports = OfferDuration; diff --git a/ghost/offers/lib/domain/models/OfferName.js b/ghost/offers/lib/domain/models/OfferName.js index 7cebe9e209..c3fd0b8dc1 100644 --- a/ghost/offers/lib/domain/models/OfferName.js +++ b/ghost/offers/lib/domain/models/OfferName.js @@ -17,8 +17,10 @@ class OfferName extends ValueObject { }); } - return new OfferName(name); + return new OfferName(name.trim()); } + + static InvalidOfferName = InvalidOfferName } module.exports = OfferName; diff --git a/ghost/offers/lib/domain/models/OfferStatus.js b/ghost/offers/lib/domain/models/OfferStatus.js index 5b63453ba3..4590f16f5f 100644 --- a/ghost/offers/lib/domain/models/OfferStatus.js +++ b/ghost/offers/lib/domain/models/OfferStatus.js @@ -18,6 +18,8 @@ class OfferStatus extends ValueObject { } return new OfferStatus(status); } + + static InvalidOfferStatus = InvalidOfferStatus } module.exports = OfferStatus; diff --git a/ghost/offers/lib/domain/models/OfferTitle.js b/ghost/offers/lib/domain/models/OfferTitle.js index 0676b362bc..1aba0ccbed 100644 --- a/ghost/offers/lib/domain/models/OfferTitle.js +++ b/ghost/offers/lib/domain/models/OfferTitle.js @@ -22,6 +22,8 @@ class OfferTitle extends ValueObject { return new OfferTitle(title.trim()); } + + static InvalidOfferTitle = InvalidOfferTitle; } module.exports = OfferTitle; diff --git a/ghost/offers/lib/domain/models/OfferType.js b/ghost/offers/lib/domain/models/OfferType.js index c6fd321bd6..7dc5470b5d 100644 --- a/ghost/offers/lib/domain/models/OfferType.js +++ b/ghost/offers/lib/domain/models/OfferType.js @@ -19,6 +19,8 @@ class OfferType extends ValueObject { return new OfferType(type); } + static InvalidOfferType = InvalidOfferType + static Percentage = new OfferType('percent') static Fixed = new OfferType('fixed') diff --git a/ghost/offers/test/lib/application/UniqueChecker.test.js b/ghost/offers/test/lib/application/UniqueChecker.test.js new file mode 100644 index 0000000000..7399e8f6b5 --- /dev/null +++ b/ghost/offers/test/lib/application/UniqueChecker.test.js @@ -0,0 +1,61 @@ +const sinon = require('sinon'); +const should = require('should'); +const UniqueChecker = require('../../../lib/application/UniqueChecker'); + +describe('UniqueChecker', function () { + describe('#isUniqueCode', function () { + it('Returns true if there is no Offer found in the repository', async function () { + const repository = { + existsByCode: sinon.stub().resolves(false) + }; + const transaction = {}; + + const checker = new UniqueChecker(repository, transaction); + + const returnVal = await checker.isUniqueCode('code'); + + should.equal(returnVal, true); + }); + + it('Returns false if there is an Offer found in the repository', async function () { + const repository = { + existsByCode: sinon.stub().resolves(true) + }; + const transaction = {}; + + const checker = new UniqueChecker(repository, transaction); + + const returnVal = await checker.isUniqueCode('code'); + + should.equal(returnVal, false); + }); + }); + + describe('#isUniqueName', function () { + it('Returns true if there is no Offer found in the repository', async function () { + const repository = { + existsByName: sinon.stub().resolves(false) + }; + const transaction = {}; + + const checker = new UniqueChecker(repository, transaction); + + const returnVal = await checker.isUniqueName('name'); + + should.equal(returnVal, true); + }); + + it('Returns false if there is an Offer found in the repository', async function () { + const repository = { + existsByName: sinon.stub().resolves(true) + }; + const transaction = {}; + + const checker = new UniqueChecker(repository, transaction); + + const returnVal = await checker.isUniqueName('name'); + + should.equal(returnVal, false); + }); + }); +}); diff --git a/ghost/offers/test/lib/domain/models/Offer.test.js b/ghost/offers/test/lib/domain/models/Offer.test.js new file mode 100644 index 0000000000..966eb370c0 --- /dev/null +++ b/ghost/offers/test/lib/domain/models/Offer.test.js @@ -0,0 +1,330 @@ +const should = require('should'); +const ObjectID = require('bson-objectid'); +const errors = require('../../../../lib/domain/errors'); +const Offer = require('../../../../lib/domain/models/Offer'); +const OfferName = require('../../../../lib/domain/models/OfferName'); +const OfferCode = require('../../../../lib/domain/models/OfferCode'); + +function createUniqueChecker(dupe) { + return { + async isUniqueCode(code) { + return code.value !== dupe; + }, + async isUniqueName(name) { + return name.value !== dupe; + } + }; +} + +const mockUniqueChecker = createUniqueChecker('dupe'); + +describe('Offer', function () { + describe('Offer#create factory', function () { + it('Creates a valid instance of an Offer', async function () { + const offer = await Offer.create({ + name: 'My Offer', + code: 'offer-code', + display_title: 'My Offer Title', + display_description: 'My Offer Description', + cadence: 'month', + type: 'fixed', + amount: 1000, + duration: 'forever', + currency: 'USD', + tier: { + id: ObjectID() + } + }, mockUniqueChecker); + should.ok( + offer instanceof Offer, + 'Offer.create should return an instance of Offer' + ); + }); + + it('Throws an error if the code is not unique', async function () { + await Offer.create({ + name: 'My Offer', + code: 'dupe', + display_title: 'My Offer Title', + display_description: 'My Offer Description', + cadence: 'month', + type: 'fixed', + amount: 1000, + duration: 'forever', + currency: 'USD', + tier: { + id: ObjectID() + } + }, mockUniqueChecker).then(() => { + should.fail('Expected an error'); + }, (err) => { + should.ok(err); + }); + }); + + it('Throws an error if the name is not unique', async function () { + await Offer.create({ + name: 'dupe', + code: 'offer-code', + display_title: 'My Offer Title', + display_description: 'My Offer Description', + cadence: 'month', + type: 'fixed', + amount: 1000, + duration: 'forever', + currency: 'USD', + tier: { + id: ObjectID() + } + }, mockUniqueChecker).then(() => { + should.fail('Expected an error'); + }, (err) => { + should.ok(err); + }); + }); + + it('Wraps the input values in value objects', async function () { + const data = { + name: 'My Offer', + code: 'offer-code', + display_title: 'My Offer Title', + display_description: 'My Offer Description', + cadence: 'month', + type: 'fixed', + amount: 1000, + duration: 'forever', + currency: 'USD', + tier: { + id: ObjectID() + } + }; + + const offer = await Offer.create(data, mockUniqueChecker); + + should.ok(offer.name.equals(OfferName.create(data.name))); + }); + + it('Errors if the repeating duration is applied to the year cadence', async function () { + const data = { + name: 'My Offer', + code: 'offer-code', + display_title: 'My Offer Title', + display_description: 'My Offer Description', + cadence: 'year', + type: 'fixed', + amount: 1000, + duration: 'repeating', + duration_in_months: 12, + currency: 'USD', + tier: { + id: ObjectID() + } + }; + + try { + await Offer.create(data, mockUniqueChecker); + should.fail(); + } catch (err) { + should.ok(err instanceof errors.InvalidOfferDuration); + } + }); + + it('Has a currency of null if the type is percent', async function () { + const data = { + name: 'My Offer', + code: 'offer-code', + display_title: 'My Offer Title', + display_description: 'My Offer Description', + cadence: 'year', + type: 'percent', + amount: 20, + duration: 'once', + currency: 'USD', + tier: { + id: ObjectID() + } + }; + + const offer = await Offer.create(data, mockUniqueChecker); + + should.equal(offer.currency, null); + }); + + it('Can handle ObjectID, string and no id', async function () { + const data = { + name: 'My Offer', + code: 'offer-code', + display_title: 'My Offer Title', + display_description: 'My Offer Description', + cadence: 'year', + type: 'percent', + amount: 20, + duration: 'once', + currency: 'USD', + tier: { + id: ObjectID() + } + }; + + await Offer.create({...data, id: ObjectID()}, mockUniqueChecker); + await Offer.create({...data, id: ObjectID().toHexString()}, mockUniqueChecker); + await Offer.create({...data, id: undefined}, mockUniqueChecker); + }); + + it('Does not accept a redemptionCount for new Offers', async function () { + const data = { + name: 'My Offer', + code: 'offer-code', + display_title: 'My Offer Title', + display_description: 'My Offer Description', + cadence: 'month', + type: 'fixed', + amount: 1000, + duration: 'repeating', + duration_in_months: 12, + currency: 'USD', + tier: { + id: ObjectID() + } + }; + + await Offer.create(data, mockUniqueChecker); + + await Offer.create({...data, redemptionCount: 2}, mockUniqueChecker).then(() => { + should.fail('Expected an error'); + }, (err) => { + should.ok(err); + }); + }); + }); + + describe('#updateCode', function () { + it('Requires the code to be unique if it has changed', async function () { + const data = { + name: 'My Offer', + code: 'offer-code', + display_title: 'My Offer Title', + display_description: 'My Offer Description', + cadence: 'month', + type: 'fixed', + amount: 1000, + duration: 'repeating', + duration_in_months: 12, + currency: 'USD', + tier: { + id: ObjectID() + } + }; + + const offer = await Offer.create(data, mockUniqueChecker); + + await offer.updateCode(OfferCode.create('dupe'), mockUniqueChecker).then(() => { + should.fail('Expected an error'); + }, (err) => { + should.ok(err); + }); + + const offer2 = await Offer.create({...data, code: 'dupe'}, createUniqueChecker()); + + await offer2.updateCode(OfferCode.create('dupe'), mockUniqueChecker); + }); + + it('Does not allow code to be changed twice', async function () { + const data = { + name: 'My Offer', + code: 'offer-code', + display_title: 'My Offer Title', + display_description: 'My Offer Description', + cadence: 'month', + type: 'fixed', + amount: 1000, + duration: 'repeating', + duration_in_months: 12, + currency: 'USD', + tier: { + id: ObjectID() + } + }; + + const offer = await Offer.create(data, mockUniqueChecker); + + await offer.updateCode(OfferCode.create('changed'), mockUniqueChecker); + await offer.updateCode(OfferCode.create('changed-again'), mockUniqueChecker).then(() => { + should.fail('Expected an error'); + }, (err) => { + should.ok(err); + }); + }); + }); + + describe('#updateName', function () { + it('Requires the name to be unique if it has changed', async function () { + const data = { + name: 'My Offer', + code: 'offer-code', + display_title: 'My Offer Title', + display_description: 'My Offer Description', + cadence: 'month', + type: 'fixed', + amount: 1000, + duration: 'repeating', + duration_in_months: 12, + currency: 'USD', + tier: { + id: ObjectID() + } + }; + + const offer = await Offer.create(data, mockUniqueChecker); + + await offer.updateName(OfferName.create('Unique!'), mockUniqueChecker); + + await offer.updateName(OfferName.create('dupe'), mockUniqueChecker).then(() => { + should.fail('Expected an error'); + }, (err) => { + should.ok(err); + }); + + const offer2 = await Offer.create({...data, name: 'dupe'}, createUniqueChecker()); + + await offer2.updateName(OfferName.create('dupe'), mockUniqueChecker); + }); + }); + + describe('Properties', function () { + it('Exposes getters for its properties', async function () { + const data = { + name: 'My Offer', + code: 'offer-code', + display_title: 'My Offer Title', + display_description: 'My Offer Description', + cadence: 'month', + type: 'fixed', + amount: 1000, + duration: 'repeating', + duration_in_months: 12, + currency: 'USD', + tier: { + id: ObjectID() + } + }; + + const offer = await Offer.create(data, mockUniqueChecker); + + should.exist(offer.id); + should.exist(offer.name); + should.exist(offer.code); + should.exist(offer.currency); + should.exist(offer.duration); + should.exist(offer.status); + should.exist(offer.redemptionCount); + should.exist(offer.displayTitle); + should.exist(offer.displayDescription); + should.exist(offer.tier); + should.exist(offer.cadence); + should.exist(offer.type); + should.exist(offer.amount); + should.exist(offer.isNew); + }); + }); +}); diff --git a/ghost/offers/test/lib/domain/models/OfferAmount.test.js b/ghost/offers/test/lib/domain/models/OfferAmount.test.js new file mode 100644 index 0000000000..d911c31172 --- /dev/null +++ b/ghost/offers/test/lib/domain/models/OfferAmount.test.js @@ -0,0 +1,121 @@ +const {OfferPercentageAmount, OfferFixedAmount} = require('../../../../lib/domain/models/OfferAmount'); + +describe('OfferAmount', function () { + describe('OfferPercentageAmount', function () { + describe('OfferPercentageAmount.create factory', function () { + it('Will only create an OfferPercentageAmount containing an integer between 1 & 100 (inclusive)', function () { + try { + OfferPercentageAmount.create(); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferPercentageAmount.InvalidOfferAmount, + 'expected an InvalidOfferAmount error' + ); + } + + try { + OfferPercentageAmount.create('1'); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferPercentageAmount.InvalidOfferAmount, + 'expected an InvalidOfferAmount error' + ); + } + + try { + OfferPercentageAmount.create(-1); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferPercentageAmount.InvalidOfferAmount, + 'expected an InvalidOfferAmount error' + ); + } + + try { + OfferPercentageAmount.create(200); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferPercentageAmount.InvalidOfferAmount, + 'expected an InvalidOfferAmount error' + ); + } + + try { + OfferPercentageAmount.create(3.14); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferPercentageAmount.InvalidOfferAmount, + 'expected an InvalidOfferAmount error' + ); + } + + OfferPercentageAmount.create(69); // nice + }); + }); + + it('Exposes a number on the value property', function () { + const cadence = OfferPercentageAmount.create(42); + + should.ok(typeof cadence.value === 'number'); + }); + }); + + describe('OfferFixedAmount', function () { + describe('OfferFixedAmount.create factory', function () { + it('Will only create an OfferFixedAmount containing an integer greater than 0', function () { + try { + OfferFixedAmount.create(); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferFixedAmount.InvalidOfferAmount, + 'expected an InvalidOfferAmount error' + ); + } + + try { + OfferFixedAmount.create('1'); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferFixedAmount.InvalidOfferAmount, + 'expected an InvalidOfferAmount error' + ); + } + + try { + OfferFixedAmount.create(-1); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferFixedAmount.InvalidOfferAmount, + 'expected an InvalidOfferAmount error' + ); + } + + try { + OfferFixedAmount.create(3.14); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferFixedAmount.InvalidOfferAmount, + 'expected an InvalidOfferAmount error' + ); + } + + OfferFixedAmount.create(200); + }); + }); + + it('Exposes a number on the value property', function () { + const cadence = OfferFixedAmount.create(42); + + should.ok(typeof cadence.value === 'number'); + }); + }); +}); diff --git a/ghost/offers/test/lib/domain/models/OfferCadence.test.js b/ghost/offers/test/lib/domain/models/OfferCadence.test.js new file mode 100644 index 0000000000..48f12d3297 --- /dev/null +++ b/ghost/offers/test/lib/domain/models/OfferCadence.test.js @@ -0,0 +1,46 @@ +const OfferCadence = require('../../../../lib/domain/models/OfferCadence'); + +describe('OfferCadence', function () { + describe('OfferCadence.create factory', function () { + it('Will only create an OfferCadence containing a string of either "month" or "year"', function () { + OfferCadence.create('month'); + OfferCadence.create('year'); + + try { + OfferCadence.create(); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferCadence.InvalidOfferCadence, + 'expected an InvalidOfferCadence error' + ); + } + + try { + OfferCadence.create(12); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferCadence.InvalidOfferCadence, + 'expected an InvalidOfferCadence error' + ); + } + + try { + OfferCadence.create('daily'); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferCadence.InvalidOfferCadence, + 'expected an InvalidOfferCadence error' + ); + } + }); + }); + + it('Exposes a string on the value property', function () { + const cadence = OfferCadence.create('month'); + + should.ok(typeof cadence.value === 'string'); + }); +}); diff --git a/ghost/offers/test/lib/domain/models/OfferCode.test.js b/ghost/offers/test/lib/domain/models/OfferCode.test.js new file mode 100644 index 0000000000..8ac1d8ed0a --- /dev/null +++ b/ghost/offers/test/lib/domain/models/OfferCode.test.js @@ -0,0 +1,55 @@ +const OfferCode = require('../../../../lib/domain/models/OfferCode'); + +describe('OfferCode', function () { + describe('OfferCode.create factory', function () { + it('Creates a sluggified code of a string', function () { + OfferCode.create('code'); + + try { + OfferCode.create(); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferCode.InvalidOfferCode, + 'expected an InvalidOfferCode error' + ); + } + + try { + OfferCode.create(1234); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferCode.InvalidOfferCode, + 'expected an InvalidOfferCode error' + ); + } + + const code = OfferCode.create('Hello, world'); + + should.equal(code.value, 'hello-world'); + }); + + it('Requires the string to be a maximum of 191 characters', function () { + const maxLengthInput = Array.from({length: 191}).map(() => 'a').join(''); + + should.equal(maxLengthInput.length, 191); + + OfferCode.create(maxLengthInput); + + const tooLong = maxLengthInput + 'a'; + + should.equal(tooLong.length, 192); + + try { + OfferCode.create(tooLong); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferCode.InvalidOfferCode, + 'expected an InvalidOfferCode error' + ); + } + }); + }); +}); diff --git a/ghost/offers/test/lib/domain/models/OfferCurrency.test.js b/ghost/offers/test/lib/domain/models/OfferCurrency.test.js new file mode 100644 index 0000000000..58f67b4290 --- /dev/null +++ b/ghost/offers/test/lib/domain/models/OfferCurrency.test.js @@ -0,0 +1,74 @@ +const OfferCurrency = require('../../../../lib/domain/models/OfferCurrency'); + +describe('OfferCurrency', function () { + describe('OfferCurrency.create factory', function () { + it('Will only allow creating a currency with a 3 letter ISO string', function () { + OfferCurrency.create('USD'); + OfferCurrency.create('gbp'); + + try { + OfferCurrency.create(); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferCurrency.InvalidOfferCurrency, + 'expected an InvalidOfferCurrency error' + ); + } + + try { + OfferCurrency.create('US Dollars'); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferCurrency.InvalidOfferCurrency, + 'expected an InvalidOfferCurrency error' + ); + } + + try { + OfferCurrency.create('$'); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferCurrency.InvalidOfferCurrency, + 'expected an InvalidOfferCurrency error' + ); + } + + try { + OfferCurrency.create('USDC'); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferCurrency.InvalidOfferCurrency, + 'expected an InvalidOfferCurrency error' + ); + } + + try { + OfferCurrency.create(2); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferCurrency.InvalidOfferCurrency, + 'expected an InvalidOfferCurrency error' + ); + } + }); + }); + + it('Store the currency as a string on the value property', function () { + const currency = OfferCurrency.create('usd'); + + should.equal(typeof currency.value, 'string'); + }); + + it('Considers currencies equal if they have the same ISO code', function () { + const currencyA = OfferCurrency.create('usd'); + const currencyB = OfferCurrency.create('USD'); + + should.ok(currencyA.equals(currencyB)); + }); +}); + diff --git a/ghost/offers/test/lib/domain/models/OfferDescription.test.js b/ghost/offers/test/lib/domain/models/OfferDescription.test.js new file mode 100644 index 0000000000..dcf31fe83f --- /dev/null +++ b/ghost/offers/test/lib/domain/models/OfferDescription.test.js @@ -0,0 +1,62 @@ +const OfferDescription = require('../../../../lib/domain/models/OfferDescription'); + +describe('OfferDescription', function () { + describe('OfferDescription.create factory', function () { + it('Creates an Offer description containing a string', function () { + OfferDescription.create('Hello, world'); + + should.equal(OfferDescription.create().value, ''); + should.equal(OfferDescription.create(undefined).value, ''); + should.equal(OfferDescription.create(null).value, ''); + + try { + OfferDescription.create(12); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferDescription.InvalidOfferDescription, + 'expected an InvalidOfferDescription error' + ); + } + + try { + OfferDescription.create({}); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferDescription.InvalidOfferDescription, + 'expected an InvalidOfferDescription error' + ); + } + }); + + it('Requires the string to be a maximum of 191 characters', function () { + const maxLengthInput = Array.from({length: 191}).map(() => 'a').join(''); + + should.equal(maxLengthInput.length, 191); + + OfferDescription.create(maxLengthInput); + + const tooLong = maxLengthInput + 'a'; + + should.equal(tooLong.length, 192); + + try { + OfferDescription.create(tooLong); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferDescription.InvalidOfferDescription, + 'expected an InvalidOfferDescription error' + ); + } + }); + + it('Trims the contents of the OfferDescription', function () { + const description = OfferDescription.create(' Trim me! '); + + should.equal(description.value, 'Trim me!'); + }); + }); +}); + diff --git a/ghost/offers/test/lib/domain/models/OfferDuration.test.js b/ghost/offers/test/lib/domain/models/OfferDuration.test.js new file mode 100644 index 0000000000..6a848c0544 --- /dev/null +++ b/ghost/offers/test/lib/domain/models/OfferDuration.test.js @@ -0,0 +1,71 @@ +const OfferDuration = require('../../../../lib/domain/models/OfferDuration'); + +describe('OfferDuration', function () { + describe('OfferDuration.create factory', function () { + it('Will only allow creating a once, repeating or forever duration', function () { + OfferDuration.create('once'); + OfferDuration.create('forever'); + OfferDuration.create('repeating', 2); + + try { + OfferDuration.create(); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferDuration.InvalidOfferDuration, + 'expected an InvalidOfferDuration error' + ); + } + + try { + OfferDuration.create('other'); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferDuration.InvalidOfferDuration, + 'expected an InvalidOfferDuration error' + ); + } + + try { + OfferDuration.create('repeating'); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferDuration.InvalidOfferDuration, + 'expected an InvalidOfferDuration error' + ); + } + + try { + OfferDuration.create('repeating', 1.5); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferDuration.InvalidOfferDuration, + 'expected an InvalidOfferDuration error' + ); + } + + try { + OfferDuration.create('repeating', -12); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferDuration.InvalidOfferDuration, + 'expected an InvalidOfferDuration error' + ); + } + + try { + OfferDuration.create('repeating', '2'); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferDuration.InvalidOfferDuration, + 'expected an InvalidOfferDuration error' + ); + } + }); + }); +}); diff --git a/ghost/offers/test/lib/domain/models/OfferName.test.js b/ghost/offers/test/lib/domain/models/OfferName.test.js new file mode 100644 index 0000000000..9ce7c272f9 --- /dev/null +++ b/ghost/offers/test/lib/domain/models/OfferName.test.js @@ -0,0 +1,78 @@ +const OfferName = require('../../../../lib/domain/models/OfferName'); + +describe('OfferName', function () { + describe('OfferName.create factory', function () { + it('Creates an Offer description containing a string', function () { + OfferName.create('My Offer'); + + try { + OfferName.create(); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferName.InvalidOfferName, + 'expected an InvalidOfferName error' + ); + } + + try { + OfferName.create(null); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferName.InvalidOfferName, + 'expected an InvalidOfferName error' + ); + } + + try { + OfferName.create(12); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferName.InvalidOfferName, + 'expected an InvalidOfferName error' + ); + } + + try { + OfferName.create({}); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferName.InvalidOfferName, + 'expected an InvalidOfferName error' + ); + } + }); + + it('Requires the string to be a maximum of 191 characters', function () { + const maxLengthInput = Array.from({length: 191}).map(() => 'a').join(''); + + should.equal(maxLengthInput.length, 191); + + OfferName.create(maxLengthInput); + + const tooLong = maxLengthInput + 'a'; + + should.equal(tooLong.length, 192); + + try { + OfferName.create(tooLong); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferName.InvalidOfferName, + 'expected an InvalidOfferName error' + ); + } + }); + + it('Trims the contents of the OfferName', function () { + const description = OfferName.create(' Trim me! '); + + should.equal(description.value, 'Trim me!'); + }); + }); +}); + diff --git a/ghost/offers/test/lib/domain/models/OfferStatus.test.js b/ghost/offers/test/lib/domain/models/OfferStatus.test.js new file mode 100644 index 0000000000..77f7415802 --- /dev/null +++ b/ghost/offers/test/lib/domain/models/OfferStatus.test.js @@ -0,0 +1,31 @@ +const OfferStatus = require('../../../../lib/domain/models/OfferStatus'); + +describe('OfferStatus', function () { + describe('OfferStatus.create factory', function () { + it('Creates an Offer type containing either "active" or "archived"', function () { + OfferStatus.create('active'); + OfferStatus.create('archived'); + + try { + OfferStatus.create('other'); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferStatus.InvalidOfferStatus, + 'expected an InvalidOfferStatus error' + ); + } + + try { + OfferStatus.create(); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferStatus.InvalidOfferStatus, + 'expected an InvalidOfferStatus error' + ); + } + }); + }); +}); + diff --git a/ghost/offers/test/lib/domain/models/OfferTitle.test.js b/ghost/offers/test/lib/domain/models/OfferTitle.test.js new file mode 100644 index 0000000000..02ce1d8c10 --- /dev/null +++ b/ghost/offers/test/lib/domain/models/OfferTitle.test.js @@ -0,0 +1,62 @@ +const OfferTitle = require('../../../../lib/domain/models/OfferTitle'); + +describe('OfferTitle', function () { + describe('OfferTitle.create factory', function () { + it('Creates an Offer description containing a string', function () { + OfferTitle.create('Hello, world'); + + should.equal(OfferTitle.create().value, ''); + should.equal(OfferTitle.create(undefined).value, ''); + should.equal(OfferTitle.create(null).value, ''); + + try { + OfferTitle.create(12); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferTitle.InvalidOfferTitle, + 'expected an InvalidOfferTitle error' + ); + } + + try { + OfferTitle.create({}); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferTitle.InvalidOfferTitle, + 'expected an InvalidOfferTitle error' + ); + } + }); + + it('Requires the string to be a maximum of 191 characters', function () { + const maxLengthInput = Array.from({length: 191}).map(() => 'a').join(''); + + should.equal(maxLengthInput.length, 191); + + OfferTitle.create(maxLengthInput); + + const tooLong = maxLengthInput + 'a'; + + should.equal(tooLong.length, 192); + + try { + OfferTitle.create(tooLong); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferTitle.InvalidOfferTitle, + 'expected an InvalidOfferTitle error' + ); + } + }); + + it('Trims the contents of the OfferTitle', function () { + const description = OfferTitle.create(' Trim me! '); + + should.equal(description.value, 'Trim me!'); + }); + }); +}); + diff --git a/ghost/offers/test/lib/domain/models/OfferType.test.js b/ghost/offers/test/lib/domain/models/OfferType.test.js new file mode 100644 index 0000000000..865beaa355 --- /dev/null +++ b/ghost/offers/test/lib/domain/models/OfferType.test.js @@ -0,0 +1,44 @@ +const OfferType = require('../../../../lib/domain/models/OfferType'); + +describe('OfferType', function () { + describe('OfferType.create factory', function () { + it('Creates an Offer type containing either "fixed" or "percent"', function () { + OfferType.create('fixed'); + OfferType.create('percent'); + + try { + OfferType.create('other'); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferType.InvalidOfferType, + 'expected an InvalidOfferType error' + ); + } + + try { + OfferType.create(); + should.fail(); + } catch (err) { + should.ok( + err instanceof OfferType.InvalidOfferType, + 'expected an InvalidOfferType error' + ); + } + }); + }); + + describe('OfferType.Percentage', function () { + it('Is an OfferType with a value of "percent"', function () { + should.equal(OfferType.Percentage.value, 'percent'); + should.ok(OfferType.Percentage.equals(OfferType.create('percent'))); + }); + }); + + describe('OfferType.Fixed', function () { + it('Is an OfferType with a value of "fixed"', function () { + should.equal(OfferType.Fixed.value, 'fixed'); + should.ok(OfferType.Fixed.equals(OfferType.create('fixed'))); + }); + }); +});