Merge remote-tracking branch 'origin/master' into 2715-feature-document-the-technical-federation-architecture

This commit is contained in:
Claus-Peter Hübner 2023-03-22 23:17:13 +01:00
commit 664a7bea70
342 changed files with 10690 additions and 4744 deletions

46
.github/file-filters.yml vendored Normal file
View File

@ -0,0 +1,46 @@
# These file filter patterns are used by the action https://github.com/dorny/paths-filter
# more differentiated filters for admin interface, which might be used later
# admin_locales: &admin_locales
# - 'admin/src/locales/**'
# - 'admin/scripts/sort*'
# admin_stylelinting: &admin_stylelinting
# - 'admin/{components,layouts,pages}/**/*.{scss,vue}'
# - 'admin/.stylelintrc.js'
# admin_linting: &admin_linting
# - 'admin/.eslint*'
# - 'admin/babel.config.js'
# - 'admin/package.json'
# - 'admin/**/*.{js,vue}'
# - *admin_locales
# admin_unit_testing: &admin_unit_testing
# - 'admin/package.json'
# - 'admin/{jest,vue}.config.js'
# - 'admin/{public,run,test}/**/*'
# - 'admin/src/!(locales)/**/*'
# admin_docker_building: &admin_docker_building
# - 'admin/.dockerignore'
# - 'admin/Dockerfile'
# - *admin_unit_testing
admin: &admin
- 'admin/**/*'
dht_node: &dht_node
- 'dht-node/**/*'
docker: &docker
- 'docker-compose.*'
federation: &federation
- 'federation/**/*'
frontend: &frontend
- 'frontend/**/*'
nginx: &nginx
- 'nginx/**/*'

58
.github/workflows/e2e-test.yml vendored Normal file
View File

@ -0,0 +1,58 @@
name: Gradido End-to-End Test CI
on: push
jobs:
end-to-end-tests:
name: End-to-End Tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Boot up test system | docker-compose mariadb
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach mariadb
- name: Boot up test system | docker-compose database
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps database
- name: Boot up test system | docker-compose backend
run: |
cd backend
cp .env.test_e2e .env
cd ..
docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps backend
- name: Sleep for 10 seconds
run: sleep 10s
- name: Boot up test system | seed backend
run: |
sudo chown runner:docker -R *
cd database
yarn && yarn dev_reset
cd ../backend
yarn && yarn seed
cd ..
- name: Boot up test system | docker-compose frontends
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps frontend admin nginx
- name: Boot up test system | docker-compose mailserver
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mailserver
- name: Sleep for 15 seconds
run: sleep 15s
- name: End-to-end tests | run tests
id: e2e-tests
run: |
cd e2e-tests/
yarn
yarn run cypress run
- name: End-to-end tests | if tests failed, upload screenshots
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
uses: actions/upload-artifact@v3
with:
name: cypress-screenshots
path: /home/runner/work/gradido/gradido/e2e-tests/cypress/screenshots/

View File

@ -0,0 +1,84 @@
name: Gradido Admin Interface Test CI
on: push
jobs:
# only (but most important) job from this workflow required for pull requests
# check results serve as run conditions for all other jobs here
files-changed:
name: Detect File Changes - Admin Interface
runs-on: ubuntu-latest
outputs:
admin: ${{ steps.changes.outputs.admin }}
steps:
- uses: actions/checkout@v3.3.0
- name: Check for admin interface file changes
uses: dorny/paths-filter@v2.11.1
id: changes
with:
token: ${{ github.token }}
filters: .github/file-filters.yml
list-files: shell
build_test:
if: needs.files-changed.outputs.admin == 'true'
name: Docker Build Test - Admin Interface
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Admin Interface | Build 'test' image
run: docker build --target test -t "gradido/admin:test" admin/ --build-arg NODE_ENV="test"
unit_test:
if: needs.files-changed.outputs.admin == 'true'
name: Unit Tests - Admin Interface
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Admin Interface | Unit tests
run: cd admin && yarn && yarn run test
lint:
if: needs.files-changed.outputs.admin == 'true'
name: Lint - Admin Interface
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Admin Interface | Lint
run: cd admin && yarn && yarn run lint
stylelint:
if: needs.files-changed.outputs.admin == 'true'
name: Stylelint - Admin Interface
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Admin Interface | Stylelint
run: cd admin && yarn && yarn run stylelint
locales:
if: needs.files-changed.outputs.admin == 'true'
name: Locales - Admin Interface
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Admin Interface | Locales
run: cd admin && yarn && yarn run locales

32
.github/workflows/test-nginx.yml vendored Normal file
View File

@ -0,0 +1,32 @@
name: Gradido Nginx Test CI
on: push
jobs:
files-changed:
name: Detect File Changes - Nginx
runs-on: ubuntu-latest
outputs:
nginx: ${{ steps.changes.outputs.nginx }}
steps:
- uses: actions/checkout@v3.3.0
- name: Check for nginx file changes
uses: dorny/paths-filter@v2.11.1
id: changes
with:
token: ${{ github.token }}
filters: .github/file-filters.yml
list-files: shell
build_test_nginx:
name: Docker Build Test - Nginx
if: needs.files-changed.outputs.nginx == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: nginx | Build 'test' image
run: docker build -t "gradido/nginx:test" nginx/

View File

@ -3,57 +3,6 @@ name: gradido test CI
on: push
jobs:
##############################################################################
# JOB: DOCKER BUILD TEST FRONTEND ############################################
##############################################################################
build_test_frontend:
name: Docker Build Test - Frontend
runs-on: ubuntu-latest
#needs: [nothing]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# FRONTEND ###############################################################
##########################################################################
- name: Frontend | Build `test` image
run: |
docker build --target test -t "gradido/frontend:test" frontend/
docker save "gradido/frontend:test" > /tmp/frontend.tar
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: docker-frontend-test
path: /tmp/frontend.tar
##############################################################################
# JOB: DOCKER BUILD TEST ADMIN INTERFACE #####################################
##############################################################################
build_test_admin:
name: Docker Build Test - Admin Interface
runs-on: ubuntu-latest
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# ADMIN INTERFACE ########################################################
##########################################################################
- name: Admin | Build `test` image
run: |
docker build --target test -t "gradido/admin:test" admin/ --build-arg NODE_ENV="test"
docker save "gradido/admin:test" > /tmp/admin.tar
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: docker-admin-test
path: /tmp/admin.tar
##############################################################################
# JOB: DOCKER BUILD TEST BACKEND #############################################
##############################################################################
@ -132,212 +81,12 @@ jobs:
name: docker-mariadb-test
path: /tmp/mariadb.tar
##############################################################################
# JOB: DOCKER BUILD TEST NGINX ###############################################
##############################################################################
build_test_nginx:
name: Docker Build Test - Nginx
runs-on: ubuntu-latest
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# BUILD NGINX DOCKER IMAGE ###############################################
##########################################################################
- name: nginx | Build `test` image
run: |
docker build -t "gradido/nginx:test" nginx/
docker save "gradido/nginx:test" > /tmp/nginx.tar
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: docker-nginx-test
path: /tmp/nginx.tar
##############################################################################
# JOB: LOCALES FRONTEND ######################################################
##############################################################################
locales_frontend:
name: Locales - Frontend
runs-on: ubuntu-latest
needs: [build_test_frontend]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Frontend)
uses: actions/download-artifact@v3
with:
name: docker-frontend-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/frontend.tar
##########################################################################
# LOCALES FRONTEND #######################################################
##########################################################################
- name: Frontend | Locales
run: docker run --rm gradido/frontend:test yarn run locales
##############################################################################
# JOB: LINT FRONTEND #########################################################
##############################################################################
lint_frontend:
name: Lint - Frontend
runs-on: ubuntu-latest
needs: [build_test_frontend]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Frontend)
uses: actions/download-artifact@v3
with:
name: docker-frontend-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/frontend.tar
##########################################################################
# LINT FRONTEND ##########################################################
##########################################################################
- name: Frontend | Lint
run: docker run --rm gradido/frontend:test yarn run lint
##############################################################################
# JOB: STYLELINT FRONTEND ####################################################
##############################################################################
stylelint_frontend:
name: Stylelint - Frontend
runs-on: ubuntu-latest
needs: [build_test_frontend]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Frontend)
uses: actions/download-artifact@v3
with:
name: docker-frontend-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/frontend.tar
##########################################################################
# STYLELINT FRONTEND #####################################################
##########################################################################
- name: Frontend | Stylelint
run: docker run --rm gradido/frontend:test yarn run stylelint
##############################################################################
# JOB: LINT ADMIN INTERFACE ##################################################
##############################################################################
lint_admin:
name: Lint - Admin Interface
runs-on: ubuntu-latest
needs: [build_test_admin]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Admin Interface)
uses: actions/download-artifact@v3
with:
name: docker-admin-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/admin.tar
##########################################################################
# LINT ADMIN INTERFACE ###################################################
##########################################################################
- name: Admin Interface | Lint
run: docker run --rm gradido/admin:test yarn run lint
##############################################################################
# JOB: STYLELINT ADMIN INTERFACE ##############################################
##############################################################################
stylelint_admin:
name: Stylelint - Admin Interface
runs-on: ubuntu-latest
needs: [build_test_admin]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Admin Interface)
uses: actions/download-artifact@v3
with:
name: docker-admin-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/admin.tar
##########################################################################
# STYLELINT ADMIN INTERFACE ##############################################
##########################################################################
- name: Admin Interface | Stylelint
run: docker run --rm gradido/admin:test yarn run stylelint
##############################################################################
# JOB: LOCALES ADMIN #########################################################
##############################################################################
locales_admin:
name: Locales - Admin Interface
runs-on: ubuntu-latest
needs: [build_test_admin]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Admin Interface)
uses: actions/download-artifact@v3
with:
name: docker-admin-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/admin.tar
##########################################################################
# LOCALES FRONTEND #######################################################
##########################################################################
- name: admin | Locales
run: docker run --rm gradido/admin:test yarn run locales
##############################################################################
# JOB: LINT BACKEND ##########################################################
##############################################################################
lint_backend:
name: Lint - Backend
runs-on: ubuntu-latest
needs: [build_test_backend]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
@ -345,20 +94,10 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Backend)
uses: actions/download-artifact@v3
with:
name: docker-backend-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/backend.tar
##########################################################################
# LINT BACKEND ###########################################################
##########################################################################
- name: backend | Lint
run: docker run --rm gradido/backend:test yarn run lint
run: cd database && yarn && cd ../backend && yarn && yarn run lint
##############################################################################
# JOB: LOCALES BACKEND #######################################################
@ -366,7 +105,6 @@ jobs:
locales_backend:
name: Locales - Backend
runs-on: ubuntu-latest
needs: [build_test_backend]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
@ -385,7 +123,6 @@ jobs:
lint_database_up:
name: Lint - Database Up
runs-on: ubuntu-latest
needs: [build_test_database_up]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
@ -393,112 +130,10 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Backend)
uses: actions/download-artifact@v3
with:
name: docker-database-test_up
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/database_up.tar
##########################################################################
# LINT DATABASE ##########################################################
##########################################################################
- name: database | Lint
run: docker run --rm gradido/database:test_up yarn run lint
##############################################################################
# JOB: UNIT TEST FRONTEND ###################################################
##############################################################################
unit_test_frontend:
name: Unit tests - Frontend
runs-on: ubuntu-latest
needs: [build_test_frontend]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGES #################################################
##########################################################################
- name: Download Docker Image (Frontend)
uses: actions/download-artifact@v3
with:
name: docker-frontend-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/frontend.tar
##########################################################################
# UNIT TESTS FRONTEND ####################################################
##########################################################################
- name: frontend | Unit tests
run: |
docker run --env NODE_ENV=test -v ~/coverage:/app/coverage --rm gradido/frontend:test yarn run test
cp -r ~/coverage ./coverage
##########################################################################
# COVERAGE REPORT FRONTEND ###############################################
##########################################################################
#- name: frontend | Coverage report
# uses: romeovs/lcov-reporter-action@v0.2.21
# with:
# github-token: ${{ secrets.GITHUB_TOKEN }}
# lcov-file: ./coverage/lcov.info
##########################################################################
# COVERAGE CHECK FRONTEND ################################################
##########################################################################
- name: frontend | Coverage check
uses: webcraftmedia/coverage-check-action@master
with:
report_name: Coverage Frontend
type: lcov
result_path: ./coverage/lcov.info
min_coverage: 95
token: ${{ github.token }}
##############################################################################
# JOB: UNIT TEST ADMIN INTERFACE #############################################
##############################################################################
unit_test_admin:
name: Unit tests - Admin Interface
runs-on: ubuntu-latest
needs: [build_test_admin]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGES #################################################
##########################################################################
- name: Download Docker Image (Admin Interface)
uses: actions/download-artifact@v3
with:
name: docker-admin-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/admin.tar
##########################################################################
# UNIT TESTS ADMIN INTERFACE #############################################
##########################################################################
- name: Admin Interface | Unit tests
run: |
docker run -v ~/coverage:/app/coverage --rm gradido/admin:test yarn run test
cp -r ~/coverage ./coverage
##########################################################################
# COVERAGE CHECK ADMIN INTERFACE #########################################
##########################################################################
- name: Admin Interface | Coverage check
uses: webcraftmedia/coverage-check-action@master
with:
report_name: Coverage Admin Interface
type: lcov
result_path: ./coverage/lcov.info
min_coverage: 96
token: ${{ github.token }}
- name: Database | Lint
run: cd database && yarn && yarn run lint
##############################################################################
# JOB: UNIT TEST BACKEND ####################################################
@ -535,18 +170,6 @@ jobs:
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps database
- name: backend Unit tests | test
run: cd database && yarn && yarn build && cd ../backend && yarn && yarn test
# run: docker-compose -f docker-compose.yml -f docker-compose.test.yml exec -T backend yarn test
##########################################################################
# COVERAGE CHECK BACKEND #################################################
##########################################################################
- name: backend | Coverage check
uses: webcraftmedia/coverage-check-action@master
with:
report_name: Coverage Backend
type: lcov
result_path: ./backend/coverage/lcov.info
min_coverage: 80
token: ${{ github.token }}
##########################################################################
# DATABASE MIGRATION TEST UP + RESET #####################################
@ -570,108 +193,3 @@ jobs:
run: docker-compose -f docker-compose.yml run -T database yarn up
- name: database | reset
run: docker-compose -f docker-compose.yml run -T database yarn reset
##############################################################################
# JOB: END-TO-END TESTS #####################################################
##############################################################################
end-to-end-tests:
name: End-to-End Tests
runs-on: ubuntu-latest
needs: [build_test_mariadb, build_test_database_up, build_test_backend, build_test_admin, build_test_frontend, build_test_nginx]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGES #################################################
##########################################################################
- name: Download Docker Image (Mariadb)
uses: actions/download-artifact@v3
with:
name: docker-mariadb-test
path: /tmp
- name: Load Docker Image (Mariadb)
run: docker load < /tmp/mariadb.tar
- name: Download Docker Image (Database Up)
uses: actions/download-artifact@v3
with:
name: docker-database-test_up
path: /tmp
- name: Load Docker Image (Database Up)
run: docker load < /tmp/database_up.tar
- name: Download Docker Image (Backend)
uses: actions/download-artifact@v3
with:
name: docker-backend-test
path: /tmp
- name: Load Docker Image (Backend)
run: docker load < /tmp/backend.tar
- name: Download Docker Image (Frontend)
uses: actions/download-artifact@v3
with:
name: docker-frontend-test
path: /tmp
- name: Load Docker Image (Frontend)
run: docker load < /tmp/frontend.tar
- name: Download Docker Image (Admin Interface)
uses: actions/download-artifact@v3
with:
name: docker-admin-test
path: /tmp
- name: Load Docker Image (Admin Interface)
run: docker load < /tmp/admin.tar
- name: Download Docker Image (Nginx)
uses: actions/download-artifact@v3
with:
name: docker-nginx-test
path: /tmp
- name: Load Docker Image (Nginx)
run: docker load < /tmp/nginx.tar
##########################################################################
# BOOT UP THE TEST SYSTEM ################################################
##########################################################################
- name: Boot up test system | docker-compose mariadb
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach mariadb
- name: Boot up test system | docker-compose database
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps database
- name: Boot up test system | docker-compose backend
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps backend
- name: Sleep for 10 seconds
run: sleep 10s
- name: Boot up test system | seed backend
run: |
sudo chown runner:docker -R *
cd database
yarn && yarn dev_reset
cd ../backend
yarn && yarn seed
cd ..
- name: Boot up test system | docker-compose frontends
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps frontend admin nginx
- name: Sleep for 15 seconds
run: sleep 15s
##########################################################################
# END-TO-END TESTS #######################################################
##########################################################################
- name: End-to-end tests | run tests
id: e2e-tests
run: |
cd e2e-tests/cypress/tests/
yarn
yarn run cypress run --spec cypress/e2e/User.Authentication.feature
- name: End-to-end tests | if tests failed, upload screenshots
if: steps.e2e-tests.outcome == 'failure'
uses: actions/upload-artifact@v3
with:
name: cypress-screenshots
path: /home/runner/work/gradido/gradido/e2e-tests/cypress/tests/cypress/screenshots/

