mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into separate-admin-interface-workflow
This commit is contained in:
commit
cbbac1afdb
@ -1,9 +1,10 @@
|
|||||||
import jwt from 'jsonwebtoken'
|
import jwt from 'jsonwebtoken'
|
||||||
import CONFIG from '@/config/'
|
import CONFIG from '@/config/'
|
||||||
import { CustomJwtPayload } from './CustomJwtPayload'
|
import { CustomJwtPayload } from './CustomJwtPayload'
|
||||||
|
import LogError from '@/server/LogError'
|
||||||
|
|
||||||
export const decode = (token: string): CustomJwtPayload | null => {
|
export const decode = (token: string): CustomJwtPayload | null => {
|
||||||
if (!token) throw new Error('401 Unauthorized')
|
if (!token) throw new LogError('401 Unauthorized')
|
||||||
try {
|
try {
|
||||||
return <CustomJwtPayload>jwt.verify(token, CONFIG.JWT_SECRET)
|
return <CustomJwtPayload>jwt.verify(token, CONFIG.JWT_SECRET)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { ROLE_UNAUTHORIZED, ROLE_USER, ROLE_ADMIN } from '@/auth/ROLES'
|
|||||||
import { RIGHTS } from '@/auth/RIGHTS'
|
import { RIGHTS } from '@/auth/RIGHTS'
|
||||||
import { INALIENABLE_RIGHTS } from '@/auth/INALIENABLE_RIGHTS'
|
import { INALIENABLE_RIGHTS } from '@/auth/INALIENABLE_RIGHTS'
|
||||||
import { User } from '@entity/User'
|
import { User } from '@entity/User'
|
||||||
|
import LogError from '@/server/LogError'
|
||||||
|
|
||||||
const isAuthorized: AuthChecker<any> = async ({ context }, rights) => {
|
const isAuthorized: AuthChecker<any> = async ({ context }, rights) => {
|
||||||
context.role = ROLE_UNAUTHORIZED // unauthorized user
|
context.role = ROLE_UNAUTHORIZED // unauthorized user
|
||||||
@ -17,13 +18,13 @@ const isAuthorized: AuthChecker<any> = async ({ context }, rights) => {
|
|||||||
|
|
||||||
// Do we have a token?
|
// Do we have a token?
|
||||||
if (!context.token) {
|
if (!context.token) {
|
||||||
throw new Error('401 Unauthorized')
|
throw new LogError('401 Unauthorized')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decode the token
|
// Decode the token
|
||||||
const decoded = decode(context.token)
|
const decoded = decode(context.token)
|
||||||
if (!decoded) {
|
if (!decoded) {
|
||||||
throw new Error('403.13 - Client certificate revoked')
|
throw new LogError('403.13 - Client certificate revoked')
|
||||||
}
|
}
|
||||||
// Set context gradidoID
|
// Set context gradidoID
|
||||||
context.gradidoID = decoded.gradidoID
|
context.gradidoID = decoded.gradidoID
|
||||||
@ -39,13 +40,13 @@ const isAuthorized: AuthChecker<any> = async ({ context }, rights) => {
|
|||||||
context.role = user.isAdmin ? ROLE_ADMIN : ROLE_USER
|
context.role = user.isAdmin ? ROLE_ADMIN : ROLE_USER
|
||||||
} catch {
|
} catch {
|
||||||
// in case the database query fails (user deleted)
|
// in case the database query fails (user deleted)
|
||||||
throw new Error('401 Unauthorized')
|
throw new LogError('401 Unauthorized')
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for correct rights
|
// check for correct rights
|
||||||
const missingRights = (<RIGHTS[]>rights).filter((right) => !context.role.hasRight(right))
|
const missingRights = (<RIGHTS[]>rights).filter((right) => !context.role.hasRight(right))
|
||||||
if (missingRights.length !== 0) {
|
if (missingRights.length !== 0) {
|
||||||
throw new Error('401 Unauthorized')
|
throw new LogError('401 Unauthorized')
|
||||||
}
|
}
|
||||||
|
|
||||||
// set new header token
|
// set new header token
|
||||||
|
|||||||
@ -257,17 +257,13 @@ describe('Contribution Links', () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [
|
errors: [new GraphQLError('A Start-Date must be set')],
|
||||||
new GraphQLError('Start-Date is not initialized. A Start-Date must be set!'),
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('logs the error thrown', () => {
|
it('logs the error thrown', () => {
|
||||||
expect(logger.error).toBeCalledWith(
|
expect(logger.error).toBeCalledWith('A Start-Date must be set')
|
||||||
'Start-Date is not initialized. A Start-Date must be set!',
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns an error if missing endDate', async () => {
|
it('returns an error if missing endDate', async () => {
|
||||||
@ -282,15 +278,13 @@ describe('Contribution Links', () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [new GraphQLError('End-Date is not initialized. An End-Date must be set!')],
|
errors: [new GraphQLError('An End-Date must be set')],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('logs the error thrown', () => {
|
it('logs the error thrown', () => {
|
||||||
expect(logger.error).toBeCalledWith(
|
expect(logger.error).toBeCalledWith('An End-Date must be set')
|
||||||
'End-Date is not initialized. An End-Date must be set!',
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns an error if endDate is before startDate', async () => {
|
it('returns an error if endDate is before startDate', async () => {
|
||||||
@ -307,7 +301,7 @@ describe('Contribution Links', () => {
|
|||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [
|
errors: [
|
||||||
new GraphQLError(`The value of validFrom must before or equals the validTo!`),
|
new GraphQLError(`The value of validFrom must before or equals the validTo`),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
@ -315,7 +309,7 @@ describe('Contribution Links', () => {
|
|||||||
|
|
||||||
it('logs the error thrown', () => {
|
it('logs the error thrown', () => {
|
||||||
expect(logger.error).toBeCalledWith(
|
expect(logger.error).toBeCalledWith(
|
||||||
`The value of validFrom must before or equals the validTo!`,
|
`The value of validFrom must before or equals the validTo`,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -33,10 +33,14 @@ export class ContributionMessageResolver {
|
|||||||
try {
|
try {
|
||||||
const contribution = await DbContribution.findOne({ id: contributionId })
|
const contribution = await DbContribution.findOne({ id: contributionId })
|
||||||
if (!contribution) {
|
if (!contribution) {
|
||||||
throw new Error('Contribution not found')
|
throw new LogError('Contribution not found', contributionId)
|
||||||
}
|
}
|
||||||
if (contribution.userId !== user.id) {
|
if (contribution.userId !== user.id) {
|
||||||
throw new Error('Can not send message to contribution of another user')
|
throw new LogError(
|
||||||
|
'Can not send message to contribution of another user',
|
||||||
|
contribution.userId,
|
||||||
|
user.id,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
contributionMessage.contributionId = contributionId
|
contributionMessage.contributionId = contributionId
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
listAllContributions,
|
listAllContributions,
|
||||||
listContributions,
|
listContributions,
|
||||||
listUnconfirmedContributions,
|
adminListAllContributions,
|
||||||
} from '@/seeds/graphql/queries'
|
} from '@/seeds/graphql/queries'
|
||||||
import { sendContributionConfirmedEmail } from '@/emails/sendEmailVariants'
|
import { sendContributionConfirmedEmail } from '@/emails/sendEmailVariants'
|
||||||
import {
|
import {
|
||||||
@ -46,9 +46,10 @@ import { EventProtocolType } from '@/event/EventProtocolType'
|
|||||||
import { logger, i18n as localization } from '@test/testSetup'
|
import { logger, i18n as localization } from '@test/testSetup'
|
||||||
import { UserInputError } from 'apollo-server-express'
|
import { UserInputError } from 'apollo-server-express'
|
||||||
import { raeuberHotzenplotz } from '@/seeds/users/raeuber-hotzenplotz'
|
import { raeuberHotzenplotz } from '@/seeds/users/raeuber-hotzenplotz'
|
||||||
import { UnconfirmedContribution } from '../model/UnconfirmedContribution'
|
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
|
||||||
import { ContributionListResult } from '../model/Contribution'
|
import { ContributionListResult } from '@model/Contribution'
|
||||||
import { ContributionStatus } from '../enum/ContributionStatus'
|
import { ContributionStatus } from '@enum/ContributionStatus'
|
||||||
|
import { Order } from '@enum/Order'
|
||||||
|
|
||||||
// mock account activation email to avoid console spam
|
// mock account activation email to avoid console spam
|
||||||
jest.mock('@/emails/sendEmailVariants', () => {
|
jest.mock('@/emails/sendEmailVariants', () => {
|
||||||
@ -245,8 +246,8 @@ describe('ContributionResolver', () => {
|
|||||||
|
|
||||||
it('logs the error found', () => {
|
it('logs the error found', () => {
|
||||||
expect(logger.error).toBeCalledWith(
|
expect(logger.error).toBeCalledWith(
|
||||||
'No information for available creations with the given creationDate=',
|
'No information for available creations for the given date',
|
||||||
'Invalid Date',
|
expect.any(Date),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -268,8 +269,8 @@ describe('ContributionResolver', () => {
|
|||||||
|
|
||||||
it('logs the error found', () => {
|
it('logs the error found', () => {
|
||||||
expect(logger.error).toBeCalledWith(
|
expect(logger.error).toBeCalledWith(
|
||||||
'No information for available creations with the given creationDate=',
|
'No information for available creations for the given date',
|
||||||
'Invalid Date',
|
expect.any(Date),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -526,14 +527,16 @@ describe('ContributionResolver', () => {
|
|||||||
})
|
})
|
||||||
expect(errorObjects).toEqual([
|
expect(errorObjects).toEqual([
|
||||||
new GraphQLError(
|
new GraphQLError(
|
||||||
'The amount (1019 GDD) to be created exceeds the amount (600 GDD) still available for this month.',
|
'The amount to be created exceeds the amount still available for this month',
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('logs the error found', () => {
|
it('logs the error found', () => {
|
||||||
expect(logger.error).toBeCalledWith(
|
expect(logger.error).toBeCalledWith(
|
||||||
'The amount (1019 GDD) to be created exceeds the amount (600 GDD) still available for this month.',
|
'The amount to be created exceeds the amount still available for this month',
|
||||||
|
new Decimal(1019),
|
||||||
|
new Decimal(600),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -875,6 +878,7 @@ describe('ContributionResolver', () => {
|
|||||||
|
|
||||||
describe('other user sends a deleteContribution', () => {
|
describe('other user sends a deleteContribution', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
await mutate({
|
await mutate({
|
||||||
mutation: login,
|
mutation: login,
|
||||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||||
@ -886,7 +890,6 @@ describe('ContributionResolver', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('returns an error', async () => {
|
it('returns an error', async () => {
|
||||||
jest.clearAllMocks()
|
|
||||||
const { errors: errorObjects }: { errors: [GraphQLError] } = await mutate({
|
const { errors: errorObjects }: { errors: [GraphQLError] } = await mutate({
|
||||||
mutation: deleteContribution,
|
mutation: deleteContribution,
|
||||||
variables: {
|
variables: {
|
||||||
@ -909,6 +912,7 @@ describe('ContributionResolver', () => {
|
|||||||
|
|
||||||
describe('User deletes own contribution', () => {
|
describe('User deletes own contribution', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
await mutate({
|
await mutate({
|
||||||
mutation: login,
|
mutation: login,
|
||||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||||
@ -1687,20 +1691,6 @@ describe('ContributionResolver', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('listUnconfirmedContributions', () => {
|
|
||||||
it('returns an error', async () => {
|
|
||||||
await expect(
|
|
||||||
query({
|
|
||||||
query: listUnconfirmedContributions,
|
|
||||||
}),
|
|
||||||
).resolves.toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
errors: [new GraphQLError('401 Unauthorized')],
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('adminDeleteContribution', () => {
|
describe('adminDeleteContribution', () => {
|
||||||
it('returns an error', async () => {
|
it('returns an error', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
@ -1795,20 +1785,6 @@ describe('ContributionResolver', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('listUnconfirmedContributions', () => {
|
|
||||||
it('returns an error', async () => {
|
|
||||||
await expect(
|
|
||||||
query({
|
|
||||||
query: listUnconfirmedContributions,
|
|
||||||
}),
|
|
||||||
).resolves.toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
errors: [new GraphQLError('401 Unauthorized')],
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('adminDeleteContribution', () => {
|
describe('adminDeleteContribution', () => {
|
||||||
it('returns an error', async () => {
|
it('returns an error', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
@ -2008,8 +1984,8 @@ describe('ContributionResolver', () => {
|
|||||||
|
|
||||||
it('logs the error thrown', () => {
|
it('logs the error thrown', () => {
|
||||||
expect(logger.error).toBeCalledWith(
|
expect(logger.error).toBeCalledWith(
|
||||||
'No information for available creations with the given creationDate=',
|
'No information for available creations for the given date',
|
||||||
new Date(variables.creationDate).toString(),
|
new Date(variables.creationDate),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -2033,8 +2009,8 @@ describe('ContributionResolver', () => {
|
|||||||
|
|
||||||
it('logs the error thrown', () => {
|
it('logs the error thrown', () => {
|
||||||
expect(logger.error).toBeCalledWith(
|
expect(logger.error).toBeCalledWith(
|
||||||
'No information for available creations with the given creationDate=',
|
'No information for available creations for the given date',
|
||||||
new Date(variables.creationDate).toString(),
|
new Date(variables.creationDate),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -2049,7 +2025,7 @@ describe('ContributionResolver', () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [
|
errors: [
|
||||||
new GraphQLError(
|
new GraphQLError(
|
||||||
'The amount (2000 GDD) to be created exceeds the amount (790 GDD) still available for this month.',
|
'The amount to be created exceeds the amount still available for this month',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@ -2058,7 +2034,9 @@ describe('ContributionResolver', () => {
|
|||||||
|
|
||||||
it('logs the error thrown', () => {
|
it('logs the error thrown', () => {
|
||||||
expect(logger.error).toBeCalledWith(
|
expect(logger.error).toBeCalledWith(
|
||||||
'The amount (2000 GDD) to be created exceeds the amount (790 GDD) still available for this month.',
|
'The amount to be created exceeds the amount still available for this month',
|
||||||
|
new Decimal(2000),
|
||||||
|
new Decimal(790),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -2098,7 +2076,7 @@ describe('ContributionResolver', () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [
|
errors: [
|
||||||
new GraphQLError(
|
new GraphQLError(
|
||||||
'The amount (1000 GDD) to be created exceeds the amount (590 GDD) still available for this month.',
|
'The amount to be created exceeds the amount still available for this month',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@ -2107,7 +2085,9 @@ describe('ContributionResolver', () => {
|
|||||||
|
|
||||||
it('logs the error thrown', () => {
|
it('logs the error thrown', () => {
|
||||||
expect(logger.error).toBeCalledWith(
|
expect(logger.error).toBeCalledWith(
|
||||||
'The amount (1000 GDD) to be created exceeds the amount (590 GDD) still available for this month.',
|
'The amount to be created exceeds the amount still available for this month',
|
||||||
|
new Decimal(1000),
|
||||||
|
new Decimal(590),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -2300,7 +2280,7 @@ describe('ContributionResolver', () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [
|
errors: [
|
||||||
new GraphQLError(
|
new GraphQLError(
|
||||||
'The amount (1900 GDD) to be created exceeds the amount (1000 GDD) still available for this month.',
|
'The amount to be created exceeds the amount still available for this month',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@ -2309,7 +2289,9 @@ describe('ContributionResolver', () => {
|
|||||||
|
|
||||||
it('logs the error thrown', () => {
|
it('logs the error thrown', () => {
|
||||||
expect(logger.error).toBeCalledWith(
|
expect(logger.error).toBeCalledWith(
|
||||||
'The amount (1900 GDD) to be created exceeds the amount (1000 GDD) still available for this month.',
|
'The amount to be created exceeds the amount still available for this month',
|
||||||
|
new Decimal(1900),
|
||||||
|
new Decimal(1000),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -2397,100 +2379,6 @@ describe('ContributionResolver', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('listUnconfirmedContributions', () => {
|
|
||||||
it('returns four pending creations', async () => {
|
|
||||||
await expect(
|
|
||||||
query({
|
|
||||||
query: listUnconfirmedContributions,
|
|
||||||
}),
|
|
||||||
).resolves.toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
data: {
|
|
||||||
listUnconfirmedContributions: expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
id: expect.any(Number),
|
|
||||||
firstName: 'Peter',
|
|
||||||
lastName: 'Lustig',
|
|
||||||
email: 'peter@lustig.de',
|
|
||||||
date: expect.any(String),
|
|
||||||
memo: 'Das war leider zu Viel!',
|
|
||||||
amount: '200',
|
|
||||||
moderator: admin.id,
|
|
||||||
creation: ['1000', '800', '500'],
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
id: expect.any(Number),
|
|
||||||
firstName: 'Peter',
|
|
||||||
lastName: 'Lustig',
|
|
||||||
email: 'peter@lustig.de',
|
|
||||||
date: expect.any(String),
|
|
||||||
memo: 'Grundeinkommen',
|
|
||||||
amount: '500',
|
|
||||||
moderator: admin.id,
|
|
||||||
creation: ['1000', '800', '500'],
|
|
||||||
}),
|
|
||||||
expect.not.objectContaining({
|
|
||||||
id: expect.any(Number),
|
|
||||||
firstName: 'Bibi',
|
|
||||||
lastName: 'Bloxberg',
|
|
||||||
email: 'bibi@bloxberg.de',
|
|
||||||
date: expect.any(String),
|
|
||||||
memo: 'Test contribution to delete',
|
|
||||||
amount: '100',
|
|
||||||
moderator: null,
|
|
||||||
creation: ['1000', '1000', '90'],
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
id: expect.any(Number),
|
|
||||||
firstName: 'Bibi',
|
|
||||||
lastName: 'Bloxberg',
|
|
||||||
email: 'bibi@bloxberg.de',
|
|
||||||
date: expect.any(String),
|
|
||||||
memo: 'Test PENDING contribution update',
|
|
||||||
amount: '10',
|
|
||||||
moderator: null,
|
|
||||||
creation: ['1000', '1000', '90'],
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
id: expect.any(Number),
|
|
||||||
firstName: 'Bibi',
|
|
||||||
lastName: 'Bloxberg',
|
|
||||||
email: 'bibi@bloxberg.de',
|
|
||||||
date: expect.any(String),
|
|
||||||
memo: 'Test IN_PROGRESS contribution',
|
|
||||||
amount: '100',
|
|
||||||
moderator: null,
|
|
||||||
creation: ['1000', '1000', '90'],
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
id: expect.any(Number),
|
|
||||||
firstName: 'Bibi',
|
|
||||||
lastName: 'Bloxberg',
|
|
||||||
email: 'bibi@bloxberg.de',
|
|
||||||
date: expect.any(String),
|
|
||||||
memo: 'Grundeinkommen',
|
|
||||||
amount: '500',
|
|
||||||
moderator: admin.id,
|
|
||||||
creation: ['1000', '1000', '90'],
|
|
||||||
}),
|
|
||||||
expect.objectContaining({
|
|
||||||
id: expect.any(Number),
|
|
||||||
firstName: 'Bibi',
|
|
||||||
lastName: 'Bloxberg',
|
|
||||||
email: 'bibi@bloxberg.de',
|
|
||||||
date: expect.any(String),
|
|
||||||
memo: 'Aktives Grundeinkommen',
|
|
||||||
amount: '200',
|
|
||||||
moderator: admin.id,
|
|
||||||
creation: ['1000', '1000', '90'],
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('adminDeleteContribution', () => {
|
describe('adminDeleteContribution', () => {
|
||||||
describe('creation id does not exist', () => {
|
describe('creation id does not exist', () => {
|
||||||
it('throws an error', async () => {
|
it('throws an error', async () => {
|
||||||
@ -2831,4 +2719,320 @@ describe('ContributionResolver', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('adminListAllContribution', () => {
|
||||||
|
describe('unauthenticated', () => {
|
||||||
|
it('returns an error', async () => {
|
||||||
|
await expect(
|
||||||
|
query({
|
||||||
|
query: adminListAllContributions,
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [new GraphQLError('401 Unauthorized')],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated as user', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await mutate({
|
||||||
|
mutation: login,
|
||||||
|
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
resetToken()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns an error', async () => {
|
||||||
|
await expect(
|
||||||
|
query({
|
||||||
|
query: adminListAllContributions,
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [new GraphQLError('401 Unauthorized')],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated as admin', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await mutate({
|
||||||
|
mutation: login,
|
||||||
|
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
resetToken()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns 19 creations in total', async () => {
|
||||||
|
const {
|
||||||
|
data: { adminListAllContributions: contributionListObject },
|
||||||
|
}: { data: { adminListAllContributions: ContributionListResult } } = await query({
|
||||||
|
query: adminListAllContributions,
|
||||||
|
})
|
||||||
|
expect(contributionListObject.contributionList).toHaveLength(19)
|
||||||
|
expect(contributionListObject).toMatchObject({
|
||||||
|
contributionCount: 19,
|
||||||
|
contributionList: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: expect.decimalEqual(50),
|
||||||
|
firstName: 'Bibi',
|
||||||
|
id: expect.any(Number),
|
||||||
|
lastName: 'Bloxberg',
|
||||||
|
memo: 'Herzlich Willkommen bei Gradido liebe Bibi!',
|
||||||
|
messagesCount: 0,
|
||||||
|
state: 'CONFIRMED',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: expect.decimalEqual(50),
|
||||||
|
firstName: 'Bibi',
|
||||||
|
id: expect.any(Number),
|
||||||
|
lastName: 'Bloxberg',
|
||||||
|
memo: 'Herzlich Willkommen bei Gradido liebe Bibi!',
|
||||||
|
messagesCount: 0,
|
||||||
|
state: 'CONFIRMED',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: expect.decimalEqual(450),
|
||||||
|
firstName: 'Bibi',
|
||||||
|
id: expect.any(Number),
|
||||||
|
lastName: 'Bloxberg',
|
||||||
|
memo: 'Herzlich Willkommen bei Gradido liebe Bibi!',
|
||||||
|
messagesCount: 0,
|
||||||
|
state: 'CONFIRMED',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: expect.decimalEqual(100),
|
||||||
|
firstName: 'Bob',
|
||||||
|
id: expect.any(Number),
|
||||||
|
lastName: 'der Baumeister',
|
||||||
|
memo: 'Confirmed Contribution',
|
||||||
|
messagesCount: 0,
|
||||||
|
state: 'CONFIRMED',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: expect.decimalEqual(400),
|
||||||
|
firstName: 'Peter',
|
||||||
|
id: expect.any(Number),
|
||||||
|
lastName: 'Lustig',
|
||||||
|
memo: 'Herzlich Willkommen bei Gradido!',
|
||||||
|
messagesCount: 0,
|
||||||
|
state: 'PENDING',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: expect.decimalEqual(100),
|
||||||
|
firstName: 'Peter',
|
||||||
|
id: expect.any(Number),
|
||||||
|
lastName: 'Lustig',
|
||||||
|
memo: 'Test env contribution',
|
||||||
|
messagesCount: 0,
|
||||||
|
state: 'PENDING',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: expect.decimalEqual(200),
|
||||||
|
firstName: 'Bibi',
|
||||||
|
id: expect.any(Number),
|
||||||
|
lastName: 'Bloxberg',
|
||||||
|
memo: 'Aktives Grundeinkommen',
|
||||||
|
messagesCount: 0,
|
||||||
|
state: 'PENDING',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: expect.decimalEqual(500),
|
||||||
|
firstName: 'Bibi',
|
||||||
|
id: expect.any(Number),
|
||||||
|
lastName: 'Bloxberg',
|
||||||
|
memo: 'Grundeinkommen',
|
||||||
|
messagesCount: 0,
|
||||||
|
state: 'PENDING',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: expect.decimalEqual(500),
|
||||||
|
firstName: 'Peter',
|
||||||
|
id: expect.any(Number),
|
||||||
|
lastName: 'Lustig',
|
||||||
|
memo: 'Grundeinkommen',
|
||||||
|
messagesCount: 0,
|
||||||
|
state: 'PENDING',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: expect.decimalEqual(10),
|
||||||
|
firstName: 'Bibi',
|
||||||
|
id: expect.any(Number),
|
||||||
|
lastName: 'Bloxberg',
|
||||||
|
memo: 'Test PENDING contribution update',
|
||||||
|
messagesCount: 0,
|
||||||
|
state: 'PENDING',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: expect.decimalEqual(200),
|
||||||
|
firstName: 'Peter',
|
||||||
|
id: expect.any(Number),
|
||||||
|
lastName: 'Lustig',
|
||||||
|
memo: 'Das war leider zu Viel!',
|
||||||
|
messagesCount: 0,
|
||||||
|
state: 'DELETED',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: expect.decimalEqual(166),
|
||||||
|
firstName: 'Räuber',
|
||||||
|
id: expect.any(Number),
|
||||||
|
lastName: 'Hotzenplotz',
|
||||||
|
memo: 'Whatever contribution',
|
||||||
|
messagesCount: 0,
|
||||||
|
state: 'DELETED',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: expect.decimalEqual(166),
|
||||||
|
firstName: 'Räuber',
|
||||||
|
id: expect.any(Number),
|
||||||
|
lastName: 'Hotzenplotz',
|
||||||
|
memo: 'Whatever contribution',
|
||||||
|
messagesCount: 0,
|
||||||
|
state: 'DENIED',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: expect.decimalEqual(166),
|
||||||
|
firstName: 'Räuber',
|
||||||
|
id: expect.any(Number),
|
||||||
|
lastName: 'Hotzenplotz',
|
||||||
|
memo: 'Whatever contribution',
|
||||||
|
messagesCount: 0,
|
||||||
|
state: 'CONFIRMED',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: expect.decimalEqual(100),
|
||||||
|
firstName: 'Bibi',
|
||||||
|
id: expect.any(Number),
|
||||||
|
lastName: 'Bloxberg',
|
||||||
|
memo: 'Test IN_PROGRESS contribution',
|
||||||
|
messagesCount: 0,
|
||||||
|
state: 'IN_PROGRESS',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: expect.decimalEqual(100),
|
||||||
|
firstName: 'Bibi',
|
||||||
|
id: expect.any(Number),
|
||||||
|
lastName: 'Bloxberg',
|
||||||
|
memo: 'Test contribution to confirm',
|
||||||
|
messagesCount: 0,
|
||||||
|
state: 'CONFIRMED',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: expect.decimalEqual(100),
|
||||||
|
firstName: 'Bibi',
|
||||||
|
id: expect.any(Number),
|
||||||
|
lastName: 'Bloxberg',
|
||||||
|
memo: 'Test contribution to deny',
|
||||||
|
messagesCount: 0,
|
||||||
|
state: 'DENIED',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: expect.decimalEqual(100),
|
||||||
|
firstName: 'Bibi',
|
||||||
|
id: expect.any(Number),
|
||||||
|
lastName: 'Bloxberg',
|
||||||
|
memo: 'Test contribution to delete',
|
||||||
|
messagesCount: 0,
|
||||||
|
state: 'DELETED',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: expect.decimalEqual(1000),
|
||||||
|
firstName: 'Bibi',
|
||||||
|
id: expect.any(Number),
|
||||||
|
lastName: 'Bloxberg',
|
||||||
|
memo: 'Herzlich Willkommen bei Gradido!',
|
||||||
|
messagesCount: 0,
|
||||||
|
state: 'CONFIRMED',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns five pending creations with page size set to 5', async () => {
|
||||||
|
const {
|
||||||
|
data: { adminListAllContributions: contributionListObject },
|
||||||
|
}: { data: { adminListAllContributions: ContributionListResult } } = await query({
|
||||||
|
query: adminListAllContributions,
|
||||||
|
variables: {
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 5,
|
||||||
|
order: Order.DESC,
|
||||||
|
statusFilter: ['PENDING'],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(contributionListObject.contributionList).toHaveLength(5)
|
||||||
|
expect(contributionListObject).toMatchObject({
|
||||||
|
contributionCount: 6,
|
||||||
|
contributionList: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: '400',
|
||||||
|
firstName: 'Peter',
|
||||||
|
id: expect.any(Number),
|
||||||
|
lastName: 'Lustig',
|
||||||
|
memo: 'Herzlich Willkommen bei Gradido!',
|
||||||
|
messagesCount: 0,
|
||||||
|
state: 'PENDING',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: '200',
|
||||||
|
firstName: 'Bibi',
|
||||||
|
id: expect.any(Number),
|
||||||
|
lastName: 'Bloxberg',
|
||||||
|
memo: 'Aktives Grundeinkommen',
|
||||||
|
messagesCount: 0,
|
||||||
|
state: 'PENDING',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: '500',
|
||||||
|
firstName: 'Bibi',
|
||||||
|
id: expect.any(Number),
|
||||||
|
lastName: 'Bloxberg',
|
||||||
|
memo: 'Grundeinkommen',
|
||||||
|
messagesCount: 0,
|
||||||
|
state: 'PENDING',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: '500',
|
||||||
|
firstName: 'Peter',
|
||||||
|
id: expect.any(Number),
|
||||||
|
lastName: 'Lustig',
|
||||||
|
memo: 'Grundeinkommen',
|
||||||
|
messagesCount: 0,
|
||||||
|
state: 'PENDING',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
amount: '100',
|
||||||
|
firstName: 'Peter',
|
||||||
|
id: expect.any(Number),
|
||||||
|
lastName: 'Lustig',
|
||||||
|
memo: 'Test env contribution',
|
||||||
|
messagesCount: 0,
|
||||||
|
state: 'PENDING',
|
||||||
|
}),
|
||||||
|
expect.not.objectContaining({
|
||||||
|
state: 'DENIED',
|
||||||
|
}),
|
||||||
|
expect.not.objectContaining({
|
||||||
|
state: 'DELETED',
|
||||||
|
}),
|
||||||
|
expect.not.objectContaining({
|
||||||
|
state: 'CONFIRMED',
|
||||||
|
}),
|
||||||
|
expect.not.objectContaining({
|
||||||
|
state: 'IN_PROGRESS',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import Decimal from 'decimal.js-light'
|
import Decimal from 'decimal.js-light'
|
||||||
import { Arg, Args, Authorized, Ctx, Int, Mutation, Query, Resolver } from 'type-graphql'
|
import { Arg, Args, Authorized, Ctx, Int, Mutation, Query, Resolver } from 'type-graphql'
|
||||||
import { FindOperator, IsNull, In, getConnection } from '@dbTools/typeorm'
|
import { FindOperator, IsNull, getConnection } from '@dbTools/typeorm'
|
||||||
|
|
||||||
import { Contribution as DbContribution } from '@entity/Contribution'
|
import { Contribution as DbContribution } from '@entity/Contribution'
|
||||||
import { ContributionMessage } from '@entity/ContributionMessage'
|
import { ContributionMessage } from '@entity/ContributionMessage'
|
||||||
@ -30,12 +30,11 @@ import { backendLogger as logger } from '@/server/logger'
|
|||||||
import {
|
import {
|
||||||
getCreationDates,
|
getCreationDates,
|
||||||
getUserCreation,
|
getUserCreation,
|
||||||
getUserCreations,
|
|
||||||
validateContribution,
|
validateContribution,
|
||||||
updateCreations,
|
updateCreations,
|
||||||
isValidDateString,
|
isValidDateString,
|
||||||
} from './util/creations'
|
} from './util/creations'
|
||||||
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS, FULL_CREATION_AVAILABLE } from './const/const'
|
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
||||||
import {
|
import {
|
||||||
EVENT_CONTRIBUTION_CREATE,
|
EVENT_CONTRIBUTION_CREATE,
|
||||||
EVENT_CONTRIBUTION_DELETE,
|
EVENT_CONTRIBUTION_DELETE,
|
||||||
@ -56,6 +55,7 @@ import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
|
|||||||
import LogError from '@/server/LogError'
|
import LogError from '@/server/LogError'
|
||||||
|
|
||||||
import { getLastTransaction } from './util/getLastTransaction'
|
import { getLastTransaction } from './util/getLastTransaction'
|
||||||
|
import { findContributions } from './util/findContributions'
|
||||||
|
|
||||||
@Resolver()
|
@Resolver()
|
||||||
export class ContributionResolver {
|
export class ContributionResolver {
|
||||||
@ -168,25 +168,14 @@ export class ContributionResolver {
|
|||||||
@Arg('statusFilter', () => [ContributionStatus], { nullable: true })
|
@Arg('statusFilter', () => [ContributionStatus], { nullable: true })
|
||||||
statusFilter?: ContributionStatus[],
|
statusFilter?: ContributionStatus[],
|
||||||
): Promise<ContributionListResult> {
|
): Promise<ContributionListResult> {
|
||||||
const where: {
|
const [dbContributions, count] = await findContributions(
|
||||||
contributionStatus?: FindOperator<string> | null
|
order,
|
||||||
} = {}
|
currentPage,
|
||||||
|
pageSize,
|
||||||
|
false,
|
||||||
|
statusFilter,
|
||||||
|
)
|
||||||
|
|
||||||
if (statusFilter && statusFilter.length) {
|
|
||||||
where.contributionStatus = In(statusFilter)
|
|
||||||
}
|
|
||||||
|
|
||||||
const [dbContributions, count] = await getConnection()
|
|
||||||
.createQueryBuilder()
|
|
||||||
.select('c')
|
|
||||||
.from(DbContribution, 'c')
|
|
||||||
.innerJoinAndSelect('c.user', 'u')
|
|
||||||
.leftJoinAndSelect('c.messages', 'm')
|
|
||||||
.where(where)
|
|
||||||
.orderBy('c.createdAt', order)
|
|
||||||
.limit(pageSize)
|
|
||||||
.offset((currentPage - 1) * pageSize)
|
|
||||||
.getManyAndCount()
|
|
||||||
return new ContributionListResult(
|
return new ContributionListResult(
|
||||||
count,
|
count,
|
||||||
dbContributions.map((contribution) => new Contribution(contribution, contribution.user)),
|
dbContributions.map((contribution) => new Contribution(contribution, contribution.user)),
|
||||||
@ -425,40 +414,25 @@ export class ContributionResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Authorized([RIGHTS.LIST_UNCONFIRMED_CONTRIBUTIONS])
|
@Authorized([RIGHTS.LIST_UNCONFIRMED_CONTRIBUTIONS])
|
||||||
@Query(() => [UnconfirmedContribution])
|
@Query(() => ContributionListResult) // [UnconfirmedContribution]
|
||||||
async listUnconfirmedContributions(@Ctx() context: Context): Promise<UnconfirmedContribution[]> {
|
async adminListAllContributions(
|
||||||
const clientTimezoneOffset = getClientTimezoneOffset(context)
|
@Args()
|
||||||
const contributions = await getConnection()
|
{ currentPage = 1, pageSize = 3, order = Order.DESC }: Paginated,
|
||||||
.createQueryBuilder()
|
@Arg('statusFilter', () => [ContributionStatus], { nullable: true })
|
||||||
.select('c')
|
statusFilter?: ContributionStatus[],
|
||||||
.from(DbContribution, 'c')
|
): Promise<ContributionListResult> {
|
||||||
.leftJoinAndSelect('c.messages', 'm')
|
const [dbContributions, count] = await findContributions(
|
||||||
.where({ confirmedAt: IsNull() })
|
order,
|
||||||
.andWhere({ deniedAt: IsNull() })
|
currentPage,
|
||||||
.getMany()
|
pageSize,
|
||||||
|
true,
|
||||||
|
statusFilter,
|
||||||
|
)
|
||||||
|
|
||||||
if (contributions.length === 0) {
|
return new ContributionListResult(
|
||||||
return []
|
count,
|
||||||
}
|
dbContributions.map((contribution) => new Contribution(contribution, contribution.user)),
|
||||||
|
)
|
||||||
const userIds = contributions.map((p) => p.userId)
|
|
||||||
const userCreations = await getUserCreations(userIds, clientTimezoneOffset)
|
|
||||||
const users = await DbUser.find({
|
|
||||||
where: { id: In(userIds) },
|
|
||||||
withDeleted: true,
|
|
||||||
relations: ['emailContact'],
|
|
||||||
})
|
|
||||||
|
|
||||||
return contributions.map((contribution) => {
|
|
||||||
const user = users.find((u) => u.id === contribution.userId)
|
|
||||||
const creation = userCreations.find((c) => c.id === contribution.userId)
|
|
||||||
|
|
||||||
return new UnconfirmedContribution(
|
|
||||||
contribution,
|
|
||||||
user,
|
|
||||||
creation ? creation.creations : FULL_CREATION_AVAILABLE,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authorized([RIGHTS.ADMIN_DELETE_CONTRIBUTION])
|
@Authorized([RIGHTS.ADMIN_DELETE_CONTRIBUTION])
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { Context, getUser } from '@/server/context'
|
|||||||
import CONFIG from '@/config'
|
import CONFIG from '@/config'
|
||||||
import { apiGet, apiPost } from '@/apis/HttpRequest'
|
import { apiGet, apiPost } from '@/apis/HttpRequest'
|
||||||
import { RIGHTS } from '@/auth/RIGHTS'
|
import { RIGHTS } from '@/auth/RIGHTS'
|
||||||
|
import LogError from '@/server/LogError'
|
||||||
|
|
||||||
@Resolver()
|
@Resolver()
|
||||||
export class GdtResolver {
|
export class GdtResolver {
|
||||||
@ -25,11 +26,11 @@ export class GdtResolver {
|
|||||||
`${CONFIG.GDT_API_URL}/GdtEntries/listPerEmailApi/${userEntity.emailContact.email}/${currentPage}/${pageSize}/${order}`,
|
`${CONFIG.GDT_API_URL}/GdtEntries/listPerEmailApi/${userEntity.emailContact.email}/${currentPage}/${pageSize}/${order}`,
|
||||||
)
|
)
|
||||||
if (!resultGDT.success) {
|
if (!resultGDT.success) {
|
||||||
throw new Error(resultGDT.data)
|
throw new LogError(resultGDT.data)
|
||||||
}
|
}
|
||||||
return new GdtEntryList(resultGDT.data)
|
return new GdtEntryList(resultGDT.data)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new Error('GDT Server is not reachable.')
|
throw new LogError('GDT Server is not reachable')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,7 +43,7 @@ export class GdtResolver {
|
|||||||
email: user.emailContact.email,
|
email: user.emailContact.email,
|
||||||
})
|
})
|
||||||
if (!resultGDTSum.success) {
|
if (!resultGDTSum.success) {
|
||||||
throw new Error('Call not successful')
|
throw new LogError('Call not successful')
|
||||||
}
|
}
|
||||||
return Number(resultGDTSum.data.sum) || 0
|
return Number(resultGDTSum.data.sum) || 0
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -59,7 +60,7 @@ export class GdtResolver {
|
|||||||
// load user
|
// load user
|
||||||
const resultPID = await apiGet(`${CONFIG.GDT_API_URL}/publishers/checkPidApi/${pid}`)
|
const resultPID = await apiGet(`${CONFIG.GDT_API_URL}/publishers/checkPidApi/${pid}`)
|
||||||
if (!resultPID.success) {
|
if (!resultPID.success) {
|
||||||
throw new Error(resultPID.data)
|
throw new LogError(resultPID.data)
|
||||||
}
|
}
|
||||||
return resultPID.data.pid
|
return resultPID.data.pid
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,65 +53,81 @@ afterAll(async () => {
|
|||||||
|
|
||||||
describe('TransactionLinkResolver', () => {
|
describe('TransactionLinkResolver', () => {
|
||||||
describe('createTransactionLink', () => {
|
describe('createTransactionLink', () => {
|
||||||
beforeAll(async () => {
|
describe('unauthenticated', () => {
|
||||||
await mutate({
|
it('throws an error', async () => {
|
||||||
mutation: login,
|
jest.clearAllMocks()
|
||||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
resetToken()
|
||||||
|
await expect(
|
||||||
|
mutate({ mutation: createTransactionLink, variables: { amount: 0, memo: 'Test' } }),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [new GraphQLError('401 Unauthorized')],
|
||||||
|
}),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('throws error when amount is zero', async () => {
|
describe('authenticated', () => {
|
||||||
jest.clearAllMocks()
|
beforeAll(async () => {
|
||||||
await expect(
|
await mutate({
|
||||||
mutate({
|
mutation: login,
|
||||||
mutation: createTransactionLink,
|
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||||
variables: {
|
})
|
||||||
amount: 0,
|
|
||||||
memo: 'Test',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
).resolves.toMatchObject({
|
|
||||||
errors: [new GraphQLError('Amount must be a positive number')],
|
|
||||||
})
|
})
|
||||||
})
|
|
||||||
it('logs the error thrown', () => {
|
|
||||||
expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(0))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('throws error when amount is negative', async () => {
|
it('throws error when amount is zero', async () => {
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: createTransactionLink,
|
mutation: createTransactionLink,
|
||||||
variables: {
|
variables: {
|
||||||
amount: -10,
|
amount: 0,
|
||||||
memo: 'Test',
|
memo: 'Test',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
errors: [new GraphQLError('Amount must be a positive number')],
|
errors: [new GraphQLError('Amount must be a positive number')],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
it('logs the error thrown', () => {
|
||||||
|
expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(0))
|
||||||
})
|
})
|
||||||
})
|
|
||||||
it('logs the error thrown', () => {
|
|
||||||
expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(-10))
|
|
||||||
})
|
|
||||||
|
|
||||||
it('throws error when user has not enough GDD', async () => {
|
it('throws error when amount is negative', async () => {
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: createTransactionLink,
|
mutation: createTransactionLink,
|
||||||
variables: {
|
variables: {
|
||||||
amount: 1001,
|
amount: -10,
|
||||||
memo: 'Test',
|
memo: 'Test',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
errors: [new GraphQLError('User has not enough GDD')],
|
errors: [new GraphQLError('Amount must be a positive number')],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
it('logs the error thrown', () => {
|
||||||
|
expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(-10))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws error when user has not enough GDD', async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: createTransactionLink,
|
||||||
|
variables: {
|
||||||
|
amount: 1001,
|
||||||
|
memo: 'Test',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
errors: [new GraphQLError('User has not enough GDD')],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
it('logs the error thrown', () => {
|
||||||
|
expect(logger.error).toBeCalledWith('User has not enough GDD', expect.any(Number))
|
||||||
})
|
})
|
||||||
})
|
|
||||||
it('logs the error thrown', () => {
|
|
||||||
expect(logger.error).toBeCalledWith('User has not enough GDD', expect.any(Number))
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -121,236 +137,37 @@ describe('TransactionLinkResolver', () => {
|
|||||||
resetToken()
|
resetToken()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('contributionLink', () => {
|
describe('unauthenticated', () => {
|
||||||
describe('input not valid', () => {
|
it('throws an error', async () => {
|
||||||
beforeAll(async () => {
|
jest.clearAllMocks()
|
||||||
await mutate({
|
resetToken()
|
||||||
mutation: login,
|
await expect(
|
||||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
mutate({ mutation: redeemTransactionLink, variables: { code: 'CL-123456' } }),
|
||||||
})
|
).resolves.toEqual(
|
||||||
})
|
expect.objectContaining({
|
||||||
|
errors: [new GraphQLError('401 Unauthorized')],
|
||||||
it('throws error when link does not exists', async () => {
|
}),
|
||||||
jest.clearAllMocks()
|
)
|
||||||
await expect(
|
|
||||||
mutate({
|
|
||||||
mutation: redeemTransactionLink,
|
|
||||||
variables: {
|
|
||||||
code: 'CL-123456',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
).resolves.toMatchObject({
|
|
||||||
errors: [new GraphQLError('Creation from contribution link was not successful')],
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('logs the error thrown', () => {
|
|
||||||
expect(logger.error).toBeCalledWith(
|
|
||||||
'No contribution link found to given code',
|
|
||||||
'CL-123456',
|
|
||||||
)
|
|
||||||
expect(logger.error).toBeCalledWith(
|
|
||||||
'Creation from contribution link was not successful',
|
|
||||||
new Error('No contribution link found to given code'),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const now = new Date()
|
|
||||||
const validFrom = new Date(now.getFullYear() + 1, 0, 1)
|
|
||||||
|
|
||||||
it('throws error when link is not valid yet', async () => {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
const {
|
|
||||||
data: { createContributionLink: contributionLink },
|
|
||||||
} = await mutate({
|
|
||||||
mutation: createContributionLink,
|
|
||||||
variables: {
|
|
||||||
amount: new Decimal(5),
|
|
||||||
name: 'Daily Contribution Link',
|
|
||||||
memo: 'Thank you for contribute daily to the community',
|
|
||||||
cycle: 'DAILY',
|
|
||||||
validFrom: validFrom.toISOString(),
|
|
||||||
validTo: new Date(now.getFullYear() + 1, 11, 31, 23, 59, 59, 999).toISOString(),
|
|
||||||
maxAmountPerMonth: new Decimal(200),
|
|
||||||
maxPerCycle: 1,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
await expect(
|
|
||||||
mutate({
|
|
||||||
mutation: redeemTransactionLink,
|
|
||||||
variables: {
|
|
||||||
code: 'CL-' + contributionLink.code,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
).resolves.toMatchObject({
|
|
||||||
errors: [new GraphQLError('Creation from contribution link was not successful')],
|
|
||||||
})
|
|
||||||
await resetEntity(DbContributionLink)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('logs the error thrown', () => {
|
|
||||||
expect(logger.error).toBeCalledWith('Contribution link is not valid yet', validFrom)
|
|
||||||
expect(logger.error).toBeCalledWith(
|
|
||||||
'Creation from contribution link was not successful',
|
|
||||||
new Error('Contribution link is not valid yet'),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('throws error when contributionLink cycle is invalid', async () => {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
const now = new Date()
|
|
||||||
const {
|
|
||||||
data: { createContributionLink: contributionLink },
|
|
||||||
} = await mutate({
|
|
||||||
mutation: createContributionLink,
|
|
||||||
variables: {
|
|
||||||
amount: new Decimal(5),
|
|
||||||
name: 'Daily Contribution Link',
|
|
||||||
memo: 'Thank you for contribute daily to the community',
|
|
||||||
cycle: 'INVALID',
|
|
||||||
validFrom: new Date(now.getFullYear(), 0, 1).toISOString(),
|
|
||||||
validTo: new Date(now.getFullYear(), 11, 31, 23, 59, 59, 999).toISOString(),
|
|
||||||
maxAmountPerMonth: new Decimal(200),
|
|
||||||
maxPerCycle: 1,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
await expect(
|
|
||||||
mutate({
|
|
||||||
mutation: redeemTransactionLink,
|
|
||||||
variables: {
|
|
||||||
code: 'CL-' + contributionLink.code,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
).resolves.toMatchObject({
|
|
||||||
errors: [new GraphQLError('Creation from contribution link was not successful')],
|
|
||||||
})
|
|
||||||
await resetEntity(DbContributionLink)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('logs the error thrown', () => {
|
|
||||||
expect(logger.error).toBeCalledWith('Contribution link has unknown cycle', 'INVALID')
|
|
||||||
expect(logger.error).toBeCalledWith(
|
|
||||||
'Creation from contribution link was not successful',
|
|
||||||
new Error('Contribution link has unknown cycle'),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const validTo = new Date(now.getFullYear() - 1, 11, 31, 23, 59, 59, 0)
|
|
||||||
it('throws error when link is no longer valid', async () => {
|
|
||||||
jest.clearAllMocks()
|
|
||||||
const {
|
|
||||||
data: { createContributionLink: contributionLink },
|
|
||||||
} = await mutate({
|
|
||||||
mutation: createContributionLink,
|
|
||||||
variables: {
|
|
||||||
amount: new Decimal(5),
|
|
||||||
name: 'Daily Contribution Link',
|
|
||||||
memo: 'Thank you for contribute daily to the community',
|
|
||||||
cycle: 'DAILY',
|
|
||||||
validFrom: new Date(now.getFullYear() - 1, 0, 1).toISOString(),
|
|
||||||
validTo: validTo.toISOString(),
|
|
||||||
maxAmountPerMonth: new Decimal(200),
|
|
||||||
maxPerCycle: 1,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
await expect(
|
|
||||||
mutate({
|
|
||||||
mutation: redeemTransactionLink,
|
|
||||||
variables: {
|
|
||||||
code: 'CL-' + contributionLink.code,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
).resolves.toMatchObject({
|
|
||||||
errors: [new GraphQLError('Creation from contribution link was not successful')],
|
|
||||||
})
|
|
||||||
await resetEntity(DbContributionLink)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('logs the error thrown', () => {
|
|
||||||
expect(logger.error).toBeCalledWith('Contribution link is no longer valid', validTo)
|
|
||||||
expect(logger.error).toBeCalledWith(
|
|
||||||
'Creation from contribution link was not successful',
|
|
||||||
new Error('Contribution link is no longer valid'),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
// TODO: have this test separated into a transactionLink and a contributionLink part
|
describe('authenticated', () => {
|
||||||
describe('redeem daily Contribution Link', () => {
|
describe('contributionLink', () => {
|
||||||
const now = new Date()
|
describe('input not valid', () => {
|
||||||
let contributionLink: DbContributionLink | undefined
|
|
||||||
let contribution: UnconfirmedContribution | undefined
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
|
||||||
await mutate({
|
|
||||||
mutation: login,
|
|
||||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
|
||||||
})
|
|
||||||
await mutate({
|
|
||||||
mutation: createContributionLink,
|
|
||||||
variables: {
|
|
||||||
amount: new Decimal(5),
|
|
||||||
name: 'Daily Contribution Link',
|
|
||||||
memo: 'Thank you for contribute daily to the community',
|
|
||||||
cycle: 'DAILY',
|
|
||||||
validFrom: new Date(now.getFullYear(), 0, 1).toISOString(),
|
|
||||||
validTo: new Date(now.getFullYear(), 11, 31, 23, 59, 59, 999).toISOString(),
|
|
||||||
maxAmountPerMonth: new Decimal(200),
|
|
||||||
maxPerCycle: 1,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('has a daily contribution link in the database', async () => {
|
|
||||||
const cls = await DbContributionLink.find()
|
|
||||||
expect(cls).toHaveLength(1)
|
|
||||||
contributionLink = cls[0]
|
|
||||||
expect(contributionLink).toEqual(
|
|
||||||
expect.objectContaining({
|
|
||||||
id: expect.any(Number),
|
|
||||||
name: 'Daily Contribution Link',
|
|
||||||
memo: 'Thank you for contribute daily to the community',
|
|
||||||
validFrom: new Date(now.getFullYear(), 0, 1),
|
|
||||||
validTo: new Date(now.getFullYear(), 11, 31, 23, 59, 59, 0),
|
|
||||||
cycle: 'DAILY',
|
|
||||||
maxPerCycle: 1,
|
|
||||||
totalMaxCountOfContribution: null,
|
|
||||||
maxAccountBalance: null,
|
|
||||||
minGapHours: null,
|
|
||||||
createdAt: expect.any(Date),
|
|
||||||
deletedAt: null,
|
|
||||||
code: expect.stringMatching(/^[0-9a-f]{24,24}$/),
|
|
||||||
linkEnabled: true,
|
|
||||||
amount: expect.decimalEqual(5),
|
|
||||||
maxAmountPerMonth: expect.decimalEqual(200),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('user has pending contribution of 1000 GDD', () => {
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await mutate({
|
await mutate({
|
||||||
mutation: login,
|
mutation: login,
|
||||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||||
})
|
})
|
||||||
const result = await mutate({
|
|
||||||
mutation: createContribution,
|
|
||||||
variables: {
|
|
||||||
amount: new Decimal(1000),
|
|
||||||
memo: 'I was brewing potions for the community the whole month',
|
|
||||||
creationDate: now.toISOString(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
contribution = result.data.createContribution
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not allow the user to redeem the contribution link', async () => {
|
it('throws error when link does not exists', async () => {
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: redeemTransactionLink,
|
mutation: redeemTransactionLink,
|
||||||
variables: {
|
variables: {
|
||||||
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
|
code: 'CL-123456',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
@ -359,85 +176,247 @@ describe('TransactionLinkResolver', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('logs the error thrown', () => {
|
it('logs the error thrown', () => {
|
||||||
|
expect(logger.error).toBeCalledWith(
|
||||||
|
'No contribution link found to given code',
|
||||||
|
'CL-123456',
|
||||||
|
)
|
||||||
expect(logger.error).toBeCalledWith(
|
expect(logger.error).toBeCalledWith(
|
||||||
'Creation from contribution link was not successful',
|
'Creation from contribution link was not successful',
|
||||||
new Error(
|
new Error('No contribution link found to given code'),
|
||||||
'The amount (5 GDD) to be created exceeds the amount (0 GDD) still available for this month.',
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
|
||||||
describe('user has no pending contributions that would not allow to redeem the link', () => {
|
const now = new Date()
|
||||||
beforeAll(async () => {
|
const validFrom = new Date(now.getFullYear() + 1, 0, 1)
|
||||||
await mutate({
|
|
||||||
mutation: login,
|
|
||||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
|
||||||
})
|
|
||||||
await mutate({
|
|
||||||
mutation: updateContribution,
|
|
||||||
variables: {
|
|
||||||
contributionId: contribution ? contribution.id : -1,
|
|
||||||
amount: new Decimal(800),
|
|
||||||
memo: 'I was brewing potions for the community the whole month',
|
|
||||||
creationDate: now.toISOString(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('allows the user to redeem the contribution link', async () => {
|
it('throws error when link is not valid yet', async () => {
|
||||||
await expect(
|
|
||||||
mutate({
|
|
||||||
mutation: redeemTransactionLink,
|
|
||||||
variables: {
|
|
||||||
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
).resolves.toMatchObject({
|
|
||||||
data: {
|
|
||||||
redeemTransactionLink: true,
|
|
||||||
},
|
|
||||||
errors: undefined,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('does not allow the user to redeem the contribution link a second time on the same day', async () => {
|
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
|
const {
|
||||||
|
data: { createContributionLink: contributionLink },
|
||||||
|
} = await mutate({
|
||||||
|
mutation: createContributionLink,
|
||||||
|
variables: {
|
||||||
|
amount: new Decimal(5),
|
||||||
|
name: 'Daily Contribution Link',
|
||||||
|
memo: 'Thank you for contribute daily to the community',
|
||||||
|
cycle: 'DAILY',
|
||||||
|
validFrom: validFrom.toISOString(),
|
||||||
|
validTo: new Date(now.getFullYear() + 1, 11, 31, 23, 59, 59, 999).toISOString(),
|
||||||
|
maxAmountPerMonth: new Decimal(200),
|
||||||
|
maxPerCycle: 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: redeemTransactionLink,
|
mutation: redeemTransactionLink,
|
||||||
variables: {
|
variables: {
|
||||||
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
|
code: 'CL-' + contributionLink.code,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
errors: [new GraphQLError('Creation from contribution link was not successful')],
|
errors: [new GraphQLError('Creation from contribution link was not successful')],
|
||||||
})
|
})
|
||||||
|
await resetEntity(DbContributionLink)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('logs the error thrown', () => {
|
it('logs the error thrown', () => {
|
||||||
|
expect(logger.error).toBeCalledWith('Contribution link is not valid yet', validFrom)
|
||||||
expect(logger.error).toBeCalledWith(
|
expect(logger.error).toBeCalledWith(
|
||||||
'Creation from contribution link was not successful',
|
'Creation from contribution link was not successful',
|
||||||
new Error('Contribution link already redeemed today'),
|
new Error('Contribution link is not valid yet'),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('after one day', () => {
|
it('throws error when contributionLink cycle is invalid', async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
const now = new Date()
|
||||||
|
const {
|
||||||
|
data: { createContributionLink: contributionLink },
|
||||||
|
} = await mutate({
|
||||||
|
mutation: createContributionLink,
|
||||||
|
variables: {
|
||||||
|
amount: new Decimal(5),
|
||||||
|
name: 'Daily Contribution Link',
|
||||||
|
memo: 'Thank you for contribute daily to the community',
|
||||||
|
cycle: 'INVALID',
|
||||||
|
validFrom: new Date(now.getFullYear(), 0, 1).toISOString(),
|
||||||
|
validTo: new Date(now.getFullYear(), 11, 31, 23, 59, 59, 999).toISOString(),
|
||||||
|
maxAmountPerMonth: new Decimal(200),
|
||||||
|
maxPerCycle: 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: redeemTransactionLink,
|
||||||
|
variables: {
|
||||||
|
code: 'CL-' + contributionLink.code,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
errors: [new GraphQLError('Creation from contribution link was not successful')],
|
||||||
|
})
|
||||||
|
await resetEntity(DbContributionLink)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs the error thrown', () => {
|
||||||
|
expect(logger.error).toBeCalledWith('Contribution link has unknown cycle', 'INVALID')
|
||||||
|
expect(logger.error).toBeCalledWith(
|
||||||
|
'Creation from contribution link was not successful',
|
||||||
|
new Error('Contribution link has unknown cycle'),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const validTo = new Date(now.getFullYear() - 1, 11, 31, 23, 59, 59, 0)
|
||||||
|
it('throws error when link is no longer valid', async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
const {
|
||||||
|
data: { createContributionLink: contributionLink },
|
||||||
|
} = await mutate({
|
||||||
|
mutation: createContributionLink,
|
||||||
|
variables: {
|
||||||
|
amount: new Decimal(5),
|
||||||
|
name: 'Daily Contribution Link',
|
||||||
|
memo: 'Thank you for contribute daily to the community',
|
||||||
|
cycle: 'DAILY',
|
||||||
|
validFrom: new Date(now.getFullYear() - 1, 0, 1).toISOString(),
|
||||||
|
validTo: validTo.toISOString(),
|
||||||
|
maxAmountPerMonth: new Decimal(200),
|
||||||
|
maxPerCycle: 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: redeemTransactionLink,
|
||||||
|
variables: {
|
||||||
|
code: 'CL-' + contributionLink.code,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
errors: [new GraphQLError('Creation from contribution link was not successful')],
|
||||||
|
})
|
||||||
|
await resetEntity(DbContributionLink)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs the error thrown', () => {
|
||||||
|
expect(logger.error).toBeCalledWith('Contribution link is no longer valid', validTo)
|
||||||
|
expect(logger.error).toBeCalledWith(
|
||||||
|
'Creation from contribution link was not successful',
|
||||||
|
new Error('Contribution link is no longer valid'),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// TODO: have this test separated into a transactionLink and a contributionLink part
|
||||||
|
describe('redeem daily Contribution Link', () => {
|
||||||
|
const now = new Date()
|
||||||
|
let contributionLink: DbContributionLink | undefined
|
||||||
|
let contribution: UnconfirmedContribution | undefined
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await mutate({
|
||||||
|
mutation: login,
|
||||||
|
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||||
|
})
|
||||||
|
await mutate({
|
||||||
|
mutation: createContributionLink,
|
||||||
|
variables: {
|
||||||
|
amount: new Decimal(5),
|
||||||
|
name: 'Daily Contribution Link',
|
||||||
|
memo: 'Thank you for contribute daily to the community',
|
||||||
|
cycle: 'DAILY',
|
||||||
|
validFrom: new Date(now.getFullYear(), 0, 1).toISOString(),
|
||||||
|
validTo: new Date(now.getFullYear(), 11, 31, 23, 59, 59, 999).toISOString(),
|
||||||
|
maxAmountPerMonth: new Decimal(200),
|
||||||
|
maxPerCycle: 1,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has a daily contribution link in the database', async () => {
|
||||||
|
const cls = await DbContributionLink.find()
|
||||||
|
expect(cls).toHaveLength(1)
|
||||||
|
contributionLink = cls[0]
|
||||||
|
expect(contributionLink).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
name: 'Daily Contribution Link',
|
||||||
|
memo: 'Thank you for contribute daily to the community',
|
||||||
|
validFrom: new Date(now.getFullYear(), 0, 1),
|
||||||
|
validTo: new Date(now.getFullYear(), 11, 31, 23, 59, 59, 0),
|
||||||
|
cycle: 'DAILY',
|
||||||
|
maxPerCycle: 1,
|
||||||
|
totalMaxCountOfContribution: null,
|
||||||
|
maxAccountBalance: null,
|
||||||
|
minGapHours: null,
|
||||||
|
createdAt: expect.any(Date),
|
||||||
|
deletedAt: null,
|
||||||
|
code: expect.stringMatching(/^[0-9a-f]{24,24}$/),
|
||||||
|
linkEnabled: true,
|
||||||
|
amount: expect.decimalEqual(5),
|
||||||
|
maxAmountPerMonth: expect.decimalEqual(200),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('user has pending contribution of 1000 GDD', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
jest.useFakeTimers()
|
|
||||||
setTimeout(jest.fn(), 1000 * 60 * 60 * 24)
|
|
||||||
jest.runAllTimers()
|
|
||||||
await mutate({
|
await mutate({
|
||||||
mutation: login,
|
mutation: login,
|
||||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||||
})
|
})
|
||||||
|
const result = await mutate({
|
||||||
|
mutation: createContribution,
|
||||||
|
variables: {
|
||||||
|
amount: new Decimal(1000),
|
||||||
|
memo: 'I was brewing potions for the community the whole month',
|
||||||
|
creationDate: now.toISOString(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
contribution = result.data.createContribution
|
||||||
})
|
})
|
||||||
|
|
||||||
afterAll(() => {
|
it('does not allow the user to redeem the contribution link', async () => {
|
||||||
jest.useRealTimers()
|
jest.clearAllMocks()
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: redeemTransactionLink,
|
||||||
|
variables: {
|
||||||
|
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
errors: [new GraphQLError('Creation from contribution link was not successful')],
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('allows the user to redeem the contribution link again', async () => {
|
it('logs the error thrown', () => {
|
||||||
|
expect(logger.error).toBeCalledWith(
|
||||||
|
'Creation from contribution link was not successful',
|
||||||
|
new Error(
|
||||||
|
'The amount to be created exceeds the amount still available for this month',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('user has no pending contributions that would not allow to redeem the link', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await mutate({
|
||||||
|
mutation: login,
|
||||||
|
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||||
|
})
|
||||||
|
await mutate({
|
||||||
|
mutation: updateContribution,
|
||||||
|
variables: {
|
||||||
|
contributionId: contribution ? contribution.id : -1,
|
||||||
|
amount: new Decimal(800),
|
||||||
|
memo: 'I was brewing potions for the community the whole month',
|
||||||
|
creationDate: now.toISOString(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows the user to redeem the contribution link', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: redeemTransactionLink,
|
mutation: redeemTransactionLink,
|
||||||
@ -473,6 +452,59 @@ describe('TransactionLinkResolver', () => {
|
|||||||
new Error('Contribution link already redeemed today'),
|
new Error('Contribution link already redeemed today'),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('after one day', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
jest.useFakeTimers()
|
||||||
|
setTimeout(jest.fn(), 1000 * 60 * 60 * 24)
|
||||||
|
jest.runAllTimers()
|
||||||
|
await mutate({
|
||||||
|
mutation: login,
|
||||||
|
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
jest.useRealTimers()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows the user to redeem the contribution link again', async () => {
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: redeemTransactionLink,
|
||||||
|
variables: {
|
||||||
|
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
data: {
|
||||||
|
redeemTransactionLink: true,
|
||||||
|
},
|
||||||
|
errors: undefined,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not allow the user to redeem the contribution link a second time on the same day', async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
await expect(
|
||||||
|
mutate({
|
||||||
|
mutation: redeemTransactionLink,
|
||||||
|
variables: {
|
||||||
|
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toMatchObject({
|
||||||
|
errors: [new GraphQLError('Creation from contribution link was not successful')],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs the error thrown', () => {
|
||||||
|
expect(logger.error).toBeCalledWith(
|
||||||
|
'Creation from contribution link was not successful',
|
||||||
|
new Error('Contribution link already redeemed today'),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -86,8 +86,8 @@ export class TransactionLinkResolver {
|
|||||||
transactionLink.code = transactionLinkCode(createdDate)
|
transactionLink.code = transactionLinkCode(createdDate)
|
||||||
transactionLink.createdAt = createdDate
|
transactionLink.createdAt = createdDate
|
||||||
transactionLink.validUntil = validUntil
|
transactionLink.validUntil = validUntil
|
||||||
await DbTransactionLink.save(transactionLink).catch(() => {
|
await DbTransactionLink.save(transactionLink).catch((e) => {
|
||||||
throw new Error('Unable to save transaction link')
|
throw new LogError('Unable to save transaction link', e)
|
||||||
})
|
})
|
||||||
|
|
||||||
return new TransactionLink(transactionLink, new User(user))
|
return new TransactionLink(transactionLink, new User(user))
|
||||||
@ -103,19 +103,23 @@ export class TransactionLinkResolver {
|
|||||||
|
|
||||||
const transactionLink = await DbTransactionLink.findOne({ id })
|
const transactionLink = await DbTransactionLink.findOne({ id })
|
||||||
if (!transactionLink) {
|
if (!transactionLink) {
|
||||||
throw new Error('Transaction Link not found!')
|
throw new LogError('Transaction link not found', id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (transactionLink.userId !== user.id) {
|
if (transactionLink.userId !== user.id) {
|
||||||
throw new Error('Transaction Link cannot be deleted!')
|
throw new LogError(
|
||||||
|
'Transaction link cannot be deleted by another user',
|
||||||
|
transactionLink.userId,
|
||||||
|
user.id,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (transactionLink.redeemedBy) {
|
if (transactionLink.redeemedBy) {
|
||||||
throw new Error('Transaction Link already redeemed!')
|
throw new LogError('Transaction link already redeemed', transactionLink.redeemedBy)
|
||||||
}
|
}
|
||||||
|
|
||||||
await transactionLink.softRemove().catch(() => {
|
await transactionLink.softRemove().catch((e) => {
|
||||||
throw new Error('Transaction Link could not be deleted!')
|
throw new LogError('Transaction link could not be deleted', e)
|
||||||
})
|
})
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@ -312,18 +316,18 @@ export class TransactionLinkResolver {
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (user.id === linkedUser.id) {
|
if (user.id === linkedUser.id) {
|
||||||
throw new Error('Cannot redeem own transaction link.')
|
throw new LogError('Cannot redeem own transaction link', user.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: The now check should be done within the semaphore lock,
|
// TODO: The now check should be done within the semaphore lock,
|
||||||
// since the program might wait a while till it is ready to proceed
|
// since the program might wait a while till it is ready to proceed
|
||||||
// writing the transaction.
|
// writing the transaction.
|
||||||
if (transactionLink.validUntil.getTime() < now.getTime()) {
|
if (transactionLink.validUntil.getTime() < now.getTime()) {
|
||||||
throw new Error('Transaction Link is not valid anymore.')
|
throw new LogError('Transaction link is not valid anymore', transactionLink.validUntil)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (transactionLink.redeemedBy) {
|
if (transactionLink.redeemedBy) {
|
||||||
throw new Error('Transaction Link already redeemed.')
|
throw new LogError('Transaction link already redeemed', transactionLink.redeemedBy)
|
||||||
}
|
}
|
||||||
|
|
||||||
await executeTransaction(
|
await executeTransaction(
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import LogError from '@/server/LogError'
|
||||||
import { backendLogger as logger } from '@/server/logger'
|
import { backendLogger as logger } from '@/server/logger'
|
||||||
import { getConnection } from '@dbTools/typeorm'
|
import { getConnection } from '@dbTools/typeorm'
|
||||||
import { Contribution } from '@entity/Contribution'
|
import { Contribution } from '@entity/Contribution'
|
||||||
@ -19,19 +20,14 @@ export const validateContribution = (
|
|||||||
const index = getCreationIndex(creationDate.getMonth(), timezoneOffset)
|
const index = getCreationIndex(creationDate.getMonth(), timezoneOffset)
|
||||||
|
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
logger.error(
|
throw new LogError('No information for available creations for the given date', creationDate)
|
||||||
'No information for available creations with the given creationDate=',
|
|
||||||
creationDate.toString(),
|
|
||||||
)
|
|
||||||
throw new Error('No information for available creations for the given date')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (amount.greaterThan(creations[index].toString())) {
|
if (amount.greaterThan(creations[index].toString())) {
|
||||||
logger.error(
|
throw new LogError(
|
||||||
`The amount (${amount} GDD) to be created exceeds the amount (${creations[index]} GDD) still available for this month.`,
|
'The amount to be created exceeds the amount still available for this month',
|
||||||
)
|
amount,
|
||||||
throw new Error(
|
creations[index],
|
||||||
`The amount (${amount} GDD) to be created exceeds the amount (${creations[index]} GDD) still available for this month.`,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -126,19 +122,16 @@ export const isStartEndDateValid = (
|
|||||||
endDate: string | null | undefined,
|
endDate: string | null | undefined,
|
||||||
): void => {
|
): void => {
|
||||||
if (!startDate) {
|
if (!startDate) {
|
||||||
logger.error('Start-Date is not initialized. A Start-Date must be set!')
|
throw new LogError('A Start-Date must be set')
|
||||||
throw new Error('Start-Date is not initialized. A Start-Date must be set!')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!endDate) {
|
if (!endDate) {
|
||||||
logger.error('End-Date is not initialized. An End-Date must be set!')
|
throw new LogError('An End-Date must be set')
|
||||||
throw new Error('End-Date is not initialized. An End-Date must be set!')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if endDate is before startDate
|
// check if endDate is before startDate
|
||||||
if (new Date(endDate).getTime() - new Date(startDate).getTime() < 0) {
|
if (new Date(endDate).getTime() - new Date(startDate).getTime() < 0) {
|
||||||
logger.error(`The value of validFrom must before or equals the validTo!`)
|
throw new LogError(`The value of validFrom must before or equals the validTo`)
|
||||||
throw new Error(`The value of validFrom must before or equals the validTo!`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,7 +143,7 @@ export const updateCreations = (
|
|||||||
const index = getCreationIndex(contribution.contributionDate.getMonth(), timezoneOffset)
|
const index = getCreationIndex(contribution.contributionDate.getMonth(), timezoneOffset)
|
||||||
|
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
throw new Error('You cannot create GDD for a month older than the last three months.')
|
throw new LogError('You cannot create GDD for a month older than the last three months')
|
||||||
}
|
}
|
||||||
creations[index] = creations[index].plus(contribution.amount.toString())
|
creations[index] = creations[index].plus(contribution.amount.toString())
|
||||||
return creations
|
return creations
|
||||||
|
|||||||
24
backend/src/graphql/resolver/util/findContributions.ts
Normal file
24
backend/src/graphql/resolver/util/findContributions.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { ContributionStatus } from '@enum/ContributionStatus'
|
||||||
|
import { Order } from '@enum/Order'
|
||||||
|
import { Contribution as DbContribution } from '@entity/Contribution'
|
||||||
|
import { In } from '@dbTools/typeorm'
|
||||||
|
|
||||||
|
export const findContributions = async (
|
||||||
|
order: Order,
|
||||||
|
currentPage: number,
|
||||||
|
pageSize: number,
|
||||||
|
withDeleted: boolean,
|
||||||
|
statusFilter?: ContributionStatus[],
|
||||||
|
): Promise<[DbContribution[], number]> =>
|
||||||
|
DbContribution.findAndCount({
|
||||||
|
where: {
|
||||||
|
...(statusFilter && statusFilter.length && { contributionStatus: In(statusFilter) }),
|
||||||
|
},
|
||||||
|
withDeleted: withDeleted,
|
||||||
|
order: {
|
||||||
|
createdAt: order,
|
||||||
|
},
|
||||||
|
relations: ['user'],
|
||||||
|
skip: (currentPage - 1) * pageSize,
|
||||||
|
take: pageSize,
|
||||||
|
})
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import CONFIG from '@/config'
|
import CONFIG from '@/config'
|
||||||
|
import LogError from '@/server/LogError'
|
||||||
import { backendLogger as logger } from '@/server/logger'
|
import { backendLogger as logger } from '@/server/logger'
|
||||||
import { User } from '@entity/User'
|
import { User } from '@entity/User'
|
||||||
import { PasswordEncryptionType } from '@enum/PasswordEncryptionType'
|
import { PasswordEncryptionType } from '@enum/PasswordEncryptionType'
|
||||||
@ -16,11 +17,10 @@ export const SecretKeyCryptographyCreateKey = (salt: string, password: string):
|
|||||||
const configLoginAppSecret = Buffer.from(CONFIG.LOGIN_APP_SECRET, 'hex')
|
const configLoginAppSecret = Buffer.from(CONFIG.LOGIN_APP_SECRET, 'hex')
|
||||||
const configLoginServerKey = Buffer.from(CONFIG.LOGIN_SERVER_KEY, 'hex')
|
const configLoginServerKey = Buffer.from(CONFIG.LOGIN_SERVER_KEY, 'hex')
|
||||||
if (configLoginServerKey.length !== sodium.crypto_shorthash_KEYBYTES) {
|
if (configLoginServerKey.length !== sodium.crypto_shorthash_KEYBYTES) {
|
||||||
logger.error(
|
throw new LogError(
|
||||||
`ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`,
|
'ServerKey has an invalid size',
|
||||||
)
|
configLoginServerKey.length,
|
||||||
throw new Error(
|
sodium.crypto_shorthash_KEYBYTES,
|
||||||
`ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,20 +52,13 @@ export const SecretKeyCryptographyCreateKey = (salt: string, password: string):
|
|||||||
|
|
||||||
export const getUserCryptographicSalt = (dbUser: User): string => {
|
export const getUserCryptographicSalt = (dbUser: User): string => {
|
||||||
switch (dbUser.passwordEncryptionType) {
|
switch (dbUser.passwordEncryptionType) {
|
||||||
case PasswordEncryptionType.NO_PASSWORD: {
|
case PasswordEncryptionType.NO_PASSWORD:
|
||||||
logger.error('Password not set for user ' + dbUser.id)
|
throw new LogError('User has no password set', dbUser.id)
|
||||||
throw new Error('Password not set for user ' + dbUser.id) // user has no password
|
case PasswordEncryptionType.EMAIL:
|
||||||
}
|
|
||||||
case PasswordEncryptionType.EMAIL: {
|
|
||||||
return dbUser.emailContact.email
|
return dbUser.emailContact.email
|
||||||
break
|
case PasswordEncryptionType.GRADIDO_ID:
|
||||||
}
|
|
||||||
case PasswordEncryptionType.GRADIDO_ID: {
|
|
||||||
return dbUser.gradidoID
|
return dbUser.gradidoID
|
||||||
break
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
logger.error(`Unknown password encryption type: ${dbUser.passwordEncryptionType}`)
|
throw new LogError('Unknown password encryption type', dbUser.passwordEncryptionType)
|
||||||
throw new Error(`Unknown password encryption type: ${dbUser.passwordEncryptionType}`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -186,6 +186,40 @@ query ($currentPage: Int = 1, $pageSize: Int = 5, $order: Order = DESC, $statusF
|
|||||||
contributionCount
|
contributionCount
|
||||||
contributionList {
|
contributionList {
|
||||||
id
|
id
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
amount
|
||||||
|
memo
|
||||||
|
createdAt
|
||||||
|
confirmedAt
|
||||||
|
confirmedBy
|
||||||
|
contributionDate
|
||||||
|
state
|
||||||
|
messagesCount
|
||||||
|
deniedAt
|
||||||
|
deniedBy
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
// from admin interface
|
||||||
|
|
||||||
|
export const adminListAllContributions = gql`
|
||||||
|
query (
|
||||||
|
$currentPage: Int = 1
|
||||||
|
$pageSize: Int = 25
|
||||||
|
$order: Order = DESC
|
||||||
|
$statusFilter: [ContributionStatus!]
|
||||||
|
) {
|
||||||
|
adminListAllContributions(
|
||||||
|
currentPage: $currentPage
|
||||||
|
pageSize: $pageSize
|
||||||
|
order: $order
|
||||||
|
statusFilter: $statusFilter
|
||||||
|
) {
|
||||||
|
contributionCount
|
||||||
|
contributionList {
|
||||||
|
id
|
||||||
firstName
|
firstName
|
||||||
lastName
|
lastName
|
||||||
amount
|
amount
|
||||||
@ -198,24 +232,7 @@ query ($currentPage: Int = 1, $pageSize: Int = 5, $order: Order = DESC, $statusF
|
|||||||
messagesCount
|
messagesCount
|
||||||
deniedAt
|
deniedAt
|
||||||
deniedBy
|
deniedBy
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
// from admin interface
|
|
||||||
|
|
||||||
export const listUnconfirmedContributions = gql`
|
|
||||||
query {
|
|
||||||
listUnconfirmedContributions {
|
|
||||||
id
|
|
||||||
firstName
|
|
||||||
lastName
|
|
||||||
email
|
|
||||||
amount
|
|
||||||
memo
|
|
||||||
date
|
|
||||||
moderator
|
|
||||||
creation
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { User as dbUser } from '@entity/User'
|
|||||||
import { Transaction as dbTransaction } from '@entity/Transaction'
|
import { Transaction as dbTransaction } from '@entity/Transaction'
|
||||||
import Decimal from 'decimal.js-light'
|
import Decimal from 'decimal.js-light'
|
||||||
import { ExpressContext } from 'apollo-server-express'
|
import { ExpressContext } from 'apollo-server-express'
|
||||||
|
import LogError from './LogError'
|
||||||
|
|
||||||
export interface Context {
|
export interface Context {
|
||||||
token: string | null
|
token: string | null
|
||||||
@ -35,7 +36,7 @@ const context = (args: ExpressContext): Context => {
|
|||||||
|
|
||||||
export const getUser = (context: Context): dbUser => {
|
export const getUser = (context: Context): dbUser => {
|
||||||
if (context.user) return context.user
|
if (context.user) return context.user
|
||||||
throw new Error('No user given in context!')
|
throw new LogError('No user given in context')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getClientTimezoneOffset = (context: Context): number => {
|
export const getClientTimezoneOffset = (context: Context): number => {
|
||||||
@ -45,7 +46,7 @@ export const getClientTimezoneOffset = (context: Context): number => {
|
|||||||
) {
|
) {
|
||||||
return context.clientTimezoneOffset
|
return context.clientTimezoneOffset
|
||||||
}
|
}
|
||||||
throw new Error('No valid client time zone offset in context!')
|
throw new LogError('No valid client time zone offset in context')
|
||||||
}
|
}
|
||||||
|
|
||||||
export default context
|
export default context
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import Decimal from 'decimal.js-light'
|
import Decimal from 'decimal.js-light'
|
||||||
import CONFIG from '@/config'
|
import CONFIG from '@/config'
|
||||||
import { Decay } from '@model/Decay'
|
import { Decay } from '@model/Decay'
|
||||||
|
import LogError from '@/server/LogError'
|
||||||
|
|
||||||
// TODO: externalize all those definitions and functions into an external decay library
|
// TODO: externalize all those definitions and functions into an external decay library
|
||||||
|
|
||||||
@ -22,7 +23,7 @@ function calculateDecay(
|
|||||||
const startBlockMs = startBlock.getTime()
|
const startBlockMs = startBlock.getTime()
|
||||||
|
|
||||||
if (toMs < fromMs) {
|
if (toMs < fromMs) {
|
||||||
throw new Error('to < from, reverse decay calculation is invalid')
|
throw new LogError('calculateDecay: to < from, reverse decay calculation is invalid')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize with no decay
|
// Initialize with no decay
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
import connection from '@/typeorm/connection'
|
import connection from '@/typeorm/connection'
|
||||||
import { getKlickTippUser } from '@/apis/KlicktippController'
|
import { getKlickTippUser } from '@/apis/KlicktippController'
|
||||||
import { User } from '@entity/User'
|
import { User } from '@entity/User'
|
||||||
|
import LogError from '@/server/LogError'
|
||||||
|
|
||||||
export async function retrieveNotRegisteredEmails(): Promise<string[]> {
|
export async function retrieveNotRegisteredEmails(): Promise<string[]> {
|
||||||
const con = await connection()
|
const con = await connection()
|
||||||
if (!con) {
|
if (!con) {
|
||||||
throw new Error('No connection to database')
|
throw new LogError('No connection to database')
|
||||||
}
|
}
|
||||||
const users = await User.find({ relations: ['emailContact'] })
|
const users = await User.find({ relations: ['emailContact'] })
|
||||||
const notRegisteredUser = []
|
const notRegisteredUser = []
|
||||||
|
|||||||
@ -2,19 +2,19 @@
|
|||||||
<div class="nav-community container">
|
<div class="nav-community container">
|
||||||
<b-row class="nav-row">
|
<b-row class="nav-row">
|
||||||
<b-col cols="12" lg="4" md="4" class="px-0">
|
<b-col cols="12" lg="4" md="4" class="px-0">
|
||||||
<b-btn active-class="btn-active" block variant="link" to="/community#edit">
|
<b-btn to="contribute" active-class="btn-active" block variant="link">
|
||||||
<b-icon icon="pencil" class="mr-2" />
|
<b-icon icon="pencil" class="mr-2" />
|
||||||
{{ $t('community.submitContribution') }}
|
{{ $t('community.submitContribution') }}
|
||||||
</b-btn>
|
</b-btn>
|
||||||
</b-col>
|
</b-col>
|
||||||
<b-col cols="12" lg="4" md="4" class="px-0">
|
<b-col cols="12" lg="4" md="4" class="px-0">
|
||||||
<b-btn active-class="btn-active" block variant="link" to="/community#my">
|
<b-btn to="contributions" active-class="btn-active" block variant="link">
|
||||||
<b-icon icon="person" class="mr-2" />
|
<b-icon icon="person" class="mr-2" />
|
||||||
{{ $t('community.myContributions') }}
|
{{ $t('community.myContributions') }}
|
||||||
</b-btn>
|
</b-btn>
|
||||||
</b-col>
|
</b-col>
|
||||||
<b-col cols="12" lg="4" md="4" class="px-0">
|
<b-col cols="12" lg="4" md="4" class="px-0">
|
||||||
<b-btn active-class="btn-active" block variant="link" to="/community#all">
|
<b-btn to="community" active-class="btn-active" block variant="link">
|
||||||
<b-icon icon="people" class="mr-2" />
|
<b-icon icon="people" class="mr-2" />
|
||||||
{{ $t('community.community') }}
|
{{ $t('community.community') }}
|
||||||
</b-btn>
|
</b-btn>
|
||||||
|
|||||||
@ -1,76 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="contribution-info d-none d-lg-block">
|
<div class="contribution-info d-none d-lg-block">
|
||||||
<div v-if="hash === '#my'">
|
<slot :name="$route.params.tab" />
|
||||||
<h4 class="alert-heading">{{ $t('community.myContributions') }}</h4>
|
|
||||||
<p>
|
|
||||||
{{ $t('contribution.alert.myContributionNoteList') }}
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<b-icon icon="bell-fill" variant="primary"></b-icon>
|
|
||||||
{{ $t('contribution.alert.pending') }}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<b-icon icon="question-square" variant="warning"></b-icon>
|
|
||||||
{{ $t('contribution.alert.in_progress') }}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<b-icon icon="check" variant="success"></b-icon>
|
|
||||||
{{ $t('contribution.alert.confirm') }}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<b-icon icon="x-circle" variant="warning"></b-icon>
|
|
||||||
{{ $t('contribution.alert.denied') }}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<b-icon icon="trash" variant="danger"></b-icon>
|
|
||||||
{{ $t('contribution.alert.deleted') }}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div v-if="hash === '#all'" show fade variant="secondary" class="text-dark">
|
|
||||||
<h4 class="alert-heading">{{ $t('navigation.community') }}</h4>
|
|
||||||
<p>
|
|
||||||
{{ $t('contribution.alert.communityNoteList') }}
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<b-icon icon="bell-fill" variant="primary"></b-icon>
|
|
||||||
{{ $t('contribution.alert.pending') }}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<b-icon icon="question-square" variant="warning"></b-icon>
|
|
||||||
{{ $t('contribution.alert.in_progress') }}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<b-icon icon="check" variant="success"></b-icon>
|
|
||||||
{{ $t('contribution.alert.confirm') }}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<b-icon icon="x-circle" variant="warning"></b-icon>
|
|
||||||
{{ $t('contribution.alert.denied') }}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div v-if="hash === '#edit'" show fade variant="secondary" class="text-dark">
|
|
||||||
<div>
|
|
||||||
<h3>{{ $t('contribution.formText.yourContribution') }}</h3>
|
|
||||||
{{ $t('contribution.formText.bringYourTalentsTo') }}
|
|
||||||
|
|
||||||
<div class="my-3">
|
|
||||||
<b>{{ $t('contribution.formText.describeYourCommunity') }}</b>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: 'ContributionInfo',
|
name: 'ContributionInfo',
|
||||||
computed: {
|
|
||||||
hash() {
|
|
||||||
return this.$route.hash
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -121,11 +121,7 @@
|
|||||||
</b-col>
|
</b-col>
|
||||||
<!-- Right Side Mobil -->
|
<!-- Right Side Mobil -->
|
||||||
<b-col class="d-block d-lg-none">
|
<b-col class="d-block d-lg-none">
|
||||||
<right-side
|
<right-side>
|
||||||
:transactions="transactions"
|
|
||||||
:transactionCount="transactionCount"
|
|
||||||
:transactionLinkCount="transactionLinkCount"
|
|
||||||
>
|
|
||||||
<template #transactions>
|
<template #transactions>
|
||||||
<last-transactions
|
<last-transactions
|
||||||
:transactions="transactions"
|
:transactions="transactions"
|
||||||
@ -135,7 +131,7 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #community>
|
<template #community>
|
||||||
<contribution-info />
|
<community-template />
|
||||||
</template>
|
</template>
|
||||||
<template #empty />
|
<template #empty />
|
||||||
</right-side>
|
</right-side>
|
||||||
@ -162,11 +158,7 @@
|
|||||||
</b-col>
|
</b-col>
|
||||||
<!-- RightSide Desktop -->
|
<!-- RightSide Desktop -->
|
||||||
<b-col cols="3" class="d-none d-lg-block">
|
<b-col cols="3" class="d-none d-lg-block">
|
||||||
<right-side
|
<right-side>
|
||||||
:transactions="transactions"
|
|
||||||
:transactionCount="transactionCount"
|
|
||||||
:transactionLinkCount="transactionLinkCount"
|
|
||||||
>
|
|
||||||
<template #transactions>
|
<template #transactions>
|
||||||
<last-transactions
|
<last-transactions
|
||||||
:transactions="transactions"
|
:transactions="transactions"
|
||||||
@ -175,10 +167,10 @@
|
|||||||
@set-tunneled-email="setTunneledEmail"
|
@set-tunneled-email="setTunneledEmail"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #community>
|
|
||||||
<contribution-info />
|
|
||||||
</template>
|
|
||||||
<template #empty />
|
<template #empty />
|
||||||
|
<template #community>
|
||||||
|
<community-template />
|
||||||
|
</template>
|
||||||
</right-side>
|
</right-side>
|
||||||
</b-col>
|
</b-col>
|
||||||
</b-row>
|
</b-row>
|
||||||
@ -194,6 +186,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import ContentHeader from '@/layouts/templates/ContentHeader.vue'
|
import ContentHeader from '@/layouts/templates/ContentHeader.vue'
|
||||||
|
import CommunityTemplate from '@/layouts/templates/CommunityTemplate.vue'
|
||||||
import Breadcrumb from '@/components/Breadcrumb/breadcrumb.vue'
|
import Breadcrumb from '@/components/Breadcrumb/breadcrumb.vue'
|
||||||
import RightSide from '@/layouts/templates/RightSide.vue'
|
import RightSide from '@/layouts/templates/RightSide.vue'
|
||||||
import SkeletonOverview from '@/components/skeleton/Overview.vue'
|
import SkeletonOverview from '@/components/skeleton/Overview.vue'
|
||||||
@ -211,7 +204,6 @@ import GdtAmount from '@/components/Template/ContentHeader/GdtAmount.vue'
|
|||||||
import CommunityMember from '@/components/Template/ContentHeader/CommunityMember.vue'
|
import CommunityMember from '@/components/Template/ContentHeader/CommunityMember.vue'
|
||||||
import NavCommunity from '@/components/Template/ContentHeader/NavCommunity.vue'
|
import NavCommunity from '@/components/Template/ContentHeader/NavCommunity.vue'
|
||||||
import LastTransactions from '@/components/Template/RightSide/LastTransactions.vue'
|
import LastTransactions from '@/components/Template/RightSide/LastTransactions.vue'
|
||||||
import ContributionInfo from '@/components/Template/RightSide/ContributionInfo.vue'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DashboardLayout',
|
name: 'DashboardLayout',
|
||||||
@ -231,7 +223,7 @@ export default {
|
|||||||
CommunityMember,
|
CommunityMember,
|
||||||
NavCommunity,
|
NavCommunity,
|
||||||
LastTransactions,
|
LastTransactions,
|
||||||
ContributionInfo,
|
CommunityTemplate,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import ContributionInfo from './ContributionInfo'
|
import CommunityTemplate from './CommunityTemplate'
|
||||||
|
|
||||||
const localVue = global.localVue
|
const localVue = global.localVue
|
||||||
|
|
||||||
@ -10,15 +10,17 @@ const mocks = {
|
|||||||
$t: jest.fn((t) => t),
|
$t: jest.fn((t) => t),
|
||||||
$d: jest.fn((d) => d),
|
$d: jest.fn((d) => d),
|
||||||
$route: {
|
$route: {
|
||||||
hash: '',
|
params: {
|
||||||
|
tab: 'contribute',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('ContributionInfo', () => {
|
describe('CommunityTemplate', () => {
|
||||||
let wrapper
|
let wrapper
|
||||||
|
|
||||||
const Wrapper = () => {
|
const Wrapper = () => {
|
||||||
return mount(ContributionInfo, { localVue, mocks })
|
return mount(CommunityTemplate, { localVue, mocks })
|
||||||
}
|
}
|
||||||
describe('mount', () => {
|
describe('mount', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -29,9 +31,9 @@ describe('ContributionInfo', () => {
|
|||||||
expect(wrapper.findComponent({ name: 'ContributionInfo' }).exists()).toBe(true)
|
expect(wrapper.findComponent({ name: 'ContributionInfo' }).exists()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('mounted with hash #my', () => {
|
describe('mounted with parameter contributions', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mocks.$route.hash = '#my'
|
mocks.$route.params.tab = 'contributions'
|
||||||
})
|
})
|
||||||
|
|
||||||
it('has a header related to "my contribitions"', () => {
|
it('has a header related to "my contribitions"', () => {
|
||||||
@ -59,9 +61,9 @@ describe('ContributionInfo', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('mounted with hash #all', () => {
|
describe('mounted with parameter community', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mocks.$route.hash = '#all'
|
mocks.$route.params.tab = 'community'
|
||||||
})
|
})
|
||||||
|
|
||||||
it('has a header related to "the community"', () => {
|
it('has a header related to "the community"', () => {
|
||||||
@ -89,9 +91,9 @@ describe('ContributionInfo', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('mounted with hash #edit', () => {
|
describe('mounted with parameter contribute', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mocks.$route.hash = '#edit'
|
mocks.$route.params.tab = 'contribute'
|
||||||
})
|
})
|
||||||
|
|
||||||
it('has a header related to "the community"', () => {
|
it('has a header related to "the community"', () => {
|
||||||
82
frontend/src/layouts/templates/CommunityTemplate.vue
Normal file
82
frontend/src/layouts/templates/CommunityTemplate.vue
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
<template>
|
||||||
|
<contribution-info>
|
||||||
|
<template #contribute>
|
||||||
|
<div show fade variant="secondary" class="text-dark">
|
||||||
|
<div>
|
||||||
|
<h3>{{ $t('contribution.formText.yourContribution') }}</h3>
|
||||||
|
{{ $t('contribution.formText.bringYourTalentsTo') }}
|
||||||
|
|
||||||
|
<div class="my-3">
|
||||||
|
<b>{{ $t('contribution.formText.describeYourCommunity') }}</b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #contributions>
|
||||||
|
<div show fade variant="secondary" class="text-dark">
|
||||||
|
<h4 class="alert-heading">{{ $t('community.myContributions') }}</h4>
|
||||||
|
<p>
|
||||||
|
{{ $t('contribution.alert.myContributionNoteList') }}
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b-icon icon="bell-fill" variant="primary"></b-icon>
|
||||||
|
{{ $t('contribution.alert.pending') }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b-icon icon="question-square" variant="warning"></b-icon>
|
||||||
|
{{ $t('contribution.alert.in_progress') }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b-icon icon="check" variant="success"></b-icon>
|
||||||
|
{{ $t('contribution.alert.confirm') }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b-icon icon="x-circle" variant="warning"></b-icon>
|
||||||
|
{{ $t('contribution.alert.denied') }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b-icon icon="trash" variant="danger"></b-icon>
|
||||||
|
{{ $t('contribution.alert.deleted') }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #community>
|
||||||
|
<div show fade variant="secondary" class="text-dark">
|
||||||
|
<h4 class="alert-heading">{{ $t('navigation.community') }}</h4>
|
||||||
|
<p>
|
||||||
|
{{ $t('contribution.alert.communityNoteList') }}
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<b-icon icon="bell-fill" variant="primary"></b-icon>
|
||||||
|
{{ $t('contribution.alert.pending') }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b-icon icon="question-square" variant="warning"></b-icon>
|
||||||
|
{{ $t('contribution.alert.in_progress') }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b-icon icon="check" variant="success"></b-icon>
|
||||||
|
{{ $t('contribution.alert.confirm') }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b-icon icon="x-circle" variant="warning"></b-icon>
|
||||||
|
{{ $t('contribution.alert.denied') }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</contribution-info>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import ContributionInfo from '@/components/Template/RightSide/ContributionInfo.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CommunityTemplate',
|
||||||
|
components: {
|
||||||
|
ContributionInfo,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -9,7 +9,7 @@ export default {
|
|||||||
name: 'ContentHeader',
|
name: 'ContentHeader',
|
||||||
computed: {
|
computed: {
|
||||||
path() {
|
path() {
|
||||||
return this.$route.path.replace(/^\//, '')
|
return this.$route.path.replace(/^\/(.+?)(\/.+)?$/, '$1')
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export default {
|
|||||||
name: 'RightSide',
|
name: 'RightSide',
|
||||||
computed: {
|
computed: {
|
||||||
name() {
|
name() {
|
||||||
switch (this.$route.path.replace(/^\//, '')) {
|
switch (this.$route.path.replace(/^\/(.+?)(\/.+)?$/, '$1')) {
|
||||||
case 'settings':
|
case 'settings':
|
||||||
return 'empty'
|
return 'empty'
|
||||||
case 'community':
|
case 'community':
|
||||||
|
|||||||
@ -215,7 +215,9 @@ describe('Community', () => {
|
|||||||
push: routerPushMock,
|
push: routerPushMock,
|
||||||
},
|
},
|
||||||
$route: {
|
$route: {
|
||||||
hash: '#edit',
|
params: {
|
||||||
|
tab: 'contribute',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -260,7 +262,11 @@ describe('Community', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('check for correct tabIndex if state is "IN_PROGRESS" or not', () => {
|
it('check for correct tabIndex if state is "IN_PROGRESS" or not', () => {
|
||||||
expect(routerPushMock).toBeCalledWith({ path: '/community#my' })
|
expect(routerPushMock).toBeCalledWith({ params: { tab: 'contributions' } })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sets tab index to 1', () => {
|
||||||
|
expect(wrapper.vm.tabIndex).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('toasts an info', () => {
|
it('toasts an info', () => {
|
||||||
@ -268,16 +274,6 @@ describe('Community', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('API calls after creation', () => {
|
|
||||||
it('has a DIV .community-page', () => {
|
|
||||||
expect(wrapper.find('div.community-page').exists()).toBe(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('emits update transactions', () => {
|
|
||||||
expect(wrapper.emitted('update-transactions')).toEqual([[0]])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('save contrubtion', () => {
|
describe('save contrubtion', () => {
|
||||||
describe('with error', () => {
|
describe('with error', () => {
|
||||||
const now = new Date().toISOString()
|
const now = new Date().toISOString()
|
||||||
@ -491,6 +487,10 @@ describe('Community', () => {
|
|||||||
it('sets tab index back to 0', () => {
|
it('sets tab index back to 0', () => {
|
||||||
expect(wrapper.vm.tabIndex).toBe(0)
|
expect(wrapper.vm.tabIndex).toBe(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('pushes contribute parameter to router', () => {
|
||||||
|
expect(routerPushMock).toBeCalledWith({ params: { tab: 'contribute' } })
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('update list all contributions', () => {
|
describe('update list all contributions', () => {
|
||||||
|
|||||||
@ -64,6 +64,8 @@ import ContributionList from '@/components/Contributions/ContributionList.vue'
|
|||||||
import { createContribution, updateContribution, deleteContribution } from '@/graphql/mutations'
|
import { createContribution, updateContribution, deleteContribution } from '@/graphql/mutations'
|
||||||
import { listContributions, listAllContributions, openCreations } from '@/graphql/queries'
|
import { listContributions, listAllContributions, openCreations } from '@/graphql/queries'
|
||||||
|
|
||||||
|
const COMMUNITY_TABS = ['contribute', 'contributions', 'community']
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Community',
|
name: 'Community',
|
||||||
components: {
|
components: {
|
||||||
@ -73,8 +75,6 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
hashLink: '',
|
|
||||||
tabLinkHashes: ['#edit', '#my', '#all'],
|
|
||||||
tabIndex: 0,
|
tabIndex: 0,
|
||||||
items: [],
|
items: [],
|
||||||
itemsAll: [],
|
itemsAll: [],
|
||||||
@ -97,10 +97,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$nextTick(() => {
|
this.updateTabIndex()
|
||||||
this.tabIndex = this.tabLinkHashes.findIndex((hashLink) => hashLink === this.$route.hash)
|
|
||||||
this.hashLink = this.$route.hash
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
apollo: {
|
apollo: {
|
||||||
OpenCreations: {
|
OpenCreations: {
|
||||||
@ -122,13 +119,13 @@ export default {
|
|||||||
query() {
|
query() {
|
||||||
return listAllContributions
|
return listAllContributions
|
||||||
},
|
},
|
||||||
fetchPolicy: 'network-only',
|
|
||||||
variables() {
|
variables() {
|
||||||
return {
|
return {
|
||||||
currentPage: this.currentPageAll,
|
currentPage: this.currentPageAll,
|
||||||
pageSize: this.pageSizeAll,
|
pageSize: this.pageSizeAll,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
fetchPolicy: 'no-cache',
|
||||||
update({ listAllContributions }) {
|
update({ listAllContributions }) {
|
||||||
this.contributionCountAll = listAllContributions.contributionCount
|
this.contributionCountAll = listAllContributions.contributionCount
|
||||||
this.itemsAll = listAllContributions.contributionList
|
this.itemsAll = listAllContributions.contributionList
|
||||||
@ -153,9 +150,8 @@ export default {
|
|||||||
this.items = listContributions.contributionList
|
this.items = listContributions.contributionList
|
||||||
if (this.items.find((item) => item.state === 'IN_PROGRESS')) {
|
if (this.items.find((item) => item.state === 'IN_PROGRESS')) {
|
||||||
this.tabIndex = 1
|
this.tabIndex = 1
|
||||||
if (this.$route.hash !== '#my') {
|
if (this.$route.params.tab !== 'contributions')
|
||||||
this.$router.push({ path: '/community#my' })
|
this.$router.push({ params: { tab: 'contributions' } })
|
||||||
}
|
|
||||||
this.toastInfo(this.$t('contribution.alert.answerQuestionToast'))
|
this.toastInfo(this.$t('contribution.alert.answerQuestionToast'))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -165,21 +161,8 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
$route(to, from) {
|
'$route.params.tab'() {
|
||||||
this.tabIndex = this.tabLinkHashes.findIndex((hashLink) => hashLink === to.hash)
|
this.updateTabIndex()
|
||||||
this.hashLink = to.hash
|
|
||||||
this.closeAllOpenCollapse()
|
|
||||||
},
|
|
||||||
tabIndex(num) {
|
|
||||||
if (num !== 0) {
|
|
||||||
this.form = {
|
|
||||||
id: null,
|
|
||||||
date: new Date(),
|
|
||||||
memo: '',
|
|
||||||
hours: 0,
|
|
||||||
amount: '',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -211,6 +194,11 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
updateTabIndex() {
|
||||||
|
const index = COMMUNITY_TABS.indexOf(this.$route.params.tab)
|
||||||
|
this.tabIndex = index > -1 ? index : 0
|
||||||
|
this.closeAllOpenCollapse()
|
||||||
|
},
|
||||||
closeAllOpenCollapse() {
|
closeAllOpenCollapse() {
|
||||||
this.$el.querySelectorAll('.collapse.show').forEach((value) => {
|
this.$el.querySelectorAll('.collapse.show').forEach((value) => {
|
||||||
this.$root.$emit('bv::toggle::collapse', value.id)
|
this.$root.$emit('bv::toggle::collapse', value.id)
|
||||||
@ -294,8 +282,8 @@ export default {
|
|||||||
this.form.amount = item.amount
|
this.form.amount = item.amount
|
||||||
this.form.hours = item.amount / 20
|
this.form.hours = item.amount / 20
|
||||||
this.updateAmount = item.amount
|
this.updateAmount = item.amount
|
||||||
this.$router.push({ path: '#edit' })
|
|
||||||
this.tabIndex = 0
|
this.tabIndex = 0
|
||||||
|
this.$router.push({ params: { tab: 'contribute' } })
|
||||||
},
|
},
|
||||||
updateTransactions(pagination) {
|
updateTransactions(pagination) {
|
||||||
this.$emit('update-transactions', pagination)
|
this.$emit('update-transactions', pagination)
|
||||||
@ -304,11 +292,6 @@ export default {
|
|||||||
this.items.find((item) => item.id === id).state = 'PENDING'
|
this.items.find((item) => item.id === id).state = 'PENDING'
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
created() {
|
|
||||||
this.updateTransactions(0)
|
|
||||||
this.tabIndex = 0
|
|
||||||
this.$router.push({ path: '/community#edit' })
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@ -49,8 +49,8 @@ describe('router', () => {
|
|||||||
expect(routes.find((r) => r.path === '/').redirect()).toEqual({ path: '/login' })
|
expect(routes.find((r) => r.path === '/').redirect()).toEqual({ path: '/login' })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('has sixteen routes defined', () => {
|
it('has 19 routes defined', () => {
|
||||||
expect(routes).toHaveLength(18)
|
expect(routes).toHaveLength(19)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('overview', () => {
|
describe('overview', () => {
|
||||||
@ -75,7 +75,19 @@ describe('router', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('community', () => {
|
describe('community without tab parameter', () => {
|
||||||
|
it('requires authorization', () => {
|
||||||
|
expect(routes.find((r) => r.path === '/community').meta.requiresAuth).toBeTruthy()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('redirects to contribute tab', async () => {
|
||||||
|
expect(routes.find((r) => r.path === '/community').redirect()).toEqual({
|
||||||
|
path: '/community/contribute',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('community with tab parameter', () => {
|
||||||
it('requires authorization', () => {
|
it('requires authorization', () => {
|
||||||
expect(routes.find((r) => r.path === '/community').meta.requiresAuth).toBeTruthy()
|
expect(routes.find((r) => r.path === '/community').meta.requiresAuth).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|||||||
@ -58,6 +58,17 @@ const routes = [
|
|||||||
requiresAuth: true,
|
requiresAuth: true,
|
||||||
pageTitle: 'community',
|
pageTitle: 'community',
|
||||||
},
|
},
|
||||||
|
redirect: (to) => {
|
||||||
|
return { path: '/community/contribute' }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/community/:tab',
|
||||||
|
component: () => import('@/pages/Community.vue'),
|
||||||
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
|
pageTitle: 'community',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/information',
|
path: '/information',
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user