Merge branch 'master' into 2708-button-for-chat-with-moderator-incorrect-fetch

This commit is contained in:
Alexander Friedland 2023-02-22 08:35:08 +01:00 committed by GitHub
commit 05ad93a36e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 4934 additions and 1866 deletions

View File

@ -163,7 +163,6 @@ jobs:
locales_frontend:
name: Locales - Frontend
runs-on: ubuntu-latest
needs: [build_test_frontend]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
@ -171,20 +170,10 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Frontend)
uses: actions/download-artifact@v3
with:
name: docker-frontend-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/frontend.tar
##########################################################################
# LOCALES FRONTEND #######################################################
##########################################################################
- name: Frontend | Locales
run: docker run --rm gradido/frontend:test yarn run locales
run: cd frontend && yarn && yarn run locales
##############################################################################
# JOB: LINT FRONTEND #########################################################
@ -192,7 +181,6 @@ jobs:
lint_frontend:
name: Lint - Frontend
runs-on: ubuntu-latest
needs: [build_test_frontend]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
@ -200,20 +188,10 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Frontend)
uses: actions/download-artifact@v3
with:
name: docker-frontend-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/frontend.tar
##########################################################################
# LINT FRONTEND ##########################################################
##########################################################################
- name: Frontend | Lint
run: docker run --rm gradido/frontend:test yarn run lint
run: cd frontend && yarn && yarn run lint
##############################################################################
# JOB: STYLELINT FRONTEND ####################################################
@ -221,7 +199,6 @@ jobs:
stylelint_frontend:
name: Stylelint - Frontend
runs-on: ubuntu-latest
needs: [build_test_frontend]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
@ -229,20 +206,10 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Frontend)
uses: actions/download-artifact@v3
with:
name: docker-frontend-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/frontend.tar
##########################################################################
# STYLELINT FRONTEND #####################################################
##########################################################################
- name: Frontend | Stylelint
run: docker run --rm gradido/frontend:test yarn run stylelint
run: cd frontend && yarn && yarn run stylelint
##############################################################################
# JOB: LINT ADMIN INTERFACE ##################################################
@ -250,7 +217,6 @@ jobs:
lint_admin:
name: Lint - Admin Interface
runs-on: ubuntu-latest
needs: [build_test_admin]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
@ -258,20 +224,10 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Admin Interface)
uses: actions/download-artifact@v3
with:
name: docker-admin-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/admin.tar
##########################################################################
# LINT ADMIN INTERFACE ###################################################
##########################################################################
- name: Admin Interface | Lint
run: docker run --rm gradido/admin:test yarn run lint
run: cd admin && yarn && yarn run lint
##############################################################################
# JOB: STYLELINT ADMIN INTERFACE #############################################
@ -279,7 +235,6 @@ jobs:
stylelint_admin:
name: Stylelint - Admin Interface
runs-on: ubuntu-latest
needs: [build_test_admin]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
@ -287,20 +242,10 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Admin Interface)
uses: actions/download-artifact@v3
with:
name: docker-admin-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/admin.tar
##########################################################################
# STYLELINT ADMIN INTERFACE ##############################################
##########################################################################
- name: Admin Interface | Stylelint
run: docker run --rm gradido/admin:test yarn run stylelint
run: cd admin && yarn && yarn run stylelint
##############################################################################
# JOB: LOCALES ADMIN #########################################################
@ -308,7 +253,6 @@ jobs:
locales_admin:
name: Locales - Admin Interface
runs-on: ubuntu-latest
needs: [build_test_admin]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
@ -316,20 +260,10 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Admin Interface)
uses: actions/download-artifact@v3
with:
name: docker-admin-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/admin.tar
##########################################################################
# LOCALES FRONTEND #######################################################
##########################################################################
- name: admin | Locales
run: docker run --rm gradido/admin:test yarn run locales
- name: Admin | Locales
run: cd admin && yarn && yarn run locales
##############################################################################
# JOB: LINT BACKEND ##########################################################
@ -337,7 +271,6 @@ jobs:
lint_backend:
name: Lint - Backend
runs-on: ubuntu-latest
needs: [build_test_backend]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
@ -345,20 +278,10 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Backend)
uses: actions/download-artifact@v3
with:
name: docker-backend-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/backend.tar
##########################################################################
# LINT BACKEND ###########################################################
##########################################################################
- name: backend | Lint
run: docker run --rm gradido/backend:test yarn run lint
run: cd backend && yarn && yarn run lint
##############################################################################
# JOB: LOCALES BACKEND #######################################################
@ -366,7 +289,6 @@ jobs:
locales_backend:
name: Locales - Backend
runs-on: ubuntu-latest
needs: [build_test_backend]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
@ -385,7 +307,6 @@ jobs:
lint_database_up:
name: Lint - Database Up
runs-on: ubuntu-latest
needs: [build_test_database_up]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
@ -393,20 +314,10 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Backend)
uses: actions/download-artifact@v3
with:
name: docker-database-test_up
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/database_up.tar
##########################################################################
# LINT DATABASE ##########################################################
##########################################################################
- name: database | Lint
run: docker run --rm gradido/database:test_up yarn run lint
- name: Database | Lint
run: cd database && yarn && yarn run lint
##############################################################################
# JOB: UNIT TEST FRONTEND ###################################################
@ -414,7 +325,6 @@ jobs:
unit_test_frontend:
name: Unit tests - Frontend
runs-on: ubuntu-latest
needs: [build_test_frontend]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
@ -422,30 +332,12 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGES #################################################
##########################################################################
- name: Download Docker Image (Frontend)
uses: actions/download-artifact@v3
with:
name: docker-frontend-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/frontend.tar
##########################################################################
# UNIT TESTS FRONTEND ####################################################
##########################################################################
- name: frontend | Unit tests
- name: Frontend | Unit tests
run: |
docker run --env NODE_ENV=test -v ~/coverage:/app/coverage --rm gradido/frontend:test yarn run test
cp -r ~/coverage ./coverage
##########################################################################
# COVERAGE REPORT FRONTEND ###############################################
##########################################################################
#- name: frontend | Coverage report
# uses: romeovs/lcov-reporter-action@v0.2.21
# with:
# github-token: ${{ secrets.GITHUB_TOKEN }}
# lcov-file: ./coverage/lcov.info
cd frontend && yarn && yarn run test
cp -r ./coverage ../
##########################################################################
# COVERAGE CHECK FRONTEND ################################################
##########################################################################
@ -454,7 +346,7 @@ jobs:
with:
report_name: Coverage Frontend
type: lcov
result_path: ./coverage/lcov.info
result_path: ./frontend/coverage/lcov.info
min_coverage: 95
token: ${{ github.token }}
@ -464,7 +356,6 @@ jobs:
unit_test_admin:
name: Unit tests - Admin Interface
runs-on: ubuntu-latest
needs: [build_test_admin]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
@ -472,22 +363,12 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGES #################################################
##########################################################################
- name: Download Docker Image (Admin Interface)
uses: actions/download-artifact@v3
with:
name: docker-admin-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/admin.tar
##########################################################################
# UNIT TESTS ADMIN INTERFACE #############################################
##########################################################################
- name: Admin Interface | Unit tests
run: |
docker run -v ~/coverage:/app/coverage --rm gradido/admin:test yarn run test
cp -r ~/coverage ./coverage
cd admin && yarn && yarn run test
cp -r ./coverage ../
##########################################################################
# COVERAGE CHECK ADMIN INTERFACE #########################################
##########################################################################
@ -496,7 +377,7 @@ jobs:
with:
report_name: Coverage Admin Interface
type: lcov
result_path: ./coverage/lcov.info
result_path: ./admin/coverage/lcov.info
min_coverage: 97
token: ${{ github.token }}
@ -534,8 +415,9 @@ jobs:
- name: backend | docker-compose database
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps database
- name: backend Unit tests | test
run: cd database && yarn && yarn build && cd ../backend && yarn && yarn test
# run: docker-compose -f docker-compose.yml -f docker-compose.test.yml exec -T backend yarn test
run: |
cd database && yarn && yarn build && cd ../backend && yarn && yarn test
cp -r ./coverage ../
##########################################################################
# COVERAGE CHECK BACKEND #################################################
##########################################################################

