mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge remote-tracking branch 'origin/master' into
2630-feature-federation-implement-a-graphql-endpoint-for-request-getpublickey
This commit is contained in:
commit
62013c6bc1
51
.github/workflows/test.yml
vendored
51
.github/workflows/test.yml
vendored
@ -274,7 +274,7 @@ jobs:
|
||||
run: docker run --rm gradido/admin:test yarn run lint
|
||||
|
||||
##############################################################################
|
||||
# JOB: STYLELINT ADMIN INTERFACE ##############################################
|
||||
# JOB: STYLELINT ADMIN INTERFACE #############################################
|
||||
##############################################################################
|
||||
stylelint_admin:
|
||||
name: Stylelint - Admin Interface
|
||||
@ -360,6 +360,25 @@ jobs:
|
||||
- name: backend | Lint
|
||||
run: docker run --rm gradido/backend:test yarn run lint
|
||||
|
||||
##############################################################################
|
||||
# JOB: LOCALES BACKEND #######################################################
|
||||
##############################################################################
|
||||
locales_backend:
|
||||
name: Locales - Backend
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build_test_backend]
|
||||
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 ######################################################
|
||||
##############################################################################
|
||||
@ -478,7 +497,7 @@ jobs:
|
||||
report_name: Coverage Admin Interface
|
||||
type: lcov
|
||||
result_path: ./coverage/lcov.info
|
||||
min_coverage: 96
|
||||
min_coverage: 97
|
||||
token: ${{ github.token }}
|
||||
|
||||
##############################################################################
|
||||
@ -526,7 +545,7 @@ jobs:
|
||||
report_name: Coverage Backend
|
||||
type: lcov
|
||||
result_path: ./backend/coverage/lcov.info
|
||||
min_coverage: 78
|
||||
min_coverage: 80
|
||||
token: ${{ github.token }}
|
||||
|
||||
##########################################################################
|
||||
@ -558,7 +577,7 @@ jobs:
|
||||
end-to-end-tests:
|
||||
name: End-to-End Tests
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build_test_mariadb, build_test_database_up, build_test_backend, build_test_admin, build_test_frontend, build_test_nginx]
|
||||
needs: [build_test_mariadb, build_test_database_up, build_test_admin, build_test_frontend, build_test_nginx]
|
||||
steps:
|
||||
##########################################################################
|
||||
# CHECKOUT CODE ##########################################################
|
||||
@ -582,13 +601,6 @@ jobs:
|
||||
path: /tmp
|
||||
- name: Load Docker Image (Database Up)
|
||||
run: docker load < /tmp/database_up.tar
|
||||
- name: Download Docker Image (Backend)
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: docker-backend-test
|
||||
path: /tmp
|
||||
- name: Load Docker Image (Backend)
|
||||
run: docker load < /tmp/backend.tar
|
||||
- name: Download Docker Image (Frontend)
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
@ -621,7 +633,11 @@ jobs:
|
||||
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps database
|
||||
|
||||
- name: Boot up test system | docker-compose backend
|
||||
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps backend
|
||||
run: |
|
||||
cd backend
|
||||
cp .env.test_e2e .env
|
||||
cd ..
|
||||
docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps backend
|
||||
|
||||
- name: Sleep for 10 seconds
|
||||
run: sleep 10s
|
||||
@ -638,6 +654,9 @@ jobs:
|
||||
- name: Boot up test system | docker-compose frontends
|
||||
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps frontend admin nginx
|
||||
|
||||
- name: Boot up test system | docker-compose mailserver
|
||||
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mailserver
|
||||
|
||||
- name: Sleep for 15 seconds
|
||||
run: sleep 15s
|
||||
|
||||
@ -647,12 +666,12 @@ jobs:
|
||||
- name: End-to-end tests | run tests
|
||||
id: e2e-tests
|
||||
run: |
|
||||
cd e2e-tests/cypress/tests/
|
||||
cd e2e-tests/
|
||||
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
|
||||
if: steps.e2e-tests.outcome == 'failure'
|
||||
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: cypress-screenshots
|
||||
path: /home/runner/work/gradido/gradido/e2e-tests/cypress/tests/cypress/screenshots/
|
||||
path: /home/runner/work/gradido/gradido/e2e-tests/cypress/screenshots/
|
||||
|
||||
98
.github/workflows/test_federation.yml
vendored
Normal file
98
.github/workflows/test_federation.yml
vendored
Normal 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 }}
|
||||
49
CHANGELOG.md
49
CHANGELOG.md
@ -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).
|
||||
|
||||
#### [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)
|
||||
|
||||
> 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(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)
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
CONFIG_VERSION=v1.2022-03-18
|
||||
|
||||
GRAPHQL_URI=http://localhost:4000/graphql
|
||||
WALLET_AUTH_URL=http://localhost/authenticate?token={token}
|
||||
WALLET_URL=http://localhost/login
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
"description": "Administraion Interface for Gradido",
|
||||
"main": "index.js",
|
||||
"author": "Moriz Wahl",
|
||||
"version": "1.17.1",
|
||||
"version": "1.18.2",
|
||||
"license": "Apache-2.0",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
@ -86,5 +86,10 @@
|
||||
"> 1%",
|
||||
"last 2 versions",
|
||||
"not ie <= 10"
|
||||
]
|
||||
],
|
||||
"nodemonConfig": {
|
||||
"ignore": [
|
||||
"**/*.spec.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,14 +42,73 @@ describe('ContributionLink', () => {
|
||||
expect(wrapper.find('div.contribution-link').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('emits toggle::collapse new Contribution', async () => {
|
||||
wrapper.vm.editContributionLinkData()
|
||||
expect(wrapper.vm.$root.$emit('bv::toggle::collapse', 'newContribution')).toBeTruthy()
|
||||
it('has one contribution link in table', () => {
|
||||
expect(wrapper.find('div.contribution-link-list').find('tbody').findAll('tr')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('emits toggle::collapse close Contribution-Form ', async () => {
|
||||
wrapper.vm.closeContributionForm()
|
||||
expect(wrapper.vm.$root.$emit('bv::toggle::collapse', 'newContribution')).toBeTruthy()
|
||||
it('has contribution form not visible by default', () => {
|
||||
expect(wrapper.find('#newContribution').isVisible()).toBe(false)
|
||||
})
|
||||
|
||||
describe('click on create new contribution', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('[data-test="new-contribution-link-button"]').trigger('click')
|
||||
})
|
||||
|
||||
it('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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -10,8 +10,9 @@
|
||||
>
|
||||
<b-button
|
||||
v-if="!editContributionLink"
|
||||
v-b-toggle.newContribution
|
||||
@click="visible = !visible"
|
||||
class="my-3 d-flex justify-content-left"
|
||||
data-test="new-contribution-link-button"
|
||||
>
|
||||
{{ $t('math.plus') }} {{ $t('contributionLink.newContributionLink') }}
|
||||
</b-button>
|
||||
|
||||
@ -70,8 +70,6 @@ export default {
|
||||
formatter: (value, key, item) => {
|
||||
if (value) {
|
||||
return this.$d(new Date(value))
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
},
|
||||
@ -81,8 +79,6 @@ export default {
|
||||
formatter: (value, key, item) => {
|
||||
if (value) {
|
||||
return this.$d(new Date(value))
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@ -88,5 +88,16 @@ describe('CreationTransactionList', () => {
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -46,43 +46,45 @@ describe('NavBar', () => {
|
||||
})
|
||||
|
||||
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', () => {
|
||||
expect(wrapper.findAll('.nav-item').at(1).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')
|
||||
expect(wrapper.findAll('.nav-item').at(0).find('a').attributes('href')).toBe('/user')
|
||||
})
|
||||
|
||||
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',
|
||||
)
|
||||
})
|
||||
|
||||
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',
|
||||
)
|
||||
})
|
||||
|
||||
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', () => {
|
||||
const assignLocationSpy = jest.fn()
|
||||
const windowLocationMock = jest.fn()
|
||||
const windowLocation = window.location
|
||||
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', () => {
|
||||
expect(assignLocationSpy).toBeCalledWith('valid-token')
|
||||
expect(windowLocationMock()).toBe('valid-token')
|
||||
})
|
||||
|
||||
it('dispatches logout to store', () => {
|
||||
@ -92,12 +94,18 @@ describe('NavBar', () => {
|
||||
|
||||
describe('logout', () => {
|
||||
const windowLocationMock = jest.fn()
|
||||
const windowLocation = window.location
|
||||
beforeEach(async () => {
|
||||
delete window.location
|
||||
window.location = {
|
||||
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', () => {
|
||||
|
||||
@ -9,15 +9,12 @@
|
||||
|
||||
<b-collapse id="nav-collapse" is-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="/creation">{{ $t('navbar.multi_creation') }}</b-nav-item>
|
||||
<b-nav-item
|
||||
v-show="$store.state.openCreations > 0"
|
||||
class="bg-color-creation p-1"
|
||||
to="/creation-confirm"
|
||||
>
|
||||
{{ $store.state.openCreations }} {{ $t('navbar.open_creation') }}
|
||||
<b-nav-item class="bg-color-creation p-1" to="/creation-confirm">
|
||||
{{ $t('creation') }}
|
||||
<b-badge v-show="$store.state.openCreations > 0" variant="danger">
|
||||
{{ $store.state.openCreations }}
|
||||
</b-badge>
|
||||
</b-nav-item>
|
||||
<b-nav-item to="/contribution-links">
|
||||
{{ $t('navbar.automaticContributions') }}
|
||||
@ -57,7 +54,4 @@ export default {
|
||||
height: 2rem;
|
||||
padding-left: 10px;
|
||||
}
|
||||
.bg-color-creation {
|
||||
background-color: #cf1010dc;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -13,7 +13,8 @@
|
||||
<b-row>
|
||||
<b-col class="col-3">{{ $t('creation_for_month') }}</b-col>
|
||||
<b-col class="h3">
|
||||
{{ $d(new Date(item.date), 'month') }} {{ $d(new Date(item.date), 'year') }}
|
||||
{{ $d(new Date(item.contributionDate), 'month') }}
|
||||
{{ $d(new Date(item.contributionDate), 'year') }}
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row>
|
||||
|
||||
@ -1,6 +1,17 @@
|
||||
<template>
|
||||
<div class="open-creations-table">
|
||||
<b-table-lite :items="items" :fields="fields" caption-top striped hover stacked="md">
|
||||
<b-table-lite
|
||||
:items="items"
|
||||
:fields="fields"
|
||||
caption-top
|
||||
striped
|
||||
hover
|
||||
stacked="md"
|
||||
:tbody-tr-class="rowClass"
|
||||
>
|
||||
<template #cell(state)="row">
|
||||
<b-icon :icon="getStatusIcon(row.item.state)"></b-icon>
|
||||
</template>
|
||||
<template #cell(bookmark)="row">
|
||||
<b-button
|
||||
variant="danger"
|
||||
@ -37,6 +48,16 @@
|
||||
</b-button>
|
||||
</div>
|
||||
</template>
|
||||
<template #cell(reActive)>
|
||||
<b-button variant="warning" size="md" class="mr-2">
|
||||
<b-icon icon="arrow-up" variant="light"></b-icon>
|
||||
</b-button>
|
||||
</template>
|
||||
<template #cell(chatCreation)="row">
|
||||
<b-button v-if="row.item.messagesCount > 0" @click="rowToggleDetails(row, 0)">
|
||||
<b-icon icon="chat-dots"></b-icon>
|
||||
</b-button>
|
||||
</template>
|
||||
<template #cell(deny)="row">
|
||||
<div v-if="$store.state.moderator.id !== row.item.userId">
|
||||
<b-button
|
||||
@ -100,6 +121,14 @@ import RowDetails from '../RowDetails.vue'
|
||||
import EditCreationFormular from '../EditCreationFormular.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 {
|
||||
name: 'OpenCreationsTable',
|
||||
mixins: [toggleRowDetails],
|
||||
@ -129,6 +158,14 @@ export default {
|
||||
}
|
||||
},
|
||||
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) {
|
||||
const row = data.row
|
||||
this.$emit('update-contributions', data)
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
34
admin/src/graphql/listAllContributions.js
Normal file
34
admin/src/graphql/listAllContributions.js
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"all_emails": "Alle Nutzer",
|
||||
"back": "zurück",
|
||||
"chat": "Chat",
|
||||
"contributionLink": {
|
||||
"amount": "Betrag",
|
||||
"changeSaved": "Änderungen gespeichert",
|
||||
@ -29,10 +30,18 @@
|
||||
"validFrom": "Startdatum",
|
||||
"validTo": "Enddatum"
|
||||
},
|
||||
"contributions": {
|
||||
"all": "Alle",
|
||||
"confirms": "Bestätigt",
|
||||
"deleted": "Gelöscht",
|
||||
"denied": "Abgelehnt",
|
||||
"open": "Offen"
|
||||
},
|
||||
"created": "Geschöpft",
|
||||
"createdAt": "Angelegt",
|
||||
"creation": "Schöpfung",
|
||||
"creationList": "Schöpfungsliste",
|
||||
"creation_form": {
|
||||
"creation_failed": "Ausstehende Schöpfung für {email} konnte nicht erzeugt werden.",
|
||||
"creation_for": "Aktives Grundeinkommen für",
|
||||
"enter_text": "Text eintragen",
|
||||
"form": "Schöpfungsformular",
|
||||
@ -49,7 +58,6 @@
|
||||
"update_creation": "Schöpfung aktualisieren"
|
||||
},
|
||||
"creation_for_month": "Schöpfung für Monat",
|
||||
"date": "Datum",
|
||||
"delete": "Löschen",
|
||||
"deleted": "gelöscht",
|
||||
"deleted_user": "Alle gelöschten Nutzer",
|
||||
@ -87,23 +95,19 @@
|
||||
"lastname": "Nachname",
|
||||
"math": {
|
||||
"equals": "=",
|
||||
"exclaim": "!",
|
||||
"pipe": "|",
|
||||
"plus": "+"
|
||||
},
|
||||
"message": {
|
||||
"request": "Die Anfrage wurde gesendet."
|
||||
},
|
||||
"mod": "Mod",
|
||||
"moderator": "Moderator",
|
||||
"multiple_creation_text": "Bitte wähle ein oder mehrere Mitglieder aus für die du Schöpfen möchtest.",
|
||||
"name": "Name",
|
||||
"navbar": {
|
||||
"automaticContributions": "Automatische Beiträge",
|
||||
"logout": "Abmelden",
|
||||
"multi_creation": "Mehrfachschöpfung",
|
||||
"my-account": "Mein Konto",
|
||||
"open_creation": "Offene Schöpfungen",
|
||||
"overview": "Übersicht",
|
||||
"statistic": "Statistik",
|
||||
"user_search": "Nutzersuche"
|
||||
},
|
||||
@ -132,9 +136,7 @@
|
||||
}
|
||||
},
|
||||
"redeemed": "eingelöst",
|
||||
"remove": "Entfernen",
|
||||
"removeNotSelf": "Als Admin/Moderator kannst du dich nicht selber löschen.",
|
||||
"remove_all": "alle Nutzer entfernen",
|
||||
"save": "Speichern",
|
||||
"statistic": {
|
||||
"activeUsers": "Aktive Mitglieder",
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"all_emails": "All users",
|
||||
"back": "back",
|
||||
"chat": "Chat",
|
||||
"contributionLink": {
|
||||
"amount": "Amount",
|
||||
"changeSaved": "Changes saved",
|
||||
@ -29,10 +30,18 @@
|
||||
"validFrom": "Start-date",
|
||||
"validTo": "End-Date"
|
||||
},
|
||||
"contributions": {
|
||||
"all": "All",
|
||||
"confirms": "Confirmed",
|
||||
"deleted": "Deleted",
|
||||
"denied": "Denied",
|
||||
"open": "Open"
|
||||
},
|
||||
"created": "Confirmed",
|
||||
"createdAt": "Created",
|
||||
"creation": "Creation",
|
||||
"creationList": "Creation list",
|
||||
"creation_form": {
|
||||
"creation_failed": "Could not create pending creation for {email}",
|
||||
"creation_for": "Active Basic Income for",
|
||||
"enter_text": "Enter text",
|
||||
"form": "Creation form",
|
||||
@ -49,7 +58,6 @@
|
||||
"update_creation": "Creation update"
|
||||
},
|
||||
"creation_for_month": "Creation for month",
|
||||
"date": "Date",
|
||||
"delete": "Delete",
|
||||
"deleted": "deleted",
|
||||
"deleted_user": "All deleted user",
|
||||
@ -87,23 +95,19 @@
|
||||
"lastname": "Lastname",
|
||||
"math": {
|
||||
"equals": "=",
|
||||
"exclaim": "!",
|
||||
"pipe": "|",
|
||||
"plus": "+"
|
||||
},
|
||||
"message": {
|
||||
"request": "Request has been sent."
|
||||
},
|
||||
"mod": "Mod",
|
||||
"moderator": "Moderator",
|
||||
"multiple_creation_text": "Please select one or more members for which you would like to perform creations.",
|
||||
"name": "Name",
|
||||
"navbar": {
|
||||
"automaticContributions": "Automatic Contributions",
|
||||
"logout": "Logout",
|
||||
"multi_creation": "Multiple creation",
|
||||
"my-account": "My Account",
|
||||
"open_creation": "Open creations",
|
||||
"overview": "Overview",
|
||||
"statistic": "Statistic",
|
||||
"user_search": "User search"
|
||||
},
|
||||
@ -132,9 +136,7 @@
|
||||
}
|
||||
},
|
||||
"redeemed": "redeemed",
|
||||
"remove": "Remove",
|
||||
"removeNotSelf": "As an admin/moderator, you cannot delete yourself.",
|
||||
"remove_all": "Remove all users",
|
||||
"save": "Speichern",
|
||||
"statistic": {
|
||||
"activeUsers": "Active members",
|
||||
|
||||
18
admin/src/locales/index.test.js
Normal file
18
admin/src/locales/index.test.js
Normal 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,
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -1,10 +1,11 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import ContributionLinks from './ContributionLinks.vue'
|
||||
import { listContributionLinks } from '@/graphql/listContributionLinks.js'
|
||||
import { toastErrorSpy } from '../../test/testSetup'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
const apolloQueryMock = jest.fn().mockResolvedValueOnce({
|
||||
const apolloQueryMock = jest.fn().mockResolvedValue({
|
||||
data: {
|
||||
listContributionLinks: {
|
||||
links: [
|
||||
@ -47,12 +48,31 @@ describe('ContributionLinks', () => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('calls listContributionLinks', () => {
|
||||
expect(apolloQueryMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
query: listContributionLinks,
|
||||
}),
|
||||
)
|
||||
describe('apollo returns', () => {
|
||||
it('calls listContributionLinks', () => {
|
||||
expect(apolloQueryMock).toBeCalledWith(
|
||||
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',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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>
|
||||
@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils'
|
||||
import CreationConfirm from './CreationConfirm.vue'
|
||||
import { adminDeleteContribution } from '../graphql/adminDeleteContribution'
|
||||
import { denyContribution } from '../graphql/denyContribution'
|
||||
import { listUnconfirmedContributions } from '../graphql/listUnconfirmedContributions'
|
||||
import { listAllContributions } from '../graphql/listAllContributions'
|
||||
import { confirmContribution } from '../graphql/confirmContribution'
|
||||
import { toastErrorSpy, toastSuccessSpy } from '../../test/testSetup'
|
||||
import VueApollo from 'vue-apollo'
|
||||
@ -38,50 +38,68 @@ const mocks = {
|
||||
|
||||
const defaultData = () => {
|
||||
return {
|
||||
listUnconfirmedContributions: [
|
||||
{
|
||||
id: 1,
|
||||
firstName: 'Bibi',
|
||||
lastName: 'Bloxberg',
|
||||
userId: 99,
|
||||
email: 'bibi@bloxberg.de',
|
||||
amount: 500,
|
||||
memo: 'Danke für alles',
|
||||
date: new Date(),
|
||||
moderator: 1,
|
||||
state: 'PENDING',
|
||||
creation: [500, 500, 500],
|
||||
messageCount: 0,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
firstName: 'Räuber',
|
||||
lastName: 'Hotzenplotz',
|
||||
userId: 100,
|
||||
email: 'raeuber@hotzenplotz.de',
|
||||
amount: 1000000,
|
||||
memo: 'Gut Ergattert',
|
||||
date: new Date(),
|
||||
moderator: 1,
|
||||
state: 'PENDING',
|
||||
creation: [500, 500, 500],
|
||||
messageCount: 0,
|
||||
},
|
||||
],
|
||||
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('CreationConfirm', () => {
|
||||
let wrapper
|
||||
|
||||
const listUnconfirmedContributionsMock = jest.fn()
|
||||
const adminDeleteContributionMock = jest.fn()
|
||||
const adminDenyContributionMock = jest.fn()
|
||||
const confirmContributionMock = jest.fn()
|
||||
|
||||
mockClient.setRequestHandler(
|
||||
listUnconfirmedContributions,
|
||||
listUnconfirmedContributionsMock
|
||||
listAllContributions,
|
||||
jest
|
||||
.fn()
|
||||
.mockRejectedValueOnce({ message: 'Ouch!' })
|
||||
.mockResolvedValue({ data: defaultData() }),
|
||||
)
|
||||
@ -117,6 +135,10 @@ describe('CreationConfirm', () => {
|
||||
it('toast an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Ouch!')
|
||||
})
|
||||
|
||||
it('has statusFilter ["IN_PROGRESS", "PENDING"]', () => {
|
||||
expect(wrapper.vm.statusFilter).toEqual(['IN_PROGRESS', 'PENDING'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('server response is succes', () => {
|
||||
@ -125,17 +147,7 @@ describe('CreationConfirm', () => {
|
||||
})
|
||||
|
||||
it('has two pending creations', () => {
|
||||
expect(wrapper.vm.pendingCreations).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
describe('store', () => {
|
||||
it('commits resetOpenCreations to store', () => {
|
||||
expect(storeCommitMock).toBeCalledWith('resetOpenCreations')
|
||||
})
|
||||
|
||||
it('commits setOpenCreations to store', () => {
|
||||
expect(storeCommitMock).toBeCalledWith('setOpenCreations', 2)
|
||||
expect(wrapper.find('tbody').findAll('tr')).toHaveLength(2)
|
||||
})
|
||||
})
|
||||
|
||||
@ -259,7 +271,7 @@ describe('CreationConfirm', () => {
|
||||
|
||||
describe('deny creation', () => {
|
||||
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', () => {
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,6 +1,50 @@
|
||||
<!-- eslint-disable @intlify/vue-i18n/no-dynamic-keys -->
|
||||
<template>
|
||||
<div class="creation-confirm">
|
||||
<div>
|
||||
<b-tabs v-model="tabIndex" content-class="mt-3" fill>
|
||||
<b-tab active :title-link-attributes="{ 'data-test': 'open' }">
|
||||
<template #title>
|
||||
{{ $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">
|
||||
<overlay :item="item" @overlay-cancel="overlay = false">
|
||||
<template #title>
|
||||
@ -24,24 +68,24 @@
|
||||
</template>
|
||||
</overlay>
|
||||
</div>
|
||||
<open-creations-table
|
||||
class="mt-4"
|
||||
:items="pendingCreations"
|
||||
:fields="fields"
|
||||
@show-overlay="showOverlay"
|
||||
@update-state="updateState"
|
||||
@update-contributions="$apollo.queries.PendingContributions.refetch()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Overlay from '../components/Overlay.vue'
|
||||
import OpenCreationsTable from '../components/Tables/OpenCreationsTable.vue'
|
||||
import { listUnconfirmedContributions } from '../graphql/listUnconfirmedContributions'
|
||||
import { listAllContributions } from '../graphql/listAllContributions'
|
||||
import { adminDeleteContribution } from '../graphql/adminDeleteContribution'
|
||||
import { confirmContribution } from '../graphql/confirmContribution'
|
||||
import { denyContribution } from '../graphql/denyContribution'
|
||||
|
||||
const FILTER_TAB_MAP = [
|
||||
['IN_PROGRESS', 'PENDING'],
|
||||
['CONFIRMED'],
|
||||
['DENIED'],
|
||||
['DELETED'],
|
||||
['IN_PROGRESS', 'PENDING', 'CONFIRMED', 'DENIED', 'DELETED'],
|
||||
]
|
||||
|
||||
export default {
|
||||
name: 'CreationConfirm',
|
||||
components: {
|
||||
@ -50,10 +94,14 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
pendingCreations: [],
|
||||
tabIndex: 0,
|
||||
items: [],
|
||||
overlay: false,
|
||||
item: {},
|
||||
variant: 'confirm',
|
||||
rows: 0,
|
||||
currentPage: 1,
|
||||
pageSize: 25,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@ -112,7 +160,7 @@ export default {
|
||||
})
|
||||
},
|
||||
updatePendingCreations(id) {
|
||||
this.pendingCreations = this.pendingCreations.filter((obj) => obj.id !== id)
|
||||
this.items = this.items.filter((obj) => obj.id !== id)
|
||||
this.$store.commit('openCreationsMinus', 1)
|
||||
},
|
||||
showOverlay(item, variant) {
|
||||
@ -120,38 +168,155 @@ export default {
|
||||
this.item = item
|
||||
this.variant = variant
|
||||
},
|
||||
updateState(id) {
|
||||
this.pendingCreations.find((obj) => obj.id === id).messagesCount++
|
||||
this.pendingCreations.find((obj) => obj.id === id).state = 'IN_PROGRESS'
|
||||
updateStatus(id) {
|
||||
this.items.find((obj) => obj.id === id).messagesCount++
|
||||
this.items.find((obj) => obj.id === id).state = 'IN_PROGRESS'
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
statusFilter() {
|
||||
this.$apollo.queries.ListAllContributions.refetch()
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
fields() {
|
||||
return [
|
||||
{ key: 'bookmark', label: this.$t('delete') },
|
||||
{ key: 'email', label: this.$t('e_mail') },
|
||||
{ key: 'firstName', label: this.$t('firstname') },
|
||||
{ key: 'lastName', label: this.$t('lastname') },
|
||||
{
|
||||
key: 'amount',
|
||||
label: this.$t('creation'),
|
||||
formatter: (value) => {
|
||||
return value + ' GDD'
|
||||
[
|
||||
{ key: 'bookmark', label: this.$t('delete') },
|
||||
{ key: 'deny', label: this.$t('deny') },
|
||||
{ key: 'email', label: this.$t('e_mail') },
|
||||
{ key: 'firstName', label: this.$t('firstname') },
|
||||
{ key: 'lastName', label: this.$t('lastname') },
|
||||
{
|
||||
key: 'amount',
|
||||
label: this.$t('creation'),
|
||||
formatter: (value) => {
|
||||
return value + ' GDD'
|
||||
},
|
||||
},
|
||||
},
|
||||
{ key: 'memo', label: this.$t('text'), class: 'text-break' },
|
||||
{
|
||||
key: 'date',
|
||||
label: this.$t('date'),
|
||||
formatter: (value) => {
|
||||
return this.$d(new Date(value), 'short')
|
||||
{ key: 'memo', label: this.$t('text'), class: 'text-break' },
|
||||
{
|
||||
key: 'contributionDate',
|
||||
label: this.$t('created'),
|
||||
formatter: (value) => {
|
||||
return this.$d(new Date(value), 'short')
|
||||
},
|
||||
},
|
||||
},
|
||||
{ key: 'moderator', label: this.$t('moderator') },
|
||||
{ key: 'editCreation', label: this.$t('edit') },
|
||||
{ key: 'deny', label: this.$t('deny') },
|
||||
{ key: 'confirm', label: this.$t('save') },
|
||||
]
|
||||
{ key: 'moderator', label: this.$t('moderator') },
|
||||
{ key: 'editCreation', label: this.$t('edit') },
|
||||
{ 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() {
|
||||
return `overlay.${this.variant}.title`
|
||||
@ -182,18 +347,21 @@ export default {
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
PendingContributions: {
|
||||
ListAllContributions: {
|
||||
query() {
|
||||
return listUnconfirmedContributions
|
||||
return listAllContributions
|
||||
},
|
||||
variables() {
|
||||
// may be at some point we need a pagination here
|
||||
return {}
|
||||
return {
|
||||
currentPage: this.currentPage,
|
||||
pageSize: this.pageSize,
|
||||
statusFilter: this.statusFilter,
|
||||
}
|
||||
},
|
||||
update({ listUnconfirmedContributions }) {
|
||||
this.$store.commit('resetOpenCreations')
|
||||
this.pendingCreations = listUnconfirmedContributions
|
||||
this.$store.commit('setOpenCreations', listUnconfirmedContributions.length)
|
||||
update({ listAllContributions }) {
|
||||
this.rows = listAllContributions.contributionCount
|
||||
this.items = listAllContributions.contributionList
|
||||
},
|
||||
error({ message }) {
|
||||
this.toastError(message)
|
||||
|
||||
@ -1,41 +1,18 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
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 apolloQueryMock = jest
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
data: {
|
||||
listUnconfirmedContributions: [
|
||||
{
|
||||
pending: true,
|
||||
},
|
||||
{
|
||||
pending: true,
|
||||
},
|
||||
{
|
||||
pending: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
.mockResolvedValue({
|
||||
data: {
|
||||
listUnconfirmedContributions: [
|
||||
{
|
||||
pending: true,
|
||||
},
|
||||
{
|
||||
pending: true,
|
||||
},
|
||||
{
|
||||
pending: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
localVue.use(VueApollo)
|
||||
|
||||
const storeCommitMock = jest.fn()
|
||||
|
||||
@ -43,44 +20,114 @@ const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$n: jest.fn((n) => n),
|
||||
$d: jest.fn((d) => d),
|
||||
$apollo: {
|
||||
query: apolloQueryMock,
|
||||
},
|
||||
$store: {
|
||||
commit: storeCommitMock,
|
||||
state: {
|
||||
openCreations: 2,
|
||||
openCreations: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const defaultData = () => {
|
||||
return {
|
||||
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', () => {
|
||||
let wrapper
|
||||
const listAllContributionsMock = jest.fn()
|
||||
|
||||
mockClient.setRequestHandler(
|
||||
listAllContributions,
|
||||
listAllContributionsMock
|
||||
.mockRejectedValueOnce({ message: 'Ouch!' })
|
||||
.mockResolvedValue({ data: defaultData() }),
|
||||
)
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(Overview, { localVue, mocks })
|
||||
return mount(Overview, { localVue, mocks, apolloProvider })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('calls listUnconfirmedContributions', () => {
|
||||
expect(apolloQueryMock).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
query: listUnconfirmedContributions,
|
||||
}),
|
||||
)
|
||||
describe('server response for get pending creations is error', () => {
|
||||
it('toast an error message', () => {
|
||||
expect(toastErrorSpy).toBeCalledWith('Ouch!')
|
||||
})
|
||||
})
|
||||
|
||||
it('calls the listAllContributions query', () => {
|
||||
expect(listAllContributionsMock).toBeCalledWith({
|
||||
currentPage: 1,
|
||||
order: 'DESC',
|
||||
pageSize: 25,
|
||||
statusFilter: ['IN_PROGRESS', 'PENDING'],
|
||||
})
|
||||
})
|
||||
|
||||
it('commits three pending creations to store', () => {
|
||||
expect(storeCommitMock).toBeCalledWith('setOpenCreations', 3)
|
||||
expect(storeCommitMock).toBeCalledWith('setOpenCreations', 2)
|
||||
})
|
||||
|
||||
describe('with open creations', () => {
|
||||
it('renders a link to confirm creations', () => {
|
||||
expect(wrapper.find('a[href="creation-confirm"]').text()).toContain('2')
|
||||
beforeEach(() => {
|
||||
mocks.$store.state.openCreations = 2
|
||||
})
|
||||
it('renders a link to confirm 2 creations', () => {
|
||||
expect(wrapper.find('[data-test="open-creation"]').text()).toContain('2')
|
||||
expect(wrapper.find('a[href="creation-confirm"]').exists()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
@ -91,7 +138,7 @@ describe('Overview', () => {
|
||||
})
|
||||
|
||||
it('renders a link to confirm creations', () => {
|
||||
expect(wrapper.find('a[href="creation-confirm"]').text()).toContain('0')
|
||||
expect(wrapper.find('[data-test="open-creation"]').text()).toContain('0')
|
||||
expect(wrapper.find('a[href="creation-confirm"]').exists()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
@ -24,31 +24,40 @@
|
||||
>
|
||||
<b-card-text>
|
||||
<b-link to="creation-confirm">
|
||||
<h1>{{ $store.state.openCreations }}</h1>
|
||||
<h1 data-test="open-creation">{{ $store.state.openCreations }}</h1>
|
||||
</b-link>
|
||||
</b-card-text>
|
||||
</b-card>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { listUnconfirmedContributions } from '@/graphql/listUnconfirmedContributions.js'
|
||||
import { listAllContributions } from '../graphql/listAllContributions'
|
||||
|
||||
export default {
|
||||
name: 'overview',
|
||||
methods: {
|
||||
getPendingCreations() {
|
||||
this.$apollo
|
||||
.query({
|
||||
query: listUnconfirmedContributions,
|
||||
fetchPolicy: 'network-only',
|
||||
})
|
||||
.then((result) => {
|
||||
this.$store.commit('setOpenCreations', result.data.listUnconfirmedContributions.length)
|
||||
})
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
statusFilter: ['IN_PROGRESS', 'PENDING'],
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.getPendingCreations()
|
||||
apollo: {
|
||||
AllContributions: {
|
||||
query() {
|
||||
return 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>
|
||||
|
||||
@ -45,7 +45,7 @@ describe('router', () => {
|
||||
|
||||
describe('routes', () => {
|
||||
it('has nine routes defined', () => {
|
||||
expect(routes).toHaveLength(9)
|
||||
expect(routes).toHaveLength(8)
|
||||
})
|
||||
|
||||
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', () => {
|
||||
it('loads the "CreationConfirm" component', async () => {
|
||||
const component = await routes.find((r) => r.path === '/creation-confirm').component()
|
||||
|
||||
@ -19,10 +19,6 @@ const routes = [
|
||||
path: '/user',
|
||||
component: () => import('@/pages/UserSearch.vue'),
|
||||
},
|
||||
{
|
||||
path: '/creation',
|
||||
component: () => import('@/pages/Creation.vue'),
|
||||
},
|
||||
{
|
||||
path: '/creation-confirm',
|
||||
component: () => import('@/pages/CreationConfirm.vue'),
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
CONFIG_VERSION=v14.2022-12-22
|
||||
|
||||
# Server
|
||||
PORT=4000
|
||||
JWT_SECRET=secret123
|
||||
@ -55,16 +53,9 @@ EMAIL_CODE_REQUEST_TIME=10
|
||||
# Webhook
|
||||
WEBHOOK_ELOPAGE_SECRET=secret
|
||||
|
||||
# EventProtocol
|
||||
EVENT_PROTOCOL_DISABLED=false
|
||||
|
||||
# SET LOG LEVEL AS NEEDED IN YOUR .ENV
|
||||
# POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal
|
||||
# LOG_LEVEL=info
|
||||
|
||||
# Federation
|
||||
# if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen
|
||||
# on an hash created from this topic
|
||||
# FEDERATION_DHT_TOPIC=GRADIDO_HUB
|
||||
# FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f
|
||||
# FEDERATION_COMMUNITY_URL=http://localhost:4000/api
|
||||
FEDERATION_VALIDATE_COMMUNITY_TIMER=60000
|
||||
@ -54,10 +54,5 @@ EMAIL_CODE_REQUEST_TIME=$EMAIL_CODE_REQUEST_TIME
|
||||
# Webhook
|
||||
WEBHOOK_ELOPAGE_SECRET=$WEBHOOK_ELOPAGE_SECRET
|
||||
|
||||
# EventProtocol
|
||||
EVENT_PROTOCOL_DISABLED=$EVENT_PROTOCOL_DISABLED
|
||||
|
||||
# Federation
|
||||
FEDERATION_DHT_TOPIC=$FEDERATION_DHT_TOPIC
|
||||
FEDERATION_DHT_SEED=$FEDERATION_DHT_SEED
|
||||
FEDERATION_COMMUNITY_URL=$FEDERATION_COMMUNITY_URL
|
||||
FEDERATION_VALIDATE_COMMUNITY_TIMER=$FEDERATION_VALIDATE_COMMUNITY_TIMER
|
||||
|
||||
9
backend/.env.test_e2e
Normal file
9
backend/.env.test_e2e
Normal 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
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido-backend",
|
||||
"version": "1.17.1",
|
||||
"version": "1.18.2",
|
||||
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
|
||||
"main": "src/index.ts",
|
||||
"repository": "https://github.com/gradido/gradido/backend",
|
||||
@ -15,10 +15,10 @@
|
||||
"lint": "eslint --max-warnings=0 --ext .js,.ts .",
|
||||
"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",
|
||||
"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": {
|
||||
"@hyperswarm/dht": "^6.2.0",
|
||||
"apollo-server-express": "^2.25.2",
|
||||
"await-semaphore": "^0.1.3",
|
||||
"axios": "^0.21.1",
|
||||
@ -30,6 +30,7 @@
|
||||
"email-templates": "^10.0.1",
|
||||
"express": "^4.17.1",
|
||||
"graphql": "^15.5.1",
|
||||
"graphql-request": "5.0.0",
|
||||
"i18n": "^0.15.1",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
@ -72,5 +73,10 @@
|
||||
"ts-node": "^10.0.0",
|
||||
"tsconfig-paths": "^3.14.0",
|
||||
"typescript": "^4.3.4"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"ignore": [
|
||||
"**/*.test.ts"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
25
backend/scripts/sort.sh
Executable file
25
backend/scripts/sort.sh
Executable 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
|
||||
13
backend/scripts/sort_filter.jq
Normal file
13
backend/scripts/sort_filter.jq
Normal 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)
|
||||
@ -17,7 +17,7 @@ const constants = {
|
||||
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
|
||||
CONFIG_VERSION: {
|
||||
DEFAULT: 'DEFAULT',
|
||||
EXPECTED: 'v14.2022-12-22',
|
||||
EXPECTED: 'v15.2023-02-07',
|
||||
CURRENT: '',
|
||||
},
|
||||
}
|
||||
@ -99,11 +99,6 @@ const webhook = {
|
||||
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
|
||||
process.env.APP_SECRET = server.JWT_SECRET
|
||||
|
||||
@ -120,14 +115,8 @@ if (
|
||||
}
|
||||
|
||||
const federation = {
|
||||
FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || null,
|
||||
FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null,
|
||||
FEDERATION_COMMUNITY_URL:
|
||||
process.env.FEDERATION_COMMUNITY_URL === undefined
|
||||
? null
|
||||
: process.env.FEDERATION_COMMUNITY_URL.endsWith('/')
|
||||
? process.env.FEDERATION_COMMUNITY_URL
|
||||
: process.env.FEDERATION_COMMUNITY_URL + '/',
|
||||
FEDERATION_VALIDATE_COMMUNITY_TIMER:
|
||||
Number(process.env.FEDERATION_VALIDATE_COMMUNITY_TIMER) || 60000,
|
||||
}
|
||||
|
||||
const CONFIG = {
|
||||
@ -139,7 +128,6 @@ const CONFIG = {
|
||||
...email,
|
||||
...loginServer,
|
||||
...webhook,
|
||||
...eventProtocol,
|
||||
...federation,
|
||||
}
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
sendAccountMultiRegistrationEmail,
|
||||
sendContributionConfirmedEmail,
|
||||
sendContributionDeniedEmail,
|
||||
sendContributionDeletedEmail,
|
||||
sendResetPasswordEmail,
|
||||
sendTransactionLinkRedeemedEmail,
|
||||
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', () => {
|
||||
beforeAll(async () => {
|
||||
result = await sendResetPasswordEmail({
|
||||
|
||||
@ -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: {
|
||||
firstName: string
|
||||
lastName: string
|
||||
|
||||
16
backend/src/emails/templates/contributionDeleted/html.pug
Normal file
16
backend/src/emails/templates/contributionDeleted/html.pug
Normal 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
|
||||
@ -0,0 +1 @@
|
||||
= t('emails.contributionDeleted.subject')
|
||||
@ -1,517 +1,212 @@
|
||||
import { EventProtocol } from '@entity/EventProtocol'
|
||||
import decimal from 'decimal.js-light'
|
||||
import { EventProtocol as DbEvent } from '@entity/EventProtocol'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { EventProtocolType } from './EventProtocolType'
|
||||
|
||||
export class EventBasic {
|
||||
type: string
|
||||
createdAt: Date
|
||||
}
|
||||
export class EventBasicUserId extends EventBasic {
|
||||
userId: number
|
||||
export const Event = (
|
||||
type: EventProtocolType,
|
||||
userId: number,
|
||||
xUserId: number | null = null,
|
||||
xCommunityId: number | null = null,
|
||||
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 {
|
||||
transactionId: number
|
||||
amount: decimal
|
||||
}
|
||||
|
||||
export class EventBasicTxX extends EventBasicTx {
|
||||
xUserId: number
|
||||
xCommunityId: number
|
||||
}
|
||||
|
||||
export class EventBasicCt extends EventBasicUserId {
|
||||
contributionId: number
|
||||
amount: decimal
|
||||
}
|
||||
|
||||
export class EventBasicCtX extends EventBasicCt {
|
||||
xUserId: number
|
||||
xCommunityId: number
|
||||
}
|
||||
|
||||
export class EventBasicRedeem extends EventBasicUserId {
|
||||
transactionId?: number
|
||||
contributionId?: number
|
||||
}
|
||||
|
||||
export class EventBasicCtMsg extends EventBasicCt {
|
||||
messageId: number
|
||||
}
|
||||
|
||||
export class EventVisitGradido extends EventBasic {}
|
||||
export class EventRegister extends EventBasicUserId {}
|
||||
export class EventRedeemRegister extends EventBasicRedeem {}
|
||||
export class EventVerifyRedeem extends EventBasicRedeem {}
|
||||
export class EventInactiveAccount extends EventBasicUserId {}
|
||||
export class EventSendConfirmationEmail extends EventBasicUserId {}
|
||||
export class EventSendAccountMultiRegistrationEmail extends EventBasicUserId {}
|
||||
export class EventSendForgotPasswordEmail extends EventBasicUserId {}
|
||||
export class EventSendTransactionSendEmail extends EventBasicTxX {}
|
||||
export class EventSendTransactionReceiveEmail extends EventBasicTxX {}
|
||||
export class EventSendTransactionLinkRedeemEmail extends EventBasicTxX {}
|
||||
export class EventSendAddedContributionEmail extends EventBasicCt {}
|
||||
export class EventSendContributionConfirmEmail extends EventBasicCt {}
|
||||
export class EventConfirmationEmail extends EventBasicUserId {}
|
||||
export class EventRegisterEmailKlicktipp extends EventBasicUserId {}
|
||||
export class EventLogin extends EventBasicUserId {}
|
||||
export class EventLogout extends EventBasicUserId {}
|
||||
export class EventRedeemLogin extends EventBasicRedeem {}
|
||||
export class EventActivateAccount extends EventBasicUserId {}
|
||||
export class EventPasswordChange extends EventBasicUserId {}
|
||||
export class EventTransactionSend extends EventBasicTxX {}
|
||||
export class EventTransactionSendRedeem extends EventBasicTxX {}
|
||||
export class EventTransactionRepeateRedeem extends EventBasicTxX {}
|
||||
export class EventTransactionCreation extends EventBasicTx {}
|
||||
export class EventTransactionReceive extends EventBasicTxX {}
|
||||
export class EventTransactionReceiveRedeem extends EventBasicTxX {}
|
||||
export class EventContributionCreate extends EventBasicCt {}
|
||||
export class EventAdminContributionCreate extends EventBasicCt {}
|
||||
export class EventAdminContributionDelete extends EventBasicCt {}
|
||||
export class EventAdminContributionUpdate extends EventBasicCt {}
|
||||
export class EventUserCreateContributionMessage extends EventBasicCtMsg {}
|
||||
export class EventAdminCreateContributionMessage extends EventBasicCtMsg {}
|
||||
export class EventContributionDelete extends EventBasicCt {}
|
||||
export class EventContributionUpdate extends EventBasicCt {}
|
||||
export class EventContributionConfirm extends EventBasicCtX {}
|
||||
export class EventContributionDeny extends EventBasicCtX {}
|
||||
export class EventContributionLinkDefine extends EventBasicCt {}
|
||||
export class EventContributionLinkActivateRedeem extends EventBasicCt {}
|
||||
export class EventDeleteUser extends EventBasicUserId {}
|
||||
export class EventUndeleteUser extends EventBasicUserId {}
|
||||
export class EventChangeUserRole extends EventBasicUserId {}
|
||||
export class EventAdminUpdateContribution extends EventBasicCt {}
|
||||
export class EventAdminDeleteContribution extends EventBasicCt {}
|
||||
export class EventCreateContributionLink extends EventBasicCt {}
|
||||
export class EventDeleteContributionLink extends EventBasicCt {}
|
||||
export class EventUpdateContributionLink extends EventBasicCt {}
|
||||
|
||||
export class Event {
|
||||
constructor()
|
||||
constructor(event?: EventProtocol) {
|
||||
if (event) {
|
||||
this.id = event.id
|
||||
this.type = event.type
|
||||
this.createdAt = event.createdAt
|
||||
this.userId = event.userId
|
||||
this.xUserId = event.xUserId
|
||||
this.xCommunityId = event.xCommunityId
|
||||
this.transactionId = event.transactionId
|
||||
this.contributionId = event.contributionId
|
||||
this.amount = event.amount
|
||||
}
|
||||
}
|
||||
|
||||
public setEventBasic(): Event {
|
||||
this.type = EventProtocolType.BASIC
|
||||
this.createdAt = new Date()
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventVisitGradido(): Event {
|
||||
this.setEventBasic()
|
||||
this.type = EventProtocolType.VISIT_GRADIDO
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventRegister(ev: EventRegister): Event {
|
||||
this.setByBasicUser(ev.userId)
|
||||
this.type = EventProtocolType.REGISTER
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventRedeemRegister(ev: EventRedeemRegister): Event {
|
||||
this.setByBasicRedeem(ev.userId, ev.transactionId, ev.contributionId)
|
||||
this.type = EventProtocolType.REDEEM_REGISTER
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventVerifyRedeem(ev: EventVerifyRedeem): Event {
|
||||
this.setByBasicRedeem(ev.userId, ev.transactionId, ev.contributionId)
|
||||
this.type = EventProtocolType.VERIFY_REDEEM
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventInactiveAccount(ev: EventInactiveAccount): Event {
|
||||
this.setByBasicUser(ev.userId)
|
||||
this.type = EventProtocolType.INACTIVE_ACCOUNT
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventSendConfirmationEmail(ev: EventSendConfirmationEmail): Event {
|
||||
this.setByBasicUser(ev.userId)
|
||||
this.type = EventProtocolType.SEND_CONFIRMATION_EMAIL
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventSendAccountMultiRegistrationEmail(
|
||||
ev: EventSendAccountMultiRegistrationEmail,
|
||||
): Event {
|
||||
this.setByBasicUser(ev.userId)
|
||||
this.type = EventProtocolType.SEND_ACCOUNT_MULTIREGISTRATION_EMAIL
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventSendForgotPasswordEmail(ev: EventSendForgotPasswordEmail): Event {
|
||||
this.setByBasicUser(ev.userId)
|
||||
this.type = EventProtocolType.SEND_FORGOT_PASSWORD_EMAIL
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventSendTransactionSendEmail(ev: EventSendTransactionSendEmail): Event {
|
||||
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId)
|
||||
this.type = EventProtocolType.SEND_TRANSACTION_SEND_EMAIL
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventSendTransactionReceiveEmail(ev: EventSendTransactionReceiveEmail): Event {
|
||||
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId)
|
||||
this.type = EventProtocolType.SEND_TRANSACTION_RECEIVE_EMAIL
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventSendTransactionLinkRedeemEmail(ev: EventSendTransactionLinkRedeemEmail): Event {
|
||||
this.setByBasicTxX(ev.userId, ev.transactionId, ev.amount, ev.xUserId, ev.xCommunityId)
|
||||
this.type = EventProtocolType.SEND_TRANSACTION_LINK_REDEEM_EMAIL
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventSendAddedContributionEmail(ev: EventSendAddedContributionEmail): Event {
|
||||
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
|
||||
this.type = EventProtocolType.SEND_ADDED_CONTRIBUTION_EMAIL
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
public setEventSendContributionConfirmEmail(ev: EventSendContributionConfirmEmail): Event {
|
||||
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
|
||||
}
|
||||
export const EVENT_CONTRIBUTION_CREATE = async (
|
||||
userId: number,
|
||||
contributionId: number,
|
||||
amount: Decimal,
|
||||
): Promise<DbEvent> =>
|
||||
Event(
|
||||
EventProtocolType.CONTRIBUTION_CREATE,
|
||||
userId,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
contributionId,
|
||||
amount,
|
||||
).save()
|
||||
|
||||
export const EVENT_CONTRIBUTION_DELETE = async (
|
||||
userId: number,
|
||||
contributionId: number,
|
||||
amount: Decimal,
|
||||
): Promise<DbEvent> =>
|
||||
Event(
|
||||
EventProtocolType.CONTRIBUTION_DELETE,
|
||||
userId,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
contributionId,
|
||||
amount,
|
||||
).save()
|
||||
|
||||
export const EVENT_CONTRIBUTION_UPDATE = async (
|
||||
userId: number,
|
||||
contributionId: number,
|
||||
amount: Decimal,
|
||||
): Promise<DbEvent> =>
|
||||
Event(
|
||||
EventProtocolType.CONTRIBUTION_UPDATE,
|
||||
userId,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
contributionId,
|
||||
amount,
|
||||
).save()
|
||||
|
||||
export const EVENT_ADMIN_CONTRIBUTION_CREATE = async (
|
||||
userId: number,
|
||||
contributionId: number,
|
||||
amount: Decimal,
|
||||
): Promise<DbEvent> =>
|
||||
Event(
|
||||
EventProtocolType.ADMIN_CONTRIBUTION_CREATE,
|
||||
userId,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
contributionId,
|
||||
amount,
|
||||
).save()
|
||||
|
||||
export const EVENT_ADMIN_CONTRIBUTION_UPDATE = async (
|
||||
userId: number,
|
||||
contributionId: number,
|
||||
amount: Decimal,
|
||||
): Promise<DbEvent> =>
|
||||
Event(
|
||||
EventProtocolType.ADMIN_CONTRIBUTION_UPDATE,
|
||||
userId,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
contributionId,
|
||||
amount,
|
||||
).save()
|
||||
|
||||
export const EVENT_ADMIN_CONTRIBUTION_DELETE = async (
|
||||
userId: number,
|
||||
contributionId: number,
|
||||
amount: Decimal,
|
||||
): Promise<DbEvent> =>
|
||||
Event(
|
||||
EventProtocolType.ADMIN_CONTRIBUTION_DELETE,
|
||||
userId,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
contributionId,
|
||||
amount,
|
||||
).save()
|
||||
|
||||
export const EVENT_CONTRIBUTION_CONFIRM = async (
|
||||
userId: number,
|
||||
contributionId: number,
|
||||
amount: Decimal,
|
||||
): Promise<DbEvent> =>
|
||||
Event(
|
||||
EventProtocolType.CONTRIBUTION_CONFIRM,
|
||||
userId,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
contributionId,
|
||||
amount,
|
||||
).save()
|
||||
|
||||
export const EVENT_ADMIN_CONTRIBUTION_DENY = async (
|
||||
userId: number,
|
||||
xUserId: number,
|
||||
contributionId: number,
|
||||
amount: Decimal,
|
||||
): Promise<DbEvent> =>
|
||||
Event(
|
||||
EventProtocolType.ADMIN_CONTRIBUTION_DENY,
|
||||
userId,
|
||||
xUserId,
|
||||
null,
|
||||
null,
|
||||
contributionId,
|
||||
amount,
|
||||
).save()
|
||||
|
||||
export const EVENT_TRANSACTION_SEND = async (
|
||||
userId: number,
|
||||
xUserId: number,
|
||||
transactionId: number,
|
||||
amount: Decimal,
|
||||
): Promise<DbEvent> =>
|
||||
Event(
|
||||
EventProtocolType.TRANSACTION_SEND,
|
||||
userId,
|
||||
xUserId,
|
||||
null,
|
||||
transactionId,
|
||||
null,
|
||||
amount,
|
||||
).save()
|
||||
|
||||
export const EVENT_TRANSACTION_RECEIVE = async (
|
||||
userId: number,
|
||||
xUserId: number,
|
||||
transactionId: number,
|
||||
amount: Decimal,
|
||||
): Promise<DbEvent> =>
|
||||
Event(
|
||||
EventProtocolType.TRANSACTION_RECEIVE,
|
||||
userId,
|
||||
xUserId,
|
||||
null,
|
||||
transactionId,
|
||||
null,
|
||||
amount,
|
||||
).save()
|
||||
|
||||
export const EVENT_LOGIN = async (userId: number): Promise<DbEvent> =>
|
||||
Event(EventProtocolType.LOGIN, userId, null, null, null, null, null, null).save()
|
||||
|
||||
export const EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL = async (
|
||||
userId: number,
|
||||
): Promise<DbEvent> => Event(EventProtocolType.SEND_ACCOUNT_MULTIREGISTRATION_EMAIL, userId).save()
|
||||
|
||||
export const EVENT_SEND_CONFIRMATION_EMAIL = async (userId: number): Promise<DbEvent> =>
|
||||
Event(EventProtocolType.SEND_CONFIRMATION_EMAIL, userId).save()
|
||||
|
||||
export const EVENT_ADMIN_SEND_CONFIRMATION_EMAIL = async (userId: number): Promise<DbEvent> =>
|
||||
Event(EventProtocolType.ADMIN_SEND_CONFIRMATION_EMAIL, userId).save()
|
||||
|
||||
/* export const EVENT_REDEEM_REGISTER = async (
|
||||
userId: number,
|
||||
transactionId: number | null = null,
|
||||
contributionId: number | null = null,
|
||||
): Promise<Event> =>
|
||||
Event(
|
||||
EventProtocolType.REDEEM_REGISTER,
|
||||
userId,
|
||||
null,
|
||||
null,
|
||||
transactionId,
|
||||
contributionId,
|
||||
).save()
|
||||
*/
|
||||
|
||||
export const EVENT_REGISTER = async (userId: number): Promise<DbEvent> =>
|
||||
Event(EventProtocolType.REGISTER, userId).save()
|
||||
|
||||
export const EVENT_ACTIVATE_ACCOUNT = async (userId: number): Promise<DbEvent> =>
|
||||
Event(EventProtocolType.ACTIVATE_ACCOUNT, userId).save()
|
||||
|
||||
@ -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()
|
||||
@ -1,49 +1,50 @@
|
||||
export enum EventProtocolType {
|
||||
BASIC = 'BASIC',
|
||||
VISIT_GRADIDO = 'VISIT_GRADIDO',
|
||||
// VISIT_GRADIDO = 'VISIT_GRADIDO',
|
||||
REGISTER = 'REGISTER',
|
||||
REDEEM_REGISTER = 'REDEEM_REGISTER',
|
||||
VERIFY_REDEEM = 'VERIFY_REDEEM',
|
||||
INACTIVE_ACCOUNT = 'INACTIVE_ACCOUNT',
|
||||
// VERIFY_REDEEM = 'VERIFY_REDEEM',
|
||||
// INACTIVE_ACCOUNT = 'INACTIVE_ACCOUNT',
|
||||
SEND_CONFIRMATION_EMAIL = 'SEND_CONFIRMATION_EMAIL',
|
||||
ADMIN_SEND_CONFIRMATION_EMAIL = 'ADMIN_SEND_CONFIRMATION_EMAIL',
|
||||
SEND_ACCOUNT_MULTIREGISTRATION_EMAIL = 'SEND_ACCOUNT_MULTIREGISTRATION_EMAIL',
|
||||
CONFIRM_EMAIL = 'CONFIRM_EMAIL',
|
||||
REGISTER_EMAIL_KLICKTIPP = 'REGISTER_EMAIL_KLICKTIPP',
|
||||
// CONFIRM_EMAIL = 'CONFIRM_EMAIL',
|
||||
// REGISTER_EMAIL_KLICKTIPP = 'REGISTER_EMAIL_KLICKTIPP',
|
||||
LOGIN = 'LOGIN',
|
||||
LOGOUT = 'LOGOUT',
|
||||
REDEEM_LOGIN = 'REDEEM_LOGIN',
|
||||
// LOGOUT = 'LOGOUT',
|
||||
// REDEEM_LOGIN = 'REDEEM_LOGIN',
|
||||
ACTIVATE_ACCOUNT = 'ACTIVATE_ACCOUNT',
|
||||
SEND_FORGOT_PASSWORD_EMAIL = 'SEND_FORGOT_PASSWORD_EMAIL',
|
||||
PASSWORD_CHANGE = 'PASSWORD_CHANGE',
|
||||
SEND_TRANSACTION_SEND_EMAIL = 'SEND_TRANSACTION_SEND_EMAIL',
|
||||
SEND_TRANSACTION_RECEIVE_EMAIL = 'SEND_TRANSACTION_RECEIVE_EMAIL',
|
||||
// SEND_FORGOT_PASSWORD_EMAIL = 'SEND_FORGOT_PASSWORD_EMAIL',
|
||||
// PASSWORD_CHANGE = 'PASSWORD_CHANGE',
|
||||
// SEND_TRANSACTION_SEND_EMAIL = 'SEND_TRANSACTION_SEND_EMAIL',
|
||||
// SEND_TRANSACTION_RECEIVE_EMAIL = 'SEND_TRANSACTION_RECEIVE_EMAIL',
|
||||
TRANSACTION_SEND = 'TRANSACTION_SEND',
|
||||
TRANSACTION_SEND_REDEEM = 'TRANSACTION_SEND_REDEEM',
|
||||
TRANSACTION_REPEATE_REDEEM = 'TRANSACTION_REPEATE_REDEEM',
|
||||
TRANSACTION_CREATION = 'TRANSACTION_CREATION',
|
||||
// TRANSACTION_SEND_REDEEM = 'TRANSACTION_SEND_REDEEM',
|
||||
// TRANSACTION_REPEATE_REDEEM = 'TRANSACTION_REPEATE_REDEEM',
|
||||
// TRANSACTION_CREATION = 'TRANSACTION_CREATION',
|
||||
TRANSACTION_RECEIVE = 'TRANSACTION_RECEIVE',
|
||||
TRANSACTION_RECEIVE_REDEEM = 'TRANSACTION_RECEIVE_REDEEM',
|
||||
SEND_TRANSACTION_LINK_REDEEM_EMAIL = 'SEND_TRANSACTION_LINK_REDEEM_EMAIL',
|
||||
SEND_ADDED_CONTRIBUTION_EMAIL = 'SEND_ADDED_CONTRIBUTION_EMAIL',
|
||||
SEND_CONTRIBUTION_CONFIRM_EMAIL = 'SEND_CONTRIBUTION_CONFIRM_EMAIL',
|
||||
// TRANSACTION_RECEIVE_REDEEM = 'TRANSACTION_RECEIVE_REDEEM',
|
||||
// SEND_TRANSACTION_LINK_REDEEM_EMAIL = 'SEND_TRANSACTION_LINK_REDEEM_EMAIL',
|
||||
// SEND_ADDED_CONTRIBUTION_EMAIL = 'SEND_ADDED_CONTRIBUTION_EMAIL',
|
||||
// SEND_CONTRIBUTION_CONFIRM_EMAIL = 'SEND_CONTRIBUTION_CONFIRM_EMAIL',
|
||||
CONTRIBUTION_CREATE = 'CONTRIBUTION_CREATE',
|
||||
CONTRIBUTION_CONFIRM = 'CONTRIBUTION_CONFIRM',
|
||||
CONTRIBUTION_DENY = 'CONTRIBUTION_DENY',
|
||||
CONTRIBUTION_LINK_DEFINE = 'CONTRIBUTION_LINK_DEFINE',
|
||||
CONTRIBUTION_LINK_ACTIVATE_REDEEM = 'CONTRIBUTION_LINK_ACTIVATE_REDEEM',
|
||||
// CONTRIBUTION_DENY = 'CONTRIBUTION_DENY',
|
||||
// CONTRIBUTION_LINK_DEFINE = 'CONTRIBUTION_LINK_DEFINE',
|
||||
// CONTRIBUTION_LINK_ACTIVATE_REDEEM = 'CONTRIBUTION_LINK_ACTIVATE_REDEEM',
|
||||
CONTRIBUTION_DELETE = 'CONTRIBUTION_DELETE',
|
||||
CONTRIBUTION_UPDATE = 'CONTRIBUTION_UPDATE',
|
||||
ADMIN_CONTRIBUTION_CREATE = 'ADMIN_CONTRIBUTION_CREATE',
|
||||
ADMIN_CONTRIBUTION_DELETE = 'ADMIN_CONTRIBUTION_DELETE',
|
||||
ADMIN_CONTRIBUTION_DENY = 'ADMIN_CONTRIBUTION_DENY',
|
||||
ADMIN_CONTRIBUTION_UPDATE = 'ADMIN_CONTRIBUTION_UPDATE',
|
||||
USER_CREATE_CONTRIBUTION_MESSAGE = 'USER_CREATE_CONTRIBUTION_MESSAGE',
|
||||
ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE',
|
||||
DELETE_USER = 'DELETE_USER',
|
||||
UNDELETE_USER = 'UNDELETE_USER',
|
||||
CHANGE_USER_ROLE = 'CHANGE_USER_ROLE',
|
||||
ADMIN_UPDATE_CONTRIBUTION = 'ADMIN_UPDATE_CONTRIBUTION',
|
||||
ADMIN_DELETE_CONTRIBUTION = 'ADMIN_DELETE_CONTRIBUTION',
|
||||
CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK',
|
||||
DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK',
|
||||
UPDATE_CONTRIBUTION_LINK = 'UPDATE_CONTRIBUTION_LINK',
|
||||
// USER_CREATE_CONTRIBUTION_MESSAGE = 'USER_CREATE_CONTRIBUTION_MESSAGE',
|
||||
// ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE',
|
||||
// DELETE_USER = 'DELETE_USER',
|
||||
// UNDELETE_USER = 'UNDELETE_USER',
|
||||
// CHANGE_USER_ROLE = 'CHANGE_USER_ROLE',
|
||||
// ADMIN_UPDATE_CONTRIBUTION = 'ADMIN_UPDATE_CONTRIBUTION',
|
||||
// ADMIN_DELETE_CONTRIBUTION = 'ADMIN_DELETE_CONTRIBUTION',
|
||||
// CREATE_CONTRIBUTION_LINK = 'CREATE_CONTRIBUTION_LINK',
|
||||
// DELETE_CONTRIBUTION_LINK = 'DELETE_CONTRIBUTION_LINK',
|
||||
// UPDATE_CONTRIBUTION_LINK = 'UPDATE_CONTRIBUTION_LINK',
|
||||
}
|
||||
|
||||
34
backend/src/federation/client/1_0/FederationClient.ts
Normal file
34
backend/src/federation/client/1_0/FederationClient.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
34
backend/src/federation/client/1_1/FederationClient.ts
Normal file
34
backend/src/federation/client/1_1/FederationClient.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
35
backend/src/federation/client/GraphQLGetClient.ts
Normal file
35
backend/src/federation/client/GraphQLGetClient.ts
Normal 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
|
||||
}
|
||||
}
|
||||
4
backend/src/federation/enum/apiVersionType.ts
Normal file
4
backend/src/federation/enum/apiVersionType.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export enum ApiVersionType {
|
||||
V1_0 = '1_0',
|
||||
V1_1 = '1_1',
|
||||
}
|
||||
158
backend/src/federation/validateCommunities.test.ts
Normal file
158
backend/src/federation/validateCommunities.test.ts
Normal 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`,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
80
backend/src/federation/validateCommunities.ts
Normal file
80
backend/src/federation/validateCommunities.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@ -15,6 +15,8 @@ import { calculateDecay } from '@/util/decay'
|
||||
import { RIGHTS } from '@/auth/RIGHTS'
|
||||
import { GdtResolver } from './GdtResolver'
|
||||
|
||||
import { getLastTransaction } from './util/getLastTransaction'
|
||||
|
||||
@Resolver()
|
||||
export class BalanceResolver {
|
||||
@Authorized([RIGHTS.BALANCE])
|
||||
@ -32,7 +34,7 @@ export class BalanceResolver {
|
||||
|
||||
const lastTransaction = context.lastTransaction
|
||||
? context.lastTransaction
|
||||
: await dbTransaction.findOne({ userId: user.id }, { order: { id: 'DESC' } })
|
||||
: await getLastTransaction(user.id)
|
||||
|
||||
logger.debug(`lastTransaction=${lastTransaction}`)
|
||||
|
||||
|
||||
@ -246,6 +246,7 @@ describe('Contribution Links', () => {
|
||||
})
|
||||
|
||||
it('returns an error if missing startDate', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContributionLink,
|
||||
@ -270,6 +271,7 @@ describe('Contribution Links', () => {
|
||||
})
|
||||
|
||||
it('returns an error if missing endDate', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContributionLink,
|
||||
@ -292,6 +294,7 @@ describe('Contribution Links', () => {
|
||||
})
|
||||
|
||||
it('returns an error if endDate is before startDate', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContributionLink,
|
||||
@ -316,27 +319,8 @@ describe('Contribution Links', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('returns an error if name is an empty string', async () => {
|
||||
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 () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContributionLink,
|
||||
@ -347,22 +331,17 @@ describe('Contribution Links', () => {
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
`The value of 'name' with a length of 3 did not fulfill the requested bounderies min=5 and max=100`,
|
||||
),
|
||||
],
|
||||
errors: [new GraphQLError('The value of name is too short')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
`The value of 'name' with a length of 3 did not fulfill the requested bounderies min=5 and max=100`,
|
||||
)
|
||||
expect(logger.error).toBeCalledWith('The value of name is too short', 3)
|
||||
})
|
||||
|
||||
it('returns an error if name is longer than 100 characters', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContributionLink,
|
||||
@ -373,42 +352,17 @@ describe('Contribution Links', () => {
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
`The value of 'name' with a length of 101 did not fulfill the requested bounderies min=5 and max=100`,
|
||||
),
|
||||
],
|
||||
errors: [new GraphQLError('The value of name is too long')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
`The value of 'name' with a length of 101 did not fulfill the requested bounderies min=5 and max=100`,
|
||||
)
|
||||
})
|
||||
|
||||
it('returns an error if memo is an empty string', async () => {
|
||||
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!')
|
||||
expect(logger.error).toBeCalledWith('The value of name is too long', 101)
|
||||
})
|
||||
|
||||
it('returns an error if memo is shorter than 5 characters', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContributionLink,
|
||||
@ -419,22 +373,17 @@ describe('Contribution Links', () => {
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
`The value of 'memo' with a length of 3 did not fulfill the requested bounderies min=5 and max=255`,
|
||||
),
|
||||
],
|
||||
errors: [new GraphQLError('The value of memo is too short')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
`The value of 'memo' with a length of 3 did not fulfill the requested bounderies min=5 and max=255`,
|
||||
)
|
||||
expect(logger.error).toBeCalledWith('The value of memo is too short', 3)
|
||||
})
|
||||
|
||||
it('returns an error if memo is longer than 255 characters', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContributionLink,
|
||||
@ -445,22 +394,17 @@ describe('Contribution Links', () => {
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
`The value of 'memo' with a length of 256 did not fulfill the requested bounderies min=5 and max=255`,
|
||||
),
|
||||
],
|
||||
errors: [new GraphQLError('The value of memo is too long')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
`The value of 'memo' with a length of 256 did not fulfill the requested bounderies min=5 and max=255`,
|
||||
)
|
||||
expect(logger.error).toBeCalledWith('The value of memo is too long', 256)
|
||||
})
|
||||
|
||||
it('returns an error if amount is not positive', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContributionLink,
|
||||
@ -471,15 +415,13 @@ describe('Contribution Links', () => {
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('The amount=0 must be initialized with a positiv value!')],
|
||||
errors: [new GraphQLError('The amount must be a positiv value')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'The amount=0 must be initialized with a positiv value!',
|
||||
)
|
||||
expect(logger.error).toBeCalledWith('The amount must be a positiv value', new Decimal(0))
|
||||
})
|
||||
})
|
||||
|
||||
@ -530,14 +472,14 @@ describe('Contribution Links', () => {
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('Contribution Link not found to given id.')],
|
||||
errors: [new GraphQLError('Contribution Link not found')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('Contribution Link not found to given id: -1')
|
||||
expect(logger.error).toBeCalledWith('Contribution Link not found', -1)
|
||||
})
|
||||
|
||||
describe('valid id', () => {
|
||||
@ -601,13 +543,13 @@ describe('Contribution Links', () => {
|
||||
mutate({ mutation: deleteContributionLink, variables: { id: -1 } }),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('Contribution Link not found to given id.')],
|
||||
errors: [new GraphQLError('Contribution Link not found')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('Contribution Link not found to given id: -1')
|
||||
expect(logger.error).toBeCalledWith('Contribution Link not found', -1)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -20,6 +20,7 @@ import Paginated from '@arg/Paginated'
|
||||
|
||||
// TODO: this is a strange construct
|
||||
import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver'
|
||||
import LogError from '@/server/LogError'
|
||||
|
||||
@Resolver()
|
||||
export class ContributionLinkResolver {
|
||||
@ -39,35 +40,22 @@ export class ContributionLinkResolver {
|
||||
}: ContributionLinkArgs,
|
||||
): Promise<ContributionLink> {
|
||||
isStartEndDateValid(validFrom, validTo)
|
||||
if (!name) {
|
||||
logger.error(`The name must be initialized!`)
|
||||
throw new Error(`The name must be initialized!`)
|
||||
if (name.length < CONTRIBUTIONLINK_NAME_MIN_CHARS) {
|
||||
throw new LogError('The value of name is too short', name.length)
|
||||
}
|
||||
if (
|
||||
name.length < CONTRIBUTIONLINK_NAME_MIN_CHARS ||
|
||||
name.length > CONTRIBUTIONLINK_NAME_MAX_CHARS
|
||||
) {
|
||||
const msg = `The value of 'name' with a length of ${name.length} did not fulfill the requested bounderies min=${CONTRIBUTIONLINK_NAME_MIN_CHARS} and max=${CONTRIBUTIONLINK_NAME_MAX_CHARS}`
|
||||
logger.error(`${msg}`)
|
||||
throw new Error(`${msg}`)
|
||||
if (name.length > CONTRIBUTIONLINK_NAME_MAX_CHARS) {
|
||||
throw new LogError('The value of name is too long', name.length)
|
||||
}
|
||||
if (!memo) {
|
||||
logger.error(`The memo must be initialized!`)
|
||||
throw new Error(`The memo must be initialized!`)
|
||||
if (memo.length < MEMO_MIN_CHARS) {
|
||||
throw new LogError('The value of memo is too short', memo.length)
|
||||
}
|
||||
if (memo.length < MEMO_MIN_CHARS || memo.length > MEMO_MAX_CHARS) {
|
||||
const msg = `The value of 'memo' with a length of ${memo.length} did not fulfill the requested bounderies min=${MEMO_MIN_CHARS} and max=${MEMO_MAX_CHARS}`
|
||||
logger.error(`${msg}`)
|
||||
throw new Error(`${msg}`)
|
||||
}
|
||||
if (!amount) {
|
||||
logger.error(`The amount must be initialized!`)
|
||||
throw new Error('The amount must be initialized!')
|
||||
if (memo.length > MEMO_MAX_CHARS) {
|
||||
throw new LogError('The value of memo is too long', memo.length)
|
||||
}
|
||||
if (!new Decimal(amount).isPositive()) {
|
||||
logger.error(`The amount=${amount} must be initialized with a positiv value!`)
|
||||
throw new Error(`The amount=${amount} must be initialized with a positiv value!`)
|
||||
throw new LogError('The amount must be a positiv value', amount)
|
||||
}
|
||||
|
||||
const dbContributionLink = new DbContributionLink()
|
||||
dbContributionLink.amount = amount
|
||||
dbContributionLink.name = name
|
||||
@ -107,8 +95,7 @@ export class ContributionLinkResolver {
|
||||
async deleteContributionLink(@Arg('id', () => Int) id: number): Promise<Date | null> {
|
||||
const contributionLink = await DbContributionLink.findOne(id)
|
||||
if (!contributionLink) {
|
||||
logger.error(`Contribution Link not found to given id: ${id}`)
|
||||
throw new Error('Contribution Link not found to given id.')
|
||||
throw new LogError('Contribution Link not found', id)
|
||||
}
|
||||
await contributionLink.softRemove()
|
||||
logger.debug(`deleteContributionLink successful!`)
|
||||
@ -134,8 +121,7 @@ export class ContributionLinkResolver {
|
||||
): Promise<ContributionLink> {
|
||||
const dbContributionLink = await DbContributionLink.findOne(id)
|
||||
if (!dbContributionLink) {
|
||||
logger.error(`Contribution Link not found to given id: ${id}`)
|
||||
throw new Error('Contribution Link not found to given id.')
|
||||
throw new LogError('Contribution Link not found', id)
|
||||
}
|
||||
dbContributionLink.amount = amount
|
||||
dbContributionLink.name = name
|
||||
|
||||
@ -88,6 +88,7 @@ describe('ContributionMessageResolver', () => {
|
||||
|
||||
describe('input not valid', () => {
|
||||
it('throws error when contribution does not exist', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: adminCreateContributionMessage,
|
||||
@ -100,14 +101,22 @@ describe('ContributionMessageResolver', () => {
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
'ContributionMessage was not successful: Error: Contribution not found',
|
||||
'ContributionMessage was not sent successfully: Error: Contribution not found',
|
||||
),
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'ContributionMessage was not sent successfully: Error: Contribution not found',
|
||||
new Error('Contribution not found'),
|
||||
)
|
||||
})
|
||||
|
||||
it('throws error when contribution.userId equals user.id', async () => {
|
||||
jest.clearAllMocks()
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
@ -132,12 +141,19 @@ describe('ContributionMessageResolver', () => {
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
'ContributionMessage was not successful: Error: Admin can not answer on own contribution',
|
||||
'ContributionMessage was not sent successfully: Error: Admin can not answer on his own contribution',
|
||||
),
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'ContributionMessage was not sent successfully: Error: Admin can not answer on his own contribution',
|
||||
new Error('Admin can not answer on his own contribution'),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('valid input', () => {
|
||||
@ -210,6 +226,7 @@ describe('ContributionMessageResolver', () => {
|
||||
|
||||
describe('input not valid', () => {
|
||||
it('throws error when contribution does not exist', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createContributionMessage,
|
||||
@ -222,14 +239,22 @@ describe('ContributionMessageResolver', () => {
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
'ContributionMessage was not successful: Error: Contribution not found',
|
||||
'ContributionMessage was not sent successfully: Error: Contribution not found',
|
||||
),
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'ContributionMessage was not sent successfully: Error: Contribution not found',
|
||||
new Error('Contribution not found'),
|
||||
)
|
||||
})
|
||||
|
||||
it('throws error when other user tries to send createContributionMessage', async () => {
|
||||
jest.clearAllMocks()
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
@ -246,12 +271,19 @@ describe('ContributionMessageResolver', () => {
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
'ContributionMessage was not successful: Error: Can not send message to contribution of another user',
|
||||
'ContributionMessage was not sent successfully: Error: Can not send message to contribution of another user',
|
||||
),
|
||||
],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'ContributionMessage was not sent successfully: Error: Can not send message to contribution of another user',
|
||||
new Error('Can not send message to contribution of another user'),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('valid input', () => {
|
||||
|
||||
@ -12,10 +12,10 @@ import { ContributionStatus } from '@enum/ContributionStatus'
|
||||
import { Order } from '@enum/Order'
|
||||
import Paginated from '@arg/Paginated'
|
||||
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import { RIGHTS } from '@/auth/RIGHTS'
|
||||
import { Context, getUser } from '@/server/context'
|
||||
import { sendAddedContributionMessageEmail } from '@/emails/sendEmailVariants'
|
||||
import LogError from '@/server/LogError'
|
||||
|
||||
@Resolver()
|
||||
export class ContributionMessageResolver {
|
||||
@ -54,8 +54,7 @@ export class ContributionMessageResolver {
|
||||
await queryRunner.commitTransaction()
|
||||
} catch (e) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
logger.error(`ContributionMessage was not successful: ${e}`)
|
||||
throw new Error(`ContributionMessage was not successful: ${e}`)
|
||||
throw new LogError(`ContributionMessage was not sent successfully: ${e}`, e)
|
||||
} finally {
|
||||
await queryRunner.release()
|
||||
}
|
||||
@ -95,9 +94,7 @@ export class ContributionMessageResolver {
|
||||
@Ctx() context: Context,
|
||||
): Promise<ContributionMessage> {
|
||||
const user = getUser(context)
|
||||
if (!user.emailContact) {
|
||||
user.emailContact = await UserContact.findOneOrFail({ where: { id: user.emailId } })
|
||||
}
|
||||
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('REPEATABLE READ')
|
||||
@ -108,12 +105,10 @@ export class ContributionMessageResolver {
|
||||
relations: ['user'],
|
||||
})
|
||||
if (!contribution) {
|
||||
logger.error('Contribution not found')
|
||||
throw new Error('Contribution not found')
|
||||
throw new LogError('Contribution not found', contributionId)
|
||||
}
|
||||
if (contribution.userId === user.id) {
|
||||
logger.error('Admin can not answer on own contribution')
|
||||
throw new Error('Admin can not answer on own contribution')
|
||||
throw new LogError('Admin can not answer on his own contribution', contributionId)
|
||||
}
|
||||
if (!contribution.user.emailContact) {
|
||||
contribution.user.emailContact = await UserContact.findOneOrFail({
|
||||
@ -149,8 +144,7 @@ export class ContributionMessageResolver {
|
||||
await queryRunner.commitTransaction()
|
||||
} catch (e) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
logger.error(`ContributionMessage was not successful: ${e}`)
|
||||
throw new Error(`ContributionMessage was not successful: ${e}`)
|
||||
throw new LogError(`ContributionMessage was not sent successfully: ${e}`, e)
|
||||
} finally {
|
||||
await queryRunner.release()
|
||||
}
|
||||
|
||||
@ -22,11 +22,7 @@ import {
|
||||
listContributions,
|
||||
listUnconfirmedContributions,
|
||||
} from '@/seeds/graphql/queries'
|
||||
import {
|
||||
// sendAccountActivationEmail,
|
||||
sendContributionConfirmedEmail,
|
||||
// sendContributionRejectedEmail,
|
||||
} from '@/emails/sendEmailVariants'
|
||||
import { sendContributionConfirmedEmail } from '@/emails/sendEmailVariants'
|
||||
import {
|
||||
cleanDB,
|
||||
resetToken,
|
||||
@ -46,8 +42,8 @@ import { User } from '@entity/User'
|
||||
import { EventProtocolType } from '@/event/EventProtocolType'
|
||||
import { logger, i18n as localization } from '@test/testSetup'
|
||||
import { UserInputError } from 'apollo-server-express'
|
||||
import { ContributionStatus } from '../enum/ContributionStatus'
|
||||
|
||||
// mock account activation email to avoid console spam
|
||||
// mock account activation email to avoid console spam
|
||||
jest.mock('@/emails/sendEmailVariants', () => {
|
||||
const originalModule = jest.requireActual('@/emails/sendEmailVariants')
|
||||
@ -132,13 +128,13 @@ describe('ContributionResolver', () => {
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('memo text is too short (5 characters minimum)')],
|
||||
errors: [new GraphQLError('Memo text is too short')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error found', () => {
|
||||
expect(logger.error).toBeCalledWith(`memo text is too short: memo.length=4 < 5`)
|
||||
expect(logger.error).toBeCalledWith('Memo text is too short', 4)
|
||||
})
|
||||
|
||||
it('throws error when memo length greater than 255 chars', async () => {
|
||||
@ -155,13 +151,13 @@ describe('ContributionResolver', () => {
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('memo text is too long (255 characters maximum)')],
|
||||
errors: [new GraphQLError('Memo text is too long')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error found', () => {
|
||||
expect(logger.error).toBeCalledWith(`memo text is too long: memo.length=259 > 255`)
|
||||
expect(logger.error).toBeCalledWith('Memo text is too long', 259)
|
||||
})
|
||||
|
||||
it('throws error when creationDate not-valid', async () => {
|
||||
@ -248,7 +244,7 @@ describe('ContributionResolver', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('stores the create contribution event in the database', async () => {
|
||||
it('stores the CONTRIBUTION_CREATE event in the database', async () => {
|
||||
await expect(EventProtocol.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventProtocolType.CONTRIBUTION_CREATE,
|
||||
@ -422,31 +418,6 @@ describe('ContributionResolver', () => {
|
||||
resetToken()
|
||||
})
|
||||
|
||||
describe('wrong contribution id', () => {
|
||||
it('throws an error', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: updateContribution,
|
||||
variables: {
|
||||
contributionId: -1,
|
||||
amount: 100.0,
|
||||
memo: 'Test env contribution',
|
||||
creationDate: new Date().toString(),
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('No contribution found to given id.')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error found', () => {
|
||||
expect(logger.error).toBeCalledWith('No contribution found to given id')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Memo length smaller than 5 chars', () => {
|
||||
it('throws error', async () => {
|
||||
jest.clearAllMocks()
|
||||
@ -463,13 +434,13 @@ describe('ContributionResolver', () => {
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('memo text is too short (5 characters minimum)')],
|
||||
errors: [new GraphQLError('Memo text is too short')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error found', () => {
|
||||
expect(logger.error).toBeCalledWith('memo text is too short: memo.length=4 < 5')
|
||||
expect(logger.error).toBeCalledWith('Memo text is too short', 4)
|
||||
})
|
||||
})
|
||||
|
||||
@ -489,13 +460,38 @@ describe('ContributionResolver', () => {
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('memo text is too long (255 characters maximum)')],
|
||||
errors: [new GraphQLError('Memo text is too long')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error found', () => {
|
||||
expect(logger.error).toBeCalledWith('memo text is too long: memo.length=259 > 255')
|
||||
expect(logger.error).toBeCalledWith('Memo text is too long', 259)
|
||||
})
|
||||
})
|
||||
|
||||
describe('wrong contribution id', () => {
|
||||
it('throws an error', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: updateContribution,
|
||||
variables: {
|
||||
contributionId: -1,
|
||||
amount: 100.0,
|
||||
memo: 'Test env contribution',
|
||||
creationDate: new Date().toString(),
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('Contribution not found')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error found', () => {
|
||||
expect(logger.error).toBeCalledWith('Contribution not found', -1)
|
||||
})
|
||||
})
|
||||
|
||||
@ -521,18 +517,16 @@ describe('ContributionResolver', () => {
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
'user of the pending contribution and send user does not correspond',
|
||||
),
|
||||
],
|
||||
errors: [new GraphQLError('Can not update contribution of another user')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error found', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'user of the pending contribution and send user does not correspond',
|
||||
'Can not update contribution of another user',
|
||||
expect.any(Object),
|
||||
expect.any(Number),
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -553,12 +547,64 @@ describe('ContributionResolver', () => {
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('An admin is not allowed to update a user contribution.')],
|
||||
errors: [new GraphQLError('An admin is not allowed to update an user contribution')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
// TODO check that the error is logged (need to modify AdminResolver, avoid conflicts)
|
||||
it('logs the error found', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'An admin is not allowed to update an user contribution',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('contribution has wrong status', () => {
|
||||
beforeAll(async () => {
|
||||
const contribution = await Contribution.findOneOrFail({
|
||||
id: result.data.createContribution.id,
|
||||
})
|
||||
contribution.contributionStatus = ContributionStatus.DELETED
|
||||
contribution.save()
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
const contribution = await Contribution.findOneOrFail({
|
||||
id: result.data.createContribution.id,
|
||||
})
|
||||
contribution.contributionStatus = ContributionStatus.PENDING
|
||||
contribution.save()
|
||||
})
|
||||
|
||||
it('throws an error', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: updateContribution,
|
||||
variables: {
|
||||
contributionId: result.data.createContribution.id,
|
||||
amount: 10.0,
|
||||
memo: 'Test env contribution',
|
||||
creationDate: new Date().toString(),
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('Contribution can not be updated due to status')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error found', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'Contribution can not be updated due to status',
|
||||
ContributionStatus.DELETED,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('update too much so that the limit is exceeded', () => {
|
||||
@ -615,16 +661,13 @@ describe('ContributionResolver', () => {
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('Currently the month of the contribution cannot change.')],
|
||||
errors: [new GraphQLError('Month of contribution can not be changed')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it.skip('logs the error found', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'No information for available creations with the given creationDate=',
|
||||
'Invalid Date',
|
||||
)
|
||||
it('logs the error found', () => {
|
||||
expect(logger.error).toBeCalledWith('Month of contribution can not be changed')
|
||||
})
|
||||
})
|
||||
|
||||
@ -653,7 +696,7 @@ describe('ContributionResolver', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('stores the update contribution event in the database', async () => {
|
||||
it('stores the CONTRIBUTION_UPDATE event in the database', async () => {
|
||||
bibi = await query({
|
||||
query: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
@ -1158,6 +1201,7 @@ describe('ContributionResolver', () => {
|
||||
|
||||
describe('wrong contribution id', () => {
|
||||
it('returns an error', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: deleteContribution,
|
||||
@ -1167,18 +1211,19 @@ describe('ContributionResolver', () => {
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('Contribution not found for given id.')],
|
||||
errors: [new GraphQLError('Contribution not found')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error found', () => {
|
||||
expect(logger.error).toBeCalledWith('Contribution not found for given id')
|
||||
expect(logger.error).toBeCalledWith('Contribution not found', -1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('other user sends a deleteContribution', () => {
|
||||
it('returns an error', async () => {
|
||||
jest.clearAllMocks()
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
@ -1198,7 +1243,11 @@ describe('ContributionResolver', () => {
|
||||
})
|
||||
|
||||
it('logs the error found', () => {
|
||||
expect(logger.error).toBeCalledWith('Can not delete contribution of another user')
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'Can not delete contribution of another user',
|
||||
expect.any(Object),
|
||||
expect.any(Number),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@ -1214,7 +1263,7 @@ describe('ContributionResolver', () => {
|
||||
).resolves.toBeTruthy()
|
||||
})
|
||||
|
||||
it('stores the delete contribution event in the database', async () => {
|
||||
it('stores the CONTRIBUTION_DELETE event in the database', async () => {
|
||||
const contribution = await mutate({
|
||||
mutation: createContribution,
|
||||
variables: {
|
||||
@ -1274,7 +1323,10 @@ describe('ContributionResolver', () => {
|
||||
})
|
||||
|
||||
it('logs the error found', () => {
|
||||
expect(logger.error).toBeCalledWith('A confirmed contribution can not be deleted')
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'A confirmed contribution can not be deleted',
|
||||
expect.objectContaining({ contributionStatus: 'CONFIRMED' }),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1540,15 +1592,13 @@ describe('ContributionResolver', () => {
|
||||
mutate({ mutation: adminCreateContribution, variables }),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('Could not find user with email: bibi@bloxberg.de')],
|
||||
errors: [new GraphQLError('Could not find user')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'Could not find user with email: bibi@bloxberg.de',
|
||||
)
|
||||
expect(logger.error).toBeCalledWith('Could not find user', 'bibi@bloxberg.de')
|
||||
})
|
||||
})
|
||||
|
||||
@ -1568,7 +1618,7 @@ describe('ContributionResolver', () => {
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError('This user was deleted. Cannot create a contribution.'),
|
||||
new GraphQLError('Cannot create contribution since the user was deleted'),
|
||||
],
|
||||
}),
|
||||
)
|
||||
@ -1576,7 +1626,12 @@ describe('ContributionResolver', () => {
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'This user was deleted. Cannot create a contribution.',
|
||||
'Cannot create contribution since the user was deleted',
|
||||
expect.objectContaining({
|
||||
user: expect.objectContaining({
|
||||
deletedAt: new Date('2018-03-14T09:17:52.000Z'),
|
||||
}),
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -1597,7 +1652,9 @@ describe('ContributionResolver', () => {
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError('Contribution could not be saved, Email is not activated'),
|
||||
new GraphQLError(
|
||||
'Cannot create contribution since the users email is not activated',
|
||||
),
|
||||
],
|
||||
}),
|
||||
)
|
||||
@ -1605,7 +1662,8 @@ describe('ContributionResolver', () => {
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'Contribution could not be saved, Email is not activated',
|
||||
'Cannot create contribution since the users email is not activated',
|
||||
expect.objectContaining({ emailChecked: false }),
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -1624,13 +1682,13 @@ describe('ContributionResolver', () => {
|
||||
mutate({ mutation: adminCreateContribution, variables }),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError(`invalid Date for creationDate=invalid-date`)],
|
||||
errors: [new GraphQLError('CreationDate is invalid')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(`invalid Date for creationDate=invalid-date`)
|
||||
expect(logger.error).toBeCalledWith('CreationDate is invalid', 'invalid-date')
|
||||
})
|
||||
})
|
||||
|
||||
@ -1722,7 +1780,7 @@ describe('ContributionResolver', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('stores the admin create contribution event in the database', async () => {
|
||||
it('stores the ADMIN_CONTRIBUTION_CREATE event in the database', async () => {
|
||||
await expect(EventProtocol.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventProtocolType.ADMIN_CONTRIBUTION_CREATE,
|
||||
@ -1826,17 +1884,13 @@ describe('ContributionResolver', () => {
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError('Could not find UserContact with email: bob@baumeister.de'),
|
||||
],
|
||||
errors: [new GraphQLError('Could not find User')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'Could not find UserContact with email: bob@baumeister.de',
|
||||
)
|
||||
expect(logger.error).toBeCalledWith('Could not find User', 'bob@baumeister.de')
|
||||
})
|
||||
})
|
||||
|
||||
@ -1856,13 +1910,13 @@ describe('ContributionResolver', () => {
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('User was deleted (stephen@hawking.uk)')],
|
||||
errors: [new GraphQLError('User was deleted')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('User was deleted (stephen@hawking.uk)')
|
||||
expect(logger.error).toBeCalledWith('User was deleted', 'stephen@hawking.uk')
|
||||
})
|
||||
})
|
||||
|
||||
@ -1882,13 +1936,13 @@ describe('ContributionResolver', () => {
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('No contribution found to given id.')],
|
||||
errors: [new GraphQLError('Contribution not found')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('No contribution found to given id.')
|
||||
expect(logger.error).toBeCalledWith('Contribution not found', -1)
|
||||
})
|
||||
})
|
||||
|
||||
@ -1912,7 +1966,7 @@ describe('ContributionResolver', () => {
|
||||
expect.objectContaining({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
'user of the pending contribution and send user does not correspond',
|
||||
'User of the pending contribution and send user does not correspond',
|
||||
),
|
||||
],
|
||||
}),
|
||||
@ -1921,7 +1975,7 @@ describe('ContributionResolver', () => {
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'user of the pending contribution and send user does not correspond',
|
||||
'User of the pending contribution and send user does not correspond',
|
||||
)
|
||||
})
|
||||
})
|
||||
@ -1991,7 +2045,7 @@ describe('ContributionResolver', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('stores the admin update contribution event in the database', async () => {
|
||||
it('stores the ADMIN_CONTRIBUTION_UPDATE event in the database', async () => {
|
||||
await expect(EventProtocol.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventProtocolType.ADMIN_CONTRIBUTION_UPDATE,
|
||||
@ -2031,7 +2085,7 @@ describe('ContributionResolver', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('stores the admin update contribution event in the database', async () => {
|
||||
it('stores the ADMIN_CONTRIBUTION_UPDATE event in the database', async () => {
|
||||
await expect(EventProtocol.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventProtocolType.ADMIN_CONTRIBUTION_UPDATE,
|
||||
@ -2116,13 +2170,13 @@ describe('ContributionResolver', () => {
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('Contribution not found for given id.')],
|
||||
errors: [new GraphQLError('Contribution not found')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('Contribution not found for given id: -1')
|
||||
expect(logger.error).toBeCalledWith('Contribution not found', -1)
|
||||
})
|
||||
})
|
||||
|
||||
@ -2175,7 +2229,7 @@ describe('ContributionResolver', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('stores the admin delete contribution event in the database', async () => {
|
||||
it('stores the ADMIN_CONTRIBUTION_DELETE event in the database', async () => {
|
||||
await expect(EventProtocol.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventProtocolType.ADMIN_CONTRIBUTION_DELETE,
|
||||
@ -2242,13 +2296,13 @@ describe('ContributionResolver', () => {
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('Contribution not found to given id.')],
|
||||
errors: [new GraphQLError('Contribution not found')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('Contribution not found for given id: -1')
|
||||
expect(logger.error).toBeCalledWith('Contribution not found', -1)
|
||||
})
|
||||
})
|
||||
|
||||
@ -2317,7 +2371,7 @@ describe('ContributionResolver', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('stores the contribution confirm event in the database', async () => {
|
||||
it('stores the CONTRIBUTION_CONFIRM event in the database', async () => {
|
||||
await expect(EventProtocol.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventProtocolType.CONTRIBUTION_CONFIRM,
|
||||
@ -2349,7 +2403,7 @@ describe('ContributionResolver', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('stores the send confirmation email event in the database', async () => {
|
||||
it('stores the SEND_CONFIRMATION_EMAIL event in the database', async () => {
|
||||
await expect(EventProtocol.find()).resolves.toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: EventProtocolType.SEND_CONFIRMATION_EMAIL,
|
||||
@ -2359,6 +2413,7 @@ describe('ContributionResolver', () => {
|
||||
|
||||
describe('confirm same contribution again', () => {
|
||||
it('throws an error', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: confirmContribution,
|
||||
@ -2368,11 +2423,18 @@ describe('ContributionResolver', () => {
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('Contribution already confirmd.')],
|
||||
errors: [new GraphQLError('Contribution already confirmed')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'Contribution already confirmed',
|
||||
expect.any(Number),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('confirm two creations one after the other quickly', () => {
|
||||
|
||||
@ -37,22 +37,25 @@ import {
|
||||
} from './util/creations'
|
||||
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS, FULL_CREATION_AVAILABLE } from './const/const'
|
||||
import {
|
||||
Event,
|
||||
EventContributionCreate,
|
||||
EventContributionDelete,
|
||||
EventContributionUpdate,
|
||||
EventContributionConfirm,
|
||||
EventAdminContributionCreate,
|
||||
EventAdminContributionDelete,
|
||||
EventAdminContributionUpdate,
|
||||
EVENT_CONTRIBUTION_CREATE,
|
||||
EVENT_CONTRIBUTION_DELETE,
|
||||
EVENT_CONTRIBUTION_UPDATE,
|
||||
EVENT_ADMIN_CONTRIBUTION_CREATE,
|
||||
EVENT_ADMIN_CONTRIBUTION_UPDATE,
|
||||
EVENT_ADMIN_CONTRIBUTION_DELETE,
|
||||
EVENT_CONTRIBUTION_CONFIRM,
|
||||
EVENT_ADMIN_CONTRIBUTION_DENY,
|
||||
} from '@/event/Event'
|
||||
import { eventProtocol } from '@/event/EventProtocolEmitter'
|
||||
import { calculateDecay } from '@/util/decay'
|
||||
import {
|
||||
sendContributionConfirmedEmail,
|
||||
sendContributionDeletedEmail,
|
||||
sendContributionDeniedEmail,
|
||||
} from '@/emails/sendEmailVariants'
|
||||
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
|
||||
import LogError from '@/server/LogError'
|
||||
|
||||
import { getLastTransaction } from './util/getLastTransaction'
|
||||
|
||||
@Resolver()
|
||||
export class ContributionResolver {
|
||||
@ -63,17 +66,12 @@ export class ContributionResolver {
|
||||
@Ctx() context: Context,
|
||||
): Promise<UnconfirmedContribution> {
|
||||
const clientTimezoneOffset = getClientTimezoneOffset(context)
|
||||
if (memo.length > MEMO_MAX_CHARS) {
|
||||
logger.error(`memo text is too long: memo.length=${memo.length} > ${MEMO_MAX_CHARS}`)
|
||||
throw new Error(`memo text is too long (${MEMO_MAX_CHARS} characters maximum)`)
|
||||
}
|
||||
|
||||
if (memo.length < MEMO_MIN_CHARS) {
|
||||
logger.error(`memo text is too short: memo.length=${memo.length} < ${MEMO_MIN_CHARS}`)
|
||||
throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`)
|
||||
throw new LogError('Memo text is too short', memo.length)
|
||||
}
|
||||
if (memo.length > MEMO_MAX_CHARS) {
|
||||
throw new LogError('Memo text is too long', memo.length)
|
||||
}
|
||||
|
||||
const event = new Event()
|
||||
|
||||
const user = getUser(context)
|
||||
const creations = await getUserCreation(user.id, clientTimezoneOffset)
|
||||
@ -93,11 +91,7 @@ export class ContributionResolver {
|
||||
logger.trace('contribution to save', contribution)
|
||||
await DbContribution.save(contribution)
|
||||
|
||||
const eventCreateContribution = new EventContributionCreate()
|
||||
eventCreateContribution.userId = user.id
|
||||
eventCreateContribution.amount = amount
|
||||
eventCreateContribution.contributionId = contribution.id
|
||||
await eventProtocol.writeEvent(event.setEventContributionCreate(eventCreateContribution))
|
||||
await EVENT_CONTRIBUTION_CREATE(user.id, contribution.id, amount)
|
||||
|
||||
return new UnconfirmedContribution(contribution, user, creations)
|
||||
}
|
||||
@ -108,20 +102,16 @@ export class ContributionResolver {
|
||||
@Arg('id', () => Int) id: number,
|
||||
@Ctx() context: Context,
|
||||
): Promise<boolean> {
|
||||
const event = new Event()
|
||||
const user = getUser(context)
|
||||
const contribution = await DbContribution.findOne(id)
|
||||
if (!contribution) {
|
||||
logger.error('Contribution not found for given id')
|
||||
throw new Error('Contribution not found for given id.')
|
||||
throw new LogError('Contribution not found', id)
|
||||
}
|
||||
if (contribution.userId !== user.id) {
|
||||
logger.error('Can not delete contribution of another user')
|
||||
throw new Error('Can not delete contribution of another user')
|
||||
throw new LogError('Can not delete contribution of another user', contribution, user.id)
|
||||
}
|
||||
if (contribution.confirmedAt) {
|
||||
logger.error('A confirmed contribution can not be deleted')
|
||||
throw new Error('A confirmed contribution can not be deleted')
|
||||
throw new LogError('A confirmed contribution can not be deleted', contribution)
|
||||
}
|
||||
|
||||
contribution.contributionStatus = ContributionStatus.DELETED
|
||||
@ -129,11 +119,7 @@ export class ContributionResolver {
|
||||
contribution.deletedAt = new Date()
|
||||
await contribution.save()
|
||||
|
||||
const eventDeleteContribution = new EventContributionDelete()
|
||||
eventDeleteContribution.userId = user.id
|
||||
eventDeleteContribution.contributionId = contribution.id
|
||||
eventDeleteContribution.amount = contribution.amount
|
||||
await eventProtocol.writeEvent(event.setEventContributionDelete(eventDeleteContribution))
|
||||
await EVENT_CONTRIBUTION_DELETE(user.id, contribution.id, contribution.amount)
|
||||
|
||||
const res = await contribution.softRemove()
|
||||
return !!res
|
||||
@ -195,6 +181,7 @@ export class ContributionResolver {
|
||||
.select('c')
|
||||
.from(DbContribution, 'c')
|
||||
.innerJoinAndSelect('c.user', 'u')
|
||||
.leftJoinAndSelect('c.messages', 'm')
|
||||
.where(where)
|
||||
.orderBy('c.createdAt', order)
|
||||
.limit(pageSize)
|
||||
@ -215,14 +202,11 @@ export class ContributionResolver {
|
||||
@Ctx() context: Context,
|
||||
): Promise<UnconfirmedContribution> {
|
||||
const clientTimezoneOffset = getClientTimezoneOffset(context)
|
||||
if (memo.length > MEMO_MAX_CHARS) {
|
||||
logger.error(`memo text is too long: memo.length=${memo.length} > ${MEMO_MAX_CHARS}`)
|
||||
throw new Error(`memo text is too long (${MEMO_MAX_CHARS} characters maximum)`)
|
||||
}
|
||||
|
||||
if (memo.length < MEMO_MIN_CHARS) {
|
||||
logger.error(`memo text is too short: memo.length=${memo.length} < ${MEMO_MIN_CHARS}`)
|
||||
throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`)
|
||||
throw new LogError('Memo text is too short', memo.length)
|
||||
}
|
||||
if (memo.length > MEMO_MAX_CHARS) {
|
||||
throw new LogError('Memo text is too long', memo.length)
|
||||
}
|
||||
|
||||
const user = getUser(context)
|
||||
@ -231,22 +215,22 @@ export class ContributionResolver {
|
||||
where: { id: contributionId, confirmedAt: IsNull(), deniedAt: IsNull() },
|
||||
})
|
||||
if (!contributionToUpdate) {
|
||||
logger.error('No contribution found to given id')
|
||||
throw new Error('No contribution found to given id.')
|
||||
throw new LogError('Contribution not found', contributionId)
|
||||
}
|
||||
if (contributionToUpdate.userId !== user.id) {
|
||||
logger.error('user of the pending contribution and send user does not correspond')
|
||||
throw new Error('user of the pending contribution and send user does not correspond')
|
||||
throw new LogError(
|
||||
'Can not update contribution of another user',
|
||||
contributionToUpdate,
|
||||
user.id,
|
||||
)
|
||||
}
|
||||
if (
|
||||
contributionToUpdate.contributionStatus !== ContributionStatus.IN_PROGRESS &&
|
||||
contributionToUpdate.contributionStatus !== ContributionStatus.PENDING
|
||||
) {
|
||||
logger.error(
|
||||
`Contribution can not be updated since the state is ${contributionToUpdate.contributionStatus}`,
|
||||
)
|
||||
throw new Error(
|
||||
`Contribution can not be updated since the state is ${contributionToUpdate.contributionStatus}`,
|
||||
throw new LogError(
|
||||
'Contribution can not be updated due to status',
|
||||
contributionToUpdate.contributionStatus,
|
||||
)
|
||||
}
|
||||
const creationDateObj = new Date(creationDate)
|
||||
@ -254,8 +238,7 @@ export class ContributionResolver {
|
||||
if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) {
|
||||
creations = updateCreations(creations, contributionToUpdate, clientTimezoneOffset)
|
||||
} else {
|
||||
logger.error('Currently the month of the contribution cannot change.')
|
||||
throw new Error('Currently the month of the contribution cannot change.')
|
||||
throw new LogError('Month of contribution can not be changed')
|
||||
}
|
||||
|
||||
// all possible cases not to be true are thrown in this function
|
||||
@ -284,13 +267,7 @@ export class ContributionResolver {
|
||||
contributionToUpdate.updatedAt = new Date()
|
||||
DbContribution.save(contributionToUpdate)
|
||||
|
||||
const event = new Event()
|
||||
|
||||
const eventUpdateContribution = new EventContributionUpdate()
|
||||
eventUpdateContribution.userId = user.id
|
||||
eventUpdateContribution.contributionId = contributionId
|
||||
eventUpdateContribution.amount = amount
|
||||
await eventProtocol.writeEvent(event.setEventContributionUpdate(eventUpdateContribution))
|
||||
await EVENT_CONTRIBUTION_UPDATE(user.id, contributionId, amount)
|
||||
|
||||
return new UnconfirmedContribution(contributionToUpdate, user, creations)
|
||||
}
|
||||
@ -306,32 +283,26 @@ export class ContributionResolver {
|
||||
)
|
||||
const clientTimezoneOffset = getClientTimezoneOffset(context)
|
||||
if (!isValidDateString(creationDate)) {
|
||||
logger.error(`invalid Date for creationDate=${creationDate}`)
|
||||
throw new Error(`invalid Date for creationDate=${creationDate}`)
|
||||
throw new LogError('CreationDate is invalid', creationDate)
|
||||
}
|
||||
const emailContact = await UserContact.findOne({
|
||||
where: { email },
|
||||
withDeleted: true,
|
||||
relations: ['user'],
|
||||
})
|
||||
if (!emailContact) {
|
||||
logger.error(`Could not find user with email: ${email}`)
|
||||
throw new Error(`Could not find user with email: ${email}`)
|
||||
if (!emailContact || !emailContact.user) {
|
||||
throw new LogError('Could not find user', email)
|
||||
}
|
||||
if (emailContact.deletedAt) {
|
||||
logger.error('This emailContact was deleted. Cannot create a contribution.')
|
||||
throw new Error('This emailContact was deleted. Cannot create a contribution.')
|
||||
}
|
||||
if (emailContact.user.deletedAt) {
|
||||
logger.error('This user was deleted. Cannot create a contribution.')
|
||||
throw new Error('This user was deleted. Cannot create a contribution.')
|
||||
if (emailContact.deletedAt || emailContact.user.deletedAt) {
|
||||
throw new LogError('Cannot create contribution since the user was deleted', emailContact)
|
||||
}
|
||||
if (!emailContact.emailChecked) {
|
||||
logger.error('Contribution could not be saved, Email is not activated')
|
||||
throw new Error('Contribution could not be saved, Email is not activated')
|
||||
throw new LogError(
|
||||
'Cannot create contribution since the users email is not activated',
|
||||
emailContact,
|
||||
)
|
||||
}
|
||||
|
||||
const event = new Event()
|
||||
const moderator = getUser(context)
|
||||
logger.trace('moderator: ', moderator.id)
|
||||
const creations = await getUserCreation(emailContact.userId, clientTimezoneOffset)
|
||||
@ -353,13 +324,7 @@ export class ContributionResolver {
|
||||
|
||||
await DbContribution.save(contribution)
|
||||
|
||||
const eventAdminCreateContribution = new EventAdminContributionCreate()
|
||||
eventAdminCreateContribution.userId = moderator.id
|
||||
eventAdminCreateContribution.amount = amount
|
||||
eventAdminCreateContribution.contributionId = contribution.id
|
||||
await eventProtocol.writeEvent(
|
||||
event.setEventAdminContributionCreate(eventAdminCreateContribution),
|
||||
)
|
||||
await EVENT_ADMIN_CONTRIBUTION_CREATE(moderator.id, contribution.id, amount)
|
||||
|
||||
return getUserCreation(emailContact.userId, clientTimezoneOffset)
|
||||
}
|
||||
@ -403,18 +368,11 @@ export class ContributionResolver {
|
||||
withDeleted: true,
|
||||
relations: ['user'],
|
||||
})
|
||||
if (!emailContact) {
|
||||
logger.error(`Could not find UserContact with email: ${email}`)
|
||||
throw new Error(`Could not find UserContact with email: ${email}`)
|
||||
if (!emailContact || !emailContact.user) {
|
||||
throw new LogError('Could not find User', email)
|
||||
}
|
||||
const user = emailContact.user
|
||||
if (!user) {
|
||||
logger.error(`Could not find User to emailContact: ${email}`)
|
||||
throw new Error(`Could not find User to emailContact: ${email}`)
|
||||
}
|
||||
if (user.deletedAt) {
|
||||
logger.error(`User was deleted (${email})`)
|
||||
throw new Error(`User was deleted (${email})`)
|
||||
if (emailContact.deletedAt || emailContact.user.deletedAt) {
|
||||
throw new LogError('User was deleted', email)
|
||||
}
|
||||
|
||||
const moderator = getUser(context)
|
||||
@ -423,28 +381,25 @@ export class ContributionResolver {
|
||||
where: { id, confirmedAt: IsNull(), deniedAt: IsNull() },
|
||||
})
|
||||
if (!contributionToUpdate) {
|
||||
logger.error('No contribution found to given id.')
|
||||
throw new Error('No contribution found to given id.')
|
||||
throw new LogError('Contribution not found', id)
|
||||
}
|
||||
|
||||
if (contributionToUpdate.userId !== user.id) {
|
||||
logger.error('user of the pending contribution and send user does not correspond')
|
||||
throw new Error('user of the pending contribution and send user does not correspond')
|
||||
if (contributionToUpdate.userId !== emailContact.user.id) {
|
||||
throw new LogError('User of the pending contribution and send user does not correspond')
|
||||
}
|
||||
|
||||
if (contributionToUpdate.moderatorId === null) {
|
||||
logger.error('An admin is not allowed to update a user contribution.')
|
||||
throw new Error('An admin is not allowed to update a user contribution.')
|
||||
throw new LogError('An admin is not allowed to update an user contribution')
|
||||
}
|
||||
|
||||
const creationDateObj = new Date(creationDate)
|
||||
let creations = await getUserCreation(user.id, clientTimezoneOffset)
|
||||
let creations = await getUserCreation(emailContact.user.id, clientTimezoneOffset)
|
||||
|
||||
// TODO: remove this restriction
|
||||
if (contributionToUpdate.contributionDate.getMonth() === creationDateObj.getMonth()) {
|
||||
creations = updateCreations(creations, contributionToUpdate, clientTimezoneOffset)
|
||||
} else {
|
||||
logger.error('Currently the month of the contribution cannot change.')
|
||||
throw new Error('Currently the month of the contribution cannot change.')
|
||||
throw new LogError('Month of contribution can not be changed')
|
||||
}
|
||||
|
||||
// all possible cases not to be true are thrown in this function
|
||||
@ -462,16 +417,9 @@ export class ContributionResolver {
|
||||
result.memo = contributionToUpdate.memo
|
||||
result.date = contributionToUpdate.contributionDate
|
||||
|
||||
result.creation = await getUserCreation(user.id, clientTimezoneOffset)
|
||||
result.creation = await getUserCreation(emailContact.user.id, clientTimezoneOffset)
|
||||
|
||||
const event = new Event()
|
||||
const eventAdminContributionUpdate = new EventAdminContributionUpdate()
|
||||
eventAdminContributionUpdate.userId = user.id
|
||||
eventAdminContributionUpdate.amount = amount
|
||||
eventAdminContributionUpdate.contributionId = contributionToUpdate.id
|
||||
await eventProtocol.writeEvent(
|
||||
event.setEventAdminContributionUpdate(eventAdminContributionUpdate),
|
||||
)
|
||||
await EVENT_ADMIN_CONTRIBUTION_UPDATE(emailContact.user.id, contributionToUpdate.id, amount)
|
||||
|
||||
return result
|
||||
}
|
||||
@ -521,19 +469,17 @@ export class ContributionResolver {
|
||||
): Promise<boolean> {
|
||||
const contribution = await DbContribution.findOne(id)
|
||||
if (!contribution) {
|
||||
logger.error(`Contribution not found for given id: ${id}`)
|
||||
throw new Error('Contribution not found for given id.')
|
||||
throw new LogError('Contribution not found', id)
|
||||
}
|
||||
if (contribution.confirmedAt) {
|
||||
logger.error('A confirmed contribution can not be deleted')
|
||||
throw new Error('A confirmed contribution can not be deleted')
|
||||
throw new LogError('A confirmed contribution can not be deleted')
|
||||
}
|
||||
const moderator = getUser(context)
|
||||
if (
|
||||
contribution.contributionType === ContributionType.USER &&
|
||||
contribution.userId === moderator.id
|
||||
) {
|
||||
throw new Error('Own contribution can not be deleted as admin')
|
||||
throw new LogError('Own contribution can not be deleted as admin')
|
||||
}
|
||||
const user = await DbUser.findOneOrFail(
|
||||
{ id: contribution.userId },
|
||||
@ -544,15 +490,9 @@ export class ContributionResolver {
|
||||
await contribution.save()
|
||||
const res = await contribution.softRemove()
|
||||
|
||||
const event = new Event()
|
||||
const eventAdminContributionDelete = new EventAdminContributionDelete()
|
||||
eventAdminContributionDelete.userId = contribution.userId
|
||||
eventAdminContributionDelete.amount = contribution.amount
|
||||
eventAdminContributionDelete.contributionId = contribution.id
|
||||
await eventProtocol.writeEvent(
|
||||
event.setEventAdminContributionDelete(eventAdminContributionDelete),
|
||||
)
|
||||
sendContributionDeniedEmail({
|
||||
await EVENT_ADMIN_CONTRIBUTION_DELETE(contribution.userId, contribution.id, contribution.amount)
|
||||
|
||||
sendContributionDeletedEmail({
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
email: user.emailContact.email,
|
||||
@ -577,29 +517,24 @@ export class ContributionResolver {
|
||||
const clientTimezoneOffset = getClientTimezoneOffset(context)
|
||||
const contribution = await DbContribution.findOne(id)
|
||||
if (!contribution) {
|
||||
logger.error(`Contribution not found for given id: ${id}`)
|
||||
throw new Error('Contribution not found to given id.')
|
||||
throw new LogError('Contribution not found', id)
|
||||
}
|
||||
if (contribution.confirmedAt) {
|
||||
logger.error(`Contribution already confirmd: ${id}`)
|
||||
throw new Error('Contribution already confirmd.')
|
||||
throw new LogError('Contribution already confirmed', id)
|
||||
}
|
||||
if (contribution.contributionStatus === 'DENIED') {
|
||||
logger.error(`Contribution already denied: ${id}`)
|
||||
throw new Error('Contribution already denied.')
|
||||
throw new LogError('Contribution already denied', id)
|
||||
}
|
||||
const moderatorUser = getUser(context)
|
||||
if (moderatorUser.id === contribution.userId) {
|
||||
logger.error('Moderator can not confirm own contribution')
|
||||
throw new Error('Moderator can not confirm own contribution')
|
||||
throw new LogError('Moderator can not confirm own contribution')
|
||||
}
|
||||
const user = await DbUser.findOneOrFail(
|
||||
{ id: contribution.userId },
|
||||
{ withDeleted: true, relations: ['emailContact'] },
|
||||
)
|
||||
if (user.deletedAt) {
|
||||
logger.error('This user was deleted. Cannot confirm a contribution.')
|
||||
throw new Error('This user was deleted. Cannot confirm a contribution.')
|
||||
throw new LogError('Can not confirm contribution since the user was deleted')
|
||||
}
|
||||
const creations = await getUserCreation(contribution.userId, clientTimezoneOffset, false)
|
||||
validateContribution(
|
||||
@ -613,16 +548,11 @@ export class ContributionResolver {
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
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 decay: Decay | null = null
|
||||
if (lastTransaction) {
|
||||
@ -668,18 +598,12 @@ export class ContributionResolver {
|
||||
})
|
||||
} catch (e) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
logger.error('Creation was not successful', e)
|
||||
throw new Error('Creation was not successful.')
|
||||
throw new LogError('Creation was not successful', e)
|
||||
} finally {
|
||||
await queryRunner.release()
|
||||
}
|
||||
|
||||
const event = new Event()
|
||||
const eventContributionConfirm = new EventContributionConfirm()
|
||||
eventContributionConfirm.userId = user.id
|
||||
eventContributionConfirm.amount = contribution.amount
|
||||
eventContributionConfirm.contributionId = contribution.id
|
||||
await eventProtocol.writeEvent(event.setEventContributionConfirm(eventContributionConfirm))
|
||||
await EVENT_CONTRIBUTION_CONFIRM(user.id, contribution.id, contribution.amount)
|
||||
} finally {
|
||||
releaseLock()
|
||||
}
|
||||
@ -744,17 +668,16 @@ export class ContributionResolver {
|
||||
deniedBy: IsNull(),
|
||||
})
|
||||
if (!contributionToUpdate) {
|
||||
logger.error(`Contribution not found for given id: ${id}`)
|
||||
throw new Error(`Contribution not found for given id.`)
|
||||
throw new LogError('Contribution not found', id)
|
||||
}
|
||||
if (
|
||||
contributionToUpdate.contributionStatus !== ContributionStatus.IN_PROGRESS &&
|
||||
contributionToUpdate.contributionStatus !== ContributionStatus.PENDING
|
||||
) {
|
||||
logger.error(
|
||||
`Contribution state (${contributionToUpdate.contributionStatus}) is not allowed.`,
|
||||
throw new LogError(
|
||||
'Status of the contribution is not allowed',
|
||||
contributionToUpdate.contributionStatus,
|
||||
)
|
||||
throw new Error(`State of the contribution is not allowed.`)
|
||||
}
|
||||
const moderator = getUser(context)
|
||||
const user = await DbUser.findOne(
|
||||
@ -762,10 +685,7 @@ export class ContributionResolver {
|
||||
{ relations: ['emailContact'] },
|
||||
)
|
||||
if (!user) {
|
||||
logger.error(
|
||||
`Could not find User for the Contribution (userId: ${contributionToUpdate.userId}).`,
|
||||
)
|
||||
throw new Error('Could not find User for the Contribution.')
|
||||
throw new LogError('Could not find User of the Contribution', contributionToUpdate.userId)
|
||||
}
|
||||
|
||||
contributionToUpdate.contributionStatus = ContributionStatus.DENIED
|
||||
@ -773,6 +693,13 @@ export class ContributionResolver {
|
||||
contributionToUpdate.deniedAt = new Date()
|
||||
const res = await contributionToUpdate.save()
|
||||
|
||||
await EVENT_ADMIN_CONTRIBUTION_DENY(
|
||||
contributionToUpdate.userId,
|
||||
moderator.id,
|
||||
contributionToUpdate.id,
|
||||
contributionToUpdate.amount,
|
||||
)
|
||||
|
||||
sendContributionDeniedEmail({
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
|
||||
@ -16,6 +16,7 @@ import {
|
||||
redeemTransactionLink,
|
||||
createContribution,
|
||||
updateContribution,
|
||||
createTransactionLink,
|
||||
} from '@/seeds/graphql/mutations'
|
||||
import { listTransactionLinksAdmin } from '@/seeds/graphql/queries'
|
||||
import { ContributionLink as DbContributionLink } from '@entity/ContributionLink'
|
||||
@ -24,6 +25,7 @@ import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { GraphQLError } from 'graphql'
|
||||
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
|
||||
import { logger } from '@test/testSetup'
|
||||
|
||||
// mock semaphore to allow use fake timers
|
||||
jest.mock('@/util/TRANSACTIONS_LOCK')
|
||||
@ -50,7 +52,75 @@ afterAll(async () => {
|
||||
})
|
||||
|
||||
describe('TransactionLinkResolver', () => {
|
||||
describe('createTransactionLink', () => {
|
||||
beforeAll(async () => {
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
|
||||
it('throws error when amount is zero', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createTransactionLink,
|
||||
variables: {
|
||||
amount: 0,
|
||||
memo: 'Test',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [new GraphQLError('Amount must be a positive number')],
|
||||
})
|
||||
})
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(0))
|
||||
})
|
||||
|
||||
it('throws error when amount is negative', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createTransactionLink,
|
||||
variables: {
|
||||
amount: -10,
|
||||
memo: 'Test',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [new GraphQLError('Amount must be a positive number')],
|
||||
})
|
||||
})
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('Amount must be a positive number', new Decimal(-10))
|
||||
})
|
||||
|
||||
it('throws error when user has not enough GDD', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: createTransactionLink,
|
||||
variables: {
|
||||
amount: 1001,
|
||||
memo: 'Test',
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [new GraphQLError('User has not enough GDD')],
|
||||
})
|
||||
})
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('User has not enough GDD', expect.any(Number))
|
||||
})
|
||||
})
|
||||
|
||||
describe('redeemTransactionLink', () => {
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
resetToken()
|
||||
})
|
||||
|
||||
describe('contributionLink', () => {
|
||||
describe('input not valid', () => {
|
||||
beforeAll(async () => {
|
||||
@ -61,6 +131,7 @@ describe('TransactionLinkResolver', () => {
|
||||
})
|
||||
|
||||
it('throws error when link does not exists', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: redeemTransactionLink,
|
||||
@ -69,16 +140,26 @@ describe('TransactionLinkResolver', () => {
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
'Creation from contribution link was not successful. Error: No contribution link found to given code: CL-123456',
|
||||
),
|
||||
],
|
||||
errors: [new GraphQLError('Creation from contribution link was not successful')],
|
||||
})
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'No contribution link found to given code',
|
||||
'CL-123456',
|
||||
)
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'Creation from contribution link was not successful',
|
||||
new Error('No contribution link found to given code'),
|
||||
)
|
||||
})
|
||||
|
||||
const now = new Date()
|
||||
const validFrom = new Date(now.getFullYear() + 1, 0, 1)
|
||||
|
||||
it('throws error when link is not valid yet', async () => {
|
||||
const now = new Date()
|
||||
jest.clearAllMocks()
|
||||
const {
|
||||
data: { createContributionLink: contributionLink },
|
||||
} = await mutate({
|
||||
@ -88,7 +169,7 @@ describe('TransactionLinkResolver', () => {
|
||||
name: 'Daily Contribution Link',
|
||||
memo: 'Thank you for contribute daily to the community',
|
||||
cycle: 'DAILY',
|
||||
validFrom: new Date(now.getFullYear() + 1, 0, 1).toISOString(),
|
||||
validFrom: validFrom.toISOString(),
|
||||
validTo: new Date(now.getFullYear() + 1, 11, 31, 23, 59, 59, 999).toISOString(),
|
||||
maxAmountPerMonth: new Decimal(200),
|
||||
maxPerCycle: 1,
|
||||
@ -102,16 +183,21 @@ describe('TransactionLinkResolver', () => {
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
'Creation from contribution link was not successful. Error: Contribution link not valid yet',
|
||||
),
|
||||
],
|
||||
errors: [new GraphQLError('Creation from contribution link was not successful')],
|
||||
})
|
||||
await resetEntity(DbContributionLink)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('Contribution link is not valid yet', validFrom)
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'Creation from contribution link was not successful',
|
||||
new Error('Contribution link is not valid yet'),
|
||||
)
|
||||
})
|
||||
|
||||
it('throws error when contributionLink cycle is invalid', async () => {
|
||||
jest.clearAllMocks()
|
||||
const now = new Date()
|
||||
const {
|
||||
data: { createContributionLink: contributionLink },
|
||||
@ -136,17 +222,22 @@ describe('TransactionLinkResolver', () => {
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
'Creation from contribution link was not successful. Error: Contribution link has unknown cycle',
|
||||
),
|
||||
],
|
||||
errors: [new GraphQLError('Creation from contribution link was not successful')],
|
||||
})
|
||||
await resetEntity(DbContributionLink)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('Contribution link has unknown cycle', 'INVALID')
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'Creation from contribution link was not successful',
|
||||
new Error('Contribution link has unknown cycle'),
|
||||
)
|
||||
})
|
||||
|
||||
const validTo = new Date(now.getFullYear() - 1, 11, 31, 23, 59, 59, 0)
|
||||
it('throws error when link is no longer valid', async () => {
|
||||
const now = new Date()
|
||||
jest.clearAllMocks()
|
||||
const {
|
||||
data: { createContributionLink: contributionLink },
|
||||
} = await mutate({
|
||||
@ -157,7 +248,7 @@ describe('TransactionLinkResolver', () => {
|
||||
memo: 'Thank you for contribute daily to the community',
|
||||
cycle: 'DAILY',
|
||||
validFrom: new Date(now.getFullYear() - 1, 0, 1).toISOString(),
|
||||
validTo: new Date(now.getFullYear() - 1, 11, 31, 23, 59, 59, 999).toISOString(),
|
||||
validTo: validTo.toISOString(),
|
||||
maxAmountPerMonth: new Decimal(200),
|
||||
maxPerCycle: 1,
|
||||
},
|
||||
@ -170,14 +261,18 @@ describe('TransactionLinkResolver', () => {
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
'Creation from contribution link was not successful. Error: Contribution link is no longer valid',
|
||||
),
|
||||
],
|
||||
errors: [new GraphQLError('Creation from contribution link was not successful')],
|
||||
})
|
||||
await resetEntity(DbContributionLink)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('Contribution link is no longer valid', validTo)
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'Creation from contribution link was not successful',
|
||||
new Error('Contribution link is no longer valid'),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// TODO: have this test separated into a transactionLink and a contributionLink part
|
||||
@ -250,6 +345,7 @@ describe('TransactionLinkResolver', () => {
|
||||
})
|
||||
|
||||
it('does not allow the user to redeem the contribution link', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: redeemTransactionLink,
|
||||
@ -258,13 +354,18 @@ describe('TransactionLinkResolver', () => {
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
'Creation from contribution link was not successful. Error: The amount (5 GDD) to be created exceeds the amount (0 GDD) still available for this month.',
|
||||
),
|
||||
],
|
||||
errors: [new GraphQLError('Creation from contribution link was not successful')],
|
||||
})
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'Creation from contribution link was not successful',
|
||||
new Error(
|
||||
'The amount (5 GDD) to be created exceeds the amount (0 GDD) still available for this month.',
|
||||
),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('user has no pending contributions that would not allow to redeem the link', () => {
|
||||
@ -301,6 +402,7 @@ describe('TransactionLinkResolver', () => {
|
||||
})
|
||||
|
||||
it('does not allow the user to redeem the contribution link a second time on the same day', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: redeemTransactionLink,
|
||||
@ -309,14 +411,17 @@ describe('TransactionLinkResolver', () => {
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
'Creation from contribution link was not successful. Error: Contribution link already redeemed today',
|
||||
),
|
||||
],
|
||||
errors: [new GraphQLError('Creation from contribution link was not successful')],
|
||||
})
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'Creation from contribution link was not successful',
|
||||
new Error('Contribution link already redeemed today'),
|
||||
)
|
||||
})
|
||||
|
||||
describe('after one day', () => {
|
||||
beforeAll(async () => {
|
||||
jest.useFakeTimers()
|
||||
@ -349,6 +454,7 @@ describe('TransactionLinkResolver', () => {
|
||||
})
|
||||
|
||||
it('does not allow the user to redeem the contribution link a second time on the same day', async () => {
|
||||
jest.clearAllMocks()
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: redeemTransactionLink,
|
||||
@ -357,33 +463,65 @@ describe('TransactionLinkResolver', () => {
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
errors: [
|
||||
new GraphQLError(
|
||||
'Creation from contribution link was not successful. Error: Contribution link already redeemed today',
|
||||
),
|
||||
],
|
||||
errors: [new GraphQLError('Creation from contribution link was not successful')],
|
||||
})
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
'Creation from contribution link was not successful',
|
||||
new Error('Contribution link already redeemed today'),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('transaction links list', () => {
|
||||
const variables = {
|
||||
userId: 1, // dummy, may be replaced
|
||||
filters: null,
|
||||
currentPage: 1,
|
||||
pageSize: 5,
|
||||
}
|
||||
describe('listTransactionLinksAdmin', () => {
|
||||
const variables = {
|
||||
userId: 1, // dummy, may be replaced
|
||||
filters: null,
|
||||
currentPage: 1,
|
||||
pageSize: 5,
|
||||
}
|
||||
|
||||
// TODO: there is a test not cleaning up after itself! Fix it!
|
||||
beforeAll(async () => {
|
||||
await cleanDB()
|
||||
resetToken()
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
resetToken()
|
||||
})
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
it('returns an error', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: listTransactionLinksAdmin,
|
||||
variables,
|
||||
}),
|
||||
).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()
|
||||
})
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
it('returns an error', async () => {
|
||||
await expect(
|
||||
query({
|
||||
@ -398,22 +536,40 @@ describe('TransactionLinkResolver', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
describe('without admin rights', () => {
|
||||
beforeAll(async () => {
|
||||
user = await userFactory(testEnv, bibiBloxberg)
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
describe('with admin rights', () => {
|
||||
beforeAll(async () => {
|
||||
// admin 'peter@lustig.de' has to exists for 'creationFactory'
|
||||
await userFactory(testEnv, peterLustig)
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
resetToken()
|
||||
})
|
||||
user = await userFactory(testEnv, bibiBloxberg)
|
||||
variables.userId = user.id
|
||||
variables.pageSize = 25
|
||||
// bibi needs GDDs
|
||||
const bibisCreation = creations.find((creation) => creation.email === 'bibi@bloxberg.de')
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
await creationFactory(testEnv, bibisCreation!)
|
||||
// bibis transaktion links
|
||||
const bibisTransaktionLinks = transactionLinks.filter(
|
||||
(transactionLink) => transactionLink.email === 'bibi@bloxberg.de',
|
||||
)
|
||||
for (let i = 0; i < bibisTransaktionLinks.length; i++) {
|
||||
await transactionLinkFactory(testEnv, bibisTransaktionLinks[i])
|
||||
}
|
||||
|
||||
it('returns an error', async () => {
|
||||
// admin: only now log in
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
resetToken()
|
||||
})
|
||||
|
||||
describe('without any filters', () => {
|
||||
it('finds 6 open transaction links and no deleted or redeemed', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: listTransactionLinksAdmin,
|
||||
@ -421,219 +577,169 @@ describe('TransactionLinkResolver', () => {
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('401 Unauthorized')],
|
||||
data: {
|
||||
listTransactionLinksAdmin: {
|
||||
linkCount: 6,
|
||||
linkList: expect.not.arrayContaining([
|
||||
expect.objectContaining({
|
||||
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
|
||||
createdAt: expect.any(String),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
memo: 'Da habe ich mich wohl etwas übernommen.',
|
||||
deletedAt: expect.any(String),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with admin rights', () => {
|
||||
beforeAll(async () => {
|
||||
// admin 'peter@lustig.de' has to exists for 'creationFactory'
|
||||
await userFactory(testEnv, peterLustig)
|
||||
|
||||
user = await userFactory(testEnv, bibiBloxberg)
|
||||
variables.userId = user.id
|
||||
variables.pageSize = 25
|
||||
// bibi needs GDDs
|
||||
const bibisCreation = creations.find(
|
||||
(creation) => creation.email === 'bibi@bloxberg.de',
|
||||
describe('all filters are null', () => {
|
||||
it('finds 6 open transaction links and no deleted or redeemed', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: listTransactionLinksAdmin,
|
||||
variables: {
|
||||
...variables,
|
||||
filters: {
|
||||
withDeleted: null,
|
||||
withExpired: null,
|
||||
withRedeemed: null,
|
||||
},
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
listTransactionLinksAdmin: {
|
||||
linkCount: 6,
|
||||
linkList: expect.not.arrayContaining([
|
||||
expect.objectContaining({
|
||||
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
|
||||
createdAt: expect.any(String),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
memo: 'Da habe ich mich wohl etwas übernommen.',
|
||||
deletedAt: expect.any(String),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
await creationFactory(testEnv, bibisCreation!)
|
||||
// bibis transaktion links
|
||||
const bibisTransaktionLinks = transactionLinks.filter(
|
||||
(transactionLink) => transactionLink.email === 'bibi@bloxberg.de',
|
||||
})
|
||||
})
|
||||
|
||||
describe('filter with deleted', () => {
|
||||
it('finds 6 open transaction links, 1 deleted, and no redeemed', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: listTransactionLinksAdmin,
|
||||
variables: {
|
||||
...variables,
|
||||
filters: {
|
||||
withDeleted: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
listTransactionLinksAdmin: {
|
||||
linkCount: 7,
|
||||
linkList: expect.arrayContaining([
|
||||
expect.not.objectContaining({
|
||||
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
|
||||
createdAt: expect.any(String),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
memo: 'Da habe ich mich wohl etwas übernommen.',
|
||||
deletedAt: expect.any(String),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
for (let i = 0; i < bibisTransaktionLinks.length; i++) {
|
||||
await transactionLinkFactory(testEnv, bibisTransaktionLinks[i])
|
||||
}
|
||||
|
||||
// admin: only now log in
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
resetToken()
|
||||
describe('filter by expired', () => {
|
||||
it('finds 5 open transaction links, 1 expired, and no redeemed', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: listTransactionLinksAdmin,
|
||||
variables: {
|
||||
...variables,
|
||||
filters: {
|
||||
withExpired: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
listTransactionLinksAdmin: {
|
||||
linkCount: 7,
|
||||
linkList: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
|
||||
createdAt: expect.any(String),
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
memo: 'Da habe ich mich wohl etwas übernommen.',
|
||||
deletedAt: expect.any(String),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('without any filters', () => {
|
||||
it('finds 6 open transaction links and no deleted or redeemed', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: listTransactionLinksAdmin,
|
||||
variables,
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
listTransactionLinksAdmin: {
|
||||
linkCount: 6,
|
||||
linkList: expect.not.arrayContaining([
|
||||
expect.objectContaining({
|
||||
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
|
||||
createdAt: expect.any(String),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
memo: 'Da habe ich mich wohl etwas übernommen.',
|
||||
deletedAt: expect.any(String),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
// TODO: works not as expected, because 'redeemedAt' and 'redeemedBy' have to be added to the transaktion link factory
|
||||
describe.skip('filter by redeemed', () => {
|
||||
it('finds 6 open transaction links, 1 deleted, and no redeemed', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: listTransactionLinksAdmin,
|
||||
variables: {
|
||||
...variables,
|
||||
filters: {
|
||||
withDeleted: null,
|
||||
withExpired: null,
|
||||
withRedeemed: true,
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('all filters are null', () => {
|
||||
it('finds 6 open transaction links and no deleted or redeemed', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: listTransactionLinksAdmin,
|
||||
variables: {
|
||||
...variables,
|
||||
filters: {
|
||||
withDeleted: null,
|
||||
withExpired: null,
|
||||
withRedeemed: null,
|
||||
},
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
listTransactionLinksAdmin: {
|
||||
linkCount: 6,
|
||||
linkList: expect.arrayContaining([
|
||||
expect.not.objectContaining({
|
||||
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
|
||||
createdAt: expect.any(String),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
memo: 'Yeah, eingelöst!',
|
||||
redeemedAt: expect.any(String),
|
||||
redeemedBy: expect.any(Number),
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
memo: 'Da habe ich mich wohl etwas übernommen.',
|
||||
deletedAt: expect.any(String),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
listTransactionLinksAdmin: {
|
||||
linkCount: 6,
|
||||
linkList: expect.not.arrayContaining([
|
||||
expect.objectContaining({
|
||||
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
|
||||
createdAt: expect.any(String),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
memo: 'Da habe ich mich wohl etwas übernommen.',
|
||||
deletedAt: expect.any(String),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('filter with deleted', () => {
|
||||
it('finds 6 open transaction links, 1 deleted, and no redeemed', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: listTransactionLinksAdmin,
|
||||
variables: {
|
||||
...variables,
|
||||
filters: {
|
||||
withDeleted: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
listTransactionLinksAdmin: {
|
||||
linkCount: 7,
|
||||
linkList: expect.arrayContaining([
|
||||
expect.not.objectContaining({
|
||||
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
|
||||
createdAt: expect.any(String),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
memo: 'Da habe ich mich wohl etwas übernommen.',
|
||||
deletedAt: expect.any(String),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('filter by expired', () => {
|
||||
it('finds 5 open transaction links, 1 expired, and no redeemed', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: listTransactionLinksAdmin,
|
||||
variables: {
|
||||
...variables,
|
||||
filters: {
|
||||
withExpired: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
listTransactionLinksAdmin: {
|
||||
linkCount: 7,
|
||||
linkList: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
|
||||
createdAt: expect.any(String),
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
memo: 'Da habe ich mich wohl etwas übernommen.',
|
||||
deletedAt: expect.any(String),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// TODO: works not as expected, because 'redeemedAt' and 'redeemedBy' have to be added to the transaktion link factory
|
||||
describe.skip('filter by redeemed', () => {
|
||||
it('finds 6 open transaction links, 1 deleted, and no redeemed', async () => {
|
||||
await expect(
|
||||
query({
|
||||
query: listTransactionLinksAdmin,
|
||||
variables: {
|
||||
...variables,
|
||||
filters: {
|
||||
withDeleted: null,
|
||||
withExpired: null,
|
||||
withRedeemed: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
listTransactionLinksAdmin: {
|
||||
linkCount: 6,
|
||||
linkList: expect.arrayContaining([
|
||||
expect.not.objectContaining({
|
||||
memo: 'Leider wollte niemand meine Gradidos zum Neujahr haben :(',
|
||||
createdAt: expect.any(String),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
memo: 'Yeah, eingelöst!',
|
||||
redeemedAt: expect.any(String),
|
||||
redeemedBy: expect.any(Number),
|
||||
}),
|
||||
expect.not.objectContaining({
|
||||
memo: 'Da habe ich mich wohl etwas übernommen.',
|
||||
deletedAt: expect.any(String),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -32,6 +32,9 @@ import { getUserCreation, validateContribution } from './util/creations'
|
||||
import { executeTransaction } from './TransactionResolver'
|
||||
import QueryLinkResult from '@union/QueryLinkResult'
|
||||
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
|
||||
import LogError from '@/server/LogError'
|
||||
|
||||
import { getLastTransaction } from './util/getLastTransaction'
|
||||
|
||||
// TODO: do not export, test it inside the resolver
|
||||
export const transactionLinkCode = (date: Date): string => {
|
||||
@ -63,12 +66,16 @@ export class TransactionLinkResolver {
|
||||
const createdDate = new Date()
|
||||
const validUntil = transactionLinkExpireDate(createdDate)
|
||||
|
||||
if (amount.lessThanOrEqualTo(0)) {
|
||||
throw new LogError('Amount must be a positive number', amount)
|
||||
}
|
||||
|
||||
const holdAvailableAmount = amount.minus(calculateDecay(amount, createdDate, validUntil).decay)
|
||||
|
||||
// validate amount
|
||||
const sendBalance = await calculateBalance(user.id, holdAvailableAmount.mul(-1), createdDate)
|
||||
if (!sendBalance) {
|
||||
throw new Error("user hasn't enough GDD or amount is < 0")
|
||||
throw new LogError('User has not enough GDD', user.id)
|
||||
}
|
||||
|
||||
const transactionLink = DbTransactionLink.create()
|
||||
@ -184,24 +191,15 @@ export class TransactionLinkResolver {
|
||||
.where('contributionLink.code = :code', { code: code.replace('CL-', '') })
|
||||
.getOne()
|
||||
if (!contributionLink) {
|
||||
logger.error('no contribution link found to given code:', code)
|
||||
throw new Error(`No contribution link found to given code: ${code}`)
|
||||
throw new LogError('No contribution link found to given code', code)
|
||||
}
|
||||
logger.info('...contribution link found with id', contributionLink.id)
|
||||
if (new Date(contributionLink.validFrom).getTime() > now.getTime()) {
|
||||
logger.error(
|
||||
'contribution link is not valid yet. Valid from: ',
|
||||
contributionLink.validFrom,
|
||||
)
|
||||
throw new Error('Contribution link not valid yet')
|
||||
throw new LogError('Contribution link is not valid yet', contributionLink.validFrom)
|
||||
}
|
||||
if (contributionLink.validTo) {
|
||||
if (new Date(contributionLink.validTo).setHours(23, 59, 59) < now.getTime()) {
|
||||
logger.error(
|
||||
'contribution link is no longer valid. Valid to: ',
|
||||
contributionLink.validTo,
|
||||
)
|
||||
throw new Error('Contribution link is no longer valid')
|
||||
throw new LogError('Contribution link is no longer valid', contributionLink.validTo)
|
||||
}
|
||||
}
|
||||
let alreadyRedeemed: DbContribution | undefined
|
||||
@ -217,11 +215,7 @@ export class TransactionLinkResolver {
|
||||
})
|
||||
.getOne()
|
||||
if (alreadyRedeemed) {
|
||||
logger.error(
|
||||
'contribution link with rule ONCE already redeemed by user with id',
|
||||
user.id,
|
||||
)
|
||||
throw new Error('Contribution link already redeemed')
|
||||
throw new LogError('Contribution link already redeemed', user.id)
|
||||
}
|
||||
break
|
||||
}
|
||||
@ -246,17 +240,12 @@ export class TransactionLinkResolver {
|
||||
)
|
||||
.getOne()
|
||||
if (alreadyRedeemed) {
|
||||
logger.error(
|
||||
'contribution link with rule DAILY already redeemed by user with id',
|
||||
user.id,
|
||||
)
|
||||
throw new Error('Contribution link already redeemed today')
|
||||
throw new LogError('Contribution link already redeemed today', user.id)
|
||||
}
|
||||
break
|
||||
}
|
||||
default: {
|
||||
logger.error('contribution link has unknown cycle', contributionLink.cycle)
|
||||
throw new Error('Contribution link has unknown cycle')
|
||||
throw new LogError('Contribution link has unknown cycle', contributionLink.cycle)
|
||||
}
|
||||
}
|
||||
|
||||
@ -275,13 +264,7 @@ export class TransactionLinkResolver {
|
||||
|
||||
await queryRunner.manager.insert(DbContribution, contribution)
|
||||
|
||||
const lastTransaction = await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.select('transaction')
|
||||
.from(DbTransaction, 'transaction')
|
||||
.where('transaction.userId = :id', { id: user.id })
|
||||
.orderBy('transaction.id', 'DESC')
|
||||
.getOne()
|
||||
const lastTransaction = await getLastTransaction(user.id)
|
||||
let newBalance = new Decimal(0)
|
||||
|
||||
let decay: Decay | null = null
|
||||
@ -312,8 +295,7 @@ export class TransactionLinkResolver {
|
||||
logger.info('creation from contribution link commited successfuly.')
|
||||
} catch (e) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
logger.error(`Creation from contribution link was not successful: ${e}`)
|
||||
throw new Error(`Creation from contribution link was not successful. ${e}`)
|
||||
throw new LogError('Creation from contribution link was not successful', e)
|
||||
} finally {
|
||||
await queryRunner.release()
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { EventProtocolType } from '@/event/EventProtocolType'
|
||||
import { userFactory } from '@/seeds/factory/user'
|
||||
import {
|
||||
@ -118,10 +119,8 @@ describe('send coins', () => {
|
||||
|
||||
it('logs the error thrown', async () => {
|
||||
// find peter to check the log
|
||||
const user = await findUserByEmail(peterData.email)
|
||||
expect(logger.error).toBeCalledWith(
|
||||
`The recipient account was deleted: recipientUser=${user}`,
|
||||
)
|
||||
const user = await findUserByEmail('stephen@hawking.uk')
|
||||
expect(logger.error).toBeCalledWith('The recipient account was deleted', user)
|
||||
})
|
||||
})
|
||||
|
||||
@ -151,10 +150,8 @@ describe('send coins', () => {
|
||||
|
||||
it('logs the error thrown', async () => {
|
||||
// find peter to check the log
|
||||
const user = await findUserByEmail(peterData.email)
|
||||
expect(logger.error).toBeCalledWith(
|
||||
`The recipient account is not activated: recipientUser=${user}`,
|
||||
)
|
||||
const user = await findUserByEmail('garrick@ollivander.com')
|
||||
expect(logger.error).toBeCalledWith('The recipient account is not activated', user)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -181,37 +178,13 @@ describe('send coins', () => {
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('Sender and Recipient are the same.')],
|
||||
errors: [new GraphQLError('Sender and Recipient are the same')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('Sender and Recipient are the same.')
|
||||
})
|
||||
})
|
||||
|
||||
describe('memo text is too long', () => {
|
||||
it('throws an error', async () => {
|
||||
jest.clearAllMocks()
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
amount: 100,
|
||||
memo: 'test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test t',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('memo text is too long (255 characters maximum)')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('memo text is too long: memo.length=256 > 255')
|
||||
expect(logger.error).toBeCalledWith('Sender and Recipient are the same', expect.any(Number))
|
||||
})
|
||||
})
|
||||
|
||||
@ -229,13 +202,37 @@ describe('send coins', () => {
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('memo text is too short (5 characters minimum)')],
|
||||
errors: [new GraphQLError('Memo text is too short')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('memo text is too short: memo.length=4 < 5')
|
||||
expect(logger.error).toBeCalledWith('Memo text is too short', 4)
|
||||
})
|
||||
})
|
||||
|
||||
describe('memo text is too long', () => {
|
||||
it('throws an error', async () => {
|
||||
jest.clearAllMocks()
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
amount: 100,
|
||||
memo: 'test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test test t',
|
||||
},
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError('Memo text is too long')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith('Memo text is too long', 256)
|
||||
})
|
||||
})
|
||||
|
||||
@ -253,15 +250,13 @@ describe('send coins', () => {
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError(`user hasn't enough GDD or amount is < 0`)],
|
||||
errors: [new GraphQLError('User has not enough GDD or amount is < 0')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(
|
||||
`user hasn't enough GDD or amount is < 0 : balance=null`,
|
||||
)
|
||||
expect(logger.error).toBeCalledWith('User has not enough GDD or amount is < 0', null)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -293,6 +288,7 @@ describe('send coins', () => {
|
||||
|
||||
describe('trying to send negative amount', () => {
|
||||
it('throws an error', async () => {
|
||||
jest.clearAllMocks()
|
||||
expect(
|
||||
await mutate({
|
||||
mutation: sendCoins,
|
||||
@ -304,13 +300,13 @@ describe('send coins', () => {
|
||||
}),
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
errors: [new GraphQLError(`Amount to send must be positive`)],
|
||||
errors: [new GraphQLError('Amount to send must be positive')],
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it('logs the error thrown', () => {
|
||||
expect(logger.error).toBeCalledWith(`Amount to send must be positive`)
|
||||
expect(logger.error).toBeCalledWith('Amount to send must be positive', new Decimal(-50))
|
||||
})
|
||||
})
|
||||
|
||||
@ -334,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)
|
||||
const transaction = await Transaction.find({
|
||||
userId: user[1].id,
|
||||
@ -351,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)
|
||||
const transaction = await Transaction.find({
|
||||
userId: user[0].id,
|
||||
|
||||
@ -29,14 +29,16 @@ import {
|
||||
sendTransactionLinkRedeemedEmail,
|
||||
sendTransactionReceivedEmail,
|
||||
} from '@/emails/sendEmailVariants'
|
||||
import { Event, EventTransactionReceive, EventTransactionSend } from '@/event/Event'
|
||||
import { eventProtocol } from '@/event/EventProtocolEmitter'
|
||||
import { EVENT_TRANSACTION_RECEIVE, EVENT_TRANSACTION_SEND } from '@/event/Event'
|
||||
|
||||
import { BalanceResolver } from './BalanceResolver'
|
||||
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
||||
import { findUserByEmail } from './UserResolver'
|
||||
|
||||
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
|
||||
import LogError from '@/server/LogError'
|
||||
|
||||
import { getLastTransaction } from './util/getLastTransaction'
|
||||
|
||||
export const executeTransaction = async (
|
||||
amount: Decimal,
|
||||
@ -53,18 +55,15 @@ export const executeTransaction = async (
|
||||
)
|
||||
|
||||
if (sender.id === recipient.id) {
|
||||
logger.error(`Sender and Recipient are the same.`)
|
||||
throw new Error('Sender and Recipient are the same.')
|
||||
}
|
||||
|
||||
if (memo.length > MEMO_MAX_CHARS) {
|
||||
logger.error(`memo text is too long: memo.length=${memo.length} > ${MEMO_MAX_CHARS}`)
|
||||
throw new Error(`memo text is too long (${MEMO_MAX_CHARS} characters maximum)`)
|
||||
throw new LogError('Sender and Recipient are the same', sender.id)
|
||||
}
|
||||
|
||||
if (memo.length < MEMO_MIN_CHARS) {
|
||||
logger.error(`memo text is too short: memo.length=${memo.length} < ${MEMO_MIN_CHARS}`)
|
||||
throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`)
|
||||
throw new LogError('Memo text is too short', memo.length)
|
||||
}
|
||||
|
||||
if (memo.length > MEMO_MAX_CHARS) {
|
||||
throw new LogError('Memo text is too long', memo.length)
|
||||
}
|
||||
|
||||
// validate amount
|
||||
@ -77,8 +76,7 @@ export const executeTransaction = async (
|
||||
)
|
||||
logger.debug(`calculated Balance=${sendBalance}`)
|
||||
if (!sendBalance) {
|
||||
logger.error(`user hasn't enough GDD or amount is < 0 : balance=${sendBalance}`)
|
||||
throw new Error("user hasn't enough GDD or amount is < 0")
|
||||
throw new LogError('User has not enough GDD or amount is < 0', sendBalance)
|
||||
}
|
||||
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
@ -139,25 +137,22 @@ export const executeTransaction = async (
|
||||
await queryRunner.commitTransaction()
|
||||
logger.info(`commit Transaction successful...`)
|
||||
|
||||
const eventTransactionSend = new EventTransactionSend()
|
||||
eventTransactionSend.userId = transactionSend.userId
|
||||
eventTransactionSend.xUserId = transactionSend.linkedUserId
|
||||
eventTransactionSend.transactionId = transactionSend.id
|
||||
eventTransactionSend.amount = transactionSend.amount.mul(-1)
|
||||
await eventProtocol.writeEvent(new Event().setEventTransactionSend(eventTransactionSend))
|
||||
await EVENT_TRANSACTION_SEND(
|
||||
transactionSend.userId,
|
||||
transactionSend.linkedUserId,
|
||||
transactionSend.id,
|
||||
transactionSend.amount.mul(-1),
|
||||
)
|
||||
|
||||
const eventTransactionReceive = new EventTransactionReceive()
|
||||
eventTransactionReceive.userId = transactionReceive.userId
|
||||
eventTransactionReceive.xUserId = transactionReceive.linkedUserId
|
||||
eventTransactionReceive.transactionId = transactionReceive.id
|
||||
eventTransactionReceive.amount = transactionReceive.amount
|
||||
await eventProtocol.writeEvent(
|
||||
new Event().setEventTransactionReceive(eventTransactionReceive),
|
||||
await EVENT_TRANSACTION_RECEIVE(
|
||||
transactionReceive.userId,
|
||||
transactionReceive.linkedUserId,
|
||||
transactionReceive.id,
|
||||
transactionReceive.amount,
|
||||
)
|
||||
} catch (e) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
logger.error(`Transaction was not successful: ${e}`)
|
||||
throw new Error(`Transaction was not successful: ${e}`)
|
||||
throw new LogError('Transaction was not successful', e)
|
||||
} finally {
|
||||
await queryRunner.release()
|
||||
}
|
||||
@ -208,10 +203,7 @@ export class TransactionResolver {
|
||||
logger.info(`transactionList(user=${user.firstName}.${user.lastName}, ${user.emailId})`)
|
||||
|
||||
// find current balance
|
||||
const lastTransaction = await dbTransaction.findOne(
|
||||
{ userId: user.id },
|
||||
{ order: { id: 'DESC' }, relations: ['contribution'] },
|
||||
)
|
||||
const lastTransaction = await getLastTransaction(user.id, ['contribution'])
|
||||
logger.debug(`lastTransaction=${lastTransaction}`)
|
||||
|
||||
const balanceResolver = new BalanceResolver()
|
||||
@ -319,8 +311,7 @@ export class TransactionResolver {
|
||||
): Promise<boolean> {
|
||||
logger.info(`sendCoins(email=${email}, amount=${amount}, memo=${memo})`)
|
||||
if (amount.lte(0)) {
|
||||
logger.error(`Amount to send must be positive`)
|
||||
throw new Error('Amount to send must be positive')
|
||||
throw new LogError('Amount to send must be positive', amount)
|
||||
}
|
||||
|
||||
// TODO this is subject to replay attacks
|
||||
@ -329,13 +320,11 @@ export class TransactionResolver {
|
||||
// validate recipient user
|
||||
const recipientUser = await findUserByEmail(email)
|
||||
if (recipientUser.deletedAt) {
|
||||
logger.error(`The recipient account was deleted: recipientUser=${recipientUser}`)
|
||||
throw new Error('The recipient account was deleted')
|
||||
throw new LogError('The recipient account was deleted', recipientUser)
|
||||
}
|
||||
const emailContact = recipientUser.emailContact
|
||||
if (!emailContact.emailChecked) {
|
||||
logger.error(`The recipient account is not activated: recipientUser=${recipientUser}`)
|
||||
throw new Error('The recipient account is not activated')
|
||||
throw new LogError('The recipient account is not activated', recipientUser)
|
||||
}
|
||||
|
||||
await executeTransaction(amount, memo, senderUser, recipientUser)
|
||||
|
||||
@ -19,6 +19,7 @@ import {
|
||||
setUserRole,
|
||||
deleteUser,
|
||||
unDeleteUser,
|
||||
sendActivationEmail,
|
||||
} from '@/seeds/graphql/mutations'
|
||||
import { verifyLogin, queryOptIn, searchAdminUsers, searchUsers } from '@/seeds/graphql/queries'
|
||||
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', () => {
|
||||
@ -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.objectContaining({
|
||||
type: EventProtocolType.SEND_CONFIRMATION_EMAIL,
|
||||
@ -206,7 +220,7 @@ describe('UserResolver', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('email already exists', () => {
|
||||
describe('user already exists', () => {
|
||||
let mutation: User
|
||||
beforeAll(async () => {
|
||||
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', () => {
|
||||
@ -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.objectContaining({
|
||||
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.objectContaining({
|
||||
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(
|
||||
expect.objectContaining({
|
||||
type: EventProtocolType.REDEEM_REGISTER,
|
||||
@ -647,6 +674,19 @@ describe('UserResolver', () => {
|
||||
it('sets the token in the header', () => {
|
||||
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', () => {
|
||||
@ -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.objectContaining({
|
||||
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('unauthenticated', () => {
|
||||
it('returns an error', async () => {
|
||||
|
||||
@ -48,15 +48,14 @@ import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddle
|
||||
import { klicktippSignIn } from '@/apis/KlicktippController'
|
||||
import { RIGHTS } from '@/auth/RIGHTS'
|
||||
import { hasElopageBuys } from '@/util/hasElopageBuys'
|
||||
import { eventProtocol } from '@/event/EventProtocolEmitter'
|
||||
import {
|
||||
Event,
|
||||
EventLogin,
|
||||
EventRedeemRegister,
|
||||
EventRegister,
|
||||
EventSendAccountMultiRegistrationEmail,
|
||||
EventSendConfirmationEmail,
|
||||
EventActivateAccount,
|
||||
EVENT_LOGIN,
|
||||
EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL,
|
||||
EVENT_SEND_CONFIRMATION_EMAIL,
|
||||
EVENT_REGISTER,
|
||||
EVENT_ACTIVATE_ACCOUNT,
|
||||
EVENT_ADMIN_SEND_CONFIRMATION_EMAIL,
|
||||
} from '@/event/Event'
|
||||
import { getUserCreations } from './util/creations'
|
||||
import { isValidPassword } from '@/password/EncryptorUtils'
|
||||
@ -64,6 +63,7 @@ import { FULL_CREATION_AVAILABLE } from './const/const'
|
||||
import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptor'
|
||||
import { PasswordEncryptionType } from '../enum/PasswordEncryptionType'
|
||||
import LogError from '@/server/LogError'
|
||||
import { EventProtocolType } from '@/event/EventProtocolType'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const sodium = require('sodium-native')
|
||||
@ -177,9 +177,8 @@ export class UserResolver {
|
||||
key: 'token',
|
||||
value: encode(dbUser.gradidoID),
|
||||
})
|
||||
const ev = new EventLogin()
|
||||
ev.userId = user.id
|
||||
eventProtocol.writeEvent(new Event().setEventLogin(ev))
|
||||
|
||||
await EVENT_LOGIN(user.id)
|
||||
logger.info(`successful Login: ${JSON.stringify(user, null, 2)}`)
|
||||
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?
|
||||
// default int publisher_id = 0;
|
||||
const event = new Event()
|
||||
|
||||
// Validate Language (no throw)
|
||||
if (!language || !isLanguage(language)) {
|
||||
@ -249,11 +247,9 @@ export class UserResolver {
|
||||
email,
|
||||
language: foundUser.language, // use language of the emails owner for sending
|
||||
})
|
||||
const eventSendAccountMultiRegistrationEmail = new EventSendAccountMultiRegistrationEmail()
|
||||
eventSendAccountMultiRegistrationEmail.userId = foundUser.id
|
||||
eventProtocol.writeEvent(
|
||||
event.setEventSendConfirmationEmail(eventSendAccountMultiRegistrationEmail),
|
||||
)
|
||||
|
||||
await EVENT_SEND_ACCOUNT_MULTIREGISTRATION_EMAIL(foundUser.id)
|
||||
|
||||
logger.info(
|
||||
`sendAccountMultiRegistrationEmail by ${firstName} ${lastName} to ${foundUser.firstName} ${foundUser.lastName} <${email}>`,
|
||||
)
|
||||
@ -270,10 +266,7 @@ export class UserResolver {
|
||||
|
||||
const gradidoID = await newGradidoID()
|
||||
|
||||
const eventRegister = new EventRegister()
|
||||
const eventRedeemRegister = new EventRedeemRegister()
|
||||
const eventSendConfirmEmail = new EventSendConfirmationEmail()
|
||||
|
||||
const eventRegisterRedeem = Event(EventProtocolType.REDEEM_REGISTER, 0)
|
||||
let dbUser = new DbUser()
|
||||
dbUser.gradidoID = gradidoID
|
||||
dbUser.firstName = firstName
|
||||
@ -290,14 +283,14 @@ export class UserResolver {
|
||||
logger.info('redeemCode found contributionLink=' + contributionLink)
|
||||
if (contributionLink) {
|
||||
dbUser.contributionLinkId = contributionLink.id
|
||||
eventRedeemRegister.contributionId = contributionLink.id
|
||||
eventRegisterRedeem.contributionId = contributionLink.id
|
||||
}
|
||||
} else {
|
||||
const transactionLink = await DbTransactionLink.findOne({ code: redeemCode })
|
||||
logger.info('redeemCode found transactionLink=' + transactionLink)
|
||||
if (transactionLink) {
|
||||
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),
|
||||
})
|
||||
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) {
|
||||
logger.debug(`Account confirmation link: ${activationLink}`)
|
||||
@ -353,11 +346,10 @@ export class UserResolver {
|
||||
logger.info('createUser() successful...')
|
||||
|
||||
if (redeemCode) {
|
||||
eventRedeemRegister.userId = dbUser.id
|
||||
await eventProtocol.writeEvent(event.setEventRedeemRegister(eventRedeemRegister))
|
||||
eventRegisterRedeem.userId = dbUser.id
|
||||
await eventRegisterRedeem.save()
|
||||
} else {
|
||||
eventRegister.userId = dbUser.id
|
||||
await eventProtocol.writeEvent(event.setEventRegister(eventRegister))
|
||||
await EVENT_REGISTER(dbUser.id)
|
||||
}
|
||||
|
||||
return new User(dbUser)
|
||||
@ -460,8 +452,6 @@ export class UserResolver {
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('REPEATABLE READ')
|
||||
|
||||
const event = new Event()
|
||||
|
||||
try {
|
||||
// Save user
|
||||
await queryRunner.manager.save(user).catch((error) => {
|
||||
@ -475,9 +465,7 @@ export class UserResolver {
|
||||
await queryRunner.commitTransaction()
|
||||
logger.info('User and UserContact data written successfully...')
|
||||
|
||||
const eventActivateAccount = new EventActivateAccount()
|
||||
eventActivateAccount.userId = user.id
|
||||
eventProtocol.writeEvent(event.setEventActivateAccount(eventActivateAccount))
|
||||
await EVENT_ACTIVATE_ACCOUNT(user.id)
|
||||
} catch (e) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
throw new LogError('Error on writing User and User Contact data', e)
|
||||
@ -793,19 +781,12 @@ export class UserResolver {
|
||||
email = email.trim().toLowerCase()
|
||||
// const user = await dbUser.findOne({ id: emailContact.userId })
|
||||
const user = await findUserByEmail(email)
|
||||
if (!user) {
|
||||
throw new LogError('Could not find user to given email contact', email)
|
||||
}
|
||||
if (user.deletedAt) {
|
||||
if (user.deletedAt || user.emailContact.deletedAt) {
|
||||
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++
|
||||
await emailContact.save()
|
||||
user.emailContact.emailResendCount++
|
||||
await user.emailContact.save()
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const emailSent = await sendAccountActivationEmail({
|
||||
@ -813,7 +794,7 @@ export class UserResolver {
|
||||
lastName: user.lastName,
|
||||
email,
|
||||
language: user.language,
|
||||
activationLink: activationLink(emailContact.emailVerificationCode),
|
||||
activationLink: activationLink(user.emailContact.emailVerificationCode),
|
||||
timeDurationObject: getTimeDurationObject(CONFIG.EMAIL_CODE_VALID_TIME),
|
||||
})
|
||||
|
||||
@ -821,12 +802,7 @@ export class UserResolver {
|
||||
if (!emailSent) {
|
||||
logger.info(`Account confirmation link: ${activationLink}`)
|
||||
} else {
|
||||
const event = new Event()
|
||||
const eventSendConfirmationEmail = new EventSendConfirmationEmail()
|
||||
eventSendConfirmationEmail.userId = user.id
|
||||
await eventProtocol.writeEvent(
|
||||
event.setEventSendConfirmationEmail(eventSendConfirmationEmail),
|
||||
)
|
||||
await EVENT_ADMIN_SEND_CONFIRMATION_EMAIL(user.id)
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
14
backend/src/graphql/resolver/util/getLastTransaction.ts
Normal file
14
backend/src/graphql/resolver/util/getLastTransaction.ts
Normal 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,
|
||||
},
|
||||
)
|
||||
}
|
||||
@ -4,6 +4,7 @@ import createServer from './server/createServer'
|
||||
|
||||
// config
|
||||
import CONFIG from './config'
|
||||
import { startValidateCommunities } from './federation/validateCommunities'
|
||||
|
||||
async function main() {
|
||||
const { app } = await createServer()
|
||||
@ -16,6 +17,7 @@ async function main() {
|
||||
console.log(`GraphIQL available at http://localhost:${CONFIG.PORT}`)
|
||||
}
|
||||
})
|
||||
startValidateCommunities(Number(CONFIG.FEDERATION_VALIDATE_COMMUNITY_TIMER))
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
|
||||
@ -1,10 +1,5 @@
|
||||
{
|
||||
"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": {
|
||||
"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.",
|
||||
@ -19,12 +14,22 @@
|
||||
"onForgottenPasswordCopyLink": "oder kopiere den obigen Link in dein Browserfenster.",
|
||||
"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": {
|
||||
"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"
|
||||
},
|
||||
"contributionRejected": {
|
||||
"commonGoodContributionRejected": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde von {senderFirstName} {senderLastName} abgelehnt.",
|
||||
"contributionDeleted": {
|
||||
"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",
|
||||
"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“!"
|
||||
},
|
||||
|
||||
@ -1,10 +1,5 @@
|
||||
{
|
||||
"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": {
|
||||
"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.",
|
||||
@ -19,10 +14,20 @@
|
||||
"onForgottenPasswordCopyLink": "or copy the link above into your browser window.",
|
||||
"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": {
|
||||
"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"
|
||||
},
|
||||
"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": {
|
||||
"commonGoodContributionDenied": "Your public good contribution “{contributionMemo}” was rejected by {senderFirstName} {senderLastName}.",
|
||||
"subject": "Gradido: Your common good contribution was rejected",
|
||||
|
||||
@ -68,6 +68,12 @@ export const createUser = gql`
|
||||
}
|
||||
`
|
||||
|
||||
export const sendActivationEmail = gql`
|
||||
mutation ($email: String!) {
|
||||
sendActivationEmail(email: $email)
|
||||
}
|
||||
`
|
||||
|
||||
export const sendCoins = gql`
|
||||
mutation ($email: String!, $amount: Decimal!, $memo: String!) {
|
||||
sendCoins(email: $email, amount: $amount, memo: $memo)
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { calculateDecay } from './decay'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
import { Decay } from '@model/Decay'
|
||||
import { getCustomRepository } from '@dbTools/typeorm'
|
||||
import { TransactionLinkRepository } from '@repository/TransactionLink'
|
||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||
import { getLastTransaction } from '../graphql/resolver/util/getLastTransaction'
|
||||
|
||||
function isStringBoolean(value: string): boolean {
|
||||
const lowerValue = value.toLowerCase()
|
||||
@ -20,7 +20,7 @@ async function calculateBalance(
|
||||
time: Date,
|
||||
transactionLink?: dbTransactionLink | 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
|
||||
|
||||
const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time)
|
||||
|
||||
@ -404,6 +404,11 @@
|
||||
minimatch "^3.0.4"
|
||||
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":
|
||||
version "10.0.0"
|
||||
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"
|
||||
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":
|
||||
version "1.1.0"
|
||||
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:
|
||||
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:
|
||||
version "27.2.5"
|
||||
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"
|
||||
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:
|
||||
version "3.7.2"
|
||||
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"
|
||||
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:
|
||||
version "1.0.0"
|
||||
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"
|
||||
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:
|
||||
version "2.4.2"
|
||||
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"
|
||||
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:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
@ -2193,6 +2115,13 @@ cross-env@^7.0.3:
|
||||
dependencies:
|
||||
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:
|
||||
version "6.0.5"
|
||||
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:
|
||||
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:
|
||||
version "2.5.1"
|
||||
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"
|
||||
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:
|
||||
version "0.3.0"
|
||||
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"
|
||||
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:
|
||||
version "0.10.0"
|
||||
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"
|
||||
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:
|
||||
version "5.5.3"
|
||||
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"
|
||||
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:
|
||||
version "3.2.7"
|
||||
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:
|
||||
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:
|
||||
version "1.2.1"
|
||||
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"
|
||||
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:
|
||||
version "2.8.9"
|
||||
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"
|
||||
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:
|
||||
version "0.0.5"
|
||||
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"
|
||||
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:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.1.0.tgz#ecc228486f69991e49e9476485a5be1e8fc5c4d9"
|
||||
@ -4932,26 +4819,6 @@ named-placeholders@^1.1.2:
|
||||
dependencies:
|
||||
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:
|
||||
version "1.4.0"
|
||||
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"
|
||||
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
|
||||
|
||||
node-fetch@^2.6.0:
|
||||
node-fetch@2.6.7, node-fetch@^2.6.0:
|
||||
version "2.6.7"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
|
||||
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
|
||||
@ -4991,7 +4858,7 @@ node-fetch@^2.6.1:
|
||||
dependencies:
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.5.0.tgz#7a64eefa0b21112f89f58379da128ac177f20e40"
|
||||
integrity sha512-2iGbaQBV+ITgCz76ZEjmhUKAKVf7xfY1sRl4UiKQspfZMH2h06SyhNsnSVy50cwkFQDGLyif6m/6uFXHkOZ6rg==
|
||||
@ -5042,25 +4909,6 @@ nodemon@^2.0.7:
|
||||
undefsafe "^2.0.3"
|
||||
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:
|
||||
version "1.0.10"
|
||||
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"
|
||||
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:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e"
|
||||
@ -5743,13 +5586,6 @@ readdirp@~3.6.0:
|
||||
dependencies:
|
||||
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:
|
||||
version "0.1.13"
|
||||
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"
|
||||
path-parse "^1.0.6"
|
||||
|
||||
resolve@^1.15.1, resolve@^1.17.0:
|
||||
resolve@^1.15.1:
|
||||
version "1.22.1"
|
||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177"
|
||||
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"
|
||||
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:
|
||||
version "5.0.1"
|
||||
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"
|
||||
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:
|
||||
version "1.2.0"
|
||||
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"
|
||||
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:
|
||||
version "1.0.5"
|
||||
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"
|
||||
integrity sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==
|
||||
|
||||
sodium-javascript@~0.8.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:
|
||||
sodium-native@^3.3.0:
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/sodium-native/-/sodium-native-3.3.0.tgz#50ee52ac843315866cce3d0c08ab03eb78f22361"
|
||||
integrity sha512-rg6lCDM/qa3p07YGqaVD+ciAbUqm6SoO4xmlcfkbU5r1zIGrguXztLiEtaLYTV5U6k8KSIUFmnU3yQUSKmf6DA==
|
||||
dependencies:
|
||||
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:
|
||||
version "0.5.20"
|
||||
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"
|
||||
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:
|
||||
version "4.0.2"
|
||||
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"
|
||||
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:
|
||||
version "2.1.0"
|
||||
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"
|
||||
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:
|
||||
version "1.0.1"
|
||||
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"
|
||||
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:
|
||||
version "4.0.0"
|
||||
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"
|
||||
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:
|
||||
version "1.0.10"
|
||||
resolved "https://registry.yarnpkg.com/xss/-/xss-1.0.10.tgz#5cd63a9b147a755a14cb0455c7db8866120eb4d2"
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
CONFIG_VERSION=v1.2022-03-18
|
||||
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USER=root
|
||||
|
||||
@ -16,17 +16,17 @@ export class EventProtocol extends BaseEntity {
|
||||
@Column({ name: 'user_id', unsigned: true, nullable: false })
|
||||
userId: number
|
||||
|
||||
@Column({ name: 'x_user_id', unsigned: true, nullable: true })
|
||||
xUserId: number
|
||||
@Column({ name: 'x_user_id', type: 'int', unsigned: true, nullable: true })
|
||||
xUserId: number | null
|
||||
|
||||
@Column({ name: 'x_community_id', unsigned: true, nullable: true })
|
||||
xCommunityId: number
|
||||
@Column({ name: 'x_community_id', type: 'int', unsigned: true, nullable: true })
|
||||
xCommunityId: number | null
|
||||
|
||||
@Column({ name: 'transaction_id', unsigned: true, nullable: true })
|
||||
transactionId: number
|
||||
@Column({ name: 'transaction_id', type: 'int', unsigned: true, nullable: true })
|
||||
transactionId: number | null
|
||||
|
||||
@Column({ name: 'contribution_id', unsigned: true, nullable: true })
|
||||
contributionId: number
|
||||
@Column({ name: 'contribution_id', type: 'int', unsigned: true, nullable: true })
|
||||
contributionId: number | null
|
||||
|
||||
@Column({
|
||||
type: 'decimal',
|
||||
@ -35,8 +35,8 @@ export class EventProtocol extends BaseEntity {
|
||||
nullable: true,
|
||||
transformer: DecimalTransformer,
|
||||
})
|
||||
amount: Decimal
|
||||
amount: Decimal | null
|
||||
|
||||
@Column({ name: 'message_id', unsigned: true, nullable: true })
|
||||
messageId: number
|
||||
@Column({ name: 'message_id', type: 'int', unsigned: true, nullable: true })
|
||||
messageId: number | null
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ export class Community extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({ name: 'foreign', type: 'bool', nullable: false, default: false })
|
||||
@Column({ name: 'foreign', type: 'bool', nullable: false, default: true })
|
||||
foreign: boolean
|
||||
|
||||
@Column({ name: 'public_key', type: 'binary', length: 64, default: null, nullable: true })
|
||||
@ -24,13 +24,13 @@ export class Community extends BaseEntity {
|
||||
@Column({ name: 'end_point', length: 255, nullable: false })
|
||||
endPoint: string
|
||||
|
||||
@Column({ name: 'last_announced_at', type: 'datetime', nullable: false })
|
||||
@Column({ name: 'last_announced_at', type: 'datetime', nullable: true })
|
||||
lastAnnouncedAt: Date
|
||||
|
||||
@Column({ name: 'verified_at', type: 'datetime', nullable: false })
|
||||
@Column({ name: 'verified_at', type: 'datetime', nullable: true })
|
||||
verifiedAt: Date
|
||||
|
||||
@Column({ name: 'last_error_at', type: 'datetime', nullable: false })
|
||||
@Column({ name: 'last_error_at', type: 'datetime', nullable: true })
|
||||
lastErrorAt: Date
|
||||
|
||||
@CreateDateColumn({
|
||||
|
||||
@ -8,7 +8,10 @@
|
||||
|
||||
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn(
|
||||
'ALTER TABLE `communities` ADD COLUMN `foreign` tinyint(4) NOT NULL DEFAULT 0 AFTER `id`;',
|
||||
'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`;',
|
||||
@ -20,7 +23,10 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis
|
||||
|
||||
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
// write downgrade logic as parameter of queryFn
|
||||
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;')
|
||||
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`;')
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gradido-database",
|
||||
"version": "1.17.1",
|
||||
"version": "1.18.2",
|
||||
"description": "Gradido Database Tool to execute database migrations",
|
||||
"main": "src/index.ts",
|
||||
"repository": "https://github.com/gradido/gradido/database",
|
||||
|
||||
@ -27,7 +27,7 @@ COMMUNITY_DESCRIPTION="Gradido Development Stage1 Test Community"
|
||||
COMMUNITY_SUPPORT_MAIL=support@supportmail.com
|
||||
|
||||
# backend
|
||||
BACKEND_CONFIG_VERSION=v14.2022-12-22
|
||||
BACKEND_CONFIG_VERSION=v15.2023-02-07
|
||||
|
||||
JWT_EXPIRES_IN=10m
|
||||
GDT_API_URL=https://gdt.gradido.net
|
||||
@ -56,9 +56,6 @@ EMAIL_CODE_REQUEST_TIME=10
|
||||
|
||||
WEBHOOK_ELOPAGE_SECRET=secret
|
||||
|
||||
# EventProtocol
|
||||
EVENT_PROTOCOL_DISABLED=false
|
||||
|
||||
# Federation
|
||||
# if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen
|
||||
# on an hash created from this topic
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
CONFIG_VERSION=v1.2023-01-01
|
||||
|
||||
# Database
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
@ -8,9 +6,6 @@ DB_PASSWORD=
|
||||
DB_DATABASE=gradido_community
|
||||
TYPEORM_LOGGING_RELATIVE_PATH=typeorm.dht-node.log
|
||||
|
||||
# EventProtocol
|
||||
EVENT_PROTOCOL_DISABLED=false
|
||||
|
||||
# SET LOG LEVEL AS NEEDED IN YOUR .ENV
|
||||
# POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal
|
||||
# LOG_LEVEL=info
|
||||
@ -20,3 +15,5 @@ EVENT_PROTOCOL_DISABLED=false
|
||||
# on an hash created from this topic
|
||||
FEDERATION_DHT_TOPIC=GRADIDO_HUB
|
||||
# FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f
|
||||
# FEDERATION_COMMUNITY_URL=http://localhost
|
||||
# FEDERATION_COMMUNITY_API_PORT=5000
|
||||
|
||||
@ -8,10 +8,8 @@ DB_PASSWORD=$DB_PASSWORD
|
||||
DB_DATABASE=gradido_community
|
||||
TYPEORM_LOGGING_RELATIVE_PATH=$TYPEORM_LOGGING_RELATIVE_PATH
|
||||
|
||||
# EventProtocol
|
||||
EVENT_PROTOCOL_DISABLED=$EVENT_PROTOCOL_DISABLED
|
||||
|
||||
# Federation
|
||||
FEDERATION_DHT_TOPIC=$FEDERATION_DHT_TOPIC
|
||||
FEDERATION_DHT_SEED=$FEDERATION_DHT_SEED
|
||||
FEDERATION_COMMUNITY_URL=$FEDERATION_COMMUNITY_URL
|
||||
FEDERATION_COMMUNITY_API_PORT=$FEDERATION_COMMUNITY_API_PORT
|
||||
|
||||
@ -3,13 +3,13 @@ import dotenv from 'dotenv'
|
||||
dotenv.config()
|
||||
|
||||
const constants = {
|
||||
DB_VERSION: '0059-add_hide_amount_to_users',
|
||||
DB_VERSION: '0060-update_communities_table',
|
||||
LOG4JS_CONFIG: 'log4js-config.json',
|
||||
// default log level on production should be info
|
||||
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
|
||||
CONFIG_VERSION: {
|
||||
DEFAULT: 'DEFAULT',
|
||||
EXPECTED: 'v1.2023-01-01',
|
||||
EXPECTED: 'v2.2023-02-07',
|
||||
CURRENT: '',
|
||||
},
|
||||
}
|
||||
@ -28,15 +28,11 @@ const database = {
|
||||
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 = {
|
||||
FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || 'GRADIDO_HUB',
|
||||
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
|
||||
@ -55,7 +51,6 @@ const CONFIG = {
|
||||
...constants,
|
||||
...server,
|
||||
...database,
|
||||
...eventProtocol,
|
||||
...federation,
|
||||
}
|
||||
|
||||
|
||||
@ -116,6 +116,7 @@ describe('federation', () => {
|
||||
beforeEach(async () => {
|
||||
DHT.mockClear()
|
||||
jest.clearAllMocks()
|
||||
await cleanDB()
|
||||
await startDHT(TEST_TOPIC)
|
||||
})
|
||||
|
||||
@ -234,18 +235,18 @@ describe('federation', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
jsonArray = [
|
||||
{ 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' },
|
||||
{ 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' },
|
||||
]
|
||||
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
||||
})
|
||||
|
||||
it('logs the received data', () => {
|
||||
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 = [
|
||||
{
|
||||
wrong: 'wrong but tolerated property test',
|
||||
api: 'v1_0',
|
||||
api: '1_0',
|
||||
url: 'url1',
|
||||
},
|
||||
{
|
||||
api: 'v2_0',
|
||||
api: '2_0',
|
||||
url: 'url2',
|
||||
wrong: 'wrong but tolerated property test',
|
||||
},
|
||||
]
|
||||
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
||||
result = await DbCommunity.find()
|
||||
result = await DbCommunity.find({ foreign: true })
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
@ -287,13 +288,14 @@ describe('federation', () => {
|
||||
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.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
foreign: true,
|
||||
publicKey: expect.any(Buffer),
|
||||
apiVersion: 'v1_0',
|
||||
apiVersion: '1_0',
|
||||
endPoint: 'url1',
|
||||
lastAnnouncedAt: 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.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
foreign: true,
|
||||
publicKey: expect.any(Buffer),
|
||||
apiVersion: 'v2_0',
|
||||
apiVersion: '2_0',
|
||||
endPoint: 'url2',
|
||||
lastAnnouncedAt: expect.any(Date),
|
||||
createdAt: expect.any(Date),
|
||||
@ -535,7 +538,7 @@ describe('federation', () => {
|
||||
{ api: 'toolong api', url: 'some valid url' },
|
||||
]
|
||||
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
||||
result = await DbCommunity.find()
|
||||
result = await DbCommunity.find({ foreign: true })
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
@ -551,6 +554,7 @@ describe('federation', () => {
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
foreign: true,
|
||||
publicKey: expect.any(Buffer),
|
||||
apiVersion: 'valid api',
|
||||
endPoint:
|
||||
@ -588,7 +592,7 @@ describe('federation', () => {
|
||||
},
|
||||
]
|
||||
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
||||
result = await DbCommunity.find()
|
||||
result = await DbCommunity.find({ foreign: true })
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
@ -604,6 +608,7 @@ describe('federation', () => {
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
foreign: true,
|
||||
publicKey: expect.any(Buffer),
|
||||
apiVersion: 'valid api1',
|
||||
endPoint:
|
||||
@ -621,6 +626,7 @@ describe('federation', () => {
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
foreign: true,
|
||||
publicKey: expect.any(Buffer),
|
||||
apiVersion: 'valid api2',
|
||||
endPoint:
|
||||
@ -638,6 +644,7 @@ describe('federation', () => {
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
foreign: true,
|
||||
publicKey: expect.any(Buffer),
|
||||
apiVersion: 'valid api3',
|
||||
endPoint:
|
||||
@ -655,6 +662,7 @@ describe('federation', () => {
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
foreign: true,
|
||||
publicKey: expect.any(Buffer),
|
||||
apiVersion: 'valid api4',
|
||||
endPoint:
|
||||
@ -710,17 +718,17 @@ describe('federation', () => {
|
||||
Buffer.from(
|
||||
JSON.stringify([
|
||||
{
|
||||
api: 'v1_0',
|
||||
url: 'http://localhost:4000/api/v1_0',
|
||||
api: '1_0',
|
||||
url: 'http://localhost:5001/api/',
|
||||
},
|
||||
{
|
||||
api: 'v2_0',
|
||||
url: 'http://localhost:4000/api/v2_0',
|
||||
api: '2_0',
|
||||
url: 'http://localhost:5002/api/',
|
||||
},
|
||||
]),
|
||||
),
|
||||
)
|
||||
result = await DbCommunity.find()
|
||||
result = await DbCommunity.find({ foreign: true })
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
@ -736,9 +744,10 @@ describe('federation', () => {
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
foreign: true,
|
||||
publicKey: expect.any(Buffer),
|
||||
apiVersion: 'v1_0',
|
||||
endPoint: 'http://localhost:4000/api/v1_0',
|
||||
apiVersion: '1_0',
|
||||
endPoint: 'http://localhost:5001/api/',
|
||||
lastAnnouncedAt: expect.any(Date),
|
||||
createdAt: expect.any(Date),
|
||||
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.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: expect.any(Number),
|
||||
foreign: true,
|
||||
publicKey: expect.any(Buffer),
|
||||
apiVersion: 'v2_0',
|
||||
endPoint: 'http://localhost:4000/api/v2_0',
|
||||
apiVersion: '2_0',
|
||||
endPoint: 'http://localhost:5002/api/',
|
||||
lastAnnouncedAt: expect.any(Date),
|
||||
createdAt: expect.any(Date),
|
||||
updatedAt: null,
|
||||
@ -775,16 +785,16 @@ describe('federation', () => {
|
||||
Buffer.from(
|
||||
JSON.stringify([
|
||||
{
|
||||
api: 'v1_0',
|
||||
url: 'http://localhost:4000/api/v1_0',
|
||||
api: '1_0',
|
||||
url: 'http://localhost:5001/api/',
|
||||
},
|
||||
{
|
||||
api: 'v1_1',
|
||||
url: 'http://localhost:4000/api/v1_1',
|
||||
api: '1_1',
|
||||
url: 'http://localhost:5002/api/',
|
||||
},
|
||||
{
|
||||
api: 'v2_0',
|
||||
url: 'http://localhost:4000/api/v2_0',
|
||||
api: '2_0',
|
||||
url: 'http://localhost:5003/api/',
|
||||
},
|
||||
]),
|
||||
),
|
||||
|
||||
@ -15,9 +15,9 @@ const ERRORTIME = 240000
|
||||
const ANNOUNCETIME = 30000
|
||||
|
||||
enum ApiVersionType {
|
||||
V1_0 = 'v1_0',
|
||||
V1_1 = 'v1_1',
|
||||
V2_0 = 'v2_0',
|
||||
V1_0 = '1_0',
|
||||
V1_1 = '1_1',
|
||||
V2_0 = '2_0',
|
||||
}
|
||||
type CommunityApi = {
|
||||
api: string
|
||||
@ -31,13 +31,7 @@ export const startDHT = async (topic: string): Promise<void> => {
|
||||
logger.info(`keyPairDHT: publicKey=${keyPair.publicKey.toString('hex')}`)
|
||||
logger.debug(`keyPairDHT: secretKey=${keyPair.secretKey.toString('hex')}`)
|
||||
|
||||
const ownApiVersions = Object.values(ApiVersionType).map(function (apiEnum) {
|
||||
const comApi: CommunityApi = {
|
||||
api: apiEnum,
|
||||
url: CONFIG.FEDERATION_COMMUNITY_URL + apiEnum,
|
||||
}
|
||||
return comApi
|
||||
})
|
||||
const ownApiVersions = writeHomeCommunityEnries(keyPair.publicKey)
|
||||
logger.debug(`ApiList: ${JSON.stringify(ownApiVersions)}`)
|
||||
|
||||
const node = new DHT({ keyPair })
|
||||
@ -184,3 +178,34 @@ export const startDHT = async (topic: string): Promise<void> => {
|
||||
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
|
||||
}
|
||||
|
||||
@ -84,6 +84,29 @@ services:
|
||||
- ./dht-node:/app
|
||||
- ./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 ##############################################
|
||||
########################################################
|
||||
@ -155,5 +178,8 @@ volumes:
|
||||
dht_node_modules:
|
||||
dht_database_node_modules:
|
||||
dht_database_build:
|
||||
federation_node_modules:
|
||||
federation_database_node_modules:
|
||||
federation_database_build:
|
||||
database_node_modules:
|
||||
database_build:
|
||||
@ -36,6 +36,21 @@ services:
|
||||
- NODE_ENV="test"
|
||||
- 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 #############################################
|
||||
########################################################
|
||||
|
||||
@ -147,6 +147,42 @@ services:
|
||||
# <host_machine_directory>:<container_directory> – mirror bidirectional path in local context with path in Docker container
|
||||
- ./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 #############################################
|
||||
########################################################
|
||||
|
||||
26
e2e-tests/.eslintrc.js
Normal file
26
e2e-tests/.eslintrc.js
Normal 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
5
e2e-tests/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
cypress/screenshots/
|
||||
cypress/videos/
|
||||
cucumber-messages.ndjson
|
||||
|
||||
9
e2e-tests/.prettierrc.js
Normal file
9
e2e-tests/.prettierrc.js
Normal file
@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
semi: false,
|
||||
printWidth: 100,
|
||||
singleQuote: true,
|
||||
trailingComma: "all",
|
||||
tabWidth: 2,
|
||||
bracketSpacing: true,
|
||||
endOfLine: "auto",
|
||||
};
|
||||
@ -11,7 +11,7 @@
|
||||
###############################################################################
|
||||
FROM cypress/base:16.14.2-slim
|
||||
|
||||
ARG DOCKER_WORKDIR=/tests/
|
||||
ARG DOCKER_WORKDIR="/tests"
|
||||
WORKDIR $DOCKER_WORKDIR
|
||||
|
||||
# install dependencies
|
||||
@ -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
|
||||
```
|
||||
|
||||
79
e2e-tests/cypress.config.ts
Normal file
79
e2e-tests/cypress.config.ts
Normal 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,
|
||||
},
|
||||
})
|
||||
4
e2e-tests/cypress/.gitignore
vendored
4
e2e-tests/cypress/.gitignore
vendored
@ -1,4 +0,0 @@
|
||||
tests/node_modules/
|
||||
tests/cypress/screenshots/
|
||||
tests/cypress/videos/
|
||||
tests/cucumber-messages.ndjson
|
||||
@ -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
|
||||
```
|
||||
@ -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"
|
||||
@ -11,7 +11,7 @@ Feature: User authentication
|
||||
# | bibi@bloxberg.de | Aa12345_ | Bibi Bloxberg |
|
||||
|
||||
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_"
|
||||
Then the user is logged in with username "Bibi Bloxberg"
|
||||
|
||||
@ -4,7 +4,7 @@ Feature: User registration
|
||||
|
||||
@skip
|
||||
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"
|
||||
And the user agrees to the privacy policy
|
||||
And the user submits the registration form
|
||||
@ -12,7 +12,7 @@ Feature: User profile - change password
|
||||
Given the user is logged in as "bibi@bloxberg.de" "Aa12345_"
|
||||
|
||||
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
|
||||
When the user fills the password form with:
|
||||
| Old password | Aa12345_ |
|
||||
18
e2e-tests/cypress/e2e/models/ForgotPasswordPage.ts
Normal file
18
e2e-tests/cypress/e2e/models/ForgotPasswordPage.ts
Normal file
@ -0,0 +1,18 @@
|
||||
/// <reference types='cypress' />
|
||||
|
||||
export class ForgotPasswordPage {
|
||||
// selectors
|
||||
emailInput = 'input[type=email]'
|
||||
submitBtn = 'button[type=submit]'
|
||||
successComponent = '[data-test="forgot-password-success"]'
|
||||
|
||||
enterEmail(email: string) {
|
||||
cy.get(this.emailInput).clear().type(email)
|
||||
return this
|
||||
}
|
||||
|
||||
submitEmail() {
|
||||
cy.get(this.submitBtn).click()
|
||||
return this
|
||||
}
|
||||
}
|
||||
35
e2e-tests/cypress/e2e/models/LoginPage.ts
Normal file
35
e2e-tests/cypress/e2e/models/LoginPage.ts
Normal file
@ -0,0 +1,35 @@
|
||||
/// <reference types='cypress' />
|
||||
|
||||
export class LoginPage {
|
||||
// selectors
|
||||
emailInput = 'input[type=email]'
|
||||
passwordInput = 'input[type=password]'
|
||||
forgotPasswordLink = '[data-test="forgot-password-link"]'
|
||||
submitBtn = '[type=submit]'
|
||||
emailHint = '#vee_Email'
|
||||
passwordHint = '#vee_Password'
|
||||
|
||||
goto() {
|
||||
cy.visit('/')
|
||||
return this
|
||||
}
|
||||
|
||||
enterEmail(email: string) {
|
||||
cy.get(this.emailInput).clear().type(email)
|
||||
return this
|
||||
}
|
||||
|
||||
enterPassword(password: string) {
|
||||
cy.get(this.passwordInput).clear().type(password)
|
||||
return this
|
||||
}
|
||||
|
||||
submitLogin() {
|
||||
cy.get(this.submitBtn).click()
|
||||
return this
|
||||
}
|
||||
|
||||
openForgotPasswordPage() {
|
||||
cy.get(this.forgotPasswordLink).click()
|
||||
}
|
||||
}
|
||||
10
e2e-tests/cypress/e2e/models/OverviewPage.ts
Normal file
10
e2e-tests/cypress/e2e/models/OverviewPage.ts
Normal file
@ -0,0 +1,10 @@
|
||||
/// <reference types='cypress' />
|
||||
|
||||
export class OverviewPage {
|
||||
navbarName = '[data-test="navbar-item-username"]'
|
||||
|
||||
goto() {
|
||||
cy.visit('/overview')
|
||||
return this
|
||||
}
|
||||
}
|
||||
35
e2e-tests/cypress/e2e/models/ProfilePage.ts
Normal file
35
e2e-tests/cypress/e2e/models/ProfilePage.ts
Normal file
@ -0,0 +1,35 @@
|
||||
/// <reference types='cypress' />
|
||||
|
||||
export class ProfilePage {
|
||||
// selectors
|
||||
openChangePassword = '[data-test=open-password-change-form]'
|
||||
oldPasswordInput = '#password-input-field'
|
||||
newPasswordInput = '#new-password-input-field'
|
||||
newPasswordRepeatInput = '#repeat-new-password-input-field'
|
||||
submitNewPasswordBtn = '[data-test=submit-new-password-btn]'
|
||||
|
||||
goto() {
|
||||
cy.visit('/profile')
|
||||
return this
|
||||
}
|
||||
|
||||
enterOldPassword(password: string) {
|
||||
cy.get(this.oldPasswordInput).clear().type(password)
|
||||
return this
|
||||
}
|
||||
|
||||
enterNewPassword(password: string) {
|
||||
cy.get(this.newPasswordInput).find('input').clear().type(password)
|
||||
return this
|
||||
}
|
||||
|
||||
enterRepeatPassword(password: string) {
|
||||
cy.get(this.newPasswordRepeatInput).find('input').clear().type(password)
|
||||
return this
|
||||
}
|
||||
|
||||
submitPasswordForm() {
|
||||
cy.get(this.submitNewPasswordBtn).click()
|
||||
return this
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user