Merge branch 'master' into logger_creations

This commit is contained in:
Ulf Gebhardt 2023-02-16 18:19:40 +01:00
commit aa0b3ad671
Signed by: ulfgebhardt
GPG Key ID: DA6B843E748679C9
80 changed files with 4693 additions and 1536 deletions

View File

@ -274,7 +274,7 @@ jobs:
run: docker run --rm gradido/admin:test yarn run lint run: docker run --rm gradido/admin:test yarn run lint
############################################################################## ##############################################################################
# JOB: STYLELINT ADMIN INTERFACE ############################################## # JOB: STYLELINT ADMIN INTERFACE #############################################
############################################################################## ##############################################################################
stylelint_admin: stylelint_admin:
name: Stylelint - Admin Interface name: Stylelint - Admin Interface
@ -577,7 +577,7 @@ jobs:
end-to-end-tests: end-to-end-tests:
name: End-to-End Tests name: End-to-End Tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build_test_mariadb, build_test_database_up, build_test_backend, build_test_admin, build_test_frontend, build_test_nginx] needs: [build_test_mariadb, build_test_database_up, build_test_admin, build_test_frontend, build_test_nginx]
steps: steps:
########################################################################## ##########################################################################
# CHECKOUT CODE ########################################################## # CHECKOUT CODE ##########################################################
@ -601,13 +601,6 @@ jobs:
path: /tmp path: /tmp
- name: Load Docker Image (Database Up) - name: Load Docker Image (Database Up)
run: docker load < /tmp/database_up.tar run: docker load < /tmp/database_up.tar
- name: Download Docker Image (Backend)
uses: actions/download-artifact@v3
with:
name: docker-backend-test
path: /tmp
- name: Load Docker Image (Backend)
run: docker load < /tmp/backend.tar
- name: Download Docker Image (Frontend) - name: Download Docker Image (Frontend)
uses: actions/download-artifact@v3 uses: actions/download-artifact@v3
with: with:
@ -640,7 +633,11 @@ jobs:
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps database run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps database
- name: Boot up test system | docker-compose backend - name: Boot up test system | docker-compose backend
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps backend run: |
cd backend
cp .env.test_e2e .env
cd ..
docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps backend
- name: Sleep for 10 seconds - name: Sleep for 10 seconds
run: sleep 10s run: sleep 10s
@ -657,6 +654,9 @@ jobs:
- name: Boot up test system | docker-compose frontends - name: Boot up test system | docker-compose frontends
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps frontend admin nginx run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps frontend admin nginx
- name: Boot up test system | docker-compose mailserver
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mailserver
- name: Sleep for 15 seconds - name: Sleep for 15 seconds
run: sleep 15s run: sleep 15s
@ -666,12 +666,12 @@ jobs:
- name: End-to-end tests | run tests - name: End-to-end tests | run tests
id: e2e-tests id: e2e-tests
run: | run: |
cd e2e-tests/cypress/tests/ cd e2e-tests/
yarn yarn
yarn run cypress run --spec cypress/e2e/User.Authentication.feature yarn run cypress run --spec cypress/e2e/User.Authentication.feature,cypress/e2e/User.Authentication.ResetPassword.feature
- name: End-to-end tests | if tests failed, upload screenshots - name: End-to-end tests | if tests failed, upload screenshots
if: steps.e2e-tests.outcome == 'failure' if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: cypress-screenshots name: cypress-screenshots
path: /home/runner/work/gradido/gradido/e2e-tests/cypress/tests/cypress/screenshots/ path: /home/runner/work/gradido/gradido/e2e-tests/cypress/screenshots/

98
.github/workflows/test_federation.yml vendored Normal file
View File

@ -0,0 +1,98 @@
name: gradido test_federation CI
on: push
jobs:
##############################################################################
# JOB: DOCKER BUILD TEST #####################################################
##############################################################################
build:
name: Docker Build Test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Build `test` image
run: |
docker build --target test -t "gradido/federation:test" -f federation/Dockerfile .
docker save "gradido/federation:test" > /tmp/federation.tar
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: docker-federation-test
path: /tmp/federation.tar
##############################################################################
# JOB: LINT ##################################################################
##############################################################################
lint:
name: Lint
runs-on: ubuntu-latest
needs: [build]
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Download Docker Image
uses: actions/download-artifact@v3
with:
name: docker-federation-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/federation.tar
- name: Lint
run: docker run --rm gradido/federation:test yarn run lint
##############################################################################
# JOB: UNIT TEST #############################################################
##############################################################################
unit_test:
name: Unit tests
runs-on: ubuntu-latest
needs: [build]
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Download Docker Image
uses: actions/download-artifact@v3
with:
name: docker-federation-test
path: /tmp
- name: Load Docker Image
run: docker load < /tmp/federation.tar
- name: docker-compose mariadb
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps mariadb
- name: Sleep for 30 seconds
run: sleep 30s
shell: bash
- name: docker-compose database
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps database
- name: Sleep for 30 seconds
run: sleep 30s
shell: bash
#- name: Unit tests
# run: cd database && yarn && yarn build && cd ../dht-node && yarn && yarn test
- name: Unit tests
run: |
docker run --env NODE_ENV=test --env DB_HOST=mariadb --network gradido_internal-net -v ~/coverage:/app/coverage --rm gradido/federation:test yarn run test
cp -r ~/coverage ./coverage
- name: Coverage check
uses: webcraftmedia/coverage-check-action@master
with:
report_name: Coverage federation
type: lcov
#result_path: ./federation/coverage/lcov.info
result_path: ./coverage/lcov.info
min_coverage: 72
token: ${{ github.token }}

9
backend/.env.test_e2e Normal file
View File

@ -0,0 +1,9 @@
# Server
JWT_EXPIRES_IN=1m
# Email
EMAIL=true
EMAIL_TEST_MODUS=false
EMAIL_TLS=false
# for testing password reset
EMAIL_CODE_REQUEST_TIME=1

File diff suppressed because it is too large Load Diff

View File

@ -16,7 +16,7 @@ export const nMonthsBefore = (date: Date, months = 1): string => {
export const creationFactory = async ( export const creationFactory = async (
client: ApolloServerTestClient, client: ApolloServerTestClient,
creation: CreationInterface, creation: CreationInterface,
): Promise<Contribution | void> => { ): Promise<Contribution> => {
const { mutate } = client const { mutate } = client
await mutate({ mutation: login, variables: { email: creation.email, password: 'Aa12345_' } }) await mutate({ mutation: login, variables: { email: creation.email, password: 'Aa12345_' } })
@ -51,6 +51,7 @@ export const creationFactory = async (
await confirmedContribution.save() await confirmedContribution.save()
} }
} }
return confirmedContribution
} else { } else {
return contribution return contribution
} }

View File

@ -272,6 +272,12 @@ export const deleteContribution = gql`
} }
` `
export const denyContribution = gql`
mutation ($id: Int!) {
denyContribution(id: $id)
}
`
export const createContributionMessage = gql` export const createContributionMessage = gql`
mutation ($contributionId: Float!, $message: String!) { mutation ($contributionId: Float!, $message: String!) {
createContributionMessage(contributionId: $contributionId, message: $message) { createContributionMessage(contributionId: $contributionId, message: $message) {

View File

@ -166,6 +166,15 @@ export const listContributions = gql`
id id
amount amount
memo memo
createdAt
contributionDate
confirmedAt
confirmedBy
deletedAt
state
messagesCount
deniedAt
deniedBy
} }
} }
} }

View File

