diff --git a/backend/.eslintrc.js b/backend/.eslintrc.js index e853c8bf6..a27eba9a2 100644 --- a/backend/.eslintrc.js +++ b/backend/.eslintrc.js @@ -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', diff --git a/backend/src/graphql/resolver/ContributionMessageResolver.test.ts b/backend/src/graphql/resolver/ContributionMessageResolver.test.ts index 2df996c64..12df7f056 100644 --- a/backend/src/graphql/resolver/ContributionMessageResolver.test.ts +++ b/backend/src/graphql/resolver/ContributionMessageResolver.test.ts @@ -485,7 +485,7 @@ describe('ContributionMessageResolver', () => { }) }) - describe('authenticated', () => { + describe('authenticated as admin', () => { beforeAll(async () => { await mutate({ mutation: login, diff --git a/dht-node/package.json b/dht-node/package.json index 049d0373f..564eed726 100644 --- a/dht-node/package.json +++ b/dht-node/package.json @@ -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" diff --git a/e2e-tests/.eslintignore b/e2e-tests/.eslintignore index 3c3629e64..48192fb7f 100644 --- a/e2e-tests/.eslintignore +++ b/e2e-tests/.eslintignore @@ -1 +1,2 @@ node_modules +playwright diff --git a/e2e-tests/.eslintrc.js b/e2e-tests/.eslintrc.js index 98f13d176..49c60ba99 100644 --- a/e2e-tests/.eslintrc.js +++ b/e2e-tests/.eslintrc.js @@ -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', ], diff --git a/e2e-tests/cypress.config.ts b/e2e-tests/cypress.config.ts index e26259626..1424ab99f 100644 --- a/e2e-tests/cypress.config.ts +++ b/e2e-tests/cypress.config.ts @@ -6,7 +6,7 @@ let emailLink: string async function setupNodeEvents( on: Cypress.PluginEvents, - config: Cypress.PluginConfigOptions + config: Cypress.PluginConfigOptions, ): Promise { await addCucumberPreprocessorPlugin(on, config) @@ -14,7 +14,7 @@ async function setupNodeEvents( 'file:preprocessor', browserify(config, { typescript: require.resolve('typescript'), - }) + }), ) on('task', { diff --git a/e2e-tests/cypress/e2e/SendCoins.feature b/e2e-tests/cypress/e2e/SendCoins.feature new file mode 100644 index 000000000..00267d331 --- /dev/null +++ b/e2e-tests/cypress/e2e/SendCoins.feature @@ -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 "" "" "" + And the user submits the send form + Then the transaction details are presented for confirmation "" "" "" "" "" + When the user submits the transaction by confirming + Then the "" and "" are displayed on the "send" page + When the user navigates to page "/transactions" + Then the "" and "" 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 "" GDD from "" + When the user opens the "transaction" link in the browser + Then the "" and "120.50" are displayed on the "overview" page + When the user navigates to page "/transactions" + Then the "" and "120.50" are displayed on the "transactions" page + + Examples: + | senderName | amount | + | Bob der Baumeister | 120,50 | + diff --git a/e2e-tests/cypress/e2e/models/OverviewPage.ts b/e2e-tests/cypress/e2e/models/OverviewPage.ts index 345124c66..1ee9fa47b 100644 --- a/e2e-tests/cypress/e2e/models/OverviewPage.ts +++ b/e2e-tests/cypress/e2e/models/OverviewPage.ts @@ -2,6 +2,7 @@ export class OverviewPage { navbarName = '[data-test="navbar-item-username"]' + rightLastTransactionsList = '.rightside-last-transactions' goto() { cy.visit('/overview') diff --git a/e2e-tests/cypress/e2e/models/ResetPasswordPage.ts b/e2e-tests/cypress/e2e/models/ResetPasswordPage.ts index 153b41a82..7131be3c8 100644 --- a/e2e-tests/cypress/e2e/models/ResetPasswordPage.ts +++ b/e2e-tests/cypress/e2e/models/ResetPasswordPage.ts @@ -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 } diff --git a/e2e-tests/cypress/e2e/models/SendPage.ts b/e2e-tests/cypress/e2e/models/SendPage.ts new file mode 100644 index 000000000..25bf8c02b --- /dev/null +++ b/e2e-tests/cypress/e2e/models/SendPage.ts @@ -0,0 +1,25 @@ +/// + +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() + } +} diff --git a/e2e-tests/cypress/e2e/models/UserEMailSite.ts b/e2e-tests/cypress/e2e/models/UserEMailSite.ts index f46f5677b..af5deefeb 100644 --- a/e2e-tests/cypress/e2e/models/UserEMailSite.ts +++ b/e2e-tests/cypress/e2e/models/UserEMailSite.ts @@ -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') } } diff --git a/e2e-tests/cypress/support/index.ts b/e2e-tests/cypress/support/index.ts index f8d1abacf..6224f0f0e 100644 --- a/e2e-tests/cypress/support/index.ts +++ b/e2e-tests/cypress/support/index.ts @@ -7,6 +7,7 @@ import './e2e' declare global { namespace Cypress { + // eslint-disable-next-line @typescript-eslint/no-unused-vars interface Chainable { login(email: string, password: string): Chainable } diff --git a/e2e-tests/cypress/support/step_definitions/common_steps.ts b/e2e-tests/cypress/support/step_definitions/common_steps.ts index c5d3004ac..c2128a308 100644 --- a/e2e-tests/cypress/support/step_definitions/common_steps.ts +++ b/e2e-tests/cypress/support/step_definitions/common_steps.ts @@ -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() diff --git a/e2e-tests/cypress/support/step_definitions/email_steps.ts b/e2e-tests/cypress/support/step_definitions/email_steps.ts index d31e2474e..a2e6dbcb8 100644 --- a/e2e-tests/cypress/support/step_definitions/email_steps.ts +++ b/e2e-tests/cypress/support/step_definitions/email_steps.ts @@ -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}"`, + ) + } }) diff --git a/e2e-tests/cypress/support/step_definitions/send_coin_steps.ts b/e2e-tests/cypress/support/step_definitions/send_coin_steps.ts new file mode 100644 index 000000000..d26733221 --- /dev/null +++ b/e2e-tests/cypress/support/step_definitions/send_coin_steps.ts @@ -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}"`, + ) + } + }, +) diff --git a/e2e-tests/cypress/support/step_definitions/user_authentication_steps.ts b/e2e-tests/cypress/support/step_definitions/user_authentication_steps.ts index 5b25f5391..f96499409 100644 --- a/e2e-tests/cypress/support/step_definitions/user_authentication_steps.ts +++ b/e2e-tests/cypress/support/step_definitions/user_authentication_steps.ts @@ -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 diff --git a/e2e-tests/cypress/support/step_definitions/user_profile_change_password_steps.ts b/e2e-tests/cypress/support/step_definitions/user_profile_change_password_steps.ts index 1dcbe69ef..c08236cde 100644 --- a/e2e-tests/cypress/support/step_definitions/user_profile_change_password_steps.ts +++ b/e2e-tests/cypress/support/step_definitions/user_profile_change_password_steps.ts @@ -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) diff --git a/e2e-tests/cypress/support/step_definitions/user_registration_steps.ts b/e2e-tests/cypress/support/step_definitions/user_registration_steps.ts index e90956b6e..df4972077 100644 --- a/e2e-tests/cypress/support/step_definitions/user_registration_steps.ts +++ b/e2e-tests/cypress/support/step_definitions/user_registration_steps.ts @@ -10,7 +10,7 @@ When( registrationPage.enterFirstname(firstname) registrationPage.enterLastname(lastname) registrationPage.enterEmail(email) - } + }, ) And('the user agrees to the privacy policy', () => { diff --git a/frontend/src/components/DecayInformations/DecayInformation-Long.vue b/frontend/src/components/DecayInformations/DecayInformation-Long.vue index d7e943225..d4ff66af0 100644 --- a/frontend/src/components/DecayInformations/DecayInformation-Long.vue +++ b/frontend/src/components/DecayInformations/DecayInformation-Long.vue @@ -1,5 +1,5 @@