mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into 2332-mark-contribution-as-rejected
This commit is contained in:
commit
447d252e58
3
.github/workflows/lint_pr.yml
vendored
3
.github/workflows/lint_pr.yml
vendored
@ -29,6 +29,9 @@ jobs:
|
||||
admin
|
||||
database
|
||||
release
|
||||
federation
|
||||
workflow
|
||||
docker
|
||||
other
|
||||
# Configure that a scope must always be provided.
|
||||
requireScope: true
|
||||
|
||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@ -437,7 +437,7 @@ jobs:
|
||||
report_name: Coverage Frontend
|
||||
type: lcov
|
||||
result_path: ./coverage/lcov.info
|
||||
min_coverage: 95
|
||||
min_coverage: 89
|
||||
token: ${{ github.token }}
|
||||
|
||||
##############################################################################
|
||||
@ -527,7 +527,7 @@ jobs:
|
||||
report_name: Coverage Backend
|
||||
type: lcov
|
||||
result_path: ./backend/coverage/lcov.info
|
||||
min_coverage: 74
|
||||
min_coverage: 76
|
||||
token: ${{ github.token }}
|
||||
|
||||
##########################################################################
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="content-footer">
|
||||
<hr />
|
||||
<div align-v="center" class="mt-4 mb-4 justify-content-lg-between">
|
||||
<b-row align-v="center" class="mt-4 mb-4 justify-content-lg-between">
|
||||
<b-col>
|
||||
<div class="copyright text-center text-lg-center text-muted">
|
||||
{{ $t('footer.copyright.year', { year }) }}
|
||||
@ -25,7 +25,7 @@
|
||||
</a>
|
||||
</div>
|
||||
</b-col>
|
||||
</div>
|
||||
</b-row>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
CONFIG_VERSION=v12.2022-11-10
|
||||
CONFIG_VERSION=v14.2022-12-22
|
||||
|
||||
# Server
|
||||
PORT=4000
|
||||
@ -30,6 +30,7 @@ COMMUNITY_REGISTER_URL=http://localhost/register
|
||||
COMMUNITY_REDEEM_URL=http://localhost/redeem/{code}
|
||||
COMMUNITY_REDEEM_CONTRIBUTION_URL=http://localhost/redeem/CL-{code}
|
||||
COMMUNITY_DESCRIPTION=Die lokale Entwicklungsumgebung von Gradido.
|
||||
COMMUNITY_SUPPORT_MAIL=support@supportmail.com
|
||||
|
||||
# Login Server
|
||||
LOGIN_APP_SECRET=21ffbbc616fe
|
||||
@ -65,4 +66,5 @@ EVENT_PROTOCOL_DISABLED=false
|
||||
# if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen
|
||||
# on an hash created from this topic
|
||||
# FEDERATION_DHT_TOPIC=GRADIDO_HUB
|
||||
# FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f
|
||||
# FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f
|
||||
# FEDERATION_COMMUNITY_URL=http://localhost:4000/api
|
||||
|
||||
@ -29,6 +29,7 @@ COMMUNITY_REGISTER_URL=$COMMUNITY_REGISTER_URL
|
||||
COMMUNITY_REDEEM_URL=$COMMUNITY_REDEEM_URL
|
||||
COMMUNITY_REDEEM_CONTRIBUTION_URL=$COMMUNITY_REDEEM_CONTRIBUTION_URL
|
||||
COMMUNITY_DESCRIPTION=$COMMUNITY_DESCRIPTION
|
||||
COMMUNITY_SUPPORT_MAIL=$COMMUNITY_SUPPORT_MAIL
|
||||
|
||||
# Login Server
|
||||
LOGIN_APP_SECRET=21ffbbc616fe
|
||||
@ -59,3 +60,4 @@ EVENT_PROTOCOL_DISABLED=$EVENT_PROTOCOL_DISABLED
|
||||
# Federation
|
||||
FEDERATION_DHT_TOPIC=$FEDERATION_DHT_TOPIC
|
||||
FEDERATION_DHT_SEED=$FEDERATION_DHT_SEED
|
||||
FEDERATION_COMMUNITY_URL=$FEDERATION_COMMUNITY_URL
|
||||
|
||||
@ -20,6 +20,7 @@
|
||||
"dependencies": {
|
||||
"@hyperswarm/dht": "^6.2.0",
|
||||
"apollo-server-express": "^2.25.2",
|
||||
"await-semaphore": "^0.1.3",
|
||||
"axios": "^0.21.1",
|
||||
"class-validator": "^0.13.1",
|
||||
"cors": "^2.8.5",
|
||||
|
||||
@ -10,14 +10,14 @@ Decimal.set({
|
||||
})
|
||||
|
||||
const constants = {
|
||||
DB_VERSION: '0057-clear_old_password_junk',
|
||||
DB_VERSION: '0058-add_communities_table',
|
||||
DECAY_START_TIME: new Date('2021-05-13 17:46:31-0000'), // GMT+0
|
||||
LOG4JS_CONFIG: 'log4js-config.json',
|
||||
// default log level on production should be info
|
||||
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
|
||||
CONFIG_VERSION: {
|
||||
DEFAULT: 'DEFAULT',
|
||||
EXPECTED: 'v12.2022-11-10',
|
||||
EXPECTED: 'v14.2022-12-22',
|
||||
CURRENT: '',
|
||||
},
|
||||
}
|
||||
@ -58,6 +58,7 @@ const community = {
|
||||
process.env.COMMUNITY_REDEEM_CONTRIBUTION_URL || 'http://localhost/redeem/CL-{code}',
|
||||
COMMUNITY_DESCRIPTION:
|
||||
process.env.COMMUNITY_DESCRIPTION || 'Die lokale Entwicklungsumgebung von Gradido.',
|
||||
COMMUNITY_SUPPORT_MAIL: process.env.COMMUNITY_SUPPORT_MAIL || 'support@supportmail.com',
|
||||
}
|
||||
|
||||
const loginServer = {
|
||||
@ -119,6 +120,12 @@ if (
|
||||
const federation = {
|
||||
FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || null,
|
||||
FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null,
|
||||
FEDERATION_COMMUNITY_URL:
|
||||
process.env.FEDERATION_COMMUNITY_URL === undefined
|
||||
? null
|
||||
: process.env.FEDERATION_COMMUNITY_URL.endsWith('/')
|
||||
? process.env.FEDERATION_COMMUNITY_URL
|
||||
: process.env.FEDERATION_COMMUNITY_URL + '/',
|
||||
}
|
||||
|
||||
const CONFIG = {
|
||||
|
||||
@ -70,6 +70,8 @@ describe('sendEmailVariants', () => {
|
||||
senderLastName: 'Bloxberg',
|
||||
contributionMemo: 'My contribution.',
|
||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
|
||||
communityURL: CONFIG.COMMUNITY_URL,
|
||||
},
|
||||
})
|
||||
})
|
||||
@ -106,10 +108,14 @@ describe('sendEmailVariants', () => {
|
||||
'To view and reply to the message, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain(
|
||||
`Link to your account:<span> </span><a href="${CONFIG.EMAIL_LINK_OVERVIEW}">${CONFIG.EMAIL_LINK_OVERVIEW}</a>`,
|
||||
`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><span>your Gradido team')
|
||||
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>',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -140,6 +146,8 @@ describe('sendEmailVariants', () => {
|
||||
activationLink: 'http://localhost/checkEmail/6627633878930542284',
|
||||
timeDurationObject: { hours: 23, minutes: 30 },
|
||||
resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD,
|
||||
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
|
||||
communityURL: CONFIG.COMMUNITY_URL,
|
||||
},
|
||||
})
|
||||
})
|
||||
@ -178,12 +186,16 @@ describe('sendEmailVariants', () => {
|
||||
'or copy the link above into your browser window.',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'The link has a validity of 23 hours and 30 minutes. If the validity of the link has already expired, you can have a new link sent to you here by entering your email address:',
|
||||
'The link has a validity of 23 hours and 30 minutes. If the validity of the link has already expired, you can have a new link sent to you here:',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain(
|
||||
`<a href="${CONFIG.EMAIL_LINK_FORGOTPASSWORD}">${CONFIG.EMAIL_LINK_FORGOTPASSWORD}</a>`,
|
||||
)
|
||||
expect(result.originalMessage.html).toContain('Kind regards,<br><span>your Gradido team')
|
||||
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>',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -210,6 +222,8 @@ describe('sendEmailVariants', () => {
|
||||
lastName: 'Lustig',
|
||||
locale: 'en',
|
||||
resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD,
|
||||
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
|
||||
communityURL: CONFIG.COMMUNITY_URL,
|
||||
},
|
||||
})
|
||||
})
|
||||
@ -255,9 +269,13 @@ describe('sendEmailVariants', () => {
|
||||
'or copy the link above into your browser window.',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'If you are not the one who tried to register again, please contact our support:',
|
||||
'If you are not the one who tried to register again, please contact our support:<br><a href="mailto:support@supportmail.com">support@supportmail.com</a>',
|
||||
)
|
||||
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>',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain('Kind regards,<br><span>your Gradido team')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -292,6 +310,8 @@ describe('sendEmailVariants', () => {
|
||||
contributionMemo: 'My contribution.',
|
||||
contributionAmount: '23.54',
|
||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
|
||||
communityURL: CONFIG.COMMUNITY_URL,
|
||||
},
|
||||
})
|
||||
})
|
||||
@ -307,18 +327,20 @@ describe('sendEmailVariants', () => {
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
from: 'Gradido (do not answer) <info@gradido.net>',
|
||||
attachments: [],
|
||||
subject: 'Gradido: Your common good contribution was confirmed',
|
||||
subject: 'Gradido: Your contribution to the common good was confirmed',
|
||||
html: expect.any(String),
|
||||
text: expect.stringContaining('GRADIDO: YOUR COMMON GOOD CONTRIBUTION WAS CONFIRMED'),
|
||||
text: expect.stringContaining(
|
||||
'GRADIDO: YOUR CONTRIBUTION TO THE COMMON GOOD WAS CONFIRMED',
|
||||
),
|
||||
}),
|
||||
})
|
||||
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 confirmed</title>',
|
||||
'<title>Gradido: Your contribution to the common good was confirmed</title>',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'>Gradido: Your common good contribution was confirmed</h1>',
|
||||
'>Gradido: Your contribution to the common good was confirmed</h1>',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain('Hello Peter Lustig')
|
||||
expect(result.originalMessage.html).toContain(
|
||||
@ -326,10 +348,14 @@ describe('sendEmailVariants', () => {
|
||||
)
|
||||
expect(result.originalMessage.html).toContain('Amount: 23.54 GDD')
|
||||
expect(result.originalMessage.html).toContain(
|
||||
`Link to your account:<span> </span><a href="${CONFIG.EMAIL_LINK_OVERVIEW}">${CONFIG.EMAIL_LINK_OVERVIEW}</a>`,
|
||||
`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><span>your Gradido team')
|
||||
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>',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -362,6 +388,8 @@ describe('sendEmailVariants', () => {
|
||||
senderLastName: 'Bloxberg',
|
||||
contributionMemo: 'My contribution.',
|
||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
|
||||
communityURL: CONFIG.COMMUNITY_URL,
|
||||
},
|
||||
})
|
||||
})
|
||||
@ -398,10 +426,14 @@ describe('sendEmailVariants', () => {
|
||||
'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:<span> </span><a href="${CONFIG.EMAIL_LINK_OVERVIEW}">${CONFIG.EMAIL_LINK_OVERVIEW}</a>`,
|
||||
`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><span>your Gradido team')
|
||||
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>',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -432,6 +464,8 @@ describe('sendEmailVariants', () => {
|
||||
resetLink: 'http://localhost/reset-password/3762660021544901417',
|
||||
timeDurationObject: { hours: 23, minutes: 30 },
|
||||
resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD,
|
||||
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
|
||||
communityURL: CONFIG.COMMUNITY_URL,
|
||||
},
|
||||
})
|
||||
})
|
||||
@ -468,12 +502,16 @@ describe('sendEmailVariants', () => {
|
||||
'or copy the link above into your browser window.',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'The link has a validity of 23 hours and 30 minutes. If the validity of the link has already expired, you can have a new link sent to you here by entering your email address:',
|
||||
'The link has a validity of 23 hours and 30 minutes. If the validity of the link has already expired, you can have a new link sent to you here:',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain(
|
||||
`<a href="${CONFIG.EMAIL_LINK_FORGOTPASSWORD}">${CONFIG.EMAIL_LINK_FORGOTPASSWORD}</a>`,
|
||||
)
|
||||
expect(result.originalMessage.html).toContain('Kind regards,<br><span>your Gradido team')
|
||||
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>',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -510,6 +548,8 @@ describe('sendEmailVariants', () => {
|
||||
transactionMemo: 'You deserve it! 🙏🏼',
|
||||
transactionAmount: '17.65',
|
||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
|
||||
communityURL: CONFIG.COMMUNITY_URL,
|
||||
},
|
||||
})
|
||||
})
|
||||
@ -525,30 +565,34 @@ describe('sendEmailVariants', () => {
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
from: 'Gradido (do not answer) <info@gradido.net>',
|
||||
attachments: [],
|
||||
subject: 'Gradido: Your Gradido link has been redeemed',
|
||||
subject: 'Gradido: Bibi Bloxberg has redeemed your Gradido link',
|
||||
html: expect.any(String),
|
||||
text: expect.stringContaining('GRADIDO: YOUR GRADIDO LINK HAS BEEN REDEEMED'),
|
||||
text: expect.stringContaining('BIBI BLOXBERG HAS REDEEMED YOUR GRADIDO LINK'),
|
||||
}),
|
||||
})
|
||||
expect(result.originalMessage.html).toContain('<!DOCTYPE html>')
|
||||
expect(result.originalMessage.html).toContain('<html lang="en">')
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'<title>Gradido: Your Gradido link has been redeemed</title>',
|
||||
'<title>Gradido: Bibi Bloxberg has redeemed your Gradido link</title>',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'>Gradido: Your Gradido link has been redeemed</h1>',
|
||||
'>Gradido: Bibi Bloxberg has redeemed your Gradido link</h1>',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain('Hello Peter Lustig')
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'Bibi Bloxberg (bibi@bloxberg.de) has just redeemed your link.',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain('Amount: 17.65 GDD')
|
||||
expect(result.originalMessage.html).toContain('Memo: You deserve it! 🙏🏼')
|
||||
expect(result.originalMessage.html).toContain('Message: You deserve it! 🙏🏼')
|
||||
expect(result.originalMessage.html).toContain(
|
||||
`You can find transaction details in your Gradido account:<span> </span><a href="${CONFIG.EMAIL_LINK_OVERVIEW}">${CONFIG.EMAIL_LINK_OVERVIEW}</a>`,
|
||||
`You can find transaction details in your Gradido 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><span>your Gradido team')
|
||||
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>',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -583,6 +627,8 @@ describe('sendEmailVariants', () => {
|
||||
senderEmail: 'bibi@bloxberg.de',
|
||||
transactionAmount: '37.40',
|
||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
|
||||
communityURL: CONFIG.COMMUNITY_URL,
|
||||
},
|
||||
})
|
||||
})
|
||||
@ -598,26 +644,32 @@ describe('sendEmailVariants', () => {
|
||||
to: 'Peter Lustig <peter@lustig.de>',
|
||||
from: 'Gradido (do not answer) <info@gradido.net>',
|
||||
attachments: [],
|
||||
subject: 'Gradido: You have received Gradidos',
|
||||
subject: 'Gradido: Bibi Bloxberg has sent you 37.40 Gradido',
|
||||
html: expect.any(String),
|
||||
text: expect.stringContaining('GRADIDO: YOU HAVE RECEIVED GRADIDOS'),
|
||||
text: expect.stringContaining('GRADIDO: BIBI BLOXBERG HAS SENT YOU 37.40 GRADIDO'),
|
||||
}),
|
||||
})
|
||||
expect(result.originalMessage.html).toContain('<!DOCTYPE html>')
|
||||
expect(result.originalMessage.html).toContain('<html lang="en">')
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'<title>Gradido: You have received Gradidos</title>',
|
||||
'<title>Gradido: Bibi Bloxberg has sent you 37.40 Gradido</title>',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'>Gradido: Bibi Bloxberg has sent you 37.40 Gradido</h1>',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain('>Gradido: You have received Gradidos</h1>')
|
||||
expect(result.originalMessage.html).toContain('Hello Peter Lustig')
|
||||
expect(result.originalMessage.html).toContain(
|
||||
'You have just received 37.40 GDD from Bibi Bloxberg (bibi@bloxberg.de).',
|
||||
)
|
||||
expect(result.originalMessage.html).toContain(
|
||||
`You can find transaction details in your Gradido account:<span> </span><a href="${CONFIG.EMAIL_LINK_OVERVIEW}">${CONFIG.EMAIL_LINK_OVERVIEW}</a>`,
|
||||
`You can find transaction details in your Gradido 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><span>your Gradido team')
|
||||
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>',
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -25,6 +25,8 @@ export const sendAddedContributionMessageEmail = (data: {
|
||||
senderLastName: data.senderLastName,
|
||||
contributionMemo: data.contributionMemo,
|
||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
|
||||
communityURL: CONFIG.COMMUNITY_URL,
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -47,6 +49,8 @@ export const sendAccountActivationEmail = (data: {
|
||||
activationLink: data.activationLink,
|
||||
timeDurationObject: data.timeDurationObject,
|
||||
resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD,
|
||||
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
|
||||
communityURL: CONFIG.COMMUNITY_URL,
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -65,6 +69,8 @@ export const sendAccountMultiRegistrationEmail = (data: {
|
||||
lastName: data.lastName,
|
||||
locale: data.language,
|
||||
resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD,
|
||||
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
|
||||
communityURL: CONFIG.COMMUNITY_URL,
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -91,6 +97,8 @@ export const sendContributionConfirmedEmail = (data: {
|
||||
contributionMemo: data.contributionMemo,
|
||||
contributionAmount: decimalSeparatorByLanguage(data.contributionAmount, data.language),
|
||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
|
||||
communityURL: CONFIG.COMMUNITY_URL,
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -115,6 +123,8 @@ export const sendContributionRejectedEmail = (data: {
|
||||
senderLastName: data.senderLastName,
|
||||
contributionMemo: data.contributionMemo,
|
||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
|
||||
communityURL: CONFIG.COMMUNITY_URL,
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -137,6 +147,8 @@ export const sendResetPasswordEmail = (data: {
|
||||
resetLink: data.resetLink,
|
||||
timeDurationObject: data.timeDurationObject,
|
||||
resendLink: CONFIG.EMAIL_LINK_FORGOTPASSWORD,
|
||||
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
|
||||
communityURL: CONFIG.COMMUNITY_URL,
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -165,6 +177,8 @@ export const sendTransactionLinkRedeemedEmail = (data: {
|
||||
transactionMemo: data.transactionMemo,
|
||||
transactionAmount: decimalSeparatorByLanguage(data.transactionAmount, data.language),
|
||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
|
||||
communityURL: CONFIG.COMMUNITY_URL,
|
||||
},
|
||||
})
|
||||
}
|
||||
@ -191,6 +205,8 @@ export const sendTransactionReceivedEmail = (data: {
|
||||
senderEmail: data.senderEmail,
|
||||
transactionAmount: decimalSeparatorByLanguage(data.transactionAmount, data.language),
|
||||
overviewURL: CONFIG.EMAIL_LINK_OVERVIEW,
|
||||
supportEmail: CONFIG.COMMUNITY_SUPPORT_MAIL,
|
||||
communityURL: CONFIG.COMMUNITY_URL,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -5,16 +5,16 @@ html(lang=locale)
|
||||
body
|
||||
h1(style='margin-bottom: 24px;')= t('emails.accountActivation.subject')
|
||||
#container.col
|
||||
p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName })
|
||||
include ../hello.pug
|
||||
p= t('emails.accountActivation.emailRegistered')
|
||||
p= t('emails.accountActivation.pleaseClickLink')
|
||||
p
|
||||
= t('emails.accountActivation.pleaseClickLink')
|
||||
br
|
||||
a(href=activationLink) #{activationLink}
|
||||
br
|
||||
span= t('emails.general.orCopyLink')
|
||||
p= t('emails.accountActivation.duration', { hours: timeDurationObject.hours, minutes: timeDurationObject.minutes })
|
||||
= t('emails.general.orCopyLink')
|
||||
p
|
||||
= t('emails.accountActivation.duration', { hours: timeDurationObject.hours, minutes: timeDurationObject.minutes })
|
||||
br
|
||||
a(href=resendLink) #{resendLink}
|
||||
p(style='margin-top: 24px;')= t('emails.general.sincerelyYours')
|
||||
br
|
||||
span= t('emails.general.yourGradidoTeam')
|
||||
include ../greatingFormularImprint.pug
|
||||
|
||||
@ -5,18 +5,19 @@ html(lang=locale)
|
||||
body
|
||||
h1(style='margin-bottom: 24px;')= t('emails.accountMultiRegistration.subject')
|
||||
#container.col
|
||||
p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName })
|
||||
p= t('emails.accountMultiRegistration.emailReused')
|
||||
include ../hello.pug
|
||||
p
|
||||
= t('emails.accountMultiRegistration.emailReused')
|
||||
br
|
||||
span= t('emails.accountMultiRegistration.emailExists')
|
||||
p= t('emails.accountMultiRegistration.onForgottenPasswordClickLink')
|
||||
= t('emails.accountMultiRegistration.emailExists')
|
||||
p
|
||||
= t('emails.accountMultiRegistration.onForgottenPasswordClickLink')
|
||||
br
|
||||
a(href=resendLink) #{resendLink}
|
||||
br
|
||||
span= t('emails.accountMultiRegistration.onForgottenPasswordCopyLink')
|
||||
p= t('emails.accountMultiRegistration.ifYouAreNotTheOne')
|
||||
= t('emails.accountMultiRegistration.onForgottenPasswordCopyLink')
|
||||
p
|
||||
= t('emails.accountMultiRegistration.ifYouAreNotTheOne')
|
||||
br
|
||||
a(href='https://gradido.net/de/contact/') https://gradido.net/de/contact/
|
||||
p(style='margin-top: 24px;')= t('emails.general.sincerelyYours')
|
||||
br
|
||||
span= t('emails.general.yourGradidoTeam')
|
||||
a(href='mailto:' + supportEmail)= supportEmail
|
||||
include ../greatingFormularImprint.pug
|
||||
|
||||
@ -5,13 +5,12 @@ html(lang=locale)
|
||||
body
|
||||
h1(style='margin-bottom: 24px;')= t('emails.addedContributionMessage.subject')
|
||||
#container.col
|
||||
p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName })
|
||||
include ../hello.pug
|
||||
p= t('emails.addedContributionMessage.commonGoodContributionMessage', { senderFirstName, senderLastName, contributionMemo })
|
||||
p= t('emails.addedContributionMessage.toSeeAndAnswerMessage')
|
||||
p= t('emails.general.linkToYourAccount')
|
||||
span= " "
|
||||
p
|
||||
= t('emails.general.linkToYourAccount')
|
||||
= " "
|
||||
a(href=overviewURL) #{overviewURL}
|
||||
p= t('emails.general.pleaseDoNotReply')
|
||||
p(style='margin-top: 24px;')= t('emails.general.sincerelyYours')
|
||||
br
|
||||
span= t('emails.general.yourGradidoTeam')
|
||||
include ../greatingFormularImprint.pug
|
||||
|
||||
@ -5,13 +5,12 @@ html(lang=locale)
|
||||
body
|
||||
h1(style='margin-bottom: 24px;')= t('emails.contributionConfirmed.subject')
|
||||
#container.col
|
||||
p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName })
|
||||
include ../hello.pug
|
||||
p= t('emails.contributionConfirmed.commonGoodContributionConfirmed', { senderFirstName, senderLastName, contributionMemo })
|
||||
p= t('emails.general.amountGDD', { amountGDD: contributionAmount })
|
||||
p= t('emails.general.linkToYourAccount')
|
||||
span= " "
|
||||
p
|
||||
= t('emails.general.linkToYourAccount')
|
||||
= " "
|
||||
a(href=overviewURL) #{overviewURL}
|
||||
p= t('emails.general.pleaseDoNotReply')
|
||||
p(style='margin-top: 24px;')= t('emails.general.sincerelyYours')
|
||||
br
|
||||
span= t('emails.general.yourGradidoTeam')
|
||||
include ../greatingFormularImprint.pug
|
||||
|
||||
@ -5,13 +5,12 @@ html(lang=locale)
|
||||
body
|
||||
h1(style='margin-bottom: 24px;')= t('emails.contributionRejected.subject')
|
||||
#container.col
|
||||
p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName })
|
||||
include ../hello.pug
|
||||
p= t('emails.contributionRejected.commonGoodContributionRejected', { senderFirstName, senderLastName, contributionMemo })
|
||||
p= t('emails.contributionRejected.toSeeContributionsAndMessages')
|
||||
p= t('emails.general.linkToYourAccount')
|
||||
span= " "
|
||||
p
|
||||
= t('emails.general.linkToYourAccount')
|
||||
= " "
|
||||
a(href=overviewURL) #{overviewURL}
|
||||
p= t('emails.general.pleaseDoNotReply')
|
||||
p(style='margin-top: 24px;')= t('emails.general.sincerelyYours')
|
||||
br
|
||||
span= t('emails.general.yourGradidoTeam')
|
||||
include ../greatingFormularImprint.pug
|
||||
|
||||
16
backend/src/emails/templates/greatingFormularImprint.pug
Normal file
16
backend/src/emails/templates/greatingFormularImprint.pug
Normal file
@ -0,0 +1,16 @@
|
||||
p(style='margin-top: 24px;')
|
||||
= t('emails.general.sincerelyYours')
|
||||
br
|
||||
= t('emails.general.yourGradidoTeam')
|
||||
p(style='margin-top: 24px;')= '—————'
|
||||
p(style='margin-top: 24px;')
|
||||
if t('general.imprintImageURL').length > 0
|
||||
div(style='position: relative; left: -22px;')
|
||||
img(src=t('general.imprintImageURL'), width='200', alt=t('general.imprintImageAlt'))
|
||||
br
|
||||
each line in t('general.imprint').split(/\n/)
|
||||
= line
|
||||
br
|
||||
a(href='mailto:' + supportEmail)= supportEmail
|
||||
br
|
||||
a(href=communityURL)= communityURL
|
||||
1
backend/src/emails/templates/hello.pug
Normal file
1
backend/src/emails/templates/hello.pug
Normal file
@ -0,0 +1 @@
|
||||
p= t('emails.general.helloName', { firstName, lastName })
|
||||
@ -5,16 +5,16 @@ html(lang=locale)
|
||||
body
|
||||
h1(style='margin-bottom: 24px;')= t('emails.resetPassword.subject')
|
||||
#container.col
|
||||
p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName })
|
||||
include ../hello.pug
|
||||
p= t('emails.resetPassword.youOrSomeoneResetPassword')
|
||||
p= t('emails.resetPassword.pleaseClickLink')
|
||||
p
|
||||
= t('emails.resetPassword.pleaseClickLink')
|
||||
br
|
||||
a(href=resetLink) #{resetLink}
|
||||
br
|
||||
span= t('emails.general.orCopyLink')
|
||||
p= t('emails.resetPassword.duration', { hours: timeDurationObject.hours, minutes: timeDurationObject.minutes })
|
||||
= t('emails.general.orCopyLink')
|
||||
p
|
||||
= t('emails.resetPassword.duration', { hours: timeDurationObject.hours, minutes: timeDurationObject.minutes })
|
||||
br
|
||||
a(href=resendLink) #{resendLink}
|
||||
p(style='margin-top: 24px;')= t('emails.general.sincerelyYours')
|
||||
br
|
||||
span= t('emails.general.yourGradidoTeam')
|
||||
include ../greatingFormularImprint.pug
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
doctype html
|
||||
html(lang=locale)
|
||||
head
|
||||
title= t('emails.transactionLinkRedeemed.subject')
|
||||
title= t('emails.transactionLinkRedeemed.subject', { senderFirstName, senderLastName })
|
||||
body
|
||||
h1(style='margin-bottom: 24px;')= t('emails.transactionLinkRedeemed.subject')
|
||||
h1(style='margin-bottom: 24px;')= t('emails.transactionLinkRedeemed.subject', { senderFirstName, senderLastName })
|
||||
#container.col
|
||||
p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName })
|
||||
include ../hello.pug
|
||||
p= t('emails.transactionLinkRedeemed.hasRedeemedYourLink', { senderFirstName, senderLastName, senderEmail })
|
||||
p= t('emails.general.amountGDD', { amountGDD: transactionAmount })
|
||||
p
|
||||
= t('emails.general.amountGDD', { amountGDD: transactionAmount })
|
||||
br
|
||||
span= t('emails.transactionLinkRedeemed.memo', { transactionMemo })
|
||||
p= t('emails.general.detailsYouFindOnLinkToYourAccount')
|
||||
span= " "
|
||||
= t('emails.transactionLinkRedeemed.memo', { transactionMemo })
|
||||
p
|
||||
= t('emails.general.detailsYouFindOnLinkToYourAccount')
|
||||
= " "
|
||||
a(href=overviewURL) #{overviewURL}
|
||||
p= t('emails.general.pleaseDoNotReply')
|
||||
p(style='margin-top: 24px;')= t('emails.general.sincerelyYours')
|
||||
br
|
||||
span= t('emails.general.yourGradidoTeam')
|
||||
include ../greatingFormularImprint.pug
|
||||
|
||||
@ -1 +1 @@
|
||||
= t('emails.transactionLinkRedeemed.subject')
|
||||
= t('emails.transactionLinkRedeemed.subject', { senderFirstName, senderLastName })
|
||||
|
||||
@ -1,16 +1,15 @@
|
||||
doctype html
|
||||
html(lang=locale)
|
||||
head
|
||||
title= t('emails.transactionReceived.subject')
|
||||
title= t('emails.transactionReceived.subject', { senderFirstName, senderLastName, transactionAmount })
|
||||
body
|
||||
h1(style='margin-bottom: 24px;')= t('emails.transactionReceived.subject')
|
||||
h1(style='margin-bottom: 24px;')= t('emails.transactionReceived.subject', { senderFirstName, senderLastName, transactionAmount })
|
||||
#container.col
|
||||
p(style='margin-bottom: 24px;')= t('emails.general.helloName', { firstName, lastName })
|
||||
include ../hello.pug
|
||||
p= t('emails.transactionReceived.haveReceivedAmountGDDFrom', { transactionAmount, senderFirstName, senderLastName, senderEmail })
|
||||
p= t('emails.general.detailsYouFindOnLinkToYourAccount')
|
||||
span= " "
|
||||
p
|
||||
= t('emails.general.detailsYouFindOnLinkToYourAccount')
|
||||
= " "
|
||||
a(href=overviewURL) #{overviewURL}
|
||||
p= t('emails.general.pleaseDoNotReply')
|
||||
p(style='margin-top: 24px;')= t('emails.general.sincerelyYours')
|
||||
br
|
||||
span= t('emails.general.yourGradidoTeam')
|
||||
include ../greatingFormularImprint.pug
|
||||
|
||||
@ -1 +1 @@
|
||||
= t('emails.transactionReceived.subject')
|
||||
= t('emails.transactionReceived.subject', { senderFirstName, senderLastName, transactionAmount })
|
||||
|
||||
798
backend/src/federation/index.test.ts
Normal file
798
backend/src/federation/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(logger)
|
||||
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 in JSON at position 1'),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
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 in JSON at position 0'),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
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 in JSON at position 0'),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
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',
|
||||
},
|
||||
]),
|
||||
),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,14 +1,9 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import DHT from '@hyperswarm/dht'
|
||||
// import { Connection } from '@dbTools/typeorm'
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import CONFIG from '@/config'
|
||||
|
||||
function between(min: number, max: number) {
|
||||
return Math.floor(Math.random() * (max - min + 1) + min)
|
||||
}
|
||||
import { Community as DbCommunity } from '@entity/Community'
|
||||
|
||||
const KEY_SECRET_SEEDBYTES = 32
|
||||
const getSeed = (): Buffer | null =>
|
||||
@ -18,37 +13,107 @@ const POLLTIME = 20000
|
||||
const SUCCESSTIME = 120000
|
||||
const ERRORTIME = 240000
|
||||
const ANNOUNCETIME = 30000
|
||||
const nodeRand = between(1, 99)
|
||||
const nodeURL = `https://test${nodeRand}.org`
|
||||
const nodeAPI = {
|
||||
API_1_00: `${nodeURL}/api/1_00/`,
|
||||
API_1_01: `${nodeURL}/api/1_01/`,
|
||||
API_2_00: `${nodeURL}/graphql/2_00/`,
|
||||
|
||||
enum ApiVersionType {
|
||||
V1_0 = 'v1_0',
|
||||
V1_1 = 'v1_1',
|
||||
V2_0 = 'v2_0',
|
||||
}
|
||||
type CommunityApi = {
|
||||
api: string
|
||||
url: string
|
||||
}
|
||||
|
||||
export const startDHT = async (
|
||||
// connection: Connection,
|
||||
topic: string,
|
||||
): Promise<void> => {
|
||||
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) {
|
||||
// noiseSocket is E2E between you and the other peer
|
||||
// pipe it somewhere like any duplex stream
|
||||
logger.info(`Remote public key: ${socket.remotePublicKey.toString('hex')}`)
|
||||
// console.log("Local public key", noiseSocket.publicKey.toString("hex")); // same as keyPair.publicKey
|
||||
logger.info(`server on... with Remote public key: ${socket.remotePublicKey.toString('hex')}`)
|
||||
|
||||
socket.on('data', (data: Buffer) => logger.info(`data: ${data.toString('ascii')}`))
|
||||
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'))
|
||||
|
||||
// process.stdin.pipe(noiseSocket).pipe(process.stdout);
|
||||
// TODO better to introduce the validation by https://github.com/typestack/class-validato
|
||||
if (recApiVersions && Array.isArray(recApiVersions) && recApiVersions.length < 5) {
|
||||
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)}`,
|
||||
)
|
||||
// in a forEach-loop use return instead of continue
|
||||
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,
|
||||
)}`,
|
||||
)
|
||||
// in a forEach-loop use return instead of continue
|
||||
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...`)
|
||||
}
|
||||
} else {
|
||||
logger.warn(
|
||||
`received totaly wrong or too much apiVersions-Definition JSON-String: ${JSON.stringify(
|
||||
recApiVersions,
|
||||
)}`,
|
||||
)
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('Error on receiving data from socket:', e)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
await server.listen()
|
||||
@ -93,7 +158,6 @@ export const startDHT = async (
|
||||
logger.info(`Found new peers: ${collectedPubKeys}`)
|
||||
|
||||
collectedPubKeys.forEach((remotePubKey) => {
|
||||
// publicKey here is keyPair.publicKey from above
|
||||
const socket = node.connect(Buffer.from(remotePubKey, 'hex'))
|
||||
|
||||
// socket.once("connect", function () {
|
||||
@ -110,17 +174,12 @@ export const startDHT = async (
|
||||
})
|
||||
|
||||
socket.on('open', function () {
|
||||
// noiseSocket fully open with the other peer
|
||||
// console.log("writing to socket");
|
||||
socket.write(Buffer.from(`${nodeRand}`))
|
||||
socket.write(Buffer.from(JSON.stringify(nodeAPI)))
|
||||
socket.write(Buffer.from(JSON.stringify(ownApiVersions)))
|
||||
successfulRequests.push(remotePubKey)
|
||||
})
|
||||
// pipe it somewhere like any duplex stream
|
||||
// process.stdin.pipe(noiseSocket).pipe(process.stdout)
|
||||
})
|
||||
}, POLLTIME)
|
||||
} catch (err) {
|
||||
logger.error(err)
|
||||
logger.error('DHT unexpected error:', err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1146,13 +1146,21 @@ describe('ContributionResolver', () => {
|
||||
const now = new Date()
|
||||
|
||||
beforeAll(async () => {
|
||||
creation = await creationFactory(testEnv, {
|
||||
email: 'peter@lustig.de',
|
||||
amount: 400,
|
||||
memo: 'Herzlich Willkommen bei Gradido!',
|
||||
creationDate: contributionDateFormatter(
|
||||
new Date(now.getFullYear(), now.getMonth() - 1, 1),
|
||||
),
|
||||
await mutate({
|
||||
mutation: adminCreateContribution,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
amount: 400,
|
||||
memo: 'Herzlich Willkommen bei Gradido!',
|
||||
creationDate: contributionDateFormatter(
|
||||
new Date(now.getFullYear(), now.getMonth() - 1, 1),
|
||||
),
|
||||
},
|
||||
})
|
||||
creation = await Contribution.findOneOrFail({
|
||||
where: {
|
||||
memo: 'Herzlich Willkommen bei Gradido!',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@ -1879,6 +1887,10 @@ describe('ContributionResolver', () => {
|
||||
new Date(now.getFullYear(), now.getMonth() - 2, 1),
|
||||
),
|
||||
})
|
||||
await query({
|
||||
query: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
|
||||
it('returns true', async () => {
|
||||
@ -1959,10 +1971,13 @@ describe('ContributionResolver', () => {
|
||||
new Date(now.getFullYear(), now.getMonth() - 2, 1),
|
||||
),
|
||||
})
|
||||
await query({
|
||||
query: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
})
|
||||
|
||||
// In the futrue this should not throw anymore
|
||||
it('throws an error for the second confirmation', async () => {
|
||||
it('throws no error for the second confirmation', async () => {
|
||||
const r1 = mutate({
|
||||
mutation: confirmContribution,
|
||||
variables: {
|
||||
@ -1982,8 +1997,7 @@ describe('ContributionResolver', () => {
|
||||
)
|
||||
await expect(r2).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
// data: { confirmContribution: true },
|
||||
errors: [new GraphQLError('Creation was not successful.')],
|
||||
data: { confirmContribution: true },
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@ -50,6 +50,7 @@ import {
|
||||
sendContributionConfirmedEmail,
|
||||
sendContributionRejectedEmail,
|
||||
} from '@/emails/sendEmailVariants'
|
||||
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
|
||||
|
||||
@Resolver()
|
||||
export class ContributionResolver {
|
||||
@ -581,8 +582,10 @@ export class ContributionResolver {
|
||||
clientTimezoneOffset,
|
||||
)
|
||||
|
||||
const receivedCallDate = new Date()
|
||||
// acquire lock
|
||||
const releaseLock = await TRANSACTIONS_LOCK.acquire()
|
||||
|
||||
const receivedCallDate = new Date()
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('REPEATABLE READ') // 'READ COMMITTED')
|
||||
@ -592,7 +595,7 @@ export class ContributionResolver {
|
||||
.select('transaction')
|
||||
.from(DbTransaction, 'transaction')
|
||||
.where('transaction.userId = :id', { id: contribution.userId })
|
||||
.orderBy('transaction.balanceDate', 'DESC')
|
||||
.orderBy('transaction.id', 'DESC')
|
||||
.getOne()
|
||||
logger.info('lastTransaction ID', lastTransaction ? lastTransaction.id : 'undefined')
|
||||
|
||||
@ -641,10 +644,11 @@ export class ContributionResolver {
|
||||
})
|
||||
} catch (e) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
logger.error(`Creation was not successful: ${e}`)
|
||||
throw new Error(`Creation was not successful.`)
|
||||
logger.error('Creation was not successful', e)
|
||||
throw new Error('Creation was not successful.')
|
||||
} finally {
|
||||
await queryRunner.release()
|
||||
releaseLock()
|
||||
}
|
||||
|
||||
const event = new Event()
|
||||
|
||||
@ -23,6 +23,11 @@ import { User } from '@entity/User'
|
||||
import { UnconfirmedContribution } from '@model/UnconfirmedContribution'
|
||||
import Decimal from 'decimal.js-light'
|
||||
import { GraphQLError } from 'graphql'
|
||||
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
|
||||
|
||||
// mock semaphore to allow use fake timers
|
||||
jest.mock('@/util/TRANSACTIONS_LOCK')
|
||||
TRANSACTIONS_LOCK.acquire = jest.fn().mockResolvedValue(jest.fn())
|
||||
|
||||
let mutate: any, query: any, con: any
|
||||
let testEnv: any
|
||||
@ -185,8 +190,7 @@ describe('TransactionLinkResolver', () => {
|
||||
describe('after one day', () => {
|
||||
beforeAll(async () => {
|
||||
jest.useFakeTimers()
|
||||
/* eslint-disable-next-line @typescript-eslint/no-empty-function */
|
||||
setTimeout(() => {}, 1000 * 60 * 60 * 24)
|
||||
setTimeout(jest.fn(), 1000 * 60 * 60 * 24)
|
||||
jest.runAllTimers()
|
||||
await mutate({
|
||||
mutation: login,
|
||||
|
||||
@ -31,6 +31,7 @@ import { calculateDecay } from '@/util/decay'
|
||||
import { getUserCreation, validateContribution } from './util/creations'
|
||||
import { executeTransaction } from './TransactionResolver'
|
||||
import QueryLinkResult from '@union/QueryLinkResult'
|
||||
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
|
||||
|
||||
// TODO: do not export, test it inside the resolver
|
||||
export const transactionLinkCode = (date: Date): string => {
|
||||
@ -165,10 +166,12 @@ export class TransactionLinkResolver {
|
||||
): Promise<boolean> {
|
||||
const clientTimezoneOffset = getClientTimezoneOffset(context)
|
||||
const user = getUser(context)
|
||||
const now = new Date()
|
||||
|
||||
if (code.match(/^CL-/)) {
|
||||
// acquire lock
|
||||
const releaseLock = await TRANSACTIONS_LOCK.acquire()
|
||||
logger.info('redeem contribution link...')
|
||||
const now = new Date()
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('REPEATABLE READ')
|
||||
@ -273,7 +276,7 @@ export class TransactionLinkResolver {
|
||||
.select('transaction')
|
||||
.from(DbTransaction, 'transaction')
|
||||
.where('transaction.userId = :id', { id: user.id })
|
||||
.orderBy('transaction.balanceDate', 'DESC')
|
||||
.orderBy('transaction.id', 'DESC')
|
||||
.getOne()
|
||||
let newBalance = new Decimal(0)
|
||||
|
||||
@ -309,9 +312,11 @@ export class TransactionLinkResolver {
|
||||
throw new Error(`Creation from contribution link was not successful. ${e}`)
|
||||
} finally {
|
||||
await queryRunner.release()
|
||||
releaseLock()
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
const now = new Date()
|
||||
const transactionLink = await DbTransactionLink.findOneOrFail({ code })
|
||||
const linkedUser = await DbUser.findOneOrFail(
|
||||
{ id: transactionLink.userId },
|
||||
@ -322,6 +327,9 @@ export class TransactionLinkResolver {
|
||||
throw new Error('Cannot redeem own transaction link.')
|
||||
}
|
||||
|
||||
// TODO: The now check should be done within the semaphore lock,
|
||||
// since the program might wait a while till it is ready to proceed
|
||||
// writing the transaction.
|
||||
if (transactionLink.validUntil.getTime() < now.getTime()) {
|
||||
throw new Error('Transaction Link is not valid anymore.')
|
||||
}
|
||||
|
||||
@ -368,5 +368,74 @@ describe('send coins', () => {
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('more transactions to test semaphore', () => {
|
||||
it('sends the coins four times in a row', async () => {
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
amount: 10,
|
||||
memo: 'first transaction',
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
sendCoins: 'true',
|
||||
},
|
||||
}),
|
||||
)
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
amount: 20,
|
||||
memo: 'second transaction',
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
sendCoins: 'true',
|
||||
},
|
||||
}),
|
||||
)
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
amount: 30,
|
||||
memo: 'third transaction',
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
sendCoins: 'true',
|
||||
},
|
||||
}),
|
||||
)
|
||||
await expect(
|
||||
mutate({
|
||||
mutation: sendCoins,
|
||||
variables: {
|
||||
email: 'peter@lustig.de',
|
||||
amount: 40,
|
||||
memo: 'fourth transaction',
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
sendCoins: 'true',
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -36,6 +36,8 @@ import { BalanceResolver } from './BalanceResolver'
|
||||
import { MEMO_MAX_CHARS, MEMO_MIN_CHARS } from './const/const'
|
||||
import { findUserByEmail } from './UserResolver'
|
||||
|
||||
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
|
||||
|
||||
export const executeTransaction = async (
|
||||
amount: Decimal,
|
||||
memo: string,
|
||||
@ -62,124 +64,133 @@ export const executeTransaction = async (
|
||||
throw new Error(`memo text is too short (${MEMO_MIN_CHARS} characters minimum)`)
|
||||
}
|
||||
|
||||
// validate amount
|
||||
const receivedCallDate = new Date()
|
||||
const sendBalance = await calculateBalance(
|
||||
sender.id,
|
||||
amount.mul(-1),
|
||||
receivedCallDate,
|
||||
transactionLink,
|
||||
)
|
||||
logger.debug(`calculated Balance=${sendBalance}`)
|
||||
if (!sendBalance) {
|
||||
logger.error(`user hasn't enough GDD or amount is < 0 : balance=${sendBalance}`)
|
||||
throw new Error("user hasn't enough GDD or amount is < 0")
|
||||
}
|
||||
// acquire lock
|
||||
const releaseLock = await TRANSACTIONS_LOCK.acquire()
|
||||
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('REPEATABLE READ')
|
||||
logger.debug(`open Transaction to write...`)
|
||||
try {
|
||||
// transaction
|
||||
const transactionSend = new dbTransaction()
|
||||
transactionSend.typeId = TransactionTypeId.SEND
|
||||
transactionSend.memo = memo
|
||||
transactionSend.userId = sender.id
|
||||
transactionSend.linkedUserId = recipient.id
|
||||
transactionSend.amount = amount.mul(-1)
|
||||
transactionSend.balance = sendBalance.balance
|
||||
transactionSend.balanceDate = receivedCallDate
|
||||
transactionSend.decay = sendBalance.decay.decay
|
||||
transactionSend.decayStart = sendBalance.decay.start
|
||||
transactionSend.previous = sendBalance.lastTransactionId
|
||||
transactionSend.transactionLinkId = transactionLink ? transactionLink.id : null
|
||||
await queryRunner.manager.insert(dbTransaction, transactionSend)
|
||||
|
||||
logger.debug(`sendTransaction inserted: ${dbTransaction}`)
|
||||
|
||||
const transactionReceive = new dbTransaction()
|
||||
transactionReceive.typeId = TransactionTypeId.RECEIVE
|
||||
transactionReceive.memo = memo
|
||||
transactionReceive.userId = recipient.id
|
||||
transactionReceive.linkedUserId = sender.id
|
||||
transactionReceive.amount = amount
|
||||
const receiveBalance = await calculateBalance(recipient.id, amount, receivedCallDate)
|
||||
transactionReceive.balance = receiveBalance ? receiveBalance.balance : amount
|
||||
transactionReceive.balanceDate = receivedCallDate
|
||||
transactionReceive.decay = receiveBalance ? receiveBalance.decay.decay : new Decimal(0)
|
||||
transactionReceive.decayStart = receiveBalance ? receiveBalance.decay.start : null
|
||||
transactionReceive.previous = receiveBalance ? receiveBalance.lastTransactionId : null
|
||||
transactionReceive.linkedTransactionId = transactionSend.id
|
||||
transactionReceive.transactionLinkId = transactionLink ? transactionLink.id : null
|
||||
await queryRunner.manager.insert(dbTransaction, transactionReceive)
|
||||
logger.debug(`receive Transaction inserted: ${dbTransaction}`)
|
||||
|
||||
// Save linked transaction id for send
|
||||
transactionSend.linkedTransactionId = transactionReceive.id
|
||||
await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend)
|
||||
logger.debug(`send Transaction updated: ${transactionSend}`)
|
||||
|
||||
if (transactionLink) {
|
||||
logger.info(`transactionLink: ${transactionLink}`)
|
||||
transactionLink.redeemedAt = receivedCallDate
|
||||
transactionLink.redeemedBy = recipient.id
|
||||
await queryRunner.manager.update(
|
||||
dbTransactionLink,
|
||||
{ id: transactionLink.id },
|
||||
transactionLink,
|
||||
)
|
||||
// validate amount
|
||||
const receivedCallDate = new Date()
|
||||
const sendBalance = await calculateBalance(
|
||||
sender.id,
|
||||
amount.mul(-1),
|
||||
receivedCallDate,
|
||||
transactionLink,
|
||||
)
|
||||
logger.debug(`calculated Balance=${sendBalance}`)
|
||||
if (!sendBalance) {
|
||||
logger.error(`user hasn't enough GDD or amount is < 0 : balance=${sendBalance}`)
|
||||
throw new Error("user hasn't enough GDD or amount is < 0")
|
||||
}
|
||||
|
||||
await queryRunner.commitTransaction()
|
||||
logger.info(`commit Transaction successful...`)
|
||||
const queryRunner = getConnection().createQueryRunner()
|
||||
await queryRunner.connect()
|
||||
await queryRunner.startTransaction('REPEATABLE READ')
|
||||
logger.debug(`open Transaction to write...`)
|
||||
try {
|
||||
// transaction
|
||||
const transactionSend = new dbTransaction()
|
||||
transactionSend.typeId = TransactionTypeId.SEND
|
||||
transactionSend.memo = memo
|
||||
transactionSend.userId = sender.id
|
||||
transactionSend.linkedUserId = recipient.id
|
||||
transactionSend.amount = amount.mul(-1)
|
||||
transactionSend.balance = sendBalance.balance
|
||||
transactionSend.balanceDate = receivedCallDate
|
||||
transactionSend.decay = sendBalance.decay.decay
|
||||
transactionSend.decayStart = sendBalance.decay.start
|
||||
transactionSend.previous = sendBalance.lastTransactionId
|
||||
transactionSend.transactionLinkId = transactionLink ? transactionLink.id : null
|
||||
await queryRunner.manager.insert(dbTransaction, transactionSend)
|
||||
|
||||
const eventTransactionSend = new EventTransactionSend()
|
||||
eventTransactionSend.userId = transactionSend.userId
|
||||
eventTransactionSend.xUserId = transactionSend.linkedUserId
|
||||
eventTransactionSend.transactionId = transactionSend.id
|
||||
eventTransactionSend.amount = transactionSend.amount.mul(-1)
|
||||
await eventProtocol.writeEvent(new Event().setEventTransactionSend(eventTransactionSend))
|
||||
logger.debug(`sendTransaction inserted: ${dbTransaction}`)
|
||||
|
||||
const eventTransactionReceive = new EventTransactionReceive()
|
||||
eventTransactionReceive.userId = transactionReceive.userId
|
||||
eventTransactionReceive.xUserId = transactionReceive.linkedUserId
|
||||
eventTransactionReceive.transactionId = transactionReceive.id
|
||||
eventTransactionReceive.amount = transactionReceive.amount
|
||||
await eventProtocol.writeEvent(new Event().setEventTransactionReceive(eventTransactionReceive))
|
||||
} catch (e) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
logger.error(`Transaction was not successful: ${e}`)
|
||||
throw new Error(`Transaction was not successful: ${e}`)
|
||||
} finally {
|
||||
await queryRunner.release()
|
||||
}
|
||||
logger.debug(`prepare Email for transaction received...`)
|
||||
await sendTransactionReceivedEmail({
|
||||
firstName: recipient.firstName,
|
||||
lastName: recipient.lastName,
|
||||
email: recipient.emailContact.email,
|
||||
language: recipient.language,
|
||||
senderFirstName: sender.firstName,
|
||||
senderLastName: sender.lastName,
|
||||
senderEmail: sender.emailContact.email,
|
||||
transactionAmount: amount,
|
||||
})
|
||||
if (transactionLink) {
|
||||
await sendTransactionLinkRedeemedEmail({
|
||||
firstName: sender.firstName,
|
||||
lastName: sender.lastName,
|
||||
email: sender.emailContact.email,
|
||||
language: sender.language,
|
||||
senderFirstName: recipient.firstName,
|
||||
senderLastName: recipient.lastName,
|
||||
senderEmail: recipient.emailContact.email,
|
||||
const transactionReceive = new dbTransaction()
|
||||
transactionReceive.typeId = TransactionTypeId.RECEIVE
|
||||
transactionReceive.memo = memo
|
||||
transactionReceive.userId = recipient.id
|
||||
transactionReceive.linkedUserId = sender.id
|
||||
transactionReceive.amount = amount
|
||||
const receiveBalance = await calculateBalance(recipient.id, amount, receivedCallDate)
|
||||
transactionReceive.balance = receiveBalance ? receiveBalance.balance : amount
|
||||
transactionReceive.balanceDate = receivedCallDate
|
||||
transactionReceive.decay = receiveBalance ? receiveBalance.decay.decay : new Decimal(0)
|
||||
transactionReceive.decayStart = receiveBalance ? receiveBalance.decay.start : null
|
||||
transactionReceive.previous = receiveBalance ? receiveBalance.lastTransactionId : null
|
||||
transactionReceive.linkedTransactionId = transactionSend.id
|
||||
transactionReceive.transactionLinkId = transactionLink ? transactionLink.id : null
|
||||
await queryRunner.manager.insert(dbTransaction, transactionReceive)
|
||||
logger.debug(`receive Transaction inserted: ${dbTransaction}`)
|
||||
|
||||
// Save linked transaction id for send
|
||||
transactionSend.linkedTransactionId = transactionReceive.id
|
||||
await queryRunner.manager.update(dbTransaction, { id: transactionSend.id }, transactionSend)
|
||||
logger.debug(`send Transaction updated: ${transactionSend}`)
|
||||
|
||||
if (transactionLink) {
|
||||
logger.info(`transactionLink: ${transactionLink}`)
|
||||
transactionLink.redeemedAt = receivedCallDate
|
||||
transactionLink.redeemedBy = recipient.id
|
||||
await queryRunner.manager.update(
|
||||
dbTransactionLink,
|
||||
{ id: transactionLink.id },
|
||||
transactionLink,
|
||||
)
|
||||
}
|
||||
|
||||
await queryRunner.commitTransaction()
|
||||
logger.info(`commit Transaction successful...`)
|
||||
|
||||
const eventTransactionSend = new EventTransactionSend()
|
||||
eventTransactionSend.userId = transactionSend.userId
|
||||
eventTransactionSend.xUserId = transactionSend.linkedUserId
|
||||
eventTransactionSend.transactionId = transactionSend.id
|
||||
eventTransactionSend.amount = transactionSend.amount.mul(-1)
|
||||
await eventProtocol.writeEvent(new Event().setEventTransactionSend(eventTransactionSend))
|
||||
|
||||
const eventTransactionReceive = new EventTransactionReceive()
|
||||
eventTransactionReceive.userId = transactionReceive.userId
|
||||
eventTransactionReceive.xUserId = transactionReceive.linkedUserId
|
||||
eventTransactionReceive.transactionId = transactionReceive.id
|
||||
eventTransactionReceive.amount = transactionReceive.amount
|
||||
await eventProtocol.writeEvent(
|
||||
new Event().setEventTransactionReceive(eventTransactionReceive),
|
||||
)
|
||||
} catch (e) {
|
||||
await queryRunner.rollbackTransaction()
|
||||
logger.error(`Transaction was not successful: ${e}`)
|
||||
throw new Error(`Transaction was not successful: ${e}`)
|
||||
} finally {
|
||||
await queryRunner.release()
|
||||
}
|
||||
logger.debug(`prepare Email for transaction received...`)
|
||||
await sendTransactionReceivedEmail({
|
||||
firstName: recipient.firstName,
|
||||
lastName: recipient.lastName,
|
||||
email: recipient.emailContact.email,
|
||||
language: recipient.language,
|
||||
senderFirstName: sender.firstName,
|
||||
senderLastName: sender.lastName,
|
||||
senderEmail: sender.emailContact.email,
|
||||
transactionAmount: amount,
|
||||
transactionMemo: memo,
|
||||
})
|
||||
if (transactionLink) {
|
||||
await sendTransactionLinkRedeemedEmail({
|
||||
firstName: sender.firstName,
|
||||
lastName: sender.lastName,
|
||||
email: sender.emailContact.email,
|
||||
language: sender.language,
|
||||
senderFirstName: recipient.firstName,
|
||||
senderLastName: recipient.lastName,
|
||||
senderEmail: recipient.emailContact.email,
|
||||
transactionAmount: amount,
|
||||
transactionMemo: memo,
|
||||
})
|
||||
}
|
||||
logger.info(`finished executeTransaction successfully`)
|
||||
return true
|
||||
} finally {
|
||||
releaseLock()
|
||||
}
|
||||
logger.info(`finished executeTransaction successfully`)
|
||||
return true
|
||||
}
|
||||
|
||||
@Resolver()
|
||||
|
||||
190
backend/src/graphql/resolver/semaphore.test.ts
Normal file
190
backend/src/graphql/resolver/semaphore.test.ts
Normal file
@ -0,0 +1,190 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import Decimal from 'decimal.js-light'
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { logger } from '@test/testSetup'
|
||||
import { userFactory } from '@/seeds/factory/user'
|
||||
import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg'
|
||||
import { bobBaumeister } from '@/seeds/users/bob-baumeister'
|
||||
import { peterLustig } from '@/seeds/users/peter-lustig'
|
||||
import { creationFactory, nMonthsBefore } from '@/seeds/factory/creation'
|
||||
import { cleanDB, testEnvironment, contributionDateFormatter } from '@test/helpers'
|
||||
import {
|
||||
confirmContribution,
|
||||
createContribution,
|
||||
createTransactionLink,
|
||||
redeemTransactionLink,
|
||||
login,
|
||||
createContributionLink,
|
||||
sendCoins,
|
||||
} from '@/seeds/graphql/mutations'
|
||||
|
||||
let mutate: any, con: any
|
||||
let testEnv: any
|
||||
|
||||
beforeAll(async () => {
|
||||
testEnv = await testEnvironment()
|
||||
mutate = testEnv.mutate
|
||||
con = testEnv.con
|
||||
await cleanDB()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanDB()
|
||||
await con.close()
|
||||
})
|
||||
|
||||
describe('semaphore', () => {
|
||||
let contributionLinkCode = ''
|
||||
let bobsTransactionLinkCode = ''
|
||||
let bibisTransactionLinkCode = ''
|
||||
let bibisOpenContributionId = -1
|
||||
let bobsOpenContributionId = -1
|
||||
|
||||
beforeAll(async () => {
|
||||
const now = new Date()
|
||||
await userFactory(testEnv, bibiBloxberg)
|
||||
await userFactory(testEnv, peterLustig)
|
||||
await userFactory(testEnv, bobBaumeister)
|
||||
await creationFactory(testEnv, {
|
||||
email: 'bibi@bloxberg.de',
|
||||
amount: 1000,
|
||||
memo: 'Herzlich Willkommen bei Gradido!',
|
||||
creationDate: nMonthsBefore(new Date()),
|
||||
confirmed: true,
|
||||
})
|
||||
await creationFactory(testEnv, {
|
||||
email: 'bob@baumeister.de',
|
||||
amount: 1000,
|
||||
memo: 'Herzlich Willkommen bei Gradido!',
|
||||
creationDate: nMonthsBefore(new Date()),
|
||||
confirmed: true,
|
||||
})
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
const {
|
||||
data: { createContributionLink: contributionLink },
|
||||
} = await mutate({
|
||||
mutation: createContributionLink,
|
||||
variables: {
|
||||
amount: new Decimal(200),
|
||||
name: 'Test Contribution Link',
|
||||
memo: 'Danke für deine Teilnahme an dem Test der Contribution Links',
|
||||
cycle: 'ONCE',
|
||||
validFrom: new Date(2022, 5, 18).toISOString(),
|
||||
validTo: new Date(now.getFullYear() + 1, 7, 14).toISOString(),
|
||||
maxAmountPerMonth: new Decimal(200),
|
||||
maxPerCycle: 1,
|
||||
},
|
||||
})
|
||||
contributionLinkCode = 'CL-' + contributionLink.code
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bob@baumeister.de', password: 'Aa12345_' },
|
||||
})
|
||||
const {
|
||||
data: { createTransactionLink: bobsLink },
|
||||
} = await mutate({
|
||||
mutation: createTransactionLink,
|
||||
variables: {
|
||||
email: 'bob@baumeister.de',
|
||||
amount: 20,
|
||||
memo: 'Bobs Link',
|
||||
},
|
||||
})
|
||||
const {
|
||||
data: { createContribution: bobsContribution },
|
||||
} = await mutate({
|
||||
mutation: createContribution,
|
||||
variables: {
|
||||
creationDate: contributionDateFormatter(new Date()),
|
||||
amount: 200,
|
||||
memo: 'Bobs Contribution',
|
||||
},
|
||||
})
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
const {
|
||||
data: { createTransactionLink: bibisLink },
|
||||
} = await mutate({
|
||||
mutation: createTransactionLink,
|
||||
variables: {
|
||||
amount: 20,
|
||||
memo: 'Bibis Link',
|
||||
},
|
||||
})
|
||||
const {
|
||||
data: { createContribution: bibisContribution },
|
||||
} = await mutate({
|
||||
mutation: createContribution,
|
||||
variables: {
|
||||
creationDate: contributionDateFormatter(new Date()),
|
||||
amount: 200,
|
||||
memo: 'Bibis Contribution',
|
||||
},
|
||||
})
|
||||
bobsTransactionLinkCode = bobsLink.code
|
||||
bibisTransactionLinkCode = bibisLink.code
|
||||
bibisOpenContributionId = bibisContribution.id
|
||||
bobsOpenContributionId = bobsContribution.id
|
||||
})
|
||||
|
||||
it('creates a lot of transactions without errors', async () => {
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' },
|
||||
})
|
||||
const bibiRedeemContributionLink = mutate({
|
||||
mutation: redeemTransactionLink,
|
||||
variables: { code: contributionLinkCode },
|
||||
})
|
||||
const redeemBobsLink = mutate({
|
||||
mutation: redeemTransactionLink,
|
||||
variables: { code: bobsTransactionLinkCode },
|
||||
})
|
||||
const bibisTransaction = mutate({
|
||||
mutation: sendCoins,
|
||||
variables: { email: 'bob@baumeister.de', amount: '50', memo: 'Das ist für dich, Bob' },
|
||||
})
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'bob@baumeister.de', password: 'Aa12345_' },
|
||||
})
|
||||
const bobRedeemContributionLink = mutate({
|
||||
mutation: redeemTransactionLink,
|
||||
variables: { code: contributionLinkCode },
|
||||
})
|
||||
const redeemBibisLink = mutate({
|
||||
mutation: redeemTransactionLink,
|
||||
variables: { code: bibisTransactionLinkCode },
|
||||
})
|
||||
const bobsTransaction = mutate({
|
||||
mutation: sendCoins,
|
||||
variables: { email: 'bibi@bloxberg.de', amount: '50', memo: 'Das ist für dich, Bibi' },
|
||||
})
|
||||
await mutate({
|
||||
mutation: login,
|
||||
variables: { email: 'peter@lustig.de', password: 'Aa12345_' },
|
||||
})
|
||||
const confirmBibisContribution = mutate({
|
||||
mutation: confirmContribution,
|
||||
variables: { id: bibisOpenContributionId },
|
||||
})
|
||||
const confirmBobsContribution = mutate({
|
||||
mutation: confirmContribution,
|
||||
variables: { id: bobsOpenContributionId },
|
||||
})
|
||||
await expect(bibiRedeemContributionLink).resolves.toMatchObject({ errors: undefined })
|
||||
await expect(redeemBobsLink).resolves.toMatchObject({ errors: undefined })
|
||||
await expect(bibisTransaction).resolves.toMatchObject({ errors: undefined })
|
||||
await expect(bobRedeemContributionLink).resolves.toMatchObject({ errors: undefined })
|
||||
await expect(redeemBibisLink).resolves.toMatchObject({ errors: undefined })
|
||||
await expect(bobsTransaction).resolves.toMatchObject({ errors: undefined })
|
||||
await expect(confirmBibisContribution).resolves.toMatchObject({ errors: undefined })
|
||||
await expect(confirmBobsContribution).resolves.toMatchObject({ errors: undefined })
|
||||
})
|
||||
})
|
||||
@ -183,8 +183,10 @@ describe('util/creation', () => {
|
||||
})
|
||||
|
||||
it('has the clock set correctly', () => {
|
||||
const targetMonthString =
|
||||
(targetDate.getMonth() + 1 < 10 ? '0' : '') + String(targetDate.getMonth() + 1)
|
||||
expect(new Date().toISOString()).toContain(
|
||||
`${targetDate.getFullYear()}-${targetDate.getMonth() + 1}-${targetDate.getDate()}T23:`,
|
||||
`${targetDate.getFullYear()}-${targetMonthString}-${targetDate.getDate()}T23:`,
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@ -20,6 +20,9 @@ async function main() {
|
||||
|
||||
// start DHT hyperswarm when DHT_TOPIC is set in .env
|
||||
if (CONFIG.FEDERATION_DHT_TOPIC) {
|
||||
if (CONFIG.FEDERATION_COMMUNITY_URL === null) {
|
||||
throw Error(`Config-Error: missing configuration of property FEDERATION_COMMUNITY_URL`)
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(
|
||||
`starting Federation on ${CONFIG.FEDERATION_DHT_TOPIC} ${
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
"toSeeAndAnswerMessage": "Um die Nachricht zu sehen und darauf zu antworten, gehe in deinem Gradido-Konto ins Menü „Gemeinschaft“ auf den Tab „Meine Beiträge zum Gemeinwohl“!"
|
||||
},
|
||||
"accountActivation": {
|
||||
"duration": "Der Link hat eine Gültigkeit von {hours} Stunden und {minutes} Minuten. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen, in dem du deine E-Mail-Adresse eingibst:",
|
||||
"duration": "Der Link hat eine Gültigkeit von {hours} Stunden und {minutes} Minuten. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen:",
|
||||
"emailRegistered": "deine E-Mail-Adresse wurde soeben bei Gradido registriert.",
|
||||
"pleaseClickLink": "Klicke bitte auf diesen Link, um die Registrierung abzuschließen und dein Gradido-Konto zu aktivieren:",
|
||||
"subject": "Gradido: E-Mail Überprüfung"
|
||||
@ -14,7 +14,7 @@
|
||||
"accountMultiRegistration": {
|
||||
"emailExists": "Es existiert jedoch zu deiner E-Mail-Adresse schon ein Konto.",
|
||||
"emailReused": "deine E-Mail-Adresse wurde soeben erneut benutzt, um bei Gradido ein Konto zu registrieren.",
|
||||
"ifYouAreNotTheOne": "Wenn du nicht derjenige bist, der sich versucht hat erneut zu registrieren, wende dich bitte an unseren support:",
|
||||
"ifYouAreNotTheOne": "Wenn du nicht derjenige bist, der versucht hat sich erneut zu registrieren, wende dich bitte an unseren Support:",
|
||||
"onForgottenPasswordClickLink": "Klicke bitte auf den folgenden Link, falls du dein Passwort vergessen haben solltest:",
|
||||
"onForgottenPasswordCopyLink": "oder kopiere den obigen Link in dein Browserfenster.",
|
||||
"subject": "Gradido: Erneuter Registrierungsversuch mit deiner E-Mail"
|
||||
@ -40,22 +40,25 @@
|
||||
"yourGradidoTeam": "dein Gradido-Team"
|
||||
},
|
||||
"resetPassword": {
|
||||
"duration": "Der Link hat eine Gültigkeit von {hours} Stunden und {minutes} Minuten. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen, in dem du deine E-Mail-Adresse eingibst:",
|
||||
"duration": "Der Link hat eine Gültigkeit von {hours} Stunden und {minutes} Minuten. Sollte die Gültigkeit des Links bereits abgelaufen sein, kannst du dir hier einen neuen Link schicken lassen:",
|
||||
"pleaseClickLink": "Wenn du es warst, klicke bitte auf den Link:",
|
||||
"subject": "Gradido: Passwort zurücksetzen",
|
||||
"youOrSomeoneResetPassword": "du, oder jemand anderes, hast für dieses Konto ein Zurücksetzen des Passworts angefordert."
|
||||
},
|
||||
"transactionLinkRedeemed": {
|
||||
"hasRedeemedYourLink": "{senderFirstName} {senderLastName} ({senderEmail}) hat soeben deinen Link eingelöst.",
|
||||
"memo": "Memo: {transactionMemo}",
|
||||
"subject": "Gradido: Dein Gradido-Link wurde eingelöst"
|
||||
"memo": "Nachricht: {transactionMemo}",
|
||||
"subject": "Gradido: {senderFirstName} {senderLastName} hat deinen Gradido-Link eingelöst"
|
||||
},
|
||||
"transactionReceived": {
|
||||
"haveReceivedAmountGDDFrom": "du hast soeben {transactionAmount} GDD von {senderFirstName} {senderLastName} ({senderEmail}) erhalten.",
|
||||
"subject": "Gradido: Du hast Gradidos erhalten"
|
||||
"subject": "Gradido: {senderFirstName} {senderLastName} hat dir {transactionAmount} Gradido gesendet"
|
||||
}
|
||||
},
|
||||
"general": {
|
||||
"decimalSeparator": ","
|
||||
"decimalSeparator": ",",
|
||||
"imprint": "Gradido-Akademie\nInstitut für Wirtschaftsbionik\nPfarrweg 2\n74653 Künzelsau\nDeutschland",
|
||||
"imprintImageAlt": "Gradido-Akademie Logo",
|
||||
"imprintImageURL": "https://gdd.gradido.net/img/brand/green.png"
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
"toSeeAndAnswerMessage": "To view and reply to the message, go to the “Community” menu in your Gradido account and click on the “My contributions to the common good” tab!"
|
||||
},
|
||||
"accountActivation": {
|
||||
"duration": "The link has a validity of {hours} hours and {minutes} minutes. If the validity of the link has already expired, you can have a new link sent to you here by entering your email address:",
|
||||
"duration": "The link has a validity of {hours} hours and {minutes} minutes. If the validity of the link has already expired, you can have a new link sent to you here:",
|
||||
"emailRegistered": "Your email address has just been registered with Gradido.",
|
||||
"pleaseClickLink": "Please click on this link to complete the registration and activate your Gradido account:",
|
||||
"subject": "Gradido: Email Verification"
|
||||
@ -21,7 +21,7 @@
|
||||
},
|
||||
"contributionConfirmed": {
|
||||
"commonGoodContributionConfirmed": "Your public good contribution “{contributionMemo}” has just been confirmed by {senderFirstName} {senderLastName} and credited to your Gradido account.",
|
||||
"subject": "Gradido: Your common good contribution was confirmed"
|
||||
"subject": "Gradido: Your contribution to the common good was confirmed"
|
||||
},
|
||||
"contributionRejected": {
|
||||
"commonGoodContributionRejected": "Your public good contribution “{contributionMemo}” was rejected by {senderFirstName} {senderLastName}.",
|
||||
@ -40,22 +40,25 @@
|
||||
"yourGradidoTeam": "your Gradido team"
|
||||
},
|
||||
"resetPassword": {
|
||||
"duration": "The link has a validity of {hours} hours and {minutes} minutes. If the validity of the link has already expired, you can have a new link sent to you here by entering your email address:",
|
||||
"duration": "The link has a validity of {hours} hours and {minutes} minutes. If the validity of the link has already expired, you can have a new link sent to you here:",
|
||||
"pleaseClickLink": "If it was you, please click on the link:",
|
||||
"subject": "Gradido: Reset password",
|
||||
"youOrSomeoneResetPassword": "You, or someone else, requested a password reset for this account."
|
||||
},
|
||||
"transactionLinkRedeemed": {
|
||||
"hasRedeemedYourLink": "{senderFirstName} {senderLastName} ({senderEmail}) has just redeemed your link.",
|
||||
"memo": "Memo: {transactionMemo}",
|
||||
"subject": "Gradido: Your Gradido link has been redeemed"
|
||||
"memo": "Message: {transactionMemo}",
|
||||
"subject": "Gradido: {senderFirstName} {senderLastName} has redeemed your Gradido link"
|
||||
},
|
||||
"transactionReceived": {
|
||||
"haveReceivedAmountGDDFrom": "You have just received {transactionAmount} GDD from {senderFirstName} {senderLastName} ({senderEmail}).",
|
||||
"subject": "Gradido: You have received Gradidos"
|
||||
"subject": "Gradido: {senderFirstName} {senderLastName} has sent you {transactionAmount} Gradido"
|
||||
}
|
||||
},
|
||||
"general": {
|
||||
"decimalSeparator": "."
|
||||
"decimalSeparator": ".",
|
||||
"imprint": "Gradido-Akademie\nInstitut für Wirtschaftsbionik\nPfarrweg 2\n74653 Künzelsau\nDeutschland",
|
||||
"imprintImageAlt": "Gradido-Akademie Logo",
|
||||
"imprintImageURL": "https://gdd.gradido.net/img/brand/green.png"
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
|
||||
import { backendLogger as logger } from '@/server/logger'
|
||||
import { login, adminCreateContribution, confirmContribution } from '@/seeds/graphql/mutations'
|
||||
import { login, createContribution, confirmContribution } from '@/seeds/graphql/mutations'
|
||||
import { CreationInterface } from '@/seeds/creation/CreationInterface'
|
||||
import { ApolloServerTestClient } from 'apollo-server-testing'
|
||||
import { Transaction } from '@entity/Transaction'
|
||||
@ -19,43 +18,27 @@ export const creationFactory = async (
|
||||
creation: CreationInterface,
|
||||
): Promise<Contribution | void> => {
|
||||
const { mutate } = client
|
||||
logger.trace('creationFactory...')
|
||||
await mutate({ mutation: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' } })
|
||||
logger.trace('creationFactory... after login')
|
||||
// TODO it would be nice to have this mutation return the id
|
||||
await mutate({ mutation: adminCreateContribution, variables: { ...creation } })
|
||||
logger.trace('creationFactory... after adminCreateContribution')
|
||||
await mutate({ mutation: login, variables: { email: creation.email, password: 'Aa12345_' } })
|
||||
|
||||
const user = await findUserByEmail(creation.email) // userContact.user
|
||||
const {
|
||||
data: { createContribution: contribution },
|
||||
} = await mutate({ mutation: createContribution, variables: { ...creation } })
|
||||
|
||||
const pendingCreation = await Contribution.findOneOrFail({
|
||||
where: { userId: user.id, amount: creation.amount },
|
||||
order: { createdAt: 'DESC' },
|
||||
})
|
||||
logger.trace(
|
||||
'creationFactory... after Contribution.findOneOrFail pendingCreation=',
|
||||
pendingCreation,
|
||||
)
|
||||
if (creation.confirmed) {
|
||||
logger.trace('creationFactory... creation.confirmed=', creation.confirmed)
|
||||
await mutate({ mutation: confirmContribution, variables: { id: pendingCreation.id } })
|
||||
logger.trace('creationFactory... after confirmContribution')
|
||||
const confirmedCreation = await Contribution.findOneOrFail({ id: pendingCreation.id })
|
||||
logger.trace(
|
||||
'creationFactory... after Contribution.findOneOrFail confirmedCreation=',
|
||||
confirmedCreation,
|
||||
)
|
||||
const user = await findUserByEmail(creation.email) // userContact.user
|
||||
|
||||
await mutate({ mutation: login, variables: { email: 'peter@lustig.de', password: 'Aa12345_' } })
|
||||
await mutate({ mutation: confirmContribution, variables: { id: contribution.id } })
|
||||
const confirmedContribution = await Contribution.findOneOrFail({ id: contribution.id })
|
||||
|
||||
if (creation.moveCreationDate) {
|
||||
logger.trace('creationFactory... creation.moveCreationDate=', creation.moveCreationDate)
|
||||
const transaction = await Transaction.findOneOrFail({
|
||||
where: { userId: user.id, creationDate: new Date(creation.creationDate) },
|
||||
order: { balanceDate: 'DESC' },
|
||||
})
|
||||
logger.trace('creationFactory... after Transaction.findOneOrFail transaction=', transaction)
|
||||
|
||||
if (transaction.decay.equals(0) && transaction.creationDate) {
|
||||
confirmedCreation.contributionDate = new Date(
|
||||
confirmedContribution.contributionDate = new Date(
|
||||
nMonthsBefore(transaction.creationDate, creation.moveCreationDate),
|
||||
)
|
||||
transaction.creationDate = new Date(
|
||||
@ -64,17 +47,11 @@ export const creationFactory = async (
|
||||
transaction.balanceDate = new Date(
|
||||
nMonthsBefore(transaction.balanceDate, creation.moveCreationDate),
|
||||
)
|
||||
logger.trace('creationFactory... before transaction.save transaction=', transaction)
|
||||
await transaction.save()
|
||||
logger.trace(
|
||||
'creationFactory... before confirmedCreation.save confirmedCreation=',
|
||||
confirmedCreation,
|
||||
)
|
||||
await confirmedCreation.save()
|
||||
await confirmedContribution.save()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.trace('creationFactory... pendingCreation=', pendingCreation)
|
||||
return pendingCreation
|
||||
return contribution
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,10 +75,7 @@ const run = async () => {
|
||||
|
||||
// create GDD
|
||||
for (let i = 0; i < creations.length; i++) {
|
||||
const now = new Date().getTime() // we have to wait a little! quick fix for account sum problem of bob@baumeister.de, (see https://github.com/gradido/gradido/issues/1886)
|
||||
await creationFactory(seedClient, creations[i])
|
||||
// eslint-disable-next-line no-empty
|
||||
while (new Date().getTime() < now + 1000) {} // we have to wait a little! quick fix for account sum problem of bob@baumeister.de, (see https://github.com/gradido/gradido/issues/1886)
|
||||
}
|
||||
logger.info('##seed## seeding all creations successful...')
|
||||
|
||||
|
||||
@ -23,8 +23,8 @@ const setHeadersPlugin = {
|
||||
|
||||
const filterVariables = (variables: any) => {
|
||||
const vars = clonedeep(variables)
|
||||
if (vars.password) vars.password = '***'
|
||||
if (vars.passwordNew) vars.passwordNew = '***'
|
||||
if (vars && vars.password) vars.password = '***'
|
||||
if (vars && vars.passwordNew) vars.passwordNew = '***'
|
||||
return vars
|
||||
}
|
||||
|
||||
|
||||
4
backend/src/util/TRANSACTIONS_LOCK.ts
Normal file
4
backend/src/util/TRANSACTIONS_LOCK.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { Semaphore } from 'await-semaphore'
|
||||
|
||||
const CONCURRENT_TRANSACTIONS = 1
|
||||
export const TRANSACTIONS_LOCK = new Semaphore(CONCURRENT_TRANSACTIONS)
|
||||
@ -20,7 +20,7 @@ async function calculateBalance(
|
||||
time: Date,
|
||||
transactionLink?: dbTransactionLink | null,
|
||||
): Promise<{ balance: Decimal; decay: Decay; lastTransactionId: number } | null> {
|
||||
const lastTransaction = await Transaction.findOne({ userId }, { order: { balanceDate: 'DESC' } })
|
||||
const lastTransaction = await Transaction.findOne({ userId }, { order: { id: 'DESC' } })
|
||||
if (!lastTransaction) return null
|
||||
|
||||
const decay = calculateDecay(lastTransaction.balance, lastTransaction.balanceDate, time)
|
||||
|
||||
@ -1643,6 +1643,11 @@ asynckit@^0.4.0:
|
||||
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
|
||||
integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
|
||||
|
||||
await-semaphore@^0.1.3:
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/await-semaphore/-/await-semaphore-0.1.3.tgz#2b88018cc8c28e06167ae1cdff02504f1f9688d3"
|
||||
integrity sha512-d1W2aNSYcz/sxYO4pMGX9vq65qOTu0P800epMud+6cYYX0QcT7zyqcxec3VWzpgvdXo57UWmVbZpLMjX2m1I7Q==
|
||||
|
||||
axios@^0.21.1:
|
||||
version "0.21.4"
|
||||
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575"
|
||||
|
||||
42
database/entity/0058-add_communities_table/Community.ts
Normal file
42
database/entity/0058-add_communities_table/Community.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import {
|
||||
BaseEntity,
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm'
|
||||
|
||||
@Entity('communities')
|
||||
export class Community extends BaseEntity {
|
||||
@PrimaryGeneratedColumn('increment', { unsigned: true })
|
||||
id: number
|
||||
|
||||
@Column({ name: 'public_key', type: 'binary', length: 64, default: null, nullable: true })
|
||||
publicKey: Buffer
|
||||
|
||||
@Column({ name: 'api_version', length: 10, nullable: false })
|
||||
apiVersion: string
|
||||
|
||||
@Column({ name: 'end_point', length: 255, nullable: false })
|
||||
endPoint: string
|
||||
|
||||
@Column({ name: 'last_announced_at', type: 'datetime', nullable: false })
|
||||
lastAnnouncedAt: Date
|
||||
|
||||
@CreateDateColumn({
|
||||
name: 'created_at',
|
||||
type: 'datetime',
|
||||
default: () => 'CURRENT_TIMESTAMP(3)',
|
||||
nullable: false,
|
||||
})
|
||||
createdAt: Date
|
||||
|
||||
@UpdateDateColumn({
|
||||
name: 'updated_at',
|
||||
type: 'datetime',
|
||||
onUpdate: 'CURRENT_TIMESTAMP(3)',
|
||||
nullable: true,
|
||||
})
|
||||
updatedAt: Date | null
|
||||
}
|
||||
1
database/entity/Community.ts
Normal file
1
database/entity/Community.ts
Normal file
@ -0,0 +1 @@
|
||||
export { Community } from './0058-add_communities_table/Community'
|
||||
@ -9,6 +9,7 @@ import { UserContact } from './UserContact'
|
||||
import { Contribution } from './Contribution'
|
||||
import { EventProtocol } from './EventProtocol'
|
||||
import { ContributionMessage } from './ContributionMessage'
|
||||
import { Community } from './Community'
|
||||
|
||||
export const entities = [
|
||||
Contribution,
|
||||
@ -22,4 +23,5 @@ export const entities = [
|
||||
EventProtocol,
|
||||
ContributionMessage,
|
||||
UserContact,
|
||||
Community,
|
||||
]
|
||||
|
||||
28
database/migrations/0058-add_communities_table.ts
Normal file
28
database/migrations/0058-add_communities_table.ts
Normal file
@ -0,0 +1,28 @@
|
||||
/* MIGRATION TO CREATE THE FEDERATION COMMUNITY TABLES
|
||||
*
|
||||
* This migration creates the `community` and 'communityfederation' tables in the `apollo` database (`gradido_community`).
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn(`
|
||||
CREATE TABLE communities (
|
||||
id int unsigned NOT NULL AUTO_INCREMENT,
|
||||
public_key binary(64),
|
||||
api_version varchar(10) NOT NULL,
|
||||
end_point varchar(255) NOT NULL,
|
||||
last_announced_at datetime(3) NOT NULL,
|
||||
created_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at datetime(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY public_api_key (public_key, api_version)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
`)
|
||||
}
|
||||
|
||||
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
// write downgrade logic as parameter of queryFn
|
||||
await queryFn(`DROP TABLE communities;`)
|
||||
}
|
||||
@ -24,13 +24,13 @@ COMMUNITY_REGISTER_URL=https://stage1.gradido.net/register
|
||||
COMMUNITY_REDEEM_URL=https://stage1.gradido.net/redeem/{code}
|
||||
COMMUNITY_REDEEM_CONTRIBUTION_URL=https://stage1.gradido.net/redeem/CL-{code}
|
||||
COMMUNITY_DESCRIPTION="Gradido Development Stage1 Test Community"
|
||||
COMMUNITY_SUPPORT_MAIL=support@supportmail.com
|
||||
|
||||
# backend
|
||||
BACKEND_CONFIG_VERSION=v12.2022-11-10
|
||||
BACKEND_CONFIG_VERSION=v14.2022-12-22
|
||||
|
||||
JWT_EXPIRES_IN=10m
|
||||
GDT_API_URL=https://gdt.gradido.net
|
||||
ENV_NAME=stage1
|
||||
|
||||
TYPEORM_LOGGING_RELATIVE_PATH=../deployment/bare_metal/log/typeorm.backend.log
|
||||
|
||||
@ -63,13 +63,13 @@ EVENT_PROTOCOL_DISABLED=false
|
||||
# if you set the value of FEDERATION_DHT_TOPIC, the DHT hyperswarm will start to announce and listen
|
||||
# on an hash created from this topic
|
||||
# FEDERATION_DHT_TOPIC=GRADIDO_HUB
|
||||
# FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f
|
||||
# FEDERATION_DHT_SEED=64ebcb0e3ad547848fef4197c6e2332f
|
||||
|
||||
# database
|
||||
DATABASE_CONFIG_VERSION=v1.2022-03-18
|
||||
|
||||
# frontend
|
||||
FRONTEND_CONFIG_VERSION=v3.2022-09-16
|
||||
FRONTEND_CONFIG_VERSION=v4.2022-12-20
|
||||
|
||||
GRAPHQL_URI=https://stage1.gradido.net/graphql
|
||||
ADMIN_AUTH_URL=https://stage1.gradido.net/admin/authenticate?token={token}
|
||||
@ -85,8 +85,6 @@ META_KEYWORDS_DE="Grundeinkommen, Währung, Dankbarkeit, Schenk-Ökonomie, Natü
|
||||
META_KEYWORDS_EN="Basic Income, Currency, Gratitude, Gift Economy, Natural Economy of Life, Economy, Ecology, Potential Development, Giving and Thanking, Cycle of Life, Monetary System"
|
||||
META_AUTHOR="Bernd Hückstädt - Gradido-Akademie"
|
||||
|
||||
SUPPORT_MAIL=support@supportmail.com
|
||||
|
||||
# admin
|
||||
ADMIN_CONFIG_VERSION=v1.2022-03-18
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
CONFIG_VERSION=v3.2022-09-16
|
||||
CONFIG_VERSION=v4.2022-12-20
|
||||
|
||||
# Environment
|
||||
DEFAULT_PUBLISHER_ID=2896
|
||||
@ -12,6 +12,7 @@ COMMUNITY_NAME=Gradido Entwicklung
|
||||
COMMUNITY_URL=http://localhost/
|
||||
COMMUNITY_REGISTER_URL=http://localhost/register
|
||||
COMMUNITY_DESCRIPTION=Die lokale Entwicklungsumgebung von Gradido.
|
||||
COMMUNITY_SUPPORT_MAIL=support@supportmail.com
|
||||
|
||||
# Meta
|
||||
META_URL=http://localhost
|
||||
@ -22,6 +23,3 @@ META_DESCRIPTION_EN="Gratitude is the currency of the new age. More and more peo
|
||||
META_KEYWORDS_DE="Grundeinkommen, Währung, Dankbarkeit, Schenk-Ökonomie, Natürliche Ökonomie des Lebens, Ökonomie, Ökologie, Potenzialentfaltung, Schenken und Danken, Kreislauf des Lebens, Geldsystem"
|
||||
META_KEYWORDS_EN="Basic Income, Currency, Gratitude, Gift Economy, Natural Economy of Life, Economy, Ecology, Potential Development, Giving and Thanking, Cycle of Life, Monetary System"
|
||||
META_AUTHOR="Bernd Hückstädt - Gradido-Akademie"
|
||||
|
||||
# Support Mail
|
||||
SUPPORT_MAIL=support@supportmail.com
|
||||
@ -12,6 +12,7 @@ COMMUNITY_NAME=$COMMUNITY_NAME
|
||||
COMMUNITY_URL=$COMMUNITY_URL
|
||||
COMMUNITY_REGISTER_URL=$COMMUNITY_REGISTER_URL
|
||||
COMMUNITY_DESCRIPTION=$COMMUNITY_DESCRIPTION
|
||||
COMMUNITY_SUPPORT_MAIL=$COMMUNITY_SUPPORT_MAIL
|
||||
|
||||
# Meta
|
||||
META_URL=$META_URL
|
||||
@ -22,6 +23,3 @@ META_DESCRIPTION_EN=$META_DESCRIPTION_EN
|
||||
META_KEYWORDS_DE=$META_KEYWORDS_DE
|
||||
META_KEYWORDS_EN=$META_KEYWORDS_EN
|
||||
META_AUTHOR=$META_AUTHOR
|
||||
|
||||
# Support Mail
|
||||
SUPPORT_MAIL=$SUPPORT_MAIL
|
||||
@ -54,6 +54,8 @@ module.exports = {
|
||||
'settings.password.set',
|
||||
'settings.password.set-password.text',
|
||||
'settings.password.subtitle',
|
||||
'math.asterisk',
|
||||
'/pageTitle./',
|
||||
],
|
||||
enableFix: false,
|
||||
},
|
||||
|
||||
@ -52,6 +52,7 @@
|
||||
"vee-validate": "^3.4.5",
|
||||
"vue": "2.6.12",
|
||||
"vue-apollo": "^3.0.7",
|
||||
"vue-avatar": "^2.3.3",
|
||||
"vue-flatpickr-component": "^8.1.2",
|
||||
"vue-focus": "^2.1.0",
|
||||
"vue-i18n": "^8.22.4",
|
||||
|
||||
22
frontend/public/img/svg/Gradido_Blaetter_Mainpage.svg
Normal file
22
frontend/public/img/svg/Gradido_Blaetter_Mainpage.svg
Normal file
@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 26.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Ebene_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 673.47 722.49" style="enable-background:new 0 0 673.47 722.49;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#F2F2F2;}
|
||||
</style>
|
||||
<path class="st0" d="M651.42,228.24c-60.85,51.18-92.46,94.8-99.91,105.69c0,0.02,0,0.03,0,0.05
|
||||
c1.42,86.34-28.15,168.15-50.15,216.73c39.98-24.28,89.26-65.02,118.74-128.7l0,0C659.32,337.31,659.75,271.5,651.42,228.24z"/>
|
||||
<path class="st0" d="M646.33,207.44c-0.05-0.18-0.1-0.36-0.15-0.53c-2.99-10.12-6.9-19.95-11.68-29.36
|
||||
c-17.24,6.73-56.21,25.49-96.38,68.97c5.66,18.49,9.52,37.49,11.52,56.73C566.8,281.58,598.73,246.5,646.33,207.44z"/>
|
||||
<path class="st0" d="M298.67,20.88c-0.2-0.07-0.4-0.15-0.59-0.21c-10.31-3.64-20.85-6.59-31.56-8.81
|
||||
c-25.13,29.67-143.01,183.42-63.67,369.93c40.68,95.63,123.09,145.6,185.48,170.76c0.11,0.04,0.21,0.08,0.31,0.12
|
||||
C324.51,465.51,231.5,283.62,298.67,20.88z"/>
|
||||
<path class="st0" d="M510.68,247.67l-2.43-7.18C459.77,109.65,374.24,52.52,317.22,28.11c-71.14,281.83,47.26,466.57,106.05,536.85
|
||||
c16.02,4.94,28.92,7.91,36.87,9.52l0,0c11.18-20.87,40.87-80.69,55.88-153.12c0.01-0.04,0.02-0.09,0.03-0.13
|
||||
c0.17-0.83,0.34-1.66,0.51-2.5C527.35,364.97,529.9,304.36,510.68,247.67z"/>
|
||||
<path class="st0" d="M421.89,593.39l0.57-0.38c-52.89-15.28-143.12-52.46-204.42-132.98c-16.54,7.11-32.45,15.63-47.53,25.46
|
||||
C93.05,535.92,53.61,590.95,33.65,631.56c-0.11,0.22-0.21,0.43-0.31,0.66C212.12,676.57,341.56,639.33,421.89,593.39z"/>
|
||||
<path class="st0" d="M25.21,650.55c-4.32,10.7-7.73,21.74-10.19,33.01c32.95,14.7,159.32,62.04,304.57-0.12l0,0
|
||||
c26.69-12.2,58.63-31.05,88.89-60.51c-54.82,27.16-127.46,48.99-217.62,48.99C141.92,671.91,85.22,665.71,25.21,650.55z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@ -1,4 +1,4 @@
|
||||
import { mount, RouterLinkStub } from '@vue/test-utils'
|
||||
import { shallowMount, RouterLinkStub } from '@vue/test-utils'
|
||||
import App from './App'
|
||||
|
||||
const localVue = global.localVue
|
||||
@ -32,7 +32,7 @@ describe('App', () => {
|
||||
let wrapper
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(App, { localVue, mocks, stubs })
|
||||
return shallowMount(App, { localVue, mocks, stubs })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
@ -49,7 +49,7 @@ describe('App', () => {
|
||||
})
|
||||
|
||||
describe('route requires authorization', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
mocks.$route.meta.requiresAuth = true
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
<template>
|
||||
<div id="app" class="h-100">
|
||||
<component :is="$route.meta.requiresAuth ? 'DashboardLayout' : 'AuthLayout'" />
|
||||
<div class="goldrand position-fixed w-100 fixed-bottom zindex1000"></div>
|
||||
<div id="app">
|
||||
<div :class="$route.meta.requiresAuth ? 'appContent' : ''">
|
||||
<component :is="$route.meta.requiresAuth ? 'DashboardLayout' : 'AuthLayout'" />
|
||||
<div class="goldrand position-fixed fixed-bottom zindex1000"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -24,16 +26,30 @@ export default {
|
||||
src: url(./assets/scss/fonts/WorkSans-VariableFont_wght.ttf) format('truetype');
|
||||
}
|
||||
#app {
|
||||
min-width: 360px;
|
||||
font-size: 1rem;
|
||||
font-family: 'WorkSans', sans-serif !important;
|
||||
}
|
||||
|
||||
.appContent {
|
||||
min-width: 360px;
|
||||
max-width: 1320px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
.appBoxShadow {
|
||||
-webkit-box-shadow: 20pt 20pt 50pt 0 #3838384f;
|
||||
box-shadow: 20pt 20pt 50pt 0 #3838384f;
|
||||
}
|
||||
@media screen and (max-width: 500px) {
|
||||
#app {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 1024px) {
|
||||
#app {
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.goldrand {
|
||||
background: linear-gradient(
|
||||
@ -46,4 +62,8 @@ export default {
|
||||
);
|
||||
height: 13px;
|
||||
}
|
||||
|
||||
.text-color-gdd-yellow {
|
||||
color: rgb(197 141 56);
|
||||
}
|
||||
</style>
|
||||
|
||||
37
frontend/src/assets/News/news.json
Normal file
37
frontend/src/assets/News/news.json
Normal file
@ -0,0 +1,37 @@
|
||||
[
|
||||
{
|
||||
"locale": "de",
|
||||
"date": "01. Januar 2023",
|
||||
"text": "Gradido-Konto 2023: neues Design und dezentrale Communities",
|
||||
"url": "https://gradido.net/de/gradido-konto-2023-neues-design-und-dezentrale-communities/",
|
||||
"extra": "Oft sind es die leiseren Menschen, die still, fleißig und mit Herzblut die Grundlagen für großartige Entwicklungen schaffen. Unsere Entwickler haben in den vergangenen Monaten großartige Vorarbeiten gemacht, die im Jahr 2023 zum Tragen kommen werden."
|
||||
},
|
||||
{
|
||||
"locale": "en",
|
||||
"date": "01 January 2023",
|
||||
"text": "Gradido account 2023: new design and decentralized communities",
|
||||
"url": "https://gradido.net/en/gradido-konto-2023-neues-design-und-dezentrale-communities/",
|
||||
"extra": "It is often the quieter people who quietly, diligently and with heart and soul create the foundations for great developments. Our Developer have done great preparatory work in recent months that will come to fruition in 2023."
|
||||
},
|
||||
{
|
||||
"locale": "fr",
|
||||
"date": "01 janvier 2023",
|
||||
"text": "Compte Gradido 2023 : nouveau design et communautés décentralisées",
|
||||
"url": "https://gradido.net/fr/gradido-konto-2023-neues-design-und-dezentrale-communities/",
|
||||
"extra": "Ce sont souvent les personnes les plus discrètes qui créent silencieusement, avec application et passion, les bases de grands développements. Notre site Développeur ont effectué ces derniers mois un travail préparatoire formidable qui sera mis à profit en 2023."
|
||||
},
|
||||
{
|
||||
"locale": "es",
|
||||
"date": "01 de enero de 20233",
|
||||
"text": "Cuenta Gradido 2023: nuevo diseño y comunidades descentralizadas",
|
||||
"url": "https://gradido.net/es/gradido-konto-2023-neues-design-und-dezentrale-communities/",
|
||||
"extra": "A menudo son las personas más calladas las que, en silencio, con diligencia y con el corazón y el alma, crean los cimientos de los grandes avances. Nuestra Desarrollador han realizado un gran trabajo preparatorio en los últimos meses, que dará sus frutos en 2023."
|
||||
},
|
||||
{
|
||||
"locale": "nl",
|
||||
"date": "01 januari 2023",
|
||||
"text": "Gradidorekening 2023: nieuw ontwerp en gedecentraliseerde gemeenschappen",
|
||||
"url": "https://gradido.net/nl/gradido-konto-2023-neues-design-und-dezentrale-communities/",
|
||||
"extra": "Het zijn vaak de stillere mensen die stilletjes, ijverig en met hart en ziel de basis leggen voor grote ontwikkelingen. Onze Ontwikkelaar hebben de afgelopen maanden veel voorbereidend werk gedaan, dat in 2023 zijn vruchten zal afwerpen."
|
||||
}
|
||||
]
|
||||
@ -12,6 +12,12 @@ $gray-600: #8898aa !default; // Line footer color
|
||||
$gray-700: #525f7f !default; // Line p color
|
||||
$gray-800: #32325d !default; // Line heading color
|
||||
$gray-900: #212529 !default;
|
||||
$gradido-f5: #f5f5f5 !default;
|
||||
$gradido-248: rgb(248 248 248) !default;
|
||||
$gradido-140: rgb(140 66 5) !default;
|
||||
$gradido-205: rgb(205 86 86) !default;
|
||||
$gradido-197: rgb(197 141 56) !default;
|
||||
$gradido-4: rgb(4 112 6) !default;
|
||||
$black: #000 !default;
|
||||
$grays: () !default;
|
||||
$grays: map.merge(
|
||||
@ -24,7 +30,13 @@ $grays: map.merge(
|
||||
"600": $gray-600,
|
||||
"700": $gray-700,
|
||||
"800": $gray-800,
|
||||
"900": $gray-900
|
||||
"900": $gray-900,
|
||||
"f5": $gradido-f5,
|
||||
"248": $gradido-248,
|
||||
"140": $gradido-140,
|
||||
"205": $gradido-205,
|
||||
"197": $gradido-197,
|
||||
"4": $gradido-4
|
||||
),
|
||||
$grays
|
||||
);
|
||||
@ -57,10 +69,17 @@ $colors: map.merge(
|
||||
"gray": $gray-600,
|
||||
"light": $gray-400,
|
||||
"lighter": $gray-200,
|
||||
"gray-dark": $gray-800
|
||||
"gray-dark": $gray-800,
|
||||
"f5": $gradido-f5,
|
||||
"248": $gradido-248,
|
||||
"140": $gradido-140,
|
||||
"205": $gradido-205,
|
||||
"197": $gradido-197,
|
||||
"4": $gradido-4
|
||||
),
|
||||
$colors
|
||||
);
|
||||
$f5f5f5: $gradido-f5 !default;
|
||||
$default: #172b4d !default;
|
||||
$primary: #5e72e4 !default;
|
||||
$secondary: #f7fafc !default;
|
||||
@ -93,7 +112,13 @@ $theme-colors: map.merge(
|
||||
"white": $white,
|
||||
"neutral": $white,
|
||||
"dark": $dark,
|
||||
"darker": $darker
|
||||
"darker": $darker,
|
||||
"f5": $gradido-f5,
|
||||
"248": $gradido-248,
|
||||
"140": $gradido-140,
|
||||
"205": $gradido-205,
|
||||
"197": $gradido-197,
|
||||
"4": $gradido-4
|
||||
),
|
||||
$theme-colors
|
||||
);
|
||||
|
||||
@ -33,8 +33,11 @@ $spacers: map.merge(
|
||||
$sizes: () !default;
|
||||
$sizes: map.merge(
|
||||
(
|
||||
10: 10%,
|
||||
15: 15%,
|
||||
25: 25%,
|
||||
50: 50%,
|
||||
60: 60%,
|
||||
75: 75%,
|
||||
100: 100%
|
||||
),
|
||||
|
||||
18
frontend/src/assets/scss/gradido-template-dark.scss
Normal file
18
frontend/src/assets/scss/gradido-template-dark.scss
Normal file
@ -0,0 +1,18 @@
|
||||
$dark: #171717;
|
||||
$mode-toggle-bg: #262626;
|
||||
|
||||
#app {
|
||||
&.dark-mode {
|
||||
background-color: black;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
#app a,
|
||||
.navbar-light,
|
||||
.navbar-nav,
|
||||
.nav-link {
|
||||
&.dark-mode {
|
||||
color: #a7ffa9;
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,34 @@
|
||||
html,
|
||||
body {
|
||||
background-color: #f5f5f5;
|
||||
height: 100%;
|
||||
transition: background-color 0.5s ease, color 0.5s ease;
|
||||
}
|
||||
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.bg-gradient {
|
||||
background: rgb(4 112 6);
|
||||
background: linear-gradient(90deg, rgb(4 112 6 / 100%) 73%, rgb(197 141 56 / 100%) 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.hover-icon:hover {
|
||||
background-color: rgb(220 216 217);
|
||||
border-radius: 29px;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.word-break {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.shadow-default {
|
||||
box-shadow: rgb(0 0 0 / 14%) 0 4px 10px;
|
||||
}
|
||||
|
||||
.c-grey {
|
||||
color: #383838 !important;
|
||||
}
|
||||
@ -15,14 +37,6 @@ body {
|
||||
color: #0e79bc !important;
|
||||
}
|
||||
|
||||
.text-gradido {
|
||||
color: rgb(249 205 105 / 100%);
|
||||
}
|
||||
|
||||
.gradient-gradido {
|
||||
background-image: linear-gradient(146deg, rgb(220 167 44) 50%, rgb(197 141 56 / 100%) 100%);
|
||||
}
|
||||
|
||||
/* Navbar */
|
||||
a,
|
||||
.navbar-light,
|
||||
@ -103,11 +117,16 @@ a:hover,
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.rounded-right {
|
||||
.input-group .rounded-right {
|
||||
border-top-right-radius: 17px !important;
|
||||
border-bottom-right-radius: 17px !important;
|
||||
}
|
||||
|
||||
.alert {
|
||||
border-radius: 26px;
|
||||
box-shadow: rgb(0 0 0 / 14%) 0 24px 80px;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background-color: #d4edda;
|
||||
border-color: #c3e6cb;
|
||||
@ -144,6 +163,18 @@ a:hover,
|
||||
border-bottom-color: rgb(195 230 203 / 85%);
|
||||
}
|
||||
|
||||
.b-toast-warning .toast .toast-header {
|
||||
color: #fcfcfb;
|
||||
background-color: #c58d38 !important;
|
||||
border-bottom-color: rgb(207 130 14 / 85%);
|
||||
}
|
||||
|
||||
.b-toast-warning .toast .toast-body {
|
||||
color: #010602;
|
||||
background-color: rgb(247 248 247 / 85%);
|
||||
border-bottom-color: rgb(207 130 14 / 85%);
|
||||
}
|
||||
|
||||
// .btn-primary pim {
|
||||
.btn-primary {
|
||||
background-color: #5a7b02;
|
||||
@ -159,6 +190,14 @@ a:hover,
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.zindex-1 {
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.zindex1 {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.zindex10 {
|
||||
z-index: 10;
|
||||
}
|
||||
@ -179,6 +218,14 @@ a:hover,
|
||||
z-index: 100000;
|
||||
}
|
||||
|
||||
.opacity-1 {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.opacity-05 {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.gradido-global-color-blue {
|
||||
color: #0e79bc;
|
||||
}
|
||||
@ -187,6 +234,14 @@ a:hover,
|
||||
color: #047006;
|
||||
}
|
||||
|
||||
.gradido-global-border-color-accent {
|
||||
border-color: #047006 !important;
|
||||
}
|
||||
|
||||
.gradido-global-border-color-danger {
|
||||
border-color: rgb(140 5 5) !important;
|
||||
}
|
||||
|
||||
.gradido-global-color-gray {
|
||||
color: #858383;
|
||||
}
|
||||
@ -196,6 +251,14 @@ a:hover,
|
||||
border-radius: 25pt;
|
||||
}
|
||||
|
||||
.gradido-bg-f5 {
|
||||
background-color: #f5f5f5 !important;
|
||||
}
|
||||
|
||||
.gradido-bg-orange {
|
||||
background-color: rgb(197 141 56) !important;
|
||||
}
|
||||
|
||||
.gradido-width-300 {
|
||||
width: 300px;
|
||||
}
|
||||
@ -204,6 +267,11 @@ a:hover,
|
||||
width: 96%;
|
||||
}
|
||||
|
||||
.gradido-border-radius {
|
||||
border-radius: 26px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gradido-no-border-radius {
|
||||
border-radius: 0;
|
||||
}
|
||||
@ -215,3 +283,40 @@ a:hover,
|
||||
.gradido-font-15rem {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.list-group-item {
|
||||
background-color: rgb(255 255 255 / 0%);
|
||||
}
|
||||
|
||||
.pulse {
|
||||
box-shadow: 0 0 0 #c58d38;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 #c58d387e;
|
||||
}
|
||||
|
||||
70% {
|
||||
box-shadow: 0 0 0 10px rgb(204 169 44 / 0%);
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgb(204 169 44 / 0%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 #c58d387e;
|
||||
}
|
||||
|
||||
70% {
|
||||
box-shadow: 0 0 0 20px rgb(204 169 44 / 0%);
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgb(204 169 44 / 0%);
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,3 +52,4 @@
|
||||
// Bootstrap-vue (2.21.1) scss
|
||||
@import "~bootstrap-vue/src/index";
|
||||
@import "gradido-template";
|
||||
@import "gradido-template-dark";
|
||||
|
||||
19
frontend/src/components/Breadcrumb/breadcrumb.vue
Normal file
19
frontend/src/components/Breadcrumb/breadcrumb.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<div class="breadcrumb bg-transparent">
|
||||
<h1>{{ pageTitle }}</h1>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import CONFIG from '@/config'
|
||||
|
||||
export default {
|
||||
name: 'Breadcrumb',
|
||||
computed: {
|
||||
pageTitle() {
|
||||
const options = { name: this.$store.state.firstName, community: CONFIG.COMMUNITY_NAME }
|
||||
// eslint-disable-next-line @intlify/vue-i18n/no-dynamic-keys
|
||||
return this.$t(`pageTitle.${this.$route.meta.pageTitle}`, options)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -1,19 +1,23 @@
|
||||
<template>
|
||||
<div class="clipboard-copy">
|
||||
<b-input-group v-if="canCopyLink" size="lg" class="mb-3" prepend="Link">
|
||||
<b-form-input :value="link" type="text" readonly></b-form-input>
|
||||
<b-input-group-append>
|
||||
<b-button size="sm" text="Button" variant="primary" @click="copyLinkWithText">
|
||||
{{ $t('gdd_per_link.copy-link-with-text') }}
|
||||
</b-button>
|
||||
<b-button size="sm" text="Button" variant="primary" @click="copyLink">
|
||||
{{ $t('gdd_per_link.copy-link') }}
|
||||
</b-button>
|
||||
<b-button variant="primary" class="text-light" @click="$emit('show-qr-code-button')">
|
||||
<b-img src="img/svg/qr-code.svg" width="19" class="svg"></b-img>
|
||||
</b-button>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
<div v-if="canCopyLink" size="lg" class="mb-5">
|
||||
<div class="d-flex">
|
||||
<div>
|
||||
<label>{{ $t('gdd_per_link.copy-link') }}</label>
|
||||
<div class="pointer text-center bg-secondary gradido-border-radius p-4" @click="copyLink">
|
||||
{{ link }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-5">
|
||||
<label>{{ $t('gdd_per_link.copy-link-with-text') }}</label>
|
||||
<div>
|
||||
<b-button @click="copyLinkWithText" class="p-4">
|
||||
<b-icon icon="link45deg"></b-icon>
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div class="alert-danger p-3">{{ $t('gdd_per_link.not-copied') }}</div>
|
||||
<div class="alert-muted h3 p-3">{{ link }}</div>
|
||||
|
||||
@ -32,8 +32,8 @@ describe('ContentFooter', () => {
|
||||
expect(wrapper.find('div.copyright').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
it('renders the copyright year', () => {
|
||||
expect(mocks.$t).toBeCalledWith('footer.copyright.year', { year: 2022 })
|
||||
it('renders the current year as copyright year', () => {
|
||||
expect(mocks.$t).toBeCalledWith('footer.copyright.year', { year: new Date().getFullYear() })
|
||||
})
|
||||
|
||||
it('renders a link to Gradido-Akademie', () => {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<div class="contribution-messages-formular">
|
||||
<small class="pl-2 pt-3">{{ $t('form.reply') }}</small>
|
||||
<div>
|
||||
<b-form @submit.prevent="onSubmit" @reset="onReset">
|
||||
<b-form-textarea
|
||||
@ -8,12 +9,12 @@
|
||||
:placeholder="$t('form.memo')"
|
||||
rows="3"
|
||||
></b-form-textarea>
|
||||
<b-row class="mt-4 mb-6">
|
||||
<b-row class="mt-4 mb-4">
|
||||
<b-col>
|
||||
<b-button type="reset" variant="danger">{{ $t('form.cancel') }}</b-button>
|
||||
<b-button type="reset" variant="secondary">{{ $t('form.cancel') }}</b-button>
|
||||
</b-col>
|
||||
<b-col class="text-right">
|
||||
<b-button type="submit" variant="primary" :disabled="disabled">
|
||||
<b-button type="submit" variant="gradido" :disabled="disabled">
|
||||
{{ $t('form.reply') }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
|
||||
@ -1,24 +1,21 @@
|
||||
<template>
|
||||
<div class="contribution-messages-list">
|
||||
<b-container>
|
||||
<div v-for="message in messages" v-bind:key="message.id">
|
||||
<div>
|
||||
<div v-for="message in messages" v-bind:key="message.id" class="mt-3">
|
||||
<contribution-messages-list-item :message="message" />
|
||||
</div>
|
||||
</b-container>
|
||||
<b-container>
|
||||
</div>
|
||||
<div>
|
||||
<contribution-messages-formular
|
||||
v-if="['PENDING', 'IN_PROGRESS'].includes(state)"
|
||||
:contributionId="contributionId"
|
||||
v-on="$listeners"
|
||||
@update-state="updateState"
|
||||
/>
|
||||
</b-container>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-b-toggle="'collapse' + String(contributionId)"
|
||||
class="text-center pointer h2 clearboth pt-1"
|
||||
>
|
||||
<b-button variant="outline-primary" block class="mt-4">
|
||||
<div v-b-toggle="'collapse' + String(contributionId)" class="text-center pointer clearboth">
|
||||
<b-button variant="outline-primary" block class="mb-3">
|
||||
<b-icon icon="arrow-up-short"></b-icon>
|
||||
{{ $t('form.close') }}
|
||||
</b-button>
|
||||
@ -57,9 +54,6 @@ export default {
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.temp-message {
|
||||
margin-top: 50px;
|
||||
}
|
||||
.clearboth {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
@ -96,30 +96,26 @@ describe('ContributionMessagesListItem', () => {
|
||||
wrapper = ItemWrapper()
|
||||
})
|
||||
|
||||
it('has a DIV .is-moderator.text-left', () => {
|
||||
expect(wrapper.find('div.is-moderator.text-left').exists()).toBe(true)
|
||||
it('has a DIV .is-moderator', () => {
|
||||
expect(wrapper.find('div.is-moderator').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has the complete user name', () => {
|
||||
expect(wrapper.find('div.is-moderator.text-left > span:nth-child(2)').text()).toBe(
|
||||
'Bibi Bloxberg',
|
||||
)
|
||||
expect(wrapper.find('span[data-test="username"]').text()).toBe('Bibi Bloxberg')
|
||||
})
|
||||
|
||||
it('has the message creation date', () => {
|
||||
expect(wrapper.find('div.is-moderator.text-left > span:nth-child(3)').text()).toMatch(
|
||||
expect(wrapper.find('div[data-test="date"]').text()).toMatch(
|
||||
'Mon Aug 29 2022 12:25:34 GMT+0000',
|
||||
)
|
||||
})
|
||||
|
||||
it('has the moderator label', () => {
|
||||
expect(wrapper.find('div.is-moderator.text-left > small:nth-child(4)').text()).toBe(
|
||||
'community.moderator',
|
||||
)
|
||||
expect(wrapper.find('span[data-test="moderator"]').text()).toBe('community.moderator')
|
||||
})
|
||||
|
||||
it('has the message', () => {
|
||||
expect(wrapper.find('div.is-moderator.text-left > div:nth-child(5)').text()).toBe(
|
||||
expect(wrapper.find('div[data-test="message"]').text()).toBe(
|
||||
'Asda sdad ad asdasd, das Ass das Das.',
|
||||
)
|
||||
})
|
||||
@ -154,26 +150,22 @@ describe('ContributionMessagesListItem', () => {
|
||||
wrapper = ModeratorItemWrapper()
|
||||
})
|
||||
|
||||
it('has a DIV .is-not-moderator.text-right', () => {
|
||||
expect(wrapper.find('div.is-not-moderator.text-right').exists()).toBe(true)
|
||||
it('has a DIV .is-not-moderator', () => {
|
||||
expect(wrapper.find('div.is-not-moderator').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has the complete user name', () => {
|
||||
expect(wrapper.find('div.is-not-moderator.text-right > span:nth-child(2)').text()).toBe(
|
||||
'Peter Lustig',
|
||||
)
|
||||
expect(wrapper.find('div[data-test="username"]').text()).toBe('Peter Lustig')
|
||||
})
|
||||
|
||||
it('has the message creation date', () => {
|
||||
expect(wrapper.find('div.is-not-moderator.text-right > span:nth-child(3)').text()).toMatch(
|
||||
expect(wrapper.find('div[data-test="date"]').text()).toMatch(
|
||||
'Mon Aug 29 2022 12:23:27 GMT+0000',
|
||||
)
|
||||
})
|
||||
|
||||
it('has the message', () => {
|
||||
expect(wrapper.find('div.is-not-moderator.text-right > div:nth-child(4)').text()).toBe(
|
||||
'Lorem ipsum?',
|
||||
)
|
||||
expect(wrapper.find('div[data-test="message"]').text()).toBe('Lorem ipsum?')
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -207,7 +199,7 @@ describe('ContributionMessagesListItem', () => {
|
||||
beforeEach(() => {
|
||||
propsData.message.message = 'https://gradido.net/de/'
|
||||
wrapper = ModeratorItemWrapper()
|
||||
messageField = wrapper.find('div.is-not-moderator.text-right > div:nth-child(4)')
|
||||
messageField = wrapper.find('div[data-test="message"]')
|
||||
})
|
||||
|
||||
it('contains the link as text', () => {
|
||||
@ -224,7 +216,7 @@ describe('ContributionMessagesListItem', () => {
|
||||
propsData.message.message = `Here you find all you need to know about Gradido: https://gradido.net/de/
|
||||
and here is the link to the repository: https://github.com/gradido/gradido`
|
||||
wrapper = ModeratorItemWrapper()
|
||||
messageField = wrapper.find('div.is-not-moderator.text-right > div:nth-child(4)')
|
||||
messageField = wrapper.find('div[data-test="message"]')
|
||||
})
|
||||
|
||||
it('contains the whole text', () => {
|
||||
@ -275,7 +267,7 @@ This message also contains a link: https://gradido.net/de/
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
wrapper = itemWrapper()
|
||||
messageField = wrapper.find('div.is-not-moderator.text-right > div:nth-child(4)')
|
||||
messageField = wrapper.find('div[data-test="message"]')
|
||||
})
|
||||
|
||||
it('renders the date', () => {
|
||||
|
||||
@ -1,27 +1,46 @@
|
||||
<template>
|
||||
<div class="contribution-messages-list-item">
|
||||
<div v-if="isNotModerator" class="is-not-moderator text-right">
|
||||
<b-avatar variant="info"></b-avatar>
|
||||
<span class="ml-2 mr-2">{{ message.userFirstName }} {{ message.userLastName }}</span>
|
||||
<span class="ml-2">{{ $d(new Date(message.createdAt), 'short') }}</span>
|
||||
<parse-message v-bind="message"></parse-message>
|
||||
<div v-if="isNotModerator" class="text-right pr-4 pr-lg-0 is-not-moderator">
|
||||
<b-row class="mb-3">
|
||||
<b-col cols="10">
|
||||
<div class="font-weight-bold" data-test="username">{{ storeName.username }}</div>
|
||||
<div class="small" data-test="date">{{ $d(new Date(message.createdAt), 'short') }}</div>
|
||||
<parse-message v-bind="message" data-test="message"></parse-message>
|
||||
</b-col>
|
||||
<b-col cols="2">
|
||||
<avatar :username="storeName.username" :initials="storeName.initials"></avatar>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
<div v-else class="is-moderator text-left">
|
||||
<b-avatar square variant="warning"></b-avatar>
|
||||
<span class="ml-2 mr-2">{{ message.userFirstName }} {{ message.userLastName }}</span>
|
||||
<span class="ml-2">{{ $d(new Date(message.createdAt), 'short') }}</span>
|
||||
<small class="ml-4 text-success">{{ $t('community.moderator') }}</small>
|
||||
<parse-message v-bind="message"></parse-message>
|
||||
<div v-else>
|
||||
<b-row class="mb-3 bg-f5 p-2 is-moderator">
|
||||
<b-col cols="2">
|
||||
<avatar :username="moderationName.username" :initials="moderationName.initials"></avatar>
|
||||
</b-col>
|
||||
<b-col cols="10">
|
||||
<div class="font-weight-bold">
|
||||
<span data-test="username">{{ moderationName.username }}</span>
|
||||
<span class="ml-2 text-success small" data-test="moderator">
|
||||
{{ $t('community.moderator') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="small" data-test="date">{{ $d(new Date(message.createdAt), 'short') }}</div>
|
||||
<parse-message v-bind="message" data-test="message"></parse-message>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Avatar from 'vue-avatar'
|
||||
import ParseMessage from '@/components/ContributionMessages/ParseMessage.vue'
|
||||
|
||||
export default {
|
||||
name: 'ContributionMessagesListItem',
|
||||
components: {
|
||||
Avatar,
|
||||
ParseMessage,
|
||||
},
|
||||
props: {
|
||||
@ -30,32 +49,22 @@ export default {
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
storeName: `${this.$store.state.firstName} ${this.$store.state.lastName}`,
|
||||
moderationName: `${this.message.userFirstName} ${this.message.userLastName}`,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isNotModerator() {
|
||||
return this.storeName === this.moderationName
|
||||
return this.storeName.username === this.moderationName.username
|
||||
},
|
||||
storeName() {
|
||||
return {
|
||||
username: `${this.$store.state.firstName} ${this.$store.state.lastName}`,
|
||||
initials: `${this.$store.state.firstName[0]}${this.$store.state.lastName[0]}`,
|
||||
}
|
||||
},
|
||||
moderationName() {
|
||||
return {
|
||||
username: `${this.message.userFirstName} ${this.message.userLastName}`,
|
||||
initials: `${this.message.userFirstName[0]}${this.message.userLastName[0]}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
.is-not-moderator {
|
||||
float: right;
|
||||
/* background-color: rgb(261, 204, 221); */
|
||||
width: 75%;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
clear: both;
|
||||
}
|
||||
.is-moderator {
|
||||
clear: both;
|
||||
/* background-color: rgb(255, 255, 128); */
|
||||
width: 75%;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="mt-2">
|
||||
<div class="mt-1">
|
||||
<span v-for="({ type, text }, index) in parsedMessage" :key="index">
|
||||
<b-link v-if="type === 'link'" :href="text" target="_blank">{{ text }}</b-link>
|
||||
<span v-else-if="type === 'date'">
|
||||
|
||||
@ -13,6 +13,10 @@ describe('ContributionForm', () => {
|
||||
memo: '',
|
||||
amount: '',
|
||||
},
|
||||
isThisMonth: true,
|
||||
minimalDate: new Date(),
|
||||
maxGddLastMonth: 1000,
|
||||
maxGddThisMonth: 1000,
|
||||
}
|
||||
|
||||
const mocks = {
|
||||
@ -81,7 +85,7 @@ describe('ContributionForm', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('month before', () => {
|
||||
describe.skip('month before', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper
|
||||
.findComponent({ name: 'BFormDatepicker' })
|
||||
@ -96,7 +100,7 @@ describe('ContributionForm', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('date in middle of year', () => {
|
||||
describe.skip('date in middle of year', () => {
|
||||
describe('same month', () => {
|
||||
beforeEach(async () => {
|
||||
// jest.useFakeTimers('modern')
|
||||
@ -149,7 +153,7 @@ describe('ContributionForm', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('date in january', () => {
|
||||
describe.skip('date in january', () => {
|
||||
describe('same month', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.setData({
|
||||
@ -199,7 +203,7 @@ describe('ContributionForm', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('date with the 31st day of the month', () => {
|
||||
describe.skip('date with the 31st day of the month', () => {
|
||||
describe('same month', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.setData({
|
||||
@ -222,7 +226,7 @@ describe('ContributionForm', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('date with the 28th day of the month', () => {
|
||||
describe.skip('date with the 28th day of the month', () => {
|
||||
describe('same month', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.setData({
|
||||
@ -245,7 +249,7 @@ describe('ContributionForm', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('date with 29.02.2024 leap year', () => {
|
||||
describe.skip('date with 29.02.2024 leap year', () => {
|
||||
describe('same month', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.setData({
|
||||
@ -470,7 +474,7 @@ describe('ContributionForm', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('on trigger submit', () => {
|
||||
describe.skip('on trigger submit', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('form').trigger('submit')
|
||||
})
|
||||
|
||||
@ -1,19 +1,11 @@
|
||||
<template>
|
||||
<div class="container contribution-form">
|
||||
<div class="my-3">
|
||||
<h3>{{ $t('contribution.formText.yourContribution') }}</h3>
|
||||
{{ $t('contribution.formText.bringYourTalentsTo') }}
|
||||
<ul class="my-3">
|
||||
<li v-html="textForMonth(new Date(minimalDate), maxGddLastMonth)"></li>
|
||||
<li v-html="textForMonth(new Date(), maxGddThisMonth)"></li>
|
||||
</ul>
|
||||
|
||||
<div class="my-3">
|
||||
<b>{{ $t('contribution.formText.describeYourCommunity') }}</b>
|
||||
</div>
|
||||
</div>
|
||||
<b-form ref="form" @submit.prevent="submit" class="border p-3">
|
||||
<label>{{ $t('contribution.selectDate') }} {{ $t('math.asterisk') }}</label>
|
||||
<div class="contribution-form">
|
||||
<b-form
|
||||
ref="form"
|
||||
@submit.prevent="submit"
|
||||
class="border p-3 bg-white appBoxShadow gradido-border-radius"
|
||||
>
|
||||
<label>{{ $t('contribution.selectDate') }}</label>
|
||||
<b-form-datepicker
|
||||
id="contribution-date"
|
||||
v-model="form.date"
|
||||
@ -21,7 +13,7 @@
|
||||
:locale="$i18n.locale"
|
||||
:max="maximalDate"
|
||||
:min="minimalDate"
|
||||
class="mb-4"
|
||||
class="mb-4 bg-248"
|
||||
reset-value=""
|
||||
:label-no-date-selected="$t('contribution.noDateSelected')"
|
||||
required
|
||||
@ -30,87 +22,87 @@
|
||||
<template #nav-prev-year><span></span></template>
|
||||
<template #nav-next-year><span></span></template>
|
||||
</b-form-datepicker>
|
||||
<validation-provider
|
||||
:rules="{
|
||||
min: minlength,
|
||||
max: maxlength,
|
||||
}"
|
||||
:name="$t('form.message')"
|
||||
v-slot="{ errors }"
|
||||
>
|
||||
<label class="mt-3">{{ $t('contribution.activity') }} {{ $t('math.asterisk') }}</label>
|
||||
<b-form-textarea
|
||||
<div v-if="validMaxGDD > 0">
|
||||
<input-textarea
|
||||
id="contribution-memo"
|
||||
v-model="form.memo"
|
||||
rows="3"
|
||||
:name="$t('form.message')"
|
||||
:label="$t('contribution.activity')"
|
||||
:placeholder="$t('contribution.yourActivity')"
|
||||
required
|
||||
></b-form-textarea>
|
||||
<b-col v-if="errors">
|
||||
<span v-for="error in errors" class="errors" :key="error">{{ error }}</span>
|
||||
</b-col>
|
||||
</validation-provider>
|
||||
<label class="mt-3">{{ $t('form.amount') }} {{ $t('math.asterisk') }}</label>
|
||||
<b-input-group size="lg" prepend="GDD">
|
||||
<b-form-input
|
||||
:rules="{ required: true, min: 5, max: 255 }"
|
||||
/>
|
||||
<input-hour
|
||||
v-model="form.hours"
|
||||
:name="$t('form.hours')"
|
||||
:label="$t('form.hours')"
|
||||
placeholder="0.5"
|
||||
:rules="{
|
||||
required: true,
|
||||
min: 0.5,
|
||||
max: validMaxTime,
|
||||
gddCreationTime: [0.5, validMaxTime],
|
||||
}"
|
||||
:validMaxTime="validMaxTime"
|
||||
@updateAmount="updateAmount"
|
||||
></input-hour>
|
||||
<input-amount
|
||||
id="contribution-amount"
|
||||
v-model="form.amount"
|
||||
type="text"
|
||||
:formatter="numberFormat"
|
||||
></b-form-input>
|
||||
</b-input-group>
|
||||
<div
|
||||
v-if="isThisMonth && parseInt(form.amount) > parseInt(maxGddThisMonth)"
|
||||
class="text-danger text-right"
|
||||
>
|
||||
{{ $t('contribution.formText.maxGDDforMonth', { amount: maxGddThisMonth }) }}
|
||||
:name="$t('form.amount')"
|
||||
:label="$t('form.amount')"
|
||||
placeholder="20"
|
||||
:rules="{ required: true, gddSendAmount: [20, validMaxGDD] }"
|
||||
typ="ContributionForm"
|
||||
></input-amount>
|
||||
</div>
|
||||
<div
|
||||
v-if="!isThisMonth && parseInt(form.amount) > parseInt(maxGddLastMonth)"
|
||||
class="text-danger text-right"
|
||||
>
|
||||
{{ $t('contribution.formText.maxGDDforMonth', { amount: maxGddLastMonth }) }}
|
||||
</div>
|
||||
<b-row class="mt-3">
|
||||
<div v-else class="mb-5">{{ $t('contribution.exhausted') }}</div>
|
||||
<b-row class="mt-5">
|
||||
<b-col>
|
||||
<b-button type="reset" variant="secondary" @click="reset" data-test="button-cancel">
|
||||
{{ $t('form.cancel') }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
<b-col class="text-right">
|
||||
<b-button type="submit" variant="primary" :disabled="disabled" data-test="button-submit">
|
||||
<b-button type="submit" variant="gradido" :disabled="disabled" data-test="button-submit">
|
||||
{{ form.id ? $t('form.change') : $t('contribution.submit') }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-form>
|
||||
<p class="p-2">{{ $t('math.asterisk') }} {{ $t('form.mandatoryField') }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
const PATTERN_NON_DIGIT = /\D/g
|
||||
import InputHour from '@/components/Inputs/InputHour.vue'
|
||||
import InputAmount from '@/components/Inputs/InputAmount.vue'
|
||||
import InputTextarea from '@/components/Inputs/InputTextarea.vue'
|
||||
|
||||
export default {
|
||||
name: 'ContributionForm',
|
||||
components: {
|
||||
InputHour,
|
||||
InputAmount,
|
||||
InputTextarea,
|
||||
},
|
||||
props: {
|
||||
value: { type: Object, required: true },
|
||||
updateAmount: { type: String, required: false },
|
||||
isThisMonth: { type: Boolean, required: true },
|
||||
minimalDate: { type: Date, required: true },
|
||||
maxGddLastMonth: { type: Number, required: true },
|
||||
maxGddThisMonth: { type: Number, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
minlength: 5,
|
||||
maxlength: 255,
|
||||
maximalDate: new Date(),
|
||||
form: this.value, // includes 'id'
|
||||
form: this.value, // includes 'id' and time
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
numberFormat(value) {
|
||||
return value.replace(PATTERN_NON_DIGIT, '')
|
||||
updateAmount(amount) {
|
||||
this.form.amount = (amount * 20).toFixed(2).toString()
|
||||
},
|
||||
submit() {
|
||||
this.form.amount = this.form.amount.replace(PATTERN_NON_DIGIT, '')
|
||||
// spreading is needed for testing
|
||||
this.$emit(this.form.id ? 'update-contribution' : 'set-contribution', { ...this.form })
|
||||
this.reset()
|
||||
},
|
||||
@ -119,50 +111,33 @@ export default {
|
||||
this.form.id = null
|
||||
this.form.date = ''
|
||||
this.form.memo = ''
|
||||
this.form.hours = 0.0
|
||||
this.form.amount = ''
|
||||
},
|
||||
textForMonth(date, availableAmount) {
|
||||
const obj = {
|
||||
monthAndYear: this.$d(date, 'monthAndYear'),
|
||||
creation: availableAmount,
|
||||
}
|
||||
return this.$t('contribution.formText.openAmountForMonth', obj)
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
minimalDate() {
|
||||
const date = new Date(this.maximalDate)
|
||||
return new Date(date.setMonth(date.getMonth() - 1, 1))
|
||||
},
|
||||
disabled() {
|
||||
return (
|
||||
this.form.date === '' ||
|
||||
this.form.memo.length < this.minlength ||
|
||||
this.form.memo.length > this.maxlength ||
|
||||
this.form.amount === '' ||
|
||||
parseInt(this.form.amount) <= 0 ||
|
||||
parseInt(this.form.amount) > 1000 ||
|
||||
(this.isThisMonth && parseInt(this.form.amount) > parseInt(this.maxGddThisMonth)) ||
|
||||
(!this.isThisMonth && parseInt(this.form.amount) > parseInt(this.maxGddLastMonth))
|
||||
)
|
||||
},
|
||||
isThisMonth() {
|
||||
const formDate = new Date(this.form.date)
|
||||
return (
|
||||
formDate.getFullYear() === this.maximalDate.getFullYear() &&
|
||||
formDate.getMonth() === this.maximalDate.getMonth()
|
||||
)
|
||||
validMaxGDD() {
|
||||
return Number(this.isThisMonth ? this.maxGddThisMonth : this.maxGddLastMonth)
|
||||
},
|
||||
maxGddLastMonth() {
|
||||
// when existing contribution is edited, the amount is added back on top of the amount
|
||||
return this.form.id && !this.isThisMonth
|
||||
? parseInt(this.$store.state.creation[1]) + parseInt(this.updateAmount)
|
||||
: this.$store.state.creation[1]
|
||||
validMaxTime() {
|
||||
return Number(this.validMaxGDD / 20)
|
||||
},
|
||||
maxGddThisMonth() {
|
||||
// when existing contribution is edited, the amount is added back on top of the amount
|
||||
return this.form.id && this.isThisMonth
|
||||
? parseInt(this.$store.state.creation[2]) + parseInt(this.updateAmount)
|
||||
: this.$store.state.creation[2]
|
||||
},
|
||||
watch: {
|
||||
value() {
|
||||
return (this.form = this.value)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<div class="contribution-list container">
|
||||
<div class="list-group" v-for="item in items" :key="item.id">
|
||||
<div class="contribution-list">
|
||||
<div class="mb-3" v-for="item in items" :key="item.id + 'a'">
|
||||
<contribution-list-item
|
||||
v-if="item.state === 'IN_PROGRESS'"
|
||||
v-bind="item"
|
||||
@closeAllOpenCollapse="$emit('closeAllOpenCollapse')"
|
||||
:contributionId="item.id"
|
||||
@ -11,6 +12,18 @@
|
||||
@update-state="updateState"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-3" v-for="item2 in items" :key="item2.id">
|
||||
<contribution-list-item
|
||||
v-if="item2.state !== 'IN_PROGRESS'"
|
||||
v-bind="item2"
|
||||
@closeAllOpenCollapse="$emit('closeAllOpenCollapse')"
|
||||
:contributionId="item2.id"
|
||||
:allContribution="allContribution"
|
||||
@update-contribution-form="updateContributionForm"
|
||||
@delete-contribution="deleteContribution"
|
||||
@update-state="updateState"
|
||||
/>
|
||||
</div>
|
||||
<b-pagination
|
||||
v-if="isPaginationVisible"
|
||||
class="mt-3"
|
||||
|
||||
@ -74,7 +74,7 @@ describe('ContributionListItem', () => {
|
||||
|
||||
it('is warning at when state is IN_PROGRESS', async () => {
|
||||
await wrapper.setProps({ state: 'IN_PROGRESS' })
|
||||
expect(wrapper.vm.variant).toBe('warning')
|
||||
expect(wrapper.vm.variant).toBe('f5')
|
||||
})
|
||||
})
|
||||
|
||||
@ -89,7 +89,7 @@ describe('ContributionListItem', () => {
|
||||
|
||||
describe('edit contribution', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.findAll('div.pointer').at(0).trigger('click')
|
||||
wrapper.find('div.test-edit-contribution').trigger('click')
|
||||
})
|
||||
|
||||
it('emits update contribution form', () => {
|
||||
@ -110,7 +110,7 @@ describe('ContributionListItem', () => {
|
||||
beforeEach(() => {
|
||||
spy = jest.spyOn(wrapper.vm.$bvModal, 'msgBoxConfirm')
|
||||
spy.mockImplementation(() => Promise.resolve(true))
|
||||
wrapper.findAll('div.pointer').at(1).trigger('click')
|
||||
wrapper.find('div.test-delete-contribution').trigger('click')
|
||||
})
|
||||
|
||||
it('opens the modal', () => {
|
||||
|
||||
@ -1,97 +1,107 @@
|
||||
<template>
|
||||
<div class="contribution-list-item">
|
||||
<slot>
|
||||
<div class="border p-3 w-100 mb-1" :class="`border-${variant}`">
|
||||
<div>
|
||||
<div class="d-inline-flex">
|
||||
<div class="mr-2">
|
||||
<b-icon
|
||||
v-if="state === 'IN_PROGRESS'"
|
||||
icon="question-square"
|
||||
font-scale="2"
|
||||
variant="warning"
|
||||
></b-icon>
|
||||
<b-icon v-else :icon="icon" :variant="variant" class="h2"></b-icon>
|
||||
</div>
|
||||
<div v-if="firstName" class="mr-3">{{ firstName }} {{ lastName }}</div>
|
||||
<div class="mr-2" :class="state !== 'DELETED' ? 'font-weight-bold' : ''">
|
||||
{{ amount | GDD }}
|
||||
</div>
|
||||
{{ $t('math.minus') }}
|
||||
<div class="mx-2">{{ $d(new Date(date), 'short') }}</div>
|
||||
<div>
|
||||
<div
|
||||
class="contribution-list-item bg-white appBoxShadow gradido-border-radius pt-3 px-3"
|
||||
:class="state === 'IN_PROGRESS' ? 'pulse border border-205' : ''"
|
||||
>
|
||||
<b-row>
|
||||
<b-col cols="3" lg="2" md="2">
|
||||
<avatar
|
||||
v-if="firstName"
|
||||
:username="username.username"
|
||||
:initials="username.initials"
|
||||
color="#fff"
|
||||
class="font-weight-bold"
|
||||
></avatar>
|
||||
<b-avatar v-else :icon="icon" :variant="variant" size="3em"></b-avatar>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<div v-if="firstName" class="mr-3 font-weight-bold">{{ firstName }} {{ lastName }}</div>
|
||||
<div class="small">
|
||||
{{ $d(new Date(contributionDate), 'monthAndYear') }}
|
||||
</div>
|
||||
<div class="mr-2">
|
||||
<span>{{ $t('contribution.date') }}</span>
|
||||
<span>
|
||||
{{ $d(new Date(contributionDate), 'monthAndYear') }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mr-2">{{ memo }}</div>
|
||||
<div class="d-flex flex-row-reverse">
|
||||
<div
|
||||
v-if="!['CONFIRMED', 'DELETED'].includes(state) && !allContribution"
|
||||
class="pointer ml-5"
|
||||
@click="
|
||||
$emit('closeAllOpenCollapse'),
|
||||
$emit('update-contribution-form', {
|
||||
id: id,
|
||||
contributionDate: contributionDate,
|
||||
memo: memo,
|
||||
amount: amount,
|
||||
})
|
||||
"
|
||||
>
|
||||
<b-icon icon="pencil" class="h2"></b-icon>
|
||||
</div>
|
||||
<div
|
||||
v-if="!['CONFIRMED', 'DELETED'].includes(state) && !allContribution"
|
||||
class="pointer"
|
||||
@click="deleteContribution({ id })"
|
||||
>
|
||||
<b-icon icon="trash" class="h2"></b-icon>
|
||||
</div>
|
||||
<div v-if="messagesCount > 0" class="pointer">
|
||||
<b-icon
|
||||
v-b-toggle="collapsId"
|
||||
icon="chat-dots"
|
||||
class="h2 mr-5"
|
||||
@click="getListContributionMessages"
|
||||
></b-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="messagesCount > 0">
|
||||
<b-button
|
||||
v-if="state === 'IN_PROGRESS'"
|
||||
v-b-toggle="collapsId"
|
||||
variant="warning"
|
||||
@click="getListContributionMessages"
|
||||
>
|
||||
<div class="mt-3 font-weight-bold">{{ $t('contributionText') }}</div>
|
||||
<div class="mb-3">{{ memo }}</div>
|
||||
<div v-if="state === 'IN_PROGRESS'" class="text-205">
|
||||
{{ $t('contribution.alert.answerQuestion') }}
|
||||
</b-button>
|
||||
<b-collapse :id="collapsId" class="mt-2">
|
||||
<b-card>
|
||||
<contribution-messages-list
|
||||
:messages="messages_get"
|
||||
:state="state"
|
||||
:contributionId="contributionId"
|
||||
@get-list-contribution-messages="getListContributionMessages"
|
||||
@update-state="updateState"
|
||||
/>
|
||||
</b-card>
|
||||
</b-collapse>
|
||||
</div>
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
</b-col>
|
||||
<b-col cols="12" lg="3" offset="3" offset-md="0" offset-lg="0">
|
||||
<div class="small">
|
||||
{{ $t('creation') }} {{ $t('(') }}{{ amount / 20 }} {{ $t('h') }}{{ $t(')') }}
|
||||
</div>
|
||||
<div class="font-weight-bold">{{ amount | GDD }}</div>
|
||||
</b-col>
|
||||
<b-col cols="12" md="1" lg="1" class="text-right align-items-center">
|
||||
<div v-if="messagesCount > 0" @click="visible = !visible">
|
||||
<collapse-icon class="text-right" :visible="visible" />
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row
|
||||
v-if="(!['CONFIRMED', 'DELETED'].includes(state) && !allContribution) || messagesCount > 0"
|
||||
class="p-2"
|
||||
>
|
||||
<b-col cols="3" class="mr-auto text-center">
|
||||
<div
|
||||
v-if="!['CONFIRMED', 'DELETED'].includes(state) && !allContribution"
|
||||
class="test-delete-contribution pointer mr-3"
|
||||
@click="deleteContribution({ id })"
|
||||
>
|
||||
<b-icon icon="trash"></b-icon>
|
||||
|
||||
<div>{{ $t('delete') }}</div>
|
||||
</div>
|
||||
</b-col>
|
||||
<b-col cols="3" class="text-center">
|
||||
<div
|
||||
v-if="!['CONFIRMED', 'DELETED'].includes(state) && !allContribution"
|
||||
class="test-edit-contribution pointer mr-3"
|
||||
@click="
|
||||
$emit('update-contribution-form', {
|
||||
id: id,
|
||||
contributionDate: contributionDate,
|
||||
memo: memo,
|
||||
amount: amount,
|
||||
})
|
||||
"
|
||||
>
|
||||
<b-icon icon="pencil"></b-icon>
|
||||
<div>{{ $t('edit') }}</div>
|
||||
</div>
|
||||
</b-col>
|
||||
|
||||
<b-col cols="6" class="text-center">
|
||||
<div v-if="messagesCount > 0" class="pointer" @click="visible = !visible">
|
||||
<b-icon icon="chat-dots"></b-icon>
|
||||
<div>{{ $t('moderatorChat') }}</div>
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<div v-else class="pb-3"></div>
|
||||
<b-collapse :id="collapsId" class="mt-2" v-model="visible">
|
||||
<contribution-messages-list
|
||||
:messages="messages_get"
|
||||
:state="state"
|
||||
:contributionId="contributionId"
|
||||
@get-list-contribution-messages="getListContributionMessages"
|
||||
@update-state="updateState"
|
||||
/>
|
||||
</b-collapse>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Avatar from 'vue-avatar'
|
||||
import CollapseIcon from '../TransactionRows/CollapseIcon'
|
||||
import ContributionMessagesList from '@/components/ContributionMessages/ContributionMessagesList.vue'
|
||||
import { listContributionMessages } from '../../graphql/queries.js'
|
||||
|
||||
export default {
|
||||
name: 'ContributionListItem',
|
||||
components: {
|
||||
Avatar,
|
||||
CollapseIcon,
|
||||
ContributionMessagesList,
|
||||
},
|
||||
props: {
|
||||
@ -141,6 +151,7 @@ export default {
|
||||
state: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
messagesCount: {
|
||||
type: Number,
|
||||
@ -160,6 +171,7 @@ export default {
|
||||
return {
|
||||
inProcess: true,
|
||||
messages_get: [],
|
||||
visible: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -167,13 +179,14 @@ export default {
|
||||
if (this.deletedAt) return 'x-circle'
|
||||
if (this.deniedAt) return 'x-circle'
|
||||
if (this.confirmedAt) return 'check'
|
||||
if (this.state === 'IN_PROGRESS') return 'question-circle'
|
||||
return 'bell-fill'
|
||||
},
|
||||
variant() {
|
||||
if (this.deletedAt) return 'danger'
|
||||
if (this.deniedAt) return 'danger'
|
||||
if (this.confirmedAt) return 'success'
|
||||
if (this.state === 'IN_PROGRESS') return 'warning'
|
||||
if (this.state === 'IN_PROGRESS') return 'f5'
|
||||
return 'primary'
|
||||
},
|
||||
date() {
|
||||
@ -182,6 +195,12 @@ export default {
|
||||
collapsId() {
|
||||
return 'collapse' + String(this.id)
|
||||
},
|
||||
username() {
|
||||
return {
|
||||
username: `${this.firstName} ${this.lastName}`,
|
||||
initials: `${this.firstName[0]}${this.lastName[0]}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
deleteContribution(item) {
|
||||
@ -202,7 +221,6 @@ export default {
|
||||
fetchPolicy: 'no-cache',
|
||||
})
|
||||
.then((result) => {
|
||||
// console.log('result', result.data.listContributionMessages.messages)
|
||||
this.messages_get = result.data.listContributionMessages.messages
|
||||
})
|
||||
.catch((error) => {
|
||||
@ -213,5 +231,10 @@ export default {
|
||||
this.$emit('update-state', id)
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
visible() {
|
||||
if (this.visible) this.getListContributionMessages()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div class="bg-white appBoxShadow gradido-border-radius p-3">
|
||||
<div class="pl-3">
|
||||
<b-row class="small">
|
||||
<b-col>{{ $t('time.months') }}</b-col>
|
||||
<b-col class="d-none d-md-inline">{{ $t('status') }}</b-col>
|
||||
<b-col class="d-none d-md-inline text-center">{{ $t('submitted') }}</b-col>
|
||||
<b-col class="text-center">{{ $t('openHours') }}</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-row class="font-weight-bold pt-3">
|
||||
<b-col>{{ $d(new Date(minimalDate), 'monthAndYear') }}</b-col>
|
||||
<b-col class="d-none d-md-inline">
|
||||
{{ maxGddLastMonth > 0 ? $t('contribution.submit') : $t('maxReached') }}
|
||||
</b-col>
|
||||
<b-col class="d-none d-md-inline text-197 text-center">
|
||||
{{ (1000 - maxGddLastMonth) / 20 }} {{ $t('h') }}
|
||||
</b-col>
|
||||
<b-col class="text-4 text-center">{{ maxGddLastMonth / 20 }} {{ $t('h') }}</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-row class="font-weight-bold">
|
||||
<b-col>{{ $d(new Date(), 'monthAndYear') }}</b-col>
|
||||
<b-col class="d-none d-md-inline">
|
||||
{{ maxGddThisMonth > 0 ? $t('contribution.submit') : $t('maxReached') }}
|
||||
</b-col>
|
||||
<b-col class="d-none d-md-inline text-197 text-center">
|
||||
{{ (1000 - maxGddThisMonth) / 20 }} {{ $t('h') }}
|
||||
</b-col>
|
||||
<b-col class="text-4 text-center">{{ maxGddThisMonth / 20 }} {{ $t('h') }}</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'OpenCreationsAmount',
|
||||
props: {
|
||||
minimalDate: { type: Date, required: true },
|
||||
maxGddLastMonth: { type: Number, required: true },
|
||||
maxGddThisMonth: { type: Number, required: true },
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -1,20 +1,16 @@
|
||||
<template>
|
||||
<div class="decayinformation-decay">
|
||||
<div class="mb-3">
|
||||
<b-icon icon="droplet-half" class="mr-2" />
|
||||
<b>{{ $t('decay.calculation_decay') }}</b>
|
||||
</div>
|
||||
<b-row>
|
||||
<b-col>
|
||||
<div class="text-center pb-3">
|
||||
<b-icon icon="droplet-half" class="mr-2" />
|
||||
<b>{{ $t('decay.calculation_decay') }}</b>
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row>
|
||||
<b-col offset="1" cols="11">
|
||||
<b-row>
|
||||
<b-col cols="5" class="text-right">
|
||||
<b-col cols="12" lg="4" md="4">
|
||||
<div>{{ $t('decay.decay') }}</div>
|
||||
</b-col>
|
||||
<b-col cols="7">
|
||||
<b-col offset="1" offset-md="0" offset-lg="0">
|
||||
<div>
|
||||
{{ previousBookedBalance | GDD }}
|
||||
{{ decay === '0' ? $t('math.minus') : '' }}
|
||||
|
||||
@ -1,22 +1,20 @@
|
||||
<template>
|
||||
<div class="decayinformation-long">
|
||||
<div class="decayinformation-long px-2">
|
||||
<div class="word-break mb-5 mt-lg-3">
|
||||
<div class="font-weight-bold pb-2">{{ $t('form.memo') }}</div>
|
||||
<div class="">{{ memo }}</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<b-icon icon="droplet-half" class="mr-2" />
|
||||
<b>{{ $t('decay.calculation_decay') }}</b>
|
||||
</div>
|
||||
<b-row>
|
||||
<b-col>
|
||||
<div>
|
||||
<div class="text-center pb-3">
|
||||
<b-icon icon="droplet-half" class="mr-2" />
|
||||
<b>{{ $t('decay.calculation_decay') }}</b>
|
||||
</div>
|
||||
</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row>
|
||||
<b-col offset="1" cols="11">
|
||||
<b-row>
|
||||
<b-col cols="5" class="text-right">
|
||||
<b-col cols="12" lg="4" md="4">
|
||||
<div>{{ $t('decay.last_transaction') }}</div>
|
||||
</b-col>
|
||||
<b-col cols="7">
|
||||
<b-col offset="1" offset-md="0" offset-lg="0">
|
||||
<div>
|
||||
<span>
|
||||
{{ $d(new Date(decay.start), 'long') }}
|
||||
@ -28,38 +26,27 @@
|
||||
|
||||
<!-- Decay-->
|
||||
<b-row>
|
||||
<b-col cols="5" class="text-right">
|
||||
<b-col cols="12" lg="4" md="4">
|
||||
<div>{{ $t('decay.decay') }}</div>
|
||||
</b-col>
|
||||
<b-col cols="7">{{ decay.decay | GDD }}</b-col>
|
||||
<b-col offset="1" offset-md="0" offset-lg="0">{{ decay.decay | GDD }}</b-col>
|
||||
</b-row>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<hr class="mt-3 mb-3" />
|
||||
<b-row>
|
||||
<b-col class="text-center pb-3">
|
||||
<b>{{ $t('decay.calculation_total') }}</b>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<!-- Type-->
|
||||
<b-row>
|
||||
<b-col offset="1" cols="11">
|
||||
<b-col>
|
||||
<b-row>
|
||||
<!-- eslint-disable-next-line @intlify/vue-i18n/no-dynamic-keys-->
|
||||
<b-col cols="5" class="text-right">{{ $t(`decay.types.${typeId.toLowerCase()}`) }}</b-col>
|
||||
<b-col cols="7">{{ amount | GDD }}</b-col>
|
||||
</b-row>
|
||||
<!-- Decay-->
|
||||
<b-row>
|
||||
<b-col cols="5" class="text-right">{{ $t('decay.decay') }}</b-col>
|
||||
<b-col cols="7">{{ decay.decay | GDD }}</b-col>
|
||||
<b-col cols="12" lg="4" md="4">{{ $t(`decay.types.${typeId.toLowerCase()}`) }}</b-col>
|
||||
<b-col offset="1" offset-md="0" offset-lg="0">{{ amount | GDD }}</b-col>
|
||||
</b-row>
|
||||
<!-- Total-->
|
||||
<b-row>
|
||||
<b-col cols="5" class="text-right">
|
||||
<b-col cols="12" lg="4" md="4">
|
||||
<div>{{ $t('decay.total') }}</div>
|
||||
</b-col>
|
||||
<b-col cols="7">
|
||||
<b-col offset="1" offset-md="0" offset-lg="0">
|
||||
<b>{{ (Number(amount) + Number(decay.decay)) | GDD }}</b>
|
||||
</b-col>
|
||||
</b-row>
|
||||
@ -78,6 +65,7 @@ export default {
|
||||
props: {
|
||||
amount: { type: String, default: '0' },
|
||||
typeId: { type: String, default: '' },
|
||||
memo: { type: String, default: '' },
|
||||
decay: {
|
||||
type: Object,
|
||||
},
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
:decay="decay"
|
||||
:typeId="typeId"
|
||||
/>
|
||||
<decay-information-long v-else :amount="amount" :decay="decay" :typeId="typeId" />
|
||||
<decay-information-long v-else :amount="amount" :decay="decay" :typeId="typeId" :memo="memo" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
@ -31,6 +31,10 @@ export default {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
memo: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
typeId: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
||||
@ -41,10 +41,6 @@ describe('GddSend confirm', () => {
|
||||
selected: 'link',
|
||||
})
|
||||
})
|
||||
|
||||
it('renders the component div.confirm-box-link', () => {
|
||||
expect(wrapper.findAll('div.confirm-box-link').at(0).exists()).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
describe('has totalBalance under 0', () => {
|
||||
@ -58,5 +54,31 @@ describe('GddSend confirm', () => {
|
||||
expect(wrapper.find('.send-button').attributes('disabled')).toBe('disabled')
|
||||
})
|
||||
})
|
||||
|
||||
describe('send now button', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('single click', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('button.btn.btn-gradido').trigger('click')
|
||||
})
|
||||
|
||||
it('emits send transaction one time', () => {
|
||||
expect(wrapper.emitted('send-transaction')).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('double click', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('button.btn.btn-gradido').trigger('click')
|
||||
})
|
||||
|
||||
it('emits send transaction one time', () => {
|
||||
expect(wrapper.emitted('send-transaction')).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,57 +1,56 @@
|
||||
<template>
|
||||
<div class="transaction-confirm-link">
|
||||
<b-row class="confirm-box-link">
|
||||
<b-col class="text-right mt-4 mb-3">
|
||||
<div class="alert-heading text-left h3">{{ $t('gdd_per_link.header') }}</div>
|
||||
<div class="bg-white appBoxShadow gradido-border-radius p-3">
|
||||
<div class="h3 mb-4">{{ $t('gdd_per_link.header') }}</div>
|
||||
<b-row class="mt-5">
|
||||
<b-col offset="2">
|
||||
<div class="mt-3 h5">{{ $t('form.memo') }}</div>
|
||||
<div>{{ memo }}</div>
|
||||
</b-col>
|
||||
<b-col cols="3">
|
||||
<div class="small">{{ $t('send_gdd') }}</div>
|
||||
<div>{{ amount | GDD }}</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<h1>{{ (amount * -1) | GDD }}</h1>
|
||||
<b class="mt-2">{{ memo }}</b>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-container class="bv-example-row mt-3 mb-3 gray-background p-2">
|
||||
<div class="alert-heading text-left h3">{{ $t('advanced-calculation') }}</div>
|
||||
<b-row class="pr-3">
|
||||
<b-col class="text-right">{{ $t('form.current_balance') }}</b-col>
|
||||
<b-col class="text-right">{{ balance | GDD }}</b-col>
|
||||
<b-row class="mt-5 pr-3 text-color-gdd-yellow h3">
|
||||
<b-col cols="2" class="text-right">
|
||||
<b-icon class="text-color-gdd-yellow" icon="droplet-half"></b-icon>
|
||||
</b-col>
|
||||
<b-col>{{ $t('advanced-calculation') }}</b-col>
|
||||
</b-row>
|
||||
<b-row class="pr-3" offset="2">
|
||||
<b-col offset="2">{{ $t('form.current_balance') }}</b-col>
|
||||
<b-col>{{ balance | GDD }}</b-col>
|
||||
</b-row>
|
||||
<b-row class="pr-3">
|
||||
<b-col class="text-right">
|
||||
<b-col offset="2">
|
||||
<strong>{{ $t('form.your_amount') }}</strong>
|
||||
</b-col>
|
||||
<b-col class="text-right">
|
||||
<b-col class="borderbottom">
|
||||
<strong>{{ (amount * -1) | GDD }}</strong>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row class="pr-3">
|
||||
<b-col offset="2">{{ $t('form.new_balance') }}</b-col>
|
||||
<b-col>{{ (balance - amount) | GDD }}</b-col>
|
||||
</b-row>
|
||||
<b-row class="mt-5 p-5">
|
||||
<b-col>
|
||||
<b-button @click="$emit('on-reset')">{{ $t('back') }}</b-button>
|
||||
</b-col>
|
||||
<b-col class="text-right">
|
||||
<strong>{{ $t('gdd_per_link.decay-14-day') }}</strong>
|
||||
</b-col>
|
||||
<b-col class="text-right borderbottom">
|
||||
<strong>{{ $t('math.aprox') }} {{ (amount * -0.028) | GDD }}</strong>
|
||||
<b-button
|
||||
class="send-button"
|
||||
variant="gradido"
|
||||
:disabled="disabled"
|
||||
@click="$emit('send-transaction')"
|
||||
>
|
||||
{{ $t('form.generate_now') }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row class="pr-3">
|
||||
<b-col class="text-right">{{ $t('form.new_balance') }}</b-col>
|
||||
<b-col class="text-right">{{ $t('math.aprox') }} {{ totalBalance | GDD }}</b-col>
|
||||
</b-row>
|
||||
</b-container>
|
||||
|
||||
<b-row class="mt-4">
|
||||
<b-col>
|
||||
<b-button @click="$emit('on-reset')">{{ $t('back') }}</b-button>
|
||||
</b-col>
|
||||
<b-col class="text-right">
|
||||
<b-button
|
||||
class="send-button"
|
||||
variant="primary"
|
||||
:disabled="disabled"
|
||||
@click="$emit('send-transaction')"
|
||||
>
|
||||
{{ $t('form.generate_now') }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
@ -42,10 +42,6 @@ describe('GddSend confirm', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('renders the component div.confirm-box-send', () => {
|
||||
expect(wrapper.find('div.confirm-box-send').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
describe('send now button', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
@ -53,7 +49,7 @@ describe('GddSend confirm', () => {
|
||||
|
||||
describe('single click', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('button.btn-primary').trigger('click')
|
||||
await wrapper.find('button.btn.btn-gradido').trigger('click')
|
||||
})
|
||||
|
||||
it('emits send transaction one time', () => {
|
||||
@ -63,8 +59,8 @@ describe('GddSend confirm', () => {
|
||||
|
||||
describe('double click', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('button.btn-primary').trigger('click')
|
||||
await wrapper.find('button.btn-primary').trigger('click')
|
||||
await wrapper.find('button.btn.btn-gradido').trigger('click')
|
||||
await wrapper.find('button.btn.btn-gradido').trigger('click')
|
||||
})
|
||||
|
||||
it('emits send transaction one time', () => {
|
||||
|
||||
@ -1,72 +1,59 @@
|
||||
<template>
|
||||
<div class="transaction-confirm-send">
|
||||
<b-row class="confirm-box-send">
|
||||
<b-col>
|
||||
<div class="display-4 pb-4">{{ $t('form.send_check') }}</div>
|
||||
<b-list-group class="">
|
||||
<label class="input-1" for="input-1">{{ $t('form.recipient') }}</label>
|
||||
<b-input-group id="input-group-1" class="borderbottom" size="lg">
|
||||
<b-input-group-prepend class="d-none d-md-block gray-background">
|
||||
<b-icon icon="envelope" class="display-4 m-3"></b-icon>
|
||||
</b-input-group-prepend>
|
||||
<div class="p-3">{{ email }}</div>
|
||||
</b-input-group>
|
||||
<br />
|
||||
<label class="input-2" for="input-2">{{ $t('form.amount') }}</label>
|
||||
<b-input-group id="input-group-2" class="borderbottom" size="lg">
|
||||
<b-input-group-prepend class="p-2 d-none d-md-block gray-background">
|
||||
<div class="m-1 mt-2">{{ $t('GDD') }}</div>
|
||||
</b-input-group-prepend>
|
||||
<div class="bg-white appBoxShadow gradido-border-radius p-3">
|
||||
<div class="h3 mb-4">{{ $t('form.send_check') }}</div>
|
||||
<b-row class="mt-5">
|
||||
<b-col cols="2"></b-col>
|
||||
<b-col>
|
||||
<div class="h4">
|
||||
{{ email }}
|
||||
</div>
|
||||
<div class="mt-3 h5">{{ $t('form.memo') }}</div>
|
||||
<div>{{ memo }}</div>
|
||||
</b-col>
|
||||
<b-col cols="3">
|
||||
<div class="small">{{ $t('send_gdd') }}</div>
|
||||
<div>{{ amount | GDD }}</div>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<div class="p-3">{{ amount | GDD }}</div>
|
||||
</b-input-group>
|
||||
|
||||
<br />
|
||||
<label class="input-3" for="input-3">{{ $t('form.message') }}</label>
|
||||
<b-input-group id="input-group-3" class="borderbottom">
|
||||
<b-input-group-prepend class="d-none d-md-block gray-background">
|
||||
<b-icon icon="chat-right-text" class="display-4 m-3 mt-4"></b-icon>
|
||||
</b-input-group-prepend>
|
||||
<div class="p-3">{{ memo ? memo : $t('em-dash') }}</div>
|
||||
</b-input-group>
|
||||
</b-list-group>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<b-container class="bv-example-row mt-3 mb-3 gray-background p-2">
|
||||
<div class="alert-heading text-left h3">{{ $t('advanced-calculation') }}</div>
|
||||
<b-row class="pr-3">
|
||||
<b-col class="text-right">{{ $t('form.current_balance') }}</b-col>
|
||||
<b-col class="text-right">{{ balance | GDD }}</b-col>
|
||||
<b-row class="mt-5 pr-3 text-color-gdd-yellow h3">
|
||||
<b-col cols="2" class="text-right">
|
||||
<b-icon class="text-color-gdd-yellow" icon="droplet-half"></b-icon>
|
||||
</b-col>
|
||||
<b-col>{{ $t('advanced-calculation') }}</b-col>
|
||||
</b-row>
|
||||
<b-row class="pr-3" offset="2">
|
||||
<b-col offset="2">{{ $t('form.current_balance') }}</b-col>
|
||||
<b-col>{{ balance | GDD }}</b-col>
|
||||
</b-row>
|
||||
<b-row class="pr-3">
|
||||
<b-col class="text-right">
|
||||
<b-col offset="2">
|
||||
<strong>{{ $t('form.your_amount') }}</strong>
|
||||
</b-col>
|
||||
<b-col class="text-right borderbottom">
|
||||
<b-col class="borderbottom">
|
||||
<strong>{{ (amount * -1) | GDD }}</strong>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row class="pr-3">
|
||||
<b-col class="text-right">{{ $t('form.new_balance') }}</b-col>
|
||||
<b-col class="text-right">{{ (balance - amount) | GDD }}</b-col>
|
||||
<b-col offset="2">{{ $t('form.new_balance') }}</b-col>
|
||||
<b-col>{{ (balance - amount) | GDD }}</b-col>
|
||||
</b-row>
|
||||
</b-container>
|
||||
|
||||
<b-row class="mt-4">
|
||||
<b-col>
|
||||
<b-button @click="$emit('on-reset')">{{ $t('back') }}</b-button>
|
||||
</b-col>
|
||||
<b-col class="text-right">
|
||||
<b-button
|
||||
variant="primary"
|
||||
:disabled="disabled"
|
||||
@click="$emit('send-transaction'), (disabled = true)"
|
||||
>
|
||||
{{ $t('form.send_now') }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<b-row class="mt-5 p-5">
|
||||
<b-col>
|
||||
<b-button @click="$emit('on-reset')">{{ $t('back') }}</b-button>
|
||||
</b-col>
|
||||
<b-col class="text-right">
|
||||
<b-button
|
||||
variant="gradido"
|
||||
:disabled="disabled"
|
||||
@click="$emit('send-transaction'), (disabled = true)"
|
||||
>
|
||||
{{ $t('form.send_now') }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import TransactionForm from './TransactionForm'
|
||||
import TransactionForm from './TransactionForm.vue'
|
||||
import flushPromises from 'flush-promises'
|
||||
import { SEND_TYPES } from '@/pages/Send.vue'
|
||||
import DashboardLayout from '@/layouts/DashboardLayout.vue'
|
||||
@ -20,6 +20,9 @@ describe('TransactionForm', () => {
|
||||
email: 'user@example.org',
|
||||
},
|
||||
},
|
||||
$route: {
|
||||
params: {},
|
||||
},
|
||||
}
|
||||
|
||||
const propsData = {
|
||||
@ -44,92 +47,97 @@ describe('TransactionForm', () => {
|
||||
expect(wrapper.find('div.transaction-form').exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('transaction form disable because balance 0,0 GDD', () => {
|
||||
describe('with balance <= 0.00 GDD the form is disabled', () => {
|
||||
it('has a disabled input field of type email', () => {
|
||||
expect(wrapper.find('#input-group-1').find('input').attributes('disabled')).toBe('disabled')
|
||||
expect(
|
||||
wrapper.find('div[data-test="input-email"]').find('input').attributes('disabled'),
|
||||
).toBe('disabled')
|
||||
})
|
||||
|
||||
it('has a disabled input field for amount', () => {
|
||||
expect(wrapper.find('#input-2').find('input').attributes('disabled')).toBe('disabled')
|
||||
expect(
|
||||
wrapper.find('div[data-test="input-amount"]').find('input').attributes('disabled'),
|
||||
).toBe('disabled')
|
||||
})
|
||||
|
||||
it('has a disabled textarea field ', () => {
|
||||
expect(wrapper.find('#input-3').find('textarea').attributes('disabled')).toBe('disabled')
|
||||
expect(
|
||||
wrapper.find('div[data-test="input-textarea').find('textarea').attributes('disabled'),
|
||||
).toBe('disabled')
|
||||
})
|
||||
|
||||
it('has a message indicating that there are no GDDs to send ', () => {
|
||||
expect(wrapper.find('.text-danger').text()).toBe('form.no_gdd_available')
|
||||
expect(wrapper.find('form').find('.text-danger').text()).toBe('form.no_gdd_available')
|
||||
})
|
||||
|
||||
it('has no reset button and no submit button ', () => {
|
||||
expect(wrapper.find('.test-buttons').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('send GDD', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.findAll('input[type="radio"]').at(0).setChecked()
|
||||
describe('with balance greater 0.00 (100.00) GDD the form is fully enabled', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setProps({ balance: 100.0 })
|
||||
})
|
||||
|
||||
it('has SEND_TYPES = send', () => {
|
||||
expect(wrapper.vm.radioSelected).toBe(SEND_TYPES.send)
|
||||
it('has no warning message ', () => {
|
||||
expect(wrapper.find('form').find('.text-danger').exists()).toBe(false)
|
||||
})
|
||||
|
||||
describe('transaction form', () => {
|
||||
beforeEach(() => {
|
||||
wrapper.setProps({ balance: 100.0 })
|
||||
describe('send GDD', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.findAll('input[type="radio"]').at(0).setChecked()
|
||||
})
|
||||
|
||||
describe('transaction form show because balance 100,0 GDD', () => {
|
||||
it('has no warning message ', () => {
|
||||
expect(wrapper.find('.errors').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('has a reset button', () => {
|
||||
expect(wrapper.find('.test-buttons').findAll('button').at(0).attributes('type')).toBe(
|
||||
'reset',
|
||||
)
|
||||
})
|
||||
|
||||
it('has a submit button', () => {
|
||||
expect(wrapper.find('.test-buttons').findAll('button').at(1).attributes('type')).toBe(
|
||||
'submit',
|
||||
)
|
||||
})
|
||||
it('has SEND_TYPES = send', () => {
|
||||
expect(wrapper.vm.radioSelected).toBe(SEND_TYPES.send)
|
||||
})
|
||||
|
||||
describe('email field', () => {
|
||||
it('has an input field of type email', () => {
|
||||
expect(wrapper.find('#input-group-1').find('input').attributes('type')).toBe('email')
|
||||
})
|
||||
|
||||
it('has an envelope icon', () => {
|
||||
expect(wrapper.find('#input-group-1').find('svg').attributes('aria-label')).toBe(
|
||||
'envelope',
|
||||
)
|
||||
expect(
|
||||
wrapper.find('div[data-test="input-email"]').find('input').attributes('type'),
|
||||
).toBe('email')
|
||||
})
|
||||
|
||||
it('has a label form.receiver', () => {
|
||||
expect(wrapper.find('label.input-1').text()).toBe('form.recipient')
|
||||
})
|
||||
|
||||
it('has a placeholder "E-Mail"', () => {
|
||||
expect(wrapper.find('#input-group-1').find('input').attributes('placeholder')).toBe(
|
||||
'E-Mail',
|
||||
expect(wrapper.find('div[data-test="input-email"]').find('label').text()).toBe(
|
||||
'form.recipient',
|
||||
)
|
||||
})
|
||||
|
||||
it('flushes an error message when no valid email is given', async () => {
|
||||
await wrapper.find('#input-group-1').find('input').setValue('a')
|
||||
await flushPromises()
|
||||
expect(wrapper.find('span.errors').text()).toBe('validations.messages.email')
|
||||
it('has a placeholder "E-Mail"', () => {
|
||||
expect(
|
||||
wrapper.find('div[data-test="input-email"]').find('input').attributes('placeholder'),
|
||||
).toBe('form.email')
|
||||
})
|
||||
|
||||
it('flushes an error message when email is the email of logged in user', async () => {
|
||||
await wrapper.find('#input-group-1').find('input').setValue('user@example.org')
|
||||
it('flushes an error message when no valid email is given', async () => {
|
||||
await wrapper.find('div[data-test="input-email"]').find('input').setValue('a')
|
||||
await flushPromises()
|
||||
expect(wrapper.find('span.errors').text()).toBe('form.validation.is-not')
|
||||
expect(
|
||||
wrapper.find('div[data-test="input-email"]').find('.invalid-feedback').text(),
|
||||
).toBe('validations.messages.email')
|
||||
})
|
||||
|
||||
// TODO:SKIPPED there is no check that the email being sent to is the same as the user's email.
|
||||
it.skip('flushes an error message when email is the email of logged in user', async () => {
|
||||
await wrapper
|
||||
.find('div[data-test="input-email"]')
|
||||
.find('input')
|
||||
.setValue('user@example.org')
|
||||
await flushPromises()
|
||||
expect(
|
||||
wrapper.find('div[data-test="input-email"]').find('.invalid-feedback').text(),
|
||||
).toBe('form.validation.is-not')
|
||||
})
|
||||
|
||||
it('trims the email after blur', async () => {
|
||||
await wrapper.find('#input-group-1').find('input').setValue(' valid@email.com ')
|
||||
await wrapper.find('#input-group-1').find('input').trigger('blur')
|
||||
await wrapper
|
||||
.find('div[data-test="input-email"]')
|
||||
.find('input')
|
||||
.setValue(' valid@email.com ')
|
||||
await wrapper.find('div[data-test="input-email"]').find('input').trigger('blur')
|
||||
await flushPromises()
|
||||
expect(wrapper.vm.form.email).toBe('valid@email.com')
|
||||
})
|
||||
@ -137,72 +145,81 @@ describe('TransactionForm', () => {
|
||||
|
||||
describe('amount field', () => {
|
||||
it('has an input field of type text', () => {
|
||||
expect(wrapper.find('#input-group-2').find('input').attributes('type')).toBe('text')
|
||||
})
|
||||
|
||||
it('has an GDD text icon', () => {
|
||||
expect(wrapper.find('#input-group-2').find('div.m-1').text()).toBe('GDD')
|
||||
expect(
|
||||
wrapper.find('div[data-test="input-amount"]').find('input').attributes('type'),
|
||||
).toBe('text')
|
||||
})
|
||||
|
||||
it('has a label form.amount', () => {
|
||||
expect(wrapper.find('label.input-2').text()).toBe('form.amount')
|
||||
})
|
||||
|
||||
it('has a placeholder "0.01"', () => {
|
||||
expect(wrapper.find('#input-group-2').find('input').attributes('placeholder')).toBe(
|
||||
'0.01',
|
||||
expect(wrapper.find('div[data-test="input-amount"]').find('label').text()).toBe(
|
||||
'form.amount',
|
||||
)
|
||||
})
|
||||
|
||||
it('does not update form amount when invalid', async () => {
|
||||
await wrapper.find('#input-group-2').find('input').setValue('invalid')
|
||||
await wrapper.find('#input-group-2').find('input').trigger('blur')
|
||||
it('has a placeholder "0.01"', () => {
|
||||
expect(
|
||||
wrapper.find('div[data-test="input-amount"]').find('input').attributes('placeholder'),
|
||||
).toBe('0.01')
|
||||
})
|
||||
|
||||
it.skip('does not update form amount when invalid', async () => {
|
||||
await wrapper.find('div[data-test="input-amount"]').find('input').setValue('invalid')
|
||||
await wrapper.find('div[data-test="input-amount"]').find('input').trigger('blur')
|
||||
await flushPromises()
|
||||
expect(wrapper.vm.form.amountValue).toBe(0)
|
||||
expect(wrapper.vm.form.amount).toBe(0)
|
||||
})
|
||||
|
||||
it('flushes an error message when no valid amount is given', async () => {
|
||||
await wrapper.find('#input-group-2').find('input').setValue('a')
|
||||
await wrapper.find('div[data-test="input-amount"]').find('input').setValue('a')
|
||||
await flushPromises()
|
||||
expect(wrapper.find('span.errors').text()).toBe('form.validation.gddSendAmount')
|
||||
expect(
|
||||
wrapper.find('div[data-test="input-amount"]').find('.invalid-feedback').text(),
|
||||
).toBe('form.validation.gddSendAmount')
|
||||
})
|
||||
|
||||
it('flushes an error message when amount is too high', async () => {
|
||||
await wrapper.find('#input-group-2').find('input').setValue('123.34')
|
||||
await wrapper.find('div[data-test="input-amount"]').find('input').setValue('123.34')
|
||||
await flushPromises()
|
||||
expect(wrapper.find('span.errors').text()).toBe('form.validation.gddSendAmount')
|
||||
expect(
|
||||
wrapper.find('div[data-test="input-amount"]').find('.invalid-feedback').text(),
|
||||
).toBe('form.validation.gddSendAmount')
|
||||
})
|
||||
|
||||
it('flushes no errors when amount is valid', async () => {
|
||||
await wrapper.find('#input-group-2').find('input').setValue('87.34')
|
||||
await wrapper.find('div[data-test="input-amount"]').find('input').setValue('87.34')
|
||||
await flushPromises()
|
||||
expect(wrapper.find('span.errors').exists()).toBe(false)
|
||||
expect(
|
||||
wrapper
|
||||
.find('div[data-test="input-amount"]')
|
||||
.find('.invalid-feedback')
|
||||
.attributes('aria-live'),
|
||||
).toBe('off')
|
||||
})
|
||||
})
|
||||
|
||||
describe('message text box', () => {
|
||||
it('has an textarea field', () => {
|
||||
expect(wrapper.find('#input-group-3').find('textarea').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has an chat-right-text icon', () => {
|
||||
expect(wrapper.find('#input-group-3').find('svg').attributes('aria-label')).toBe(
|
||||
'chat right text',
|
||||
expect(wrapper.find('div[data-test="input-textarea').find('textarea').exists()).toBe(
|
||||
true,
|
||||
)
|
||||
})
|
||||
|
||||
it('has a label form.message', () => {
|
||||
expect(wrapper.find('label.input-3').text()).toBe('form.message')
|
||||
expect(wrapper.find('div[data-test="input-textarea').find('label').text()).toBe(
|
||||
'form.message',
|
||||
)
|
||||
})
|
||||
|
||||
it('flushes an error message when memo is less than 5 characters', async () => {
|
||||
await wrapper.find('#input-group-3').find('textarea').setValue('a')
|
||||
await wrapper.find('div[data-test="input-textarea').find('textarea').setValue('a')
|
||||
await flushPromises()
|
||||
expect(wrapper.find('span.errors').text()).toBe('validations.messages.min')
|
||||
expect(
|
||||
wrapper.find('div[data-test="input-textarea').find('.invalid-feedback').text(),
|
||||
).toBe('validations.messages.min')
|
||||
})
|
||||
|
||||
it('flushes an error message when memo is more than 255 characters', async () => {
|
||||
await wrapper.find('#input-group-3').find('textarea').setValue(`
|
||||
await wrapper.find('div[data-test="input-textarea').find('textarea').setValue(`
|
||||
Es ist ein König in Thule, der trinkt
|
||||
Champagner, es geht ihm nichts drüber;
|
||||
Und wenn er seinen Champagner trinkt,
|
||||
@ -233,13 +250,23 @@ Mir später weit besser gelingen;
|
||||
Dann werde ich, taumelnd von Krug zu Krug,
|
||||
Die ganze Welt bezwingen.“`)
|
||||
await flushPromises()
|
||||
expect(wrapper.find('span.errors').text()).toBe('validations.messages.max')
|
||||
expect(
|
||||
wrapper.find('div[data-test="input-textarea').find('.invalid-feedback').text(),
|
||||
).toBe('validations.messages.max')
|
||||
})
|
||||
|
||||
it('flushes no error message when memo is valid', async () => {
|
||||
await wrapper.find('#input-group-3').find('textarea').setValue('Long enough')
|
||||
await wrapper
|
||||
.find('div[data-test="input-textarea')
|
||||
.find('textarea')
|
||||
.setValue('Long enough')
|
||||
await flushPromises()
|
||||
expect(wrapper.find('span.errors').exists()).toBe(false)
|
||||
expect(
|
||||
wrapper
|
||||
.find('div[data-test="input-amount"]')
|
||||
.find('.invalid-feedback')
|
||||
.attributes('aria-live'),
|
||||
).toBe('off')
|
||||
})
|
||||
})
|
||||
|
||||
@ -248,14 +275,20 @@ Die ganze Welt bezwingen.“`)
|
||||
expect(wrapper.find('button[type="reset"]').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has the text "form.cancel"', () => {
|
||||
expect(wrapper.find('button[type="reset"]').text()).toBe('form.cancel')
|
||||
it('has the text "form.reset"', () => {
|
||||
expect(wrapper.find('button[type="reset"]').text()).toBe('form.reset')
|
||||
})
|
||||
|
||||
it('clears all fields on click', async () => {
|
||||
await wrapper.find('#input-group-1').find('input').setValue('someone@watches.tv')
|
||||
await wrapper.find('#input-group-2').find('input').setValue('87.23')
|
||||
await wrapper.find('#input-group-3').find('textarea').setValue('Long enough')
|
||||
await wrapper
|
||||
.find('div[data-test="input-email"]')
|
||||
.find('input')
|
||||
.setValue('someone@watches.tv')
|
||||
await wrapper.find('div[data-test="input-amount"]').find('input').setValue('87.23')
|
||||
await wrapper
|
||||
.find('div[data-test="input-textarea')
|
||||
.find('textarea')
|
||||
.setValue('Long enough')
|
||||
await flushPromises()
|
||||
expect(wrapper.vm.form.email).toBe('someone@watches.tv')
|
||||
expect(wrapper.vm.form.amount).toBe('87.23')
|
||||
@ -270,9 +303,15 @@ Die ganze Welt bezwingen.“`)
|
||||
|
||||
describe('submit', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.find('#input-group-1').find('input').setValue('someone@watches.tv')
|
||||
await wrapper.find('#input-group-2').find('input').setValue('87.23')
|
||||
await wrapper.find('#input-group-3').find('textarea').setValue('Long enough')
|
||||
await wrapper
|
||||
.find('div[data-test="input-email"]')
|
||||
.find('input')
|
||||
.setValue('someone@watches.tv')
|
||||
await wrapper.find('div[data-test="input-amount"]').find('input').setValue('87.23')
|
||||
await wrapper
|
||||
.find('div[data-test="input-textarea')
|
||||
.find('textarea')
|
||||
.setValue('Long enough')
|
||||
await wrapper.find('form').trigger('submit')
|
||||
await flushPromises()
|
||||
})
|
||||
@ -283,7 +322,7 @@ Die ganze Welt bezwingen.“`)
|
||||
[
|
||||
{
|
||||
email: 'someone@watches.tv',
|
||||
amount: 87.23,
|
||||
amount: '87.23',
|
||||
memo: 'Long enough',
|
||||
selected: 'send',
|
||||
},
|
||||
@ -292,19 +331,19 @@ Die ganze Welt bezwingen.“`)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('create transaction link', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.findAll('input[type="radio"]').at(1).setChecked()
|
||||
})
|
||||
describe('create transaction link', () => {
|
||||
beforeEach(async () => {
|
||||
await wrapper.findAll('input[type="radio"]').at(1).setChecked()
|
||||
})
|
||||
|
||||
it('has SEND_TYPES = link', () => {
|
||||
expect(wrapper.vm.radioSelected).toBe(SEND_TYPES.link)
|
||||
})
|
||||
it('has SEND_TYPES = link', () => {
|
||||
expect(wrapper.vm.radioSelected).toBe(SEND_TYPES.link)
|
||||
})
|
||||
|
||||
it('has no input field of id input-group-1', () => {
|
||||
expect(wrapper.find('#input-group-1').exists()).toBe(false)
|
||||
it('has no input field of id input-group-1', () => {
|
||||
expect(wrapper.find('#input-group-1').exists()).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,158 +1,107 @@
|
||||
<template>
|
||||
<b-row class="transaction-form">
|
||||
<b-col xl="12" md="12" class="p-0">
|
||||
<b-card class="p-0 m-0 gradido-custom-background">
|
||||
<b-col cols="12">
|
||||
<b-card class="appBoxShadow gradido-border-radius" body-class="p-3">
|
||||
<validation-observer v-slot="{ handleSubmit }" ref="formValidator">
|
||||
<b-form role="form" @submit.prevent="handleSubmit(onSubmit)" @reset="onReset">
|
||||
<b-form-radio-group v-model="radioSelected" class="container">
|
||||
<b-row class="mb-4">
|
||||
<b-col cols="12" lg="6">
|
||||
<b-row class="bg-248 gradido-border-radius pt-lg-2 mr-lg-2">
|
||||
<b-col cols="10" @click="radioSelected = sendTypes.send" class="pointer">
|
||||
{{ $t('send_gdd') }}
|
||||
</b-col>
|
||||
<b-col cols="2">
|
||||
<b-form-radio
|
||||
name="shipping"
|
||||
size="lg"
|
||||
:value="sendTypes.send"
|
||||
stacked
|
||||
class="custom-radio-button pointer"
|
||||
></b-form-radio>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<b-row class="bg-248 gradido-border-radius pt-lg-2 ml-lg-2 mt-2 mt-lg-0">
|
||||
<b-col cols="10" @click="radioSelected = sendTypes.link" class="pointer">
|
||||
{{ $t('send_per_link') }}
|
||||
</b-col>
|
||||
<b-col cols="2" class="pointer">
|
||||
<b-form-radio
|
||||
name="shipping"
|
||||
:value="sendTypes.link"
|
||||
size="lg"
|
||||
class="custom-radio-button"
|
||||
></b-form-radio>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-col>
|
||||
</b-row>
|
||||
|
||||
<div class="mt-4 mb-4" v-if="radioSelected === sendTypes.link">
|
||||
<h2 class="alert-heading">{{ $t('gdd_per_link.header') }}</h2>
|
||||
<div>
|
||||
{{ $t('gdd_per_link.choose-amount') }}
|
||||
</div>
|
||||
</div>
|
||||
</b-form-radio-group>
|
||||
<b-row>
|
||||
<b-col>
|
||||
<b-form-radio
|
||||
v-model="radioSelected"
|
||||
name="radios"
|
||||
:value="sendTypes.send"
|
||||
size="lg"
|
||||
>
|
||||
{{ $t('send_gdd') }}
|
||||
</b-form-radio>
|
||||
</b-col>
|
||||
<b-col>
|
||||
<b-form-radio
|
||||
v-model="radioSelected"
|
||||
name="radios"
|
||||
:value="sendTypes.link"
|
||||
size="lg"
|
||||
>
|
||||
{{ $t('send_per_link') }}
|
||||
</b-form-radio>
|
||||
<b-row>
|
||||
<b-col cols="12">
|
||||
<div v-if="radioSelected === sendTypes.send">
|
||||
<input-email
|
||||
:name="$t('form.recipient')"
|
||||
:label="$t('form.recipient')"
|
||||
:placeholder="$t('form.email')"
|
||||
v-model="form.email"
|
||||
:disabled="isBalanceDisabled"
|
||||
/>
|
||||
</div>
|
||||
</b-col>
|
||||
<b-col cols="12" lg="6">
|
||||
<input-amount
|
||||
v-model="form.amount"
|
||||
:name="$t('form.amount')"
|
||||
:label="$t('form.amount')"
|
||||
:placeholder="'0.01'"
|
||||
:rules="{ required: true, gddSendAmount: [0.01, balance] }"
|
||||
typ="TransactionForm"
|
||||
:disabled="isBalanceDisabled"
|
||||
></input-amount>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<div class="mt-4" v-if="radioSelected === sendTypes.link">
|
||||
<h2 class="alert-heading">{{ $t('gdd_per_link.header') }}</h2>
|
||||
<div>
|
||||
{{ $t('gdd_per_link.choose-amount') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="radioSelected === sendTypes.send">
|
||||
<validation-provider
|
||||
name="Email"
|
||||
:rules="{
|
||||
required: radioSelected === sendTypes.send ? true : false,
|
||||
email: true,
|
||||
is_not: $store.state.email,
|
||||
}"
|
||||
v-slot="{ errors }"
|
||||
>
|
||||
<label class="input-1 mt-4" for="input-1">{{ $t('form.recipient') }}</label>
|
||||
<b-input-group
|
||||
id="input-group-1"
|
||||
class="border border-default border-radius"
|
||||
description="We'll never share your email with anyone else."
|
||||
size="lg"
|
||||
>
|
||||
<b-input-group-prepend class="d-none d-md-block">
|
||||
<b-icon icon="envelope" class="display-4 m-3"></b-icon>
|
||||
</b-input-group-prepend>
|
||||
<b-form-input
|
||||
id="input-1"
|
||||
v-model="form.email"
|
||||
v-focus="emailFocused"
|
||||
@focus="emailFocused = true"
|
||||
@blur="normalizeEmail()"
|
||||
type="email"
|
||||
placeholder="E-Mail"
|
||||
class="pl-3 gradido-font-large"
|
||||
:disabled="isBalanceDisabled"
|
||||
></b-form-input>
|
||||
</b-input-group>
|
||||
<b-col v-if="errors">
|
||||
<span v-for="error in errors" :key="error" class="errors">{{ error }}</span>
|
||||
</b-col>
|
||||
</validation-provider>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 mb-4">
|
||||
<validation-provider
|
||||
:name="$t('form.amount')"
|
||||
:rules="{
|
||||
required: true,
|
||||
gddSendAmount: [0.01, balance],
|
||||
}"
|
||||
v-slot="{ errors, valid }"
|
||||
>
|
||||
<label class="input-2" for="input-2">{{ $t('form.amount') }}</label>
|
||||
<b-input-group
|
||||
id="input-group-2"
|
||||
class="border border-default border-radius"
|
||||
size="lg"
|
||||
>
|
||||
<b-input-group-prepend class="p-2 d-none d-md-block">
|
||||
<div class="m-1 mt-2">{{ $t('GDD') }}</div>
|
||||
</b-input-group-prepend>
|
||||
|
||||
<b-form-input
|
||||
id="input-2"
|
||||
v-model="form.amount"
|
||||
type="text"
|
||||
v-focus="amountFocused"
|
||||
@focus="amountFocused = true"
|
||||
@blur="normalizeAmount(valid)"
|
||||
:placeholder="$n(0.01)"
|
||||
class="pl-3 gradido-font-large"
|
||||
:disabled="isBalanceDisabled"
|
||||
></b-form-input>
|
||||
</b-input-group>
|
||||
<b-col v-if="errors">
|
||||
<span v-for="error in errors" class="errors" :key="error">{{ error }}</span>
|
||||
</b-col>
|
||||
</validation-provider>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<validation-provider
|
||||
:rules="{
|
||||
required: true,
|
||||
min: 5,
|
||||
max: 255,
|
||||
}"
|
||||
:name="$t('form.message')"
|
||||
v-slot="{ errors }"
|
||||
>
|
||||
<label class="input-3" for="input-3">{{ $t('form.message') }}</label>
|
||||
<b-input-group id="input-group-3" class="border border-default border-radius">
|
||||
<b-input-group-prepend class="d-none d-md-block">
|
||||
<b-icon icon="chat-right-text" class="display-4 m-3 mt-4"></b-icon>
|
||||
</b-input-group-prepend>
|
||||
<b-form-textarea
|
||||
id="input-3"
|
||||
rows="3"
|
||||
v-model="form.memo"
|
||||
class="pl-3 gradido-font-large"
|
||||
:disabled="isBalanceDisabled"
|
||||
></b-form-textarea>
|
||||
</b-input-group>
|
||||
<b-col v-if="errors">
|
||||
<span v-for="error in errors" class="errors" :key="error">{{ error }}</span>
|
||||
</b-col>
|
||||
</validation-provider>
|
||||
</div>
|
||||
|
||||
<div v-if="!!isBalanceDisabled" class="text-danger">
|
||||
<b-row>
|
||||
<b-col>
|
||||
<input-textarea
|
||||
v-model="form.memo"
|
||||
:name="$t('form.message')"
|
||||
:label="$t('form.message')"
|
||||
:placeholder="$t('form.message')"
|
||||
:rules="{ required: true, min: 5, max: 255 }"
|
||||
:disabled="isBalanceDisabled"
|
||||
/>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<div v-if="!!isBalanceDisabled" class="text-danger mt-5">
|
||||
{{ $t('form.no_gdd_available') }}
|
||||
</div>
|
||||
<b-row v-else class="test-buttons">
|
||||
<b-row v-else class="test-buttons mt-5">
|
||||
<b-col>
|
||||
<b-button type="reset" variant="secondary" @click="onReset">
|
||||
{{ $t('form.cancel') }}
|
||||
{{ $t('form.reset') }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
<b-col class="text-right">
|
||||
<b-button type="submit" variant="primary">
|
||||
<b-button type="submit" variant="gradido">
|
||||
{{ $t('form.check_now') }}
|
||||
</b-button>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<br />
|
||||
</b-form>
|
||||
</validation-observer>
|
||||
</b-card>
|
||||
@ -160,13 +109,17 @@
|
||||
</b-row>
|
||||
</template>
|
||||
<script>
|
||||
import { BIcon } from 'bootstrap-vue'
|
||||
import { SEND_TYPES } from '@/pages/Send.vue'
|
||||
import InputEmail from '@/components/Inputs/InputEmail.vue'
|
||||
import InputAmount from '@/components/Inputs/InputAmount.vue'
|
||||
import InputTextarea from '@/components/Inputs/InputTextarea.vue'
|
||||
|
||||
export default {
|
||||
name: 'TransactionForm',
|
||||
components: {
|
||||
BIcon,
|
||||
InputEmail,
|
||||
InputAmount,
|
||||
InputTextarea,
|
||||
},
|
||||
props: {
|
||||
balance: { type: Number, default: 0 },
|
||||
@ -178,24 +131,20 @@ export default {
|
||||
inject: ['getTunneledEmail'],
|
||||
data() {
|
||||
return {
|
||||
amountFocused: false,
|
||||
emailFocused: false,
|
||||
form: {
|
||||
email: this.email,
|
||||
amount: this.amount ? String(this.amount) : '',
|
||||
memo: this.memo,
|
||||
amountValue: 0.0,
|
||||
},
|
||||
radioSelected: this.selected,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onSubmit() {
|
||||
this.normalizeAmount(true)
|
||||
this.$emit('set-transaction', {
|
||||
selected: this.radioSelected,
|
||||
email: this.form.email,
|
||||
amount: this.form.amountValue,
|
||||
amount: this.form.amount,
|
||||
memo: this.form.memo,
|
||||
})
|
||||
},
|
||||
@ -205,15 +154,13 @@ export default {
|
||||
this.form.amount = ''
|
||||
this.form.memo = ''
|
||||
},
|
||||
normalizeAmount(isValid) {
|
||||
this.amountFocused = false
|
||||
if (!isValid) return
|
||||
this.form.amountValue = Number(this.form.amount.replace(',', '.'))
|
||||
this.form.amount = this.$n(this.form.amountValue, 'ungroupedDecimal')
|
||||
setNewRecipientEmail() {
|
||||
this.form.email = this.recipientEmail ? this.recipientEmail : this.form.email
|
||||
},
|
||||
normalizeEmail() {
|
||||
this.emailFocused = false
|
||||
this.form.email = this.form.email.trim()
|
||||
},
|
||||
watch: {
|
||||
recipientEmail() {
|
||||
this.setNewRecipientEmail()
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
@ -228,7 +175,7 @@ export default {
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.form.email = this.recipientEmail ? this.recipientEmail : this.form.email
|
||||
this.setNewRecipientEmail()
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -244,4 +191,21 @@ span.errors {
|
||||
.border-radius {
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.custom-control-input:checked ~ .custom-control-label::before {
|
||||
color: #678000;
|
||||
border-color: #678000;
|
||||
background-color: #f1f2ec;
|
||||
}
|
||||
|
||||
.custom-radio .custom-control-input:checked ~ .custom-control-label::after {
|
||||
content: '\2714';
|
||||
margin-left: 5px;
|
||||
color: #678000;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,26 +1,21 @@
|
||||
<template>
|
||||
<b-row>
|
||||
<b-col>
|
||||
<b-card class="p-0 gradido-custom-background">
|
||||
<div class="h3 mb-4">{{ $t('gdd_per_link.created') }}</div>
|
||||
<clipboard-copy
|
||||
:link="link"
|
||||
:amount="amount"
|
||||
:memo="memo"
|
||||
:validUntil="validUntil"
|
||||
@show-qr-code-button="showQrCodeButton"
|
||||
></clipboard-copy>
|
||||
|
||||
<div class="text-center">
|
||||
<figure-qr-code v-if="showQrcode" :link="link" />
|
||||
|
||||
<b-button variant="secondary" @click="$emit('on-reset')" class="mt-4">
|
||||
{{ $t('form.close') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</b-card>
|
||||
</b-col>
|
||||
</b-row>
|
||||
<div class="bg-white appBoxShadow gradido-border-radius p-5">
|
||||
<div class="h3 mb-4">{{ $t('gdd_per_link.created') }}</div>
|
||||
<clipboard-copy
|
||||
:link="link"
|
||||
:amount="amount"
|
||||
:memo="memo"
|
||||
:validUntil="validUntil"
|
||||
></clipboard-copy>
|
||||
<div class="text-center">
|
||||
<div><figure-qr-code :link="link" /></div>
|
||||
<div>
|
||||
<b-button variant="secondary" @click="$emit('on-reset')" class="mt-4" data-test="close-btn">
|
||||
{{ $t('form.close') }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import ClipboardCopy from '../ClipboardCopy.vue'
|
||||
@ -38,15 +33,5 @@ export default {
|
||||
memo: { type: String, required: true },
|
||||
validUntil: { type: String, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showQrcode: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showQrCodeButton() {
|
||||
this.showQrcode = !this.showQrcode
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -1,35 +1,29 @@
|
||||
<template>
|
||||
<b-container>
|
||||
<b-row>
|
||||
<b-col>
|
||||
<b-card class="p-0 gradido-custom-background">
|
||||
<div class="p-4 gradido-font-15rem">
|
||||
<div>{{ $t('form.sorry') }}</div>
|
||||
<hr />
|
||||
<div class="bg-white appBoxShadow gradido-border-radius p-4">
|
||||
<div>
|
||||
<div class="gradido-font-15rem">{{ $t('form.sorry') }}</div>
|
||||
<hr />
|
||||
|
||||
<div class="test-send_transaction_error">{{ $t('form.send_transaction_error') }}</div>
|
||||
<div class="test-send_transaction_error">{{ $t('form.send_transaction_error') }}</div>
|
||||
|
||||
<hr />
|
||||
<div class="test-receiver-not-found" v-if="errorResult === 'recipient not known'">
|
||||
{{ $t('transaction.receiverNotFound') }}
|
||||
</div>
|
||||
<div
|
||||
class="test-receiver-not-found"
|
||||
v-if="errorResult === 'GraphQL error: The recipient account was deleted'"
|
||||
>
|
||||
{{ $t('transaction.receiverDeleted') }}
|
||||
</div>
|
||||
<div v-else>{{ errorResult }}</div>
|
||||
</div>
|
||||
<p class="text-center mt-3">
|
||||
<b-button variant="secondary" @click="$emit('on-reset')">
|
||||
{{ $t('form.close') }}
|
||||
</b-button>
|
||||
</p>
|
||||
</b-card>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-container>
|
||||
<hr />
|
||||
<div class="test-receiver-not-found" v-if="errorResult === 'recipient not known'">
|
||||
{{ $t('transaction.receiverNotFound') }}
|
||||
</div>
|
||||
<div
|
||||
class="test-receiver-not-found"
|
||||
v-if="errorResult === 'GraphQL error: The recipient account was deleted'"
|
||||
>
|
||||
{{ $t('transaction.receiverDeleted') }}
|
||||
</div>
|
||||
<div v-else>{{ errorResult }}</div>
|
||||
</div>
|
||||
<p class="text-center mt-5">
|
||||
<b-button variant="secondary" @click="$emit('on-reset')">
|
||||
{{ $t('form.close') }}
|
||||
</b-button>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
|
||||
@ -1,23 +1,17 @@
|
||||
<template>
|
||||
<b-container>
|
||||
<b-row>
|
||||
<b-col>
|
||||
<b-card class="p-0 gradido-custom-background">
|
||||
<div class="p-4">
|
||||
{{ $t('form.thx') }}
|
||||
<hr />
|
||||
{{ $t('form.send_transaction_success') }}
|
||||
</div>
|
||||
<p class="text-center mt-3">
|
||||
<b-button variant="primary" @click="$emit('on-reset')">{{ $t('form.close') }}</b-button>
|
||||
</p>
|
||||
</b-card>
|
||||
</b-col>
|
||||
</b-row>
|
||||
</b-container>
|
||||
<div class="bg-white appBoxShadow gradido-border-radius p-3">
|
||||
<div class="p-4" data-test="send-transaction-success-text">
|
||||
{{ $t('form.thx') }}
|
||||
<hr />
|
||||
{{ $t('form.send_transaction_success') }}
|
||||
</div>
|
||||
<div class="text-center mt-5">
|
||||
<b-button variant="primary" @click="$emit('on-reset')">{{ $t('form.close') }}</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'TransactionResultSend',
|
||||
name: 'TransactionResultSendSuccess',
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -85,6 +85,8 @@ describe('GddTransactionList', () => {
|
||||
})
|
||||
|
||||
describe('with transactions', () => {
|
||||
let transaction
|
||||
|
||||
beforeEach(async () => {
|
||||
await wrapper.setProps({
|
||||
transactions: [
|
||||
@ -166,39 +168,52 @@ describe('GddTransactionList', () => {
|
||||
})
|
||||
|
||||
it('renders 4 transactions', () => {
|
||||
expect(wrapper.findAll('div.list-group-item')).toHaveLength(4)
|
||||
expect(wrapper.findAll('div.test-list-group-item')).toHaveLength(4)
|
||||
})
|
||||
|
||||
describe('decay transactions', () => {
|
||||
let transaction
|
||||
// let transaction
|
||||
beforeEach(() => {
|
||||
transaction = wrapper.findAll('div.list-group-item').at(0)
|
||||
transaction = wrapper.findAll('div.test-list-group-item').at(0)
|
||||
})
|
||||
|
||||
it('has a bi-caret-down-square icon', () => {
|
||||
it('has a bi-droplet-half icon', () => {
|
||||
expect(transaction.findAll('svg').at(0).classes()).toEqual([
|
||||
'bi-caret-down-square',
|
||||
'bi-droplet-half',
|
||||
'm-mb-1',
|
||||
'font2em',
|
||||
'b-icon',
|
||||
'bi',
|
||||
'text-color-gdd-yellow',
|
||||
])
|
||||
})
|
||||
|
||||
it('has a bi-arrow-down-circle icon', () => {
|
||||
expect(transaction.findAll('svg').at(1).classes()).toEqual([
|
||||
'bi-arrow-down-circle',
|
||||
'h1',
|
||||
'b-icon',
|
||||
'bi',
|
||||
'text-muted',
|
||||
])
|
||||
})
|
||||
|
||||
it('has a bi-droplet-half icon', () => {
|
||||
expect(transaction.findAll('svg').at(1).classes()).toContain('bi-droplet-half')
|
||||
it.skip('has gradido-global-color-gray color', () => {
|
||||
expect(transaction.findAll('svg').at(1).classes()).toEqual([
|
||||
'bi-arrow-down-circle',
|
||||
'b-icon',
|
||||
'bi',
|
||||
'text-muted',
|
||||
])
|
||||
})
|
||||
|
||||
it('has gradido-global-color-gray color', () => {
|
||||
expect(transaction.findAll('svg').at(1).classes()).toContain('gradido-global-color-gray')
|
||||
})
|
||||
|
||||
it('shows the amount of transaction', () => {
|
||||
it.skip('shows the amount of transaction', () => {
|
||||
expect(transaction.findAll('.gdd-transaction-list-item-amount').at(0).text()).toContain(
|
||||
'0.16778637075575395',
|
||||
)
|
||||
})
|
||||
|
||||
it('shows the name of the receiver', () => {
|
||||
it.skip('shows the name of the receiver', () => {
|
||||
expect(transaction.findAll('.gdd-transaction-list-item-name').at(0).text()).toBe(
|
||||
'decay.decay_since_last_transaction',
|
||||
)
|
||||
@ -206,26 +221,37 @@ describe('GddTransactionList', () => {
|
||||
})
|
||||
|
||||
describe('send transactions', () => {
|
||||
let transaction
|
||||
// let transaction
|
||||
beforeEach(() => {
|
||||
transaction = wrapper.findAll('div.list-group-item').at(1)
|
||||
transaction = wrapper.findAll('div.test-list-group-item').at(1)
|
||||
})
|
||||
|
||||
it('has a bi-caret-down-square icon', () => {
|
||||
it('has a bi-arrow-down-circle icon', () => {
|
||||
expect(transaction.findAll('svg').at(0).classes()).toEqual([
|
||||
'bi-caret-down-square',
|
||||
'bi-arrow-down-circle',
|
||||
'h1',
|
||||
'b-icon',
|
||||
'bi',
|
||||
'text-muted',
|
||||
])
|
||||
})
|
||||
|
||||
it('has a bi-arrow-left-circle icon', () => {
|
||||
expect(transaction.findAll('svg').at(1).classes()).toContain('bi-arrow-left-circle')
|
||||
it('has a bi-droplet-half icon', () => {
|
||||
expect(transaction.findAll('svg').at(1).classes()).toEqual([
|
||||
'bi-droplet-half',
|
||||
'mr-2',
|
||||
'b-icon',
|
||||
'bi',
|
||||
])
|
||||
})
|
||||
|
||||
it('has text-danger color', () => {
|
||||
expect(transaction.findAll('svg').at(1).classes()).toContain('text-danger')
|
||||
it.skip('has text-danger color', () => {
|
||||
expect(transaction.findAll('svg').at(1).classes()).toEqual([
|
||||
'bi-droplet-half',
|
||||
'mr-2',
|
||||
'b-icon',
|
||||
'bi',
|
||||
])
|
||||
})
|
||||
|
||||
// operators are renderd by GDD filter
|
||||
@ -235,65 +261,59 @@ describe('GddTransactionList', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('shows the amount of transaction', () => {
|
||||
it.skip('shows the amount of transaction', () => {
|
||||
expect(transaction.findAll('.gdd-transaction-list-item-amount').at(0).text()).toContain(
|
||||
'1',
|
||||
)
|
||||
})
|
||||
|
||||
it('shows the name of the receiver', () => {
|
||||
it.skip('shows the name of the receiver', () => {
|
||||
expect(transaction.findAll('.gdd-transaction-list-item-name').at(0).text()).toContain(
|
||||
'Bibi Bloxberg',
|
||||
)
|
||||
})
|
||||
|
||||
it('shows the message of the transaction', () => {
|
||||
it.skip('shows the message of the transaction', () => {
|
||||
expect(transaction.findAll('.gdd-transaction-list-message').at(0).text()).toContain(
|
||||
'Um den Kessel schlingt den Reihn, Werft die Eingeweid‘ hinein. Kröte du, die Nacht und Tag Unterm kalten Steine lag,',
|
||||
)
|
||||
})
|
||||
|
||||
it('shows the date of the transaction', () => {
|
||||
it.skip('shows the date of the transaction', () => {
|
||||
expect(transaction.findAll('.gdd-transaction-list-item-date').at(0).text()).toContain(
|
||||
'Mon Feb 28 2022 13:55:47 GMT+0000',
|
||||
)
|
||||
})
|
||||
|
||||
it('shows the decay calculation', () => {
|
||||
it.skip('shows the decay calculation', () => {
|
||||
expect(transaction.findAll('div.gdd-transaction-list-item-decay').at(0).text()).toContain(
|
||||
'− 0.2038314055482643084',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('creation transactions', () => {
|
||||
let transaction
|
||||
describe('receive transactions', () => {
|
||||
// let transaction
|
||||
|
||||
beforeEach(() => {
|
||||
transaction = wrapper.findAll('div.list-group-item').at(2)
|
||||
transaction = wrapper.findAll('div.test-list-group-item').at(2)
|
||||
})
|
||||
|
||||
it('has a bi-caret-down-square icon', () => {
|
||||
it('has a bi-arrow-down-circle icon', () => {
|
||||
expect(transaction.findAll('svg').at(0).classes()).toEqual([
|
||||
'bi-caret-down-square',
|
||||
'bi-arrow-down-circle',
|
||||
'h1',
|
||||
'b-icon',
|
||||
'bi',
|
||||
'text-muted',
|
||||
])
|
||||
})
|
||||
|
||||
it('has a bi-gift icon', () => {
|
||||
expect(transaction.findAll('svg').at(1).classes()).toEqual([
|
||||
'bi-arrow-right-circle',
|
||||
'm-mb-1',
|
||||
'font2em',
|
||||
'b-icon',
|
||||
'bi',
|
||||
'gradido-global-color-accent',
|
||||
])
|
||||
it.skip('has a bi-gift icon', () => {
|
||||
expect(transaction.findAll('svg').at(1).classes()).toEqual(['bi-gift', 'b-icon', 'bi'])
|
||||
})
|
||||
|
||||
it('has gradido-global-color-accent color', () => {
|
||||
it.skip('has gradido-global-color-accent color', () => {
|
||||
expect(transaction.findAll('svg').at(1).classes()).toEqual([
|
||||
'bi-arrow-right-circle',
|
||||
'm-mb-1',
|
||||
@ -311,62 +331,45 @@ describe('GddTransactionList', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('shows the amount of transaction', () => {
|
||||
it.skip('shows the amount of transaction', () => {
|
||||
expect(transaction.findAll('.gdd-transaction-list-item-amount').at(0).text()).toContain(
|
||||
'+ 10 GDD',
|
||||
)
|
||||
})
|
||||
|
||||
it('shows the name of the receiver', () => {
|
||||
it.skip('shows the name of the receiver', () => {
|
||||
expect(transaction.findAll('.gdd-transaction-list-item-name').at(0).text()).toContain(
|
||||
'Bibi Bloxberg',
|
||||
)
|
||||
})
|
||||
|
||||
it('shows the date of the transaction', () => {
|
||||
it.skip('shows the date of the transaction', () => {
|
||||
expect(transaction.findAll('.gdd-transaction-list-item-date').at(0).text()).toContain(
|
||||
'Wed Feb 23 2022 10:55:30 GMT+0000',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('receive transactions', () => {
|
||||
let transaction
|
||||
describe('creation transactions', () => {
|
||||
// let transaction
|
||||
beforeEach(() => {
|
||||
transaction = wrapper.findAll('div.list-group-item').at(3)
|
||||
transaction = wrapper.findAll('div.test-list-group-item').at(3)
|
||||
})
|
||||
|
||||
it('has a bi-caret-down-square icon', () => {
|
||||
expect(transaction.findAll('svg').at(0).classes()).toEqual([
|
||||
'bi-caret-down-square',
|
||||
it('has a bi-gift icon', () => {
|
||||
expect(transaction.findAll('svg').at(0).classes()).toEqual(['bi-gift', 'b-icon', 'bi'])
|
||||
})
|
||||
|
||||
it('has a bi-arrow-down-circle icon', () => {
|
||||
expect(transaction.findAll('svg').at(1).classes()).toEqual([
|
||||
'bi-arrow-down-circle',
|
||||
'h1',
|
||||
'b-icon',
|
||||
'bi',
|
||||
'text-muted',
|
||||
])
|
||||
})
|
||||
|
||||
it('has a bi-arrow-right-circle icon', () => {
|
||||
expect(transaction.findAll('svg').at(1).classes()).toEqual([
|
||||
'bi-gift',
|
||||
'm-mb-1',
|
||||
'font2em',
|
||||
'b-icon',
|
||||
'bi',
|
||||
'gradido-global-color-accent',
|
||||
])
|
||||
})
|
||||
|
||||
it('has gradido-global-color-accent color', () => {
|
||||
expect(transaction.findAll('svg').at(1).classes()).toEqual([
|
||||
'bi-gift',
|
||||
'm-mb-1',
|
||||
'font2em',
|
||||
'b-icon',
|
||||
'bi',
|
||||
'gradido-global-color-accent',
|
||||
])
|
||||
})
|
||||
|
||||
// operators are renderd by GDD filter
|
||||
it.skip('has a plus operator', () => {
|
||||
expect(transaction.findAll('.gdd-transaction-list-item-operator').at(0).text()).toContain(
|
||||
@ -374,31 +377,31 @@ describe('GddTransactionList', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('shows the amount of transaction', () => {
|
||||
it.skip('shows the amount of transaction', () => {
|
||||
expect(transaction.findAll('.gdd-transaction-list-item-amount').at(0).text()).toContain(
|
||||
'10',
|
||||
)
|
||||
})
|
||||
|
||||
it('shows the name of the recipient', () => {
|
||||
it.skip('shows the name of the recipient', () => {
|
||||
expect(transaction.findAll('.gdd-transaction-list-item-name').at(0).text()).toContain(
|
||||
'Gradido Akademie',
|
||||
)
|
||||
})
|
||||
|
||||
it('shows the message of the transaction', () => {
|
||||
it.skip('shows the message of the transaction', () => {
|
||||
expect(transaction.findAll('.gdd-transaction-list-message').at(0).text()).toContain(
|
||||
'Jammern hilft nichts, sondern ich kann selber meinen Teil dazu beitragen.',
|
||||
)
|
||||
})
|
||||
|
||||
it('shows the date of the transaction', () => {
|
||||
it.skip('shows the date of the transaction', () => {
|
||||
expect(transaction.findAll('.gdd-transaction-list-item-date').at(0).text()).toContain(
|
||||
'Fri Feb 25 2022 07:29:26 GMT+0000',
|
||||
)
|
||||
})
|
||||
|
||||
it('shows the decay calculation', () => {
|
||||
it.skip('shows the decay calculation', () => {
|
||||
expect(transaction.findAll('.gdd-transaction-list-item-decay').at(0).text()).toContain(
|
||||
'0',
|
||||
)
|
||||
@ -444,7 +447,7 @@ describe('GddTransactionList', () => {
|
||||
describe('next page button clicked', () => {
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
// await wrapper.vm.$nextTick()
|
||||
await wrapper.vm.$nextTick()
|
||||
await wrapper.findComponent({ name: 'BPagination' }).vm.$emit('input', 2)
|
||||
})
|
||||
|
||||
|
||||
@ -12,19 +12,29 @@
|
||||
<small>{{ $t('error.empty-transactionlist') }}</small>
|
||||
</div>
|
||||
|
||||
<div v-for="({ id, typeId }, index) in transactions" :key="id">
|
||||
<transaction-list-item :typeId="typeId" class="pointer">
|
||||
<div v-for="({ id, typeId }, index) in transactions" :key="`l1-` + id">
|
||||
<transaction-list-item
|
||||
v-if="typeId === 'DECAY'"
|
||||
:typeId="typeId"
|
||||
class="pointer bg-white appBoxShadow gradido-border-radius px-4 pt-2 test-list-group-item"
|
||||
>
|
||||
<template #DECAY>
|
||||
<transaction-decay
|
||||
class="list-group-item"
|
||||
v-bind="transactions[index]"
|
||||
:previousBookedBalance="previousBookedBalance(index)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
</transaction-list-item>
|
||||
</div>
|
||||
<div v-if="transactionCount > 0" class="h4 m-3">{{ $t('lastMonth') }}</div>
|
||||
<div v-for="({ id, typeId }, index) in transactions" :key="`l2-` + id">
|
||||
<transaction-list-item
|
||||
v-if="typeId !== 'DECAY'"
|
||||
:typeId="typeId"
|
||||
class="pointer mb-4 bg-white appBoxShadow gradido-border-radius p-3 test-list-group-item"
|
||||
>
|
||||
<template #SEND>
|
||||
<transaction-send
|
||||
class="list-group-item"
|
||||
v-bind="transactions[index]"
|
||||
:previousBookedBalance="previousBookedBalance(index)"
|
||||
v-on="$listeners"
|
||||
@ -33,7 +43,6 @@
|
||||
|
||||
<template #RECEIVE>
|
||||
<transaction-receive
|
||||
class="list-group-item"
|
||||
v-bind="transactions[index]"
|
||||
:previousBookedBalance="previousBookedBalance(index)"
|
||||
v-on="$listeners"
|
||||
@ -42,7 +51,6 @@
|
||||
|
||||
<template #CREATION>
|
||||
<transaction-creation
|
||||
class="list-group-item"
|
||||
v-bind="transactions[index]"
|
||||
:previousBookedBalance="previousBookedBalance(index)"
|
||||
v-on="$listeners"
|
||||
@ -51,7 +59,6 @@
|
||||
|
||||
<template #LINK_SUMMARY>
|
||||
<transaction-link-summary
|
||||
class="list-group-item"
|
||||
v-bind="transactions[index]"
|
||||
:transactionLinkCount="transactionLinkCount"
|
||||
@update-transactions="updateTransactions"
|
||||
|
||||
@ -45,7 +45,7 @@
|
||||
import Transaction from '@/components/Transaction.vue'
|
||||
|
||||
export default {
|
||||
name: 'gdt-transaction-list',
|
||||
name: 'GdtTransactionList',
|
||||
components: {
|
||||
Transaction,
|
||||
},
|
||||
|
||||
38
frontend/src/components/Inputs/FirstName.spec.js
Normal file
38
frontend/src/components/Inputs/FirstName.spec.js
Normal file
@ -0,0 +1,38 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import FirstName from './FirstName'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
describe('FirstName', () => {
|
||||
let wrapper
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$i18n: {
|
||||
locale: jest.fn(() => 'en'),
|
||||
},
|
||||
$n: jest.fn((n) => String(n)),
|
||||
}
|
||||
|
||||
const propsData = {
|
||||
balance: 0.0,
|
||||
}
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(FirstName, {
|
||||
localVue,
|
||||
mocks,
|
||||
propsData,
|
||||
})
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('renders the component', () => {
|
||||
expect(wrapper.find('div.first-name').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
40
frontend/src/components/Inputs/FirstName.vue
Normal file
40
frontend/src/components/Inputs/FirstName.vue
Normal file
@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div role="group" class="first-name">
|
||||
<label for="input-firstName">{{ $t('form.firstname') }}</label>
|
||||
<b-form-input
|
||||
id="input-firstName"
|
||||
v-model="firstName"
|
||||
:state="firstNameState"
|
||||
aria-describedby="input-live-help input-live-feedback"
|
||||
placeholder="Enter your firstName"
|
||||
trim
|
||||
></b-form-input>
|
||||
|
||||
<!-- This will only be shown if the preceding input has an invalid state -->
|
||||
<!-- <b-form-invalid-feedback id="input-live-feedback">
|
||||
Enter at least 3 letters
|
||||
</b-form-invalid-feedback> -->
|
||||
|
||||
<!-- This is a form text block (formerly known as help block) -->
|
||||
<!-- <b-form-text id="input-live-help">Dein Vorname</b-form-text> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'FirstName',
|
||||
props: {
|
||||
value: { type: String, default: '' },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
firstName: this.value,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
firstNameState() {
|
||||
return this.firstName.length > 2
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
122
frontend/src/components/Inputs/InputAmount.spec.js
Normal file
122
frontend/src/components/Inputs/InputAmount.spec.js
Normal file
@ -0,0 +1,122 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import InputAmount from './InputAmount'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
describe('InputAmount', () => {
|
||||
let wrapper
|
||||
let valid
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$i18n: {
|
||||
locale: jest.fn(() => 'en'),
|
||||
},
|
||||
$n: jest.fn((n) => String(n)),
|
||||
$route: {
|
||||
params: {},
|
||||
},
|
||||
}
|
||||
|
||||
describe('mount in a TransactionForm', () => {
|
||||
const propsData = {
|
||||
name: '',
|
||||
label: '',
|
||||
placeholder: '',
|
||||
typ: 'TransactionForm',
|
||||
value: '12,34',
|
||||
}
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(InputAmount, {
|
||||
localVue,
|
||||
mocks,
|
||||
propsData,
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
wrapper.vm.$options.watch.value.call(wrapper.vm)
|
||||
})
|
||||
|
||||
it('renders the component input-amount', () => {
|
||||
expect(wrapper.find('div.input-amount').exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('amount normalization', () => {
|
||||
describe('if invalid', () => {
|
||||
beforeEach(() => {
|
||||
valid = false
|
||||
})
|
||||
|
||||
it('is not normalized', () => {
|
||||
wrapper.vm.normalizeAmount(valid)
|
||||
expect(wrapper.vm.amountValue).toBe(0.0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('if valid', () => {
|
||||
beforeEach(() => {
|
||||
valid = true
|
||||
})
|
||||
|
||||
it('is normalized to a number - not rounded', async () => {
|
||||
wrapper.vm.normalizeAmount(valid)
|
||||
expect(wrapper.vm.currentValue).toBe('12.34')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('mount in a ContributionForm', () => {
|
||||
const propsData = {
|
||||
name: '',
|
||||
label: '',
|
||||
placeholder: '',
|
||||
typ: 'ContributionForm',
|
||||
value: '12.34',
|
||||
}
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(InputAmount, {
|
||||
localVue,
|
||||
mocks,
|
||||
propsData,
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
wrapper.vm.$options.watch.value.call(wrapper.vm)
|
||||
})
|
||||
|
||||
it('renders the component input-amount', () => {
|
||||
expect(wrapper.find('div.input-amount').exists()).toBe(true)
|
||||
})
|
||||
|
||||
describe('amount normalization', () => {
|
||||
describe('if invalid', () => {
|
||||
beforeEach(() => {
|
||||
valid = false
|
||||
})
|
||||
|
||||
it('is not normalized', () => {
|
||||
wrapper.vm.normalizeAmount(valid)
|
||||
expect(wrapper.vm.amountValue).toBe(0.0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('if valid', () => {
|
||||
beforeEach(() => {
|
||||
valid = true
|
||||
})
|
||||
|
||||
it('is normalized to a ungroupedDecimal number', () => {
|
||||
wrapper.vm.normalizeAmount(valid)
|
||||
expect(wrapper.vm.currentValue).toBe('12.34')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
93
frontend/src/components/Inputs/InputAmount.vue
Normal file
93
frontend/src/components/Inputs/InputAmount.vue
Normal file
@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<div class="input-amount">
|
||||
<validation-provider
|
||||
v-if="typ === 'TransactionForm'"
|
||||
tag="div"
|
||||
:rules="rules"
|
||||
:name="name"
|
||||
v-slot="{ errors, valid, validated, ariaInput, ariaMsg }"
|
||||
>
|
||||
<b-form-group :label="label" :label-for="labelFor" data-test="input-amount">
|
||||
<b-form-input
|
||||
v-model="currentValue"
|
||||
v-bind="ariaInput"
|
||||
:id="labelFor"
|
||||
:class="$route.path === '/send' ? 'bg-248' : ''"
|
||||
:name="name"
|
||||
:placeholder="placeholder"
|
||||
type="text"
|
||||
:state="validated ? valid : false"
|
||||
trim
|
||||
v-focus="amountFocused"
|
||||
@focus="amountFocused = true"
|
||||
@blur="normalizeAmount(true)"
|
||||
:disabled="disabled"
|
||||
></b-form-input>
|
||||
|
||||
<b-form-invalid-feedback v-bind="ariaMsg">
|
||||
{{ errors[0] }}
|
||||
</b-form-invalid-feedback>
|
||||
</b-form-group>
|
||||
</validation-provider>
|
||||
<b-input-group v-else append="GDD" :label="label" :label-for="labelFor">
|
||||
<b-form-input
|
||||
v-model="currentValue"
|
||||
:id="labelFor"
|
||||
:name="name"
|
||||
:placeholder="placeholder"
|
||||
type="text"
|
||||
readonly
|
||||
trim
|
||||
v-focus="amountFocused"
|
||||
@focus="amountFocused = true"
|
||||
@blur="normalizeAmount(valid)"
|
||||
></b-form-input>
|
||||
</b-input-group>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'InputAmount',
|
||||
props: {
|
||||
rules: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
typ: { type: String, default: 'TransactionForm' },
|
||||
name: { type: String, required: true, default: 'Amount' },
|
||||
label: { type: String, required: true, default: 'Amount' },
|
||||
placeholder: { type: String, required: true, default: 'Amount' },
|
||||
value: { type: String, required: true },
|
||||
balance: { type: Number, default: 0.0 },
|
||||
disabled: { required: false, type: Boolean, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentValue: '',
|
||||
amountValue: 0.0,
|
||||
amountFocused: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labelFor() {
|
||||
return this.name + '-input-field'
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentValue() {
|
||||
this.$emit('input', this.currentValue)
|
||||
},
|
||||
value() {
|
||||
if (this.value !== this.currentValue) this.currentValue = this.value
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
normalizeAmount(isValid) {
|
||||
this.amountFocused = false
|
||||
if (!isValid) return
|
||||
this.amountValue = this.currentValue.replace(',', '.')
|
||||
this.currentValue = this.$n(this.amountValue, 'ungroupedDecimal')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -1,6 +1,7 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
|
||||
import InputEmail from './InputEmail'
|
||||
import flushPromises from 'flush-promises'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
@ -14,8 +15,14 @@ describe('InputEmail', () => {
|
||||
value: '',
|
||||
}
|
||||
|
||||
const mocks = {
|
||||
$route: {
|
||||
params: {},
|
||||
},
|
||||
}
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(InputEmail, { localVue, propsData })
|
||||
return mount(InputEmail, { localVue, propsData, mocks })
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
@ -54,10 +61,17 @@ describe('InputEmail', () => {
|
||||
})
|
||||
|
||||
describe('input value changes', () => {
|
||||
it.skip('trims the email after blur', async () => {
|
||||
await wrapper.find('input').setValue(' valid@email.com ')
|
||||
await wrapper.find('input').trigger('blur')
|
||||
await flushPromises()
|
||||
expect(wrapper.vm.currentValue).toBe('valid@email.com')
|
||||
})
|
||||
|
||||
it('emits input with new value', async () => {
|
||||
await wrapper.find('input').setValue('12')
|
||||
await wrapper.find('input').setValue('user@example.org')
|
||||
expect(wrapper.emitted('input')).toBeTruthy()
|
||||
expect(wrapper.emitted('input')).toEqual([['12']])
|
||||
expect(wrapper.emitted('input')).toEqual([['user@example.org']])
|
||||
})
|
||||
})
|
||||
|
||||
@ -67,5 +81,13 @@ describe('InputEmail', () => {
|
||||
expect(wrapper.vm.currentValue).toEqual('user@example.org')
|
||||
})
|
||||
})
|
||||
|
||||
describe('email normalization', () => {
|
||||
it('is trimmed', async () => {
|
||||
await wrapper.setData({ currentValue: ' valid@email.com ' })
|
||||
wrapper.vm.normalizeEmail()
|
||||
expect(wrapper.vm.currentValue).toBe('valid@email.com')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -5,16 +5,22 @@
|
||||
:name="name"
|
||||
v-slot="{ errors, valid, validated, ariaInput, ariaMsg }"
|
||||
>
|
||||
<b-form-group :label="label" :label-for="labelFor">
|
||||
<b-form-group :label="label" :label-for="labelFor" data-test="input-email">
|
||||
<b-form-input
|
||||
v-model="currentValue"
|
||||
v-bind="ariaInput"
|
||||
data-test="input-email"
|
||||
:id="labelFor"
|
||||
:name="name"
|
||||
:placeholder="placeholder"
|
||||
type="email"
|
||||
:state="validated ? valid : false"
|
||||
trim
|
||||
:class="$route.path === '/send' ? 'bg-248' : ''"
|
||||
v-focus="emailFocused"
|
||||
@focus="emailFocused = true"
|
||||
@blur="normalizeEmail()"
|
||||
:disabled="disabled"
|
||||
></b-form-input>
|
||||
<b-form-invalid-feedback v-bind="ariaMsg">
|
||||
{{ errors[0] }}
|
||||
@ -37,11 +43,13 @@ export default {
|
||||
name: { type: String, default: 'Email' },
|
||||
label: { type: String, default: 'Email' },
|
||||
placeholder: { type: String, default: 'Email' },
|
||||
value: { required: true, type: String },
|
||||
value: { required: true, type: String, default: '' },
|
||||
disabled: { required: false, type: Boolean, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentValue: '',
|
||||
currentValue: this.value,
|
||||
emailFocused: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@ -57,5 +65,11 @@ export default {
|
||||
if (this.value !== this.currentValue) this.currentValue = this.value
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
normalizeEmail() {
|
||||
this.emailFocused = false
|
||||
this.currentValue = this.currentValue.trim()
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
88
frontend/src/components/Inputs/InputHour.spec.js
Normal file
88
frontend/src/components/Inputs/InputHour.spec.js
Normal file
@ -0,0 +1,88 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import InputHour from './InputHour'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
describe('InputHour', () => {
|
||||
let wrapper
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$i18n: {
|
||||
locale: jest.fn(() => 'en'),
|
||||
},
|
||||
$n: jest.fn((n) => String(n)),
|
||||
$route: {
|
||||
params: {},
|
||||
},
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
const propsData = {
|
||||
rules: {},
|
||||
name: 'input-field-name',
|
||||
label: 'input-field-label',
|
||||
placeholder: 'input-field-placeholder',
|
||||
value: 500,
|
||||
validMaxTime: 25,
|
||||
}
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(InputHour, {
|
||||
localVue,
|
||||
mocks,
|
||||
propsData,
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
// await wrapper.setData({ currentValue: 15 })
|
||||
})
|
||||
|
||||
it('renders the component input-hour', () => {
|
||||
expect(wrapper.find('div.input-hour').exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has an input field', () => {
|
||||
expect(wrapper.find('input').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
describe('properties', () => {
|
||||
it('has the id "input-field-name-input-field"', () => {
|
||||
expect(wrapper.find('input').attributes('id')).toEqual('input-field-name-input-field')
|
||||
})
|
||||
|
||||
it('has the placeholder "input-field-placeholder"', () => {
|
||||
expect(wrapper.find('input').attributes('placeholder')).toEqual('input-field-placeholder')
|
||||
})
|
||||
|
||||
it('has the value 0', () => {
|
||||
expect(wrapper.vm.currentValue).toEqual(0)
|
||||
})
|
||||
|
||||
it('has the label "input-field-label"', () => {
|
||||
expect(wrapper.find('label').text()).toEqual('input-field-label')
|
||||
})
|
||||
|
||||
it('has the label for "input-field-name-input-field"', () => {
|
||||
expect(wrapper.find('label').attributes('for')).toEqual('input-field-name-input-field')
|
||||
})
|
||||
})
|
||||
|
||||
describe('input value changes', () => {
|
||||
it('emits input with new value', async () => {
|
||||
await wrapper.find('input').setValue('12')
|
||||
expect(wrapper.emitted('input')).toBeTruthy()
|
||||
expect(wrapper.emitted('input')).toEqual([['12']])
|
||||
})
|
||||
})
|
||||
|
||||
describe('value property changes', () => {
|
||||
it('updates data model', async () => {
|
||||
await wrapper.setProps({ value: 15 })
|
||||
expect(wrapper.vm.currentValue).toEqual(15)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
61
frontend/src/components/Inputs/InputHour.vue
Normal file
61
frontend/src/components/Inputs/InputHour.vue
Normal file
@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div class="input-hour">
|
||||
<validation-provider
|
||||
tag="div"
|
||||
:rules="rules"
|
||||
:name="name"
|
||||
v-slot="{ valid, validated, ariaInput }"
|
||||
>
|
||||
<b-form-group :label="label" :label-for="labelFor">
|
||||
<b-form-input
|
||||
v-model="currentValue"
|
||||
v-bind="ariaInput"
|
||||
:id="labelFor"
|
||||
:name="name"
|
||||
:placeholder="placeholder"
|
||||
type="number"
|
||||
:state="validated ? valid : false"
|
||||
step="0.5"
|
||||
min="0"
|
||||
:max="validMaxTime"
|
||||
class="bg-248"
|
||||
></b-form-input>
|
||||
</b-form-group>
|
||||
</validation-provider>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'InputHour',
|
||||
props: {
|
||||
rules: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
name: { type: String, required: true, default: 'Time' },
|
||||
label: { type: String, required: true, default: 'Time' },
|
||||
placeholder: { type: String, required: true, default: 'Time' },
|
||||
value: { type: Number, required: true, default: 0 },
|
||||
validMaxTime: { type: Number, required: true, default: 0 },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentValue: 0,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labelFor() {
|
||||
return this.name + '-input-field'
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentValue() {
|
||||
this.$emit('input', this.currentValue)
|
||||
},
|
||||
value() {
|
||||
if (this.value !== this.currentValue) this.currentValue = this.value
|
||||
this.$emit('updateAmount', this.currentValue)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
88
frontend/src/components/Inputs/InputTextarea.spec.js
Normal file
88
frontend/src/components/Inputs/InputTextarea.spec.js
Normal file
@ -0,0 +1,88 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import InputTextarea from './InputTextarea'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
describe('InputTextarea', () => {
|
||||
let wrapper
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$i18n: {
|
||||
locale: jest.fn(() => 'en'),
|
||||
},
|
||||
$n: jest.fn((n) => String(n)),
|
||||
$route: {
|
||||
params: {},
|
||||
},
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
const propsData = {
|
||||
rules: {},
|
||||
name: 'input-field-name',
|
||||
label: 'input-field-label',
|
||||
placeholder: 'input-field-placeholder',
|
||||
value: 'Long enough',
|
||||
}
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(InputTextarea, {
|
||||
localVue,
|
||||
mocks,
|
||||
propsData,
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('renders the component InputTextarea', () => {
|
||||
expect(wrapper.findComponent({ name: 'InputTextarea' }).exists()).toBe(true)
|
||||
})
|
||||
|
||||
it('has an textarea field', () => {
|
||||
expect(wrapper.find('textarea').exists()).toBeTruthy()
|
||||
})
|
||||
|
||||
describe('properties', () => {
|
||||
it('has the id "input-field-name-input-field"', () => {
|
||||
expect(wrapper.find('textarea').attributes('id')).toEqual('input-field-name-input-field')
|
||||
})
|
||||
|
||||
it('has the placeholder "input-field-placeholder"', () => {
|
||||
expect(wrapper.find('textarea').attributes('placeholder')).toEqual(
|
||||
'input-field-placeholder',
|
||||
)
|
||||
})
|
||||
|
||||
it('has the value ""', () => {
|
||||
expect(wrapper.vm.currentValue).toEqual('')
|
||||
})
|
||||
|
||||
it('has the label "input-field-label"', () => {
|
||||
expect(wrapper.find('label').text()).toEqual('input-field-label')
|
||||
})
|
||||
|
||||
it('has the label for "input-field-name-input-field"', () => {
|
||||
expect(wrapper.find('label').attributes('for')).toEqual('input-field-name-input-field')
|
||||
})
|
||||
})
|
||||
|
||||
describe('input value changes', () => {
|
||||
it('emits input with new value', async () => {
|
||||
await wrapper.find('textarea').setValue('Long enough')
|
||||
expect(wrapper.emitted('input')).toBeTruthy()
|
||||
expect(wrapper.emitted('input')).toEqual([['Long enough']])
|
||||
})
|
||||
})
|
||||
|
||||
describe('value property changes', () => {
|
||||
it('updates data model', async () => {
|
||||
await wrapper.setProps({ value: 'new text message' })
|
||||
expect(wrapper.vm.currentValue).toEqual('new text message')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
61
frontend/src/components/Inputs/InputTextarea.vue
Normal file
61
frontend/src/components/Inputs/InputTextarea.vue
Normal file
@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<validation-provider
|
||||
tag="div"
|
||||
:rules="rules"
|
||||
:name="name"
|
||||
v-slot="{ errors, valid, validated, ariaInput, ariaMsg }"
|
||||
>
|
||||
<b-form-group :label="label" :label-for="labelFor" data-test="input-textarea">
|
||||
<b-form-textarea
|
||||
v-model="currentValue"
|
||||
v-bind="ariaInput"
|
||||
:id="labelFor"
|
||||
class="bg-248"
|
||||
:name="name"
|
||||
:placeholder="placeholder"
|
||||
:state="validated ? valid : false"
|
||||
trim
|
||||
rows="4"
|
||||
max-rows="4"
|
||||
:disabled="disabled"
|
||||
></b-form-textarea>
|
||||
<b-form-invalid-feedback v-bind="ariaMsg">
|
||||
{{ errors[0] }}
|
||||
</b-form-invalid-feedback>
|
||||
</b-form-group>
|
||||
</validation-provider>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'InputTextarea',
|
||||
props: {
|
||||
rules: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
name: { type: String, required: true },
|
||||
label: { type: String, required: true },
|
||||
placeholder: { type: String, required: true },
|
||||
value: { type: String, required: true },
|
||||
disabled: { required: false, type: Boolean, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentValue: '',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labelFor() {
|
||||
return this.name + '-input-field'
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentValue() {
|
||||
this.$emit('input', this.currentValue)
|
||||
},
|
||||
value() {
|
||||
if (this.value !== this.currentValue) this.currentValue = this.value
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
40
frontend/src/components/Inputs/Job.NEW
Normal file
40
frontend/src/components/Inputs/Job.NEW
Normal file
@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div role="group" class="input-job">
|
||||
<label for="input-lastName"></label>
|
||||
<b-form-input
|
||||
id="input-job"
|
||||
v-model="job"
|
||||
:state="jobState"
|
||||
aria-describedby="input-live-help input-live-feedback"
|
||||
placeholder="Enter your Job"
|
||||
trim
|
||||
></b-form-input>
|
||||
|
||||
<!-- This will only be shown if the preceding input has an invalid state -->
|
||||
<!-- <b-form-invalid-feedback id="input-live-feedback">
|
||||
Enter at least 3 letters
|
||||
</b-form-invalid-feedback> -->
|
||||
|
||||
<!-- This is a form text block (formerly known as help block) -->
|
||||
<!-- <b-form-text id="input-live-help">Was ist dein Beruf</b-form-text> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Job',
|
||||
props: {
|
||||
value: { type: String, default: '' },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
job: this.value,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
jobState() {
|
||||
return this.job.length > 2
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
38
frontend/src/components/Inputs/LastName.spec.js
Normal file
38
frontend/src/components/Inputs/LastName.spec.js
Normal file
@ -0,0 +1,38 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import LastName from './LastName'
|
||||
|
||||
const localVue = global.localVue
|
||||
|
||||
describe('LastName', () => {
|
||||
let wrapper
|
||||
|
||||
const mocks = {
|
||||
$t: jest.fn((t) => t),
|
||||
$i18n: {
|
||||
locale: jest.fn(() => 'en'),
|
||||
},
|
||||
$n: jest.fn((n) => String(n)),
|
||||
}
|
||||
|
||||
const propsData = {
|
||||
balance: 0.0,
|
||||
}
|
||||
|
||||
const Wrapper = () => {
|
||||
return mount(LastName, {
|
||||
localVue,
|
||||
mocks,
|
||||
propsData,
|
||||
})
|
||||
}
|
||||
|
||||
describe('mount', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('renders the component', () => {
|
||||
expect(wrapper.find('div.last-name').exists()).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
40
frontend/src/components/Inputs/LastName.vue
Normal file
40
frontend/src/components/Inputs/LastName.vue
Normal file
@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div role="group" class="last-name">
|
||||
<label for="input-lastName">{{ $t('form.lastname') }}</label>
|
||||
<b-form-input
|
||||
id="input-lastName"
|
||||
v-model="lastName"
|
||||
:state="lastNameState"
|
||||
aria-describedby="input-live-help input-live-feedback"
|
||||
placeholder="Enter your lastName"
|
||||
trim
|
||||
></b-form-input>
|
||||
|
||||
<!-- This will only be shown if the preceding input has an invalid state -->
|
||||
<!-- <b-form-invalid-feedback id="input-live-feedback">
|
||||
Enter at least 3 letters
|
||||
</b-form-invalid-feedback> -->
|
||||
|
||||
<!-- This is a form text block (formerly known as help block) -->
|
||||
<!-- <b-form-text id="input-live-help">Dein Nachname</b-form-text> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'lastName',
|
||||
props: {
|
||||
value: { type: String, default: '' },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
lastName: this.value,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
lastNameState() {
|
||||
return this.lastName.length > 2
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user