@ -32,15 +32,6 @@ export const startDHT = async (topic: string): Promise<void> => {
logger.debug(`keyPairDHT: secretKey=${keyPair.secretKey.toString('hex')}`) logger.debug(`keyPairDHT: secretKey=${keyPair.secretKey.toString('hex')}`)
const ownApiVersions = writeHomeCommunityEnries(keyPair.publicKey) const ownApiVersions = writeHomeCommunityEnries(keyPair.publicKey)
/*
const ownApiVersions = Object.values(ApiVersionType).map(function (apiEnum) {
const comApi: CommunityApi = {
api: apiEnum,
url: CONFIG.FEDERATION_COMMUNITY_URL,
}
return comApi
})
*/
logger.debug(`ApiList: ${JSON.stringify(ownApiVersions)}`) logger.debug(`ApiList: ${JSON.stringify(ownApiVersions)}`)
const node = new DHT({ keyPair }) const node = new DHT({ keyPair })
@ -216,6 +207,5 @@ async function writeHomeCommunityEnries(pubKey: any): Promise<CommunityApi[]> {
} catch (err) { } catch (err) {
throw new Error(`Federation: Error writing HomeCommunity-Entries: ${err}`) throw new Error(`Federation: Error writing HomeCommunity-Entries: ${err}`)
} }
return homeApiVersions return homeApiVersions
} }

View File

@ -84,6 +84,29 @@ services:
- ./dht-node:/app - ./dht-node:/app
- ./database:/database - ./database:/database
########################################################
# FEDERATION ###########################################
########################################################
federation:
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
image: gradido/federation:local-development
build:
target: development
networks:
- external-net
- internal-net
environment:
- NODE_ENV="development"
volumes:
# This makes sure the docker container has its own node modules.
# Therefore it is possible to have a different node version on the host machine
- federation_node_modules:/app/node_modules
- federation_database_node_modules:/database/node_modules
- federation_database_build:/database/build
# bind the local folder to the docker to allow live reload
- ./federation:/app
- ./database:/database
######################################################## ########################################################
# DATABASE ############################################## # DATABASE ##############################################
######################################################## ########################################################
@ -155,5 +178,8 @@ volumes:
dht_node_modules: dht_node_modules:
dht_database_node_modules: dht_database_node_modules:
dht_database_build: dht_database_build:
federation_node_modules:
federation_database_node_modules:
federation_database_build:
database_node_modules: database_node_modules:
database_build: database_build:

View File

@ -36,6 +36,21 @@ services:
- NODE_ENV="test" - NODE_ENV="test"
- DB_HOST=mariadb - DB_HOST=mariadb
########################################################
# FEDERATION ###########################################
########################################################
federation:
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
image: gradido/federation:test
build:
target: test
networks:
- external-net
- internal-net
environment:
- NODE_ENV="test"
- DB_HOST=mariadb
######################################################## ########################################################
# DATABASE ############################################# # DATABASE #############################################
######################################################## ########################################################

View File

@ -147,6 +147,42 @@ services:
# <host_machine_directory>:<container_directory> mirror bidirectional path in local context with path in Docker container # <host_machine_directory>:<container_directory> mirror bidirectional path in local context with path in Docker container
- ./logs/dht-node:/logs/dht-node - ./logs/dht-node:/logs/dht-node
########################################################
# FEDERATION ###########################################
########################################################
federation:
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
image: gradido/federation:local-production
build:
# since we have to include the entities from ./database we cannot define the context as ./federation
# this might blow build image size to the moon ?!
context: ./
dockerfile: ./federation/Dockerfile
target: production
networks:
- internal-net
- external-net
ports:
- 5010:5010
depends_on:
- mariadb
restart: always
environment:
# Envs used in Dockerfile
# - DOCKER_WORKDIR="/app"
- PORT=5010
- BUILD_DATE
- BUILD_VERSION
- BUILD_COMMIT
- NODE_ENV="production"
- DB_HOST=mariadb
# Application only envs
#env_file:
# - ./frontend/.env
volumes:
# <host_machine_directory>:<container_directory> mirror bidirectional path in local context with path in Docker container
- ./logs/federation:/logs/federation
######################################################## ########################################################
# DATABASE ############################################# # DATABASE #############################################
######################################################## ########################################################

26
e2e-tests/.eslintrc.js Normal file
View File

@ -0,0 +1,26 @@
module.exports = {
root: true,
env: {
node: true,
cypress: true,
},
parser: '@typescript-eslint/parser',
plugins: ['cypress', 'prettier', '@typescript-eslint' /*, 'jest' */],
extends: [
'standard',
'eslint:recommended',
'plugin:prettier/recommended',
'plugin:@typescript-eslint/recommended',
],
// add your custom rules here
rules: {
'no-console': ['error'],
'no-debugger': 'error',
'prettier/prettier': [
'error',
{
htmlWhitespaceSensitivity: 'ignore',
},
],
},
}

5
e2e-tests/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules/
cypress/screenshots/
cypress/videos/
cucumber-messages.ndjson

9
e2e-tests/.prettierrc.js Normal file
View File

@ -0,0 +1,9 @@
module.exports = {
semi: false,
printWidth: 100,
singleQuote: true,
trailingComma: "all",
tabWidth: 2,
bracketSpacing: true,
endOfLine: "auto",
};

View File

@ -11,7 +11,7 @@
############################################################################### ###############################################################################
FROM cypress/base:16.14.2-slim FROM cypress/base:16.14.2-slim
ARG DOCKER_WORKDIR=/tests/ ARG DOCKER_WORKDIR="/tests"
WORKDIR $DOCKER_WORKDIR WORKDIR $DOCKER_WORKDIR
# install dependencies # install dependencies

View File

