Merge branch 'master' into logger_encryptorUtils

This commit is contained in:
Ulf Gebhardt 2023-02-17 11:33:38 +01:00 committed by GitHub
commit 1a8e98cce9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
174 changed files with 7123 additions and 4015 deletions

View File

@ -163,7 +163,6 @@ jobs:
locales_frontend: locales_frontend:
name: Locales - Frontend name: Locales - Frontend
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build_test_frontend]
steps: steps:
########################################################################## ##########################################################################
# CHECKOUT CODE ########################################################## # CHECKOUT CODE ##########################################################
@ -171,20 +170,10 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v3
########################################################################## ##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Frontend)
uses: actions/download-artifact@v3
with:
name: docker-frontend-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/frontend.tar
##########################################################################
# LOCALES FRONTEND ####################################################### # LOCALES FRONTEND #######################################################
########################################################################## ##########################################################################
- name: Frontend | Locales - name: Frontend | Locales
run: docker run --rm gradido/frontend:test yarn run locales run: cd frontend && yarn && yarn run locales
############################################################################## ##############################################################################
# JOB: LINT FRONTEND ######################################################### # JOB: LINT FRONTEND #########################################################
@ -192,7 +181,6 @@ jobs:
lint_frontend: lint_frontend:
name: Lint - Frontend name: Lint - Frontend
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build_test_frontend]
steps: steps:
########################################################################## ##########################################################################
# CHECKOUT CODE ########################################################## # CHECKOUT CODE ##########################################################
@ -200,20 +188,10 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v3
########################################################################## ##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Frontend)
uses: actions/download-artifact@v3
with:
name: docker-frontend-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/frontend.tar
##########################################################################
# LINT FRONTEND ########################################################## # LINT FRONTEND ##########################################################
########################################################################## ##########################################################################
- name: Frontend | Lint - name: Frontend | Lint
run: docker run --rm gradido/frontend:test yarn run lint run: cd frontend && yarn && yarn run lint
############################################################################## ##############################################################################
# JOB: STYLELINT FRONTEND #################################################### # JOB: STYLELINT FRONTEND ####################################################
@ -221,7 +199,6 @@ jobs:
stylelint_frontend: stylelint_frontend:
name: Stylelint - Frontend name: Stylelint - Frontend
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build_test_frontend]
steps: steps:
########################################################################## ##########################################################################
# CHECKOUT CODE ########################################################## # CHECKOUT CODE ##########################################################
@ -229,20 +206,10 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v3
########################################################################## ##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Frontend)
uses: actions/download-artifact@v3
with:
name: docker-frontend-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/frontend.tar
##########################################################################
# STYLELINT FRONTEND ##################################################### # STYLELINT FRONTEND #####################################################
########################################################################## ##########################################################################
- name: Frontend | Stylelint - name: Frontend | Stylelint
run: docker run --rm gradido/frontend:test yarn run stylelint run: cd frontend && yarn && yarn run stylelint
############################################################################## ##############################################################################
# JOB: LINT ADMIN INTERFACE ################################################## # JOB: LINT ADMIN INTERFACE ##################################################
@ -250,7 +217,6 @@ jobs:
lint_admin: lint_admin:
name: Lint - Admin Interface name: Lint - Admin Interface
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build_test_admin]
steps: steps:
########################################################################## ##########################################################################
# CHECKOUT CODE ########################################################## # CHECKOUT CODE ##########################################################
@ -258,28 +224,17 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v3
########################################################################## ##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Admin Interface)
uses: actions/download-artifact@v3
with:
name: docker-admin-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/admin.tar
##########################################################################
# LINT ADMIN INTERFACE ################################################### # LINT ADMIN INTERFACE ###################################################
########################################################################## ##########################################################################
- name: Admin Interface | Lint - name: Admin Interface | Lint
run: docker run --rm gradido/admin:test yarn run lint run: cd admin && yarn && yarn run lint
############################################################################## ##############################################################################
# JOB: STYLELINT ADMIN INTERFACE ############################################## # JOB: STYLELINT ADMIN INTERFACE #############################################
############################################################################## ##############################################################################
stylelint_admin: stylelint_admin:
name: Stylelint - Admin Interface name: Stylelint - Admin Interface
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build_test_admin]
steps: steps:
########################################################################## ##########################################################################
# CHECKOUT CODE ########################################################## # CHECKOUT CODE ##########################################################
@ -287,20 +242,10 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v3
########################################################################## ##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Admin Interface)
uses: actions/download-artifact@v3
with:
name: docker-admin-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/admin.tar
##########################################################################
# STYLELINT ADMIN INTERFACE ############################################## # STYLELINT ADMIN INTERFACE ##############################################
########################################################################## ##########################################################################
- name: Admin Interface | Stylelint - name: Admin Interface | Stylelint
run: docker run --rm gradido/admin:test yarn run stylelint run: cd admin && yarn && yarn run stylelint
############################################################################## ##############################################################################
# JOB: LOCALES ADMIN ######################################################### # JOB: LOCALES ADMIN #########################################################
@ -308,7 +253,6 @@ jobs:
locales_admin: locales_admin:
name: Locales - Admin Interface name: Locales - Admin Interface
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build_test_admin]
steps: steps:
########################################################################## ##########################################################################
# CHECKOUT CODE ########################################################## # CHECKOUT CODE ##########################################################
@ -316,20 +260,10 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v3
########################################################################## ##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Admin Interface)
uses: actions/download-artifact@v3
with:
name: docker-admin-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/admin.tar
##########################################################################
# LOCALES FRONTEND ####################################################### # LOCALES FRONTEND #######################################################
########################################################################## ##########################################################################
- name: admin | Locales - name: Admin | Locales
run: docker run --rm gradido/admin:test yarn run locales run: cd admin && yarn && yarn run locales
############################################################################## ##############################################################################
# JOB: LINT BACKEND ########################################################## # JOB: LINT BACKEND ##########################################################
@ -337,7 +271,6 @@ jobs:
lint_backend: lint_backend:
name: Lint - Backend name: Lint - Backend
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build_test_backend]
steps: steps:
########################################################################## ##########################################################################
# CHECKOUT CODE ########################################################## # CHECKOUT CODE ##########################################################
@ -345,28 +278,35 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v3
########################################################################## ##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Backend)
uses: actions/download-artifact@v3
with:
name: docker-backend-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/backend.tar
##########################################################################
# LINT BACKEND ########################################################### # LINT BACKEND ###########################################################
########################################################################## ##########################################################################
- name: backend | Lint - name: backend | Lint
run: docker run --rm gradido/backend:test yarn run lint run: cd backend && yarn && yarn run lint
##############################################################################
# JOB: LOCALES BACKEND #######################################################
##############################################################################
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 ###################################################### # JOB: LINT DATABASE UP ######################################################
############################################################################## ##############################################################################
lint_database_up: lint_database_up:
name: Lint - Database Up name: Lint - Database Up
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build_test_database_up]
steps: steps:
########################################################################## ##########################################################################
# CHECKOUT CODE ########################################################## # CHECKOUT CODE ##########################################################
@ -374,20 +314,10 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v3
########################################################################## ##########################################################################
# DOWNLOAD DOCKER IMAGE ##################################################
##########################################################################
- name: Download Docker Image (Backend)
uses: actions/download-artifact@v3
with:
name: docker-database-test_up
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/database_up.tar
##########################################################################
# LINT DATABASE ########################################################## # LINT DATABASE ##########################################################
########################################################################## ##########################################################################
- name: database | Lint - name: Database | Lint
run: docker run --rm gradido/database:test_up yarn run lint run: cd database && yarn && yarn run lint
############################################################################## ##############################################################################
# JOB: UNIT TEST FRONTEND ################################################### # JOB: UNIT TEST FRONTEND ###################################################
@ -395,7 +325,6 @@ jobs:
unit_test_frontend: unit_test_frontend:
name: Unit tests - Frontend name: Unit tests - Frontend
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build_test_frontend]
steps: steps:
########################################################################## ##########################################################################
# CHECKOUT CODE ########################################################## # CHECKOUT CODE ##########################################################
@ -403,30 +332,12 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v3
########################################################################## ##########################################################################
# DOWNLOAD DOCKER IMAGES #################################################
##########################################################################
- name: Download Docker Image (Frontend)
uses: actions/download-artifact@v3
with:
name: docker-frontend-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/frontend.tar
##########################################################################
# UNIT TESTS FRONTEND #################################################### # UNIT TESTS FRONTEND ####################################################
########################################################################## ##########################################################################
- name: frontend | Unit tests - name: Frontend | Unit tests
run: | run: |
docker run --env NODE_ENV=test -v ~/coverage:/app/coverage --rm gradido/frontend:test yarn run test cd frontend && yarn && yarn run test
cp -r ~/coverage ./coverage cp -r ./coverage ../
##########################################################################
# COVERAGE REPORT FRONTEND ###############################################
##########################################################################
#- name: frontend | Coverage report
# uses: romeovs/lcov-reporter-action@v0.2.21
# with:
# github-token: ${{ secrets.GITHUB_TOKEN }}
# lcov-file: ./coverage/lcov.info
########################################################################## ##########################################################################
# COVERAGE CHECK FRONTEND ################################################ # COVERAGE CHECK FRONTEND ################################################
########################################################################## ##########################################################################
@ -435,7 +346,7 @@ jobs:
with: with:
report_name: Coverage Frontend report_name: Coverage Frontend
type: lcov type: lcov
result_path: ./coverage/lcov.info result_path: ./frontend/coverage/lcov.info
min_coverage: 95 min_coverage: 95
token: ${{ github.token }} token: ${{ github.token }}
@ -445,7 +356,6 @@ jobs:
unit_test_admin: unit_test_admin:
name: Unit tests - Admin Interface name: Unit tests - Admin Interface
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build_test_admin]
steps: steps:
########################################################################## ##########################################################################
# CHECKOUT CODE ########################################################## # CHECKOUT CODE ##########################################################
@ -453,22 +363,12 @@ jobs:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v3
########################################################################## ##########################################################################
# DOWNLOAD DOCKER IMAGES #################################################
##########################################################################
- name: Download Docker Image (Admin Interface)
uses: actions/download-artifact@v3
with:
name: docker-admin-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/admin.tar
##########################################################################
# UNIT TESTS ADMIN INTERFACE ############################################# # UNIT TESTS ADMIN INTERFACE #############################################
########################################################################## ##########################################################################
- name: Admin Interface | Unit tests - name: Admin Interface | Unit tests
run: | run: |
docker run -v ~/coverage:/app/coverage --rm gradido/admin:test yarn run test cd admin && yarn && yarn run test
cp -r ~/coverage ./coverage cp -r ./coverage ../
########################################################################## ##########################################################################
# COVERAGE CHECK ADMIN INTERFACE ######################################### # COVERAGE CHECK ADMIN INTERFACE #########################################
########################################################################## ##########################################################################
@ -477,8 +377,8 @@ jobs:
with: with:
report_name: Coverage Admin Interface report_name: Coverage Admin Interface
type: lcov type: lcov
result_path: ./coverage/lcov.info result_path: ./admin/coverage/lcov.info
min_coverage: 96 min_coverage: 97
token: ${{ github.token }} token: ${{ github.token }}
############################################################################## ##############################################################################
@ -515,8 +415,9 @@ jobs:
- name: backend | docker-compose database - name: backend | docker-compose database
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps database run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps database
- name: backend Unit tests | test - name: backend Unit tests | test
run: cd database && yarn && yarn build && cd ../backend && yarn && yarn test run: |
# run: docker-compose -f docker-compose.yml -f docker-compose.test.yml exec -T backend yarn test cd database && yarn && yarn build && cd ../backend && yarn && yarn test
cp -r ./coverage ../
########################################################################## ##########################################################################
# COVERAGE CHECK BACKEND ################################################# # COVERAGE CHECK BACKEND #################################################
########################################################################## ##########################################################################
@ -526,7 +427,7 @@ jobs:
report_name: Coverage Backend report_name: Coverage Backend
type: lcov type: lcov
result_path: ./backend/coverage/lcov.info result_path: ./backend/coverage/lcov.info
min_coverage: 78 min_coverage: 80
token: ${{ github.token }} token: ${{ github.token }}
########################################################################## ##########################################################################
@ -558,7 +459,7 @@ jobs:
end-to-end-tests: end-to-end-tests:
name: End-to-End Tests name: End-to-End Tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build_test_mariadb, build_test_database_up, build_test_backend, build_test_admin, build_test_frontend, build_test_nginx] needs: [build_test_mariadb, build_test_database_up, build_test_admin, build_test_frontend, build_test_nginx]
steps: steps:
########################################################################## ##########################################################################
# CHECKOUT CODE ########################################################## # CHECKOUT CODE ##########################################################
@ -582,13 +483,6 @@ jobs:
path: /tmp path: /tmp
- name: Load Docker Image (Database Up) - name: Load Docker Image (Database Up)
run: docker load < /tmp/database_up.tar run: docker load < /tmp/database_up.tar
- name: Download Docker Image (Backend)
uses: actions/download-artifact@v3
with:
name: docker-backend-test
path: /tmp
- name: Load Docker Image (Backend)
run: docker load < /tmp/backend.tar
- name: Download Docker Image (Frontend) - name: Download Docker Image (Frontend)
uses: actions/download-artifact@v3 uses: actions/download-artifact@v3
with: with:
@ -621,7 +515,11 @@ jobs:
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps database run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps database
- name: Boot up test system | docker-compose backend - name: Boot up test system | docker-compose backend
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps backend 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 - name: Sleep for 10 seconds
run: sleep 10s run: sleep 10s
@ -638,6 +536,9 @@ jobs:
- name: Boot up test system | docker-compose frontends - 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 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 - name: Sleep for 15 seconds
run: sleep 15s run: sleep 15s
@ -647,12 +548,12 @@ jobs:
- name: End-to-end tests | run tests - name: End-to-end tests | run tests
id: e2e-tests id: e2e-tests
run: | run: |
cd e2e-tests/cypress/tests/ cd e2e-tests/
yarn yarn
yarn run cypress run --spec cypress/e2e/User.Authentication.feature 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 - name: End-to-end tests | if tests failed, upload screenshots
if: steps.e2e-tests.outcome == 'failure' if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: cypress-screenshots name: cypress-screenshots
path: /home/runner/work/gradido/gradido/e2e-tests/cypress/tests/cypress/screenshots/ path: /home/runner/work/gradido/gradido/e2e-tests/cypress/screenshots/

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

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

View File

