Merge branch 'master' into events-unify-names

This commit is contained in:
Ulf Gebhardt 2023-03-29 09:47:40 +02:00
commit 1f0b07b708
Signed by: ulfgebhardt
GPG Key ID: DA6B843E748679C9
166 changed files with 2575 additions and 2225 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,139 +81,6 @@ 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
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# LOCALES FRONTEND #######################################################
##########################################################################
- name: Frontend | Locales
run: cd frontend && yarn && yarn run locales
##############################################################################
# JOB: LINT FRONTEND #########################################################
##############################################################################
lint_frontend:
name: Lint - Frontend
runs-on: ubuntu-latest
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# LINT FRONTEND ##########################################################
##########################################################################
- name: Frontend | Lint
run: cd frontend && yarn && yarn run lint
##############################################################################
# JOB: STYLELINT FRONTEND ####################################################
##############################################################################
stylelint_frontend:
name: Stylelint - Frontend
runs-on: ubuntu-latest
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# STYLELINT FRONTEND #####################################################
##########################################################################
- name: Frontend | Stylelint
run: cd frontend && yarn && yarn run stylelint
##############################################################################
# JOB: LINT ADMIN INTERFACE ##################################################
##############################################################################
lint_admin:
name: Lint - Admin Interface
runs-on: ubuntu-latest
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# LINT ADMIN INTERFACE ###################################################
##########################################################################
- name: Admin Interface | Lint
run: cd admin && yarn && yarn run lint
##############################################################################
# JOB: STYLELINT ADMIN INTERFACE #############################################
##############################################################################
stylelint_admin:
name: Stylelint - Admin Interface
runs-on: ubuntu-latest
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# STYLELINT ADMIN INTERFACE ##############################################
##########################################################################
- name: Admin Interface | Stylelint
run: cd admin && yarn && yarn run stylelint
##############################################################################
# JOB: LOCALES ADMIN #########################################################
##############################################################################
locales_admin:
name: Locales - Admin Interface
runs-on: ubuntu-latest
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# LOCALES FRONTEND #######################################################
##########################################################################
- name: Admin | Locales
run: cd admin && yarn && yarn run locales
##############################################################################
# JOB: LINT BACKEND ##########################################################
##############################################################################
@ -281,7 +97,7 @@ jobs:
# LINT BACKEND ###########################################################
##########################################################################
- name: backend | Lint
run: cd backend && yarn && yarn run lint
run: cd database && yarn && cd ../backend && yarn && yarn run lint
##############################################################################
# JOB: LOCALES BACKEND #######################################################
@ -319,68 +135,6 @@ jobs:
- name: Database | Lint
run: cd database && yarn && yarn run lint
##############################################################################
# JOB: UNIT TEST FRONTEND ###################################################
##############################################################################
unit_test_frontend:
name: Unit tests - Frontend
runs-on: ubuntu-latest
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# UNIT TESTS FRONTEND ####################################################
##########################################################################
- name: Frontend | Unit tests
run: |
cd frontend && yarn && yarn run test
cp -r ./coverage ../
##########################################################################
# COVERAGE CHECK FRONTEND ################################################
##########################################################################
- name: frontend | Coverage check
uses: webcraftmedia/coverage-check-action@master
with:
report_name: Coverage Frontend
type: lcov
result_path: ./frontend/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
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# UNIT TESTS ADMIN INTERFACE #############################################
##########################################################################
- name: Admin Interface | Unit tests
run: |
cd admin && yarn && yarn run test
cp -r ./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: ./admin/coverage/lcov.info
min_coverage: 97
token: ${{ github.token }}
##############################################################################
# JOB: UNIT TEST BACKEND ####################################################
##############################################################################
@ -415,20 +169,7 @@ jobs:
- name: backend | docker-compose database
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps database
- name: backend Unit tests | test
run: |
cd database && yarn && yarn build && cd ../backend && yarn && yarn test
cp -r ./coverage ../
##########################################################################
# 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: 81
token: ${{ github.token }}
run: cd database && yarn && yarn build && cd ../backend && yarn && yarn test
##########################################################################
# DATABASE MIGRATION TEST UP + RESET #####################################
@ -452,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_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 (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: |
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
##########################################################################
# END-TO-END TESTS #######################################################
##########################################################################
- name: End-to-end tests | run tests
id: e2e-tests
run: |
cd e2e-tests/
yarn
yarn run cypress run --spec cypress/e2e/User.Authentication.feature,cypress/e2e/User.Authentication.ResetPassword.feature,cypress/e2e/User.Registration.feature
- 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

@ -3,17 +3,38 @@ 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 - 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
@ -29,30 +50,24 @@ jobs:
##############################################################################
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 - 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

View File

@ -3,11 +3,32 @@ 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
@ -29,30 +50,24 @@ jobs:
##############################################################################
lint:
name: Lint - Federation
if: needs.files-changed.outputs.federation == '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-federation-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/federation.tar
- name: Lint
run: docker run --rm gradido/federation:test yarn run lint
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
needs: [build]
steps:
- name: Checkout code
uses: actions/checkout@v3
@ -84,15 +99,4 @@ jobs:
# run: cd database && yarn && yarn build && cd ../dht-node && yarn && yarn test
- name: Unit tests
run: |
docker run --env NODE_ENV=test --env DB_HOST=mariadb --network gradido_internal-net -v ~/coverage:/app/coverage --rm gradido/federation:test yarn run test
cp -r ~/coverage ./coverage
- name: Coverage check
uses: webcraftmedia/coverage-check-action@master
with:
report_name: Coverage Federation
type: lcov
#result_path: ./federation/coverage/lcov.info
result_path: ./coverage/lcov.info
min_coverage: 72
token: ${{ github.token }}
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

@ -76,7 +76,11 @@ git clone git@github.com:gradido/gradido.git
git submodule update --recursive --init
```
### 2. Run docker-compose
### 2. Install modules
You can go in each under folder (admin, frontend, database, backend, ...) and call ``yarn`` in each folder or you can call ``yarn installAll``.
### 3. Run docker-compose
Run docker-compose to bring up the development environment

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

@ -10,6 +10,7 @@ describe('ContributionMessagesList', () => {
const propsData = {
contributionId: 42,
contributionState: 'PENDING',
}
const mocks = {

View File

@ -1,17 +1,18 @@
<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>
@ -30,6 +31,10 @@ export default {
type: Number,
required: true,
},
contributionState: {
type: String,
required: true,
},
},
data() {
return {

View File

@ -1,43 +1,75 @@
import { mount } from '@vue/test-utils'
import CreationTransactionList from './CreationTransactionList'
import { toastErrorSpy } from '../../test/testSetup'
import VueApollo from 'vue-apollo'
import { createMockClient } from 'mock-apollo-client'
import { adminListContributions } from '../graphql/adminListContributions'
const mockClient = createMockClient()
const apolloProvider = new VueApollo({
defaultClient: mockClient,
})
const localVue = global.localVue
localVue.use(VueApollo)
const apolloQueryMock = jest.fn().mockResolvedValue({
data: {
creationTransactionList: {
const defaultData = () => {
return {
adminListContributions: {
contributionCount: 2,
contributionList: [
{
id: 1,
amount: 5.8,
createdAt: '2022-09-21T11:09:51.000Z',
confirmedAt: null,
contributionDate: '2022-08-01T00:00:00.000Z',
memo: 'für deine Hilfe, Fräulein Rottenmeier',
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,
amount: '47',
createdAt: '2022-09-21T11:09:28.000Z',
confirmedAt: '2022-09-21T11:09:28.000Z',
contributionDate: '2022-08-01T00:00:00.000Z',
memo: 'für deine Hilfe, Frau Holle',
state: 'CONFIRMED',
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: new Date(),
contributionDate: new Date(),
deletedBy: null,
deletedAt: null,
createdAt: new Date(),
},
],
},
},
})
}
}
const mocks = {
$d: jest.fn((t) => t),
$t: jest.fn((t) => t),
$apollo: {
query: apolloQueryMock,
},
}
const propsData = {
@ -48,55 +80,53 @@ const propsData = {
describe('CreationTransactionList', () => {
let wrapper
const adminListContributionsMock = jest.fn()
mockClient.setRequestHandler(
adminListContributions,
adminListContributionsMock
.mockRejectedValueOnce({ message: 'Ouch!' })
.mockResolvedValue({ data: defaultData() }),
)
const Wrapper = () => {
return mount(CreationTransactionList, { localVue, mocks, propsData })
return mount(CreationTransactionList, { localVue, mocks, propsData, apolloProvider })
}
describe('mount', () => {
beforeEach(() => {
jest.clearAllMocks()
wrapper = Wrapper()
})
it('sends query to Apollo when created', () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
currentPage: 1,
pageSize: 10,
order: 'DESC',
userId: 1,
},
}),
)
})
it('has two values for the transaction', () => {
expect(wrapper.find('tbody').findAll('tr').length).toBe(2)
})
describe('query transaction with error', () => {
beforeEach(() => {
apolloQueryMock.mockRejectedValue({ message: 'OUCH!' })
wrapper = Wrapper()
})
it('calls the API', () => {
expect(apolloQueryMock).toBeCalled()
})
describe('server error', () => {
it('toast error', () => {
expect(toastErrorSpy).toBeCalledWith('OUCH!')
expect(toastErrorSpy).toBeCalledWith('Ouch!')
})
})
describe('watch currentPage', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper.setData({ currentPage: 2 })
describe('sever success', () => {
it('sends query to Apollo when created', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
pageSize: 10,
order: 'DESC',
userId: 1,
})
})
it('returns the string in normal order if reversed property is not true', () => {
expect(wrapper.vm.currentPage).toBe(2)
it('has two values for the transaction', () => {
expect(wrapper.find('tbody').findAll('tr').length).toBe(2)
})
describe('watch currentPage', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper.setData({ currentPage: 2 })
})
it('returns the string in normal order if reversed property is not true', () => {
expect(wrapper.vm.currentPage).toBe(2)
})
})
})
})

View File

@ -42,7 +42,7 @@
</div>
</template>
<script>
import { creationTransactionList } from '../graphql/creationTransactionList'
import { adminListContributions } from '../graphql/adminListContributions'
export default {
name: 'CreationTransactionList',
props: {
@ -92,33 +92,26 @@ export default {
],
}
},
methods: {
getTransactions() {
this.$apollo
.query({
query: creationTransactionList,
variables: {
currentPage: this.currentPage,
pageSize: this.perPage,
order: 'DESC',
userId: parseInt(this.userId),
},
})
.then((result) => {
this.rows = result.data.creationTransactionList.contributionCount
this.items = result.data.creationTransactionList.contributionList
})
.catch((error) => {
this.toastError(error.message)
})
},
},
created() {
this.getTransactions()
},
watch: {
currentPage() {
this.getTransactions()
apollo: {
AdminListContributions: {
query() {
return adminListContributions
},
variables() {
return {
currentPage: this.currentPage,
pageSize: this.perPage,
order: 'DESC',
userId: parseInt(this.userId),
}
},
update({ adminListContributions }) {
this.rows = adminListContributions.contributionCount
this.items = adminListContributions.contributionList
},
error({ message }) {
this.toastError(message)
},
},
},
}

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,7 +10,7 @@
<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 class="bg-color-creation p-1" to="/creation-confirm">
<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 }}
@ -52,6 +52,5 @@ export default {
<style>
.navbar-brand-img {
height: 2rem;
padding-left: 10px;
}
</style>

View File

@ -13,17 +13,19 @@
<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"
@ -36,30 +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(reActive)>
<b-button variant="warning" size="md" class="mr-2">
<b-icon icon="arrow-up" variant="light"></b-icon>
</b-button>
</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"
@ -71,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"
@ -104,6 +102,7 @@
<div v-else>
<contribution-messages-list
:contributionId="row.item.id"
:contributionState="row.item.state"
@update-state="updateState"
@update-user-data="updateUserData"
/>
@ -158,13 +157,22 @@ 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-info'
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

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,17 +1,19 @@
import gql from 'graphql-tag'
export const adminListAllContributions = gql`
export const adminListContributions = gql`
query (
$currentPage: Int = 1
$pageSize: Int = 25
$order: Order = DESC
$statusFilter: [ContributionStatus!]
$userId: Int
) {
adminListAllContributions(
adminListContributions(
currentPage: $currentPage
pageSize: $pageSize
order: $order
statusFilter: $statusFilter
userId: $userId
) {
contributionCount
contributionList {

View File

@ -1,23 +0,0 @@
import gql from 'graphql-tag'
export const creationTransactionList = gql`
query ($currentPage: Int = 1, $pageSize: Int = 25, $order: Order = DESC, $userId: Int!) {
creationTransactionList(
currentPage: $currentPage
pageSize: $pageSize
order: $order
userId: $userId
) {
contributionCount
contributionList {
id
amount
createdAt
confirmedAt
contributionDate
memo
state
}
}
}
`

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

@ -63,7 +63,6 @@
"deleted_user": "Alle gelöschten Nutzer",
"delete_user": "Nutzer löschen",
"deny": "Ablehnen",
"edit": "Bearbeiten",
"enabled": "aktiviert",
"error": "Fehler",
"expired": "abgelaufen",
@ -101,7 +100,6 @@
"message": {
"request": "Die Anfrage wurde gesendet."
},
"mod": "Mod",
"moderator": "Moderator",
"name": "Name",
"navbar": {

View File

@ -34,11 +34,11 @@
"all": "All",
"confirms": "Confirmed",
"deleted": "Deleted",
"denied": "Denied",
"denied": "Rejected",
"open": "Open"
},
"created": "Confirmed",
"createdAt": "Created",
"created": "Created for",
"createdAt": "Created at",
"creation": "Creation",
"creationList": "Creation list",
"creation_form": {
@ -53,7 +53,7 @@
"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"
},
@ -63,7 +63,6 @@
"deleted_user": "All deleted user",
"delete_user": "Delete user",
"deny": "Reject",
"edit": "Edit",
"enabled": "enabled",
"error": "Error",
"expired": "expired",
@ -87,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"
}
},
@ -101,7 +100,6 @@
"message": {
"request": "Request has been sent."
},
"mod": "Mod",
"moderator": "Moderator",
"name": "Name",
"navbar": {

View File

@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils'
import CreationConfirm from './CreationConfirm'
import { adminDeleteContribution } from '../graphql/adminDeleteContribution'
import { denyContribution } from '../graphql/denyContribution'
import { adminListAllContributions } from '../graphql/adminListAllContributions'
import { adminListContributions } from '../graphql/adminListContributions'
import { confirmContribution } from '../graphql/confirmContribution'
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
import VueApollo from 'vue-apollo'
@ -38,7 +38,7 @@ const mocks = {
const defaultData = () => {
return {
adminListAllContributions: {
adminListContributions: {
contributionCount: 2,
contributionList: [
{
@ -92,14 +92,14 @@ const defaultData = () => {
describe('CreationConfirm', () => {
let wrapper
const adminListAllContributionsMock = jest.fn()
const adminListContributionsMock = jest.fn()
const adminDeleteContributionMock = jest.fn()
const adminDenyContributionMock = jest.fn()
const confirmContributionMock = jest.fn()
mockClient.setRequestHandler(
adminListAllContributions,
adminListAllContributionsMock
adminListContributions,
adminListContributionsMock
.mockRejectedValueOnce({ message: 'Ouch!' })
.mockResolvedValue({ data: defaultData() }),
)
@ -337,7 +337,7 @@ describe('CreationConfirm', () => {
})
it('refetches contributions with proper filter', () => {
expect(adminListAllContributionsMock).toBeCalledWith({
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
order: 'DESC',
pageSize: 25,
@ -352,7 +352,7 @@ describe('CreationConfirm', () => {
})
it('refetches contributions with proper filter', () => {
expect(adminListAllContributionsMock).toBeCalledWith({
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
order: 'DESC',
pageSize: 25,
@ -368,7 +368,7 @@ describe('CreationConfirm', () => {
})
it('refetches contributions with proper filter', () => {
expect(adminListAllContributionsMock).toBeCalledWith({
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
order: 'DESC',
pageSize: 25,
@ -384,7 +384,7 @@ describe('CreationConfirm', () => {
})
it('refetches contributions with proper filter', () => {
expect(adminListAllContributionsMock).toBeCalledWith({
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
order: 'DESC',
pageSize: 25,
@ -400,7 +400,7 @@ describe('CreationConfirm', () => {
})
it('refetches contributions with proper filter', () => {
expect(adminListAllContributionsMock).toBeCalledWith({
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
order: 'DESC',
pageSize: 25,

View File

@ -5,25 +5,37 @@
<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="$t('contributions.confirms')"
:title-link-attributes="{ 'data-test': 'confirmed' }"
/>
<b-tab
:title="$t('contributions.denied')"
:title-link-attributes="{ 'data-test': 'denied' }"
/>
<b-tab
:title="$t('contributions.deleted')"
:title-link-attributes="{ 'data-test': 'deleted' }"
/>
<b-tab :title="$t('contributions.all')" :title-link-attributes="{ 'data-test': 'all' }" />
<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
@ -73,7 +85,7 @@
<script>
import Overlay from '../components/Overlay'
import OpenCreationsTable from '../components/Tables/OpenCreationsTable'
import { adminListAllContributions } from '../graphql/adminListAllContributions'
import { adminListContributions } from '../graphql/adminListContributions'
import { adminDeleteContribution } from '../graphql/adminDeleteContribution'
import { confirmContribution } from '../graphql/confirmContribution'
import { denyContribution } from '../graphql/denyContribution'
@ -172,6 +184,9 @@ export default {
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() {
@ -180,7 +195,6 @@ export default {
// open contributions
{ 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') },
{
@ -195,11 +209,11 @@ export default {
key: 'contributionDate',
label: this.$t('created'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
return this.formatDateOrDash(value)
},
},
{ key: 'moderator', label: this.$t('moderator') },
{ key: 'editCreation', label: this.$t('edit') },
{ key: 'editCreation', label: this.$t('chat') },
{ key: 'confirm', label: this.$t('save') },
],
[
@ -218,28 +232,28 @@ export default {
key: 'contributionDate',
label: this.$t('created'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
return this.formatDateOrDash(value)
},
},
{
key: 'createdAt',
label: this.$t('createdAt'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
return this.formatDateOrDash(value)
},
},
{
key: 'confirmedAt',
label: this.$t('contributions.confirms'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
return this.formatDateOrDash(value)
},
},
{ key: 'confirmedBy', label: this.$t('moderator') },
{ key: 'chatCreation', label: this.$t('chat') },
],
[
// denied contributions
{ key: 'reActive', label: 'reActive' },
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{
@ -254,29 +268,28 @@ export default {
key: 'contributionDate',
label: this.$t('created'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
return this.formatDateOrDash(value)
},
},
{
key: 'createdAt',
label: this.$t('createdAt'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
return this.formatDateOrDash(value)
},
},
{
key: 'deniedAt',
label: this.$t('contributions.denied'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
return this.formatDateOrDash(value)
},
},
{ key: 'deniedBy', label: this.$t('mod') },
{ key: 'deniedBy', label: this.$t('moderator') },
{ key: 'chatCreation', label: this.$t('chat') },
],
[
// deleted contributions
{ key: 'reActive', label: 'reActive' },
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{
@ -291,29 +304,29 @@ export default {
key: 'contributionDate',
label: this.$t('created'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
return this.formatDateOrDash(value)
},
},
{
key: 'createdAt',
label: this.$t('createdAt'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
return this.formatDateOrDash(value)
},
},
{
key: 'deletedAt',
label: this.$t('contributions.deleted'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
return this.formatDateOrDash(value)
},
},
{ key: 'deletedBy', label: this.$t('mod') },
{ key: 'deletedBy', label: this.$t('moderator') },
{ key: 'chatCreation', label: this.$t('chat') },
],
[
// all contributions
{ key: 'state', label: 'state' },
{ key: 'state', label: this.$t('status') },
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{
@ -328,24 +341,24 @@ export default {
key: 'contributionDate',
label: this.$t('created'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
return this.formatDateOrDash(value)
},
},
{
key: 'createdAt',
label: this.$t('createdAt'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
return this.formatDateOrDash(value)
},
},
{
key: 'confirmedAt',
label: this.$t('contributions.confirms'),
formatter: (value) => {
return this.$d(new Date(value), 'short')
return this.formatDateOrDash(value)
},
},
{ key: 'confirmedBy', label: this.$t('mod') },
{ key: 'confirmedBy', label: this.$t('moderator') },
{ key: 'chatCreation', label: this.$t('chat') },
],
][this.tabIndex]
@ -384,7 +397,7 @@ export default {
apollo: {
ListAllContributions: {
query() {
return adminListAllContributions
return adminListContributions
},
variables() {
return {
@ -394,9 +407,12 @@ export default {
}
},
fetchPolicy: 'no-cache',
update({ adminListAllContributions }) {
this.rows = adminListAllContributions.contributionCount
this.items = adminListAllContributions.contributionList
update({ adminListContributions }) {
this.rows = adminListContributions.contributionCount
this.items = adminListContributions.contributionList
if (this.statusFilter === FILTER_TAB_MAP[0]) {
this.$store.commit('setOpenCreations', adminListContributions.contributionCount)
}
},
error({ message }) {
this.toastError(message)

View File

@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils'
import Overview from './Overview'
import { adminListAllContributions } from '../graphql/adminListAllContributions'
import { adminListContributions } from '../graphql/adminListContributions'
import VueApollo from 'vue-apollo'
import { createMockClient } from 'mock-apollo-client'
import { toastErrorSpy } from '../../test/testSetup'
@ -30,7 +30,7 @@ const mocks = {
const defaultData = () => {
return {
adminListAllContributions: {
adminListContributions: {
contributionCount: 2,
contributionList: [
{
@ -84,11 +84,11 @@ const defaultData = () => {
describe('Overview', () => {
let wrapper
const adminListAllContributionsMock = jest.fn()
const adminListContributionsMock = jest.fn()
mockClient.setRequestHandler(
adminListAllContributions,
adminListAllContributionsMock
adminListContributions,
adminListContributionsMock
.mockRejectedValueOnce({ message: 'Ouch!' })
.mockResolvedValue({ data: defaultData() }),
)
@ -109,8 +109,8 @@ describe('Overview', () => {
})
})
it('calls the adminListAllContributions query', () => {
expect(adminListAllContributionsMock).toBeCalledWith({
it('calls the adminListContributions query', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
order: 'DESC',
pageSize: 25,

View File

@ -31,7 +31,7 @@
</div>
</template>
<script>
import { adminListAllContributions } from '../graphql/adminListAllContributions'
import { adminListContributions } from '../graphql/adminListContributions'
export default {
name: 'overview',
@ -43,7 +43,7 @@ export default {
apollo: {
AllContributions: {
query() {
return adminListAllContributions
return adminListContributions
},
variables() {
// may be at some point we need a pagination here
@ -51,8 +51,8 @@ export default {
statusFilter: this.statusFilter,
}
},
update({ adminListAllContributions }) {
this.$store.commit('setOpenCreations', adminListAllContributions.contributionCount)
update({ adminListContributions }) {
this.$store.commit('setOpenCreations', adminListContributions.contributionCount)
},
error({ message }) {
this.toastError(message)

View File

@ -1,5 +1,5 @@
# Server
JWT_EXPIRES_IN=1m
JWT_EXPIRES_IN=2m
# Email
EMAIL=true

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', 'jest'],
extends: ['standard', 'eslint:recommended', 'plugin:prettier/recommended'],
// add your custom rules here
rules: {
'no-console': ['error'],
@ -22,5 +16,35 @@ module.exports = {
htmlWhitespaceSensitivity: 'ignore',
},
],
// jest
'jest/no-disabled-tests': 'error',
'jest/no-focused-tests': 'error',
'jest/no-identical-title': 'error',
'jest/prefer-to-have-length': 'error',
'jest/valid-expect': 'error',
},
overrides: [
// only for ts files
{
files: ['*.ts', '*.tsx'],
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

@ -0,0 +1 @@
declare module 'klicktipp-api'

View File

@ -4,6 +4,11 @@ module.exports = {
preset: 'ts-jest',
collectCoverage: true,
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
coverageThreshold: {
global: {
lines: 85,
},
},
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,7 +13,7 @@
"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"
@ -62,11 +62,14 @@
"eslint-config-prettier": "^8.3.0",
"eslint-config-standard": "^16.0.3",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-jest": "^27.2.1",
"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",
"klicktipp-api": "^1.0.2",
"nodemon": "^2.0.7",
"prettier": "^2.3.1",
"ts-jest": "^27.0.5",

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,6 +1,10 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* 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'
import KlicktippConnector from 'klicktipp-api'
import CONFIG from '@/config'
const klicktippConnector = new KlicktippConnector()

View File

@ -1,620 +0,0 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import axios, { AxiosRequestConfig, Method } from 'axios'
export class KlicktippConnector {
private baseURL: string
private sessionName: string
private sessionId: string
private error: string
constructor(service?: string) {
this.baseURL = service !== undefined ? service : 'https://api.klicktipp.com'
this.sessionName = ''
this.sessionId = ''
}
/**
* Get last error
*
* @return string an error description of the last error
*/
getLastError(): string {
const result = this.error
return result
}
/**
* login
*
* @param username The login name of the user to login.
* @param password The password of the user.
* @return TRUE on success
*/
async login(username: string, password: string): Promise<boolean> {
if (!(username.length > 0 && password.length > 0)) {
throw new Error('Klicktipp Login failed: Illegal Arguments')
}
const res = await this.httpRequest('/account/login', 'POST', { username, password }, false)
if (!res.isAxiosError) {
this.sessionId = res.data.sessid
this.sessionName = res.data.session_name
return true
}
throw new Error(`Klicktipp Login failed: ${res.response.statusText}`)
}
/**
* Logs out the user currently logged in.
*
* @return TRUE on success
*/
async logout(): Promise<boolean> {
const res = await this.httpRequest('/account/logout', 'POST')
if (!res.isAxiosError) {
this.sessionId = ''
this.sessionName = ''
return true
}
throw new Error(`Klicktipp Logout failed: ${res.response.statusText}`)
}
/**
* Get all subscription processes (lists) of the logged in user. Requires to be logged in.
*
* @return A associative obeject <list id> => <list name>
*/
async subscriptionProcessIndex(): Promise<any> {
const res = await this.httpRequest('/list', 'GET', {}, true)
if (!res.isAxiosError) {
return res.data
}
throw new Error(`Klicktipp Subscription process index failed: ${res.response.statusText}`)
}
/**
* Get subscription process (list) definition. Requires to be logged in.
*
* @param listid The id of the subscription process
*
* @return An object representing the Klicktipp subscription process.
*/
async subscriptionProcessGet(listid: string): Promise<any> {
if (!listid || listid === '') {
throw new Error('Klicktipp Illegal Arguments')
}
// retrieve
const res = await this.httpRequest(`/subscriber/${listid}`, 'GET', {}, true)
if (!res.isAxiosError) {
return res.data
}
throw new Error(`Klicktipp Subscription process get failed: ${res.response.statusText}`)
}
/**
* Get subscription process (list) redirection url for given subscription.
*
* @param listid The id of the subscription process.
* @param email The email address of the subscriber.
*
* @return A redirection url as defined in the subscription process.
*/
async subscriptionProcessRedirect(listid: string, email: string): Promise<any> {
if (!listid || listid === '' || !email || email === '') {
throw new Error('Klicktipp Illegal Arguments')
}
// update
const data = { listid, email }
const res = await this.httpRequest('/list/redirect', 'POST', data)
if (!res.isAxiosError) {
return res.data
}
throw new Error(
`Klicktipp Subscription process get redirection url failed: ${res.response.statusText}`,
)
}
/**
* Get all manual tags of the logged in user. Requires to be logged in.
*
* @return A associative object <tag id> => <tag name>
*/
async tagIndex(): Promise<any> {
const res = await this.httpRequest('/tag', 'GET', {}, true)
if (!res.isAxiosError) {
return res.data
}
throw new Error(`Klicktipp Tag index failed: ${res.response.statusText}`)
}
/**
* Get a tag definition. Requires to be logged in.
*
* @param tagid The tag id.
*
* @return An object representing the Klicktipp tag object.
*/
async tagGet(tagid: string): Promise<any> {
if (!tagid || tagid === '') {
throw new Error('Klicktipp Illegal Arguments')
}
const res = await this.httpRequest(`/tag/${tagid}`, 'GET', {}, true)
if (!res.isAxiosError) {
return res.data
}
throw new Error(`Klicktipp Tag get failed: ${res.response.statusText}`)
}
/**
* Create a new manual tag. Requires to be logged in.
*
* @param name The name of the tag.
* @param text (optional) An additional description of the tag.
*
* @return The id of the newly created tag or false if failed.
*/
async tagCreate(name: string, text?: string): Promise<boolean> {
if (!name || name === '') {
throw new Error('Klicktipp Illegal Arguments')
}
const data = {
name,
text: text !== undefined ? text : '',
}
const res = await this.httpRequest('/tag', 'POST', data, true)
if (!res.isAxiosError) {
return res.data
}
throw new Error(`Klicktipp Tag creation failed: ${res.response.statusText}`)
}
/**
* Updates a tag. Requires to be logged in.
*
* @param tagid The tag id used to identify which tag to modify.
* @param name (optional) The new tag name. Set empty to leave it unchanged.
* @param text (optional) The new tag description. Set empty to leave it unchanged.
*
* @return TRUE on success
*/
async tagUpdate(tagid: string, name?: string, text?: string): Promise<boolean> {
if (!tagid || tagid === '' || (name === '' && text === '')) {
throw new Error('Klicktipp Illegal Arguments')
}
const data = {
name: name !== undefined ? name : '',
text: text !== undefined ? text : '',
}
const res = await this.httpRequest(`/tag/${tagid}`, 'PUT', data, true)
if (!res.isAxiosError) {
return true
}
throw new Error(`Klicktipp Tag update failed: ${res.response.statusText}`)
}
/**
* Deletes a tag. Requires to be logged in.
*
* @param tagid The user id of the user to delete.
*
* @return TRUE on success
*/
async tagDelete(tagid: string): Promise<boolean> {
if (!tagid || tagid === '') {
throw new Error('Klicktipp Illegal Arguments')
}
const res = await this.httpRequest(`/tag/${tagid}`, 'DELETE')
if (!res.isAxiosError) {
return true
}
throw new Error(`Klicktipp Tag deletion failed: ${res.response.statusText}`)
}
/**
* Get all contact fields of the logged in user. Requires to be logged in.
*
* @return A associative object <field id> => <field name>
*/
async fieldIndex(): Promise<any> {
const res = await this.httpRequest('/field', 'GET', {}, true)
if (!res.isAxiosError) {
return res.data
}
throw new Error(`Klicktipp Field index failed: ${res.response.statusText}`)
}
/**
* Subscribe an email. Requires to be logged in.
*
* @param email The email address of the subscriber.
* @param listid (optional) The id subscription process.
* @param tagid (optional) The id of the manual tag the subscriber will be tagged with.
* @param fields (optional) Additional fields of the subscriber.
*
* @return An object representing the Klicktipp subscriber object.
*/
async subscribe(
email: string,
listid?: number,
tagid?: number,
fields?: any,
smsnumber?: string,
): Promise<any> {
if ((!email || email === '') && smsnumber === '') {
throw new Error('Illegal Arguments')
}
// subscribe
const data = {
email,
fields: fields !== undefined ? fields : {},
smsnumber: smsnumber !== undefined ? smsnumber : '',
listid: listid !== undefined ? listid : 0,
tagid: tagid !== undefined ? tagid : 0,
}
const res = await this.httpRequest('/subscriber', 'POST', data, true)
if (!res.isAxiosError) {
return res.data
}
throw new Error(`Klicktipp Subscription failed: ${res.response.statusText}`)
}
/**
* Unsubscribe an email. Requires to be logged in.
*
* @param email The email address of the subscriber.
*
* @return TRUE on success
*/
async unsubscribe(email: string): Promise<boolean> {
if (!email || email === '') {
throw new Error('Klicktipp Illegal Arguments')
}
// unsubscribe
const data = { email }
const res = await this.httpRequest('/subscriber/unsubscribe', 'POST', data, true)
if (!res.isAxiosError) {
return true
}
throw new Error(`Klicktipp Unsubscription failed: ${res.response.statusText}`)
}
/**
* Tag an email. Requires to be logged in.
*
* @param email The email address of the subscriber.
* @param tagids an array of the manual tag(s) the subscriber will be tagged with.
*
* @return TRUE on success
*/
async tag(email: string, tagids: string): Promise<boolean> {
if (!email || email === '' || !tagids || tagids === '') {
throw new Error('Klicktipp Illegal Arguments')
}
// tag
const data = {
email,
tagids,
}
const res = await this.httpRequest('/subscriber/tag', 'POST', data, true)
if (!res.isAxiosError) {
return res.data
}
throw new Error(`Klicktipp Tagging failed: ${res.response.statusText}`)
}
/**
* Untag an email. Requires to be logged in.
*
* @param mixed $email The email address of the subscriber.
* @param mixed $tagid The id of the manual tag that will be removed from the subscriber.
*
* @return TRUE on success.
*/
async untag(email: string, tagid: string): Promise<boolean> {
if (!email || email === '' || !tagid || tagid === '') {
throw new Error('Klicktipp Illegal Arguments')
}
// subscribe
const data = {
email,
tagid,
}
const res = await this.httpRequest('/subscriber/untag', 'POST', data, true)
if (!res.isAxiosError) {
return true
}
throw new Error(`Klicktipp Untagging failed: ${res.response.statusText}`)
}
/**
* Resend an autoresponder for an email address. Requires to be logged in.
*
* @param email A valid email address
* @param autoresponder An id of the autoresponder
*
* @return TRUE on success
*/
async resend(email: string, autoresponder: string): Promise<boolean> {
if (!email || email === '' || !autoresponder || autoresponder === '') {
throw new Error('Klicktipp Illegal Arguments')
}
// resend/reset autoresponder
const data = { email, autoresponder }
const res = await this.httpRequest('/subscriber/resend', 'POST', data, true)
if (!res.isAxiosError) {
return true
}
throw new Error(`Klicktipp Resend failed: ${res.response.statusText}`)
}
/**
* Get all active subscribers. Requires to be logged in.
*
* @return An array of subscriber ids.
*/
async subscriberIndex(): Promise<[string]> {
const res = await this.httpRequest('/subscriber', 'GET', undefined, true)
if (!res.isAxiosError) {
return res.data
}
throw new Error(`Klicktipp Subscriber index failed: ${res.response.statusText}`)
}
/**
* Get subscriber information. Requires to be logged in.
*
* @param subscriberid The subscriber id.
*
* @return An object representing the Klicktipp subscriber.
*/
async subscriberGet(subscriberid: string): Promise<any> {
if (!subscriberid || subscriberid === '') {
throw new Error('Klicktipp Illegal Arguments')
}
// retrieve
const res = await this.httpRequest(`/subscriber/${subscriberid}`, 'GET', {}, true)
if (!res.isAxiosError) {
return res.data
}
throw new Error(`Klicktipp Subscriber get failed: ${res.response.statusText}`)
}
/**
* Get a subscriber id by email. Requires to be logged in.
*
* @param email The email address of the subscriber.
*
* @return The id of the subscriber. Use subscriber_get to get subscriber details.
*/
async subscriberSearch(email: string): Promise<any> {
if (!email || email === '') {
throw new Error('Klicktipp Illegal Arguments')
}
// search
const data = { email }
const res = await this.httpRequest('/subscriber/search', 'POST', data, true)
if (!res.isAxiosError) {
return res.data
}
throw new Error(`Klicktipp Subscriber search failed: ${res.response.statusText}`)
}
/**
* Get all active subscribers tagged with the given tag id. Requires to be logged in.
*
* @param tagid The id of the tag.
*
* @return An array with id -> subscription date of the tagged subscribers. Use subscriber_get to get subscriber details.
*/
async subscriberTagged(tagid: string): Promise<any> {
if (!tagid || tagid === '') {
throw new Error('Klicktipp Illegal Arguments')
}
// search
const data = { tagid }
const res = await this.httpRequest('/subscriber/tagged', 'POST', data, true)
if (!res.isAxiosError) {
return res.data
}
throw new Error(`Klicktipp subscriber tagged failed: ${res.response.statusText}`)
}
/**
* Updates a subscriber. Requires to be logged in.
*
* @param subscriberid The id of the subscriber to update.
* @param fields (optional) The fields of the subscriber to update
* @param newemail (optional) The new email of the subscriber to update
*
* @return TRUE on success
*/
async subscriberUpdate(
subscriberid: string,
fields?: any,
newemail?: string,
newsmsnumber?: string,
): Promise<boolean> {
if (!subscriberid || subscriberid === '') {
throw new Error('Klicktipp Illegal Arguments')
}
// update
const data = {
fields: fields !== undefined ? fields : {},
newemail: newemail !== undefined ? newemail : '',
newsmsnumber: newsmsnumber !== undefined ? newsmsnumber : '',
}
const res = await this.httpRequest(`/subscriber/${subscriberid}`, 'PUT', data, true)
if (!res.isAxiosError) {
return true
}
throw new Error(`Klicktipp Subscriber update failed: ${res.response.statusText}`)
}
/**
* Delete a subscribe. Requires to be logged in.
*
* @param subscriberid The id of the subscriber to update.
*
* @return TRUE on success.
*/
async subscriberDelete(subscriberid: string): Promise<boolean> {
if (!subscriberid || subscriberid === '') {
throw new Error('Klicktipp Illegal Arguments')
}
// delete
const res = await this.httpRequest(`/subscriber/${subscriberid}`, 'DELETE', {}, true)
if (!res.isAxiosError) {
return true
}
throw new Error(`Klicktipp Subscriber deletion failed: ${res.response.statusText}`)
}
/**
* Subscribe an email. Requires an api key.
*
* @param apikey The api key (listbuildng configuration).
* @param email The email address of the subscriber.
* @param fields (optional) Additional fields of the subscriber.
*
* @return A redirection url as defined in the subscription process.
*/
async signin(apikey: string, email: string, fields?: any, smsnumber?: string): Promise<boolean> {
if (!apikey || apikey === '' || ((!email || email === '') && smsnumber === '')) {
throw new Error('Klicktipp Illegal Arguments')
}
// subscribe
const data = {
apikey,
email,
fields: fields !== undefined ? fields : {},
smsnumber: smsnumber !== undefined ? smsnumber : '',
}
const res = await this.httpRequest('/subscriber/signin', 'POST', data)
if (!res.isAxiosError) {
return true
}
throw new Error(`Klicktipp Subscription failed: ${res.response.statusText}`)
}
/**
* Untag an email. Requires an api key.
*
* @param apikey The api key (listbuildng configuration).
* @param email The email address of the subscriber.
*
* @return TRUE on success
*/
async signout(apikey: string, email: string): Promise<boolean> {
if (!apikey || apikey === '' || !email || email === '') {
throw new Error('Klicktipp Illegal Arguments')
}
// untag
const data = { apikey, email }
const res = await this.httpRequest('/subscriber/signout', 'POST', data)
if (!res.isAxiosError) {
return true
}
throw new Error(`Klicktipp Untagging failed: ${res.response.statusText}`)
}
/**
* Unsubscribe an email. Requires an api key.
*
* @param apikey The api key (listbuildng configuration).
* @param email The email address of the subscriber.
*
* @return TRUE on success
*/
async signoff(apikey: string, email: string): Promise<boolean> {
if (!apikey || apikey === '' || !email || email === '') {
throw new Error('Klicktipp Illegal Arguments')
}
// unsubscribe
const data = { apikey, email }
const res = await this.httpRequest('/subscriber/signoff', 'POST', data)
if (!res.isAxiosError) {
return true
}
throw new Error(`Klicktipp Unsubscription failed: ${res.response.statusText}`)
}
async httpRequest(path: string, method?: Method, data?: any, usesession?: boolean): Promise<any> {
if (method === undefined) {
method = 'GET'
}
const options: AxiosRequestConfig = {
baseURL: this.baseURL,
method,
url: path,
data,
headers: {
'Content-Type': 'application/json',
Content: 'application/json',
Cookie:
usesession && this.sessionName !== '' ? `${this.sessionName}=${this.sessionId}` : '',
},
}
return axios(options)
.then((res) => res)
.catch((error) => error)
}
}

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

@ -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',
@ -37,21 +36,21 @@ export enum RIGHTS {
LIST_ALL_CONTRIBUTION_MESSAGES = 'LIST_ALL_CONTRIBUTION_MESSAGES',
OPEN_CREATIONS = 'OPEN_CREATIONS',
// Admin
ADMIN_SEARCH_USERS = 'SEARCH_USERS',
ADMIN_SET_USER_ROLE = 'SET_USER_ROLE',
ADMIN_DELETE_USER = 'DELETE_USER',
ADMIN_UNDELETE_USER = 'UNDELETE_USER',
SEARCH_USERS = 'SEARCH_USERS',
SET_USER_ROLE = 'SET_USER_ROLE',
DELETE_USER = 'DELETE_USER',
UNDELETE_USER = 'UNDELETE_USER',
ADMIN_CREATE_CONTRIBUTION = 'ADMIN_CREATE_CONTRIBUTION',
ADMIN_UPDATE_CONTRIBUTION = 'ADMIN_UPDATE_CONTRIBUTION',
ADMIN_DELETE_CONTRIBUTION = 'ADMIN_DELETE_CONTRIBUTION',
ADMIN_LIST_UNCONFIRMED_CONTRIBUTIONS = 'LIST_UNCONFIRMED_CONTRIBUTIONS',
ADMIN_CONFIRM_CONTRIBUTION = 'CONFIRM_CONTRIBUTION',
ADMIN_SEND_ACTIVATION_EMAIL = 'SEND_ACTIVATION_EMAIL',
ADMIN_CREATION_TRANSACTION_LIST = 'CREATION_TRANSACTION_LIST',
ADMIN_LIST_TRANSACTION_LINKS_ADMIN = 'LIST_TRANSACTION_LINKS_ADMIN',
ADMIN_CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK',
ADMIN_DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK',
ADMIN_UPDATE_CONTRIBUTION_LINK = 'UPDATE_CONTRIBUTION_LINK',
ADMIN_LIST_CONTRIBUTIONS = 'ADMIN_LIST_CONTRIBUTIONS',
CONFIRM_CONTRIBUTION = 'CONFIRM_CONTRIBUTION',
SEND_ACTIVATION_EMAIL = 'SEND_ACTIVATION_EMAIL',
LIST_TRANSACTION_LINKS_ADMIN = 'LIST_TRANSACTION_LINKS_ADMIN',
CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK',
DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK',
UPDATE_CONTRIBUTION_LINK = 'UPDATE_CONTRIBUTION_LINK',
ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE',
ADMIN_DENY_CONTRIBUTION = 'DENY_CONTRIBUTION',
DENY_CONTRIBUTION = 'DENY_CONTRIBUTION',
ADMIN_OPEN_CREATIONS = 'ADMIN_OPEN_CREATIONS',
}

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'
@ -100,10 +102,12 @@ describe('sendEmailTranslated', () => {
})
})
// eslint-disable-next-line jest/no-disabled-tests
it.skip('calls "i18n.setLocale" with "en"', () => {
expect(i18n.setLocale).toBeCalledWith('en')
})
// eslint-disable-next-line jest/no-disabled-tests
it.skip('calls "i18n.__" for translation', () => {
expect(i18n.__).toBeCalled()
})

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'

View File

@ -1,3 +1,6 @@
/* 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'
@ -18,9 +21,13 @@ export async function requestGetPublicKey(dbCom: DbCommunity): Promise<string |
}
}
`
const variables = {}
try {
const { data, errors, extensions, headers, status } = await graphQLClient.rawRequest(query)
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)

View File

@ -1,3 +1,6 @@
/* 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'
@ -18,9 +21,13 @@ export async function requestGetPublicKey(dbCom: DbCommunity): Promise<string |
}
}
`
const variables = {}
try {
const { data, errors, extensions, headers, status } = await graphQLClient.rawRequest(query)
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)

View File

@ -1,8 +1,14 @@
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 instance: GraphQLGetClient
private static instanceArray: ClientInstance[] = []
/**
* The Singleton's constructor should always be private to prevent direct
@ -20,16 +26,18 @@ export class GraphQLGetClient extends GraphQLClient {
* just one instance of each subclass around.
*/
public static getInstance(url: string): GraphQLGetClient {
if (!GraphQLGetClient.instance) {
GraphQLGetClient.instance = new GraphQLGetClient(url, {
method: 'GET',
jsonSerializer: {
parse: JSON.parse,
stringify: JSON.stringify,
},
})
const instance = GraphQLGetClient.instanceArray.find((instance) => instance.url === url)
if (instance) {
return instance.client
}
return GraphQLGetClient.instance
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

@ -1,3 +1,7 @@
/* 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 */
@ -150,7 +154,8 @@ describe('validate Communities', () => {
})
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`,
`Federation: dbCom: ${dbCom.id} with unsupported apiVersion=2_0; supported versions`,
['1_0', '1_1'],
)
})
})

View File

@ -8,14 +8,14 @@ import { backendLogger as logger } from '@/server/logger'
import { ApiVersionType } from './enum/apiVersionType'
import LogError from '@/server/LogError'
export async function startValidateCommunities(timerInterval: number): Promise<void> {
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() {
validateCommunities()
void validateCommunities()
setTimeout(run, timerInterval)
}, timerInterval)
}
@ -27,8 +27,8 @@ export async function validateCommunities(): Promise<void> {
.getMany()
logger.debug(`Federation: found ${dbCommunities.length} dbCommunities`)
dbCommunities.forEach(async function (dbCom) {
logger.debug(`Federation: dbCom: ${JSON.stringify(dbCom)}`)
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)) {
@ -38,19 +38,22 @@ export async function validateCommunities(): Promise<void> {
try {
const pubKey = await invokeVersionedRequestGetPublicKey(dbCom)
logger.info(
`Federation: received publicKey=${pubKey} from endpoint=${dbCom.endPoint}/${dbCom.apiVersion}`,
'Federation: received publicKey from endpoint',
pubKey,
`${dbCom.endPoint}/${dbCom.apiVersion}`,
)
if (pubKey && pubKey === dbCom.publicKey.toString('hex')) {
if (pubKey && pubKey === dbCom.publicKey.toString()) {
logger.info(`Federation: matching publicKey: ${pubKey}`)
DbCommunity.update({ id: dbCom.id }, { verifiedAt: new Date() })
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 })
}
/*
else {
logger.warn(`Federation: received unknown publicKey -> delete dbCom with id=${dbCom.id} `)
DbCommunity.delete({ id: dbCom.id })
}
*/
} catch (err) {
if (!isLogError(err)) {
logger.error(`Error:`, err)
@ -58,10 +61,11 @@ export async function validateCommunities(): Promise<void> {
}
} else {
logger.warn(
`Federation: dbCom: ${dbCom.id} with unsupported apiVersion=${dbCom.apiVersion}; supported versions=${apiValueStrings}`,
`Federation: dbCom: ${dbCom.id} with unsupported apiVersion=${dbCom.apiVersion}; supported versions`,
apiValueStrings,
)
}
})
}
}
function isLogError(err: unknown) {

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,10 +0,0 @@
import { ArgsType, Field } from 'type-graphql'
@ArgsType()
export default class SubscribeNewsletterArgs {
@Field(() => String)
email: string
@Field(() => String)
language: string
}

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'

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)

View File

@ -23,7 +23,7 @@ export class Contribution {
this.deletedBy = contribution.deletedBy
}
@Field(() => Number)
@Field(() => Int)
id: number
@Field(() => String, { nullable: true })
@ -44,25 +44,25 @@ export class Contribution {
@Field(() => Date, { nullable: true })
confirmedAt: Date | null
@Field(() => Number, { nullable: true })
@Field(() => Int, { nullable: true })
confirmedBy: number | null
@Field(() => Date, { nullable: true })
deniedAt: Date | null
@Field(() => Number, { nullable: true })
@Field(() => Int, { nullable: true })
deniedBy: number | null
@Field(() => Date, { nullable: true })
deletedAt: Date | null
@Field(() => Number, { nullable: true })
@Field(() => Int, { nullable: true })
deletedBy: number | null
@Field(() => Date)
contributionDate: Date
@Field(() => Number)
@Field(() => Int)
messagesCount: number
@Field(() => String)

View File

@ -21,7 +21,7 @@ export class ContributionLink {
this.link = CONFIG.COMMUNITY_REDEEM_CONTRIBUTION_URL.replace(/{code}/g, this.code)
}
@Field(() => Number)
@Field(() => Int)
id: number
@Field(() => Decimal)

View File

@ -1,4 +1,4 @@
import { ObjectType, Field } from 'type-graphql'
import { ObjectType, Field, Int } from 'type-graphql'
import { ContributionLink } from '@model/ContributionLink'
@ObjectType()
@ -6,6 +6,6 @@ export class ContributionLinkList {
@Field(() => [ContributionLink])
links: ContributionLink[]
@Field(() => Number)
@Field(() => Int)
count: number
}

View File

@ -1,4 +1,4 @@
import { Field, ObjectType } from 'type-graphql'
import { Field, Int, ObjectType } from 'type-graphql'
import { ContributionMessage as DbContributionMessage } from '@entity/ContributionMessage'
import { User } from '@entity/User'
@ -16,7 +16,7 @@ export class ContributionMessage {
this.isModerator = contributionMessage.isModerator
}
@Field(() => Number)
@Field(() => Int)
id: number
@Field(() => String)
@ -26,7 +26,7 @@ export class ContributionMessage {
createdAt: Date
@Field(() => Date, { nullable: true })
updatedAt?: Date | null
updatedAt: Date | null
@Field(() => String)
type: string
@ -37,7 +37,7 @@ export class ContributionMessage {
@Field(() => String, { nullable: true })
userLastName: string | null
@Field(() => Number, { nullable: true })
@Field(() => Int, { nullable: true })
userId: number | null
@Field(() => Boolean)
@ -45,7 +45,7 @@ export class ContributionMessage {
}
@ObjectType()
export class ContributionMessageListResult {
@Field(() => Number)
@Field(() => Int)
count: number
@Field(() => [ContributionMessage])

View File

@ -1,6 +1,8 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { ObjectType, Field } from 'type-graphql'
import { ObjectType, Field, Float, Int } from 'type-graphql'
import { GdtEntryType } from '@enum/GdtEntryType'
@ObjectType()
@ -19,10 +21,10 @@ export class GdtEntry {
this.gdt = json.gdt
}
@Field(() => Number)
@Field(() => Int)
id: number
@Field(() => Number)
@Field(() => Float)
amount: number
@Field(() => String)
@ -40,15 +42,15 @@ export class GdtEntry {
@Field(() => GdtEntryType)
gdtEntryType: GdtEntryType
@Field(() => Number)
@Field(() => Float)
factor: number
@Field(() => Number)
@Field(() => Float)
amount2: number
@Field(() => Number)
@Field(() => Float)
factor2: number
@Field(() => Number)
@Field(() => Float)
gdt: number
}

View File

@ -1,7 +1,10 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { GdtEntry } from './GdtEntry'
import { ObjectType, Field } from 'type-graphql'
import { ObjectType, Field, Int, Float } from 'type-graphql'
@ObjectType()
export class GdtEntryList {
@ -16,15 +19,15 @@ export class GdtEntryList {
@Field(() => String)
state: string
@Field(() => Number)
@Field(() => Int)
count: number
@Field(() => [GdtEntry], { nullable: true })
gdtEntries?: GdtEntry[]
gdtEntries: GdtEntry[] | null
@Field(() => Number)
@Field(() => Float)
gdtSum: number
@Field(() => Number)
@Field(() => Float)
timeUsed: number
}

View File

@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { ObjectType, Field } from 'type-graphql'

View File

@ -1,4 +1,4 @@
import { ObjectType, Field } from 'type-graphql'
import { ObjectType, Field, Int } from 'type-graphql'
import { Decay } from './Decay'
import { Transaction as dbTransaction } from '@entity/Transaction'
import Decimal from 'decimal.js-light'
@ -41,19 +41,19 @@ export class Transaction {
this.memo = transaction.memo
this.creationDate = transaction.creationDate
this.linkedUser = linkedUser
this.linkedTransactionId = transaction.linkedTransactionId
this.linkedTransactionId = transaction.linkedTransactionId || null
this.linkId = transaction.contribution
? transaction.contribution.contributionLinkId
: transaction.transactionLinkId
: transaction.transactionLinkId || null
}
@Field(() => Number)
@Field(() => Int)
id: number
@Field(() => User)
user: User
@Field(() => Number, { nullable: true })
@Field(() => Int, { nullable: true })
previous: number | null
@Field(() => TransactionTypeId)
@ -80,10 +80,10 @@ export class Transaction {
@Field(() => User, { nullable: true })
linkedUser: User | null
@Field(() => Number, { nullable: true })
linkedTransactionId?: number | null
@Field(() => Int, { nullable: true })
linkedTransactionId: number | null
// Links to the TransactionLink/ContributionLink when transaction was created by a link
@Field(() => Number, { nullable: true })
linkId?: number | null
@Field(() => Int, { nullable: true })
linkId: number | null
}

View File

@ -21,7 +21,7 @@ export class TransactionLink {
this.link = CONFIG.COMMUNITY_REDEEM_URL.replace(/{code}/g, this.code)
}
@Field(() => Number)
@Field(() => Int)
id: number
@Field(() => User)

View File

@ -24,12 +24,12 @@ export class UnconfirmedContribution {
firstName: string
@Field(() => Int)
id?: number
id: number
@Field(() => String)
lastName: string
@Field(() => Number)
@Field(() => Int)
userId: number
@Field(() => String)
@ -44,7 +44,7 @@ export class UnconfirmedContribution {
@Field(() => Decimal)
amount: Decimal
@Field(() => Number, { nullable: true })
@Field(() => Int, { nullable: true })
moderator: number | null
@Field(() => [Decimal])
@ -53,6 +53,6 @@ export class UnconfirmedContribution {
@Field(() => String)
state: string
@Field(() => Number)
@Field(() => Int)
messageCount: number
}

View File

@ -1,4 +1,4 @@
import { ObjectType, Field } from 'type-graphql'
import { ObjectType, Field, Int } from 'type-graphql'
import { KlickTipp } from './KlickTipp'
import { User as dbUser } from '@entity/User'
import { UserContact } from './UserContact'
@ -28,21 +28,21 @@ export class User {
this.hideAmountGDT = user.hideAmountGDT
}
@Field(() => Number)
@Field(() => Int)
id: number
@Field(() => String)
gradidoID: string
@Field(() => String, { nullable: true })
alias?: string
alias: string | null
@Field(() => Number, { nullable: true })
@Field(() => Int, { nullable: true })
emailId: number | null
// TODO privacy issue here
@Field(() => String, { nullable: true })
email: string
email: string | null
@Field(() => UserContact)
emailContact: UserContact
@ -72,7 +72,7 @@ export class User {
hideAmountGDT: boolean
// This is not the users publisherId, but the one of the users who recommend him
@Field(() => Number, { nullable: true })
@Field(() => Int, { nullable: true })
publisherId: number | null
@Field(() => Date, { nullable: true })

View File

@ -17,7 +17,7 @@ export class UserAdmin {
this.isAdmin = user.isAdmin
}
@Field(() => Number)
@Field(() => Int)
userId: number
@Field(() => String)
@ -39,10 +39,10 @@ export class UserAdmin {
hasElopage: boolean
@Field(() => Date, { nullable: true })
deletedAt?: Date | null
deletedAt: Date | null
@Field(() => String, { nullable: true })
emailConfirmationSend?: string
emailConfirmationSend: string | null
@Field(() => Date, { nullable: true })
isAdmin: Date | null

View File

@ -1,4 +1,4 @@
import { ObjectType, Field } from 'type-graphql'
import { ObjectType, Field, Int } from 'type-graphql'
import { UserContact as dbUserContact } from '@entity/UserContact'
@ObjectType()
@ -18,13 +18,13 @@ export class UserContact {
this.deletedAt = userContact.deletedAt
}
@Field(() => Number)
@Field(() => Int)
id: number
@Field(() => String)
type: string
@Field(() => Number)
@Field(() => Int)
userId: number
@Field(() => String)
@ -33,10 +33,10 @@ export class UserContact {
// @Field(() => BigInt, { nullable: true })
// emailVerificationCode: BigInt | null
@Field(() => Number, { nullable: true })
@Field(() => Int, { nullable: true })
emailOptInTypeId: number | null
@Field(() => Number, { nullable: true })
@Field(() => Int, { nullable: true })
emailResendCount: number | null
@Field(() => Boolean)

View File

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions */
import Decimal from 'decimal.js-light'
import { Resolver, Query, Ctx, Authorized } from 'type-graphql'
import { getCustomRepository } from '@dbTools/typeorm'

View File

@ -1,21 +1,25 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/unbound-method */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { createTestClient } from 'apollo-server-testing'
import createServer from '@/server/createServer'
import CONFIG from '@/config'
jest.mock('@/config')
import { getCommunities } from '@/seeds/graphql/queries'
import { Community as DbCommunity } from '@entity/Community'
import { testEnvironment } from '@test/helpers'
let query: any
// to do: We need a setup for the tests that closes the connection
let con: any
let testEnv: any
beforeAll(async () => {
const server = await createServer({})
con = server.con
query = createTestClient(server.apollo).query
testEnv = await testEnvironment()
query = testEnv.query
con = testEnv.con
await DbCommunity.clear()
})
afterAll(async () => {
@ -23,74 +27,90 @@ afterAll(async () => {
})
describe('CommunityResolver', () => {
const getCommunityInfoQuery = `
query {
getCommunityInfo {
name
description
url
registerUrl
}
}
`
const communities = `
query {
communities {
id
name
url
description
registerUrl
}
}
`
describe('getCommunityInfo', () => {
it('returns the default values', async () => {
await expect(query({ query: getCommunityInfoQuery })).resolves.toMatchObject({
data: {
getCommunityInfo: {
name: 'Gradido Entwicklung',
description: 'Die lokale Entwicklungsumgebung von Gradido.',
url: 'http://localhost/',
registerUrl: 'http://localhost/register',
describe('getCommunities', () => {
let homeCom1: DbCommunity
let homeCom2: DbCommunity
let homeCom3: DbCommunity
let foreignCom1: DbCommunity
let foreignCom2: DbCommunity
let foreignCom3: DbCommunity
describe('with empty list', () => {
it('returns no community entry', async () => {
// const result: Community[] = await query({ query: getCommunities })
// expect(result.length).toEqual(0)
await expect(query({ query: getCommunities })).resolves.toMatchObject({
data: {
getCommunities: [],
},
},
})
})
})
})
describe('communities', () => {
describe('PRODUCTION = false', () => {
beforeEach(() => {
CONFIG.PRODUCTION = false
describe('only home-communities entries', () => {
beforeEach(async () => {
jest.clearAllMocks()
homeCom1 = DbCommunity.create()
homeCom1.foreign = false
homeCom1.publicKey = Buffer.from('publicKey-HomeCommunity')
homeCom1.apiVersion = '1_0'
homeCom1.endPoint = 'http://localhost'
homeCom1.createdAt = new Date()
await DbCommunity.insert(homeCom1)
homeCom2 = DbCommunity.create()
homeCom2.foreign = false
homeCom2.publicKey = Buffer.from('publicKey-HomeCommunity')
homeCom2.apiVersion = '1_1'
homeCom2.endPoint = 'http://localhost'
homeCom2.createdAt = new Date()
await DbCommunity.insert(homeCom2)
homeCom3 = DbCommunity.create()
homeCom3.foreign = false
homeCom3.publicKey = Buffer.from('publicKey-HomeCommunity')
homeCom3.apiVersion = '2_0'
homeCom3.endPoint = 'http://localhost'
homeCom3.createdAt = new Date()
await DbCommunity.insert(homeCom3)
})
it('returns three communities', async () => {
await expect(query({ query: communities })).resolves.toMatchObject({
it('returns three home-community entries', async () => {
await expect(query({ query: getCommunities })).resolves.toMatchObject({
data: {
communities: [
getCommunities: [
{
id: 1,
name: 'Gradido Entwicklung',
description: 'Die lokale Entwicklungsumgebung von Gradido.',
url: 'http://localhost/',
registerUrl: 'http://localhost/register-community',
foreign: homeCom1.foreign,
publicKey: expect.stringMatching('publicKey-HomeCommunity'),
url: expect.stringMatching('http://localhost/api/1_0'),
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
createdAt: homeCom1.createdAt.toISOString(),
updatedAt: null,
},
{
id: 2,
name: 'Gradido Staging',
description: 'Der Testserver der Gradido-Akademie.',
url: 'https://stage1.gradido.net/',
registerUrl: 'https://stage1.gradido.net/register-community',
foreign: homeCom2.foreign,
publicKey: expect.stringMatching('publicKey-HomeCommunity'),
url: expect.stringMatching('http://localhost/api/1_1'),
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
createdAt: homeCom2.createdAt.toISOString(),
updatedAt: null,
},
{
id: 3,
name: 'Gradido-Akademie',
description: 'Freies Institut für Wirtschaftsbionik.',
url: 'https://gradido.net',
registerUrl: 'https://gdd1.gradido.com/register-community',
foreign: homeCom3.foreign,
publicKey: expect.stringMatching('publicKey-HomeCommunity'),
url: expect.stringMatching('http://localhost/api/2_0'),
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
createdAt: homeCom3.createdAt.toISOString(),
updatedAt: null,
},
],
},
@ -98,21 +118,104 @@ describe('CommunityResolver', () => {
})
})
describe('PRODUCTION = true', () => {
beforeEach(() => {
CONFIG.PRODUCTION = true
describe('plus foreign-communities entries', () => {
beforeEach(async () => {
jest.clearAllMocks()
foreignCom1 = DbCommunity.create()
foreignCom1.foreign = true
foreignCom1.publicKey = Buffer.from('publicKey-ForeignCommunity')
foreignCom1.apiVersion = '1_0'
foreignCom1.endPoint = 'http://remotehost'
foreignCom1.createdAt = new Date()
await DbCommunity.insert(foreignCom1)
foreignCom2 = DbCommunity.create()
foreignCom2.foreign = true
foreignCom2.publicKey = Buffer.from('publicKey-ForeignCommunity')
foreignCom2.apiVersion = '1_1'
foreignCom2.endPoint = 'http://remotehost'
foreignCom2.createdAt = new Date()
await DbCommunity.insert(foreignCom2)
foreignCom3 = DbCommunity.create()
foreignCom3.foreign = true
foreignCom3.publicKey = Buffer.from('publicKey-ForeignCommunity')
foreignCom3.apiVersion = '1_2'
foreignCom3.endPoint = 'http://remotehost'
foreignCom3.createdAt = new Date()
await DbCommunity.insert(foreignCom3)
})
it('returns one community', async () => {
await expect(query({ query: communities })).resolves.toMatchObject({
it('returns 3x home and 3x foreign-community entries', async () => {
await expect(query({ query: getCommunities })).resolves.toMatchObject({
data: {
communities: [
getCommunities: [
{
id: 1,
foreign: homeCom1.foreign,
publicKey: expect.stringMatching('publicKey-HomeCommunity'),
url: expect.stringMatching('http://localhost/api/1_0'),
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
createdAt: homeCom1.createdAt.toISOString(),
updatedAt: null,
},
{
id: 2,
foreign: homeCom2.foreign,
publicKey: expect.stringMatching('publicKey-HomeCommunity'),
url: expect.stringMatching('http://localhost/api/1_1'),
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
createdAt: homeCom2.createdAt.toISOString(),
updatedAt: null,
},
{
id: 3,
name: 'Gradido-Akademie',
description: 'Freies Institut für Wirtschaftsbionik.',
url: 'https://gradido.net',
registerUrl: 'https://gdd1.gradido.com/register-community',
foreign: homeCom3.foreign,
publicKey: expect.stringMatching('publicKey-HomeCommunity'),
url: expect.stringMatching('http://localhost/api/2_0'),
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
createdAt: homeCom3.createdAt.toISOString(),
updatedAt: null,
},
{
id: 4,
foreign: foreignCom1.foreign,
publicKey: expect.stringMatching('publicKey-ForeignCommunity'),
url: expect.stringMatching('http://remotehost/api/1_0'),
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
createdAt: foreignCom1.createdAt.toISOString(),
updatedAt: null,
},
{
id: 5,
foreign: foreignCom2.foreign,
publicKey: expect.stringMatching('publicKey-ForeignCommunity'),
url: expect.stringMatching('http://remotehost/api/1_1'),
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
createdAt: foreignCom2.createdAt.toISOString(),
updatedAt: null,
},
{
id: 6,
foreign: foreignCom3.foreign,
publicKey: expect.stringMatching('publicKey-ForeignCommunity'),
url: expect.stringMatching('http://remotehost/api/1_2'),
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
createdAt: foreignCom3.createdAt.toISOString(),
updatedAt: null,
},
],
},

View File

@ -1,58 +1,18 @@
import { Resolver, Query, Authorized } from 'type-graphql'
import { Community } from '@model/Community'
import { Community as DbCommunity } from '@entity/Community'
import { RIGHTS } from '@/auth/RIGHTS'
import CONFIG from '@/config'
@Resolver()
export class CommunityResolver {
@Authorized([RIGHTS.GET_COMMUNITY_INFO])
@Query(() => Community)
async getCommunityInfo(): Promise<Community> {
return new Community({
name: CONFIG.COMMUNITY_NAME,
description: CONFIG.COMMUNITY_DESCRIPTION,
url: CONFIG.COMMUNITY_URL,
registerUrl: CONFIG.COMMUNITY_REGISTER_URL,
})
}
@Authorized([RIGHTS.COMMUNITIES])
@Query(() => [Community])
async communities(): Promise<Community[]> {
if (CONFIG.PRODUCTION)
return [
new Community({
id: 3,
name: 'Gradido-Akademie',
description: 'Freies Institut für Wirtschaftsbionik.',
url: 'https://gradido.net',
registerUrl: 'https://gdd1.gradido.com/register-community',
}),
]
return [
new Community({
id: 1,
name: 'Gradido Entwicklung',
description: 'Die lokale Entwicklungsumgebung von Gradido.',
url: 'http://localhost/',
registerUrl: 'http://localhost/register-community',
}),
new Community({
id: 2,
name: 'Gradido Staging',
description: 'Der Testserver der Gradido-Akademie.',
url: 'https://stage1.gradido.net/',
registerUrl: 'https://stage1.gradido.net/register-community',
}),
new Community({
id: 3,
name: 'Gradido-Akademie',
description: 'Freies Institut für Wirtschaftsbionik.',
url: 'https://gradido.net',
registerUrl: 'https://gdd1.gradido.com/register-community',
}),
]
async getCommunities(): Promise<Community[]> {
const dbCommunities: DbCommunity[] = await DbCommunity.find({
order: { foreign: 'ASC', publicKey: 'ASC', apiVersion: 'ASC' },
})
return dbCommunities.map((dbCom: DbCommunity) => new Community(dbCom))
}
}

View File

@ -1,3 +1,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/unbound-method */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-explicit-any */
import Decimal from 'decimal.js-light'
@ -276,7 +280,7 @@ describe('Contribution Links', () => {
)
})
it('logs the error thrown', () => {
it('logs the error "A Start-Date must be set"', () => {
expect(logger.error).toBeCalledWith('A Start-Date must be set')
})
@ -297,7 +301,7 @@ describe('Contribution Links', () => {
)
})
it('logs the error thrown', () => {
it('logs the error "An End-Date must be set"', () => {
expect(logger.error).toBeCalledWith('An End-Date must be set')
})
@ -321,7 +325,7 @@ describe('Contribution Links', () => {
)
})
it('logs the error thrown', () => {
it('logs the error "The value of validFrom must before or equals the validTo"', () => {
expect(logger.error).toBeCalledWith(
`The value of validFrom must before or equals the validTo`,
)
@ -344,7 +348,7 @@ describe('Contribution Links', () => {
)
})
it('logs the error thrown', () => {
it('logs the error "The value of name is too short"', () => {
expect(logger.error).toBeCalledWith('The value of name is too short', 3)
})
@ -365,7 +369,7 @@ describe('Contribution Links', () => {
)
})
it('logs the error thrown', () => {
it('logs the error "The value of name is too long"', () => {
expect(logger.error).toBeCalledWith('The value of name is too long', 101)
})
@ -386,7 +390,7 @@ describe('Contribution Links', () => {
)
})
it('logs the error thrown', () => {
it('logs the error "The value of memo is too short"', () => {
expect(logger.error).toBeCalledWith('The value of memo is too short', 3)
})
@ -407,7 +411,7 @@ describe('Contribution Links', () => {
)
})
it('logs the error thrown', () => {
it('logs the error "The value of memo is too long"', () => {
expect(logger.error).toBeCalledWith('The value of memo is too long', 256)
})
@ -428,7 +432,7 @@ describe('Contribution Links', () => {
)
})
it('logs the error thrown', () => {
it('logs the error "The amount must be a positiv value"', () => {
expect(logger.error).toBeCalledWith('The amount must be a positiv value', new Decimal(0))
})
})
@ -486,7 +490,7 @@ describe('Contribution Links', () => {
})
})
it('logs the error thrown', () => {
it('logs the error "Contribution Link not found"', () => {
expect(logger.error).toBeCalledWith('Contribution Link not found', -1)
})
@ -568,7 +572,7 @@ describe('Contribution Links', () => {
)
})
it('logs the error thrown', () => {
it('logs the error "Contribution Link not found"', () => {
expect(logger.error).toBeCalledWith('Contribution Link not found', -1)
})
})

View File

@ -29,7 +29,7 @@ import {
@Resolver()
export class ContributionLinkResolver {
@Authorized([RIGHTS.ADMIN_CREATE_CONTRIBUTION_LINK])
@Authorized([RIGHTS.CREATE_CONTRIBUTION_LINK])
@Mutation(() => ContributionLink)
async createContributionLink(
@Args()
@ -40,7 +40,7 @@ export class ContributionLinkResolver {
cycle,
validFrom,
validTo,
maxAmountPerMonth,
maxAmountPerMonth = null,
maxPerCycle,
}: ContributionLinkArgs,
@Ctx() context: Context,
@ -97,8 +97,8 @@ export class ContributionLinkResolver {
}
}
@Authorized([RIGHTS.ADMIN_DELETE_CONTRIBUTION_LINK])
@Mutation(() => Boolean, { nullable: true })
@Authorized([RIGHTS.DELETE_CONTRIBUTION_LINK])
@Mutation(() => Boolean)
async deleteContributionLink(
@Arg('id', () => Int) id: number,
@Ctx() context: Context,
@ -113,7 +113,7 @@ export class ContributionLinkResolver {
return true
}
@Authorized([RIGHTS.ADMIN_UPDATE_CONTRIBUTION_LINK])
@Authorized([RIGHTS.UPDATE_CONTRIBUTION_LINK])
@Mutation(() => ContributionLink)
async updateContributionLink(
@Args()
@ -124,7 +124,7 @@ export class ContributionLinkResolver {
cycle,
validFrom,
validTo,
maxAmountPerMonth,
maxAmountPerMonth = null,
maxPerCycle,
}: ContributionLinkArgs,
@Arg('id', () => Int) id: number,

View File

@ -1,3 +1,8 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/unbound-method */
/* 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 */
@ -110,7 +115,7 @@ describe('ContributionMessageResolver', () => {
)
})
it('logs the error thrown', () => {
it('logs the error "ContributionMessage was not sent successfully: Error: Contribution not found"', () => {
expect(logger.error).toBeCalledWith(
'ContributionMessage was not sent successfully: Error: Contribution not found',
new Error('Contribution not found'),
@ -150,7 +155,7 @@ describe('ContributionMessageResolver', () => {
)
})
it('logs the error thrown', () => {
it('logs the error "ContributionMessage was not sent successfully: Error: Admin can not answer on his own contribution"', () => {
expect(logger.error).toBeCalledWith(
'ContributionMessage was not sent successfully: Error: Admin can not answer on his own contribution',
new Error('Admin can not answer on his own contribution'),
@ -183,7 +188,7 @@ describe('ContributionMessageResolver', () => {
)
})
it('calls sendAddedContributionMessageEmail', async () => {
it('calls sendAddedContributionMessageEmail', () => {
expect(sendAddedContributionMessageEmail).toBeCalledWith({
firstName: 'Bibi',
lastName: 'Bloxberg',
@ -260,7 +265,7 @@ describe('ContributionMessageResolver', () => {
)
})
it('logs the error thrown', () => {
it('logs the error "ContributionMessage was not sent successfully: Error: Contribution not found"', () => {
expect(logger.error).toBeCalledWith(
'ContributionMessage was not sent successfully: Error: Contribution not found',
new Error('Contribution not found'),
@ -292,7 +297,7 @@ describe('ContributionMessageResolver', () => {
)
})
it('logs the error thrown', () => {
it('logs the error "ContributionMessage was not sent successfully: Error: Can not send message to contribution of another user"', () => {
expect(logger.error).toBeCalledWith(
'ContributionMessage was not sent successfully: Error: Can not send message to contribution of another user',
new Error('Can not send message to contribution of another user'),

View File

@ -1,4 +1,5 @@
import { Arg, Args, Authorized, Ctx, Mutation, Query, Resolver } from 'type-graphql'
/* eslint-disable @typescript-eslint/restrict-template-expressions */
import { Arg, Args, Authorized, Ctx, Int, Mutation, Query, Resolver } from 'type-graphql'
import { getConnection } from '@dbTools/typeorm'
import { ContributionMessage as DbContributionMessage } from '@entity/ContributionMessage'
@ -78,7 +79,7 @@ export class ContributionMessageResolver {
@Authorized([RIGHTS.LIST_ALL_CONTRIBUTION_MESSAGES])
@Query(() => ContributionMessageListResult)
async listContributionMessages(
@Arg('contributionId') contributionId: number,
@Arg('contributionId', () => Int) contributionId: number,
@Args()
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
): Promise<ContributionMessageListResult> {

View File

@ -1,3 +1,8 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/unbound-method */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
@ -22,7 +27,7 @@ import {
import {
listAllContributions,
listContributions,
adminListAllContributions,
adminListContributions,
} from '@/seeds/graphql/queries'
import {
sendContributionConfirmedEmail,
@ -176,7 +181,7 @@ describe('ContributionResolver', () => {
})
})
afterAll(async () => {
afterAll(() => {
resetToken()
})
@ -196,7 +201,7 @@ describe('ContributionResolver', () => {
expect(errorObjects).toEqual([new GraphQLError('Memo text is too short')])
})
it('logs the error found', () => {
it('logs the error "Memo text is too short"', () => {
expect(logger.error).toBeCalledWith('Memo text is too short', 4)
})
@ -214,7 +219,7 @@ describe('ContributionResolver', () => {
expect(errorObjects).toEqual([new GraphQLError('Memo text is too long')])
})
it('logs the error found', () => {
it('logs the error "Memo text is too long"', () => {
expect(logger.error).toBeCalledWith('Memo text is too long', 259)
})
@ -233,7 +238,7 @@ describe('ContributionResolver', () => {
])
})
it('logs the error found', () => {
it('logs the error "No information for available creations for the given date"', () => {
expect(logger.error).toBeCalledWith(
'No information for available creations for the given date',
expect.any(Date),
@ -256,7 +261,7 @@ describe('ContributionResolver', () => {
])
})
it('logs the error found', () => {
it('logs the error "No information for available creations for the given date" again', () => {
expect(logger.error).toBeCalledWith(
'No information for available creations for the given date',
expect.any(Date),
@ -265,7 +270,7 @@ describe('ContributionResolver', () => {
})
describe('valid input', () => {
it('creates contribution', async () => {
it('creates contribution', () => {
expect(pendingContribution.data.createContribution).toMatchObject({
id: expect.any(Number),
amount: '100',
@ -312,7 +317,7 @@ describe('ContributionResolver', () => {
})
})
afterAll(async () => {
afterAll(() => {
resetToken()
})
@ -332,7 +337,7 @@ describe('ContributionResolver', () => {
expect(errorObjects).toEqual([new GraphQLError('Memo text is too short')])
})
it('logs the error found', () => {
it('logs the error "Memo text is too short"', () => {
expect(logger.error).toBeCalledWith('Memo text is too short', 4)
})
})
@ -353,7 +358,7 @@ describe('ContributionResolver', () => {
expect(errorObjects).toEqual([new GraphQLError('Memo text is too long')])
})
it('logs the error found', () => {
it('logs the error "Memo text is too long"', () => {
expect(logger.error).toBeCalledWith('Memo text is too long', 259)
})
})
@ -378,7 +383,7 @@ describe('ContributionResolver', () => {
)
})
it('logs the error found', () => {
it('logs the error "Contribution not found"', () => {
expect(logger.error).toBeCalledWith('Contribution not found', -1)
})
})
@ -407,7 +412,7 @@ describe('ContributionResolver', () => {
])
})
it('logs the error found', () => {
it('logs the error "Can not update contribution of another user"', () => {
expect(logger.error).toBeCalledWith(
'Can not update contribution of another user',
expect.any(Object),
@ -441,7 +446,7 @@ describe('ContributionResolver', () => {
])
})
it('logs the error found', () => {
it('logs the error "An admin is not allowed to update an user contribution"', () => {
expect(logger.error).toBeCalledWith(
'An admin is not allowed to update an user contribution',
)
@ -453,7 +458,7 @@ describe('ContributionResolver', () => {
id: pendingContribution.data.createContribution.id,
})
contribution.contributionStatus = ContributionStatus.DELETED
contribution.save()
await contribution.save()
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
@ -465,7 +470,7 @@ describe('ContributionResolver', () => {
id: pendingContribution.data.createContribution.id,
})
contribution.contributionStatus = ContributionStatus.PENDING
contribution.save()
await contribution.save()
})
it('throws an error', async () => {
@ -487,7 +492,7 @@ describe('ContributionResolver', () => {
)
})
it('logs the error found', () => {
it('logs the error "Contribution can not be updated due to status"', () => {
expect(logger.error).toBeCalledWith(
'Contribution can not be updated due to status',
ContributionStatus.DELETED,
@ -522,7 +527,7 @@ describe('ContributionResolver', () => {
])
})
it('logs the error found', () => {
it('logs the error "The amount to be created exceeds the amount still available for this month"', () => {
expect(logger.error).toBeCalledWith(
'The amount to be created exceeds the amount still available for this month',
new Decimal(1019),
@ -549,7 +554,7 @@ describe('ContributionResolver', () => {
])
})
it('logs the error found', () => {
it('logs the error "Month of contribution can not be changed"', () => {
expect(logger.error).toBeCalledWith('Month of contribution can not be changed')
})
})
@ -638,7 +643,7 @@ describe('ContributionResolver', () => {
})
})
afterAll(async () => {
afterAll(() => {
resetToken()
})
@ -654,7 +659,7 @@ describe('ContributionResolver', () => {
expect(errorObjects).toEqual([new GraphQLError('Contribution not found')])
})
it('logs the error found', () => {
it('logs the error "Contribution not found"', () => {
expect(logger.error).toBeCalledWith('Contribution not found', -1)
})
})
@ -698,7 +703,7 @@ describe('ContributionResolver', () => {
expect(errorObjects).toEqual([new GraphQLError('Contribution not found')])
})
it('logs the error found', () => {
it('logs the error "Contribution not found"', () => {
expect(logger.error).toBeCalledWith('Contribution not found', expect.any(Number))
})
})
@ -743,7 +748,7 @@ describe('ContributionResolver', () => {
expect(errorObjects).toEqual([new GraphQLError('Contribution not found')])
})
it('logs the error found', () => {
it('logs the error "Contribution not found"', () => {
expect(logger.error).toBeCalledWith(`Contribution not found`, expect.any(Number))
})
})
@ -788,7 +793,7 @@ describe('ContributionResolver', () => {
expect(errorObjects).toEqual([new GraphQLError('Contribution not found')])
})
it('logs the error found', () => {
it('logs the error "Contribution not found"', () => {
expect(logger.error).toBeCalledWith(`Contribution not found`, expect.any(Number))
})
})
@ -822,7 +827,7 @@ describe('ContributionResolver', () => {
)
})
it('calls sendContributionDeniedEmail', async () => {
it('calls sendContributionDeniedEmail', () => {
expect(sendContributionDeniedEmail).toBeCalledWith({
firstName: 'Bibi',
lastName: 'Bloxberg',
@ -858,7 +863,7 @@ describe('ContributionResolver', () => {
})
})
afterAll(async () => {
afterAll(() => {
resetToken()
})
@ -874,7 +879,7 @@ describe('ContributionResolver', () => {
expect(errorObjects).toEqual([new GraphQLError('Contribution not found')])
})
it('logs the error found', () => {
it('logs the error "Contribution not found"', () => {
expect(logger.error).toBeCalledWith('Contribution not found', expect.any(Number))
})
})
@ -904,7 +909,7 @@ describe('ContributionResolver', () => {
])
})
it('logs the error found', () => {
it('logs the error "Can not delete contribution of another user"', () => {
expect(logger.error).toBeCalledWith(
'Can not delete contribution of another user',
expect.any(Contribution),
@ -979,7 +984,7 @@ describe('ContributionResolver', () => {
])
})
it('logs the error found', () => {
it('logs the error "A confirmed contribution can not be deleted"', () => {
expect(logger.error).toBeCalledWith(
'A confirmed contribution can not be deleted',
expect.objectContaining({ contributionStatus: 'CONFIRMED' }),
@ -998,7 +1003,6 @@ describe('ContributionResolver', () => {
currentPage: 1,
pageSize: 25,
order: 'DESC',
filterConfirmed: false,
},
})
expect(errorObjects).toEqual([new GraphQLError('401 Unauthorized')])
@ -1013,11 +1017,11 @@ describe('ContributionResolver', () => {
})
})
afterAll(async () => {
afterAll(() => {
resetToken()
})
describe('filter confirmed is false', () => {
describe('no status filter', () => {
it('returns creations', async () => {
const {
data: { listContributions: contributionListResult },
@ -1069,7 +1073,7 @@ describe('ContributionResolver', () => {
})
})
describe('filter confirmed is true', () => {
describe('with status filter [PENDING, IN_PROGRESS, DENIED, DELETED]', () => {
it('returns only unconfirmed creations', async () => {
const {
data: { listContributions: contributionListResult },
@ -1079,7 +1083,7 @@ describe('ContributionResolver', () => {
currentPage: 1,
pageSize: 25,
order: 'DESC',
filterConfirmed: true,
statusFilter: ['PENDING', 'IN_PROGRESS', 'DENIED', 'DELETED'],
},
})
expect(contributionListResult).toMatchObject({
@ -1144,7 +1148,7 @@ describe('ContributionResolver', () => {
})
})
afterAll(async () => {
afterAll(() => {
resetToken()
})
@ -1724,7 +1728,7 @@ describe('ContributionResolver', () => {
})
})
afterAll(async () => {
afterAll(() => {
resetToken()
})
@ -1802,7 +1806,7 @@ describe('ContributionResolver', () => {
})
})
afterAll(async () => {
afterAll(() => {
resetToken()
})
@ -1845,7 +1849,7 @@ describe('ContributionResolver', () => {
)
})
it('logs the error thrown', () => {
it('logs the error "Could not find user"', () => {
expect(logger.error).toBeCalledWith('Could not find user', 'some@fake.email')
})
})
@ -1872,7 +1876,7 @@ describe('ContributionResolver', () => {
)
})
it('logs the error thrown', () => {
it('logs the error "Cannot create contribution since the user was deleted"', () => {
expect(logger.error).toBeCalledWith(
'Cannot create contribution since the user was deleted',
expect.objectContaining({
@ -1908,7 +1912,7 @@ describe('ContributionResolver', () => {
)
})
it('logs the error thrown', () => {
it('logs the error "Cannot create contribution since the users email is not activated"', () => {
expect(logger.error).toBeCalledWith(
'Cannot create contribution since the users email is not activated',
expect.objectContaining({ emailChecked: false }),
@ -1917,7 +1921,7 @@ describe('ContributionResolver', () => {
})
describe('valid user to create for', () => {
beforeAll(async () => {
beforeAll(() => {
variables.email = 'bibi@bloxberg.de'
variables.creationDate = 'invalid-date'
})
@ -1934,7 +1938,7 @@ describe('ContributionResolver', () => {
)
})
it('logs the error thrown', () => {
it('logs the error "CreationDate is invalid"', () => {
expect(logger.error).toBeCalledWith('CreationDate is invalid', 'invalid-date')
})
})
@ -1956,7 +1960,7 @@ describe('ContributionResolver', () => {
)
})
it('logs the error thrown', () => {
it('logs the error "No information for available creations for the given date"', () => {
expect(logger.error).toBeCalledWith(
'No information for available creations for the given date',
new Date(variables.creationDate),
@ -1981,7 +1985,7 @@ describe('ContributionResolver', () => {
)
})
it('logs the error thrown', () => {
it('logs the error "No information for available creations for the given date"', () => {
expect(logger.error).toBeCalledWith(
'No information for available creations for the given date',
new Date(variables.creationDate),
@ -2006,7 +2010,7 @@ describe('ContributionResolver', () => {
)
})
it('logs the error thrown', () => {
it('logs the error "The amount to be created exceeds the amount still available for this month"', () => {
expect(logger.error).toBeCalledWith(
'The amount to be created exceeds the amount still available for this month',
new Decimal(2000),
@ -2023,7 +2027,7 @@ describe('ContributionResolver', () => {
).resolves.toEqual(
expect.objectContaining({
data: {
adminCreateContribution: [1000, 1000, 590],
adminCreateContribution: ['1000', '1000', '590'],
},
}),
)
@ -2058,7 +2062,7 @@ describe('ContributionResolver', () => {
)
})
it('logs the error thrown', () => {
it('logs the error "The amount to be created exceeds the amount still available for this month"', () => {
expect(logger.error).toBeCalledWith(
'The amount to be created exceeds the amount still available for this month',
new Decimal(1000),
@ -2097,7 +2101,7 @@ describe('ContributionResolver', () => {
)
})
it('logs the error thrown', () => {
it('logs the error "Could not find User"', () => {
expect(logger.error).toBeCalledWith('Could not find User', 'bob@baumeister.de')
})
})
@ -2123,7 +2127,7 @@ describe('ContributionResolver', () => {
)
})
it('logs the error thrown', () => {
it('logs the error "User was deleted"', () => {
expect(logger.error).toBeCalledWith('User was deleted', 'stephen@hawking.uk')
})
})
@ -2149,7 +2153,7 @@ describe('ContributionResolver', () => {
)
})
it('logs the error thrown', () => {
it('logs the error "Contribution not found"', () => {
expect(logger.error).toBeCalledWith('Contribution not found', -1)
})
})
@ -2181,7 +2185,7 @@ describe('ContributionResolver', () => {
)
})
it('logs the error thrown', () => {
it('logs the error "User of the pending contribution and send user does not correspond"', () => {
expect(logger.error).toBeCalledWith(
'User of the pending contribution and send user does not correspond',
)
@ -2216,7 +2220,7 @@ describe('ContributionResolver', () => {
)
})
it('logs the error thrown', () => {
it('logs the error "The amount to be created exceeds the amount still available for this month"', () => {
expect(logger.error).toBeCalledWith(
'The amount to be created exceeds the amount still available for this month',
new Decimal(1900),
@ -2225,6 +2229,7 @@ describe('ContributionResolver', () => {
})
})
// eslint-disable-next-line jest/no-disabled-tests
describe.skip('creation update is successful changing month', () => {
// skipped as changing the month is currently disable
it('returns update creation object', async () => {
@ -2328,7 +2333,7 @@ describe('ContributionResolver', () => {
)
})
it('logs the error thrown', () => {
it('logs the error "Contribution not found"', () => {
expect(logger.error).toBeCalledWith('Contribution not found', -1)
})
})
@ -2395,7 +2400,7 @@ describe('ContributionResolver', () => {
)
})
it('calls sendContributionDeletedEmail', async () => {
it('calls sendContributionDeletedEmail', () => {
expect(sendContributionDeletedEmail).toBeCalledWith({
firstName: 'Peter',
lastName: 'Lustig',
@ -2470,7 +2475,7 @@ describe('ContributionResolver', () => {
)
})
it('logs the error thrown', () => {
it('logs the error "Contribution not found"', () => {
expect(logger.error).toBeCalledWith('Contribution not found', -1)
})
})
@ -2504,7 +2509,7 @@ describe('ContributionResolver', () => {
)
})
it('logs the error thrown', () => {
it('logs the error "Moderator can not confirm own contribution"', () => {
expect(logger.error).toBeCalledWith('Moderator can not confirm own contribution')
})
})
@ -2560,7 +2565,7 @@ describe('ContributionResolver', () => {
expect(transaction[0].typeId).toEqual(1)
})
it('calls sendContributionConfirmedEmail', async () => {
it('calls sendContributionConfirmedEmail', () => {
expect(sendContributionConfirmedEmail).toBeCalledWith({
firstName: 'Bibi',
lastName: 'Bloxberg',
@ -2599,7 +2604,7 @@ describe('ContributionResolver', () => {
})
})
it('logs the error thrown', () => {
it('logs the error "Contribution already confirmed"', () => {
expect(logger.error).toBeCalledWith(
'Contribution already confirmed',
expect.any(Number),
@ -2665,12 +2670,12 @@ describe('ContributionResolver', () => {
})
})
describe('adminListAllContribution', () => {
describe('adminListContributions', () => {
describe('unauthenticated', () => {
it('returns an error', async () => {
await expect(
query({
query: adminListAllContributions,
query: adminListContributions,
}),
).resolves.toEqual(
expect.objectContaining({
@ -2695,7 +2700,7 @@ describe('ContributionResolver', () => {
it('returns an error', async () => {
await expect(
query({
query: adminListAllContributions,
query: adminListContributions,
}),
).resolves.toEqual(
expect.objectContaining({
@ -2719,9 +2724,9 @@ describe('ContributionResolver', () => {
it('returns 17 creations in total', async () => {
const {
data: { adminListAllContributions: contributionListObject },
}: { data: { adminListAllContributions: ContributionListResult } } = await query({
query: adminListAllContributions,
data: { adminListContributions: contributionListObject },
}: { data: { adminListContributions: ContributionListResult } } = await query({
query: adminListContributions,
})
expect(contributionListObject.contributionList).toHaveLength(17)
expect(contributionListObject).toMatchObject({
@ -2754,15 +2759,6 @@ describe('ContributionResolver', () => {
messagesCount: 0,
state: 'CONFIRMED',
}),
expect.objectContaining({
amount: expect.decimalEqual(100),
firstName: 'Bob',
id: expect.any(Number),
lastName: 'der Baumeister',
memo: 'Confirmed Contribution',
messagesCount: 0,
state: 'CONFIRMED',
}),
expect.objectContaining({
amount: expect.decimalEqual(400),
firstName: 'Peter',
@ -2772,6 +2768,15 @@ describe('ContributionResolver', () => {
messagesCount: 0,
state: 'PENDING',
}),
expect.objectContaining({
amount: expect.decimalEqual(100),
firstName: 'Bob',
id: expect.any(Number),
lastName: 'der Baumeister',
memo: 'Confirmed Contribution',
messagesCount: 0,
state: 'CONFIRMED',
}),
expect.objectContaining({
amount: expect.decimalEqual(100),
firstName: 'Peter',
@ -2790,15 +2795,6 @@ describe('ContributionResolver', () => {
messagesCount: 0,
state: 'PENDING',
}),
expect.objectContaining({
amount: expect.decimalEqual(10),
firstName: 'Bibi',
id: expect.any(Number),
lastName: 'Bloxberg',
memo: 'Test PENDING contribution update',
messagesCount: 0,
state: 'PENDING',
}),
expect.objectContaining({
amount: expect.decimalEqual(200),
firstName: 'Peter',
@ -2808,15 +2804,6 @@ describe('ContributionResolver', () => {
messagesCount: 0,
state: 'DELETED',
}),
expect.objectContaining({
amount: expect.decimalEqual(166),
firstName: 'Räuber',
id: expect.any(Number),
lastName: 'Hotzenplotz',
memo: 'Whatever contribution',
messagesCount: 0,
state: 'DELETED',
}),
expect.objectContaining({
amount: expect.decimalEqual(166),
firstName: 'Räuber',
@ -2826,6 +2813,15 @@ describe('ContributionResolver', () => {
messagesCount: 0,
state: 'DENIED',
}),
expect.objectContaining({
amount: expect.decimalEqual(166),
firstName: 'Räuber',
id: expect.any(Number),
lastName: 'Hotzenplotz',
memo: 'Whatever contribution',
messagesCount: 0,
state: 'DELETED',
}),
expect.objectContaining({
amount: expect.decimalEqual(166),
firstName: 'Räuber',
@ -2840,18 +2836,9 @@ describe('ContributionResolver', () => {
firstName: 'Bibi',
id: expect.any(Number),
lastName: 'Bloxberg',
memo: 'Test IN_PROGRESS contribution',
memo: 'Test contribution to delete',
messagesCount: 0,
state: 'IN_PROGRESS',
}),
expect.objectContaining({
amount: expect.decimalEqual(100),
firstName: 'Bibi',
id: expect.any(Number),
lastName: 'Bloxberg',
memo: 'Test contribution to confirm',
messagesCount: 0,
state: 'CONFIRMED',
state: 'DELETED',
}),
expect.objectContaining({
amount: expect.decimalEqual(100),
@ -2867,9 +2854,27 @@ describe('ContributionResolver', () => {
firstName: 'Bibi',
id: expect.any(Number),
lastName: 'Bloxberg',
memo: 'Test contribution to delete',
memo: 'Test contribution to confirm',
messagesCount: 0,
state: 'DELETED',
state: 'CONFIRMED',
}),
expect.objectContaining({
amount: expect.decimalEqual(100),
firstName: 'Bibi',
id: expect.any(Number),
lastName: 'Bloxberg',
memo: 'Test IN_PROGRESS contribution',
messagesCount: 1,
state: 'IN_PROGRESS',
}),
expect.objectContaining({
amount: expect.decimalEqual(10),
firstName: 'Bibi',
id: expect.any(Number),
lastName: 'Bloxberg',
memo: 'Test PENDING contribution update',
messagesCount: 1,
state: 'PENDING',
}),
expect.objectContaining({
amount: expect.decimalEqual(1000),
@ -2886,9 +2891,9 @@ describe('ContributionResolver', () => {
it('returns two pending creations with page size set to 2', async () => {
const {
data: { adminListAllContributions: contributionListObject },
}: { data: { adminListAllContributions: ContributionListResult } } = await query({
query: adminListAllContributions,
data: { adminListContributions: contributionListObject },
}: { data: { adminListContributions: ContributionListResult } } = await query({
query: adminListContributions,
variables: {
currentPage: 1,
pageSize: 2,

View File

@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions */
import Decimal from 'decimal.js-light'
import { Arg, Args, Authorized, Ctx, Int, Mutation, Query, Resolver } from 'type-graphql'
import { FindOperator, IsNull, getConnection } from '@dbTools/typeorm'
import { IsNull, getConnection } from '@dbTools/typeorm'
import { Contribution as DbContribution } from '@entity/Contribution'
import { ContributionMessage } from '@entity/ContributionMessage'
@ -27,11 +28,11 @@ import { RIGHTS } from '@/auth/RIGHTS'
import { Context, getUser, getClientTimezoneOffset } from '@/server/context'
import { backendLogger as logger } from '@/server/logger'
import {
getCreationDates,
getUserCreation,
validateContribution,
updateCreations,
isValidDateString,
getOpenCreations,
} from './util/creations'
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
import {
@ -125,35 +126,26 @@ export class ContributionResolver {
@Authorized([RIGHTS.LIST_CONTRIBUTIONS])
@Query(() => ContributionListResult)
async listContributions(
@Ctx() context: Context,
@Args()
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
@Arg('filterConfirmed', () => Boolean)
filterConfirmed: boolean | null,
@Ctx() context: Context,
@Arg('statusFilter', () => [ContributionStatus], { nullable: true })
statusFilter?: ContributionStatus[] | null,
): Promise<ContributionListResult> {
const user = getUser(context)
const where: {
userId: number
confirmedBy?: FindOperator<number> | null
} = { userId: user.id }
if (filterConfirmed) where.confirmedBy = IsNull()
const [contributions, count] = await getConnection()
.createQueryBuilder()
.select('c')
.from(DbContribution, 'c')
.leftJoinAndSelect('c.messages', 'm')
.where(where)
.withDeleted()
.orderBy('c.createdAt', order)
.limit(pageSize)
.offset((currentPage - 1) * pageSize)
.getManyAndCount()
const [dbContributions, count] = await findContributions({
order,
currentPage,
pageSize,
withDeleted: true,
relations: ['messages'],
userId: user.id,
statusFilter,
})
return new ContributionListResult(
count,
contributions.map((contribution) => new Contribution(contribution, user)),
dbContributions.map((contribution) => new Contribution(contribution, user)),
)
}
@ -163,15 +155,15 @@ export class ContributionResolver {
@Args()
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
@Arg('statusFilter', () => [ContributionStatus], { nullable: true })
statusFilter?: ContributionStatus[],
statusFilter?: ContributionStatus[] | null,
): Promise<ContributionListResult> {
const [dbContributions, count] = await findContributions(
const [dbContributions, count] = await findContributions({
order,
currentPage,
pageSize,
false,
relations: ['user'],
statusFilter,
)
})
return new ContributionListResult(
count,
@ -244,21 +236,22 @@ export class ContributionResolver {
contributionMessage.isModerator = false
contributionMessage.userId = user.id
contributionMessage.type = ContributionMessageType.HISTORY
ContributionMessage.save(contributionMessage)
await ContributionMessage.save(contributionMessage)
contributionToUpdate.amount = amount
contributionToUpdate.memo = memo
contributionToUpdate.contributionDate = new Date(creationDate)
contributionToUpdate.contributionStatus = ContributionStatus.PENDING
contributionToUpdate.updatedAt = new Date()
DbContribution.save(contributionToUpdate)
await DbContribution.save(contributionToUpdate)
await EVENT_CONTRIBUTION_UPDATE(user, contributionToUpdate, amount)
return new UnconfirmedContribution(contributionToUpdate, user, creations)
}
@Authorized([RIGHTS.ADMIN_CREATE_CONTRIBUTION])
@Mutation(() => [Number])
@Mutation(() => [Decimal])
async adminCreateContribution(
@Args() { email, amount, memo, creationDate }: AdminCreateContributionArgs,
@Ctx() context: Context,
@ -382,21 +375,25 @@ export class ContributionResolver {
return result
}
@Authorized([RIGHTS.ADMIN_LIST_UNCONFIRMED_CONTRIBUTIONS])
@Query(() => ContributionListResult) // [UnconfirmedContribution]
async adminListAllContributions(
@Authorized([RIGHTS.ADMIN_LIST_CONTRIBUTIONS])
@Query(() => ContributionListResult)
async adminListContributions(
@Args()
{ currentPage = 1, pageSize = 3, order = Order.DESC }: Paginated,
@Arg('statusFilter', () => [ContributionStatus], { nullable: true })
statusFilter?: ContributionStatus[],
statusFilter?: ContributionStatus[] | null,
@Arg('userId', () => Int, { nullable: true })
userId?: number | null,
): Promise<ContributionListResult> {
const [dbContributions, count] = await findContributions(
const [dbContributions, count] = await findContributions({
order,
currentPage,
pageSize,
true,
withDeleted: true,
userId,
relations: ['user', 'messages'],
statusFilter,
)
})
return new ContributionListResult(
count,
@ -438,7 +435,8 @@ export class ContributionResolver {
contribution,
contribution.amount,
)
sendContributionDeletedEmail({
void sendContributionDeletedEmail({
firstName: user.firstName,
lastName: user.lastName,
email: user.emailContact.email,
@ -451,7 +449,7 @@ export class ContributionResolver {
return !!res
}
@Authorized([RIGHTS.ADMIN_CONFIRM_CONTRIBUTION])
@Authorized([RIGHTS.CONFIRM_CONTRIBUTION])
@Mutation(() => Boolean)
async confirmContribution(
@Arg('id', () => Int) id: number,
@ -532,7 +530,7 @@ export class ContributionResolver {
await queryRunner.commitTransaction()
logger.info('creation commited successfuly.')
sendContributionConfirmedEmail({
void sendContributionConfirmedEmail({
firstName: user.firstName,
lastName: user.lastName,
email: user.emailContact.email,
@ -555,53 +553,22 @@ export class ContributionResolver {
return true
}
@Authorized([RIGHTS.ADMIN_CREATION_TRANSACTION_LIST])
@Query(() => ContributionListResult)
async creationTransactionList(
@Args()
{ currentPage = 1, pageSize = 25, order = Order.DESC }: Paginated,
@Arg('userId', () => Int) userId: number,
): Promise<ContributionListResult> {
const offset = (currentPage - 1) * pageSize
const [contributionResult, count] = await getConnection()
.createQueryBuilder()
.select('c')
.from(DbContribution, 'c')
.leftJoinAndSelect('c.user', 'u')
.where(`user_id = ${userId}`)
.withDeleted()
.limit(pageSize)
.offset(offset)
.orderBy('c.created_at', order)
.getManyAndCount()
return new ContributionListResult(
count,
contributionResult.map((contribution) => new Contribution(contribution, contribution.user)),
)
// return userTransactions.map((t) => new Transaction(t, new User(user), communityUser))
}
@Authorized([RIGHTS.OPEN_CREATIONS])
@Query(() => [OpenCreation])
async openCreations(
@Arg('userId', () => Int, { nullable: true }) userId: number | null,
@Ctx() context: Context,
): Promise<OpenCreation[]> {
const id = userId || getUser(context).id
const clientTimezoneOffset = getClientTimezoneOffset(context)
const creationDates = getCreationDates(clientTimezoneOffset)
const creations = await getUserCreation(id, clientTimezoneOffset)
return creationDates.map((date, index) => {
return {
month: date.getMonth(),
year: date.getFullYear(),
amount: creations[index],
}
})
async openCreations(@Ctx() context: Context): Promise<OpenCreation[]> {
return getOpenCreations(getUser(context).id, getClientTimezoneOffset(context))
}
@Authorized([RIGHTS.ADMIN_DENY_CONTRIBUTION])
@Authorized([RIGHTS.ADMIN_OPEN_CREATIONS])
@Query(() => [OpenCreation])
async adminOpenCreations(
@Arg('userId', () => Int) userId: number,
@Ctx() context: Context,
): Promise<OpenCreation[]> {
return getOpenCreations(userId, getClientTimezoneOffset(context))
}
@Authorized([RIGHTS.DENY_CONTRIBUTION])
@Mutation(() => Boolean)
async denyContribution(
@Arg('id', () => Int) id: number,
@ -643,7 +610,8 @@ export class ContributionResolver {
contributionToUpdate,
contributionToUpdate.amount,
)
sendContributionDeniedEmail({
void sendContributionDeniedEmail({
firstName: user.firstName,
lastName: user.lastName,
email: user.emailContact.email,

View File

@ -1,3 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */

View File

@ -1,4 +1,7 @@
import { Resolver, Query, Args, Ctx, Authorized, Arg } from 'type-graphql'
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import { Resolver, Query, Args, Ctx, Authorized, Arg, Int, Float } from 'type-graphql'
import { GdtEntryList } from '@model/GdtEntryList'
import { Order } from '@enum/Order'
@ -23,6 +26,7 @@ export class GdtResolver {
try {
const resultGDT = await apiGet(
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
`${CONFIG.GDT_API_URL}/GdtEntries/listPerEmailApi/${userEntity.emailContact.email}/${currentPage}/${pageSize}/${order}`,
)
if (!resultGDT.success) {
@ -35,7 +39,7 @@ export class GdtResolver {
}
@Authorized([RIGHTS.GDT_BALANCE])
@Query(() => Number)
@Query(() => Float, { nullable: true })
async gdtBalance(@Ctx() context: Context): Promise<number | null> {
const user = getUser(context)
try {
@ -54,9 +58,9 @@ export class GdtResolver {
}
@Authorized([RIGHTS.EXIST_PID])
@Query(() => Number)
@Query(() => Int)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async existPid(@Arg('pid') pid: number): Promise<number> {
async existPid(@Arg('pid', () => Int) pid: number): Promise<number> {
// load user
const resultPID = await apiGet(`${CONFIG.GDT_API_URL}/publishers/checkPidApi/${pid}`)
if (!resultPID.success) {

View File

@ -1,6 +1,5 @@
import { Resolver, Query, Authorized, Arg, Mutation, Args } from 'type-graphql'
import SubscribeNewsletterArgs from '@arg/SubscribeNewsletterArgs'
/* eslint-disable @typescript-eslint/no-unsafe-return */
import { Resolver, Query, Authorized, Arg, Mutation, Ctx } from 'type-graphql'
import {
getKlickTippUser,
@ -9,6 +8,7 @@ import {
klicktippSignIn,
} from '@/apis/KlicktippController'
import { RIGHTS } from '@/auth/RIGHTS'
import { Context, getUser } from '@/server/context'
@Resolver()
export class KlicktippResolver {
@ -26,15 +26,15 @@ export class KlicktippResolver {
@Authorized([RIGHTS.UNSUBSCRIBE_NEWSLETTER])
@Mutation(() => Boolean)
async unsubscribeNewsletter(@Arg('email') email: string): Promise<boolean> {
return await unsubscribe(email)
async unsubscribeNewsletter(@Ctx() context: Context): Promise<boolean> {
const user = getUser(context)
return unsubscribe(user.emailContact.email)
}
@Authorized([RIGHTS.SUBSCRIBE_NEWSLETTER])
@Mutation(() => Boolean)
async subscribeNewsletter(
@Args() { email, language }: SubscribeNewsletterArgs,
): Promise<boolean> {
return await klicktippSignIn(email, language)
async subscribeNewsletter(@Ctx() context: Context): Promise<boolean> {
const user = getUser(context)
return klicktippSignIn(user.emailContact.email, user.language)
}
}

View File

@ -1,3 +1,5 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import Decimal from 'decimal.js-light'
import { Resolver, Query, Authorized, FieldResolver } from 'type-graphql'
import { getConnection } from '@dbTools/typeorm'
@ -15,7 +17,7 @@ import { calculateDecay } from '@/util/decay'
export class StatisticsResolver {
@Authorized([RIGHTS.COMMUNITY_STATISTICS])
@Query(() => CommunityStatistics)
async communityStatistics(): Promise<CommunityStatistics> {
communityStatistics(): CommunityStatistics {
return new CommunityStatistics()
}

View File

@ -1,3 +1,8 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* 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/unbound-method */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
@ -17,10 +22,12 @@ import {
createContribution,
updateContribution,
createTransactionLink,
confirmContribution,
} from '@/seeds/graphql/mutations'
import { listTransactionLinksAdmin } from '@/seeds/graphql/queries'
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
import { User } from '@entity/User'
import { Transaction } from '@entity/Transaction'
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
import Decimal from 'decimal.js-light'
import { GraphQLError } from 'graphql'
@ -92,7 +99,7 @@ describe('TransactionLinkResolver', () => {
errors: [new GraphQLError('Amount must be a positive number')],
})
})
it('logs the error thrown', () => {
it('logs the error "Amount must be a positive number" - 0', () => {
expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(0))
})
@ -110,7 +117,7 @@ describe('TransactionLinkResolver', () => {
errors: [new GraphQLError('Amount must be a positive number')],
})
})
it('logs the error thrown', () => {
it('logs the error "Amount must be a positive number" - -10', () => {
expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(-10))
})
@ -128,7 +135,7 @@ describe('TransactionLinkResolver', () => {
errors: [new GraphQLError('User has not enough GDD')],
})
})
it('logs the error thrown', () => {
it('logs the error "User has not enough GDD"', () => {
expect(logger.error).toBeCalledWith('User has not enough GDD', expect.any(Number))
})
})
@ -140,6 +147,8 @@ describe('TransactionLinkResolver', () => {
resetToken()
})
let contributionId: number
describe('unauthenticated', () => {
it('throws an error', async () => {
jest.clearAllMocks()
@ -178,7 +187,7 @@ describe('TransactionLinkResolver', () => {
})
})
it('logs the error thrown', () => {
it('logs the error "No contribution link found to given code"', () => {
expect(logger.error).toBeCalledWith(
'No contribution link found to given code',
'CL-123456',
@ -213,7 +222,7 @@ describe('TransactionLinkResolver', () => {
mutate({
mutation: redeemTransactionLink,
variables: {
code: 'CL-' + contributionLink.code,
code: `CL-${contributionLink.code}`,
},
}),
).resolves.toMatchObject({
@ -222,7 +231,7 @@ describe('TransactionLinkResolver', () => {
await resetEntity(DbContributionLink)
})
it('logs the error thrown', () => {
it('logs the error "Contribution link is not valid yet"', () => {
expect(logger.error).toBeCalledWith('Contribution link is not valid yet', validFrom)
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
@ -252,7 +261,7 @@ describe('TransactionLinkResolver', () => {
mutate({
mutation: redeemTransactionLink,
variables: {
code: 'CL-' + contributionLink.code,
code: `CL-${contributionLink.code}`,
},
}),
).resolves.toMatchObject({
@ -261,7 +270,7 @@ describe('TransactionLinkResolver', () => {
await resetEntity(DbContributionLink)
})
it('logs the error thrown', () => {
it('logs the error "Contribution link has unknown cycle"', () => {
expect(logger.error).toBeCalledWith('Contribution link has unknown cycle', 'INVALID')
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
@ -291,7 +300,7 @@ describe('TransactionLinkResolver', () => {
mutate({
mutation: redeemTransactionLink,
variables: {
code: 'CL-' + contributionLink.code,
code: `CL-${contributionLink.code}`,
},
}),
).resolves.toMatchObject({
@ -300,7 +309,7 @@ describe('TransactionLinkResolver', () => {
await resetEntity(DbContributionLink)
})
it('logs the error thrown', () => {
it('logs the error "Contribution link is no longer valid"', () => {
expect(logger.error).toBeCalledWith('Contribution link is no longer valid', validTo)
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
@ -309,7 +318,6 @@ describe('TransactionLinkResolver', () => {
})
})
// TODO: have this test separated into a transactionLink and a contributionLink part
describe('redeem daily Contribution Link', () => {
const now = new Date()
let contributionLink: DbContributionLink | undefined
@ -335,6 +343,10 @@ describe('TransactionLinkResolver', () => {
})
})
afterAll(async () => {
await resetEntity(Transaction)
})
it('has a daily contribution link in the database', async () => {
const cls = await DbContributionLink.find()
expect(cls).toHaveLength(1)
@ -376,6 +388,7 @@ describe('TransactionLinkResolver', () => {
},
})
contribution = result.data.createContribution
contributionId = result.data.createContribution.id
})
it('does not allow the user to redeem the contribution link', async () => {
@ -392,7 +405,7 @@ describe('TransactionLinkResolver', () => {
})
})
it('logs the error thrown', () => {
it('logs the error "Creation from contribution link was not successful"', () => {
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error(
@ -440,7 +453,7 @@ describe('TransactionLinkResolver', () => {
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
expect(DbEvent.find()).resolves.toContainEqual(
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.CONTRIBUTION_LINK_REDEEM,
affectedUserId: userConatct.user.id,
@ -467,7 +480,7 @@ describe('TransactionLinkResolver', () => {
})
})
it('logs the error thrown', () => {
it('logs the error "Creation from contribution link was not successful"', () => {
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error('Contribution link already redeemed today'),
@ -519,7 +532,7 @@ describe('TransactionLinkResolver', () => {
})
})
it('logs the error thrown', () => {
it('logs the error "Creation from contribution link was not successful"', () => {
expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful',
new Error('Contribution link already redeemed today'),
@ -529,6 +542,92 @@ describe('TransactionLinkResolver', () => {
})
})
})
describe('transaction link', () => {
beforeEach(() => {
jest.clearAllMocks()
})
describe('link does not exits', () => {
beforeAll(async () => {
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
})
it('throws and logs the error', async () => {
await expect(
mutate({
mutation: redeemTransactionLink,
variables: {
code: 'not-valid',
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('Transaction link not found')],
})
expect(logger.error).toBeCalledWith('Transaction link not found', 'not-valid')
})
})
describe('link exists', () => {
let myCode: string
beforeAll(async () => {
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
await mutate({
mutation: confirmContribution,
variables: { id: contributionId },
})
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
const {
data: {
createTransactionLink: { code },
},
} = await mutate({
mutation: createTransactionLink,
variables: {
amount: 200,
memo: 'This is a transaction link from bibi',
},
})
myCode = code
})
describe('own link', () => {
beforeAll(async () => {
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
})
it('throws and logs an error', async () => {
await expect(
mutate({
mutation: redeemTransactionLink,
variables: {
code: myCode,
},
}),
).resolves.toMatchObject({
errors: [new GraphQLError('Cannot redeem own transaction link')],
})
expect(logger.error).toBeCalledWith(
'Cannot redeem own transaction link',
expect.any(Number),
)
})
})
})
})
})
})
@ -636,7 +735,7 @@ describe('TransactionLinkResolver', () => {
})
})
it('logs the error thrown', () => {
it('logs the error "Could not find requested User"', () => {
expect(logger.error).toBeCalledWith('Could not find requested User', -1)
})
})
@ -775,6 +874,7 @@ describe('TransactionLinkResolver', () => {
})
// TODO: works not as expected, because 'redeemedAt' and 'redeemedBy' have to be added to the transaktion link factory
// eslint-disable-next-line jest/no-disabled-tests
describe.skip('filter by redeemed', () => {
it('finds 6 open transaction links, 1 deleted, and no redeemed', async () => {
await expect(

View File

@ -300,12 +300,20 @@ export class TransactionLinkResolver {
return true
} else {
const now = new Date()
const transactionLink = await DbTransactionLink.findOneOrFail({ code })
const linkedUser = await DbUser.findOneOrFail(
const transactionLink = await DbTransactionLink.findOne({ code })
if (!transactionLink) {
throw new LogError('Transaction link not found', code)
}
const linkedUser = await DbUser.findOne(
{ id: transactionLink.userId },
{ relations: ['emailContact'] },
)
if (!linkedUser) {
throw new LogError('Linked user not found for given link', transactionLink.userId)
}
if (user.id === linkedUser.id) {
throw new LogError('Cannot redeem own transaction link', user.id)
}
@ -357,13 +365,14 @@ export class TransactionLinkResolver {
)
}
@Authorized([RIGHTS.ADMIN_LIST_TRANSACTION_LINKS_ADMIN])
@Authorized([RIGHTS.LIST_TRANSACTION_LINKS_ADMIN])
@Query(() => TransactionLinkResult)
async listTransactionLinksAdmin(
@Args()
paginated: Paginated,
// eslint-disable-next-line type-graphql/wrong-decorator-signature
@Arg('filters', () => TransactionLinkFilters, { nullable: true })
filters: TransactionLinkFilters | null,
filters: TransactionLinkFilters | null, // eslint-disable-line type-graphql/invalid-nullable-input-type
@Arg('userId', () => Int)
userId: number,
): Promise<TransactionLinkResult> {

View File

@ -1,3 +1,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/unbound-method */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
@ -324,7 +328,7 @@ describe('send coins', () => {
).toEqual(
expect.objectContaining({
data: {
sendCoins: 'true',
sendCoins: true,
},
}),
)
@ -337,7 +341,7 @@ describe('send coins', () => {
memo: 'unrepeatable memo',
})
expect(DbEvent.find()).resolves.toContainEqual(
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.TRANSACTION_SEND,
affectedUserId: user[1].id,
@ -355,7 +359,7 @@ describe('send coins', () => {
memo: 'unrepeatable memo',
})
expect(DbEvent.find()).resolves.toContainEqual(
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.TRANSACTION_RECEIVE,
affectedUserId: user[0].id,
@ -381,7 +385,7 @@ describe('send coins', () => {
).resolves.toEqual(
expect.objectContaining({
data: {
sendCoins: 'true',
sendCoins: true,
},
}),
)
@ -397,7 +401,7 @@ describe('send coins', () => {
).resolves.toEqual(
expect.objectContaining({
data: {
sendCoins: 'true',
sendCoins: true,
},
}),
)
@ -413,7 +417,7 @@ describe('send coins', () => {
).resolves.toEqual(
expect.objectContaining({
data: {
sendCoins: 'true',
sendCoins: true,
},
}),
)
@ -429,7 +433,7 @@ describe('send coins', () => {
).resolves.toEqual(
expect.objectContaining({
data: {
sendCoins: 'true',
sendCoins: true,
},
}),
)

View File

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable new-cap */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
@ -137,13 +138,7 @@ export const executeTransaction = async (
await queryRunner.commitTransaction()
logger.info(`commit Transaction successful...`)
await EVENT_TRANSACTION_SEND(
sender,
recipient,
transactionSend,
// TODO why mul -1?
transactionSend.amount.mul(-1),
)
await EVENT_TRANSACTION_SEND(sender, recipient, transactionSend, transactionSend.amount)
await EVENT_TRANSACTION_RECEIVE(
recipient,
@ -305,7 +300,7 @@ export class TransactionResolver {
}
@Authorized([RIGHTS.SEND_COINS])
@Mutation(() => String)
@Mutation(() => Boolean)
async sendCoins(
@Args() { email, amount, memo }: TransactionSendArgs,
@Ctx() context: Context,

View File

@ -1,3 +1,8 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/unbound-method */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
@ -182,7 +187,7 @@ describe('UserResolver', () => {
{ email: 'peter@lustig.de' },
{ relations: ['user'] },
)
expect(DbEvent.find()).resolves.toContainEqual(
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.USER_REGISTER,
affectedUserId: userConatct.user.id,
@ -212,7 +217,7 @@ describe('UserResolver', () => {
})
it('stores the EMAIL_CONFIRMATION event in the database', () => {
expect(DbEvent.find()).resolves.toContainEqual(
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.EMAIL_CONFIRMATION,
affectedUserId: user[0].id,
@ -228,7 +233,7 @@ describe('UserResolver', () => {
mutation = await mutate({ mutation: createUser, variables })
})
it('logs an info', async () => {
it('logs an info', () => {
expect(logger.info).toBeCalledWith('User already exists with this email=peter@lustig.de')
})
@ -241,7 +246,7 @@ describe('UserResolver', () => {
})
})
it('results with partly faked user with random "id"', async () => {
it('results with partly faked user with random "id"', () => {
expect(mutation).toEqual(
expect.objectContaining({
data: {
@ -258,7 +263,7 @@ describe('UserResolver', () => {
{ email: 'peter@lustig.de' },
{ relations: ['user'] },
)
expect(DbEvent.find()).resolves.toContainEqual(
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.EMAIL_ACCOUNT_MULTIREGISTRATION,
affectedUserId: userConatct.user.id,
@ -286,7 +291,7 @@ describe('UserResolver', () => {
})
describe('no publisher id', () => {
it('sets publisher id to null', async () => {
it('sets publisher id to 0', async () => {
await mutate({
mutation: createUser,
variables: { ...variables, email: 'raeuber@hotzenplotz.de', publisherId: undefined },
@ -297,7 +302,7 @@ describe('UserResolver', () => {
emailContact: expect.objectContaining({
email: 'raeuber@hotzenplotz.de',
}),
publisherId: null,
publisherId: 0,
}),
]),
)
@ -359,7 +364,7 @@ describe('UserResolver', () => {
})
it('stores the USER_ACTIVATE_ACCOUNT event in the database', () => {
expect(DbEvent.find()).resolves.toContainEqual(
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.USER_ACTIVATE_ACCOUNT,
affectedUserId: user[0].id,
@ -369,7 +374,7 @@ describe('UserResolver', () => {
})
it('stores the USER_REGISTER_REDEEM event in the database', () => {
expect(DbEvent.find()).resolves.toContainEqual(
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.USER_REGISTER_REDEEM,
affectedUserId: result.data.createUser.id,
@ -687,7 +692,7 @@ describe('UserResolver', () => {
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
expect(DbEvent.find()).resolves.toContainEqual(
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.USER_LOGIN,
affectedUserId: userConatct.user.id,
@ -792,6 +797,7 @@ describe('UserResolver', () => {
})
})
// eslint-disable-next-line jest/no-disabled-tests
describe.skip('user is in database but password is not set', () => {
beforeAll(async () => {
jest.clearAllMocks()
@ -868,7 +874,7 @@ describe('UserResolver', () => {
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
expect(DbEvent.find()).resolves.toContainEqual(
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.USER_LOGOUT,
affectedUserId: userConatct.user.id,
@ -950,7 +956,7 @@ describe('UserResolver', () => {
})
it('stores the USER_LOGIN event in the database', () => {
expect(DbEvent.find()).resolves.toContainEqual(
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.USER_LOGIN,
affectedUserId: user[0].id,
@ -1037,7 +1043,7 @@ describe('UserResolver', () => {
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
expect(DbEvent.find()).resolves.toContainEqual(
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.EMAIL_FORGOT_PASSWORD,
affectedUserId: userConatct.user.id,
@ -1175,7 +1181,7 @@ describe('UserResolver', () => {
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
expect(DbEvent.find()).resolves.toContainEqual(
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.USER_INFO_UPDATE,
affectedUserId: userConatct.user.id,
@ -1563,7 +1569,7 @@ describe('UserResolver', () => {
{ email: 'peter@lustig.de' },
{ relations: ['user'] },
)
expect(DbEvent.find()).resolves.toContainEqual(
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.ADMIN_USER_ROLE_SET,
affectedUserId: userConatct.user.id,
@ -1765,7 +1771,7 @@ describe('UserResolver', () => {
{ email: 'peter@lustig.de' },
{ relations: ['user'] },
)
expect(DbEvent.find()).resolves.toContainEqual(
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.ADMIN_USER_DELETE,
affectedUserId: userConatct.user.id,
@ -1934,7 +1940,7 @@ describe('UserResolver', () => {
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
expect(DbEvent.find()).resolves.toContainEqual(
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.EMAIL_ADMIN_CONFIRMATION,
affectedUserId: userConatct.user.id,
@ -2059,7 +2065,7 @@ describe('UserResolver', () => {
{ email: 'peter@lustig.de' },
{ relations: ['user'] },
)
expect(DbEvent.find()).resolves.toContainEqual(
await expect(DbEvent.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventType.ADMIN_USER_UNDELETE,
affectedUserId: userConatct.user.id,

View File

@ -1,3 +1,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
import i18n from 'i18n'
import { v4 as uuidv4 } from 'uuid'
import {
@ -172,11 +176,11 @@ export class UserResolver {
// Elopage Status & Stored PublisherId
user.hasElopage = await this.hasElopage({ ...context, user: dbUser })
logger.info('user.hasElopage=' + user.hasElopage)
logger.info('user.hasElopage', user.hasElopage)
if (!user.hasElopage && publisherId) {
user.publisherId = publisherId
dbUser.publisherId = publisherId
DbUser.save(dbUser)
await DbUser.save(dbUser)
}
context.setHeaders.push({
@ -202,7 +206,7 @@ export class UserResolver {
@Mutation(() => User)
async createUser(
@Args()
{ email, firstName, lastName, language, publisherId, redeemCode = null }: CreateUserArgs,
{ email, firstName, lastName, language, publisherId = null, redeemCode = null }: CreateUserArgs,
): Promise<User> {
logger.addContext('user', 'unknown')
logger.info(
@ -239,7 +243,7 @@ export class UserResolver {
user.lastName = lastName
user.language = language
user.publisherId = publisherId
logger.debug('partly faked user=' + user)
logger.debug('partly faked user', user)
const emailSent = await sendAccountMultiRegistrationEmail({
firstName: foundUser.firstName, // this is the real name of the email owner, but just "firstName" would be the name of the new registrant which shall not be passed to the outside
@ -275,22 +279,22 @@ export class UserResolver {
dbUser.firstName = firstName
dbUser.lastName = lastName
dbUser.language = language
dbUser.publisherId = publisherId
dbUser.publisherId = publisherId || 0
dbUser.passwordEncryptionType = PasswordEncryptionType.NO_PASSWORD
logger.debug('new dbUser=' + dbUser)
logger.debug('new dbUser', dbUser)
if (redeemCode) {
if (redeemCode.match(/^CL-/)) {
const contributionLink = await DbContributionLink.findOne({
code: redeemCode.replace('CL-', ''),
})
logger.info('redeemCode found contributionLink=' + contributionLink)
logger.info('redeemCode found contributionLink', contributionLink)
if (contributionLink) {
dbUser.contributionLinkId = contributionLink.id
eventRegisterRedeem.involvedContributionLink = contributionLink
}
} else {
const transactionLink = await DbTransactionLink.findOne({ code: redeemCode })
logger.info('redeemCode found transactionLink=' + transactionLink)
logger.info('redeemCode found transactionLink', transactionLink)
if (transactionLink) {
dbUser.referrerId = transactionLink.userId
eventRegisterRedeem.involvedTransactionLink = transactionLink
@ -632,7 +636,7 @@ export class UserResolver {
}
}
@Authorized([RIGHTS.ADMIN_SEARCH_USERS])
@Authorized([RIGHTS.SEARCH_USERS])
@Query(() => SearchUsersResult)
async searchUsers(
@Args()
@ -655,7 +659,7 @@ export class UserResolver {
return 'user.' + fieldName
}),
searchText,
filters,
filters || null,
currentPage,
pageSize,
)
@ -698,7 +702,7 @@ export class UserResolver {
}
}
@Authorized([RIGHTS.ADMIN_SET_USER_ROLE])
@Authorized([RIGHTS.SET_USER_ROLE])
@Mutation(() => Date, { nullable: true })
async setUserRole(
@Arg('userId', () => Int)
@ -741,7 +745,7 @@ export class UserResolver {
return newUser ? newUser.isAdmin : null
}
@Authorized([RIGHTS.ADMIN_DELETE_USER])
@Authorized([RIGHTS.DELETE_USER])
@Mutation(() => Date, { nullable: true })
async deleteUser(
@Arg('userId', () => Int) userId: number,
@ -764,7 +768,7 @@ export class UserResolver {
return newUser ? newUser.deletedAt : null
}
@Authorized([RIGHTS.ADMIN_UNDELETE_USER])
@Authorized([RIGHTS.UNDELETE_USER])
@Mutation(() => Date, { nullable: true })
async unDeleteUser(
@Arg('userId', () => Int) userId: number,
@ -783,7 +787,7 @@ export class UserResolver {
}
// TODO this is an admin function - needs refactor
@Authorized([RIGHTS.ADMIN_SEND_ACTIVATION_EMAIL])
@Authorized([RIGHTS.SEND_ACTIVATION_EMAIL])
@Mutation(() => Boolean)
async sendActivationEmail(
@Arg('email') email: string,

View File

@ -1,8 +1,10 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/restrict-template-expressions */
/* eslint-disable @typescript-eslint/no-explicit-any */
import Decimal from 'decimal.js-light'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { logger } from '@test/testSetup'
import { userFactory } from '@/seeds/factory/user'
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
import { bobBaumeister } from '@/seeds/users/bob-baumeister'
@ -79,7 +81,7 @@ describe('semaphore', () => {
maxPerCycle: 1,
},
})
contributionLinkCode = 'CL-' + contributionLink.code
contributionLinkCode = `CL-${contributionLink.code}`
await mutate({
mutation: login,
variables: { email: 'bob@baumeister.de', password: 'Aa12345_' },
@ -187,4 +189,50 @@ describe('semaphore', () => {
await expect(confirmBibisContribution).resolves.toMatchObject({ errors: undefined })
await expect(confirmBobsContribution).resolves.toMatchObject({ errors: undefined })
})
describe('redeem transaction link twice', () => {
let myCode: string
beforeAll(async () => {
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
const {
data: { createTransactionLink: bibisLink },
} = await mutate({
mutation: createTransactionLink,
variables: {
amount: 20,
memo: 'Bibis Link',
},
})
myCode = bibisLink.code
await mutate({
mutation: login,
variables: { email: 'bob@baumeister.de', password: 'Aa12345_' },
})
})
it('does not throw, but should', async () => {
const redeem1 = mutate({
mutation: redeemTransactionLink,
variables: {
code: myCode,
},
})
const redeem2 = mutate({
mutation: redeemTransactionLink,
variables: {
code: myCode,
},
})
await expect(redeem1).resolves.toMatchObject({
errors: undefined,
})
await expect(redeem2).resolves.toMatchObject({
errors: undefined,
})
})
})
})

View File

@ -1,3 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */

View File

@ -1,9 +1,12 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import LogError from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger'
import { getConnection } from '@dbTools/typeorm'
import { Contribution } from '@entity/Contribution'
import Decimal from 'decimal.js-light'
import { FULL_CREATION_AVAILABLE, MAX_CREATION_AMOUNT } from '../const/const'
import { OpenCreation } from '@model/OpenCreation'
interface CreationMap {
id: number
@ -100,7 +103,7 @@ const getCreationMonths = (timezoneOffset: number): number[] => {
return getCreationDates(timezoneOffset).map((date) => date.getMonth() + 1)
}
export const getCreationDates = (timezoneOffset: number): Date[] => {
const getCreationDates = (timezoneOffset: number): Date[] => {
const clientNow = new Date()
clientNow.setTime(clientNow.getTime() - timezoneOffset * 60 * 1000)
logger.info(
@ -152,3 +155,18 @@ export const updateCreations = (
export const isValidDateString = (dateString: string): boolean => {
return new Date(dateString).toString() !== 'Invalid Date'
}
export const getOpenCreations = async (
userId: number,
timezoneOffset: number,
): Promise<OpenCreation[]> => {
const creations = await getUserCreation(userId, timezoneOffset)
const creationDates = getCreationDates(timezoneOffset)
return creationDates.map((date, index) => {
return {
month: date.getMonth(),
year: date.getFullYear(),
amount: creations[index],
}
})
}

View File

@ -3,23 +3,36 @@ import { Order } from '@enum/Order'
import { Contribution as DbContribution } from '@entity/Contribution'
import { In } from '@dbTools/typeorm'
interface FindContributionsOptions {
order: Order
currentPage: number
pageSize: number
withDeleted?: boolean
relations?: string[]
userId?: number | null
statusFilter?: ContributionStatus[] | null
}
export const findContributions = async (
order: Order,
currentPage: number,
pageSize: number,
withDeleted: boolean,
statusFilter?: ContributionStatus[],
): Promise<[DbContribution[], number]> =>
DbContribution.findAndCount({
options: FindContributionsOptions,
): Promise<[DbContribution[], number]> => {
const { order, currentPage, pageSize, withDeleted, relations, userId, statusFilter } = {
withDeleted: false,
relations: [],
...options,
}
return DbContribution.findAndCount({
where: {
...(statusFilter && statusFilter.length && { contributionStatus: In(statusFilter) }),
...(userId && { userId }),
},
withDeleted: withDeleted,
withDeleted,
order: {
createdAt: order,
id: order,
},
relations: ['user'],
relations,
skip: (currentPage - 1) * pageSize,
take: pageSize,
})
}

View File

@ -17,7 +17,7 @@ async function main() {
console.log(`GraphIQL available at http://localhost:${CONFIG.PORT}`)
}
})
startValidateCommunities(Number(CONFIG.FEDERATION_VALIDATE_COMMUNITY_TIMER))
void startValidateCommunities(Number(CONFIG.FEDERATION_VALIDATE_COMMUNITY_TIMER))
}
main().catch((e) => {

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 */
import { MiddlewareFn } from 'type-graphql'
import { /* klicktippSignIn, */ getKlickTippUser } from '@/apis/KlicktippController'
import { KlickTipp } from '@model/KlickTipp'

View File

@ -1,3 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import CONFIG from '@/config'
import LogError from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger'

View File

@ -1,3 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/unbound-method */
import { ApolloServerTestClient } from 'apollo-server-testing'
import { login, createContributionLink } from '@/seeds/graphql/mutations'
import { ContributionLink } from '@model/ContributionLink'

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