@ -1,7 +1,73 @@
# Gradido end-to-end tests # Gradido End-to-End Testing with [Cypress](https://www.cypress.io/) (CI-ready via Docker)
This is still WIP. A setup to show-case Cypress as an end-to-end testing tool for Gradido running in a Docker container.
The tests are organized in feature files written in Gherkin syntax.
For automated end-to-end testing one of the frameworks Cypress or Playwright will be utilized.
For more details on how to run them, see the subfolders' README instructions. ## Features under test
So far these features are initially tested
- [User authentication](https://github.com/gradido/gradido/blob/master/e2e-tests/cypress/tests/cypress/e2e/User.Authentication.feature)
- [User profile - change password](https://github.com/gradido/gradido/blob/master/e2e-tests/cypress/tests/cypress/e2e/UserProfile.ChangePassword.feature)
- [User registration]((https://github.com/gradido/gradido/blob/master/e2e-tests/cypress/tests/cypress/e2e/User.Registration.feature)) (WIP)
## Precondition
Before running the tests, change to the repo's root directory (gradido).
### Boot up the system under test
```bash
docker-compose up
```
### Seed the database
The database has to be seeded upfront to every test run.
```bash
# change to the backend directory
cd /path/to/gradido/gradido/backend
# install all dependencies
yarn
# seed the database (everytime before running the tests)
yarn seed
```
## Execute the test
This setup will be integrated in the Gradido Github Actions to automatically support the CI/CD process.
For now the test setup can only be used locally in two modes.
### Run Cypress directly from the code
```bash
# change to the tests directory
cd /path/to/gradido/e2e-tests/
# install all dependencies
yarn install
# a) run the tests on command line
yarn cypress run
# b) open the Cypress GUI to run the tests in interactive mode
yarn cypress open
```
### Run Cyprss from a separate Docker container
```bash
# change to the cypress directory
cd /path/to/gradido/e2e-tests/
# build a Docker image from the Dockerfile
docker build -t gradido_e2e-tests-cypress .
# run the Docker image and execute the given tests
docker run -it --network=host gradido_e2e-tests-cypress yarn cypress-e2e
```

View File

@ -0,0 +1,79 @@
import { defineConfig } from 'cypress'
import { addCucumberPreprocessorPlugin } from '@badeball/cypress-cucumber-preprocessor'
import browserify from '@badeball/cypress-cucumber-preprocessor/browserify'
let resetPasswordLink: string
async function setupNodeEvents(
on: Cypress.PluginEvents,
config: Cypress.PluginConfigOptions
): Promise<Cypress.PluginConfigOptions> {
await addCucumberPreprocessorPlugin(on, config)
on(
'file:preprocessor',
browserify(config, {
typescript: require.resolve('typescript'),
})
)
on('task', {
setResetPasswordLink: (val) => {
return (resetPasswordLink = val)
},
getResetPasswordLink: () => {
return resetPasswordLink
},
})
on('after:run', (results) => {
if (results) {
// results will be undefined in interactive mode
// eslint-disable-next-line no-console
console.log(results.status)
}
})
return config
}
export default defineConfig({
e2e: {
specPattern: '**/*.feature',
excludeSpecPattern: '*.js',
experimentalSessionAndOrigin: true,
baseUrl: 'http://localhost:3000',
chromeWebSecurity: false,
defaultCommandTimeout: 10000,
supportFile: 'cypress/support/index.ts',
viewportHeight: 720,
viewportWidth: 1280,
video: false,
retries: {
runMode: 2,
openMode: 0,
},
env: {
backendURL: 'http://localhost:4000',
mailserverURL: 'http://localhost:1080',
loginQuery: `query ($email: String!, $password: String!, $publisherId: Int) {
login(email: $email, password: $password, publisherId: $publisherId) {
email
firstName
lastName
language
klickTipp {
newsletterState
__typename
}
hasElopage
publisherId
isAdmin
creation
__typename
}
}`,
},
setupNodeEvents,
},
})

View File

@ -1,4 +0,0 @@
tests/node_modules/
tests/cypress/screenshots/
tests/cypress/videos/
tests/cucumber-messages.ndjson

View File

@ -1,73 +0,0 @@
# Gradido End-to-End Testing with [Cypress](https://www.cypress.io/) (CI-ready via Docker)
A setup to show-case Cypress as an end-to-end testing tool for Gradido running in a Docker container.
The tests are organized in feature files written in Gherkin syntax.
## Features under test
So far these features are initially tested
- [User authentication](https://github.com/gradido/gradido/blob/master/e2e-tests/cypress/tests/cypress/e2e/User.Authentication.feature)
- [User profile - change password](https://github.com/gradido/gradido/blob/master/e2e-tests/cypress/tests/cypress/e2e/UserProfile.ChangePassword.feature)
- [User registration]((https://github.com/gradido/gradido/blob/master/e2e-tests/cypress/tests/cypress/e2e/User.Registration.feature)) (WIP)
## Precondition
Before running the tests, change to the repo's root directory (gradido).
### Boot up the system under test
```bash
docker-compose up
```
### Seed the database
The database has to be seeded upfront to every test run.
```bash
# change to the backend directory
cd /path/to/gradido/gradido/backend
# install all dependencies
yarn
# seed the database (everytime before running the tests)
yarn seed
```
## Execute the test
This setup will be integrated in the Gradido Github Actions to automatically support the CI/CD process.
For now the test setup can only be used locally in two modes.
### Run Cypress directly from the code
```bash
# change to the tests directory
cd /path/to/gradido/e2e-tests/cypress/tests
# install all dependencies
yarn install
# a) run the tests on command line
yarn cypress run
# b) open the Cypress GUI to run the tests in interactive mode
yarn cypress open
```
### Run Cyprss from a separate Docker container
```bash
# change to the cypress directory
cd /path/to/gradido/e2e-tests/cypress/
# build a Docker image from the Dockerfile
docker build -t gradido_e2e-tests-cypress .
# run the Docker image and execute the given tests
docker run -it --network=host gradido_e2e-tests-cypress yarn cypress-e2e
```

View File

@ -0,0 +1,25 @@
Feature: User Authentication - reset password
As a user
I want to reset my password from the sign in page
# TODO for these pre-conditions utilize seeding or API check, if user exists in test system
# Background:
# Given the following "users" are in the database:
# | email | password | name |
# | bibi@bloxberg.de | Aa12345_ | Bibi Bloxberg |
Scenario: Reset password from signin page successfully
Given the user navigates to page "/login"
And the user navigates to the forgot password page
When the user enters the e-mail address "bibi@bloxberg.de"
And the user submits the e-mail form
Then the user receives an e-mail containing the password reset link
When the user opens the password reset link in the browser
And the user enters the password "12345Aa_"
And the user repeats the password "12345Aa_"
And the user submits the password form
And the user clicks the sign in button
Then the user submits the credentials "bibi@bloxberg.de" "Aa12345_"
And the user cannot login
But the user submits the credentials "bibi@bloxberg.de" "12345Aa_"
And the user is logged in with username "Bibi Bloxberg"

View File

@ -11,7 +11,7 @@ Feature: User authentication
# | bibi@bloxberg.de | Aa12345_ | Bibi Bloxberg | # | bibi@bloxberg.de | Aa12345_ | Bibi Bloxberg |
Scenario: Log in successfully Scenario: Log in successfully
Given the browser navigates to page "/login" Given the user navigates to page "/login"
When the user submits the credentials "bibi@bloxberg.de" "Aa12345_" When the user submits the credentials "bibi@bloxberg.de" "Aa12345_"
Then the user is logged in with username "Bibi Bloxberg" Then the user is logged in with username "Bibi Bloxberg"

View File

@ -4,7 +4,7 @@ Feature: User registration
@skip @skip
Scenario: Register successfully Scenario: Register successfully
Given the browser navigates to page "/register" Given the user navigates to page "/register"
When the user fills name and email "Regina" "Register" "regina@register.com" When the user fills name and email "Regina" "Register" "regina@register.com"
And the user agrees to the privacy policy And the user agrees to the privacy policy
And the user submits the registration form And the user submits the registration form

View File

@ -12,7 +12,7 @@ Feature: User profile - change password
Given the user is logged in as "bibi@bloxberg.de" "Aa12345_" Given the user is logged in as "bibi@bloxberg.de" "Aa12345_"
Scenario: Change password successfully Scenario: Change password successfully
Given the browser navigates to page "/profile" Given the user navigates to page "/profile"
And the user opens the change password menu And the user opens the change password menu
When the user fills the password form with: When the user fills the password form with:
| Old password | Aa12345_ | | Old password | Aa12345_ |

View File

@ -0,0 +1,18 @@
/// <reference types='cypress' />
export class ForgotPasswordPage {
// selectors
emailInput = 'input[type=email]'
submitBtn = 'button[type=submit]'
successComponent = '[data-test="forgot-password-success"]'
enterEmail(email: string) {
cy.get(this.emailInput).clear().type(email)
return this
}
submitEmail() {
cy.get(this.submitBtn).click()
return this
}
}

View File

@ -0,0 +1,35 @@
/// <reference types='cypress' />
export class LoginPage {
// selectors
emailInput = 'input[type=email]'
passwordInput = 'input[type=password]'
forgotPasswordLink = '[data-test="forgot-password-link"]'
submitBtn = '[type=submit]'
emailHint = '#vee_Email'
passwordHint = '#vee_Password'
goto() {
cy.visit('/')
return this
}
enterEmail(email: string) {
cy.get(this.emailInput).clear().type(email)
return this
}
enterPassword(password: string) {
cy.get(this.passwordInput).clear().type(password)
return this
}
submitLogin() {
cy.get(this.submitBtn).click()
return this
}
openForgotPasswordPage() {
cy.get(this.forgotPasswordLink).click()
}
}

View File

@ -0,0 +1,10 @@
/// <reference types='cypress' />
export class OverviewPage {
navbarName = '[data-test="navbar-item-username"]'
goto() {
cy.visit('/overview')
return this
}
}

View File

@ -0,0 +1,35 @@
/// <reference types='cypress' />
export class ProfilePage {
// selectors
openChangePassword = '[data-test=open-password-change-form]'
oldPasswordInput = '#password-input-field'
newPasswordInput = '#new-password-input-field'
newPasswordRepeatInput = '#repeat-new-password-input-field'
submitNewPasswordBtn = '[data-test=submit-new-password-btn]'
goto() {
cy.visit('/profile')
return this
}
enterOldPassword(password: string) {
cy.get(this.oldPasswordInput).clear().type(password)
return this
}
enterNewPassword(password: string) {
cy.get(this.newPasswordInput).find('input').clear().type(password)
return this
}
enterRepeatPassword(password: string) {
cy.get(this.newPasswordRepeatInput).find('input').clear().type(password)
return this
}
submitPasswordForm() {
cy.get(this.submitNewPasswordBtn).click()
return this
}
}

View File

@ -0,0 +1,42 @@
/// <reference types='cypress' />
export class RegistrationPage {
// selectors
firstnameInput = '#registerFirstname'
lastnameInput = '#registerLastname'
emailInput = '#Email-input-field'
checkbox = '#registerCheckbox'
submitBtn = '[type=submit]'
RegistrationThanxHeadline = '.test-message-headline'
RegistrationThanxText = '.test-message-subtitle'
goto() {
cy.visit('/register')
return this
}
enterFirstname(firstname: string) {
cy.get(this.firstnameInput).clear().type(firstname)
return this
}
enterLastname(lastname: string) {
cy.get(this.lastnameInput).clear().type(lastname)
return this
}
enterEmail(email: string) {
cy.get(this.emailInput).clear().type(email)
return this
}
checkPrivacyCheckbox() {
cy.get(this.checkbox).click({ force: true })
}
submitRegistrationPage() {
cy.get(this.submitBtn).should('be.enabled')
cy.get(this.submitBtn).click()
}
}

View File

@ -0,0 +1,32 @@
/// <reference types='cypress' />
export class ResetPasswordPage {
// selectors
newPasswordBlock = '#new-password-input-field'
newPasswordRepeatBlock = '#repeat-new-password-input-field'
resetPasswordBtn = 'button[type=submit]'
resetPasswordMessageBlock = '[data-test="reset-password-message"]'
signinBtn = '.btn.test-message-button'
enterNewPassword(password: string) {
cy.get(this.newPasswordBlock).find('input[type=password]').type(password)
return this
}
repeatNewPassword(password: string) {
cy.get(this.newPasswordRepeatBlock)
.find('input[type=password]')
.type(password)
return this
}
submitNewPassword() {
cy.get(this.resetPasswordBtn).click()
return this
}
openSigninPage() {
cy.get(this.signinBtn).click()
return this
}
}

View File

@ -0,0 +1,17 @@
/// <reference types='cypress' />
export class SideNavMenu {
// selectors
profileMenu = '[data-test=profile-menu]'
logoutMenu = '[data-test=logout-menu]'
openUserProfile() {
cy.get(this.profileMenu).click()
return this
}
logout() {
cy.get(this.logoutMenu).click()
return this
}
}

View File

@ -0,0 +1,10 @@
/// <reference types='cypress' />
export class Toasts {
// selectors
toastSlot = '.b-toaster-slot'
toastTypeSuccess = '.b-toast-success'
toastTypeError = '.b-toast-danger'
toastTitle = '.gdd-toaster-title'
toastMessage = '.gdd-toaster-body'
}

View File

@ -0,0 +1,17 @@
/// <reference types='cypress' />
export class UserEMailSite {
// selectors
emailInbox = '.sidebar-emails-container'
emailList = '.email-list'
emailMeta = '.email-meta'
emailSubject = '.subject'
openRecentPasswordResetEMail() {
cy.get(this.emailList)
.find('email-item')
.filter(':contains(asswor)')
.click()
expect(cy.get(this.emailSubject)).to('contain', 'asswor')
}
}

View File

@ -0,0 +1,40 @@
import jwtDecode from 'jwt-decode'
Cypress.Commands.add('login', (email, password) => {
cy.clearLocalStorage('vuex')
cy.request({
method: 'POST',
url: Cypress.env('backendURL'),
body: {
operationName: null,
variables: {
email: email,
password: password,
},
query: Cypress.env('loginQuery'),
},
}).then(async (response) => {
const tokens = response.headers.token
const token = Array.isArray(tokens) ? tokens[0] : tokens
let tokenTime
if (!token) return
// to avoid JWT InvalidTokenError, the decoding of the token is wrapped
// in a try-catch block (see
// https://github.com/auth0/jwt-decode/issues/65#issuecomment-395493807)
try {
tokenTime = jwtDecode(token).exp
} catch (tokenDecodingError) {
cy.log('JWT decoding error: ', tokenDecodingError)
}
const vuexToken = {
token: token,
tokenTime: tokenTime,
}
cy.visit('/')
window.localStorage.setItem('vuex', JSON.stringify(vuexToken))
})
})

View File

@ -1,14 +1,14 @@
/* eslint-disable @typescript-eslint/no-namespace */ /* eslint-disable @typescript-eslint/no-namespace */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
/// <reference types="cypress" /> /// <reference types='cypress' />
import "./e2e"; import './e2e'
declare global { declare global {
namespace Cypress { namespace Cypress {
interface Chainable<Subject> { interface Chainable<Subject> {
login(email: string, password: string): Chainable<any>; login(email: string, password: string): Chainable<any>
} }
} }
} }

View File

@ -0,0 +1,39 @@
import { Given, Then, When } from '@badeball/cypress-cucumber-preprocessor'
import { OverviewPage } from '../../e2e/models/OverviewPage'
import { SideNavMenu } from '../../e2e/models/SideNavMenu'
import { Toasts } from '../../e2e/models/Toasts'
Given('the user navigates to page {string}', (page: string) => {
cy.visit(page)
})
// login related
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()
cy.url().should('include', '/overview')
cy.get(overviewPage.navbarName).should('contain', username)
})
Then('the user cannot login', () => {
const toast = new Toasts()
cy.get(toast.toastSlot).within(() => {
cy.get(toast.toastTypeError)
cy.get(toast.toastTitle).should('be.visible')
cy.get(toast.toastMessage).should('be.visible')
})
})
// logout
Then('the user logs out', () => {
const sideNavMenu = new SideNavMenu()
sideNavMenu.logout()
})

View File

@ -0,0 +1,45 @@
import { Then, When } from '@badeball/cypress-cucumber-preprocessor'
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 password reset link', () => {
cy.origin(
Cypress.env('mailserverURL'),
{ args: userEMailSite },
(userEMailSite) => {
const linkPattern = /\/reset-password\/[0-9]+\d/
cy.visit('/') // navigate to user's e-maile site (on fake mail server)
cy.get(userEMailSite.emailInbox).should('be.visible')
cy.get(userEMailSite.emailList)
.find('.email-item')
.filter(':contains(asswor)')
.first()
.click()
cy.get(userEMailSite.emailMeta)
.find(userEMailSite.emailSubject)
.contains('asswor')
cy.get('.email-content')
.find('.plain-text')
.contains(linkPattern)
.invoke('text')
.then((text) => {
const resetPasswordLink = text.match(linkPattern)[0]
cy.task('setResetPasswordLink', resetPasswordLink)
})
}
)
})
When('the user opens the password reset link in the browser', () => {
cy.task('getResetPasswordLink').then((passwordResetLink) => {
cy.visit(passwordResetLink)
})
cy.get(resetPasswordPage.newPasswordRepeatBlock).should('be.visible')
})

View File

@ -0,0 +1,69 @@
import { When, And } from '@badeball/cypress-cucumber-preprocessor'
import { ForgotPasswordPage } from '../../e2e/models/ForgotPasswordPage'
import { LoginPage } from '../../e2e/models/LoginPage'
import { ResetPasswordPage } from '../../e2e/models/ResetPasswordPage'
const loginPage = new LoginPage()
const forgotPasswordPage = new ForgotPasswordPage()
const resetPasswordPage = new ResetPasswordPage()
// login related
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'
}
})
loginPage.enterEmail(email)
loginPage.enterPassword(password)
loginPage.submitLogin()
cy.wait('@login').then((interception) => {
expect(interception.response.statusCode).equals(200)
})
}
)
// password reset related
And('the user navigates to the forgot password page', () => {
loginPage.openForgotPasswordPage()
cy.url().should('include', '/forgot-password')
})
When('the user enters the e-mail address {string}', (email: string) => {
forgotPasswordPage.enterEmail(email)
})
And('the user submits the e-mail form', () => {
forgotPasswordPage.submitEmail()
cy.get(forgotPasswordPage.successComponent).should('be.visible')
})
And('the user enters the password {string}', (password: string) => {
resetPasswordPage.enterNewPassword(password)
})
And('the user repeats the password {string}', (password: string) => {
resetPasswordPage.repeatNewPassword(password)
})
And('the user submits the new password', () => {
resetPasswordPage.submitNewPassword()
cy.get(resetPasswordPage.resetPasswordMessageBlock).should('be.visible')
})
And('the user clicks the sign in button', () => {
resetPasswordPage.openSigninPage()
cy.url().should('contain', '/login')
})