@ -4,8 +4,57 @@ 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). Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
#### [1.18.2](https://github.com/gradido/gradido/compare/1.18.1...1.18.2)
- 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)
> 10 February 2023
- chore(release): version 1.18.1 [`#2698`](https://github.com/gradido/gradido/pull/2698)
- fix(frontend): fix is last month for empty form date [`#2697`](https://github.com/gradido/gradido/pull/2697)
- fix(frontend): community link [`#2696`](https://github.com/gradido/gradido/pull/2696)
#### [1.18.0](https://github.com/gradido/gradido/compare/1.17.1...1.18.0)
> 9 February 2023
- feat(release): version 1.18.0 [`#2690`](https://github.com/gradido/gradido/pull/2690)
- refactor(frontend): toast by automatically logged out [`#2681`](https://github.com/gradido/gradido/pull/2681)
- refactor(frontend): change text for gdd_per_link.choose-amount [`#2638`](https://github.com/gradido/gradido/pull/2638)
- fix(backend): emails for deny and delete contribution [`#2688`](https://github.com/gradido/gradido/pull/2688)
- refactor(other): remove config version from `.env.dist` [`#2686`](https://github.com/gradido/gradido/pull/2686)
- refactor(backend): use LogError on contributionMessageResolver [`#2663`](https://github.com/gradido/gradido/pull/2663)
- refactor(backend): get last transaction by only one function [`#2668`](https://github.com/gradido/gradido/pull/2668)
- refactor(other): don't rebuild modul if unit test file has been changed [`#2667`](https://github.com/gradido/gradido/pull/2667)
- refactor(backend): use LogError on contributionLinkResolver [`#2662`](https://github.com/gradido/gradido/pull/2662)
- refactor(backend): remove event protocol config switch [`#2670`](https://github.com/gradido/gradido/pull/2670)
- refactor(backend): event protocol [`#2652`](https://github.com/gradido/gradido/pull/2652)
- refactor(frontend): sidebar becomes smaller when critical phase [`#2649`](https://github.com/gradido/gradido/pull/2649)
- refactor(backend): use LogError on sendEMailTranslated [`#2656`](https://github.com/gradido/gradido/pull/2656)
- refactor(backend): log error class [`#2640`](https://github.com/gradido/gradido/pull/2640)
- feat(backend): add filterState parameter to listAllContributions query [`#2619`](https://github.com/gradido/gradido/pull/2619)
- refactor(frontend): there is no message when a month is fully created [`#2626`](https://github.com/gradido/gradido/pull/2626)
- refactor(frontend): better text alignment on send via link [`#2637`](https://github.com/gradido/gradido/pull/2637)
- refactor(frontend): text changed as indicated in the issues [`#2642`](https://github.com/gradido/gradido/pull/2642)
- refactor(frontend): when you click on create, you will be directed to the form [`#2645`](https://github.com/gradido/gradido/pull/2645)
- feat(backend): federation: separated dht-hub features in new dht-node modul [`#2510`](https://github.com/gradido/gradido/pull/2510)
- refactor(backend): refine assembly of error message in user resolver [`#2636`](https://github.com/gradido/gradido/pull/2636)
- fix(backend): unit tests creations for 31st day [`#2641`](https://github.com/gradido/gradido/pull/2641)
- fix(workflow): properly lint pr - prevent requirement to restart linting [`#2635`](https://github.com/gradido/gradido/pull/2635)
- feat(frontend): 'yes'-button shows which dialog is currently open with a different color [`#2629`](https://github.com/gradido/gradido/pull/2629)
- feat(backend): federation implement multiple apollo graphql endpoints [`#2459`](https://github.com/gradido/gradido/pull/2459)
- refactor(frontend): add legend to all contribution tab, and add tests. [`#2625`](https://github.com/gradido/gradido/pull/2625)
- feat(frontend): unit tests community page [`#2587`](https://github.com/gradido/gradido/pull/2587)
- feat(backend): deny contributions [`#2461`](https://github.com/gradido/gradido/pull/2461)
- refactor(admin): update yarn.lock file of admin. [`#2579`](https://github.com/gradido/gradido/pull/2579)
#### [1.17.1](https://github.com/gradido/gradido/compare/1.17.0...1.17.1) #### [1.17.1](https://github.com/gradido/gradido/compare/1.17.0...1.17.1)
> 20 January 2023
- chore(release): v1.17.1 [`#2588`](https://github.com/gradido/gradido/pull/2588)
- refactor(frontend): change contribution memo add word-break [`#2583`](https://github.com/gradido/gradido/pull/2583) - refactor(frontend): change contribution memo add word-break [`#2583`](https://github.com/gradido/gradido/pull/2583)
- refactor(admin): add text-break on all table memo fields [`#2584`](https://github.com/gradido/gradido/pull/2584) - refactor(admin): add text-break on all table memo fields [`#2584`](https://github.com/gradido/gradido/pull/2584)
- fix(frontend): throw proper frontend warning errors [`#2586`](https://github.com/gradido/gradido/pull/2586) - fix(frontend): throw proper frontend warning errors [`#2586`](https://github.com/gradido/gradido/pull/2586)

View File

@ -1,5 +1,3 @@
CONFIG_VERSION=v1.2022-03-18
GRAPHQL_URI=http://localhost:4000/graphql GRAPHQL_URI=http://localhost:4000/graphql
WALLET_AUTH_URL=http://localhost/authenticate?token={token} WALLET_AUTH_URL=http://localhost/authenticate?token={token}
WALLET_URL=http://localhost/login WALLET_URL=http://localhost/login

View File

@ -3,7 +3,7 @@
"description": "Administraion Interface for Gradido", "description": "Administraion Interface for Gradido",
"main": "index.js", "main": "index.js",
"author": "Moriz Wahl", "author": "Moriz Wahl",
"version": "1.17.1", "version": "1.18.2",
"license": "Apache-2.0", "license": "Apache-2.0",
"private": false, "private": false,
"scripts": { "scripts": {
@ -86,5 +86,10 @@
"> 1%", "> 1%",
"last 2 versions", "last 2 versions",
"not ie <= 10" "not ie <= 10"
] ],
"nodemonConfig": {
"ignore": [
"**/*.spec.js"
]
}
} }

View File

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

View File

@ -10,8 +10,9 @@
> >
<b-button <b-button
v-if="!editContributionLink" v-if="!editContributionLink"
v-b-toggle.newContribution @click="visible = !visible"
class="my-3 d-flex justify-content-left" class="my-3 d-flex justify-content-left"
data-test="new-contribution-link-button"
> >
{{ $t('math.plus') }} {{ $t('contributionLink.newContributionLink') }} {{ $t('math.plus') }} {{ $t('contributionLink.newContributionLink') }}
</b-button> </b-button>

View File

@ -70,8 +70,6 @@ export default {
formatter: (value, key, item) => { formatter: (value, key, item) => {
if (value) { if (value) {
return this.$d(new Date(value)) return this.$d(new Date(value))
} else {
return null
} }
}, },
}, },
@ -81,8 +79,6 @@ export default {
formatter: (value, key, item) => { formatter: (value, key, item) => {
if (value) { if (value) {
return this.$d(new Date(value)) return this.$d(new Date(value))
} else {
return null
} }
}, },
}, },

View File

@ -88,5 +88,16 @@ describe('CreationTransactionList', () => {
expect(toastErrorSpy).toBeCalledWith('OUCH!') expect(toastErrorSpy).toBeCalledWith('OUCH!')
}) })
}) })
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

@ -46,43 +46,45 @@ describe('NavBar', () => {
}) })
describe('Navbar Menu', () => { describe('Navbar Menu', () => {
it('has a link to overview', () => {
expect(wrapper.findAll('.nav-item').at(0).find('a').attributes('href')).toBe('/')
})
it('has a link to /user', () => { it('has a link to /user', () => {
expect(wrapper.findAll('.nav-item').at(1).find('a').attributes('href')).toBe('/user') expect(wrapper.findAll('.nav-item').at(0).find('a').attributes('href')).toBe('/user')
})
it('has a link to /creation', () => {
expect(wrapper.findAll('.nav-item').at(2).find('a').attributes('href')).toBe('/creation')
}) })
it('has a link to /creation-confirm', () => { it('has a link to /creation-confirm', () => {
expect(wrapper.findAll('.nav-item').at(3).find('a').attributes('href')).toBe( expect(wrapper.findAll('.nav-item').at(1).find('a').attributes('href')).toBe(
'/creation-confirm', '/creation-confirm',
) )
}) })
it('has a link to /contribution-links', () => { it('has a link to /contribution-links', () => {
expect(wrapper.findAll('.nav-item').at(4).find('a').attributes('href')).toBe( expect(wrapper.findAll('.nav-item').at(2).find('a').attributes('href')).toBe(
'/contribution-links', '/contribution-links',
) )
}) })
it('has a link to /statistic', () => { it('has a link to /statistic', () => {
expect(wrapper.findAll('.nav-item').at(5).find('a').attributes('href')).toBe('/statistic') expect(wrapper.findAll('.nav-item').at(3).find('a').attributes('href')).toBe('/statistic')
}) })
}) })
describe('wallet', () => { describe('wallet', () => {
const assignLocationSpy = jest.fn() const windowLocationMock = jest.fn()
const windowLocation = window.location
beforeEach(async () => { beforeEach(async () => {
await wrapper.findAll('.nav-item').at(6).find('a').trigger('click') delete window.location
window.location = {
assign: windowLocationMock,
}
await wrapper.findAll('.nav-item').at(5).find('a').trigger('click')
})
afterEach(() => {
delete window.location
window.location = windowLocation
}) })
it.skip('changes window location to wallet', () => { it.skip('changes window location to wallet', () => {
expect(assignLocationSpy).toBeCalledWith('valid-token') expect(windowLocationMock()).toBe('valid-token')
}) })
it('dispatches logout to store', () => { it('dispatches logout to store', () => {
@ -92,12 +94,18 @@ describe('NavBar', () => {
describe('logout', () => { describe('logout', () => {
const windowLocationMock = jest.fn() const windowLocationMock = jest.fn()
const windowLocation = window.location
beforeEach(async () => { beforeEach(async () => {
delete window.location delete window.location
window.location = { window.location = {
assign: windowLocationMock, assign: windowLocationMock,
} }
await wrapper.findAll('.nav-item').at(7).find('a').trigger('click') await wrapper.findAll('.nav-item').at(5).find('a').trigger('click')
})
afterEach(() => {
delete window.location
window.location = windowLocation
}) })
it('redirects to /logout', () => { it('redirects to /logout', () => {

View File

@ -9,15 +9,12 @@
<b-collapse id="nav-collapse" is-nav> <b-collapse id="nav-collapse" is-nav>
<b-navbar-nav> <b-navbar-nav>
<b-nav-item to="/">{{ $t('navbar.overview') }}</b-nav-item>
<b-nav-item to="/user">{{ $t('navbar.user_search') }}</b-nav-item> <b-nav-item to="/user">{{ $t('navbar.user_search') }}</b-nav-item>
<b-nav-item to="/creation">{{ $t('navbar.multi_creation') }}</b-nav-item> <b-nav-item class="bg-color-creation p-1" to="/creation-confirm">
<b-nav-item {{ $t('creation') }}
v-show="$store.state.openCreations > 0" <b-badge v-show="$store.state.openCreations > 0" variant="danger">
class="bg-color-creation p-1" {{ $store.state.openCreations }}
to="/creation-confirm" </b-badge>
>
{{ $store.state.openCreations }} {{ $t('navbar.open_creation') }}
</b-nav-item> </b-nav-item>
<b-nav-item to="/contribution-links"> <b-nav-item to="/contribution-links">
{{ $t('navbar.automaticContributions') }} {{ $t('navbar.automaticContributions') }}
@ -57,7 +54,4 @@ export default {
height: 2rem; height: 2rem;
padding-left: 10px; padding-left: 10px;
} }
.bg-color-creation {
background-color: #cf1010dc;
}
</style> </style>

View File

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

View File

@ -1,6 +1,17 @@
<template> <template>
<div class="open-creations-table"> <div class="open-creations-table">
<b-table-lite :items="items" :fields="fields" caption-top striped hover stacked="md"> <b-table-lite
:items="items"
:fields="fields"
caption-top
striped
hover
stacked="md"
:tbody-tr-class="rowClass"
>
<template #cell(state)="row">
<b-icon :icon="getStatusIcon(row.item.state)"></b-icon>
</template>
<template #cell(bookmark)="row"> <template #cell(bookmark)="row">
<b-button <b-button
variant="danger" variant="danger"
@ -37,6 +48,16 @@
</b-button> </b-button>
</div> </div>
</template> </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"> <template #cell(deny)="row">
<div v-if="$store.state.moderator.id !== row.item.userId"> <div v-if="$store.state.moderator.id !== row.item.userId">
<b-button <b-button
@ -100,6 +121,14 @@ import RowDetails from '../RowDetails.vue'
import EditCreationFormular from '../EditCreationFormular.vue' import EditCreationFormular from '../EditCreationFormular.vue'
import ContributionMessagesList from '../ContributionMessages/ContributionMessagesList.vue' import ContributionMessagesList from '../ContributionMessages/ContributionMessagesList.vue'
const iconMap = {
IN_PROGRESS: 'question-square',
PENDING: 'bell-fill',
CONFIRMED: 'check',
DELETED: 'trash',
DENIED: 'x-circle',
}
export default { export default {
name: 'OpenCreationsTable', name: 'OpenCreationsTable',
mixins: [toggleRowDetails], mixins: [toggleRowDetails],
@ -129,6 +158,14 @@ export default {
} }
}, },
methods: { methods: {
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) { updateCreationData(data) {
const row = data.row const row = data.row
this.$emit('update-contributions', data) this.$emit('update-contributions', data)

View File

@ -1,35 +0,0 @@
<template>
<div class="component-select-users-table">
<b-table-lite :items="items" :fields="fields" caption-top striped hover stacked="md">
<template #cell(bookmark)="row">
<div>
<b-button
v-if="row.item.emailChecked"
variant="warning"
size="md"
@click="$emit('push-item', row.item)"
class="mr-2"
>
<b-icon icon="plus" variant="success"></b-icon>
</b-button>
<div v-else>{{ $t('e_mail') }}{{ $t('math.exclaim') }}</div>
</div>
</template>
</b-table-lite>
</div>
</template>
<script>
export default {
name: 'SelectUsersTable',
props: {
items: {
type: Array,
required: true,
},
fields: {
type: Array,
required: true,
},
},
}
</script>

View File

@ -1,26 +0,0 @@
<template>
<div class="component-selected-users-table">
<b-table-lite :items="items" :fields="fields" caption-top striped hover stacked="md">
<template #cell(bookmark)="row">
<b-button variant="danger" size="md" @click="$emit('remove-item', row.item)" class="mr-2">
<b-icon icon="x" variant="light"></b-icon>
</b-button>
</template>
</b-table-lite>
</div>
</template>
<script>
export default {
name: 'SelectedUsersTable',
props: {
items: {
type: Array,
required: true,
},
fields: {
type: Array,
required: true,
},
},
}
</script>

View File

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

View File

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

View File

@ -1,6 +1,7 @@
{ {
"all_emails": "Alle Nutzer", "all_emails": "Alle Nutzer",
"back": "zurück", "back": "zurück",
"chat": "Chat",
"contributionLink": { "contributionLink": {
"amount": "Betrag", "amount": "Betrag",
"changeSaved": "Änderungen gespeichert", "changeSaved": "Änderungen gespeichert",
@ -29,10 +30,18 @@
"validFrom": "Startdatum", "validFrom": "Startdatum",
"validTo": "Enddatum" "validTo": "Enddatum"
}, },
"contributions": {
"all": "Alle",
"confirms": "Bestätigt",
"deleted": "Gelöscht",
"denied": "Abgelehnt",
"open": "Offen"
},
"created": "Geschöpft",
"createdAt": "Angelegt",
"creation": "Schöpfung", "creation": "Schöpfung",
"creationList": "Schöpfungsliste", "creationList": "Schöpfungsliste",
"creation_form": { "creation_form": {
"creation_failed": "Ausstehende Schöpfung für {email} konnte nicht erzeugt werden.",
"creation_for": "Aktives Grundeinkommen für", "creation_for": "Aktives Grundeinkommen für",
"enter_text": "Text eintragen", "enter_text": "Text eintragen",
"form": "Schöpfungsformular", "form": "Schöpfungsformular",
@ -49,7 +58,6 @@
"update_creation": "Schöpfung aktualisieren" "update_creation": "Schöpfung aktualisieren"
}, },
"creation_for_month": "Schöpfung für Monat", "creation_for_month": "Schöpfung für Monat",
"date": "Datum",
"delete": "Löschen", "delete": "Löschen",
"deleted": "gelöscht", "deleted": "gelöscht",
"deleted_user": "Alle gelöschten Nutzer", "deleted_user": "Alle gelöschten Nutzer",
@ -87,23 +95,19 @@
"lastname": "Nachname", "lastname": "Nachname",
"math": { "math": {
"equals": "=", "equals": "=",
"exclaim": "!",
"pipe": "|", "pipe": "|",
"plus": "+" "plus": "+"
}, },
"message": { "message": {
"request": "Die Anfrage wurde gesendet." "request": "Die Anfrage wurde gesendet."
}, },
"mod": "Mod",
"moderator": "Moderator", "moderator": "Moderator",
"multiple_creation_text": "Bitte wähle ein oder mehrere Mitglieder aus für die du Schöpfen möchtest.",
"name": "Name", "name": "Name",
"navbar": { "navbar": {
"automaticContributions": "Automatische Beiträge", "automaticContributions": "Automatische Beiträge",
"logout": "Abmelden", "logout": "Abmelden",
"multi_creation": "Mehrfachschöpfung",
"my-account": "Mein Konto", "my-account": "Mein Konto",
"open_creation": "Offene Schöpfungen",
"overview": "Übersicht",
"statistic": "Statistik", "statistic": "Statistik",
"user_search": "Nutzersuche" "user_search": "Nutzersuche"
}, },
@ -132,9 +136,7 @@
} }
}, },
"redeemed": "eingelöst", "redeemed": "eingelöst",
"remove": "Entfernen",
"removeNotSelf": "Als Admin/Moderator kannst du dich nicht selber löschen.", "removeNotSelf": "Als Admin/Moderator kannst du dich nicht selber löschen.",
"remove_all": "alle Nutzer entfernen",
"save": "Speichern", "save": "Speichern",
"statistic": { "statistic": {
"activeUsers": "Aktive Mitglieder", "activeUsers": "Aktive Mitglieder",

View File

@ -1,6 +1,7 @@
{ {
"all_emails": "All users", "all_emails": "All users",
"back": "back", "back": "back",
"chat": "Chat",
"contributionLink": { "contributionLink": {
"amount": "Amount", "amount": "Amount",
"changeSaved": "Changes saved", "changeSaved": "Changes saved",
@ -29,10 +30,18 @@
"validFrom": "Start-date", "validFrom": "Start-date",
"validTo": "End-Date" "validTo": "End-Date"
}, },
"contributions": {
"all": "All",
"confirms": "Confirmed",
"deleted": "Deleted",
"denied": "Denied",
"open": "Open"
},
"created": "Confirmed",
"createdAt": "Created",
"creation": "Creation", "creation": "Creation",
"creationList": "Creation list", "creationList": "Creation list",
"creation_form": { "creation_form": {
"creation_failed": "Could not create pending creation for {email}",
"creation_for": "Active Basic Income for", "creation_for": "Active Basic Income for",
"enter_text": "Enter text", "enter_text": "Enter text",
"form": "Creation form", "form": "Creation form",
@ -49,7 +58,6 @@
"update_creation": "Creation update" "update_creation": "Creation update"
}, },
"creation_for_month": "Creation for month", "creation_for_month": "Creation for month",
"date": "Date",
"delete": "Delete", "delete": "Delete",
"deleted": "deleted", "deleted": "deleted",
"deleted_user": "All deleted user", "deleted_user": "All deleted user",
@ -87,23 +95,19 @@
"lastname": "Lastname", "lastname": "Lastname",
"math": { "math": {
"equals": "=", "equals": "=",
"exclaim": "!",
"pipe": "|", "pipe": "|",
"plus": "+" "plus": "+"
}, },
"message": { "message": {
"request": "Request has been sent." "request": "Request has been sent."
}, },
"mod": "Mod",
"moderator": "Moderator", "moderator": "Moderator",
"multiple_creation_text": "Please select one or more members for which you would like to perform creations.",
"name": "Name", "name": "Name",
"navbar": { "navbar": {
"automaticContributions": "Automatic Contributions", "automaticContributions": "Automatic Contributions",
"logout": "Logout", "logout": "Logout",
"multi_creation": "Multiple creation",
"my-account": "My Account", "my-account": "My Account",
"open_creation": "Open creations",
"overview": "Overview",
"statistic": "Statistic", "statistic": "Statistic",
"user_search": "User search" "user_search": "User search"
}, },
@ -132,9 +136,7 @@
} }
}, },
"redeemed": "redeemed", "redeemed": "redeemed",
"remove": "Remove",
"removeNotSelf": "As an admin/moderator, you cannot delete yourself.", "removeNotSelf": "As an admin/moderator, you cannot delete yourself.",
"remove_all": "Remove all users",
"save": "Speichern", "save": "Speichern",
"statistic": { "statistic": {
"activeUsers": "Active members", "activeUsers": "Active members",

View File

@ -0,0 +1,18 @@
import locales from './index.js'
describe('locales', () => {
it('should contain 2 locales', () => {
expect(locales).toHaveLength(2)
})
it('should contain a German locale', () => {
expect(locales).toContainEqual(
expect.objectContaining({
name: 'Deutsch',
code: 'de',
iso: 'de-DE',
enabled: true,
}),
)
})
})

View File

@ -1,10 +1,11 @@
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import ContributionLinks from './ContributionLinks.vue' import ContributionLinks from './ContributionLinks.vue'
import { listContributionLinks } from '@/graphql/listContributionLinks.js' import { listContributionLinks } from '@/graphql/listContributionLinks.js'
import { toastErrorSpy } from '../../test/testSetup'
const localVue = global.localVue const localVue = global.localVue
const apolloQueryMock = jest.fn().mockResolvedValueOnce({ const apolloQueryMock = jest.fn().mockResolvedValue({
data: { data: {
listContributionLinks: { listContributionLinks: {
links: [ links: [
@ -47,12 +48,31 @@ describe('ContributionLinks', () => {
wrapper = Wrapper() wrapper = Wrapper()
}) })
it('calls listContributionLinks', () => { describe('apollo returns', () => {
expect(apolloQueryMock).toBeCalledWith( it('calls listContributionLinks', () => {
expect.objectContaining({ expect(apolloQueryMock).toBeCalledWith(
query: listContributionLinks, expect.objectContaining({
}), query: listContributionLinks,
) }),
)
})
})
describe('query transaction with error', () => {
beforeEach(() => {
apolloQueryMock.mockRejectedValue({ message: 'OUCH!' })
wrapper = Wrapper()
})
it('calls the API', () => {
expect(apolloQueryMock).toBeCalled()
})
it('toast error', () => {
expect(toastErrorSpy).toBeCalledWith(
'listContributionLinks has no result, use default data',
)
})
}) })
}) })
}) })

View File

@ -1,337 +0,0 @@
import { mount } from '@vue/test-utils'
import Creation from './Creation.vue'
import { toastErrorSpy } from '../../test/testSetup'
const localVue = global.localVue
const apolloQueryMock = jest.fn().mockResolvedValue({
data: {
searchUsers: {
userCount: 2,
userList: [
{
userId: 1,
firstName: 'Bibi',
lastName: 'Bloxberg',
email: 'bibi@bloxberg.de',
creation: [200, 400, 600],
emailChecked: true,
},
{
userId: 2,
firstName: 'Benjamin',
lastName: 'Blümchen',
email: 'benjamin@bluemchen.de',
creation: [800, 600, 400],
emailChecked: true,
},
],
},
},
})
const storeCommitMock = jest.fn()
const mocks = {
$t: jest.fn((t, options) => (options ? [t, options] : t)),
$d: jest.fn((d) => d),
$apollo: {
query: apolloQueryMock,
},
$store: {
commit: storeCommitMock,
state: {
userSelectedInMassCreation: [],
},
},
}
describe('Creation', () => {
let wrapper
const Wrapper = () => {
return mount(Creation, { localVue, mocks })
}
describe('mount', () => {
beforeEach(() => {
jest.clearAllMocks()
wrapper = Wrapper()
})
it('has a DIV element with the class.creation', () => {
expect(wrapper.find('div.creation').exists()).toBeTruthy()
})
describe('apollo returns user array', () => {
it('calls the searchUser query', () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
searchText: '',
currentPage: 1,
pageSize: 25,
filters: {
byActivated: true,
byDeleted: false,
},
},
}),
)
})
it('has two rows in the left table', () => {
expect(wrapper.findAll('table').at(0).findAll('tbody > tr')).toHaveLength(2)
})
it('has nwo rows in the right table', () => {
expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(0)
})
it('has correct data in first row ', () => {
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain('Bibi')
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain(
'Bloxberg',
)
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain(
'200 | 400 | 600',
)
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain(
'bibi@bloxberg.de',
)
})
it('has correct data in second row ', () => {
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(1).text()).toContain(
'Benjamin',
)
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(1).text()).toContain(
'Blümchen',
)
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(1).text()).toContain(
'800 | 600 | 400',
)
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(1).text()).toContain(
'benjamin@bluemchen.de',
)
})
})
describe('push item', () => {
beforeEach(() => {
wrapper.findAll('table').at(0).findAll('tbody > tr').at(1).find('button').trigger('click')
})
it('has one item in left table', () => {
expect(wrapper.findAll('table').at(0).findAll('tbody > tr')).toHaveLength(1)
})
it('has one item in right table', () => {
expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(1)
})
it('has the correct user in left table', () => {
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain(
'bibi@bloxberg.de',
)
})
it('has the correct user in right table', () => {
expect(wrapper.findAll('table').at(1).findAll('tbody > tr').at(0).text()).toContain(
'benjamin@bluemchen.de',
)
})
it('updates userSelectedInMassCreation in store', () => {
expect(storeCommitMock).toBeCalledWith('setUserSelectedInMassCreation', [
{
userId: 2,
firstName: 'Benjamin',
lastName: 'Blümchen',
email: 'benjamin@bluemchen.de',
creation: [800, 600, 400],
showDetails: false,
emailChecked: true,
},
])
})
describe('remove item', () => {
beforeEach(async () => {
await wrapper
.findAll('table')
.at(1)
.findAll('tbody > tr')
.at(0)
.find('button')
.trigger('click')
})
it('has two items in left table', () => {
expect(wrapper.findAll('table').at(0).findAll('tbody > tr')).toHaveLength(2)
})
it('has the removed user in first row', () => {
expect(wrapper.findAll('table').at(0).findAll('tbody > tr').at(0).text()).toContain(
'benjamin@bluemchen.de',
)
})
it('has no items in right table', () => {
expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(0)
})
it('commits empty array as userSelectedInMassCreation', () => {
expect(storeCommitMock).toBeCalledWith('setUserSelectedInMassCreation', [])
})
})
describe('remove all bookmarks', () => {
beforeEach(async () => {
jest.clearAllMocks()
await wrapper.find('button.btn-light').trigger('click')
})
it('has no items in right table', () => {
expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(0)
})
it('commits empty array to userSelectedInMassCreation', () => {
expect(storeCommitMock).toBeCalledWith('setUserSelectedInMassCreation', [])
})
it('calls searchUsers', () => {
expect(apolloQueryMock).toBeCalled()
})
})
})
describe('store has items in userSelectedInMassCreation', () => {
beforeEach(() => {
mocks.$store.state.userSelectedInMassCreation = [
{
userId: 2,
firstName: 'Benjamin',
lastName: 'Blümchen',
email: 'benjamin@bluemchen.de',
creation: [800, 600, 400],
showDetails: false,
emailChecked: true,
},
]
wrapper = Wrapper()
})
it('has one item in left table', () => {
expect(wrapper.findAll('table').at(0).findAll('tbody > tr')).toHaveLength(1)
})
it('has one item in right table', () => {
expect(wrapper.findAll('table').at(1).findAll('tbody > tr')).toHaveLength(1)
})
it('has the stored user in second row', () => {
expect(wrapper.findAll('table').at(1).findAll('tbody > tr').at(0).text()).toContain(
'benjamin@bluemchen.de',
)
})
})
describe('failed creations', () => {
beforeEach(async () => {
await wrapper
.findComponent({ name: 'CreationFormular' })
.vm.$emit('toast-failed-creations', ['bibi@bloxberg.de', 'benjamin@bluemchen.de'])
})
it('toasts two error messages', () => {
expect(toastErrorSpy).toBeCalledWith([
'creation_form.creation_failed',
{ email: 'bibi@bloxberg.de' },
])
expect(toastErrorSpy).toBeCalledWith([
'creation_form.creation_failed',
{ email: 'benjamin@bluemchen.de' },
])
})
})
describe('watchers', () => {
beforeEach(() => {
jest.clearAllMocks()
})
describe('search criteria', () => {
beforeEach(async () => {
await wrapper.setData({ criteria: 'XX' })
})
it('calls API when criteria changes', async () => {
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
searchText: 'XX',
currentPage: 1,
pageSize: 25,
filters: {
byActivated: true,
byDeleted: false,
},
},
}),
)
})
describe('reset search criteria', () => {
it('calls the API', async () => {
jest.clearAllMocks()
await wrapper.find('.test-click-clear-criteria').trigger('click')
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
searchText: '',
currentPage: 1,
pageSize: 25,
filters: {
byActivated: true,
byDeleted: false,
},
},
}),
)
})
})
})
it('calls API when currentPage changes', async () => {
await wrapper.setData({ currentPage: 2 })
expect(apolloQueryMock).toBeCalledWith(
expect.objectContaining({
variables: {
searchText: '',
currentPage: 2,
pageSize: 25,
filters: {
byActivated: true,
byDeleted: false,
},
},
}),
)
})
})
describe('apollo returns error', () => {
beforeEach(() => {
apolloQueryMock.mockRejectedValue({
message: 'Ouch',
})
wrapper = Wrapper()
})
it('toasts an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Ouch')
})
})
})
})

View File

@ -1,200 +0,0 @@
<template>
<div class="creation">
<b-row>
<b-col cols="12" lg="6">
<label>{{ $t('user_search') }}</label>
<b-input-group>
<b-form-input
type="text"
class="test-input-criteria"
v-model="criteria"
:placeholder="$t('user_search')"
></b-form-input>
<b-input-group-append class="test-click-clear-criteria" @click="criteria = ''">
<b-input-group-text class="pointer">
<b-icon icon="x" />
</b-input-group-text>
</b-input-group-append>
</b-input-group>
<select-users-table
v-if="itemsList.length > 0"
:items="itemsList"
:fields="Searchfields"
@push-item="pushItem"
/>
<b-pagination
pills
v-model="currentPage"
per-page="perPage"
:total-rows="rows"
align="center"
:hide-ellipsis="true"
></b-pagination>
</b-col>
<b-col cols="12" lg="6" class="shadow p-3 mb-5 rounded bg-info">
<div v-show="itemsMassCreation.length > 0">
<div class="text-right pr-4 mb-1">
<b-button @click="removeAllBookmarks()" variant="light">
<b-icon icon="x" scale="2" variant="danger"></b-icon>
{{ $t('remove_all') }}
</b-button>
</div>
<selected-users-table
class="shadow p-3 mb-5 bg-white rounded"
:items="itemsMassCreation"
:fields="fields"
@remove-item="removeItem"
/>
</div>
<div v-if="itemsMassCreation.length === 0">
{{ $t('multiple_creation_text') }}
</div>
<creation-formular
v-else
type="massCreation"
:creation="creation"
:items="itemsMassCreation"
@remove-all-bookmark="removeAllBookmarks"
@toast-failed-creations="toastFailedCreations"
/>
</b-col>
</b-row>
</div>
</template>
<script>
import CreationFormular from '../components/CreationFormular.vue'
import SelectUsersTable from '../components/Tables/SelectUsersTable.vue'
import SelectedUsersTable from '../components/Tables/SelectedUsersTable.vue'
import { searchUsers } from '../graphql/searchUsers'
import { creationMonths } from '../mixins/creationMonths'
export default {
name: 'Creation',
mixins: [creationMonths],
components: {
CreationFormular,
SelectUsersTable,
SelectedUsersTable,
},
data() {
return {
showArrays: false,
itemsList: [],
itemsMassCreation: this.$store.state.userSelectedInMassCreation,
radioSelectedMass: '',
criteria: '',
rows: 0,
currentPage: 1,
perPage: 25,
now: Date.now(),
}
},
async created() {
await this.getUsers()
},
methods: {
async getUsers() {
this.$apollo
.query({
query: searchUsers,
variables: {
searchText: this.criteria,
currentPage: this.currentPage,
pageSize: this.perPage,
filters: {
byActivated: true,
byDeleted: false,
},
},
fetchPolicy: 'network-only',
})
.then((result) => {
this.rows = result.data.searchUsers.userCount
this.itemsList = result.data.searchUsers.userList.map((user) => {
return {
...user,
showDetails: false,
}
})
if (this.itemsMassCreation.length !== 0) {
const selectedIndices = this.itemsMassCreation.map((item) => item.userId)
this.itemsList = this.itemsList.filter((item) => !selectedIndices.includes(item.userId))
}
})
.catch((error) => {
this.toastError(error.message)
})
},
pushItem(selectedItem) {
this.itemsMassCreation = [
this.itemsList.find((item) => selectedItem.userId === item.userId),
...this.itemsMassCreation,
]
this.itemsList = this.itemsList.filter((item) => selectedItem.userId !== item.userId)
this.$store.commit('setUserSelectedInMassCreation', this.itemsMassCreation)
},
removeItem(selectedItem) {
this.itemsList = [
this.itemsMassCreation.find((item) => selectedItem.userId === item.userId),
...this.itemsList,
]
this.itemsMassCreation = this.itemsMassCreation.filter(
(item) => selectedItem.userId !== item.userId,
)
this.$store.commit('setUserSelectedInMassCreation', this.itemsMassCreation)
},
removeAllBookmarks() {
this.itemsMassCreation = []
this.$store.commit('setUserSelectedInMassCreation', [])
this.getUsers()
},
toastFailedCreations(failedCreations) {
failedCreations.forEach((email) =>
this.toastError(this.$t('creation_form.creation_failed', { email })),
)
},
},
computed: {
Searchfields() {
return [
{ key: 'bookmark', label: 'bookmark' },
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{
key: 'creation',
label: this.creationLabel,
formatter: (value, key, item) => {
return value.join(' | ')
},
},
{ key: 'email', label: this.$t('e_mail') },
]
},
fields() {
return [
{ key: 'email', label: this.$t('e_mail') },
{ key: 'firstName', label: this.$t('firstname') },
{ key: 'lastName', label: this.$t('lastname') },
{
key: 'creation',
label: this.creationLabel,
formatter: (value, key, item) => {
return value.join(' | ')
},
},
{ key: 'bookmark', label: this.$t('remove') },
]
},
},
watch: {
currentPage() {
this.getUsers()
},
criteria() {
this.getUsers()
},
},
}
</script>

View File

@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils'
import CreationConfirm from './CreationConfirm.vue' import CreationConfirm from './CreationConfirm.vue'
import { adminDeleteContribution } from '../graphql/adminDeleteContribution' import { adminDeleteContribution } from '../graphql/adminDeleteContribution'
import { denyContribution } from '../graphql/denyContribution' import { denyContribution } from '../graphql/denyContribution'
import { listUnconfirmedContributions } from '../graphql/listUnconfirmedContributions' import { listAllContributions } from '../graphql/listAllContributions'
import { confirmContribution } from '../graphql/confirmContribution' import { confirmContribution } from '../graphql/confirmContribution'
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup' import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
import VueApollo from 'vue-apollo' import VueApollo from 'vue-apollo'
@ -38,50 +38,68 @@ const mocks = {
const defaultData = () => { const defaultData = () => {
return { return {
listUnconfirmedContributions: [ listAllContributions: {
{ contributionCount: 2,
id: 1, contributionList: [
firstName: 'Bibi', {
lastName: 'Bloxberg', id: 1,
userId: 99, firstName: 'Bibi',
email: 'bibi@bloxberg.de', lastName: 'Bloxberg',
amount: 500, userId: 99,
memo: 'Danke für alles', email: 'bibi@bloxberg.de',
date: new Date(), amount: 500,
moderator: 1, memo: 'Danke für alles',
state: 'PENDING', date: new Date(),
creation: [500, 500, 500], moderator: 1,
messageCount: 0, state: 'PENDING',
}, creation: [500, 500, 500],
{ messagesCount: 0,
id: 2, deniedBy: null,
firstName: 'Räuber', deniedAt: null,
lastName: 'Hotzenplotz', confirmedBy: null,
userId: 100, confirmedAt: null,
email: 'raeuber@hotzenplotz.de', contributionDate: new Date(),
amount: 1000000, deletedBy: null,
memo: 'Gut Ergattert', deletedAt: null,
date: new Date(), createdAt: new Date(),
moderator: 1, },
state: 'PENDING', {
creation: [500, 500, 500], id: 2,
messageCount: 0, firstName: 'Räuber',
}, lastName: 'Hotzenplotz',
], userId: 100,
email: 'raeuber@hotzenplotz.de',
amount: 1000000,
memo: 'Gut Ergattert',
date: new Date(),
moderator: 1,
state: 'PENDING',
creation: [500, 500, 500],
messagesCount: 0,
deniedBy: null,
deniedAt: null,
confirmedBy: null,
confirmedAt: null,
contributionDate: new Date(),
deletedBy: null,
deletedAt: null,
createdAt: new Date(),
},
],
},
} }
} }
describe('CreationConfirm', () => { describe('CreationConfirm', () => {
let wrapper let wrapper
const listUnconfirmedContributionsMock = jest.fn()
const adminDeleteContributionMock = jest.fn() const adminDeleteContributionMock = jest.fn()
const adminDenyContributionMock = jest.fn() const adminDenyContributionMock = jest.fn()
const confirmContributionMock = jest.fn() const confirmContributionMock = jest.fn()
mockClient.setRequestHandler( mockClient.setRequestHandler(
listUnconfirmedContributions, listAllContributions,
listUnconfirmedContributionsMock jest
.fn()
.mockRejectedValueOnce({ message: 'Ouch!' }) .mockRejectedValueOnce({ message: 'Ouch!' })
.mockResolvedValue({ data: defaultData() }), .mockResolvedValue({ data: defaultData() }),
) )
@ -117,6 +135,10 @@ describe('CreationConfirm', () => {
it('toast an error message', () => { it('toast an error message', () => {
expect(toastErrorSpy).toBeCalledWith('Ouch!') expect(toastErrorSpy).toBeCalledWith('Ouch!')
}) })
it('has statusFilter ["IN_PROGRESS", "PENDING"]', () => {
expect(wrapper.vm.statusFilter).toEqual(['IN_PROGRESS', 'PENDING'])
})
}) })
describe('server response is succes', () => { describe('server response is succes', () => {
@ -125,17 +147,7 @@ describe('CreationConfirm', () => {
}) })
it('has two pending creations', () => { it('has two pending creations', () => {
expect(wrapper.vm.pendingCreations).toHaveLength(2) expect(wrapper.find('tbody').findAll('tr')).toHaveLength(2)
})
})
describe('store', () => {
it('commits resetOpenCreations to store', () => {
expect(storeCommitMock).toBeCalledWith('resetOpenCreations')
})
it('commits setOpenCreations to store', () => {
expect(storeCommitMock).toBeCalledWith('setOpenCreations', 2)
}) })
}) })
@ -259,7 +271,7 @@ describe('CreationConfirm', () => {
describe('deny creation', () => { describe('deny creation', () => {
beforeEach(async () => { beforeEach(async () => {
await wrapper.findAll('tr').at(1).findAll('button').at(2).trigger('click') await wrapper.findAll('tr').at(1).findAll('button').at(1).trigger('click')
}) })
it('opens the overlay', () => { it('opens the overlay', () => {
@ -316,5 +328,94 @@ 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()
})
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()
})
})
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', () => {
expect(refetchSpy).toBeCalled()
})
})
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({
statusFilter: ['IN_PROGRESS', 'PENDING', 'CONFIRMED', 'DENIED', 'DELETED'],
})
})
it('refetches contributions', () => {
expect(refetchSpy).toBeCalled()
})
})
})
})
describe('update status', () => {
beforeEach(async () => {
await wrapper.findComponent({ name: 'OpenCreationsTable' }).vm.$emit('update-state', 2)
})
it.skip('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')
})
})
}) })
}) })

View File

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

View File

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

View File

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

View File

@ -45,7 +45,7 @@ describe('router', () => {
describe('routes', () => { describe('routes', () => {
it('has nine routes defined', () => { it('has nine routes defined', () => {
expect(routes).toHaveLength(9) expect(routes).toHaveLength(8)
}) })
it('has "/overview" as default', async () => { it('has "/overview" as default', async () => {
@ -67,13 +67,6 @@ describe('router', () => {
}) })
}) })
describe('creation', () => {
it('loads the "Creation" component', async () => {
const component = await routes.find((r) => r.path === '/creation').component()
expect(component.default.name).toBe('Creation')
})
})
describe('creation-confirm', () => { describe('creation-confirm', () => {
it('loads the "CreationConfirm" component', async () => { it('loads the "CreationConfirm" component', async () => {
const component = await routes.find((r) => r.path === '/creation-confirm').component() const component = await routes.find((r) => r.path === '/creation-confirm').component()

View File

@ -19,10 +19,6 @@ const routes = [
path: '/user', path: '/user',
component: () => import('@/pages/UserSearch.vue'), component: () => import('@/pages/UserSearch.vue'),
}, },
{
path: '/creation',
component: () => import('@/pages/Creation.vue'),
},
{ {
path: '/creation-confirm', path: '/creation-confirm',
component: () => import('@/pages/CreationConfirm.vue'), component: () => import('@/pages/CreationConfirm.vue'),

View File

@ -1,5 +1,3 @@
CONFIG_VERSION=v14.2022-12-22
# Server # Server
PORT=4000 PORT=4000
JWT_SECRET=secret123 JWT_SECRET=secret123
@ -55,16 +53,9 @@ EMAIL_CODE_REQUEST_TIME=10
# Webhook # Webhook
WEBHOOK_ELOPAGE_SECRET=secret WEBHOOK_ELOPAGE_SECRET=secret
# EventProtocol
EVENT_PROTOCOL_DISABLED=false
# SET LOG LEVEL AS NEEDED IN YOUR .ENV # SET LOG LEVEL AS NEEDED IN YOUR .ENV
# POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal # POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal
# LOG_LEVEL=info # LOG_LEVEL=info
# Federation # Federation
# if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen FEDERATION_VALIDATE_COMMUNITY_TIMER=60000
# on an hash created from this topic
# FEDERATION_DHT_TOPIC=GRADIDO_HUB
# FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f
# FEDERATION_COMMUNITY_URL=http://localhost:4000/api

View File

@ -54,10 +54,5 @@ EMAIL_CODE_REQUEST_TIME=$EMAIL_CODE_REQUEST_TIME
# Webhook # Webhook
WEBHOOK_ELOPAGE_SECRET=$WEBHOOK_ELOPAGE_SECRET WEBHOOK_ELOPAGE_SECRET=$WEBHOOK_ELOPAGE_SECRET
# EventProtocol
EVENT_PROTOCOL_DISABLED=$EVENT_PROTOCOL_DISABLED
# Federation # Federation
FEDERATION_DHT_TOPIC=$FEDERATION_DHT_TOPIC FEDERATION_VALIDATE_COMMUNITY_TIMER=$FEDERATION_VALIDATE_COMMUNITY_TIMER
FEDERATION_DHT_SEED=$FEDERATION_DHT_SEED
FEDERATION_COMMUNITY_URL=$FEDERATION_COMMUNITY_URL

9
backend/.env.test_e2e Normal file
View File

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

View File

@ -1,6 +1,6 @@
{ {
"name": "gradido-backend", "name": "gradido-backend",
"version": "1.17.1", "version": "1.18.2",
"description": "Gradido unified backend providing an API-Service for Gradido Transactions", "description": "Gradido unified backend providing an API-Service for Gradido Transactions",
"main": "src/index.ts", "main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/backend", "repository": "https://github.com/gradido/gradido/backend",
@ -15,10 +15,10 @@
"lint": "eslint --max-warnings=0 --ext .js,.ts .", "lint": "eslint --max-warnings=0 --ext .js,.ts .",
"test": "cross-env TZ=UTC NODE_ENV=development jest --runInBand --coverage --forceExit --detectOpenHandles", "test": "cross-env TZ=UTC NODE_ENV=development jest --runInBand --coverage --forceExit --detectOpenHandles",
"seed": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/seeds/index.ts", "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" "klicktipp": "cross-env TZ=UTC NODE_ENV=development ts-node -r tsconfig-paths/register src/util/klicktipp.ts",
"locales": "scripts/sort.sh"
}, },
"dependencies": { "dependencies": {
"@hyperswarm/dht": "^6.2.0",
"apollo-server-express": "^2.25.2", "apollo-server-express": "^2.25.2",
"await-semaphore": "^0.1.3", "await-semaphore": "^0.1.3",
"axios": "^0.21.1", "axios": "^0.21.1",
@ -30,6 +30,7 @@
"email-templates": "^10.0.1", "email-templates": "^10.0.1",
"express": "^4.17.1", "express": "^4.17.1",
"graphql": "^15.5.1", "graphql": "^15.5.1",
"graphql-request": "5.0.0",
"i18n": "^0.15.1", "i18n": "^0.15.1",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
@ -72,5 +73,10 @@
"ts-node": "^10.0.0", "ts-node": "^10.0.0",
"tsconfig-paths": "^3.14.0", "tsconfig-paths": "^3.14.0",
"typescript": "^4.3.4" "typescript": "^4.3.4"
},
"nodemonConfig": {
"ignore": [
"**/*.test.ts"
]
} }
} }

25
backend/scripts/sort.sh Executable file
View File

@ -0,0 +1,25 @@
#!/bin/bash
ROOT_DIR=$(dirname "$0")/..
tmp=$(mktemp)
exit_code=0
for locale_file in $ROOT_DIR/src/locales/*.json
do
jq -f $(dirname "$0")/sort_filter.jq $locale_file > "$tmp"
if [ "$*" == "--fix" ]
then
mv "$tmp" $locale_file
else
if diff -q "$tmp" $locale_file > /dev/null ;
then
: # all good
else
exit_code=$?
echo "$(basename -- $locale_file) is not sorted by keys"
fi
fi
done
exit $exit_code

View File

@ -0,0 +1,13 @@
def walk(f):
. as $in
| if type == "object" then
reduce keys_unsorted[] as $key
( {}; . + { ($key): ($in[$key] | walk(f)) } ) | f
elif type == "array" then map( walk(f) ) | f
else f
end;
def keys_sort_by(f):
to_entries | sort_by(.key|f ) | from_entries;
walk(if type == "object" then keys_sort_by(ascii_upcase) else . end)

View File

@ -10,14 +10,14 @@ Decimal.set({
}) })
const constants = { const constants = {
DB_VERSION: '0059-add_hide_amount_to_users', DB_VERSION: '0060-update_communities_table',
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0 DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
LOG4JS_CONFIG: 'log4js-config.json', LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info // default log level on production should be info
LOG_LEVEL: process.env.LOG_LEVEL || 'info', LOG_LEVEL: process.env.LOG_LEVEL || 'info',
CONFIG_VERSION: { CONFIG_VERSION: {
DEFAULT: 'DEFAULT', DEFAULT: 'DEFAULT',
EXPECTED: 'v14.2022-12-22', EXPECTED: 'v15.2023-02-07',
CURRENT: '', CURRENT: '',
}, },
} }
@ -99,11 +99,6 @@ const webhook = {
WEBHOOK_ELOPAGE_SECRET: process.env.WEBHOOK_ELOPAGE_SECRET || 'secret', WEBHOOK_ELOPAGE_SECRET: process.env.WEBHOOK_ELOPAGE_SECRET || 'secret',
} }
const eventProtocol = {
// global switch to enable writing of EventProtocol-Entries
EVENT_PROTOCOL_DISABLED: process.env.EVENT_PROTOCOL_DISABLED === 'true' || false,
}
// This is needed by graphql-directive-auth // This is needed by graphql-directive-auth
process.env.APP_SECRET = server.JWT_SECRET process.env.APP_SECRET = server.JWT_SECRET
@ -120,14 +115,8 @@ if (
} }
const federation = { const federation = {
FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || null, FEDERATION_VALIDATE_COMMUNITY_TIMER:
FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null, Number(process.env.FEDERATION_VALIDATE_COMMUNITY_TIMER) || 60000,
FEDERATION_COMMUNITY_URL:
process.env.FEDERATION_COMMUNITY_URL === undefined
? null
: process.env.FEDERATION_COMMUNITY_URL.endsWith('/')
? process.env.FEDERATION_COMMUNITY_URL
: process.env.FEDERATION_COMMUNITY_URL + '/',
} }
const CONFIG = { const CONFIG = {
@ -139,7 +128,6 @@ const CONFIG = {
...email, ...email,
...loginServer, ...loginServer,
...webhook, ...webhook,
...eventProtocol,
...federation, ...federation,
} }

View File

@ -10,6 +10,7 @@ import {
sendAccountMultiRegistrationEmail, sendAccountMultiRegistrationEmail,
sendContributionConfirmedEmail, sendContributionConfirmedEmail,
sendContributionDeniedEmail, sendContributionDeniedEmail,
sendContributionDeletedEmail,
sendResetPasswordEmail, sendResetPasswordEmail,
sendTransactionLinkRedeemedEmail, sendTransactionLinkRedeemedEmail,
sendTransactionReceivedEmail, sendTransactionReceivedEmail,
@ -438,6 +439,84 @@ describe('sendEmailVariants', () => {
}) })
}) })
describe('sendContributionDeletedEmail', () => {
beforeAll(async () => {
result = await sendContributionDeletedEmail({
firstName: 'Peter',
lastName: 'Lustig',
email: 'peter@lustig.de',
language: 'en',
senderFirstName: 'Bibi',
senderLastName: 'Bloxberg',
contributionMemo: 'My contribution.',
})
})
describe('calls "sendEmailTranslated"', () => {
it('with expected parameters', () => {
expect(sendEmailTranslated).toBeCalledWith({
receiver: {
to: 'Peter Lustig <peter@lustig.de>',
},
template: 'contributionDeleted',
locals: {
firstName: 'Peter',
lastName: 'Lustig',
locale: 'en',
senderFirstName: 'Bibi',
senderLastName: 'Bloxberg',
contributionMemo: 'My contribution.',
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
communityURL: CONFIG.COMMUNITY_URL,
},
})
})
it('has expected result', () => {
expect(result).toMatchObject({
envelope: {
from: 'info@gradido.net',
to: ['peter@lustig.de'],
},
message: expect.any(String),
originalMessage: expect.objectContaining({
to: 'Peter Lustig <peter@lustig.de>',
from: 'Gradido (do not answer) <info@gradido.net>',
attachments: [],
subject: 'Gradido: Your common good contribution was deleted',
html: expect.any(String),
text: expect.stringContaining('GRADIDO: YOUR COMMON GOOD CONTRIBUTION WAS DELETED'),
}),
})
expect(result.originalMessage.html).toContain('<!DOCTYPE html>')
expect(result.originalMessage.html).toContain('<html lang="en">')
expect(result.originalMessage.html).toContain(
'<title>Gradido: Your common good contribution was deleted</title>',
)
expect(result.originalMessage.html).toContain(
'>Gradido: Your common good contribution was deleted</h1>',
)
expect(result.originalMessage.html).toContain('Hello Peter Lustig')
expect(result.originalMessage.html).toContain(
'Your public good contribution “My contribution.” was deleted by Bibi Bloxberg.',
)
expect(result.originalMessage.html).toContain(
'To see your common good contributions and related messages, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!',
)
expect(result.originalMessage.html).toContain(
`Link to your account: <a href="${CONFIG.EMAIL_LINK_OVERVIEW}">${CONFIG.EMAIL_LINK_OVERVIEW}</a>`,
)
expect(result.originalMessage.html).toContain('Please do not reply to this email!')
expect(result.originalMessage.html).toContain('Kind regards,<br>your Gradido team')
expect(result.originalMessage.html).toContain('—————')
expect(result.originalMessage.html).toContain(
'<div style="position: relative; left: -22px;"><img src="https://gdd.gradido.net/img/brand/green.png" width="200" alt="Gradido-Akademie Logo"></div><br>Gradido-Akademie<br>Institut für Wirtschaftsbionik<br>Pfarrweg 2<br>74653 Künzelsau<br>Deutschland<br><a href="mailto:support@supportmail.com">support@supportmail.com</a><br><a href="http://localhost/">http://localhost/</a>',
)
})
})
})
describe('sendResetPasswordEmail', () => { describe('sendResetPasswordEmail', () => {
beforeAll(async () => { beforeAll(async () => {
result = await sendResetPasswordEmail({ result = await sendResetPasswordEmail({

View File

@ -103,6 +103,32 @@ export const sendContributionConfirmedEmail = (data: {
}) })
} }
export const sendContributionDeletedEmail = (data: {
firstName: string
lastName: string
email: string
language: string
senderFirstName: string
senderLastName: string
contributionMemo: string
}): Promise<Record<string, unknown> | null> => {
return sendEmailTranslated({
receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` },
template: 'contributionDeleted',
locals: {
firstName: data.firstName,
lastName: data.lastName,
locale: data.language,
senderFirstName: data.senderFirstName,
senderLastName: data.senderLastName,
contributionMemo: data.contributionMemo,
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
communityURL: CONFIG.COMMUNITY_URL,
},
})
}
export const sendContributionDeniedEmail = (data: { export const sendContributionDeniedEmail = (data: {
firstName: string firstName: string
lastName: string lastName: string

View File

@ -0,0 +1,16 @@
doctype html
html(lang=locale)
head
title= t('emails.contributionDeleted.subject')
body
h1(style='margin-bottom: 24px;')= t('emails.contributionDeleted.subject')
#container.col
include ../hello.pug
p= t('emails.contributionDeleted.commonGoodContributionDeleted', { senderFirstName, senderLastName, contributionMemo })
p= t('emails.contributionDeleted.toSeeContributionsAndMessages')
p
= t('emails.general.linkToYourAccount')
= " "
a(href=overviewURL) #{overviewURL}
p= t('emails.general.pleaseDoNotReply')
include ../greatingFormularImprint.pug

View File

@ -0,0 +1 @@
= t('emails.contributionDeleted.subject')

View File

@ -1,517 +1,212 @@
import { EventProtocol } from '@entity/EventProtocol' import { EventProtocol as DbEvent } from '@entity/EventProtocol'
import decimal from 'decimal.js-light' import Decimal from 'decimal.js-light'
import { EventProtocolType } from './EventProtocolType' import { EventProtocolType } from './EventProtocolType'
export class EventBasic { export const Event = (
type: string type: EventProtocolType,
createdAt: Date userId: number,
} xUserId: number | null = null,
export class EventBasicUserId extends EventBasic { xCommunityId: number | null = null,
userId: number transactionId: number | null = null,
contributionId: number | null = null,
amount: Decimal | null = null,
messageId: number | null = null,
): DbEvent => {
const event = new DbEvent()
event.type = type
event.userId = userId
event.xUserId = xUserId
event.xCommunityId = xCommunityId
event.transactionId = transactionId
event.contributionId = contributionId
event.amount = amount
event.messageId = messageId
return event
} }
export class EventBasicTx extends EventBasicUserId { export const EVENT_CONTRIBUTION_CREATE = async (
transactionId: number userId: number,
amount: decimal contributionId: number,
} amount: Decimal,
): Promise<DbEvent> =>
export class EventBasicTxX extends EventBasicTx { Event(
xUserId: number EventProtocolType.CONTRIBUTION_CREATE,
xCommunityId: number userId,
} null,
null,
export class EventBasicCt extends EventBasicUserId { null,
contributionId: number contributionId,
amount: decimal amount,
} ).save()
export class EventBasicCtX extends EventBasicCt { export const EVENT_CONTRIBUTION_DELETE = async (
xUserId: number userId: number,
xCommunityId: number contributionId: number,
} amount: Decimal,
): Promise<DbEvent> =>
export class EventBasicRedeem extends EventBasicUserId { Event(
transactionId?: number EventProtocolType.CONTRIBUTION_DELETE,
contributionId?: number userId,
} null,
null,
export class EventBasicCtMsg extends EventBasicCt { null,
messageId: number contributionId,
} amount,
).save()
export class EventVisitGradido extends EventBasic {}
export class EventRegister extends EventBasicUserId {} export const EVENT_CONTRIBUTION_UPDATE = async (
export class EventRedeemRegister extends EventBasicRedeem {} userId: number,
export class EventVerifyRedeem extends EventBasicRedeem {} contributionId: number,
export class EventInactiveAccount extends EventBasicUserId {} amount: Decimal,
export class EventSendConfirmationEmail extends EventBasicUserId {} ): Promise<DbEvent> =>
export class EventSendAccountMultiRegistrationEmail extends EventBasicUserId {} Event(
export class EventSendForgotPasswordEmail extends EventBasicUserId {} EventProtocolType.CONTRIBUTION_UPDATE,
export class EventSendTransactionSendEmail extends EventBasicTxX {} userId,
export class EventSendTransactionReceiveEmail extends EventBasicTxX {} null,
export class EventSendTransactionLinkRedeemEmail extends EventBasicTxX {} null,
export class EventSendAddedContributionEmail extends EventBasicCt {} null,
export class EventSendContributionConfirmEmail extends EventBasicCt {} contributionId,
export class EventConfirmationEmail extends EventBasicUserId {} amount,
export class EventRegisterEmailKlicktipp extends EventBasicUserId {} ).save()
export class EventLogin extends EventBasicUserId {}
export class EventLogout extends EventBasicUserId {} export const EVENT_ADMIN_CONTRIBUTION_CREATE = async (
export class EventRedeemLogin extends EventBasicRedeem {} userId: number,
export class EventActivateAccount extends EventBasicUserId {} contributionId: number,
export class EventPasswordChange extends EventBasicUserId {} amount: Decimal,
export class EventTransactionSend extends EventBasicTxX {} ): Promise<DbEvent> =>
export class EventTransactionSendRedeem extends EventBasicTxX {} Event(
export class EventTransactionRepeateRedeem extends EventBasicTxX {} EventProtocolType.ADMIN_CONTRIBUTION_CREATE,
export class EventTransactionCreation extends EventBasicTx {} userId,
export class EventTransactionReceive extends EventBasicTxX {} null,
export class EventTransactionReceiveRedeem extends EventBasicTxX {} null,
export class EventContributionCreate extends EventBasicCt {} null,
export class EventAdminContributionCreate extends EventBasicCt {} contributionId,
export class EventAdminContributionDelete extends EventBasicCt {} amount,
export class EventAdminContributionUpdate extends EventBasicCt {} ).save()
export class EventUserCreateContributionMessage extends EventBasicCtMsg {}
export class EventAdminCreateContributionMessage extends EventBasicCtMsg {} export const EVENT_ADMIN_CONTRIBUTION_UPDATE = async (
export class EventContributionDelete extends EventBasicCt {} userId: number,
export class EventContributionUpdate extends EventBasicCt {} contributionId: number,
export class EventContributionConfirm extends EventBasicCtX {} amount: Decimal,
export class EventContributionDeny extends EventBasicCtX {} ): Promise<DbEvent> =>
export class EventContributionLinkDefine extends EventBasicCt {} Event(
export class EventContributionLinkActivateRedeem extends EventBasicCt {} EventProtocolType.ADMIN_CONTRIBUTION_UPDATE,
export class EventDeleteUser extends EventBasicUserId {} userId,
export class EventUndeleteUser extends EventBasicUserId {} null,
export class EventChangeUserRole extends EventBasicUserId {} null,
export class EventAdminUpdateContribution extends EventBasicCt {} null,
export class EventAdminDeleteContribution extends EventBasicCt {} contributionId,
export class EventCreateContributionLink extends EventBasicCt {} amount,
export class EventDeleteContributionLink extends EventBasicCt {} ).save()
export class EventUpdateContributionLink extends EventBasicCt {}
export const EVENT_ADMIN_CONTRIBUTION_DELETE = async (
export class Event { userId: number,
constructor() contributionId: number,
constructor(event?: EventProtocol) { amount: Decimal,
if (event) { ): Promise<DbEvent> =>
this.id = event.id Event(
this.type = event.type EventProtocolType.ADMIN_CONTRIBUTION_DELETE,
this.createdAt = event.createdAt userId,
this.userId = event.userId null,
this.xUserId = event.xUserId null,
this.xCommunityId = event.xCommunityId null,
this.transactionId = event.transactionId contributionId,
this.contributionId = event.contributionId amount,
this.amount = event.amount ).save()
}
} export const EVENT_CONTRIBUTION_CONFIRM = async (
userId: number,
public setEventBasic(): Event { contributionId: number,
this.type = EventProtocolType.BASIC amount: Decimal,
this.createdAt = new Date() ): Promise<DbEvent> =>
Event(
return this EventProtocolType.CONTRIBUTION_CONFIRM,
} userId,
null,
public setEventVisitGradido(): Event { null,
this.setEventBasic() null,
this.type = EventProtocolType.VISIT_GRADIDO contributionId,
amount,
return this ).save()
}
export const EVENT_ADMIN_CONTRIBUTION_DENY = async (
public setEventRegister(ev: EventRegister): Event { userId: number,
this.setByBasicUser(ev.userId) xUserId: number,
this.type = EventProtocolType.REGISTER contributionId: number,
amount: Decimal,
return this ): Promise<DbEvent> =>
} Event(
EventProtocolType.ADMIN_CONTRIBUTION_DENY,
public setEventRedeemRegister(ev: EventRedeemRegister): Event { userId,
this.setByBasicRedeem(ev.userId, ev.transactionId, ev.contributionId) xUserId,
this.type = EventProtocolType.REDEEM_REGISTER null,
null,
return this contributionId,
} amount,
).save()
public setEventVerifyRedeem(ev: EventVerifyRedeem): Event {
this.setByBasicRedeem(ev.userId, ev.transactionId, ev.contributionId) export const EVENT_TRANSACTION_SEND = async (
this.type = EventProtocolType.VERIFY_REDEEM userId: number,
xUserId: number,
return this transactionId: number,
} amount: Decimal,
): Promise<DbEvent> =>
public setEventInactiveAccount(ev: EventInactiveAccount): Event { Event(
this.setByBasicUser(ev.userId) EventProtocolType.TRANSACTION_SEND,
this.type = EventProtocolType.INACTIVE_ACCOUNT userId,
xUserId,
return this null,
} transactionId,
null,
public setEventSendConfirmationEmail(ev: EventSendConfirmationEmail): Event { amount,
this.setByBasicUser(ev.userId) ).save()
this.type = EventProtocolType.SEND_CONFIRMATION_EMAIL
export const EVENT_TRANSACTION_RECEIVE = async (
return this userId: number,
} xUserId: number,
transactionId: number,
public setEventSendAccountMultiRegistrationEmail( amount: Decimal,
ev: EventSendAccountMultiRegistrationEmail, ): Promise<DbEvent> =>
): Event { Event(
this.setByBasicUser(ev.userId) EventProtocolType.TRANSACTION_RECEIVE,
this.type = EventProtocolType.SEND_ACCOUNT_MULTIREGISTRATION_EMAIL userId,
xUserId,
return this null,
} transactionId,
null,
public setEventSendForgotPasswordEmail(ev: EventSendForgotPasswordEmail): Event { amount,
this.setByBasicUser(ev.userId) ).save()
this.type = EventProtocolType.SEND_FORGOT_PASSWORD_EMAIL
export const EVENT_LOGIN = async (userId: number): Promise<DbEvent> =>
return this Event(EventProtocolType.LOGIN, userId, null, null, null, null, null, null).save()
}
export const EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL = async (
public setEventSendTransactionSendEmail(ev: EventSendTransactionSendEmail): Event { userId: number,
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId) ): Promise<DbEvent> => Event(EventProtocolType.SEND_ACCOUNT_MULTIREGISTRATION_EMAIL, userId).save()
this.type = EventProtocolType.SEND_TRANSACTION_SEND_EMAIL
export const EVENT_SEND_CONFIRMATION_EMAIL = async (userId: number): Promise<DbEvent> =>
return this Event(EventProtocolType.SEND_CONFIRMATION_EMAIL, userId).save()
}
export const EVENT_ADMIN_SEND_CONFIRMATION_EMAIL = async (userId: number): Promise<DbEvent> =>
public setEventSendTransactionReceiveEmail(ev: EventSendTransactionReceiveEmail): Event { Event(EventProtocolType.ADMIN_SEND_CONFIRMATION_EMAIL, userId).save()
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId)
this.type = EventProtocolType.SEND_TRANSACTION_RECEIVE_EMAIL /* export const EVENT_REDEEM_REGISTER = async (
userId: number,
return this transactionId: number | null = null,
} contributionId: number | null = null,
): Promise<Event> =>
public setEventSendTransactionLinkRedeemEmail(ev: EventSendTransactionLinkRedeemEmail): Event { Event(
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId) EventProtocolType.REDEEM_REGISTER,
this.type = EventProtocolType.SEND_TRANSACTION_LINK_REDEEM_EMAIL userId,
null,
return this null,
} transactionId,
contributionId,
public setEventSendAddedContributionEmail(ev: EventSendAddedContributionEmail): Event { ).save()
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount) */
this.type = EventProtocolType.SEND_ADDED_CONTRIBUTION_EMAIL
export const EVENT_REGISTER = async (userId: number): Promise<DbEvent> =>
return this Event(EventProtocolType.REGISTER, userId).save()
}
export const EVENT_ACTIVATE_ACCOUNT = async (userId: number): Promise<DbEvent> =>
public setEventSendContributionConfirmEmail(ev: EventSendContributionConfirmEmail): Event { Event(EventProtocolType.ACTIVATE_ACCOUNT, userId).save()
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.SEND_CONTRIBUTION_CONFIRM_EMAIL
return this
}
public setEventConfirmationEmail(ev: EventConfirmationEmail): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.CONFIRM_EMAIL
return this
}
public setEventRegisterEmailKlicktipp(ev: EventRegisterEmailKlicktipp): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.REGISTER_EMAIL_KLICKTIPP
return this
}
public setEventLogin(ev: EventLogin): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.LOGIN
return this
}
public setEventLogout(ev: EventLogout): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.LOGOUT
return this
}
public setEventRedeemLogin(ev: EventRedeemLogin): Event {
this.setByBasicRedeem(ev.userId, ev.transactionId, ev.contributionId)
this.type = EventProtocolType.REDEEM_LOGIN
return this
}
public setEventActivateAccount(ev: EventActivateAccount): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.ACTIVATE_ACCOUNT
return this
}
public setEventPasswordChange(ev: EventPasswordChange): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.PASSWORD_CHANGE
return this
}
public setEventTransactionSend(ev: EventTransactionSend): Event {
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId)
this.type = EventProtocolType.TRANSACTION_SEND
return this
}
public setEventTransactionSendRedeem(ev: EventTransactionSendRedeem): Event {
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId)
this.type = EventProtocolType.TRANSACTION_SEND_REDEEM
return this
}
public setEventTransactionRepeateRedeem(ev: EventTransactionRepeateRedeem): Event {
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId)
this.type = EventProtocolType.TRANSACTION_REPEATE_REDEEM
return this
}
public setEventTransactionCreation(ev: EventTransactionCreation): Event {
this.setByBasicTx(ev.userId, ev.transactionId, ev.amount)
this.type = EventProtocolType.TRANSACTION_CREATION
return this
}
public setEventTransactionReceive(ev: EventTransactionReceive): Event {
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId)
this.type = EventProtocolType.TRANSACTION_RECEIVE
return this
}
public setEventTransactionReceiveRedeem(ev: EventTransactionReceiveRedeem): Event {
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId)
this.type = EventProtocolType.TRANSACTION_RECEIVE_REDEEM
return this
}
public setEventContributionCreate(ev: EventContributionCreate): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.CONTRIBUTION_CREATE
return this
}
public setEventAdminContributionCreate(ev: EventAdminContributionCreate): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.ADMIN_CONTRIBUTION_CREATE
return this
}
public setEventAdminContributionDelete(ev: EventAdminContributionDelete): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.ADMIN_CONTRIBUTION_DELETE
return this
}
public setEventAdminContributionUpdate(ev: EventAdminContributionUpdate): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.ADMIN_CONTRIBUTION_UPDATE
return this
}
public setEventUserCreateContributionMessage(ev: EventUserCreateContributionMessage): Event {
this.setByBasicCtMsg(ev.userId, ev.contributionId, ev.amount, ev.messageId)
this.type = EventProtocolType.USER_CREATE_CONTRIBUTION_MESSAGE
return this
}
public setEventAdminCreateContributionMessage(ev: EventAdminCreateContributionMessage): Event {
this.setByBasicCtMsg(ev.userId, ev.contributionId, ev.amount, ev.messageId)
this.type = EventProtocolType.ADMIN_CREATE_CONTRIBUTION_MESSAGE
return this
}
public setEventContributionDelete(ev: EventContributionDelete): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.CONTRIBUTION_DELETE
return this
}
public setEventContributionUpdate(ev: EventContributionUpdate): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.CONTRIBUTION_UPDATE
return this
}
public setEventContributionConfirm(ev: EventContributionConfirm): Event {
this.setByBasicCtX(ev.userId, ev.contributionId, ev.amount, ev.xUserId, ev.xCommunityId)
this.type = EventProtocolType.CONTRIBUTION_CONFIRM
return this
}
public setEventContributionDeny(ev: EventContributionDeny): Event {
this.setByBasicCtX(ev.userId, ev.contributionId, ev.amount, ev.xUserId, ev.xCommunityId)
this.type = EventProtocolType.CONTRIBUTION_DENY
return this
}
public setEventContributionLinkDefine(ev: EventContributionLinkDefine): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.CONTRIBUTION_LINK_DEFINE
return this
}
public setEventContributionLinkActivateRedeem(ev: EventContributionLinkActivateRedeem): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.CONTRIBUTION_LINK_ACTIVATE_REDEEM
return this
}
public setEventDeleteUser(ev: EventDeleteUser): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.DELETE_USER
return this
}
public setEventUndeleteUser(ev: EventUndeleteUser): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.UNDELETE_USER
return this
}
public setEventChangeUserRole(ev: EventChangeUserRole): Event {
this.setByBasicUser(ev.userId)
this.type = EventProtocolType.CHANGE_USER_ROLE
return this
}
public setEventAdminUpdateContribution(ev: EventAdminUpdateContribution): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.ADMIN_UPDATE_CONTRIBUTION
return this
}
public setEventAdminDeleteContribution(ev: EventAdminDeleteContribution): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.ADMIN_DELETE_CONTRIBUTION
return this
}
public setEventCreateContributionLink(ev: EventCreateContributionLink): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.CREATE_CONTRIBUTION_LINK
return this
}
public setEventDeleteContributionLink(ev: EventDeleteContributionLink): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.DELETE_CONTRIBUTION_LINK
return this
}
public setEventUpdateContributionLink(ev: EventUpdateContributionLink): Event {
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
this.type = EventProtocolType.UPDATE_CONTRIBUTION_LINK
return this
}
setByBasicUser(userId: number): Event {
this.setEventBasic()
this.userId = userId
return this
}
setByBasicTx(userId: number, transactionId: number, amount: decimal): Event {
this.setByBasicUser(userId)
this.transactionId = transactionId
this.amount = amount
return this
}
setByBasicTxX(
userId: number,
transactionId: number,
amount: decimal,
xUserId: number,
xCommunityId: number,
): Event {
this.setByBasicTx(userId, transactionId, amount)
this.xUserId = xUserId
this.xCommunityId = xCommunityId
return this
}
setByBasicCt(userId: number, contributionId: number, amount: decimal): Event {
this.setByBasicUser(userId)
this.contributionId = contributionId
this.amount = amount
return this
}
setByBasicCtMsg(
userId: number,
contributionId: number,
amount: decimal,
messageId: number,
): Event {
this.setByBasicCt(userId, contributionId, amount)
this.messageId = messageId
return this
}
setByBasicCtX(
userId: number,
contributionId: number,
amount: decimal,
xUserId: number,
xCommunityId: number,
): Event {
this.setByBasicCt(userId, contributionId, amount)
this.xUserId = xUserId
this.xCommunityId = xCommunityId
return this
}
setByBasicRedeem(userId: number, transactionId?: number, contributionId?: number): Event {
this.setByBasicUser(userId)
if (transactionId) this.transactionId = transactionId
if (contributionId) this.contributionId = contributionId
return this
}
id: number
type: string
createdAt: Date
userId: number
xUserId?: number
xCommunityId?: number
transactionId?: number
contributionId?: number
amount?: decimal
messageId?: number
}

