Merge branch 'master' into alias-update-user-info

This commit is contained in:
Moriz Wahl 2023-05-08 14:33:42 +02:00 committed by GitHub
commit 3b7f565b02
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
442 changed files with 10822 additions and 6044 deletions

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

@ -0,0 +1,52 @@
# 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/**/*'
backend: &backend
- 'backend/**/*'
dht_node: &dht_node
- 'dht-node/**/*'
docker-compose: &docker-compose
- 'docker-compose.*'
federation: &federation
- 'federation/**/*'
frontend: &frontend
- 'frontend/**/*'
mariadb: &mariadb
- 'mariadb/**/*'
nginx: &nginx
- 'nginx/**/*'

View File

@ -1,559 +0,0 @@
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 #############################################
##############################################################################
build_test_backend:
name: Docker Build Test - Backend
runs-on: ubuntu-latest
#needs: [nothing]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# BACKEND ################################################################
##########################################################################
- name: Backend | Build `test` image
run: |
docker build -f ./backend/Dockerfile --target test -t "gradido/backend:test" .
docker save "gradido/backend:test" > /tmp/backend.tar
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: docker-backend-test
path: /tmp/backend.tar
##############################################################################
# JOB: DOCKER BUILD TEST DATABASE UP #########################################
##############################################################################
build_test_database_up:
name: Docker Build Test - Database up
runs-on: ubuntu-latest
#needs: [nothing]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DATABASE UP ############################################################
##########################################################################
- name: Database | Build `test_up` image
run: |
docker build --target test_up -t "gradido/database:test_up" database/
docker save "gradido/database:test_up" > /tmp/database_up.tar
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: docker-database-test_up
path: /tmp/database_up.tar
##############################################################################
# JOB: DOCKER BUILD TEST MARIADB #############################################
##############################################################################
build_test_mariadb:
name: Docker Build Test - MariaDB
runs-on: ubuntu-latest
#needs: [nothing]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# BUILD MARIADB DOCKER IMAGE #############################################
##########################################################################
- name: mariadb | Build `test` image
run: |
docker build --target mariadb_server -t "gradido/mariadb:test" -f ./mariadb/Dockerfile ./
docker save "gradido/mariadb:test" > /tmp/mariadb.tar
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
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 ##########################################################
##############################################################################
lint_backend:
name: Lint - Backend
runs-on: ubuntu-latest
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# LINT BACKEND ###########################################################
##########################################################################
- name: backend | Lint
run: cd backend && yarn && yarn run lint
##############################################################################
# JOB: LOCALES BACKEND #######################################################
##############################################################################
locales_backend:
name: Locales - Backend
runs-on: ubuntu-latest
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# LOCALES BACKEND #####################################################
##########################################################################
- name: Backend | Locales
run: cd backend && yarn && yarn locales
##############################################################################
# JOB: LINT DATABASE UP ######################################################
##############################################################################
lint_database_up:
name: Lint - Database Up
runs-on: ubuntu-latest
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# LINT DATABASE ##########################################################
##########################################################################
- 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 ####################################################
##############################################################################
unit_test_backend:
name: Unit tests - Backend
runs-on: ubuntu-latest
needs: [build_test_mariadb]
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
run: docker load < /tmp/mariadb.tar
##########################################################################
# UNIT TESTS BACKEND #####################################################
##########################################################################
- name: backend | docker-compose mariadb
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb
- name: Sleep for 30 seconds
run: sleep 30s
shell: bash
- name: 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: 80
token: ${{ github.token }}
##########################################################################
# DATABASE MIGRATION TEST UP + RESET #####################################
##########################################################################
database_migration_test:
name: Database Migration Test - Up + Reset
runs-on: ubuntu-latest
#needs: [nothing]
steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOCKER COMPOSE DATABASE UP + RESET #####################################
##########################################################################
- name: database | docker-compose
run: docker-compose -f docker-compose.yml up --detach mariadb
- name: database | up
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
- 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

81
.github/workflows/test_backend.yml vendored Normal file
View File

@ -0,0 +1,81 @@
name: Gradido Backend Test CI
on: push
jobs:
files-changed:
name: Detect File Changes - Backend
runs-on: ubuntu-latest
outputs:
backend: ${{ steps.changes.outputs.backend }}
database: ${{ steps.changes.outputs.database }}
docker-compose: ${{ steps.changes.outputs.docker-compose }}
mariadb: ${{ steps.changes.outputs.mariadb }}
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.backend == 'true'
name: Docker Build Test - Backend
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Backend | Build 'test' image
run: docker build -f ./backend/Dockerfile --target test -t "gradido/backend:test" .
unit_test:
if: needs.files-changed.outputs.backend == 'true' || needs.files-changed.outputs.database == 'true' || needs.files-changed.outputs.docker-compose == 'true' || needs.files-changed.outputs.mariadb == 'true'
name: Unit tests - Backend
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Backend | docker-compose mariadb
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb
- name: Sleep for 30 seconds
run: sleep 30s
shell: bash
- name: 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
run: cd database && yarn && yarn build && cd ../backend && yarn && yarn test
lint:
if: needs.files-changed.outputs.backend == 'true'
name: Lint - Backend
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Backend | Lint
run: cd database && yarn && cd ../backend && yarn && yarn run lint
locales:
if: needs.files-changed.outputs.backend == 'true'
name: Locales - Backend
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Backend | Locales
run: cd backend && yarn && yarn locales

64
.github/workflows/test_database.yml vendored Normal file
View File

@ -0,0 +1,64 @@
name: Gradido Database Test CI
on: push
jobs:
files-changed:
name: Detect File Changes - Database
runs-on: ubuntu-latest
outputs:
database: ${{ steps.changes.outputs.database }}
docker-compose: ${{ steps.changes.outputs.docker-compose }}
mariadb: ${{ steps.changes.outputs.mariadb }}
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:
if: needs.files-changed.outputs.database == 'true'
name: Docker Build Test - Database up
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Database | Build 'test_up' image
run: docker build --target test_up -t "gradido/database:test_up" database/
database_migration_test:
if: needs.files-changed.outputs.database == 'true' || needs.files-changed.outputs.docker-compose == 'true' || needs.files-changed.outputs.mariadb == 'true'
name: Database Migration Test - Up + Reset
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Database | docker-compose
run: docker-compose -f docker-compose.yml up --detach mariadb
- name: Database | up
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
lint:
if: needs.files-changed.outputs.database == 'true'
name: Lint - Database Up
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Database | Lint
run: cd database && yarn && yarn run lint

View File

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

91
.github/workflows/test_dht_node.yml vendored Normal file
View File

@ -0,0 +1,91 @@
name: Gradido DHT Node Test CI
on: push
jobs:
files-changed:
name: Detect File Changes - DHT Node
runs-on: ubuntu-latest
outputs:
database: ${{ steps.changes.outputs.database }}
dht_node: ${{ steps.changes.outputs.dht_node }}
docker-compose: ${{ steps.changes.outputs.docker-compose }}
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:
name: Docker Build Test - DHT Node
if: needs.files-changed.outputs.dht_node == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Build 'test' image
run: |
docker build --target test -t "gradido/dht-node:test" -f dht-node/Dockerfile .
docker save "gradido/dht-node:test" > /tmp/dht-node.tar
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: docker-dht-node-test
path: /tmp/dht-node.tar
lint:
name: Lint - DHT Node
if: needs.files-changed.outputs.dht_node == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Lint
run: cd dht-node && yarn && yarn run lint
unit_test:
name: Unit Tests - DHT Node
if: needs.files-changed.outputs.database == 'true' || needs.files-changed.outputs.dht_node == 'true' || needs.files-changed.outputs.docker-compose == 'true' || needs.files-changed.outputs.mariadb == 'true'
needs: [files-changed, build]
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Download Docker Image
uses: actions/download-artifact@v3
with:
name: docker-dht-node-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/dht-node.tar
- name: docker-compose mariadb
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb
- name: Sleep for 30 seconds
run: sleep 30s
shell: bash
- name: docker-compose database
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps database
- name: Sleep for 30 seconds
run: sleep 30s
shell: bash
#- name: Unit tests
# run: cd database && yarn && yarn build && cd ../dht-node && yarn && yarn test
- name: Unit tests
run: docker run --env NODE_ENV=test --env DB_HOST=mariadb --network gradido_internal-net --rm gradido/dht-node:test yarn run test

58
.github/workflows/test_e2e.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

@ -1,13 +1,29 @@
name: gradido test_federation CI
name: Gradido Federation Test CI
on: push
jobs:
##############################################################################
# JOB: DOCKER BUILD TEST #####################################################
##############################################################################
files-changed:
name: Detect File Changes - Federation
runs-on: ubuntu-latest
outputs:
docker-compose: ${{ steps.changes.outputs.docker-compose }}
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
build:
name: Docker Build Test
name: Docker Build Test - Federation
if: needs.files-changed.outputs.federation == 'true'
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
@ -24,35 +40,23 @@ jobs:
name: docker-federation-test
path: /tmp/federation.tar
##############################################################################
# JOB: LINT ##################################################################
##############################################################################
lint:
name: 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
name: Unit Tests - Federation
if: needs.files-changed.outputs.database == 'true' || needs.files-changed.outputs.docker-compose == 'true' || needs.files-changed.outputs.federation == 'true' || needs.files-changed.outputs.mariadb == 'true'
needs: [files-changed, build]
runs-on: ubuntu-latest
needs: [build]
steps:
- name: Checkout code
uses: actions/checkout@v3
@ -84,15 +88,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

32
.github/workflows/test_mariadb.yml vendored Normal file
View File

@ -0,0 +1,32 @@
name: Gradido MariaDB Test CI
on: push
jobs:
files-changed:
name: Detect File Changes - MariaDB
runs-on: ubuntu-latest
outputs:
mariadb: ${{ steps.changes.outputs.mariadb }}
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.mariadb == 'true'
name: Docker Build Test - MariaDB
needs: files-changed
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: MariaDB | Build 'test' image
run: docker build --target mariadb_server -t "gradido/mariadb:test" -f ./mariadb/Dockerfile ./

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