View File

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

View File

@ -0,0 +1,24 @@
import { And, When } from '@badeball/cypress-cucumber-preprocessor'
import { RegistrationPage } from '../../e2e/models/RegistrationPage'
const registrationPage = new RegistrationPage()
When(
'the user fills name and email {string} {string} {string}',
(firstname: string, lastname: string, email: string) => {
const registrationPage = new RegistrationPage()
registrationPage.enterFirstname(firstname)
registrationPage.enterLastname(lastname)
registrationPage.enterEmail(email)
}
)
And('the user agrees to the privacy policy', () => {
registrationPage.checkPrivacyCheckbox()
})
And('the user submits the registration form', () => {
registrationPage.submitRegistrationPage()
cy.get(registrationPage.RegistrationThanxHeadline).should('be.visible')
cy.get(registrationPage.RegistrationThanxText).should('be.visible')
})

View File

@ -1,24 +0,0 @@
module.exports = {
root: true,
env: {
node: true,
},
parser: "@typescript-eslint/parser",
plugins: ["cypress", "prettier", "@typescript-eslint"],
extends: [
"standard",
"eslint:recommended",
"plugin:prettier/recommended",
"plugin:@typescript-eslint/recommended",
],
rules: {
"no-console": ["error"],
"no-debugger": "error",
"prettier/prettier": [
"error",
{
htmlWhitespaceSensitivity: "ignore",
},
],
},
};