98
.github/workflows/test_federation.yml vendored Normal file
View File

@ -0,0 +1,98 @@
name: gradido test_federation CI
on: push
jobs:
##############################################################################
# JOB: DOCKER BUILD TEST #####################################################
##############################################################################
build:
name: Docker Build Test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Build `test` image
run: |
docker build --target test -t "gradido/federation:test" -f federation/Dockerfile .
docker save "gradido/federation:test" > /tmp/federation.tar
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: docker-federation-test
path: /tmp/federation.tar
##############################################################################
# JOB: LINT ##################################################################
##############################################################################
lint:
name: Lint
runs-on: ubuntu-latest
needs: [build]
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Download Docker Image
uses: actions/download-artifact@v3
with:
name: docker-federation-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/federation.tar
- name: Lint
run: docker run --rm gradido/federation:test yarn run lint
##############################################################################
# JOB: UNIT TEST #############################################################
##############################################################################
unit_test:
name: Unit tests
runs-on: ubuntu-latest
needs: [build]
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Download Docker Image
uses: actions/download-artifact@v3
with:
name: docker-federation-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/federation.tar
- name: docker-compose mariadb
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb
- name: Sleep for 30 seconds
run: sleep 30s
shell: bash
- name: docker-compose database
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps database
- name: Sleep for 30 seconds
run: sleep 30s
shell: bash
#- name: Unit tests
# run: cd database && yarn && yarn build && cd ../dht-node && yarn && yarn test
- name: Unit tests
run: |
docker run --env NODE_ENV=test --env DB_HOST=mariadb --network gradido_internal-net -v ~/coverage:/app/coverage --rm gradido/federation:test yarn run test
cp -r ~/coverage ./coverage
- name: Coverage check
uses: webcraftmedia/coverage-check-action@master
with:
report_name: Coverage federation
type: lcov
#result_path: ./federation/coverage/lcov.info
result_path: ./coverage/lcov.info
min_coverage: 72
token: ${{ github.token }}

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

