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: locales_frontend:
name: Locales - Frontend name: Locales - Frontend
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build_test_frontend]
steps: steps:
########################################################################## ##########################################################################
# CHECKOUT CODE ########################################################## # CHECKOUT CODE ##########################################################
@ -171,20 +170,10 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 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 ####################################################### # LOCALES FRONTEND #######################################################
########################################################################## ##########################################################################
- name: Frontend | Locales - name: Frontend | Locales
run: docker run --rm gradido/frontend:test yarn run locales run: cd frontend && yarn && yarn run locales
############################################################################## ##############################################################################
# JOB: LINT FRONTEND ######################################################### # JOB: LINT FRONTEND #########################################################
@ -192,7 +181,6 @@ jobs:
lint_frontend: lint_frontend:
name: Lint - Frontend name: Lint - Frontend
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build_test_frontend]
steps: steps:
########################################################################## ##########################################################################
# CHECKOUT CODE ########################################################## # CHECKOUT CODE ##########################################################
@ -200,20 +188,10 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 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 ########################################################## # LINT FRONTEND ##########################################################
########################################################################## ##########################################################################
- name: Frontend | Lint - name: Frontend | Lint
run: docker run --rm gradido/frontend:test yarn run lint run: cd frontend && yarn && yarn run lint
############################################################################## ##############################################################################
# JOB: STYLELINT FRONTEND #################################################### # JOB: STYLELINT FRONTEND ####################################################
@ -221,7 +199,6 @@ jobs:
stylelint_frontend: stylelint_frontend:
name: Stylelint - Frontend name: Stylelint - Frontend
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build_test_frontend]
steps: steps:
########################################################################## ##########################################################################
# CHECKOUT CODE ########################################################## # CHECKOUT CODE ##########################################################
@ -229,20 +206,10 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 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 ##################################################### # STYLELINT FRONTEND #####################################################
########################################################################## ##########################################################################
- name: Frontend | Stylelint - name: Frontend | Stylelint
run: docker run --rm gradido/frontend:test yarn run stylelint run: cd frontend && yarn && yarn run stylelint
############################################################################## ##############################################################################
# JOB: LINT ADMIN INTERFACE ################################################## # JOB: LINT ADMIN INTERFACE ##################################################
@ -250,7 +217,6 @@ jobs:
lint_admin: lint_admin:
name: Lint - Admin Interface name: Lint - Admin Interface
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build_test_admin]
steps: steps:
########################################################################## ##########################################################################
# CHECKOUT CODE ########################################################## # CHECKOUT CODE ##########################################################
@ -258,20 +224,10 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 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 ################################################### # LINT ADMIN INTERFACE ###################################################
########################################################################## ##########################################################################
- name: Admin Interface | Lint - 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 ############################################# # JOB: STYLELINT ADMIN INTERFACE #############################################
@ -279,7 +235,6 @@ jobs:
stylelint_admin: stylelint_admin:
name: Stylelint - Admin Interface name: Stylelint - Admin Interface
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build_test_admin]
steps: steps:
########################################################################## ##########################################################################
# CHECKOUT CODE ########################################################## # CHECKOUT CODE ##########################################################
@ -287,20 +242,10 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 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 ############################################## # STYLELINT ADMIN INTERFACE ##############################################
########################################################################## ##########################################################################
- name: Admin Interface | Stylelint - name: Admin Interface | Stylelint
run: docker run --rm gradido/admin:test yarn run stylelint run: cd admin && yarn && yarn run stylelint
############################################################################## ##############################################################################
# JOB: LOCALES ADMIN ######################################################### # JOB: LOCALES ADMIN #########################################################
@ -308,7 +253,6 @@ jobs:
locales_admin: locales_admin:
name: Locales - Admin Interface name: Locales - Admin Interface
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build_test_admin]
steps: steps:
########################################################################## ##########################################################################
# CHECKOUT CODE ########################################################## # CHECKOUT CODE ##########################################################
@ -316,20 +260,10 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 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 ####################################################### # LOCALES FRONTEND #######################################################
########################################################################## ##########################################################################
- name: admin | Locales - name: Admin | Locales
run: docker run --rm gradido/admin:test yarn run locales run: cd admin && yarn && yarn run locales
############################################################################## ##############################################################################
# JOB: LINT BACKEND ########################################################## # JOB: LINT BACKEND ##########################################################
@ -337,7 +271,6 @@ jobs:
lint_backend: lint_backend:
name: Lint - Backend name: Lint - Backend
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build_test_backend]
steps: steps:
########################################################################## ##########################################################################
# CHECKOUT CODE ########################################################## # CHECKOUT CODE ##########################################################
@ -345,20 +278,10 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 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 ########################################################### # LINT BACKEND ###########################################################
########################################################################## ##########################################################################
- name: backend | Lint - name: backend | Lint
run: docker run --rm gradido/backend:test yarn run lint run: cd backend && yarn && yarn run lint
############################################################################## ##############################################################################
# JOB: LOCALES BACKEND ####################################################### # JOB: LOCALES BACKEND #######################################################
@ -366,7 +289,6 @@ jobs:
locales_backend: locales_backend:
name: Locales - Backend name: Locales - Backend
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build_test_backend]
steps: steps:
########################################################################## ##########################################################################
# CHECKOUT CODE ########################################################## # CHECKOUT CODE ##########################################################
@ -385,7 +307,6 @@ jobs:
lint_database_up: lint_database_up:
name: Lint - Database Up name: Lint - Database Up
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build_test_database_up]
steps: steps:
########################################################################## ##########################################################################
# CHECKOUT CODE ########################################################## # CHECKOUT CODE ##########################################################
@ -393,20 +314,10 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 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 ########################################################## # LINT DATABASE ##########################################################
########################################################################## ##########################################################################
- name: database | Lint - name: Database | Lint
run: docker run --rm gradido/database:test_up yarn run lint run: cd database && yarn && yarn run lint
############################################################################## ##############################################################################
# JOB: UNIT TEST FRONTEND ################################################### # JOB: UNIT TEST FRONTEND ###################################################
@ -414,7 +325,6 @@ jobs:
unit_test_frontend: unit_test_frontend:
name: Unit tests - Frontend name: Unit tests - Frontend
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build_test_frontend]
steps: steps:
########################################################################## ##########################################################################
# CHECKOUT CODE ########################################################## # CHECKOUT CODE ##########################################################
@ -422,30 +332,12 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 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 #################################################### # UNIT TESTS FRONTEND ####################################################
########################################################################## ##########################################################################
- name: frontend | Unit tests - name: Frontend | Unit tests
run: | run: |
docker run --env NODE_ENV=test -v ~/coverage:/app/coverage --rm gradido/frontend:test yarn run test cd frontend && yarn && yarn run test
cp -r ~/coverage ./coverage cp -r ./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
########################################################################## ##########################################################################
# COVERAGE CHECK FRONTEND ################################################ # COVERAGE CHECK FRONTEND ################################################
########################################################################## ##########################################################################
@ -454,7 +346,7 @@ jobs:
with: with:
report_name: Coverage Frontend report_name: Coverage Frontend
type: lcov type: lcov
result_path: ./coverage/lcov.info result_path: ./frontend/coverage/lcov.info
min_coverage: 95 min_coverage: 95
token: ${{ github.token }} token: ${{ github.token }}
@ -464,7 +356,6 @@ jobs:
unit_test_admin: unit_test_admin:
name: Unit tests - Admin Interface name: Unit tests - Admin Interface
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build_test_admin]
steps: steps:
########################################################################## ##########################################################################
# CHECKOUT CODE ########################################################## # CHECKOUT CODE ##########################################################
@ -472,22 +363,12 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 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 ############################################# # UNIT TESTS ADMIN INTERFACE #############################################
########################################################################## ##########################################################################
- name: Admin Interface | Unit tests - name: Admin Interface | Unit tests
run: | run: |
docker run -v ~/coverage:/app/coverage --rm gradido/admin:test yarn run test cd admin && yarn && yarn run test
cp -r ~/coverage ./coverage cp -r ./coverage ../
########################################################################## ##########################################################################
# COVERAGE CHECK ADMIN INTERFACE ######################################### # COVERAGE CHECK ADMIN INTERFACE #########################################
########################################################################## ##########################################################################
@ -496,7 +377,7 @@ jobs:
with: with:
report_name: Coverage Admin Interface report_name: Coverage Admin Interface
type: lcov type: lcov
result_path: ./coverage/lcov.info result_path: ./admin/coverage/lcov.info
min_coverage: 97 min_coverage: 97
token: ${{ github.token }} token: ${{ github.token }}
@ -534,8 +415,9 @@ jobs:
- name: backend | docker-compose database - name: backend | docker-compose database
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps database run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps database
- name: backend Unit tests | test - name: backend Unit tests | test
run: cd database && yarn && yarn build && cd ../backend && yarn && yarn test run: |
# run: docker-compose -f docker-compose.yml -f docker-compose.test.yml exec -T backend yarn test cd database && yarn && yarn build && cd ../backend && yarn && yarn test
cp -r ./coverage ../
########################################################################## ##########################################################################
# COVERAGE CHECK BACKEND ################################################# # 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 jwt from 'jsonwebtoken'
import CONFIG from '@/config/' import CONFIG from '@/config/'
import { CustomJwtPayload } from './CustomJwtPayload' import { CustomJwtPayload } from './CustomJwtPayload'
import LogError from '@/server/LogError'
export const decode = (token: string): CustomJwtPayload | null => { export const decode = (token: string): CustomJwtPayload | null => {
if (!token) throw new Error('401 Unauthorized') if (!token) throw new LogError('401 Unauthorized')
try { try {
return <CustomJwtPayload>jwt.verify(token, CONFIG.JWT_SECRET) return <CustomJwtPayload>jwt.verify(token, CONFIG.JWT_SECRET)
} catch (err) { } catch (err) {

View File

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

View File

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

View File

@ -33,10 +33,14 @@ export class ContributionMessageResolver {
try { try {
const contribution = await DbContribution.findOne({ id: contributionId }) const contribution = await DbContribution.findOne({ id: contributionId })
if (!contribution) { if (!contribution) {
throw new Error('Contribution not found') throw new LogError('Contribution not found', contributionId)
} }
if (contribution.userId !== user.id) { if (contribution.userId !== user.id) {
throw new Error('Can not send message to contribution of another user') throw new LogError(
'Can not send message to contribution of another user',
contribution.userId,
user.id,
)
} }
contributionMessage.contributionId = contributionId contributionMessage.contributionId = contributionId

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
import Decimal from 'decimal.js-light' import Decimal from 'decimal.js-light'
import { Arg, Args, Authorized, Ctx, Int, Mutation, Query, Resolver } from 'type-graphql' import { Arg, Args, Authorized, Ctx, Int, Mutation, Query, Resolver } from 'type-graphql'
import { FindOperator, IsNull, In, getConnection } from '@dbTools/typeorm' import { FindOperator, IsNull, getConnection } from '@dbTools/typeorm'
import { Contribution as DbContribution } from '@entity/Contribution' import { Contribution as DbContribution } from '@entity/Contribution'
import { ContributionMessage } from '@entity/ContributionMessage' import { ContributionMessage } from '@entity/ContributionMessage'
@ -30,12 +30,11 @@ import { backendLogger as logger } from '@/server/logger'
import { import {
getCreationDates, getCreationDates,
getUserCreation, getUserCreation,
getUserCreations,
validateContribution, validateContribution,
updateCreations, updateCreations,
isValidDateString, isValidDateString,
} from './util/creations' } from './util/creations'
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS, FULL_CREATION_AVAILABLE } from './const/const' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
import { import {
EVENT_CONTRIBUTION_CREATE, EVENT_CONTRIBUTION_CREATE,
EVENT_CONTRIBUTION_DELETE, EVENT_CONTRIBUTION_DELETE,
@ -56,6 +55,7 @@ import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
import LogError from '@/server/LogError' import LogError from '@/server/LogError'
import { getLastTransaction } from './util/getLastTransaction' import { getLastTransaction } from './util/getLastTransaction'
import { findContributions } from './util/findContributions'
@Resolver() @Resolver()
export class ContributionResolver { export class ContributionResolver {
@ -168,25 +168,14 @@ export class ContributionResolver {
@Arg('statusFilter', () => [ContributionStatus], { nullable: true }) @Arg('statusFilter', () => [ContributionStatus], { nullable: true })
statusFilter?: ContributionStatus[], statusFilter?: ContributionStatus[],
): Promise<ContributionListResult> { ): Promise<ContributionListResult> {
const where: { const [dbContributions, count] = await findContributions(
contributionStatus?: FindOperator<string> | null order,
} = {} currentPage,
pageSize,
false,
statusFilter,
)
if (statusFilter && statusFilter.length) {
where.contributionStatus = In(statusFilter)
}
const [dbContributions, count] = await getConnection()
.createQueryBuilder()
.select('c')
.from(DbContribution, 'c')
.innerJoinAndSelect('c.user', 'u')
.leftJoinAndSelect('c.messages', 'm')
.where(where)
.orderBy('c.createdAt', order)
.limit(pageSize)
.offset((currentPage - 1) * pageSize)
.getManyAndCount()
return new ContributionListResult( return new ContributionListResult(
count, count,
dbContributions.map((contribution) => new Contribution(contribution, contribution.user)), dbContributions.map((contribution) => new Contribution(contribution, contribution.user)),
@ -425,40 +414,25 @@ export class ContributionResolver {
} }
@Authorized([RIGHTS.LIST_UNCONFIRMED_CONTRIBUTIONS]) @Authorized([RIGHTS.LIST_UNCONFIRMED_CONTRIBUTIONS])
@Query(() => [UnconfirmedContribution]) @Query(() => ContributionListResult) // [UnconfirmedContribution]
async listUnconfirmedContributions(@Ctx() context: Context): Promise<UnconfirmedContribution[]> { async adminListAllContributions(
const clientTimezoneOffset = getClientTimezoneOffset(context) @Args()
const contributions = await getConnection() { currentPage = 1, pageSize = 3, order = Order.DESC }: Paginated,
.createQueryBuilder() @Arg('statusFilter', () => [ContributionStatus], { nullable: true })
.select('c') statusFilter?: ContributionStatus[],
.from(DbContribution, 'c') ): Promise<ContributionListResult> {
.leftJoinAndSelect('c.messages', 'm') const [dbContributions, count] = await findContributions(
.where({ confirmedAt: IsNull() }) order,
.andWhere({ deniedAt: IsNull() }) currentPage,
.getMany() pageSize,
true,
statusFilter,
)
if (contributions.length === 0) { return new ContributionListResult(
return [] count,
} dbContributions.map((contribution) => new Contribution(contribution, contribution.user)),
)
const userIds = contributions.map((p) => p.userId)
const userCreations = await getUserCreations(userIds, clientTimezoneOffset)
const users = await DbUser.find({
where: { id: In(userIds) },
withDeleted: true,
relations: ['emailContact'],
})
return contributions.map((contribution) => {
const user = users.find((u) => u.id === contribution.userId)
const creation = userCreations.find((c) => c.id === contribution.userId)
return new UnconfirmedContribution(
contribution,
user,
creation ? creation.creations : FULL_CREATION_AVAILABLE,
)
})
} }
@Authorized([RIGHTS.ADMIN_DELETE_CONTRIBUTION]) @Authorized([RIGHTS.ADMIN_DELETE_CONTRIBUTION])

View File

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

View File

@ -53,65 +53,81 @@ afterAll(async () => {
describe('TransactionLinkResolver', () => { describe('TransactionLinkResolver', () => {
describe('createTransactionLink', () => { describe('createTransactionLink', () => {
beforeAll(async () => { describe('unauthenticated', () => {
await mutate({ it('throws an error', async () => {
mutation: login, jest.clearAllMocks()
variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, resetToken()
await expect(
mutate({ mutation: createTransactionLink, variables: { amount: 0, memo: 'Test' } }),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
}) })
}) })
it('throws error when amount is zero', async () => { describe('authenticated', () => {
jest.clearAllMocks() beforeAll(async () => {
await expect( await mutate({
mutate({ mutation: login,
mutation: createTransactionLink, variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
variables: { })
amount: 0,
memo: 'Test',
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('Amount must be a positive number')],
}) })
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(0))
})
it('throws error when amount is negative', async () => { it('throws error when amount is zero', async () => {
jest.clearAllMocks() jest.clearAllMocks()
await expect( await expect(
mutate({ mutate({
mutation: createTransactionLink, mutation: createTransactionLink,
variables: { variables: {
amount: -10, amount: 0,
memo: 'Test', memo: 'Test',
}, },
}), }),
).resolves.toMatchObject({ ).resolves.toMatchObject({
errors: [new GraphQLError('Amount must be a positive number')], errors: [new GraphQLError('Amount must be a positive number')],
})
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(0))
}) })
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(-10))
})
it('throws error when user has not enough GDD', async () => { it('throws error when amount is negative', async () => {
jest.clearAllMocks() jest.clearAllMocks()
await expect( await expect(
mutate({ mutate({
mutation: createTransactionLink, mutation: createTransactionLink,
variables: { variables: {
amount: 1001, amount: -10,
memo: 'Test', memo: 'Test',
}, },
}), }),
).resolves.toMatchObject({ ).resolves.toMatchObject({
errors: [new GraphQLError('User has not enough GDD')], errors: [new GraphQLError('Amount must be a positive number')],
})
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(-10))
})
it('throws error when user has not enough GDD', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: createTransactionLink,
variables: {
amount: 1001,
memo: 'Test',
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('User has not enough GDD')],
})
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('User has not enough GDD', expect.any(Number))
}) })
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('User has not enough GDD', expect.any(Number))
}) })
}) })
@ -121,236 +137,37 @@ describe('TransactionLinkResolver', () => {
resetToken() resetToken()
}) })
describe('contributionLink', () => { describe('unauthenticated', () => {
describe('input not valid', () => { it('throws an error', async () => {
beforeAll(async () => { jest.clearAllMocks()
await mutate({ resetToken()
mutation: login, await expect(
variables: { email: 'peter@lustig.de', password: 'Aa12345_' }, mutate({ mutation: redeemTransactionLink, variables: { code: 'CL-123456' } }),
}) ).resolves.toEqual(
}) expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
it('throws error when link does not exists', async () => { }),
jest.clearAllMocks() )
await expect(
mutate({
mutation: redeemTransactionLink,
variables: {
code: 'CL-123456',
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('Creation from contribution link was not successful')],
})
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'No contribution link found to given code',
'CL-123456',
)
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error('No contribution link found to given code'),
)
})
const now = new Date()
const validFrom = new Date(now.getFullYear() + 1, 0, 1)
it('throws error when link is not valid yet', async () => {
jest.clearAllMocks()
const {
data: { createContributionLink: contributionLink },
} = await mutate({
mutation: createContributionLink,
variables: {
amount: new Decimal(5),
name: 'Daily Contribution Link',
memo: 'Thank you for contribute daily to the community',
cycle: 'DAILY',
validFrom: validFrom.toISOString(),
validTo: new Date(now.getFullYear() + 1, 11, 31, 23, 59, 59, 999).toISOString(),
maxAmountPerMonth: new Decimal(200),
maxPerCycle: 1,
},
})
await expect(
mutate({
mutation: redeemTransactionLink,
variables: {
code: 'CL-' + contributionLink.code,
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('Creation from contribution link was not successful')],
})
await resetEntity(DbContributionLink)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Contribution link is not valid yet', validFrom)
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error('Contribution link is not valid yet'),
)
})
it('throws error when contributionLink cycle is invalid', async () => {
jest.clearAllMocks()
const now = new Date()
const {
data: { createContributionLink: contributionLink },
} = await mutate({
mutation: createContributionLink,
variables: {
amount: new Decimal(5),
name: 'Daily Contribution Link',
memo: 'Thank you for contribute daily to the community',
cycle: 'INVALID',
validFrom: new Date(now.getFullYear(), 0, 1).toISOString(),
validTo: new Date(now.getFullYear(), 11, 31, 23, 59, 59, 999).toISOString(),
maxAmountPerMonth: new Decimal(200),
maxPerCycle: 1,
},
})
await expect(
mutate({
mutation: redeemTransactionLink,
variables: {
code: 'CL-' + contributionLink.code,
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('Creation from contribution link was not successful')],
})
await resetEntity(DbContributionLink)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Contribution link has unknown cycle', 'INVALID')
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error('Contribution link has unknown cycle'),
)
})
const validTo = new Date(now.getFullYear() - 1, 11, 31, 23, 59, 59, 0)
it('throws error when link is no longer valid', async () => {
jest.clearAllMocks()
const {
data: { createContributionLink: contributionLink },
} = await mutate({
mutation: createContributionLink,
variables: {
amount: new Decimal(5),
name: 'Daily Contribution Link',
memo: 'Thank you for contribute daily to the community',
cycle: 'DAILY',
validFrom: new Date(now.getFullYear() - 1, 0, 1).toISOString(),
validTo: validTo.toISOString(),
maxAmountPerMonth: new Decimal(200),
maxPerCycle: 1,
},
})
await expect(
mutate({
mutation: redeemTransactionLink,
variables: {
code: 'CL-' + contributionLink.code,
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('Creation from contribution link was not successful')],
})
await resetEntity(DbContributionLink)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Contribution link is no longer valid', validTo)
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error('Contribution link is no longer valid'),
)
})
}) })
})
// TODO: have this test separated into a transactionLink and a contributionLink part describe('authenticated', () => {
describe('redeem daily Contribution Link', () => { describe('contributionLink', () => {
const now = new Date() describe('input not valid', () => {
let contributionLink: DbContributionLink | undefined
let contribution: UnconfirmedContribution | undefined
beforeAll(async () => {
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
await mutate({
mutation: createContributionLink,
variables: {
amount: new Decimal(5),
name: 'Daily Contribution Link',
memo: 'Thank you for contribute daily to the community',
cycle: 'DAILY',
validFrom: new Date(now.getFullYear(), 0, 1).toISOString(),
validTo: new Date(now.getFullYear(), 11, 31, 23, 59, 59, 999).toISOString(),
maxAmountPerMonth: new Decimal(200),
maxPerCycle: 1,
},
})
})
it('has a daily contribution link in the database', async () => {
const cls = await DbContributionLink.find()
expect(cls).toHaveLength(1)
contributionLink = cls[0]
expect(contributionLink).toEqual(
expect.objectContaining({
id: expect.any(Number),
name: 'Daily Contribution Link',
memo: 'Thank you for contribute daily to the community',
validFrom: new Date(now.getFullYear(), 0, 1),
validTo: new Date(now.getFullYear(), 11, 31, 23, 59, 59, 0),
cycle: 'DAILY',
maxPerCycle: 1,
totalMaxCountOfContribution: null,
maxAccountBalance: null,
minGapHours: null,
createdAt: expect.any(Date),
deletedAt: null,
code: expect.stringMatching(/^[0-9a-f]{24,24}$/),
linkEnabled: true,
amount: expect.decimalEqual(5),
maxAmountPerMonth: expect.decimalEqual(200),
}),
)
})
describe('user has pending contribution of 1000 GDD', () => {
beforeAll(async () => { beforeAll(async () => {
await mutate({ await mutate({
mutation: login, mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
}) })
const result = await mutate({
mutation: createContribution,
variables: {
amount: new Decimal(1000),
memo: 'I was brewing potions for the community the whole month',
creationDate: now.toISOString(),
},
})
contribution = result.data.createContribution
}) })
it('does not allow the user to redeem the contribution link', async () => { it('throws error when link does not exists', async () => {
jest.clearAllMocks() jest.clearAllMocks()
await expect( await expect(
mutate({ mutate({
mutation: redeemTransactionLink, mutation: redeemTransactionLink,
variables: { variables: {
code: 'CL-' + (contributionLink ? contributionLink.code : ''), code: 'CL-123456',
}, },
}), }),
).resolves.toMatchObject({ ).resolves.toMatchObject({
@ -359,85 +176,247 @@ describe('TransactionLinkResolver', () => {
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'No contribution link found to given code',
'CL-123456',
)
expect(logger.error).toBeCalledWith( expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful', 'Creation from contribution link was not successful',
new Error( new Error('No contribution link found to given code'),
'The amount (5 GDD) to be created exceeds the amount (0 GDD) still available for this month.',
),
) )
}) })
})
describe('user has no pending contributions that would not allow to redeem the link', () => { const now = new Date()
beforeAll(async () => { const validFrom = new Date(now.getFullYear() + 1, 0, 1)
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
await mutate({
mutation: updateContribution,
variables: {
contributionId: contribution ? contribution.id : -1,
amount: new Decimal(800),
memo: 'I was brewing potions for the community the whole month',
creationDate: now.toISOString(),
},
})
})
it('allows the user to redeem the contribution link', async () => { it('throws error when link is not valid yet', async () => {
await expect(
mutate({
mutation: redeemTransactionLink,
variables: {
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
},
}),
).resolves.toMatchObject({
data: {
redeemTransactionLink: true,
},
errors: undefined,
})
})
it('does not allow the user to redeem the contribution link a second time on the same day', async () => {
jest.clearAllMocks() jest.clearAllMocks()
const {
data: { createContributionLink: contributionLink },
} = await mutate({
mutation: createContributionLink,
variables: {
amount: new Decimal(5),
name: 'Daily Contribution Link',
memo: 'Thank you for contribute daily to the community',
cycle: 'DAILY',
validFrom: validFrom.toISOString(),
validTo: new Date(now.getFullYear() + 1, 11, 31, 23, 59, 59, 999).toISOString(),
maxAmountPerMonth: new Decimal(200),
maxPerCycle: 1,
},
})
await expect( await expect(
mutate({ mutate({
mutation: redeemTransactionLink, mutation: redeemTransactionLink,
variables: { variables: {
code: 'CL-' + (contributionLink ? contributionLink.code : ''), code: 'CL-' + contributionLink.code,
}, },
}), }),
).resolves.toMatchObject({ ).resolves.toMatchObject({
errors: [new GraphQLError('Creation from contribution link was not successful')], errors: [new GraphQLError('Creation from contribution link was not successful')],
}) })
await resetEntity(DbContributionLink)
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Contribution link is not valid yet', validFrom)
expect(logger.error).toBeCalledWith( expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful', 'Creation from contribution link was not successful',
new Error('Contribution link already redeemed today'), new Error('Contribution link is not valid yet'),
) )
}) })
describe('after one day', () => { it('throws error when contributionLink cycle is invalid', async () => {
jest.clearAllMocks()
const now = new Date()
const {
data: { createContributionLink: contributionLink },
} = await mutate({
mutation: createContributionLink,
variables: {
amount: new Decimal(5),
name: 'Daily Contribution Link',
memo: 'Thank you for contribute daily to the community',
cycle: 'INVALID',
validFrom: new Date(now.getFullYear(), 0, 1).toISOString(),
validTo: new Date(now.getFullYear(), 11, 31, 23, 59, 59, 999).toISOString(),
maxAmountPerMonth: new Decimal(200),
maxPerCycle: 1,
},
})
await expect(
mutate({
mutation: redeemTransactionLink,
variables: {
code: 'CL-' + contributionLink.code,
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('Creation from contribution link was not successful')],
})
await resetEntity(DbContributionLink)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Contribution link has unknown cycle', 'INVALID')
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error('Contribution link has unknown cycle'),
)
})
const validTo = new Date(now.getFullYear() - 1, 11, 31, 23, 59, 59, 0)
it('throws error when link is no longer valid', async () => {
jest.clearAllMocks()
const {
data: { createContributionLink: contributionLink },
} = await mutate({
mutation: createContributionLink,
variables: {
amount: new Decimal(5),
name: 'Daily Contribution Link',
memo: 'Thank you for contribute daily to the community',
cycle: 'DAILY',
validFrom: new Date(now.getFullYear() - 1, 0, 1).toISOString(),
validTo: validTo.toISOString(),
maxAmountPerMonth: new Decimal(200),
maxPerCycle: 1,
},
})
await expect(
mutate({
mutation: redeemTransactionLink,
variables: {
code: 'CL-' + contributionLink.code,
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('Creation from contribution link was not successful')],
})
await resetEntity(DbContributionLink)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('Contribution link is no longer valid', validTo)
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error('Contribution link is no longer valid'),
)
})
})
// TODO: have this test separated into a transactionLink and a contributionLink part
describe('redeem daily Contribution Link', () => {
const now = new Date()
let contributionLink: DbContributionLink | undefined
let contribution: UnconfirmedContribution | undefined
beforeAll(async () => {
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
await mutate({
mutation: createContributionLink,
variables: {
amount: new Decimal(5),
name: 'Daily Contribution Link',
memo: 'Thank you for contribute daily to the community',
cycle: 'DAILY',
validFrom: new Date(now.getFullYear(), 0, 1).toISOString(),
validTo: new Date(now.getFullYear(), 11, 31, 23, 59, 59, 999).toISOString(),
maxAmountPerMonth: new Decimal(200),
maxPerCycle: 1,
},
})
})
it('has a daily contribution link in the database', async () => {
const cls = await DbContributionLink.find()
expect(cls).toHaveLength(1)
contributionLink = cls[0]
expect(contributionLink).toEqual(
expect.objectContaining({
id: expect.any(Number),
name: 'Daily Contribution Link',
memo: 'Thank you for contribute daily to the community',
validFrom: new Date(now.getFullYear(), 0, 1),
validTo: new Date(now.getFullYear(), 11, 31, 23, 59, 59, 0),
cycle: 'DAILY',
maxPerCycle: 1,
totalMaxCountOfContribution: null,
maxAccountBalance: null,
minGapHours: null,
createdAt: expect.any(Date),
deletedAt: null,
code: expect.stringMatching(/^[0-9a-f]{24,24}$/),
linkEnabled: true,
amount: expect.decimalEqual(5),
maxAmountPerMonth: expect.decimalEqual(200),
}),
)
})
describe('user has pending contribution of 1000 GDD', () => {
beforeAll(async () => { beforeAll(async () => {
jest.useFakeTimers()
setTimeout(jest.fn(), 1000 * 60 * 60 * 24)
jest.runAllTimers()
await mutate({ await mutate({
mutation: login, mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
}) })
const result = await mutate({
mutation: createContribution,
variables: {
amount: new Decimal(1000),
memo: 'I was brewing potions for the community the whole month',
creationDate: now.toISOString(),
},
})
contribution = result.data.createContribution
}) })
afterAll(() => { it('does not allow the user to redeem the contribution link', async () => {
jest.useRealTimers() jest.clearAllMocks()
await expect(
mutate({
mutation: redeemTransactionLink,
variables: {
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('Creation from contribution link was not successful')],
})
}) })
it('allows the user to redeem the contribution link again', async () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error(
'The amount to be created exceeds the amount still available for this month',
),
)
})
})
describe('user has no pending contributions that would not allow to redeem the link', () => {
beforeAll(async () => {
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
await mutate({
mutation: updateContribution,
variables: {
contributionId: contribution ? contribution.id : -1,
amount: new Decimal(800),
memo: 'I was brewing potions for the community the whole month',
creationDate: now.toISOString(),
},
})
})
it('allows the user to redeem the contribution link', async () => {
await expect( await expect(
mutate({ mutate({
mutation: redeemTransactionLink, mutation: redeemTransactionLink,
@ -473,6 +452,59 @@ describe('TransactionLinkResolver', () => {
new Error('Contribution link already redeemed today'), new Error('Contribution link already redeemed today'),
) )
}) })
describe('after one day', () => {
beforeAll(async () => {
jest.useFakeTimers()
setTimeout(jest.fn(), 1000 * 60 * 60 * 24)
jest.runAllTimers()
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
})
afterAll(() => {
jest.useRealTimers()
})
it('allows the user to redeem the contribution link again', async () => {
await expect(
mutate({
mutation: redeemTransactionLink,
variables: {
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
},
}),
).resolves.toMatchObject({
data: {
redeemTransactionLink: true,
},
errors: undefined,
})
})
it('does not allow the user to redeem the contribution link a second time on the same day', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: redeemTransactionLink,
variables: {
code: 'CL-' + (contributionLink ? contributionLink.code : ''),
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('Creation from contribution link was not successful')],
})
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error('Contribution link already redeemed today'),
)
})
})
}) })
}) })
}) })

View File

@ -86,8 +86,8 @@ export class TransactionLinkResolver {
transactionLink.code = transactionLinkCode(createdDate) transactionLink.code = transactionLinkCode(createdDate)
transactionLink.createdAt = createdDate transactionLink.createdAt = createdDate
transactionLink.validUntil = validUntil transactionLink.validUntil = validUntil
await DbTransactionLink.save(transactionLink).catch(() => { await DbTransactionLink.save(transactionLink).catch((e) => {
throw new Error('Unable to save transaction link') throw new LogError('Unable to save transaction link', e)
}) })
return new TransactionLink(transactionLink, new User(user)) return new TransactionLink(transactionLink, new User(user))
@ -103,19 +103,23 @@ export class TransactionLinkResolver {
const transactionLink = await DbTransactionLink.findOne({ id }) const transactionLink = await DbTransactionLink.findOne({ id })
if (!transactionLink) { if (!transactionLink) {
throw new Error('Transaction Link not found!') throw new LogError('Transaction link not found', id)
} }
if (transactionLink.userId !== user.id) { if (transactionLink.userId !== user.id) {
throw new Error('Transaction Link cannot be deleted!') throw new LogError(
'Transaction link cannot be deleted by another user',
transactionLink.userId,
user.id,
)
} }
if (transactionLink.redeemedBy) { if (transactionLink.redeemedBy) {
throw new Error('Transaction Link already redeemed!') throw new LogError('Transaction link already redeemed', transactionLink.redeemedBy)
} }
await transactionLink.softRemove().catch(() => { await transactionLink.softRemove().catch((e) => {
throw new Error('Transaction Link could not be deleted!') throw new LogError('Transaction link could not be deleted', e)
}) })
return true return true
@ -312,18 +316,18 @@ export class TransactionLinkResolver {
) )
if (user.id === linkedUser.id) { if (user.id === linkedUser.id) {
throw new Error('Cannot redeem own transaction link.') throw new LogError('Cannot redeem own transaction link', user.id)
} }
// TODO: The now check should be done within the semaphore lock, // TODO: The now check should be done within the semaphore lock,
// since the program might wait a while till it is ready to proceed // since the program might wait a while till it is ready to proceed
// writing the transaction. // writing the transaction.
if (transactionLink.validUntil.getTime() < now.getTime()) { if (transactionLink.validUntil.getTime() < now.getTime()) {
throw new Error('Transaction Link is not valid anymore.') throw new LogError('Transaction link is not valid anymore', transactionLink.validUntil)
} }
if (transactionLink.redeemedBy) { if (transactionLink.redeemedBy) {
throw new Error('Transaction Link already redeemed.') throw new LogError('Transaction link already redeemed', transactionLink.redeemedBy)
} }
await executeTransaction( await executeTransaction(

View File

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

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@ import { User as dbUser } from '@entity/User'
import { Transaction as dbTransaction } from '@entity/Transaction' import { Transaction as dbTransaction } from '@entity/Transaction'
import Decimal from 'decimal.js-light' import Decimal from 'decimal.js-light'
import { ExpressContext } from 'apollo-server-express' import { ExpressContext } from 'apollo-server-express'
import LogError from './LogError'
export interface Context { export interface Context {
token: string | null token: string | null
@ -35,7 +36,7 @@ const context = (args: ExpressContext): Context => {
export const getUser = (context: Context): dbUser => { export const getUser = (context: Context): dbUser => {
if (context.user) return context.user if (context.user) return context.user
throw new Error('No user given in context!') throw new LogError('No user given in context')
} }
export const getClientTimezoneOffset = (context: Context): number => { export const getClientTimezoneOffset = (context: Context): number => {
@ -45,7 +46,7 @@ export const getClientTimezoneOffset = (context: Context): number => {
) { ) {
return context.clientTimezoneOffset return context.clientTimezoneOffset
} }
throw new Error('No valid client time zone offset in context!') throw new LogError('No valid client time zone offset in context')
} }
export default context export default context

View File

@ -1,6 +1,7 @@
import Decimal from 'decimal.js-light' import Decimal from 'decimal.js-light'
import CONFIG from '@/config' import CONFIG from '@/config'
import { Decay } from '@model/Decay' import { Decay } from '@model/Decay'
import LogError from '@/server/LogError'
// TODO: externalize all those definitions and functions into an external decay library // TODO: externalize all those definitions and functions into an external decay library
@ -22,7 +23,7 @@ function calculateDecay(
const startBlockMs = startBlock.getTime() const startBlockMs = startBlock.getTime()
if (toMs < fromMs) { if (toMs < fromMs) {
throw new Error('to < from, reverse decay calculation is invalid') throw new LogError('calculateDecay: to < from, reverse decay calculation is invalid')
} }
// Initialize with no decay // Initialize with no decay

View File

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

View File

@ -32,15 +32,6 @@ export const startDHT = async (topic: string): Promise<void> => {
logger.debug(`keyPairDHT: secretKey=${keyPair.secretKey.toString('hex')}`) logger.debug(`keyPairDHT: secretKey=${keyPair.secretKey.toString('hex')}`)
const ownApiVersions = writeHomeCommunityEnries(keyPair.publicKey) 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)}`) logger.debug(`ApiList: ${JSON.stringify(ownApiVersions)}`)
const node = new DHT({ keyPair }) const node = new DHT({ keyPair })
@ -216,6 +207,5 @@ async function writeHomeCommunityEnries(pubKey: any): Promise<CommunityApi[]> {
} catch (err) { } catch (err) {
throw new Error(`Federation: Error writing HomeCommunity-Entries: ${err}`) throw new Error(`Federation: Error writing HomeCommunity-Entries: ${err}`)
} }
return homeApiVersions return homeApiVersions
} }

View File

@ -84,6 +84,29 @@ services:
- ./dht-node:/app - ./dht-node:/app
- ./database:/database - ./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 ############################################## # DATABASE ##############################################
######################################################## ########################################################
@ -155,5 +178,8 @@ volumes:
dht_node_modules: dht_node_modules:
dht_database_node_modules: dht_database_node_modules:
dht_database_build: dht_database_build:
federation_node_modules:
federation_database_node_modules:
federation_database_build:
database_node_modules: database_node_modules:
database_build: database_build:

View File

@ -36,6 +36,21 @@ services:
- NODE_ENV="test" - NODE_ENV="test"
- DB_HOST=mariadb - 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 ############################################# # DATABASE #############################################
######################################################## ########################################################

View File

@ -147,6 +147,42 @@ services:
# <host_machine_directory>:<container_directory> mirror bidirectional path in local context with path in Docker container # <host_machine_directory>:<container_directory> mirror bidirectional path in local context with path in Docker container
- ./logs/dht-node:/logs/dht-node - ./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 ############################################# # 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", "build": "tsc --build",
"clean": "tsc --build --clean", "clean": "tsc --build --clean",
"start": "cross-env TZ=UTC TS_NODE_BASEURL=./build node -r tsconfig-paths/register build/src/index.js", "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", "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 ." "lint": "eslint --max-warnings=0 --ext .js,.ts ."
}, },
@ -26,15 +27,16 @@
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"log4js": "^6.7.1", "log4js": "^6.7.1",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.1.1",
"type-graphql": "^1.1.1" "type-graphql": "^1.1.1"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^4.28.0", "@types/express": "4.17.12",
"@typescript-eslint/parser": "^4.28.0", "@types/jest": "27.0.2",
"@types/lodash.clonedeep": "^4.5.6", "@types/lodash.clonedeep": "^4.5.6",
"@types/node": "^16.10.3", "@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": "^7.29.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-config-standard": "^16.0.3", "eslint-config-standard": "^16.0.3",
@ -42,8 +44,15 @@
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.4.0", "eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-promise": "^5.1.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", "prettier": "^2.3.1",
"typescript": "^4.3.4", "typescript": "^4.3.4"
"nodemon": "^2.0.7" },
"nodemonConfig": {
"ignore": ["**/*.test.ts"]
} }
} }

View File

@ -24,7 +24,7 @@ const constants = {
} }
const server = { const server = {
PORT: process.env.PORT || 5000, PORT: process.env.PORT || 5010,
// JWT_SECRET: process.env.JWT_SECRET || 'secret123', // JWT_SECRET: process.env.JWT_SECRET || 'secret123',
// JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '10m', // JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '10m',
GRAPHIQL: process.env.GRAPHIQL === 'true' || false, GRAPHIQL: process.env.GRAPHIQL === 'true' || false,
@ -73,7 +73,7 @@ if (
const federation = { const federation = {
// FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || null, // FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || null,
// FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || 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_API: process.env.FEDERATION_API || '1_0',
FEDERATION_COMMUNITY_URL: process.env.FEDERATION_COMMUNITY_URL || null, 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 { federationLogger as logger } from '@/server/logger'
import { GetTestApiResult } from '../../GetTestApiResult' import { GetTestApiResult } from '../../GetTestApiResult'
@Resolver() @Resolver()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export class TestResolver { export class TestResolver {
@Query(() => GetTestApiResult) @Query(() => GetTestApiResult)
async test(): Promise<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 { federationLogger as logger } from '@/server/logger'
import { GetTestApiResult } from '../../GetTestApiResult' import { GetTestApiResult } from '../../GetTestApiResult'
@Resolver() @Resolver()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export class TestResolver { export class TestResolver {
@Query(() => GetTestApiResult) @Query(() => GetTestApiResult)
async test(): Promise<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 { federationLogger as logger } from '@/server/logger'
import { GetTestApiResult } from '../../GetTestApiResult' import { GetTestApiResult } from '../../GetTestApiResult'
@Resolver() @Resolver()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export class TestResolver { export class TestResolver {
@Query(() => GetTestApiResult) @Query(() => GetTestApiResult)
async test(): Promise<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' import { Field, ObjectType } from 'type-graphql'
@ObjectType() @ObjectType()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export class GetTestApiResult { export class GetTestApiResult {
constructor(apiVersion: string) { constructor(apiVersion: string) {
this.api = apiVersion 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/*"], // "@enum/*": ["src/graphql/enum/*"],
// "@model/*": ["src/graphql/model/*"], // "@model/*": ["src/graphql/model/*"],
"@repository/*": ["src/typeorm/repository/*"], "@repository/*": ["src/typeorm/repository/*"],
// "@test/*": ["test/*"], "@test/*": ["test/*"],
/* external */ /* external */
"@typeorm/*": ["../backend/src/typeorm/*", "../../backend/src/typeorm/*"], "@typeorm/*": ["../backend/src/typeorm/*", "../../backend/src/typeorm/*"],
"@dbTools/*": ["../database/src/*", "../../database/build/src/*"], "@dbTools/*": ["../database/src/*", "../../database/build/src/*"],
"@entity/*": ["../database/entity/*", "../../database/build/entity/*"] "@entity/*": ["../database/entity/*", "../../database/build/entity/*"]
}, },
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "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. */ // "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. */ // "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'. */ "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 :toggleable="false" class="pr-4">
<b-navbar-brand class="d-none d-lg-block"> <b-navbar-brand class="d-none d-lg-block">
<b-img <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" :src="logo"
width="200" width="200"
alt="..." alt="Logo"
/> />
<b-img <b-img
class="imgLogoBack mt--3 ml--3" class="mt--3 ml--3"
src="/img/template/gradido_background_header.png" :src="background_header"
width="230" width="230"
alt="start background image" alt="Background Image"
></b-img> ></b-img>
</b-navbar-brand> </b-navbar-brand>
<b-img class="sheet-img position-absolute d-block d-lg-none zindex1000" :src="sheet"></b-img> <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], mixins: [authLinks],
data() { data() {
return { 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', sheet: '/img/template/Blaetter.png',
} }
}, },

View File

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

View File

@ -2,19 +2,19 @@
<div class="nav-community container"> <div class="nav-community container">
<b-row class="nav-row"> <b-row class="nav-row">
<b-col cols="12" lg="4" md="4" class="px-0"> <b-col cols="12" lg="4" md="4" class="px-0">
<b-btn active-class="btn-active" block variant="link" to="/community#edit"> <b-btn to="contribute" active-class="btn-active" block variant="link">
<b-icon icon="pencil" class="mr-2" /> <b-icon icon="pencil" class="mr-2" />
{{ $t('community.submitContribution') }} {{ $t('community.submitContribution') }}
</b-btn> </b-btn>
</b-col> </b-col>
<b-col cols="12" lg="4" md="4" class="px-0"> <b-col cols="12" lg="4" md="4" class="px-0">
<b-btn active-class="btn-active" block variant="link" to="/community#my"> <b-btn to="contributions" active-class="btn-active" block variant="link">
<b-icon icon="person" class="mr-2" /> <b-icon icon="person" class="mr-2" />
{{ $t('community.myContributions') }} {{ $t('community.myContributions') }}
</b-btn> </b-btn>
</b-col> </b-col>
<b-col cols="12" lg="4" md="4" class="px-0"> <b-col cols="12" lg="4" md="4" class="px-0">
<b-btn active-class="btn-active" block variant="link" to="/community#all"> <b-btn to="community" active-class="btn-active" block variant="link">
<b-icon icon="people" class="mr-2" /> <b-icon icon="people" class="mr-2" />
{{ $t('community.community') }} {{ $t('community.community') }}
</b-btn> </b-btn>

View File

@ -1,76 +1,10 @@
<template> <template>
<div class="contribution-info d-none d-lg-block"> <div class="contribution-info d-none d-lg-block">
<div v-if="hash === '#my'"> <slot :name="$route.params.tab" />
<h4 class="alert-heading">{{ $t('community.myContributions') }}</h4>
<p>
{{ $t('contribution.alert.myContributionNoteList') }}
</p>
<ul>
<li>
<b-icon icon="bell-fill" variant="primary"></b-icon>
{{ $t('contribution.alert.pending') }}
</li>
<li>
<b-icon icon="question-square" variant="warning"></b-icon>
{{ $t('contribution.alert.in_progress') }}
</li>
<li>
<b-icon icon="check" variant="success"></b-icon>
{{ $t('contribution.alert.confirm') }}
</li>
<li>
<b-icon icon="x-circle" variant="warning"></b-icon>
{{ $t('contribution.alert.denied') }}
</li>
<li>
<b-icon icon="trash" variant="danger"></b-icon>
{{ $t('contribution.alert.deleted') }}
</li>
</ul>
</div>
<div v-if="hash === '#all'" show fade variant="secondary" class="text-dark">
<h4 class="alert-heading">{{ $t('navigation.community') }}</h4>
<p>
{{ $t('contribution.alert.communityNoteList') }}
</p>
<ul>
<li>
<b-icon icon="bell-fill" variant="primary"></b-icon>
{{ $t('contribution.alert.pending') }}
</li>
<li>
<b-icon icon="question-square" variant="warning"></b-icon>
{{ $t('contribution.alert.in_progress') }}
</li>
<li>
<b-icon icon="check" variant="success"></b-icon>
{{ $t('contribution.alert.confirm') }}
</li>
<li>
<b-icon icon="x-circle" variant="warning"></b-icon>
{{ $t('contribution.alert.denied') }}
</li>
</ul>
</div>
<div v-if="hash === '#edit'" show fade variant="secondary" class="text-dark">
<div>
<h3>{{ $t('contribution.formText.yourContribution') }}</h3>
{{ $t('contribution.formText.bringYourTalentsTo') }}
<div class="my-3">
<b>{{ $t('contribution.formText.describeYourCommunity') }}</b>
</div>
</div>
</div>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
name: 'ContributionInfo', name: 'ContributionInfo',
computed: {
hash() {
return this.$route.hash
},
},
} }
</script> </script>

View File

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

View File

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

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', name: 'ContentHeader',
computed: { computed: {
path() { path() {
return this.$route.path.replace(/^\//, '') return this.$route.path.replace(/^\/(.+?)(\/.+)?$/, '$1')
}, },
}, },
} }

View File

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

View File

@ -215,7 +215,9 @@ describe('Community', () => {
push: routerPushMock, push: routerPushMock,
}, },
$route: { $route: {
hash: '#edit', params: {
tab: 'contribute',
},
}, },
} }
@ -260,7 +262,11 @@ describe('Community', () => {
}) })
it('check for correct tabIndex if state is "IN_PROGRESS" or not', () => { it('check for correct tabIndex if state is "IN_PROGRESS" or not', () => {
expect(routerPushMock).toBeCalledWith({ path: '/community#my' }) expect(routerPushMock).toBeCalledWith({ params: { tab: 'contributions' } })
})
it('sets tab index to 1', () => {
expect(wrapper.vm.tabIndex).toBe(1)
}) })
it('toasts an info', () => { it('toasts an info', () => {
@ -268,16 +274,6 @@ describe('Community', () => {
}) })
}) })
describe('API calls after creation', () => {
it('has a DIV .community-page', () => {
expect(wrapper.find('div.community-page').exists()).toBe(true)
})
it('emits update transactions', () => {
expect(wrapper.emitted('update-transactions')).toEqual([[0]])
})
})
describe('save contrubtion', () => { describe('save contrubtion', () => {
describe('with error', () => { describe('with error', () => {
const now = new Date().toISOString() const now = new Date().toISOString()
@ -491,6 +487,10 @@ describe('Community', () => {
it('sets tab index back to 0', () => { it('sets tab index back to 0', () => {
expect(wrapper.vm.tabIndex).toBe(0) expect(wrapper.vm.tabIndex).toBe(0)
}) })
it('pushes contribute parameter to router', () => {
expect(routerPushMock).toBeCalledWith({ params: { tab: 'contribute' } })
})
}) })
describe('update list all contributions', () => { describe('update list all contributions', () => {

View File

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

View File

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

View File

@ -49,8 +49,8 @@ describe('router', () => {
expect(routes.find((r) => r.path === '/').redirect()).toEqual({ path: '/login' }) expect(routes.find((r) => r.path === '/').redirect()).toEqual({ path: '/login' })
}) })
it('has sixteen routes defined', () => { it('has 19 routes defined', () => {
expect(routes).toHaveLength(18) expect(routes).toHaveLength(19)
}) })
describe('overview', () => { describe('overview', () => {
@ -75,7 +75,19 @@ describe('router', () => {
}) })
}) })
describe('community', () => { describe('community without tab parameter', () => {
it('requires authorization', () => {
expect(routes.find((r) => r.path === '/community').meta.requiresAuth).toBeTruthy()
})
it('redirects to contribute tab', async () => {
expect(routes.find((r) => r.path === '/community').redirect()).toEqual({
path: '/community/contribute',
})
})
})
describe('community with tab parameter', () => {
it('requires authorization', () => { it('requires authorization', () => {
expect(routes.find((r) => r.path === '/community').meta.requiresAuth).toBeTruthy() expect(routes.find((r) => r.path === '/community').meta.requiresAuth).toBeTruthy()
}) })

View File

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