View File

@ -1,41 +0,0 @@
import { Event } from '@/event/Event'
import { backendLogger as logger } from '@/server/logger'
import { EventProtocol } from '@entity/EventProtocol'
import CONFIG from '@/config'
class EventProtocolEmitter {
/* }extends EventEmitter { */
private events: Event[]
/*
public addEvent(event: Event) {
this.events.push(event)
}
public getEvents(): Event[] {
return this.events
}
*/
public isDisabled() {
logger.info(`EventProtocol - isDisabled=${CONFIG.EVENT_PROTOCOL_DISABLED}`)
return CONFIG.EVENT_PROTOCOL_DISABLED === true
}
public async writeEvent(event: Event): Promise<void> {
if (!eventProtocol.isDisabled()) {
logger.info(`writeEvent(${JSON.stringify(event)})`)
const dbEvent = new EventProtocol()
dbEvent.type = event.type
dbEvent.createdAt = event.createdAt
dbEvent.userId = event.userId
if (event.xUserId) dbEvent.xUserId = event.xUserId
if (event.xCommunityId) dbEvent.xCommunityId = event.xCommunityId
if (event.contributionId) dbEvent.contributionId = event.contributionId
if (event.transactionId) dbEvent.transactionId = event.transactionId
if (event.amount) dbEvent.amount = event.amount
await dbEvent.save()
}
}
}
export const eventProtocol = new EventProtocolEmitter()