@ -4,8 +4,140 @@ 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.20.0](https://github.com/gradido/gradido/compare/1.19.1...1.20.0)
- fix(backend): no await for emails [`#2918`](https://github.com/gradido/gradido/pull/2918)
- fix(frontend): no receiver on send by link [`#2933`](https://github.com/gradido/gradido/pull/2933)
- fix(admin): pagination set currentPage by switch tabs [`#2902`](https://github.com/gradido/gradido/pull/2902)
- fix(federation): correct export of the community url [`#2931`](https://github.com/gradido/gradido/pull/2931)
- fix(frontend): displayed decay duration [`#2927`](https://github.com/gradido/gradido/pull/2927)
- fix(frontend): community tab navigation [`#2928`](https://github.com/gradido/gradido/pull/2928)
- fix(frontend): moderator id missing [`#2925`](https://github.com/gradido/gradido/pull/2925)
- fix(frontend): reset button send coins [`#2924`](https://github.com/gradido/gradido/pull/2924)
- refactor(backend): eslint plugin import export style [`#2908`](https://github.com/gradido/gradido/pull/2908)
- fix(backend): vscode intellisense fixes [`#2919`](https://github.com/gradido/gradido/pull/2919)
- refactor(backend): eslint import-no-cycle enabled [`#2905`](https://github.com/gradido/gradido/pull/2905)
- docs(backend): alias rules and conventions [`#2881`](https://github.com/gradido/gradido/pull/2881)
- refactor(backend): get transaction list [`#2923`](https://github.com/gradido/gradido/pull/2923)
- feat(backend): previous balance in transaction [`#2914`](https://github.com/gradido/gradido/pull/2914)
- refactor(backend): eslint update packages [`#2829`](https://github.com/gradido/gradido/pull/2829)
- feat(frontend): send coins via gradido ID [`#2837`](https://github.com/gradido/gradido/pull/2837)
- refactor(database): cleanup database [`#2808`](https://github.com/gradido/gradido/pull/2808)
- refactor(backend): eslint plugin n + fixes [`#2828`](https://github.com/gradido/gradido/pull/2828)
- feat(frontend): add link to download QR-Code [`#2889`](https://github.com/gradido/gradido/pull/2889)
- fix(other): delete node_modules and /tmp/yarn--* on start.sh [`#2904`](https://github.com/gradido/gradido/pull/2904)
- fix(admin): add confirmation modal for user role change and user (un)deletion [`#2880`](https://github.com/gradido/gradido/pull/2880)
- fix(admin): admin open contribution edit button [`#2811`](https://github.com/gradido/gradido/pull/2811)
- fix(backend): import order [`#2911`](https://github.com/gradido/gradido/pull/2911)
- fix(other): use default values for undefined .env database values [`#2910`](https://github.com/gradido/gradido/pull/2910)
- refactor(backend): eslint plugin import + fixes [`#2827`](https://github.com/gradido/gradido/pull/2827)
- refactor(backend): missing event tests [`#2900`](https://github.com/gradido/gradido/pull/2900)
- refactor(backend): unify event names [`#2799`](https://github.com/gradido/gradido/pull/2799)
- feat(backend): events for users [`#2797`](https://github.com/gradido/gradido/pull/2797)
- fix(backend): subscription get's email and languages from user [`#2885`](https://github.com/gradido/gradido/pull/2885)
- refactor(backend): eslint-plugin-jest + fixes [`#2816`](https://github.com/gradido/gradido/pull/2816)
- fix(frontend): fr change ` and ´ to ' [`#2888`](https://github.com/gradido/gradido/pull/2888)
- feat(backend): events for transaction links [`#2792`](https://github.com/gradido/gradido/pull/2792)
- feat(backend): events for contributions [`#2784`](https://github.com/gradido/gradido/pull/2784)
- feat(backend): events for contribution messages [`#2783`](https://github.com/gradido/gradido/pull/2783)
- refactor(backend): upgrade coverage to 85% [`#2884`](https://github.com/gradido/gradido/pull/2884)
- refactor(backend): add klicktipp-api library [`#2883`](https://github.com/gradido/gradido/pull/2883)
- feat(backend): events for contribution links [`#2780`](https://github.com/gradido/gradido/pull/2780)
- refactor(backend): separate events in own files [`#2777`](https://github.com/gradido/gradido/pull/2777)
- refactor(backend): rename `evenProtocolType` to `eventType` [`#2776`](https://github.com/gradido/gradido/pull/2776)
- refactor(other): add yarn installAll on gradido folder [`#2703`](https://github.com/gradido/gradido/pull/2703)
- docs(federation): describe the technical federation architecture [`#2716`](https://github.com/gradido/gradido/pull/2716)
- refactor(admin): admin list contributions for creation transaction list query [`#2791`](https://github.com/gradido/gradido/pull/2791)
- refactor(workflow): separate workflow with file filter for nginx testing [`#2871`](https://github.com/gradido/gradido/pull/2871)
- feat(backend): read communities data from database [`#2807`](https://github.com/gradido/gradido/pull/2807)
- feat(federation): implement a graphql endpoint to answer getpublickey-request [`#2651`](https://github.com/gradido/gradido/pull/2651)
- refactor(workflow): add file filters to dht node and federation workflows [`#2838`](https://github.com/gradido/gradido/pull/2838)
- refactor(workflow): separate test workflow for end-to-end tests [`#2836`](https://github.com/gradido/gradido/pull/2836)
- refactor(workflow): separate test workflow for frontend [`#2835`](https://github.com/gradido/gradido/pull/2835)
- feat(federation): add federation modul to deployment scripts [`#2733`](https://github.com/gradido/gradido/pull/2733)
- refactor(database): event table [`#2720`](https://github.com/gradido/gradido/pull/2720)
- refactor(workflow): separate test workflow with file change filters for admin interface [`#2734`](https://github.com/gradido/gradido/pull/2734)
- fix(admin): fix translation in menu (english) [`#2814`](https://github.com/gradido/gradido/pull/2814)
- refactor(workflow): configure jest to directly check coverage to schlanken the test workflows [`#2790`](https://github.com/gradido/gradido/pull/2790)
- fix(database): removed commands from package.json not working [`#2805`](https://github.com/gradido/gradido/pull/2805)
- fix(other): repair UserProfile.ChangePassword.feature scenario [`#2782`](https://github.com/gradido/gradido/pull/2782)
- feat(backend): test double redeem transaction links [`#2788`](https://github.com/gradido/gradido/pull/2788)
- feat(backend): admin open creations query [`#2813`](https://github.com/gradido/gradido/pull/2813)
- refactor(backend): eslint-plugin-type-graphql + fixes [`#2745`](https://github.com/gradido/gradido/pull/2745)
#### [1.19.1](https://github.com/gradido/gradido/compare/1.19.0...1.19.1)
> 10 March 2023
- chore(other): upgrade version to 1.19.1 [`#2812`](https://github.com/gradido/gradido/pull/2812)
- 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.20.0",
"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": {
@ -33,6 +33,7 @@
"bootstrap": "4.3.1",
"bootstrap-vue": "^2.21.2",
"core-js": "^3.6.5",
"date-fns": "^2.29.3",
"dotenv-webpack": "^7.0.3",
"express": "^4.17.1",
"graphql": "^15.6.1",

View File

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

View File

@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils'
import ChangeUserRoleFormular from './ChangeUserRoleFormular.vue'
import ChangeUserRoleFormular from './ChangeUserRoleFormular'
import { setUserRole } from '../graphql/setUserRole'
import { toastSuccessSpy, toastErrorSpy } from '../../test/testSetup'
@ -28,6 +28,7 @@ const mocks = {
let propsData
let wrapper
let spy
describe('ChangeUserRoleFormular', () => {
const Wrapper = () => {
@ -70,12 +71,16 @@ describe('ChangeUserRoleFormular', () => {
expect(wrapper.text()).toContain('userRole.notChangeYourSelf')
})
it('has role select disabled', () => {
expect(wrapper.find('select[disabled="disabled"]').exists()).toBe(true)
it('has no role select', () => {
expect(wrapper.find('select.role-select').exists()).toBe(false)
})
it('has no button', () => {
expect(wrapper.find('button.btn.btn-dange').exists()).toBe(false)
})
})
describe('change others role', () => {
describe("change other user's role", () => {
let rolesToSelect
describe('general', () => {
@ -106,19 +111,12 @@ describe('ChangeUserRoleFormular', () => {
expect(wrapper.find('select.role-select[disabled="disabled"]').exists()).toBe(false)
})
describe('on API error', () => {
beforeEach(() => {
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
rolesToSelect.at(1).setSelected()
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Oh no!')
})
it('has "change_user_role" button', () => {
expect(wrapper.find('button.btn.btn-danger').text()).toBe('change_user_role')
})
})
describe('user is usual user', () => {
describe('user has role "usual user"', () => {
beforeEach(() => {
apolloMutateMock.mockResolvedValue({
data: {
@ -141,6 +139,10 @@ describe('ChangeUserRoleFormular', () => {
describe('change select to', () => {
describe('same role', () => {
it('has "change_user_role" button disabled', () => {
expect(wrapper.find('button.btn.btn-danger[disabled="disabled"]').exists()).toBe(true)
})
it('does not call the API', () => {
rolesToSelect.at(0).setSelected()
expect(apolloMutateMock).not.toHaveBeenCalled()
@ -152,39 +154,75 @@ describe('ChangeUserRoleFormular', () => {
rolesToSelect.at(1).setSelected()
})
it('calls the API', () => {
expect(apolloMutateMock).toBeCalledWith(
expect.objectContaining({
mutation: setUserRole,
variables: {
userId: 1,
isAdmin: true,
},
}),
it('has "change_user_role" button enabled', () => {
expect(wrapper.find('button.btn.btn-danger').exists()).toBe(true)
expect(wrapper.find('button.btn.btn-danger[disabled="disabled"]').exists()).toBe(
false,
)
})
it('emits "updateIsAdmin"', () => {
expect(wrapper.emitted('updateIsAdmin')).toEqual(
expect.arrayContaining([
expect.arrayContaining([
{
userId: 1,
isAdmin: expect.any(Date),
},
]),
]),
)
})
describe('clicking the "change_user_role" button', () => {
beforeEach(async () => {
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
spy.mockImplementation(() => Promise.resolve(true))
await wrapper.find('button').trigger('click')
await wrapper.vm.$nextTick()
})
it('toasts success message', () => {
expect(toastSuccessSpy).toBeCalledWith('userRole.successfullyChangedTo')
it('calls the modal', () => {
expect(wrapper.emitted('showModal'))
expect(spy).toHaveBeenCalled()
})
describe('confirm role change with success', () => {
it('calls the API', () => {
expect(apolloMutateMock).toBeCalledWith(
expect.objectContaining({
mutation: setUserRole,
variables: {
userId: 1,
isAdmin: true,
},
}),
)
})
it('emits "updateIsAdmin"', () => {
expect(wrapper.emitted('updateIsAdmin')).toEqual(
expect.arrayContaining([
expect.arrayContaining([
{
userId: 1,
isAdmin: expect.any(Date),
},
]),
]),
)
})
it('toasts success message', () => {
expect(toastSuccessSpy).toBeCalledWith('userRole.successfullyChangedTo')
})
})
describe('confirm role change with error', () => {
beforeEach(async () => {
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
await wrapper.find('button').trigger('click')
await wrapper.vm.$nextTick()
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Oh no!')
})
})
})
})
})
})
describe('user is admin', () => {
describe('user has role "admin"', () => {
beforeEach(() => {
apolloMutateMock.mockResolvedValue({
data: {
@ -207,6 +245,10 @@ describe('ChangeUserRoleFormular', () => {
describe('change select to', () => {
describe('same role', () => {
it('has "change_user_role" button disabled', () => {
expect(wrapper.find('button.btn.btn-danger[disabled="disabled"]').exists()).toBe(true)
})
it('does not call the API', () => {
rolesToSelect.at(1).setSelected()
expect(apolloMutateMock).not.toHaveBeenCalled()
@ -218,33 +260,69 @@ describe('ChangeUserRoleFormular', () => {
rolesToSelect.at(0).setSelected()
})
it('calls the API', () => {
expect(apolloMutateMock).toBeCalledWith(
expect.objectContaining({
mutation: setUserRole,
variables: {
userId: 1,
isAdmin: false,
},
}),
it('has "change_user_role" button enabled', () => {
expect(wrapper.find('button.btn.btn-danger').exists()).toBe(true)
expect(wrapper.find('button.btn.btn-danger[disabled="disabled"]').exists()).toBe(
false,
)
})
it('emits "updateIsAdmin"', () => {
expect(wrapper.emitted('updateIsAdmin')).toEqual(
expect.arrayContaining([
expect.arrayContaining([
{
userId: 1,
isAdmin: null,
},
]),
]),
)
})
describe('clicking the "change_user_role" button', () => {
beforeEach(async () => {
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
spy.mockImplementation(() => Promise.resolve(true))
await wrapper.find('button').trigger('click')
await wrapper.vm.$nextTick()
})
it('toasts success message', () => {
expect(toastSuccessSpy).toBeCalledWith('userRole.successfullyChangedTo')
it('calls the modal', () => {
expect(wrapper.emitted('showModal'))
expect(spy).toHaveBeenCalled()
})
describe('confirm role change with success', () => {
it('calls the API', () => {
expect(apolloMutateMock).toBeCalledWith(
expect.objectContaining({
mutation: setUserRole,
variables: {
userId: 1,
isAdmin: false,
},
}),
)
})
it('emits "updateIsAdmin"', () => {
expect(wrapper.emitted('updateIsAdmin')).toEqual(
expect.arrayContaining([
expect.arrayContaining([
{
userId: 1,
isAdmin: null,
},
]),
]),
)
})
it('toasts success message', () => {
expect(toastSuccessSpy).toBeCalledWith('userRole.successfullyChangedTo')
})
})
describe('confirm role change with error', () => {
beforeEach(async () => {
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
await wrapper.find('button').trigger('click')
await wrapper.vm.$nextTick()
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Oh no!')
})
})
})
})
})

View File

@ -4,19 +4,23 @@
<div v-if="item.userId === $store.state.moderator.id" class="m-3 mb-4">
{{ $t('userRole.notChangeYourSelf') }}
</div>
<div class="m-3">
<div v-else class="m-3">
<label for="role" class="mr-3">{{ $t('userRole.selectLabel') }}</label>
<b-form-select
class="role-select"
v-model="roleSelected"
:options="roles"
:disabled="item.userId === $store.state.moderator.id"
/>
<b-form-select class="role-select" v-model="roleSelected" :options="roles" />
<div class="mt-3 mb-5">
<b-button
variant="danger"
v-b-modal.user-role-modal
:disabled="currentRole === roleSelected"
@click="showModal()"
>
{{ $t('change_user_role') }}
</b-button>
</div>
</div>
</div>
</div>
</template>
<script>
import { setUserRole } from '../graphql/setUserRole'
@ -35,6 +39,7 @@ export default {
},
data() {
return {
currentRole: this.item.isAdmin ? rolesValues.admin : rolesValues.user,
roleSelected: this.item.isAdmin ? rolesValues.admin : rolesValues.user,
roles: [
{ value: rolesValues.user, text: this.$t('userRole.selectRoles.user') },
@ -42,14 +47,35 @@ export default {
],
}
},
watch: {
roleSelected(newRole, oldRole) {
if (newRole !== oldRole) {
this.setUserRole(newRole, oldRole)
}
},
},
methods: {
showModal() {
this.$bvModal
.msgBoxConfirm(
this.$t('overlay.changeUserRole.question', {
username: `${this.item.firstName} ${this.item.lastName}`,
newRole:
this.roleSelected === 'admin'
? this.$t('userRole.selectRoles.admin')
: this.$t('userRole.selectRoles.user'),
}),
{
cancelTitle: this.$t('overlay.cancel'),
centered: true,
hideHeaderClose: true,
title: this.$t('overlay.changeUserRole.title'),
okTitle: this.$t('overlay.changeUserRole.yes'),
okVariant: 'danger',
},
)
.then((okClicked) => {
if (okClicked) {
this.setUserRole(this.roleSelected, this.currentRole)
}
})
.catch((error) => {
this.toastError(error.message)
})
},
setUserRole(newRole, oldRole) {
this.$apollo
.mutate({

View File

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

View File

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

View File

@ -43,8 +43,8 @@
</div>
</template>
<script>
import ContributionLinkForm from '../ContributionLink/ContributionLinkForm.vue'
import ContributionLinkList from '../ContributionLink/ContributionLinkList.vue'
import ContributionLinkForm from '../ContributionLink/ContributionLinkForm'
import ContributionLinkList from '../ContributionLink/ContributionLinkList'
export default {
name: 'ContributionLink',

View File

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

View File

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

View File

@ -46,7 +46,7 @@
</template>
<script>
import { deleteContributionLink } from '@/graphql/deleteContributionLink.js'
import FigureQrCode from '../FigureQrCode.vue'
import FigureQrCode from '../FigureQrCode'
export default {
name: 'ContributionLinkList',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,16 +1,19 @@
import { mount } from '@vue/test-utils'
import CreationFormular from './CreationFormular.vue'
import CreationFormular from './CreationFormular'
import { adminCreateContribution } from '../graphql/adminCreateContribution'
import { adminCreateContributions } from '../graphql/adminCreateContributions'
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
import VueApollo from 'vue-apollo'
import { createMockClient } from 'mock-apollo-client'
import { adminOpenCreations } from '../graphql/adminOpenCreations'
const mockClient = createMockClient()
const apolloProvider = new VueApollo({
defaultClient: mockClient,
})
const localVue = global.localVue
localVue.use(VueApollo)
const apolloMutateMock = jest.fn().mockResolvedValue({
data: {
adminCreateContribution: [0, 0, 0],
},
})
const stateCommitMock = jest.fn()
const mocks = {
@ -19,9 +22,6 @@ const mocks = {
const date = new Date(d)
return date.toISOString().split('T')[0]
}),
$apollo: {
mutate: apolloMutateMock,
},
$store: {
commit: stateCommitMock,
},
@ -32,7 +32,8 @@ const propsData = {
creation: [],
}
const now = new Date(Date.now())
const now = new Date()
const getCreationDate = (sub) => {
const date = sub === 0 ? now : new Date(now.getFullYear(), now.getMonth() - sub, 1, 0)
return date.toISOString().split('T')[0]
@ -41,8 +42,43 @@ const getCreationDate = (sub) => {
describe('CreationFormular', () => {
let wrapper
const adminOpenCreationsMock = jest.fn()
const adminCreateContributionMock = jest.fn()
mockClient.setRequestHandler(
adminOpenCreations,
adminOpenCreationsMock.mockResolvedValue({
data: {
adminOpenCreations: [
{
month: new Date(now.getFullYear(), now.getMonth() - 2).getMonth(),
year: new Date(now.getFullYear(), now.getMonth() - 2).getFullYear(),
amount: '200',
},
{
month: new Date(now.getFullYear(), now.getMonth() - 1).getMonth(),
year: new Date(now.getFullYear(), now.getMonth() - 1).getFullYear(),
amount: '400',
},
{
month: now.getMonth(),
year: now.getFullYear(),
amount: '600',
},
],
},
}),
)
mockClient.setRequestHandler(
adminCreateContribution,
adminCreateContributionMock.mockResolvedValue({
data: {
adminCreateContribution: [0, 0, 0],
},
}),
)
const Wrapper = () => {
return mount(CreationFormular, { localVue, mocks, propsData })
return mount(CreationFormular, { localVue, mocks, propsData, apolloProvider })
}
describe('mount', () => {
@ -108,17 +144,12 @@ describe('CreationFormular', () => {
})
it('sends ... to apollo', () => {
expect(apolloMutateMock).toBeCalledWith(
expect.objectContaining({
mutation: adminCreateContribution,
variables: {
email: 'benjamin@bluemchen.de',
creationDate: getCreationDate(2),
amount: 90,
memo: 'Test create coins',
},
}),
)
expect(adminCreateContributionMock).toBeCalledWith({
email: 'benjamin@bluemchen.de',
creationDate: getCreationDate(2),
amount: 90,
memo: 'Test create coins',
})
})
it('emits update-user-data', () => {
@ -145,7 +176,7 @@ describe('CreationFormular', () => {
describe('sendForm with server error', () => {
beforeEach(async () => {
apolloMutateMock.mockRejectedValueOnce({ message: 'Ouch!' })
adminCreateContributionMock.mockRejectedValueOnce({ message: 'Ouch!' })
await wrapper.find('.test-submit').trigger('click')
})
@ -213,7 +244,7 @@ describe('CreationFormular', () => {
})
it('sends ... to apollo', () => {
expect(apolloMutateMock).toBeCalled()
expect(adminCreateContributionMock).toBeCalled()
})
})
@ -276,7 +307,7 @@ describe('CreationFormular', () => {
})
it('sends mutation to apollo', () => {
expect(apolloMutateMock).toBeCalled()
expect(adminCreateContributionMock).toBeCalled()
})
it('toast success message', () => {
@ -328,122 +359,6 @@ describe('CreationFormular', () => {
})
})
})
describe('mass creation with success', () => {
beforeEach(async () => {
jest.clearAllMocks()
apolloMutateMock.mockResolvedValue({
data: {
adminCreateContributions: {
success: true,
successfulContribution: ['bob@baumeister.de', 'bibi@bloxberg.de'],
failedContribution: [],
},
},
})
await wrapper.setProps({
type: 'massCreation',
creation: [200, 400, 600],
items: [{ email: 'bob@baumeister.de' }, { email: 'bibi@bloxberg.de' }],
})
await wrapper.findAll('input[type="radio"]').at(1).setChecked()
await wrapper.find('textarea').setValue('Test mass create coins')
await wrapper.find('input[type="number"]').setValue(200)
await wrapper.find('.test-submit').trigger('click')
})
it('calls the API', () => {
expect(apolloMutateMock).toBeCalledWith(
expect.objectContaining({
mutation: adminCreateContributions,
variables: {
pendingCreations: [
{
email: 'bob@baumeister.de',
creationDate: getCreationDate(1),
amount: 200,
memo: 'Test mass create coins',
},
{
email: 'bibi@bloxberg.de',
creationDate: getCreationDate(1),
amount: 200,
memo: 'Test mass create coins',
},
],
},
}),
)
})
it('updates open creations in store', () => {
expect(stateCommitMock).toBeCalledWith('openCreationsPlus', 2)
})
it('emits remove-all-bookmark', () => {
expect(wrapper.emitted('remove-all-bookmark')).toBeTruthy()
})
})
describe('mass creation with success but all failed', () => {
beforeEach(async () => {
jest.clearAllMocks()
apolloMutateMock.mockResolvedValue({
data: {
adminCreateContributions: {
success: true,
successfulContribution: [],
failedContribution: ['bob@baumeister.de', 'bibi@bloxberg.de'],
},
},
})
await wrapper.setProps({
type: 'massCreation',
creation: [200, 400, 600],
items: [{ email: 'bob@baumeister.de' }, { email: 'bibi@bloxberg.de' }],
})
await wrapper.findAll('input[type="radio"]').at(1).setChecked()
await wrapper.find('textarea').setValue('Test mass create coins')
await wrapper.find('input[type="number"]').setValue(200)
await wrapper.find('.test-submit').trigger('click')
})
it('updates open creations in store', () => {
expect(stateCommitMock).toBeCalledWith('openCreationsPlus', 0)
})
it('emits remove all bookmarks', () => {
expect(wrapper.emitted('remove-all-bookmark')).toBeTruthy()
})
it('emits toast failed creations with two emails', () => {
expect(wrapper.emitted('toast-failed-creations')).toEqual([
[['bob@baumeister.de', 'bibi@bloxberg.de']],
])
})
})
describe('mass creation with error', () => {
beforeEach(async () => {
jest.clearAllMocks()
apolloMutateMock.mockRejectedValue({
message: 'Oh no!',
})
await wrapper.setProps({
type: 'massCreation',
creation: [200, 400, 600],
items: [{ email: 'bob@baumeister.de' }, { email: 'bibi@bloxberg.de' }],
})
await wrapper.findAll('input[type="radio"]').at(1).setChecked()
await wrapper.find('textarea').setValue('Test mass create coins')
await wrapper.find('input[type="number"]').setValue(200)
await wrapper.find('.test-submit').trigger('click')
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Oh no!')
})
})
})
})
})

View File

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

View File

@ -1,43 +1,75 @@
import { mount } from '@vue/test-utils'
import CreationTransactionList from './CreationTransactionList.vue'
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,5 +1,5 @@
import { mount } from '@vue/test-utils'
import DeletedUserFormular from './DeletedUserFormular.vue'
import DeletedUserFormular from './DeletedUserFormular'
import { deleteUser } from '../graphql/deleteUser'
import { unDeleteUser } from '../graphql/unDeleteUser'
import { toastErrorSpy } from '../../test/testSetup'
@ -35,6 +35,7 @@ const propsData = {
describe('DeletedUserFormular', () => {
let wrapper
let spy
const Wrapper = () => {
return mount(DeletedUserFormular, { localVue, mocks, propsData })
@ -62,6 +63,10 @@ describe('DeletedUserFormular', () => {
it('shows a text that you cannot delete yourself', () => {
expect(wrapper.text()).toBe('removeNotSelf')
})
it('has no "delete_user" button', () => {
expect(wrapper.find('button').exists()).toBe(false)
})
})
describe('delete other user', () => {
@ -71,35 +76,32 @@ describe('DeletedUserFormular', () => {
userId: 1,
deletedAt: null,
},
static: true,
})
})
it('has a checkbox', () => {
expect(wrapper.find('input[type="checkbox"]').exists()).toBe(true)
})
it('shows the text "delete_user"', () => {
expect(wrapper.text()).toBe('delete_user')
})
describe('click on checkbox', () => {
it('has a "delete_user" button', () => {
expect(wrapper.find('button').text()).toBe('delete_user')
})
describe('click on "delete_user" button', () => {
beforeEach(async () => {
await wrapper.find('input[type="checkbox"]').setChecked()
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
spy.mockImplementation(() => Promise.resolve(true))
await wrapper.find('button').trigger('click')
await wrapper.vm.$nextTick()
})
it('has a confirmation button', () => {
expect(wrapper.find('button').exists()).toBe(true)
})
it('has the button text "delete_user"', () => {
expect(wrapper.find('button').text()).toBe('delete_user')
it('calls the modal', () => {
expect(wrapper.emitted('showDeleteModal'))
expect(spy).toHaveBeenCalled()
})
describe('confirm delete with success', () => {
beforeEach(async () => {
await wrapper.find('button').trigger('click')
})
it('calls the API', () => {
expect(apolloMutateMock).toBeCalledWith(
expect.objectContaining({
@ -123,32 +125,20 @@ describe('DeletedUserFormular', () => {
]),
)
})
it('unchecks the checkbox', () => {
expect(wrapper.find('input').attributes('checked')).toBe(undefined)
})
})
describe('confirm delete with error', () => {
beforeEach(async () => {
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
await wrapper.find('button').trigger('click')
await wrapper.vm.$nextTick()
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Oh no!')
})
})
describe('click on checkbox again', () => {
beforeEach(async () => {
await wrapper.find('input[type="checkbox"]').setChecked(false)
})
it('has no confirmation button anymore', () => {
expect(wrapper.find('button').exists()).toBe(false)
})
})
})
})
@ -162,37 +152,33 @@ describe('DeletedUserFormular', () => {
})
})
it('has a checkbox', () => {
expect(wrapper.find('input[type="checkbox"]').exists()).toBe(true)
})
it('shows the text "undelete_user"', () => {
expect(wrapper.text()).toBe('undelete_user')
})
describe('click on checkbox', () => {
it('has a "undelete_user" button', () => {
expect(wrapper.find('button').text()).toBe('undelete_user')
})
describe('click on "undelete_user" button', () => {
beforeEach(async () => {
apolloMutateMock.mockResolvedValue({
data: {
unDeleteUser: null,
},
})
await wrapper.find('input[type="checkbox"]').setChecked()
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
spy.mockImplementation(() => Promise.resolve(true))
await wrapper.find('button').trigger('click')
await wrapper.vm.$nextTick()
})
it('has a confirmation button', () => {
expect(wrapper.find('button').exists()).toBe(true)
})
it('has the button text "undelete_user"', () => {
expect(wrapper.find('button').text()).toBe('undelete_user')
it('calls the modal', () => {
expect(wrapper.emitted('showUndeleteModal'))
expect(spy).toHaveBeenCalled()
})
describe('confirm recover with success', () => {
beforeEach(async () => {
await wrapper.find('button').trigger('click')
})
it('calls the API', () => {
expect(apolloMutateMock).toBeCalledWith(
expect.objectContaining({
@ -205,7 +191,7 @@ describe('DeletedUserFormular', () => {
})
it('emits update deleted At', () => {
expect(wrapper.emitted('updateDeletedAt')).toEqual(
expect(wrapper.emitted('updateDeletedAt')).toMatchObject(
expect.arrayContaining([
expect.arrayContaining([
{
@ -216,10 +202,6 @@ describe('DeletedUserFormular', () => {
]),
)
})
it('unchecks the checkbox', () => {
expect(wrapper.find('input').attributes('checked')).toBe(undefined)
})
})
describe('confirm recover with error', () => {
@ -232,16 +214,6 @@ describe('DeletedUserFormular', () => {
expect(toastErrorSpy).toBeCalledWith('Oh no!')
})
})
describe('click on checkbox again', () => {
beforeEach(async () => {
await wrapper.find('input[type="checkbox"]').setChecked(false)
})
it('has no confirmation button anymore', () => {
expect(wrapper.find('button').exists()).toBe(false)
})
})
})
})
})

View File

@ -4,15 +4,16 @@
{{ $t('removeNotSelf') }}
</div>
<div v-else class="mt-5">
<b-form-checkbox switch size="lg" v-model="checked">
<div>{{ item.deletedAt ? $t('undelete_user') : $t('delete_user') }}</div>
</b-form-checkbox>
<div class="mt-3 mb-5">
<b-button v-if="checked && item.deletedAt === null" variant="danger" @click="deleteUser">
<b-button
v-if="!item.deletedAt"
variant="danger"
v-b-modal.delete-user-modal
@click="showDeleteModal()"
>
{{ $t('delete_user') }}
</b-button>
<b-button v-if="checked && item.deletedAt !== null" variant="success" @click="unDeleteUser">
<b-button v-else variant="success" v-b-modal.delete-user-modal @click="showUndeleteModal()">
{{ $t('undelete_user') }}
</b-button>
</div>
@ -31,12 +32,56 @@ export default {
required: true,
},
},
data() {
return {
checked: false,
}
},
methods: {
showDeleteModal() {
this.$bvModal
.msgBoxConfirm(
this.$t('overlay.deleteUser.question', {
username: `${this.item.firstName} ${this.item.lastName}`,
}),
{
cancelTitle: this.$t('overlay.cancel'),
centered: true,
hideHeaderClose: true,
title: this.$t('overlay.deleteUser.title'),
okTitle: this.$t('overlay.deleteUser.yes'),
okVariant: 'danger',
static: true,
},
)
.then((okClicked) => {
if (okClicked) {
this.deleteUser()
}
})
.catch((error) => {
this.toastError(error.message)
})
},
showUndeleteModal() {
this.$bvModal
.msgBoxConfirm(
this.$t('overlay.undeleteUser.question', {
username: `${this.item.firstName} ${this.item.lastName}`,
}),
{
cancelTitle: this.$t('overlay.cancel'),
centered: true,
hideHeaderClose: true,
title: this.$t('overlay.undeleteUser.title'),
okTitle: this.$t('overlay.undeleteUser.yes'),
okVariant: 'success',
},
)
.then((okClicked) => {
if (okClicked) {
this.unDeleteUser()
}
})
.catch((error) => {
this.toastError(error.message)
})
},
deleteUser() {
this.$apollo
.mutate({
@ -50,7 +95,6 @@ export default {
userId: this.item.userId,
deletedAt: result.data.deleteUser,
})
this.checked = false
})
.catch((error) => {
this.toastError(error.message)
@ -69,7 +113,6 @@ export default {
userId: this.item.userId,
deletedAt: result.data.unDeleteUser,
})
this.checked = false
})
.catch((error) => {
this.toastError(error.message)

View File

@ -1,19 +1,18 @@
import { mount } from '@vue/test-utils'
import EditCreationFormular from './EditCreationFormular.vue'
import EditCreationFormular from './EditCreationFormular'
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
import VueApollo from 'vue-apollo'
import { createMockClient } from 'mock-apollo-client'
import { adminOpenCreations } from '../graphql/adminOpenCreations'
import { adminUpdateContribution } from '../graphql/adminUpdateContribution'
const mockClient = createMockClient()
const apolloProvider = new VueApollo({
defaultClient: mockClient,
})
const localVue = global.localVue
const apolloMutateMock = jest.fn().mockResolvedValue({
data: {
adminUpdateContribution: {
creation: [0, 0, 0],
amount: 500,
date: new Date(),
memo: 'Test Schöpfung 2',
},
},
})
localVue.use(VueApollo)
const stateCommitMock = jest.fn()
@ -23,22 +22,18 @@ const mocks = {
const date = new Date(d)
return date.toISOString().split('T')[0]
}),
$apollo: {
mutate: apolloMutateMock,
},
$store: {
commit: stateCommitMock,
},
}
const now = new Date(Date.now())
const now = new Date()
const getCreationDate = (sub) => {
const date = sub === 0 ? now : new Date(now.getFullYear(), now.getMonth() - sub, 1, 0)
return date.toISOString().split('T')[0]
}
const propsData = {
creation: [200, 400, 600],
creationUserData: {
memo: 'Test schöpfung 1',
amount: 100,
@ -46,20 +41,65 @@ const propsData = {
},
item: {
id: 0,
email: 'bob@baumeister.de',
amount: '300',
contributionDate: `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}`,
},
}
const data = () => {
return { creation: ['1000', '1000', '400'] }
}
describe('EditCreationFormular', () => {
let wrapper
const adminUpdateContributionMock = jest.fn()
const adminOpenCreationsMock = jest.fn()
mockClient.setRequestHandler(
adminOpenCreations,
adminOpenCreationsMock.mockResolvedValue({
data: {
adminOpenCreations: [
{
month: new Date(now.getFullYear(), now.getMonth() - 2).getMonth(),
year: new Date(now.getFullYear(), now.getMonth() - 2).getFullYear(),
amount: '1000',
},
{
month: new Date(now.getFullYear(), now.getMonth() - 1).getMonth(),
year: new Date(now.getFullYear(), now.getMonth() - 1).getFullYear(),
amount: '1000',
},
{
month: now.getMonth(),
year: now.getFullYear(),
amount: '400',
},
],
},
}),
)
mockClient.setRequestHandler(
adminUpdateContribution,
adminUpdateContributionMock.mockResolvedValue({
data: {
adminUpdateContribution: {
amount: '600',
date: new Date(),
memo: 'This is my memo',
},
},
}),
)
const Wrapper = () => {
return mount(EditCreationFormular, { localVue, mocks, propsData })
return mount(EditCreationFormular, { localVue, mocks, propsData, data, apolloProvider })
}
describe('mount', () => {
beforeEach(() => {
beforeEach(async () => {
wrapper = Wrapper()
await wrapper.vm.$nextTick()
})
it('has a DIV element with the class.component-edit-creation-formular', () => {
@ -89,42 +129,16 @@ describe('EditCreationFormular', () => {
})
it('calls the API', () => {
expect(apolloMutateMock).toBeCalledWith(
expect.objectContaining({
variables: {
id: 0,
email: 'bob@baumeister.de',
creationDate: getCreationDate(0),
amount: 500,
memo: 'Test Schöpfung 2',
},
}),
)
})
it('emits update-user-data', () => {
expect(wrapper.emitted('update-user-data')).toEqual([
[
{
id: 0,
email: 'bob@baumeister.de',
},
[0, 0, 0],
],
])
expect(adminUpdateContributionMock).toBeCalledWith({
id: 0,
creationDate: getCreationDate(0),
amount: 500,
memo: 'Test Schöpfung 2',
})
})
it('emits update-creation-data', () => {
expect(wrapper.emitted('update-creation-data')).toEqual([
[
{
amount: 500,
date: expect.any(Date),
memo: 'Test Schöpfung 2',
row: expect.any(Object),
},
],
])
expect(wrapper.emitted('update-creation-data')).toBeTruthy()
})
it('toasts a success message', () => {
@ -134,7 +148,7 @@ describe('EditCreationFormular', () => {
describe('change and save memo and value with error', () => {
beforeEach(async () => {
apolloMutateMock.mockRejectedValue({ message: 'Oh no!' })
adminUpdateContributionMock.mockRejectedValue({ message: 'Oh no!' })
await wrapper.find('input[type="number"]').setValue(500)
await wrapper.find('textarea').setValue('Test Schöpfung 2')
await wrapper.find('.test-submit').trigger('click')

View File

@ -96,18 +96,14 @@ export default {
type: Object,
required: true,
},
creation: {
type: Array,
required: true,
},
},
data() {
return {
text: !this.creationUserData.memo ? '' : this.creationUserData.memo,
value: !this.creationUserData.amount ? 0 : Number(this.creationUserData.amount),
rangeMin: 0,
rangeMax: 1000,
selected: '',
selected: this.selectedComputed,
userId: this.item.userId,
}
},
methods: {
@ -117,20 +113,13 @@ export default {
mutation: adminUpdateContribution,
variables: {
id: this.item.id,
email: this.item.email,
creationDate: this.selected.date,
amount: Number(this.value),
memo: this.text,
},
})
.then((result) => {
this.$emit('update-user-data', this.item, result.data.adminUpdateContribution.creation)
this.$emit('update-creation-data', {
amount: Number(result.data.adminUpdateContribution.amount),
date: result.data.adminUpdateContribution.date,
memo: result.data.adminUpdateContribution.memo,
row: this.row,
})
this.$emit('update-creation-data')
this.toastSuccess(
this.$t('creation_form.toasted_update', {
value: this.value,
@ -149,15 +138,29 @@ export default {
// Den geschöpften Wert auf o setzen
this.value = 0
})
.finally(() => {
this.$apollo.queries.OpenCreations.refetch()
})
},
},
created() {
if (this.creationUserData.date) {
const month = this.$d(new Date(this.creationUserData.date), 'month')
const index = this.radioOptions.findIndex((obj) => obj.item.short === month)
this.selected = this.radioOptions[index].item
this.rangeMax = Number(this.creation[index]) + Number(this.creationUserData.amount)
}
computed: {
creationIndex() {
const month = this.$d(new Date(this.item.contributionDate), 'month')
return this.radioOptions.findIndex((obj) => {
return obj.item.short === month
})
},
selectedComputed() {
return this.radioOptions[this.creationIndex].item
},
rangeMax() {
return Number(this.creation[this.creationIndex]) + Number(this.item.amount)
},
},
watch: {
selectedComputed() {
this.selected = this.selectedComputed
},
},
}
</script>

View File

@ -0,0 +1,183 @@
import { mount } from '@vue/test-utils'
import FederationVisualizeItem from './FederationVisualizeItem.vue'
const localVue = global.localVue
const today = new Date()
const createdDate = new Date()
createdDate.setDate(createdDate.getDate() - 3)
let propsData = {
item: {
id: 7590,
foreign: false,
publicKey: 'eaf6a426b24fd54f8fbae11c17700fc595080ca25159579c63d38dbc64284ba7',
url: 'http://localhost/api/2_0',
lastAnnouncedAt: createdDate,
verifiedAt: today,
lastErrorAt: null,
createdAt: createdDate,
updatedAt: null,
},
}
const mocks = {
$i18n: {
locale: 'en',
},
}
describe('FederationVisualizeItem', () => {
let wrapper
const Wrapper = () => {
return mount(FederationVisualizeItem, { localVue, mocks, propsData })
}
describe('mount', () => {
beforeEach(() => {
wrapper = Wrapper()
})
it('renders the component', () => {
expect(wrapper.find('div.federation-visualize-item').exists()).toBe(true)
})
describe('rendering item properties', () => {
it('has the url', () => {
expect(wrapper.find('.row > div:nth-child(2) > div').text()).toBe(
'http://localhost/api/2_0',
)
})
it('has the public key', () => {
expect(wrapper.find('.row > div:nth-child(2) > small').text()).toContain(
'eaf6a426b24fd54f8fbae11c17700fc595080ca25159579c63d38dbc64284ba7'.substring(0, 26),
)
})
describe('verified item', () => {
it('has the check icon', () => {
expect(wrapper.find('svg.bi-check').exists()).toBe(true)
})
it('has the text variant "success"', () => {
expect(wrapper.find('.text-success').exists()).toBe(true)
})
})
describe('not verified item', () => {
beforeEach(() => {
propsData = {
item: {
id: 7590,
foreign: false,
publicKey: 'eaf6a426b24fd54f8fbae11c17700fc595080ca25159579c63d38dbc64284ba7',
url: 'http://localhost/api/2_0',
lastAnnouncedAt: createdDate,
verifiedAt: null,
lastErrorAt: null,
createdAt: createdDate,
updatedAt: null,
},
}
wrapper = Wrapper()
})
it('has the x-circle icon', () => {
expect(wrapper.find('svg.bi-x-circle').exists()).toBe(true)
})
it('has the text variant "danger"', () => {
expect(wrapper.find('.text-danger').exists()).toBe(true)
})
})
// describe('with different locales (de, en, fr, es, nl)', () => {
describe('lastAnnouncedAt', () => {
it('computes the time string for different locales (de, en, fr, es, nl)', () => {
wrapper.vm.$i18n.locale = 'de'
wrapper = Wrapper()
expect(wrapper.vm.lastAnnouncedAt).toBe('vor 3 Tagen')
wrapper.vm.$i18n.locale = 'fr'
wrapper = Wrapper()
expect(wrapper.vm.lastAnnouncedAt).toBe('il y a 3 jours')
wrapper.vm.$i18n.locale = 'es'
wrapper = Wrapper()
expect(wrapper.vm.lastAnnouncedAt).toBe('hace 3 días')
wrapper.vm.$i18n.locale = 'nl'
wrapper = Wrapper()
expect(wrapper.vm.lastAnnouncedAt).toBe('3 dagen geleden')
})
describe('lastAnnouncedAt == null', () => {
beforeEach(() => {
propsData = {
item: {
id: 7590,
foreign: false,
publicKey: 'eaf6a426b24fd54f8fbae11c17700fc595080ca25159579c63d38dbc64284ba7',
url: 'http://localhost/api/2_0',
lastAnnouncedAt: null,
verifiedAt: null,
lastErrorAt: null,
createdAt: createdDate,
updatedAt: null,
},
}
wrapper = Wrapper()
})
it('computes empty string', async () => {
expect(wrapper.vm.lastAnnouncedAt).toBe('')
})
})
})
describe('createdAt', () => {
it('computes the time string for different locales (de, en, fr, es, nl)', () => {
wrapper.vm.$i18n.locale = 'de'
wrapper = Wrapper()
expect(wrapper.vm.createdAt).toBe('vor 3 Tagen')
wrapper.vm.$i18n.locale = 'fr'
wrapper = Wrapper()
expect(wrapper.vm.createdAt).toBe('il y a 3 jours')
wrapper.vm.$i18n.locale = 'es'
wrapper = Wrapper()
expect(wrapper.vm.createdAt).toBe('hace 3 días')
wrapper.vm.$i18n.locale = 'nl'
wrapper = Wrapper()
expect(wrapper.vm.createdAt).toBe('3 dagen geleden')
})
describe('createdAt == null', () => {
beforeEach(() => {
propsData = {
item: {
id: 7590,
foreign: false,
publicKey: 'eaf6a426b24fd54f8fbae11c17700fc595080ca25159579c63d38dbc64284ba7',
url: 'http://localhost/api/2_0',
lastAnnouncedAt: createdDate,
verifiedAt: null,
lastErrorAt: null,
createdAt: null,
updatedAt: null,
},
}
wrapper = Wrapper()
})
it('computes empty string', async () => {
expect(wrapper.vm.createdAt).toBe('')
})
})
})
})
})
})

View File

@ -0,0 +1,63 @@
<template>
<div class="federation-visualize-item">
<b-row>
<b-col cols="1"><b-icon :icon="icon" :variant="variant" class="mr-4"></b-icon></b-col>
<b-col>
<div>{{ item.url }}</div>
<small>{{ `${item.publicKey.substring(0, 26)}` }}</small>
</b-col>
<b-col cols="2">{{ lastAnnouncedAt }}</b-col>
<b-col cols="2">{{ createdAt }}</b-col>
</b-row>
</div>
</template>
<script>
import { formatDistanceToNow } from 'date-fns'
import { de, en, fr, es, nl } from 'date-fns/locale'
const locales = { en, de, es, fr, nl }
export default {
name: 'FederationVisualizeItem',
props: {
item: { type: Object },
},
data() {
return {
formatDistanceToNow,
locale: this.$i18n.locale,
}
},
computed: {
verified() {
return new Date(this.item.verifiedAt) >= new Date(this.item.lastAnnouncedAt)
},
icon() {
return this.verified ? 'check' : 'x-circle'
},
variant() {
return this.verified ? 'success' : 'danger'
},
lastAnnouncedAt() {
if (this.item.lastAnnouncedAt) {
return formatDistanceToNow(new Date(this.item.lastAnnouncedAt), {
includeSecond: true,
addSuffix: true,
locale: locales[this.locale],
})
}
return ''
},
createdAt() {
if (this.item.createdAt) {
return formatDistanceToNow(new Date(this.item.createdAt), {
includeSecond: true,
addSuffix: true,
locale: locales[this.locale],
})
}
return ''
},
},
}
</script>

View File

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

View File

@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils'
import NavBar from './NavBar.vue'
import NavBar from './NavBar'
const localVue = global.localVue
@ -62,19 +62,20 @@ describe('NavBar', () => {
)
})
it('has a link to /federation', () => {
expect(wrapper.findAll('.nav-item').at(3).find('a').attributes('href')).toBe('/federation')
})
it('has a link to /statistic', () => {
expect(wrapper.findAll('.nav-item').at(3).find('a').attributes('href')).toBe('/statistic')
expect(wrapper.findAll('.nav-item').at(4).find('a').attributes('href')).toBe('/statistic')
})
})
describe('wallet', () => {
const windowLocationMock = jest.fn()
const windowLocation = window.location
beforeEach(async () => {
delete window.location
window.location = {
assign: windowLocationMock,
}
window.location = ''
await wrapper.findAll('.nav-item').at(5).find('a').trigger('click')
})
@ -83,8 +84,8 @@ describe('NavBar', () => {
window.location = windowLocation
})
it.skip('changes window location to wallet', () => {
expect(windowLocationMock()).toBe('valid-token')
it('changes window location to wallet', () => {
expect(window.location).toBe('http://localhost/authenticate?token=valid-token')
})
it('dispatches logout to store', () => {
@ -100,7 +101,7 @@ describe('NavBar', () => {
window.location = {
assign: windowLocationMock,
}
await wrapper.findAll('.nav-item').at(5).find('a').trigger('click')
await wrapper.findAll('.nav-item').at(6).find('a').trigger('click')
})
afterEach(() => {

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="lg" type="dark" class="bg-dark">
<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 }}
@ -19,6 +19,9 @@
<b-nav-item to="/contribution-links">
{{ $t('navbar.automaticContributions') }}
</b-nav-item>
<b-nav-item to="/federation">
{{ $t('navbar.instances') }}
</b-nav-item>
<b-nav-item to="/statistic">{{ $t('navbar.statistic') }}</b-nav-item>
<b-nav-item @click="wallet">{{ $t('navbar.my-account') }}</b-nav-item>
<b-nav-item @click="logout">{{ $t('navbar.logout') }}</b-nav-item>
@ -52,6 +55,5 @@ export default {
<style>
.navbar-brand-img {
height: 2rem;
padding-left: 10px;
}
</style>

View File

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

View File

@ -1,11 +1,10 @@
import { mount } from '@vue/test-utils'
import OpenCreationsTable from './OpenCreationsTable.vue'
import OpenCreationsTable from './OpenCreationsTable'
const localVue = global.localVue
const apolloMutateMock = jest.fn().mockResolvedValue({})
const apolloQueryMock = jest.fn().mockResolvedValue({})
const toggleDetailsMock = jest.fn()
const propsData = {
items: [
@ -17,7 +16,7 @@ const propsData = {
amount: 300,
memo: 'Aktives Grundeinkommen für Januar 2022',
date: '2022-01-01T00:00:00.000Z',
moderator: 1,
moderatorId: 1,
creation: [700, 1000, 1000],
__typename: 'PendingCreation',
},
@ -29,7 +28,7 @@ const propsData = {
amount: 210,
memo: 'Aktives Grundeinkommen für Januar 2022',
date: '2022-01-01T00:00:00.000Z',
moderator: null,
moderatorId: null,
creation: [790, 1000, 1000],
__typename: 'PendingCreation',
},
@ -41,7 +40,7 @@ const propsData = {
amount: 330,
memo: 'Aktives Grundeinkommen für Januar 2022',
date: '2022-01-01T00:00:00.000Z',
moderator: 1,
moderatorId: 1,
creation: [670, 1000, 1000],
__typename: 'PendingCreation',
},
@ -83,7 +82,7 @@ const mocks = {
$store: {
state: {
moderator: {
id: 0,
id: 1,
name: 'test moderator',
},
},
@ -132,14 +131,6 @@ describe('OpenCreationsTable', () => {
})
})
describe('call updateUserData', () => {
it('user creations has updated data', async () => {
wrapper.vm.updateUserData(propsData.items[0], [444, 555, 666])
await wrapper.vm.$nextTick()
expect(wrapper.vm.items[0].creation).toEqual([444, 555, 666])
})
})
describe('call updateState', () => {
beforeEach(() => {
wrapper.vm.updateState(4)
@ -149,40 +140,5 @@ describe('OpenCreationsTable', () => {
expect(wrapper.vm.$root.$emit('update-state', 4)).toBeTruthy()
})
})
describe('call updateCreationData', () => {
const date = new Date()
beforeEach(() => {
wrapper.vm.updateCreationData({
amount: Number(80.0),
date: date,
memo: 'Test memo',
row: {
item: {},
detailsShowing: false,
toggleDetails: toggleDetailsMock,
},
})
})
it('emits update-state', () => {
expect(
wrapper.vm.$emit('update-contributions', {
amount: Number(80.0),
date: date,
memo: 'Test memo',
row: {
item: {},
detailsShowing: false,
toggleDetails: toggleDetailsMock,
},
}),
).toBeTruthy()
})
it('calls toggleDetails', () => {
expect(toggleDetailsMock).toBeCalled()
})
})
})
})

View File

@ -13,21 +13,24 @@
<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"
v-if="row.item.moderatorId"
variant="info"
size="md"
:index="0"
@click="rowToggleDetails(row, 0)"
class="mr-2"
>
@ -36,30 +39,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 +70,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"
@ -91,21 +90,20 @@
@row-toggle-details="rowToggleDetails(row, 0)"
>
<template #show-creation>
<div v-if="row.item.moderator">
<div v-if="row.item.moderatorId">
<edit-creation-formular
type="singleCreation"
:creation="row.item.creation"
:item="row.item"
:row="row"
:creationUserData="creationUserData"
@update-creation-data="updateCreationData"
@update-creation-data="$emit('update-contributions')"
/>
</div>
<div v-else>
<contribution-messages-list
:contributionId="row.item.id"
:contributionState="row.item.state"
@update-state="updateState"
@update-user-data="updateUserData"
/>
</div>
</template>
@ -117,9 +115,9 @@
<script>
import { toggleRowDetails } from '../../mixins/toggleRowDetails'
import RowDetails from '../RowDetails.vue'
import EditCreationFormular from '../EditCreationFormular.vue'
import ContributionMessagesList from '../ContributionMessages/ContributionMessagesList.vue'
import RowDetails from '../RowDetails'
import EditCreationFormular from '../EditCreationFormular'
import ContributionMessagesList from '../ContributionMessages/ContributionMessagesList'
const iconMap = {
IN_PROGRESS: 'question-square',
@ -147,34 +145,20 @@ export default {
required: true,
},
},
data() {
return {
creationUserData: {
amount: null,
date: null,
memo: null,
moderator: null,
},
}
},
methods: {
myself(item) {
return item.userId === this.$store.state.moderator.id
},
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'
},
updateCreationData(data) {
const row = data.row
this.$emit('update-contributions', data)
delete data.row
this.creationUserData = { ...this.creationUserData, ...data }
row.toggleDetails()
},
updateUserData(rowItem, newCreation) {
rowItem.creation = newCreation
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'
},
updateState(id) {
this.$emit('update-state', id)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,17 +1,19 @@
import gql from 'graphql-tag'
export const listAllContributions = gql`
export const adminListContributions = gql`
query (
$currentPage: Int = 1
$pageSize: Int = 25
$order: Order = DESC
$statusFilter: [ContributionStatus!]
$userId: Int
) {
listAllContributions(
adminListContributions(
currentPage: $currentPage
pageSize: $pageSize
order: $order
statusFilter: $statusFilter
userId: $userId
) {
contributionCount
contributionList {
@ -28,6 +30,10 @@ export const listAllContributions = gql`
messagesCount
deniedAt
deniedBy
deletedAt
deletedBy
moderatorId
userId
}
}
}

View File

@ -0,0 +1,11 @@
import gql from 'graphql-tag'
export const adminOpenCreations = gql`
query ($userId: Int!) {
adminOpenCreations(userId: $userId) {
year
month
amount
}
}
`

View File

@ -1,18 +1,11 @@
import gql from 'graphql-tag'
export const adminUpdateContribution = gql`
mutation ($id: Int!, $email: String!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
adminUpdateContribution(
id: $id
email: $email
amount: $amount
memo: $memo
creationDate: $creationDate
) {
mutation ($id: Int!, $amount: Decimal!, $memo: String!, $creationDate: String!) {
adminUpdateContribution(id: $id, amount: $amount, memo: $memo, creationDate: $creationDate) {
amount
date
memo
creation
}
}
`

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

@ -0,0 +1,17 @@
import gql from 'graphql-tag'
export const getCommunities = gql`
query {
getCommunities {
id
foreign
publicKey
url
lastAnnouncedAt
verifiedAt
lastErrorAt
createdAt
updatedAt
}
}
`

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
{
"all_emails": "Alle Nutzer",
"back": "zurück",
"change_user_role": "Nutzerrolle ändern",
"chat": "Chat",
"contributionLink": {
"amount": "Betrag",
@ -63,11 +64,17 @@
"deleted_user": "Alle gelöschten Nutzer",
"delete_user": "Nutzer löschen",
"deny": "Ablehnen",
"edit": "Bearbeiten",
"enabled": "aktiviert",
"error": "Fehler",
"expired": "abgelaufen",
"e_mail": "E-Mail",
"federation": {
"createdAt": "Erstellt am",
"gradidoInstances": "Gradido Instanzen",
"lastAnnouncedAt": "letzte Bekanntgabe",
"url": "Url",
"verified": "Verifiziert"
},
"firstname": "Vorname",
"footer": {
"app_version": "App version {version}",
@ -101,11 +108,11 @@
"message": {
"request": "Die Anfrage wurde gesendet."
},
"mod": "Mod",
"moderator": "Moderator",
"name": "Name",
"navbar": {
"automaticContributions": "Automatische Beiträge",
"instances": "Instanzen",
"logout": "Abmelden",
"my-account": "Mein Konto",
"statistic": "Statistik",
@ -116,6 +123,11 @@
"open_creations": "Offene Schöpfungen",
"overlay": {
"cancel": "Abbrechen",
"changeUserRole": {
"question": "Willst du die Rolle von {username} wirklich zu {newRole} ändern?",
"title": "Nutzerrolle ändern",
"yes": "Ja, Nutzerrolle ändern"
},
"confirm": {
"question": "Willst du diesen Gemeinwohl-Beitrag wirklich bestätigen und gutschreiben?",
"text": "Nach dem Speichern ist der Datensatz nicht mehr änderbar. Bitte überprüfe genau, dass alles stimmt.",
@ -128,11 +140,21 @@
"title": "Gemeinwohl-Beitrag löschen!",
"yes": "Ja, Beitrag löschen!"
},
"deleteUser": {
"question": "Willst du {username} wirklich löschen?",
"title": "Nutzer löschen",
"yes": "Ja, Nutzer löschen"
},
"deny": {
"question": "Willst du diesen Gemeinwohl-Beitrag wirklich ablehnen?",
"text": "Nach dem Speichern ist der Datensatz nicht mehr änderbar und kann auch nicht mehr gelöscht werden. Bitte überprüfe genau, dass alles stimmt.",
"title": "Gemeinwohl-Beitrag ablehnen!",
"yes": "Ja, Beitrag ablehnen und speichern!"
},
"undeleteUser": {
"question": "Willst du wirklich {username} wiederherstellen?",
"title": "Nutzer wiederherstellen",
"yes": "Ja, Nutzer wiederherstellen"
}
},
"redeemed": "eingelöst",

View File

@ -1,6 +1,7 @@
{
"all_emails": "All users",
"back": "back",
"change_user_role": "Change user role",
"chat": "Chat",
"contributionLink": {
"amount": "Amount",
@ -34,11 +35,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 +54,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,11 +64,17 @@
"deleted_user": "All deleted user",
"delete_user": "Delete user",
"deny": "Reject",
"edit": "Edit",
"enabled": "enabled",
"error": "Error",
"expired": "expired",
"e_mail": "E-mail",
"federation": {
"createdAt": "Created At ",
"gradidoInstances": "Gradido Instances",
"lastAnnouncedAt": "Last Announced",
"url": "Url",
"verified": "Verified"
},
"firstname": "Firstname",
"footer": {
"app_version": "App version {version}",
@ -87,7 +94,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,11 +108,11 @@
"message": {
"request": "Request has been sent."
},
"mod": "Mod",
"moderator": "Moderator",
"name": "Name",
"navbar": {
"automaticContributions": "Automatic Contributions",
"instances": "Instances",
"logout": "Logout",
"my-account": "My Account",
"statistic": "Statistic",
@ -116,6 +123,11 @@
"open_creations": "Open creations",
"overlay": {
"cancel": "Cancel",
"changeUserRole": {
"question": "Do you really want to change {username}'s role to {newRole}?",
"title": "Change user role",
"yes": "Yes, change user role"
},
"confirm": {
"question": "Do you really want to carry out and finally save this pre-stored creation?",
"text": "After saving, the record can no longer be changed. Please check carefully that everything is correct.",
@ -128,11 +140,21 @@
"title": "Delete creation!",
"yes": "Yes, delete and save creation!"
},
"deleteUser": {
"question": "Do you really want to delete {username}?",
"title": "Delete user",
"yes": "Yes, delete user"
},
"deny": {
"question": "Do you really want to carry out and finally save this pre-stored creation?",
"text": "After saving, the record can no longer be changed or deleted. Please check carefully that everything is correct.",
"title": "Reject creation!",
"yes": "Yes, reject and save creation!"
},
"undeleteUser": {
"question": "Do you really want to undelete {username}",
"title": "Undelete user",
"yes": "Yes,undelete user"
}
},
"redeemed": "redeemed",

View File

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

View File

@ -1,9 +1,11 @@
import { adminOpenCreations } from '../graphql/adminOpenCreations'
export const creationMonths = {
props: {
creation: {
type: Array,
default: () => [1000, 1000, 1000],
},
data() {
return {
creation: [1000, 1000, 1000],
userId: 0,
}
},
computed: {
creationDates() {
@ -38,4 +40,23 @@ export const creationMonths = {
return this.creationDates.map((date) => this.$d(date, 'monthShort')).join(' | ')
},
},
apollo: {
OpenCreations: {
query() {
return adminOpenCreations
},
variables() {
return {
userId: this.userId,
}
},
fetchPolicy: 'no-cache',
update({ adminOpenCreations }) {
this.creation = adminOpenCreations.map((obj) => obj.amount)
},
error({ message }) {
this.toastError(message)
},
},
},
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
import { mount } from '@vue/test-utils'
import CreationConfirm from './CreationConfirm.vue'
import CreationConfirm from './CreationConfirm'
import { adminDeleteContribution } from '../graphql/adminDeleteContribution'
import { denyContribution } from '../graphql/denyContribution'
import { listAllContributions } from '../graphql/listAllContributions'
import { adminListContributions } from '../graphql/adminListContributions'
import { confirmContribution } from '../graphql/confirmContribution'
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
import VueApollo from 'vue-apollo'
@ -38,8 +38,8 @@ const mocks = {
const defaultData = () => {
return {
listAllContributions: {
contributionCount: 2,
adminListContributions: {
contributionCount: 30,
contributionList: [
{
id: 1,
@ -92,14 +92,14 @@ const defaultData = () => {
describe('CreationConfirm', () => {
let wrapper
const adminListContributionsMock = jest.fn()
const adminDeleteContributionMock = jest.fn()
const adminDenyContributionMock = jest.fn()
const confirmContributionMock = jest.fn()
mockClient.setRequestHandler(
listAllContributions,
jest
.fn()
adminListContributions,
adminListContributionsMock
.mockRejectedValueOnce({ message: 'Ouch!' })
.mockResolvedValue({ data: defaultData() }),
)
@ -331,77 +331,119 @@ describe('CreationConfirm', () => {
describe('filter tabs', () => {
describe('click tab "confirmed"', () => {
let refetchSpy
beforeEach(async () => {
jest.clearAllMocks()
refetchSpy = jest.spyOn(wrapper.vm.$apollo.queries.ListAllContributions, 'refetch')
await wrapper.find('a[data-test="confirmed"]').trigger('click')
})
it('has statusFilter set to ["CONFIRMED"]', () => {
expect(
wrapper.vm.$apollo.queries.ListAllContributions.observer.options.variables,
).toMatchObject({ statusFilter: ['CONFIRMED'] })
})
it('refetches contributions', () => {
expect(refetchSpy).toBeCalled()
it('refetches contributions with proper filter', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
order: 'DESC',
pageSize: 25,
statusFilter: ['CONFIRMED'],
})
})
describe('click tab "open"', () => {
beforeEach(async () => {
jest.clearAllMocks()
refetchSpy = jest.spyOn(wrapper.vm.$apollo.queries.ListAllContributions, 'refetch')
await wrapper.find('a[data-test="open"]').trigger('click')
})
it('has statusFilter set to ["IN_PROGRESS", "PENDING"]', () => {
expect(
wrapper.vm.$apollo.queries.ListAllContributions.observer.options.variables,
).toMatchObject({ statusFilter: ['IN_PROGRESS', 'PENDING'] })
})
it('refetches contributions', () => {
expect(refetchSpy).toBeCalled()
it('refetches contributions with proper filter', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
order: 'DESC',
pageSize: 25,
statusFilter: ['IN_PROGRESS', 'PENDING'],
})
})
})
describe('click tab "denied"', () => {
beforeEach(async () => {
jest.clearAllMocks()
refetchSpy = jest.spyOn(wrapper.vm.$apollo.queries.ListAllContributions, 'refetch')
await wrapper.find('a[data-test="denied"]').trigger('click')
})
it('has statusFilter set to ["DENIED"]', () => {
expect(
wrapper.vm.$apollo.queries.ListAllContributions.observer.options.variables,
).toMatchObject({ statusFilter: ['DENIED'] })
it('refetches contributions with proper filter', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
order: 'DESC',
pageSize: 25,
statusFilter: ['DENIED'],
})
})
})
describe('click tab "deleted"', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper.find('a[data-test="deleted"]').trigger('click')
})
it('refetches contributions', () => {
expect(refetchSpy).toBeCalled()
it('refetches contributions with proper filter', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
order: 'DESC',
pageSize: 25,
statusFilter: ['DELETED'],
})
})
})
describe('click tab "all"', () => {
beforeEach(async () => {
jest.clearAllMocks()
refetchSpy = jest.spyOn(wrapper.vm.$apollo.queries.ListAllContributions, 'refetch')
await wrapper.find('a[data-test="all"]').trigger('click')
})
it('has statusFilter set to ["IN_PROGRESS", "PENDING", "CONFIRMED", "DENIED", "DELETED"]', () => {
expect(
wrapper.vm.$apollo.queries.ListAllContributions.observer.options.variables,
).toMatchObject({
it('refetches contributions with proper filter', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
order: 'DESC',
pageSize: 25,
statusFilter: ['IN_PROGRESS', 'PENDING', 'CONFIRMED', 'DENIED', 'DELETED'],
})
})
it('refetches contributions', () => {
expect(refetchSpy).toBeCalled()
describe('change pagination', () => {
it('has pagination buttons', () => {
expect(wrapper.findComponent({ name: 'BPagination' }).exists()).toBe(true)
})
describe('next page', () => {
beforeEach(() => {
jest.clearAllMocks()
wrapper.findComponent({ name: 'BPagination' }).vm.$emit('input', 2)
})
it('calls the API again', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 2,
order: 'DESC',
pageSize: 25,
statusFilter: ['IN_PROGRESS', 'PENDING', 'CONFIRMED', 'DENIED', 'DELETED'],
})
})
describe('click tab "open" again', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper.find('a[data-test="open"]').trigger('click')
})
it('refetches contributions with proper filter and current page = 1', () => {
expect(adminListContributionsMock).toBeCalledWith({
currentPage: 1,
order: 'DESC',
pageSize: 25,
statusFilter: ['IN_PROGRESS', 'PENDING'],
})
})
})
})
})
})
})
@ -412,10 +454,20 @@ describe('CreationConfirm', () => {
await wrapper.findComponent({ name: 'OpenCreationsTable' }).vm.$emit('update-state', 2)
})
it.skip('updates the status', () => {
it('updates the status', () => {
expect(wrapper.vm.items.find((obj) => obj.id === 2).messagesCount).toBe(1)
expect(wrapper.vm.items.find((obj) => obj.id === 2).state).toBe('IN_PROGRESS')
})
})
describe('unknown variant', () => {
beforeEach(async () => {
await wrapper.setData({ variant: 'unknown' })
})
it('has overlay icon "info"', () => {
expect(wrapper.vm.overlayIcon).toBe('info')
})
})
})
})

View File

@ -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
@ -32,7 +44,7 @@
:fields="fields"
@show-overlay="showOverlay"
@update-state="updateStatus"
@update-contributions="$apollo.queries.AllContributions.refetch()"
@update-contributions="$apollo.queries.ListAllContributions.refetch()"
/>
<b-pagination
@ -71,9 +83,9 @@
</div>
</template>
<script>
import Overlay from '../components/Overlay.vue'
import OpenCreationsTable from '../components/Tables/OpenCreationsTable.vue'
import { listAllContributions } from '../graphql/listAllContributions'
import Overlay from '../components/Overlay'
import OpenCreationsTable from '../components/Tables/OpenCreationsTable'
import { adminListContributions } from '../graphql/adminListContributions'
import { adminDeleteContribution } from '../graphql/adminDeleteContribution'
import { confirmContribution } from '../graphql/confirmContribution'
import { denyContribution } from '../graphql/denyContribution'
@ -104,6 +116,11 @@ export default {
pageSize: 25,
}
},
watch: {
tabIndex() {
this.currentPage = 1
},
},
methods: {
deleteCreation() {
this.$apollo
@ -172,19 +189,17 @@ export default {
this.items.find((obj) => obj.id === id).messagesCount++
this.items.find((obj) => obj.id === id).state = 'IN_PROGRESS'
},
},
watch: {
statusFilter() {
this.$apollo.queries.ListAllContributions.refetch()
formatDateOrDash(value) {
return value ? this.$d(new Date(value), 'short') : '—'
},
},
computed: {
fields() {
return [
[
// 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') },
{
@ -199,14 +214,15 @@ 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: 'moderatorId', label: this.$t('moderator') },
{ key: 'editCreation', label: this.$t('chat') },
{ key: 'confirm', label: this.$t('save') },
],
[
// confirmed contributions
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{
@ -221,27 +237,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') },
],
[
{ key: 'reActive', label: 'reActive' },
// denied contributions
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{
@ -256,29 +273,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') },
],
[],
[
{ key: 'state', label: 'state' },
// deleted contributions
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{
@ -293,24 +309,61 @@ 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.formatDateOrDash(value)
},
},
{ key: 'deletedBy', label: this.$t('moderator') },
{ key: 'chatCreation', label: this.$t('chat') },
],
[
// all contributions
{ key: 'state', label: this.$t('status') },
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{
key: 'amount',
label: this.$t('creation'),
formatter: (value) => {
return value + ' GDD'
},
},
{ key: 'memo', label: this.$t('text'), class: 'text-break' },
{
key: 'contributionDate',
label: this.$t('created'),
formatter: (value) => {
return this.formatDateOrDash(value)
},
},
{
key: 'createdAt',
label: this.$t('createdAt'),
formatter: (value) => {
return this.formatDateOrDash(value)
},
},
{
key: 'confirmedAt',
label: this.$t('contributions.confirms'),
formatter: (value) => {
return this.$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]
@ -349,19 +402,22 @@ export default {
apollo: {
ListAllContributions: {
query() {
return listAllContributions
return adminListContributions
},
variables() {
// may be at some point we need a pagination here
return {
currentPage: this.currentPage,
pageSize: this.pageSize,
statusFilter: this.statusFilter,
}
},
update({ listAllContributions }) {
this.rows = listAllContributions.contributionCount
this.items = listAllContributions.contributionList
fetchPolicy: 'no-cache',
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

@ -0,0 +1,125 @@
import { mount } from '@vue/test-utils'
import FederationVisualize from './FederationVisualize'
import VueApollo from 'vue-apollo'
import { createMockClient } from 'mock-apollo-client'
import { getCommunities } from '@/graphql/getCommunities'
import { toastErrorSpy } from '../../test/testSetup'
const mockClient = createMockClient()
const apolloProvider = new VueApollo({
defaultClient: mockClient,
})
const localVue = global.localVue
localVue.use(VueApollo)
const mocks = {
$t: (key) => key,
$d: jest.fn((d) => d),
$i18n: {
locale: 'en',
t: (key) => key,
},
}
const defaultData = () => {
return {
getCommunities: [
{
id: 1776,
foreign: true,
publicKey: 'c7ca9e742421bb167b8666cb78f90b40c665b8f35db8f001988d44dbb3ce8527',
url: 'http://localhost/api/2_0',
lastAnnouncedAt: '2023-04-07T12:27:24.037Z',
verifiedAt: null,
lastErrorAt: null,
createdAt: '2023-04-07T11:45:06.254Z',
updatedAt: null,
__typename: 'Community',
},
{
id: 1775,
foreign: true,
publicKey: 'c7ca9e742421bb167b8666cb78f90b40c665b8f35db8f001988d44dbb3ce8527',
url: 'http://localhost/api/1_1',
lastAnnouncedAt: '2023-04-07T12:27:24.023Z',
verifiedAt: null,
lastErrorAt: null,
createdAt: '2023-04-07T11:45:06.234Z',
updatedAt: null,
__typename: 'Community',
},
{
id: 1774,
foreign: true,
publicKey: 'c7ca9e742421bb167b8666cb78f90b40c665b8f35db8f001988d44dbb3ce8527',
url: 'http://localhost/api/1_0',
lastAnnouncedAt: '2023-04-07T12:27:24.009Z',
verifiedAt: null,
lastErrorAt: null,
createdAt: '2023-04-07T11:45:06.218Z',
updatedAt: null,
__typename: 'Community',
},
],
}
}
describe('FederationVisualize', () => {
let wrapper
const getCommunitiesMock = jest.fn()
mockClient.setRequestHandler(
getCommunities,
getCommunitiesMock
.mockRejectedValueOnce({ message: 'Ouch!' })
.mockResolvedValue({ data: defaultData() }),
)
const Wrapper = () => {
return mount(FederationVisualize, { localVue, mocks, apolloProvider })
}
describe('mount', () => {
beforeEach(() => {
jest.clearAllMocks()
wrapper = Wrapper()
})
describe('server error', () => {
it('toast error', () => {
expect(toastErrorSpy).toBeCalledWith('Ouch!')
})
})
describe('sever success', () => {
it('sends query to Apollo when created', () => {
expect(getCommunitiesMock).toBeCalled()
})
it('has a DIV element with the class "federation-visualize"', () => {
expect(wrapper.find('div.federation-visualize').exists()).toBe(true)
})
it('has a refresh button', () => {
expect(wrapper.find('[data-test="federation-communities-refresh-btn"]').exists()).toBe(true)
})
it('renders 3 community list items', () => {
expect(wrapper.findAll('.list-group-item').length).toBe(3)
})
describe('cklicking the refresh button', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper.find('[data-test="federation-communities-refresh-btn"]').trigger('click')
})
it('calls the API', async () => {
expect(getCommunitiesMock).toBeCalled()
})
})
})
})
})

View File

@ -0,0 +1,69 @@
<template>
<div class="federation-visualize">
<div class="d-flex justify-content-between align-items-center mb-3">
<span class="h2">{{ $t('federation.gradidoInstances') }}</span>
<b-button>
<b-icon
icon="arrow-clockwise"
font-scale="2"
:animation="animation"
@click="$apollo.queries.GetCommunities.refresh()"
data-test="federation-communities-refresh-btn"
></b-icon>
</b-button>
</div>
<b-list-group>
<b-row>
<b-col cols="1" class="ml-1">{{ $t('federation.verified') }}</b-col>
<b-col class="ml-3">{{ $t('federation.url') }}</b-col>
<b-col cols="2">{{ $t('federation.lastAnnouncedAt') }}</b-col>
<b-col cols="2">{{ $t('federation.createdAt') }}</b-col>
</b-row>
<b-list-group-item
v-for="item in communities"
:key="item.id"
:variant="!item.foreign ? 'primary' : 'warning'"
>
<federation-visualize-item :item="item" />
</b-list-group-item>
</b-list-group>
</div>
</template>
<script>
import { getCommunities } from '@/graphql/getCommunities'
import FederationVisualizeItem from '../components/Fedaration/FederationVisualizeItem.vue'
export default {
name: 'FederationVisualize',
components: {
FederationVisualizeItem,
},
data() {
return {
oldPublicKey: '',
communities: [],
icon: '',
}
},
computed: {
animation() {
return this.$apollo.queries.GetCommunities.loading ? 'spin' : ''
},
},
apollo: {
GetCommunities: {
fetchPolicy: 'network-only',
query() {
return getCommunities
},
update({ getCommunities }) {
this.communities = getCommunities
},
error({ message }) {
this.toastError(message)
},
},
},
}
</script>

View File

@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils'
import Overview from './Overview.vue'
import { listAllContributions } from '../graphql/listAllContributions'
import Overview from './Overview'
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 {
listAllContributions: {
adminListContributions: {
contributionCount: 2,
contributionList: [
{
@ -42,7 +42,7 @@ const defaultData = () => {
amount: 500,
memo: 'Danke für alles',
date: new Date(),
moderator: 1,
moderatorId: 1,
state: 'PENDING',
creation: [500, 500, 500],
messagesCount: 0,
@ -64,7 +64,7 @@ const defaultData = () => {
amount: 1000000,
memo: 'Gut Ergattert',
date: new Date(),
moderator: 1,
moderatorId: 1,
state: 'PENDING',
creation: [500, 500, 500],
messagesCount: 0,
@ -84,11 +84,11 @@ const defaultData = () => {
describe('Overview', () => {
let wrapper
const listAllContributionsMock = jest.fn()
const adminListContributionsMock = jest.fn()
mockClient.setRequestHandler(
listAllContributions,
listAllContributionsMock
adminListContributions,
adminListContributionsMock
.mockRejectedValueOnce({ message: 'Ouch!' })
.mockResolvedValue({ data: defaultData() }),
)
@ -109,8 +109,8 @@ describe('Overview', () => {
})
})
it('calls the listAllContributions query', () => {
expect(listAllContributionsMock).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 { listAllContributions } from '../graphql/listAllContributions'
import { adminListContributions } from '../graphql/adminListContributions'
export default {
name: 'overview',
@ -43,7 +43,7 @@ export default {
apollo: {
AllContributions: {
query() {
return listAllContributions
return adminListContributions
},
variables() {
// may be at some point we need a pagination here
@ -51,8 +51,8 @@ export default {
statusFilter: this.statusFilter,
}
},
update({ listAllContributions }) {
this.$store.commit('setOpenCreations', listAllContributions.contributionCount)
update({ adminListContributions }) {
this.$store.commit('setOpenCreations', adminListContributions.contributionCount)
},
error({ message }) {
this.toastError(message)

View File

@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils'
import UserSearch from './UserSearch.vue'
import UserSearch from './UserSearch'
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
const localVue = global.localVue
@ -25,7 +25,7 @@ const apolloQueryMock = jest.fn().mockResolvedValue({
email: 'benjamin@bluemchen.de',
creation: [1000, 1000, 1000],
emailChecked: true,
deletedAt: null,
deletedAt: new Date(),
},
{
userId: 3,
@ -243,6 +243,17 @@ describe('UserSearch', () => {
})
})
describe('recover user', () => {
const userId = 2
beforeEach(() => {
wrapper.findComponent({ name: 'SearchUserTable' }).vm.$emit('updateDeletedAt', userId, null)
})
it('toasts a success message', () => {
expect(toastSuccessSpy).toBeCalledWith('user_recovered')
})
})
describe('apollo returns error', () => {
beforeEach(() => {
apolloQueryMock.mockRejectedValue({

View File

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

View File

@ -45,7 +45,7 @@ describe('router', () => {
describe('routes', () => {
it('has nine routes defined', () => {
expect(routes).toHaveLength(8)
expect(routes).toHaveLength(9)
})
it('has "/overview" as default', async () => {
@ -88,6 +88,13 @@ describe('router', () => {
})
})
describe('federation', () => {
it('loads the "FederationVisualize" page', async () => {
const component = await routes.find((r) => r.path === '/federation').component()
expect(component.default.name).toBe('FederationVisualize')
})
})
describe('not found page', () => {
it('renders the "NotFound" component', async () => {
const component = await routes.find((r) => r.path === '*').component()

View File

@ -31,6 +31,10 @@ const routes = [
path: '*',
component: () => import('@/components/NotFoundPage.vue'),
},
{
path: '/federation',
component: () => import('@/pages/FederationVisualize.vue'),
},
]
export default routes

View File

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

View File

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

View File

@ -5038,6 +5038,11 @@ data-urls@^2.0.0:
whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.0"
date-fns@^2.29.3:
version "2.29.3"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8"
integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==
de-indent@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"

View File

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

View File

@ -1,18 +1,29 @@
// eslint-disable-next-line import/no-commonjs, import/unambiguous
module.exports = {
root: true,
env: {
node: true,
// jest: true,
},
parser: '@typescript-eslint/parser',
plugins: ['prettier', '@typescript-eslint' /*, 'jest' */],
plugins: ['prettier', '@typescript-eslint', 'type-graphql', 'import', 'n', 'promise'],
extends: [
'standard',
'eslint:recommended',
'plugin:prettier/recommended',
'plugin:@typescript-eslint/recommended',
'plugin:import/recommended',
'plugin:import/typescript',
],
// add your custom rules here
settings: {
'import/parsers': {
'@typescript-eslint/parser': ['.ts', '.tsx'],
},
'import/resolver': {
typescript: {
project: ['./tsconfig.json', '**/tsconfig.json'],
},
node: true,
},
},
rules: {
'no-console': ['error'],
'no-debugger': 'error',
@ -22,5 +33,162 @@ module.exports = {
htmlWhitespaceSensitivity: 'ignore',
},
],
// import
'import/export': 'error',
'import/no-deprecated': 'error',
'import/no-empty-named-blocks': 'error',
'import/no-extraneous-dependencies': 'error',
'import/no-mutable-exports': 'error',
'import/no-unused-modules': 'error',
'import/no-named-as-default': 'error',
'import/no-named-as-default-member': 'error',
'import/no-amd': 'error',
'import/no-commonjs': 'error',
'import/no-import-module-exports': 'error',
'import/no-nodejs-modules': 'off',
'import/unambiguous': 'error',
'import/default': 'error',
'import/named': 'error',
'import/namespace': 'error',
'import/no-absolute-path': 'error',
'import/no-cycle': 'error',
'import/no-dynamic-require': 'error',
'import/no-internal-modules': 'off',
'import/no-relative-packages': 'error',
'import/no-relative-parent-imports': ['error', { ignore: ['@/*'] }],
'import/no-self-import': 'error',
'import/no-unresolved': 'error',
'import/no-useless-path-segments': 'error',
'import/no-webpack-loader-syntax': 'error',
'import/consistent-type-specifier-style': 'error',
'import/exports-last': 'off',
'import/extensions': 'error',
'import/first': 'error',
'import/group-exports': 'off',
'import/newline-after-import': 'error',
'import/no-anonymous-default-export': 'error',
'import/no-default-export': 'error',
'import/no-duplicates': 'error',
'import/no-named-default': 'error',
'import/no-namespace': 'error',
'import/no-unassigned-import': 'error',
'import/order': [
'error',
{
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'object', 'type'],
'newlines-between': 'always',
pathGroups: [
{
pattern: '@?*/**',
group: 'external',
position: 'after',
},
{
pattern: '@/**',
group: 'external',
position: 'after',
},
],
alphabetize: {
order: 'asc' /* sort in ascending order. Options: ['ignore', 'asc', 'desc'] */,
caseInsensitive: true /* ignore case. Options: [true, false] */,
},
distinctGroup: true,
},
],
'import/prefer-default-export': 'off',
// n
'n/handle-callback-err': 'error',
'n/no-callback-literal': 'error',
'n/no-exports-assign': 'error',
'n/no-extraneous-import': 'error',
'n/no-extraneous-require': 'error',
'n/no-hide-core-modules': 'error',
'n/no-missing-import': 'off', // not compatible with typescript
'n/no-missing-require': 'error',
'n/no-new-require': 'error',
'n/no-path-concat': 'error',
'n/no-process-exit': 'error',
'n/no-unpublished-bin': 'error',
'n/no-unpublished-import': 'off', // TODO need to exclude seeds
'n/no-unpublished-require': 'error',
'n/no-unsupported-features': ['error', { ignores: ['modules'] }],
'n/no-unsupported-features/es-builtins': 'error',
'n/no-unsupported-features/es-syntax': 'error',
'n/no-unsupported-features/node-builtins': 'error',
'n/process-exit-as-throw': 'error',
'n/shebang': 'error',
'n/callback-return': 'error',
'n/exports-style': 'error',
'n/file-extension-in-import': 'off',
'n/global-require': 'error',
'n/no-mixed-requires': 'error',
'n/no-process-env': 'error',
'n/no-restricted-import': 'error',
'n/no-restricted-require': 'error',
'n/no-sync': 'error',
'n/prefer-global/buffer': 'error',
'n/prefer-global/console': 'error',
'n/prefer-global/process': 'error',
'n/prefer-global/text-decoder': 'error',
'n/prefer-global/text-encoder': 'error',
'n/prefer-global/url': 'error',
'n/prefer-global/url-search-params': 'error',
'n/prefer-promises/dns': 'error',
'n/prefer-promises/fs': 'error',
// promise
'promise/catch-or-return': 'error',
'promise/no-return-wrap': 'error',
'promise/param-names': 'error',
'promise/always-return': 'error',
'promise/no-native': 'off',
'promise/no-nesting': 'warn',
'promise/no-promise-in-callback': 'warn',
'promise/no-callback-in-promise': 'warn',
'promise/avoid-new': 'warn',
'promise/no-new-statics': 'error',
'promise/no-return-in-finally': 'warn',
'promise/valid-params': 'warn',
'promise/prefer-await-to-callbacks': 'error',
'promise/no-multiple-resolved': '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',
// this should not run on ts files: https://github.com/import-js/eslint-plugin-import/issues/2215#issuecomment-911245486
'import/unambiguous': 'off',
},
parserOptions: {
tsconfigRootDir: __dirname,
project: ['./tsconfig.json', '**/tsconfig.json'],
// this is to properly reference the referenced project database without requirement of compiling it
EXPERIMENTAL_useSourceOfProjectReferenceRedirect: true,
},
},
{
files: ['*.test.ts'],
plugins: ['jest'],
rules: {
'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',
'@typescript-eslint/unbound-method': 'off',
'jest/unbound-method': 'error',
},
},
],
}

View File

@ -1,9 +1,14 @@
module.exports = {
semi: false,
printWidth: 100,
singleQuote: true,
trailingComma: "all",
tabWidth: 2,
useTabs: false,
semi: false,
singleQuote: true,
quoteProps: "as-needed",
jsxSingleQuote: true,
trailingComma: "all",
bracketSpacing: true,
bracketSameLine: false,
arrowParens: "always",
endOfLine: "auto",
};

View File

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

View File

@ -1,9 +1,15 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
// eslint-disable-next-line import/no-commonjs, import/unambiguous
module.exports = {
verbose: true,
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/'],
@ -16,10 +22,12 @@ module.exports = {
'@repository/(.*)': '<rootDir>/src/typeorm/repository/$1',
'@test/(.*)': '<rootDir>/test/$1',
'@entity/(.*)':
// eslint-disable-next-line n/no-process-env
process.env.NODE_ENV === 'development'
? '<rootDir>/../database/entity/$1'
: '<rootDir>/../database/build/entity/$1',
'@dbTools/(.*)':
// eslint-disable-next-line n/no-process-env
process.env.NODE_ENV === 'development'
? '<rootDir>/../database/src/$1'
: '<rootDir>/../database/build/src/$1',

View File

@ -8,7 +8,7 @@
"pattern": "yyyy-MM-dd",
"layout":
{
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
},
"keepFileExt" : true,
"fileNameSep" : "_",
@ -21,7 +21,7 @@
"pattern": "yyyy-MM-dd",
"layout":
{
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
},
"keepFileExt" : true,
"fileNameSep" : "_",
@ -34,7 +34,7 @@
"pattern": "yyyy-MM-dd",
"layout":
{
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
},
"keepFileExt" : true,
"fileNameSep" : "_",
@ -47,7 +47,7 @@
"pattern": "yyyy-MM-dd",
"layout":
{
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
},
"keepFileExt" : true,
"fileNameSep" : "_",
@ -60,7 +60,7 @@
"pattern": "yyyy-MM-dd",
"layout":
{
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m %s"
},
"keepFileExt" : true,
"fileNameSep" : "_",
@ -77,7 +77,7 @@
"type": "stdout",
"layout":
{
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
}
},
"apolloOut":
@ -85,7 +85,7 @@
"type": "stdout",
"layout":
{
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
}
}
},

View File

@ -1,6 +1,6 @@
{
"name": "gradido-backend",
"version": "1.18.2",
"version": "1.20.0",
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
"main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/backend",
@ -12,8 +12,8 @@
"clean": "tsc --build --clean",
"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",
"lint": "eslint --max-warnings=0 .",
"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"
@ -29,6 +29,7 @@
"dotenv": "^10.0.0",
"email-templates": "^10.0.1",
"express": "^4.17.1",
"gradido-database": "file:../database",
"graphql": "^15.5.1",
"graphql-request": "5.0.0",
"i18n": "^0.15.1",
@ -55,20 +56,25 @@
"@types/node": "^16.10.3",
"@types/nodemailer": "^6.4.4",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^4.28.0",
"@typescript-eslint/parser": "^4.28.0",
"@typescript-eslint/eslint-plugin": "^5.57.1",
"@typescript-eslint/parser": "^5.57.1",
"apollo-server-testing": "^2.25.2",
"eslint": "^7.29.0",
"eslint-config-prettier": "^8.3.0",
"eslint-config-standard": "^16.0.3",
"eslint-plugin-import": "^2.23.4",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-promise": "^5.1.0",
"eslint": "^8.37.0",
"eslint-config-prettier": "^8.8.0",
"eslint-config-standard": "^17.0.0",
"eslint-import-resolver-typescript": "^3.5.4",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-jest": "^27.2.1",
"eslint-plugin-n": "^15.7.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-type-graphql": "^1.0.0",
"faker": "^5.5.3",
"graphql-tag": "^2.12.6",
"jest": "^27.2.4",
"klicktipp-api": "^1.0.2",
"nodemon": "^2.0.7",
"prettier": "^2.3.1",
"prettier": "^2.8.7",
"ts-jest": "^27.0.5",
"ts-node": "^10.0.0",
"tsconfig-paths": "^3.14.0",
@ -78,5 +84,8 @@
"ignore": [
"**/*.test.ts"
]
},
"engines": {
"node": ">=14"
}
}

View File

@ -1,43 +1,44 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import axios from 'axios'
import { LogError } from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger'
// 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)
return axios
.post(url, payload)
.then((result) => {
logger.trace('POST-Response: result=' + result)
if (result.status !== 200) {
throw new Error('HTTP Status Error ' + result.status)
}
if (result.data.state !== 'success') {
throw new Error(result.data.msg)
}
return { success: true, data: result.data }
})
.catch((error) => {
return { success: false, data: error.message }
})
logger.trace('POST', url, payload)
try {
const result = await axios.post(url, payload)
logger.trace('POST-Response', result)
if (result.status !== 200) {
throw new LogError('HTTP Status Error', result.status)
}
if (result.data.state !== 'success') {
throw new LogError(result.data.msg)
}
return { success: true, data: result.data }
} catch (error: any) {
return { success: false, data: error.message }
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const apiGet = async (url: string): Promise<any> => {
logger.trace('GET: url=' + url)
return axios
.get(url)
.then((result) => {
logger.trace('GET-Response: result=' + result)
if (result.status !== 200) {
throw new Error('HTTP Status Error ' + result.status)
}
if (!['success', 'warning'].includes(result.data.state)) {
throw new Error(result.data.msg)
}
return { success: true, data: result.data }
})
.catch((error) => {
return { success: false, data: error.message }
})
try {
const result = await axios.get(url)
logger.trace('GET-Response', result)
if (result.status !== 200) {
throw new LogError('HTTP Status Error', result.status)
}
if (!['success', 'warning'].includes(result.data.state)) {
throw new LogError(result.data.msg)
}
return { success: true, data: result.data }
} catch (error: any) {
return { success: false, data: error.message }
}
}

View File

@ -1,7 +1,14 @@
/* 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 CONFIG from '@/config'
import { CONFIG } from '@/config'
// eslint-disable-next-line import/no-relative-parent-imports
import KlicktippConnector from 'klicktipp-api'
const klicktippConnector = new KlicktippConnector()
@ -11,6 +18,7 @@ export const klicktippSignIn = async (
firstName?: string,
lastName?: string,
): Promise<boolean> => {
if (!CONFIG.KLICKTIPP) return true
const fields = {
fieldFirstName: firstName,
fieldLastName: lastName,
@ -21,12 +29,14 @@ export const klicktippSignIn = async (
}
export const signout = async (email: string, language: string): Promise<boolean> => {
if (!CONFIG.KLICKTIPP) return true
const apiKey = language === 'de' ? CONFIG.KLICKTIPP_APIKEY_DE : CONFIG.KLICKTIPP_APIKEY_EN
const result = await klicktippConnector.signoff(apiKey, email)
return result
}
export const unsubscribe = async (email: string): Promise<boolean> => {
if (!CONFIG.KLICKTIPP) return true
const isLogin = await loginKlicktippUser()
if (isLogin) {
return await klicktippConnector.unsubscribe(email)
@ -35,6 +45,7 @@ export const unsubscribe = async (email: string): Promise<boolean> => {
}
export const getKlickTippUser = async (email: string): Promise<any> => {
if (!CONFIG.KLICKTIPP) return true
const isLogin = await loginKlicktippUser()
if (isLogin) {
const subscriberId = await klicktippConnector.subscriberSearch(email)
@ -45,14 +56,17 @@ export const getKlickTippUser = async (email: string): Promise<any> => {
}
export const loginKlicktippUser = async (): Promise<boolean> => {
if (!CONFIG.KLICKTIPP) return true
return await klicktippConnector.login(CONFIG.KLICKTIPP_USER, CONFIG.KLICKTIPP_PASSWORD)
}
export const logoutKlicktippUser = async (): Promise<boolean> => {
if (!CONFIG.KLICKTIPP) return true
return await klicktippConnector.logout()
}
export const untagUser = async (email: string, tagId: string): Promise<boolean> => {
if (!CONFIG.KLICKTIPP) return true
const isLogin = await loginKlicktippUser()
if (isLogin) {
return await klicktippConnector.untag(email, tagId)
@ -61,6 +75,7 @@ export const untagUser = async (email: string, tagId: string): Promise<boolean>
}
export const tagUser = async (email: string, tagIds: string): Promise<boolean> => {
if (!CONFIG.KLICKTIPP) return true
const isLogin = await loginKlicktippUser()
if (isLogin) {
return await klicktippConnector.tag(email, tagIds)
@ -69,9 +84,25 @@ export const tagUser = async (email: string, tagIds: string): Promise<boolean> =
}
export const getKlicktippTagMap = async () => {
if (!CONFIG.KLICKTIPP) return true
const isLogin = await loginKlicktippUser()
if (isLogin) {
return await klicktippConnector.tagIndex()
}
return ''
}
export const addFieldsToSubscriber = async (
email: string,
fields: any = {},
newemail = '',
newsmsnumber = '',
) => {
if (!CONFIG.KLICKTIPP) return true
const isLogin = await loginKlicktippUser()
if (isLogin) {
const subscriberId = await klicktippConnector.subscriberSearch(email)
return klicktippConnector.subscriberUpdate(subscriberId, fields, newemail, newsmsnumber)
}
return false
}

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

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

View File

@ -2,12 +2,9 @@ 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',
GET_KLICKTIPP_USER = 'GET_KLICKTIPP_USER',
GET_KLICKTIPP_TAG_MAP = 'GET_KLICKTIPP_TAG_MAP',
UNSUBSCRIBE_NEWSLETTER = 'UNSUBSCRIBE_NEWSLETTER',
SUBSCRIBE_NEWSLETTER = 'SUBSCRIBE_NEWSLETTER',
TRANSACTION_LIST = 'TRANSACTION_LIST',
@ -36,23 +33,23 @@ export enum RIGHTS {
CREATE_CONTRIBUTION_MESSAGE = 'CREATE_CONTRIBUTION_MESSAGE',
LIST_ALL_CONTRIBUTION_MESSAGES = 'LIST_ALL_CONTRIBUTION_MESSAGES',
OPEN_CREATIONS = 'OPEN_CREATIONS',
USER = 'USER',
// Admin
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_CREATE_CONTRIBUTIONS = 'ADMIN_CREATE_CONTRIBUTIONS',
ADMIN_UPDATE_CONTRIBUTION = 'ADMIN_UPDATE_CONTRIBUTION',
ADMIN_DELETE_CONTRIBUTION = 'ADMIN_DELETE_CONTRIBUTION',
LIST_UNCONFIRMED_CONTRIBUTIONS = 'LIST_UNCONFIRMED_CONTRIBUTIONS',
ADMIN_LIST_CONTRIBUTIONS = 'ADMIN_LIST_CONTRIBUTIONS',
CONFIRM_CONTRIBUTION = 'CONFIRM_CONTRIBUTION',
SEND_ACTIVATION_EMAIL = 'SEND_ACTIVATION_EMAIL',
CREATION_TRANSACTION_LIST = 'CREATION_TRANSACTION_LIST',
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',
DENY_CONTRIBUTION = 'DENY_CONTRIBUTION',
ADMIN_OPEN_CREATIONS = 'ADMIN_OPEN_CREATIONS',
}

View File

@ -9,8 +9,6 @@ export const ROLE_USER = new Role('user', [
RIGHTS.BALANCE,
RIGHTS.LIST_GDT_ENTRIES,
RIGHTS.EXIST_PID,
RIGHTS.GET_KLICKTIPP_USER,
RIGHTS.GET_KLICKTIPP_TAG_MAP,
RIGHTS.UNSUBSCRIBE_NEWSLETTER,
RIGHTS.SUBSCRIBE_NEWSLETTER,
RIGHTS.TRANSACTION_LIST,
@ -34,6 +32,7 @@ export const ROLE_USER = new Role('user', [
RIGHTS.CREATE_CONTRIBUTION_MESSAGE,
RIGHTS.LIST_ALL_CONTRIBUTION_MESSAGES,
RIGHTS.OPEN_CREATIONS,
RIGHTS.USER,
])
export const ROLE_ADMIN = new Role('admin', Object.values(RIGHTS)) // all rights

View File

@ -1,4 +1,4 @@
import CONFIG from './index'
import { CONFIG } from './index'
describe('config/index', () => {
describe('decay start block', () => {

View File

@ -1,7 +1,9 @@
// ATTENTION: DO NOT PUT ANY SECRETS IN HERE (or the .env)
/* eslint-disable n/no-process-env */
import { Decimal } from 'decimal.js-light'
import dotenv from 'dotenv'
import Decimal from 'decimal.js-light'
dotenv.config()
Decimal.set({
@ -10,7 +12,7 @@ Decimal.set({
})
const constants = {
DB_VERSION: '0060-update_communities_table',
DB_VERSION: '0065-refactor_communities_table',
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info
@ -74,7 +76,7 @@ const email = {
EMAIL_SENDER: process.env.EMAIL_SENDER || 'info@gradido.net',
EMAIL_PASSWORD: process.env.EMAIL_PASSWORD || '',
EMAIL_SMTP_URL: process.env.EMAIL_SMTP_URL || 'mailserver',
EMAIL_SMTP_PORT: process.env.EMAIL_SMTP_PORT || '1025',
EMAIL_SMTP_PORT: Number(process.env.EMAIL_SMTP_PORT) || 1025,
// eslint-disable-next-line no-unneeded-ternary
EMAIL_TLS: process.env.EMAIL_TLS === 'false' ? false : true,
EMAIL_LINK_VERIFICATION:
@ -119,7 +121,7 @@ const federation = {
Number(process.env.FEDERATION_VALIDATE_COMMUNITY_TIMER) || 60000,
}
const CONFIG = {
export const CONFIG = {
...constants,
...server,
...database,
@ -130,5 +132,3 @@ const CONFIG = {
...webhook,
...federation,
}
export default CONFIG

View File

@ -1,13 +1,18 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { createTransport } from 'nodemailer'
import { logger, i18n } from '@test/testSetup'
import CONFIG from '@/config'
import { CONFIG } from '@/config'
import { sendEmailTranslated } from './sendEmailTranslated'
CONFIG.EMAIL = false
CONFIG.EMAIL_SMTP_URL = 'EMAIL_SMTP_URL'
CONFIG.EMAIL_SMTP_PORT = '1234'
CONFIG.EMAIL_SMTP_PORT = 1234
CONFIG.EMAIL_USERNAME = 'user'
CONFIG.EMAIL_PASSWORD = 'pwd'
CONFIG.EMAIL_TLS = true
jest.mock('nodemailer', () => {
return {
@ -25,7 +30,7 @@ jest.mock('nodemailer', () => {
})
describe('sendEmailTranslated', () => {
let result: Record<string, unknown> | null
let result: Record<string, unknown> | boolean | null
describe('config email is false', () => {
beforeEach(async () => {
@ -99,10 +104,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,46 +1,52 @@
import CONFIG from '@/config'
import { backendLogger as logger } from '@/server/logger'
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import path from 'path'
import { createTransport } from 'nodemailer'
import Email from 'email-templates'
import i18n from 'i18n'
import LogError from '@/server/LogError'
import { createTransport } from 'nodemailer'
export const sendEmailTranslated = async (params: {
import { CONFIG } from '@/config'
import { backendLogger as logger } from '@/server/logger'
export const sendEmailTranslated = async ({
receiver,
template,
locals,
}: {
receiver: {
to: string
cc?: string
}
template: string
locals: Record<string, unknown>
}): Promise<Record<string, unknown> | null> => {
let resultSend: Record<string, unknown> | null = null
}): Promise<Record<string, unknown> | boolean | null> => {
// TODO: test the calling order of 'i18n.setLocale' for example: language of logging 'en', language of email receiver 'es', reset language of current user 'de'
// because language of receiver can differ from language of current user who triggers the sending
const rememberLocaleToRestore = i18n.getLocale()
i18n.setLocale('en') // for logging
logger.info(
`send Email: language=${params.locals.locale} to=${params.receiver.to}` +
(params.receiver.cc ? `, cc=${params.receiver.cc}` : '') +
`, subject=${i18n.__('emails.' + params.template + '.subject')}`,
)
if (!CONFIG.EMAIL) {
logger.info(`Emails are disabled via config...`)
return null
}
// because language of receiver can differ from language of current user who triggers the sending
// const rememberLocaleToRestore = i18n.getLocale()
i18n.setLocale('en') // for logging
logger.info(
`send Email: language=${locals.locale as string} to=${receiver.to}` +
(receiver.cc ? `, cc=${receiver.cc}` : '') +
`, subject=${i18n.__('emails.' + template + '.subject')}`,
)
if (CONFIG.EMAIL_TEST_MODUS) {
logger.info(
`Testmodus=ON: change receiver from ${params.receiver.to} to ${CONFIG.EMAIL_TEST_RECEIVER}`,
`Testmodus=ON: change receiver from ${receiver.to} to ${CONFIG.EMAIL_TEST_RECEIVER}`,
)
params.receiver.to = CONFIG.EMAIL_TEST_RECEIVER
receiver.to = CONFIG.EMAIL_TEST_RECEIVER
}
const transport = createTransport({
host: CONFIG.EMAIL_SMTP_URL,
port: Number(CONFIG.EMAIL_SMTP_PORT),
port: CONFIG.EMAIL_SMTP_PORT,
secure: false, // true for 465, false for other ports
requireTLS: CONFIG.EMAIL_TLS,
auth: {
@ -49,7 +55,7 @@ export const sendEmailTranslated = async (params: {
},
})
i18n.setLocale(params.locals.locale as string) // for email
i18n.setLocale(locals.locale as string) // for email
// TESTING: see 'README.md'
const email = new Email({
@ -61,23 +67,16 @@ export const sendEmailTranslated = async (params: {
// i18n, // is only needed if you don't install i18n
})
// ATTENTION: await is needed, because otherwise on send the email gets send in the language of the current user, because below the language gets reset
await email
const resultSend = await email
.send({
template: path.join(__dirname, 'templates', params.template),
message: params.receiver,
locals: params.locals, // the 'locale' in here seems not to be used by 'email-template', because it doesn't work if the language isn't set before by 'i18n.setLocale'
})
.then((result: Record<string, unknown>) => {
resultSend = result
logger.info('Send email successfully !!!')
logger.info('Result: ', result)
template: path.join(__dirname, 'templates', template),
message: receiver,
locals, // the 'locale' in here seems not to be used by 'email-template', because it doesn't work if the language isn't set before by 'i18n.setLocale'
})
.catch((error: unknown) => {
throw new LogError('Error sending notification email', error)
logger.error('Error sending notification email', error)
return false
})
i18n.setLocale(rememberLocaleToRestore)
return resultSend
}

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