View File

@ -1,66 +0,0 @@
import { defineConfig } from "cypress";
import { addCucumberPreprocessorPlugin } from "@badeball/cypress-cucumber-preprocessor";
import browserify from "@badeball/cypress-cucumber-preprocessor/browserify";
async function setupNodeEvents(
on: Cypress.PluginEvents,
config: Cypress.PluginConfigOptions
): Promise<Cypress.PluginConfigOptions> {
await addCucumberPreprocessorPlugin(on, config);
on(
"file:preprocessor",
browserify(config, {
typescript: require.resolve("typescript"),
})
);
on("after:run", (results) => {
if (results) {
// results will be undefined in interactive mode
// eslint-disable-next-line no-console
console.log(results.status);
}
});
return config;
}
export default defineConfig({
e2e: {
specPattern: "**/*.feature",
excludeSpecPattern: "*.js",
baseUrl: "http://localhost:3000",
chromeWebSecurity: false,
defaultCommandTimeout: 10000,
supportFile: "cypress/support/index.ts",
viewportHeight: 720,
viewportWidth: 1280,
video: false,
retries: {
runMode: 2,
openMode: 0,
},
env: {
backendURL: "http://localhost:4000",
loginQuery: `query ($email: String!, $password: String!, $publisherId: Int) {
login(email: $email, password: $password, publisherId: $publisherId) {
email
firstName
lastName
language
klickTipp {
newsletterState
__typename
}
hasElopage
publisherId
isAdmin
creation
__typename
}
}`,
},
setupNodeEvents,
},
});

View File

@ -1,30 +0,0 @@
/// <reference types="cypress" />
export class LoginPage {
// selectors
emailInput = "input[type=email]";
passwordInput = "input[type=password]";
submitBtn = "[type=submit]";
emailHint = "#vee_Email";
passwordHint = "#vee_Password";
goto() {
cy.visit("/");
return this;
}
enterEmail(email: string) {
cy.get(this.emailInput).clear().type(email);
return this;
}
enterPassword(password: string) {
cy.get(this.passwordInput).clear().type(password);
return this;
}
submitLogin() {
cy.get(this.submitBtn).click();
return this;
}
}

View File

@ -1,10 +0,0 @@
/// <reference types="cypress" />
export class OverviewPage {
navbarName = '[data-test="navbar-item-username"]';
goto() {
cy.visit("/overview");
return this;
}
}

View File

@ -1,35 +0,0 @@
/// <reference types="cypress" />
export class ProfilePage {
// selectors
openChangePassword = "[data-test=open-password-change-form]";
oldPasswordInput = "#password-input-field";
newPasswordInput = "#new-password-input-field";
newPasswordRepeatInput = "#repeat-new-password-input-field";
submitNewPasswordBtn = "[data-test=submit-new-password-btn]";
goto() {
cy.visit("/profile");
return this;
}
enterOldPassword(password: string) {
cy.get(this.oldPasswordInput).clear().type(password);
return this;
}
enterNewPassword(password: string) {
cy.get(this.newPasswordInput).find("input").clear().type(password);
return this;
}
enterRepeatPassword(password: string) {
cy.get(this.newPasswordRepeatInput).find("input").clear().type(password);
return this;
}
submitPasswordForm() {
cy.get(this.submitNewPasswordBtn).click();
return this;
}
}

View File

@ -1,42 +0,0 @@
/// <reference types="cypress" />
export class RegistrationPage {
// selectors
firstnameInput = "#registerFirstname";
lastnameInput = "#registerLastname";
emailInput = "#Email-input-field";
checkbox = "#registerCheckbox";
submitBtn = "[type=submit]";
RegistrationThanxHeadline = ".test-message-headline";
RegistrationThanxText = ".test-message-subtitle";
goto() {
cy.visit("/register");
return this;
}
enterFirstname(firstname: string) {
cy.get(this.firstnameInput).clear().type(firstname);
return this;
}
enterLastname(lastname: string) {
cy.get(this.lastnameInput).clear().type(lastname);
return this;
}
enterEmail(email: string) {
cy.get(this.emailInput).clear().type(email);
return this;
}
checkPrivacyCheckbox() {
cy.get(this.checkbox).click({ force: true });
}
submitRegistrationPage() {
cy.get(this.submitBtn).should("be.enabled");
cy.get(this.submitBtn).click();
}
}

View File

@ -1,17 +0,0 @@
/// <reference types="cypress" />
export class SideNavMenu {
// selectors
profileMenu = "[data-test=profile-menu]";
logoutMenu = "[data-test=logout-menu]";
openUserProfile() {
cy.get(this.profileMenu).click();
return this;
}
logout() {
cy.get(this.logoutMenu).click();
return this;
}
}

View File

@ -1,10 +0,0 @@
/// <reference types="cypress" />
export class Toasts {
// selectors
toastSlot = ".b-toaster-slot";
toastTypeSuccess = ".b-toast-success";
toastTypeError = ".b-toast-danger";
toastTitle = ".gdd-toaster-title";
toastMessage = ".gdd-toaster-body";
}

View File

@ -1,38 +0,0 @@
import jwtDecode from "jwt-decode";
Cypress.Commands.add("login", (email, password) => {
cy.clearLocalStorage("vuex");
cy.request({
method: "POST",
url: Cypress.env("backendURL"),
body: {
operationName: null,
variables: {
email: email,
password: password,
},
query: Cypress.env("loginQuery"),
},
}).then(async (response) => {
const token = response.headers.token;
let tokenTime;
// to avoid JWT InvalidTokenError, the decoding of the token is wrapped
// in a try-catch block (see
// https://github.com/auth0/jwt-decode/issues/65#issuecomment-395493807)
try {
tokenTime = jwtDecode(token).exp;
} catch (tokenDecodingError) {
cy.log("JWT decoding error: ", tokenDecodingError);
}
const vuexToken = {
token: token,
tokenTime: tokenTime,
};
cy.visit("/");
window.localStorage.setItem("vuex", JSON.stringify(vuexToken));
});
});

View File

@ -1,52 +0,0 @@
import { Given, Then, When } from "@badeball/cypress-cucumber-preprocessor";
import { LoginPage } from "../../e2e/models/LoginPage";
import { OverviewPage } from "../../e2e/models/OverviewPage";
import { SideNavMenu } from "../../e2e/models/SideNavMenu";
import { Toasts } from "../../e2e/models/Toasts";
Given("the browser navigates to page {string}", (page: string) => {
cy.visit(page);
});
// login-related
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();
cy.url().should("include", "/overview");
cy.get(overviewPage.navbarName).should("contain", username);
});
Then("the user cannot login", () => {
const toast = new Toasts();
cy.get(toast.toastSlot).within(() => {
cy.get(toast.toastTypeError);
cy.get(toast.toastTitle).should("be.visible");
cy.get(toast.toastMessage).should("be.visible");
});
});
//
When(
"the user submits the credentials {string} {string}",
(email: string, password: string) => {
const loginPage = new LoginPage();
loginPage.enterEmail(email);
loginPage.enterPassword(password);
loginPage.submitLogin();
}
);
// logout
Then("the user logs out", () => {
const sideNavMenu = new SideNavMenu();
sideNavMenu.logout();
});