View File

@ -1,49 +1,50 @@
export enum EventProtocolType { export enum EventProtocolType {
BASIC = 'BASIC', // VISIT_GRADIDO = 'VISIT_GRADIDO',
VISIT_GRADIDO = 'VISIT_GRADIDO',
REGISTER = 'REGISTER', REGISTER = 'REGISTER',
REDEEM_REGISTER = 'REDEEM_REGISTER', REDEEM_REGISTER = 'REDEEM_REGISTER',
VERIFY_REDEEM = 'VERIFY_REDEEM', // VERIFY_REDEEM = 'VERIFY_REDEEM',
INACTIVE_ACCOUNT = 'INACTIVE_ACCOUNT', // INACTIVE_ACCOUNT = 'INACTIVE_ACCOUNT',
SEND_CONFIRMATION_EMAIL = 'SEND_CONFIRMATION_EMAIL', SEND_CONFIRMATION_EMAIL = 'SEND_CONFIRMATION_EMAIL',
ADMIN_SEND_CONFIRMATION_EMAIL = 'ADMIN_SEND_CONFIRMATION_EMAIL',
SEND_ACCOUNT_MULTIREGISTRATION_EMAIL = 'SEND_ACCOUNT_MULTIREGISTRATION_EMAIL', SEND_ACCOUNT_MULTIREGISTRATION_EMAIL = 'SEND_ACCOUNT_MULTIREGISTRATION_EMAIL',
CONFIRM_EMAIL = 'CONFIRM_EMAIL', // CONFIRM_EMAIL = 'CONFIRM_EMAIL',
REGISTER_EMAIL_KLICKTIPP = 'REGISTER_EMAIL_KLICKTIPP', // REGISTER_EMAIL_KLICKTIPP = 'REGISTER_EMAIL_KLICKTIPP',
LOGIN = 'LOGIN', LOGIN = 'LOGIN',
LOGOUT = 'LOGOUT', // LOGOUT = 'LOGOUT',
REDEEM_LOGIN = 'REDEEM_LOGIN', // REDEEM_LOGIN = 'REDEEM_LOGIN',
ACTIVATE_ACCOUNT = 'ACTIVATE_ACCOUNT', ACTIVATE_ACCOUNT = 'ACTIVATE_ACCOUNT',
SEND_FORGOT_PASSWORD_EMAIL = 'SEND_FORGOT_PASSWORD_EMAIL', // SEND_FORGOT_PASSWORD_EMAIL = 'SEND_FORGOT_PASSWORD_EMAIL',
PASSWORD_CHANGE = 'PASSWORD_CHANGE', // PASSWORD_CHANGE = 'PASSWORD_CHANGE',
SEND_TRANSACTION_SEND_EMAIL = 'SEND_TRANSACTION_SEND_EMAIL', // SEND_TRANSACTION_SEND_EMAIL = 'SEND_TRANSACTION_SEND_EMAIL',
SEND_TRANSACTION_RECEIVE_EMAIL = 'SEND_TRANSACTION_RECEIVE_EMAIL', // SEND_TRANSACTION_RECEIVE_EMAIL = 'SEND_TRANSACTION_RECEIVE_EMAIL',
TRANSACTION_SEND = 'TRANSACTION_SEND', TRANSACTION_SEND = 'TRANSACTION_SEND',
TRANSACTION_SEND_REDEEM = 'TRANSACTION_SEND_REDEEM', // TRANSACTION_SEND_REDEEM = 'TRANSACTION_SEND_REDEEM',
TRANSACTION_REPEATE_REDEEM = 'TRANSACTION_REPEATE_REDEEM', // TRANSACTION_REPEATE_REDEEM = 'TRANSACTION_REPEATE_REDEEM',
TRANSACTION_CREATION = 'TRANSACTION_CREATION', // TRANSACTION_CREATION = 'TRANSACTION_CREATION',
TRANSACTION_RECEIVE = 'TRANSACTION_RECEIVE', TRANSACTION_RECEIVE = 'TRANSACTION_RECEIVE',
TRANSACTION_RECEIVE_REDEEM = 'TRANSACTION_RECEIVE_REDEEM', // TRANSACTION_RECEIVE_REDEEM = 'TRANSACTION_RECEIVE_REDEEM',
SEND_TRANSACTION_LINK_REDEEM_EMAIL = 'SEND_TRANSACTION_LINK_REDEEM_EMAIL', // SEND_TRANSACTION_LINK_REDEEM_EMAIL = 'SEND_TRANSACTION_LINK_REDEEM_EMAIL',
SEND_ADDED_CONTRIBUTION_EMAIL = 'SEND_ADDED_CONTRIBUTION_EMAIL', // SEND_ADDED_CONTRIBUTION_EMAIL = 'SEND_ADDED_CONTRIBUTION_EMAIL',
SEND_CONTRIBUTION_CONFIRM_EMAIL = 'SEND_CONTRIBUTION_CONFIRM_EMAIL', // SEND_CONTRIBUTION_CONFIRM_EMAIL = 'SEND_CONTRIBUTION_CONFIRM_EMAIL',
CONTRIBUTION_CREATE = 'CONTRIBUTION_CREATE', CONTRIBUTION_CREATE = 'CONTRIBUTION_CREATE',
CONTRIBUTION_CONFIRM = 'CONTRIBUTION_CONFIRM', CONTRIBUTION_CONFIRM = 'CONTRIBUTION_CONFIRM',
CONTRIBUTION_DENY = 'CONTRIBUTION_DENY', // CONTRIBUTION_DENY = 'CONTRIBUTION_DENY',
CONTRIBUTION_LINK_DEFINE = 'CONTRIBUTION_LINK_DEFINE', // CONTRIBUTION_LINK_DEFINE = 'CONTRIBUTION_LINK_DEFINE',
CONTRIBUTION_LINK_ACTIVATE_REDEEM = 'CONTRIBUTION_LINK_ACTIVATE_REDEEM', // CONTRIBUTION_LINK_ACTIVATE_REDEEM = 'CONTRIBUTION_LINK_ACTIVATE_REDEEM',
CONTRIBUTION_DELETE = 'CONTRIBUTION_DELETE', CONTRIBUTION_DELETE = 'CONTRIBUTION_DELETE',
CONTRIBUTION_UPDATE = 'CONTRIBUTION_UPDATE', CONTRIBUTION_UPDATE = 'CONTRIBUTION_UPDATE',
ADMIN_CONTRIBUTION_CREATE = 'ADMIN_CONTRIBUTION_CREATE', ADMIN_CONTRIBUTION_CREATE = 'ADMIN_CONTRIBUTION_CREATE',
ADMIN_CONTRIBUTION_DELETE = 'ADMIN_CONTRIBUTION_DELETE', ADMIN_CONTRIBUTION_DELETE = 'ADMIN_CONTRIBUTION_DELETE',
ADMIN_CONTRIBUTION_DENY = 'ADMIN_CONTRIBUTION_DENY',
ADMIN_CONTRIBUTION_UPDATE = 'ADMIN_CONTRIBUTION_UPDATE', ADMIN_CONTRIBUTION_UPDATE = 'ADMIN_CONTRIBUTION_UPDATE',
USER_CREATE_CONTRIBUTION_MESSAGE = 'USER_CREATE_CONTRIBUTION_MESSAGE', // USER_CREATE_CONTRIBUTION_MESSAGE = 'USER_CREATE_CONTRIBUTION_MESSAGE',
ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE', // ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE',
DELETE_USER = 'DELETE_USER', // DELETE_USER = 'DELETE_USER',
UNDELETE_USER = 'UNDELETE_USER', // UNDELETE_USER = 'UNDELETE_USER',
CHANGE_USER_ROLE = 'CHANGE_USER_ROLE', // CHANGE_USER_ROLE = 'CHANGE_USER_ROLE',
ADMIN_UPDATE_CONTRIBUTION = 'ADMIN_UPDATE_CONTRIBUTION', // ADMIN_UPDATE_CONTRIBUTION = 'ADMIN_UPDATE_CONTRIBUTION',
ADMIN_DELETE_CONTRIBUTION = 'ADMIN_DELETE_CONTRIBUTION', // ADMIN_DELETE_CONTRIBUTION = 'ADMIN_DELETE_CONTRIBUTION',
CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK', // CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK',
DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK', // DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK',
UPDATE_CONTRIBUTION_LINK = 'UPDATE_CONTRIBUTION_LINK', // UPDATE_CONTRIBUTION_LINK = 'UPDATE_CONTRIBUTION_LINK',
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,6 +15,8 @@ import { calculateDecay } from '@/util/decay'
import { RIGHTS } from '@/auth/RIGHTS' import { RIGHTS } from '@/auth/RIGHTS'
import { GdtResolver } from './GdtResolver' import { GdtResolver } from './GdtResolver'
import { getLastTransaction } from './util/getLastTransaction'
@Resolver() @Resolver()
export class BalanceResolver { export class BalanceResolver {
@Authorized([RIGHTS.BALANCE]) @Authorized([RIGHTS.BALANCE])
@ -32,7 +34,7 @@ export class BalanceResolver {
const lastTransaction = context.lastTransaction const lastTransaction = context.lastTransaction
? context.lastTransaction ? context.lastTransaction
: await dbTransaction.findOne({ userId: user.id }, { order: { id: 'DESC' } }) : await getLastTransaction(user.id)
logger.debug(`lastTransaction=${lastTransaction}`) logger.debug(`lastTransaction=${lastTransaction}`)

View File

@ -313,27 +313,6 @@ describe('Contribution Links', () => {
) )
}) })
it('returns an error if name is an empty string', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: createContributionLink,
variables: {
...variables,
name: '',
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('The name must be initialized')],
}),
)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('The name must be initialized')
})
it('returns an error if name is shorter than 5 characters', async () => { it('returns an error if name is shorter than 5 characters', async () => {
jest.clearAllMocks() jest.clearAllMocks()
await expect( await expect(
@ -376,27 +355,6 @@ describe('Contribution Links', () => {
expect(logger.error).toBeCalledWith('The value of name is too long', 101) expect(logger.error).toBeCalledWith('The value of name is too long', 101)
}) })
it('returns an error if memo is an empty string', async () => {
jest.clearAllMocks()
await expect(
mutate({
mutation: createContributionLink,
variables: {
...variables,
memo: '',
},
}),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('The memo must be initialized')],
}),
)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('The memo must be initialized')
})
it('returns an error if memo is shorter than 5 characters', async () => { it('returns an error if memo is shorter than 5 characters', async () => {
jest.clearAllMocks() jest.clearAllMocks()
await expect( await expect(

View File

@ -40,20 +40,12 @@ export class ContributionLinkResolver {
}: ContributionLinkArgs, }: ContributionLinkArgs,
): Promise<ContributionLink> { ): Promise<ContributionLink> {
isStartEndDateValid(validFrom, validTo) isStartEndDateValid(validFrom, validTo)
// TODO: this should be enforced by the schema.
if (!name) {
throw new LogError('The name must be initialized')
}
if (name.length < CONTRIBUTIONLINK_NAME_MIN_CHARS) { if (name.length < CONTRIBUTIONLINK_NAME_MIN_CHARS) {
throw new LogError('The value of name is too short', name.length) throw new LogError('The value of name is too short', name.length)
} }
if (name.length > CONTRIBUTIONLINK_NAME_MAX_CHARS) { if (name.length > CONTRIBUTIONLINK_NAME_MAX_CHARS) {
throw new LogError('The value of name is too long', name.length) throw new LogError('The value of name is too long', name.length)
} }
// TODO: this should be enforced by the schema.
if (!memo) {
throw new LogError('The memo must be initialized')
}
if (memo.length < MEMO_MIN_CHARS) { if (memo.length < MEMO_MIN_CHARS) {
throw new LogError('The value of memo is too short', memo.length) throw new LogError('The value of memo is too short', memo.length)
} }

View File

@ -99,14 +99,18 @@ describe('ContributionMessageResolver', () => {
}), }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('ContributionMessage was not sent successfully')], errors: [
new GraphQLError(
'ContributionMessage was not sent successfully: Error: Contribution not found',
),
],
}), }),
) )
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith( expect(logger.error).toBeCalledWith(
'ContributionMessage was not sent successfully', 'ContributionMessage was not sent successfully: Error: Contribution not found',
new Error('Contribution not found'), new Error('Contribution not found'),
) )
}) })
@ -135,14 +139,18 @@ describe('ContributionMessageResolver', () => {
}), }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('ContributionMessage was not sent successfully')], errors: [
new GraphQLError(
'ContributionMessage was not sent successfully: Error: Admin can not answer on his own contribution',
),
],
}), }),
) )
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith( expect(logger.error).toBeCalledWith(
'ContributionMessage was not sent successfully', 'ContributionMessage was not sent successfully: Error: Admin can not answer on his own contribution',
new Error('Admin can not answer on his own contribution'), new Error('Admin can not answer on his own contribution'),
) )
}) })
@ -229,14 +237,18 @@ describe('ContributionMessageResolver', () => {
}), }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('ContributionMessage was not sent successfully')], errors: [
new GraphQLError(
'ContributionMessage was not sent successfully: Error: Contribution not found',
),
],
}), }),
) )
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith( expect(logger.error).toBeCalledWith(
'ContributionMessage was not sent successfully', 'ContributionMessage was not sent successfully: Error: Contribution not found',
new Error('Contribution not found'), new Error('Contribution not found'),
) )
}) })
@ -257,14 +269,18 @@ describe('ContributionMessageResolver', () => {
}), }),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
errors: [new GraphQLError('ContributionMessage was not sent successfully')], errors: [
new GraphQLError(
'ContributionMessage was not sent successfully: Error: Can not send message to contribution of another user',
),
],
}), }),
) )
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith( expect(logger.error).toBeCalledWith(
'ContributionMessage was not sent successfully', 'ContributionMessage was not sent successfully: Error: Can not send message to contribution of another user',
new Error('Can not send message to contribution of another user'), new Error('Can not send message to contribution of another user'),
) )
}) })

View File

@ -54,7 +54,7 @@ export class ContributionMessageResolver {
await queryRunner.commitTransaction() await queryRunner.commitTransaction()
} catch (e) { } catch (e) {
await queryRunner.rollbackTransaction() await queryRunner.rollbackTransaction()
throw new LogError('ContributionMessage was not sent successfully', e) throw new LogError(`ContributionMessage was not sent successfully: ${e}`, e)
} finally { } finally {
await queryRunner.release() await queryRunner.release()
} }
@ -144,7 +144,7 @@ export class ContributionMessageResolver {
await queryRunner.commitTransaction() await queryRunner.commitTransaction()
} catch (e) { } catch (e) {
await queryRunner.rollbackTransaction() await queryRunner.rollbackTransaction()
throw new LogError('ContributionMessage was not sent successfully', e) throw new LogError(`ContributionMessage was not sent successfully: ${e}`, e)
} finally { } finally {
await queryRunner.release() await queryRunner.release()
} }

File diff suppressed because it is too large Load Diff

View File

