Merge branch 'master' into message-type-admin-frontend

This commit is contained in:
Moriz Wahl 2023-06-28 13:35:55 +02:00 committed by GitHub
commit 9c5543ad22
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 279 additions and 71 deletions

View File

@ -197,6 +197,9 @@ module.exports = {
{
files: ['*.test.ts'],
plugins: ['jest'],
env: {
jest: true,
},
rules: {
'jest/no-disabled-tests': 'error',
'jest/no-focused-tests': 'error',

View File

@ -485,7 +485,7 @@ describe('ContributionMessageResolver', () => {
})
})
describe('authenticated', () => {
describe('authenticated as admin', () => {
beforeAll(async () => {
await mutate({
mutation: login,

View File

@ -21,9 +21,7 @@
"dotenv": "10.0.0",
"log4js": "^6.7.1",
"nodemon": "^2.0.20",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.1.2",
"typescript": "^4.9.4",
"uuid": "^8.3.2"
},
"devDependencies": {
@ -46,7 +44,9 @@
"eslint-plugin-security": "^1.7.1",
"prettier": "^2.8.7",
"jest": "^27.2.4",
"ts-jest": "^27.0.5"
"ts-jest": "^27.0.5",
"ts-node": "^10.9.1",
"typescript": "^4.9.4"
},
"engines": {
"node": ">=14"

View File

@ -1 +1,2 @@
node_modules
playwright

View File

@ -2,13 +2,13 @@ module.exports = {
root: true,
env: {
node: true,
cypress: true,
},
parser: '@typescript-eslint/parser',
plugins: ['cypress', 'prettier', '@typescript-eslint' /*, 'jest' */],
extends: [
'standard',
'eslint:recommended',
'plugin:cypress/recommended',
'plugin:prettier/recommended',
'plugin:@typescript-eslint/recommended',
],

View File

@ -6,7 +6,7 @@ let emailLink: string
async function setupNodeEvents(
on: Cypress.PluginEvents,
config: Cypress.PluginConfigOptions
config: Cypress.PluginConfigOptions,
): Promise<Cypress.PluginConfigOptions> {
await addCucumberPreprocessorPlugin(on, config)
@ -14,7 +14,7 @@ async function setupNodeEvents(
'file:preprocessor',
browserify(config, {
typescript: require.resolve('typescript'),
})
}),
)
on('task', {

View File

@ -0,0 +1,38 @@
Feature: Send coins
As a user
I want to send and receive GDD
I want to see transaction details on overview and transactions pages
# Background:
# Given the following "users" are in the database:
# | email | password | name |
# | bob@baumeister.de | Aa12345_ | Bob Baumeister |
# | raeuber@hotzenplotz.de | Aa12345_ | Räuber Hotzenplotz |
Scenario: Send GDD to other user
Given the user is logged in as "bob@baumeister.de" "Aa12345_"
And the user navigates to page "/send"
When the user fills the send form with "<receiverEmail>" "<amount>" "<memoText>"
And the user submits the send form
Then the transaction details are presented for confirmation "<receiverEmail>" "<amount>" "<memoText>" "<senderBalance>" "<newSenderBalance>"
When the user submits the transaction by confirming
Then the "<receiverName>" and "<amount>" are displayed on the "send" page
When the user navigates to page "/transactions"
Then the "<receiverName>" and "<amount>" are displayed on the "transactions" page
Examples:
| receiverName | receiverEmail | amount | memoText | senderBalance | newSenderBalance |
| Räuber Hotzenplotz | raeuber@hotzenplotz.de | 120.50 | Some memo text | 515.11 | 394.61 |
Scenario: Receive GDD from other user
Given the user is logged in as "raeuber@hotzenplotz.de" "Aa12345_"
And the user receives the transaction e-mail about "<amount>" GDD from "<senderName>"
When the user opens the "transaction" link in the browser
Then the "<senderName>" and "120.50" are displayed on the "overview" page
When the user navigates to page "/transactions"
Then the "<senderName>" and "120.50" are displayed on the "transactions" page
Examples:
| senderName | amount |
| Bob der Baumeister | 120,50 |

View File

@ -2,6 +2,7 @@
export class OverviewPage {
navbarName = '[data-test="navbar-item-username"]'
rightLastTransactionsList = '.rightside-last-transactions'
goto() {
cy.visit('/overview')

View File

@ -14,9 +14,7 @@ export class ResetPasswordPage {
}
repeatNewPassword(password: string) {
cy.get(this.newPasswordRepeatInput)
.find('input[type=password]')
.type(password)
cy.get(this.newPasswordRepeatInput).find('input[type=password]').type(password)
return this
}

View File

@ -0,0 +1,25 @@
/// <reference types='cypress' />
export class SendPage {
confirmationBox = '.transaction-confirm-send'
submitBtn = '.btn-gradido'
enterReceiverEmail(email: string) {
cy.get('[data-test="input-identifier"]').find('input').clear().type(email)
return this
}
enterAmount(amount: string) {
cy.get('[data-test="input-amount"]').find('input').clear().type(amount)
return this
}
enterMemoText(text: string) {
cy.get('[data-test="input-textarea"]').find('textarea').clear().type(text)
return this
}
submit() {
cy.get(this.submitBtn).click()
}
}

View File

@ -8,10 +8,7 @@ export class UserEMailSite {
emailSubject = '.subject'
openRecentPasswordResetEMail() {
cy.get(this.emailList)
.find('email-item')
.filter(':contains(asswor)')
.click()
cy.get(this.emailList).find('email-item').filter(':contains(asswor)').click()
expect(cy.get(this.emailSubject)).to('contain', 'asswor')
}
}

View File

@ -7,6 +7,7 @@ import './e2e'
declare global {
namespace Cypress {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Chainable<Subject> {
login(email: string, password: string): Chainable<any>
}

View File

@ -1,4 +1,4 @@
import { Given, Then, When } from '@badeball/cypress-cucumber-preprocessor'
import { Given, Then } from '@badeball/cypress-cucumber-preprocessor'
import { OverviewPage } from '../../e2e/models/OverviewPage'
import { SideNavMenu } from '../../e2e/models/SideNavMenu'
import { Toasts } from '../../e2e/models/Toasts'
@ -9,12 +9,9 @@ Given('the user navigates to page {string}', (page: string) => {
// login related
Given(
'the user is logged in as {string} {string}',
(email: string, password: string) => {
cy.login(email, password)
}
)
Given('the user is logged in as {string} {string}', (email: string, password: string) => {
cy.login(email, password)
})
Then('the user is logged in with username {string}', (username: string) => {
const overviewPage = new OverviewPage()

View File

@ -1,9 +1,9 @@
import { Then, When } from '@badeball/cypress-cucumber-preprocessor'
import { OverviewPage } from '../../e2e/models/OverviewPage'
import { ResetPasswordPage } from '../../e2e/models/ResetPasswordPage'
import { UserEMailSite } from '../../e2e/models/UserEMailSite'
const userEMailSite = new UserEMailSite()
const resetPasswordPage = new ResetPasswordPage()
Then('the user receives an e-mail containing the {string} link', (linkName: string) => {
let emailSubject: string
@ -18,14 +18,20 @@ Then('the user receives an e-mail containing the {string} link', (linkName: stri
emailSubject = 'asswor'
linkPattern = /\/reset-password\/[0-9]+\d/
break
case 'transaction':
emailSubject = 'Gradido gesendet'
linkPattern = /\/overview/
break
default:
throw new Error(`Error in "Then the user receives an e-mail containing the {string} link" step: incorrect linkname string "${linkName}"`)
throw new Error(
`Error in "Then the user receives an e-mail containing the {string} link" step: incorrect linkname string "${linkName}"`,
)
}
cy.origin(
Cypress.env('mailserverURL'),
{ args: { emailSubject, linkPattern, userEMailSite } },
({ emailSubject, linkPattern, userEMailSite }) => {
({ emailSubject, linkPattern, userEMailSite }) => {
cy.visit('/') // navigate to user's e-mail site (on fake mail server)
cy.get(userEMailSite.emailInbox).should('be.visible')
@ -35,11 +41,9 @@ Then('the user receives an e-mail containing the {string} link', (linkName: stri
.first()
.click()
cy.get(userEMailSite.emailMeta)
.find(userEMailSite.emailSubject)
.contains(emailSubject)
cy.get(userEMailSite.emailMeta).find(userEMailSite.emailSubject).contains(emailSubject)
cy.get('.email-content', { timeout: 2000})
cy.get('.email-content', { timeout: 2000 })
.find('.plain-text')
.contains(linkPattern)
.invoke('text')
@ -47,13 +51,64 @@ Then('the user receives an e-mail containing the {string} link', (linkName: stri
const emailLink = text.match(linkPattern)[0]
cy.task('setEmailLink', emailLink)
})
}
},
)
})
When(
'the user receives the transaction e-mail about {string} GDD from {string}',
(amount: string, senderName: string) => {
cy.origin(
Cypress.env('mailserverURL'),
{ args: { amount, senderName, userEMailSite } },
({ amount, senderName, userEMailSite }) => {
const subject = `${senderName} hat dir ${amount} Gradido gesendet`
const linkPattern = /\/transactions/
cy.visit('/')
cy.get(userEMailSite.emailInbox).should('be.visible')
cy.get(userEMailSite.emailList)
.find('.email-item')
.filter(`:contains(${subject})`)
.first()
.click()
cy.get(userEMailSite.emailMeta).find(userEMailSite.emailSubject).contains(subject)
cy.get('.email-content', { timeout: 2000 })
.find('.plain-text')
.contains(linkPattern)
.invoke('text')
.then((text) => {
const emailLink = text.match(linkPattern)[0]
cy.task('setEmailLink', emailLink)
})
},
)
},
)
When('the user opens the {string} link in the browser', (linkName: string) => {
const resetPasswordPage = new ResetPasswordPage()
cy.task('getEmailLink').then((emailLink) => {
cy.visit(emailLink)
})
cy.get(resetPasswordPage.newPasswordInput).should('be.visible')
switch (linkName) {
case 'activation':
cy.get(resetPasswordPage.newPasswordInput).should('be.visible')
break
case 'password reset':
cy.get(resetPasswordPage.newPasswordInput).should('be.visible')
break
case 'transaction':
// eslint-disable-next-line no-case-declarations
const overviewPage = new OverviewPage()
cy.get(overviewPage.rightLastTransactionsList).should('be.visible')
break
default:
throw new Error(
`Error in "Then the user receives an e-mail containing the {string} link" step: incorrect link name string "${linkName}"`,
)
}
})

View File

@ -0,0 +1,90 @@
import { And, Then, When } from '@badeball/cypress-cucumber-preprocessor'
import { SendPage } from '../../e2e/models/SendPage'
const sendPage = new SendPage()
When(
'the user fills the send form with {string} {string} {string}',
(email: string, amount: string, memoText: string) => {
sendPage.enterReceiverEmail(email)
sendPage.enterAmount(amount)
sendPage.enterMemoText(memoText)
},
)
And('the user submits the send form', () => {
sendPage.submit()
cy.get(sendPage.confirmationBox).should('be.visible')
})
Then(
'the transaction details are presented for confirmation {string} {string} {string} {string} {string}',
(
receiverEmail: string,
sendAmount: string,
memoText: string,
senderBalance: string,
newSenderBalance: string,
) => {
cy.get('.transaction-confirm-send').contains(receiverEmail)
cy.get('.transaction-confirm-send').contains(`+ ${sendAmount} GDD`)
cy.get('.transaction-confirm-send').contains(memoText)
cy.get('.transaction-confirm-send').contains(`+ ${senderBalance} GDD`)
cy.get('.transaction-confirm-send').contains(` ${sendAmount} GDD`)
cy.get('.transaction-confirm-send').contains(`+ ${newSenderBalance} GDD`)
},
)
When('the user submits the transaction by confirming', () => {
cy.intercept({
method: 'POST',
url: '/graphql',
hostname: 'localhost',
}).as('sendCoins')
sendPage.submit()
cy.wait('@sendCoins').then((interception) => {
cy.wrap(interception.response?.statusCode).should('eq', 200)
cy.wrap(interception.request.body).should(
'have.property',
'query',
`mutation ($identifier: String!, $amount: Decimal!, $memo: String!) {
sendCoins(identifier: $identifier, amount: $amount, memo: $memo)
}
`,
)
cy.wrap(interception.response?.body)
.should('have.nested.property', 'data.sendCoins')
.and('equal', true)
})
cy.get('[data-test="send-transaction-success-text"]').should('be.visible')
})
Then(
'the {string} and {string} are displayed on the {string} page',
(name: string, amount: string, page: string) => {
switch (page) {
case 'overview':
cy.get('.align-items-center').contains(`${name}`)
cy.get('.align-items-center').contains(`${amount} GDD`)
break
case 'send':
cy.get('.align-items-center').contains(`${name}`)
cy.get('.align-items-center').contains(`${amount} GDD`)
break
case 'transactions':
cy.get('div.mt-3 > div > div.test-list-group-item')
.eq(0)
.contains('div.gdd-transaction-list-item-name', `${name}`)
cy.get('div.mt-3 > div > div.test-list-group-item')
.eq(0)
.contains('[data-test="transaction-amount"]', `${amount} GDD`)
break
default:
throw new Error(
`Error in "Then the {string} and {string} are displayed on the {string}} page" step: incorrect page name string "${page}"`,
)
}
},
)

View File

@ -13,26 +13,21 @@ When('the user submits no credentials', () => {
loginPage.submitLogin()
})
When(
'the user submits the credentials {string} {string}',
(email: string, password: string) => {
cy.intercept('POST', '/graphql', (req) => {
if (
req.body.hasOwnProperty('query') &&
req.body.query.includes('mutation')
) {
req.alias = 'login'
}
})
When('the user submits the credentials {string} {string}', (email: string, password: string) => {
cy.intercept('POST', '/graphql', (req) => {
// eslint-disable-next-line no-prototype-builtins
if (req.body.hasOwnProperty('query') && req.body.query.includes('mutation')) {
req.alias = 'login'
}
})
loginPage.enterEmail(email)
loginPage.enterPassword(password)
loginPage.submitLogin()
cy.wait('@login').then((interception) => {
expect(interception.response.statusCode).equals(200)
})
}
)
loginPage.enterEmail(email)
loginPage.enterPassword(password)
loginPage.submitLogin()
cy.wait('@login').then((interception) => {
expect(interception.response.statusCode).equals(200)
})
})
// password reset related

View File

@ -1,4 +1,4 @@
import { And, When } from '@badeball/cypress-cucumber-preprocessor'
import { And, DataTable, When } from '@badeball/cypress-cucumber-preprocessor'
import { ProfilePage } from '../../e2e/models/ProfilePage'
import { Toasts } from '../../e2e/models/Toasts'
@ -10,8 +10,8 @@ And('the user opens the change password menu', () => {
cy.get(profilePage.submitNewPasswordBtn).should('be.disabled')
})
When('the user fills the password form with:', (table) => {
let hashedTableRows = table.rowsHash()
When('the user fills the password form with:', (table: DataTable) => {
const hashedTableRows = table.rowsHash()
profilePage.enterOldPassword(hashedTableRows['Old password'])
profilePage.enterNewPassword(hashedTableRows['New password'])
profilePage.enterRepeatPassword(hashedTableRows['Repeat new password'])
@ -22,7 +22,7 @@ And('the user submits the password form', () => {
profilePage.submitPasswordForm()
})
When('the user is presented a {string} message', (type: string) => {
When('the user is presented a {string} message', () => {
const toast = new Toasts()
cy.get(toast.toastSlot).within(() => {
cy.get(toast.toastTypeSuccess)

View File

@ -10,7 +10,7 @@ When(
registrationPage.enterFirstname(firstname)
registrationPage.enterLastname(lastname)
registrationPage.enterEmail(email)
}
},
)
And('the user agrees to the privacy policy', () => {

View File

@ -1,5 +1,5 @@
<template>
<div class="decayinformation-long px-2">
<div class="decayinformation-long px-1">
<div class="word-break mb-5 mt-lg-3">
<div class="font-weight-bold pb-2">{{ $t('form.memo') }}</div>
<div class="">{{ memo }}</div>
@ -11,10 +11,10 @@
<b-row>
<b-col>
<b-row>
<b-col cols="12" lg="4" md="4">
<b-col cols="6" lg="4" md="6" sm="6">
<div>{{ $t('decay.last_transaction') }}</div>
</b-col>
<b-col offset="1" offset-md="0" offset-lg="0" class="text-right mr-5">
<b-col offset="0" class="text-right mr-0">
<div>
<span>
{{ $d(new Date(decay.start), 'long') }}
@ -26,20 +26,20 @@
<!-- Previous Balance -->
<b-row class="mt-2">
<b-col cols="12" lg="6" md="3">
<b-col cols="6" lg="4" md="6" sm="6">
<div>{{ $t('decay.old_balance') }}</div>
</b-col>
<b-col offset="1" offset-md="0" offset-lg="0" class="text-right mr-5">
<b-col offset="0" class="text-right mr-0">
{{ previousBalance | GDD }}
</b-col>
</b-row>
<!-- Decay-->
<b-row class="mt-0">
<b-col cols="12" lg="3" md="3">
<b-col cols="6" lg="3" md="6" sm="6">
<div>{{ $t('decay.decay') }}</div>
</b-col>
<b-col offset="1" offset-md="0" offset-lg="0" class="text-right mr-5">
<b-col offset="0" class="text-right mr-0">
{{ decay.decay | GDD }}
</b-col>
</b-row>
@ -49,18 +49,21 @@
<b-row>
<b-col>
<b-row class="mb-2">
<!-- eslint-disable-next-line @intlify/vue-i18n/no-dynamic-keys-->
<b-col cols="12" lg="3" md="3">{{ $t(`decay.types.${typeId.toLowerCase()}`) }}</b-col>
<b-col offset="1" offset-md="0" offset-lg="0" class="text-right mr-5">
<!-- eslint-disable @intlify/vue-i18n/no-dynamic-keys-->
<b-col cols="6" lg="3" md="6" sm="6">
{{ $t(`decay.types.${typeId.toLowerCase()}`) }}
</b-col>
<!-- eslint-enable @intlify/vue-i18n/no-dynamic-keys-->
<b-col offset="0" class="text-right mr-0">
{{ amount | GDD }}
</b-col>
</b-row>
<!-- Total-->
<b-row class="border-top pt-2">
<b-col cols="12" lg="3" md="3">
<b-col cols="6" lg="3" md="6" sm="6">
<div>{{ $t('decay.new_balance') }}</div>
</b-col>
<b-col offset="1" offset-md="0" offset-lg="0" class="text-right mr-5">
<b-col offset="0" class="text-right mr-0">
<b>{{ balance | GDD }}</b>
</b-col>
</b-row>

View File

@ -1,10 +1,10 @@
<template>
<div class="duration-row">
<b-row>
<b-col cols="12" lg="4" md="4">
<b-col cols="6" lg="4" md="6" sm="6">
<div>{{ $t('decay.past_time') }}</div>
</b-col>
<b-col offset="1" offset-md="0" offset-lg="0" class="text-right mr-5">
<b-col offset="0" class="text-right mr-0">
<span v-if="duration">{{ duration }}</span>
</b-col>
</b-row>

View File

@ -26,7 +26,9 @@
<div class="small mb-2">
{{ $t('decay.types.receive') }}
</div>
<div class="font-weight-bold gradido-global-color-accent">{{ amount | GDD }}</div>
<div class="font-weight-bold gradido-global-color-accent" data-test="transaction-amount">
{{ amount | GDD }}
</div>
<div v-if="linkId" class="small">
{{ $t('via_link') }}
<b-icon

View File

@ -25,7 +25,9 @@
<div class="small mb-2">
{{ $t('decay.types.send') }}
</div>
<div class="font-weight-bold text-140">{{ amount | GDD }}</div>
<div class="font-weight-bold text-140" data-test="transaction-amount">
{{ amount | GDD }}
</div>
<div v-if="linkId" class="small">
{{ $t('via_link') }}
<b-icon