View File

@ -1,19 +1,40 @@
name: gradido test_dht-node CI
name: Gradido DHT Node Test CI
on: push
jobs:
# only (but most important) job from this workflow required for pull requests
# check results serve as run conditions for all other jobs here
files-changed:
name: Detect File Changes - DHT Node
runs-on: ubuntu-latest
outputs:
dht_node: ${{ steps.changes.outputs.dht_node }}
docker: ${{ steps.changes.outputs.docker }}
steps:
- uses: actions/checkout@v3.3.0
- name: Check for frontend file changes
uses: dorny/paths-filter@v2.11.1
id: changes
with:
token: ${{ github.token }}
filters: .github/file-filters.yml
list-files: shell
##############################################################################
# JOB: DOCKER BUILD TEST #####################################################
##############################################################################
build:
name: Docker Build Test
name: Docker Build Test - DHT Node
if: needs.files-changed.outputs.dht_node == 'true' || needs.files-changed.outputs.docker == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Build `test` image
- name: Build 'test' image
run: |
docker build --target test -t "gradido/dht-node:test" -f dht-node/Dockerfile .
docker save "gradido/dht-node:test" > /tmp/dht-node.tar
@ -28,31 +49,25 @@ jobs:
# JOB: LINT ##################################################################
##############################################################################
lint:
name: Lint
name: Lint - DHT Node
if: needs.files-changed.outputs.dht_node == 'true'
needs: files-changed
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-dht-node-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/dht-node.tar
- name: Lint
run: docker run --rm gradido/dht-node:test yarn run lint
run: cd dht-node && yarn && yarn run lint
##############################################################################
# JOB: UNIT TEST #############################################################
##############################################################################
unit_test:
name: Unit tests
name: Unit Tests - DHT Node
if: needs.files-changed.outputs.dht_node == 'true' || needs.files-changed.outputs.docker == 'true'
needs: [files-changed, build]
runs-on: ubuntu-latest
needs: [build]
steps:
- name: Checkout code
uses: actions/checkout@v3
@ -83,16 +98,4 @@ jobs:
#- 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/dht-node:test yarn run test
cp -r ~/coverage ./coverage
- name: Coverage check
uses: webcraftmedia/coverage-check-action@master
with:
report_name: Coverage dht-node
type: lcov
#result_path: ./dht-node/coverage/lcov.info
result_path: ./coverage/lcov.info
min_coverage: 79
token: ${{ github.token }}
run: docker run --env NODE_ENV=test --env DB_HOST=mariadb --network gradido_internal-net --rm gradido/dht-node:test yarn run test

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

@ -0,0 +1,102 @@
name: Gradido Federation Test CI
on: push
jobs:
# only (but most important) job from this workflow required for pull requests
# check results serve as run conditions for all other jobs here
files-changed:
name: Detect File Changes - Federation
runs-on: ubuntu-latest
outputs:
docker: ${{ steps.changes.outputs.docker }}
federation: ${{ steps.changes.outputs.federation }}
steps:
- uses: actions/checkout@v3.3.0
- name: Check for frontend file changes
uses: dorny/paths-filter@v2.11.1
id: changes
with:
token: ${{ github.token }}
filters: .github/file-filters.yml
list-files: shell
##############################################################################
# JOB: DOCKER BUILD TEST #####################################################
##############################################################################
build:
name: Docker Build Test - Federation
if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.federation == 'true'
needs: files-changed
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 - Federation
if: needs.files-changed.outputs.federation == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Lint
run: cd federation && yarn && yarn run lint
##############################################################################
# JOB: UNIT TEST #############################################################
##############################################################################
unit_test:
name: Unit Tests - Federation
if: needs.files-changed.outputs.docker == 'true' || needs.files-changed.outputs.federation == 'true'
needs: [files-changed, build]
runs-on: ubuntu-latest
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 --rm gradido/federation:test yarn run test

84
.github/workflows/test_frontend.yml vendored Normal file
View File

@ -0,0 +1,84 @@
name: Gradido Frontend Test CI
on: push
jobs:
# only (but most important) job from this workflow required for pull requests
# check results serve as run conditions for all other jobs here
files-changed:
name: Detect File Changes - Frontend
runs-on: ubuntu-latest
outputs:
frontend: ${{ steps.changes.outputs.frontend }}
steps:
- uses: actions/checkout@v3.3.0
- name: Check for frontend file changes
uses: dorny/paths-filter@v2.11.1
id: changes
with:
token: ${{ github.token }}
filters: .github/file-filters.yml
list-files: shell
build_test:
if: needs.files-changed.outputs.frontend == 'true'
name: Docker Build Test - Frontend
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Frontend | Build 'test' image
run: docker build --target test -t "gradido/frontend:test" frontend/ --build-arg NODE_ENV="test"
unit_test:
if: needs.files-changed.outputs.frontend == 'true'
name: Unit Tests - Frontend
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Frontend | Unit tests
run: cd frontend && yarn && yarn run test
lint:
if: needs.files-changed.outputs.frontend == 'true'
name: Lint - Frontend
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Frontend | Lint
run: cd frontend && yarn && yarn run lint
stylelint:
if: needs.files-changed.outputs.frontend == 'true'
name: Stylelint - Frontend
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Frontend | Stylelint
run: cd frontend && yarn && yarn run stylelint
locales:
if: needs.files-changed.outputs.frontend == 'true'
name: Locales - Frontend
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Frontend | Locales
run: cd frontend && yarn && yarn run locales

View File

