diff --git a/config.json.example b/config.json.example index 877acff22a..cbd9dda81e 100644 --- a/config.json.example +++ b/config.json.example @@ -89,5 +89,6 @@ "REDIS_PASSWORD": "12345678", "TRUSTED_DOMAINS": "localhost,https://habitica.com", "TIME_TRAVEL_ENABLED": "false", - "DEBUG_ENABLED": "false" + "DEBUG_ENABLED": "false", + "CONTENT_SWITCHOVER_TIME_OFFSET": 8 } diff --git a/package.json b/package.json index fc02c18271..cf8979704d 100644 --- a/package.json +++ b/package.json @@ -116,8 +116,8 @@ "chalk": "^5.3.0", "cross-spawn": "^7.0.3", "mocha": "^5.1.1", - "nyc": "^15.1.0", "monk": "^7.3.4", + "nyc": "^15.1.0", "require-again": "^2.0.0", "run-rs": "^0.7.7", "sinon-chai": "^3.7.0", diff --git a/test/common/fns/datedMemoize.test.js b/test/common/fns/datedMemoize.test.js new file mode 100644 index 0000000000..f950b4c0b7 --- /dev/null +++ b/test/common/fns/datedMemoize.test.js @@ -0,0 +1,67 @@ +/* eslint-disable global-require */ +import { expect } from 'chai'; +import nconf from 'nconf'; + +const SWITCHOVER_TIME = nconf.get('CONTENT_SWITCHOVER_TIME_OFFSET') || 0; + +describe('datedMemoize', () => { + it('should return a function that returns a function', () => { + const datedMemoize = require('../../../website/common/script/fns/datedMemoize').default; + const memoized = datedMemoize(() => {}); + expect(memoized).to.be.a('function'); + }); + + it('should not call multiple times', () => { + const stub = sandbox.stub().returns({}); + const datedMemoize = require('../../../website/common/script/fns/datedMemoize').default; + const memoized = datedMemoize(stub); + memoized(1, 2); + memoized(1, 3); + expect(stub).to.have.been.calledOnce; + }); + + it('call multiple times for different identifiers', () => { + const stub = sandbox.stub().returns({}); + const datedMemoize = require('../../../website/common/script/fns/datedMemoize').default; + const memoized = datedMemoize(stub); + memoized({ identifier: 'a', memoizeConfig: true }, 1, 2); + memoized({ identifier: 'b', memoizeConfig: true }), 1, 2; + expect(stub).to.have.been.calledTwice; + }); + + it('call once for the same identifier', () => { + const stub = sandbox.stub().returns({}); + const datedMemoize = require('../../../website/common/script/fns/datedMemoize').default; + const memoized = datedMemoize(stub); + memoized({ identifier: 'a', memoizeConfig: true }, 1, 2); + memoized({ identifier: 'a', memoizeConfig: true }, 1, 2); + expect(stub).to.have.been.calledOnce; + }); + + it('call once on the same day', () => { + const stub = sandbox.stub().returns({}); + const datedMemoize = require('../../../website/common/script/fns/datedMemoize').default; + const memoized = datedMemoize(stub); + memoized({ date: new Date('2024-01-01'), memoizeConfig: true }, 1, 2); + memoized({ date: new Date('2024-01-01'), memoizeConfig: true }, 1, 2); + expect(stub).to.have.been.calledOnce; + }); + + it('call multiple times on different days', () => { + const stub = sandbox.stub().returns({}); + const datedMemoize = require('../../../website/common/script/fns/datedMemoize').default; + const memoized = datedMemoize(stub); + memoized({ date: new Date('2024-01-01'), memoizeConfig: true }, 1, 2); + memoized({ date: new Date('2024-01-02'), memoizeConfig: true }, 1, 2); + expect(stub).to.have.been.calledTwice; + }); + + it('respects switchover time', () => { + const stub = sandbox.stub().returns({}); + const datedMemoize = require('../../../website/common/script/fns/datedMemoize').default; + const memoized = datedMemoize(stub); + memoized({ date: new Date('2024-01-01T00:00:00.000Z'), memoizeConfig: true }, 1, 2); + memoized({ date: new Date(`2024-01-01T${String(SWITCHOVER_TIME).padStart(2, '0')}`), memoizeConfig: true }, 1, 2); + expect(stub).to.have.been.calledTwice; + }); +}); diff --git a/test/content/armoire.test.js b/test/content/armoire.test.js index 64cc99a3f2..788dc05bee 100644 --- a/test/content/armoire.test.js +++ b/test/content/armoire.test.js @@ -26,7 +26,7 @@ describe('armoire', () => { clock.restore(); }); it('does not return unreleased gear', async () => { - clock = sinon.useFakeTimers(new Date('2024-01-01')); + clock = sinon.useFakeTimers(new Date('2024-01-02')); const items = makeArmoireIitemList(); expect(items.length).to.equal(377); expect(items.filter(item => item.set === 'pottersSet' || item.set === 'optimistSet' || item.set === 'schoolUniform')).to.be.an('array').that.is.empty; @@ -47,7 +47,7 @@ describe('armoire', () => { }); it('releases gear when appropriate', async () => { - clock = sinon.useFakeTimers(new Date('2024-01-01')); + clock = sinon.useFakeTimers(new Date('2024-01-01T00:00:00.000Z')); const items = makeArmoireIitemList(); expect(items.length).to.equal(377); clock.restore(); @@ -57,8 +57,13 @@ describe('armoire', () => { expect(januaryItems.length).to.equal(381); clock.restore(); delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')]; - clock = sinon.useFakeTimers(new Date('2024-02-20')); + clock = sinon.useFakeTimers(new Date('2024-02-07')); + const januaryItems2 = makeArmoireIitemList(); + expect(januaryItems2.length).to.equal(381); + clock.restore(); + delete require.cache[require.resolve('../../website/common/script/content/gear/sets/armoire')]; + clock = sinon.useFakeTimers(new Date('2024-02-07T09:00:00.000Z')); const febuaryItems = makeArmoireIitemList(); - expect(febuaryItems.length).to.equal(384); + expect(febuaryItems.length).to.equal(381); }); }); diff --git a/test/content/schedule.test.js b/test/content/schedule.test.js index 1393a1c425..270fc7d582 100644 --- a/test/content/schedule.test.js +++ b/test/content/schedule.test.js @@ -1,5 +1,6 @@ // eslint-disable-next-line max-len import moment from 'moment'; +import nconf from 'nconf'; import { getAllScheduleMatchingGroups, clearCachedMatchers, MONTHLY_SCHEDULE, GALA_SCHEDULE, } from '../../website/common/script/content/constants/schedule'; @@ -16,7 +17,10 @@ function validateMatcher (matcher, checkedDate) { } describe('Content Schedule', () => { + let switchoverTime; + beforeEach(() => { + switchoverTime = nconf.get('CONTENT_SWITCHOVER_TIME_OFFSET') || 0; clearCachedMatchers(); }); @@ -50,8 +54,8 @@ describe('Content Schedule', () => { } }); - it('assembles scheduled items on march 21st', () => { - const date = new Date('2024-03-21'); + it('assembles scheduled items on march 22st', () => { + const date = new Date('2024-03-22'); const matchers = getAllScheduleMatchingGroups(date); for (const key in matchers) { if (matchers[key]) { @@ -92,31 +96,31 @@ describe('Content Schedule', () => { it('sets the end date if its in the same month', () => { const date = new Date('2024-04-03'); const matchers = getAllScheduleMatchingGroups(date); - expect(matchers.backgrounds.end).to.eql(moment.utc('2024-04-07').toDate()); + expect(matchers.backgrounds.end).to.eql(moment.utc(`2024-04-07T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate()); }); it('sets the end date if its in the next day', () => { const date = new Date('2024-05-06T14:00:00.000Z'); const matchers = getAllScheduleMatchingGroups(date); - expect(matchers.backgrounds.end).to.eql(moment.utc('2024-05-07').toDate()); + expect(matchers.backgrounds.end).to.eql(moment.utc(`2024-05-07T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate()); }); it('sets the end date if its on the release day', () => { - const date = new Date('2024-05-07'); + const date = new Date('2024-05-07T07:00:00.000Z'); const matchers = getAllScheduleMatchingGroups(date); - expect(matchers.backgrounds.end).to.eql(moment.utc('2024-06-07').toDate()); + expect(matchers.backgrounds.end).to.eql(moment.utc(`2024-06-07T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate()); }); it('sets the end date if its next month', () => { const date = new Date('2024-05-20T01:00:00.000Z'); const matchers = getAllScheduleMatchingGroups(date); - expect(matchers.backgrounds.end).to.eql(moment.utc('2024-06-07').toDate()); + expect(matchers.backgrounds.end).to.eql(moment.utc(`2024-06-07T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate()); }); it('sets the end date for a gala', () => { const date = new Date('2024-05-20'); const matchers = getAllScheduleMatchingGroups(date); - expect(matchers.seasonalGear.end).to.eql(moment.utc('2024-06-21').toDate()); + expect(matchers.seasonalGear.end).to.eql(moment.utc(`2024-06-21T${String(switchoverTime).padStart(2, '0')}:00:00.000Z`).toDate()); }); it('contains content for repeating events', () => { diff --git a/website/common/script/content/constants/schedule.js b/website/common/script/content/constants/schedule.js index 8864c962b0..da31939373 100644 --- a/website/common/script/content/constants/schedule.js +++ b/website/common/script/content/constants/schedule.js @@ -1,4 +1,5 @@ import moment from 'moment'; +import nconf from 'nconf'; import SEASONAL_SETS from './seasonalSets'; import { getRepeatingEvents } from './events'; @@ -773,6 +774,8 @@ export const GALA_SCHEDULE = { }, }; +const SWITCHOVER_TIME = nconf.get('CONTENT_SWITCHOVER_TIME_OFFSET') || 0; + export const TYPE_SCHEDULE = { timeTravelers: FIRST_RELEASE_DAY, backgrounds: SECOND_RELEASE_DAY, @@ -790,7 +793,9 @@ function getDay (date) { if (date === undefined) { return 0; } - return date instanceof moment ? date.date() : date.getDate(); + const checkDate = new Date(date.getTime()); + checkDate.setHours(checkDate.getHours() - SWITCHOVER_TIME); + return checkDate.getDate(); } function getMonth (date) { @@ -871,7 +876,7 @@ function makeMatcherClass (date) { function makeEndDate (checkedDate, matcher) { let end = moment.utc(checkedDate); end.date(TYPE_SCHEDULE[matcher.type]); - end.hour(0); + end.hour(SWITCHOVER_TIME); end.minute(0); end.second(0); if (matcher.endMonth !== undefined) { diff --git a/website/common/script/content/gear/sets/armoire.js b/website/common/script/content/gear/sets/armoire.js index 065c5d8e47..8c4084ed9d 100644 --- a/website/common/script/content/gear/sets/armoire.js +++ b/website/common/script/content/gear/sets/armoire.js @@ -2,6 +2,7 @@ import defaults from 'lodash/defaults'; import find from 'lodash/find'; import forEach from 'lodash/forEach'; import moment from 'moment'; +import nconf from 'nconf'; import upperFirst from 'lodash/upperFirst'; import { ownsItem } from '../gear-helper'; import { ATTRIBUTES } from '../../../constants'; @@ -1832,6 +1833,7 @@ const weapon = { }, }; +const SWITCHOVER_TIME = nconf.get('CONTENT_SWITCHOVER_TIME_OFFSET') || 0; const releaseDay = 7; const releaseDates = { somethingSpooky: { year: 2023, month: 10 }, @@ -1888,7 +1890,7 @@ forEach({ function updateReleased (type) { const today = moment(); - const releaseDateEndPart = `${String(releaseDay).padStart(2, '0')}T08:00-0500`; + const releaseDateEndPart = `${String(releaseDay).padStart(2, '0')}T${String(SWITCHOVER_TIME).padStart(2, '0')}:00-0500`; const returnType = {}; forEach(type, (gearItem, gearKey) => { let released; diff --git a/website/common/script/fns/datedMemoize.js b/website/common/script/fns/datedMemoize.js index 028887392a..6e91fbc52c 100644 --- a/website/common/script/fns/datedMemoize.js +++ b/website/common/script/fns/datedMemoize.js @@ -1,10 +1,15 @@ import moment from 'moment'; +import nconf from 'nconf'; + +const SWITCHOVER_TIME = nconf.get('CONTENT_SWITCHOVER_TIME_OFFSET') || 0; function getDay (date) { if (date === undefined) { return 0; } - return date instanceof moment ? date.date() : date.getDate(); + const checkDate = new Date(date.getTime()); + checkDate.setHours(checkDate.getHours() - SWITCHOVER_TIME); + return checkDate.getDate(); } function getMonth (date) {