View File

@ -1,7 +0,0 @@
import { When } from "@badeball/cypress-cucumber-preprocessor";
import { LoginPage } from "../../e2e/models/LoginPage";
When("the user submits no credentials", () => {
const loginPage = new LoginPage();
loginPage.submitLogin();
});

View File

@ -1,32 +0,0 @@
import { And, When } from "@badeball/cypress-cucumber-preprocessor";
import { ProfilePage } from "../../e2e/models/ProfilePage";
import { Toasts } from "../../e2e/models/Toasts";
const profilePage = new ProfilePage();
And("the user opens the change password menu", () => {
cy.get(profilePage.openChangePassword).click();
cy.get(profilePage.newPasswordRepeatInput).should("be.visible");
cy.get(profilePage.submitNewPasswordBtn).should("be.disabled");
});
When("the user fills the password form with:", (table) => {
table = table.rowsHash();
profilePage.enterOldPassword(table["Old password"]);
profilePage.enterNewPassword(table["New password"]);
profilePage.enterRepeatPassword(table["Repeat new password"]);
cy.get(profilePage.submitNewPasswordBtn).should("be.enabled");
});
And("the user submits the password form", () => {
profilePage.submitPasswordForm();
});
When("the user is presented a {string} message", (type: string) => {
const toast = new Toasts();
cy.get(toast.toastSlot).within(() => {
cy.get(toast.toastTypeSuccess);
cy.get(toast.toastTitle).should("be.visible");
cy.get(toast.toastMessage).should("be.visible");
});
});

View File

@ -1,24 +0,0 @@
import { And, When } from "@badeball/cypress-cucumber-preprocessor";
import { RegistrationPage } from "../../e2e/models/RegistrationPage";
const registrationPage = new RegistrationPage();
When(
"the user fills name and email {string} {string} {string}",
(firstname: string, lastname: string, email: string) => {
const registrationPage = new RegistrationPage();
registrationPage.enterFirstname(firstname);
registrationPage.enterLastname(lastname);
registrationPage.enterEmail(email);
}
);
And("the user agrees to the privacy policy", () => {
registrationPage.checkPrivacyCheckbox();
});
And("the user submits the registration form", () => {
registrationPage.submitRegistrationPage();
cy.get(registrationPage.RegistrationThanxHeadline).should("be.visible");
cy.get(registrationPage.RegistrationThanxText).should("be.visible");
});

View File

@ -1,10 +0,0 @@
{
"compilerOptions": {
"target": "es2016",
"lib": ["es6", "dom"],
"baseUrl": "../node_modules",
"types": ["cypress", "node"],
"strict": true
},
"include": ["**/*.ts"]
}

16
e2e-tests/tsconfig.json Normal file
View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "es6",
"lib": ["es6", "dom"],
"baseUrl": ".",
"types": ["cypress", "node"],
"strict": true,
"esModuleInterop": true,
"moduleResolution": "node",
"paths": {
"@/*": ["cypress/*"],
"@models/*": ["cypress/e2e/models/*"],
}
},
"include": ["**/*.ts"],
}

116
federation/Dockerfile Normal file
View File

@ -0,0 +1,116 @@
##################################################################################
# BASE ###########################################################################
##################################################################################
FROM node:18.7.0-alpine3.16 as base
# ENVs (available in production aswell, can be overwritten by commandline or env file)
## DOCKER_WORKDIR would be a classical ARG, but that is not multi layer persistent - shame
ENV DOCKER_WORKDIR="/app"
## We Cannot do `$(date -u +'%Y-%m-%dT%H:%M:%SZ')` here so we use unix timestamp=0
ENV BUILD_DATE="1970-01-01T00:00:00.00Z"
## We cannot do $(npm run version).${BUILD_NUMBER} here so we default to 0.0.0.0
ENV BUILD_VERSION="0.0.0.0"
## We cannot do `$(git rev-parse --short HEAD)` here so we default to 0000000
ENV BUILD_COMMIT="0000000"
## SET NODE_ENV
ENV NODE_ENV="production"
## App relevant Envs
ENV PORT="5010"
# ENV PORT="${env.FEDERATION_PORT}"
# Labels
LABEL org.label-schema.build-date="${BUILD_DATE}"
LABEL org.label-schema.name="gradido:federation"
LABEL org.label-schema.description="Gradido GraphQL Federation"
LABEL org.label-schema.usage="https://github.com/gradido/gradido/blob/master/README.md"
LABEL org.label-schema.url="https://gradido.net"
LABEL org.label-schema.vcs-url="https://github.com/gradido/gradido/tree/master/federation"
LABEL org.label-schema.vcs-ref="${BUILD_COMMIT}"
LABEL org.label-schema.vendor="Gradido Community"
LABEL org.label-schema.version="${BUILD_VERSION}"
LABEL org.label-schema.schema-version="1.0"
LABEL maintainer="support@gradido.net"
# Install Additional Software
## install: git
#RUN apk --no-cache add git
# Settings
## Expose Container Port
EXPOSE ${PORT}
## Workdir
RUN mkdir -p ${DOCKER_WORKDIR}
WORKDIR ${DOCKER_WORKDIR}
RUN mkdir -p /database
##################################################################################
# DEVELOPMENT (Connected to the local environment, to reload on demand) ##########
##################################################################################
FROM base as development
# We don't need to copy or build anything since we gonna bind to the
# local filesystem which will need a rebuild anyway
# Run command
# (for development we need to execute yarn install since the
# node_modules are on another volume and need updating)
CMD /bin/sh -c "cd /database && yarn install && yarn build && cd /app && yarn install && yarn run dev"
##################################################################################
# BUILD (Does contain all files and is therefore bloated) ########################
##################################################################################
FROM base as build
# Copy everything from federation
COPY ./federation/ ./
# Copy everything from database
COPY ./database/ ../database/
# yarn install federation
RUN yarn install --production=false --frozen-lockfile --non-interactive
# yarn install database
RUN cd ../database && yarn install --production=false --frozen-lockfile --non-interactive
# yarn build
RUN yarn run build
# yarn build database
RUN cd ../database && yarn run build
##################################################################################
# TEST ###########################################################################
##################################################################################
FROM build as test
# Run command
CMD /bin/sh -c "yarn run start"
##################################################################################
# PRODUCTION (Does contain only "binary"- and static-files to reduce image size) #
##################################################################################
FROM base as production
# Copy "binary"-files from build image
COPY --from=build ${DOCKER_WORKDIR}/build ./build
COPY --from=build ${DOCKER_WORKDIR}/../database/build ../database/build
# We also copy the node_modules express and serve-static for the run script
COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules
COPY --from=build ${DOCKER_WORKDIR}/../database/node_modules ../database/node_modules
# Copy static files
# COPY --from=build ${DOCKER_WORKDIR}/public ./public
# Copy package.json for script definitions (lock file should not be needed)
COPY --from=build ${DOCKER_WORKDIR}/package.json ./package.json
# Copy tsconfig.json to provide alias path definitions
COPY --from=build ${DOCKER_WORKDIR}/tsconfig.json ./tsconfig.json
# Copy log4js-config.json to provide log configuration
COPY --from=build ${DOCKER_WORKDIR}/log4js-config.json ./log4js-config.json
# Copy run scripts run/
# COPY --from=build ${DOCKER_WORKDIR}/run ./run
# Run command
CMD /bin/sh -c "yarn run start"

32
federation/jest.config.js Normal file
View File