File diff suppressed because it is too large Load Diff

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()
@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,
)
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,
)
})
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,65 +53,81 @@ afterAll(async () => {
describe('TransactionLinkResolver', () => {
describe('createTransactionLink', () => {
beforeAll(async () => {
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
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')],
}),
)
})
})
it('throws error when amount is zero', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: createTransactionLink,
variables: {
amount: 0,
memo: 'Test',
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('Amount must be a positive number')],
describe('authenticated', () => {
beforeAll(async () => {
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
})
})
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 () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: createTransactionLink,
variables: {
amount: -10,
memo: 'Test',
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('Amount must be a positive number')],
it('throws error when amount is zero', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: createTransactionLink,
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('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('throws error when amount is negative', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: createTransactionLink,
variables: {
amount: -10,
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(-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()
})
describe('contributionLink', () => {
describe('input not valid', () => {
beforeAll(async () => {
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
})
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'),
)
})
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')],
}),
)
})
})
// 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', () => {
describe('authenticated', () => {
describe('contributionLink', () => {
describe('input not valid', () => {
beforeAll(async () => {
await mutate({
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()
await expect(
mutate({
mutation: redeemTransactionLink,
variables: {
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
code: 'CL-123456',
},
}),
).resolves.toMatchObject({
@ -359,85 +176,247 @@ describe('TransactionLinkResolver', () => {
})
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(
'The amount (5 GDD) to be created exceeds the amount (0 GDD) still available for this month.',
),
new Error('No contribution link found to given code'),
)
})
})
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(),
},
})
})
const now = new Date()
const validFrom = new Date(now.getFullYear() + 1, 0, 1)
it('allows the user to redeem the contribution link', 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 () => {
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 ? contributionLink.code : ''),
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 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 () => {
jest.useFakeTimers()
setTimeout(jest.fn(), 1000 * 60 * 60 * 24)
jest.runAllTimers()
await mutate({
mutation: login,
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(() => {
jest.useRealTimers()
it('does not allow the user to redeem the contribution link', 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('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(
mutate({
mutation: redeemTransactionLink,
@ -473,6 +452,59 @@ describe('TransactionLinkResolver', () => {
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'),
)
})
})
})
})
})

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

@ -16,7 +16,7 @@ export const nMonthsBefore = (date: Date, months = 1): string => {
export const creationFactory = async (
client: ApolloServerTestClient,
creation: CreationInterface,
): Promise<Contribution | void> => {
): Promise<Contribution> => {
const { mutate } = client
await mutate({ mutation: login, variables: { email: creation.email, password: 'Aa12345_' } })
@ -51,6 +51,7 @@ export const creationFactory = async (
await confirmedContribution.save()
}
}
return confirmedContribution
} else {
return contribution
}

View File

@ -272,6 +272,12 @@ export const deleteContribution = gql`
}
`
export const denyContribution = gql`
mutation ($id: Int!) {
denyContribution(id: $id)
}
`
export const createContributionMessage = gql`
mutation ($contributionId: Float!, $message: String!) {
createContributionMessage(contributionId: $contributionId, message: $message) {

View File

@ -166,6 +166,15 @@ export const listContributions = gql`
id
amount
memo
createdAt
contributionDate
confirmedAt
confirmedBy
deletedAt
state
messagesCount
deniedAt
deniedBy
}
}
}
@ -177,6 +186,40 @@ query ($currentPage: Int = 1, $pageSize: Int = 5, $order: Order = DESC, $statusF
contributionCount
contributionList {
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
lastName
amount
@ -189,24 +232,7 @@ query ($currentPage: Int = 1, $pageSize: Int = 5, $order: Order = DESC, $statusF
messagesCount
deniedAt
deniedBy
}
}
}
`
// from admin interface
export const listUnconfirmedContributions = gql`
query {
listUnconfirmedContributions {
id
firstName
lastName
email
amount
memo
date
moderator
creation
}
}
}
`

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

@ -32,15 +32,6 @@ export const startDHT = async (topic: string): Promise<void> => {
logger.debug(`keyPairDHT: secretKey=${keyPair.secretKey.toString('hex')}`)
const ownApiVersions = writeHomeCommunityEnries(keyPair.publicKey)
/*
const ownApiVersions = Object.values(ApiVersionType).map(function (apiEnum) {
const comApi: CommunityApi = {
api: apiEnum,
url: CONFIG.FEDERATION_COMMUNITY_URL,
}
return comApi
})
*/
logger.debug(`ApiList: ${JSON.stringify(ownApiVersions)}`)
const node = new DHT({ keyPair })
@ -216,6 +207,5 @@ async function writeHomeCommunityEnries(pubKey: any): Promise<CommunityApi[]> {
} catch (err) {
throw new Error(`Federation: Error writing HomeCommunity-Entries: ${err}`)
}
return homeApiVersions
}

View File

@ -84,6 +84,29 @@ services:
- ./dht-node:/app
- ./database:/database
########################################################
# FEDERATION ###########################################
########################################################
federation:
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
image: gradido/federation:local-development
build:
target: development
networks:
- external-net
- internal-net
environment:
- NODE_ENV="development"
volumes:
# This makes sure the docker container has its own node modules.
# Therefore it is possible to have a different node version on the host machine
- federation_node_modules:/app/node_modules
- federation_database_node_modules:/database/node_modules
- federation_database_build:/database/build
# bind the local folder to the docker to allow live reload
- ./federation:/app
- ./database:/database
########################################################
# DATABASE ##############################################
########################################################
@ -155,5 +178,8 @@ volumes:
dht_node_modules:
dht_database_node_modules:
dht_database_build:
federation_node_modules:
federation_database_node_modules:
federation_database_build:
database_node_modules:
database_build:

View File

@ -36,6 +36,21 @@ services:
- NODE_ENV="test"
- DB_HOST=mariadb
########################################################
# FEDERATION ###########################################
########################################################
federation:
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
image: gradido/federation:test
build:
target: test
networks:
- external-net
- internal-net
environment:
- NODE_ENV="test"
- DB_HOST=mariadb
########################################################
# DATABASE #############################################
########################################################

View File

@ -147,6 +147,42 @@ services:
# <host_machine_directory>:<container_directory> mirror bidirectional path in local context with path in Docker container
- ./logs/dht-node:/logs/dht-node
########################################################
# FEDERATION ###########################################
########################################################
federation:
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
image: gradido/federation:local-production
build:
# since we have to include the entities from ./database we cannot define the context as ./federation
# this might blow build image size to the moon ?!
context: ./
dockerfile: ./federation/Dockerfile
target: production
networks:
- internal-net
- external-net
ports:
- 5010:5010
depends_on:
- mariadb
restart: always
environment:
# Envs used in Dockerfile
# - DOCKER_WORKDIR="/app"
- PORT=5010
- BUILD_DATE
- BUILD_VERSION
- BUILD_COMMIT
- NODE_ENV="production"
- DB_HOST=mariadb
# Application only envs
#env_file:
# - ./frontend/.env
volumes:
# <host_machine_directory>:<container_directory> mirror bidirectional path in local context with path in Docker container
- ./logs/federation:/logs/federation
########################################################
# DATABASE #############################################
########################################################

116
federation/Dockerfile Normal file
View File

@ -0,0 +1,116 @@
##################################################################################
# BASE ###########################################################################
##################################################################################
FROM node:18.7.0-alpine3.16 as base
# ENVs (available in production aswell, can be overwritten by commandline or env file)
## DOCKER_WORKDIR would be a classical ARG, but that is not multi layer persistent - shame
ENV DOCKER_WORKDIR="/app"
## We Cannot do `$(date -u +'%Y-%m-%dT%H:%M:%SZ')` here so we use unix timestamp=0
ENV BUILD_DATE="1970-01-01T00:00:00.00Z"
## We cannot do $(npm run version).${BUILD_NUMBER} here so we default to 0.0.0.0
ENV BUILD_VERSION="0.0.0.0"
## We cannot do `$(git rev-parse --short HEAD)` here so we default to 0000000
ENV BUILD_COMMIT="0000000"
## SET NODE_ENV
ENV NODE_ENV="production"
## App relevant Envs
ENV PORT="5010"
# ENV PORT="${env.FEDERATION_PORT}"
# Labels
LABEL org.label-schema.build-date="${BUILD_DATE}"
LABEL org.label-schema.name="gradido:federation"
LABEL org.label-schema.description="Gradido GraphQL Federation"
LABEL org.label-schema.usage="https://github.com/gradido/gradido/blob/master/README.md"
LABEL org.label-schema.url="https://gradido.net"
LABEL org.label-schema.vcs-url="https://github.com/gradido/gradido/tree/master/federation"
LABEL org.label-schema.vcs-ref="${BUILD_COMMIT}"
LABEL org.label-schema.vendor="Gradido Community"
LABEL org.label-schema.version="${BUILD_VERSION}"
LABEL org.label-schema.schema-version="1.0"
LABEL maintainer="support@gradido.net"
# Install Additional Software
## install: git
#RUN apk --no-cache add git
# Settings
## Expose Container Port
EXPOSE ${PORT}
## Workdir
RUN mkdir -p ${DOCKER_WORKDIR}
WORKDIR ${DOCKER_WORKDIR}
RUN mkdir -p /database
##################################################################################
# DEVELOPMENT (Connected to the local environment, to reload on demand) ##########
##################################################################################
FROM base as development
# We don't need to copy or build anything since we gonna bind to the
# local filesystem which will need a rebuild anyway
# Run command
# (for development we need to execute yarn install since the
# node_modules are on another volume and need updating)
CMD /bin/sh -c "cd /database && yarn install && yarn build && cd /app && yarn install && yarn run dev"
##################################################################################
# BUILD (Does contain all files and is therefore bloated) ########################
##################################################################################
FROM base as build
# Copy everything from federation
COPY ./federation/ ./
# Copy everything from database
COPY ./database/ ../database/
# yarn install federation
RUN yarn install --production=false --frozen-lockfile --non-interactive
# yarn install database
RUN cd ../database && yarn install --production=false --frozen-lockfile --non-interactive
# yarn build
RUN yarn run build
# yarn build database
RUN cd ../database && yarn run build
##################################################################################
# TEST ###########################################################################
##################################################################################
FROM build as test
# Run command
CMD /bin/sh -c "yarn run start"
##################################################################################
# PRODUCTION (Does contain only "binary"- and static-files to reduce image size) #
##################################################################################
FROM base as production
# Copy "binary"-files from build image
COPY --from=build ${DOCKER_WORKDIR}/build ./build
COPY --from=build ${DOCKER_WORKDIR}/../database/build ../database/build
# We also copy the node_modules express and serve-static for the run script
COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules
COPY --from=build ${DOCKER_WORKDIR}/../database/node_modules ../database/node_modules
# Copy static files
# COPY --from=build ${DOCKER_WORKDIR}/public ./public
# Copy package.json for script definitions (lock file should not be needed)
COPY --from=build ${DOCKER_WORKDIR}/package.json ./package.json
# Copy tsconfig.json to provide alias path definitions
COPY --from=build ${DOCKER_WORKDIR}/tsconfig.json ./tsconfig.json
# Copy log4js-config.json to provide log configuration
COPY --from=build ${DOCKER_WORKDIR}/log4js-config.json ./log4js-config.json
# Copy run scripts run/
# COPY --from=build ${DOCKER_WORKDIR}/run ./run
# Run command
CMD /bin/sh -c "yarn run start"

32
federation/jest.config.js Normal file
View File

@ -0,0 +1,32 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
verbose: true,
preset: 'ts-jest',
collectCoverage: true,
collectCoverageFrom: [
'src/**/*.ts',
'!**/node_modules/**',
'!src/seeds/**',
'!build/**',
],
setupFiles: ['<rootDir>/test/testSetup.ts'],
setupFilesAfterEnv: [],
modulePathIgnorePatterns: ['<rootDir>/build/'],
moduleNameMapper: {
'@/(.*)': '<rootDir>/src/$1',
'@arg/(.*)': '<rootDir>/src/graphql/arg/$1',
'@enum/(.*)': '<rootDir>/src/graphql/enum/$1',
'@model/(.*)': '<rootDir>/src/graphql/model/$1',
'@union/(.*)': '<rootDir>/src/graphql/union/$1',
'@repository/(.*)': '<rootDir>/src/typeorm/repository/$1',
'@test/(.*)': '<rootDir>/test/$1',
'@entity/(.*)':
process.env.NODE_ENV === 'development'
? '<rootDir>/../database/entity/$1'
: '<rootDir>/../database/build/entity/$1',
'@dbTools/(.*)':
process.env.NODE_ENV === 'development'
? '<rootDir>/../database/src/$1'
: '<rootDir>/../database/build/src/$1',
},
}

View File

@ -11,6 +11,7 @@
"build": "tsc --build",
"clean": "tsc --build --clean",
"start": "cross-env TZ=UTC TS_NODE_BASEURL=./build node -r tsconfig-paths/register build/src/index.js",
"test": "cross-env TZ=UTC NODE_ENV=development jest --runInBand --coverage --forceExit --detectOpenHandles",
"dev": "cross-env TZ=UTC nodemon -w src --ext ts --exec ts-node -r dotenv/config -r tsconfig-paths/register src/index.ts",
"lint": "eslint --max-warnings=0 --ext .js,.ts ."
},
@ -26,15 +27,16 @@
"lodash.clonedeep": "^4.5.0",
"log4js": "^6.7.1",
"reflect-metadata": "^0.1.13",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.1.1",
"type-graphql": "^1.1.1"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^4.28.0",
"@typescript-eslint/parser": "^4.28.0",
"@types/express": "4.17.12",
"@types/jest": "27.0.2",
"@types/lodash.clonedeep": "^4.5.6",
"@types/node": "^16.10.3",
"@typescript-eslint/eslint-plugin": "^4.28.0",
"@typescript-eslint/parser": "^4.28.0",
"apollo-server-testing": "2.25.2",
"eslint": "^7.29.0",
"eslint-config-prettier": "^8.3.0",
"eslint-config-standard": "^16.0.3",
@ -42,8 +44,15 @@
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-promise": "^5.1.0",
"jest": "27.2.4",
"ts-jest": "27.0.5",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.1.1",
"nodemon": "^2.0.7",
"prettier": "^2.3.1",
"typescript": "^4.3.4",
"nodemon": "^2.0.7"
"typescript": "^4.3.4"
},
"nodemonConfig": {
"ignore": ["**/*.test.ts"]
}
}

View File

@ -24,7 +24,7 @@ const constants = {
}
const server = {
PORT: process.env.PORT || 5000,
PORT: process.env.PORT || 5010,
// JWT_SECRET: process.env.JWT_SECRET || 'secret123',
// JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '10m',
GRAPHIQL: process.env.GRAPHIQL === 'true' || false,
@ -73,7 +73,7 @@ if (
const federation = {
// FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || null,
// FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null,
FEDERATION_PORT: process.env.FEDERATION_PORT || 5000,
FEDERATION_PORT: process.env.FEDERATION_PORT || 5010,
FEDERATION_API: process.env.FEDERATION_API || '1_0',
FEDERATION_COMMUNITY_URL: process.env.FEDERATION_COMMUNITY_URL || null,
}

View File

@ -1,12 +0,0 @@
import { Query, Resolver } from 'type-graphql'
import { federationLogger as logger } from '@/server/logger'
import { GetTestApiResult } from '../../GetTestApiResult'
@Resolver()
export class Test2Resolver {
@Query(() => GetTestApiResult)
async test2(): Promise<GetTestApiResult> {
logger.info(`test api 2 1_0`)
return new GetTestApiResult('1_0')
}
}

View File

@ -0,0 +1,41 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { createTestClient } from 'apollo-server-testing'
import createServer from '@/server/createServer'
let query: any
// to do: We need a setup for the tests that closes the connection
let con: any
beforeAll(async () => {
const server = await createServer()
con = server.con
query = createTestClient(server.apollo).query
})
afterAll(async () => {
await con.close()
})
describe('TestResolver', () => {
const getTestQuery = `
query {
test {
api
}
}
`
describe('getTestApi', () => {
it('returns 1_0', async () => {
await expect(query({ query: getTestQuery })).resolves.toMatchObject({
data: {
test: {
api: '1_0',
},
},
})
})
})
})

View File

@ -1,8 +1,10 @@
import { Field, ObjectType, Query, Resolver } from 'type-graphql'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { Query, Resolver } from 'type-graphql'
import { federationLogger as logger } from '@/server/logger'
import { GetTestApiResult } from '../../GetTestApiResult'
@Resolver()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export class TestResolver {
@Query(() => GetTestApiResult)
async test(): Promise<GetTestApiResult> {

View File

@ -0,0 +1,44 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { createTestClient } from 'apollo-server-testing'
import createServer from '@/server/createServer'
import CONFIG from '@/config'
CONFIG.FEDERATION_API = '1_1'
let query: any
// to do: We need a setup for the tests that closes the connection
let con: any
beforeAll(async () => {
const server = await createServer()
con = server.con
query = createTestClient(server.apollo).query
})
afterAll(async () => {
await con.close()
})
describe('TestResolver', () => {
const getTestQuery = `
query {
test {
api
}
}
`
describe('getTestApi', () => {
it('returns 1_1', async () => {
await expect(query({ query: getTestQuery })).resolves.toMatchObject({
data: {
test: {
api: '1_1',
},
},
})
})
})
})

View File

@ -1,8 +1,10 @@
import { Field, ObjectType, Query, Resolver } from 'type-graphql'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { Query, Resolver } from 'type-graphql'
import { federationLogger as logger } from '@/server/logger'
import { GetTestApiResult } from '../../GetTestApiResult'
@Resolver()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export class TestResolver {
@Query(() => GetTestApiResult)
async test(): Promise<GetTestApiResult> {

View File

@ -0,0 +1,44 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { createTestClient } from 'apollo-server-testing'
import createServer from '@/server/createServer'
import CONFIG from '@/config'
CONFIG.FEDERATION_API = '2_0'
let query: any
// to do: We need a setup for the tests that closes the connection
let con: any
beforeAll(async () => {
const server = await createServer()
con = server.con
query = createTestClient(server.apollo).query
})
afterAll(async () => {
await con.close()
})
describe('TestResolver', () => {
const getTestQuery = `
query {
test {
api
}
}
`
describe('getTestApi', () => {
it('returns 2_0', async () => {
await expect(query({ query: getTestQuery })).resolves.toMatchObject({
data: {
test: {
api: '2_0',
},
},
})
})
})
})

View File

@ -1,8 +1,10 @@
import { Field, ObjectType, Query, Resolver } from 'type-graphql'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { Query, Resolver } from 'type-graphql'
import { federationLogger as logger } from '@/server/logger'
import { GetTestApiResult } from '../../GetTestApiResult'
@Resolver()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export class TestResolver {
@Query(() => GetTestApiResult)
async test(): Promise<GetTestApiResult> {

View File

@ -1,6 +1,8 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { Field, ObjectType } from 'type-graphql'
@ObjectType()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export class GetTestApiResult {
constructor(apiVersion: string) {
this.api = apiVersion

View File

@ -0,0 +1,22 @@
import { federationLogger as logger } from '@/server/logger'
jest.setTimeout(1000000)
jest.mock('@/server/logger', () => {
const originalModule = jest.requireActual('@/server/logger')
return {
__esModule: true,
...originalModule,
backendLogger: {
addContext: jest.fn(),
trace: jest.fn(),
debug: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
error: jest.fn(),
fatal: jest.fn(),
},
}
})
export { logger }

View File

@ -52,14 +52,14 @@
// "@enum/*": ["src/graphql/enum/*"],
// "@model/*": ["src/graphql/model/*"],
"@repository/*": ["src/typeorm/repository/*"],
// "@test/*": ["test/*"],
"@test/*": ["test/*"],
/* external */
"@typeorm/*": ["../backend/src/typeorm/*", "../../backend/src/typeorm/*"],
"@dbTools/*": ["../database/src/*", "../../database/build/src/*"],
"@entity/*": ["../database/entity/*", "../../database/build/entity/*"]
},
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
"typeRoots": ["src/dht_node/@types", "node_modules/@types"], /* List of folders to include type definitions from. */
"typeRoots": ["node_modules/@types"], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@ -3,16 +3,16 @@
<b-navbar :toggleable="false" class="pr-4">
<b-navbar-brand class="d-none d-lg-block">
<b-img
class="imgLogo position-absolute ml--3 mt-lg--2 mt-3 p-2 zindex1000"
class="position-absolute ml--3 mt-lg--2 mt-3 p-2 zindex1000"
:src="logo"
width="200"
alt="..."
alt="Logo"
/>
<b-img
class="imgLogoBack mt--3 ml--3"
src="/img/template/gradido_background_header.png"
class="mt--3 ml--3"
:src="background_header"
width="230"
alt="start background image"
alt="Background Image"
></b-img>
</b-navbar-brand>
<b-img class="sheet-img position-absolute d-block d-lg-none zindex1000" :src="sheet"></b-img>
@ -35,7 +35,8 @@ export default {
mixins: [authLinks],
data() {
return {
logo: '/img/brand/green.png',
background_header: '/img/template/gradido_background_header.png',
logo: '/img/brand/gradido-logo.png',
sheet: '/img/template/Blaetter.png',
}
},

View File

@ -4,10 +4,10 @@
<b-navbar toggleable="lg" class="pr-4">
<b-navbar-brand>
<b-img
class="imgLogo mt-lg--2 mt-3 mb-3 d-none d-lg-block zindex10"
class="mt-lg--2 mt-3 mb-3 d-none d-lg-block zindex10"
:src="logo"
width=""
alt="..."
width="200"
alt="Logo"
/>
<div v-b-toggle.sidebar-mobile variant="link" class="d-block d-lg-none">
<span class="navbar-toggler-icon h2"></span>
@ -60,7 +60,7 @@ export default {
},
data() {
return {
logo: '/img/brand/green.png',
logo: '/img/brand/gradido-logo.png',
sheet: '/img/template/Blaetter.png',
}
},

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: {
@ -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

@ -45,7 +45,6 @@ export default {
},
data() {
return {
img: '/img/brand/green.png',
linkData: {
__typename: 'TransactionLink',
amount: '123.45',

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',