@ -4,8 +4,76 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [1.19.1](https://github.com/gradido/gradido/compare/1.19.0...1.19.1)
- fix(frontend): admin question clickable [`#2810`](https://github.com/gradido/gradido/pull/2810)
- refactor(frontend): change b-img to b-icon send [`#2809`](https://github.com/gradido/gradido/pull/2809)
- fix(admin): update openCreation in case of tab open. [`#2806`](https://github.com/gradido/gradido/pull/2806)
- fix(admin): english language for contributions in admin [`#2804`](https://github.com/gradido/gradido/pull/2804)
- refactor(admin): event buttons for myself turned off in open contributions [`#2760`](https://github.com/gradido/gradido/pull/2760)
- fix(admin): contribution page [`#2794`](https://github.com/gradido/gradido/pull/2794)
- fix(frontend): info.svg [`#2798`](https://github.com/gradido/gradido/pull/2798)
- fix(backend): add relation messages to database query [`#2795`](https://github.com/gradido/gradido/pull/2795)
- fix(admin): header and menu [`#2793`](https://github.com/gradido/gradido/pull/2793)
- fix(frontend): send gdd - change first submit button text to 'Check Now' [`#2774`](https://github.com/gradido/gradido/pull/2774)
- refactor(frontend): creations generated by link NL [`#2771`](https://github.com/gradido/gradido/pull/2771)
- refactor(frontend): creations generated by link (FR) + (NL) [`#2770`](https://github.com/gradido/gradido/pull/2770)
- refactor(frontend): update German locales [`#2765`](https://github.com/gradido/gradido/pull/2765)
- refactor(backend): use find contributions helper for list contributions [`#2762`](https://github.com/gradido/gradido/pull/2762)
#### [1.19.0](https://github.com/gradido/gradido/compare/1.18.2...1.19.0)
> 7 March 2023
- chore(release): version 1.19.0 [`#2786`](https://github.com/gradido/gradido/pull/2786)
- fix(frontend): change contribution design [`#2731`](https://github.com/gradido/gradido/pull/2731)
- refactor(frontend): commnity navbar- & unauthenticated b-gradido styles [`#2732`](https://github.com/gradido/gradido/pull/2732)
- fix(database): change downwards migration to delete entries with last_announced_at IS NULL [`#2767`](https://github.com/gradido/gradido/pull/2767)
- feat(admin): deleted contributions visible [`#2759`](https://github.com/gradido/gradido/pull/2759)
- feat(other): e2e test user story user registration [`#2753`](https://github.com/gradido/gradido/pull/2753)
- refactor(frontend): style and design changes to a contribution [`#2648`](https://github.com/gradido/gradido/pull/2648)
- test(backend): add tests that ``sendContributionDeleted`` and ``sendContributionDenied`` are called [`#2740`](https://github.com/gradido/gradido/pull/2740)
- fix(backend): set email tls true in test [`#2763`](https://github.com/gradido/gradido/pull/2763)
- refactor(frontend): add visible event an answer question [`#2750`](https://github.com/gradido/gradido/pull/2750)
- refactor(frontend): style sidebar, add icons [`#2737`](https://github.com/gradido/gradido/pull/2737)
- fix(backend): possible flaky test [`#2761`](https://github.com/gradido/gradido/pull/2761)
- fix(backend): emails adjust namings of menus to new design [`#2756`](https://github.com/gradido/gradido/pull/2756)
- ci(other): rename dht node and federation workflow jobs for better branch protection maintenance [`#2743`](https://github.com/gradido/gradido/pull/2743)
- refactor(backend): combine logic for `listTransactionLinks` & `listTransactionLinksAdmin` [`#2706`](https://github.com/gradido/gradido/pull/2706)
- refactor(backend): remove admin create contributions [`#2724`](https://github.com/gradido/gradido/pull/2724)
- refactor(frontend): remove .vue as imports [`#2725`](https://github.com/gradido/gradido/pull/2725)
- feat(federation): add dht-node to deployment scripts [`#2729`](https://github.com/gradido/gradido/pull/2729)
- fix(frontend): change fetchPolicy, add scripts.update [`#2718`](https://github.com/gradido/gradido/pull/2718)
- refactor(backend): list unconfirmed contribution to admin list all contribution [`#2666`](https://github.com/gradido/gradido/pull/2666)
- refactor(frontend): community routes [`#2721`](https://github.com/gradido/gradido/pull/2721)
- test(backend): authentication tests for TransactionLinkResolver [`#2705`](https://github.com/gradido/gradido/pull/2705)
- refactor(backend): use LogError on errors [`#2679`](https://github.com/gradido/gradido/pull/2679)
- refactor(backend): use LogError on encryptorUtils [`#2678`](https://github.com/gradido/gradido/pull/2678)
- refactor(backend): use LogError on creations [`#2677`](https://github.com/gradido/gradido/pull/2677)
- refactor(other): decrease docker build dependencies in test workflow [`#2719`](https://github.com/gradido/gradido/pull/2719)
- refactor(backend): unit test for the method denyContribution [`#2639`](https://github.com/gradido/gradido/pull/2639)
- refactor(frontend): logo inserted with better quality. [`#2646`](https://github.com/gradido/gradido/pull/2646)
- feat(federation): harmonize and sync modules and data of federation [`#2665`](https://github.com/gradido/gradido/pull/2665)
- feat(federation): add docker and github-workflow files [`#2680`](https://github.com/gradido/gradido/pull/2680)
- feat(other): e2e test user authentication reset password [`#2644`](https://github.com/gradido/gradido/pull/2644)
- refactor(admin): add tabs for all statusus on contributions [`#2623`](https://github.com/gradido/gradido/pull/2623)
- feat(federation): implement a graphql client to request getpublickey [`#2511`](https://github.com/gradido/gradido/pull/2511)
- refactor(frontend): style refactor mobil auth area [`#2643`](https://github.com/gradido/gradido/pull/2643)
- refactor(backend): use LogError on TransactionResolver [`#2676`](https://github.com/gradido/gradido/pull/2676)
- refactor(backend): event protocol rework [`#2691`](https://github.com/gradido/gradido/pull/2691)
- refactor(backend): use LogError on TransactionLinkResolver [`#2673`](https://github.com/gradido/gradido/pull/2673)
- fix(frontend): simple disabled function on submit send [`#2647`](https://github.com/gradido/gradido/pull/2647)
- refactor(frontend): missing message on old transactions [`#2660`](https://github.com/gradido/gradido/pull/2660)
- refactor(admin): remove overview and multi creation menu entry [`#2661`](https://github.com/gradido/gradido/pull/2661)
- feat(other): add locales check to backend and integrate it to test workflow [`#2693`](https://github.com/gradido/gradido/pull/2693)
- refactor(other): add linting rules like in backend modul [`#2695`](https://github.com/gradido/gradido/pull/2695)
- refactor(backend): use LogError on contributionResolver [`#2669`](https://github.com/gradido/gradido/pull/2669)
#### [1.18.2](https://github.com/gradido/gradido/compare/1.18.1...1.18.2)
> 10 February 2023
- chore(release): version 1.18.2 [`#2700`](https://github.com/gradido/gradido/pull/2700)
- fix(admin): deny contribution button to left [`#2699`](https://github.com/gradido/gradido/pull/2699)
#### [1.18.1](https://github.com/gradido/gradido/compare/1.18.0...1.18.1)

View File

@ -1,11 +1,17 @@
module.exports = {
verbose: true,
collectCoverage: true,
collectCoverageFrom: [
'src/**/*.{js,vue}',
'!**/node_modules/**',
'!src/assets/**',
'!**/?(*.)+(spec|test).js?(x)',
],
coverageThreshold: {
global: {
lines: 97,
},
},
moduleFileExtensions: [
'js',
// 'jsx',

View File

@ -3,7 +3,7 @@
"description": "Administraion Interface for Gradido",
"main": "index.js",
"author": "Moriz Wahl",
"version": "1.18.2",
"version": "1.19.1",
"license": "Apache-2.0",
"private": false,
"scripts": {
@ -14,7 +14,7 @@
"analyse-bundle": "yarn build && webpack-bundle-analyzer dist/webpack.stats.json",
"lint": "eslint --max-warnings=0 --ext .js,.vue,.json .",
"stylelint": "stylelint --max-warnings=0 '**/*.{scss,vue}'",
"test": "cross-env TZ=UTC jest --coverage",
"test": "cross-env TZ=UTC jest",
"locales": "scripts/sort.sh"
},
"dependencies": {

View File

@ -6,7 +6,7 @@
</template>
<script>
import defaultLayout from '@/layouts/defaultLayout.vue'
import defaultLayout from '@/layouts/defaultLayout'
export default {
name: 'app',

View File

@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils'
import ChangeUserRoleFormular from './ChangeUserRoleFormular.vue'
import ChangeUserRoleFormular from './ChangeUserRoleFormular'
import { setUserRole } from '../graphql/setUserRole'
import { toastSuccessSpy, toastErrorSpy } from '../../test/testSetup'

View File

@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils'
import ConfirmRegisterMailFormular from './ConfirmRegisterMailFormular.vue'
import ConfirmRegisterMailFormular from './ConfirmRegisterMailFormular'
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'

View File

@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils'
import ContributionLink from './ContributionLink.vue'
import ContributionLink from './ContributionLink'
const localVue = global.localVue
@ -42,29 +42,72 @@ describe('ContributionLink', () => {
expect(wrapper.find('div.contribution-link').exists()).toBe(true)
})
describe('function editContributionLinkData', () => {
beforeEach(() => {
wrapper.vm.editContributionLinkData()
it('has one contribution link in table', () => {
expect(wrapper.find('div.contribution-link-list').find('tbody').findAll('tr')).toHaveLength(1)
})
it('has contribution form not visible by default', () => {
expect(wrapper.find('#newContribution').isVisible()).toBe(false)
})
describe('click on create new contribution', () => {
beforeEach(async () => {
await wrapper.find('[data-test="new-contribution-link-button"]').trigger('click')
})
it('emits toggle::collapse new Contribution', async () => {
await expect(wrapper.vm.$root.$emit('bv::toggle::collapse', 'newContribution')).toBeTruthy()
it('shows the contribution form', () => {
expect(wrapper.find('#newContribution').isVisible()).toBe(true)
})
describe('click on create new contribution again', () => {
beforeEach(async () => {
await wrapper.find('[data-test="new-contribution-link-button"]').trigger('click')
})
it('closes the contribution form', () => {
expect(wrapper.find('#newContribution').isVisible()).toBe(false)
})
})
describe('click on close button', () => {
beforeEach(async () => {
await wrapper.find('button.btn-secondary').trigger('click')
})
it('closes the contribution form', () => {
expect(wrapper.find('#newContribution').isVisible()).toBe(false)
})
})
})
describe('function closeContributionForm', () => {
describe('edit contribution link', () => {
beforeEach(async () => {
await wrapper.setData({ visible: true })
wrapper.vm.closeContributionForm()
await wrapper
.find('div.contribution-link-list')
.find('tbody')
.findAll('tr')
.at(0)
.findAll('button')
.at(1)
.trigger('click')
})
it('emits toggle::collapse close Contribution-Form ', async () => {
await expect(wrapper.vm.$root.$emit('bv::toggle::collapse', 'newContribution')).toBeTruthy()
it('shows the contribution form', () => {
expect(wrapper.find('#newContribution').isVisible()).toBe(true)
})
it('editContributionLink is false', async () => {
await expect(wrapper.vm.editContributionLink).toBe(false)
it('does not show the new contribution button', () => {
expect(wrapper.find('[data-test="new-contribution-link-button"]').exists()).toBe(false)
})
it('contributionLinkData is empty', async () => {
await expect(wrapper.vm.contributionLinkData).toEqual({})
describe('click on close button', () => {
beforeEach(async () => {
await wrapper.find('button.btn-secondary').trigger('click')
})
it('closes the contribution form', () => {
expect(wrapper.find('#newContribution').isVisible()).toBe(false)
})
})
})
})

View File

@ -10,8 +10,9 @@
>
<b-button
v-if="!editContributionLink"
v-b-toggle.newContribution
@click="visible = !visible"
class="my-3 d-flex justify-content-left"
data-test="new-contribution-link-button"
>
{{ $t('math.plus') }} {{ $t('contributionLink.newContributionLink') }}
</b-button>
@ -42,8 +43,8 @@
</div>
</template>
<script>
import ContributionLinkForm from '../ContributionLink/ContributionLinkForm.vue'
import ContributionLinkList from '../ContributionLink/ContributionLinkList.vue'
import ContributionLinkForm from '../ContributionLink/ContributionLinkForm'
import ContributionLinkList from '../ContributionLink/ContributionLinkList'
export default {
name: 'ContributionLink',

View File

@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils'
import ContributionLinkForm from './ContributionLinkForm.vue'
import ContributionLinkForm from './ContributionLinkForm'
import { toastErrorSpy, toastSuccessSpy } from '../../../test/testSetup'
import { createContributionLink } from '@/graphql/createContributionLink.js'

View File

@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils'
import ContributionLinkList from './ContributionLinkList.vue'
import ContributionLinkList from './ContributionLinkList'
import { toastSuccessSpy, toastErrorSpy } from '../../../test/testSetup'
// import { deleteContributionLink } from '../graphql/deleteContributionLink'

View File

@ -46,7 +46,7 @@
</template>
<script>
import { deleteContributionLink } from '@/graphql/deleteContributionLink.js'
import FigureQrCode from '../FigureQrCode.vue'
import FigureQrCode from '../FigureQrCode'
export default {
name: 'ContributionLinkList',
@ -70,8 +70,6 @@ export default {
formatter: (value, key, item) => {
if (value) {
return this.$d(new Date(value))
} else {
return null
}
},
},
@ -81,8 +79,6 @@ export default {
formatter: (value, key, item) => {
if (value) {
return this.$d(new Date(value))
} else {
return null
}
},
},

View File

@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils'
import ContributionMessagesFormular from './ContributionMessagesFormular.vue'
import ContributionMessagesFormular from './ContributionMessagesFormular'
import { toastErrorSpy, toastSuccessSpy } from '../../../test/testSetup'
const localVue = global.localVue

View File

@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils'
import ContributionMessagesList from './ContributionMessagesList.vue'
import ContributionMessagesList from './ContributionMessagesList'
const localVue = global.localVue
@ -10,6 +10,7 @@ describe('ContributionMessagesList', () => {
const propsData = {
contributionId: 42,
contributionState: 'PENDING',
}
const mocks = {

View File

@ -1,22 +1,23 @@
<template>
<div class="contribution-messages-list">
<b-container>
{{ messages.lenght }}
<div v-for="message in messages" v-bind:key="message.id">
<contribution-messages-list-item :message="message" />
</div>
</b-container>
<contribution-messages-formular
:contributionId="contributionId"
@get-list-contribution-messages="getListContributionMessages"
@update-state="updateState"
/>
<div v-if="contributionState === 'PENDING' || contributionState === 'IN_PROGRESS'">
<contribution-messages-formular
:contributionId="contributionId"
@get-list-contribution-messages="getListContributionMessages"
@update-state="updateState"
/>
</div>
</div>
</template>
<script>
import ContributionMessagesListItem from './slots/ContributionMessagesListItem.vue'
import ContributionMessagesFormular from '../ContributionMessages/ContributionMessagesFormular.vue'
import ContributionMessagesListItem from './slots/ContributionMessagesListItem'
import ContributionMessagesFormular from '../ContributionMessages/ContributionMessagesFormular'
import { listContributionMessages } from '../../graphql/listContributionMessages.js'
export default {
@ -30,6 +31,10 @@ export default {
type: Number,
required: true,
},
contributionState: {
type: String,
required: true,
},
},
data() {
return {

View File

@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils'
import ContributionMessagesListItem from './ContributionMessagesListItem.vue'
import ContributionMessagesListItem from './ContributionMessagesListItem'
const localVue = global.localVue

View File

@ -16,7 +16,7 @@
</div>
</template>
<script>
import ParseMessage from '@/components/ContributionMessages/ParseMessage.vue'
import ParseMessage from '@/components/ContributionMessages/ParseMessage'
export default {
name: 'ContributionMessagesListItem',

View File

@ -1,7 +1,6 @@
import { mount } from '@vue/test-utils'
import CreationFormular from './CreationFormular.vue'
import CreationFormular from './CreationFormular'
import { adminCreateContribution } from '../graphql/adminCreateContribution'
import { adminCreateContributions } from '../graphql/adminCreateContributions'
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
const localVue = global.localVue
@ -328,122 +327,6 @@ describe('CreationFormular', () => {
})
})
})
describe('mass creation with success', () => {
beforeEach(async () => {
jest.clearAllMocks()
apolloMutateMock.mockResolvedValue({
data: {
adminCreateContributions: {
success: true,
successfulContribution: ['bob@baumeister.de', 'bibi@bloxberg.de'],
failedContribution: [],
},
},
})
await wrapper.setProps({
type: 'massCreation',
creation: [200, 400, 600],
items: [{ email: 'bob@baumeister.de' }, { email: 'bibi@bloxberg.de' }],
})
await wrapper.findAll('input[type="radio"]').at(1).setChecked()
await wrapper.find('textarea').setValue('Test mass create coins')
await wrapper.find('input[type="number"]').setValue(200)
await wrapper.find('.test-submit').trigger('click')
})
it('calls the API', () => {
expect(apolloMutateMock).toBeCalledWith(
expect.objectContaining({
mutation: adminCreateContributions,
variables: {
pendingCreations: [
{
email: 'bob@baumeister.de',
creationDate: getCreationDate(1),
amount: 200,
memo: 'Test mass create coins',
},
{
email: 'bibi@bloxberg.de',
creationDate: getCreationDate(1),
amount: 200,
memo: 'Test mass create coins',
},
],
},
}),
)
})
it('updates open creations in store', () => {
expect(stateCommitMock).toBeCalledWith('openCreationsPlus', 2)
})
it('emits remove-all-bookmark', () => {
expect(wrapper.emitted('remove-all-bookmark')).toBeTruthy()
})
})
describe('mass creation with success but all failed', () => {
beforeEach(async () => {
jest.clearAllMocks()
apolloMutateMock.mockResolvedValue({
data: {
adminCreateContributions: {
success: true,
successfulContribution: [],
failedContribution: ['bob@baumeister.de', 'bibi@bloxberg.de'],
},
},
})
await wrapper.setProps({
type: 'massCreation',
creation: [200, 400, 600],
items: [{ email: 'bob@baumeister.de' }, { email: 'bibi@bloxberg.de' }],
})
await wrapper.findAll('input[type="radio"]').at(1).setChecked()
await wrapper.find('textarea').setValue('Test mass create coins')
await wrapper.find('input[type="number"]').setValue(200)
await wrapper.find('.test-submit').trigger('click')
})
it('updates open creations in store', () => {
expect(stateCommitMock).toBeCalledWith('openCreationsPlus', 0)
})
it('emits remove all bookmarks', () => {
expect(wrapper.emitted('remove-all-bookmark')).toBeTruthy()
})
it('emits toast failed creations with two emails', () => {
expect(wrapper.emitted('toast-failed-creations')).toEqual([
[['bob@baumeister.de', 'bibi@bloxberg.de']],
])
})
})
describe('mass creation with error', () => {
beforeEach(async () => {
jest.clearAllMocks()
apolloMutateMock.mockRejectedValue({
message: 'Oh no!',
})
await wrapper.setProps({
type: 'massCreation',
creation: [200, 400, 600],
items: [{ email: 'bob@baumeister.de' }, { email: 'bibi@bloxberg.de' }],
})
await wrapper.findAll('input[type="radio"]').at(1).setChecked()
await wrapper.find('textarea').setValue('Test mass create coins')
await wrapper.find('input[type="number"]').setValue(200)
await wrapper.find('.test-submit').trigger('click')
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Oh no!')
})
})
})
})
})

View File

@ -86,16 +86,11 @@
</template>
<script>
import { adminCreateContribution } from '../graphql/adminCreateContribution'
import { adminCreateContributions } from '../graphql/adminCreateContributions'
import { creationMonths } from '../mixins/creationMonths'
export default {
name: 'CreationFormular',
mixins: [creationMonths],
props: {
type: {
type: String,
required: false,
},
pagetype: {
type: String,
required: false,
@ -140,78 +135,38 @@ export default {
updateRadioSelected(name) {
// do we want to reset the memo everytime the month changes?
this.text = this.$t('creation_form.creation_for') + ' ' + name.short + ' ' + name.year
if (this.type === 'singleCreation') {
this.rangeMin = 0
this.rangeMax = name.creation
}
this.rangeMin = 0
this.rangeMax = name.creation
},
submitCreation() {
let submitObj = []
if (this.type === 'massCreation') {
this.items.forEach((item) => {
submitObj.push({
email: item.email,
this.$apollo
.mutate({
mutation: adminCreateContribution,
variables: {
email: this.item.email,
creationDate: this.selected.date,
amount: Number(this.value),
memo: this.text,
})
},
})
.then((result) => {
this.$emit('update-user-data', this.item, result.data.adminCreateContribution)
this.$store.commit('openCreationsPlus', 1)
this.toastSuccess(
this.$t('creation_form.toasted', {
value: this.value,
email: this.item.email,
}),
)
// what is this? Tests says that this.text is not reseted
this.$refs.creationForm.reset()
this.value = 0
})
.catch((error) => {
this.toastError(error.message)
this.$refs.creationForm.reset()
this.value = 0
})
this.$apollo
.mutate({
mutation: adminCreateContributions,
variables: {
pendingCreations: submitObj,
},
fetchPolicy: 'no-cache',
})
.then((result) => {
const failedContributions = []
this.$store.commit(
'openCreationsPlus',
result.data.adminCreateContributions.successfulContribution.length,
)
if (result.data.adminCreateContributions.failedContribution.length > 0) {
result.data.adminCreateContributions.failedContribution.forEach((email) => {
failedContributions.push(email)
})
}
this.$emit('remove-all-bookmark')
this.$emit('toast-failed-creations', failedContributions)
})
.catch((error) => {
this.toastError(error.message)
})
} else if (this.type === 'singleCreation') {
submitObj = {
email: this.item.email,
creationDate: this.selected.date,
amount: Number(this.value),
memo: this.text,
}
this.$apollo
.mutate({
mutation: adminCreateContribution,
variables: submitObj,
})
.then((result) => {
this.$emit('update-user-data', this.item, result.data.adminCreateContribution)
this.$store.commit('openCreationsPlus', 1)
this.toastSuccess(
this.$t('creation_form.toasted', {
value: this.value,
email: this.item.email,
}),
)
// what is this? Tests says that this.text is not reseted
this.$refs.creationForm.reset()
this.value = 0
})
.catch((error) => {
this.toastError(error.message)
this.$refs.creationForm.reset()
this.value = 0
})
}
},
},
watch: {

View File

@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils'
import CreationTransactionList from './CreationTransactionList.vue'
import CreationTransactionList from './CreationTransactionList'
import { toastErrorSpy } from '../../test/testSetup'
const localVue = global.localVue

View File

@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils'
import DeletedUserFormular from './DeletedUserFormular.vue'
import DeletedUserFormular from './DeletedUserFormular'
import { deleteUser } from '../graphql/deleteUser'
import { unDeleteUser } from '../graphql/unDeleteUser'
import { toastErrorSpy } from '../../test/testSetup'

View File

@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils'
import EditCreationFormular from './EditCreationFormular.vue'
import EditCreationFormular from './EditCreationFormular'
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
const localVue = global.localVue

View File

@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils'
import FigureQrCode from './FigureQrCode.vue'
import FigureQrCode from './FigureQrCode'
const localVue = global.localVue

View File

@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils'
import NavBar from './NavBar.vue'
import NavBar from './NavBar'
const localVue = global.localVue
@ -68,13 +68,20 @@ describe('NavBar', () => {
})
describe('wallet', () => {
const assignLocationSpy = jest.fn()
const windowLocation = window.location
beforeEach(async () => {
await wrapper.findAll('.nav-item').at(5).find('a').trigger('click')
delete window.location
window.location = ''
await wrapper.findAll('.nav-item').at(4).find('a').trigger('click')
})
it.skip('changes window location to wallet', () => {
expect(assignLocationSpy).toBeCalledWith('valid-token')
afterEach(() => {
delete window.location
window.location = windowLocation
})
it('changes window location to wallet', () => {
expect(window.location).toBe('http://localhost/authenticate?token=valid-token')
})
it('dispatches logout to store', () => {
@ -84,6 +91,7 @@ describe('NavBar', () => {
describe('logout', () => {
const windowLocationMock = jest.fn()
const windowLocation = window.location
beforeEach(async () => {
delete window.location
window.location = {
@ -92,6 +100,11 @@ describe('NavBar', () => {
await wrapper.findAll('.nav-item').at(5).find('a').trigger('click')
})
afterEach(() => {
delete window.location
window.location = windowLocation
})
it('redirects to /logout', () => {
expect(windowLocationMock).toBeCalledWith('http://localhost/login')
})

View File

@ -1,8 +1,8 @@
<template>
<div class="component-nabvar">
<b-navbar toggleable="md" type="dark" variant="success" class="p-3">
<b-navbar-brand to="/">
<img src="img/brand/gradido_logo_w.png" class="navbar-brand-img" alt="..." />
<b-navbar toggleable="md" type="dark" variant="success">
<b-navbar-brand class="mb-2" to="/">
<img src="img/brand/gradido_logo_w.png" class="navbar-brand-img pl-2" alt="..." />
</b-navbar-brand>
<b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
@ -10,12 +10,11 @@
<b-collapse id="nav-collapse" is-nav>
<b-navbar-nav>
<b-nav-item to="/user">{{ $t('navbar.user_search') }}</b-nav-item>
<b-nav-item
v-show="$store.state.openCreations > 0"
class="bg-color-creation p-1"
to="/creation-confirm"
>
{{ $store.state.openCreations }} {{ $t('navbar.open_creation') }}
<b-nav-item class="bg-color-creation" to="/creation-confirm">
{{ $t('creation') }}
<b-badge v-show="$store.state.openCreations > 0" variant="danger">
{{ $store.state.openCreations }}
</b-badge>
</b-nav-item>
<b-nav-item to="/contribution-links">
{{ $t('navbar.automaticContributions') }}
@ -53,9 +52,5 @@ export default {
<style>
.navbar-brand-img {
height: 2rem;
padding-left: 10px;
}
.bg-color-creation {
background-color: #cf1010dc;
}
</style>

View File

@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils'
import Overlay from './Overlay.vue'
import Overlay from './Overlay'
const localVue = global.localVue

View File

@ -13,7 +13,8 @@
<b-row>
<b-col class="col-3">{{ $t('creation_for_month') }}</b-col>
<b-col class="h3">
{{ $d(new Date(item.date), 'month') }} {{ $d(new Date(item.date), 'year') }}
{{ $d(new Date(item.contributionDate), 'month') }}
{{ $d(new Date(item.contributionDate), 'year') }}
</b-col>
</b-row>
<b-row>

View File

@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils'
import OpenCreationsTable from './OpenCreationsTable.vue'
import OpenCreationsTable from './OpenCreationsTable'
const localVue = global.localVue

View File

@ -1,18 +1,31 @@
<template>
<div class="open-creations-table">
<b-table-lite :items="items" :fields="fields" caption-top striped hover stacked="md">
<b-table-lite
:items="items"
:fields="fields"
caption-top
striped
hover
stacked="md"
:tbody-tr-class="rowClass"
>
<template #cell(state)="row">
<b-icon :icon="getStatusIcon(row.item.state)"></b-icon>
</template>
<template #cell(bookmark)="row">
<b-button
variant="danger"
size="md"
@click="$emit('show-overlay', row.item, 'delete')"
class="mr-2"
>
<b-icon icon="trash" variant="light"></b-icon>
</b-button>
<div v-if="!myself(row.item)">
<b-button
variant="danger"
size="md"
@click="$emit('show-overlay', row.item, 'delete')"
class="mr-2"
>
<b-icon icon="trash" variant="light"></b-icon>
</b-button>
</div>
</template>
<template #cell(editCreation)="row">
<div v-if="$store.state.moderator.id !== row.item.userId">
<div v-if="!myself(row.item)">
<b-button
v-if="row.item.moderator"
variant="info"
@ -25,20 +38,26 @@
<b-button v-else @click="rowToggleDetails(row, 0)">
<b-icon icon="chat-dots"></b-icon>
<b-icon
v-if="row.item.state === 'PENDING' && row.item.messageCount > 0"
v-if="row.item.state === 'PENDING' && row.item.messagesCount > 0"
icon="exclamation-circle-fill"
variant="warning"
></b-icon>
<b-icon
v-if="row.item.state === 'IN_PROGRESS' && row.item.messageCount > 0"
v-if="row.item.state === 'IN_PROGRESS' && row.item.messagesCount > 0"
icon="question-diamond"
variant="light"
variant="warning"
class="pl-1"
></b-icon>
</b-button>
</div>
</template>
<template #cell(chatCreation)="row">
<b-button v-if="row.item.messagesCount > 0" @click="rowToggleDetails(row, 0)">
<b-icon icon="chat-dots"></b-icon>
</b-button>
</template>
<template #cell(deny)="row">
<div v-if="$store.state.moderator.id !== row.item.userId">
<div v-if="!myself(row.item)">
<b-button
variant="warning"
size="md"
@ -50,7 +69,7 @@
</div>
</template>
<template #cell(confirm)="row">
<div v-if="$store.state.moderator.id !== row.item.userId">
<div v-if="!myself(row.item)">
<b-button
variant="success"
size="md"
@ -83,6 +102,7 @@
<div v-else>
<contribution-messages-list
:contributionId="row.item.id"
:contributionState="row.item.state"
@update-state="updateState"
@update-user-data="updateUserData"
/>
@ -96,9 +116,17 @@
<script>
import { toggleRowDetails } from '../../mixins/toggleRowDetails'
import RowDetails from '../RowDetails.vue'
import EditCreationFormular from '../EditCreationFormular.vue'
import ContributionMessagesList from '../ContributionMessages/ContributionMessagesList.vue'
import RowDetails from '../RowDetails'
import EditCreationFormular from '../EditCreationFormular'
import ContributionMessagesList from '../ContributionMessages/ContributionMessagesList'
const iconMap = {
IN_PROGRESS: 'question-square',
PENDING: 'bell-fill',
CONFIRMED: 'check',
DELETED: 'trash',
DENIED: 'x-circle',
}
export default {
name: 'OpenCreationsTable',
@ -129,6 +157,23 @@ export default {
}
},
methods: {
myself(item) {
return (
`${item.firstName} ${item.lastName}` ===
`${this.$store.state.moderator.firstName} ${this.$store.state.moderator.lastName}`
)
},
getStatusIcon(status) {
return iconMap[status] ? iconMap[status] : 'default-icon'
},
rowClass(item, type) {
if (!item || type !== 'row') return
if (item.state === 'CONFIRMED') return 'table-success'
if (item.state === 'DENIED') return 'table-warning'
if (item.state === 'DELETED') return 'table-danger'
if (item.state === 'IN_PROGRESS') return 'table-primary'
if (item.state === 'PENDING') return 'table-primary'
},
updateCreationData(data) {
const row = data.row
this.$emit('update-contributions', data)

View File

@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils'
import SearchUserTable from './SearchUserTable.vue'
import SearchUserTable from './SearchUserTable'
const localVue = global.localVue

View File

@ -53,7 +53,6 @@
<b-tab :title="$t('creation')" active :disabled="row.item.deletedAt !== null">
<creation-formular
v-if="!row.item.deletedAt"
type="singleCreation"
pagetype="singleCreation"
:creation="row.item.creation"
:item="row.item"
@ -92,12 +91,12 @@
</div>
</template>
<script>
import CreationFormular from '../CreationFormular.vue'
import ConfirmRegisterMailFormular from '../ConfirmRegisterMailFormular.vue'
import CreationTransactionList from '../CreationTransactionList.vue'
import TransactionLinkList from '../TransactionLinkList.vue'
import ChangeUserRoleFormular from '../ChangeUserRoleFormular.vue'
import DeletedUserFormular from '../DeletedUserFormular.vue'
import CreationFormular from '../CreationFormular'
import ConfirmRegisterMailFormular from '../ConfirmRegisterMailFormular'
import CreationTransactionList from '../CreationTransactionList'
import TransactionLinkList from '../TransactionLinkList'
import ChangeUserRoleFormular from '../ChangeUserRoleFormular'
import DeletedUserFormular from '../DeletedUserFormular'
export default {
name: 'SearchUserTable',

View File

@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils'
import StatisticTable from './StatisticTable.vue'
import StatisticTable from './StatisticTable'
const localVue = global.localVue

View File

@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils'
import TransactionLinkList from './TransactionLinkList.vue'
import TransactionLinkList from './TransactionLinkList'
import { listTransactionLinksAdmin } from '../graphql/listTransactionLinksAdmin.js'
import { toastErrorSpy } from '../../test/testSetup'
@ -9,8 +9,8 @@ const apolloQueryMock = jest.fn()
apolloQueryMock.mockResolvedValue({
data: {
listTransactionLinksAdmin: {
linkCount: 8,
linkList: [
count: 8,
links: [
{
amount: '19.99',
code: '62ef8236ace7217fbd066c5a',

View File

@ -42,8 +42,8 @@ export default {
},
})
.then((result) => {
this.rows = result.data.listTransactionLinksAdmin.linkCount
this.items = result.data.listTransactionLinksAdmin.linkList
this.rows = result.data.listTransactionLinksAdmin.count
this.items = result.data.listTransactionLinksAdmin.links
})
.catch((error) => {
this.toastError(error.message)

View File

@ -1,7 +1,7 @@
import gql from 'graphql-tag'
export const adminCreateContributionMessage = gql`
mutation ($contributionId: Float!, $message: String!) {
mutation ($contributionId: Int!, $message: String!) {
adminCreateContributionMessage(contributionId: $contributionId, message: $message) {
id
message

View File

@ -1,11 +0,0 @@
import gql from 'graphql-tag'
export const adminCreateContributions = gql`
mutation ($pendingCreations: [AdminCreateContributionArgs!]!) {
adminCreateContributions(pendingCreations: $pendingCreations) {
success
successfulContribution
failedContribution
}
}
`

View File

@ -0,0 +1,36 @@
import gql from 'graphql-tag'
export const adminListAllContributions = gql`
query (
$currentPage: Int = 1
$pageSize: Int = 25
$order: Order = DESC
$statusFilter: [ContributionStatus!]
) {
adminListAllContributions(
currentPage: $currentPage
pageSize: $pageSize
order: $order
statusFilter: $statusFilter
) {
contributionCount
contributionList {
id
firstName
lastName
amount
memo
createdAt
contributionDate
confirmedAt
confirmedBy
state
messagesCount
deniedAt
deniedBy
deletedAt
deletedBy
}
}
}
`

View File

@ -1,7 +1,7 @@
import gql from 'graphql-tag'
export const listContributionMessages = gql`
query ($contributionId: Float!, $pageSize: Int = 25, $currentPage: Int = 1, $order: Order = ASC) {
query ($contributionId: Int!, $pageSize: Int = 25, $currentPage: Int = 1, $order: Order = ASC) {
listContributionMessages(
contributionId: $contributionId
pageSize: $pageSize

View File

@ -8,8 +8,8 @@ export const listTransactionLinksAdmin = gql`
userId: $userId
filters: { withRedeemed: true, withExpired: true, withDeleted: true }
) {
linkCount
linkList {
count
links {
id
amount
holdAvailableAmount

View File

@ -1,20 +0,0 @@
import gql from 'graphql-tag'
export const listUnconfirmedContributions = gql`
query {
listUnconfirmedContributions {
id
firstName
lastName
userId
email
amount
memo
date
moderator
creation
state
messageCount
}
}
`

View File

@ -7,8 +7,8 @@
</template>
<script>
import NavBar from '@/components/NavBar.vue'
import ContentFooter from '@/components/ContentFooter.vue'
import NavBar from '@/components/NavBar'
import ContentFooter from '@/components/ContentFooter'
export default {
name: 'defaultLayout',
components: {

View File

@ -1,6 +1,7 @@
{
"all_emails": "Alle Nutzer",
"back": "zurück",
"chat": "Chat",
"contributionLink": {
"amount": "Betrag",
"changeSaved": "Änderungen gespeichert",
@ -29,6 +30,15 @@
"validFrom": "Startdatum",
"validTo": "Enddatum"
},
"contributions": {
"all": "Alle",
"confirms": "Bestätigt",
"deleted": "Gelöscht",
"denied": "Abgelehnt",
"open": "Offen"
},
"created": "Geschöpft",
"createdAt": "Angelegt",
"creation": "Schöpfung",
"creationList": "Schöpfungsliste",
"creation_form": {
@ -48,13 +58,11 @@
"update_creation": "Schöpfung aktualisieren"
},
"creation_for_month": "Schöpfung für Monat",
"date": "Datum",
"delete": "Löschen",
"deleted": "gelöscht",
"deleted_user": "Alle gelöschten Nutzer",
"delete_user": "Nutzer löschen",
"deny": "Ablehnen",
"edit": "Bearbeiten",
"enabled": "aktiviert",
"error": "Fehler",
"expired": "abgelaufen",
@ -98,7 +106,6 @@
"automaticContributions": "Automatische Beiträge",
"logout": "Abmelden",
"my-account": "Mein Konto",
"open_creation": "Offene Schöpfungen",
"statistic": "Statistik",
"user_search": "Nutzersuche"
},

View File

@ -1,6 +1,7 @@
{
"all_emails": "All users",
"back": "back",
"chat": "Chat",
"contributionLink": {
"amount": "Amount",
"changeSaved": "Changes saved",
@ -29,6 +30,15 @@
"validFrom": "Start-date",
"validTo": "End-Date"
},
"contributions": {
"all": "All",
"confirms": "Confirmed",
"deleted": "Deleted",
"denied": "Rejected",
"open": "Open"
},
"created": "Created for",
"createdAt": "Created at",
"creation": "Creation",
"creationList": "Creation list",
"creation_form": {
@ -43,18 +53,16 @@
"toasted": "Open creation ({value} GDD) for {email} has been saved and is ready for confirmation.",
"toasted_created": "Creation has been successfully saved",
"toasted_delete": "Open creation has been deleted",
"toasted_denied": "Open creation has been denied",
"toasted_denied": "Open creation has been rejected",
"toasted_update": "Open creation {value} GDD) for {email} has been changed and is ready for confirmation.",
"update_creation": "Creation update"
},
"creation_for_month": "Creation for month",
"date": "Date",
"delete": "Delete",
"deleted": "deleted",
"deleted_user": "All deleted user",
"delete_user": "Delete user",
"deny": "Reject",
"edit": "Edit",
"enabled": "enabled",
"error": "Error",
"expired": "expired",
@ -78,7 +86,7 @@
"transactionlist": {
"confirmed": "When was it confirmed by a moderator / admin.",
"periods": "For what period was it submitted by the member.",
"state": "[PENDING = submitted, DELETED = deleted, IN_PROGRESS = in dialogue with moderator, DENIED = denied, CONFIRMED = confirmed]",
"state": "[PENDING = submitted, DELETED = deleted, IN_PROGRESS = in dialogue with moderator, DENIED = rejected, CONFIRMED = confirmed]",
"submitted": "When was it submitted by the member"
}
},
@ -98,7 +106,6 @@
"automaticContributions": "Automatic Contributions",
"logout": "Logout",
"my-account": "My Account",
"open_creation": "Open creations",
"statistic": "Statistic",
"user_search": "User search"
},

View File

@ -1,5 +1,5 @@
import Vue from 'vue'
import App from './App.vue'
import App from './App'
// without this async calls are not working
import 'regenerator-runtime'

View File

@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils'
import CommunityStatistic from './CommunityStatistic.vue'
import CommunityStatistic from './CommunityStatistic'
import { communityStatistics } from '@/graphql/communityStatistics.js'
import { toastErrorSpy } from '../../test/testSetup'
import VueApollo from 'vue-apollo'

View File

@ -5,7 +5,7 @@
</template>
<script>
import { communityStatistics } from '@/graphql/communityStatistics.js'
import StatisticTable from '../components/Tables/StatisticTable.vue'
import StatisticTable from '../components/Tables/StatisticTable'
export default {
name: 'CommunityStatistic',

View File

@ -1,11 +1,11 @@
import { mount } from '@vue/test-utils'
import ContributionLinks from './ContributionLinks.vue'
import ContributionLinks from './ContributionLinks'
import { listContributionLinks } from '@/graphql/listContributionLinks.js'
import { toastErrorSpy } from '../../test/testSetup'
const localVue = global.localVue
const apolloQueryMock = jest.fn().mockResolvedValueOnce({
const apolloQueryMock = jest.fn().mockResolvedValue({
data: {
listContributionLinks: {
links: [
@ -47,6 +47,7 @@ describe('ContributionLinks', () => {
beforeEach(() => {
wrapper = Wrapper()
})
describe('apollo returns', () => {
it('calls listContributionLinks', () => {
expect(apolloQueryMock).toBeCalledWith(
@ -57,7 +58,7 @@ describe('ContributionLinks', () => {
})
})
describe.skip('query transaction with error', () => {
describe('query transaction with error', () => {
beforeEach(() => {
apolloQueryMock.mockRejectedValue({ message: 'OUCH!' })
wrapper = Wrapper()

View File

@ -9,7 +9,7 @@
</template>
<script>
import { listContributionLinks } from '@/graphql/listContributionLinks.js'
import ContributionLink from '../components/ContributionLink/ContributionLink.vue'
import ContributionLink from '../components/ContributionLink/ContributionLink'
export default {
name: 'ContributionLinks',

View File

@ -1,8 +1,8 @@
import { mount } from '@vue/test-utils'
import CreationConfirm from './CreationConfirm.vue'
import CreationConfirm from './CreationConfirm'
import { adminDeleteContribution } from '../graphql/adminDeleteContribution'
import { denyContribution } from '../graphql/denyContribution'
import { listUnconfirmedContributions } from '../graphql/listUnconfirmedContributions'
import { adminListAllContributions } from '../graphql/adminListAllContributions'
import { confirmContribution } from '../graphql/confirmContribution'
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
import VueApollo from 'vue-apollo'
@ -38,50 +38,68 @@ const mocks = {
const defaultData = () => {
return {
listUnconfirmedContributions: [
{
id: 1,
firstName: 'Bibi',
lastName: 'Bloxberg',
userId: 99,
email: 'bibi@bloxberg.de',
amount: 500,
memo: 'Danke für alles',
date: new Date(),
moderator: 1,
state: 'PENDING',
creation: [500, 500, 500],
messageCount: 0,
},
{
id: 2,
firstName: 'Räuber',
lastName: 'Hotzenplotz',
userId: 100,
email: 'raeuber@hotzenplotz.de',
amount: 1000000,
memo: 'Gut Ergattert',
date: new Date(),
moderator: 1,
state: 'PENDING',
creation: [500, 500, 500],
messageCount: 0,
},
],
adminListAllContributions: {
contributionCount: 2,
contributionList: [
{
id: 1,
firstName: 'Bibi',
lastName: 'Bloxberg',
userId: 99,
email: 'bibi@bloxberg.de',
amount: 500,
memo: 'Danke für alles',
date: new Date(),
moderator: 1,
state: 'PENDING',
creation: [500, 500, 500],
messagesCount: 0,
deniedBy: null,
deniedAt: null,
confirmedBy: null,
confirmedAt: null,
contributionDate: new Date(),
deletedBy: null,
deletedAt: null,
createdAt: new Date(),
},
{
id: 2,
firstName: 'Räuber',
lastName: 'Hotzenplotz',
userId: 100,
email: 'raeuber@hotzenplotz.de',
amount: 1000000,
memo: 'Gut Ergattert',
date: new Date(),
moderator: 1,
state: 'PENDING',
creation: [500, 500, 500],
messagesCount: 0,
deniedBy: null,
deniedAt: null,
confirmedBy: null,
confirmedAt: null,
contributionDate: new Date(),
deletedBy: null,
deletedAt: null,
createdAt: new Date(),
},
],
},
}
}
describe('CreationConfirm', () => {
let wrapper
const listUnconfirmedContributionsMock = jest.fn()
const adminListAllContributionsMock = jest.fn()
const adminDeleteContributionMock = jest.fn()
const adminDenyContributionMock = jest.fn()
const confirmContributionMock = jest.fn()
mockClient.setRequestHandler(
listUnconfirmedContributions,
listUnconfirmedContributionsMock
adminListAllContributions,
adminListAllContributionsMock
.mockRejectedValueOnce({ message: 'Ouch!' })
.mockResolvedValue({ data: defaultData() }),
)
@ -117,6 +135,10 @@ describe('CreationConfirm', () => {
it('toast an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Ouch!')
})
it('has statusFilter ["IN_PROGRESS", "PENDING"]', () => {
expect(wrapper.vm.statusFilter).toEqual(['IN_PROGRESS', 'PENDING'])
})
})
describe('server response is succes', () => {
@ -125,17 +147,7 @@ describe('CreationConfirm', () => {
})
it('has two pending creations', () => {
expect(wrapper.vm.pendingCreations).toHaveLength(2)
})
})
describe('store', () => {
it('commits resetOpenCreations to store', () => {
expect(storeCommitMock).toBeCalledWith('resetOpenCreations')
})
it('commits setOpenCreations to store', () => {
expect(storeCommitMock).toBeCalledWith('setOpenCreations', 2)
expect(wrapper.find('tbody').findAll('tr')).toHaveLength(2)
})
})
@ -316,5 +328,108 @@ describe('CreationConfirm', () => {
})
})
})
describe('filter tabs', () => {
describe('click tab "confirmed"', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper.find('a[data-test="confirmed"]').trigger('click')
})
it('refetches contributions with proper filter', () => {
expect(adminListAllContributionsMock).toBeCalledWith({
currentPage: 1,
order: 'DESC',
pageSize: 25,
statusFilter: ['CONFIRMED'],
})
})
describe('click tab "open"', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper.find('a[data-test="open"]').trigger('click')
})
it('refetches contributions with proper filter', () => {
expect(adminListAllContributionsMock).toBeCalledWith({
currentPage: 1,
order: 'DESC',
pageSize: 25,
statusFilter: ['IN_PROGRESS', 'PENDING'],
})
})
})
describe('click tab "denied"', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper.find('a[data-test="denied"]').trigger('click')
})
it('refetches contributions with proper filter', () => {
expect(adminListAllContributionsMock).toBeCalledWith({
currentPage: 1,
order: 'DESC',
pageSize: 25,
statusFilter: ['DENIED'],
})
})
})
describe('click tab "deleted"', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper.find('a[data-test="deleted"]').trigger('click')
})
it('refetches contributions with proper filter', () => {
expect(adminListAllContributionsMock).toBeCalledWith({
currentPage: 1,
order: 'DESC',
pageSize: 25,
statusFilter: ['DELETED'],
})
})
})
describe('click tab "all"', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper.find('a[data-test="all"]').trigger('click')
})
it('refetches contributions with proper filter', () => {
expect(adminListAllContributionsMock).toBeCalledWith({
currentPage: 1,
order: 'DESC',
pageSize: 25,
statusFilter: ['IN_PROGRESS', 'PENDING', 'CONFIRMED', 'DENIED', 'DELETED'],
})
})
})
})
})
describe('update status', () => {
beforeEach(async () => {
await wrapper.findComponent({ name: 'OpenCreationsTable' }).vm.$emit('update-state', 2)
})
it('updates the status', () => {
expect(wrapper.vm.items.find((obj) => obj.id === 2).messagesCount).toBe(1)
expect(wrapper.vm.items.find((obj) => obj.id === 2).state).toBe('IN_PROGRESS')
})
})
describe('unknown variant', () => {
beforeEach(async () => {
await wrapper.setData({ variant: 'unknown' })
})
it('has overlay icon "info"', () => {
expect(wrapper.vm.overlayIcon).toBe('info')
})
})
})
})

View File

@ -1,6 +1,62 @@
<!-- eslint-disable @intlify/vue-i18n/no-dynamic-keys -->
<template>
<div class="creation-confirm">
<div>
<b-tabs v-model="tabIndex" content-class="mt-3" fill>
<b-tab active :title-link-attributes="{ 'data-test': 'open' }">
<template #title>
<b-icon icon="bell-fill" variant="primary"></b-icon>
{{ $t('contributions.open') }}
<b-badge v-if="$store.state.openCreations > 0" variant="danger">
{{ $store.state.openCreations }}
</b-badge>
</template>
</b-tab>
<b-tab :title-link-attributes="{ 'data-test': 'confirmed' }">
<template #title>
<b-icon icon="check" variant="success"></b-icon>
{{ $t('contributions.confirms') }}
</template>
</b-tab>
<b-tab :title-link-attributes="{ 'data-test': 'denied' }">
<template #title>
<b-icon icon="x-circle" variant="warning"></b-icon>
{{ $t('contributions.denied') }}
</template>
</b-tab>
<b-tab :title-link-attributes="{ 'data-test': 'deleted' }">
<template #title>
<b-icon icon="trash" variant="danger"></b-icon>
{{ $t('contributions.deleted') }}
</template>
</b-tab>
<b-tab :title-link-attributes="{ 'data-test': 'all' }">
<template #title>
<b-icon icon="list"></b-icon>
{{ $t('contributions.all') }}
</template>
</b-tab>
</b-tabs>
</div>
<open-creations-table
class="mt-4"
:items="items"
:fields="fields"
@show-overlay="showOverlay"
@update-state="updateStatus"
@update-contributions="$apollo.queries.AllContributions.refetch()"
/>
<b-pagination
pills
size="lg"
v-model="currentPage"
:per-page="pageSize"
:total-rows="rows"
align="center"
:hide-ellipsis="true"
></b-pagination>
<div v-if="overlay" id="overlay" @dblclick="overlay = false">
<overlay :item="item" @overlay-cancel="overlay = false">
<template #title>
@ -24,24 +80,24 @@
</template>
</overlay>
</div>
<open-creations-table
class="mt-4"
:items="pendingCreations"
:fields="fields"
@show-overlay="showOverlay"
@update-state="updateState"
@update-contributions="$apollo.queries.PendingContributions.refetch()"
/>
</div>
</template>
<script>
import Overlay from '../components/Overlay.vue'
import OpenCreationsTable from '../components/Tables/OpenCreationsTable.vue'
import { listUnconfirmedContributions } from '../graphql/listUnconfirmedContributions'
import Overlay from '../components/Overlay'
import OpenCreationsTable from '../components/Tables/OpenCreationsTable'
import { adminListAllContributions } from '../graphql/adminListAllContributions'
import { adminDeleteContribution } from '../graphql/adminDeleteContribution'
import { confirmContribution } from '../graphql/confirmContribution'
import { denyContribution } from '../graphql/denyContribution'
const FILTER_TAB_MAP = [
['IN_PROGRESS', 'PENDING'],
['CONFIRMED'],
['DENIED'],
['DELETED'],
['IN_PROGRESS', 'PENDING', 'CONFIRMED', 'DENIED', 'DELETED'],
]
export default {
name: 'CreationConfirm',
components: {
@ -50,10 +106,14 @@ export default {
},
data() {
return {
pendingCreations: [],
tabIndex: 0,
items: [],
overlay: false,
item: {},
variant: 'confirm',
rows: 0,
currentPage: 1,
pageSize: 25,
}
},
methods: {
@ -112,7 +172,7 @@ export default {
})
},
updatePendingCreations(id) {
this.pendingCreations = this.pendingCreations.filter((obj) => obj.id !== id)
this.items = this.items.filter((obj) => obj.id !== id)
this.$store.commit('openCreationsMinus', 1)
},
showOverlay(item, variant) {
@ -120,38 +180,191 @@ export default {
this.item = item
this.variant = variant
},
updateState(id) {
this.pendingCreations.find((obj) => obj.id === id).messagesCount++
this.pendingCreations.find((obj) => obj.id === id).state = 'IN_PROGRESS'
updateStatus(id) {
this.items.find((obj) => obj.id === id).messagesCount++
this.items.find((obj) => obj.id === id).state = 'IN_PROGRESS'
},
formatDateOrDash(value) {
return value ? this.$d(new Date(value), 'short') : '—'
},
},
computed: {
fields() {
return [
{ key: 'bookmark', label: this.$t('delete') },
{ key: 'deny', label: this.$t('deny') },
{ key: 'email', label: this.$t('e_mail') },
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{
key: 'amount',
label: this.$t('creation'),
formatter: (value) => {
return value + ' GDD'
[
// open contributions
{ key: 'bookmark', label: this.$t('delete') },
{ key: 'deny', label: this.$t('deny') },
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{
key: 'amount',
label: this.$t('creation'),
formatter: (value) => {
return value + ' GDD'
},
},
},
{ key: 'memo', label: this.$t('text'), class: 'text-break' },
{
key: 'date',
label: this.$t('date'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
{ key: 'memo', label: this.$t('text'), class: 'text-break' },
{
key: 'contributionDate',
label: this.$t('created'),
formatter: (value) => {
return this.formatDateOrDash(value)
},
},
},
{ key: 'moderator', label: this.$t('moderator') },
{ key: 'editCreation', label: this.$t('edit') },
{ key: 'confirm', label: this.$t('save') },
]
{ key: 'moderator', label: this.$t('moderator') },
{ key: 'editCreation', label: this.$t('chat') },
{ key: 'confirm', label: this.$t('save') },
],
[
// confirmed contributions
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{
key: 'amount',
label: this.$t('creation'),
formatter: (value) => {
return value + ' GDD'
},
},
{ key: 'memo', label: this.$t('text'), class: 'text-break' },
{
key: 'contributionDate',
label: this.$t('created'),
formatter: (value) => {
return this.formatDateOrDash(value)
},
},
{
key: 'createdAt',
label: this.$t('createdAt'),
formatter: (value) => {
return this.formatDateOrDash(value)
},
},
{
key: 'confirmedAt',
label: this.$t('contributions.confirms'),
formatter: (value) => {
return this.formatDateOrDash(value)
},
},
{ key: 'confirmedBy', label: this.$t('moderator') },
{ key: 'chatCreation', label: this.$t('chat') },
],
[
// denied contributions
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{
key: 'amount',
label: this.$t('creation'),
formatter: (value) => {
return value + ' GDD'
},
},
{ key: 'memo', label: this.$t('text'), class: 'text-break' },
{
key: 'contributionDate',
label: this.$t('created'),
formatter: (value) => {
return this.formatDateOrDash(value)
},
},
{
key: 'createdAt',
label: this.$t('createdAt'),
formatter: (value) => {
return this.formatDateOrDash(value)
},
},
{
key: 'deniedAt',
label: this.$t('contributions.denied'),
formatter: (value) => {
return this.formatDateOrDash(value)
},
},
{ key: 'deniedBy', label: this.$t('moderator') },
{ key: 'chatCreation', label: this.$t('chat') },
],
[
// deleted contributions
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{
key: 'amount',
label: this.$t('creation'),
formatter: (value) => {
return value + ' GDD'
},
},
{ key: 'memo', label: this.$t('text'), class: 'text-break' },
{
key: 'contributionDate',
label: this.$t('created'),
formatter: (value) => {
return this.formatDateOrDash(value)
},
},
{
key: 'createdAt',
label: this.$t('createdAt'),
formatter: (value) => {
return this.formatDateOrDash(value)
},
},
{
key: 'deletedAt',
label: this.$t('contributions.deleted'),
formatter: (value) => {
return this.formatDateOrDash(value)
},
},
{ key: 'deletedBy', label: this.$t('moderator') },
{ key: 'chatCreation', label: this.$t('chat') },
],
[
// all contributions
{ key: 'state', label: this.$t('status') },
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{
key: 'amount',
label: this.$t('creation'),
formatter: (value) => {
return value + ' GDD'
},
},
{ key: 'memo', label: this.$t('text'), class: 'text-break' },
{
key: 'contributionDate',
label: this.$t('created'),
formatter: (value) => {
return this.formatDateOrDash(value)
},
},
{
key: 'createdAt',
label: this.$t('createdAt'),
formatter: (value) => {
return this.formatDateOrDash(value)
},
},
{
key: 'confirmedAt',
label: this.$t('contributions.confirms'),
formatter: (value) => {
return this.formatDateOrDash(value)
},
},
{ key: 'confirmedBy', label: this.$t('moderator') },
{ key: 'chatCreation', label: this.$t('chat') },
],
][this.tabIndex]
},
statusFilter() {
return FILTER_TAB_MAP[this.tabIndex]
},
overlayTitle() {
return `overlay.${this.variant}.title`
@ -182,18 +395,24 @@ export default {
},
},
apollo: {
PendingContributions: {
ListAllContributions: {
query() {
return listUnconfirmedContributions
return adminListAllContributions
},
variables() {
// may be at some point we need a pagination here
return {}
return {
currentPage: this.currentPage,
pageSize: this.pageSize,
statusFilter: this.statusFilter,
}
},
update({ listUnconfirmedContributions }) {
this.$store.commit('resetOpenCreations')
this.pendingCreations = listUnconfirmedContributions
this.$store.commit('setOpenCreations', listUnconfirmedContributions.length)
fetchPolicy: 'no-cache',
update({ adminListAllContributions }) {
this.rows = adminListAllContributions.contributionCount
this.items = adminListAllContributions.contributionList
if (this.statusFilter === FILTER_TAB_MAP[0]) {
this.$store.commit('setOpenCreations', adminListAllContributions.contributionCount)
}
},
error({ message }) {
this.toastError(message)

View File

@ -1,41 +1,18 @@
import { mount } from '@vue/test-utils'
import Overview from './Overview.vue'
import { listUnconfirmedContributions } from '@/graphql/listUnconfirmedContributions.js'
import Overview from './Overview'
import { adminListAllContributions } from '../graphql/adminListAllContributions'
import VueApollo from 'vue-apollo'
import { createMockClient } from 'mock-apollo-client'
import { toastErrorSpy } from '../../test/testSetup'
const mockClient = createMockClient()
const apolloProvider = new VueApollo({
defaultClient: mockClient,
})
const localVue = global.localVue
const apolloQueryMock = jest
.fn()
.mockResolvedValueOnce({
data: {
listUnconfirmedContributions: [
{
pending: true,
},
{
pending: true,
},
{
pending: true,
},
],
},
})
.mockResolvedValue({
data: {
listUnconfirmedContributions: [
{
pending: true,
},
{
pending: true,
},
{
pending: true,
},
],
},
})
localVue.use(VueApollo)
const storeCommitMock = jest.fn()
@ -43,44 +20,114 @@ const mocks = {
$t: jest.fn((t) => t),
$n: jest.fn((n) => n),
$d: jest.fn((d) => d),
$apollo: {
query: apolloQueryMock,
},
$store: {
commit: storeCommitMock,
state: {
openCreations: 2,
openCreations: 1,
},
},
}
const defaultData = () => {
return {
adminListAllContributions: {
contributionCount: 2,
contributionList: [
{
id: 1,
firstName: 'Bibi',
lastName: 'Bloxberg',
userId: 99,
email: 'bibi@bloxberg.de',
amount: 500,
memo: 'Danke für alles',
date: new Date(),
moderator: 1,
state: 'PENDING',
creation: [500, 500, 500],
messagesCount: 0,
deniedBy: null,
deniedAt: null,
confirmedBy: null,
confirmedAt: null,
contributionDate: new Date(),
deletedBy: null,
deletedAt: null,
createdAt: new Date(),
},
{
id: 2,
firstName: 'Räuber',
lastName: 'Hotzenplotz',
userId: 100,
email: 'raeuber@hotzenplotz.de',
amount: 1000000,
memo: 'Gut Ergattert',
date: new Date(),
moderator: 1,
state: 'PENDING',
creation: [500, 500, 500],
messagesCount: 0,
deniedBy: null,
deniedAt: null,
confirmedBy: null,
confirmedAt: null,
contributionDate: new Date(),
deletedBy: null,
deletedAt: null,
createdAt: new Date(),
},
],
},
}
}
describe('Overview', () => {
let wrapper
const adminListAllContributionsMock = jest.fn()
mockClient.setRequestHandler(
adminListAllContributions,
adminListAllContributionsMock
.mockRejectedValueOnce({ message: 'Ouch!' })
.mockResolvedValue({ data: defaultData() }),
)
const Wrapper = () => {
return mount(Overview, { localVue, mocks })
return mount(Overview, { localVue, mocks, apolloProvider })
}
describe('mount', () => {
beforeEach(() => {
jest.clearAllMocks()
wrapper = Wrapper()
})
it('calls listUnconfirmedContributions', () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
query: listUnconfirmedContributions,
}),
)
describe('server response for get pending creations is error', () => {
it('toast an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Ouch!')
})
})
it('calls the adminListAllContributions query', () => {
expect(adminListAllContributionsMock).toBeCalledWith({
currentPage: 1,
order: 'DESC',
pageSize: 25,
statusFilter: ['IN_PROGRESS', 'PENDING'],
})
})
it('commits three pending creations to store', () => {
expect(storeCommitMock).toBeCalledWith('setOpenCreations', 3)
expect(storeCommitMock).toBeCalledWith('setOpenCreations', 2)
})
describe('with open creations', () => {
it('renders a link to confirm creations', () => {
expect(wrapper.find('a[href="creation-confirm"]').text()).toContain('2')
beforeEach(() => {
mocks.$store.state.openCreations = 2
})
it('renders a link to confirm 2 creations', () => {
expect(wrapper.find('[data-test="open-creation"]').text()).toContain('2')
expect(wrapper.find('a[href="creation-confirm"]').exists()).toBeTruthy()
})
})
@ -91,7 +138,7 @@ describe('Overview', () => {
})
it('renders a link to confirm creations', () => {
expect(wrapper.find('a[href="creation-confirm"]').text()).toContain('0')
expect(wrapper.find('[data-test="open-creation"]').text()).toContain('0')
expect(wrapper.find('a[href="creation-confirm"]').exists()).toBeTruthy()
})
})

View File

@ -24,31 +24,40 @@
>
<b-card-text>
<b-link to="creation-confirm">
<h1>{{ $store.state.openCreations }}</h1>
<h1 data-test="open-creation">{{ $store.state.openCreations }}</h1>
</b-link>
</b-card-text>
</b-card>
</div>
</template>
<script>
import { listUnconfirmedContributions } from '@/graphql/listUnconfirmedContributions.js'
import { adminListAllContributions } from '../graphql/adminListAllContributions'
export default {
name: 'overview',
methods: {
getPendingCreations() {
this.$apollo
.query({
query: listUnconfirmedContributions,
fetchPolicy: 'network-only',
})
.then((result) => {
this.$store.commit('setOpenCreations', result.data.listUnconfirmedContributions.length)
})
},
data() {
return {
statusFilter: ['IN_PROGRESS', 'PENDING'],
}
},
created() {
this.getPendingCreations()
apollo: {
AllContributions: {
query() {
return adminListAllContributions
},
variables() {
// may be at some point we need a pagination here
return {
statusFilter: this.statusFilter,
}
},
update({ adminListAllContributions }) {
this.$store.commit('setOpenCreations', adminListAllContributions.contributionCount)
},
error({ message }) {
this.toastError(message)
},
},
},
}
</script>

View File

@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils'
import UserSearch from './UserSearch.vue'
import UserSearch from './UserSearch'
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
const localVue = global.localVue

View File

@ -58,7 +58,7 @@
</div>
</template>
<script>
import SearchUserTable from '../components/Tables/SearchUserTable.vue'
import SearchUserTable from '../components/Tables/SearchUserTable'
import { searchUsers } from '../graphql/searchUsers'
import { creationMonths } from '../mixins/creationMonths'
@ -72,7 +72,6 @@ export default {
return {
showArrays: false,
searchResult: [],
massCreation: [],
criteria: '',
filters: {
byActivated: null,

View File

@ -24,9 +24,6 @@ export const mutations = {
moderator: (state, moderator) => {
state.moderator = moderator
},
setUserSelectedInMassCreation: (state, userSelectedInMassCreation) => {
state.userSelectedInMassCreation = userSelectedInMassCreation
},
}
export const actions = {

View File

@ -10,7 +10,6 @@ const {
resetOpenCreations,
setOpenCreations,
moderator,
setUserSelectedInMassCreation,
} = mutations
const { logout } = actions
@ -65,14 +64,6 @@ describe('Vuex store', () => {
expect(state.openCreations).toEqual(12)
})
})
describe('setUserSelectedInMassCreation', () => {
it('sets userSelectedInMassCreation to given value', () => {
const state = { userSelectedInMassCreation: [] }
setUserSelectedInMassCreation(state, [0, 1, 2])
expect(state.userSelectedInMassCreation).toEqual([0, 1, 2])
})
})
})
describe('actions', () => {

View File

@ -58,8 +58,4 @@ WEBHOOK_ELOPAGE_SECRET=secret
# LOG_LEVEL=info
# Federation
# if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen
# on an hash created from this topic
# FEDERATION_DHT_TOPIC=GRADIDO_HUB
# FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f
# FEDERATION_COMMUNITY_URL=http://localhost:4000/api
FEDERATION_VALIDATE_COMMUNITY_TIMER=60000

View File

@ -55,6 +55,4 @@ EMAIL_CODE_REQUEST_TIME=$EMAIL_CODE_REQUEST_TIME
WEBHOOK_ELOPAGE_SECRET=$WEBHOOK_ELOPAGE_SECRET
# Federation
FEDERATION_DHT_TOPIC=$FEDERATION_DHT_TOPIC
FEDERATION_DHT_SEED=$FEDERATION_DHT_SEED
FEDERATION_COMMUNITY_URL=$FEDERATION_COMMUNITY_URL
FEDERATION_VALIDATE_COMMUNITY_TIMER=$FEDERATION_VALIDATE_COMMUNITY_TIMER

9
backend/.env.test_e2e Normal file
View File

@ -0,0 +1,9 @@
# Server
JWT_EXPIRES_IN=2m
# Email
EMAIL=true
EMAIL_TEST_MODUS=false
EMAIL_TLS=false
# for testing password reset
EMAIL_CODE_REQUEST_TIME=1

View File

@ -2,16 +2,10 @@ module.exports = {
root: true,
env: {
node: true,
// jest: true,
},
parser: '@typescript-eslint/parser',
plugins: ['prettier', '@typescript-eslint' /*, 'jest' */],
extends: [
'standard',
'eslint:recommended',
'plugin:prettier/recommended',
'plugin:@typescript-eslint/recommended',
],
plugins: ['prettier', '@typescript-eslint', 'type-graphql'],
extends: ['standard', 'eslint:recommended', 'plugin:prettier/recommended'],
// add your custom rules here
rules: {
'no-console': ['error'],
@ -23,4 +17,28 @@ module.exports = {
},
],
},
overrides: [
// only for ts files
{
files: ['*.ts'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:@typescript-eslint/recommended-requiring-type-checking',
'plugin:type-graphql/recommended',
],
rules: {
// allow explicitly defined dangling promises
'@typescript-eslint/no-floating-promises': ['error', { ignoreVoid: true }],
'no-void': ['error', { allowAsStatement: true }],
// ignore prefer-regexp-exec rule to allow string.match(regex)
'@typescript-eslint/prefer-regexp-exec': 'off',
},
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json'],
// this is to properly reference the referenced project database without requirement of compiling it
EXPERIMENTAL_useSourceOfProjectReferenceRedirect: true,
},
},
],
}

View File

@ -4,6 +4,11 @@ module.exports = {
preset: 'ts-jest',
collectCoverage: true,
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
coverageThreshold: {
global: {
lines: 80,
},
},
setupFiles: ['<rootDir>/test/testSetup.ts'],
setupFilesAfterEnv: ['<rootDir>/test/extensions.ts'],
modulePathIgnorePatterns: ['<rootDir>/build/'],

View File

@ -1,6 +1,6 @@
{
"name": "gradido-backend",
"version": "1.18.2",
"version": "1.19.1",
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/backend",
@ -13,13 +13,12 @@
"start": "cross-env TZ=UTC TS_NODE_BASEURL=./build node -r tsconfig-paths/register build/src/index.js",
"dev": "cross-env TZ=UTC nodemon -w src --ext ts --exec ts-node -r tsconfig-paths/register src/index.ts",
"lint": "eslint --max-warnings=0 --ext .js,.ts .",
"test": "cross-env TZ=UTC NODE_ENV=development jest --runInBand --coverage --forceExit --detectOpenHandles",
"test": "cross-env TZ=UTC NODE_ENV=development jest --runInBand --forceExit --detectOpenHandles",
"seed": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/seeds/index.ts",
"klicktipp": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/util/klicktipp.ts",
"locales": "scripts/sort.sh"
},
"dependencies": {
"@hyperswarm/dht": "^6.2.0",
"apollo-server-express": "^2.25.2",
"await-semaphore": "^0.1.3",
"axios": "^0.21.1",
@ -31,6 +30,7 @@
"email-templates": "^10.0.1",
"express": "^4.17.1",
"graphql": "^15.5.1",
"graphql-request": "5.0.0",
"i18n": "^0.15.1",
"jsonwebtoken": "^8.5.1",
"lodash.clonedeep": "^4.5.0",
@ -65,6 +65,7 @@
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-type-graphql": "^1.0.0",
"faker": "^5.5.3",
"jest": "^27.2.4",
"nodemon": "^2.0.7",

View File

@ -1,16 +1,19 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import axios from 'axios'
import { backendLogger as logger } from '@/server/logger'
import LogError from '@/server/LogError'
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const apiPost = async (url: string, payload: unknown): Promise<any> => {
logger.trace('POST: url=' + url + ' payload=' + payload)
logger.trace('POST', url, payload)
return axios
.post(url, payload)
.then((result) => {
logger.trace('POST-Response: result=' + result)
logger.trace('POST-Response', result)
if (result.status !== 200) {
throw new Error('HTTP Status Error ' + result.status)
throw new LogError('HTTP Status Error', result.status)
}
if (result.data.state !== 'success') {
throw new Error(result.data.msg)
@ -28,9 +31,9 @@ export const apiGet = async (url: string): Promise<any> => {
return axios
.get(url)
.then((result) => {
logger.trace('GET-Response: result=' + result)
logger.trace('GET-Response', result)
if (result.status !== 200) {
throw new Error('HTTP Status Error ' + result.status)
throw new LogError('HTTP Status Error', result.status)
}
if (!['success', 'warning'].includes(result.data.state)) {
throw new Error(result.data.msg)

View File

@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { KlicktippConnector } from './klicktippConnector'

View File

@ -1,3 +1,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import axios, { AxiosRequestConfig, Method } from 'axios'

View File

@ -2,7 +2,6 @@ import { RIGHTS } from './RIGHTS'
export const INALIENABLE_RIGHTS = [
RIGHTS.LOGIN,
RIGHTS.GET_COMMUNITY_INFO,
RIGHTS.COMMUNITIES,
RIGHTS.CREATE_USER,
RIGHTS.SEND_RESET_PASSWORD_EMAIL,

View File

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

View File

@ -2,7 +2,6 @@ export enum RIGHTS {
LOGIN = 'LOGIN',
VERIFY_LOGIN = 'VERIFY_LOGIN',
BALANCE = 'BALANCE',
GET_COMMUNITY_INFO = 'GET_COMMUNITY_INFO',
COMMUNITIES = 'COMMUNITIES',
LIST_GDT_ENTRIES = 'LIST_GDT_ENTRIES',
EXIST_PID = 'EXIST_PID',
@ -42,7 +41,6 @@ export enum RIGHTS {
DELETE_USER = 'DELETE_USER',
UNDELETE_USER = 'UNDELETE_USER',
ADMIN_CREATE_CONTRIBUTION = 'ADMIN_CREATE_CONTRIBUTION',
ADMIN_CREATE_CONTRIBUTIONS = 'ADMIN_CREATE_CONTRIBUTIONS',
ADMIN_UPDATE_CONTRIBUTION = 'ADMIN_UPDATE_CONTRIBUTION',
ADMIN_DELETE_CONTRIBUTION = 'ADMIN_DELETE_CONTRIBUTION',
LIST_UNCONFIRMED_CONTRIBUTIONS = 'LIST_UNCONFIRMED_CONTRIBUTIONS',
@ -55,4 +53,5 @@ export enum RIGHTS {
UPDATE_CONTRIBUTION_LINK = 'UPDATE_CONTRIBUTION_LINK',
ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE',
DENY_CONTRIBUTION = 'DENY_CONTRIBUTION',
ADMIN_OPEN_CREATIONS = 'ADMIN_OPEN_CREATIONS',
}

View File

@ -10,7 +10,7 @@ Decimal.set({
})
const constants = {
DB_VERSION: '0059-add_hide_amount_to_users',
DB_VERSION: '0061-event_refactoring',
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info
@ -115,14 +115,8 @@ if (
}
const federation = {
FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || null,
FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null,
FEDERATION_COMMUNITY_URL:
process.env.FEDERATION_COMMUNITY_URL === undefined
? null
: process.env.FEDERATION_COMMUNITY_URL.endsWith('/')
? process.env.FEDERATION_COMMUNITY_URL
: process.env.FEDERATION_COMMUNITY_URL + '/',
FEDERATION_VALIDATE_COMMUNITY_TIMER:
Number(process.env.FEDERATION_VALIDATE_COMMUNITY_TIMER) || 60000,
}
const CONFIG = {

View File

@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/unbound-method */
import { createTransport } from 'nodemailer'
import { logger, i18n } from '@test/testSetup'
import CONFIG from '@/config'
@ -8,6 +10,7 @@ CONFIG.EMAIL_SMTP_URL = 'EMAIL_SMTP_URL'
CONFIG.EMAIL_SMTP_PORT = '1234'
CONFIG.EMAIL_USERNAME = 'user'
CONFIG.EMAIL_PASSWORD = 'pwd'
CONFIG.EMAIL_TLS = true
jest.mock('nodemailer', () => {
return {

View File

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions */
import CONFIG from '@/config'
import { backendLogger as logger } from '@/server/logger'
import path from 'path'

View File

@ -1,5 +1,8 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import Decimal from 'decimal.js-light'
import { testEnvironment } from '@test/helpers'
import { logger, i18n as localization } from '@test/testSetup'
@ -106,7 +109,7 @@ describe('sendEmailVariants', () => {
'you have received a message from Bibi Bloxberg regarding your common good contribution “My contribution.”.',
)
expect(result.originalMessage.html).toContain(
'To view and reply to the message, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!',
'To view and reply to the message, go to the “Creation” menu in your Gradido account and click on the “My contributions” tab!',
)
expect(result.originalMessage.html).toContain(
`Link to your account: <a href="${CONFIG.EMAIL_LINK_OVERVIEW}">${CONFIG.EMAIL_LINK_OVERVIEW}</a>`,
@ -424,7 +427,7 @@ describe('sendEmailVariants', () => {
'Your public good contribution “My contribution.” was rejected by Bibi Bloxberg.',
)
expect(result.originalMessage.html).toContain(
'To see your common good contributions and related messages, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!',
'To see your common good contributions and related messages, go to the “Creation” menu in your Gradido account and click on the “My contributions” tab!',
)
expect(result.originalMessage.html).toContain(
`Link to your account: <a href="${CONFIG.EMAIL_LINK_OVERVIEW}">${CONFIG.EMAIL_LINK_OVERVIEW}</a>`,
@ -502,7 +505,7 @@ describe('sendEmailVariants', () => {
'Your public good contribution “My contribution.” was deleted by Bibi Bloxberg.',
)
expect(result.originalMessage.html).toContain(
'To see your common good contributions and related messages, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!',
'To see your common good contributions and related messages, go to the “Creation” menu in your Gradido account and click on the “My contributions” tab!',
)
expect(result.originalMessage.html).toContain(
`Link to your account: <a href="${CONFIG.EMAIL_LINK_OVERVIEW}">${CONFIG.EMAIL_LINK_OVERVIEW}</a>`,

View File

@ -1,212 +1,217 @@
import { EventProtocol as DbEvent } from '@entity/EventProtocol'
import { Event as DbEvent } from '@entity/Event'
import { User as DbUser } from '@entity/User'
import { ContributionMessage as DbContributionMessage } from '@entity/ContributionMessage'
import { Contribution as DbContribution } from '@entity/Contribution'
import { Transaction as DbTransaction } from '@entity/Transaction'
import Decimal from 'decimal.js-light'
import { EventProtocolType } from './EventProtocolType'
export const Event = (
type: EventProtocolType,
userId: number,
xUserId: number | null = null,
xCommunityId: number | null = null,
transactionId: number | null = null,
contributionId: number | null = null,
affectedUser: DbUser,
actingUser: DbUser,
involvedUser: DbUser | null = null,
involvedTransaction: DbTransaction | null = null,
involvedContribution: DbContribution | null = null,
involvedContributionMessage: DbContributionMessage | null = null,
amount: Decimal | null = null,
messageId: number | null = null,
): DbEvent => {
const event = new DbEvent()
event.type = type
event.userId = userId
event.xUserId = xUserId
event.xCommunityId = xCommunityId
event.transactionId = transactionId
event.contributionId = contributionId
event.affectedUser = affectedUser
event.actingUser = actingUser
event.involvedUser = involvedUser
event.involvedTransaction = involvedTransaction
event.involvedContribution = involvedContribution
event.involvedContributionMessage = involvedContributionMessage
event.amount = amount
event.messageId = messageId
return event
}
export const EVENT_CONTRIBUTION_CREATE = async (
userId: number,
contributionId: number,
user: DbUser,
contribution: DbContribution,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.CONTRIBUTION_CREATE,
userId,
user,
user,
null,
null,
contribution,
null,
contributionId,
amount,
).save()
export const EVENT_CONTRIBUTION_DELETE = async (
userId: number,
contributionId: number,
user: DbUser,
contribution: DbContribution,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.CONTRIBUTION_DELETE,
userId,
user,
user,
null,
null,
contribution,
null,
contributionId,
amount,
).save()
export const EVENT_CONTRIBUTION_UPDATE = async (
userId: number,
contributionId: number,
user: DbUser,
contribution: DbContribution,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.CONTRIBUTION_UPDATE,
userId,
user,
user,
null,
null,
contribution,
null,
contributionId,
amount,
).save()
export const EVENT_ADMIN_CONTRIBUTION_CREATE = async (
userId: number,
contributionId: number,
user: DbUser,
moderator: DbUser,
contribution: DbContribution,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.ADMIN_CONTRIBUTION_CREATE,
userId,
user,
moderator,
null,
null,
contribution,
null,
contributionId,
amount,
).save()
export const EVENT_ADMIN_CONTRIBUTION_UPDATE = async (
userId: number,
contributionId: number,
user: DbUser,
moderator: DbUser,
contribution: DbContribution,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.ADMIN_CONTRIBUTION_UPDATE,
userId,
user,
moderator,
null,
null,
contribution,
null,
contributionId,
amount,
).save()
export const EVENT_ADMIN_CONTRIBUTION_DELETE = async (
userId: number,
contributionId: number,
user: DbUser,
moderator: DbUser,
contribution: DbContribution,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.ADMIN_CONTRIBUTION_DELETE,
userId,
user,
moderator,
null,
null,
contribution,
null,
contributionId,
amount,
).save()
export const EVENT_CONTRIBUTION_CONFIRM = async (
userId: number,
contributionId: number,
user: DbUser,
moderator: DbUser,
contribution: DbContribution,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.CONTRIBUTION_CONFIRM,
userId,
user,
moderator,
null,
null,
contribution,
null,
contributionId,
amount,
).save()
export const EVENT_ADMIN_CONTRIBUTION_DENY = async (
userId: number,
xUserId: number,
contributionId: number,
user: DbUser,
moderator: DbUser,
contribution: DbContribution,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.ADMIN_CONTRIBUTION_DENY,
userId,
xUserId,
user,
moderator,
null,
null,
contributionId,
contribution,
null,
amount,
).save()
export const EVENT_TRANSACTION_SEND = async (
userId: number,
xUserId: number,
transactionId: number,
user: DbUser,
involvedUser: DbUser,
transaction: DbTransaction,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.TRANSACTION_SEND,
userId,
xUserId,
user,
user,
involvedUser,
transaction,
null,
transactionId,
null,
amount,
).save()
export const EVENT_TRANSACTION_RECEIVE = async (
userId: number,
xUserId: number,
transactionId: number,
user: DbUser,
involvedUser: DbUser,
transaction: DbTransaction,
amount: Decimal,
): Promise<DbEvent> =>
Event(
EventProtocolType.TRANSACTION_RECEIVE,
userId,
xUserId,
user,
involvedUser,
involvedUser,
transaction,
null,
transactionId,
null,
amount,
).save()
export const EVENT_LOGIN = async (userId: number): Promise<DbEvent> =>
Event(EventProtocolType.LOGIN, userId, null, null, null, null, null, null).save()
export const EVENT_LOGIN = async (user: DbUser): Promise<DbEvent> =>
Event(EventProtocolType.LOGIN, user, user).save()
export const EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL = async (
userId: number,
): Promise<DbEvent> => Event(EventProtocolType.SEND_ACCOUNT_MULTIREGISTRATION_EMAIL, userId).save()
export const EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL = async (user: DbUser): Promise<DbEvent> =>
Event(EventProtocolType.SEND_ACCOUNT_MULTIREGISTRATION_EMAIL, user, { id: 0 } as DbUser).save()
export const EVENT_SEND_CONFIRMATION_EMAIL = async (userId: number): Promise<DbEvent> =>
Event(EventProtocolType.SEND_CONFIRMATION_EMAIL, userId).save()
export const EVENT_SEND_CONFIRMATION_EMAIL = async (user: DbUser): Promise<DbEvent> =>
Event(EventProtocolType.SEND_CONFIRMATION_EMAIL, user, user).save()
export const EVENT_ADMIN_SEND_CONFIRMATION_EMAIL = async (userId: number): Promise<DbEvent> =>
Event(EventProtocolType.ADMIN_SEND_CONFIRMATION_EMAIL, userId).save()
export const EVENT_ADMIN_SEND_CONFIRMATION_EMAIL = async (
user: DbUser,
moderator: DbUser,
): Promise<DbEvent> =>
Event(EventProtocolType.ADMIN_SEND_CONFIRMATION_EMAIL, user, moderator).save()
/* export const EVENT_REDEEM_REGISTER = async (
userId: number,
transactionId: number | null = null,
contributionId: number | null = null,
): Promise<Event> =>
Event(
EventProtocolType.REDEEM_REGISTER,
userId,
null,
null,
transactionId,
contributionId,
).save()
*/
export const EVENT_REGISTER = async (user: DbUser): Promise<DbEvent> =>
Event(EventProtocolType.REGISTER, user, user).save()
export const EVENT_REGISTER = async (userId: number): Promise<DbEvent> =>
Event(EventProtocolType.REGISTER, userId).save()
export const EVENT_ACTIVATE_ACCOUNT = async (userId: number): Promise<DbEvent> =>
Event(EventProtocolType.ACTIVATE_ACCOUNT, userId).save()
export const EVENT_ACTIVATE_ACCOUNT = async (user: DbUser): Promise<DbEvent> =>
Event(EventProtocolType.ACTIVATE_ACCOUNT, user, user).save()

View File

@ -0,0 +1,41 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { gql } from 'graphql-request'
import { backendLogger as logger } from '@/server/logger'
import { Community as DbCommunity } from '@entity/Community'
import { GraphQLGetClient } from '../GraphQLGetClient'
import LogError from '@/server/LogError'
export async function requestGetPublicKey(dbCom: DbCommunity): Promise<string | undefined> {
let endpoint = dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/'
endpoint = `${endpoint}${dbCom.apiVersion}/`
logger.info(`requestGetPublicKey with endpoint='${endpoint}'...`)
const graphQLClient = GraphQLGetClient.getInstance(endpoint)
logger.debug(`graphQLClient=${JSON.stringify(graphQLClient)}`)
const query = gql`
query {
getPublicKey {
publicKey
}
}
`
const variables = {}
try {
const { data, errors, extensions, headers, status } = await graphQLClient.rawRequest(
query,
variables,
)
logger.debug(`Response-Data:`, data, errors, extensions, headers, status)
if (data) {
logger.debug(`Response-PublicKey:`, data.getPublicKey.publicKey)
logger.info(`requestGetPublicKey processed successfully`)
return data.getPublicKey.publicKey
}
logger.warn(`requestGetPublicKey processed without response data`)
} catch (err) {
throw new LogError(`Request-Error:`, err)
}
}

View File

@ -0,0 +1,41 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import { gql } from 'graphql-request'
import { backendLogger as logger } from '@/server/logger'
import { Community as DbCommunity } from '@entity/Community'
import { GraphQLGetClient } from '../GraphQLGetClient'
import LogError from '@/server/LogError'
export async function requestGetPublicKey(dbCom: DbCommunity): Promise<string | undefined> {
let endpoint = dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/'
endpoint = `${endpoint}${dbCom.apiVersion}/`
logger.info(`requestGetPublicKey with endpoint='${endpoint}'...`)
const graphQLClient = GraphQLGetClient.getInstance(endpoint)
logger.debug(`graphQLClient=${JSON.stringify(graphQLClient)}`)
const query = gql`
query {
getPublicKey {
publicKey
}
}
`
const variables = {}
try {
const { data, errors, extensions, headers, status } = await graphQLClient.rawRequest(
query,
variables,
)
logger.debug(`Response-Data:`, data, errors, extensions, headers, status)
if (data) {
logger.debug(`Response-PublicKey:`, data.getPublicKey.publicKey)
logger.info(`requestGetPublicKey processed successfully`)
return data.getPublicKey.publicKey
}
logger.warn(`requestGetPublicKey processed without response data`)
} catch (err) {
throw new LogError(`Request-Error:`, err)
}
}

View File

@ -0,0 +1,43 @@
import { GraphQLClient } from 'graphql-request'
import { PatchedRequestInit } from 'graphql-request/dist/types'
type ClientInstance = {
url: string
// eslint-disable-next-line no-use-before-define
client: GraphQLGetClient
}
export class GraphQLGetClient extends GraphQLClient {
private static instanceArray: ClientInstance[] = []
/**
* The Singleton's constructor should always be private to prevent direct
* construction calls with the `new` operator.
*/
// eslint-disable-next-line no-useless-constructor
private constructor(url: string, options?: PatchedRequestInit) {
super(url, options)
}
/**
* The static method that controls the access to the singleton instance.
*
* This implementation let you subclass the Singleton class while keeping
* just one instance of each subclass around.
*/
public static getInstance(url: string): GraphQLGetClient {
const instance = GraphQLGetClient.instanceArray.find((instance) => instance.url === url)
if (instance) {
return instance.client
}
const client = new GraphQLGetClient(url, {
method: 'GET',
jsonSerializer: {
parse: JSON.parse,
stringify: JSON.stringify,
},
})
GraphQLGetClient.instanceArray.push({ url, client } as ClientInstance)
return client
}
}

View File

@ -0,0 +1,4 @@
export enum ApiVersionType {
V1_0 = '1_0',
V1_1 = '1_1',
}

View File

@ -0,0 +1,163 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/unbound-method */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { logger } from '@test/testSetup'
import { Community as DbCommunity } from '@entity/Community'
import { testEnvironment, cleanDB } from '@test/helpers'
import { validateCommunities } from './validateCommunities'
let con: any
let testEnv: any
beforeAll(async () => {
testEnv = await testEnvironment(logger)
con = testEnv.con
await cleanDB()
})
afterAll(async () => {
// await cleanDB()
await con.close()
})
describe('validate Communities', () => {
/*
describe('start validation loop', () => {
beforeEach(async () => {
jest.clearAllMocks()
startValidateCommunities(0)
})
it('logs loop started', () => {
expect(logger.info).toBeCalledWith(
`Federation: startValidateCommunities loop with an interval of 0 ms...`,
)
})
})
*/
describe('start validation logic without loop', () => {
beforeEach(async () => {
jest.clearAllMocks()
await validateCommunities()
})
it('logs zero communities found', () => {
expect(logger.debug).toBeCalledWith(`Federation: found 0 dbCommunities`)
})
describe('with one Community of api 1_0', () => {
beforeEach(async () => {
const variables1 = {
publicKey: Buffer.from('11111111111111111111111111111111'),
apiVersion: '1_0',
endPoint: 'http//localhost:5001/api/',
lastAnnouncedAt: new Date(),
}
await DbCommunity.createQueryBuilder()
.insert()
.into(DbCommunity)
.values(variables1)
.orUpdate({
conflict_target: ['id', 'publicKey', 'apiVersion'],
overwrite: ['end_point', 'last_announced_at'],
})
.execute()
jest.clearAllMocks()
await validateCommunities()
})
it('logs one community found', () => {
expect(logger.debug).toBeCalledWith(`Federation: found 1 dbCommunities`)
})
it('logs requestGetPublicKey for community api 1_0 ', () => {
expect(logger.info).toBeCalledWith(
`requestGetPublicKey with endpoint='http//localhost:5001/api/1_0/'...`,
)
})
})
describe('with two Communities of api 1_0 and 1_1', () => {
beforeEach(async () => {
const variables2 = {
publicKey: Buffer.from('11111111111111111111111111111111'),
apiVersion: '1_1',
endPoint: 'http//localhost:5001/api/',
lastAnnouncedAt: new Date(),
}
await DbCommunity.createQueryBuilder()
.insert()
.into(DbCommunity)
.values(variables2)
.orUpdate({
conflict_target: ['id', 'publicKey', 'apiVersion'],
overwrite: ['end_point', 'last_announced_at'],
})
.execute()
jest.clearAllMocks()
await validateCommunities()
})
it('logs two communities found', () => {
expect(logger.debug).toBeCalledWith(`Federation: found 2 dbCommunities`)
})
it('logs requestGetPublicKey for community api 1_0 ', () => {
expect(logger.info).toBeCalledWith(
`requestGetPublicKey with endpoint='http//localhost:5001/api/1_0/'...`,
)
})
it('logs requestGetPublicKey for community api 1_1 ', () => {
expect(logger.info).toBeCalledWith(
`requestGetPublicKey with endpoint='http//localhost:5001/api/1_1/'...`,
)
})
})
describe('with three Communities of api 1_0, 1_1 and 2_0', () => {
let dbCom: DbCommunity
beforeEach(async () => {
const variables3 = {
publicKey: Buffer.from('11111111111111111111111111111111'),
apiVersion: '2_0',
endPoint: 'http//localhost:5001/api/',
lastAnnouncedAt: new Date(),
}
await DbCommunity.createQueryBuilder()
.insert()
.into(DbCommunity)
.values(variables3)
.orUpdate({
conflict_target: ['id', 'publicKey', 'apiVersion'],
overwrite: ['end_point', 'last_announced_at'],
})
.execute()
dbCom = await DbCommunity.findOneOrFail({
where: { publicKey: variables3.publicKey, apiVersion: variables3.apiVersion },
})
jest.clearAllMocks()
await validateCommunities()
})
it('logs three community found', () => {
expect(logger.debug).toBeCalledWith(`Federation: found 3 dbCommunities`)
})
it('logs requestGetPublicKey for community api 1_0 ', () => {
expect(logger.info).toBeCalledWith(
`requestGetPublicKey with endpoint='http//localhost:5001/api/1_0/'...`,
)
})
it('logs requestGetPublicKey for community api 1_1 ', () => {
expect(logger.info).toBeCalledWith(
`requestGetPublicKey with endpoint='http//localhost:5001/api/1_1/'...`,
)
})
it('logs unsupported api for community with api 2_0 ', () => {
expect(logger.warn).toBeCalledWith(
`Federation: dbCom: ${dbCom.id} with unsupported apiVersion=2_0; supported versions`,
['1_0', '1_1'],
)
})
})
})
})

View File

@ -0,0 +1,84 @@
import { Community as DbCommunity } from '@entity/Community'
import { IsNull } from '@dbTools/typeorm'
// eslint-disable-next-line camelcase
import { requestGetPublicKey as v1_0_requestGetPublicKey } from './client/1_0/FederationClient'
// eslint-disable-next-line camelcase
import { requestGetPublicKey as v1_1_requestGetPublicKey } from './client/1_1/FederationClient'
import { backendLogger as logger } from '@/server/logger'
import { ApiVersionType } from './enum/apiVersionType'
import LogError from '@/server/LogError'
export function startValidateCommunities(timerInterval: number): void {
logger.info(
`Federation: startValidateCommunities loop with an interval of ${timerInterval} ms...`,
)
// TODO: replace the timer-loop by an event-based communication to verify announced foreign communities
// better to use setTimeout twice than setInterval once -> see https://javascript.info/settimeout-setinterval
setTimeout(function run() {
void validateCommunities()
setTimeout(run, timerInterval)
}, timerInterval)
}
export async function validateCommunities(): Promise<void> {
const dbCommunities: DbCommunity[] = await DbCommunity.createQueryBuilder()
.where({ foreign: true, verifiedAt: IsNull() })
.orWhere('verified_at < last_announced_at')
.getMany()
logger.debug(`Federation: found ${dbCommunities.length} dbCommunities`)
for (const dbCom of dbCommunities) {
logger.debug('Federation: dbCom', dbCom)
const apiValueStrings: string[] = Object.values(ApiVersionType)
logger.debug(`suppported ApiVersions=`, apiValueStrings)
if (apiValueStrings.includes(dbCom.apiVersion)) {
logger.debug(
`Federation: validate publicKey for dbCom: ${dbCom.id} with apiVersion=${dbCom.apiVersion}`,
)
try {
const pubKey = await invokeVersionedRequestGetPublicKey(dbCom)
logger.info(
'Federation: received publicKey from endpoint',
pubKey,
`${dbCom.endPoint}/${dbCom.apiVersion}`,
)
if (pubKey && pubKey === dbCom.publicKey.toString()) {
logger.info(`Federation: matching publicKey: ${pubKey}`)
await DbCommunity.update({ id: dbCom.id }, { verifiedAt: new Date() })
logger.debug(`Federation: updated dbCom: ${JSON.stringify(dbCom)}`)
} else {
logger.warn(
`Federation: received not matching publicKey -> received: ${
pubKey || 'null'
}, expected: ${dbCom.publicKey.toString()} `,
)
// DbCommunity.delete({ id: dbCom.id })
}
} catch (err) {
if (!isLogError(err)) {
logger.error(`Error:`, err)
}
}
} else {
logger.warn(
`Federation: dbCom: ${dbCom.id} with unsupported apiVersion=${dbCom.apiVersion}; supported versions`,
apiValueStrings,
)
}
}
}
function isLogError(err: unknown) {
return err instanceof LogError
}
async function invokeVersionedRequestGetPublicKey(dbCom: DbCommunity): Promise<string | undefined> {
switch (dbCom.apiVersion) {
case ApiVersionType.V1_0:
return v1_0_requestGetPublicKey(dbCom)
case ApiVersionType.V1_1:
return v1_1_requestGetPublicKey(dbCom)
default:
return undefined
}
}

View File

@ -22,7 +22,7 @@ export default class ContributionLinkArgs {
validTo?: string | null
@Field(() => Decimal, { nullable: true })
maxAmountPerMonth: Decimal | null
maxAmountPerMonth?: Decimal | null
@Field(() => Int)
maxPerCycle: number

View File

@ -1,9 +1,9 @@
import { ArgsType, Field, InputType } from 'type-graphql'
import { ArgsType, Field, Int, InputType } from 'type-graphql'
@InputType()
@ArgsType()
export default class ContributionMessageArgs {
@Field(() => Number)
@Field(() => Int)
contributionId: number
@Field(() => String)

View File

@ -11,11 +11,11 @@ export default class CreateUserArgs {
@Field(() => String)
lastName: string
@Field(() => String)
language?: string // Will default to DEFAULT_LANGUAGE
@Field(() => String, { nullable: true })
language?: string | null
@Field(() => Int, { nullable: true })
publisherId: number
publisherId?: number | null
@Field(() => String, { nullable: true })
redeemCode?: string | null

View File

@ -1,3 +1,4 @@
/* eslint-disable type-graphql/invalid-nullable-input-type */
import { ArgsType, Field, Int } from 'type-graphql'
import { Order } from '@enum/Order'

View File

@ -7,11 +7,14 @@ export default class SearchUsersArgs {
searchText: string
@Field(() => Int, { nullable: true })
// eslint-disable-next-line type-graphql/invalid-nullable-input-type
currentPage?: number
@Field(() => Int, { nullable: true })
// eslint-disable-next-line type-graphql/invalid-nullable-input-type
pageSize?: number
// eslint-disable-next-line type-graphql/wrong-decorator-signature
@Field(() => SearchUsersFilters, { nullable: true, defaultValue: null })
filters: SearchUsersFilters
filters?: SearchUsersFilters | null
}

View File

@ -3,8 +3,8 @@ import { Field, InputType } from 'type-graphql'
@InputType()
export default class SearchUsersFilters {
@Field(() => Boolean, { nullable: true, defaultValue: null })
byActivated: boolean
byActivated?: boolean | null
@Field(() => Boolean, { nullable: true, defaultValue: null })
byDeleted: boolean
byDeleted?: boolean | null
}

View File

@ -1,13 +1,14 @@
/* eslint-disable type-graphql/invalid-nullable-input-type */
import { Field, InputType } from 'type-graphql'
@InputType()
export default class TransactionLinkFilters {
@Field(() => Boolean, { nullable: true })
withDeleted: boolean
withDeleted?: boolean
@Field(() => Boolean, { nullable: true })
withExpired: boolean
withExpired?: boolean
@Field(() => Boolean, { nullable: true })
withRedeemed: boolean
withRedeemed?: boolean
}

View File

@ -9,5 +9,5 @@ export default class UnsecureLoginArgs {
password: string
@Field(() => Int, { nullable: true })
publisherId: number
publisherId?: number | null
}

View File

@ -1,4 +1,4 @@
import { ArgsType, Field } from 'type-graphql'
import { ArgsType, Field, Int } from 'type-graphql'
@ArgsType()
export default class UpdateUserInfosArgs {
@ -11,8 +11,8 @@ export default class UpdateUserInfosArgs {
@Field({ nullable: true })
language?: string
@Field({ nullable: true })
publisherId?: number
@Field(() => Int, { nullable: true })
publisherId?: number | null
@Field({ nullable: true })
password?: string

View File

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

View File

@ -1,19 +0,0 @@
import { ObjectType, Field } from 'type-graphql'
@ObjectType()
export class AdminCreateContributions {
constructor() {
this.success = false
this.successfulContribution = []
this.failedContribution = []
}
@Field(() => Boolean)
success: boolean
@Field(() => [String])
successfulContribution: string[]
@Field(() => [String])
failedContribution: string[]
}

View File

@ -1,4 +1,4 @@
import { ObjectType, Field } from 'type-graphql'
import { ObjectType, Field, Int, Float } from 'type-graphql'
import Decimal from 'decimal.js-light'
@ObjectType()
@ -19,14 +19,14 @@ export class Balance {
@Field(() => Decimal)
balance: Decimal
@Field(() => Number, { nullable: true })
@Field(() => Float, { nullable: true })
balanceGDT: number | null
// the count of all transactions
@Field(() => Number)
@Field(() => Int)
count: number
// the count of transaction links
@Field(() => Number)
@Field(() => Int)
linkCount: number
}

View File

@ -1,31 +1,47 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { ObjectType, Field } from 'type-graphql'
import { ObjectType, Field, Int } from 'type-graphql'
import { Community as DbCommunity } from '@entity/Community'
@ObjectType()
export class Community {
constructor(json?: any) {
if (json) {
this.id = Number(json.id)
this.name = json.name
this.url = json.url
this.description = json.description
this.registerUrl = json.registerUrl
}
constructor(dbCom: DbCommunity) {
this.id = dbCom.id
this.foreign = dbCom.foreign
this.publicKey = dbCom.publicKey.toString()
this.url =
(dbCom.endPoint.endsWith('/') ? dbCom.endPoint : dbCom.endPoint + '/') +
'api/' +
dbCom.apiVersion
this.lastAnnouncedAt = dbCom.lastAnnouncedAt
this.verifiedAt = dbCom.verifiedAt
this.lastErrorAt = dbCom.lastErrorAt
this.createdAt = dbCom.createdAt
this.updatedAt = dbCom.updatedAt
}
@Field(() => Number)
@Field(() => Int)
id: number
@Field(() => Boolean)
foreign: boolean
@Field(() => String)
name: string
publicKey: string
@Field(() => String)
url: string
@Field(() => String)
description: string
@Field(() => Date, { nullable: true })
lastAnnouncedAt: Date | null
@Field(() => String)
registerUrl: string
@Field(() => Date, { nullable: true })
verifiedAt: Date | null
@Field(() => Date, { nullable: true })
lastErrorAt: Date | null
@Field(() => Date, { nullable: true })
createdAt: Date | null
@Field(() => Date, { nullable: true })
updatedAt: Date | null
}

View File

@ -1,9 +1,9 @@
import { ObjectType, Field } from 'type-graphql'
import { ObjectType, Field, Int } from 'type-graphql'
import Decimal from 'decimal.js-light'
@ObjectType()
export class DynamicStatisticsFields {
@Field(() => Number)
@Field(() => Int)
activeUsers: number
@Field(() => Decimal)
@ -15,13 +15,13 @@ export class DynamicStatisticsFields {
@ObjectType()
export class CommunityStatistics {
@Field(() => Number)
@Field(() => Int)
allUsers: number
@Field(() => Number)
@Field(() => Int)
totalUsers: number
@Field(() => Number)
@Field(() => Int)
deletedUsers: number
@Field(() => Decimal)

Some files were not shown because too many files have changed in this diff Show More