mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into 2592-Gradido-Logo-blurred
This commit is contained in:
commit
8d89409e21
98
.github/workflows/test_dht-node.yml
vendored
Normal file
98
.github/workflows/test_dht-node.yml
vendored
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
name: gradido test_dht-node CI
|
||||||
|
|
||||||
|
on: push
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
##############################################################################
|
||||||
|
# JOB: DOCKER BUILD TEST #####################################################
|
||||||
|
##############################################################################
|
||||||
|
build:
|
||||||
|
name: Docker Build Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Build `test` image
|
||||||
|
run: |
|
||||||
|
docker build --target test -t "gradido/dht-node:test" -f dht-node/Dockerfile .
|
||||||
|
docker save "gradido/dht-node:test" > /tmp/dht-node.tar
|
||||||
|
|
||||||
|
- name: Upload Artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: docker-dht-node-test
|
||||||
|
path: /tmp/dht-node.tar
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# JOB: LINT ##################################################################
|
||||||
|
##############################################################################
|
||||||
|
lint:
|
||||||
|
name: Lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [build]
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Download Docker Image
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: docker-dht-node-test
|
||||||
|
path: /tmp
|
||||||
|
- name: Load Docker Image
|
||||||
|
run: docker load < /tmp/dht-node.tar
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: docker run --rm gradido/dht-node:test yarn run lint
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
# JOB: UNIT TEST #############################################################
|
||||||
|
##############################################################################
|
||||||
|
unit_test:
|
||||||
|
name: Unit tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [build]
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Download Docker Image
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: docker-dht-node-test
|
||||||
|
path: /tmp
|
||||||
|
|
||||||
|
- name: Load Docker Image
|
||||||
|
run: docker load < /tmp/dht-node.tar
|
||||||
|
|
||||||
|
- name: docker-compose mariadb
|
||||||
|
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb
|
||||||
|
|
||||||
|
- name: Sleep for 30 seconds
|
||||||
|
run: sleep 30s
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: docker-compose database
|
||||||
|
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps database
|
||||||
|
|
||||||
|
- name: Sleep for 30 seconds
|
||||||
|
run: sleep 30s
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
#- name: Unit tests
|
||||||
|
# run: cd database && yarn && yarn build && cd ../dht-node && yarn && yarn test
|
||||||
|
- name: Unit tests
|
||||||
|
run: |
|
||||||
|
docker run --env NODE_ENV=test --env DB_HOST=mariadb --network gradido_internal-net -v ~/coverage:/app/coverage --rm gradido/dht-node:test yarn run test
|
||||||
|
cp -r ~/coverage ./coverage
|
||||||
|
|
||||||
|
- name: Coverage check
|
||||||
|
uses: webcraftmedia/coverage-check-action@master
|
||||||
|
with:
|
||||||
|
report_name: Coverage dht-node
|
||||||
|
type: lcov
|
||||||
|
#result_path: ./dht-node/coverage/lcov.info
|
||||||
|
result_path: ./coverage/lcov.info
|
||||||
|
min_coverage: 79
|
||||||
|
token: ${{ github.token }}
|
||||||
42
CHANGELOG.md
42
CHANGELOG.md
@ -4,8 +4,50 @@ All notable changes to this project will be documented in this file. Dates are d
|
|||||||
|
|
||||||
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
||||||
|
|
||||||
|
#### [1.18.1](https://github.com/gradido/gradido/compare/1.18.0...1.18.1)
|
||||||
|
|
||||||
|
- fix(frontend): fix is last month for empty form date [`#2697`](https://github.com/gradido/gradido/pull/2697)
|
||||||
|
- fix(frontend): community link [`#2696`](https://github.com/gradido/gradido/pull/2696)
|
||||||
|
|
||||||
|
#### [1.18.0](https://github.com/gradido/gradido/compare/1.17.1...1.18.0)
|
||||||
|
|
||||||
|
> 9 February 2023
|
||||||
|
|
||||||
|
- feat(release): version 1.18.0 [`#2690`](https://github.com/gradido/gradido/pull/2690)
|
||||||
|
- refactor(frontend): toast by automatically logged out [`#2681`](https://github.com/gradido/gradido/pull/2681)
|
||||||
|
- refactor(frontend): change text for gdd_per_link.choose-amount [`#2638`](https://github.com/gradido/gradido/pull/2638)
|
||||||
|
- fix(backend): emails for deny and delete contribution [`#2688`](https://github.com/gradido/gradido/pull/2688)
|
||||||
|
- refactor(other): remove config version from `.env.dist` [`#2686`](https://github.com/gradido/gradido/pull/2686)
|
||||||
|
- refactor(backend): use LogError on contributionMessageResolver [`#2663`](https://github.com/gradido/gradido/pull/2663)
|
||||||
|
- refactor(backend): get last transaction by only one function [`#2668`](https://github.com/gradido/gradido/pull/2668)
|
||||||
|
- refactor(other): don't rebuild modul if unit test file has been changed [`#2667`](https://github.com/gradido/gradido/pull/2667)
|
||||||
|
- refactor(backend): use LogError on contributionLinkResolver [`#2662`](https://github.com/gradido/gradido/pull/2662)
|
||||||
|
- refactor(backend): remove event protocol config switch [`#2670`](https://github.com/gradido/gradido/pull/2670)
|
||||||
|
- refactor(backend): event protocol [`#2652`](https://github.com/gradido/gradido/pull/2652)
|
||||||
|
- refactor(frontend): sidebar becomes smaller when critical phase [`#2649`](https://github.com/gradido/gradido/pull/2649)
|
||||||
|
- refactor(backend): use LogError on sendEMailTranslated [`#2656`](https://github.com/gradido/gradido/pull/2656)
|
||||||
|
- refactor(backend): log error class [`#2640`](https://github.com/gradido/gradido/pull/2640)
|
||||||
|
- feat(backend): add filterState parameter to listAllContributions query [`#2619`](https://github.com/gradido/gradido/pull/2619)
|
||||||
|
- refactor(frontend): there is no message when a month is fully created [`#2626`](https://github.com/gradido/gradido/pull/2626)
|
||||||
|
- refactor(frontend): better text alignment on send via link [`#2637`](https://github.com/gradido/gradido/pull/2637)
|
||||||
|
- refactor(frontend): text changed as indicated in the issues [`#2642`](https://github.com/gradido/gradido/pull/2642)
|
||||||
|
- refactor(frontend): when you click on create, you will be directed to the form [`#2645`](https://github.com/gradido/gradido/pull/2645)
|
||||||
|
- feat(backend): federation: separated dht-hub features in new dht-node modul [`#2510`](https://github.com/gradido/gradido/pull/2510)
|
||||||
|
- refactor(backend): refine assembly of error message in user resolver [`#2636`](https://github.com/gradido/gradido/pull/2636)
|
||||||
|
- fix(backend): unit tests creations for 31st day [`#2641`](https://github.com/gradido/gradido/pull/2641)
|
||||||
|
- fix(workflow): properly lint pr - prevent requirement to restart linting [`#2635`](https://github.com/gradido/gradido/pull/2635)
|
||||||
|
- feat(frontend): 'yes'-button shows which dialog is currently open with a different color [`#2629`](https://github.com/gradido/gradido/pull/2629)
|
||||||
|
- feat(backend): federation implement multiple apollo graphql endpoints [`#2459`](https://github.com/gradido/gradido/pull/2459)
|
||||||
|
- refactor(frontend): add legend to all contribution tab, and add tests. [`#2625`](https://github.com/gradido/gradido/pull/2625)
|
||||||
|
- feat(frontend): unit tests community page [`#2587`](https://github.com/gradido/gradido/pull/2587)
|
||||||
|
- feat(backend): deny contributions [`#2461`](https://github.com/gradido/gradido/pull/2461)
|
||||||
|
- refactor(admin): update yarn.lock file of admin. [`#2579`](https://github.com/gradido/gradido/pull/2579)
|
||||||
|
|
||||||
#### [1.17.1](https://github.com/gradido/gradido/compare/1.17.0...1.17.1)
|
#### [1.17.1](https://github.com/gradido/gradido/compare/1.17.0...1.17.1)
|
||||||
|
|
||||||
|
> 20 January 2023
|
||||||
|
|
||||||
|
- chore(release): v1.17.1 [`#2588`](https://github.com/gradido/gradido/pull/2588)
|
||||||
- refactor(frontend): change contribution memo add word-break [`#2583`](https://github.com/gradido/gradido/pull/2583)
|
- refactor(frontend): change contribution memo add word-break [`#2583`](https://github.com/gradido/gradido/pull/2583)
|
||||||
- refactor(admin): add text-break on all table memo fields [`#2584`](https://github.com/gradido/gradido/pull/2584)
|
- refactor(admin): add text-break on all table memo fields [`#2584`](https://github.com/gradido/gradido/pull/2584)
|
||||||
- fix(frontend): throw proper frontend warning errors [`#2586`](https://github.com/gradido/gradido/pull/2586)
|
- fix(frontend): throw proper frontend warning errors [`#2586`](https://github.com/gradido/gradido/pull/2586)
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
CONFIG_VERSION=v1.2022-03-18
|
|
||||||
|
|
||||||
GRAPHQL_URI=http://localhost:4000/graphql
|
GRAPHQL_URI=http://localhost:4000/graphql
|
||||||
WALLET_AUTH_URL=http://localhost/authenticate?token={token}
|
WALLET_AUTH_URL=http://localhost/authenticate?token={token}
|
||||||
WALLET_URL=http://localhost/login
|
WALLET_URL=http://localhost/login
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
"description": "Administraion Interface for Gradido",
|
"description": "Administraion Interface for Gradido",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"author": "Moriz Wahl",
|
"author": "Moriz Wahl",
|
||||||
"version": "1.17.1",
|
"version": "1.18.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -86,5 +86,10 @@
|
|||||||
"> 1%",
|
"> 1%",
|
||||||
"last 2 versions",
|
"last 2 versions",
|
||||||
"not ie <= 10"
|
"not ie <= 10"
|
||||||
|
],
|
||||||
|
"nodemonConfig": {
|
||||||
|
"ignore": [
|
||||||
|
"**/*.spec.js"
|
||||||
]
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
CONFIG_VERSION=v14.2022-12-22
|
|
||||||
|
|
||||||
# Server
|
# Server
|
||||||
PORT=4000
|
PORT=4000
|
||||||
JWT_SECRET=secret123
|
JWT_SECRET=secret123
|
||||||
@ -55,9 +53,6 @@ EMAIL_CODE_REQUEST_TIME=10
|
|||||||
# Webhook
|
# Webhook
|
||||||
WEBHOOK_ELOPAGE_SECRET=secret
|
WEBHOOK_ELOPAGE_SECRET=secret
|
||||||
|
|
||||||
# EventProtocol
|
|
||||||
EVENT_PROTOCOL_DISABLED=false
|
|
||||||
|
|
||||||
# SET LOG LEVEL AS NEEDED IN YOUR .ENV
|
# SET LOG LEVEL AS NEEDED IN YOUR .ENV
|
||||||
# POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal
|
# POSSIBLE VALUES: all | trace | debug | info | warn | error | fatal
|
||||||
# LOG_LEVEL=info
|
# LOG_LEVEL=info
|
||||||
|
|||||||
@ -54,9 +54,6 @@ EMAIL_CODE_REQUEST_TIME=$EMAIL_CODE_REQUEST_TIME
|
|||||||
# Webhook
|
# Webhook
|
||||||
WEBHOOK_ELOPAGE_SECRET=$WEBHOOK_ELOPAGE_SECRET
|
WEBHOOK_ELOPAGE_SECRET=$WEBHOOK_ELOPAGE_SECRET
|
||||||
|
|
||||||
# EventProtocol
|
|
||||||
EVENT_PROTOCOL_DISABLED=$EVENT_PROTOCOL_DISABLED
|
|
||||||
|
|
||||||
# Federation
|
# Federation
|
||||||
FEDERATION_DHT_TOPIC=$FEDERATION_DHT_TOPIC
|
FEDERATION_DHT_TOPIC=$FEDERATION_DHT_TOPIC
|
||||||
FEDERATION_DHT_SEED=$FEDERATION_DHT_SEED
|
FEDERATION_DHT_SEED=$FEDERATION_DHT_SEED
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "gradido-backend",
|
"name": "gradido-backend",
|
||||||
"version": "1.17.1",
|
"version": "1.18.1",
|
||||||
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
|
"description": "Gradido unified backend providing an API-Service for Gradido Transactions",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"repository": "https://github.com/gradido/gradido/backend",
|
"repository": "https://github.com/gradido/gradido/backend",
|
||||||
@ -72,5 +72,10 @@
|
|||||||
"ts-node": "^10.0.0",
|
"ts-node": "^10.0.0",
|
||||||
"tsconfig-paths": "^3.14.0",
|
"tsconfig-paths": "^3.14.0",
|
||||||
"typescript": "^4.3.4"
|
"typescript": "^4.3.4"
|
||||||
|
},
|
||||||
|
"nodemonConfig": {
|
||||||
|
"ignore": [
|
||||||
|
"**/*.test.ts"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ const constants = {
|
|||||||
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
|
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
|
||||||
CONFIG_VERSION: {
|
CONFIG_VERSION: {
|
||||||
DEFAULT: 'DEFAULT',
|
DEFAULT: 'DEFAULT',
|
||||||
EXPECTED: 'v14.2022-12-22',
|
EXPECTED: 'v15.2023-02-07',
|
||||||
CURRENT: '',
|
CURRENT: '',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -99,11 +99,6 @@ const webhook = {
|
|||||||
WEBHOOK_ELOPAGE_SECRET: process.env.WEBHOOK_ELOPAGE_SECRET || 'secret',
|
WEBHOOK_ELOPAGE_SECRET: process.env.WEBHOOK_ELOPAGE_SECRET || 'secret',
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventProtocol = {
|
|
||||||
// global switch to enable writing of EventProtocol-Entries
|
|
||||||
EVENT_PROTOCOL_DISABLED: process.env.EVENT_PROTOCOL_DISABLED === 'true' || false,
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is needed by graphql-directive-auth
|
// This is needed by graphql-directive-auth
|
||||||
process.env.APP_SECRET = server.JWT_SECRET
|
process.env.APP_SECRET = server.JWT_SECRET
|
||||||
|
|
||||||
@ -139,7 +134,6 @@ const CONFIG = {
|
|||||||
...email,
|
...email,
|
||||||
...loginServer,
|
...loginServer,
|
||||||
...webhook,
|
...webhook,
|
||||||
...eventProtocol,
|
|
||||||
...federation,
|
...federation,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import path from 'path'
|
|||||||
import { createTransport } from 'nodemailer'
|
import { createTransport } from 'nodemailer'
|
||||||
import Email from 'email-templates'
|
import Email from 'email-templates'
|
||||||
import i18n from 'i18n'
|
import i18n from 'i18n'
|
||||||
|
import LogError from '@/server/LogError'
|
||||||
|
|
||||||
export const sendEmailTranslated = async (params: {
|
export const sendEmailTranslated = async (params: {
|
||||||
receiver: {
|
receiver: {
|
||||||
@ -73,8 +74,7 @@ export const sendEmailTranslated = async (params: {
|
|||||||
logger.info('Result: ', result)
|
logger.info('Result: ', result)
|
||||||
})
|
})
|
||||||
.catch((error: unknown) => {
|
.catch((error: unknown) => {
|
||||||
logger.error('Error sending notification email: ', error)
|
throw new LogError('Error sending notification email', error)
|
||||||
throw new Error('Error sending notification email!')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
i18n.setLocale(rememberLocaleToRestore)
|
i18n.setLocale(rememberLocaleToRestore)
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
sendAccountMultiRegistrationEmail,
|
sendAccountMultiRegistrationEmail,
|
||||||
sendContributionConfirmedEmail,
|
sendContributionConfirmedEmail,
|
||||||
sendContributionDeniedEmail,
|
sendContributionDeniedEmail,
|
||||||
|
sendContributionDeletedEmail,
|
||||||
sendResetPasswordEmail,
|
sendResetPasswordEmail,
|
||||||
sendTransactionLinkRedeemedEmail,
|
sendTransactionLinkRedeemedEmail,
|
||||||
sendTransactionReceivedEmail,
|
sendTransactionReceivedEmail,
|
||||||
@ -438,6 +439,84 @@ describe('sendEmailVariants', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('sendContributionDeletedEmail', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
result = await sendContributionDeletedEmail({
|
||||||
|
firstName: 'Peter',
|
||||||
|
lastName: 'Lustig',
|
||||||
|
email: 'peter@lustig.de',
|
||||||
|
language: 'en',
|
||||||
|
senderFirstName: 'Bibi',
|
||||||
|
senderLastName: 'Bloxberg',
|
||||||
|
contributionMemo: 'My contribution.',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('calls "sendEmailTranslated"', () => {
|
||||||
|
it('with expected parameters', () => {
|
||||||
|
expect(sendEmailTranslated).toBeCalledWith({
|
||||||
|
receiver: {
|
||||||
|
to: 'Peter Lustig <peter@lustig.de>',
|
||||||
|
},
|
||||||
|
template: 'contributionDeleted',
|
||||||
|
locals: {
|
||||||
|
firstName: 'Peter',
|
||||||
|
lastName: 'Lustig',
|
||||||
|
locale: 'en',
|
||||||
|
senderFirstName: 'Bibi',
|
||||||
|
senderLastName: 'Bloxberg',
|
||||||
|
contributionMemo: 'My contribution.',
|
||||||
|
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||||
|
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
|
||||||
|
communityURL: CONFIG.COMMUNITY_URL,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has expected result', () => {
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
envelope: {
|
||||||
|
from: 'info@gradido.net',
|
||||||
|
to: ['peter@lustig.de'],
|
||||||
|
},
|
||||||
|
message: expect.any(String),
|
||||||
|
originalMessage: expect.objectContaining({
|
||||||
|
to: 'Peter Lustig <peter@lustig.de>',
|
||||||
|
from: 'Gradido (do not answer) <info@gradido.net>',
|
||||||
|
attachments: [],
|
||||||
|
subject: 'Gradido: Your common good contribution was deleted',
|
||||||
|
html: expect.any(String),
|
||||||
|
text: expect.stringContaining('GRADIDO: YOUR COMMON GOOD CONTRIBUTION WAS DELETED'),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
expect(result.originalMessage.html).toContain('<!DOCTYPE html>')
|
||||||
|
expect(result.originalMessage.html).toContain('<html lang="en">')
|
||||||
|
expect(result.originalMessage.html).toContain(
|
||||||
|
'<title>Gradido: Your common good contribution was deleted</title>',
|
||||||
|
)
|
||||||
|
expect(result.originalMessage.html).toContain(
|
||||||
|
'>Gradido: Your common good contribution was deleted</h1>',
|
||||||
|
)
|
||||||
|
expect(result.originalMessage.html).toContain('Hello Peter Lustig')
|
||||||
|
expect(result.originalMessage.html).toContain(
|
||||||
|
'Your public good contribution “My contribution.” was deleted by Bibi Bloxberg.',
|
||||||
|
)
|
||||||
|
expect(result.originalMessage.html).toContain(
|
||||||
|
'To see your common good contributions and related messages, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!',
|
||||||
|
)
|
||||||
|
expect(result.originalMessage.html).toContain(
|
||||||
|
`Link to your account: <a href="${CONFIG.EMAIL_LINK_OVERVIEW}">${CONFIG.EMAIL_LINK_OVERVIEW}</a>`,
|
||||||
|
)
|
||||||
|
expect(result.originalMessage.html).toContain('Please do not reply to this email!')
|
||||||
|
expect(result.originalMessage.html).toContain('Kind regards,<br>your Gradido team')
|
||||||
|
expect(result.originalMessage.html).toContain('—————')
|
||||||
|
expect(result.originalMessage.html).toContain(
|
||||||
|
'<div style="position: relative; left: -22px;"><img src="https://gdd.gradido.net/img/brand/green.png" width="200" alt="Gradido-Akademie Logo"></div><br>Gradido-Akademie<br>Institut für Wirtschaftsbionik<br>Pfarrweg 2<br>74653 Künzelsau<br>Deutschland<br><a href="mailto:support@supportmail.com">support@supportmail.com</a><br><a href="http://localhost/">http://localhost/</a>',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('sendResetPasswordEmail', () => {
|
describe('sendResetPasswordEmail', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
result = await sendResetPasswordEmail({
|
result = await sendResetPasswordEmail({
|
||||||
|
|||||||
@ -103,6 +103,32 @@ export const sendContributionConfirmedEmail = (data: {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const sendContributionDeletedEmail = (data: {
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
email: string
|
||||||
|
language: string
|
||||||
|
senderFirstName: string
|
||||||
|
senderLastName: string
|
||||||
|
contributionMemo: string
|
||||||
|
}): Promise<Record<string, unknown> | null> => {
|
||||||
|
return sendEmailTranslated({
|
||||||
|
receiver: { to: `${data.firstName} ${data.lastName} <${data.email}>` },
|
||||||
|
template: 'contributionDeleted',
|
||||||
|
locals: {
|
||||||
|
firstName: data.firstName,
|
||||||
|
lastName: data.lastName,
|
||||||
|
locale: data.language,
|
||||||
|
senderFirstName: data.senderFirstName,
|
||||||
|
senderLastName: data.senderLastName,
|
||||||
|
contributionMemo: data.contributionMemo,
|
||||||
|
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||||
|
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
|
||||||
|
communityURL: CONFIG.COMMUNITY_URL,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const sendContributionDeniedEmail = (data: {
|
export const sendContributionDeniedEmail = (data: {
|
||||||
firstName: string
|
firstName: string
|
||||||
lastName: string
|
lastName: string
|
||||||
|
|||||||
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,4 +1,3 @@
|
|||||||
import { EventProtocol } from '@entity/EventProtocol'
|
|
||||||
import decimal from 'decimal.js-light'
|
import decimal from 'decimal.js-light'
|
||||||
import { EventProtocolType } from './EventProtocolType'
|
import { EventProtocolType } from './EventProtocolType'
|
||||||
|
|
||||||
@ -68,6 +67,7 @@ export class EventTransactionReceiveRedeem extends EventBasicTxX {}
|
|||||||
export class EventContributionCreate extends EventBasicCt {}
|
export class EventContributionCreate extends EventBasicCt {}
|
||||||
export class EventAdminContributionCreate extends EventBasicCt {}
|
export class EventAdminContributionCreate extends EventBasicCt {}
|
||||||
export class EventAdminContributionDelete extends EventBasicCt {}
|
export class EventAdminContributionDelete extends EventBasicCt {}
|
||||||
|
export class EventAdminContributionDeny extends EventBasicCt {}
|
||||||
export class EventAdminContributionUpdate extends EventBasicCt {}
|
export class EventAdminContributionUpdate extends EventBasicCt {}
|
||||||
export class EventUserCreateContributionMessage extends EventBasicCtMsg {}
|
export class EventUserCreateContributionMessage extends EventBasicCtMsg {}
|
||||||
export class EventAdminCreateContributionMessage extends EventBasicCtMsg {}
|
export class EventAdminCreateContributionMessage extends EventBasicCtMsg {}
|
||||||
@ -87,21 +87,6 @@ export class EventDeleteContributionLink extends EventBasicCt {}
|
|||||||
export class EventUpdateContributionLink extends EventBasicCt {}
|
export class EventUpdateContributionLink extends EventBasicCt {}
|
||||||
|
|
||||||
export class Event {
|
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 {
|
public setEventBasic(): Event {
|
||||||
this.type = EventProtocolType.BASIC
|
this.type = EventProtocolType.BASIC
|
||||||
this.createdAt = new Date()
|
this.createdAt = new Date()
|
||||||
@ -314,6 +299,13 @@ export class Event {
|
|||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setEventAdminContributionDeny(ev: EventAdminContributionDeny): Event {
|
||||||
|
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
|
||||||
|
this.type = EventProtocolType.ADMIN_CONTRIBUTION_DENY
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
public setEventAdminContributionUpdate(ev: EventAdminContributionUpdate): Event {
|
public setEventAdminContributionUpdate(ev: EventAdminContributionUpdate): Event {
|
||||||
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
|
this.setByBasicCt(ev.userId, ev.contributionId, ev.amount)
|
||||||
this.type = EventProtocolType.ADMIN_CONTRIBUTION_UPDATE
|
this.type = EventProtocolType.ADMIN_CONTRIBUTION_UPDATE
|
||||||
|
|||||||
@ -1,41 +1,17 @@
|
|||||||
import { Event } from '@/event/Event'
|
import { Event } from '@/event/Event'
|
||||||
import { backendLogger as logger } from '@/server/logger'
|
import { backendLogger as logger } from '@/server/logger'
|
||||||
import { EventProtocol } from '@entity/EventProtocol'
|
import { EventProtocol } from '@entity/EventProtocol'
|
||||||
import CONFIG from '@/config'
|
|
||||||
|
|
||||||
class EventProtocolEmitter {
|
export const writeEvent = async (event: Event): Promise<EventProtocol | null> => {
|
||||||
/* }extends EventEmitter { */
|
logger.info('writeEvent', event)
|
||||||
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()
|
const dbEvent = new EventProtocol()
|
||||||
dbEvent.type = event.type
|
dbEvent.type = event.type
|
||||||
dbEvent.createdAt = event.createdAt
|
dbEvent.createdAt = event.createdAt
|
||||||
dbEvent.userId = event.userId
|
dbEvent.userId = event.userId
|
||||||
if (event.xUserId) dbEvent.xUserId = event.xUserId
|
dbEvent.xUserId = event.xUserId || null
|
||||||
if (event.xCommunityId) dbEvent.xCommunityId = event.xCommunityId
|
dbEvent.xCommunityId = event.xCommunityId || null
|
||||||
if (event.contributionId) dbEvent.contributionId = event.contributionId
|
dbEvent.contributionId = event.contributionId || null
|
||||||
if (event.transactionId) dbEvent.transactionId = event.transactionId
|
dbEvent.transactionId = event.transactionId || null
|
||||||
if (event.amount) dbEvent.amount = event.amount
|
dbEvent.amount = event.amount || null
|
||||||
await dbEvent.save()
|
return dbEvent.save()
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
export const eventProtocol = new EventProtocolEmitter()
|
|
||||||
|
|||||||
@ -35,6 +35,7 @@ export enum EventProtocolType {
|
|||||||
CONTRIBUTION_UPDATE = 'CONTRIBUTION_UPDATE',
|
CONTRIBUTION_UPDATE = 'CONTRIBUTION_UPDATE',
|
||||||
ADMIN_CONTRIBUTION_CREATE = 'ADMIN_CONTRIBUTION_CREATE',
|
ADMIN_CONTRIBUTION_CREATE = 'ADMIN_CONTRIBUTION_CREATE',
|
||||||
ADMIN_CONTRIBUTION_DELETE = 'ADMIN_CONTRIBUTION_DELETE',
|
ADMIN_CONTRIBUTION_DELETE = 'ADMIN_CONTRIBUTION_DELETE',
|
||||||
|
ADMIN_CONTRIBUTION_DENY = 'ADMIN_CONTRIBUTION_DENY',
|
||||||
ADMIN_CONTRIBUTION_UPDATE = 'ADMIN_CONTRIBUTION_UPDATE',
|
ADMIN_CONTRIBUTION_UPDATE = 'ADMIN_CONTRIBUTION_UPDATE',
|
||||||
USER_CREATE_CONTRIBUTION_MESSAGE = 'USER_CREATE_CONTRIBUTION_MESSAGE',
|
USER_CREATE_CONTRIBUTION_MESSAGE = 'USER_CREATE_CONTRIBUTION_MESSAGE',
|
||||||
ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE',
|
ADMIN_CREATE_CONTRIBUTION_MESSAGE = 'ADMIN_CREATE_CONTRIBUTION_MESSAGE',
|
||||||
|
|||||||
@ -15,6 +15,8 @@ import { calculateDecay } from '@/util/decay'
|
|||||||
import { RIGHTS } from '@/auth/RIGHTS'
|
import { RIGHTS } from '@/auth/RIGHTS'
|
||||||
import { GdtResolver } from './GdtResolver'
|
import { GdtResolver } from './GdtResolver'
|
||||||
|
|
||||||
|
import { getLastTransaction } from './util/getLastTransaction'
|
||||||
|
|
||||||
@Resolver()
|
@Resolver()
|
||||||
export class BalanceResolver {
|
export class BalanceResolver {
|
||||||
@Authorized([RIGHTS.BALANCE])
|
@Authorized([RIGHTS.BALANCE])
|
||||||
@ -32,7 +34,7 @@ export class BalanceResolver {
|
|||||||
|
|
||||||
const lastTransaction = context.lastTransaction
|
const lastTransaction = context.lastTransaction
|
||||||
? context.lastTransaction
|
? context.lastTransaction
|
||||||
: await dbTransaction.findOne({ userId: user.id }, { order: { id: 'DESC' } })
|
: await getLastTransaction(user.id)
|
||||||
|
|
||||||
logger.debug(`lastTransaction=${lastTransaction}`)
|
logger.debug(`lastTransaction=${lastTransaction}`)
|
||||||
|
|
||||||
|
|||||||
@ -246,6 +246,7 @@ describe('Contribution Links', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('returns an error if missing startDate', async () => {
|
it('returns an error if missing startDate', async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: createContributionLink,
|
mutation: createContributionLink,
|
||||||
@ -270,6 +271,7 @@ describe('Contribution Links', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('returns an error if missing endDate', async () => {
|
it('returns an error if missing endDate', async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: createContributionLink,
|
mutation: createContributionLink,
|
||||||
@ -292,6 +294,7 @@ describe('Contribution Links', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('returns an error if endDate is before startDate', async () => {
|
it('returns an error if endDate is before startDate', async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: createContributionLink,
|
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 () => {
|
it('returns an error if name is shorter than 5 characters', async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: createContributionLink,
|
mutation: createContributionLink,
|
||||||
@ -347,22 +331,17 @@ describe('Contribution Links', () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [
|
errors: [new GraphQLError('The value of name is too short')],
|
||||||
new GraphQLError(
|
|
||||||
`The value of 'name' with a length of 3 did not fulfill the requested bounderies min=5 and max=100`,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('logs the error thrown', () => {
|
it('logs the error thrown', () => {
|
||||||
expect(logger.error).toBeCalledWith(
|
expect(logger.error).toBeCalledWith('The value of name is too short', 3)
|
||||||
`The value of 'name' with a length of 3 did not fulfill the requested bounderies min=5 and max=100`,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns an error if name is longer than 100 characters', async () => {
|
it('returns an error if name is longer than 100 characters', async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: createContributionLink,
|
mutation: createContributionLink,
|
||||||
@ -373,42 +352,17 @@ describe('Contribution Links', () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [
|
errors: [new GraphQLError('The value of name is too long')],
|
||||||
new GraphQLError(
|
|
||||||
`The value of 'name' with a length of 101 did not fulfill the requested bounderies min=5 and max=100`,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('logs the error thrown', () => {
|
it('logs the error thrown', () => {
|
||||||
expect(logger.error).toBeCalledWith(
|
expect(logger.error).toBeCalledWith('The value of name is too long', 101)
|
||||||
`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!')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns an error if memo is shorter than 5 characters', async () => {
|
it('returns an error if memo is shorter than 5 characters', async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: createContributionLink,
|
mutation: createContributionLink,
|
||||||
@ -419,22 +373,17 @@ describe('Contribution Links', () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [
|
errors: [new GraphQLError('The value of memo is too short')],
|
||||||
new GraphQLError(
|
|
||||||
`The value of 'memo' with a length of 3 did not fulfill the requested bounderies min=5 and max=255`,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('logs the error thrown', () => {
|
it('logs the error thrown', () => {
|
||||||
expect(logger.error).toBeCalledWith(
|
expect(logger.error).toBeCalledWith('The value of memo is too short', 3)
|
||||||
`The value of 'memo' with a length of 3 did not fulfill the requested bounderies min=5 and max=255`,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns an error if memo is longer than 255 characters', async () => {
|
it('returns an error if memo is longer than 255 characters', async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: createContributionLink,
|
mutation: createContributionLink,
|
||||||
@ -445,22 +394,17 @@ describe('Contribution Links', () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [
|
errors: [new GraphQLError('The value of memo is too long')],
|
||||||
new GraphQLError(
|
|
||||||
`The value of 'memo' with a length of 256 did not fulfill the requested bounderies min=5 and max=255`,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('logs the error thrown', () => {
|
it('logs the error thrown', () => {
|
||||||
expect(logger.error).toBeCalledWith(
|
expect(logger.error).toBeCalledWith('The value of memo is too long', 256)
|
||||||
`The value of 'memo' with a length of 256 did not fulfill the requested bounderies min=5 and max=255`,
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns an error if amount is not positive', async () => {
|
it('returns an error if amount is not positive', async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: createContributionLink,
|
mutation: createContributionLink,
|
||||||
@ -471,15 +415,13 @@ describe('Contribution Links', () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
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', () => {
|
it('logs the error thrown', () => {
|
||||||
expect(logger.error).toBeCalledWith(
|
expect(logger.error).toBeCalledWith('The amount must be a positiv value', new Decimal(0))
|
||||||
'The amount=0 must be initialized with a positiv value!',
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -530,14 +472,14 @@ describe('Contribution Links', () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [new GraphQLError('Contribution Link not found to given id.')],
|
errors: [new GraphQLError('Contribution Link not found')],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('logs the error thrown', () => {
|
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', () => {
|
describe('valid id', () => {
|
||||||
@ -601,13 +543,13 @@ describe('Contribution Links', () => {
|
|||||||
mutate({ mutation: deleteContributionLink, variables: { id: -1 } }),
|
mutate({ mutation: deleteContributionLink, variables: { id: -1 } }),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [new GraphQLError('Contribution Link not found to given id.')],
|
errors: [new GraphQLError('Contribution Link not found')],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('logs the error thrown', () => {
|
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
|
// TODO: this is a strange construct
|
||||||
import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver'
|
import { transactionLinkCode as contributionLinkCode } from './TransactionLinkResolver'
|
||||||
|
import LogError from '@/server/LogError'
|
||||||
|
|
||||||
@Resolver()
|
@Resolver()
|
||||||
export class ContributionLinkResolver {
|
export class ContributionLinkResolver {
|
||||||
@ -39,35 +40,22 @@ export class ContributionLinkResolver {
|
|||||||
}: ContributionLinkArgs,
|
}: ContributionLinkArgs,
|
||||||
): Promise<ContributionLink> {
|
): Promise<ContributionLink> {
|
||||||
isStartEndDateValid(validFrom, validTo)
|
isStartEndDateValid(validFrom, validTo)
|
||||||
if (!name) {
|
if (name.length < CONTRIBUTIONLINK_NAME_MIN_CHARS) {
|
||||||
logger.error(`The name must be initialized!`)
|
throw new LogError('The value of name is too short', name.length)
|
||||||
throw new Error(`The name must be initialized!`)
|
|
||||||
}
|
}
|
||||||
if (
|
if (name.length > CONTRIBUTIONLINK_NAME_MAX_CHARS) {
|
||||||
name.length < CONTRIBUTIONLINK_NAME_MIN_CHARS ||
|
throw new LogError('The value of name is too long', name.length)
|
||||||
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 (!memo) {
|
if (memo.length < MEMO_MIN_CHARS) {
|
||||||
logger.error(`The memo must be initialized!`)
|
throw new LogError('The value of memo is too short', memo.length)
|
||||||
throw new Error(`The memo must be initialized!`)
|
|
||||||
}
|
}
|
||||||
if (memo.length < MEMO_MIN_CHARS || memo.length > MEMO_MAX_CHARS) {
|
if (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}`
|
throw new LogError('The value of memo is too long', memo.length)
|
||||||
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 (!new Decimal(amount).isPositive()) {
|
if (!new Decimal(amount).isPositive()) {
|
||||||
logger.error(`The amount=${amount} must be initialized with a positiv value!`)
|
throw new LogError('The amount must be a positiv value', amount)
|
||||||
throw new Error(`The amount=${amount} must be initialized with a positiv value!`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const dbContributionLink = new DbContributionLink()
|
const dbContributionLink = new DbContributionLink()
|
||||||
dbContributionLink.amount = amount
|
dbContributionLink.amount = amount
|
||||||
dbContributionLink.name = name
|
dbContributionLink.name = name
|
||||||
@ -107,8 +95,7 @@ export class ContributionLinkResolver {
|
|||||||
async deleteContributionLink(@Arg('id', () => Int) id: number): Promise<Date | null> {
|
async deleteContributionLink(@Arg('id', () => Int) id: number): Promise<Date | null> {
|
||||||
const contributionLink = await DbContributionLink.findOne(id)
|
const contributionLink = await DbContributionLink.findOne(id)
|
||||||
if (!contributionLink) {
|
if (!contributionLink) {
|
||||||
logger.error(`Contribution Link not found to given id: ${id}`)
|
throw new LogError('Contribution Link not found', id)
|
||||||
throw new Error('Contribution Link not found to given id.')
|
|
||||||
}
|
}
|
||||||
await contributionLink.softRemove()
|
await contributionLink.softRemove()
|
||||||
logger.debug(`deleteContributionLink successful!`)
|
logger.debug(`deleteContributionLink successful!`)
|
||||||
@ -134,8 +121,7 @@ export class ContributionLinkResolver {
|
|||||||
): Promise<ContributionLink> {
|
): Promise<ContributionLink> {
|
||||||
const dbContributionLink = await DbContributionLink.findOne(id)
|
const dbContributionLink = await DbContributionLink.findOne(id)
|
||||||
if (!dbContributionLink) {
|
if (!dbContributionLink) {
|
||||||
logger.error(`Contribution Link not found to given id: ${id}`)
|
throw new LogError('Contribution Link not found', id)
|
||||||
throw new Error('Contribution Link not found to given id.')
|
|
||||||
}
|
}
|
||||||
dbContributionLink.amount = amount
|
dbContributionLink.amount = amount
|
||||||
dbContributionLink.name = name
|
dbContributionLink.name = name
|
||||||
|
|||||||
@ -88,6 +88,7 @@ describe('ContributionMessageResolver', () => {
|
|||||||
|
|
||||||
describe('input not valid', () => {
|
describe('input not valid', () => {
|
||||||
it('throws error when contribution does not exist', async () => {
|
it('throws error when contribution does not exist', async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: adminCreateContributionMessage,
|
mutation: adminCreateContributionMessage,
|
||||||
@ -100,14 +101,22 @@ describe('ContributionMessageResolver', () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [
|
errors: [
|
||||||
new GraphQLError(
|
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 () => {
|
it('throws error when contribution.userId equals user.id', async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
await mutate({
|
await mutate({
|
||||||
mutation: login,
|
mutation: login,
|
||||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||||
@ -132,12 +141,19 @@ describe('ContributionMessageResolver', () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [
|
errors: [
|
||||||
new GraphQLError(
|
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', () => {
|
describe('valid input', () => {
|
||||||
@ -210,6 +226,7 @@ describe('ContributionMessageResolver', () => {
|
|||||||
|
|
||||||
describe('input not valid', () => {
|
describe('input not valid', () => {
|
||||||
it('throws error when contribution does not exist', async () => {
|
it('throws error when contribution does not exist', async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
await expect(
|
await expect(
|
||||||
mutate({
|
mutate({
|
||||||
mutation: createContributionMessage,
|
mutation: createContributionMessage,
|
||||||
@ -222,14 +239,22 @@ describe('ContributionMessageResolver', () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [
|
errors: [
|
||||||
new GraphQLError(
|
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 () => {
|
it('throws error when other user tries to send createContributionMessage', async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
await mutate({
|
await mutate({
|
||||||
mutation: login,
|
mutation: login,
|
||||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||||
@ -246,12 +271,19 @@ describe('ContributionMessageResolver', () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [
|
errors: [
|
||||||
new GraphQLError(
|
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', () => {
|
describe('valid input', () => {
|
||||||
|
|||||||
@ -12,10 +12,10 @@ import { ContributionStatus } from '@enum/ContributionStatus'
|
|||||||
import { Order } from '@enum/Order'
|
import { Order } from '@enum/Order'
|
||||||
import Paginated from '@arg/Paginated'
|
import Paginated from '@arg/Paginated'
|
||||||
|
|
||||||
import { backendLogger as logger } from '@/server/logger'
|
|
||||||
import { RIGHTS } from '@/auth/RIGHTS'
|
import { RIGHTS } from '@/auth/RIGHTS'
|
||||||
import { Context, getUser } from '@/server/context'
|
import { Context, getUser } from '@/server/context'
|
||||||
import { sendAddedContributionMessageEmail } from '@/emails/sendEmailVariants'
|
import { sendAddedContributionMessageEmail } from '@/emails/sendEmailVariants'
|
||||||
|
import LogError from '@/server/LogError'
|
||||||
|
|
||||||
@Resolver()
|
@Resolver()
|
||||||
export class ContributionMessageResolver {
|
export class ContributionMessageResolver {
|
||||||
@ -54,8 +54,7 @@ export class ContributionMessageResolver {
|
|||||||
await queryRunner.commitTransaction()
|
await queryRunner.commitTransaction()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await queryRunner.rollbackTransaction()
|
await queryRunner.rollbackTransaction()
|
||||||
logger.error(`ContributionMessage was not successful: ${e}`)
|
throw new LogError(`ContributionMessage was not sent successfully: ${e}`, e)
|
||||||
throw new Error(`ContributionMessage was not successful: ${e}`)
|
|
||||||
} finally {
|
} finally {
|
||||||
await queryRunner.release()
|
await queryRunner.release()
|
||||||
}
|
}
|
||||||
@ -95,9 +94,7 @@ export class ContributionMessageResolver {
|
|||||||
@Ctx() context: Context,
|
@Ctx() context: Context,
|
||||||
): Promise<ContributionMessage> {
|
): Promise<ContributionMessage> {
|
||||||
const user = getUser(context)
|
const user = getUser(context)
|
||||||
if (!user.emailContact) {
|
|
||||||
user.emailContact = await UserContact.findOneOrFail({ where: { id: user.emailId } })
|
|
||||||
}
|
|
||||||
const queryRunner = getConnection().createQueryRunner()
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
await queryRunner.startTransaction('REPEATABLE READ')
|
await queryRunner.startTransaction('REPEATABLE READ')
|
||||||
@ -108,12 +105,10 @@ export class ContributionMessageResolver {
|
|||||||
relations: ['user'],
|
relations: ['user'],
|
||||||
})
|
})
|
||||||
if (!contribution) {
|
if (!contribution) {
|
||||||
logger.error('Contribution not found')
|
throw new LogError('Contribution not found', contributionId)
|
||||||
throw new Error('Contribution not found')
|
|
||||||
}
|
}
|
||||||
if (contribution.userId === user.id) {
|
if (contribution.userId === user.id) {
|
||||||
logger.error('Admin can not answer on own contribution')
|
throw new LogError('Admin can not answer on his own contribution', contributionId)
|
||||||
throw new Error('Admin can not answer on own contribution')
|
|
||||||
}
|
}
|
||||||
if (!contribution.user.emailContact) {
|
if (!contribution.user.emailContact) {
|
||||||
contribution.user.emailContact = await UserContact.findOneOrFail({
|
contribution.user.emailContact = await UserContact.findOneOrFail({
|
||||||
@ -149,8 +144,7 @@ export class ContributionMessageResolver {
|
|||||||
await queryRunner.commitTransaction()
|
await queryRunner.commitTransaction()
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await queryRunner.rollbackTransaction()
|
await queryRunner.rollbackTransaction()
|
||||||
logger.error(`ContributionMessage was not successful: ${e}`)
|
throw new LogError(`ContributionMessage was not sent successfully: ${e}`, e)
|
||||||
throw new Error(`ContributionMessage was not successful: ${e}`)
|
|
||||||
} finally {
|
} finally {
|
||||||
await queryRunner.release()
|
await queryRunner.release()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,11 +22,7 @@ import {
|
|||||||
listContributions,
|
listContributions,
|
||||||
listUnconfirmedContributions,
|
listUnconfirmedContributions,
|
||||||
} from '@/seeds/graphql/queries'
|
} from '@/seeds/graphql/queries'
|
||||||
import {
|
import { sendContributionConfirmedEmail } from '@/emails/sendEmailVariants'
|
||||||
// sendAccountActivationEmail,
|
|
||||||
sendContributionConfirmedEmail,
|
|
||||||
// sendContributionRejectedEmail,
|
|
||||||
} from '@/emails/sendEmailVariants'
|
|
||||||
import {
|
import {
|
||||||
cleanDB,
|
cleanDB,
|
||||||
resetToken,
|
resetToken,
|
||||||
@ -45,8 +41,8 @@ import { Transaction as DbTransaction } from '@entity/Transaction'
|
|||||||
import { User } from '@entity/User'
|
import { User } from '@entity/User'
|
||||||
import { EventProtocolType } from '@/event/EventProtocolType'
|
import { EventProtocolType } from '@/event/EventProtocolType'
|
||||||
import { logger, i18n as localization } from '@test/testSetup'
|
import { logger, i18n as localization } from '@test/testSetup'
|
||||||
|
import { UserInputError } from 'apollo-server-express'
|
||||||
|
|
||||||
// mock account activation email to avoid console spam
|
|
||||||
// mock account activation email to avoid console spam
|
// mock account activation email to avoid console spam
|
||||||
jest.mock('@/emails/sendEmailVariants', () => {
|
jest.mock('@/emails/sendEmailVariants', () => {
|
||||||
const originalModule = jest.requireActual('@/emails/sendEmailVariants')
|
const originalModule = jest.requireActual('@/emails/sendEmailVariants')
|
||||||
@ -681,7 +677,7 @@ describe('ContributionResolver', () => {
|
|||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 25,
|
pageSize: 25,
|
||||||
order: 'DESC',
|
order: 'DESC',
|
||||||
filterConfirmed: false,
|
statusFilter: null,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
@ -718,7 +714,76 @@ describe('ContributionResolver', () => {
|
|||||||
resetToken()
|
resetToken()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('returns allCreation', async () => {
|
it('throws an error with "NOT_VALID" in statusFilter', async () => {
|
||||||
|
await expect(
|
||||||
|
query({
|
||||||
|
query: listAllContributions,
|
||||||
|
variables: {
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 25,
|
||||||
|
order: 'DESC',
|
||||||
|
statusFilter: ['NOT_VALID'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [
|
||||||
|
new UserInputError(
|
||||||
|
'Variable "$statusFilter" got invalid value "NOT_VALID" at "statusFilter[0]"; Value "NOT_VALID" does not exist in "ContributionStatus" enum.',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws an error with a null in statusFilter', async () => {
|
||||||
|
await expect(
|
||||||
|
query({
|
||||||
|
query: listAllContributions,
|
||||||
|
variables: {
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 25,
|
||||||
|
order: 'DESC',
|
||||||
|
statusFilter: [null],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [
|
||||||
|
new UserInputError(
|
||||||
|
'Variable "$statusFilter" got invalid value null at "statusFilter[0]"; Expected non-nullable type "ContributionStatus!" not to be null.',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws an error with null and "NOT_VALID" in statusFilter', async () => {
|
||||||
|
await expect(
|
||||||
|
query({
|
||||||
|
query: listAllContributions,
|
||||||
|
variables: {
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 25,
|
||||||
|
order: 'DESC',
|
||||||
|
statusFilter: [null, 'NOT_VALID'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [
|
||||||
|
new UserInputError(
|
||||||
|
'Variable "$statusFilter" got invalid value null at "statusFilter[0]"; Expected non-nullable type "ContributionStatus!" not to be null.',
|
||||||
|
),
|
||||||
|
new UserInputError(
|
||||||
|
'Variable "$statusFilter" got invalid value "NOT_VALID" at "statusFilter[1]"; Value "NOT_VALID" does not exist in "ContributionStatus" enum.',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns all contributions without statusFilter', async () => {
|
||||||
await expect(
|
await expect(
|
||||||
query({
|
query({
|
||||||
query: listAllContributions,
|
query: listAllContributions,
|
||||||
@ -726,7 +791,6 @@ describe('ContributionResolver', () => {
|
|||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 25,
|
pageSize: 25,
|
||||||
order: 'DESC',
|
order: 'DESC',
|
||||||
filterConfirmed: false,
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
@ -737,11 +801,301 @@ describe('ContributionResolver', () => {
|
|||||||
contributionList: expect.arrayContaining([
|
contributionList: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: expect.any(Number),
|
id: expect.any(Number),
|
||||||
|
state: 'CONFIRMED',
|
||||||
memo: 'Herzlich Willkommen bei Gradido!',
|
memo: 'Herzlich Willkommen bei Gradido!',
|
||||||
amount: '1000',
|
amount: '1000',
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: expect.any(Number),
|
id: expect.any(Number),
|
||||||
|
state: 'PENDING',
|
||||||
|
memo: 'Test env contribution',
|
||||||
|
amount: '100',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns all contributions for statusFilter = null', async () => {
|
||||||
|
await expect(
|
||||||
|
query({
|
||||||
|
query: listAllContributions,
|
||||||
|
variables: {
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 25,
|
||||||
|
order: 'DESC',
|
||||||
|
statusFilter: null,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
listAllContributions: {
|
||||||
|
contributionCount: 2,
|
||||||
|
contributionList: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
state: 'CONFIRMED',
|
||||||
|
memo: 'Herzlich Willkommen bei Gradido!',
|
||||||
|
amount: '1000',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
state: 'PENDING',
|
||||||
|
memo: 'Test env contribution',
|
||||||
|
amount: '100',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns all contributions for statusFilter = []', async () => {
|
||||||
|
await expect(
|
||||||
|
query({
|
||||||
|
query: listAllContributions,
|
||||||
|
variables: {
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 25,
|
||||||
|
order: 'DESC',
|
||||||
|
statusFilter: [],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
listAllContributions: {
|
||||||
|
contributionCount: 2,
|
||||||
|
contributionList: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
state: 'CONFIRMED',
|
||||||
|
memo: 'Herzlich Willkommen bei Gradido!',
|
||||||
|
amount: '1000',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
state: 'PENDING',
|
||||||
|
memo: 'Test env contribution',
|
||||||
|
amount: '100',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns all CONFIRMED contributions', async () => {
|
||||||
|
await expect(
|
||||||
|
query({
|
||||||
|
query: listAllContributions,
|
||||||
|
variables: {
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 25,
|
||||||
|
order: 'DESC',
|
||||||
|
statusFilter: ['CONFIRMED'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
listAllContributions: {
|
||||||
|
contributionCount: 1,
|
||||||
|
contributionList: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
state: 'CONFIRMED',
|
||||||
|
memo: 'Herzlich Willkommen bei Gradido!',
|
||||||
|
amount: '1000',
|
||||||
|
}),
|
||||||
|
expect.not.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
state: 'PENDING',
|
||||||
|
memo: 'Test env contribution',
|
||||||
|
amount: '100',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns all PENDING contributions', async () => {
|
||||||
|
await expect(
|
||||||
|
query({
|
||||||
|
query: listAllContributions,
|
||||||
|
variables: {
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 25,
|
||||||
|
order: 'DESC',
|
||||||
|
statusFilter: ['PENDING'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
listAllContributions: {
|
||||||
|
contributionCount: 1,
|
||||||
|
contributionList: expect.arrayContaining([
|
||||||
|
expect.not.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
state: 'CONFIRMED',
|
||||||
|
memo: 'Herzlich Willkommen bei Gradido!',
|
||||||
|
amount: '1000',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
state: 'PENDING',
|
||||||
|
memo: 'Test env contribution',
|
||||||
|
amount: '100',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns all IN_PROGRESS Creation', async () => {
|
||||||
|
await expect(
|
||||||
|
query({
|
||||||
|
query: listAllContributions,
|
||||||
|
variables: {
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 25,
|
||||||
|
order: 'DESC',
|
||||||
|
statusFilter: ['IN_PROGRESS'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
listAllContributions: {
|
||||||
|
contributionCount: 0,
|
||||||
|
contributionList: expect.not.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
state: 'CONFIRMED',
|
||||||
|
memo: 'Herzlich Willkommen bei Gradido!',
|
||||||
|
amount: '1000',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
state: 'PENDING',
|
||||||
|
memo: 'Test env contribution',
|
||||||
|
amount: '100',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns all DENIED Creation', async () => {
|
||||||
|
await expect(
|
||||||
|
query({
|
||||||
|
query: listAllContributions,
|
||||||
|
variables: {
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 25,
|
||||||
|
order: 'DESC',
|
||||||
|
statusFilter: ['DENIED'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
listAllContributions: {
|
||||||
|
contributionCount: 0,
|
||||||
|
contributionList: expect.not.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
state: 'CONFIRMED',
|
||||||
|
memo: 'Herzlich Willkommen bei Gradido!',
|
||||||
|
amount: '1000',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
state: 'PENDING',
|
||||||
|
memo: 'Test env contribution',
|
||||||
|
amount: '100',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns all DELETED Creation', async () => {
|
||||||
|
await expect(
|
||||||
|
query({
|
||||||
|
query: listAllContributions,
|
||||||
|
variables: {
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 25,
|
||||||
|
order: 'DESC',
|
||||||
|
statusFilter: ['DELETED'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
listAllContributions: {
|
||||||
|
contributionCount: 0,
|
||||||
|
contributionList: expect.not.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
state: 'CONFIRMED',
|
||||||
|
memo: 'Herzlich Willkommen bei Gradido!',
|
||||||
|
amount: '1000',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
state: 'PENDING',
|
||||||
|
memo: 'Test env contribution',
|
||||||
|
amount: '100',
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns all CONFIRMED and PENDING Creation', async () => {
|
||||||
|
await expect(
|
||||||
|
query({
|
||||||
|
query: listAllContributions,
|
||||||
|
variables: {
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: 25,
|
||||||
|
order: 'DESC',
|
||||||
|
statusFilter: ['CONFIRMED', 'PENDING'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
listAllContributions: {
|
||||||
|
contributionCount: 2,
|
||||||
|
contributionList: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
state: 'CONFIRMED',
|
||||||
|
memo: 'Herzlich Willkommen bei Gradido!',
|
||||||
|
amount: '1000',
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
state: 'PENDING',
|
||||||
memo: 'Test env contribution',
|
memo: 'Test env contribution',
|
||||||
amount: '100',
|
amount: '100',
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -44,16 +44,20 @@ import {
|
|||||||
EventContributionConfirm,
|
EventContributionConfirm,
|
||||||
EventAdminContributionCreate,
|
EventAdminContributionCreate,
|
||||||
EventAdminContributionDelete,
|
EventAdminContributionDelete,
|
||||||
|
EventAdminContributionDeny,
|
||||||
EventAdminContributionUpdate,
|
EventAdminContributionUpdate,
|
||||||
} from '@/event/Event'
|
} from '@/event/Event'
|
||||||
import { eventProtocol } from '@/event/EventProtocolEmitter'
|
import { writeEvent } from '@/event/EventProtocolEmitter'
|
||||||
import { calculateDecay } from '@/util/decay'
|
import { calculateDecay } from '@/util/decay'
|
||||||
import {
|
import {
|
||||||
sendContributionConfirmedEmail,
|
sendContributionConfirmedEmail,
|
||||||
|
sendContributionDeletedEmail,
|
||||||
sendContributionDeniedEmail,
|
sendContributionDeniedEmail,
|
||||||
} from '@/emails/sendEmailVariants'
|
} from '@/emails/sendEmailVariants'
|
||||||
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
|
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
|
||||||
|
|
||||||
|
import { getLastTransaction } from './util/getLastTransaction'
|
||||||
|
|
||||||
@Resolver()
|
@Resolver()
|
||||||
export class ContributionResolver {
|
export class ContributionResolver {
|
||||||
@Authorized([RIGHTS.CREATE_CONTRIBUTION])
|
@Authorized([RIGHTS.CREATE_CONTRIBUTION])
|
||||||
@ -97,7 +101,7 @@ export class ContributionResolver {
|
|||||||
eventCreateContribution.userId = user.id
|
eventCreateContribution.userId = user.id
|
||||||
eventCreateContribution.amount = amount
|
eventCreateContribution.amount = amount
|
||||||
eventCreateContribution.contributionId = contribution.id
|
eventCreateContribution.contributionId = contribution.id
|
||||||
await eventProtocol.writeEvent(event.setEventContributionCreate(eventCreateContribution))
|
await writeEvent(event.setEventContributionCreate(eventCreateContribution))
|
||||||
|
|
||||||
return new UnconfirmedContribution(contribution, user, creations)
|
return new UnconfirmedContribution(contribution, user, creations)
|
||||||
}
|
}
|
||||||
@ -133,7 +137,7 @@ export class ContributionResolver {
|
|||||||
eventDeleteContribution.userId = user.id
|
eventDeleteContribution.userId = user.id
|
||||||
eventDeleteContribution.contributionId = contribution.id
|
eventDeleteContribution.contributionId = contribution.id
|
||||||
eventDeleteContribution.amount = contribution.amount
|
eventDeleteContribution.amount = contribution.amount
|
||||||
await eventProtocol.writeEvent(event.setEventContributionDelete(eventDeleteContribution))
|
await writeEvent(event.setEventContributionDelete(eventDeleteContribution))
|
||||||
|
|
||||||
const res = await contribution.softRemove()
|
const res = await contribution.softRemove()
|
||||||
return !!res
|
return !!res
|
||||||
@ -179,12 +183,23 @@ export class ContributionResolver {
|
|||||||
async listAllContributions(
|
async listAllContributions(
|
||||||
@Args()
|
@Args()
|
||||||
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
|
{ currentPage = 1, pageSize = 5, order = Order.DESC }: Paginated,
|
||||||
|
@Arg('statusFilter', () => [ContributionStatus], { nullable: true })
|
||||||
|
statusFilter?: ContributionStatus[],
|
||||||
): Promise<ContributionListResult> {
|
): Promise<ContributionListResult> {
|
||||||
|
const where: {
|
||||||
|
contributionStatus?: FindOperator<string> | null
|
||||||
|
} = {}
|
||||||
|
|
||||||
|
if (statusFilter && statusFilter.length) {
|
||||||
|
where.contributionStatus = In(statusFilter)
|
||||||
|
}
|
||||||
|
|
||||||
const [dbContributions, count] = await getConnection()
|
const [dbContributions, count] = await getConnection()
|
||||||
.createQueryBuilder()
|
.createQueryBuilder()
|
||||||
.select('c')
|
.select('c')
|
||||||
.from(DbContribution, 'c')
|
.from(DbContribution, 'c')
|
||||||
.innerJoinAndSelect('c.user', 'u')
|
.innerJoinAndSelect('c.user', 'u')
|
||||||
|
.where(where)
|
||||||
.orderBy('c.createdAt', order)
|
.orderBy('c.createdAt', order)
|
||||||
.limit(pageSize)
|
.limit(pageSize)
|
||||||
.offset((currentPage - 1) * pageSize)
|
.offset((currentPage - 1) * pageSize)
|
||||||
@ -279,7 +294,7 @@ export class ContributionResolver {
|
|||||||
eventUpdateContribution.userId = user.id
|
eventUpdateContribution.userId = user.id
|
||||||
eventUpdateContribution.contributionId = contributionId
|
eventUpdateContribution.contributionId = contributionId
|
||||||
eventUpdateContribution.amount = amount
|
eventUpdateContribution.amount = amount
|
||||||
await eventProtocol.writeEvent(event.setEventContributionUpdate(eventUpdateContribution))
|
await writeEvent(event.setEventContributionUpdate(eventUpdateContribution))
|
||||||
|
|
||||||
return new UnconfirmedContribution(contributionToUpdate, user, creations)
|
return new UnconfirmedContribution(contributionToUpdate, user, creations)
|
||||||
}
|
}
|
||||||
@ -346,9 +361,7 @@ export class ContributionResolver {
|
|||||||
eventAdminCreateContribution.userId = moderator.id
|
eventAdminCreateContribution.userId = moderator.id
|
||||||
eventAdminCreateContribution.amount = amount
|
eventAdminCreateContribution.amount = amount
|
||||||
eventAdminCreateContribution.contributionId = contribution.id
|
eventAdminCreateContribution.contributionId = contribution.id
|
||||||
await eventProtocol.writeEvent(
|
await writeEvent(event.setEventAdminContributionCreate(eventAdminCreateContribution))
|
||||||
event.setEventAdminContributionCreate(eventAdminCreateContribution),
|
|
||||||
)
|
|
||||||
|
|
||||||
return getUserCreation(emailContact.userId, clientTimezoneOffset)
|
return getUserCreation(emailContact.userId, clientTimezoneOffset)
|
||||||
}
|
}
|
||||||
@ -458,9 +471,7 @@ export class ContributionResolver {
|
|||||||
eventAdminContributionUpdate.userId = user.id
|
eventAdminContributionUpdate.userId = user.id
|
||||||
eventAdminContributionUpdate.amount = amount
|
eventAdminContributionUpdate.amount = amount
|
||||||
eventAdminContributionUpdate.contributionId = contributionToUpdate.id
|
eventAdminContributionUpdate.contributionId = contributionToUpdate.id
|
||||||
await eventProtocol.writeEvent(
|
await writeEvent(event.setEventAdminContributionUpdate(eventAdminContributionUpdate))
|
||||||
event.setEventAdminContributionUpdate(eventAdminContributionUpdate),
|
|
||||||
)
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@ -538,10 +549,8 @@ export class ContributionResolver {
|
|||||||
eventAdminContributionDelete.userId = contribution.userId
|
eventAdminContributionDelete.userId = contribution.userId
|
||||||
eventAdminContributionDelete.amount = contribution.amount
|
eventAdminContributionDelete.amount = contribution.amount
|
||||||
eventAdminContributionDelete.contributionId = contribution.id
|
eventAdminContributionDelete.contributionId = contribution.id
|
||||||
await eventProtocol.writeEvent(
|
await writeEvent(event.setEventAdminContributionDelete(eventAdminContributionDelete))
|
||||||
event.setEventAdminContributionDelete(eventAdminContributionDelete),
|
sendContributionDeletedEmail({
|
||||||
)
|
|
||||||
sendContributionDeniedEmail({
|
|
||||||
firstName: user.firstName,
|
firstName: user.firstName,
|
||||||
lastName: user.lastName,
|
lastName: user.lastName,
|
||||||
email: user.emailContact.email,
|
email: user.emailContact.email,
|
||||||
@ -602,16 +611,11 @@ export class ContributionResolver {
|
|||||||
const queryRunner = getConnection().createQueryRunner()
|
const queryRunner = getConnection().createQueryRunner()
|
||||||
await queryRunner.connect()
|
await queryRunner.connect()
|
||||||
await queryRunner.startTransaction('REPEATABLE READ') // 'READ COMMITTED')
|
await queryRunner.startTransaction('REPEATABLE READ') // 'READ COMMITTED')
|
||||||
try {
|
|
||||||
const lastTransaction = await queryRunner.manager
|
const lastTransaction = await getLastTransaction(contribution.userId)
|
||||||
.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')
|
logger.info('lastTransaction ID', lastTransaction ? lastTransaction.id : 'undefined')
|
||||||
|
|
||||||
|
try {
|
||||||
let newBalance = new Decimal(0)
|
let newBalance = new Decimal(0)
|
||||||
let decay: Decay | null = null
|
let decay: Decay | null = null
|
||||||
if (lastTransaction) {
|
if (lastTransaction) {
|
||||||
@ -668,7 +672,7 @@ export class ContributionResolver {
|
|||||||
eventContributionConfirm.userId = user.id
|
eventContributionConfirm.userId = user.id
|
||||||
eventContributionConfirm.amount = contribution.amount
|
eventContributionConfirm.amount = contribution.amount
|
||||||
eventContributionConfirm.contributionId = contribution.id
|
eventContributionConfirm.contributionId = contribution.id
|
||||||
await eventProtocol.writeEvent(event.setEventContributionConfirm(eventContributionConfirm))
|
await writeEvent(event.setEventContributionConfirm(eventContributionConfirm))
|
||||||
} finally {
|
} finally {
|
||||||
releaseLock()
|
releaseLock()
|
||||||
}
|
}
|
||||||
@ -762,6 +766,13 @@ export class ContributionResolver {
|
|||||||
contributionToUpdate.deniedAt = new Date()
|
contributionToUpdate.deniedAt = new Date()
|
||||||
const res = await contributionToUpdate.save()
|
const res = await contributionToUpdate.save()
|
||||||
|
|
||||||
|
const event = new Event()
|
||||||
|
const eventAdminContributionDeny = new EventAdminContributionDeny()
|
||||||
|
eventAdminContributionDeny.userId = contributionToUpdate.userId
|
||||||
|
eventAdminContributionDeny.amount = contributionToUpdate.amount
|
||||||
|
eventAdminContributionDeny.contributionId = contributionToUpdate.id
|
||||||
|
await writeEvent(event.setEventAdminContributionDeny(eventAdminContributionDeny))
|
||||||
|
|
||||||
sendContributionDeniedEmail({
|
sendContributionDeniedEmail({
|
||||||
firstName: user.firstName,
|
firstName: user.firstName,
|
||||||
lastName: user.lastName,
|
lastName: user.lastName,
|
||||||
|
|||||||
@ -75,7 +75,7 @@ describe('EmailOptinCodes', () => {
|
|||||||
query({ query: queryOptIn, variables: { optIn: optinCode } }),
|
query({ query: queryOptIn, variables: { optIn: optinCode } }),
|
||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: null,
|
data: null,
|
||||||
errors: [new GraphQLError('email was sent more than 24 hours ago')],
|
errors: [new GraphQLError('Email was sent more than 24 hours ago')],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -84,7 +84,7 @@ describe('EmailOptinCodes', () => {
|
|||||||
mutate({ mutation: setPassword, variables: { code: optinCode, password: 'Aa12345_' } }),
|
mutate({ mutation: setPassword, variables: { code: optinCode, password: 'Aa12345_' } }),
|
||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: null,
|
data: null,
|
||||||
errors: [new GraphQLError('email was sent more than 24 hours ago')],
|
errors: [new GraphQLError('Email was sent more than 24 hours ago')],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -96,7 +96,7 @@ describe('EmailOptinCodes', () => {
|
|||||||
mutate({ mutation: forgotPassword, variables: { email: 'peter@lustig.de' } }),
|
mutate({ mutation: forgotPassword, variables: { email: 'peter@lustig.de' } }),
|
||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
data: null,
|
data: null,
|
||||||
errors: [new GraphQLError('email already sent less than 10 minutes minutes ago')],
|
errors: [new GraphQLError('Email already sent less than 10 minutes ago')],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -33,6 +33,8 @@ import { executeTransaction } from './TransactionResolver'
|
|||||||
import QueryLinkResult from '@union/QueryLinkResult'
|
import QueryLinkResult from '@union/QueryLinkResult'
|
||||||
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
|
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
|
||||||
|
|
||||||
|
import { getLastTransaction } from './util/getLastTransaction'
|
||||||
|
|
||||||
// TODO: do not export, test it inside the resolver
|
// TODO: do not export, test it inside the resolver
|
||||||
export const transactionLinkCode = (date: Date): string => {
|
export const transactionLinkCode = (date: Date): string => {
|
||||||
const time = date.getTime().toString(16)
|
const time = date.getTime().toString(16)
|
||||||
@ -275,13 +277,7 @@ export class TransactionLinkResolver {
|
|||||||
|
|
||||||
await queryRunner.manager.insert(DbContribution, contribution)
|
await queryRunner.manager.insert(DbContribution, contribution)
|
||||||
|
|
||||||
const lastTransaction = await queryRunner.manager
|
const lastTransaction = await getLastTransaction(user.id)
|
||||||
.createQueryBuilder()
|
|
||||||
.select('transaction')
|
|
||||||
.from(DbTransaction, 'transaction')
|
|
||||||
.where('transaction.userId = :id', { id: user.id })
|
|
||||||
.orderBy('transaction.id', 'DESC')
|
|
||||||
.getOne()
|
|
||||||
let newBalance = new Decimal(0)
|
let newBalance = new Decimal(0)
|
||||||
|
|
||||||
let decay: Decay | null = null
|
let decay: Decay | null = null
|
||||||
|
|||||||
@ -89,7 +89,7 @@ describe('send coins', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('logs the error thrown', () => {
|
it('logs the error thrown', () => {
|
||||||
expect(logger.error).toBeCalledWith(`UserContact with email=wrong@email.com does not exists`)
|
expect(logger.error).toBeCalledWith('No user with this credentials', 'wrong@email.com')
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('deleted recipient', () => {
|
describe('deleted recipient', () => {
|
||||||
|
|||||||
@ -30,7 +30,7 @@ import {
|
|||||||
sendTransactionReceivedEmail,
|
sendTransactionReceivedEmail,
|
||||||
} from '@/emails/sendEmailVariants'
|
} from '@/emails/sendEmailVariants'
|
||||||
import { Event, EventTransactionReceive, EventTransactionSend } from '@/event/Event'
|
import { Event, EventTransactionReceive, EventTransactionSend } from '@/event/Event'
|
||||||
import { eventProtocol } from '@/event/EventProtocolEmitter'
|
import { writeEvent } from '@/event/EventProtocolEmitter'
|
||||||
|
|
||||||
import { BalanceResolver } from './BalanceResolver'
|
import { BalanceResolver } from './BalanceResolver'
|
||||||
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
||||||
@ -38,6 +38,8 @@ import { findUserByEmail } from './UserResolver'
|
|||||||
|
|
||||||
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
|
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
|
||||||
|
|
||||||
|
import { getLastTransaction } from './util/getLastTransaction'
|
||||||
|
|
||||||
export const executeTransaction = async (
|
export const executeTransaction = async (
|
||||||
amount: Decimal,
|
amount: Decimal,
|
||||||
memo: string,
|
memo: string,
|
||||||
@ -144,16 +146,14 @@ export const executeTransaction = async (
|
|||||||
eventTransactionSend.xUserId = transactionSend.linkedUserId
|
eventTransactionSend.xUserId = transactionSend.linkedUserId
|
||||||
eventTransactionSend.transactionId = transactionSend.id
|
eventTransactionSend.transactionId = transactionSend.id
|
||||||
eventTransactionSend.amount = transactionSend.amount.mul(-1)
|
eventTransactionSend.amount = transactionSend.amount.mul(-1)
|
||||||
await eventProtocol.writeEvent(new Event().setEventTransactionSend(eventTransactionSend))
|
await writeEvent(new Event().setEventTransactionSend(eventTransactionSend))
|
||||||
|
|
||||||
const eventTransactionReceive = new EventTransactionReceive()
|
const eventTransactionReceive = new EventTransactionReceive()
|
||||||
eventTransactionReceive.userId = transactionReceive.userId
|
eventTransactionReceive.userId = transactionReceive.userId
|
||||||
eventTransactionReceive.xUserId = transactionReceive.linkedUserId
|
eventTransactionReceive.xUserId = transactionReceive.linkedUserId
|
||||||
eventTransactionReceive.transactionId = transactionReceive.id
|
eventTransactionReceive.transactionId = transactionReceive.id
|
||||||
eventTransactionReceive.amount = transactionReceive.amount
|
eventTransactionReceive.amount = transactionReceive.amount
|
||||||
await eventProtocol.writeEvent(
|
await writeEvent(new Event().setEventTransactionReceive(eventTransactionReceive))
|
||||||
new Event().setEventTransactionReceive(eventTransactionReceive),
|
|
||||||
)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await queryRunner.rollbackTransaction()
|
await queryRunner.rollbackTransaction()
|
||||||
logger.error(`Transaction was not successful: ${e}`)
|
logger.error(`Transaction was not successful: ${e}`)
|
||||||
@ -208,10 +208,7 @@ export class TransactionResolver {
|
|||||||
logger.info(`transactionList(user=${user.firstName}.${user.lastName}, ${user.emailId})`)
|
logger.info(`transactionList(user=${user.firstName}.${user.lastName}, ${user.emailId})`)
|
||||||
|
|
||||||
// find current balance
|
// find current balance
|
||||||
const lastTransaction = await dbTransaction.findOne(
|
const lastTransaction = await getLastTransaction(user.id, ['contribution'])
|
||||||
{ userId: user.id },
|
|
||||||
{ order: { id: 'DESC' }, relations: ['contribution'] },
|
|
||||||
)
|
|
||||||
logger.debug(`lastTransaction=${lastTransaction}`)
|
logger.debug(`lastTransaction=${lastTransaction}`)
|
||||||
|
|
||||||
const balanceResolver = new BalanceResolver()
|
const balanceResolver = new BalanceResolver()
|
||||||
|
|||||||
@ -549,7 +549,9 @@ describe('UserResolver', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('logs the error thrown', () => {
|
it('logs the error thrown', () => {
|
||||||
expect(logger.error).toBeCalledWith('Password entered is lexically invalid')
|
expect(logger.error).toBeCalledWith(
|
||||||
|
'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -606,9 +608,7 @@ describe('UserResolver', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('logs the error found', () => {
|
it('logs the error found', () => {
|
||||||
expect(logger.error).toBeCalledWith(
|
expect(logger.error).toBeCalledWith('No user with this credentials', variables.email)
|
||||||
'UserContact with email=bibi@bloxberg.de does not exists',
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -668,7 +668,112 @@ describe('UserResolver', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('logs the error thrown', () => {
|
it('logs the error thrown', () => {
|
||||||
expect(logger.error).toBeCalledWith('The User has no valid credentials.')
|
expect(logger.error).toBeCalledWith('No user with this credentials', variables.email)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('user is in database but deleted', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
await userFactory(testEnv, stephenHawking)
|
||||||
|
const variables = {
|
||||||
|
email: stephenHawking.email,
|
||||||
|
password: 'Aa12345_',
|
||||||
|
publisherId: 1234,
|
||||||
|
}
|
||||||
|
result = await mutate({ mutation: login, variables })
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await cleanDB()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns an error', () => {
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [
|
||||||
|
new GraphQLError('This user was permanently deleted. Contact support for questions'),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs the error thrown', () => {
|
||||||
|
expect(logger.error).toBeCalledWith(
|
||||||
|
'This user was permanently deleted. Contact support for questions',
|
||||||
|
expect.objectContaining({
|
||||||
|
firstName: stephenHawking.firstName,
|
||||||
|
lastName: stephenHawking.lastName,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('user is in database but email not confirmed', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
await userFactory(testEnv, garrickOllivander)
|
||||||
|
const variables = {
|
||||||
|
email: garrickOllivander.email,
|
||||||
|
password: 'Aa12345_',
|
||||||
|
publisherId: 1234,
|
||||||
|
}
|
||||||
|
result = await mutate({ mutation: login, variables })
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await cleanDB()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns an error', () => {
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [new GraphQLError('The Users email is not validate yet')],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs the error thrown', () => {
|
||||||
|
expect(logger.error).toBeCalledWith(
|
||||||
|
'The Users email is not validate yet',
|
||||||
|
expect.objectContaining({
|
||||||
|
firstName: garrickOllivander.firstName,
|
||||||
|
lastName: garrickOllivander.lastName,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe.skip('user is in database but password is not set', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
// TODO: we need an user without password set
|
||||||
|
const user = await userFactory(testEnv, bibiBloxberg)
|
||||||
|
user.password = BigInt(0)
|
||||||
|
await user.save()
|
||||||
|
result = await mutate({ mutation: login, variables })
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await cleanDB()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns an error', () => {
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
errors: [new GraphQLError('The User has not set a password yet')],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs the error thrown', () => {
|
||||||
|
expect(logger.error).toBeCalledWith(
|
||||||
|
'The User has not set a password yet',
|
||||||
|
expect.objectContaining({
|
||||||
|
firstName: bibiBloxberg.firstName,
|
||||||
|
lastName: bibiBloxberg.lastName,
|
||||||
|
}),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -828,9 +933,9 @@ describe('UserResolver', () => {
|
|||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [
|
errors: [
|
||||||
new GraphQLError(
|
new GraphQLError(
|
||||||
`email already sent less than ${printTimeDuration(
|
`Email already sent less than ${printTimeDuration(
|
||||||
CONFIG.EMAIL_CODE_REQUEST_TIME,
|
CONFIG.EMAIL_CODE_REQUEST_TIME,
|
||||||
)} minutes ago`,
|
)} ago`,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@ -870,13 +975,13 @@ describe('UserResolver', () => {
|
|||||||
CONFIG.EMAIL_CODE_REQUEST_TIME = emailCodeRequestTime
|
CONFIG.EMAIL_CODE_REQUEST_TIME = emailCodeRequestTime
|
||||||
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
|
await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [new GraphQLError('email already sent less than 10 minutes minutes ago')],
|
errors: [new GraphQLError('Email already sent less than 10 minutes ago')],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('logs the error found', () => {
|
it('logs the error found', () => {
|
||||||
expect(logger.error).toBeCalledWith(`email already sent less than 10 minutes minutes ago`)
|
expect(logger.error).toBeCalledWith(`Email already sent less than 10 minutes ago`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -1001,13 +1106,13 @@ describe('UserResolver', () => {
|
|||||||
}),
|
}),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [new GraphQLError(`"not-valid" isn't a valid language`)],
|
errors: [new GraphQLError('Given language is not a valid language')],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('logs the error found', () => {
|
it('logs the error found', () => {
|
||||||
expect(logger.error).toBeCalledWith(`"not-valid" isn't a valid language`)
|
expect(logger.error).toBeCalledWith('Given language is not a valid language', 'not-valid')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1058,7 +1163,9 @@ describe('UserResolver', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('logs the error found', () => {
|
it('logs the error found', () => {
|
||||||
expect(logger.error).toBeCalledWith('newPassword does not fullfil the rules')
|
expect(logger.error).toBeCalledWith(
|
||||||
|
'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1116,7 +1223,9 @@ describe('UserResolver', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('logs the error thrown', () => {
|
it('logs the error thrown', () => {
|
||||||
expect(logger.error).toBeCalledWith('The User has no valid credentials.')
|
expect(logger.error).toBeCalledWith(
|
||||||
|
'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -1322,13 +1431,13 @@ describe('UserResolver', () => {
|
|||||||
mutate({ mutation: setUserRole, variables: { userId: admin.id + 1, isAdmin: true } }),
|
mutate({ mutation: setUserRole, variables: { userId: admin.id + 1, isAdmin: true } }),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [new GraphQLError(`Could not find user with userId: ${admin.id + 1}`)],
|
errors: [new GraphQLError('Could not find user with given ID')],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('logs the error thrown', () => {
|
it('logs the error thrown', () => {
|
||||||
expect(logger.error).toBeCalledWith(`Could not find user with userId: ${admin.id + 1}`)
|
expect(logger.error).toBeCalledWith('Could not find user with given ID', admin.id + 1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1379,12 +1488,12 @@ describe('UserResolver', () => {
|
|||||||
mutate({ mutation: setUserRole, variables: { userId: admin.id, isAdmin: false } }),
|
mutate({ mutation: setUserRole, variables: { userId: admin.id, isAdmin: false } }),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [new GraphQLError('Administrator can not change his own role!')],
|
errors: [new GraphQLError('Administrator can not change his own role')],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
it('logs the error thrown', () => {
|
it('logs the error thrown', () => {
|
||||||
expect(logger.error).toBeCalledWith('Administrator can not change his own role!')
|
expect(logger.error).toBeCalledWith('Administrator can not change his own role')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1400,13 +1509,13 @@ describe('UserResolver', () => {
|
|||||||
mutate({ mutation: setUserRole, variables: { userId: user.id, isAdmin: true } }),
|
mutate({ mutation: setUserRole, variables: { userId: user.id, isAdmin: true } }),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [new GraphQLError('User is already admin!')],
|
errors: [new GraphQLError('User is already admin')],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('logs the error thrown', () => {
|
it('logs the error thrown', () => {
|
||||||
expect(logger.error).toBeCalledWith('User is already admin!')
|
expect(logger.error).toBeCalledWith('User is already admin')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1421,13 +1530,13 @@ describe('UserResolver', () => {
|
|||||||
mutate({ mutation: setUserRole, variables: { userId: user.id, isAdmin: false } }),
|
mutate({ mutation: setUserRole, variables: { userId: user.id, isAdmin: false } }),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [new GraphQLError('User is already a usual user!')],
|
errors: [new GraphQLError('User is already an usual user')],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('logs the error thrown', () => {
|
it('logs the error thrown', () => {
|
||||||
expect(logger.error).toBeCalledWith('User is already a usual user!')
|
expect(logger.error).toBeCalledWith('User is already an usual user')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -1494,13 +1603,13 @@ describe('UserResolver', () => {
|
|||||||
mutate({ mutation: deleteUser, variables: { userId: admin.id + 1 } }),
|
mutate({ mutation: deleteUser, variables: { userId: admin.id + 1 } }),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [new GraphQLError(`Could not find user with userId: ${admin.id + 1}`)],
|
errors: [new GraphQLError('Could not find user with given ID')],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('logs the error thrown', () => {
|
it('logs the error thrown', () => {
|
||||||
expect(logger.error).toBeCalledWith(`Could not find user with userId: ${admin.id + 1}`)
|
expect(logger.error).toBeCalledWith('Could not find user with given ID', admin.id + 1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1511,13 +1620,13 @@ describe('UserResolver', () => {
|
|||||||
mutate({ mutation: deleteUser, variables: { userId: admin.id } }),
|
mutate({ mutation: deleteUser, variables: { userId: admin.id } }),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [new GraphQLError('Moderator can not delete his own account!')],
|
errors: [new GraphQLError('Moderator can not delete his own account')],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('logs the error thrown', () => {
|
it('logs the error thrown', () => {
|
||||||
expect(logger.error).toBeCalledWith('Moderator can not delete his own account!')
|
expect(logger.error).toBeCalledWith('Moderator can not delete his own account')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1545,13 +1654,13 @@ describe('UserResolver', () => {
|
|||||||
mutate({ mutation: deleteUser, variables: { userId: user.id } }),
|
mutate({ mutation: deleteUser, variables: { userId: user.id } }),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [new GraphQLError(`Could not find user with userId: ${user.id}`)],
|
errors: [new GraphQLError('Could not find user with given ID')],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('logs the error thrown', () => {
|
it('logs the error thrown', () => {
|
||||||
expect(logger.error).toBeCalledWith(`Could not find user with userId: ${user.id}`)
|
expect(logger.error).toBeCalledWith('Could not find user with given ID', user.id)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -1617,13 +1726,13 @@ describe('UserResolver', () => {
|
|||||||
mutate({ mutation: unDeleteUser, variables: { userId: admin.id + 1 } }),
|
mutate({ mutation: unDeleteUser, variables: { userId: admin.id + 1 } }),
|
||||||
).resolves.toEqual(
|
).resolves.toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
errors: [new GraphQLError(`Could not find user with userId: ${admin.id + 1}`)],
|
errors: [new GraphQLError('Could not find user with given ID')],
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('logs the error thrown', () => {
|
it('logs the error thrown', () => {
|
||||||
expect(logger.error).toBeCalledWith(`Could not find user with userId: ${admin.id + 1}`)
|
expect(logger.error).toBeCalledWith('Could not find user with given ID', admin.id + 1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -48,7 +48,7 @@ import { klicktippNewsletterStateMiddleware } from '@/middleware/klicktippMiddle
|
|||||||
import { klicktippSignIn } from '@/apis/KlicktippController'
|
import { klicktippSignIn } from '@/apis/KlicktippController'
|
||||||
import { RIGHTS } from '@/auth/RIGHTS'
|
import { RIGHTS } from '@/auth/RIGHTS'
|
||||||
import { hasElopageBuys } from '@/util/hasElopageBuys'
|
import { hasElopageBuys } from '@/util/hasElopageBuys'
|
||||||
import { eventProtocol } from '@/event/EventProtocolEmitter'
|
import { writeEvent } from '@/event/EventProtocolEmitter'
|
||||||
import {
|
import {
|
||||||
Event,
|
Event,
|
||||||
EventLogin,
|
EventLogin,
|
||||||
@ -63,6 +63,7 @@ import { isValidPassword } from '@/password/EncryptorUtils'
|
|||||||
import { FULL_CREATION_AVAILABLE } from './const/const'
|
import { FULL_CREATION_AVAILABLE } from './const/const'
|
||||||
import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptor'
|
import { encryptPassword, verifyPassword } from '@/password/PasswordEncryptor'
|
||||||
import { PasswordEncryptionType } from '../enum/PasswordEncryptionType'
|
import { PasswordEncryptionType } from '../enum/PasswordEncryptionType'
|
||||||
|
import LogError from '@/server/LogError'
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const sodium = require('sodium-native')
|
const sodium = require('sodium-native')
|
||||||
@ -134,22 +135,19 @@ export class UserResolver {
|
|||||||
email = email.trim().toLowerCase()
|
email = email.trim().toLowerCase()
|
||||||
const dbUser = await findUserByEmail(email)
|
const dbUser = await findUserByEmail(email)
|
||||||
if (dbUser.deletedAt) {
|
if (dbUser.deletedAt) {
|
||||||
logger.error('The User was permanently deleted in database.')
|
throw new LogError('This user was permanently deleted. Contact support for questions', dbUser)
|
||||||
throw new Error('This user was permanently deleted. Contact support for questions.')
|
|
||||||
}
|
}
|
||||||
if (!dbUser.emailContact.emailChecked) {
|
if (!dbUser.emailContact.emailChecked) {
|
||||||
logger.error('The Users email is not validate yet.')
|
throw new LogError('The Users email is not validate yet', dbUser)
|
||||||
throw new Error('User email not validated')
|
|
||||||
}
|
}
|
||||||
|
// TODO: at least in test this does not work since `dbUser.password = 0` and `BigInto(0) = 0n`
|
||||||
if (dbUser.password === BigInt(0)) {
|
if (dbUser.password === BigInt(0)) {
|
||||||
logger.error('The User has not set a password yet.')
|
|
||||||
// TODO we want to catch this on the frontend and ask the user to check his emails or resend code
|
// TODO we want to catch this on the frontend and ask the user to check his emails or resend code
|
||||||
throw new Error('User has no password set yet')
|
throw new LogError('The User has not set a password yet', dbUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!verifyPassword(dbUser, password)) {
|
if (!verifyPassword(dbUser, password)) {
|
||||||
logger.error('The User has no valid credentials.')
|
throw new LogError('No user with this credentials', dbUser)
|
||||||
throw new Error('No user with this credentials')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dbUser.passwordEncryptionType !== PasswordEncryptionType.GRADIDO_ID) {
|
if (dbUser.passwordEncryptionType !== PasswordEncryptionType.GRADIDO_ID) {
|
||||||
@ -181,7 +179,7 @@ export class UserResolver {
|
|||||||
})
|
})
|
||||||
const ev = new EventLogin()
|
const ev = new EventLogin()
|
||||||
ev.userId = user.id
|
ev.userId = user.id
|
||||||
eventProtocol.writeEvent(new Event().setEventLogin(ev))
|
writeEvent(new Event().setEventLogin(ev))
|
||||||
logger.info(`successful Login: ${JSON.stringify(user, null, 2)}`)
|
logger.info(`successful Login: ${JSON.stringify(user, null, 2)}`)
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
@ -253,9 +251,7 @@ export class UserResolver {
|
|||||||
})
|
})
|
||||||
const eventSendAccountMultiRegistrationEmail = new EventSendAccountMultiRegistrationEmail()
|
const eventSendAccountMultiRegistrationEmail = new EventSendAccountMultiRegistrationEmail()
|
||||||
eventSendAccountMultiRegistrationEmail.userId = foundUser.id
|
eventSendAccountMultiRegistrationEmail.userId = foundUser.id
|
||||||
eventProtocol.writeEvent(
|
writeEvent(event.setEventSendConfirmationEmail(eventSendAccountMultiRegistrationEmail))
|
||||||
event.setEventSendConfirmationEmail(eventSendAccountMultiRegistrationEmail),
|
|
||||||
)
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`sendAccountMultiRegistrationEmail by ${firstName} ${lastName} to ${foundUser.firstName} ${foundUser.lastName} <${email}>`,
|
`sendAccountMultiRegistrationEmail by ${firstName} ${lastName} to ${foundUser.firstName} ${foundUser.lastName} <${email}>`,
|
||||||
)
|
)
|
||||||
@ -309,30 +305,19 @@ export class UserResolver {
|
|||||||
await queryRunner.startTransaction('REPEATABLE READ')
|
await queryRunner.startTransaction('REPEATABLE READ')
|
||||||
try {
|
try {
|
||||||
dbUser = await queryRunner.manager.save(dbUser).catch((error) => {
|
dbUser = await queryRunner.manager.save(dbUser).catch((error) => {
|
||||||
logger.error('Error while saving dbUser', error)
|
throw new LogError('Error while saving dbUser', error)
|
||||||
throw new Error('error saving user')
|
|
||||||
})
|
})
|
||||||
let emailContact = newEmailContact(email, dbUser.id)
|
let emailContact = newEmailContact(email, dbUser.id)
|
||||||
emailContact = await queryRunner.manager.save(emailContact).catch((error) => {
|
emailContact = await queryRunner.manager.save(emailContact).catch((error) => {
|
||||||
logger.error('Error while saving emailContact', error)
|
throw new LogError('Error while saving user email contact', error)
|
||||||
throw new Error('error saving email user contact')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
dbUser.emailContact = emailContact
|
dbUser.emailContact = emailContact
|
||||||
dbUser.emailId = emailContact.id
|
dbUser.emailId = emailContact.id
|
||||||
await queryRunner.manager.save(dbUser).catch((error) => {
|
await queryRunner.manager.save(dbUser).catch((error) => {
|
||||||
logger.error('Error while updating dbUser', error)
|
throw new LogError('Error while updating dbUser', error)
|
||||||
throw new Error('error updating user')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
/*
|
|
||||||
const emailOptIn = newEmailOptIn(dbUser.id)
|
|
||||||
await queryRunner.manager.save(emailOptIn).catch((error) => {
|
|
||||||
logger.error('Error while saving emailOptIn', error)
|
|
||||||
throw new Error('error saving email opt in')
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
|
|
||||||
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
|
const activationLink = CONFIG.EMAIL_LINK_VERIFICATION.replace(
|
||||||
/{optin}/g,
|
/{optin}/g,
|
||||||
emailContact.emailVerificationCode.toString(),
|
emailContact.emailVerificationCode.toString(),
|
||||||
@ -349,7 +334,7 @@ export class UserResolver {
|
|||||||
})
|
})
|
||||||
logger.info(`sendAccountActivationEmail of ${firstName}.${lastName} to ${email}`)
|
logger.info(`sendAccountActivationEmail of ${firstName}.${lastName} to ${email}`)
|
||||||
eventSendConfirmEmail.userId = dbUser.id
|
eventSendConfirmEmail.userId = dbUser.id
|
||||||
eventProtocol.writeEvent(event.setEventSendConfirmationEmail(eventSendConfirmEmail))
|
writeEvent(event.setEventSendConfirmationEmail(eventSendConfirmEmail))
|
||||||
|
|
||||||
if (!emailSent) {
|
if (!emailSent) {
|
||||||
logger.debug(`Account confirmation link: ${activationLink}`)
|
logger.debug(`Account confirmation link: ${activationLink}`)
|
||||||
@ -358,9 +343,8 @@ export class UserResolver {
|
|||||||
await queryRunner.commitTransaction()
|
await queryRunner.commitTransaction()
|
||||||
logger.addContext('user', dbUser.id)
|
logger.addContext('user', dbUser.id)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(`error during create user with ${e}`)
|
|
||||||
await queryRunner.rollbackTransaction()
|
await queryRunner.rollbackTransaction()
|
||||||
throw e
|
throw new LogError('Error creating user', e)
|
||||||
} finally {
|
} finally {
|
||||||
await queryRunner.release()
|
await queryRunner.release()
|
||||||
}
|
}
|
||||||
@ -368,10 +352,10 @@ export class UserResolver {
|
|||||||
|
|
||||||
if (redeemCode) {
|
if (redeemCode) {
|
||||||
eventRedeemRegister.userId = dbUser.id
|
eventRedeemRegister.userId = dbUser.id
|
||||||
await eventProtocol.writeEvent(event.setEventRedeemRegister(eventRedeemRegister))
|
await writeEvent(event.setEventRedeemRegister(eventRedeemRegister))
|
||||||
} else {
|
} else {
|
||||||
eventRegister.userId = dbUser.id
|
eventRegister.userId = dbUser.id
|
||||||
await eventProtocol.writeEvent(event.setEventRegister(eventRegister))
|
await writeEvent(event.setEventRegister(eventRegister))
|
||||||
}
|
}
|
||||||
|
|
||||||
return new User(dbUser)
|
return new User(dbUser)
|
||||||
@ -392,15 +376,8 @@ export class UserResolver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!canEmailResend(user.emailContact.updatedAt || user.emailContact.createdAt)) {
|
if (!canEmailResend(user.emailContact.updatedAt || user.emailContact.createdAt)) {
|
||||||
logger.error(
|
throw new LogError(
|
||||||
`email already sent less than ${printTimeDuration(
|
`Email already sent less than ${printTimeDuration(CONFIG.EMAIL_CODE_REQUEST_TIME)} ago`,
|
||||||
CONFIG.EMAIL_CODE_REQUEST_TIME,
|
|
||||||
)} minutes ago`,
|
|
||||||
)
|
|
||||||
throw new Error(
|
|
||||||
`email already sent less than ${printTimeDuration(
|
|
||||||
CONFIG.EMAIL_CODE_REQUEST_TIME,
|
|
||||||
)} minutes ago`,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -409,8 +386,7 @@ export class UserResolver {
|
|||||||
user.emailContact.emailVerificationCode = random(64)
|
user.emailContact.emailVerificationCode = random(64)
|
||||||
user.emailContact.emailOptInTypeId = OptInType.EMAIL_OPT_IN_RESET_PASSWORD
|
user.emailContact.emailOptInTypeId = OptInType.EMAIL_OPT_IN_RESET_PASSWORD
|
||||||
await user.emailContact.save().catch(() => {
|
await user.emailContact.save().catch(() => {
|
||||||
logger.error('Unable to save email verification code= ' + user.emailContact)
|
throw new LogError('Unable to save email verification code', user.emailContact)
|
||||||
throw new Error('Unable to save email verification code.')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.info(`optInCode for ${email}=${user.emailContact}`)
|
logger.info(`optInCode for ${email}=${user.emailContact}`)
|
||||||
@ -445,34 +421,23 @@ export class UserResolver {
|
|||||||
logger.info(`setPassword(${code}, ***)...`)
|
logger.info(`setPassword(${code}, ***)...`)
|
||||||
// Validate Password
|
// Validate Password
|
||||||
if (!isValidPassword(password)) {
|
if (!isValidPassword(password)) {
|
||||||
logger.error('Password entered is lexically invalid')
|
throw new LogError(
|
||||||
throw new Error(
|
|
||||||
'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
|
'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load code
|
// load code
|
||||||
/*
|
|
||||||
const optInCode = await LoginEmailOptIn.findOneOrFail({ verificationCode: code }).catch(() => {
|
|
||||||
logger.error('Could not login with emailVerificationCode')
|
|
||||||
throw new Error('Could not login with emailVerificationCode')
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
const userContact = await DbUserContact.findOneOrFail(
|
const userContact = await DbUserContact.findOneOrFail(
|
||||||
{ emailVerificationCode: code },
|
{ emailVerificationCode: code },
|
||||||
{ relations: ['user'] },
|
{ relations: ['user'] },
|
||||||
).catch(() => {
|
).catch(() => {
|
||||||
logger.error('Could not login with emailVerificationCode')
|
throw new LogError('Could not login with emailVerificationCode')
|
||||||
throw new Error('Could not login with emailVerificationCode')
|
|
||||||
})
|
})
|
||||||
logger.debug('userContact loaded...')
|
logger.debug('userContact loaded...')
|
||||||
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
|
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
|
||||||
if (!isEmailVerificationCodeValid(userContact.updatedAt || userContact.createdAt)) {
|
if (!isEmailVerificationCodeValid(userContact.updatedAt || userContact.createdAt)) {
|
||||||
logger.error(
|
throw new LogError(
|
||||||
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
|
`Email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
|
||||||
)
|
|
||||||
throw new Error(
|
|
||||||
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
logger.debug('EmailVerificationCode is valid...')
|
logger.debug('EmailVerificationCode is valid...')
|
||||||
@ -498,13 +463,11 @@ export class UserResolver {
|
|||||||
try {
|
try {
|
||||||
// Save user
|
// Save user
|
||||||
await queryRunner.manager.save(user).catch((error) => {
|
await queryRunner.manager.save(user).catch((error) => {
|
||||||
logger.error('error saving user: ' + error)
|
throw new LogError('Error saving user', error)
|
||||||
throw new Error('error saving user: ' + error)
|
|
||||||
})
|
})
|
||||||
// Save userContact
|
// Save userContact
|
||||||
await queryRunner.manager.save(userContact).catch((error) => {
|
await queryRunner.manager.save(userContact).catch((error) => {
|
||||||
logger.error('error saving userContact: ' + error)
|
throw new LogError('Error saving userContact', error)
|
||||||
throw new Error('error saving userContact: ' + error)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
await queryRunner.commitTransaction()
|
await queryRunner.commitTransaction()
|
||||||
@ -512,11 +475,10 @@ export class UserResolver {
|
|||||||
|
|
||||||
const eventActivateAccount = new EventActivateAccount()
|
const eventActivateAccount = new EventActivateAccount()
|
||||||
eventActivateAccount.userId = user.id
|
eventActivateAccount.userId = user.id
|
||||||
eventProtocol.writeEvent(event.setEventActivateAccount(eventActivateAccount))
|
writeEvent(event.setEventActivateAccount(eventActivateAccount))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await queryRunner.rollbackTransaction()
|
await queryRunner.rollbackTransaction()
|
||||||
logger.error('Error on writing User and UserContact data:' + e)
|
throw new LogError('Error on writing User and User Contact data', e)
|
||||||
throw e
|
|
||||||
} finally {
|
} finally {
|
||||||
await queryRunner.release()
|
await queryRunner.release()
|
||||||
}
|
}
|
||||||
@ -530,7 +492,7 @@ export class UserResolver {
|
|||||||
`klicktippSignIn(${userContact.email}, ${user.language}, ${user.firstName}, ${user.lastName})`,
|
`klicktippSignIn(${userContact.email}, ${user.language}, ${user.firstName}, ${user.lastName})`,
|
||||||
)
|
)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error('Error subscribe to klicktipp:' + e)
|
logger.error('Error subscribing to klicktipp', e)
|
||||||
// TODO is this a problem?
|
// TODO is this a problem?
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
/* uncomment this, when you need the activation link on the console
|
/* uncomment this, when you need the activation link on the console
|
||||||
@ -550,11 +512,8 @@ export class UserResolver {
|
|||||||
logger.debug(`found optInCode=${userContact}`)
|
logger.debug(`found optInCode=${userContact}`)
|
||||||
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
|
// Code is only valid for `CONFIG.EMAIL_CODE_VALID_TIME` minutes
|
||||||
if (!isEmailVerificationCodeValid(userContact.updatedAt || userContact.createdAt)) {
|
if (!isEmailVerificationCodeValid(userContact.updatedAt || userContact.createdAt)) {
|
||||||
logger.error(
|
throw new LogError(
|
||||||
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
|
`Email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
|
||||||
)
|
|
||||||
throw new Error(
|
|
||||||
`email was sent more than ${printTimeDuration(CONFIG.EMAIL_CODE_VALID_TIME)} ago`,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
logger.info(`queryOptIn(${optIn}) successful...`)
|
logger.info(`queryOptIn(${optIn}) successful...`)
|
||||||
@ -589,8 +548,7 @@ export class UserResolver {
|
|||||||
|
|
||||||
if (language) {
|
if (language) {
|
||||||
if (!isLanguage(language)) {
|
if (!isLanguage(language)) {
|
||||||
logger.error(`"${language}" isn't a valid language`)
|
throw new LogError('Given language is not a valid language', language)
|
||||||
throw new Error(`"${language}" isn't a valid language`)
|
|
||||||
}
|
}
|
||||||
userEntity.language = language
|
userEntity.language = language
|
||||||
i18n.setLocale(language)
|
i18n.setLocale(language)
|
||||||
@ -599,15 +557,13 @@ export class UserResolver {
|
|||||||
if (password && passwordNew) {
|
if (password && passwordNew) {
|
||||||
// Validate Password
|
// Validate Password
|
||||||
if (!isValidPassword(passwordNew)) {
|
if (!isValidPassword(passwordNew)) {
|
||||||
logger.error('newPassword does not fullfil the rules')
|
throw new LogError(
|
||||||
throw new Error(
|
|
||||||
'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
|
'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!verifyPassword(userEntity, password)) {
|
if (!verifyPassword(userEntity, password)) {
|
||||||
logger.error(`Old password is invalid`)
|
throw new LogError(`Old password is invalid`)
|
||||||
throw new Error(`Old password is invalid`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save new password hash and newly encrypted private key
|
// Save new password hash and newly encrypted private key
|
||||||
@ -630,16 +586,14 @@ export class UserResolver {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await queryRunner.manager.save(userEntity).catch((error) => {
|
await queryRunner.manager.save(userEntity).catch((error) => {
|
||||||
logger.error('error saving user: ' + error)
|
throw new LogError('Error saving user', error)
|
||||||
throw new Error('error saving user: ' + error)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
await queryRunner.commitTransaction()
|
await queryRunner.commitTransaction()
|
||||||
logger.debug('writing User data successful...')
|
logger.debug('writing User data successful...')
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await queryRunner.rollbackTransaction()
|
await queryRunner.rollbackTransaction()
|
||||||
logger.error(`error on writing updated user data: ${e}`)
|
throw new LogError('Error on writing updated user data', e)
|
||||||
throw e
|
|
||||||
} finally {
|
} finally {
|
||||||
await queryRunner.release()
|
await queryRunner.release()
|
||||||
}
|
}
|
||||||
@ -766,14 +720,12 @@ export class UserResolver {
|
|||||||
const user = await DbUser.findOne({ id: userId })
|
const user = await DbUser.findOne({ id: userId })
|
||||||
// user exists ?
|
// user exists ?
|
||||||
if (!user) {
|
if (!user) {
|
||||||
logger.error(`Could not find user with userId: ${userId}`)
|
throw new LogError('Could not find user with given ID', userId)
|
||||||
throw new Error(`Could not find user with userId: ${userId}`)
|
|
||||||
}
|
}
|
||||||
// administrator user changes own role?
|
// administrator user changes own role?
|
||||||
const moderatorUser = getUser(context)
|
const moderatorUser = getUser(context)
|
||||||
if (moderatorUser.id === userId) {
|
if (moderatorUser.id === userId) {
|
||||||
logger.error('Administrator can not change his own role!')
|
throw new LogError('Administrator can not change his own role')
|
||||||
throw new Error('Administrator can not change his own role!')
|
|
||||||
}
|
}
|
||||||
// change isAdmin
|
// change isAdmin
|
||||||
switch (user.isAdmin) {
|
switch (user.isAdmin) {
|
||||||
@ -781,16 +733,14 @@ export class UserResolver {
|
|||||||
if (isAdmin === true) {
|
if (isAdmin === true) {
|
||||||
user.isAdmin = new Date()
|
user.isAdmin = new Date()
|
||||||
} else {
|
} else {
|
||||||
logger.error('User is already a usual user!')
|
throw new LogError('User is already an usual user')
|
||||||
throw new Error('User is already a usual user!')
|
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
if (isAdmin === false) {
|
if (isAdmin === false) {
|
||||||
user.isAdmin = null
|
user.isAdmin = null
|
||||||
} else {
|
} else {
|
||||||
logger.error('User is already admin!')
|
throw new LogError('User is already admin')
|
||||||
throw new Error('User is already admin!')
|
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -808,14 +758,12 @@ export class UserResolver {
|
|||||||
const user = await DbUser.findOne({ id: userId })
|
const user = await DbUser.findOne({ id: userId })
|
||||||
// user exists ?
|
// user exists ?
|
||||||
if (!user) {
|
if (!user) {
|
||||||
logger.error(`Could not find user with userId: ${userId}`)
|
throw new LogError('Could not find user with given ID', userId)
|
||||||
throw new Error(`Could not find user with userId: ${userId}`)
|
|
||||||
}
|
}
|
||||||
// moderator user disabled own account?
|
// moderator user disabled own account?
|
||||||
const moderatorUser = getUser(context)
|
const moderatorUser = getUser(context)
|
||||||
if (moderatorUser.id === userId) {
|
if (moderatorUser.id === userId) {
|
||||||
logger.error('Moderator can not delete his own account!')
|
throw new LogError('Moderator can not delete his own account')
|
||||||
throw new Error('Moderator can not delete his own account!')
|
|
||||||
}
|
}
|
||||||
// soft-delete user
|
// soft-delete user
|
||||||
await user.softRemove()
|
await user.softRemove()
|
||||||
@ -828,12 +776,10 @@ export class UserResolver {
|
|||||||
async unDeleteUser(@Arg('userId', () => Int) userId: number): Promise<Date | null> {
|
async unDeleteUser(@Arg('userId', () => Int) userId: number): Promise<Date | null> {
|
||||||
const user = await DbUser.findOne({ id: userId }, { withDeleted: true })
|
const user = await DbUser.findOne({ id: userId }, { withDeleted: true })
|
||||||
if (!user) {
|
if (!user) {
|
||||||
logger.error(`Could not find user with userId: ${userId}`)
|
throw new LogError('Could not find user with given ID', userId)
|
||||||
throw new Error(`Could not find user with userId: ${userId}`)
|
|
||||||
}
|
}
|
||||||
if (!user.deletedAt) {
|
if (!user.deletedAt) {
|
||||||
logger.error('User is not deleted')
|
throw new LogError('User is not deleted')
|
||||||
throw new Error('User is not deleted')
|
|
||||||
}
|
}
|
||||||
await user.recover()
|
await user.recover()
|
||||||
return null
|
return null
|
||||||
@ -846,17 +792,14 @@ export class UserResolver {
|
|||||||
// const user = await dbUser.findOne({ id: emailContact.userId })
|
// const user = await dbUser.findOne({ id: emailContact.userId })
|
||||||
const user = await findUserByEmail(email)
|
const user = await findUserByEmail(email)
|
||||||
if (!user) {
|
if (!user) {
|
||||||
logger.error(`Could not find User to emailContact: ${email}`)
|
throw new LogError('Could not find user to given email contact', email)
|
||||||
throw new Error(`Could not find User to emailContact: ${email}`)
|
|
||||||
}
|
}
|
||||||
if (user.deletedAt) {
|
if (user.deletedAt) {
|
||||||
logger.error(`User with emailContact: ${email} is deleted.`)
|
throw new LogError('User with given email contact is deleted', email)
|
||||||
throw new Error(`User with emailContact: ${email} is deleted.`)
|
|
||||||
}
|
}
|
||||||
const emailContact = user.emailContact
|
const emailContact = user.emailContact
|
||||||
if (emailContact.deletedAt) {
|
if (emailContact.deletedAt) {
|
||||||
logger.error(`The emailContact: ${email} of this User is deleted.`)
|
throw new LogError('The given email contact for this user is deleted', email)
|
||||||
throw new Error(`The emailContact: ${email} of this User is deleted.`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
emailContact.emailResendCount++
|
emailContact.emailResendCount++
|
||||||
@ -879,9 +822,7 @@ export class UserResolver {
|
|||||||
const event = new Event()
|
const event = new Event()
|
||||||
const eventSendConfirmationEmail = new EventSendConfirmationEmail()
|
const eventSendConfirmationEmail = new EventSendConfirmationEmail()
|
||||||
eventSendConfirmationEmail.userId = user.id
|
eventSendConfirmationEmail.userId = user.id
|
||||||
await eventProtocol.writeEvent(
|
await writeEvent(event.setEventSendConfirmationEmail(eventSendConfirmationEmail))
|
||||||
event.setEventSendConfirmationEmail(eventSendConfirmationEmail),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@ -893,8 +834,7 @@ export async function findUserByEmail(email: string): Promise<DbUser> {
|
|||||||
{ email: email },
|
{ email: email },
|
||||||
{ withDeleted: true, relations: ['user'] },
|
{ withDeleted: true, relations: ['user'] },
|
||||||
).catch(() => {
|
).catch(() => {
|
||||||
logger.error(`UserContact with email=${email} does not exists`)
|
throw new LogError('No user with this credentials', email)
|
||||||
throw new Error('No user with this credentials')
|
|
||||||
})
|
})
|
||||||
const dbUser = dbUserContact.user
|
const dbUser = dbUserContact.user
|
||||||
dbUser.emailContact = dbUserContact
|
dbUser.emailContact = dbUserContact
|
||||||
@ -909,31 +849,16 @@ async function checkEmailExists(email: string): Promise<boolean> {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
const isTimeExpired = (optIn: LoginEmailOptIn, duration: number): boolean => {
|
|
||||||
const timeElapsed = Date.now() - new Date(optIn.updatedAt).getTime()
|
|
||||||
// time is given in minutes
|
|
||||||
return timeElapsed <= duration * 60 * 1000
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const isTimeExpired = (updatedAt: Date, duration: number): boolean => {
|
const isTimeExpired = (updatedAt: Date, duration: number): boolean => {
|
||||||
const timeElapsed = Date.now() - new Date(updatedAt).getTime()
|
const timeElapsed = Date.now() - new Date(updatedAt).getTime()
|
||||||
// time is given in minutes
|
// time is given in minutes
|
||||||
return timeElapsed <= duration * 60 * 1000
|
return timeElapsed <= duration * 60 * 1000
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
const isOptInValid = (optIn: LoginEmailOptIn): boolean => {
|
|
||||||
return isTimeExpired(optIn, CONFIG.EMAIL_CODE_VALID_TIME)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const isEmailVerificationCodeValid = (updatedAt: Date): boolean => {
|
const isEmailVerificationCodeValid = (updatedAt: Date): boolean => {
|
||||||
return isTimeExpired(updatedAt, CONFIG.EMAIL_CODE_VALID_TIME)
|
return isTimeExpired(updatedAt, CONFIG.EMAIL_CODE_VALID_TIME)
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
const canResendOptIn = (optIn: LoginEmailOptIn): boolean => {
|
|
||||||
return !isTimeExpired(optIn, CONFIG.EMAIL_CODE_REQUEST_TIME)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const canEmailResend = (updatedAt: Date): boolean => {
|
const canEmailResend = (updatedAt: Date): boolean => {
|
||||||
return !isTimeExpired(updatedAt, CONFIG.EMAIL_CODE_REQUEST_TIME)
|
return !isTimeExpired(updatedAt, CONFIG.EMAIL_CODE_REQUEST_TIME)
|
||||||
}
|
}
|
||||||
|
|||||||
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,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -23,8 +23,13 @@
|
|||||||
"commonGoodContributionConfirmed": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde soeben von {senderFirstName} {senderLastName} bestätigt und in deinem Gradido-Konto gutgeschrieben.",
|
"commonGoodContributionConfirmed": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde soeben von {senderFirstName} {senderLastName} bestätigt und in deinem Gradido-Konto gutgeschrieben.",
|
||||||
"subject": "Gradido: Dein Gemeinwohl-Beitrag wurde bestätigt"
|
"subject": "Gradido: Dein Gemeinwohl-Beitrag wurde bestätigt"
|
||||||
},
|
},
|
||||||
"contributionRejected": {
|
"contributionDeleted": {
|
||||||
"commonGoodContributionRejected": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde von {senderFirstName} {senderLastName} abgelehnt.",
|
"commonGoodContributionDeleted": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde von {senderFirstName} {senderLastName} gelöscht.",
|
||||||
|
"subject": "Gradido: Dein Gemeinwohl-Beitrag wurde gelöscht",
|
||||||
|
"toSeeContributionsAndMessages": "Um deine Gemeinwohl-Beiträge und dazugehörige Nachrichten zu sehen, gehe in deinem Gradido-Konto ins Menü „Gemeinschaft“ auf den Tab „Meine Beiträge zum Gemeinwohl“!"
|
||||||
|
},
|
||||||
|
"contributionDenied": {
|
||||||
|
"commonGoodContributionDenied": "dein Gemeinwohl-Beitrag „{contributionMemo}“ wurde von {senderFirstName} {senderLastName} abgelehnt.",
|
||||||
"subject": "Gradido: Dein Gemeinwohl-Beitrag wurde abgelehnt",
|
"subject": "Gradido: Dein Gemeinwohl-Beitrag wurde abgelehnt",
|
||||||
"toSeeContributionsAndMessages": "Um deine Gemeinwohl-Beiträge und dazugehörige Nachrichten zu sehen, gehe in deinem Gradido-Konto ins Menü „Gemeinschaft“ auf den Tab „Meine Beiträge zum Gemeinwohl“!"
|
"toSeeContributionsAndMessages": "Um deine Gemeinwohl-Beiträge und dazugehörige Nachrichten zu sehen, gehe in deinem Gradido-Konto ins Menü „Gemeinschaft“ auf den Tab „Meine Beiträge zum Gemeinwohl“!"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -23,6 +23,11 @@
|
|||||||
"commonGoodContributionConfirmed": "Your public good contribution “{contributionMemo}” has just been confirmed by {senderFirstName} {senderLastName} and credited to your Gradido account.",
|
"commonGoodContributionConfirmed": "Your public good contribution “{contributionMemo}” has just been confirmed by {senderFirstName} {senderLastName} and credited to your Gradido account.",
|
||||||
"subject": "Gradido: Your contribution to the common good was confirmed"
|
"subject": "Gradido: Your contribution to the common good was confirmed"
|
||||||
},
|
},
|
||||||
|
"contributionDeleted": {
|
||||||
|
"commonGoodContributionDeleted": "Your public good contribution “{contributionMemo}” was deleted by {senderFirstName} {senderLastName}.",
|
||||||
|
"subject": "Gradido: Your common good contribution was deleted",
|
||||||
|
"toSeeContributionsAndMessages": "To see your common good contributions and related messages, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!"
|
||||||
|
},
|
||||||
"contributionDenied": {
|
"contributionDenied": {
|
||||||
"commonGoodContributionDenied": "Your public good contribution “{contributionMemo}” was rejected by {senderFirstName} {senderLastName}.",
|
"commonGoodContributionDenied": "Your public good contribution “{contributionMemo}” was rejected by {senderFirstName} {senderLastName}.",
|
||||||
"subject": "Gradido: Your common good contribution was rejected",
|
"subject": "Gradido: Your common good contribution was rejected",
|
||||||
|
|||||||
@ -172,8 +172,8 @@ export const listContributions = gql`
|
|||||||
`
|
`
|
||||||
|
|
||||||
export const listAllContributions = `
|
export const listAllContributions = `
|
||||||
query ($currentPage: Int = 1, $pageSize: Int = 5, $order: Order = DESC) {
|
query ($currentPage: Int = 1, $pageSize: Int = 5, $order: Order = DESC, $statusFilter: [ContributionStatus!]) {
|
||||||
listAllContributions(currentPage: $currentPage, pageSize: $pageSize, order: $order) {
|
listAllContributions(currentPage: $currentPage, pageSize: $pageSize, order: $order, statusFilter: $statusFilter) {
|
||||||
contributionCount
|
contributionCount
|
||||||
contributionList {
|
contributionList {
|
||||||
id
|
id
|
||||||
@ -184,6 +184,11 @@ query ($currentPage: Int = 1, $pageSize: Int = 5, $order: Order = DESC) {
|
|||||||
createdAt
|
createdAt
|
||||||
confirmedAt
|
confirmedAt
|
||||||
confirmedBy
|
confirmedBy
|
||||||
|
contributionDate
|
||||||
|
state
|
||||||
|
messagesCount
|
||||||
|
deniedAt
|
||||||
|
deniedBy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
26
backend/src/server/LogError.test.ts
Normal file
26
backend/src/server/LogError.test.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { logger } from '@test/testSetup'
|
||||||
|
|
||||||
|
import LogError from './LogError'
|
||||||
|
|
||||||
|
describe('LogError', () => {
|
||||||
|
it('logs an Error when created', () => {
|
||||||
|
/* eslint-disable-next-line no-new */
|
||||||
|
new LogError('new LogError')
|
||||||
|
expect(logger.error).toBeCalledWith('new LogError')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs an Error including additional data when created', () => {
|
||||||
|
/* eslint-disable-next-line no-new */
|
||||||
|
new LogError('new LogError', { some: 'data' })
|
||||||
|
expect(logger.error).toBeCalledWith('new LogError', { some: 'data' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not contain additional data in Error object when thrown', () => {
|
||||||
|
try {
|
||||||
|
throw new LogError('new LogError', { someWeirdValue123: 'arbitraryData456' })
|
||||||
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||||
|
} catch (e: any) {
|
||||||
|
expect(e.stack).not.toMatch(/(someWeirdValue123|arbitraryData456)/i)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
9
backend/src/server/LogError.ts
Normal file
9
backend/src/server/LogError.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { backendLogger as logger } from './logger'
|
||||||
|
|
||||||
|
export default class LogError extends Error {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
constructor(msg: string, ...details: any[]) {
|
||||||
|
super(msg)
|
||||||
|
logger.error(msg, ...details)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,10 +1,10 @@
|
|||||||
import { calculateDecay } from './decay'
|
import { calculateDecay } from './decay'
|
||||||
import Decimal from 'decimal.js-light'
|
import Decimal from 'decimal.js-light'
|
||||||
import { Transaction } from '@entity/Transaction'
|
|
||||||
import { Decay } from '@model/Decay'
|
import { Decay } from '@model/Decay'
|
||||||
import { getCustomRepository } from '@dbTools/typeorm'
|
import { getCustomRepository } from '@dbTools/typeorm'
|
||||||
import { TransactionLinkRepository } from '@repository/TransactionLink'
|
import { TransactionLinkRepository } from '@repository/TransactionLink'
|
||||||
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
import { TransactionLink as dbTransactionLink } from '@entity/TransactionLink'
|
||||||
|
import { getLastTransaction } from '../graphql/resolver/util/getLastTransaction'
|
||||||
|
|
||||||
function isStringBoolean(value: string): boolean {
|
function isStringBoolean(value: string): boolean {
|
||||||
const lowerValue = value.toLowerCase()
|
const lowerValue = value.toLowerCase()
|
||||||
@ -20,7 +20,7 @@ async function calculateBalance(
|
|||||||
time: Date,
|
time: Date,
|
||||||
transactionLink?: dbTransactionLink | null,
|
transactionLink?: dbTransactionLink | null,
|
||||||
): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number } | null> {
|
): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number } | null> {
|
||||||
const lastTransaction = await Transaction.findOne({ userId }, { order: { id: 'DESC' } })
|
const lastTransaction = await getLastTransaction(userId)
|
||||||
if (!lastTransaction) return null
|
if (!lastTransaction) return null
|
||||||
|
|
||||||
const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time)
|
const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time)
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
CONFIG_VERSION=v1.2022-03-18
|
|
||||||
|
|
||||||
DB_HOST=localhost
|
DB_HOST=localhost
|
||||||
DB_PORT=3306
|
DB_PORT=3306
|
||||||
DB_USER=root
|
DB_USER=root
|
||||||
|
|||||||
@ -16,17 +16,17 @@ export class EventProtocol extends BaseEntity {
|
|||||||
@Column({ name: 'user_id', unsigned: true, nullable: false })
|
@Column({ name: 'user_id', unsigned: true, nullable: false })
|
||||||
userId: number
|
userId: number
|
||||||
|
|
||||||
@Column({ name: 'x_user_id', unsigned: true, nullable: true })
|
@Column({ name: 'x_user_id', type: 'int', unsigned: true, nullable: true })
|
||||||
xUserId: number
|
xUserId: number | null
|
||||||
|
|
||||||
@Column({ name: 'x_community_id', unsigned: true, nullable: true })
|
@Column({ name: 'x_community_id', type: 'int', unsigned: true, nullable: true })
|
||||||
xCommunityId: number
|
xCommunityId: number | null
|
||||||
|
|
||||||
@Column({ name: 'transaction_id', unsigned: true, nullable: true })
|
@Column({ name: 'transaction_id', type: 'int', unsigned: true, nullable: true })
|
||||||
transactionId: number
|
transactionId: number | null
|
||||||
|
|
||||||
@Column({ name: 'contribution_id', unsigned: true, nullable: true })
|
@Column({ name: 'contribution_id', type: 'int', unsigned: true, nullable: true })
|
||||||
contributionId: number
|
contributionId: number | null
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
type: 'decimal',
|
type: 'decimal',
|
||||||
@ -35,8 +35,8 @@ export class EventProtocol extends BaseEntity {
|
|||||||
nullable: true,
|
nullable: true,
|
||||||
transformer: DecimalTransformer,
|
transformer: DecimalTransformer,
|
||||||
})
|
})
|
||||||
amount: Decimal
|
amount: Decimal | null
|
||||||
|
|
||||||
@Column({ name: 'message_id', unsigned: true, nullable: true })
|
@Column({ name: 'message_id', type: 'int', unsigned: true, nullable: true })
|
||||||
messageId: number
|
messageId: number | null
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "gradido-database",
|
"name": "gradido-database",
|
||||||
"version": "1.17.1",
|
"version": "1.18.1",
|
||||||
"description": "Gradido Database Tool to execute database migrations",
|
"description": "Gradido Database Tool to execute database migrations",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"repository": "https://github.com/gradido/gradido/database",
|
"repository": "https://github.com/gradido/gradido/database",
|
||||||
|
|||||||
@ -27,7 +27,7 @@ COMMUNITY_DESCRIPTION="Gradido Development Stage1 Test Community"
|
|||||||
COMMUNITY_SUPPORT_MAIL=support@supportmail.com
|
COMMUNITY_SUPPORT_MAIL=support@supportmail.com
|
||||||
|
|
||||||
# backend
|
# backend
|
||||||
BACKEND_CONFIG_VERSION=v14.2022-12-22
|
BACKEND_CONFIG_VERSION=v15.2023-02-07
|
||||||
|
|
||||||
JWT_EXPIRES_IN=10m
|
JWT_EXPIRES_IN=10m
|
||||||
GDT_API_URL=https://gdt.gradido.net
|
GDT_API_URL=https://gdt.gradido.net
|
||||||
@ -56,9 +56,6 @@ EMAIL_CODE_REQUEST_TIME=10
|
|||||||
|
|
||||||
WEBHOOK_ELOPAGE_SECRET=secret
|
WEBHOOK_ELOPAGE_SECRET=secret
|
||||||
|
|
||||||
# EventProtocol
|
|
||||||
EVENT_PROTOCOL_DISABLED=false
|
|
||||||
|
|
||||||
# Federation
|
# Federation
|
||||||
# if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen
|
# if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen
|
||||||
# on an hash created from this topic
|
# on an hash created from this topic
|
||||||
|
|||||||
17
dht-node/.env.dist
Normal file
17
dht-node/.env.dist
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
# Database
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_USER=root
|
||||||
|
DB_PASSWORD=
|
||||||
|
DB_DATABASE=gradido_community
|
||||||
|
TYPEORM_LOGGING_RELATIVE_PATH=typeorm.dht-node.log
|
||||||
|
|
||||||
|
# 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
|
||||||
14
dht-node/.env.template
Normal file
14
dht-node/.env.template
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
CONFIG_VERSION=$BACKEND_CONFIG_VERSION
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DB_HOST=localhost
|
||||||
|
DB_PORT=3306
|
||||||
|
DB_USER=$DB_USER
|
||||||
|
DB_PASSWORD=$DB_PASSWORD
|
||||||
|
DB_DATABASE=gradido_community
|
||||||
|
TYPEORM_LOGGING_RELATIVE_PATH=$TYPEORM_LOGGING_RELATIVE_PATH
|
||||||
|
|
||||||
|
# Federation
|
||||||
|
FEDERATION_DHT_TOPIC=$FEDERATION_DHT_TOPIC
|
||||||
|
FEDERATION_DHT_SEED=$FEDERATION_DHT_SEED
|
||||||
|
FEDERATION_COMMUNITY_URL=$FEDERATION_COMMUNITY_URL
|
||||||
3
dht-node/.eslintignore
Normal file
3
dht-node/.eslintignore
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
node_modules
|
||||||
|
**/*.min.js
|
||||||
|
build
|
||||||
26
dht-node/.eslintrc.js
Normal file
26
dht-node/.eslintrc.js
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
// jest: true,
|
||||||
|
},
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
plugins: ['prettier', '@typescript-eslint' /*, 'jest' */],
|
||||||
|
extends: [
|
||||||
|
'standard',
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:prettier/recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
],
|
||||||
|
// add your custom rules here
|
||||||
|
rules: {
|
||||||
|
'no-console': ['error'],
|
||||||
|
'no-debugger': 'error',
|
||||||
|
'prettier/prettier': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
htmlWhitespaceSensitivity: 'ignore',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
8
dht-node/.gitignore
vendored
Normal file
8
dht-node/.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/node_modules/
|
||||||
|
/.env
|
||||||
|
/.env.bak
|
||||||
|
/build/
|
||||||
|
package-json.lock
|
||||||
|
coverage
|
||||||
|
# emacs
|
||||||
|
*~
|
||||||
9
dht-node/.prettierrc.js
Normal file
9
dht-node/.prettierrc.js
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
module.exports = {
|
||||||
|
semi: false,
|
||||||
|
printWidth: 100,
|
||||||
|
singleQuote: true,
|
||||||
|
trailingComma: "all",
|
||||||
|
tabWidth: 2,
|
||||||
|
bracketSpacing: true,
|
||||||
|
endOfLine: "auto",
|
||||||
|
};
|
||||||
119
dht-node/Dockerfile
Normal file
119
dht-node/Dockerfile
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
##################################################################################
|
||||||
|
# BASE ###########################################################################
|
||||||
|
##################################################################################
|
||||||
|
FROM node:19.5.0-alpine3.17 as base
|
||||||
|
#FROM ubuntu:latest as base
|
||||||
|
|
||||||
|
# ENVs (available in production aswell, can be overwritten by commandline or env file)
|
||||||
|
## DOCKER_WORKDIR would be a classical ARG, but that is not multi layer persistent - shame
|
||||||
|
ENV DOCKER_WORKDIR="/app"
|
||||||
|
## We Cannot do `$(date -u +'%Y-%m-%dT%H:%M:%SZ')` here so we use unix timestamp=0
|
||||||
|
ENV BUILD_DATE="1970-01-01T00:00:00.00Z"
|
||||||
|
## We cannot do $(npm run version).${BUILD_NUMBER} here so we default to 0.0.0.0
|
||||||
|
ENV BUILD_VERSION="0.0.0.0"
|
||||||
|
## We cannot do `$(git rev-parse --short HEAD)` here so we default to 0000000
|
||||||
|
ENV BUILD_COMMIT="0000000"
|
||||||
|
## SET NODE_ENV
|
||||||
|
ENV NODE_ENV="production"
|
||||||
|
## App relevant Envs
|
||||||
|
#ENV PORT="5000"
|
||||||
|
|
||||||
|
# Labels
|
||||||
|
LABEL org.label-schema.build-date="${BUILD_DATE}"
|
||||||
|
LABEL org.label-schema.name="gradido:dht-node"
|
||||||
|
LABEL org.label-schema.description="Gradido dht-node"
|
||||||
|
LABEL org.label-schema.usage="https://github.com/gradido/gradido/blob/master/README.md"
|
||||||
|
LABEL org.label-schema.url="https://gradido.net"
|
||||||
|
LABEL org.label-schema.vcs-url="https://github.com/gradido/gradido/tree/master/dht-node"
|
||||||
|
LABEL org.label-schema.vcs-ref="${BUILD_COMMIT}"
|
||||||
|
LABEL org.label-schema.vendor="Gradido Community"
|
||||||
|
LABEL org.label-schema.version="${BUILD_VERSION}"
|
||||||
|
LABEL org.label-schema.schema-version="1.0"
|
||||||
|
LABEL maintainer="support@gradido.net"
|
||||||
|
|
||||||
|
# Install Additional Software
|
||||||
|
## install: sodium requirements
|
||||||
|
RUN apk add --no-cache --virtual build-deps python3 alpine-sdk autoconf libtool automake && \
|
||||||
|
mkdir -p /prebuilds && cd /prebuilds && npm init -y && npm install sodium-native@3.1.1 && \
|
||||||
|
apk del build-deps
|
||||||
|
ENV SODIUM_NATIVE_PREBUILD=/prebuilds/node_modules/sodium-native/
|
||||||
|
|
||||||
|
# Settings
|
||||||
|
## Expose Container Port
|
||||||
|
#EXPOSE ${PORT}
|
||||||
|
|
||||||
|
## Workdir
|
||||||
|
RUN mkdir -p ${DOCKER_WORKDIR}
|
||||||
|
WORKDIR ${DOCKER_WORKDIR}
|
||||||
|
|
||||||
|
RUN mkdir -p /database
|
||||||
|
|
||||||
|
##################################################################################
|
||||||
|
# DEVELOPMENT (Connected to the local environment, to reload on demand) ##########
|
||||||
|
##################################################################################
|
||||||
|
FROM base as development
|
||||||
|
|
||||||
|
# We don't need to copy or build anything since we gonna bind to the
|
||||||
|
# local filesystem which will need a rebuild anyway
|
||||||
|
|
||||||
|
# Run command
|
||||||
|
# (for development we need to execute yarn install since the
|
||||||
|
# node_modules are on another volume and need updating)
|
||||||
|
CMD /bin/sh -c "cd /database && yarn install && yarn build && cd /app && yarn install && yarn run dev"
|
||||||
|
|
||||||
|
##################################################################################
|
||||||
|
# BUILD (Does contain all files and is therefore bloated) ########################
|
||||||
|
##################################################################################
|
||||||
|
FROM base as build
|
||||||
|
|
||||||
|
# Copy everything from dht-node
|
||||||
|
COPY ./dht-node/ ./
|
||||||
|
# Copy everything from database
|
||||||
|
COPY ./database/ ../database/
|
||||||
|
|
||||||
|
# yarn install dht-node
|
||||||
|
RUN yarn install --production=false --frozen-lockfile --non-interactive
|
||||||
|
|
||||||
|
# yarn install database
|
||||||
|
RUN cd ../database && yarn install --production=false --frozen-lockfile --non-interactive
|
||||||
|
|
||||||
|
# yarn build
|
||||||
|
RUN yarn run build
|
||||||
|
|
||||||
|
# yarn build database
|
||||||
|
RUN cd ../database && yarn run build
|
||||||
|
|
||||||
|
##################################################################################
|
||||||
|
# TEST ###########################################################################
|
||||||
|
##################################################################################
|
||||||
|
FROM build as test
|
||||||
|
|
||||||
|
# Run command
|
||||||
|
CMD /bin/sh -c "yarn run start"
|
||||||
|
|
||||||
|
##################################################################################
|
||||||
|
# PRODUCTION (Does contain only "binary"- and static-files to reduce image size) #
|
||||||
|
##################################################################################
|
||||||
|
FROM base as production
|
||||||
|
|
||||||
|
# Copy "binary"-files from build image
|
||||||
|
COPY --from=build ${DOCKER_WORKDIR}/build ./build
|
||||||
|
COPY --from=build ${DOCKER_WORKDIR}/../database/build ../database/build
|
||||||
|
# We also copy the node_modules express and serve-static for the run script
|
||||||
|
COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules
|
||||||
|
COPY --from=build ${DOCKER_WORKDIR}/../database/node_modules ../database/node_modules
|
||||||
|
|
||||||
|
# Copy static files
|
||||||
|
# COPY --from=build ${DOCKER_WORKDIR}/public ./public
|
||||||
|
# Copy package.json for script definitions (lock file should not be needed)
|
||||||
|
COPY --from=build ${DOCKER_WORKDIR}/package.json ./package.json
|
||||||
|
# Copy tsconfig.json to provide alias path definitions
|
||||||
|
COPY --from=build ${DOCKER_WORKDIR}/tsconfig.json ./tsconfig.json
|
||||||
|
# Copy log4js-config.json to provide log configuration
|
||||||
|
COPY --from=build ${DOCKER_WORKDIR}/log4js-config.json ./log4js-config.json
|
||||||
|
|
||||||
|
# Copy run scripts run/
|
||||||
|
# COPY --from=build ${DOCKER_WORKDIR}/run ./run
|
||||||
|
|
||||||
|
# Run command
|
||||||
|
CMD /bin/sh -c "yarn run start"
|
||||||
22
dht-node/jest.config.js
Normal file
22
dht-node/jest.config.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
|
||||||
|
module.exports = {
|
||||||
|
verbose: true,
|
||||||
|
preset: 'ts-jest',
|
||||||
|
collectCoverage: true,
|
||||||
|
collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'],
|
||||||
|
setupFiles: ['<rootDir>/test/testSetup.ts'],
|
||||||
|
setupFilesAfterEnv: [],
|
||||||
|
modulePathIgnorePatterns: ['<rootDir>/build/'],
|
||||||
|
moduleNameMapper: {
|
||||||
|
'@/(.*)': '<rootDir>/src/$1',
|
||||||
|
'@test/(.*)': '<rootDir>/test/$1',
|
||||||
|
'@entity/(.*)':
|
||||||
|
process.env.NODE_ENV === 'development'
|
||||||
|
? '<rootDir>/../database/entity/$1'
|
||||||
|
: '<rootDir>/../database/build/entity/$1',
|
||||||
|
'@dbTools/(.*)':
|
||||||
|
process.env.NODE_ENV === 'development'
|
||||||
|
? '<rootDir>/../database/src/$1'
|
||||||
|
: '<rootDir>/../database/build/src/$1',
|
||||||
|
},
|
||||||
|
}
|
||||||
69
dht-node/log4js-config.json
Normal file
69
dht-node/log4js-config.json
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
{
|
||||||
|
"appenders":
|
||||||
|
{
|
||||||
|
"dht":
|
||||||
|
{
|
||||||
|
"type": "dateFile",
|
||||||
|
"filename": "../logs/dht-node/apiversion-%v.log",
|
||||||
|
"pattern": "yyyy-MM-dd",
|
||||||
|
"layout":
|
||||||
|
{
|
||||||
|
"type": "pattern", "pattern": "%d{ISO8601} %c [%X{user}] [%f : %l] - %m"
|
||||||
|
},
|
||||||
|
"keepFileExt" : true,
|
||||||
|
"fileNameSep" : "_",
|
||||||
|
"numBackups" : 30
|
||||||
|
},
|
||||||
|
"errorFile":
|
||||||
|
{
|
||||||
|
"type": "dateFile",
|
||||||
|
"filename": "../logs/dht-node/errors.log",
|
||||||
|
"pattern": "yyyy-MM-dd",
|
||||||
|
"layout":
|
||||||
|
{
|
||||||
|
"type": "pattern", "pattern": "%d{ISO8601} %c [%X{user}] [%f : %l] - %m"
|
||||||
|
},
|
||||||
|
"keepFileExt" : true,
|
||||||
|
"fileNameSep" : "_",
|
||||||
|
"numBackups" : 30
|
||||||
|
},
|
||||||
|
"errors":
|
||||||
|
{
|
||||||
|
"type": "logLevelFilter",
|
||||||
|
"level": "error",
|
||||||
|
"appender": "errorFile"
|
||||||
|
},
|
||||||
|
"out":
|
||||||
|
{
|
||||||
|
"type": "stdout",
|
||||||
|
"layout":
|
||||||
|
{
|
||||||
|
"type": "pattern", "pattern": "%d{ISO8601} %p %c [%X{user}] [%f : %l] - %m"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"categories":
|
||||||
|
{
|
||||||
|
"default":
|
||||||
|
{
|
||||||
|
"appenders":
|
||||||
|
[
|
||||||
|
"out",
|
||||||
|
"errors"
|
||||||
|
],
|
||||||
|
"level": "debug",
|
||||||
|
"enableCallStack": true
|
||||||
|
},
|
||||||
|
"dht":
|
||||||
|
{
|
||||||
|
"appenders":
|
||||||
|
[
|
||||||
|
"dht",
|
||||||
|
"out",
|
||||||
|
"errors"
|
||||||
|
],
|
||||||
|
"level": "debug",
|
||||||
|
"enableCallStack": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
45
dht-node/package.json
Normal file
45
dht-node/package.json
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "gradido-dht-node",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Gradido dht-node module",
|
||||||
|
"main": "src/index.ts",
|
||||||
|
"repository": "https://github.com/gradido/gradido/",
|
||||||
|
"author": "Claus-Peter Huebner",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"private": false,
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc --build",
|
||||||
|
"clean": "tsc --build --clean",
|
||||||
|
"start": "cross-env TZ=UTC TS_NODE_BASEURL=./build node -r tsconfig-paths/register build/src/index.js",
|
||||||
|
"dev": "cross-env TZ=UTC nodemon -w src --ext ts --exec ts-node -r dotenv/config -r tsconfig-paths/register src/index.ts",
|
||||||
|
"lint": "eslint --max-warnings=0 --ext .js,.ts .",
|
||||||
|
"test": "cross-env TZ=UTC NODE_ENV=development jest --runInBand --coverage --forceExit --detectOpenHandles"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@hyperswarm/dht": "^6.4.4",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
|
"dotenv": "10.0.0",
|
||||||
|
"log4js": "^6.7.1",
|
||||||
|
"nodemon": "^2.0.20",
|
||||||
|
"ts-node": "^10.9.1",
|
||||||
|
"tsconfig-paths": "^4.1.2",
|
||||||
|
"typescript": "^4.9.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/dotenv": "^8.2.0",
|
||||||
|
"@types/jest": "^27.0.2",
|
||||||
|
"@types/node": "^18.11.18",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.48.0",
|
||||||
|
"@typescript-eslint/parser": "^5.48.0",
|
||||||
|
"eslint": "^8.31.0",
|
||||||
|
"eslint-config-prettier": "^8.3.0",
|
||||||
|
"eslint-config-standard": "^17.0.0",
|
||||||
|
"eslint-plugin-import": "^2.23.4",
|
||||||
|
"eslint-plugin-n": "^15.6.0",
|
||||||
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
|
"eslint-plugin-promise": "^6.1.1",
|
||||||
|
"jest": "^27.2.4",
|
||||||
|
"prettier": "^2.3.1",
|
||||||
|
"ts-jest": "^27.0.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
56
dht-node/src/config/index.ts
Normal file
56
dht-node/src/config/index.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
// ATTENTION: DO NOT PUT ANY SECRETS IN HERE (or the .env)
|
||||||
|
import dotenv from 'dotenv'
|
||||||
|
dotenv.config()
|
||||||
|
|
||||||
|
const constants = {
|
||||||
|
DB_VERSION: '0059-add_hide_amount_to_users',
|
||||||
|
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: 'v2.2023-02-07',
|
||||||
|
CURRENT: '',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = {
|
||||||
|
PRODUCTION: process.env.NODE_ENV === 'production' || false,
|
||||||
|
}
|
||||||
|
|
||||||
|
const database = {
|
||||||
|
DB_HOST: process.env.DB_HOST || 'localhost',
|
||||||
|
DB_PORT: process.env.DB_PORT ? parseInt(process.env.DB_PORT) : 3306,
|
||||||
|
DB_USER: process.env.DB_USER || 'root',
|
||||||
|
DB_PASSWORD: process.env.DB_PASSWORD || '',
|
||||||
|
DB_DATABASE: process.env.DB_DATABASE || 'gradido_community',
|
||||||
|
TYPEORM_LOGGING_RELATIVE_PATH:
|
||||||
|
process.env.TYPEORM_LOGGING_RELATIVE_PATH || 'typeorm.dht-node.log',
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check config version
|
||||||
|
constants.CONFIG_VERSION.CURRENT = process.env.CONFIG_VERSION || constants.CONFIG_VERSION.DEFAULT
|
||||||
|
if (
|
||||||
|
![constants.CONFIG_VERSION.EXPECTED, constants.CONFIG_VERSION.DEFAULT].includes(
|
||||||
|
constants.CONFIG_VERSION.CURRENT,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
`Fatal: Config Version incorrect - expected "${constants.CONFIG_VERSION.EXPECTED}" or "${constants.CONFIG_VERSION.DEFAULT}", but found "${constants.CONFIG_VERSION.CURRENT}"`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const CONFIG = {
|
||||||
|
...constants,
|
||||||
|
...server,
|
||||||
|
...database,
|
||||||
|
...federation,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CONFIG
|
||||||
1
dht-node/src/dht_node/@types/@hyperswarm__dht/index.d.ts
vendored
Normal file
1
dht-node/src/dht_node/@types/@hyperswarm__dht/index.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
declare module '@hyperswarm/dht'
|
||||||
798
dht-node/src/dht_node/index.test.ts
Normal file
798
dht-node/src/dht_node/index.test.ts
Normal file
@ -0,0 +1,798 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
|
||||||
|
import { startDHT } from './index'
|
||||||
|
import DHT from '@hyperswarm/dht'
|
||||||
|
import CONFIG from '@/config'
|
||||||
|
import { logger } from '@test/testSetup'
|
||||||
|
import { Community as DbCommunity } from '@entity/Community'
|
||||||
|
import { testEnvironment, cleanDB } from '@test/helpers'
|
||||||
|
|
||||||
|
CONFIG.FEDERATION_DHT_SEED = '64ebcb0e3ad547848fef4197c6e2332f'
|
||||||
|
|
||||||
|
jest.mock('@hyperswarm/dht')
|
||||||
|
|
||||||
|
const TEST_TOPIC = 'gradido_test_topic'
|
||||||
|
|
||||||
|
const keyPairMock = {
|
||||||
|
publicKey: Buffer.from('publicKey'),
|
||||||
|
secretKey: Buffer.from('secretKey'),
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverListenSpy = jest.fn()
|
||||||
|
|
||||||
|
const serverEventMocks: { [key: string]: any } = {}
|
||||||
|
|
||||||
|
const serverOnMock = jest.fn().mockImplementation((key: string, callback) => {
|
||||||
|
serverEventMocks[key] = callback
|
||||||
|
})
|
||||||
|
|
||||||
|
const nodeCreateServerMock = jest.fn().mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
on: serverOnMock,
|
||||||
|
listen: serverListenSpy,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const nodeAnnounceMock = jest.fn().mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
finished: jest.fn(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const lookupResultMock = {
|
||||||
|
token: Buffer.from(TEST_TOPIC),
|
||||||
|
from: {
|
||||||
|
id: Buffer.from('somone'),
|
||||||
|
host: '188.95.53.5',
|
||||||
|
port: 63561,
|
||||||
|
},
|
||||||
|
to: { id: null, host: '83.53.31.27', port: 55723 },
|
||||||
|
peers: [
|
||||||
|
{
|
||||||
|
publicKey: Buffer.from('some-public-key'),
|
||||||
|
relayAddresses: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodeLookupMock = jest.fn().mockResolvedValue([lookupResultMock])
|
||||||
|
|
||||||
|
const socketEventMocks: { [key: string]: any } = {}
|
||||||
|
|
||||||
|
const socketOnMock = jest.fn().mockImplementation((key: string, callback) => {
|
||||||
|
socketEventMocks[key] = callback
|
||||||
|
})
|
||||||
|
|
||||||
|
const socketWriteMock = jest.fn()
|
||||||
|
|
||||||
|
const nodeConnectMock = jest.fn().mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
on: socketOnMock,
|
||||||
|
once: socketOnMock,
|
||||||
|
write: socketWriteMock,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
DHT.hash.mockImplementation(() => {
|
||||||
|
return Buffer.from(TEST_TOPIC)
|
||||||
|
})
|
||||||
|
|
||||||
|
DHT.keyPair.mockImplementation(() => {
|
||||||
|
return keyPairMock
|
||||||
|
})
|
||||||
|
|
||||||
|
DHT.mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
createServer: nodeCreateServerMock,
|
||||||
|
announce: nodeAnnounceMock,
|
||||||
|
lookup: nodeLookupMock,
|
||||||
|
connect: nodeConnectMock,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let con: any
|
||||||
|
let testEnv: any
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
testEnv = await testEnvironment()
|
||||||
|
con = testEnv.con
|
||||||
|
await cleanDB()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await cleanDB()
|
||||||
|
await con.close()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('federation', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
jest.useFakeTimers()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('call startDHT', () => {
|
||||||
|
const hashSpy = jest.spyOn(DHT, 'hash')
|
||||||
|
const keyPairSpy = jest.spyOn(DHT, 'keyPair')
|
||||||
|
beforeEach(async () => {
|
||||||
|
DHT.mockClear()
|
||||||
|
jest.clearAllMocks()
|
||||||
|
await startDHT(TEST_TOPIC)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('calls DHT.hash', () => {
|
||||||
|
expect(hashSpy).toBeCalledWith(Buffer.from(TEST_TOPIC))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates a key pair', () => {
|
||||||
|
expect(keyPairSpy).toBeCalledWith(expect.any(Buffer))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('initializes a new DHT object', () => {
|
||||||
|
expect(DHT).toBeCalledWith({ keyPair: keyPairMock })
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('DHT node', () => {
|
||||||
|
it('creates a server', () => {
|
||||||
|
expect(nodeCreateServerMock).toBeCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('listens on the server', () => {
|
||||||
|
expect(serverListenSpy).toBeCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('timers', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.runOnlyPendingTimers()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('announces on topic', () => {
|
||||||
|
expect(nodeAnnounceMock).toBeCalledWith(Buffer.from(TEST_TOPIC), keyPairMock)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('looks up on topic', () => {
|
||||||
|
expect(nodeLookupMock).toBeCalledWith(Buffer.from(TEST_TOPIC))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('server connection event', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
serverEventMocks.connection({
|
||||||
|
remotePublicKey: Buffer.from('another-public-key'),
|
||||||
|
on: socketOnMock,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('can be triggered', () => {
|
||||||
|
expect(socketOnMock).toBeCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('socket events', () => {
|
||||||
|
describe('on data', () => {
|
||||||
|
describe('with receiving simply a string', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
socketEventMocks.data(Buffer.from('no-json string'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs the received data', () => {
|
||||||
|
expect(logger.info).toBeCalledWith('data: no-json string')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs an error of unexpected data format and structure', () => {
|
||||||
|
expect(logger.error).toBeCalledWith(
|
||||||
|
'Error on receiving data from socket:',
|
||||||
|
new SyntaxError('Unexpected token \'o\', "no-json string" is not valid JSON'),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with receiving array of strings', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
const strArray: string[] = ['invalid type test', 'api', 'url']
|
||||||
|
socketEventMocks.data(Buffer.from(strArray.toString()))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs the received data', () => {
|
||||||
|
expect(logger.info).toBeCalledWith('data: invalid type test,api,url')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs an error of unexpected data format and structure', () => {
|
||||||
|
expect(logger.error).toBeCalledWith(
|
||||||
|
'Error on receiving data from socket:',
|
||||||
|
new SyntaxError('Unexpected token \'i\', "invalid ty"... is not valid JSON'),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with receiving array of string-arrays', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
const strArray: string[][] = [
|
||||||
|
[`api`, `url`, `invalid type in array test`],
|
||||||
|
[`wrong`, `api`, `url`],
|
||||||
|
]
|
||||||
|
await socketEventMocks.data(Buffer.from(strArray.toString()))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs the received data', () => {
|
||||||
|
expect(logger.info).toBeCalledWith(
|
||||||
|
'data: api,url,invalid type in array test,wrong,api,url',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs an error of unexpected data format and structure', () => {
|
||||||
|
expect(logger.error).toBeCalledWith(
|
||||||
|
'Error on receiving data from socket:',
|
||||||
|
new SyntaxError('Unexpected token \'a\', "api,url,in"... is not valid JSON'),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with receiving JSON-Array with too much entries', () => {
|
||||||
|
let jsonArray: { api: string; url: string }[]
|
||||||
|
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' },
|
||||||
|
]
|
||||||
|
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"}]',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs a warning of too much apiVersion-Definitions', () => {
|
||||||
|
expect(logger.warn).toBeCalledWith(
|
||||||
|
`received totaly wrong or too much apiVersions-Definition JSON-String: ${JSON.stringify(
|
||||||
|
jsonArray,
|
||||||
|
)}`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with receiving wrong but tolerated property data', () => {
|
||||||
|
let jsonArray: any[]
|
||||||
|
let result: DbCommunity[] = []
|
||||||
|
beforeAll(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
jsonArray = [
|
||||||
|
{
|
||||||
|
wrong: 'wrong but tolerated property test',
|
||||||
|
api: 'v1_0',
|
||||||
|
url: 'url1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
api: 'v2_0',
|
||||||
|
url: 'url2',
|
||||||
|
wrong: 'wrong but tolerated property test',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
||||||
|
result = await DbCommunity.find()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await cleanDB()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has two Communty entries in database', () => {
|
||||||
|
expect(result).toHaveLength(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has an entry for api version v1_0', () => {
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
publicKey: expect.any(Buffer),
|
||||||
|
apiVersion: 'v1_0',
|
||||||
|
endPoint: 'url1',
|
||||||
|
lastAnnouncedAt: expect.any(Date),
|
||||||
|
createdAt: expect.any(Date),
|
||||||
|
updatedAt: null,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has an entry for api version v2_0', () => {
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
publicKey: expect.any(Buffer),
|
||||||
|
apiVersion: 'v2_0',
|
||||||
|
endPoint: 'url2',
|
||||||
|
lastAnnouncedAt: expect.any(Date),
|
||||||
|
createdAt: expect.any(Date),
|
||||||
|
updatedAt: null,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with receiving data but missing api property', () => {
|
||||||
|
let jsonArray: any[]
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
jsonArray = [
|
||||||
|
{ test1: 'missing api proterty test', url: 'any url definition as string' },
|
||||||
|
{ api: 'some api', test2: 'missing url property test' },
|
||||||
|
]
|
||||||
|
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs the received data', () => {
|
||||||
|
expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs a warning of invalid apiVersion-Definition', () => {
|
||||||
|
expect(logger.warn).toBeCalledWith(
|
||||||
|
`received invalid apiVersion-Definition: ${JSON.stringify(jsonArray[0])}`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with receiving data but missing url property', () => {
|
||||||
|
let jsonArray: any[]
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
jsonArray = [
|
||||||
|
{ api: 'some api', test2: 'missing url property test' },
|
||||||
|
{ test1: 'missing api proterty test', url: 'any url definition as string' },
|
||||||
|
]
|
||||||
|
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs the received data', () => {
|
||||||
|
expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs a warning of invalid apiVersion-Definition', () => {
|
||||||
|
expect(logger.warn).toBeCalledWith(
|
||||||
|
`received invalid apiVersion-Definition: ${JSON.stringify(jsonArray[0])}`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with receiving data but wrong type of api property', () => {
|
||||||
|
let jsonArray: any[]
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
jsonArray = [
|
||||||
|
{ api: 1, url: 'wrong property type tests' },
|
||||||
|
{ api: 'urltyptest', url: 2 },
|
||||||
|
{ api: 1, url: 2 },
|
||||||
|
]
|
||||||
|
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs the received data', () => {
|
||||||
|
expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs a warning of invalid apiVersion-Definition', () => {
|
||||||
|
expect(logger.warn).toBeCalledWith(
|
||||||
|
`received invalid apiVersion-Definition: ${JSON.stringify(jsonArray[0])}`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with receiving data but wrong type of url property', () => {
|
||||||
|
let jsonArray: any[]
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
jsonArray = [
|
||||||
|
{ api: 'urltyptest', url: 2 },
|
||||||
|
{ api: 1, url: 'wrong property type tests' },
|
||||||
|
{ api: 1, url: 2 },
|
||||||
|
]
|
||||||
|
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs the received data', () => {
|
||||||
|
expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs a warning of invalid apiVersion-Definition', () => {
|
||||||
|
expect(logger.warn).toBeCalledWith(
|
||||||
|
`received invalid apiVersion-Definition: ${JSON.stringify(jsonArray[0])}`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with receiving data but wrong type of both properties', () => {
|
||||||
|
let jsonArray: any[]
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
jsonArray = [
|
||||||
|
{ api: 1, url: 2 },
|
||||||
|
{ api: 'urltyptest', url: 2 },
|
||||||
|
{ api: 1, url: 'wrong property type tests' },
|
||||||
|
]
|
||||||
|
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs the received data', () => {
|
||||||
|
expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs a warning of invalid apiVersion-Definition', () => {
|
||||||
|
expect(logger.warn).toBeCalledWith(
|
||||||
|
`received invalid apiVersion-Definition: ${JSON.stringify(jsonArray[0])}`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with receiving data but too long api string', () => {
|
||||||
|
let jsonArray: any[]
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
jsonArray = [
|
||||||
|
{ api: 'toolong api', url: 'some valid url' },
|
||||||
|
{
|
||||||
|
api: 'valid api',
|
||||||
|
url: 'this is a too long url definition with exact one character more than the allowed two hundert and fiftyfive characters. and here begins the fill characters with no sense of content menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmic',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
api: 'toolong api',
|
||||||
|
url: 'this is a too long url definition with exact one character more than the allowed two hundert and fiftyfive characters. and here begins the fill characters with no sense of content menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmic',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs the received data', () => {
|
||||||
|
expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs a warning of invalid apiVersion-Definition', () => {
|
||||||
|
expect(logger.warn).toBeCalledWith(
|
||||||
|
`received apiVersion with content longer than max length: ${JSON.stringify(
|
||||||
|
jsonArray[0],
|
||||||
|
)}`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with receiving data but too long url string', () => {
|
||||||
|
let jsonArray: any[]
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
jsonArray = [
|
||||||
|
{
|
||||||
|
api: 'api',
|
||||||
|
url: 'this is a too long url definition with exact one character more than the allowed two hundert and fiftyfive characters. and here begins the fill characters with no sense of content menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmic',
|
||||||
|
},
|
||||||
|
{ api: 'toolong api', url: 'some valid url' },
|
||||||
|
{
|
||||||
|
api: 'toolong api',
|
||||||
|
url: 'this is a too long url definition with exact one character more than the allowed two hundert and fiftyfive characters. and here begins the fill characters with no sense of content menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmic',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs the received data', () => {
|
||||||
|
expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs a warning of invalid apiVersion-Definition', () => {
|
||||||
|
expect(logger.warn).toBeCalledWith(
|
||||||
|
`received apiVersion with content longer than max length: ${JSON.stringify(
|
||||||
|
jsonArray[0],
|
||||||
|
)}`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with receiving data but both properties with too long strings', () => {
|
||||||
|
let jsonArray: any[]
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
jsonArray = [
|
||||||
|
{
|
||||||
|
api: 'toolong api',
|
||||||
|
url: 'this is a too long url definition with exact one character more than the allowed two hundert and fiftyfive characters. and here begins the fill characters with no sense of content menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmic',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
api: 'api',
|
||||||
|
url: 'this is a too long url definition with exact one character more than the allowed two hundert and fiftyfive characters. and here begins the fill characters with no sense of content menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmic',
|
||||||
|
},
|
||||||
|
{ api: 'toolong api', url: 'some valid url' },
|
||||||
|
]
|
||||||
|
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs the received data', () => {
|
||||||
|
expect(logger.info).toBeCalledWith(`data: ${JSON.stringify(jsonArray)}`)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with receiving data of exact max allowed properties length', () => {
|
||||||
|
let jsonArray: any[]
|
||||||
|
let result: DbCommunity[] = []
|
||||||
|
beforeAll(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
jsonArray = [
|
||||||
|
{
|
||||||
|
api: 'valid api',
|
||||||
|
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
api: 'api',
|
||||||
|
url: 'this is a too long url definition with exact one character more than the allowed two hundert and fiftyfive characters. and here begins the fill characters with no sense of content menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmic',
|
||||||
|
},
|
||||||
|
{ api: 'toolong api', url: 'some valid url' },
|
||||||
|
]
|
||||||
|
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
||||||
|
result = await DbCommunity.find()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await cleanDB()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has one Communty entry in database', () => {
|
||||||
|
expect(result).toHaveLength(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it(`has an entry with max content length for api and url`, () => {
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
publicKey: expect.any(Buffer),
|
||||||
|
apiVersion: 'valid api',
|
||||||
|
endPoint:
|
||||||
|
'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
||||||
|
lastAnnouncedAt: expect.any(Date),
|
||||||
|
createdAt: expect.any(Date),
|
||||||
|
updatedAt: null,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with receiving data of exact max allowed buffer length', () => {
|
||||||
|
let jsonArray: any[]
|
||||||
|
let result: DbCommunity[] = []
|
||||||
|
beforeAll(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
jsonArray = [
|
||||||
|
{
|
||||||
|
api: 'valid api1',
|
||||||
|
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
api: 'valid api2',
|
||||||
|
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
api: 'valid api3',
|
||||||
|
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
api: 'valid api4',
|
||||||
|
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
||||||
|
result = await DbCommunity.find()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await cleanDB()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has five Communty entries in database', () => {
|
||||||
|
expect(result).toHaveLength(4)
|
||||||
|
})
|
||||||
|
|
||||||
|
it(`has an entry 'valid api1' with max content length for api and url`, () => {
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
publicKey: expect.any(Buffer),
|
||||||
|
apiVersion: 'valid api1',
|
||||||
|
endPoint:
|
||||||
|
'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
||||||
|
lastAnnouncedAt: expect.any(Date),
|
||||||
|
createdAt: expect.any(Date),
|
||||||
|
updatedAt: null,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it(`has an entry 'valid api2' with max content length for api and url`, () => {
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
publicKey: expect.any(Buffer),
|
||||||
|
apiVersion: 'valid api2',
|
||||||
|
endPoint:
|
||||||
|
'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
||||||
|
lastAnnouncedAt: expect.any(Date),
|
||||||
|
createdAt: expect.any(Date),
|
||||||
|
updatedAt: null,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it(`has an entry 'valid api3' with max content length for api and url`, () => {
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
publicKey: expect.any(Buffer),
|
||||||
|
apiVersion: 'valid api3',
|
||||||
|
endPoint:
|
||||||
|
'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
||||||
|
lastAnnouncedAt: expect.any(Date),
|
||||||
|
createdAt: expect.any(Date),
|
||||||
|
updatedAt: null,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it(`has an entry 'valid api4' with max content length for api and url`, () => {
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
publicKey: expect.any(Buffer),
|
||||||
|
apiVersion: 'valid api4',
|
||||||
|
endPoint:
|
||||||
|
'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
||||||
|
lastAnnouncedAt: expect.any(Date),
|
||||||
|
createdAt: expect.any(Date),
|
||||||
|
updatedAt: null,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with receiving data longer than max allowed buffer length', () => {
|
||||||
|
let jsonArray: any[]
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
jsonArray = [
|
||||||
|
{
|
||||||
|
api: 'Xvalid api1',
|
||||||
|
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
api: 'valid api2',
|
||||||
|
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
api: 'valid api3',
|
||||||
|
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
api: 'valid api4',
|
||||||
|
url: 'this is a valid url definition with exact the max allowed length of two hundert and fiftyfive characters. and here begins the fill characters with no sense of content kuhwarmiga menschhabicheinhungerdassichnichtweiswoichheutnachtschlafensollsofriertesmich',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
await socketEventMocks.data(Buffer.from(JSON.stringify(jsonArray)))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('logs the received data', () => {
|
||||||
|
expect(logger.warn).toBeCalledWith(
|
||||||
|
`received more than max allowed length of data buffer: ${
|
||||||
|
JSON.stringify(jsonArray).length
|
||||||
|
} against 1141 max allowed`,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('with proper data', () => {
|
||||||
|
let result: DbCommunity[] = []
|
||||||
|
beforeAll(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
await socketEventMocks.data(
|
||||||
|
Buffer.from(
|
||||||
|
JSON.stringify([
|
||||||
|
{
|
||||||
|
api: 'v1_0',
|
||||||
|
url: 'http://localhost:4000/api/v1_0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
api: 'v2_0',
|
||||||
|
url: 'http://localhost:4000/api/v2_0',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
result = await DbCommunity.find()
|
||||||
|
})
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await cleanDB()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has two Communty entries in database', () => {
|
||||||
|
expect(result).toHaveLength(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has an entry for api version v1_0', () => {
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
publicKey: expect.any(Buffer),
|
||||||
|
apiVersion: 'v1_0',
|
||||||
|
endPoint: 'http://localhost:4000/api/v1_0',
|
||||||
|
lastAnnouncedAt: expect.any(Date),
|
||||||
|
createdAt: expect.any(Date),
|
||||||
|
updatedAt: null,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('has an entry for api version v2_0', () => {
|
||||||
|
expect(result).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: expect.any(Number),
|
||||||
|
publicKey: expect.any(Buffer),
|
||||||
|
apiVersion: 'v2_0',
|
||||||
|
endPoint: 'http://localhost:4000/api/v2_0',
|
||||||
|
lastAnnouncedAt: expect.any(Date),
|
||||||
|
createdAt: expect.any(Date),
|
||||||
|
updatedAt: null,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('on open', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
socketEventMocks.open()
|
||||||
|
})
|
||||||
|
|
||||||
|
it.skip('calls socket write with own api versions', () => {
|
||||||
|
expect(socketWriteMock).toBeCalledWith(
|
||||||
|
Buffer.from(
|
||||||
|
JSON.stringify([
|
||||||
|
{
|
||||||
|
api: 'v1_0',
|
||||||
|
url: 'http://localhost:4000/api/v1_0',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
api: 'v1_1',
|
||||||
|
url: 'http://localhost:4000/api/v1_1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
api: 'v2_0',
|
||||||
|
url: 'http://localhost:4000/api/v2_0',
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
186
dht-node/src/dht_node/index.ts
Normal file
186
dht-node/src/dht_node/index.ts
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
import DHT from '@hyperswarm/dht'
|
||||||
|
import { logger } from '@/server/logger'
|
||||||
|
import CONFIG from '@/config'
|
||||||
|
import { Community as DbCommunity } from '@entity/Community'
|
||||||
|
|
||||||
|
const KEY_SECRET_SEEDBYTES = 32
|
||||||
|
const getSeed = (): Buffer | null =>
|
||||||
|
CONFIG.FEDERATION_DHT_SEED ? Buffer.alloc(KEY_SECRET_SEEDBYTES, CONFIG.FEDERATION_DHT_SEED) : null
|
||||||
|
|
||||||
|
const POLLTIME = 20000
|
||||||
|
const SUCCESSTIME = 120000
|
||||||
|
const ERRORTIME = 240000
|
||||||
|
const ANNOUNCETIME = 30000
|
||||||
|
|
||||||
|
enum ApiVersionType {
|
||||||
|
V1_0 = 'v1_0',
|
||||||
|
V1_1 = 'v1_1',
|
||||||
|
V2_0 = 'v2_0',
|
||||||
|
}
|
||||||
|
type CommunityApi = {
|
||||||
|
api: string
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const startDHT = async (topic: string): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const TOPIC = DHT.hash(Buffer.from(topic))
|
||||||
|
const keyPair = DHT.keyPair(getSeed())
|
||||||
|
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
|
||||||
|
})
|
||||||
|
logger.debug(`ApiList: ${JSON.stringify(ownApiVersions)}`)
|
||||||
|
|
||||||
|
const node = new DHT({ keyPair })
|
||||||
|
|
||||||
|
const server = node.createServer()
|
||||||
|
|
||||||
|
server.on('connection', function (socket: any) {
|
||||||
|
logger.info(`server on... with Remote public key: ${socket.remotePublicKey.toString('hex')}`)
|
||||||
|
|
||||||
|
socket.on('data', async (data: Buffer) => {
|
||||||
|
try {
|
||||||
|
if (data.length > 1141) {
|
||||||
|
logger.warn(
|
||||||
|
`received more than max allowed length of data buffer: ${data.length} against 1141 max allowed`,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.info(`data: ${data.toString('ascii')}`)
|
||||||
|
const recApiVersions: CommunityApi[] = JSON.parse(data.toString('ascii'))
|
||||||
|
|
||||||
|
// TODO better to introduce the validation by https://github.com/typestack/class-validator
|
||||||
|
if (!recApiVersions || !Array.isArray(recApiVersions) || recApiVersions.length >= 5) {
|
||||||
|
logger.warn(
|
||||||
|
`received totaly wrong or too much apiVersions-Definition JSON-String: ${JSON.stringify(
|
||||||
|
recApiVersions,
|
||||||
|
)}`,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const recApiVersion of recApiVersions) {
|
||||||
|
if (
|
||||||
|
!recApiVersion.api ||
|
||||||
|
typeof recApiVersion.api !== 'string' ||
|
||||||
|
!recApiVersion.url ||
|
||||||
|
typeof recApiVersion.url !== 'string'
|
||||||
|
) {
|
||||||
|
logger.warn(
|
||||||
|
`received invalid apiVersion-Definition: ${JSON.stringify(recApiVersion)}`,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// TODO better to introduce the validation on entity-Level by https://github.com/typestack/class-validator
|
||||||
|
if (recApiVersion.api.length > 10 || recApiVersion.url.length > 255) {
|
||||||
|
logger.warn(
|
||||||
|
`received apiVersion with content longer than max length: ${JSON.stringify(
|
||||||
|
recApiVersion,
|
||||||
|
)}`,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const variables = {
|
||||||
|
apiVersion: recApiVersion.api,
|
||||||
|
endPoint: recApiVersion.url,
|
||||||
|
publicKey: socket.remotePublicKey.toString('hex'),
|
||||||
|
lastAnnouncedAt: new Date(),
|
||||||
|
}
|
||||||
|
logger.debug(`upsert with variables=${JSON.stringify(variables)}`)
|
||||||
|
// this will NOT update the updatedAt column, to distingue between a normal update and the last announcement
|
||||||
|
await DbCommunity.createQueryBuilder()
|
||||||
|
.insert()
|
||||||
|
.into(DbCommunity)
|
||||||
|
.values(variables)
|
||||||
|
.orUpdate({
|
||||||
|
conflict_target: ['id', 'publicKey', 'apiVersion'],
|
||||||
|
overwrite: ['end_point', 'last_announced_at'],
|
||||||
|
})
|
||||||
|
.execute()
|
||||||
|
logger.info(`federation community upserted successfully...`)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Error on receiving data from socket:', e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await server.listen()
|
||||||
|
|
||||||
|
setInterval(async () => {
|
||||||
|
logger.info(`Announcing on topic: ${TOPIC.toString('hex')}`)
|
||||||
|
await node.announce(TOPIC, keyPair).finished()
|
||||||
|
}, ANNOUNCETIME)
|
||||||
|
|
||||||
|
let successfulRequests: string[] = []
|
||||||
|
let errorfulRequests: string[] = []
|
||||||
|
|
||||||
|
setInterval(async () => {
|
||||||
|
logger.info('Refreshing successful nodes')
|
||||||
|
successfulRequests = []
|
||||||
|
}, SUCCESSTIME)
|
||||||
|
|
||||||
|
setInterval(async () => {
|
||||||
|
logger.info('Refreshing errorful nodes')
|
||||||
|
errorfulRequests = []
|
||||||
|
}, ERRORTIME)
|
||||||
|
|
||||||
|
setInterval(async () => {
|
||||||
|
const result = await node.lookup(TOPIC)
|
||||||
|
|
||||||
|
const collectedPubKeys: string[] = []
|
||||||
|
|
||||||
|
for await (const data of result) {
|
||||||
|
data.peers.forEach((peer: any) => {
|
||||||
|
const pubKey = peer.publicKey.toString('hex')
|
||||||
|
if (
|
||||||
|
pubKey !== keyPair.publicKey.toString('hex') &&
|
||||||
|
!successfulRequests.includes(pubKey) &&
|
||||||
|
!errorfulRequests.includes(pubKey) &&
|
||||||
|
!collectedPubKeys.includes(pubKey)
|
||||||
|
) {
|
||||||
|
collectedPubKeys.push(peer.publicKey.toString('hex'))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (collectedPubKeys.length) {
|
||||||
|
logger.info(`Found new peers: ${collectedPubKeys}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
collectedPubKeys.forEach((remotePubKey) => {
|
||||||
|
const socket = node.connect(Buffer.from(remotePubKey, 'hex'))
|
||||||
|
|
||||||
|
// socket.once("connect", function () {
|
||||||
|
// console.log("client side emitted connect");
|
||||||
|
// });
|
||||||
|
|
||||||
|
// socket.once("end", function () {
|
||||||
|
// console.log("client side ended");
|
||||||
|
// });
|
||||||
|
|
||||||
|
socket.once('error', (err: any) => {
|
||||||
|
errorfulRequests.push(remotePubKey)
|
||||||
|
logger.error(`error on peer ${remotePubKey}: ${err.message}`)
|
||||||
|
})
|
||||||
|
|
||||||
|
socket.on('open', function () {
|
||||||
|
socket.write(Buffer.from(JSON.stringify(ownApiVersions)))
|
||||||
|
successfulRequests.push(remotePubKey)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}, POLLTIME)
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('DHT unexpected error:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
38
dht-node/src/index.ts
Normal file
38
dht-node/src/index.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { startDHT } from '@/dht_node/index'
|
||||||
|
|
||||||
|
// config
|
||||||
|
import CONFIG from './config'
|
||||||
|
import { logger } from './server/logger'
|
||||||
|
import connection from './typeorm/connection'
|
||||||
|
import { checkDBVersion } from './typeorm/DBVersion'
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// open mysql connection
|
||||||
|
const con = await connection()
|
||||||
|
if (!con || !con.isConnected) {
|
||||||
|
logger.fatal(`Couldn't open connection to database!`)
|
||||||
|
throw new Error(`Fatal: Couldn't open connection to database`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for correct database version
|
||||||
|
const dbVersion = await checkDBVersion(CONFIG.DB_VERSION)
|
||||||
|
if (!dbVersion) {
|
||||||
|
logger.fatal('Fatal: Database Version incorrect')
|
||||||
|
throw new Error('Fatal: Database Version incorrect')
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(
|
||||||
|
`starting Federation on ${CONFIG.FEDERATION_DHT_TOPIC} ${
|
||||||
|
CONFIG.FEDERATION_DHT_SEED ? 'with seed...' : 'without seed...'
|
||||||
|
}`,
|
||||||
|
)
|
||||||
|
await startDHT(CONFIG.FEDERATION_DHT_TOPIC)
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((e) => {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(e)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
20
dht-node/src/server/logger.ts
Normal file
20
dht-node/src/server/logger.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import log4js from 'log4js'
|
||||||
|
import CONFIG from '@/config'
|
||||||
|
|
||||||
|
import { readFileSync } from 'fs'
|
||||||
|
|
||||||
|
const options = JSON.parse(readFileSync(CONFIG.LOG4JS_CONFIG, 'utf-8'))
|
||||||
|
|
||||||
|
options.categories.dht.level = CONFIG.LOG_LEVEL
|
||||||
|
let filename: string = options.appenders.dht.filename
|
||||||
|
options.appenders.dht.filename = filename.replace(
|
||||||
|
'apiversion-%v',
|
||||||
|
'dht-' + CONFIG.FEDERATION_DHT_TOPIC,
|
||||||
|
)
|
||||||
|
filename = options.appenders.errorFile.filename
|
||||||
|
|
||||||
|
log4js.configure(options)
|
||||||
|
|
||||||
|
const logger = log4js.getLogger('dht')
|
||||||
|
|
||||||
|
export { logger }
|
||||||
27
dht-node/src/typeorm/DBVersion.ts
Normal file
27
dht-node/src/typeorm/DBVersion.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { Migration } from '@entity/Migration'
|
||||||
|
import { logger } from '@/server/logger'
|
||||||
|
|
||||||
|
const getDBVersion = async (): Promise<string | null> => {
|
||||||
|
try {
|
||||||
|
const dbVersion = await Migration.findOne({ order: { version: 'DESC' } })
|
||||||
|
return dbVersion ? dbVersion.fileName : null
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkDBVersion = async (DB_VERSION: string): Promise<boolean> => {
|
||||||
|
const dbVersion = await getDBVersion()
|
||||||
|
if (!dbVersion || dbVersion.indexOf(DB_VERSION) === -1) {
|
||||||
|
logger.error(
|
||||||
|
`Wrong database version detected - the dht-node requires '${DB_VERSION}' but found '${
|
||||||
|
dbVersion || 'None'
|
||||||
|
}`,
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export { checkDBVersion, getDBVersion }
|
||||||
34
dht-node/src/typeorm/connection.ts
Normal file
34
dht-node/src/typeorm/connection.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
// TODO This is super weird - since the entities are defined in another project they have their own globals.
|
||||||
|
// We cannot use our connection here, but must use the external typeorm installation
|
||||||
|
import { Connection, createConnection, FileLogger } from '@dbTools/typeorm'
|
||||||
|
import CONFIG from '@/config'
|
||||||
|
import { entities } from '@entity/index'
|
||||||
|
|
||||||
|
const connection = async (): Promise<Connection | null> => {
|
||||||
|
try {
|
||||||
|
return createConnection({
|
||||||
|
name: 'default',
|
||||||
|
type: 'mysql',
|
||||||
|
host: CONFIG.DB_HOST,
|
||||||
|
port: CONFIG.DB_PORT,
|
||||||
|
username: CONFIG.DB_USER,
|
||||||
|
password: CONFIG.DB_PASSWORD,
|
||||||
|
database: CONFIG.DB_DATABASE,
|
||||||
|
entities,
|
||||||
|
synchronize: false,
|
||||||
|
logging: true,
|
||||||
|
logger: new FileLogger('all', {
|
||||||
|
logPath: CONFIG.TYPEORM_LOGGING_RELATIVE_PATH,
|
||||||
|
}),
|
||||||
|
extra: {
|
||||||
|
charset: 'utf8mb4_unicode_ci',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log(error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connection
|
||||||
7
dht-node/test/helpers.test.ts
Normal file
7
dht-node/test/helpers.test.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { contributionDateFormatter } from '@test/helpers'
|
||||||
|
|
||||||
|
describe('contributionDateFormatter', () => {
|
||||||
|
it('formats the date correctly', () => {
|
||||||
|
expect(contributionDateFormatter(new Date('Thu Feb 29 2024 13:12:11'))).toEqual('2/29/2024')
|
||||||
|
})
|
||||||
|
})
|
||||||
68
dht-node/test/helpers.ts
Normal file
68
dht-node/test/helpers.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
|
|
||||||
|
import CONFIG from '@/config'
|
||||||
|
import connection from '@/typeorm/connection'
|
||||||
|
import { checkDBVersion } from '@/typeorm/DBVersion'
|
||||||
|
import { initialize } from '@dbTools/helpers'
|
||||||
|
import { entities } from '@entity/index'
|
||||||
|
import { logger } from './testSetup'
|
||||||
|
|
||||||
|
export const headerPushMock = jest.fn((t) => {
|
||||||
|
context.token = t.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
token: '',
|
||||||
|
setHeaders: {
|
||||||
|
push: headerPushMock,
|
||||||
|
forEach: jest.fn(),
|
||||||
|
},
|
||||||
|
clientTimezoneOffset: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cleanDB = async () => {
|
||||||
|
// this only works as long we do not have foreign key constraints
|
||||||
|
for (let i = 0; i < entities.length; i++) {
|
||||||
|
await resetEntity(entities[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const testEnvironment = async () => {
|
||||||
|
// open mysql connection
|
||||||
|
const con = await connection()
|
||||||
|
if (!con || !con.isConnected) {
|
||||||
|
logger.fatal(`Couldn't open connection to database!`)
|
||||||
|
throw new Error(`Fatal: Couldn't open connection to database`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for correct database version
|
||||||
|
const dbVersion = await checkDBVersion(CONFIG.DB_VERSION)
|
||||||
|
if (!dbVersion) {
|
||||||
|
logger.fatal('Fatal: Database Version incorrect')
|
||||||
|
throw new Error('Fatal: Database Version incorrect')
|
||||||
|
}
|
||||||
|
await initialize()
|
||||||
|
return { con }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const resetEntity = async (entity: any) => {
|
||||||
|
const items = await entity.find({ withDeleted: true })
|
||||||
|
if (items.length > 0) {
|
||||||
|
const ids = items.map((i: any) => i.id)
|
||||||
|
await entity.delete(ids)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const resetToken = () => {
|
||||||
|
context.token = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// format date string as it comes from the frontend for the contribution date
|
||||||
|
export const contributionDateFormatter = (date: Date): string => {
|
||||||
|
return `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setClientTimezoneOffset = (offset: number): void => {
|
||||||
|
context.clientTimezoneOffset = offset
|
||||||
|
}
|
||||||
22
dht-node/test/testSetup.ts
Normal file
22
dht-node/test/testSetup.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { logger } from '@/server/logger'
|
||||||
|
|
||||||
|
jest.setTimeout(1000000)
|
||||||
|
|
||||||
|
jest.mock('@/server/logger', () => {
|
||||||
|
const originalModule = jest.requireActual('@/server/logger')
|
||||||
|
return {
|
||||||
|
__esModule: true,
|
||||||
|
...originalModule,
|
||||||
|
logger: {
|
||||||
|
addContext: jest.fn(),
|
||||||
|
trace: jest.fn(),
|
||||||
|
debug: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
info: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
fatal: jest.fn(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export { logger }
|
||||||
86
dht-node/tsconfig.json
Normal file
86
dht-node/tsconfig.json
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||||
|
|
||||||
|
/* Basic Options */
|
||||||
|
// "incremental": true, /* Enable incremental compilation */
|
||||||
|
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */
|
||||||
|
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
|
||||||
|
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||||
|
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||||
|
// "checkJs": true, /* Report errors in .js files. */
|
||||||
|
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
|
||||||
|
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||||
|
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||||
|
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||||
|
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||||
|
"outDir": "./build", /* Redirect output structure to the directory. */
|
||||||
|
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
|
||||||
|
// "composite": true, /* Enable project compilation */
|
||||||
|
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
||||||
|
// "removeComments": true, /* Do not emit comments to output. */
|
||||||
|
// "noEmit": true, /* Do not emit outputs. */
|
||||||
|
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||||
|
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||||
|
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||||
|
|
||||||
|
/* Strict Type-Checking Options */
|
||||||
|
"strict": true, /* Enable all strict type-checking options. */
|
||||||
|
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||||
|
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||||
|
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||||
|
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||||
|
"strictPropertyInitialization": false, /* Enable strict checking of property initialization in classes. */
|
||||||
|
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||||
|
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||||
|
|
||||||
|
/* Additional Checks */
|
||||||
|
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||||
|
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||||
|
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||||
|
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||||
|
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
|
||||||
|
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an 'override' modifier. */
|
||||||
|
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
|
||||||
|
|
||||||
|
/* Module Resolution Options */
|
||||||
|
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||||
|
"baseUrl": ".", /* Base directory to resolve non-absolute module names. */
|
||||||
|
"paths": { /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||||
|
"@/*": ["src/*"],
|
||||||
|
"@test/*": ["test/*"],
|
||||||
|
/* external */
|
||||||
|
"@typeorm/*": ["../backend/src/typeorm/*", "../../backend/src/typeorm/*"],
|
||||||
|
"@dbTools/*": ["../database/src/*", "../../database/build/src/*"],
|
||||||
|
"@entity/*": ["../database/entity/*", "../../database/build/entity/*"]
|
||||||
|
},
|
||||||
|
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||||
|
"typeRoots": ["src/dht_node/@types", "node_modules/@types"], /* List of folders to include type definitions from. */
|
||||||
|
// "types": [], /* Type declaration files to be included in compilation. */
|
||||||
|
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||||
|
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||||
|
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||||
|
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||||
|
|
||||||
|
/* Source Map Options */
|
||||||
|
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||||
|
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||||
|
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||||
|
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||||
|
|
||||||
|
/* Experimental Options */
|
||||||
|
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||||
|
"emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||||
|
|
||||||
|
/* Advanced Options */
|
||||||
|
"skipLibCheck": true, /* Skip type checking of declaration files. */
|
||||||
|
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
||||||
|
},
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "../database/tsconfig.json",
|
||||||
|
// add 'prepend' if you want to include the referenced project in your output file
|
||||||
|
// "prepend": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
4320
dht-node/yarn.lock
Normal file
4320
dht-node/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@ -61,6 +61,29 @@ services:
|
|||||||
- ./backend:/app
|
- ./backend:/app
|
||||||
- ./database:/database
|
- ./database:/database
|
||||||
|
|
||||||
|
########################################################
|
||||||
|
# DHT-NODE #############################################
|
||||||
|
########################################################
|
||||||
|
dht-node:
|
||||||
|
# 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/dht-node: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
|
||||||
|
- dht_node_modules:/app/node_modules
|
||||||
|
- dht_database_node_modules:/database/node_modules
|
||||||
|
- dht_database_build:/database/build
|
||||||
|
# bind the local folder to the docker to allow live reload
|
||||||
|
- ./dht-node:/app
|
||||||
|
- ./database:/database
|
||||||
|
|
||||||
########################################################
|
########################################################
|
||||||
# DATABASE ##############################################
|
# DATABASE ##############################################
|
||||||
########################################################
|
########################################################
|
||||||
@ -129,5 +152,8 @@ volumes:
|
|||||||
backend_node_modules:
|
backend_node_modules:
|
||||||
backend_database_node_modules:
|
backend_database_node_modules:
|
||||||
backend_database_build:
|
backend_database_build:
|
||||||
|
dht_node_modules:
|
||||||
|
dht_database_node_modules:
|
||||||
|
dht_database_build:
|
||||||
database_node_modules:
|
database_node_modules:
|
||||||
database_build:
|
database_build:
|
||||||
@ -111,6 +111,42 @@ services:
|
|||||||
# <host_machine_directory>:<container_directory> – mirror bidirectional path in local context with path in Docker container
|
# <host_machine_directory>:<container_directory> – mirror bidirectional path in local context with path in Docker container
|
||||||
- ./logs/backend:/logs/backend
|
- ./logs/backend:/logs/backend
|
||||||
|
|
||||||
|
########################################################
|
||||||
|
# DHT-NODE #############################################
|
||||||
|
########################################################
|
||||||
|
dht-node:
|
||||||
|
# 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/dht-node:local-production
|
||||||
|
build:
|
||||||
|
# since we have to include the entities from ./database we cannot define the context as ./backend
|
||||||
|
# this might blow build image size to the moon ?!
|
||||||
|
context: ./
|
||||||
|
dockerfile: ./dht-node/Dockerfile
|
||||||
|
target: production
|
||||||
|
networks:
|
||||||
|
- internal-net
|
||||||
|
- external-net
|
||||||
|
#ports:
|
||||||
|
# - 5000:5000
|
||||||
|
depends_on:
|
||||||
|
- mariadb
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
# Envs used in Dockerfile
|
||||||
|
# - DOCKER_WORKDIR="/app"
|
||||||
|
# - PORT=5000
|
||||||
|
- 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/dht-node:/logs/dht-node
|
||||||
|
|
||||||
########################################################
|
########################################################
|
||||||
# DATABASE #############################################
|
# DATABASE #############################################
|
||||||
########################################################
|
########################################################
|
||||||
|
|||||||
@ -52,10 +52,6 @@ const community = {
|
|||||||
process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.',
|
process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.',
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
// const eventProtocol = {
|
|
||||||
// global switch to enable writing of EventProtocol-Entries
|
|
||||||
// EVENT_PROTOCOL_DISABLED: process.env.EVENT_PROTOCOL_DISABLED === 'true' || false,
|
|
||||||
// }
|
|
||||||
|
|
||||||
// This is needed by graphql-directive-auth
|
// This is needed by graphql-directive-auth
|
||||||
// process.env.APP_SECRET = server.JWT_SECRET
|
// process.env.APP_SECRET = server.JWT_SECRET
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
CONFIG_VERSION=v4.2022-12-20
|
|
||||||
|
|
||||||
# Environment
|
# Environment
|
||||||
DEFAULT_PUBLISHER_ID=2896
|
DEFAULT_PUBLISHER_ID=2896
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "bootstrap-vue-gradido-wallet",
|
"name": "bootstrap-vue-gradido-wallet",
|
||||||
"version": "1.17.1",
|
"version": "1.18.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node run/server.js",
|
"start": "node run/server.js",
|
||||||
@ -104,5 +104,10 @@
|
|||||||
],
|
],
|
||||||
"author": "Gradido-Akademie - https://www.gradido.net/",
|
"author": "Gradido-Akademie - https://www.gradido.net/",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"description": "Gradido, the Natural Economy of Life, is a way to worldwide prosperity and peace in harmony with nature. - Gradido, die Natürliche Ökonomie des lebens, ist ein Weg zu weltweitem Wohlstand und Frieden in Harmonie mit der Natur."
|
"description": "Gradido, the Natural Economy of Life, is a way to worldwide prosperity and peace in harmony with nature. - Gradido, die Natürliche Ökonomie des lebens, ist ein Weg zu weltweitem Wohlstand und Frieden in Harmonie mit der Natur.",
|
||||||
|
"nodemonConfig": {
|
||||||
|
"ignore": [
|
||||||
|
"**/*.spec.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,11 +24,6 @@ describe('ContributionForm', () => {
|
|||||||
$t: jest.fn((t) => t),
|
$t: jest.fn((t) => t),
|
||||||
$d: jest.fn((d) => d),
|
$d: jest.fn((d) => d),
|
||||||
$n: jest.fn((n) => n),
|
$n: jest.fn((n) => n),
|
||||||
$store: {
|
|
||||||
state: {
|
|
||||||
creation: ['1000', '1000', '1000'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
$i18n: {
|
$i18n: {
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
},
|
},
|
||||||
@ -61,7 +56,7 @@ describe('ContributionForm', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('dates', () => {
|
describe('dates and max amounts', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await wrapper.setData({
|
await wrapper.setData({
|
||||||
form: {
|
form: {
|
||||||
@ -73,204 +68,176 @@ describe('ContributionForm', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('actual date', () => {
|
describe('max amount reached for both months', () => {
|
||||||
describe('same month', () => {
|
beforeEach(() => {
|
||||||
|
wrapper.setProps({
|
||||||
|
maxGddLastMonth: 0,
|
||||||
|
maxGddThisMonth: 0,
|
||||||
|
})
|
||||||
|
wrapper.setData({
|
||||||
|
form: {
|
||||||
|
id: null,
|
||||||
|
date: 'set',
|
||||||
|
memo: '',
|
||||||
|
amount: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows message that no contributions are available', () => {
|
||||||
|
expect(wrapper.find('[data-test="contribtion-message"]').text()).toBe(
|
||||||
|
'contribution.noOpenCreation.allMonth',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('max amount reached for last month, no date selected', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper.setProps({
|
||||||
|
maxGddLastMonth: 0,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows no message', () => {
|
||||||
|
expect(wrapper.find('[data-test="contribtion-message"]').exists()).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('max amount reached for last month, last month selected', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const now = new Date().toISOString()
|
wrapper.setProps({
|
||||||
await wrapper.findComponent({ name: 'BFormDatepicker' }).vm.$emit('input', now)
|
maxGddLastMonth: 0,
|
||||||
|
isThisMonth: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('isThisMonth', () => {
|
|
||||||
it('has true', () => {
|
|
||||||
expect(wrapper.vm.isThisMonth).toBe(true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe.skip('month before', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await wrapper
|
|
||||||
.findComponent({ name: 'BFormDatepicker' })
|
|
||||||
.vm.$emit('input', wrapper.vm.minimalDate)
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('isThisMonth', () => {
|
|
||||||
it('has false', () => {
|
|
||||||
expect(wrapper.vm.isThisMonth).toBe(false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe.skip('date in middle of year', () => {
|
|
||||||
describe('same month', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
// jest.useFakeTimers('modern')
|
|
||||||
// jest.setSystemTime(new Date('2020-07-06'))
|
|
||||||
// await wrapper.findComponent({ name: 'BFormDatepicker' }).vm.$emit('input', now)
|
|
||||||
await wrapper.setData({
|
await wrapper.setData({
|
||||||
maximalDate: new Date(2020, 6, 6),
|
form: {
|
||||||
form: { date: new Date(2020, 6, 6) },
|
id: null,
|
||||||
|
date: 'set',
|
||||||
|
memo: '',
|
||||||
|
amount: '',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('minimalDate', () => {
|
it('shows message that no contributions are available for last month', () => {
|
||||||
it('has "2020-06-01T00:00:00.000Z"', () => {
|
expect(wrapper.find('[data-test="contribtion-message"]').text()).toBe(
|
||||||
expect(wrapper.vm.minimalDate.toISOString()).toBe('2020-06-01T00:00:00.000Z')
|
'contribution.noOpenCreation.lastMonth',
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('isThisMonth', () => {
|
describe('max amount reached for last month, this month selected', () => {
|
||||||
it('has true', () => {
|
|
||||||
expect(wrapper.vm.isThisMonth).toBe(true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('month before', () => {
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
// jest.useFakeTimers('modern')
|
wrapper.setProps({
|
||||||
// jest.setSystemTime(new Date('2020-07-06'))
|
maxGddLastMonth: 0,
|
||||||
// console.log('middle of year date – now:', wrapper.vm.minimalDate)
|
isThisMonth: true,
|
||||||
// await wrapper
|
})
|
||||||
// .findComponent({ name: 'BFormDatepicker' })
|
|
||||||
// .vm.$emit('input', wrapper.vm.minimalDate)
|
|
||||||
await wrapper.setData({
|
await wrapper.setData({
|
||||||
maximalDate: new Date(2020, 6, 6),
|
form: {
|
||||||
form: { date: new Date(2020, 5, 6) },
|
id: null,
|
||||||
|
date: 'set',
|
||||||
|
memo: '',
|
||||||
|
amount: '',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('minimalDate', () => {
|
it('shows no message', () => {
|
||||||
it('has "2020-06-01T00:00:00.000Z"', () => {
|
expect(wrapper.find('[data-test="contribtion-message"]').exists()).toBe(false)
|
||||||
expect(wrapper.vm.minimalDate.toISOString()).toBe('2020-06-01T00:00:00.000Z')
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('isThisMonth', () => {
|
describe('max amount reached for this month, no date selected', () => {
|
||||||
it('has false', () => {
|
beforeEach(() => {
|
||||||
expect(wrapper.vm.isThisMonth).toBe(false)
|
wrapper.setProps({
|
||||||
})
|
maxGddThisMonth: 0,
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe.skip('date in january', () => {
|
it('shows no message', () => {
|
||||||
describe('same month', () => {
|
expect(wrapper.find('[data-test="contribtion-message"]').exists()).toBe(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('max amount reached for this month, this month selected', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
wrapper.setProps({
|
||||||
|
maxGddThisMonth: 0,
|
||||||
|
isThisMonth: true,
|
||||||
|
})
|
||||||
await wrapper.setData({
|
await wrapper.setData({
|
||||||
maximalDate: new Date(2020, 0, 6),
|
form: {
|
||||||
form: { date: new Date(2020, 0, 6) },
|
id: null,
|
||||||
|
date: 'set',
|
||||||
|
memo: '',
|
||||||
|
amount: '',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('minimalDate', () => {
|
it('shows message that no contributions are available for last month', () => {
|
||||||
it('has "2019-12-01T00:00:00.000Z"', () => {
|
expect(wrapper.find('[data-test="contribtion-message"]').text()).toBe(
|
||||||
expect(wrapper.vm.minimalDate.toISOString()).toBe('2019-12-01T00:00:00.000Z')
|
'contribution.noOpenCreation.thisMonth',
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('isThisMonth', () => {
|
describe('max amount reached for this month, last month selected', () => {
|
||||||
it('has true', () => {
|
|
||||||
expect(wrapper.vm.isThisMonth).toBe(true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('month before', () => {
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
// jest.useFakeTimers('modern')
|
wrapper.setProps({
|
||||||
// jest.setSystemTime(new Date('2020-07-06'))
|
maxGddThisMonth: 0,
|
||||||
// console.log('middle of year date – now:', wrapper.vm.minimalDate)
|
isThisMonth: false,
|
||||||
// await wrapper
|
})
|
||||||
// .findComponent({ name: 'BFormDatepicker' })
|
|
||||||
// .vm.$emit('input', wrapper.vm.minimalDate)
|
|
||||||
await wrapper.setData({
|
await wrapper.setData({
|
||||||
maximalDate: new Date(2020, 0, 6),
|
form: {
|
||||||
form: { date: new Date(2019, 11, 6) },
|
id: null,
|
||||||
|
date: 'set',
|
||||||
|
memo: '',
|
||||||
|
amount: '',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('minimalDate', () => {
|
it('shows no message', () => {
|
||||||
it('has "2019-12-01T00:00:00.000Z"', () => {
|
expect(wrapper.find('[data-test="contribtion-message"]').exists()).toBe(false)
|
||||||
expect(wrapper.vm.minimalDate.toISOString()).toBe('2019-12-01T00:00:00.000Z')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('isThisMonth', () => {
|
|
||||||
it('has false', () => {
|
|
||||||
expect(wrapper.vm.isThisMonth).toBe(false)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe.skip('date with the 31st day of the month', () => {
|
describe('default return message', () => {
|
||||||
describe('same month', () => {
|
it('returns an empty string', () => {
|
||||||
beforeEach(async () => {
|
expect(wrapper.vm.noOpenCreation).toBe('')
|
||||||
await wrapper.setData({
|
|
||||||
maximalDate: new Date('2022-10-31T00:00:00.000Z'),
|
|
||||||
form: { date: new Date('2022-10-31T00:00:00.000Z') },
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('minimalDate', () => {
|
describe('update amount', () => {
|
||||||
it('has "2022-09-01T00:00:00.000Z"', () => {
|
beforeEach(() => {
|
||||||
expect(wrapper.vm.minimalDate.toISOString()).toBe('2022-09-01T00:00:00.000Z')
|
wrapper.findComponent({ name: 'InputHour' }).vm.$emit('updateAmount', 20)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('updates form amount', () => {
|
||||||
|
expect(wrapper.vm.form.amount).toBe('400.00')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('isThisMonth', () => {
|
describe('watch value', () => {
|
||||||
it('has true', () => {
|
beforeEach(() => {
|
||||||
expect(wrapper.vm.isThisMonth).toBe(true)
|
wrapper.setProps({
|
||||||
})
|
value: {
|
||||||
})
|
id: 42,
|
||||||
|
date: 'set',
|
||||||
|
memo: 'Some Memo',
|
||||||
|
amount: '400.00',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe.skip('date with the 28th day of the month', () => {
|
it('updates form', () => {
|
||||||
describe('same month', () => {
|
expect(wrapper.vm.form).toEqual({
|
||||||
beforeEach(async () => {
|
id: 42,
|
||||||
await wrapper.setData({
|
date: 'set',
|
||||||
maximalDate: new Date('2023-02-28T00:00:00.000Z'),
|
memo: 'Some Memo',
|
||||||
form: { date: new Date('2023-02-28T00:00:00.000Z') },
|
amount: '400.00',
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('minimalDate', () => {
|
|
||||||
it('has "2023-01-01T00:00:00.000Z"', () => {
|
|
||||||
expect(wrapper.vm.minimalDate.toISOString()).toBe('2023-01-01T00:00:00.000Z')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('isThisMonth', () => {
|
|
||||||
it('has true', () => {
|
|
||||||
expect(wrapper.vm.isThisMonth).toBe(true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe.skip('date with 29.02.2024 leap year', () => {
|
|
||||||
describe('same month', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await wrapper.setData({
|
|
||||||
maximalDate: new Date('2024-02-29T00:00:00.000Z'),
|
|
||||||
form: { date: new Date('2024-02-29T00:00:00.000Z') },
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('minimalDate', () => {
|
|
||||||
it('has "2024-01-01T00:00:00.000Z"', () => {
|
|
||||||
expect(wrapper.vm.minimalDate.toISOString()).toBe('2024-01-01T00:00:00.000Z')
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('isThisMonth', () => {
|
|
||||||
it('has true', () => {
|
|
||||||
expect(wrapper.vm.isThisMonth).toBe(true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -477,24 +444,23 @@ describe('ContributionForm', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe.skip('on trigger submit', () => {
|
describe('on trigger submit', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await wrapper.find('form').trigger('submit')
|
await wrapper.find('form').trigger('submit')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('emits "update-contribution"', () => {
|
it('emits "update-contribution"', () => {
|
||||||
expect(wrapper.emitted('update-contribution')).toEqual(
|
expect(wrapper.emitted('update-contribution')).toEqual([
|
||||||
expect.arrayContaining([
|
[
|
||||||
expect.arrayContaining([
|
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
date: now,
|
date: now,
|
||||||
|
hours: 0,
|
||||||
memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...',
|
memo: 'Mein Beitrag zur Gemeinschaft für diesen Monat ...',
|
||||||
amount: '200',
|
amount: '200',
|
||||||
},
|
},
|
||||||
]),
|
],
|
||||||
]),
|
])
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<b-form
|
<b-form
|
||||||
ref="form"
|
ref="form"
|
||||||
@submit.prevent="submit"
|
@submit.prevent="submit"
|
||||||
class="p-3 bg-white appBoxShadow gradido-border-radius"
|
class="form-style p-3 bg-white appBoxShadow gradido-border-radius"
|
||||||
>
|
>
|
||||||
<label>{{ $t('contribution.selectDate') }}</label>
|
<label>{{ $t('contribution.selectDate') }}</label>
|
||||||
<b-form-datepicker
|
<b-form-datepicker
|
||||||
@ -23,6 +23,10 @@
|
|||||||
<template #nav-next-year><span></span></template>
|
<template #nav-next-year><span></span></template>
|
||||||
</b-form-datepicker>
|
</b-form-datepicker>
|
||||||
|
|
||||||
|
<div v-if="showMessage" class="p-3" data-test="contribtion-message">
|
||||||
|
{{ noOpenCreation }}
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
<input-textarea
|
<input-textarea
|
||||||
id="contribution-memo"
|
id="contribution-memo"
|
||||||
v-model="form.memo"
|
v-model="form.memo"
|
||||||
@ -56,17 +60,30 @@
|
|||||||
></input-amount>
|
></input-amount>
|
||||||
|
|
||||||
<b-row class="mt-5">
|
<b-row class="mt-5">
|
||||||
<b-col>
|
<b-col cols="12" lg="6">
|
||||||
<b-button type="reset" variant="secondary" @click="reset" data-test="button-cancel">
|
<b-button
|
||||||
|
block
|
||||||
|
type="reset"
|
||||||
|
variant="secondary"
|
||||||
|
@click="reset"
|
||||||
|
data-test="button-cancel"
|
||||||
|
>
|
||||||
{{ $t('form.cancel') }}
|
{{ $t('form.cancel') }}
|
||||||
</b-button>
|
</b-button>
|
||||||
</b-col>
|
</b-col>
|
||||||
<b-col class="text-right">
|
<b-col cols="12" lg="6" class="text-right mt-4 mt-lg-0">
|
||||||
<b-button type="submit" variant="gradido" :disabled="disabled" data-test="button-submit">
|
<b-button
|
||||||
|
block
|
||||||
|
type="submit"
|
||||||
|
variant="gradido"
|
||||||
|
:disabled="disabled"
|
||||||
|
data-test="button-submit"
|
||||||
|
>
|
||||||
{{ form.id ? $t('form.change') : $t('contribution.submit') }}
|
{{ form.id ? $t('form.change') : $t('contribution.submit') }}
|
||||||
</b-button>
|
</b-button>
|
||||||
</b-col>
|
</b-col>
|
||||||
</b-row>
|
</b-row>
|
||||||
|
</div>
|
||||||
</b-form>
|
</b-form>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -98,8 +115,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateAmount(amount) {
|
updateAmount(hours) {
|
||||||
this.form.amount = (amount * 20).toFixed(2).toString()
|
this.form.amount = (hours * 20).toFixed(2).toString()
|
||||||
},
|
},
|
||||||
submit() {
|
submit() {
|
||||||
this.$emit(this.form.id ? 'update-contribution' : 'set-contribution', { ...this.form })
|
this.$emit(this.form.id ? 'update-contribution' : 'set-contribution', { ...this.form })
|
||||||
@ -115,6 +132,15 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
showMessage() {
|
||||||
|
if (this.maxGddThisMonth <= 0 && this.maxGddLastMonth <= 0) return true
|
||||||
|
if (this.form.date)
|
||||||
|
return (
|
||||||
|
(this.isThisMonth && this.maxGddThisMonth <= 0) ||
|
||||||
|
(!this.isThisMonth && this.maxGddLastMonth <= 0)
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
},
|
||||||
disabled() {
|
disabled() {
|
||||||
return (
|
return (
|
||||||
this.form.date === '' ||
|
this.form.date === '' ||
|
||||||
@ -133,6 +159,18 @@ export default {
|
|||||||
validMaxTime() {
|
validMaxTime() {
|
||||||
return Number(this.validMaxGDD / 20)
|
return Number(this.validMaxGDD / 20)
|
||||||
},
|
},
|
||||||
|
noOpenCreation() {
|
||||||
|
if (this.maxGddThisMonth <= 0 && this.maxGddLastMonth <= 0) {
|
||||||
|
return this.$t('contribution.noOpenCreation.allMonth')
|
||||||
|
}
|
||||||
|
if (this.isThisMonth && this.maxGddThisMonth <= 0) {
|
||||||
|
return this.$t('contribution.noOpenCreation.thisMonth')
|
||||||
|
}
|
||||||
|
if (!this.isThisMonth && this.maxGddLastMonth <= 0) {
|
||||||
|
return this.$t('contribution.noOpenCreation.lastMonth')
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
value() {
|
value() {
|
||||||
@ -142,6 +180,9 @@ export default {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
|
.form-style {
|
||||||
|
min-height: 410px;
|
||||||
|
}
|
||||||
span.errors {
|
span.errors {
|
||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -116,5 +116,15 @@ describe('ContributionList', () => {
|
|||||||
expect(wrapper.emitted('delete-contribution')).toEqual([[{ id: 2 }]])
|
expect(wrapper.emitted('delete-contribution')).toEqual([[{ id: 2 }]])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('update status', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
wrapper.findComponent({ name: 'ContributionListItem' }).vm.$emit('update-state', { id: 2 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('emits update status', () => {
|
||||||
|
expect(wrapper.emitted('update-state')).toEqual([[{ id: 2 }]])
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -2,10 +2,10 @@
|
|||||||
<div class="transaction-form">
|
<div class="transaction-form">
|
||||||
<b-row>
|
<b-row>
|
||||||
<b-col cols="12">
|
<b-col cols="12">
|
||||||
<b-card class="appBoxShadow gradido-border-radius" body-class="p-3">
|
<b-card class="appBoxShadow gradido-border-radius" body-class="p-4">
|
||||||
<validation-observer v-slot="{ handleSubmit }" ref="formValidator">
|
<validation-observer v-slot="{ handleSubmit }" ref="formValidator">
|
||||||
<b-form role="form" @submit.prevent="handleSubmit(onSubmit)" @reset="onReset">
|
<b-form role="form" @submit.prevent="handleSubmit(onSubmit)" @reset="onReset">
|
||||||
<b-form-radio-group v-model="radioSelected" class="container">
|
<b-form-radio-group v-model="radioSelected">
|
||||||
<b-row class="mb-4">
|
<b-row class="mb-4">
|
||||||
<b-col cols="12" lg="6">
|
<b-col cols="12" lg="6">
|
||||||
<b-row class="bg-248 gradido-border-radius pt-lg-2 mr-lg-2">
|
<b-row class="bg-248 gradido-border-radius pt-lg-2 mr-lg-2">
|
||||||
@ -39,14 +39,13 @@
|
|||||||
</b-row>
|
</b-row>
|
||||||
</b-col>
|
</b-col>
|
||||||
</b-row>
|
</b-row>
|
||||||
|
</b-form-radio-group>
|
||||||
<div class="mt-4 mb-4" v-if="radioSelected === sendTypes.link">
|
<div class="mt-4 mb-4" v-if="radioSelected === sendTypes.link">
|
||||||
<h2 class="alert-heading">{{ $t('gdd_per_link.header') }}</h2>
|
<h2 class="alert-heading">{{ $t('gdd_per_link.header') }}</h2>
|
||||||
<div>
|
<div>
|
||||||
{{ $t('gdd_per_link.choose-amount') }}
|
{{ $t('gdd_per_link.choose-amount') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</b-form-radio-group>
|
|
||||||
<b-row>
|
<b-row>
|
||||||
<b-col>
|
<b-col>
|
||||||
<b-row>
|
<b-row>
|
||||||
@ -106,7 +105,10 @@
|
|||||||
</b-col>
|
</b-col>
|
||||||
<b-col cols="12" md="6" lg="6" class="text-lg-right">
|
<b-col cols="12" md="6" lg="6" class="text-lg-right">
|
||||||
<b-button block type="submit" variant="gradido">
|
<b-button block type="submit" variant="gradido">
|
||||||
{{ $t('form.check_now') }}
|
<span v-if="radioSelected === sendTypes.send">{{ $t('form.check_now') }}</span>
|
||||||
|
<span v-if="radioSelected === sendTypes.link">
|
||||||
|
{{ $t('form.generate_now') }}
|
||||||
|
</span>
|
||||||
</b-button>
|
</b-button>
|
||||||
</b-col>
|
</b-col>
|
||||||
</b-row>
|
</b-row>
|
||||||
|
|||||||
@ -78,7 +78,24 @@ export default {
|
|||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 1024px) {
|
@media screen and (min-width: 1025px) {
|
||||||
|
#side-menu {
|
||||||
|
max-width: 180px;
|
||||||
|
}
|
||||||
|
#component-sidebar {
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (min-width: 1075px) {
|
||||||
|
#side-menu {
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
#component-sidebar {
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 1108px) {
|
||||||
#side-menu {
|
#side-menu {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -67,6 +67,7 @@ export default {
|
|||||||
}
|
}
|
||||||
if (this.tokenExpiresInSeconds === 0) {
|
if (this.tokenExpiresInSeconds === 0) {
|
||||||
this.$timer.stop('tokenExpires')
|
this.$timer.stop('tokenExpires')
|
||||||
|
this.toastInfoNoHide(this.$t('session.automaticallyLoggedOut'))
|
||||||
this.$emit('logout')
|
this.$emit('logout')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -84,6 +85,7 @@ export default {
|
|||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
this.$timer.stop('tokenExpires')
|
this.$timer.stop('tokenExpires')
|
||||||
|
this.toastInfoNoHide(this.$t('session.automaticallyLoggedOut'))
|
||||||
this.$emit('logout')
|
this.$emit('logout')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|||||||
@ -51,13 +51,22 @@
|
|||||||
"deleted": "Der Beitrag wurde gelöscht! Wird aber sichtbar bleiben.",
|
"deleted": "Der Beitrag wurde gelöscht! Wird aber sichtbar bleiben.",
|
||||||
"formText": {
|
"formText": {
|
||||||
"bringYourTalentsTo": "Bring dich mit deinen Talenten in die Gemeinschaft ein! Dein freiwilliges Engagement honorieren wir mit 20 GDD pro Stunde bis maximal 1.000 GDD im Monat.",
|
"bringYourTalentsTo": "Bring dich mit deinen Talenten in die Gemeinschaft ein! Dein freiwilliges Engagement honorieren wir mit 20 GDD pro Stunde bis maximal 1.000 GDD im Monat.",
|
||||||
"describeYourCommunity": "Beschreibe deine Gemeinwohl-Tätigkeit mit Angabe der Stunden und trage einen Betrag von 20 GDD pro Stunde ein! Nach Bestätigung durch einen Moderator wird der Betrag deinem Konto gutgeschrieben.",
|
"describeYourCommunity": "Beschreibe deine Gemeinwohl-Tätigkeit und gib die Anzahl der Stunden an! Der Betrag von 20 GDD pro Stunde wird automatisch berechnet. Nach Bestätigung durch einen Moderator wird der Betrag deinem Konto gutgeschrieben.",
|
||||||
"maxGDDforMonth": "Du kannst für den ausgewählten Monat nur noch maximal {amount} GDD einreichen.",
|
"maxGDDforMonth": "Du kannst für den ausgewählten Monat nur noch maximal {amount} GDD einreichen.",
|
||||||
"openAmountForMonth": "Für <b>{monthAndYear}</b> kannst du noch <b>{creation}</b> GDD einreichen.",
|
"openAmountForMonth": "Für <b>{monthAndYear}</b> kannst du noch <b>{creation}</b> GDD einreichen.",
|
||||||
"yourContribution": "Dein Beitrag zum Gemeinwohl"
|
"yourContribution": "Dein Beitrag zum Gemeinwohl"
|
||||||
},
|
},
|
||||||
"lastContribution": "Letzte Beiträge",
|
"lastContribution": "Letzte Beiträge",
|
||||||
|
"noContributions": {
|
||||||
|
"allContributions": "Es wurden noch keine Beiträge eingereicht.",
|
||||||
|
"myContributions": "Du hast noch keine Beiträge eingereicht."
|
||||||
|
},
|
||||||
"noDateSelected": "Wähle irgendein Datum im Monat",
|
"noDateSelected": "Wähle irgendein Datum im Monat",
|
||||||
|
"noOpenCreation": {
|
||||||
|
"allMonth": "Für alle beiden Monate ist dein Schöpfungslimit erreicht. Den Nächsten Monat kannst du wieder 1000 GDD Schöpfen.",
|
||||||
|
"lastMonth": "Für den ausgewählten Monat ist das Schöpfungslimit erreicht.",
|
||||||
|
"thisMonth": "Für den aktuellen Monat ist das Schöpfungslimit erreicht."
|
||||||
|
},
|
||||||
"selectDate": "Wann war dein Beitrag?",
|
"selectDate": "Wann war dein Beitrag?",
|
||||||
"submit": "Einreichen",
|
"submit": "Einreichen",
|
||||||
"submitted": "Der Beitrag wurde eingereicht.",
|
"submitted": "Der Beitrag wurde eingereicht.",
|
||||||
@ -164,7 +173,7 @@
|
|||||||
"GDD": "GDD",
|
"GDD": "GDD",
|
||||||
"gddKonto": "GDD Konto",
|
"gddKonto": "GDD Konto",
|
||||||
"gdd_per_link": {
|
"gdd_per_link": {
|
||||||
"choose-amount": "Wähle einen Betrag aus, welchen du per Link versenden möchtest. Du kannst auch noch eine Nachricht eintragen. Beim Klick „Jetzt generieren“ wird ein Link erstellt, den du versenden kannst.",
|
"choose-amount": "Wähle einen Betrag aus, welchen du per Link versenden möchtest, und trage eine Nachricht ein. Die Nachricht ist ein Pflichtfeld.",
|
||||||
"copy-link": "Link kopieren",
|
"copy-link": "Link kopieren",
|
||||||
"copy-link-with-text": "Link und Text kopieren",
|
"copy-link-with-text": "Link und Text kopieren",
|
||||||
"created": "Der Link wurde erstellt!",
|
"created": "Der Link wurde erstellt!",
|
||||||
@ -251,7 +260,7 @@
|
|||||||
},
|
},
|
||||||
"openHours": "Offene Stunden",
|
"openHours": "Offene Stunden",
|
||||||
"pageTitle": {
|
"pageTitle": {
|
||||||
"community": "Meine Gemeinschaft",
|
"community": "Gradido schöpfen",
|
||||||
"gdt": "Deine GDT Transaktionen",
|
"gdt": "Deine GDT Transaktionen",
|
||||||
"information": "{community}",
|
"information": "{community}",
|
||||||
"overview": "Willkommen {name}",
|
"overview": "Willkommen {name}",
|
||||||
@ -263,6 +272,7 @@
|
|||||||
"send_gdd": "GDD versenden",
|
"send_gdd": "GDD versenden",
|
||||||
"send_per_link": "GDD versenden per Link",
|
"send_per_link": "GDD versenden per Link",
|
||||||
"session": {
|
"session": {
|
||||||
|
"automaticallyLoggedOut": "Du wurdest automatisch abgemeldet",
|
||||||
"extend": "Angemeldet bleiben",
|
"extend": "Angemeldet bleiben",
|
||||||
"lightText": "Wenn du länger als 10 Minuten keine Aktion getätigt hast, wirst du aus Sicherheitsgründen abgemeldet.",
|
"lightText": "Wenn du länger als 10 Minuten keine Aktion getätigt hast, wirst du aus Sicherheitsgründen abgemeldet.",
|
||||||
"logoutIn": "Abmelden in ",
|
"logoutIn": "Abmelden in ",
|
||||||
|
|||||||
@ -50,14 +50,23 @@
|
|||||||
"delete": "Delete Contribution! Are you sure?",
|
"delete": "Delete Contribution! Are you sure?",
|
||||||
"deleted": "The contribution has been deleted! But it will remain visible.",
|
"deleted": "The contribution has been deleted! But it will remain visible.",
|
||||||
"formText": {
|
"formText": {
|
||||||
"bringYourTalentsTo": "Bring your talents to the community! Your voluntary commitment will be rewarded with 20 GDD per hour up to a maximum of 1,000 GDD per month.",
|
"bringYourTalentsTo": "Bring your talents to the community! We reward your voluntary commitment with 20 GDD per hour up to a maximum of 1,000 GDD per month.",
|
||||||
"describeYourCommunity": "Describe your community service activity with hours and enter an amount of 20 GDD per hour! After confirmation by a moderator, the amount will be credited to your account.",
|
"describeYourCommunity": "Describe your community service activity and specify the number of hours! The amount of 20 GDD per hour will be calculated automatically. After confirmation by a moderator, the amount will be credited to your account.",
|
||||||
"maxGDDforMonth": "You can only submit a maximum of {amount} GDD for the selected month.",
|
"maxGDDforMonth": "You can only submit a maximum of {amount} GDD for the selected month.",
|
||||||
"openAmountForMonth": "For <b>{monthAndYear}</b>, you can still submit <b>{creation}</b> GDD.",
|
"openAmountForMonth": "For <b>{monthAndYear}</b>, you can still submit <b>{creation}</b> GDD.",
|
||||||
"yourContribution": "Your contribution to the common good"
|
"yourContribution": "Your Contributions to the Common Good"
|
||||||
},
|
},
|
||||||
"lastContribution": "Last Contributions",
|
"lastContribution": "Last Contributions",
|
||||||
|
"noContributions": {
|
||||||
|
"allContributions": "No contributions have been submitted yet.",
|
||||||
|
"myContributions": "You have not submitted any entries yet."
|
||||||
|
},
|
||||||
"noDateSelected": "Choose any date in the month",
|
"noDateSelected": "Choose any date in the month",
|
||||||
|
"noOpenCreation": {
|
||||||
|
"allMonth": "For all two months your creation limit is reached. The next month you can create 1000 GDD again.",
|
||||||
|
"lastMonth": "The creation limit is reached for the selected month.",
|
||||||
|
"thisMonth": "The creation limit has been reached for the current month."
|
||||||
|
},
|
||||||
"selectDate": "When was your contribution?",
|
"selectDate": "When was your contribution?",
|
||||||
"submit": "Submit",
|
"submit": "Submit",
|
||||||
"submitted": "The contribution was submitted.",
|
"submitted": "The contribution was submitted.",
|
||||||
@ -164,7 +173,7 @@
|
|||||||
"GDD": "GDD",
|
"GDD": "GDD",
|
||||||
"gddKonto": "GDD Konto",
|
"gddKonto": "GDD Konto",
|
||||||
"gdd_per_link": {
|
"gdd_per_link": {
|
||||||
"choose-amount": "Select an amount that you would like to send via link. You can also enter a message. Click 'Generate now' to create a link that you can share.",
|
"choose-amount": "Select an amount you want to send via link and enter a message. The message is mandatory.",
|
||||||
"copy-link": "Copy link",
|
"copy-link": "Copy link",
|
||||||
"copy-link-with-text": "Copy link and text",
|
"copy-link-with-text": "Copy link and text",
|
||||||
"created": "Link was created!",
|
"created": "Link was created!",
|
||||||
@ -251,7 +260,7 @@
|
|||||||
},
|
},
|
||||||
"openHours": "Open Hours",
|
"openHours": "Open Hours",
|
||||||
"pageTitle": {
|
"pageTitle": {
|
||||||
"community": "My community",
|
"community": "Create Gradido",
|
||||||
"gdt": "Your GDT transactions",
|
"gdt": "Your GDT transactions",
|
||||||
"information": "{community}",
|
"information": "{community}",
|
||||||
"overview": "Welcome {name}",
|
"overview": "Welcome {name}",
|
||||||
@ -263,6 +272,7 @@
|
|||||||
"send_gdd": "Send GDD",
|
"send_gdd": "Send GDD",
|
||||||
"send_per_link": "Send GDD via Link",
|
"send_per_link": "Send GDD via Link",
|
||||||
"session": {
|
"session": {
|
||||||
|
"automaticallyLoggedOut": "You have been automatically logged out.",
|
||||||
"extend": "Stay logged in",
|
"extend": "Stay logged in",
|
||||||
"lightText": "If you have not performed any action for more than 10 minutes, you will be logged out for security reasons.",
|
"lightText": "If you have not performed any action for more than 10 minutes, you will be logged out for security reasons.",
|
||||||
"logoutIn": "Log out in ",
|
"logoutIn": "Log out in ",
|
||||||
|
|||||||
@ -18,11 +18,18 @@ export const toasters = {
|
|||||||
variant: 'warning',
|
variant: 'warning',
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
toastInfoNoHide(message) {
|
||||||
|
this.toast(message, {
|
||||||
|
title: this.$t('navigation.info'),
|
||||||
|
variant: 'warning',
|
||||||
|
noAutoHide: true,
|
||||||
|
})
|
||||||
|
},
|
||||||
toast(message, options) {
|
toast(message, options) {
|
||||||
if (message.replace) message = message.replace(/^GraphQL error: /, '')
|
if (message.replace) message = message.replace(/^GraphQL error: /, '')
|
||||||
this.$root.$bvToast.toast(message, {
|
this.$root.$bvToast.toast(message, {
|
||||||
autoHideDelay: 5000,
|
|
||||||
appendToast: true,
|
appendToast: true,
|
||||||
|
autoHideDelay: 5000,
|
||||||
solid: true,
|
solid: true,
|
||||||
toaster: 'b-toaster-top-right',
|
toaster: 'b-toaster-top-right',
|
||||||
headerClass: 'gdd-toaster-title',
|
headerClass: 'gdd-toaster-title',
|
||||||
|
|||||||
@ -70,6 +70,8 @@ describe('Community', () => {
|
|||||||
lastName: 'Bloxberg',
|
lastName: 'Bloxberg',
|
||||||
state: 'IN_PROGRESS',
|
state: 'IN_PROGRESS',
|
||||||
messagesCount: 0,
|
messagesCount: 0,
|
||||||
|
deniedAt: null,
|
||||||
|
deniedBy: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 1550,
|
id: 1550,
|
||||||
@ -84,6 +86,8 @@ describe('Community', () => {
|
|||||||
lastName: 'Bloxberg',
|
lastName: 'Bloxberg',
|
||||||
state: 'CONFIRMED',
|
state: 'CONFIRMED',
|
||||||
messagesCount: 0,
|
messagesCount: 0,
|
||||||
|
deniedAt: null,
|
||||||
|
deniedBy: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
contributionCount: 1,
|
contributionCount: 1,
|
||||||
@ -112,6 +116,10 @@ describe('Community', () => {
|
|||||||
confirmedAt: null,
|
confirmedAt: null,
|
||||||
firstName: 'Bibi',
|
firstName: 'Bibi',
|
||||||
lastName: 'Bloxberg',
|
lastName: 'Bloxberg',
|
||||||
|
deniedAt: null,
|
||||||
|
deniedBy: null,
|
||||||
|
messagesCount: 0,
|
||||||
|
state: 'IN_PROGRESS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 1550,
|
id: 1550,
|
||||||
@ -124,7 +132,10 @@ describe('Community', () => {
|
|||||||
firstName: 'Bibi',
|
firstName: 'Bibi',
|
||||||
contributionDate: '2022-06-15T08:47:06.000Z',
|
contributionDate: '2022-06-15T08:47:06.000Z',
|
||||||
lastName: 'Bloxberg',
|
lastName: 'Bloxberg',
|
||||||
|
deniedAt: null,
|
||||||
|
deniedBy: null,
|
||||||
messagesCount: 0,
|
messagesCount: 0,
|
||||||
|
state: 'IN_PROGRESS',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 1556,
|
id: 1556,
|
||||||
@ -137,6 +148,10 @@ describe('Community', () => {
|
|||||||
confirmedAt: null,
|
confirmedAt: null,
|
||||||
firstName: 'Bob',
|
firstName: 'Bob',
|
||||||
lastName: 'der Baumeister',
|
lastName: 'der Baumeister',
|
||||||
|
deniedAt: null,
|
||||||
|
deniedBy: null,
|
||||||
|
messagesCount: 0,
|
||||||
|
state: 'IN_PROGRESS',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
contributionCount: 3,
|
contributionCount: 3,
|
||||||
|
|||||||
@ -20,6 +20,10 @@
|
|||||||
/>
|
/>
|
||||||
</b-tab>
|
</b-tab>
|
||||||
<b-tab no-body>
|
<b-tab no-body>
|
||||||
|
<div v-if="items.length === 0">
|
||||||
|
{{ $t('contribution.noContributions.myContributions') }}
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
<contribution-list
|
<contribution-list
|
||||||
@closeAllOpenCollapse="closeAllOpenCollapse"
|
@closeAllOpenCollapse="closeAllOpenCollapse"
|
||||||
:items="items"
|
:items="items"
|
||||||
@ -31,8 +35,13 @@
|
|||||||
:showPagination="true"
|
:showPagination="true"
|
||||||
:pageSize="pageSize"
|
:pageSize="pageSize"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</b-tab>
|
</b-tab>
|
||||||
<b-tab no-body>
|
<b-tab no-body>
|
||||||
|
<div v-if="itemsAll.length === 0">
|
||||||
|
{{ $t('contribution.noContributions.allContributions') }}
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
<contribution-list
|
<contribution-list
|
||||||
:items="itemsAll"
|
:items="itemsAll"
|
||||||
@update-list-contributions="updateListAllContributions"
|
@update-list-contributions="updateListAllContributions"
|
||||||
@ -42,6 +51,7 @@
|
|||||||
:pageSize="pageSizeAll"
|
:pageSize="pageSizeAll"
|
||||||
:allContribution="true"
|
:allContribution="true"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
</b-tab>
|
</b-tab>
|
||||||
</b-tabs>
|
</b-tabs>
|
||||||
</div>
|
</div>
|
||||||
@ -296,8 +306,8 @@ export default {
|
|||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.updateTransactions(0)
|
this.updateTransactions(0)
|
||||||
this.tabIndex = 1
|
this.tabIndex = 0
|
||||||
this.$router.push({ path: '/community#my' })
|
this.$router.push({ path: '/community#edit' })
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -6,9 +6,9 @@
|
|||||||
{{ CONFIG.COMMUNITY_DESCRIPTION }}
|
{{ CONFIG.COMMUNITY_DESCRIPTION }}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<router-link :to="CONFIG.COMMUNITY_URL">
|
<b-link :href="CONFIG.COMMUNITY_URL">
|
||||||
{{ CONFIG.COMMUNITY_URL }}
|
{{ CONFIG.COMMUNITY_URL }}
|
||||||
</router-link>
|
</b-link>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div class="h3">{{ $t('community.openContributionLinks') }}</div>
|
<div class="h3">{{ $t('community.openContributionLinks') }}</div>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "gradido",
|
"name": "gradido",
|
||||||
"version": "1.17.1",
|
"version": "1.18.1",
|
||||||
"description": "Gradido",
|
"description": "Gradido",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"repository": "git@github.com:gradido/gradido.git",
|
"repository": "git@github.com:gradido/gradido.git",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user