mirror of
https://github.com/IT4Change/gradido.git
synced 2025-12-13 07:45:54 +00:00
Merge branch 'master' into logger_creations
This commit is contained in:
commit
aa0b3ad671
28
.github/workflows/test.yml
vendored
28
.github/workflows/test.yml
vendored
@ -274,7 +274,7 @@ jobs:
|
||||
run: docker run --rm gradido/admin:test yarn run lint
|
||||
|
||||
##############################################################################
|
||||
# JOB: STYLELINT ADMIN INTERFACE ##############################################
|
||||
# JOB: STYLELINT ADMIN INTERFACE #############################################
|
||||
##############################################################################
|
||||
stylelint_admin:
|
||||
name: Stylelint - Admin Interface
|
||||
@ -577,7 +577,7 @@ jobs:
|
||||
end-to-end-tests:
|
||||
name: End-to-End Tests
|
||||
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:
|
||||
##########################################################################
|
||||
# CHECKOUT CODE ##########################################################
|
||||
@ -601,13 +601,6 @@ jobs:
|
||||
path: /tmp
|
||||
- name: Load Docker Image (Database Up)
|
||||
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)
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
@ -640,7 +633,11 @@ jobs:
|
||||
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
|
||||
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
|
||||
run: sleep 10s
|
||||
@ -657,6 +654,9 @@ jobs:
|
||||
- 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
|
||||
|
||||
- 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
|
||||
run: sleep 15s
|
||||
|
||||
@ -666,12 +666,12 @@ jobs:
|
||||
- name: End-to-end tests | run tests
|
||||
id: e2e-tests
|
||||
run: |
|
||||
cd e2e-tests/cypress/tests/
|
||||
cd e2e-tests/
|
||||
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
|
||||
if: steps.e2e-tests.outcome == 'failure'
|
||||
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
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
98
.github/workflows/test_federation.yml
vendored
Normal 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
9
backend/.env.test_e2e
Normal 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
@ -16,7 +16,7 @@ export const nMonthsBefore = (date: Date, months = 1): string => {
|
||||
export const creationFactory = async (
|
||||
client: ApolloServerTestClient,
|
||||
creation: CreationInterface,
|
||||
): Promise<Contribution | void> => {
|
||||
): Promise<Contribution> => {
|
||||
const { mutate } = client
|
||||
await mutate({ mutation: login, variables: { email: creation.email, password: 'Aa12345_' } })
|
||||
|
||||
@ -51,6 +51,7 @@ export const creationFactory = async (
|
||||
await confirmedContribution.save()
|
||||
}
|
||||
}
|
||||
return confirmedContribution
|
||||
} else {
|
||||
return contribution
|
||||
}
|
||||
|
||||
@ -272,6 +272,12 @@ export const deleteContribution = gql`
|
||||
}
|
||||
`
|
||||
|
||||
export const denyContribution = gql`
|
||||
mutation ($id: Int!) {
|
||||
denyContribution(id: $id)
|
||||
}
|
||||
`
|
||||
|
||||
export const createContributionMessage = gql`
|
||||
mutation ($contributionId: Float!, $message: String!) {
|
||||
createContributionMessage(contributionId: $contributionId, message: $message) {
|
||||
|
||||
@ -166,6 +166,15 @@ export const listContributions = gql`
|
||||
id
|
||||
amount
|
||||
memo
|
||||
createdAt
|
||||
contributionDate
|
||||
confirmedAt
|
||||
confirmedBy
|
||||
deletedAt
|
||||
state
|
||||
messagesCount
|
||||
deniedAt
|
||||
deniedBy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,15 +32,6 @@ export const startDHT = async (topic: string): Promise<void> => {
|
||||
logger.debug(`keyPairDHT: secretKey=${keyPair.secretKey.toString('hex')}`)
|
||||
|
||||
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)}`)
|
||||
|
||||
const node = new DHT({ keyPair })
|
||||
@ -216,6 +207,5 @@ async function writeHomeCommunityEnries(pubKey: any): Promise<CommunityApi[]> {
|
||||
} catch (err) {
|
||||
throw new Error(`Federation: Error writing HomeCommunity-Entries: ${err}`)
|
||||
}
|
||||
|
||||
return homeApiVersions
|
||||
}
|
||||
|
||||
@ -84,6 +84,29 @@ services:
|
||||
- ./dht-node:/app
|
||||
- ./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 ##############################################
|
||||
########################################################
|
||||
@ -155,5 +178,8 @@ volumes:
|
||||
dht_node_modules:
|
||||
dht_database_node_modules:
|
||||
dht_database_build:
|
||||
federation_node_modules:
|
||||
federation_database_node_modules:
|
||||
federation_database_build:
|
||||
database_node_modules:
|
||||
database_build:
|
||||
@ -36,6 +36,21 @@ services:
|
||||
- NODE_ENV="test"
|
||||
- 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 #############################################
|
||||
########################################################
|
||||
|
||||
@ -147,6 +147,42 @@ services:
|
||||
# <host_machine_directory>:<container_directory> – mirror bidirectional path in local context with path in Docker container
|
||||
- ./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 #############################################
|
||||
########################################################
|
||||
|
||||
26
e2e-tests/.eslintrc.js
Normal file
26
e2e-tests/.eslintrc.js
Normal 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
5
e2e-tests/.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
cypress/screenshots/
|
||||
cypress/videos/
|
||||
cucumber-messages.ndjson
|
||||
|
||||
9
e2e-tests/.prettierrc.js
Normal file
9
e2e-tests/.prettierrc.js
Normal file
@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
semi: false,
|
||||
printWidth: 100,
|
||||
singleQuote: true,
|
||||
trailingComma: "all",
|
||||
tabWidth: 2,
|
||||
bracketSpacing: true,
|
||||
endOfLine: "auto",
|
||||
};
|
||||
@ -11,7 +11,7 @@
|
||||
###############################################################################
|
||||
FROM cypress/base:16.14.2-slim
|
||||
|
||||
ARG DOCKER_WORKDIR=/tests/
|
||||
ARG DOCKER_WORKDIR="/tests"
|
||||
WORKDIR $DOCKER_WORKDIR
|
||||
|
||||
# install dependencies
|
||||
@ -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
|
||||
```
|
||||
|
||||
79
e2e-tests/cypress.config.ts
Normal file
79
e2e-tests/cypress.config.ts
Normal 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,
|
||||
},
|
||||
})
|
||||
4
e2e-tests/cypress/.gitignore
vendored
4
e2e-tests/cypress/.gitignore
vendored
@ -1,4 +0,0 @@
|
||||
tests/node_modules/
|
||||
tests/cypress/screenshots/
|
||||
tests/cypress/videos/
|
||||
tests/cucumber-messages.ndjson
|
||||
@ -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
|
||||
```
|
||||
@ -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"
|
||||
@ -11,7 +11,7 @@ Feature: User authentication
|
||||
# | bibi@bloxberg.de | Aa12345_ | Bibi Bloxberg |
|
||||
|
||||
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_"
|
||||
Then the user is logged in with username "Bibi Bloxberg"
|
||||
|
||||
@ -4,7 +4,7 @@ Feature: User registration
|
||||
|
||||
@skip
|
||||
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"
|
||||
And the user agrees to the privacy policy
|
||||
And the user submits the registration form
|
||||
@ -12,7 +12,7 @@ Feature: User profile - change password
|
||||
Given the user is logged in as "bibi@bloxberg.de" "Aa12345_"
|
||||
|
||||
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
|
||||
When the user fills the password form with:
|
||||
| Old password | Aa12345_ |
|
||||
18
e2e-tests/cypress/e2e/models/ForgotPasswordPage.ts
Normal file
18
e2e-tests/cypress/e2e/models/ForgotPasswordPage.ts
Normal 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
|
||||
}
|
||||
}
|
||||
35
e2e-tests/cypress/e2e/models/LoginPage.ts
Normal file
35
e2e-tests/cypress/e2e/models/LoginPage.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
10
e2e-tests/cypress/e2e/models/OverviewPage.ts
Normal file
10
e2e-tests/cypress/e2e/models/OverviewPage.ts
Normal file
@ -0,0 +1,10 @@
|
||||
/// <reference types='cypress' />
|
||||
|
||||
export class OverviewPage {
|
||||
navbarName = '[data-test="navbar-item-username"]'
|
||||
|
||||
goto() {
|
||||
cy.visit('/overview')
|
||||
return this
|
||||
}
|
||||
}
|
||||
35
e2e-tests/cypress/e2e/models/ProfilePage.ts
Normal file
35
e2e-tests/cypress/e2e/models/ProfilePage.ts
Normal 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
|
||||
}
|
||||
}
|
||||
42
e2e-tests/cypress/e2e/models/RegistrationPage.ts
Normal file
42
e2e-tests/cypress/e2e/models/RegistrationPage.ts
Normal 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()
|
||||
}
|
||||
}
|
||||
32
e2e-tests/cypress/e2e/models/ResetPasswordPage.ts
Normal file
32
e2e-tests/cypress/e2e/models/ResetPasswordPage.ts
Normal 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
|
||||
}
|
||||
}
|
||||
17
e2e-tests/cypress/e2e/models/SideNavMenu.ts
Normal file
17
e2e-tests/cypress/e2e/models/SideNavMenu.ts
Normal 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
|
||||
}
|
||||
}
|
||||
10
e2e-tests/cypress/e2e/models/Toasts.ts
Normal file
10
e2e-tests/cypress/e2e/models/Toasts.ts
Normal 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'
|
||||
}
|
||||
17
e2e-tests/cypress/e2e/models/UserEMailSite.ts
Normal file
17
e2e-tests/cypress/e2e/models/UserEMailSite.ts
Normal 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')
|
||||
}
|
||||
}
|
||||
40
e2e-tests/cypress/support/e2e.ts
Normal file
40
e2e-tests/cypress/support/e2e.ts
Normal 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))
|
||||
})
|
||||
})
|
||||
@ -1,14 +1,14 @@
|
||||
/* eslint-disable @typescript-eslint/no-namespace */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
/// <reference types="cypress" />
|
||||
/// <reference types='cypress' />
|
||||
|
||||
import "./e2e";
|
||||
import './e2e'
|
||||
|
||||
declare global {
|
||||
namespace Cypress {
|
||||
interface Chainable<Subject> {
|
||||
login(email: string, password: string): Chainable<any>;
|
||||
login(email: string, password: string): Chainable<any>
|
||||
}
|
||||
}
|
||||
}
|
||||
39
e2e-tests/cypress/support/step_definitions/common_steps.ts
Normal file
39
e2e-tests/cypress/support/step_definitions/common_steps.ts
Normal 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()
|
||||
})
|
||||
45
e2e-tests/cypress/support/step_definitions/email_steps.ts
Normal file
45
e2e-tests/cypress/support/step_definitions/email_steps.ts
Normal 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')
|
||||
})
|
||||
@ -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')
|
||||
})
|
||||
@ -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')
|
||||
})
|
||||
})
|
||||
@ -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')
|
||||
})
|
||||
@ -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",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
});
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
|
||||
export class OverviewPage {
|
||||
navbarName = '[data-test="navbar-item-username"]';
|
||||
|
||||
goto() {
|
||||
cy.visit("/overview");
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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";
|
||||
}
|
||||
@ -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));
|
||||
});
|
||||
});
|
||||
@ -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();
|
||||
});
|
||||
@ -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();
|
||||
});
|
||||
@ -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");
|
||||
});
|
||||
});
|
||||
@ -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");
|
||||
});
|
||||
@ -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
16
e2e-tests/tsconfig.json
Normal 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
116
federation/Dockerfile
Normal 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
32
federation/jest.config.js
Normal 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',
|
||||
},
|
||||
}
|
||||
@ -11,6 +11,7 @@
|
||||
"build": "tsc --build",
|
||||
"clean": "tsc --build --clean",
|
||||
"start": "cross-env TZ=UTC TS_NODE_BASEURL=./build node -r tsconfig-paths/register build/src/index.js",
|
||||
"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",
|
||||
"lint": "eslint --max-warnings=0 --ext .js,.ts ."
|
||||
},
|
||||
@ -26,15 +27,16 @@
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"log4js": "^6.7.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.1.1",
|
||||
"type-graphql": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^4.28.0",
|
||||
"@typescript-eslint/parser": "^4.28.0",
|
||||
"@types/express": "4.17.12",
|
||||
"@types/jest": "27.0.2",
|
||||
"@types/lodash.clonedeep": "^4.5.6",
|
||||
"@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-config-prettier": "^8.3.0",
|
||||
"eslint-config-standard": "^16.0.3",
|
||||
@ -42,8 +44,15 @@
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^3.4.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",
|
||||
"typescript": "^4.3.4",
|
||||
"nodemon": "^2.0.7"
|
||||
"typescript": "^4.3.4"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"ignore": ["**/*.test.ts"]
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,7 +24,7 @@ const constants = {
|
||||
}
|
||||
|
||||
const server = {
|
||||
PORT: process.env.PORT || 5000,
|
||||
PORT: process.env.PORT || 5010,
|
||||
// JWT_SECRET: process.env.JWT_SECRET || 'secret123',
|
||||
// JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '10m',
|
||||
GRAPHIQL: process.env.GRAPHIQL === 'true' || false,
|
||||
@ -73,7 +73,7 @@ if (
|
||||
const federation = {
|
||||
// FEDERATION_DHT_TOPIC: process.env.FEDERATION_DHT_TOPIC || 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_COMMUNITY_URL: process.env.FEDERATION_COMMUNITY_URL || null,
|
||||
}
|
||||
|
||||
@ -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')
|
||||
}
|
||||
}
|
||||
41
federation/src/graphql/api/1_0/resolver/TestResolver.test.ts
Normal file
41
federation/src/graphql/api/1_0/resolver/TestResolver.test.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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 { GetTestApiResult } from '../../GetTestApiResult'
|
||||
|
||||
@Resolver()
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export class TestResolver {
|
||||
@Query(() => GetTestApiResult)
|
||||
async test(): Promise<GetTestApiResult> {
|
||||
|
||||
44
federation/src/graphql/api/1_1/resolver/TestResolver.test.ts
Normal file
44
federation/src/graphql/api/1_1/resolver/TestResolver.test.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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 { GetTestApiResult } from '../../GetTestApiResult'
|
||||
|
||||
@Resolver()
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export class TestResolver {
|
||||
@Query(() => GetTestApiResult)
|
||||
async test(): Promise<GetTestApiResult> {
|
||||
|
||||
44
federation/src/graphql/api/2_0/resolver/TestResolver.test.ts
Normal file
44
federation/src/graphql/api/2_0/resolver/TestResolver.test.ts
Normal 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',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -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 { GetTestApiResult } from '../../GetTestApiResult'
|
||||
|
||||
@Resolver()
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export class TestResolver {
|
||||
@Query(() => GetTestApiResult)
|
||||
async test(): Promise<GetTestApiResult> {
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import { Field, ObjectType } from 'type-graphql'
|
||||
|
||||
@ObjectType()
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export class GetTestApiResult {
|
||||
constructor(apiVersion: string) {
|
||||
this.api = apiVersion
|
||||
|
||||
22
federation/test/testSetup.ts
Normal file
22
federation/test/testSetup.ts
Normal 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 }
|
||||
@ -52,14 +52,14 @@
|
||||
// "@enum/*": ["src/graphql/enum/*"],
|
||||
// "@model/*": ["src/graphql/model/*"],
|
||||
"@repository/*": ["src/typeorm/repository/*"],
|
||||
// "@test/*": ["test/*"],
|
||||
"@test/*": ["test/*"],
|
||||
/* external */
|
||||
"@typeorm/*": ["../backend/src/typeorm/*", "../../backend/src/typeorm/*"],
|
||||
"@dbTools/*": ["../database/src/*", "../../database/build/src/*"],
|
||||
"@entity/*": ["../database/entity/*", "../../database/build/entity/*"]
|
||||
},
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
"typeRoots": ["src/dht_node/@types", "node_modules/@types"], /* List of folders to include type definitions from. */
|
||||
"typeRoots": ["node_modules/@types"], /* List of folders to include type definitions from. */
|
||||
// "types": [], /* Type declaration files to be included in compilation. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
|
||||
2197
federation/yarn.lock
2197
federation/yarn.lock
File diff suppressed because it is too large
Load Diff
BIN
frontend/public/img/brand/gradido-logo.png
Normal file
BIN
frontend/public/img/brand/gradido-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
@ -3,16 +3,16 @@
|
||||
<b-navbar :toggleable="false" class="pr-4">
|
||||
<b-navbar-brand class="d-none d-lg-block">
|
||||
<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"
|
||||
width="200"
|
||||
alt="..."
|
||||
alt="Logo"
|
||||
/>
|
||||
<b-img
|
||||
class="imgLogoBack mt--3 ml--3"
|
||||
src="/img/template/gradido_background_header.png"
|
||||
class="mt--3 ml--3"
|
||||
:src="background_header"
|
||||
width="230"
|
||||
alt="start background image"
|
||||
alt="Background Image"
|
||||
></b-img>
|
||||
</b-navbar-brand>
|
||||
<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],
|
||||
data() {
|
||||
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',
|
||||
}
|
||||
},
|
||||
|
||||
@ -4,10 +4,10 @@
|
||||
<b-navbar toggleable="lg" class="pr-4">
|
||||
<b-navbar-brand>
|
||||
<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"
|
||||
width=""
|
||||
alt="..."
|
||||
width="200"
|
||||
alt="Logo"
|
||||
/>
|
||||
<div v-b-toggle.sidebar-mobile variant="link" class="d-block d-lg-none">
|
||||
<span class="navbar-toggler-icon h2"></span>
|
||||
@ -60,7 +60,7 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
logo: '/img/brand/green.png',
|
||||
logo: '/img/brand/gradido-logo.png',
|
||||
sheet: '/img/template/Blaetter.png',
|
||||
}
|
||||
},
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
<!-- eslint-disable prettier/prettier -->
|
||||
<template>
|
||||
<div class="forgot-password">
|
||||
<b-container v-if="enterData">
|
||||
@ -26,6 +27,7 @@
|
||||
<message
|
||||
:headline="success ? $t('message.title') : $t('message.errorTitle')"
|
||||
:subtitle="success ? $t('message.email') : $t('error.email-already-sent')"
|
||||
:data-test="success ? 'forgot-password-success' : 'forgot-password-error'"
|
||||
:buttonText="$t('login')"
|
||||
linkTo="/login"
|
||||
/>
|
||||
|
||||
2
frontend/src/pages/Login.vue
Executable file → Normal file
2
frontend/src/pages/Login.vue
Executable file → Normal file
@ -24,7 +24,7 @@
|
||||
</b-row>
|
||||
<b-row>
|
||||
<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') }}
|
||||
</router-link>
|
||||
</b-col>
|
||||
|
||||
@ -5,7 +5,12 @@
|
||||
<b-form role="form" @submit.prevent="handleSubmit(onSubmit)">
|
||||
<input-password-confirmation v-model="form" />
|
||||
<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-->
|
||||
{{ $t(displaySetup.button) }}
|
||||
</b-button>
|
||||
|
||||
@ -45,7 +45,6 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
img: '/img/brand/green.png',
|
||||
linkData: {
|
||||
__typename: 'TransactionLink',
|
||||
amount: '123.45',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user