Merge branch 'master' into separate-admin-interface-workflow

This commit is contained in:
mahula 2023-02-23 12:10:33 +01:00 committed by GitHub
commit cbbac1afdb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1005 additions and 744 deletions

View File

@ -1,9 +1,10 @@
import jwt from 'jsonwebtoken'
import CONFIG from '@/config/'
import { CustomJwtPayload } from './CustomJwtPayload'
import LogError from '@/server/LogError'
export const decode = (token: string): CustomJwtPayload | null => {
if (!token) throw new Error('401 Unauthorized')
if (!token) throw new LogError('401 Unauthorized')
try {
return <CustomJwtPayload>jwt.verify(token, CONFIG.JWT_SECRET)
} catch (err) {

View File

@ -7,6 +7,7 @@ import { ROLE_UNAUTHORIZED, ROLE_USER, ROLE_ADMIN } from '@/auth/ROLES'
import { RIGHTS } from '@/auth/RIGHTS'
import { INALIENABLE_RIGHTS } from '@/auth/INALIENABLE_RIGHTS'
import { User } from '@entity/User'
import LogError from '@/server/LogError'
const isAuthorized: AuthChecker<any> = async ({ context }, rights) => {
context.role = ROLE_UNAUTHORIZED // unauthorized user
@ -17,13 +18,13 @@ const isAuthorized: AuthChecker<any> = async ({ context }, rights) => {
// Do we have a token?
if (!context.token) {
throw new Error('401 Unauthorized')
throw new LogError('401 Unauthorized')
}
// Decode the token
const decoded = decode(context.token)
if (!decoded) {
throw new Error('403.13 - Client certificate revoked')
throw new LogError('403.13 - Client certificate revoked')
}
// Set context gradidoID
context.gradidoID = decoded.gradidoID
@ -39,13 +40,13 @@ const isAuthorized: AuthChecker<any> = async ({ context }, rights) => {
context.role = user.isAdmin ? ROLE_ADMIN : ROLE_USER
} catch {
// in case the database query fails (user deleted)
throw new Error('401 Unauthorized')
throw new LogError('401 Unauthorized')
}
// check for correct rights
const missingRights = (<RIGHTS[]>rights).filter((right) => !context.role.hasRight(right))
if (missingRights.length !== 0) {
throw new Error('401 Unauthorized')
throw new LogError('401 Unauthorized')
}
// set new header token

View File

@ -257,17 +257,13 @@ describe('Contribution Links', () => {
}),
).resolves.toEqual(
expect.objectContaining({
errors: [
new GraphQLError('Start-Date is not initialized. A Start-Date must be set!'),
],
errors: [new GraphQLError('A Start-Date must be set')],
}),
)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'Start-Date is not initialized. A Start-Date must be set!',
)
expect(logger.error).toBeCalledWith('A Start-Date must be set')
})
it('returns an error if missing endDate', async () => {
@ -282,15 +278,13 @@ describe('Contribution Links', () => {
}),
).resolves.toEqual(
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', () => {
expect(logger.error).toBeCalledWith(
'End-Date is not initialized. An End-Date must be set!',
)
expect(logger.error).toBeCalledWith('An End-Date must be set')
})
it('returns an error if endDate is before startDate', async () => {
@ -307,7 +301,7 @@ describe('Contribution Links', () => {
).resolves.toEqual(
expect.objectContaining({
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', () => {
expect(logger.error).toBeCalledWith(
`The value of validFrom must before or equals the validTo!`,
`The value of validFrom must before or equals the validTo`,
)
})

View File

@ -33,10 +33,14 @@ export class ContributionMessageResolver {
try {
const contribution = await DbContribution.findOne({ id: contributionId })
if (!contribution) {
throw new Error('Contribution not found')
throw new LogError('Contribution not found', contributionId)
}
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

View File

@ -23,7 +23,7 @@ import {
import {
listAllContributions,
listContributions,
listUnconfirmedContributions,
adminListAllContributions,
} from '@/seeds/graphql/queries'
import { sendContributionConfirmedEmail } from '@/emails/sendEmailVariants'
import {
@ -46,9 +46,10 @@ import { EventProtocolType } from '@/event/EventProtocolType'
import { logger, i18n as localization } from '@test/testSetup'
import { UserInputError } from 'apollo-server-express'
import { raeuberHotzenplotz } from '@/seeds/users/raeuber-hotzenplotz'
import { UnconfirmedContribution } from '../model/UnconfirmedContribution'
import { ContributionListResult } from '../model/Contribution'
import { ContributionStatus } from '../enum/ContributionStatus'
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
import { ContributionListResult } from '@model/Contribution'
import { ContributionStatus } from '@enum/ContributionStatus'
import { Order } from '@enum/Order'
// mock account activation email to avoid console spam
jest.mock('@/emails/sendEmailVariants', () => {
@ -245,8 +246,8 @@ describe('ContributionResolver', () => {
it('logs the error found', () => {
expect(logger.error).toBeCalledWith(
'No information for available creations with the given creationDate=',
'Invalid Date',
'No information for available creations for the given date',
expect.any(Date),
)
})
@ -268,8 +269,8 @@ describe('ContributionResolver', () => {
it('logs the error found', () => {
expect(logger.error).toBeCalledWith(
'No information for available creations with the given creationDate=',
'Invalid Date',
'No information for available creations for the given date',
expect.any(Date),
)
})
})
@ -526,14 +527,16 @@ describe('ContributionResolver', () => {
})
expect(errorObjects).toEqual([
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', () => {
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', () => {
beforeAll(async () => {
jest.clearAllMocks()
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
@ -886,7 +890,6 @@ describe('ContributionResolver', () => {
})
it('returns an error', async () => {
jest.clearAllMocks()
const { errors: errorObjects }: { errors: [GraphQLError] } = await mutate({
mutation: deleteContribution,
variables: {
@ -909,6 +912,7 @@ describe('ContributionResolver', () => {
describe('User deletes own contribution', () => {
beforeAll(async () => {
jest.clearAllMocks()
await mutate({
mutation: login,
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', () => {
it('returns an error', async () => {
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', () => {
it('returns an error', async () => {
await expect(
@ -2008,8 +1984,8 @@ describe('ContributionResolver', () => {
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'No information for available creations with the given creationDate=',
new Date(variables.creationDate).toString(),
'No information for available creations for the given date',
new Date(variables.creationDate),
)
})
})
@ -2033,8 +2009,8 @@ describe('ContributionResolver', () => {
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'No information for available creations with the given creationDate=',
new Date(variables.creationDate).toString(),
'No information for available creations for the given date',
new Date(variables.creationDate),
)
})
})
@ -2049,7 +2025,7 @@ describe('ContributionResolver', () => {
expect.objectContaining({
errors: [
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', () => {
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({
errors: [
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', () => {
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({
errors: [
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', () => {
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('creation id does not exist', () => {
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',
}),
]),
})
})
})
})
})

View File

@ -1,6 +1,6 @@
import Decimal from 'decimal.js-light'
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 { ContributionMessage } from '@entity/ContributionMessage'
@ -30,12 +30,11 @@ import { backendLogger as logger } from '@/server/logger'
import {
getCreationDates,
getUserCreation,
getUserCreations,
validateContribution,
updateCreations,
isValidDateString,
} 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 {
EVENT_CONTRIBUTION_CREATE,
EVENT_CONTRIBUTION_DELETE,
@ -56,6 +55,7 @@ import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
import LogError from '@/server/LogError'
import { getLastTransaction } from './util/getLastTransaction'
import { findContributions } from './util/findContributions'
@Resolver()
export class ContributionResolver {
@ -168,25 +168,14 @@ export class ContributionResolver {
@Arg('statusFilter', () => [ContributionStatus], { nullable: true })
statusFilter?: ContributionStatus[],
): Promise<ContributionListResult> {
const where: {
contributionStatus?: FindOperator<string> | null
} = {}
const [dbContributions, count] = await findContributions(
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(
count,
dbContributions.map((contribution) => new Contribution(contribution, contribution.user)),
@ -425,40 +414,25 @@ export class ContributionResolver {
}
@Authorized([RIGHTS.LIST_UNCONFIRMED_CONTRIBUTIONS])
@Query(() => [UnconfirmedContribution])
async listUnconfirmedContributions(@Ctx() context: Context): Promise<UnconfirmedContribution[]> {
const clientTimezoneOffset = getClientTimezoneOffset(context)
const contributions = await getConnection()
.createQueryBuilder()
.select('c')
.from(DbContribution, 'c')
.leftJoinAndSelect('c.messages', 'm')
.where({ confirmedAt: IsNull() })
.andWhere({ deniedAt: IsNull() })
.getMany()
if (contributions.length === 0) {
return []
}
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,
@Query(() => ContributionListResult) // [UnconfirmedContribution]
async adminListAllContributions(
@Args()
{ currentPage = 1, pageSize = 3, order = Order.DESC }: Paginated,
@Arg('statusFilter', () => [ContributionStatus], { nullable: true })
statusFilter?: ContributionStatus[],
): Promise<ContributionListResult> {
const [dbContributions, count] = await findContributions(
order,
currentPage,
pageSize,
true,
statusFilter,
)
return new ContributionListResult(
count,
dbContributions.map((contribution) => new Contribution(contribution, contribution.user)),
)
})
}
@Authorized([RIGHTS.ADMIN_DELETE_CONTRIBUTION])

View File

@ -8,6 +8,7 @@ import { Context, getUser } from '@/server/context'
import CONFIG from '@/config'
import { apiGet, apiPost } from '@/apis/HttpRequest'
import { RIGHTS } from '@/auth/RIGHTS'
import LogError from '@/server/LogError'
@Resolver()
export class GdtResolver {
@ -25,11 +26,11 @@ export class GdtResolver {
`${CONFIG.GDT_API_URL}/GdtEntries/listPerEmailApi/${userEntity.emailContact.email}/${currentPage}/${pageSize}/${order}`,
)
if (!resultGDT.success) {
throw new Error(resultGDT.data)
throw new LogError(resultGDT.data)
}
return new GdtEntryList(resultGDT.data)
} 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,
})
if (!resultGDTSum.success) {
throw new Error('Call not successful')
throw new LogError('Call not successful')
}
return Number(resultGDTSum.data.sum) || 0
} catch (err) {
@ -59,7 +60,7 @@ export class GdtResolver {
// load user
const resultPID = await apiGet(`${CONFIG.GDT_API_URL}/publishers/checkPidApi/${pid}`)
if (!resultPID.success) {
throw new Error(resultPID.data)
throw new LogError(resultPID.data)
}
return resultPID.data.pid
}

View File

@ -53,6 +53,21 @@ afterAll(async () => {
describe('TransactionLinkResolver', () => {
describe('createTransactionLink', () => {
describe('unauthenticated', () => {
it('throws an error', async () => {
jest.clearAllMocks()
resetToken()
await expect(
mutate({ mutation: createTransactionLink, variables: { amount: 0, memo: 'Test' } }),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
describe('authenticated', () => {
beforeAll(async () => {
await mutate({
mutation: login,
@ -114,6 +129,7 @@ describe('TransactionLinkResolver', () => {
expect(logger.error).toBeCalledWith('User has not enough GDD', expect.any(Number))
})
})
})
describe('redeemTransactionLink', () => {
afterAll(async () => {
@ -121,6 +137,21 @@ describe('TransactionLinkResolver', () => {
resetToken()
})
describe('unauthenticated', () => {
it('throws an error', async () => {
jest.clearAllMocks()
resetToken()
await expect(
mutate({ mutation: redeemTransactionLink, variables: { code: 'CL-123456' } }),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
describe('authenticated', () => {
describe('contributionLink', () => {
describe('input not valid', () => {
beforeAll(async () => {
@ -362,7 +393,7 @@ describe('TransactionLinkResolver', () => {
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error(
'The amount (5 GDD) to be created exceeds the amount (0 GDD) still available for this month.',
'The amount to be created exceeds the amount still available for this month',
),
)
})
@ -478,6 +509,7 @@ describe('TransactionLinkResolver', () => {
})
})
})
})
describe('listTransactionLinksAdmin', () => {
const variables = {

View File

@ -86,8 +86,8 @@ export class TransactionLinkResolver {
transactionLink.code = transactionLinkCode(createdDate)
transactionLink.createdAt = createdDate
transactionLink.validUntil = validUntil
await DbTransactionLink.save(transactionLink).catch(() => {
throw new Error('Unable to save transaction link')
await DbTransactionLink.save(transactionLink).catch((e) => {
throw new LogError('Unable to save transaction link', e)
})
return new TransactionLink(transactionLink, new User(user))
@ -103,19 +103,23 @@ export class TransactionLinkResolver {
const transactionLink = await DbTransactionLink.findOne({ id })
if (!transactionLink) {
throw new Error('Transaction Link not found!')
throw new LogError('Transaction link not found', 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) {
throw new Error('Transaction Link already redeemed!')
throw new LogError('Transaction link already redeemed', transactionLink.redeemedBy)
}
await transactionLink.softRemove().catch(() => {
throw new Error('Transaction Link could not be deleted!')
await transactionLink.softRemove().catch((e) => {
throw new LogError('Transaction link could not be deleted', e)
})
return true
@ -312,18 +316,18 @@ export class TransactionLinkResolver {
)
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,
// since the program might wait a while till it is ready to proceed
// writing the transaction.
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) {
throw new Error('Transaction Link already redeemed.')
throw new LogError('Transaction link already redeemed', transactionLink.redeemedBy)
}
await executeTransaction(

View File

@ -1,3 +1,4 @@
import LogError from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger'
import { getConnection } from '@dbTools/typeorm'
import { Contribution } from '@entity/Contribution'
@ -19,19 +20,14 @@ export const validateContribution = (
const index = getCreationIndex(creationDate.getMonth(), timezoneOffset)
if (index < 0) {
logger.error(
'No information for available creations with the given creationDate=',
creationDate.toString(),
)
throw new Error('No information for available creations for the given date')
throw new LogError('No information for available creations for the given date', creationDate)
}
if (amount.greaterThan(creations[index].toString())) {
logger.error(
`The amount (${amount} GDD) to be created exceeds the amount (${creations[index]} GDD) still available for this month.`,
)
throw new Error(
`The amount (${amount} GDD) to be created exceeds the amount (${creations[index]} GDD) still available for this month.`,
throw new LogError(
'The amount to be created exceeds the amount still available for this month',
amount,
creations[index],
)
}
}
@ -126,19 +122,16 @@ export const isStartEndDateValid = (
endDate: string | null | undefined,
): void => {
if (!startDate) {
logger.error('Start-Date is not initialized. A Start-Date must be set!')
throw new Error('Start-Date is not initialized. A Start-Date must be set!')
throw new LogError('A Start-Date must be set')
}
if (!endDate) {
logger.error('End-Date is not initialized. An End-Date must be set!')
throw new Error('End-Date is not initialized. An End-Date must be set!')
throw new LogError('An End-Date must be set')
}
// check if endDate is before startDate
if (new Date(endDate).getTime() - new Date(startDate).getTime() < 0) {
logger.error(`The value of validFrom must before or equals the validTo!`)
throw new Error(`The value of validFrom must before or equals the validTo!`)
throw new LogError(`The value of validFrom must before or equals the validTo`)
}
}
@ -150,7 +143,7 @@ export const updateCreations = (
const index = getCreationIndex(contribution.contributionDate.getMonth(), timezoneOffset)
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())
return creations

View 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,
})

View File

@ -1,4 +1,5 @@
import CONFIG from '@/config'
import LogError from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger'
import { User } from '@entity/User'
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 configLoginServerKey = Buffer.from(CONFIG.LOGIN_SERVER_KEY, 'hex')
if (configLoginServerKey.length !== sodium.crypto_shorthash_KEYBYTES) {
logger.error(
`ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`,
)
throw new Error(
`ServerKey has an invalid size. The size must be ${sodium.crypto_shorthash_KEYBYTES} bytes.`,
throw new LogError(
'ServerKey has an invalid size',
configLoginServerKey.length,
sodium.crypto_shorthash_KEYBYTES,
)
}
@ -52,20 +52,13 @@ export const SecretKeyCryptographyCreateKey = (salt: string, password: string):
export const getUserCryptographicSalt = (dbUser: User): string => {
switch (dbUser.passwordEncryptionType) {
case PasswordEncryptionType.NO_PASSWORD: {
logger.error('Password not set for user ' + dbUser.id)
throw new Error('Password not set for user ' + dbUser.id) // user has no password
}
case PasswordEncryptionType.EMAIL: {
case PasswordEncryptionType.NO_PASSWORD:
throw new LogError('User has no password set', dbUser.id)
case PasswordEncryptionType.EMAIL:
return dbUser.emailContact.email
break
}
case PasswordEncryptionType.GRADIDO_ID: {
case PasswordEncryptionType.GRADIDO_ID:
return dbUser.gradidoID
break
}
default:
logger.error(`Unknown password encryption type: ${dbUser.passwordEncryptionType}`)
throw new Error(`Unknown password encryption type: ${dbUser.passwordEncryptionType}`)
throw new LogError('Unknown password encryption type', dbUser.passwordEncryptionType)
}
}

View File

@ -204,18 +204,35 @@ query ($currentPage: Int = 1, $pageSize: Int = 5, $order: Order = DESC, $statusF
`
// from admin interface
export const listUnconfirmedContributions = gql`
query {
listUnconfirmedContributions {
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
lastName
email
amount
memo
date
moderator
creation
createdAt
confirmedAt
confirmedBy
contributionDate
state
messagesCount
deniedAt
deniedBy
}
}
}
`

View File

@ -3,6 +3,7 @@ import { User as dbUser } from '@entity/User'
import { Transaction as dbTransaction } from '@entity/Transaction'
import Decimal from 'decimal.js-light'
import { ExpressContext } from 'apollo-server-express'
import LogError from './LogError'
export interface Context {
token: string | null
@ -35,7 +36,7 @@ const context = (args: ExpressContext): Context => {
export const getUser = (context: Context): dbUser => {
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 => {
@ -45,7 +46,7 @@ export const getClientTimezoneOffset = (context: Context): number => {
) {
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

View File

@ -1,6 +1,7 @@
import Decimal from 'decimal.js-light'
import CONFIG from '@/config'
import { Decay } from '@model/Decay'
import LogError from '@/server/LogError'
// TODO: externalize all those definitions and functions into an external decay library
@ -22,7 +23,7 @@ function calculateDecay(
const startBlockMs = startBlock.getTime()
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

View File

@ -1,11 +1,12 @@
import connection from '@/typeorm/connection'
import { getKlickTippUser } from '@/apis/KlicktippController'
import { User } from '@entity/User'
import LogError from '@/server/LogError'
export async function retrieveNotRegisteredEmails(): Promise<string[]> {
const con = await connection()
if (!con) {
throw new Error('No connection to database')
throw new LogError('No connection to database')
}
const users = await User.find({ relations: ['emailContact'] })
const notRegisteredUser = []

View File

@ -2,19 +2,19 @@
<div class="nav-community container">
<b-row class="nav-row">
<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" />
{{ $t('community.submitContribution') }}
</b-btn>
</b-col>
<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" />
{{ $t('community.myContributions') }}
</b-btn>
</b-col>
<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" />
{{ $t('community.community') }}
</b-btn>

View File

@ -1,76 +1,10 @@
<template>
<div class="contribution-info d-none d-lg-block">
<div v-if="hash === '#my'">
<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>
<slot :name="$route.params.tab" />
</div>
</template>
<script>
export default {
name: 'ContributionInfo',
computed: {
hash() {
return this.$route.hash
},
},
}
</script>

View File

@ -121,11 +121,7 @@
</b-col>
<!-- Right Side Mobil -->
<b-col class="d-block d-lg-none">
<right-side
:transactions="transactions"
:transactionCount="transactionCount"
:transactionLinkCount="transactionLinkCount"
>
<right-side>
<template #transactions>
<last-transactions
:transactions="transactions"
@ -135,7 +131,7 @@
/>
</template>
<template #community>
<contribution-info />
<community-template />
</template>
<template #empty />
</right-side>
@ -162,11 +158,7 @@
</b-col>
<!-- RightSide Desktop -->
<b-col cols="3" class="d-none d-lg-block">
<right-side
:transactions="transactions"
:transactionCount="transactionCount"
:transactionLinkCount="transactionLinkCount"
>
<right-side>
<template #transactions>
<last-transactions
:transactions="transactions"
@ -175,10 +167,10 @@
@set-tunneled-email="setTunneledEmail"
/>
</template>
<template #community>
<contribution-info />
</template>
<template #empty />
<template #community>
<community-template />
</template>
</right-side>
</b-col>
</b-row>
@ -194,6 +186,7 @@
</template>
<script>
import ContentHeader from '@/layouts/templates/ContentHeader.vue'
import CommunityTemplate from '@/layouts/templates/CommunityTemplate.vue'
import Breadcrumb from '@/components/Breadcrumb/breadcrumb.vue'
import RightSide from '@/layouts/templates/RightSide.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 NavCommunity from '@/components/Template/ContentHeader/NavCommunity.vue'
import LastTransactions from '@/components/Template/RightSide/LastTransactions.vue'
import ContributionInfo from '@/components/Template/RightSide/ContributionInfo.vue'
export default {
name: 'DashboardLayout',
@ -231,7 +223,7 @@ export default {
CommunityMember,
NavCommunity,
LastTransactions,
ContributionInfo,
CommunityTemplate,
},
data() {
return {

View File

@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils'
import ContributionInfo from './ContributionInfo'
import CommunityTemplate from './CommunityTemplate'
const localVue = global.localVue
@ -10,15 +10,17 @@ const mocks = {
$t: jest.fn((t) => t),
$d: jest.fn((d) => d),
$route: {
hash: '',
params: {
tab: 'contribute',
},
},
}
describe('ContributionInfo', () => {
describe('CommunityTemplate', () => {
let wrapper
const Wrapper = () => {
return mount(ContributionInfo, { localVue, mocks })
return mount(CommunityTemplate, { localVue, mocks })
}
describe('mount', () => {
beforeEach(() => {
@ -29,9 +31,9 @@ describe('ContributionInfo', () => {
expect(wrapper.findComponent({ name: 'ContributionInfo' }).exists()).toBe(true)
})
describe('mounted with hash #my', () => {
describe('mounted with parameter contributions', () => {
beforeEach(() => {
mocks.$route.hash = '#my'
mocks.$route.params.tab = 'contributions'
})
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(() => {
mocks.$route.hash = '#all'
mocks.$route.params.tab = '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(() => {
mocks.$route.hash = '#edit'
mocks.$route.params.tab = 'contribute'
})
it('has a header related to "the community"', () => {

View 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>

View File

@ -9,7 +9,7 @@ export default {
name: 'ContentHeader',
computed: {
path() {
return this.$route.path.replace(/^\//, '')
return this.$route.path.replace(/^\/(.+?)(\/.+)?$/, '$1')
},
},
}

View File

@ -10,7 +10,7 @@ export default {
name: 'RightSide',
computed: {
name() {
switch (this.$route.path.replace(/^\//, '')) {
switch (this.$route.path.replace(/^\/(.+?)(\/.+)?$/, '$1')) {
case 'settings':
return 'empty'
case 'community':

View File

@ -215,7 +215,9 @@ describe('Community', () => {
push: routerPushMock,
},
$route: {
hash: '#edit',
params: {
tab: 'contribute',
},
},
}
@ -260,7 +262,11 @@ describe('Community', () => {
})
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', () => {
@ -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('with error', () => {
const now = new Date().toISOString()
@ -491,6 +487,10 @@ describe('Community', () => {
it('sets tab index back to 0', () => {
expect(wrapper.vm.tabIndex).toBe(0)
})
it('pushes contribute parameter to router', () => {
expect(routerPushMock).toBeCalledWith({ params: { tab: 'contribute' } })
})
})
describe('update list all contributions', () => {

View File

@ -64,6 +64,8 @@ import ContributionList from '@/components/Contributions/ContributionList.vue'
import { createContribution, updateContribution, deleteContribution } from '@/graphql/mutations'
import { listContributions, listAllContributions, openCreations } from '@/graphql/queries'
const COMMUNITY_TABS = ['contribute', 'contributions', 'community']
export default {
name: 'Community',
components: {
@ -73,8 +75,6 @@ export default {
},
data() {
return {
hashLink: '',
tabLinkHashes: ['#edit', '#my', '#all'],
tabIndex: 0,
items: [],
itemsAll: [],
@ -97,10 +97,7 @@ export default {
}
},
mounted() {
this.$nextTick(() => {
this.tabIndex = this.tabLinkHashes.findIndex((hashLink) => hashLink === this.$route.hash)
this.hashLink = this.$route.hash
})
this.updateTabIndex()
},
apollo: {
OpenCreations: {
@ -122,13 +119,13 @@ export default {
query() {
return listAllContributions
},
fetchPolicy: 'network-only',
variables() {
return {
currentPage: this.currentPageAll,
pageSize: this.pageSizeAll,
}
},
fetchPolicy: 'no-cache',
update({ listAllContributions }) {
this.contributionCountAll = listAllContributions.contributionCount
this.itemsAll = listAllContributions.contributionList
@ -153,9 +150,8 @@ export default {
this.items = listContributions.contributionList
if (this.items.find((item) => item.state === 'IN_PROGRESS')) {
this.tabIndex = 1
if (this.$route.hash !== '#my') {
this.$router.push({ path: '/community#my' })
}
if (this.$route.params.tab !== 'contributions')
this.$router.push({ params: { tab: 'contributions' } })
this.toastInfo(this.$t('contribution.alert.answerQuestionToast'))
}
},
@ -165,21 +161,8 @@ export default {
},
},
watch: {
$route(to, from) {
this.tabIndex = this.tabLinkHashes.findIndex((hashLink) => hashLink === to.hash)
this.hashLink = to.hash
this.closeAllOpenCollapse()
},
tabIndex(num) {
if (num !== 0) {
this.form = {
id: null,
date: new Date(),
memo: '',
hours: 0,
amount: '',
}
}
'$route.params.tab'() {
this.updateTabIndex()
},
},
computed: {
@ -211,6 +194,11 @@ export default {
},
},
methods: {
updateTabIndex() {
const index = COMMUNITY_TABS.indexOf(this.$route.params.tab)
this.tabIndex = index > -1 ? index : 0
this.closeAllOpenCollapse()
},
closeAllOpenCollapse() {
this.$el.querySelectorAll('.collapse.show').forEach((value) => {
this.$root.$emit('bv::toggle::collapse', value.id)
@ -294,8 +282,8 @@ export default {
this.form.amount = item.amount
this.form.hours = item.amount / 20
this.updateAmount = item.amount
this.$router.push({ path: '#edit' })
this.tabIndex = 0
this.$router.push({ params: { tab: 'contribute' } })
},
updateTransactions(pagination) {
this.$emit('update-transactions', pagination)
@ -304,11 +292,6 @@ export default {
this.items.find((item) => item.id === id).state = 'PENDING'
},
},
created() {
this.updateTransactions(0)
this.tabIndex = 0
this.$router.push({ path: '/community#edit' })
},
}
</script>
<style scoped>

View File

@ -49,8 +49,8 @@ describe('router', () => {
expect(routes.find((r) => r.path === '/').redirect()).toEqual({ path: '/login' })
})
it('has sixteen routes defined', () => {
expect(routes).toHaveLength(18)
it('has 19 routes defined', () => {
expect(routes).toHaveLength(19)
})
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', () => {
expect(routes.find((r) => r.path === '/community').meta.requiresAuth).toBeTruthy()
})

View File

@ -58,6 +58,17 @@ const routes = [
requiresAuth: true,
pageTitle: 'community',
},
redirect: (to) => {
return { path: '/community/contribute' }
},
},
{
path: '/community/:tab',
component: () => import('@/pages/Community.vue'),
meta: {
requiresAuth: true,
pageTitle: 'community',
},
},
{
path: '/information',