@ -37,24 +37,26 @@ import {
} from './util/creations' } from './util/creations'
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS, FULL_CREATION_AVAILABLE } from './const/const' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS, FULL_CREATION_AVAILABLE } from './const/const'
import { import {
Event, EVENT_CONTRIBUTION_CREATE,
EventContributionCreate, EVENT_CONTRIBUTION_DELETE,
EventContributionDelete, EVENT_CONTRIBUTION_UPDATE,
EventContributionUpdate, EVENT_ADMIN_CONTRIBUTION_CREATE,
EventContributionConfirm, EVENT_ADMIN_CONTRIBUTION_UPDATE,
EventAdminContributionCreate, EVENT_ADMIN_CONTRIBUTION_DELETE,
EventAdminContributionDelete, EVENT_CONTRIBUTION_CONFIRM,
EventAdminContributionUpdate, EVENT_ADMIN_CONTRIBUTION_DENY,
} from '@/event/Event' } from '@/event/Event'
import { eventProtocol } from '@/event/EventProtocolEmitter'
import { calculateDecay } from '@/util/decay' import { calculateDecay } from '@/util/decay'
import { import {
sendContributionConfirmedEmail, sendContributionConfirmedEmail,
sendContributionDeletedEmail,
sendContributionDeniedEmail, sendContributionDeniedEmail,
} from '@/emails/sendEmailVariants' } from '@/emails/sendEmailVariants'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
import LogError from '@/server/LogError' import LogError from '@/server/LogError'
import { getLastTransaction } from './util/getLastTransaction'
@Resolver() @Resolver()
export class ContributionResolver { export class ContributionResolver {
@Authorized([RIGHTS.CREATE_CONTRIBUTION]) @Authorized([RIGHTS.CREATE_CONTRIBUTION])
@ -71,8 +73,6 @@ export class ContributionResolver {
throw new LogError('Memo text is too long', memo.length) throw new LogError('Memo text is too long', memo.length)
} }
const event = new Event()
const user = getUser(context) const user = getUser(context)
const creations = await getUserCreation(user.id, clientTimezoneOffset) const creations = await getUserCreation(user.id, clientTimezoneOffset)
logger.trace('creations', creations) logger.trace('creations', creations)
@ -91,11 +91,7 @@ export class ContributionResolver {
logger.trace('contribution to save', contribution) logger.trace('contribution to save', contribution)
await DbContribution.save(contribution) await DbContribution.save(contribution)
const eventCreateContribution = new EventContributionCreate() await EVENT_CONTRIBUTION_CREATE(user.id, contribution.id, amount)
eventCreateContribution.userId = user.id
eventCreateContribution.amount = amount
eventCreateContribution.contributionId = contribution.id
await eventProtocol.writeEvent(event.setEventContributionCreate(eventCreateContribution))
return new UnconfirmedContribution(contribution, user, creations) return new UnconfirmedContribution(contribution, user, creations)
} }
@ -106,7 +102,6 @@ export class ContributionResolver {
@Arg('id', () => Int) id: number, @Arg('id', () => Int) id: number,
@Ctx() context: Context, @Ctx() context: Context,
): Promise<boolean> { ): Promise<boolean> {
const event = new Event()
const user = getUser(context) const user = getUser(context)
const contribution = await DbContribution.findOne(id) const contribution = await DbContribution.findOne(id)
if (!contribution) { if (!contribution) {
@ -124,11 +119,7 @@ export class ContributionResolver {
contribution.deletedAt = new Date() contribution.deletedAt = new Date()
await contribution.save() await contribution.save()
const eventDeleteContribution = new EventContributionDelete() await EVENT_CONTRIBUTION_DELETE(user.id, contribution.id, contribution.amount)
eventDeleteContribution.userId = user.id
eventDeleteContribution.contributionId = contribution.id
eventDeleteContribution.amount = contribution.amount
await eventProtocol.writeEvent(event.setEventContributionDelete(eventDeleteContribution))
const res = await contribution.softRemove() const res = await contribution.softRemove()
return !!res return !!res
@ -190,6 +181,7 @@ export class ContributionResolver {
.select('c') .select('c')
.from(DbContribution, 'c') .from(DbContribution, 'c')
.innerJoinAndSelect('c.user', 'u') .innerJoinAndSelect('c.user', 'u')
.leftJoinAndSelect('c.messages', 'm')
.where(where) .where(where)
.orderBy('c.createdAt', order) .orderBy('c.createdAt', order)
.limit(pageSize) .limit(pageSize)
@ -275,13 +267,7 @@ export class ContributionResolver {
contributionToUpdate.updatedAt = new Date() contributionToUpdate.updatedAt = new Date()
DbContribution.save(contributionToUpdate) DbContribution.save(contributionToUpdate)
const event = new Event() await EVENT_CONTRIBUTION_UPDATE(user.id, contributionId, amount)
const eventUpdateContribution = new EventContributionUpdate()
eventUpdateContribution.userId = user.id
eventUpdateContribution.contributionId = contributionId
eventUpdateContribution.amount = amount
await eventProtocol.writeEvent(event.setEventContributionUpdate(eventUpdateContribution))
return new UnconfirmedContribution(contributionToUpdate, user, creations) return new UnconfirmedContribution(contributionToUpdate, user, creations)
} }
@ -317,7 +303,6 @@ export class ContributionResolver {
) )
} }
const event = new Event()
const moderator = getUser(context) const moderator = getUser(context)
logger.trace('moderator: ', moderator.id) logger.trace('moderator: ', moderator.id)
const creations = await getUserCreation(emailContact.userId, clientTimezoneOffset) const creations = await getUserCreation(emailContact.userId, clientTimezoneOffset)
@ -339,13 +324,7 @@ export class ContributionResolver {
await DbContribution.save(contribution) await DbContribution.save(contribution)
const eventAdminCreateContribution = new EventAdminContributionCreate() await EVENT_ADMIN_CONTRIBUTION_CREATE(moderator.id, contribution.id, amount)
eventAdminCreateContribution.userId = moderator.id
eventAdminCreateContribution.amount = amount
eventAdminCreateContribution.contributionId = contribution.id
await eventProtocol.writeEvent(
event.setEventAdminContributionCreate(eventAdminCreateContribution),
)
return getUserCreation(emailContact.userId, clientTimezoneOffset) return getUserCreation(emailContact.userId, clientTimezoneOffset)
} }
@ -440,14 +419,7 @@ export class ContributionResolver {
result.creation = await getUserCreation(emailContact.user.id, clientTimezoneOffset) result.creation = await getUserCreation(emailContact.user.id, clientTimezoneOffset)
const event = new Event() await EVENT_ADMIN_CONTRIBUTION_UPDATE(emailContact.user.id, contributionToUpdate.id, amount)
const eventAdminContributionUpdate = new EventAdminContributionUpdate()
eventAdminContributionUpdate.userId = emailContact.user.id
eventAdminContributionUpdate.amount = amount
eventAdminContributionUpdate.contributionId = contributionToUpdate.id
await eventProtocol.writeEvent(
event.setEventAdminContributionUpdate(eventAdminContributionUpdate),
)
return result return result
} }
@ -518,15 +490,9 @@ export class ContributionResolver {
await contribution.save() await contribution.save()
const res = await contribution.softRemove() const res = await contribution.softRemove()
const event = new Event() await EVENT_ADMIN_CONTRIBUTION_DELETE(contribution.userId, contribution.id, contribution.amount)
const eventAdminContributionDelete = new EventAdminContributionDelete()
eventAdminContributionDelete.userId = contribution.userId sendContributionDeletedEmail({
eventAdminContributionDelete.amount = contribution.amount
eventAdminContributionDelete.contributionId = contribution.id
await eventProtocol.writeEvent(
event.setEventAdminContributionDelete(eventAdminContributionDelete),
)
sendContributionDeniedEmail({
firstName: user.firstName, firstName: user.firstName,
lastName: user.lastName, lastName: user.lastName,
email: user.emailContact.email, email: user.emailContact.email,
@ -582,16 +548,11 @@ export class ContributionResolver {
const queryRunner = getConnection().createQueryRunner() const queryRunner = getConnection().createQueryRunner()
await queryRunner.connect() await queryRunner.connect()
await queryRunner.startTransaction('REPEATABLE READ') // 'READ COMMITTED') await queryRunner.startTransaction('REPEATABLE READ') // 'READ COMMITTED')
try {
const lastTransaction = await queryRunner.manager
.createQueryBuilder()
.select('transaction')
.from(DbTransaction, 'transaction')
.where('transaction.userId = :id', { id: contribution.userId })
.orderBy('transaction.id', 'DESC')
.getOne()
logger.info('lastTransaction ID', lastTransaction ? lastTransaction.id : 'undefined')
const lastTransaction = await getLastTransaction(contribution.userId)
logger.info('lastTransaction ID', lastTransaction ? lastTransaction.id : 'undefined')
try {
let newBalance = new Decimal(0) let newBalance = new Decimal(0)
let decay: Decay | null = null let decay: Decay | null = null
if (lastTransaction) { if (lastTransaction) {
@ -642,12 +603,7 @@ export class ContributionResolver {
await queryRunner.release() await queryRunner.release()
} }
const event = new Event() await EVENT_CONTRIBUTION_CONFIRM(user.id, contribution.id, contribution.amount)
const eventContributionConfirm = new EventContributionConfirm()
eventContributionConfirm.userId = user.id
eventContributionConfirm.amount = contribution.amount
eventContributionConfirm.contributionId = contribution.id
await eventProtocol.writeEvent(event.setEventContributionConfirm(eventContributionConfirm))
} finally { } finally {
releaseLock() releaseLock()
} }
@ -737,6 +693,13 @@ export class ContributionResolver {
contributionToUpdate.deniedAt = new Date() contributionToUpdate.deniedAt = new Date()
const res = await contributionToUpdate.save() const res = await contributionToUpdate.save()
await EVENT_ADMIN_CONTRIBUTION_DENY(
contributionToUpdate.userId,
moderator.id,
contributionToUpdate.id,
contributionToUpdate.amount,
)
sendContributionDeniedEmail({ sendContributionDeniedEmail({
firstName: user.firstName, firstName: user.firstName,
lastName: user.lastName, lastName: user.lastName,

View File

@ -116,6 +116,11 @@ describe('TransactionLinkResolver', () => {
}) })
describe('redeemTransactionLink', () => { describe('redeemTransactionLink', () => {
afterAll(async () => {
await cleanDB()
resetToken()
})
describe('contributionLink', () => { describe('contributionLink', () => {
describe('input not valid', () => { describe('input not valid', () => {
beforeAll(async () => { beforeAll(async () => {
@ -354,11 +359,6 @@ describe('TransactionLinkResolver', () => {
}) })
it('logs the error thrown', () => { it('logs the error thrown', () => {
/* expect(logger.error).toBeCalledWith(
'The amount to be created exceeds the amount still available for this month',
new Decimal(5),
new Decimal(0),
) */
expect(logger.error).toBeCalledWith( expect(logger.error).toBeCalledWith(
'Creation from contribution link was not successful', 'Creation from contribution link was not successful',
new Error( new Error(
@ -487,8 +487,7 @@ describe('TransactionLinkResolver', () => {
pageSize: 5, pageSize: 5,
} }
// TODO: there is a test not cleaning up after itself! Fix it! afterAll(async () => {
beforeAll(async () => {
await cleanDB() await cleanDB()
resetToken() resetToken()
}) })

View File

@ -34,6 +34,8 @@ import QueryLinkResult from '@union/QueryLinkResult'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
import LogError from '@/server/LogError' import LogError from '@/server/LogError'
import { getLastTransaction } from './util/getLastTransaction'
// TODO: do not export, test it inside the resolver // TODO: do not export, test it inside the resolver
export const transactionLinkCode = (date: Date): string => { export const transactionLinkCode = (date: Date): string => {
const time = date.getTime().toString(16) const time = date.getTime().toString(16)
@ -262,13 +264,7 @@ export class TransactionLinkResolver {
await queryRunner.manager.insert(DbContribution, contribution) await queryRunner.manager.insert(DbContribution, contribution)
const lastTransaction = await queryRunner.manager const lastTransaction = await getLastTransaction(user.id)
.createQueryBuilder()
.select('transaction')
.from(DbTransaction, 'transaction')
.where('transaction.userId = :id', { id: user.id })
.orderBy('transaction.id', 'DESC')
.getOne()
let newBalance = new Decimal(0) let newBalance = new Decimal(0)
let decay: Decay | null = null let decay: Decay | null = null

View File

@ -330,7 +330,7 @@ describe('send coins', () => {
) )
}) })
it('stores the send transaction event in the database', async () => { it('stores the TRANSACTION_SEND event in the database', async () => {
// Find the exact transaction (sent one is the one with user[1] as user) // Find the exact transaction (sent one is the one with user[1] as user)
const transaction = await Transaction.find({ const transaction = await Transaction.find({
userId: user[1].id, userId: user[1].id,
@ -347,7 +347,7 @@ describe('send coins', () => {
) )
}) })
it('stores the receive event in the database', async () => { it('stores the TRANSACTION_RECEIVE event in the database', async () => {
// Find the exact transaction (received one is the one with user[0] as user) // Find the exact transaction (received one is the one with user[0] as user)
const transaction = await Transaction.find({ const transaction = await Transaction.find({
userId: user[0].id, userId: user[0].id,

View File

@ -29,8 +29,7 @@ import {
sendTransactionLinkRedeemedEmail, sendTransactionLinkRedeemedEmail,
sendTransactionReceivedEmail, sendTransactionReceivedEmail,
} from '@/emails/sendEmailVariants' } from '@/emails/sendEmailVariants'
import { Event, EventTransactionReceive, EventTransactionSend } from '@/event/Event' import { EVENT_TRANSACTION_RECEIVE, EVENT_TRANSACTION_SEND } from '@/event/Event'
import { eventProtocol } from '@/event/EventProtocolEmitter'
import { BalanceResolver } from './BalanceResolver' import { BalanceResolver } from './BalanceResolver'
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const' import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
@ -39,6 +38,8 @@ import { findUserByEmail } from './UserResolver'
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK' import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
import LogError from '@/server/LogError' import LogError from '@/server/LogError'
import { getLastTransaction } from './util/getLastTransaction'
export const executeTransaction = async ( export const executeTransaction = async (
amount: Decimal, amount: Decimal,
memo: string, memo: string,
@ -136,20 +137,18 @@ export const executeTransaction = async (
await queryRunner.commitTransaction() await queryRunner.commitTransaction()
logger.info(`commit Transaction successful...`) logger.info(`commit Transaction successful...`)
const eventTransactionSend = new EventTransactionSend() await EVENT_TRANSACTION_SEND(
eventTransactionSend.userId = transactionSend.userId transactionSend.userId,
eventTransactionSend.xUserId = transactionSend.linkedUserId transactionSend.linkedUserId,
eventTransactionSend.transactionId = transactionSend.id transactionSend.id,
eventTransactionSend.amount = transactionSend.amount.mul(-1) transactionSend.amount.mul(-1),
await eventProtocol.writeEvent(new Event().setEventTransactionSend(eventTransactionSend)) )
const eventTransactionReceive = new EventTransactionReceive() await EVENT_TRANSACTION_RECEIVE(
eventTransactionReceive.userId = transactionReceive.userId transactionReceive.userId,
eventTransactionReceive.xUserId = transactionReceive.linkedUserId transactionReceive.linkedUserId,
eventTransactionReceive.transactionId = transactionReceive.id transactionReceive.id,
eventTransactionReceive.amount = transactionReceive.amount transactionReceive.amount,
await eventProtocol.writeEvent(
new Event().setEventTransactionReceive(eventTransactionReceive),
) )
} catch (e) { } catch (e) {
await queryRunner.rollbackTransaction() await queryRunner.rollbackTransaction()
@ -204,10 +203,7 @@ export class TransactionResolver {
logger.info(`transactionList(user=${user.firstName}.${user.lastName}, ${user.emailId})`) logger.info(`transactionList(user=${user.firstName}.${user.lastName}, ${user.emailId})`)
// find current balance // find current balance
const lastTransaction = await dbTransaction.findOne( const lastTransaction = await getLastTransaction(user.id, ['contribution'])
{ userId: user.id },
{ order: { id: 'DESC' }, relations: ['contribution'] },
)
logger.debug(`lastTransaction=${lastTransaction}`) logger.debug(`lastTransaction=${lastTransaction}`)
const balanceResolver = new BalanceResolver() const balanceResolver = new BalanceResolver()

View File

@ -19,6 +19,7 @@ import {
setUserRole, setUserRole,
deleteUser, deleteUser,
unDeleteUser, unDeleteUser,
sendActivationEmail,
} from '@/seeds/graphql/mutations' } from '@/seeds/graphql/mutations'
import { verifyLogin, queryOptIn, searchAdminUsers, searchUsers } from '@/seeds/graphql/queries' import { verifyLogin, queryOptIn, searchAdminUsers, searchUsers } from '@/seeds/graphql/queries'
import { GraphQLError } from 'graphql' import { GraphQLError } from 'graphql'
@ -175,6 +176,19 @@ describe('UserResolver', () => {
}) })
}) })
}) })
it('stores the REGISTER event in the database', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'peter@lustig.de' },
{ relations: ['user'] },
)
expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.REGISTER,
userId: userConatct.user.id,
}),
)
})
}) })
describe('account activation email', () => { describe('account activation email', () => {
@ -196,7 +210,7 @@ describe('UserResolver', () => {
}) })
}) })
it('stores the send confirmation event in the database', () => { it('stores the SEND_CONFIRMATION_EMAIL event in the database', () => {
expect(EventProtocol.find()).resolves.toContainEqual( expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.SEND_CONFIRMATION_EMAIL, type: EventProtocolType.SEND_CONFIRMATION_EMAIL,
@ -206,7 +220,7 @@ describe('UserResolver', () => {
}) })
}) })
describe('email already exists', () => { describe('user already exists', () => {
let mutation: User let mutation: User
beforeAll(async () => { beforeAll(async () => {
mutation = await mutate({ mutation: createUser, variables }) mutation = await mutate({ mutation: createUser, variables })
@ -236,6 +250,19 @@ describe('UserResolver', () => {
}), }),
) )
}) })
it('stores the SEND_ACCOUNT_MULTIREGISTRATION_EMAIL event in the database', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'peter@lustig.de' },
{ relations: ['user'] },
)
expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.SEND_ACCOUNT_MULTIREGISTRATION_EMAIL,
userId: userConatct.user.id,
}),
)
})
}) })
describe('unknown language', () => { describe('unknown language', () => {
@ -328,7 +355,7 @@ describe('UserResolver', () => {
) )
}) })
it('stores the account activated event in the database', () => { it('stores the ACTIVATE_ACCOUNT event in the database', () => {
expect(EventProtocol.find()).resolves.toContainEqual( expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.ACTIVATE_ACCOUNT, type: EventProtocolType.ACTIVATE_ACCOUNT,
@ -337,7 +364,7 @@ describe('UserResolver', () => {
) )
}) })
it('stores the redeem register event in the database', () => { it('stores the REDEEM_REGISTER event in the database', () => {
expect(EventProtocol.find()).resolves.toContainEqual( expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.REDEEM_REGISTER, type: EventProtocolType.REDEEM_REGISTER,
@ -421,7 +448,7 @@ describe('UserResolver', () => {
) )
}) })
it('stores the redeem register event in the database', async () => { it('stores the REDEEM_REGISTER event in the database', async () => {
await expect(EventProtocol.find()).resolves.toContainEqual( await expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.REDEEM_REGISTER, type: EventProtocolType.REDEEM_REGISTER,
@ -647,6 +674,19 @@ describe('UserResolver', () => {
it('sets the token in the header', () => { it('sets the token in the header', () => {
expect(headerPushMock).toBeCalledWith({ key: 'token', value: expect.any(String) }) expect(headerPushMock).toBeCalledWith({ key: 'token', value: expect.any(String) })
}) })
it('stores the LOGIN event in the database', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.LOGIN,
userId: userConatct.user.id,
}),
)
})
}) })
describe('user is in database and wrong password', () => { describe('user is in database and wrong password', () => {
@ -887,7 +927,7 @@ describe('UserResolver', () => {
) )
}) })
it('stores the login event in the database', () => { it('stores the LOGIN event in the database', () => {
expect(EventProtocol.find()).resolves.toContainEqual( expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({ expect.objectContaining({
type: EventProtocolType.LOGIN, type: EventProtocolType.LOGIN,
@ -1668,6 +1708,157 @@ describe('UserResolver', () => {
}) })
}) })
///
describe('sendActivationEmail', () => {
describe('unauthenticated', () => {
it('returns an error', async () => {
await expect(
mutate({ mutation: sendActivationEmail, variables: { email: 'bibi@bloxberg.de' } }),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
describe('authenticated', () => {
describe('without admin rights', () => {
beforeAll(async () => {
user = await userFactory(testEnv, bibiBloxberg)
await mutate({
mutation: login,
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
})
})
afterAll(async () => {
await cleanDB()
resetToken()
})
it('returns an error', async () => {
await expect(
mutate({ mutation: sendActivationEmail, variables: { email: 'bibi@bloxberg.de' } }),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('401 Unauthorized')],
}),
)
})
})
describe('with admin rights', () => {
beforeAll(async () => {
admin = await userFactory(testEnv, peterLustig)
await mutate({
mutation: login,
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
})
})
afterAll(async () => {
await cleanDB()
resetToken()
})
describe('user does not exist', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await expect(
mutate({ mutation: sendActivationEmail, variables: { email: 'INVALID' } }),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('No user with this credentials')],
}),
)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith('No user with this credentials', 'invalid')
})
})
describe('user is deleted', () => {
it('throws an error', async () => {
jest.clearAllMocks()
await userFactory(testEnv, stephenHawking)
await expect(
mutate({ mutation: sendActivationEmail, variables: { email: 'stephen@hawking.uk' } }),
).resolves.toEqual(
expect.objectContaining({
errors: [new GraphQLError('User with given email contact is deleted')],
}),
)
})
it('logs the error thrown', () => {
expect(logger.error).toBeCalledWith(
'User with given email contact is deleted',
'stephen@hawking.uk',
)
})
})
describe('sendActivationEmail with success', () => {
beforeAll(async () => {
user = await userFactory(testEnv, bibiBloxberg)
})
it('returns true', async () => {
const result = await mutate({
mutation: sendActivationEmail,
variables: { email: 'bibi@bloxberg.de' },
})
expect(result).toEqual(
expect.objectContaining({
data: {
sendActivationEmail: true,
},
}),
)
})
it('sends an account activation email', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
/{optin}/g,
userConatct.emailVerificationCode.toString(),
).replace(/{code}/g, '')
expect(sendAccountActivationEmail).toBeCalledWith({
firstName: 'Bibi',
lastName: 'Bloxberg',
email: 'bibi@bloxberg.de',
language: 'de',
activationLink,
timeDurationObject: expect.objectContaining({
hours: expect.any(Number),
minutes: expect.any(Number),
}),
})
})
it('stores the ADMIN_SEND_CONFIRMATION_EMAIL event in the database', async () => {
const userConatct = await UserContact.findOneOrFail(
{ email: 'bibi@bloxberg.de' },
{ relations: ['user'] },
)
expect(EventProtocol.find()).resolves.toContainEqual(
expect.objectContaining({
type: EventProtocolType.ADMIN_SEND_CONFIRMATION_EMAIL,
userId: userConatct.user.id,
}),
)
})
})
})
})
})
describe('unDelete user', () => { describe('unDelete user', () => {
describe('unauthenticated', () => { describe('unauthenticated', () => {
it('returns an error', async () => { it('returns an error', async () => {

View File

@ -48,15 +48,14 @@ import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddle
import { klicktippSignIn } from '@/apis/KlicktippController' import { klicktippSignIn } from '@/apis/KlicktippController'
import { RIGHTS } from '@/auth/RIGHTS' import { RIGHTS } from '@/auth/RIGHTS'
import { hasElopageBuys } from '@/util/hasElopageBuys' import { hasElopageBuys } from '@/util/hasElopageBuys'
import { eventProtocol } from '@/event/EventProtocolEmitter'
import { import {
Event, Event,
EventLogin, EVENT_LOGIN,
EventRedeemRegister, EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL,
EventRegister, EVENT_SEND_CONFIRMATION_EMAIL,
EventSendAccountMultiRegistrationEmail, EVENT_REGISTER,
EventSendConfirmationEmail, EVENT_ACTIVATE_ACCOUNT,
EventActivateAccount, EVENT_ADMIN_SEND_CONFIRMATION_EMAIL,
} from '@/event/Event' } from '@/event/Event'
import { getUserCreations } from './util/creations' import { getUserCreations } from './util/creations'
import { isValidPassword } from '@/password/EncryptorUtils' import { isValidPassword } from '@/password/EncryptorUtils'
@ -64,6 +63,7 @@ import { FULL_CREATION_AVAILABLE } from './const/const'
import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptor' import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptor'
import { PasswordEncryptionType } from '../enum/PasswordEncryptionType' import { PasswordEncryptionType } from '../enum/PasswordEncryptionType'
import LogError from '@/server/LogError' import LogError from '@/server/LogError'
import { EventProtocolType } from '@/event/EventProtocolType'
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const sodium = require('sodium-native') const sodium = require('sodium-native')
@ -177,9 +177,8 @@ export class UserResolver {
key: 'token', key: 'token',
value: encode(dbUser.gradidoID), value: encode(dbUser.gradidoID),
}) })
const ev = new EventLogin()
ev.userId = user.id await EVENT_LOGIN(user.id)
eventProtocol.writeEvent(new Event().setEventLogin(ev))
logger.info(`successful Login: ${JSON.stringify(user, null, 2)}`) logger.info(`successful Login: ${JSON.stringify(user, null, 2)}`)
return user return user
} }
@ -211,7 +210,6 @@ export class UserResolver {
) )
// TODO: wrong default value (should be null), how does graphql work here? Is it an required field? // TODO: wrong default value (should be null), how does graphql work here? Is it an required field?
// default int publisher_id = 0; // default int publisher_id = 0;
const event = new Event()
// Validate Language (no throw) // Validate Language (no throw)
if (!language || !isLanguage(language)) { if (!language || !isLanguage(language)) {
@ -249,11 +247,9 @@ export class UserResolver {
email, email,
language: foundUser.language, // use language of the emails owner for sending language: foundUser.language, // use language of the emails owner for sending
}) })
const eventSendAccountMultiRegistrationEmail = new EventSendAccountMultiRegistrationEmail()
eventSendAccountMultiRegistrationEmail.userId = foundUser.id await EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL(foundUser.id)
eventProtocol.writeEvent(
event.setEventSendConfirmationEmail(eventSendAccountMultiRegistrationEmail),
)
logger.info( logger.info(
`sendAccountMultiRegistrationEmail by ${firstName} ${lastName} to ${foundUser.firstName} ${foundUser.lastName} <${email}>`, `sendAccountMultiRegistrationEmail by ${firstName} ${lastName} to ${foundUser.firstName} ${foundUser.lastName} <${email}>`,
) )
@ -270,10 +266,7 @@ export class UserResolver {
const gradidoID = await newGradidoID() const gradidoID = await newGradidoID()
const eventRegister = new EventRegister() const eventRegisterRedeem = Event(EventProtocolType.REDEEM_REGISTER, 0)
const eventRedeemRegister = new EventRedeemRegister()
const eventSendConfirmEmail = new EventSendConfirmationEmail()
let dbUser = new DbUser() let dbUser = new DbUser()
dbUser.gradidoID = gradidoID dbUser.gradidoID = gradidoID
dbUser.firstName = firstName dbUser.firstName = firstName
@ -290,14 +283,14 @@ export class UserResolver {
logger.info('redeemCode found contributionLink=' + contributionLink) logger.info('redeemCode found contributionLink=' + contributionLink)
if (contributionLink) { if (contributionLink) {
dbUser.contributionLinkId = contributionLink.id dbUser.contributionLinkId = contributionLink.id
eventRedeemRegister.contributionId = contributionLink.id eventRegisterRedeem.contributionId = contributionLink.id
} }
} else { } else {
const transactionLink = await DbTransactionLink.findOne({ code: redeemCode }) const transactionLink = await DbTransactionLink.findOne({ code: redeemCode })
logger.info('redeemCode found transactionLink=' + transactionLink) logger.info('redeemCode found transactionLink=' + transactionLink)
if (transactionLink) { if (transactionLink) {
dbUser.referrerId = transactionLink.userId dbUser.referrerId = transactionLink.userId
eventRedeemRegister.transactionId = transactionLink.id eventRegisterRedeem.transactionId = transactionLink.id
} }
} }
} }
@ -335,8 +328,8 @@ export class UserResolver {
timeDurationObject: getTimeDurationObject(CONFIG.EMAIL_CODE_VALID_TIME), timeDurationObject: getTimeDurationObject(CONFIG.EMAIL_CODE_VALID_TIME),
}) })
logger.info(`sendAccountActivationEmail of ${firstName}.${lastName} to ${email}`) logger.info(`sendAccountActivationEmail of ${firstName}.${lastName} to ${email}`)
eventSendConfirmEmail.userId = dbUser.id
eventProtocol.writeEvent(event.setEventSendConfirmationEmail(eventSendConfirmEmail)) await EVENT_SEND_CONFIRMATION_EMAIL(dbUser.id)
if (!emailSent) { if (!emailSent) {
logger.debug(`Account confirmation link: ${activationLink}`) logger.debug(`Account confirmation link: ${activationLink}`)
@ -353,11 +346,10 @@ export class UserResolver {
logger.info('createUser() successful...') logger.info('createUser() successful...')
if (redeemCode) { if (redeemCode) {
eventRedeemRegister.userId = dbUser.id eventRegisterRedeem.userId = dbUser.id
await eventProtocol.writeEvent(event.setEventRedeemRegister(eventRedeemRegister)) await eventRegisterRedeem.save()
} else { } else {
eventRegister.userId = dbUser.id await EVENT_REGISTER(dbUser.id)
await eventProtocol.writeEvent(event.setEventRegister(eventRegister))
} }
return new User(dbUser) return new User(dbUser)
@ -460,8 +452,6 @@ export class UserResolver {
await queryRunner.connect() await queryRunner.connect()
await queryRunner.startTransaction('REPEATABLE READ') await queryRunner.startTransaction('REPEATABLE READ')
const event = new Event()
try { try {
// Save user // Save user
await queryRunner.manager.save(user).catch((error) => { await queryRunner.manager.save(user).catch((error) => {
@ -475,9 +465,7 @@ export class UserResolver {
await queryRunner.commitTransaction() await queryRunner.commitTransaction()
logger.info('User and UserContact data written successfully...') logger.info('User and UserContact data written successfully...')
const eventActivateAccount = new EventActivateAccount() await EVENT_ACTIVATE_ACCOUNT(user.id)
eventActivateAccount.userId = user.id
eventProtocol.writeEvent(event.setEventActivateAccount(eventActivateAccount))
} catch (e) { } catch (e) {
await queryRunner.rollbackTransaction() await queryRunner.rollbackTransaction()
throw new LogError('Error on writing User and User Contact data', e) throw new LogError('Error on writing User and User Contact data', e)
@ -793,19 +781,12 @@ export class UserResolver {
email = email.trim().toLowerCase() email = email.trim().toLowerCase()
// const user = await dbUser.findOne({ id: emailContact.userId }) // const user = await dbUser.findOne({ id: emailContact.userId })
const user = await findUserByEmail(email) const user = await findUserByEmail(email)
if (!user) { if (user.deletedAt || user.emailContact.deletedAt) {
throw new LogError('Could not find user to given email contact', email)
}
if (user.deletedAt) {
throw new LogError('User with given email contact is deleted', email) throw new LogError('User with given email contact is deleted', email)
} }
const emailContact = user.emailContact
if (emailContact.deletedAt) {
throw new LogError('The given email contact for this user is deleted', email)
}
emailContact.emailResendCount++ user.emailContact.emailResendCount++
await emailContact.save() await user.emailContact.save()
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const emailSent = await sendAccountActivationEmail({ const emailSent = await sendAccountActivationEmail({
@ -813,7 +794,7 @@ export class UserResolver {
lastName: user.lastName, lastName: user.lastName,
email, email,
language: user.language, language: user.language,
activationLink: activationLink(emailContact.emailVerificationCode), activationLink: activationLink(user.emailContact.emailVerificationCode),
timeDurationObject: getTimeDurationObject(CONFIG.EMAIL_CODE_VALID_TIME), timeDurationObject: getTimeDurationObject(CONFIG.EMAIL_CODE_VALID_TIME),
}) })
@ -821,12 +802,7 @@ export class UserResolver {
if (!emailSent) { if (!emailSent) {
logger.info(`Account confirmation link: ${activationLink}`) logger.info(`Account confirmation link: ${activationLink}`)
} else { } else {
const event = new Event() await EVENT_ADMIN_SEND_CONFIRMATION_EMAIL(user.id)
const eventSendConfirmationEmail = new EventSendConfirmationEmail()
eventSendConfirmationEmail.userId = user.id
await eventProtocol.writeEvent(
event.setEventSendConfirmationEmail(eventSendConfirmationEmail),
)
} }
return true return true

View File

@ -0,0 +1,14 @@
import { Transaction as DbTransaction } from '@entity/Transaction'
export const getLastTransaction = async (
userId: number,
relations?: string[],
): Promise<DbTransaction | undefined> => {
return DbTransaction.findOne(
{ userId },
{
order: { balanceDate: 'DESC', id: 'DESC' },
relations,
},
)
}

View File

@ -4,6 +4,7 @@ import createServer from './server/createServer'
// config // config
import CONFIG from './config' import CONFIG from './config'
import { startValidateCommunities } from './federation/validateCommunities'
async function main() { async function main() {
const { app } = await createServer() const { app } = await createServer()
@ -16,6 +17,7 @@ async function main() {
console.log(`GraphIQL available at http://localhost:${CONFIG.PORT}`) console.log(`GraphIQL available at http://localhost:${CONFIG.PORT}`)
} }
}) })
startValidateCommunities(Number(CONFIG.FEDERATION_VALIDATE_COMMUNITY_TIMER))
} }
main().catch((e) => { main().catch((e) => {

View File

@ -1,10 +1,5 @@
{ {
"emails": { "emails": {
"addedContributionMessage": {
"commonGoodContributionMessage": "du hast zu deinem Gemeinwohl-Beitrag „{contributionMemo}“ eine Nachricht von {senderFirstName} {senderLastName} erhalten.",
"subject": "Gradido: Nachricht zu deinem Gemeinwohl-Beitrag",
"toSeeAndAnswerMessage": "Um die Nachricht zu sehen und darauf zu antworten, gehe in deinem Gradido-Konto ins Menü „Gemeinschaft“ auf den Tab „Meine Beiträge zum Gemeinwohl“!"
},
"accountActivation": { "accountActivation": {
"duration": "Der Link hat eine Gültigkeit von {hours} Stunden und {minutes} Minuten. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen:", "duration": "Der Link hat eine Gültigkeit von {hours} Stunden und {minutes} Minuten. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen:",
"emailRegistered": "deine E-Mail-Adresse wurde soeben bei Gradido registriert.", "emailRegistered": "deine E-Mail-Adresse wurde soeben bei Gradido registriert.",
@ -19,12 +14,22 @@
"onForgottenPasswordCopyLink": "oder kopiere den obigen Link in dein Browserfenster.", "onForgottenPasswordCopyLink": "oder kopiere den obigen Link in dein Browserfenster.",
"subject": "Gradido: Erneuter Registrierungsversuch mit deiner E-Mail" "subject": "Gradido: Erneuter Registrierungsversuch mit deiner E-Mail"
}, },
"addedContributionMessage": {
"commonGoodContributionMessage": "du hast zu deinem Gemeinwohl-Beitrag „{contributionMemo}“ eine Nachricht von {senderFirstName} {senderLastName} erhalten.",
"subject": "Gradido: Nachricht zu deinem Gemeinwohl-Beitrag",
"toSeeAndAnswerMessage": "Um die Nachricht zu sehen und darauf zu antworten, gehe in deinem Gradido-Konto ins Menü „Gemeinschaft“ auf den Tab „Meine Beiträge zum Gemeinwohl“!"
},
"contributionConfirmed": { "contributionConfirmed": {
"commonGoodContributionConfirmed": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde soeben von {senderFirstName} {senderLastName} bestätigt und in deinem Gradido-Konto gutgeschrieben.", "commonGoodContributionConfirmed": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde soeben von {senderFirstName} {senderLastName} bestätigt und in deinem Gradido-Konto gutgeschrieben.",
"subject": "Gradido: Dein Gemeinwohl-Beitrag wurde bestätigt" "subject": "Gradido: Dein Gemeinwohl-Beitrag wurde bestätigt"
}, },
"contributionRejected": { "contributionDeleted": {
"commonGoodContributionRejected": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde von {senderFirstName} {senderLastName} abgelehnt.", "commonGoodContributionDeleted": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde von {senderFirstName} {senderLastName} gelöscht.",
"subject": "Gradido: Dein Gemeinwohl-Beitrag wurde gelöscht",
"toSeeContributionsAndMessages": "Um deine Gemeinwohl-Beiträge und dazugehörige Nachrichten zu sehen, gehe in deinem Gradido-Konto ins Menü „Gemeinschaft“ auf den Tab „Meine Beiträge zum Gemeinwohl“!"
},
"contributionDenied": {
"commonGoodContributionDenied": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde von {senderFirstName} {senderLastName} abgelehnt.",
"subject": "Gradido: Dein Gemeinwohl-Beitrag wurde abgelehnt", "subject": "Gradido: Dein Gemeinwohl-Beitrag wurde abgelehnt",
"toSeeContributionsAndMessages": "Um deine Gemeinwohl-Beiträge und dazugehörige Nachrichten zu sehen, gehe in deinem Gradido-Konto ins Menü „Gemeinschaft“ auf den Tab „Meine Beiträge zum Gemeinwohl“!" "toSeeContributionsAndMessages": "Um deine Gemeinwohl-Beiträge und dazugehörige Nachrichten zu sehen, gehe in deinem Gradido-Konto ins Menü „Gemeinschaft“ auf den Tab „Meine Beiträge zum Gemeinwohl“!"
}, },

View File

@ -1,10 +1,5 @@
{ {
"emails": { "emails": {
"addedContributionMessage": {
"commonGoodContributionMessage": "you have received a message from {senderFirstName} {senderLastName} regarding your common good contribution “{contributionMemo}”.",
"subject": "Gradido: Message about your common good contribution",
"toSeeAndAnswerMessage": "To view and reply to the message, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!"
},
"accountActivation": { "accountActivation": {
"duration": "The link has a validity of {hours} hours and {minutes} minutes. If the validity of the link has already expired, you can have a new link sent to you here:", "duration": "The link has a validity of {hours} hours and {minutes} minutes. If the validity of the link has already expired, you can have a new link sent to you here:",
"emailRegistered": "Your email address has just been registered with Gradido.", "emailRegistered": "Your email address has just been registered with Gradido.",
@ -19,10 +14,20 @@
"onForgottenPasswordCopyLink": "or copy the link above into your browser window.", "onForgottenPasswordCopyLink": "or copy the link above into your browser window.",
"subject": "Gradido: Try To Register Again With Your Email" "subject": "Gradido: Try To Register Again With Your Email"
}, },
"addedContributionMessage": {
"commonGoodContributionMessage": "you have received a message from {senderFirstName} {senderLastName} regarding your common good contribution “{contributionMemo}”.",
"subject": "Gradido: Message about your common good contribution",
"toSeeAndAnswerMessage": "To view and reply to the message, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!"
},
"contributionConfirmed": { "contributionConfirmed": {
"commonGoodContributionConfirmed": "Your public good contribution “{contributionMemo}” has just been confirmed by {senderFirstName} {senderLastName} and credited to your Gradido account.", "commonGoodContributionConfirmed": "Your public good contribution “{contributionMemo}” has just been confirmed by {senderFirstName} {senderLastName} and credited to your Gradido account.",
"subject": "Gradido: Your contribution to the common good was confirmed" "subject": "Gradido: Your contribution to the common good was confirmed"
}, },
"contributionDeleted": {
"commonGoodContributionDeleted": "Your public good contribution “{contributionMemo}” was deleted by {senderFirstName} {senderLastName}.",
"subject": "Gradido: Your common good contribution was deleted",
"toSeeContributionsAndMessages": "To see your common good contributions and related messages, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!"
},
"contributionDenied": { "contributionDenied": {
"commonGoodContributionDenied": "Your public good contribution “{contributionMemo}” was rejected by {senderFirstName} {senderLastName}.", "commonGoodContributionDenied": "Your public good contribution “{contributionMemo}” was rejected by {senderFirstName} {senderLastName}.",
"subject": "Gradido: Your common good contribution was rejected", "subject": "Gradido: Your common good contribution was rejected",

View File

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

View File

@ -68,6 +68,12 @@ export const createUser = gql`
} }
` `
export const sendActivationEmail = gql`
mutation ($email: String!) {
sendActivationEmail(email: $email)
}
`
export const sendCoins = gql` export const sendCoins = gql`
mutation ($email: String!, $amount: Decimal!, $memo: String!) { mutation ($email: String!, $amount: Decimal!, $memo: String!) {
sendCoins(email: $email, amount: $amount, memo: $memo) sendCoins(email: $email, amount: $amount, memo: $memo)
@ -266,6 +272,12 @@ export const deleteContribution = gql`
} }
` `
export const denyContribution = gql`
mutation ($id: Int!) {
denyContribution(id: $id)
}
`
export const createContributionMessage = gql` export const createContributionMessage = gql`
mutation ($contributionId: Float!, $message: String!) { mutation ($contributionId: Float!, $message: String!) {
createContributionMessage(contributionId: $contributionId, message: $message) { createContributionMessage(contributionId: $contributionId, message: $message) {

View File

@ -166,6 +166,15 @@ export const listContributions = gql`
id id
amount amount
memo memo
createdAt
contributionDate
confirmedAt
confirmedBy
deletedAt
state
messagesCount
deniedAt
deniedBy
} }
} }
} }

View File

@ -1,6 +1,5 @@
import { UserInterface } from './UserInterface' import { UserInterface } from './UserInterface'
// TODO: the generated email_contact is not deleted
export const stephenHawking: UserInterface = { export const stephenHawking: UserInterface = {
email: 'stephen@hawking.uk', email: 'stephen@hawking.uk',
firstName: 'Stephen', firstName: 'Stephen',

View File

@ -1,10 +1,10 @@
import { calculateDecay } from './decay' import { calculateDecay } from './decay'
import Decimal from 'decimal.js-light' import Decimal from 'decimal.js-light'
import { Transaction } from '@entity/Transaction'
import { Decay } from '@model/Decay' import { Decay } from '@model/Decay'
import { getCustomRepository } from '@dbTools/typeorm' import { getCustomRepository } from '@dbTools/typeorm'
import { TransactionLinkRepository } from '@repository/TransactionLink' import { TransactionLinkRepository } from '@repository/TransactionLink'
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink' import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
import { getLastTransaction } from '../graphql/resolver/util/getLastTransaction'
function isStringBoolean(value: string): boolean { function isStringBoolean(value: string): boolean {
const lowerValue = value.toLowerCase() const lowerValue = value.toLowerCase()
@ -20,7 +20,7 @@ async function calculateBalance(
time: Date, time: Date,
transactionLink?: dbTransactionLink | null, transactionLink?: dbTransactionLink | null,
): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number } | null> { ): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number } | null> {
const lastTransaction = await Transaction.findOne({ userId }, { order: { id: 'DESC' } }) const lastTransaction = await getLastTransaction(userId)
if (!lastTransaction) return null if (!lastTransaction) return null
const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time) const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time)

View File

@ -404,6 +404,11 @@
minimatch "^3.0.4" minimatch "^3.0.4"
strip-json-comments "^3.1.1" strip-json-comments "^3.1.1"
"@graphql-typed-document-node/core@^3.1.1":
version "3.1.1"
resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.1.1.tgz#076d78ce99822258cf813ecc1e7fa460fa74d052"
integrity sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg==
"@hapi/boom@^10.0.0": "@hapi/boom@^10.0.0":
version "10.0.0" version "10.0.0"
resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-10.0.0.tgz#3624831d0a26b3378423b246f50eacea16e04a08" resolved "https://registry.yarnpkg.com/@hapi/boom/-/boom-10.0.0.tgz#3624831d0a26b3378423b246f50eacea16e04a08"
@ -430,42 +435,6 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf" resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz#87de7af9c231826fdd68ac7258f77c429e0e5fcf"
integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w== integrity sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==
"@hyperswarm/dht@^6.2.0":
version "6.2.0"
resolved "https://registry.yarnpkg.com/@hyperswarm/dht/-/dht-6.2.0.tgz#b2cb1218752b52fabb66f304e73448a108d1effd"
integrity sha512-AeyfRdAkfCz/J3vTC4rdpzEpT7xQ+tls87Zpzw9Py3VGUZD8hMT7pr43OOdkCBNvcln6K/5/Lxhnq5lBkzH3yw==
dependencies:
"@hyperswarm/secret-stream" "^6.0.0"
b4a "^1.3.1"
bogon "^1.0.0"
compact-encoding "^2.4.1"
compact-encoding-net "^1.0.1"
debugging-stream "^2.0.0"
dht-rpc "^6.0.0"
events "^3.3.0"
hypercore-crypto "^3.3.0"
noise-curve-ed "^1.0.2"
noise-handshake "^2.1.0"
record-cache "^1.1.1"
safety-catch "^1.0.1"
sodium-universal "^3.0.4"
udx-native "^1.1.0"
xache "^1.1.0"
"@hyperswarm/secret-stream@^6.0.0":
version "6.0.0"
resolved "https://registry.yarnpkg.com/@hyperswarm/secret-stream/-/secret-stream-6.0.0.tgz#67db820308cc9fed899cb8f5e9f47ae819d5a4e3"
integrity sha512-0xuyJIJDe8JYk4uWUx25qJvWqybdjKU2ZIfP1GTqd7dQxwdR0bpYrQKdLkrn5txWSK4a28ySC2AjH0G3I0gXTA==
dependencies:
b4a "^1.1.0"
hypercore-crypto "^3.3.0"
noise-curve-ed "^1.0.2"
noise-handshake "^2.1.0"
sodium-secretstream "^1.0.0"
sodium-universal "^3.0.4"
streamx "^2.10.2"
timeout-refresh "^2.0.0"
"@istanbuljs/load-nyc-config@^1.0.0": "@istanbuljs/load-nyc-config@^1.0.0":
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
@ -1655,11 +1624,6 @@ axios@^0.21.1:
dependencies: dependencies:
follow-redirects "^1.14.0" follow-redirects "^1.14.0"
b4a@^1.0.1, b4a@^1.1.0, b4a@^1.1.1, b4a@^1.3.0, b4a@^1.3.1, b4a@^1.5.0:
version "1.5.3"
resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.5.3.tgz#56293b5607aeda3fd81c481e516e9f103fc88341"
integrity sha512-1aCQIzQJK7G0z1Una75tWMlwVAR8o+QHoAlnWc5XAxRVBESY9WsitfBgM5nPyDBP5HrhPU1Np4Pq2Y7CJQ+tVw==
babel-jest@^27.2.5: babel-jest@^27.2.5:
version "27.2.5" version "27.2.5"
resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.2.5.tgz#6bbbc1bb4200fe0bfd1b1fbcbe02fc62ebed16aa" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-27.2.5.tgz#6bbbc1bb4200fe0bfd1b1fbcbe02fc62ebed16aa"
@ -1743,22 +1707,6 @@ binary-extensions@^2.0.0:
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
blake2b-wasm@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/blake2b-wasm/-/blake2b-wasm-2.4.0.tgz#9115649111edbbd87eb24ce7c04b427e4e2be5be"
integrity sha512-S1kwmW2ZhZFFFOghcx73+ZajEfKBqhP82JMssxtLVMxlaPea1p9uoLiUZ5WYyHn0KddwbLc+0vh4wR0KBNoT5w==
dependencies:
b4a "^1.0.1"
nanoassert "^2.0.0"
blake2b@^2.1.1:
version "2.1.4"
resolved "https://registry.yarnpkg.com/blake2b/-/blake2b-2.1.4.tgz#817d278526ddb4cd673bfb1af16d1ad61e393ba3"
integrity sha512-AyBuuJNI64gIvwx13qiICz6H6hpmjvYS5DGkG6jbXMOT8Z3WUJ3V1X0FlhIoT1b/5JtHE3ki+xjtMvu1nn+t9A==
dependencies:
blake2b-wasm "^2.4.0"
nanoassert "^2.0.0"
bluebird@^3.7.2: bluebird@^3.7.2:
version "3.7.2" version "3.7.2"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
@ -1780,11 +1728,6 @@ body-parser@1.19.0, body-parser@^1.18.3:
raw-body "2.4.0" raw-body "2.4.0"
type-is "~1.6.17" type-is "~1.6.17"
bogon@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/bogon/-/bogon-1.0.0.tgz#66b8cdd269f790e3aa988e157bb34d4ba75ee586"
integrity sha512-mXxtlBtnW8koqFWPUBtKJm97vBSKZRpOvxvMRVun33qQXwMNfQzq9eTcQzKzqEoNUhNqF9t8rDc/wakKCcHMTg==
boolbase@^1.0.0: boolbase@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
@ -1917,13 +1860,6 @@ caniuse-lite@^1.0.30001264:
resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001442.tgz" resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001442.tgz"
integrity sha512-239m03Pqy0hwxYPYR5JwOIxRJfLTWtle9FV8zosfV5pHg+/51uD4nxcUlM8+mWWGfwKtt8lJNHnD3cWw9VZ6ow== integrity sha512-239m03Pqy0hwxYPYR5JwOIxRJfLTWtle9FV8zosfV5pHg+/51uD4nxcUlM8+mWWGfwKtt8lJNHnD3cWw9VZ6ow==
chacha20-universal@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/chacha20-universal/-/chacha20-universal-1.0.4.tgz#e8a33a386500b1ce5361b811ec5e81f1797883f5"
integrity sha512-/IOxdWWNa7nRabfe7+oF+jVkGjlr2xUL4J8l/OvzZhj+c9RpMqoo3Dq+5nU1j/BflRV4BKnaQ4+4oH1yBpQG1Q==
dependencies:
nanoassert "^2.0.0"
chalk@^2.0.0: chalk@^2.0.0:
version "2.4.2" version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
@ -2093,20 +2029,6 @@ commander@^6.1.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
compact-encoding-net@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/compact-encoding-net/-/compact-encoding-net-1.0.1.tgz#4da743d52721f5d0cc73a6d00556a96bc9b9fa1b"
integrity sha512-N9k1Qwg9b1ENk+TZsZhthzkuMtn3rn4ZinN75gf3/LplE+uaTCKjyaau5sK0m2NEUa/MmR77VxiGfD/Qz1ar0g==
dependencies:
compact-encoding "^2.4.1"
compact-encoding@^2.1.0, compact-encoding@^2.4.1, compact-encoding@^2.5.1:
version "2.7.0"
resolved "https://registry.yarnpkg.com/compact-encoding/-/compact-encoding-2.7.0.tgz#e6a0df408c25cbcdf7d619c97527074478cafd06"
integrity sha512-2I0A+pYKXYwxewbLxj26tU4pJyKlFNjadzjZ+36xJ5HwTrnhD9KcMQk3McEQRl1at6jrwA8E7UjmBdsGhEAPMw==
dependencies:
b4a "^1.3.0"
concat-map@0.0.1: concat-map@0.0.1:
version "0.0.1" version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@ -2193,6 +2115,13 @@ cross-env@^7.0.3:
dependencies: dependencies:
cross-spawn "^7.0.1" cross-spawn "^7.0.1"
cross-fetch@^3.1.5:
version "3.1.5"
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f"
integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==
dependencies:
node-fetch "2.6.7"
cross-spawn@^6.0.0: cross-spawn@^6.0.0:
version "6.0.5" version "6.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
@ -2305,13 +2234,6 @@ debug@^4.3.3, debug@^4.3.4:
dependencies: dependencies:
ms "2.1.2" ms "2.1.2"
debugging-stream@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/debugging-stream/-/debugging-stream-2.0.0.tgz#515cad5a35299cf4b4bc0afcbd69d52c809c84ce"
integrity sha512-xwfl6wB/3xc553uwtGnSa94jFxnGOc02C0WU2Nmzwr80gzeqn1FX4VcbvoKIhe8L/lPq4BTQttAbrTN94uN8rA==
dependencies:
streamx "^2.12.4"
decimal.js-light@^2.5.1: decimal.js-light@^2.5.1:
version "2.5.1" version "2.5.1"
resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934" resolved "https://registry.yarnpkg.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz#134fd32508f19e208f4fb2f8dac0d2626a867934"
@ -2391,23 +2313,6 @@ detect-newline@^3.0.0:
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==
dht-rpc@^6.0.0:
version "6.1.1"
resolved "https://registry.yarnpkg.com/dht-rpc/-/dht-rpc-6.1.1.tgz#a292a22aa19b05136978d33528cb571d6e32502f"
integrity sha512-wo0nMXwn/rhxVz62V0d+l/0HuikxLQh6lkwlUIdoaUzGl9DobFj4epSScD3/lTMwKts+Ih0DFNqP+j0tYwdajQ==
dependencies:
b4a "^1.3.1"
compact-encoding "^2.1.0"
compact-encoding-net "^1.0.1"
events "^3.3.0"
fast-fifo "^1.0.0"
kademlia-routing-table "^1.0.0"
nat-sampler "^1.0.1"
sodium-universal "^3.0.4"
streamx "^2.10.3"
time-ordered-set "^1.0.2"
udx-native "^1.1.0"
dicer@0.3.0: dicer@0.3.0:
version "0.3.0" version "0.3.0"
resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.3.0.tgz#eacd98b3bfbf92e8ab5c2fdb71aaac44bb06b872" resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.3.0.tgz#eacd98b3bfbf92e8ab5c2fdb71aaac44bb06b872"
@ -2899,11 +2804,6 @@ eventemitter3@^3.1.0:
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7"
integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==
events@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
execa@^0.10.0: execa@^0.10.0:
version "0.10.0" version "0.10.0"
resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50" resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50"
@ -2985,6 +2885,11 @@ express@^4.17.1:
utils-merge "1.0.1" utils-merge "1.0.1"
vary "~1.1.2" vary "~1.1.2"
extract-files@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/extract-files/-/extract-files-9.0.0.tgz#8a7744f2437f81f5ed3250ed9f1550de902fe54a"
integrity sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ==
faker@^5.5.3: faker@^5.5.3:
version "5.5.3" version "5.5.3"
resolved "https://registry.yarnpkg.com/faker/-/faker-5.5.3.tgz#c57974ee484431b25205c2c8dc09fda861e51e0e" resolved "https://registry.yarnpkg.com/faker/-/faker-5.5.3.tgz#c57974ee484431b25205c2c8dc09fda861e51e0e"
@ -3000,11 +2905,6 @@ fast-diff@^1.1.2:
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
fast-fifo@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.1.0.tgz#17d1a3646880b9891dfa0c54e69c5fef33cad779"
integrity sha512-Kl29QoNbNvn4nhDsLYjyIAaIqaJB6rBx5p3sL9VjaefJ+eMFBWVZiaoguaoZfzEKr5RhAti0UgM8703akGPJ6g==
fast-glob@^3.1.1: fast-glob@^3.1.1:
version "3.2.7" version "3.2.7"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.7.tgz#fd6cb7a2d7e9aa7a7846111e85a196d6b2f766a1"
@ -3340,6 +3240,16 @@ graphql-query-complexity@^0.7.0:
dependencies: dependencies:
lodash.get "^4.4.2" lodash.get "^4.4.2"
graphql-request@5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/graphql-request/-/graphql-request-5.0.0.tgz#7504a807d0e11be11a3c448e900f0cc316aa18ef"
integrity sha512-SpVEnIo2J5k2+Zf76cUkdvIRaq5FMZvGQYnA4lUWYbc99m+fHh4CZYRRO/Ff4tCLQ613fzCm3SiDT64ubW5Gyw==
dependencies:
"@graphql-typed-document-node/core" "^3.1.1"
cross-fetch "^3.1.5"
extract-files "^9.0.0"
form-data "^3.0.0"
graphql-subscriptions@^1.0.0, graphql-subscriptions@^1.1.0: graphql-subscriptions@^1.0.0, graphql-subscriptions@^1.1.0:
version "1.2.1" version "1.2.1"
resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-1.2.1.tgz#2142b2d729661ddf967b7388f7cf1dd4cf2e061d" resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-1.2.1.tgz#2142b2d729661ddf967b7388f7cf1dd4cf2e061d"
@ -3414,15 +3324,6 @@ he@1.2.0, he@^1.2.0:
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
hmac-blake2b@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/hmac-blake2b/-/hmac-blake2b-2.0.0.tgz#09494e5d245d7afe45d157093080b159f7bacf15"
integrity sha512-JbGNtM1YRd8EQH/2vNTAP1oy5lJVPlBFYZfCJTu3k8sqOUm0rRIf/3+MCd5noVykETwTbun6jEOc+4Tu78ubHA==
dependencies:
nanoassert "^1.1.0"
sodium-native "^3.1.1"
sodium-universal "^3.0.0"
hosted-git-info@^2.1.4: hosted-git-info@^2.1.4:
version "2.8.9" version "2.8.9"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9"
@ -3544,15 +3445,6 @@ human-signals@^2.1.0:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
hypercore-crypto@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/hypercore-crypto/-/hypercore-crypto-3.3.0.tgz#03ab5b44608a563e131f629f671c6f90a83c52e6"
integrity sha512-zAWbDqG7kWwS6rCxxTUeB/OeFAz3PoOmouKaoMubtDJYJsLHqXtA3wE2mLsw+E2+iYyom5zrFyBTFVYxmgwW6g==
dependencies:
b4a "^1.1.0"
compact-encoding "^2.5.1"
sodium-universal "^3.0.0"
i18n-locales@^0.0.5: i18n-locales@^0.0.5:
version "0.0.5" version "0.0.5"
resolved "https://registry.yarnpkg.com/i18n-locales/-/i18n-locales-0.0.5.tgz#8f587e598ab982511d7c7db910cb45b8d93cd96a" resolved "https://registry.yarnpkg.com/i18n-locales/-/i18n-locales-0.0.5.tgz#8f587e598ab982511d7c7db910cb45b8d93cd96a"
@ -4517,11 +4409,6 @@ jws@^3.2.2:
jwa "^1.4.1" jwa "^1.4.1"
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
kademlia-routing-table@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/kademlia-routing-table/-/kademlia-routing-table-1.0.1.tgz#6f18416f612e885a8d4df128f04c490a90d772f6"
integrity sha512-dKk19sC3/+kWhBIvOKCthxVV+JH0NrswSBq4sA4eOkkPMqQM1rRuOWte1WSKXeP8r9Nx4NuiH2gny3lMddJTpw==
keyv@^3.0.0: keyv@^3.0.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9" resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9"
@ -4932,26 +4819,6 @@ named-placeholders@^1.1.2:
dependencies: dependencies:
lru-cache "^4.1.3" lru-cache "^4.1.3"
nanoassert@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/nanoassert/-/nanoassert-1.1.0.tgz#4f3152e09540fde28c76f44b19bbcd1d5a42478d"
integrity sha512-C40jQ3NzfkP53NsO8kEOFd79p4b9kDXQMwgiY1z8ZwrDZgUyom0AHwGegF4Dm99L+YoYhuaB0ceerUcXmqr1rQ==
nanoassert@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/nanoassert/-/nanoassert-2.0.0.tgz#a05f86de6c7a51618038a620f88878ed1e490c09"
integrity sha512-7vO7n28+aYO4J+8w96AzhmU8G+Y/xpPDJz/se19ICsqj/momRbb9mh9ZUtkoJ5X3nTnPdhEJyc0qnM6yAsHBaA==
napi-macros@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/napi-macros/-/napi-macros-2.0.0.tgz#2b6bae421e7b96eb687aa6c77a7858640670001b"
integrity sha512-A0xLykHtARfueITVDernsAWdtIMbOJgKgcluwENp3AlsKN/PloyO10HtmoqnFAQAcxPkgZN7wdfPfEd0zNGxbg==
nat-sampler@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/nat-sampler/-/nat-sampler-1.0.1.tgz#2b68338ea6d4c139450cd971fd00a4ac1b33d923"
integrity sha512-yQvyNN7xbqR8crTKk3U8gRgpcV1Az+vfCEijiHu9oHHsnIl8n3x+yXNHl42M6L3czGynAVoOT9TqBfS87gDdcw==
natural-compare@^1.4.0: natural-compare@^1.4.0:
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@ -4977,7 +4844,7 @@ nice-try@^1.0.4:
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
node-fetch@^2.6.0: node-fetch@2.6.7, node-fetch@^2.6.0:
version "2.6.7" version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
@ -4991,7 +4858,7 @@ node-fetch@^2.6.1:
dependencies: dependencies:
whatwg-url "^5.0.0" whatwg-url "^5.0.0"
node-gyp-build@^4.3.0, node-gyp-build@^4.4.0: node-gyp-build@^4.3.0:
version "4.5.0" version "4.5.0"
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.5.0.tgz#7a64eefa0b21112f89f58379da128ac177f20e40" resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.5.0.tgz#7a64eefa0b21112f89f58379da128ac177f20e40"
integrity sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg== integrity sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg==
@ -5042,25 +4909,6 @@ nodemon@^2.0.7:
undefsafe "^2.0.3" undefsafe "^2.0.3"
update-notifier "^5.1.0" update-notifier "^5.1.0"
noise-curve-ed@^1.0.2:
version "1.0.4"
resolved "https://registry.yarnpkg.com/noise-curve-ed/-/noise-curve-ed-1.0.4.tgz#8ae83f5d2d2e31d0c9c069271ca6e462d31cd884"
integrity sha512-plUUSEOU66FZ9TaBKpk4+fgQeeS+OLlThS2o8a1TxVpMWV2v1izvEnjSpFV9gEPZl4/1yN+S5KqLubFjogqQOw==
dependencies:
b4a "^1.1.0"
nanoassert "^2.0.0"
sodium-universal "^3.0.4"
noise-handshake@^2.1.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/noise-handshake/-/noise-handshake-2.2.0.tgz#24c98f502d49118770e1ec2af2894b8789f0ac7c"
integrity sha512-+0mFUc5YSnOPI+4K/7nr6XDGduITaUasPVurzrH03sk6yW+udKxP/qjEwEekRwIpnvcCKYnjiZ9HJenJv9ljZg==
dependencies:
b4a "^1.1.0"
hmac-blake2b "^2.0.0"
nanoassert "^2.0.0"
sodium-universal "^3.0.4"
nopt@~1.0.10: nopt@~1.0.10:
version "1.0.10" version "1.0.10"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee"
@ -5666,11 +5514,6 @@ queue-microtask@^1.2.2:
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
queue-tick@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/queue-tick/-/queue-tick-1.0.0.tgz#011104793a3309ae86bfeddd54e251dc94a36725"
integrity sha512-ULWhjjE8BmiICGn3G8+1L9wFpERNxkf8ysxkAer4+TFdRefDaXOCV5m92aMB9FtBVmn/8sETXLXY6BfW7hyaWQ==
railroad-diagrams@^1.0.0: railroad-diagrams@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e" resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e"
@ -5743,13 +5586,6 @@ readdirp@~3.6.0:
dependencies: dependencies:
picomatch "^2.2.1" picomatch "^2.2.1"
record-cache@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/record-cache/-/record-cache-1.2.0.tgz#e601bc4f164d58330cc00055e27aa4682291c882"
integrity sha512-kyy3HWCez2WrotaL3O4fTn0rsIdfRKOdQQcEJ9KpvmKmbffKVvwsloX063EgRUlpJIXHiDQFhJcTbZequ2uTZw==
dependencies:
b4a "^1.3.1"
reflect-metadata@^0.1.13: reflect-metadata@^0.1.13:
version "0.1.13" version "0.1.13"
resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08"
@ -5809,7 +5645,7 @@ resolve@^1.10.0, resolve@^1.10.1, resolve@^1.20.0:
is-core-module "^2.2.0" is-core-module "^2.2.0"
path-parse "^1.0.6" path-parse "^1.0.6"
resolve@^1.15.1, resolve@^1.17.0: resolve@^1.15.1:
version "1.22.1" version "1.22.1"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==
@ -5886,11 +5722,6 @@ safe-identifier@^0.4.1:
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
safety-catch@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/safety-catch/-/safety-catch-1.0.2.tgz#d64cbd57fd601da91c356b6ab8902f3e449a7a4b"
integrity sha512-C1UYVZ4dtbBxEtvOcpjBaaD27nP8MlvyAQEp2fOTOEe6pfUpk1cDUxij6BR1jZup6rSyUTaBBplK7LanskrULA==
saxes@^5.0.1: saxes@^5.0.1:
version "5.0.1" version "5.0.1"
resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d" resolved "https://registry.yarnpkg.com/saxes/-/saxes-5.0.1.tgz#eebab953fa3b7608dbe94e5dadb15c888fa6696d"
@ -5981,38 +5812,6 @@ sha.js@^2.4.11:
inherits "^2.0.1" inherits "^2.0.1"
safe-buffer "^5.0.1" safe-buffer "^5.0.1"
sha256-universal@^1.1.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/sha256-universal/-/sha256-universal-1.2.1.tgz#051d92decce280cd6137d42d496eac88da942c0e"
integrity sha512-ghn3muhdn1ailCQqqceNxRgkOeZSVfSE13RQWEg6njB+itsFzGVSJv+O//2hvNXZuxVIRyNzrgsZ37SPDdGJJw==
dependencies:
b4a "^1.0.1"
sha256-wasm "^2.2.1"
sha256-wasm@^2.2.1:
version "2.2.2"
resolved "https://registry.yarnpkg.com/sha256-wasm/-/sha256-wasm-2.2.2.tgz#4940b6c9ba28f3f08b700efce587ef36d4d516d4"
integrity sha512-qKSGARvao+JQlFiA+sjJZhJ/61gmW/3aNLblB2rsgIxDlDxsJPHo8a1seXj12oKtuHVgJSJJ7QEGBUYQN741lQ==
dependencies:
b4a "^1.0.1"
nanoassert "^2.0.0"
sha512-universal@^1.1.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/sha512-universal/-/sha512-universal-1.2.1.tgz#829505a7586530515cc1a10b78815c99722c4df0"
integrity sha512-kehYuigMoRkIngCv7rhgruLJNNHDnitGTBdkcYbCbooL8Cidj/bS78MDxByIjcc69M915WxcQTgZetZ1JbeQTQ==
dependencies:
b4a "^1.0.1"
sha512-wasm "^2.3.1"
sha512-wasm@^2.3.1:
version "2.3.4"
resolved "https://registry.yarnpkg.com/sha512-wasm/-/sha512-wasm-2.3.4.tgz#b86b37112ff6d1fc3740f2484a6855f17a6e1300"
integrity sha512-akWoxJPGCB3aZCrZ+fm6VIFhJ/p8idBv7AWGFng/CZIrQo51oQNsvDbTSRXWAzIiZJvpy16oIDiCCPqTe21sKg==
dependencies:
b4a "^1.0.1"
nanoassert "^2.0.0"
shebang-command@^1.2.0: shebang-command@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
@ -6056,13 +5855,6 @@ signal-exit@^3.0.2, signal-exit@^3.0.3:
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.5.tgz#9e3e8cc0c75a99472b44321033a7702e7738252f" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.5.tgz#9e3e8cc0c75a99472b44321033a7702e7738252f"
integrity sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ== integrity sha512-KWcOiKeQj6ZyXx7zq4YxSMgHRlod4czeBQZrPb8OKcohcqAXShm7E20kEMle9WBt26hFcAf0qLOcp5zmY7kOqQ==
siphash24@^1.0.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/siphash24/-/siphash24-1.3.1.tgz#7f87fd2c5db88d8d46335a68f780f281641c8b22"
integrity sha512-moemC3ZKiTzH29nbFo3Iw8fbemWWod4vNs/WgKbQ54oEs6mE6XVlguxvinYjB+UmaE0PThgyED9fUkWvirT8hA==
dependencies:
nanoassert "^2.0.0"
sisteransi@^1.0.5: sisteransi@^1.0.5:
version "1.0.5" version "1.0.5"
resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed"
@ -6087,50 +5879,13 @@ slick@^1.12.2:
resolved "https://registry.yarnpkg.com/slick/-/slick-1.12.2.tgz#bd048ddb74de7d1ca6915faa4a57570b3550c2d7" resolved "https://registry.yarnpkg.com/slick/-/slick-1.12.2.tgz#bd048ddb74de7d1ca6915faa4a57570b3550c2d7"
integrity sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A== integrity sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==
sodium-javascript@~0.8.0: sodium-native@^3.3.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/sodium-javascript/-/sodium-javascript-0.8.0.tgz#0a94d7bb58ab17be82255f3949259af59778fdbc"
integrity sha512-rEBzR5mPxPES+UjyMDvKPIXy9ImF17KOJ32nJNi9uIquWpS/nfj+h6m05J5yLJaGXjgM72LmQoUbWZVxh/rmGg==
dependencies:
blake2b "^2.1.1"
chacha20-universal "^1.0.4"
nanoassert "^2.0.0"
sha256-universal "^1.1.0"
sha512-universal "^1.1.0"
siphash24 "^1.0.1"
xsalsa20 "^1.0.0"
sodium-native@^3.1.1, sodium-native@^3.2.0, sodium-native@^3.3.0:
version "3.3.0" version "3.3.0"
resolved "https://registry.yarnpkg.com/sodium-native/-/sodium-native-3.3.0.tgz#50ee52ac843315866cce3d0c08ab03eb78f22361" resolved "https://registry.yarnpkg.com/sodium-native/-/sodium-native-3.3.0.tgz#50ee52ac843315866cce3d0c08ab03eb78f22361"
integrity sha512-rg6lCDM/qa3p07YGqaVD+ciAbUqm6SoO4xmlcfkbU5r1zIGrguXztLiEtaLYTV5U6k8KSIUFmnU3yQUSKmf6DA== integrity sha512-rg6lCDM/qa3p07YGqaVD+ciAbUqm6SoO4xmlcfkbU5r1zIGrguXztLiEtaLYTV5U6k8KSIUFmnU3yQUSKmf6DA==
dependencies: dependencies:
node-gyp-build "^4.3.0" node-gyp-build "^4.3.0"
sodium-secretstream@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/sodium-secretstream/-/sodium-secretstream-1.0.2.tgz#ae6fec16555f1a1d9fd2460b41256736d5044e13"
integrity sha512-AsWztbBHhHid+w5g28ftXA0mTrS52Dup7FYI0GR7ri1TQTlVsw0z//FNlhIqWsgtBctO/DxQosacbElCpmdcZw==
dependencies:
b4a "^1.1.1"
sodium-universal "^3.0.4"
sodium-universal@^3.0.0, sodium-universal@^3.0.4:
version "3.1.0"
resolved "https://registry.yarnpkg.com/sodium-universal/-/sodium-universal-3.1.0.tgz#f2fa0384d16b7cb99b1c8551a39cc05391a3ed41"
integrity sha512-N2gxk68Kg2qZLSJ4h0NffEhp4BjgWHCHXVlDi1aG1hA3y+ZeWEmHqnpml8Hy47QzfL1xLy5nwr9LcsWAg2Ep0A==
dependencies:
blake2b "^2.1.1"
chacha20-universal "^1.0.4"
nanoassert "^2.0.0"
resolve "^1.17.0"
sha256-universal "^1.1.0"
sha512-universal "^1.1.0"
siphash24 "^1.0.1"
sodium-javascript "~0.8.0"
sodium-native "^3.2.0"
xsalsa20 "^1.0.0"
source-map-support@^0.5.6: source-map-support@^0.5.6:
version "0.5.20" version "0.5.20"
resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.20.tgz#12166089f8f5e5e8c56926b377633392dd2cb6c9" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.20.tgz#12166089f8f5e5e8c56926b377633392dd2cb6c9"
@ -6216,14 +5971,6 @@ streamsearch@0.1.2:
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"
integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo= integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=
streamx@^2.10.2, streamx@^2.10.3, streamx@^2.12.0, streamx@^2.12.4:
version "2.12.4"
resolved "https://registry.yarnpkg.com/streamx/-/streamx-2.12.4.tgz#0369848b20b8f79c65320735372df17cafcd9aff"
integrity sha512-K3xdIp8YSkvbdI0PrCcP0JkniN8cPCyeKlcZgRFSl1o1xKINCYM93FryvTSOY57x73pz5/AjO5B8b9BYf21wWw==
dependencies:
fast-fifo "^1.0.0"
queue-tick "^1.0.0"
string-length@^4.0.1: string-length@^4.0.1:
version "4.0.2" version "4.0.2"
resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a"
@ -6388,16 +6135,6 @@ throat@^6.0.1:
resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.1.tgz#d514fedad95740c12c2d7fc70ea863eb51ade375" resolved "https://registry.yarnpkg.com/throat/-/throat-6.0.1.tgz#d514fedad95740c12c2d7fc70ea863eb51ade375"
integrity sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w== integrity sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==
time-ordered-set@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/time-ordered-set/-/time-ordered-set-1.0.2.tgz#3bd931fc048234147f8c2b8b1ebbebb0a3ecb96f"
integrity sha512-vGO99JkxvgX+u+LtOKQEpYf31Kj3i/GNwVstfnh4dyINakMgeZCpew1e3Aj+06hEslhtHEd52g7m5IV+o1K8Mw==
timeout-refresh@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/timeout-refresh/-/timeout-refresh-2.0.1.tgz#f8ec7cf1f9d93b2635b7d4388cb820c5f6c16f98"
integrity sha512-SVqEcMZBsZF9mA78rjzCrYrUs37LMJk3ShZ851ygZYW1cMeIjs9mL57KO6Iv5mmjSQnOe/29/VAfGXo+oRCiVw==
titleize@2: titleize@2:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/titleize/-/titleize-2.1.0.tgz#5530de07c22147a0488887172b5bd94f5b30a48f" resolved "https://registry.yarnpkg.com/titleize/-/titleize-2.1.0.tgz#5530de07c22147a0488887172b5bd94f5b30a48f"
@ -6622,16 +6359,6 @@ uc.micro@^1.0.1:
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac"
integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==
udx-native@^1.1.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/udx-native/-/udx-native-1.2.1.tgz#a229b8bfab8c9c9eea05c7e0d68e671ab70d562d"
integrity sha512-hLoJ3rE1PuqO/A1YENG8oYNuAGltdwXofzavYwXbg2yk/qQgGBDpUQd/qtdENxkawad5cEEdJEdwvchslDl7OA==
dependencies:
b4a "^1.5.0"
napi-macros "^2.0.0"
node-gyp-build "^4.4.0"
streamx "^2.12.0"
unbox-primitive@^1.0.1: unbox-primitive@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471"
@ -6936,11 +6663,6 @@ write-file-atomic@^3.0.0:
resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.5.tgz#8b4bc4af518cfabd0473ae4f99144287b33eb881" resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.5.tgz#8b4bc4af518cfabd0473ae4f99144287b33eb881"
integrity sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w== integrity sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==
xache@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/xache/-/xache-1.1.0.tgz#afc20dec9ff8b2260eea03f5ad9422dc0200c6e9"
integrity sha512-RQGZDHLy/uCvnIrAvaorZH/e6Dfrtxj16iVlGjkj4KD2/G/dNXNqhk5IdSucv5nSSnDK00y8Y/2csyRdHveJ+Q==
xdg-basedir@^4.0.0: xdg-basedir@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13"
@ -6956,11 +6678,6 @@ xmlchars@^2.2.0:
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
xsalsa20@^1.0.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/xsalsa20/-/xsalsa20-1.2.0.tgz#e5a05cb26f8cef723f94a559102ed50c1b44c25c"
integrity sha512-FIr/DEeoHfj7ftfylnoFt3rAIRoWXpx2AoDfrT2qD2wtp7Dp+COajvs/Icb7uHqRW9m60f5iXZwdsJJO3kvb7w==
xss@^1.0.8: xss@^1.0.8:
version "1.0.10" version "1.0.10"
resolved "https://registry.yarnpkg.com/xss/-/xss-1.0.10.tgz#5cd63a9b147a755a14cb0455c7db8866120eb4d2" resolved "https://registry.yarnpkg.com/xss/-/xss-1.0.10.tgz#5cd63a9b147a755a14cb0455c7db8866120eb4d2"

View File

@ -1,5 +1,3 @@
CONFIG_VERSION=v1.2022-03-18
DB_HOST=localhost DB_HOST=localhost
DB_PORT=3306 DB_PORT=3306
DB_USER=root DB_USER=root

View File

@ -16,17 +16,17 @@ export class EventProtocol extends BaseEntity {
@Column({ name: 'user_id', unsigned: true, nullable: false }) @Column({ name: 'user_id', unsigned: true, nullable: false })
userId: number userId: number
@Column({ name: 'x_user_id', unsigned: true, nullable: true }) @Column({ name: 'x_user_id', type: 'int', unsigned: true, nullable: true })
xUserId: number xUserId: number | null
@Column({ name: 'x_community_id', unsigned: true, nullable: true }) @Column({ name: 'x_community_id', type: 'int', unsigned: true, nullable: true })
xCommunityId: number xCommunityId: number | null
@Column({ name: 'transaction_id', unsigned: true, nullable: true }) @Column({ name: 'transaction_id', type: 'int', unsigned: true, nullable: true })
transactionId: number transactionId: number | null
@Column({ name: 'contribution_id', unsigned: true, nullable: true }) @Column({ name: 'contribution_id', type: 'int', unsigned: true, nullable: true })
contributionId: number contributionId: number | null
@Column({ @Column({
type: 'decimal', type: 'decimal',
@ -35,8 +35,8 @@ export class EventProtocol extends BaseEntity {
nullable: true, nullable: true,
transformer: DecimalTransformer, transformer: DecimalTransformer,
}) })
amount: Decimal amount: Decimal | null
@Column({ name: 'message_id', unsigned: true, nullable: true }) @Column({ name: 'message_id', type: 'int', unsigned: true, nullable: true })
messageId: number messageId: number | null
} }

View File

@ -0,0 +1,51 @@
import {
BaseEntity,
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm'
@Entity('communities')
export class Community extends BaseEntity {
@PrimaryGeneratedColumn('increment', { unsigned: true })
id: number
@Column({ name: 'foreign', type: 'bool', nullable: false, default: true })
foreign: boolean
@Column({ name: 'public_key', type: 'binary', length: 64, default: null, nullable: true })
publicKey: Buffer
@Column({ name: 'api_version', length: 10, nullable: false })
apiVersion: string
@Column({ name: 'end_point', length: 255, nullable: false })
endPoint: string
@Column({ name: 'last_announced_at', type: 'datetime', nullable: true })
lastAnnouncedAt: Date
@Column({ name: 'verified_at', type: 'datetime', nullable: true })
verifiedAt: Date
@Column({ name: 'last_error_at', type: 'datetime', nullable: true })
lastErrorAt: Date
@CreateDateColumn({
name: 'created_at',
type: 'datetime',
default: () => 'CURRENT_TIMESTAMP(3)',
nullable: false,
})
createdAt: Date
@UpdateDateColumn({
name: 'updated_at',
type: 'datetime',
onUpdate: 'CURRENT_TIMESTAMP(3)',
nullable: true,
})
updatedAt: Date | null
}

View File

@ -1 +1 @@
export { Community } from './0058-add_communities_table/Community' export { Community } from './0060-update_communities_table/Community'

View File

@ -0,0 +1,32 @@
/* MIGRATION TO CREATE THE FEDERATION COMMUNITY TABLES
*
* This migration creates the `community` and 'communityfederation' tables in the `apollo` database (`gradido_community`).
*/
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
await queryFn(
'ALTER TABLE `communities` MODIFY COLUMN `last_announced_at` datetime(3) AFTER `end_point`;',
)
await queryFn(
'ALTER TABLE `communities` ADD COLUMN `foreign` tinyint(4) NOT NULL DEFAULT 1 AFTER `id`;',
)
await queryFn(
'ALTER TABLE `communities` ADD COLUMN `verified_at` datetime(3) AFTER `last_announced_at`;',
)
await queryFn(
'ALTER TABLE `communities` ADD COLUMN `last_error_at` datetime(3) AFTER `verified_at`;',
)
}
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
// write downgrade logic as parameter of queryFn
await queryFn(
'ALTER TABLE `communities` MODIFY COLUMN `last_announced_at` datetime(3) NOT NULL AFTER `end_point`;',
)
await queryFn('ALTER TABLE `communities` DROP COLUMN `foreign`;')
await queryFn('ALTER TABLE `communities` DROP COLUMN `verified_at`;')
await queryFn('ALTER TABLE `communities` DROP COLUMN `last_error_at`;')
}

View File

@ -1,6 +1,6 @@
{ {
"name": "gradido-database", "name": "gradido-database",
"version": "1.17.1", "version": "1.18.2",
"description": "Gradido Database Tool to execute database migrations", "description": "Gradido Database Tool to execute database migrations",
"main": "src/index.ts", "main": "src/index.ts",
"repository": "https://github.com/gradido/gradido/database", "repository": "https://github.com/gradido/gradido/database",

View File

@ -27,7 +27,7 @@ COMMUNITY_DESCRIPTION="Gradido Development Stage1 Test Community"
COMMUNITY_SUPPORT_MAIL=support@supportmail.com COMMUNITY_SUPPORT_MAIL=support@supportmail.com
# backend # backend
BACKEND_CONFIG_VERSION=v14.2022-12-22 BACKEND_CONFIG_VERSION=v15.2023-02-07
JWT_EXPIRES_IN=10m JWT_EXPIRES_IN=10m
GDT_API_URL=https://gdt.gradido.net GDT_API_URL=https://gdt.gradido.net
@ -56,9 +56,6 @@ EMAIL_CODE_REQUEST_TIME=10
WEBHOOK_ELOPAGE_SECRET=secret WEBHOOK_ELOPAGE_SECRET=secret
# EventProtocol
EVENT_PROTOCOL_DISABLED=false
# Federation # Federation
# if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen # if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen
# on an hash created from this topic # on an hash created from this topic

View File

@ -1,5 +1,3 @@
CONFIG_VERSION=v1.2023-01-01
# Database # Database
DB_HOST=localhost DB_HOST=localhost
DB_PORT=3306 DB_PORT=3306
@ -8,9 +6,6 @@ DB_PASSWORD=
DB_DATABASE=gradido_community DB_DATABASE=gradido_community
TYPEORM_LOGGING_RELATIVE_PATH=typeorm.dht-node.log TYPEORM_LOGGING_RELATIVE_PATH=typeorm.dht-node.log
# EventProtocol
EVENT_PROTOCOL_DISABLED=false
# SET LOG LEVEL AS NEEDED IN YOUR .ENV # SET LOG LEVEL AS NEEDED IN YOUR .ENV
# POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal # POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal
# LOG_LEVEL=info # LOG_LEVEL=info
@ -20,3 +15,5 @@ EVENT_PROTOCOL_DISABLED=false
# on an hash created from this topic # on an hash created from this topic
FEDERATION_DHT_TOPIC=GRADIDO_HUB FEDERATION_DHT_TOPIC=GRADIDO_HUB
# FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f # FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f
# FEDERATION_COMMUNITY_URL=http://localhost
# FEDERATION_COMMUNITY_API_PORT=5000

View File

@ -8,10 +8,8 @@ DB_PASSWORD=$DB_PASSWORD
DB_DATABASE=gradido_community DB_DATABASE=gradido_community
TYPEORM_LOGGING_RELATIVE_PATH=$TYPEORM_LOGGING_RELATIVE_PATH TYPEORM_LOGGING_RELATIVE_PATH=$TYPEORM_LOGGING_RELATIVE_PATH
# EventProtocol
EVENT_PROTOCOL_DISABLED=$EVENT_PROTOCOL_DISABLED
# Federation # Federation
FEDERATION_DHT_TOPIC=$FEDERATION_DHT_TOPIC FEDERATION_DHT_TOPIC=$FEDERATION_DHT_TOPIC
FEDERATION_DHT_SEED=$FEDERATION_DHT_SEED FEDERATION_DHT_SEED=$FEDERATION_DHT_SEED
FEDERATION_COMMUNITY_URL=$FEDERATION_COMMUNITY_URL FEDERATION_COMMUNITY_URL=$FEDERATION_COMMUNITY_URL
FEDERATION_COMMUNITY_API_PORT=$FEDERATION_COMMUNITY_API_PORT

View File

@ -3,13 +3,13 @@ import dotenv from 'dotenv'
dotenv.config() dotenv.config()
const constants = { const constants = {
DB_VERSION: '0059-add_hide_amount_to_users', DB_VERSION: '0060-update_communities_table',
LOG4JS_CONFIG: 'log4js-config.json', LOG4JS_CONFIG: 'log4js-config.json',
// default log level on production should be info // default log level on production should be info
LOG_LEVEL: process.env.LOG_LEVEL || 'info', LOG_LEVEL: process.env.LOG_LEVEL || 'info',
CONFIG_VERSION: { CONFIG_VERSION: {
DEFAULT: 'DEFAULT', DEFAULT: 'DEFAULT',
EXPECTED: 'v1.2023-01-01', EXPECTED: 'v2.2023-02-07',
CURRENT: '', CURRENT: '',
}, },
} }
@ -28,15 +28,11 @@ const database = {
process.env.TYPEORM_LOGGING_RELATIVE_PATH || 'typeorm.dht-node.log', process.env.TYPEORM_LOGGING_RELATIVE_PATH || 'typeorm.dht-node.log',
} }
const eventProtocol = {
// global switch to enable writing of EventProtocol-Entries
EVENT_PROTOCOL_DISABLED: process.env.EVENT_PROTOCOL_DISABLED === 'true' || false,
}
const federation = { const federation = {
FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || 'GRADIDO_HUB', FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || 'GRADIDO_HUB',
FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null, FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null,
FEDERATION_COMMUNITY_URL: process.env.FEDERATION_COMMUNITY_URL || null, FEDERATION_COMMUNITY_URL: process.env.FEDERATION_COMMUNITY_URL || 'http://localhost',
FEDERATION_COMMUNITY_API_PORT: process.env.FEDERATION_COMMUNITY_API_PORT || '5000',
} }
// Check config version // Check config version
@ -55,7 +51,6 @@ const CONFIG = {
...constants, ...constants,
...server, ...server,
...database, ...database,
...eventProtocol,
...federation, ...federation,
} }

View File

@ -116,6 +116,7 @@ describe('federation', () => {
beforeEach(async () => { beforeEach(async () => {
DHT.mockClear() DHT.mockClear()
jest.clearAllMocks() jest.clearAllMocks()
await cleanDB()
await startDHT(TEST_TOPIC) await startDHT(TEST_TOPIC)
}) })
@ -234,18 +235,18 @@ describe('federation', () => {
beforeEach(async () => { beforeEach(async () => {
jest.clearAllMocks() jest.clearAllMocks()
jsonArray = [ jsonArray = [
{ api: 'v1_0', url: 'too much versions at the same time test' }, { api: '1_0', url: 'too much versions at the same time test' },
{ api: 'v1_0', url: 'url2' }, { api: '1_0', url: 'url2' },
{ api: 'v1_0', url: 'url3' }, { api: '1_0', url: 'url3' },
{ api: 'v1_0', url: 'url4' }, { api: '1_0', url: 'url4' },
{ api: 'v1_0', url: 'url5' }, { api: '1_0', url: 'url5' },
] ]
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray))) await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
}) })
it('logs the received data', () => { it('logs the received data', () => {
expect(logger.info).toBeCalledWith( expect(logger.info).toBeCalledWith(
'data: [{"api":"v1_0","url":"too much versions at the same time test"},{"api":"v1_0","url":"url2"},{"api":"v1_0","url":"url3"},{"api":"v1_0","url":"url4"},{"api":"v1_0","url":"url5"}]', 'data: [{"api":"1_0","url":"too much versions at the same time test"},{"api":"1_0","url":"url2"},{"api":"1_0","url":"url3"},{"api":"1_0","url":"url4"},{"api":"1_0","url":"url5"}]',
) )
}) })
@ -266,17 +267,17 @@ describe('federation', () => {
jsonArray = [ jsonArray = [
{ {
wrong: 'wrong but tolerated property test', wrong: 'wrong but tolerated property test',
api: 'v1_0', api: '1_0',
url: 'url1', url: 'url1',
}, },
{ {
api: 'v2_0', api: '2_0',
url: 'url2', url: 'url2',
wrong: 'wrong but tolerated property test', wrong: 'wrong but tolerated property test',
}, },
] ]
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray))) await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
result = await DbCommunity.find() result = await DbCommunity.find({ foreign: true })
}) })
afterAll(async () => { afterAll(async () => {
@ -287,13 +288,14 @@ describe('federation', () => {
expect(result).toHaveLength(2) expect(result).toHaveLength(2)
}) })
it('has an entry for api version v1_0', () => { it('has an entry for api version 1_0', () => {
expect(result).toEqual( expect(result).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
id: expect.any(Number), id: expect.any(Number),
foreign: true,
publicKey: expect.any(Buffer), publicKey: expect.any(Buffer),
apiVersion: 'v1_0', apiVersion: '1_0',
endPoint: 'url1', endPoint: 'url1',
lastAnnouncedAt: expect.any(Date), lastAnnouncedAt: expect.any(Date),
createdAt: expect.any(Date), createdAt: expect.any(Date),
@ -303,13 +305,14 @@ describe('federation', () => {
) )
}) })
it('has an entry for api version v2_0', () => { it('has an entry for api version 2_0', () => {
expect(result).toEqual( expect(result).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
id: expect.any(Number), id: expect.any(Number),
foreign: true,
publicKey: expect.any(Buffer), publicKey: expect.any(Buffer),
apiVersion: 'v2_0', apiVersion: '2_0',
endPoint: 'url2', endPoint: 'url2',
lastAnnouncedAt: expect.any(Date), lastAnnouncedAt: expect.any(Date),
createdAt: expect.any(Date), createdAt: expect.any(Date),
@ -535,7 +538,7 @@ describe('federation', () => {
{ api: 'toolong api', url: 'some valid url' }, { api: 'toolong api', url: 'some valid url' },
] ]
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray))) await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
result = await DbCommunity.find() result = await DbCommunity.find({ foreign: true })
}) })
afterAll(async () => { afterAll(async () => {
@ -551,6 +554,7 @@ describe('federation', () => {
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
id: expect.any(Number), id: expect.any(Number),
foreign: true,
publicKey: expect.any(Buffer), publicKey: expect.any(Buffer),
apiVersion: 'valid api', apiVersion: 'valid api',
endPoint: endPoint:
@ -588,7 +592,7 @@ describe('federation', () => {
}, },
] ]
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray))) await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
result = await DbCommunity.find() result = await DbCommunity.find({ foreign: true })
}) })
afterAll(async () => { afterAll(async () => {
@ -604,6 +608,7 @@ describe('federation', () => {
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
id: expect.any(Number), id: expect.any(Number),
foreign: true,
publicKey: expect.any(Buffer), publicKey: expect.any(Buffer),
apiVersion: 'valid api1', apiVersion: 'valid api1',
endPoint: endPoint:
@ -621,6 +626,7 @@ describe('federation', () => {
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
id: expect.any(Number), id: expect.any(Number),
foreign: true,
publicKey: expect.any(Buffer), publicKey: expect.any(Buffer),
apiVersion: 'valid api2', apiVersion: 'valid api2',
endPoint: endPoint:
@ -638,6 +644,7 @@ describe('federation', () => {
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
id: expect.any(Number), id: expect.any(Number),
foreign: true,
publicKey: expect.any(Buffer), publicKey: expect.any(Buffer),
apiVersion: 'valid api3', apiVersion: 'valid api3',
endPoint: endPoint:
@ -655,6 +662,7 @@ describe('federation', () => {
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
id: expect.any(Number), id: expect.any(Number),
foreign: true,
publicKey: expect.any(Buffer), publicKey: expect.any(Buffer),
apiVersion: 'valid api4', apiVersion: 'valid api4',
endPoint: endPoint:
@ -710,17 +718,17 @@ describe('federation', () => {
Buffer.from( Buffer.from(
JSON.stringify([ JSON.stringify([
{ {
api: 'v1_0', api: '1_0',
url: 'http://localhost:4000/api/v1_0', url: 'http://localhost:5001/api/',
}, },
{ {
api: 'v2_0', api: '2_0',
url: 'http://localhost:4000/api/v2_0', url: 'http://localhost:5002/api/',
}, },
]), ]),
), ),
) )
result = await DbCommunity.find() result = await DbCommunity.find({ foreign: true })
}) })
afterAll(async () => { afterAll(async () => {
@ -736,9 +744,10 @@ describe('federation', () => {
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
id: expect.any(Number), id: expect.any(Number),
foreign: true,
publicKey: expect.any(Buffer), publicKey: expect.any(Buffer),
apiVersion: 'v1_0', apiVersion: '1_0',
endPoint: 'http://localhost:4000/api/v1_0', endPoint: 'http://localhost:5001/api/',
lastAnnouncedAt: expect.any(Date), lastAnnouncedAt: expect.any(Date),
createdAt: expect.any(Date), createdAt: expect.any(Date),
updatedAt: null, updatedAt: null,
@ -747,14 +756,15 @@ describe('federation', () => {
) )
}) })
it('has an entry for api version v2_0', () => { it('has an entry for api version 2_0', () => {
expect(result).toEqual( expect(result).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
id: expect.any(Number), id: expect.any(Number),
foreign: true,
publicKey: expect.any(Buffer), publicKey: expect.any(Buffer),
apiVersion: 'v2_0', apiVersion: '2_0',
endPoint: 'http://localhost:4000/api/v2_0', endPoint: 'http://localhost:5002/api/',
lastAnnouncedAt: expect.any(Date), lastAnnouncedAt: expect.any(Date),
createdAt: expect.any(Date), createdAt: expect.any(Date),
updatedAt: null, updatedAt: null,
@ -775,16 +785,16 @@ describe('federation', () => {
Buffer.from( Buffer.from(
JSON.stringify([ JSON.stringify([
{ {
api: 'v1_0', api: '1_0',
url: 'http://localhost:4000/api/v1_0', url: 'http://localhost:5001/api/',
}, },
{ {
api: 'v1_1', api: '1_1',
url: 'http://localhost:4000/api/v1_1', url: 'http://localhost:5002/api/',
}, },
{ {
api: 'v2_0', api: '2_0',
url: 'http://localhost:4000/api/v2_0', url: 'http://localhost:5003/api/',
}, },
]), ]),
), ),

View File

@ -15,9 +15,9 @@ const ERRORTIME = 240000
const ANNOUNCETIME = 30000 const ANNOUNCETIME = 30000
enum ApiVersionType { enum ApiVersionType {
V1_0 = 'v1_0', V1_0 = '1_0',
V1_1 = 'v1_1', V1_1 = '1_1',
V2_0 = 'v2_0', V2_0 = '2_0',
} }
type CommunityApi = { type CommunityApi = {
api: string api: string
@ -31,13 +31,7 @@ export const startDHT = async (topic: string): Promise<void> => {
logger.info(`keyPairDHT: publicKey=${keyPair.publicKey.toString('hex')}`) logger.info(`keyPairDHT: publicKey=${keyPair.publicKey.toString('hex')}`)
logger.debug(`keyPairDHT: secretKey=${keyPair.secretKey.toString('hex')}`) logger.debug(`keyPairDHT: secretKey=${keyPair.secretKey.toString('hex')}`)
const ownApiVersions = Object.values(ApiVersionType).map(function (apiEnum) { const ownApiVersions = writeHomeCommunityEnries(keyPair.publicKey)
const comApi: CommunityApi = {
api: apiEnum,
url: CONFIG.FEDERATION_COMMUNITY_URL + apiEnum,
}
return comApi
})
logger.debug(`ApiList: ${JSON.stringify(ownApiVersions)}`) logger.debug(`ApiList: ${JSON.stringify(ownApiVersions)}`)
const node = new DHT({ keyPair }) const node = new DHT({ keyPair })
@ -184,3 +178,34 @@ export const startDHT = async (topic: string): Promise<void> => {
logger.error('DHT unexpected error:', err) logger.error('DHT unexpected error:', err)
} }
} }
async function writeHomeCommunityEnries(pubKey: any): Promise<CommunityApi[]> {
const homeApiVersions: CommunityApi[] = Object.values(ApiVersionType).map(function (apiEnum) {
const port =
Number.parseInt(CONFIG.FEDERATION_COMMUNITY_API_PORT) + Number(apiEnum.replace('_', ''))
const comApi: CommunityApi = {
api: apiEnum,
url: CONFIG.FEDERATION_COMMUNITY_URL + ':' + port.toString() + '/api/',
}
return comApi
})
try {
// first remove privious existing homeCommunity entries
DbCommunity.createQueryBuilder().delete().where({ foreign: false }).execute()
homeApiVersions.forEach(async function (homeApi) {
const homeCom = new DbCommunity()
homeCom.foreign = false
homeCom.apiVersion = homeApi.api
homeCom.endPoint = homeApi.url
homeCom.publicKey = pubKey.toString('hex')
// this will NOT update the updatedAt column, to distingue between a normal update and the last announcement
await DbCommunity.insert(homeCom)
logger.info(`federation home-community inserted successfully: ${JSON.stringify(homeCom)}`)
})
} catch (err) {
throw new Error(`Federation: Error writing HomeCommunity-Entries: ${err}`)
}
return homeApiVersions
}

View File

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

View File

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

View File

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

26
e2e-tests/.eslintrc.js Normal file
View File

@ -0,0 +1,26 @@
module.exports = {
root: true,
env: {
node: true,
cypress: true,
},
parser: '@typescript-eslint/parser',
plugins: ['cypress', 'prettier', '@typescript-eslint' /*, 'jest' */],
extends: [
'standard',
'eslint:recommended',
'plugin:prettier/recommended',
'plugin:@typescript-eslint/recommended',
],
// add your custom rules here
rules: {
'no-console': ['error'],
'no-debugger': 'error',
'prettier/prettier': [
'error',
{
htmlWhitespaceSensitivity: 'ignore',
},
],
},
}

5
e2e-tests/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules/
cypress/screenshots/
cypress/videos/
cucumber-messages.ndjson

9
e2e-tests/.prettierrc.js Normal file
View File

@ -0,0 +1,9 @@
module.exports = {
semi: false,
printWidth: 100,
singleQuote: true,
trailingComma: "all",
tabWidth: 2,
bracketSpacing: true,
endOfLine: "auto",
};

View File

@ -11,7 +11,7 @@
############################################################################### ###############################################################################
FROM cypress/base:16.14.2-slim FROM cypress/base:16.14.2-slim
ARG DOCKER_WORKDIR=/tests/ ARG DOCKER_WORKDIR="/tests"
WORKDIR $DOCKER_WORKDIR WORKDIR $DOCKER_WORKDIR
# install dependencies # install dependencies

View File

@ -1,7 +1,73 @@
# Gradido end-to-end tests # Gradido End-to-End Testing with [Cypress](https://www.cypress.io/) (CI-ready via Docker)
This is still WIP. A setup to show-case Cypress as an end-to-end testing tool for Gradido running in a Docker container.
The tests are organized in feature files written in Gherkin syntax.
For automated end-to-end testing one of the frameworks Cypress or Playwright will be utilized.
For more details on how to run them, see the subfolders' README instructions. ## Features under test
So far these features are initially tested
- [User authentication](https://github.com/gradido/gradido/blob/master/e2e-tests/cypress/tests/cypress/e2e/User.Authentication.feature)
- [User profile - change password](https://github.com/gradido/gradido/blob/master/e2e-tests/cypress/tests/cypress/e2e/UserProfile.ChangePassword.feature)
- [User registration]((https://github.com/gradido/gradido/blob/master/e2e-tests/cypress/tests/cypress/e2e/User.Registration.feature)) (WIP)
## Precondition
Before running the tests, change to the repo's root directory (gradido).
### Boot up the system under test
```bash
docker-compose up
```
### Seed the database
The database has to be seeded upfront to every test run.
```bash
# change to the backend directory
cd /path/to/gradido/gradido/backend
# install all dependencies
yarn
# seed the database (everytime before running the tests)
yarn seed
```
## Execute the test
This setup will be integrated in the Gradido Github Actions to automatically support the CI/CD process.
For now the test setup can only be used locally in two modes.
### Run Cypress directly from the code
```bash
# change to the tests directory
cd /path/to/gradido/e2e-tests/
# install all dependencies
yarn install
# a) run the tests on command line
yarn cypress run
# b) open the Cypress GUI to run the tests in interactive mode
yarn cypress open
```
### Run Cyprss from a separate Docker container
```bash
# change to the cypress directory
cd /path/to/gradido/e2e-tests/
# build a Docker image from the Dockerfile
docker build -t gradido_e2e-tests-cypress .
# run the Docker image and execute the given tests
docker run -it --network=host gradido_e2e-tests-cypress yarn cypress-e2e
```

View File

@ -0,0 +1,79 @@
import { defineConfig } from 'cypress'
import { addCucumberPreprocessorPlugin } from '@badeball/cypress-cucumber-preprocessor'
import browserify from '@badeball/cypress-cucumber-preprocessor/browserify'
let resetPasswordLink: string
async function setupNodeEvents(
on: Cypress.PluginEvents,
config: Cypress.PluginConfigOptions
): Promise<Cypress.PluginConfigOptions> {
await addCucumberPreprocessorPlugin(on, config)
on(
'file:preprocessor',
browserify(config, {
typescript: require.resolve('typescript'),
})
)
on('task', {
setResetPasswordLink: (val) => {
return (resetPasswordLink = val)
},
getResetPasswordLink: () => {
return resetPasswordLink
},
})
on('after:run', (results) => {
if (results) {
// results will be undefined in interactive mode
// eslint-disable-next-line no-console
console.log(results.status)
}
})
return config
}
export default defineConfig({
e2e: {
specPattern: '**/*.feature',
excludeSpecPattern: '*.js',
experimentalSessionAndOrigin: true,
baseUrl: 'http://localhost:3000',
chromeWebSecurity: false,
defaultCommandTimeout: 10000,
supportFile: 'cypress/support/index.ts',
viewportHeight: 720,
viewportWidth: 1280,
video: false,
retries: {
runMode: 2,
openMode: 0,
},
env: {
backendURL: 'http://localhost:4000',
mailserverURL: 'http://localhost:1080',
loginQuery: `query ($email: String!, $password: String!, $publisherId: Int) {
login(email: $email, password: $password, publisherId: $publisherId) {
email
firstName
lastName
language
klickTipp {
newsletterState
__typename
}
hasElopage
publisherId
isAdmin
creation
__typename
}
}`,
},
setupNodeEvents,
},
})

View File

@ -1,4 +0,0 @@
tests/node_modules/
tests/cypress/screenshots/
tests/cypress/videos/
tests/cucumber-messages.ndjson

View File

@ -1,73 +0,0 @@
# Gradido End-to-End Testing with [Cypress](https://www.cypress.io/) (CI-ready via Docker)
A setup to show-case Cypress as an end-to-end testing tool for Gradido running in a Docker container.
The tests are organized in feature files written in Gherkin syntax.
## Features under test
So far these features are initially tested
- [User authentication](https://github.com/gradido/gradido/blob/master/e2e-tests/cypress/tests/cypress/e2e/User.Authentication.feature)
- [User profile - change password](https://github.com/gradido/gradido/blob/master/e2e-tests/cypress/tests/cypress/e2e/UserProfile.ChangePassword.feature)
- [User registration]((https://github.com/gradido/gradido/blob/master/e2e-tests/cypress/tests/cypress/e2e/User.Registration.feature)) (WIP)
## Precondition
Before running the tests, change to the repo's root directory (gradido).
### Boot up the system under test
```bash
docker-compose up
```
### Seed the database
The database has to be seeded upfront to every test run.
```bash
# change to the backend directory
cd /path/to/gradido/gradido/backend
# install all dependencies
yarn
# seed the database (everytime before running the tests)
yarn seed
```
## Execute the test
This setup will be integrated in the Gradido Github Actions to automatically support the CI/CD process.
For now the test setup can only be used locally in two modes.
### Run Cypress directly from the code
```bash
# change to the tests directory
cd /path/to/gradido/e2e-tests/cypress/tests
# install all dependencies
yarn install
# a) run the tests on command line
yarn cypress run
# b) open the Cypress GUI to run the tests in interactive mode
yarn cypress open
```
### Run Cyprss from a separate Docker container
```bash
# change to the cypress directory
cd /path/to/gradido/e2e-tests/cypress/
# build a Docker image from the Dockerfile
docker build -t gradido_e2e-tests-cypress .
# run the Docker image and execute the given tests
docker run -it --network=host gradido_e2e-tests-cypress yarn cypress-e2e
```

View File

@ -0,0 +1,25 @@
Feature: User Authentication - reset password
As a user
I want to reset my password from the sign in page
# TODO for these pre-conditions utilize seeding or API check, if user exists in test system
# Background:
# Given the following "users" are in the database:
# | email | password | name |
# | bibi@bloxberg.de | Aa12345_ | Bibi Bloxberg |
Scenario: Reset password from signin page successfully
Given the user navigates to page "/login"
And the user navigates to the forgot password page
When the user enters the e-mail address "bibi@bloxberg.de"
And the user submits the e-mail form
Then the user receives an e-mail containing the password reset link
When the user opens the password reset link in the browser
And the user enters the password "12345Aa_"
And the user repeats the password "12345Aa_"
And the user submits the password form
And the user clicks the sign in button
Then the user submits the credentials "bibi@bloxberg.de" "Aa12345_"
And the user cannot login
But the user submits the credentials "bibi@bloxberg.de" "12345Aa_"
And the user is logged in with username "Bibi Bloxberg"

View File

@ -11,7 +11,7 @@ Feature: User authentication
# | bibi@bloxberg.de | Aa12345_ | Bibi Bloxberg | # | bibi@bloxberg.de | Aa12345_ | Bibi Bloxberg |
Scenario: Log in successfully Scenario: Log in successfully
Given the browser navigates to page "/login" Given the user navigates to page "/login"
When the user submits the credentials "bibi@bloxberg.de" "Aa12345_" When the user submits the credentials "bibi@bloxberg.de" "Aa12345_"
Then the user is logged in with username "Bibi Bloxberg" Then the user is logged in with username "Bibi Bloxberg"

View File

@ -4,7 +4,7 @@ Feature: User registration
@skip @skip
Scenario: Register successfully Scenario: Register successfully
Given the browser navigates to page "/register" Given the user navigates to page "/register"
When the user fills name and email "Regina" "Register" "regina@register.com" When the user fills name and email "Regina" "Register" "regina@register.com"
And the user agrees to the privacy policy And the user agrees to the privacy policy
And the user submits the registration form And the user submits the registration form

View File

@ -12,7 +12,7 @@ Feature: User profile - change password
Given the user is logged in as "bibi@bloxberg.de" "Aa12345_" Given the user is logged in as "bibi@bloxberg.de" "Aa12345_"
Scenario: Change password successfully Scenario: Change password successfully
Given the browser navigates to page "/profile" Given the user navigates to page "/profile"
And the user opens the change password menu And the user opens the change password menu
When the user fills the password form with: When the user fills the password form with:
| Old password | Aa12345_ | | Old password | Aa12345_ |

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