@ -0,0 +1,32 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
verbose: true,
preset: 'ts-jest',
collectCoverage: true,
collectCoverageFrom: [
'src/**/*.ts',
'!**/node_modules/**',
'!src/seeds/**',
'!build/**',
],
setupFiles: ['<rootDir>/test/testSetup.ts'],
setupFilesAfterEnv: [],
modulePathIgnorePatterns: ['<rootDir>/build/'],
moduleNameMapper: {
'@/(.*)': '<rootDir>/src/$1',
'@arg/(.*)': '<rootDir>/src/graphql/arg/$1',
'@enum/(.*)': '<rootDir>/src/graphql/enum/$1',
'@model/(.*)': '<rootDir>/src/graphql/model/$1',
'@union/(.*)': '<rootDir>/src/graphql/union/$1',
'@repository/(.*)': '<rootDir>/src/typeorm/repository/$1',
'@test/(.*)': '<rootDir>/test/$1',
'@entity/(.*)':
process.env.NODE_ENV === 'development'
? '<rootDir>/../database/entity/$1'
: '<rootDir>/../database/build/entity/$1',
'@dbTools/(.*)':
process.env.NODE_ENV === 'development'
? '<rootDir>/../database/src/$1'
: '<rootDir>/../database/build/src/$1',
},
}

View File

@ -11,6 +11,7 @@
"build": "tsc --build", "build": "tsc --build",
"clean": "tsc --build --clean", "clean": "tsc --build --clean",
"start": "cross-env TZ=UTC TS_NODE_BASEURL=./build node -r tsconfig-paths/register build/src/index.js", "start": "cross-env TZ=UTC TS_NODE_BASEURL=./build node -r tsconfig-paths/register build/src/index.js",
"test": "cross-env TZ=UTC NODE_ENV=development jest --runInBand --coverage --forceExit --detectOpenHandles",
"dev": "cross-env TZ=UTC nodemon -w src --ext ts --exec ts-node -r dotenv/config -r tsconfig-paths/register src/index.ts", "dev": "cross-env TZ=UTC nodemon -w src --ext ts --exec ts-node -r dotenv/config -r tsconfig-paths/register src/index.ts",
"lint": "eslint --max-warnings=0 --ext .js,.ts ." "lint": "eslint --max-warnings=0 --ext .js,.ts ."
}, },
@ -26,15 +27,16 @@
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"log4js": "^6.7.1", "log4js": "^6.7.1",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.1.1",
"type-graphql": "^1.1.1" "type-graphql": "^1.1.1"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^4.28.0", "@types/express": "4.17.12",
"@typescript-eslint/parser": "^4.28.0", "@types/jest": "27.0.2",
"@types/lodash.clonedeep": "^4.5.6", "@types/lodash.clonedeep": "^4.5.6",
"@types/node": "^16.10.3", "@types/node": "^16.10.3",
"@typescript-eslint/eslint-plugin": "^4.28.0",
"@typescript-eslint/parser": "^4.28.0",
"apollo-server-testing": "2.25.2",
"eslint": "^7.29.0", "eslint": "^7.29.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-config-standard": "^16.0.3", "eslint-config-standard": "^16.0.3",
@ -42,8 +44,15 @@
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^3.4.0", "eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-promise": "^5.1.0", "eslint-plugin-promise": "^5.1.0",
"jest": "27.2.4",
"ts-jest": "27.0.5",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.1.1",
"nodemon": "^2.0.7",
"prettier": "^2.3.1", "prettier": "^2.3.1",
"typescript": "^4.3.4", "typescript": "^4.3.4"
"nodemon": "^2.0.7" },
"nodemonConfig": {
"ignore": ["**/*.test.ts"]
} }
} }

View File

