fix: Timezone Problems on Change of Month

This commit is contained in:
Moriz Wahl 2022-11-10 19:01:16 +01:00
parent 5032307e2b
commit 5e2d6c3bc9
3 changed files with 305 additions and 68 deletions

View File

@ -2,7 +2,7 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { objectValuesToArray } from '@/util/utilities'
import { testEnvironment, resetToken, cleanDB } from '@test/helpers'
import { testEnvironment, resetToken, cleanDB, contributionDateFormatter } from '@test/helpers'
import { userFactory } from '@/seeds/factory/user'
import { creationFactory } from '@/seeds/factory/creation'
import { creations } from '@/seeds/creation/index'
@ -83,6 +83,12 @@ let user: User
let creation: Contribution | void
let result: any
describe('contributionDateFormatter', () => {
it('formats the date correctly', () => {
expect(contributionDateFormatter(new Date('Thu Feb 29 2024 13:12:11'))).toEqual('2/29/2024')
})
})
describe('AdminResolver', () => {
describe('set user role', () => {
describe('unauthenticated', () => {
@ -139,7 +145,6 @@ describe('AdminResolver', () => {
describe('user to get a new role does not exist', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({ mutation: setUserRole, variables: { userId: admin.id + 1, isAdmin: true } }),
).resolves.toEqual(
@ -196,7 +201,6 @@ describe('AdminResolver', () => {
describe('change role with error', () => {
describe('is own role', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({ mutation: setUserRole, variables: { userId: admin.id, isAdmin: false } }),
).resolves.toEqual(
@ -213,7 +217,6 @@ describe('AdminResolver', () => {
describe('user has already role to be set', () => {
describe('to admin', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await mutate({
mutation: setUserRole,
variables: { userId: user.id, isAdmin: true },
@ -234,7 +237,6 @@ describe('AdminResolver', () => {
describe('to usual user', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await mutate({
mutation: setUserRole,
variables: { userId: user.id, isAdmin: false },
@ -311,7 +313,6 @@ describe('AdminResolver', () => {
describe('user to be deleted does not exist', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({ mutation: deleteUser, variables: { userId: admin.id + 1 } }),
).resolves.toEqual(
@ -328,7 +329,6 @@ describe('AdminResolver', () => {
describe('delete self', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({ mutation: deleteUser, variables: { userId: admin.id } }),
).resolves.toEqual(
@ -362,7 +362,6 @@ describe('AdminResolver', () => {
describe('delete deleted user', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({ mutation: deleteUser, variables: { userId: user.id } }),
).resolves.toEqual(
@ -434,7 +433,6 @@ describe('AdminResolver', () => {
describe('user to be undelete does not exist', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({ mutation: unDeleteUser, variables: { userId: admin.id + 1 } }),
).resolves.toEqual(
@ -455,7 +453,6 @@ describe('AdminResolver', () => {
})
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({ mutation: unDeleteUser, variables: { userId: user.id } }),
).resolves.toEqual(
@ -751,7 +748,7 @@ describe('AdminResolver', () => {
email: 'bibi@bloxberg.de',
amount: new Decimal(300),
memo: 'Danke Bibi!',
creationDate: new Date().toString(),
creationDate: contributionDateFormatter(new Date()),
},
}),
).resolves.toEqual(
@ -861,7 +858,7 @@ describe('AdminResolver', () => {
email: 'bibi@bloxberg.de',
amount: new Decimal(300),
memo: 'Danke Bibi!',
creationDate: new Date().toString(),
creationDate: contributionDateFormatter(new Date()),
},
}),
).resolves.toEqual(
@ -936,19 +933,24 @@ describe('AdminResolver', () => {
})
describe('adminCreateContribution', () => {
beforeAll(async () => {
const now = new Date()
beforeAll(async () => {
creation = await creationFactory(testEnv, {
email: 'peter@lustig.de',
amount: 400,
memo: 'Herzlich Willkommen bei Gradido!',
creationDate: new Date(now.getFullYear(), now.getMonth() - 1, 1).toISOString(),
creationDate: contributionDateFormatter(
new Date(now.getFullYear(), now.getMonth() - 1, 1),
),
})
})
describe('user to create for does not exist', () => {
it('throws an error', async () => {
jest.clearAllMocks()
variables.creationDate = contributionDateFormatter(
new Date(now.getFullYear(), now.getMonth() - 1, 1),
)
await expect(
mutate({ mutation: adminCreateContribution, variables }),
).resolves.toEqual(
@ -969,10 +971,12 @@ describe('AdminResolver', () => {
beforeAll(async () => {
user = await userFactory(testEnv, stephenHawking)
variables.email = 'stephen@hawking.uk'
variables.creationDate = contributionDateFormatter(
new Date(now.getFullYear(), now.getMonth() - 1, 1),
)
})
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({ mutation: adminCreateContribution, variables }),
).resolves.toEqual(
@ -995,10 +999,12 @@ describe('AdminResolver', () => {
beforeAll(async () => {
user = await userFactory(testEnv, garrickOllivander)
variables.email = 'garrick@ollivander.com'
variables.creationDate = contributionDateFormatter(
new Date(now.getFullYear(), now.getMonth() - 1, 1),
)
})
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({ mutation: adminCreateContribution, variables }),
).resolves.toEqual(
@ -1021,39 +1027,31 @@ describe('AdminResolver', () => {
beforeAll(async () => {
user = await userFactory(testEnv, bibiBloxberg)
variables.email = 'bibi@bloxberg.de'
variables.creationDate = 'invalid-date'
})
describe('date of creation is not a date string', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({ mutation: adminCreateContribution, variables }),
).resolves.toEqual(
expect.objectContaining({
errors: [
new GraphQLError('No information for available creations for the given date'),
],
errors: [new GraphQLError(`invalid Date for creationDate=invalid-date`)],
}),
)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'No information for available creations with the given creationDate=',
'Invalid Date',
)
expect(logger.error).toBeCalledWith(`invalid Date for creationDate=invalid-date`)
})
})
describe('date of creation is four months ago', () => {
it('throws an error', async () => {
jest.clearAllMocks()
const now = new Date()
variables.creationDate = new Date(
now.getFullYear(),
now.getMonth() - 4,
1,
).toString()
variables.creationDate = contributionDateFormatter(
new Date(now.getFullYear(), now.getMonth() - 4, 1),
)
await expect(
mutate({ mutation: adminCreateContribution, variables }),
).resolves.toEqual(
@ -1068,7 +1066,7 @@ describe('AdminResolver', () => {
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'No information for available creations with the given creationDate=',
variables.creationDate,
new Date(variables.creationDate).toISOString(),
)
})
})
@ -1076,12 +1074,9 @@ describe('AdminResolver', () => {
describe('date of creation is in the future', () => {
it('throws an error', async () => {
jest.clearAllMocks()
const now = new Date()
variables.creationDate = new Date(
now.getFullYear(),
now.getMonth() + 4,
1,
).toString()
variables.creationDate = contributionDateFormatter(
new Date(now.getFullYear(), now.getMonth() + 4, 1),
)
await expect(
mutate({ mutation: adminCreateContribution, variables }),
).resolves.toEqual(
@ -1096,15 +1091,14 @@ describe('AdminResolver', () => {
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'No information for available creations with the given creationDate=',
variables.creationDate,
new Date(variables.creationDate).toISOString(),
)
})
})
describe('amount of creation is too high', () => {
it('throws an error', async () => {
jest.clearAllMocks()
variables.creationDate = new Date().toString()
variables.creationDate = contributionDateFormatter(now)
await expect(
mutate({ mutation: adminCreateContribution, variables }),
).resolves.toEqual(
@ -1192,7 +1186,7 @@ describe('AdminResolver', () => {
email,
amount: new Decimal(500),
memo: 'Grundeinkommen',
creationDate: new Date().toString(),
creationDate: contributionDateFormatter(new Date()),
}
})
@ -1229,7 +1223,6 @@ describe('AdminResolver', () => {
describe('user for creation to update does not exist', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: adminUpdateContribution,
@ -1238,7 +1231,7 @@ describe('AdminResolver', () => {
email: 'bob@baumeister.de',
amount: new Decimal(300),
memo: 'Danke Bibi!',
creationDate: new Date().toString(),
creationDate: contributionDateFormatter(new Date()),
},
}),
).resolves.toEqual(
@ -1259,7 +1252,6 @@ describe('AdminResolver', () => {
describe('user for creation to update is deleted', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: adminUpdateContribution,
@ -1268,7 +1260,7 @@ describe('AdminResolver', () => {
email: 'stephen@hawking.uk',
amount: new Decimal(300),
memo: 'Danke Bibi!',
creationDate: new Date().toString(),
creationDate: contributionDateFormatter(new Date()),
},
}),
).resolves.toEqual(
@ -1285,7 +1277,6 @@ describe('AdminResolver', () => {
describe('creation does not exist', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: adminUpdateContribution,
@ -1294,7 +1285,7 @@ describe('AdminResolver', () => {
email: 'bibi@bloxberg.de',
amount: new Decimal(300),
memo: 'Danke Bibi!',
creationDate: new Date().toString(),
creationDate: contributionDateFormatter(new Date()),
},
}),
).resolves.toEqual(
@ -1311,7 +1302,6 @@ describe('AdminResolver', () => {
describe('user email does not match creation user', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: adminUpdateContribution,
@ -1321,8 +1311,8 @@ describe('AdminResolver', () => {
amount: new Decimal(300),
memo: 'Danke Bibi!',
creationDate: creation
? creation.contributionDate.toString()
: new Date().toString(),
? contributionDateFormatter(creation.contributionDate)
: contributionDateFormatter(new Date()),
},
}),
).resolves.toEqual(
@ -1346,7 +1336,6 @@ describe('AdminResolver', () => {
describe('creation update is not valid', () => {
// as this test has not clearly defined that date, it is a false positive
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: adminUpdateContribution,
@ -1356,15 +1345,15 @@ describe('AdminResolver', () => {
amount: new Decimal(1900),
memo: 'Danke Peter!',
creationDate: creation
? creation.contributionDate.toString()
: new Date().toString(),
? contributionDateFormatter(creation.contributionDate)
: contributionDateFormatter(new Date()),
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [
new GraphQLError(
'The amount (1900 GDD) to be created exceeds the amount (1000 GDD) still available for this month.',
'The amount (1900 GDD) to be created exceeds the amount (600 GDD) still available for this month.',
),
],
}),
@ -1373,7 +1362,7 @@ describe('AdminResolver', () => {
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'The amount (1900 GDD) to be created exceeds the amount (1000 GDD) still available for this month.',
'The amount (1900 GDD) to be created exceeds the amount (600 GDD) still available for this month.',
)
})
})
@ -1390,8 +1379,8 @@ describe('AdminResolver', () => {
amount: new Decimal(300),
memo: 'Danke Peter!',
creationDate: creation
? creation.contributionDate.toString()
: new Date().toString(),
? contributionDateFormatter(creation.contributionDate)
: contributionDateFormatter(new Date()),
},
}),
).resolves.toEqual(
@ -1430,8 +1419,8 @@ describe('AdminResolver', () => {
amount: new Decimal(200),
memo: 'Das war leider zu Viel!',
creationDate: creation
? creation.contributionDate.toString()
: new Date().toString(),
? contributionDateFormatter(creation.contributionDate)
: contributionDateFormatter(new Date()),
},
}),
).resolves.toEqual(
@ -1523,7 +1512,6 @@ describe('AdminResolver', () => {
describe('adminDeleteContribution', () => {
describe('creation id does not exist', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: adminDeleteContribution,
@ -1554,13 +1542,12 @@ describe('AdminResolver', () => {
variables: {
amount: 100.0,
memo: 'Test env contribution',
creationDate: new Date().toString(),
creationDate: contributionDateFormatter(new Date()),
},
})
})
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: adminDeleteContribution,
@ -1606,7 +1593,6 @@ describe('AdminResolver', () => {
describe('confirmContribution', () => {
describe('creation does not exits', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: confirmContribution,
@ -1633,7 +1619,9 @@ describe('AdminResolver', () => {
email: 'peter@lustig.de',
amount: 400,
memo: 'Herzlich Willkommen bei Gradido!',
creationDate: new Date(now.getFullYear(), now.getMonth() - 1, 1).toISOString(),
creationDate: contributionDateFormatter(
new Date(now.getFullYear(), now.getMonth() - 1, 1),
),
})
})
@ -1664,7 +1652,9 @@ describe('AdminResolver', () => {
email: 'bibi@bloxberg.de',
amount: 450,
memo: 'Herzlich Willkommen bei Gradido liebe Bibi!',
creationDate: new Date(now.getFullYear(), now.getMonth() - 2, 1).toISOString(),
creationDate: contributionDateFormatter(
new Date(now.getFullYear(), now.getMonth() - 2, 1),
),
})
})
@ -1735,13 +1725,17 @@ describe('AdminResolver', () => {
email: 'bibi@bloxberg.de',
amount: 50,
memo: 'Herzlich Willkommen bei Gradido liebe Bibi!',
creationDate: new Date(now.getFullYear(), now.getMonth() - 2, 1).toISOString(),
creationDate: contributionDateFormatter(
new Date(now.getFullYear(), now.getMonth() - 2, 1),
),
})
c2 = await creationFactory(testEnv, {
email: 'bibi@bloxberg.de',
amount: 50,
memo: 'Herzlich Willkommen bei Gradido liebe Bibi!',
creationDate: new Date(now.getFullYear(), now.getMonth() - 2, 1).toISOString(),
creationDate: contributionDateFormatter(
new Date(now.getFullYear(), now.getMonth() - 2, 1),
),
})
})

View File

@ -0,0 +1,237 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import {
testEnvironment,
resetToken,
cleanDB,
contributionDateFormatter,
setClientRequestTime,
toJSTzone,
toPSTzone,
} from '@test/helpers'
import { logger } from '@test/testSetup'
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import { peterLustig } from '@/seeds/users/peter-lustig'
import { User } from '@entity/User'
import { Contribution } from '@entity/Contribution'
import { userFactory } from '@/seeds/factory/user'
import { login, createContribution, adminCreateContribution } from '@/seeds/graphql/mutations'
import { getUserCreation } from './creations'
let mutate: any, query: any, con: any
let testEnv: any
beforeAll(async () => {
testEnv = await testEnvironment()
mutate = testEnv.mutate
query = testEnv.query
con = testEnv.con
await cleanDB()
})
afterAll(async () => {
// await cleanDB()
await con.close()
})
const setZeroHours = (date: Date): Date => {
return new Date(date.getFullYear(), date.getMonth(), date.getDate())
}
describe('util/creation', () => {
let user: User
let admin: User
const now = new Date()
beforeAll(async () => {
user = await userFactory(testEnv, bibiBloxberg)
admin = await userFactory(testEnv, peterLustig)
})
describe('getUserCreations', () => {
beforeAll(async () => {
setClientRequestTime(now.toString())
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
await mutate({
mutation: adminCreateContribution,
variables: {
email: 'bibi@bloxberg.de',
amount: 250.0,
memo: 'Admin contribution for this month',
creationDate: contributionDateFormatter(now),
},
})
await mutate({
mutation: adminCreateContribution,
variables: {
email: 'bibi@bloxberg.de',
amount: 160.0,
memo: 'Admin contribution for the last month',
creationDate: contributionDateFormatter(
new Date(now.getFullYear(), now.getMonth() - 1, now.getDate()),
),
},
})
await mutate({
mutation: adminCreateContribution,
variables: {
email: 'bibi@bloxberg.de',
amount: 450.0,
memo: 'Admin contribution for two months ago',
creationDate: contributionDateFormatter(
new Date(now.getFullYear(), now.getMonth() - 2, now.getDate()),
),
},
})
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
await mutate({
mutation: createContribution,
variables: {
amount: 400.0,
memo: 'Contribution for this month',
creationDate: contributionDateFormatter(now),
},
})
await mutate({
mutation: createContribution,
variables: {
amount: 500.0,
memo: 'Contribution for the last month',
creationDate: contributionDateFormatter(
new Date(now.getFullYear(), now.getMonth() - 1, now.getDate()),
),
},
})
})
it('has the correct data setup', async () => {
await expect(Contribution.find()).resolves.toEqual([
expect.objectContaining({
userId: user.id,
contributionDate: setZeroHours(now),
clientRequestTime: now.toString(),
amount: expect.decimalEqual(250),
memo: 'Admin contribution for this month',
moderatorId: admin.id,
contributionType: 'ADMIN',
contributionStatus: 'PENDING',
}),
expect.objectContaining({
userId: user.id,
contributionDate: setZeroHours(
new Date(now.getFullYear(), now.getMonth() - 1, now.getDate()),
),
clientRequestTime: now.toString(),
amount: expect.decimalEqual(160),
memo: 'Admin contribution for the last month',
moderatorId: admin.id,
contributionType: 'ADMIN',
contributionStatus: 'PENDING',
}),
expect.objectContaining({
userId: user.id,
contributionDate: setZeroHours(
new Date(now.getFullYear(), now.getMonth() - 2, now.getDate()),
),
clientRequestTime: now.toString(),
amount: expect.decimalEqual(450),
memo: 'Admin contribution for two months ago',
moderatorId: admin.id,
contributionType: 'ADMIN',
contributionStatus: 'PENDING',
}),
expect.objectContaining({
userId: user.id,
contributionDate: setZeroHours(now),
clientRequestTime: now.toString(),
amount: expect.decimalEqual(400),
memo: 'Contribution for this month',
moderatorId: null,
contributionType: 'USER',
contributionStatus: 'PENDING',
}),
expect.objectContaining({
userId: user.id,
contributionDate: setZeroHours(
new Date(now.getFullYear(), now.getMonth() - 1, now.getDate()),
),
clientRequestTime: now.toString(),
amount: expect.decimalEqual(500),
memo: 'Contribution for the last month',
moderatorId: null,
contributionType: 'USER',
contributionStatus: 'PENDING',
}),
])
})
describe('call getUserCreation now', () => {
it('returns the expected open contributions', async () => {
await expect(getUserCreation(user.id, now.toString())).resolves.toEqual([
expect.decimalEqual(550),
expect.decimalEqual(340),
expect.decimalEqual(350),
])
})
describe('run forward in time one hour before next month', () => {
const targetDate = new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 0, 0)
beforeAll(() => {
jest.useFakeTimers()
/* eslint-disable-next-line @typescript-eslint/no-empty-function */
setTimeout(() => {}, targetDate.getTime() - now.getTime())
jest.runAllTimers()
})
it('has the clock set correctly', () => {
expect(new Date().toISOString()).toContain(
`${targetDate.getFullYear()}-${targetDate.getMonth() + 1}-${targetDate.getDate()}T23:`,
)
})
describe('call getUserCreation with UTC', () => {
beforeAll(() => {
setClientRequestTime(targetDate.toString())
})
it('returns the expected open contributions', async () => {
await expect(getUserCreation(user.id, now.toString())).resolves.toEqual([
expect.decimalEqual(550),
expect.decimalEqual(340),
expect.decimalEqual(350),
])
})
})
describe('call getUserCreation with JST (GMT+0900)', () => {
beforeAll(() => {
setClientRequestTime(toJSTzone(targetDate.toString()))
})
it('returns the expected open contributions', async () => {
await expect(
getUserCreation(user.id, toJSTzone(targetDate.toString())),
).resolves.toEqual([
expect.decimalEqual(340),
expect.decimalEqual(350),
expect.decimalEqual(1000),
])
})
})
afterAll(() => {
jest.useRealTimers()
})
})
})
})
})

View File

@ -16,6 +16,7 @@ const context = {
push: headerPushMock,
forEach: jest.fn(),
},
clientRequestTime: '',
}
export const cleanDB = async () => {
@ -46,3 +47,8 @@ export const resetEntity = async (entity: any) => {
export const resetToken = () => {
context.token = ''
}
// format date string as it comes from the frontend for the contribution date
export const contributionDateFormatter = (date: Date): string => {
return `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`
}