diff --git a/backend/src/graphql/resolver/ContributionResolver.test.ts b/backend/src/graphql/resolver/ContributionResolver.test.ts index a188c5d2c..9317b76e7 100644 --- a/backend/src/graphql/resolver/ContributionResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionResolver.test.ts @@ -56,6 +56,7 @@ import { garrickOllivander } from '@/seeds/users/garrick-ollivander' import { peterLustig } from '@/seeds/users/peter-lustig' import { raeuberHotzenplotz } from '@/seeds/users/raeuber-hotzenplotz' import { stephenHawking } from '@/seeds/users/stephen-hawking' +import { getFirstDayOfPreviousNMonth } from '@/util/utilities' jest.mock('@/emails/sendEmailVariants') @@ -290,8 +291,7 @@ describe('ContributionResolver', () => { it('throws error when creationDate 3 month behind', async () => { jest.clearAllMocks() - const date = new Date() - date.setMonth(date.getMonth() - 3) + const date = getFirstDayOfPreviousNMonth(new Date(), 3) const { errors: errorObjects } = await mutate({ mutation: createContribution, variables: { @@ -584,8 +584,7 @@ describe('ContributionResolver', () => { describe('update creation to a date that is older than 3 months', () => { it('throws an error', async () => { jest.clearAllMocks() - const date = new Date() - date.setMonth(date.getMonth() - 3) + const date = getFirstDayOfPreviousNMonth(new Date(), 3) const { errors: errorObjects } = await mutate({ mutation: updateContribution, variables: { diff --git a/backend/src/graphql/resolver/util/creations.test.ts b/backend/src/graphql/resolver/util/creations.test.ts index 25d8a45c2..83d787dd9 100644 --- a/backend/src/graphql/resolver/util/creations.test.ts +++ b/backend/src/graphql/resolver/util/creations.test.ts @@ -10,7 +10,7 @@ import { login, createContribution, adminCreateContribution } from '@/seeds/grap import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' import { peterLustig } from '@/seeds/users/peter-lustig' -import { getUserCreation } from './creations' +import { getOpenCreations, getUserCreation } from './creations' let mutate: ApolloServerTestClient['mutate'], con: Connection let testEnv: { @@ -270,4 +270,49 @@ describe('util/creation', () => { }) }) }) + describe('getOpenCreations', () => { + beforeAll(() => { + // enable Fake timers + jest.useFakeTimers() + // jest.setSystemTime(new Date('2022-01-01T00:00:00Z')) + }) + + afterAll(() => { + // disable Fake timers and set time back to system time + jest.useRealTimers() + }) + it('test default case', async () => { + jest.setSystemTime(new Date('2022-01-10T00:00:00Z')) + const creationDates = await getOpenCreations(user.id, 0) + expect(creationDates).toEqual( + expect.arrayContaining([ + expect.objectContaining({ month: 10 }), + expect.objectContaining({ month: 11 }), + expect.objectContaining({ month: 0 }), + ]), + ) + }) + it('test edge case with february', async () => { + jest.setSystemTime(new Date('2022-05-31T00:00:00Z')) + const creationDates = await getOpenCreations(user.id, 0) + expect(creationDates).toEqual( + expect.arrayContaining([ + expect.objectContaining({ month: 2 }), + expect.objectContaining({ month: 3 }), + expect.objectContaining({ month: 4 }), + ]), + ) + }) + it('test edge case with july', async () => { + jest.setSystemTime(new Date('2022-07-31T00:00:00Z')) + const creationDates = await getOpenCreations(user.id, 0) + expect(creationDates).toEqual( + expect.arrayContaining([ + expect.objectContaining({ month: 4 }), + expect.objectContaining({ month: 5 }), + expect.objectContaining({ month: 6 }), + ]), + ) + }) + }) }) diff --git a/backend/src/graphql/resolver/util/creations.ts b/backend/src/graphql/resolver/util/creations.ts index d6f0e9af4..6bb7214b1 100644 --- a/backend/src/graphql/resolver/util/creations.ts +++ b/backend/src/graphql/resolver/util/creations.ts @@ -10,6 +10,7 @@ import { OpenCreation } from '@model/OpenCreation' import { FULL_CREATION_AVAILABLE, MAX_CREATION_AMOUNT } from '@/graphql/resolver/const/const' import { LogError } from '@/server/LogError' import { backendLogger as logger } from '@/server/logger' +import { getFirstDayOfPreviousNMonth } from '@/util/utilities' interface CreationMap { id: number @@ -115,8 +116,8 @@ const getCreationDates = (timezoneOffset: number): Date[] => { `getCreationMonths -- offset: ${timezoneOffset} -- clientNow: ${clientNow.toISOString()}`, ) return [ - new Date(clientNow.getFullYear(), clientNow.getMonth() - 2, 1), - new Date(clientNow.getFullYear(), clientNow.getMonth() - 1, 1), + getFirstDayOfPreviousNMonth(clientNow, 2), + getFirstDayOfPreviousNMonth(clientNow, 1), clientNow, ] } diff --git a/backend/src/util/utilities.test.ts b/backend/src/util/utilities.test.ts new file mode 100644 index 000000000..abd7604a8 --- /dev/null +++ b/backend/src/util/utilities.test.ts @@ -0,0 +1,59 @@ +import { getFirstDayOfPreviousNMonth } from './utilities' // Adjust the path as necessary + +describe('getFirstDayOfPreviousNMonth', () => { + test('should calculate 3 months prior to March 31, 2024', () => { + const startDate = new Date(2024, 2, 31) // March 31, 2024 (month is 0-indexed) + const monthsAgo = 3 + const result = getFirstDayOfPreviousNMonth(startDate, monthsAgo) + expect(result).toEqual(new Date(2023, 11, 1)) // December 1, 2023 + }) + + test('should handle end of month correctly, January 31, 2024', () => { + const startDate = new Date(2024, 0, 31) // January 31, 2024 + const monthsAgo = 1 + const result = getFirstDayOfPreviousNMonth(startDate, monthsAgo) + expect(result).toEqual(new Date(2023, 11, 1)) // December 1, 2023 + }) + + test('should handle leap year, March 1, 2024', () => { + const startDate = new Date(2024, 2, 1) // March 1, 2024 + const monthsAgo = 1 + const result = getFirstDayOfPreviousNMonth(startDate, monthsAgo) + expect(result).toEqual(new Date(2024, 1, 1)) // February 1, 2024 + }) + + test('should handle leap year, February 29, 2024', () => { + const startDate = new Date(2024, 1, 29) // February 29, 2024 (leap year) + const monthsAgo = 12 + const result = getFirstDayOfPreviousNMonth(startDate, monthsAgo) + expect(result).toEqual(new Date(2023, 1, 1)) // February 1, 2023 + }) + + test('should handle end of year transition, January 1, 2024', () => { + const startDate = new Date(2024, 0, 1) // January 1, 2024 + const monthsAgo = 1 + const result = getFirstDayOfPreviousNMonth(startDate, monthsAgo) + expect(result).toEqual(new Date(2023, 11, 1)) // December 1, 2023 + }) + + test('should handle a large number of months ago, December 15, 2024', () => { + const startDate = new Date(2024, 11, 15) // December 15, 2024 + const monthsAgo = 24 + const result = getFirstDayOfPreviousNMonth(startDate, monthsAgo) + expect(result).toEqual(new Date(2022, 11, 1)) // December 1, 2022 + }) + + test('should handle start of month correctly, February 1, 2024', () => { + const startDate = new Date(2024, 1, 1) // February 1, 2024 + const monthsAgo = 1 + const result = getFirstDayOfPreviousNMonth(startDate, monthsAgo) + expect(result).toEqual(new Date(2024, 0, 1)) // January 1, 2024 + }) + + test('should handle middle of month correctly, February 14, 2024', () => { + const startDate = new Date(2024, 1, 14) // February 14, 2024 + const monthsAgo = 3 + const result = getFirstDayOfPreviousNMonth(startDate, monthsAgo) + expect(result).toEqual(new Date(2023, 10, 1)) // November 1, 2023 + }) +}) diff --git a/backend/src/util/utilities.ts b/backend/src/util/utilities.ts index bc2c2198a..905cce686 100644 --- a/backend/src/util/utilities.ts +++ b/backend/src/util/utilities.ts @@ -33,3 +33,22 @@ export function resetInterface>(obj: T): T { export const ensureUrlEndsWithSlash = (url: string): string => { return url.endsWith('/') ? url : url.concat('/') } +/** + * Calculates the date representing the first day of the month, a specified number of months prior to a given date. + * + * This function was created to address an issue with using `Date.prototype.setMonth`. + * When calculating previous months, `setMonth` can produce incorrect results at the end of months. + * For example, subtracting 3 months from May 31st using `setMonth` would result in March instead of February. + * This function ensures the correct month is calculated by setting the day to the 1st before performing the month subtraction. + * + * @param {Date} startDate - The starting date from which to calculate the previous months. + * @param {number} monthsAgo - The number of months to go back from the startDate. + * @returns {Date} A new Date object set to the first day of the month, `monthsAgo` months before the `startDate`. + * + * @example + * // Calculate the date for the first day of the month, 3 months prior to March 15, 2024 + * const date = getFirstDayOfPreviousNMonth(new Date(2024, 4, 31), 3); + * console.log(date); // Output: Fri Feb 01 2024 00:00:00 GMT+0000 (Coordinated Universal Time) + */ +export const getFirstDayOfPreviousNMonth = (startDate: Date, monthsAgo: number): Date => + new Date(startDate.getFullYear(), startDate.getMonth() - monthsAgo, 1)