@ -24,7 +24,7 @@ const constants = {
} }
const server = { const server = {
PORT: process.env.PORT || 5000, PORT: process.env.PORT || 5010,
// JWT_SECRET: process.env.JWT_SECRET || 'secret123', // JWT_SECRET: process.env.JWT_SECRET || 'secret123',
// JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '10m', // JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '10m',
GRAPHIQL: process.env.GRAPHIQL === 'true' || false, GRAPHIQL: process.env.GRAPHIQL === 'true' || false,
@ -73,7 +73,7 @@ if (
const federation = { const federation = {
// FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || null, // FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || null,
// FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null, // FEDERATION_DHT_SEED: process.env.FEDERATION_DHT_SEED || null,
FEDERATION_PORT: process.env.FEDERATION_PORT || 5000, FEDERATION_PORT: process.env.FEDERATION_PORT || 5010,
FEDERATION_API: process.env.FEDERATION_API || '1_0', FEDERATION_API: process.env.FEDERATION_API || '1_0',
FEDERATION_COMMUNITY_URL: process.env.FEDERATION_COMMUNITY_URL || null, FEDERATION_COMMUNITY_URL: process.env.FEDERATION_COMMUNITY_URL || null,
} }

View File

@ -1,12 +0,0 @@
import { Query, Resolver } from 'type-graphql'
import { federationLogger as logger } from '@/server/logger'
import { GetTestApiResult } from '../../GetTestApiResult'
@Resolver()
export class Test2Resolver {
@Query(() => GetTestApiResult)
async test2(): Promise<GetTestApiResult> {
logger.info(`test api 2 1_0`)
return new GetTestApiResult('1_0')
}
}

View File

@ -0,0 +1,41 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { createTestClient } from 'apollo-server-testing'
import createServer from '@/server/createServer'
let query: any
// to do: We need a setup for the tests that closes the connection
let con: any
beforeAll(async () => {
const server = await createServer()
con = server.con
query = createTestClient(server.apollo).query
})
afterAll(async () => {
await con.close()
})
describe('TestResolver', () => {
const getTestQuery = `
query {
test {
api
}
}
`
describe('getTestApi', () => {
it('returns 1_0', async () => {
await expect(query({ query: getTestQuery })).resolves.toMatchObject({
data: {
test: {
api: '1_0',
},
},
})
})
})
})

View File

@ -1,8 +1,10 @@
import { Field, ObjectType, Query, Resolver } from 'type-graphql' // eslint-disable-next-line @typescript-eslint/no-unused-vars
import { Query, Resolver } from 'type-graphql'
import { federationLogger as logger } from '@/server/logger' import { federationLogger as logger } from '@/server/logger'
import { GetTestApiResult } from '../../GetTestApiResult' import { GetTestApiResult } from '../../GetTestApiResult'
@Resolver() @Resolver()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export class TestResolver { export class TestResolver {
@Query(() => GetTestApiResult) @Query(() => GetTestApiResult)
async test(): Promise<GetTestApiResult> { async test(): Promise<GetTestApiResult> {

View File

@ -0,0 +1,44 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { createTestClient } from 'apollo-server-testing'
import createServer from '@/server/createServer'
import CONFIG from '@/config'
CONFIG.FEDERATION_API = '1_1'
let query: any
// to do: We need a setup for the tests that closes the connection
let con: any
beforeAll(async () => {
const server = await createServer()
con = server.con
query = createTestClient(server.apollo).query
})
afterAll(async () => {
await con.close()
})
describe('TestResolver', () => {
const getTestQuery = `
query {
test {
api
}
}
`
describe('getTestApi', () => {
it('returns 1_1', async () => {
await expect(query({ query: getTestQuery })).resolves.toMatchObject({
data: {
test: {
api: '1_1',
},
},
})
})
})
})

View File

@ -1,8 +1,10 @@
import { Field, ObjectType, Query, Resolver } from 'type-graphql' // eslint-disable-next-line @typescript-eslint/no-unused-vars
import { Query, Resolver } from 'type-graphql'
import { federationLogger as logger } from '@/server/logger' import { federationLogger as logger } from '@/server/logger'
import { GetTestApiResult } from '../../GetTestApiResult' import { GetTestApiResult } from '../../GetTestApiResult'
@Resolver() @Resolver()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export class TestResolver { export class TestResolver {
@Query(() => GetTestApiResult) @Query(() => GetTestApiResult)
async test(): Promise<GetTestApiResult> { async test(): Promise<GetTestApiResult> {

View File

@ -0,0 +1,44 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { createTestClient } from 'apollo-server-testing'
import createServer from '@/server/createServer'
import CONFIG from '@/config'
CONFIG.FEDERATION_API = '2_0'
let query: any
// to do: We need a setup for the tests that closes the connection
let con: any
beforeAll(async () => {
const server = await createServer()
con = server.con
query = createTestClient(server.apollo).query
})
afterAll(async () => {
await con.close()
})
describe('TestResolver', () => {
const getTestQuery = `
query {
test {
api
}
}
`
describe('getTestApi', () => {
it('returns 2_0', async () => {
await expect(query({ query: getTestQuery })).resolves.toMatchObject({
data: {
test: {
api: '2_0',
},
},
})
})
})
})

View File

@ -1,8 +1,10 @@
import { Field, ObjectType, Query, Resolver } from 'type-graphql' // eslint-disable-next-line @typescript-eslint/no-unused-vars
import { Query, Resolver } from 'type-graphql'
import { federationLogger as logger } from '@/server/logger' import { federationLogger as logger } from '@/server/logger'
import { GetTestApiResult } from '../../GetTestApiResult' import { GetTestApiResult } from '../../GetTestApiResult'
@Resolver() @Resolver()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export class TestResolver { export class TestResolver {
@Query(() => GetTestApiResult) @Query(() => GetTestApiResult)
async test(): Promise<GetTestApiResult> { async test(): Promise<GetTestApiResult> {

View File

@ -1,6 +1,8 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { Field, ObjectType } from 'type-graphql' import { Field, ObjectType } from 'type-graphql'
@ObjectType() @ObjectType()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export class GetTestApiResult { export class GetTestApiResult {
constructor(apiVersion: string) { constructor(apiVersion: string) {
this.api = apiVersion this.api = apiVersion

View File

@ -0,0 +1,22 @@
import { federationLogger as logger } from '@/server/logger'
jest.setTimeout(1000000)
jest.mock('@/server/logger', () => {
const originalModule = jest.requireActual('@/server/logger')
return {
__esModule: true,
...originalModule,
backendLogger: {
addContext: jest.fn(),
trace: jest.fn(),
debug: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
error: jest.fn(),
fatal: jest.fn(),
},
}
})
export { logger }

View File

@ -52,14 +52,14 @@
// "@enum/*": ["src/graphql/enum/*"], // "@enum/*": ["src/graphql/enum/*"],
// "@model/*": ["src/graphql/model/*"], // "@model/*": ["src/graphql/model/*"],
"@repository/*": ["src/typeorm/repository/*"], "@repository/*": ["src/typeorm/repository/*"],
// "@test/*": ["test/*"], "@test/*": ["test/*"],
/* external */ /* external */
"@typeorm/*": ["../backend/src/typeorm/*", "../../backend/src/typeorm/*"], "@typeorm/*": ["../backend/src/typeorm/*", "../../backend/src/typeorm/*"],
"@dbTools/*": ["../database/src/*", "../../database/build/src/*"], "@dbTools/*": ["../database/src/*", "../../database/build/src/*"],
"@entity/*": ["../database/entity/*", "../../database/build/entity/*"] "@entity/*": ["../database/entity/*", "../../database/build/entity/*"]
}, },
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
"typeRoots": ["src/dht_node/@types", "node_modules/@types"], /* List of folders to include type definitions from. */ "typeRoots": ["node_modules/@types"], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */ // "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@ -3,16 +3,16 @@
<b-navbar :toggleable="false" class="pr-4"> <b-navbar :toggleable="false" class="pr-4">
<b-navbar-brand class="d-none d-lg-block"> <b-navbar-brand class="d-none d-lg-block">
<b-img <b-img
class="imgLogo position-absolute ml--3 mt-lg--2 mt-3 p-2 zindex1000" class="position-absolute ml--3 mt-lg--2 mt-3 p-2 zindex1000"
:src="logo" :src="logo"
width="200" width="200"
alt="..." alt="Logo"
/> />
<b-img <b-img
class="imgLogoBack mt--3 ml--3" class="mt--3 ml--3"
src="/img/template/gradido_background_header.png" :src="background_header"
width="230" width="230"
alt="start background image" alt="Background Image"
></b-img> ></b-img>
</b-navbar-brand> </b-navbar-brand>
<b-img class="sheet-img position-absolute d-block d-lg-none zindex1000" :src="sheet"></b-img> <b-img class="sheet-img position-absolute d-block d-lg-none zindex1000" :src="sheet"></b-img>
@ -35,7 +35,8 @@ export default {
mixins: [authLinks], mixins: [authLinks],
data() { data() {
return { return {
logo: '/img/brand/green.png', background_header: '/img/template/gradido_background_header.png',
logo: '/img/brand/gradido-logo.png',
sheet: '/img/template/Blaetter.png', sheet: '/img/template/Blaetter.png',
} }
}, },

View File

@ -4,10 +4,10 @@
<b-navbar toggleable="lg" class="pr-4"> <b-navbar toggleable="lg" class="pr-4">
<b-navbar-brand> <b-navbar-brand>
<b-img <b-img
class="imgLogo mt-lg--2 mt-3 mb-3 d-none d-lg-block zindex10" class="mt-lg--2 mt-3 mb-3 d-none d-lg-block zindex10"
:src="logo" :src="logo"
width="" width="200"
alt="..." alt="Logo"
/> />
<div v-b-toggle.sidebar-mobile variant="link" class="d-block d-lg-none"> <div v-b-toggle.sidebar-mobile variant="link" class="d-block d-lg-none">
<span class="navbar-toggler-icon h2"></span> <span class="navbar-toggler-icon h2"></span>
@ -60,7 +60,7 @@ export default {
}, },
data() { data() {
return { return {
logo: '/img/brand/green.png', logo: '/img/brand/gradido-logo.png',
sheet: '/img/template/Blaetter.png', sheet: '/img/template/Blaetter.png',
} }
}, },

View File

@ -1,3 +1,4 @@
<!-- eslint-disable prettier/prettier -->
<template> <template>
<div class="forgot-password"> <div class="forgot-password">
<b-container v-if="enterData"> <b-container v-if="enterData">
@ -26,6 +27,7 @@
<message <message
:headline="success ? $t('message.title') : $t('message.errorTitle')" :headline="success ? $t('message.title') : $t('message.errorTitle')"
:subtitle="success ? $t('message.email') : $t('error.email-already-sent')" :subtitle="success ? $t('message.email') : $t('error.email-already-sent')"
:data-test="success ? 'forgot-password-success' : 'forgot-password-error'"
:buttonText="$t('login')" :buttonText="$t('login')"
linkTo="/login" linkTo="/login"
/> />

2
frontend/src/pages/Login.vue Executable file → Normal file
View File

@ -24,7 +24,7 @@
</b-row> </b-row>
<b-row> <b-row>
<b-col class="d-flex justify-content-end mb-4 mb-lg-0"> <b-col class="d-flex justify-content-end mb-4 mb-lg-0">
<router-link to="/forgot-password"> <router-link to="/forgot-password" data-test="forgot-password-link">
{{ $t('settings.password.forgot_pwd') }} {{ $t('settings.password.forgot_pwd') }}
</router-link> </router-link>
</b-col> </b-col>

View File

@ -5,7 +5,12 @@
<b-form role="form" @submit.prevent="handleSubmit(onSubmit)"> <b-form role="form" @submit.prevent="handleSubmit(onSubmit)">
<input-password-confirmation v-model="form" /> <input-password-confirmation v-model="form" />
<div class="text-center"> <div class="text-center">
<b-button type="submit" variant="gradido" class="mt-4"> <b-button
type="submit"
variant="gradido"
class="mt-4"
data-test="submit-new-password-btn"
>
<!-- eslint-disable-next-line @intlify/vue-i18n/no-dynamic-keys--> <!-- eslint-disable-next-line @intlify/vue-i18n/no-dynamic-keys-->
{{ $t(displaySetup.button) }} {{ $t(displaySetup.button) }}
</b-button> </b-button>

View File

@ -45,7 +45,6 @@ export default {
}, },
data() { data() {
return { return {
img: '/img/brand/green.png',
linkData: { linkData: {
__typename: 'TransactionLink', __typename: 'TransactionLink',
amount: '123.45', amount: '123.45',