diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c5433c921..ac1dfa858 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,179 +1,68 @@ version: 2 updates: +- package-ecosystem: "github-actions" + directory: "/" + rebase-strategy: "disabled" + schedule: + interval: weekly + day: "saturday" + timezone: "Europe/Berlin" + time: "03:00" + - package-ecosystem: npm directory: "/" + rebase-strategy: "disabled" schedule: - interval: daily - time: "04:00" - open-pull-requests-limit: 10 - ignore: - - dependency-name: cypress - versions: - - 6.3.0 - - 6.4.0 - - 6.5.0 - - 6.6.0 - - 6.7.1 - - 6.8.0 - - 7.0.0 - - 7.0.1 - - 7.1.0 - - dependency-name: cypress-cucumber-preprocessor - versions: - - 4.0.0 - - 4.0.1 - - 4.0.3 - - dependency-name: date-fns - versions: - - 2.16.1 - - 2.17.0 - - 2.18.0 - - 2.19.0 - - 2.20.0 - - 2.20.1 - - 2.20.2 - - 2.20.3 - - 2.21.0 - - dependency-name: cypress-file-upload - versions: - - 5.0.2 - - 5.0.3 - - 5.0.4 - - 5.0.5 - - dependency-name: neo4j-driver - versions: - - 4.2.2 + interval: weekly + day: "saturday" + timezone: "Europe/Berlin" + time: "03:00" - package-ecosystem: npm directory: "/backend" + rebase-strategy: "disabled" schedule: - interval: daily - time: "04:00" - open-pull-requests-limit: 10 - ignore: - - dependency-name: y18n - versions: - - 4.0.1 - - 4.0.2 - - dependency-name: metascraper-publisher - versions: - - 5.16.16 - - 5.18.1 - - 5.18.12 - - 5.18.2 - - 5.18.4 - - 5.18.5 - - 5.18.6 - - 5.18.9 - - 5.20.0 - - 5.21.0 - - 5.21.2 - - 5.21.3 - - 5.21.4 - - 5.21.5 - - dependency-name: metascraper-author - versions: - - 5.16.16 - - 5.18.1 - - 5.18.12 - - 5.18.2 - - 5.18.4 - - 5.18.5 - - 5.18.6 - - 5.18.9 - - 5.20.0 - - 5.21.0 - - 5.21.2 - - 5.21.3 - - 5.21.4 - - 5.21.5 - - dependency-name: neo4j-driver - versions: - - 4.2.2 - - dependency-name: neo4j-graphql-js - versions: - - 2.19.1 - - dependency-name: mustache - versions: - - 4.1.0 + interval: weekly + day: "saturday" + timezone: "Europe/Berlin" + time: "03:00" - package-ecosystem: npm directory: "/webapp" + rebase-strategy: "disabled" schedule: - interval: daily - time: "04:00" - open-pull-requests-limit: 10 - ignore: - - dependency-name: nuxt - versions: - - 2.14.12 - - 2.15.0 - - 2.15.1 - - 2.15.2 - - 2.15.3 - - dependency-name: v-tooltip - versions: - - 2.1.2 - - dependency-name: "@vue/server-test-utils" - versions: - - 1.1.2 - - 1.1.3 - - dependency-name: node-notifier - versions: - - 8.0.1 -- package-ecosystem: docker - directory: "/webapp" - schedule: - interval: daily - time: "04:00" - open-pull-requests-limit: 10 - ignore: - - dependency-name: node - versions: - - ">= 15.5.a, < 15.6" - - dependency-name: node - versions: - - 15.10.0.pre.alpine3.10 - - 15.11.0.pre.alpine3.10 - - 15.12.0.pre.alpine3.10 - - 15.13.0.pre.alpine3.10 - - 15.7.0.pre.alpine3.10 - - 15.8.0.pre.alpine3.10 - - 15.9.0.pre.alpine3.10 + interval: weekly + day: "saturday" + timezone: "Europe/Berlin" + time: "03:00" + - package-ecosystem: docker directory: "/backend" + rebase-strategy: "disabled" schedule: - interval: daily - time: "04:00" - open-pull-requests-limit: 10 - ignore: - - dependency-name: node - versions: - - ">= 15.4.a, < 15.5" - - dependency-name: node - versions: - - ">= 15.5.a, < 15.6" - - dependency-name: node - versions: - - 15.10.0.pre.alpine3.10 - - 15.11.0.pre.alpine3.10 - - 15.12.0.pre.alpine3.10 - - 15.13.0.pre.alpine3.10 - - 15.7.0.pre.alpine3.10 - - 15.8.0.pre.alpine3.10 - - 15.9.0.pre.alpine3.10 + interval: weekly + day: "saturday" + timezone: "Europe/Berlin" + time: "03:00" +- package-ecosystem: docker + directory: "/webapp" + rebase-strategy: "disabled" + schedule: + interval: weekly + day: "saturday" + timezone: "Europe/Berlin" + time: "03:00" - package-ecosystem: docker directory: "/neo4j" + rebase-strategy: "disabled" schedule: - interval: daily - time: "04:00" - open-pull-requests-limit: 10 - ignore: - - dependency-name: neo4j - versions: - - 4.2.3 - - 4.2.4 + interval: weekly + day: "saturday" + timezone: "Europe/Berlin" + time: "03:00" - package-ecosystem: docker - directory: "/deployment/legacy-migration/maintenance-worker" + directory: "/deployment/src/docker" + rebase-strategy: "disabled" schedule: - interval: daily - time: "04:00" - open-pull-requests-limit: 10 + interval: weekly + day: "saturday" + timezone: "Europe/Berlin" + time: "03:00" diff --git a/.github/workflows/lint_pr.yml b/.github/workflows/lint_pr.yml index 8ef9750a4..5a8bb662f 100644 --- a/.github/workflows/lint_pr.yml +++ b/.github/workflows/lint_pr.yml @@ -1,7 +1,6 @@ name: "ocelot.social lint pull request CI" on: - pull_request: pull_request_target: types: - opened @@ -12,6 +11,7 @@ jobs: main: name: Validate PR title runs-on: ubuntu-latest + if: ${{ github.actor != 'dependabot[bot]' }} steps: - uses: amannn/action-semantic-pull-request@v5 env: diff --git a/.github/workflows/publish-branded.yml b/.github/workflows/publish-branded.yml index 2877aa213..869eb6302 100644 --- a/.github/workflows/publish-branded.yml +++ b/.github/workflows/publish-branded.yml @@ -1,9 +1,7 @@ -name: ocelot.social publish branded CI - +name: publish-branded on: - push: - branches: - - master + repository_dispatch: + types: [trigger-build-success] jobs: build_branded: name: Docker Build Branded @@ -11,7 +9,10 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v2 - + with: + ref: ${{ github.event.client_payload.ref }} + - name: Setup env + run: echo "OCELOT_VERSION=${{ github.event.client_payload.BUILD_VERSION }}" >> $GITHUB_ENV - name: Build branded images run: | deployment/scripts/branded-images.build.sh @@ -47,6 +48,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v2 + with: + ref: ${{ github.event.client_payload.ref }} - name: Download Docker Image (Backend) uses: actions/download-artifact@v2 @@ -73,13 +76,4 @@ jobs: run: docker load < /tmp/maintenance-branded.tar - name: Upload to dockerhub - run: deployment/scripts/branded-images.upload.sh -# - name: login to dockerhub -# run: echo "${DOCKERHUB_TOKEN}" | docker login -u "${DOCKERHUB_USERNAME}" --password-stdin -# -# - name: Push Backend -# run: docker push --all-tags ocelotsocialnetwork/backend-branded -# - name: Push Webapp -# run: docker push --all-tags ocelotsocialnetwork/webapp-branded -# - name: Push Maintenance -# run: docker push --all-tags ocelotsocialnetwork/maintenance-branded \ No newline at end of file + run: deployment/scripts/branded-images.upload.sh \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 32198dd62..01256d719 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,64 +1,36 @@ -name: ocelot.social publish CI +name: publish on: push: branches: - master - # - 5059-epic-groups # for testing while developing - # template branches in repo - # - template--separate-branch-auto-deployment--5059-epic-groups jobs: - ############################################################################## - # JOB: PREPARE ############################################################### - ############################################################################## - prepare: - name: Prepare - runs-on: ubuntu-latest - # needs: [nothing] - steps: - ########################################################################## - # CHECKOUT CODE ########################################################## - ########################################################################## - - name: Checkout code - uses: actions/checkout@v3 - ########################################################################## - # TODO: DO STUFF ??? ##################################################### - ########################################################################## - - name: Check translation files - run: | - scripts/translations/sort.sh - scripts/translations/missing-keys.sh - ############################################################################## # JOB: DOCKER BUILD COMMUNITY NEO4J ########################################## ############################################################################## build_production_neo4j: name: Docker Build Production - Neo4J runs-on: ubuntu-latest - needs: [prepare] steps: - ########################################################################## - # CHECKOUT CODE ########################################################## - ########################################################################## - name: Checkout code uses: actions/checkout@v3 - ########################################################################## - # SET ENVS ############################################################### - ########################################################################## - - name: ENV - VERSION - run: echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV - - name: ENV - BUILD_DATE - run: echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV - - name: ENV - BUILD_VERSION - run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV - - name: ENV - BUILD_COMMIT - run: echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV - ########################################################################## - # NEO4J ################################################################## - ########################################################################## + - name: Setup env + run: | + echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV + echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV + echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV + - run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV - name: Neo4J | Build `community` image - run: docker build --target community -t "ocelotsocialnetwork/neo4j-community:latest" -t "ocelotsocialnetwork/neo4j-community:${VERSION}" -t "ocelotsocialnetwork/neo4j-community:${BUILD_VERSION}" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT neo4j/ + run: | + docker build --target community \ + --tag "ocelotsocialnetwork/neo4j-community:latest" \ + --tag "ocelotsocialnetwork/neo4j-community:${VERSION}" \ + --tag "ocelotsocialnetwork/neo4j-community:${BUILD_VERSION}" \ + --build-arg BBUILD_DATE=$BUILD_DATE \ + --build-arg BBUILD_VERSION=$BUILD_VERSION \ + --build-arg BBUILD_COMMIT=$BUILD_COMMIT \ + neo4j/ - name: Neo4J | Save docker image run: docker save "ocelotsocialnetwork/neo4j-community" > /tmp/neo4j.tar - name: Upload Artifact @@ -73,32 +45,41 @@ jobs: build_production_backend: name: Docker Build Production - Backend runs-on: ubuntu-latest - needs: [prepare] steps: - ########################################################################## - # CHECKOUT CODE ########################################################## - ########################################################################## - name: Checkout code uses: actions/checkout@v3 - ########################################################################## - # SET ENVS ############################################################### - ########################################################################## - - name: ENV - VERSION - run: echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV - - name: ENV - BUILD_DATE - run: echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV - - name: ENV - BUILD_VERSION - run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV - - name: ENV - BUILD_COMMIT - run: echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV - ########################################################################## - # BUILD BACKEND DOCKER IMAGE (production) ################################ - ########################################################################## + - name: Setup env + run: | + echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV + echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV + echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV + - run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV - name: Backend | Build `production` image run: | - docker build --target base -t "ocelotsocialnetwork/backend:latest-base" -t "ocelotsocialnetwork/backend:${VERSION}-base" -t "ocelotsocialnetwork/backend:${BUILD_VERSION}-base" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT backend/ - docker build --target code -t "ocelotsocialnetwork/backend:latest-code" -t "ocelotsocialnetwork/backend:${VERSION}-code" -t "ocelotsocialnetwork/backend:${BUILD_VERSION}-code" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT backend/ - docker build --target production -t "ocelotsocialnetwork/backend:latest" -t "ocelotsocialnetwork/backend:${VERSION}" -t "ocelotsocialnetwork/backend:${BUILD_VERSION}" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT backend/ + docker build --target base \ + --tag "ocelotsocialnetwork/backend:latest-base" \ + --tag "ocelotsocialnetwork/backend:${VERSION}-base" \ + --tag "ocelotsocialnetwork/backend:${BUILD_VERSION}-base" \ + --build-arg BBUILD_DATE=$BUILD_DATE \ + --build-arg BBUILD_VERSION=$BUILD_VERSION \ + --build-arg BBUILD_COMMIT=$BUILD_COMMIT \ + backend/ + docker build --target code \ + --tag "ocelotsocialnetwork/backend:latest-code" \ + --tag "ocelotsocialnetwork/backend:${VERSION}-code" \ + --tag "ocelotsocialnetwork/backend:${BUILD_VERSION}-code" \ + --build-arg BBUILD_DATE=$BUILD_DATE \ + --build-arg BBUILD_VERSION=$BUILD_VERSION \ + --build-arg BBUILD_COMMIT=$BUILD_COMMIT \ + backend/ + docker build --target production \ + --tag "ocelotsocialnetwork/backend:latest" \ + --tag "ocelotsocialnetwork/backend:${VERSION}" \ + --tag "ocelotsocialnetwork/backend:${BUILD_VERSION}" \ + --build-arg BBUILD_DATE=$BUILD_DATE \ + --build-arg BBUILD_VERSION=$BUILD_VERSION \ + --build-arg BBUILD_COMMIT=$BUILD_COMMIT \ + backend/ - name: Backend | Save docker image run: docker save "ocelotsocialnetwork/backend" > /tmp/backend.tar - name: Upload Artifact @@ -113,32 +94,41 @@ jobs: build_production_webapp: name: Docker Build Production - WebApp runs-on: ubuntu-latest - needs: [prepare] steps: - ########################################################################## - # CHECKOUT CODE ########################################################## - ########################################################################## - name: Checkout code uses: actions/checkout@v3 - ########################################################################## - # SET ENVS ############################################################### - ########################################################################## - - name: ENV - VERSION - run: echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV - - name: ENV - BUILD_DATE - run: echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV - - name: ENV - BUILD_VERSION - run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV - - name: ENV - BUILD_COMMIT - run: echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV - ########################################################################## - # BUILD WEBAPP DOCKER IMAGE (build) ###################################### - ########################################################################## + - name: Setup env + run: | + echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV + echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV + echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV + - run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV - name: Webapp | Build `production` image run: | - docker build --target base -t "ocelotsocialnetwork/webapp:latest-base" -t "ocelotsocialnetwork/webapp:${VERSION}-base" -t "ocelotsocialnetwork/webapp:${BUILD_VERSION}-base" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT webapp/ - docker build --target code -t "ocelotsocialnetwork/webapp:latest-code" -t "ocelotsocialnetwork/webapp:${VERSION}-code" -t "ocelotsocialnetwork/webapp:${BUILD_VERSION}-code" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT webapp/ - docker build --target production -t "ocelotsocialnetwork/webapp:latest" -t "ocelotsocialnetwork/webapp:${VERSION}" -t "ocelotsocialnetwork/webapp:${BUILD_VERSION}" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT webapp/ + docker build --target base \ + --tag "ocelotsocialnetwork/webapp:latest-base" \ + --tag "ocelotsocialnetwork/webapp:${VERSION}-base" \ + --tag "ocelotsocialnetwork/webapp:${BUILD_VERSION}-base" \ + --build-arg BBUILD_DATE=$BUILD_DATE \ + --build-arg BBUILD_VERSION=$BUILD_VERSION \ + --build-arg BBUILD_COMMIT=$BUILD_COMMIT \ + webapp/ + docker build --target code \ + --tag "ocelotsocialnetwork/webapp:latest-code" \ + --tag "ocelotsocialnetwork/webapp:${VERSION}-code" \ + --tag "ocelotsocialnetwork/webapp:${BUILD_VERSION}-code" \ + --build-arg BBUILD_DATE=$BUILD_DATE \ + --build-arg BBUILD_VERSION=$BUILD_VERSION \ + --build-arg BBUILD_COMMIT=$BUILD_COMMIT \ + webapp/ + docker build --target production \ + --tag "ocelotsocialnetwork/webapp:latest" \ + --tag "ocelotsocialnetwork/webapp:${VERSION}" \ + --tag "ocelotsocialnetwork/webapp:${BUILD_VERSION}" \ + --build-arg BBUILD_DATE=$BUILD_DATE \ + --build-arg BBUILD_VERSION=$BUILD_VERSION \ + --build-arg BBUILD_COMMIT=$BUILD_COMMIT \ + webapp/ - name: Webapp | Save docker image run: docker save "ocelotsocialnetwork/webapp" > /tmp/webapp.tar - name: Upload Artifact @@ -153,32 +143,44 @@ jobs: build_production_maintenance: name: Docker Build Production - Maintenance runs-on: ubuntu-latest - needs: [prepare] steps: - ########################################################################## - # CHECKOUT CODE ########################################################## - ########################################################################## - name: Checkout code uses: actions/checkout@v3 - ########################################################################## - # SET ENVS ############################################################### - ########################################################################## - - name: ENV - VERSION - run: echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV - - name: ENV - BUILD_DATE - run: echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV - - name: ENV - BUILD_VERSION - run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV - - name: ENV - BUILD_COMMIT - run: echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV - ########################################################################## - # BUILD MAINTENANCE DOCKER IMAGE (build) ################################# - ########################################################################## + - name: Setup env + run: | + echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV + echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV + echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV + - run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV - name: Maintenance | Build `production` image run: | - docker build --target base -t "ocelotsocialnetwork/maintenance:latest-base" -t "ocelotsocialnetwork/maintenance:${VERSION}-base" -t "ocelotsocialnetwork/maintenance:${BUILD_VERSION}-base" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT webapp/ -f webapp/Dockerfile.maintenance - docker build --target code -t "ocelotsocialnetwork/maintenance:latest-code" -t "ocelotsocialnetwork/maintenance:${VERSION}-code" -t "ocelotsocialnetwork/maintenance:${BUILD_VERSION}-code" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT webapp/ -f webapp/Dockerfile.maintenance - docker build --target production -t "ocelotsocialnetwork/maintenance:latest" -t "ocelotsocialnetwork/maintenance:${VERSION}" -t "ocelotsocialnetwork/maintenance:${BUILD_VERSION}" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT webapp/ -f webapp/Dockerfile.maintenance + docker build --target base \ + --tag "ocelotsocialnetwork/maintenance:latest-base" \ + --tag "ocelotsocialnetwork/maintenance:${VERSION}-base" \ + --tag "ocelotsocialnetwork/maintenance:${BUILD_VERSION}-base" \ + --build-arg BBUILD_DATE=$BUILD_DATE \ + --build-arg BBUILD_VERSION=$BUILD_VERSION \ + --build-arg BBUILD_COMMIT=$BUILD_COMMIT \ + -f webapp/Dockerfile.maintenance \ + webapp/ + docker build --target code \ + --tag "ocelotsocialnetwork/maintenance:latest-code" \ + --tag "ocelotsocialnetwork/maintenance:${VERSION}-code" \ + --tag "ocelotsocialnetwork/maintenance:${BUILD_VERSION}-code" \ + --build-arg BBUILD_DATE=$BUILD_DATE \ + --build-arg BBUILD_VERSION=$BUILD_VERSION \ + --build-arg BBUILD_COMMIT=$BUILD_COMMIT \ + -f webapp/Dockerfile.maintenance \ + webapp/ + docker build --target production \ + --tag "ocelotsocialnetwork/maintenance:latest" \ + --tag "ocelotsocialnetwork/maintenance:${VERSION}" \ + --tag "ocelotsocialnetwork/maintenance:${BUILD_VERSION}" \ + --build-arg BBUILD_DATE=$BUILD_DATE \ + --build-arg BBUILD_VERSION=$BUILD_VERSION \ + --build-arg BBUILD_COMMIT=$BUILD_COMMIT \ + -f webapp/Dockerfile.maintenance \ + webapp/ - name: Maintenance | Save docker image run: docker save "ocelotsocialnetwork/maintenance" > /tmp/maintenance.tar - name: Upload Artifact @@ -198,130 +200,39 @@ jobs: DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} steps: - ########################################################################## - # CHECKOUT CODE ########################################################## - ########################################################################## - - name: Checkout code - uses: actions/checkout@v3 - ########################################################################## - # DOWNLOAD DOCKER IMAGES ################################################# - ########################################################################## - name: Download Docker Image (Neo4J) uses: actions/download-artifact@v3 with: name: docker-neo4j-community path: /tmp - - name: Load Docker Image - run: docker load < /tmp/neo4j.tar + - run: docker load < /tmp/neo4j.tar - name: Download Docker Image (Backend) uses: actions/download-artifact@v3 with: name: docker-backend-production path: /tmp - - name: Load Docker Image - run: docker load < /tmp/backend.tar + - run: docker load < /tmp/backend.tar - name: Download Docker Image (WebApp) uses: actions/download-artifact@v3 with: name: docker-webapp-production path: /tmp - - name: Load Docker Image - run: docker load < /tmp/webapp.tar + - run: docker load < /tmp/webapp.tar - name: Download Docker Image (Maintenance) uses: actions/download-artifact@v3 with: name: docker-maintenance-production path: /tmp - - name: Load Docker Image - run: docker load < /tmp/maintenance.tar - ########################################################################## - # Upload ################################################################# - ########################################################################## + - run: docker load < /tmp/maintenance.tar + - name: login to dockerhub run: echo "${DOCKERHUB_TOKEN}" | docker login -u "${DOCKERHUB_USERNAME}" --password-stdin - - name: Push neo4j - run: docker push --all-tags ocelotsocialnetwork/neo4j-community - - name: Push backend - run: docker push --all-tags ocelotsocialnetwork/backend - - name: Push webapp - run: docker push --all-tags ocelotsocialnetwork/webapp - - name: Push maintenance - run: docker push --all-tags ocelotsocialnetwork/maintenance - - ############################################################################## - # JOB: KUBERNETES DEPLOY ACTUAL/LATEST VERSION ###################################### - ############################################################################## - kubernetes_deploy: - # see example https://github.com/do-community/example-doctl-action - # see example https://github.com/do-community/example-doctl-action/blob/main/.github/workflows/workflow.yaml - name: Kubernetes deploy of latest version to stage.ocelot.social cluster at DigitalOcean - runs-on: ubuntu-latest - needs: [upload_to_dockerhub] - steps: - ########################################################################## - # CHECKOUT CODE ########################################################## - ########################################################################## - - name: Checkout code - uses: actions/checkout@v3 - ########################################################################## - # SET ENVS ############################################################### - ########################################################################## - - name: ENV - VERSION - run: echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV - - name: ENV - BUILD_VERSION - run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV - ########################################################################## - # Install DigitalOceans doctl and set kubeconfig ######################### - ########################################################################## - - name: Install doctl - uses: digitalocean/action-doctl@v2 - with: - token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} - - name: Save DigitalOcean kubeconfig with short-lived credentials - run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 cluster-stage-ocelot-social - ########################################################################## - # Deploy new Docker images to DigitalOcean Kubernetes cluster ############ - ########################################################################## - # - name: Deploy 'latest' to DigitalOcean Kubernetes - # run: | - # kubectl -n default set image deployment/ocelot-webapp container-ocelot-webapp=ocelotsocialnetwork/webapp:latest - # kubectl -n default rollout restart deployment/ocelot-webapp - # kubectl -n default set image deployment/ocelot-backend container-ocelot-backend=ocelotsocialnetwork/backend:latest - # kubectl -n default rollout restart deployment/ocelot-backend - # kubectl -n default set image deployment/ocelot-maintenance container-ocelot-maintenance=ocelotsocialnetwork/maintenance:latest - # kubectl -n default rollout restart deployment/ocelot-maintenance - # kubectl -n default set image deployment/ocelot-neo4j container-ocelot-neo4j=ocelotsocialnetwork/neo4j-community:latest - # kubectl -n default rollout restart deployment/ocelot-neo4j - - name: Deploy actual version '$BUILD_VERSION' to DigitalOcean Kubernetes + - name: Push images run: | - kubectl -n default set image deployment/ocelot-webapp container-ocelot-webapp=ocelotsocialnetwork/webapp:$BUILD_VERSION - kubectl -n default rollout restart deployment/ocelot-webapp - kubectl -n default set image deployment/ocelot-backend container-ocelot-backend=ocelotsocialnetwork/backend:$BUILD_VERSION - kubectl -n default rollout restart deployment/ocelot-backend - kubectl -n default set image deployment/ocelot-maintenance container-ocelot-maintenance=ocelotsocialnetwork/maintenance:$BUILD_VERSION - kubectl -n default rollout restart deployment/ocelot-maintenance - kubectl -n default set image deployment/ocelot-neo4j container-ocelot-neo4j=ocelotsocialnetwork/neo4j-community:$BUILD_VERSION - kubectl -n default rollout restart deployment/ocelot-neo4j - # because this step 'kubectl -n default rollout status deployment/* --timeout=600s' does not work as expected - # and we need the pods to be up again for cleaning and seeding the Neo4j database and the backend. - # !!! this is not a perfect solution !!! - # deployments are regularly up again after 3 minutes and 10 seconds - - name: Sleep for 4 minutes, means 240 seconds - run: sleep 240s - shell: bash - - name: Verify deployment and wait for the pods of each deployment to get ready for cleaning and seeding of the database - run: | - kubectl -n default rollout status deployment/ocelot-backend --timeout=600s - kubectl -n default rollout status deployment/ocelot-neo4j --timeout=600s - kubectl -n default rollout status deployment/ocelot-maintenance --timeout=600s - kubectl -n default rollout status deployment/ocelot-webapp --timeout=600s - - name: Run migrations for Neo4j database via backend for staging - run: | - kubectl -n default exec -it $(kubectl -n default get pods | grep ocelot-backend | awk '{ print $1 }') -- /bin/sh -c "yarn prod:migrate up" - - name: Reset and seed Neo4j database via backend for staging - # db cleaning and seeding is only possible in production if env 'PRODUCTION_DB_CLEAN_ALLOW=true' is set in deployment - run: | - kubectl -n default exec -it $(kubectl -n default get pods | grep ocelot-backend | awk '{ print $1 }') -- /bin/sh -c "node --experimental-repl-await dist/db/clean.js && node --experimental-repl-await dist/db/seed.js" + docker push --all-tags ocelotsocialnetwork/neo4j-community + docker push --all-tags ocelotsocialnetwork/backend + docker push --all-tags ocelotsocialnetwork/webapp + docker push --all-tags ocelotsocialnetwork/maintenance ############################################################################## # JOB: GITHUB TAG LATEST VERSION ############################################# @@ -331,39 +242,16 @@ jobs: runs-on: ubuntu-latest needs: [upload_to_dockerhub] steps: - ########################################################################## - # CHECKOUT CODE ########################################################## - ########################################################################## - name: Checkout code uses: actions/checkout@v3 with: fetch-depth: 0 # Fetch full History for changelog - ########################################################################## - # SET ENVS ############################################################### - ########################################################################## - - name: ENV - VERSION - run: echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV - - name: ENV - BUILD_DATE - run: echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV - - name: ENV - BUILD_VERSION - run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV - - name: ENV - BUILD_COMMIT - run: echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV - ########################################################################## - # Push version tag to GitHub ############################################# - ########################################################################## - # TODO: this will error on duplicate - #- name: package-version-to-git-tag - # uses: pkgdeps/git-tag-action@v2 - # with: - # github_token: ${{ secrets.GITHUB_TOKEN }} - # github_repo: ${{ github.repository }} - # version: ${{ env.VERSION }} - # git_commit_sha: ${{ github.sha }} - # git_tag_prefix: "v" - ########################################################################## - # Push build tag to GitHub ############################################### - ########################################################################## + - name: Setup env + run: | + echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV + echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV + echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV + - run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV - name: package-version-to-git-tag + build number uses: pkgdeps/git-tag-action@v2 with: @@ -372,13 +260,10 @@ jobs: version: ${{ env.BUILD_VERSION }} git_commit_sha: ${{ github.sha }} git_tag_prefix: "b" - ########################################################################## - # Push release tag to GitHub ############################################# - ########################################################################## - - name: yarn install - run: yarn install - - name: generate changelog - run: yarn auto-changelog --latest-version ${{ env.VERSION }} --unreleased-only + - name: Generate changelog + run: | + yarn install + yarn auto-changelog --latest-version ${{ env.VERSION }} --unreleased-only - name: package-version-to-git-release continue-on-error: true # Will fail if tag exists id: create_release @@ -391,3 +276,101 @@ jobs: body_path: ./CHANGELOG.md draft: false prerelease: false + + build_trigger: + name: Trigger successful build + runs-on: ubuntu-latest + needs: [github_tag] + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + fetch-depth: 0 # Fetch full History for changelog + - name: Setup env + run: | + echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV + echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV + echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV + - run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV + - name: Repository Dispatch + uses: peter-evans/repository-dispatch@v1 + with: + token: ${{ github.token }} + event-type: trigger-build-success + repository: ${{ github.repository }} + client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}", "VERSION": "${VERSION}", "BUILD_DATE": "${BUILD_DATE}", "BUILD_COMMIT": "${BUILD_COMMIT}", "BUILD_VERSION": "${BUILD_VERSION}"}' + + # ############################################################################## + # # JOB: KUBERNETES DEPLOY ACTUAL/LATEST VERSION ###################################### + # ############################################################################## + # kubernetes_deploy: + # # see example https://github.com/do-community/example-doctl-action + # # see example https://github.com/do-community/example-doctl-action/blob/main/.github/workflows/workflow.yaml + # name: Kubernetes deploy of latest version to stage.ocelot.social cluster at DigitalOcean + # runs-on: ubuntu-latest + # needs: [upload_to_dockerhub] + # steps: + # ########################################################################## + # # CHECKOUT CODE ########################################################## + # ########################################################################## + # - name: Checkout code + # uses: actions/checkout@v3 + # ########################################################################## + # # SET ENVS ############################################################### + # ########################################################################## + # - name: ENV - VERSION + # run: echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV + # - name: ENV - BUILD_VERSION + # run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV + # ########################################################################## + # # Install DigitalOceans doctl and set kubeconfig ######################### + # ########################################################################## + # - name: Install doctl + # uses: digitalocean/action-doctl@v2 + # with: + # token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} + # - name: Save DigitalOcean kubeconfig with short-lived credentials + # run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 cluster-stage-ocelot-social + # ########################################################################## + # # Deploy new Docker images to DigitalOcean Kubernetes cluster ############ + # ########################################################################## + # # - name: Deploy 'latest' to DigitalOcean Kubernetes + # # run: | + # # kubectl -n default set image deployment/ocelot-webapp container-ocelot-webapp=ocelotsocialnetwork/webapp:latest + # # kubectl -n default rollout restart deployment/ocelot-webapp + # # kubectl -n default set image deployment/ocelot-backend container-ocelot-backend=ocelotsocialnetwork/backend:latest + # # kubectl -n default rollout restart deployment/ocelot-backend + # # kubectl -n default set image deployment/ocelot-maintenance container-ocelot-maintenance=ocelotsocialnetwork/maintenance:latest + # # kubectl -n default rollout restart deployment/ocelot-maintenance + # # kubectl -n default set image deployment/ocelot-neo4j container-ocelot-neo4j=ocelotsocialnetwork/neo4j-community:latest + # # kubectl -n default rollout restart deployment/ocelot-neo4j + # - name: Deploy actual version '$BUILD_VERSION' to DigitalOcean Kubernetes + # run: | + # kubectl -n default set image deployment/ocelot-webapp container-ocelot-webapp=ocelotsocialnetwork/webapp:$BUILD_VERSION + # kubectl -n default rollout restart deployment/ocelot-webapp + # kubectl -n default set image deployment/ocelot-backend container-ocelot-backend=ocelotsocialnetwork/backend:$BUILD_VERSION + # kubectl -n default rollout restart deployment/ocelot-backend + # kubectl -n default set image deployment/ocelot-maintenance container-ocelot-maintenance=ocelotsocialnetwork/maintenance:$BUILD_VERSION + # kubectl -n default rollout restart deployment/ocelot-maintenance + # kubectl -n default set image deployment/ocelot-neo4j container-ocelot-neo4j=ocelotsocialnetwork/neo4j-community:$BUILD_VERSION + # kubectl -n default rollout restart deployment/ocelot-neo4j + # # because this step 'kubectl -n default rollout status deployment/* --timeout=600s' does not work as expected + # # and we need the pods to be up again for cleaning and seeding the Neo4j database and the backend. + # # !!! this is not a perfect solution !!! + # # deployments are regularly up again after 3 minutes and 10 seconds + # - name: Sleep for 4 minutes, means 240 seconds + # run: sleep 240s + # shell: bash + # - name: Verify deployment and wait for the pods of each deployment to get ready for cleaning and seeding of the database + # run: | + # kubectl -n default rollout status deployment/ocelot-backend --timeout=600s + # kubectl -n default rollout status deployment/ocelot-neo4j --timeout=600s + # kubectl -n default rollout status deployment/ocelot-maintenance --timeout=600s + # kubectl -n default rollout status deployment/ocelot-webapp --timeout=600s + # - name: Run migrations for Neo4j database via backend for staging + # run: | + # kubectl -n default exec -it $(kubectl -n default get pods | grep ocelot-backend | awk '{ print $1 }') -- /bin/sh -c "yarn prod:migrate up" + # - name: Reset and seed Neo4j database via backend for staging + # # db cleaning and seeding is only possible in production if env 'PRODUCTION_DB_CLEAN_ALLOW=true' is set in deployment + # run: | + # kubectl -n default exec -it $(kubectl -n default get pods | grep ocelot-backend | awk '{ print $1 }') -- /bin/sh -c "node --experimental-repl-await dist/db/clean.js && node --experimental-repl-await dist/db/seed.js" \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 96aaf53a1..46d80241f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -168,6 +168,8 @@ jobs: name: Unit tests - backend runs-on: ubuntu-latest needs: [build_test_neo4j,build_test_backend] + permissions: + checks: write steps: ########################################################################## # CHECKOUT CODE ########################################################## @@ -225,6 +227,8 @@ jobs: name: Unit tests - webapp runs-on: ubuntu-latest needs: [build_test_webapp] + permissions: + checks: write steps: ########################################################################## # CHECKOUT CODE ########################################################## @@ -269,7 +273,7 @@ jobs: report_name: Coverage Webapp type: lcov result_path: ./coverage/lcov.info - min_coverage: 63 + min_coverage: 83 token: ${{ github.token }} ############################################################################## diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 83a63b36a..650785331 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -174,34 +174,14 @@ Please copy and paste the following quotes for the languages: #### Environment Variable For Apple M1 Platform -To set the Docker platform environment variable in your terminal tab, run: +If you encounter trouble building the docker containers on an Apple M1 chip you can try to explicitly define the target platform docker builds and pulls images for: ```bash # set env variable for your shell $ export DOCKER_DEFAULT_PLATFORM=linux/amd64 ``` -#### Docker Compose Override File For Apple M1 Platform - -For Docker compose `up` or `build` commands, you can use our Apple M1 override file that specifies the M1 platform: - -```bash -# in main folder - -# for development -$ docker compose -f docker-compose.yml -f docker-compose.override.yml -f docker-compose.apple-m1.override.yml up -# only once: init admin user and create indexes and contraints in Neo4j database -$ docker compose exec backend yarn prod:migrate init -# clean db -$ docker compose exec backend yarn db:reset -# seed db -$ docker compose exec backend yarn db:seed - -# for production -$ docker compose -f docker-compose.yml -f docker-compose.apple-m1.override.yml up -# only once: init admin user and create indexes and contraints in Neo4j database -$ docker compose exec backend /bin/sh -c "yarn prod:migrate init" -``` +Or alternatively use a `YAML` docker compose overwrite file with `platform: linux/amd64`. ### Analyzing Docker Builds diff --git a/README.md b/README.md index f0688f820..8ddf97fe5 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,71 @@ At the same time, it should be possible in the future to link these networks wit In other words, we are interested in a network of networks and in keeping the data as close as possible to the user and the operator they trusts. -## Introduction +## Screenshots + +Post feed + +Check out more screenshots [here](/wiki/en:Screenshots). + +## Features + +Ocelot.social networks feature: +* user accounts +* user roles +* posts and comments +* groups +* maps +* search +* filters +* and more … + +Check out the [full feature list](/wiki/en:FAQ#what-are-the-features). + +## User Guide and Frequently Asked Questions + +In the [wiki](/wiki) you can find more information. + +* [User Guide](/wiki/en:User-Guide) +* [Frequently Asked Questions](/wiki/en:FAQ) + +## Demo + +Try out our live demo network, see [here](#live-demo-and-developer-logins). + +## Help us + +If you're wondering how you could help, there are plenty of ways, e.g.: + +* Spread the good word about ocelot.social to make it more popular: + * Add the link [ocelot.social](https://ocelot.social) to your website. + * Give ocelot.social a Like at https://alternativeto.net/software/ocelot-social/. + * Star our project on GitHub at https://github.com/Ocelot-Social-Community/Ocelot-Social/. + * Promote it on your social networks. + * Tell your friends about it by word-of-mouth. + * Write a press article on ocelot.social or contact the editorial office of your local newspage or radio station. +* Take a [good first issue](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) or issues that need help. Make sure to read [CONTRIBUTING.md](/CONTRIBUTING.md) first though. +* Testing and [reporting](/issues/new/choose) bugs. +* Translating: Please [contact us](#contact). +* Reviewing and testing pull requests. + +## Donate + +Your donation is very welcome and helps to enhance and improve the network. This software is mostly developped and maintained by the association [busFaktor() e.V.](https://www.busfaktor.org/en). Please support us with a [donation](https://www.busfaktor.org/en/donations) to busFaktor() e.V.. Thanks a lot! ❤️ + +## Contact + +Are you interested in operating your own ocelot.social network or becoming a user? Please contact us here: +* [hello@ocelot.social](mailto:hello@ocelot.social) +* [Discord](https://discord.com/invite/DFSjPaX) + +## For Developers and Contributors + +### Introduction Have a look into our short video: [ocelot.social - GitHub - Developer Welcome - Tutorial (english)](https://www.youtube.com/watch?v=gZSL6KvBIiY&list=PLFMD5liPP01kbuReHxYXxv_1fI5rIgS1f&index=1) -## Directory Layout +### Directory Layout There are three important directories: @@ -41,9 +100,9 @@ There are two approaches: 1. [Local](#local-installation) installation, which means you have to take care of dependencies yourself. 2. **Or** Install everything through [Docker](#docker-installation) which takes care of dependencies for you. -## Installation +### Installation -### Clone the Repository +#### Clone the Repository Clone the repository, this will create a new folder called `Ocelot-Social`: @@ -65,7 +124,7 @@ Change into the new folder. $ cd Ocelot-Social ``` -## Live Demo And Developer Logins +### Live Demo And Developer Logins **Try out our deployed [development environment](https://stage.ocelot.social).** @@ -74,7 +133,7 @@ Visit our staging networks: * central staging network: [stage.ocelot.social](https://stage.ocelot.social) -### Login +#### Login Logins for the live demos and developers (local developers after the following installations) in the browser: @@ -84,11 +143,11 @@ Logins for the live demos and developers (local developers after the following i | `moderator@example.org` | 1234 | moderator | | `admin@example.org` | 1234 | admin | -### Docker Installation +#### Docker Installation Docker is a software development container tool that combines software and its dependencies into one standardized unit that contains everything needed to run it. This helps us to avoid problems with dependencies and makes installation easier. -#### General Installation of Docker +##### General Installation of Docker There are [several ways to install Docker CE](https://docs.docker.com/install/) on your computer or server. @@ -105,7 +164,7 @@ $ docker-compose --version docker-compose version 1.23.2 ``` -#### Start Ocelot-Social via Docker-Compose +##### Start Ocelot-Social via Docker-Compose Prepare ENVs once beforehand: @@ -152,11 +211,11 @@ $ docker-compose exec backend yarn run db:seed For a closer description see [backend README.md](./backend/README.md). For a full documentation see [SUMMARY](./SUMMARY.md). -### Local Installation +#### Local Installation For a full documentation see [SUMMARY](./SUMMARY.md). -## Contributing +### Contributing Choose an issue (consider our label [good-first-issue](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)) and leave a comment there. We will then invite you to join our volunteers team. To have the necessary permission to push directly to this repository, please accept our invitation to join our volunteers team, you will receive via the email, Github will send you, once invited. If we did not invite you yet, please request an invitation via Discord. @@ -182,7 +241,7 @@ $ yarn test Check out our [contribution guideline](./CONTRIBUTING.md), too! -### Developer Chat +#### Developer Chat Join our friendly open-source community on [Discord](https://discord.gg/AJSX9DCSUA) :heart_eyes_cat: Just introduce yourself at `#introduce-yourself` and mention a mentor or `@@Mentors` to get you onboard :neckbeard: @@ -190,13 +249,13 @@ Just introduce yourself at `#introduce-yourself` and mention a mentor or `@@Ment We give write permissions to every developer who asks for it. Just text us on [Discord](https://discord.gg/AJSX9DCSUA). -## Deployment +### Deployment Deployment methods can be found in the [Ocelot-Social-Deploy-Rebranding](https://github.com/Ocelot-Social-Community/Ocelot-Social-Deploy-Rebranding) repository. The only deployment method in this repository for development purposes as described above is `docker-compose`. -## Technology Stack +### Technology Stack * [VueJS](https://vuejs.org/) * [NuxtJS](https://nuxtjs.org/) @@ -204,7 +263,7 @@ The only deployment method in this repository for development purposes as descri * [NodeJS](https://nodejs.org/en/) * [Neo4J](https://neo4j.com/) -### For Testing +#### For Testing * [Cypress](https://docs.cypress.io/) * [Storybook](https://storybook.js.org/) @@ -212,7 +271,7 @@ The only deployment method in this repository for development purposes as descri * [Vue Test Utils](https://vue-test-utils.vuejs.org/) * [ESLint](https://eslint.org/) -## Attributions +### Attributions Locale Icons made by [Freepik](http://www.freepik.com/) from [www.flaticon.com](https://www.flaticon.com/) is licensed by [CC 3.0 BY](http://creativecommons.org/licenses/by/3.0/). @@ -220,6 +279,6 @@ Browser compatibility testing with [BrowserStack](https://www.browserstack.com/) BrowserStack Logo -## License +### License See the [LICENSE](LICENSE.md) file for license rights and limitations (MIT). diff --git a/backend/README.md b/backend/README.md index 4ce74dc40..03724ce54 100644 --- a/backend/README.md +++ b/backend/README.md @@ -79,9 +79,9 @@ More details about our GraphQL playground and how to use it with ocelot.social c ![GraphQL Playground](../.gitbook/assets/graphql-playground.png) -### Database Indices and Constraints +### Database Indexes and Constraints -Database indices and constraints need to be created when the database and the +Database indexes and constraints need to be created when the database and the backend is running: {% tabs %} @@ -90,6 +90,12 @@ backend is running: ```bash # in main folder while docker-compose is running $ docker exec backend yarn run db:migrate init + +# only once: init admin user and create indexes and constraints in Neo4j database +# for development +$ docker compose exec backend yarn prod:migrate init +# in production mode use command +$ docker compose exec backend /bin/sh -c "yarn prod:migrate init" ``` {% endtab %} @@ -126,7 +132,7 @@ To reset the database run: $ docker exec backend yarn run db:reset # you could also wipe out your neo4j database and delete all volumes with: $ docker-compose down -v -# if container is not running, run this command to set up your database indeces and contstraints +# if container is not running, run this command to set up your database indexes and constraints $ docker exec backend yarn run db:migrate init ``` diff --git a/backend/src/db/factories.js b/backend/src/db/factories.js index 3e164d51b..ecc792736 100644 --- a/backend/src/db/factories.js +++ b/backend/src/db/factories.js @@ -65,7 +65,6 @@ Factory.define('basicUser') name: faker.name.findName, password: '1234', role: 'user', - about: faker.lorem.paragraph, termsAndConditionsAgreedVersion: '0.0.1', termsAndConditionsAgreedAt: '2019-08-01T10:47:19.212Z', allowEmbedIframes: false, @@ -82,12 +81,28 @@ Factory.define('basicUser') Factory.define('userWithoutEmailAddress') .extend('basicUser') + .option('about', faker.lorem.paragraph) + .after(async (buildObject, options) => { + return neode.create('User', buildObject) + }) + +Factory.define('userWithAboutNull') + .extend('basicUser') + .option('about', null) + .after(async (buildObject, options) => { + return neode.create('User', buildObject) + }) + +Factory.define('userWithAboutEmpty') + .extend('basicUser') + .option('about', '') .after(async (buildObject, options) => { return neode.create('User', buildObject) }) Factory.define('user') .extend('basicUser') + .option('about', faker.lorem.paragraph) .option('email', faker.internet.exampleEmail) .option('avatar', () => Factory.build('image', { diff --git a/backend/src/graphql/groups.js b/backend/src/graphql/groups.js index e388b2cd9..a7cfc3351 100644 --- a/backend/src/graphql/groups.js +++ b/backend/src/graphql/groups.js @@ -150,6 +150,19 @@ export const changeGroupMemberRoleMutation = () => { ` } +export const removeUserFromGroupMutation = () => { + return gql` + mutation ($groupId: ID!, $userId: ID!) { + RemoveUserFromGroup(groupId: $groupId, userId: $userId) { + id + name + slug + myRoleInGroup + } + } + ` +} + // ------ queries export const groupQuery = () => { diff --git a/backend/src/helpers/jest.js b/backend/src/helpers/jest.js index e3f6a3c84..09744e9f2 100644 --- a/backend/src/helpers/jest.js +++ b/backend/src/helpers/jest.js @@ -1,13 +1,3 @@ -// TODO: can be replaced with: (which is no a fake) -// import gql from 'graphql-tag' -// See issue: https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/5152 - -//* This is a fake ES2015 template string, just to benefit of syntax -// highlighting of `gql` template strings in certain editors. -export function gql(strings) { - return strings.join('') -} - // sometime we have to wait to check a db state by having a look into the db in a certain moment // or we wait a bit to check if we missed to set an await somewhere // see: https://www.sitepoint.com/delay-sleep-pause-wait/ diff --git a/backend/src/middleware/hashtags/hashtagsMiddleware.spec.js b/backend/src/middleware/hashtags/hashtagsMiddleware.spec.js index bcecee73b..10d53ab7b 100644 --- a/backend/src/middleware/hashtags/hashtagsMiddleware.spec.js +++ b/backend/src/middleware/hashtags/hashtagsMiddleware.spec.js @@ -1,4 +1,4 @@ -import { gql } from '../../helpers/jest' +import gql from 'graphql-tag' import { cleanDatabase } from '../../db/factories' import { createTestClient } from 'apollo-server-testing' import { getNeode, getDriver } from '../../db/neo4j' diff --git a/backend/src/middleware/languages/languages.spec.js b/backend/src/middleware/languages/languages.spec.js index 99e87152e..f4f57adc4 100644 --- a/backend/src/middleware/languages/languages.spec.js +++ b/backend/src/middleware/languages/languages.spec.js @@ -1,5 +1,5 @@ import Factory, { cleanDatabase } from '../../db/factories' -import { gql } from '../../helpers/jest' +import gql from 'graphql-tag' import { getNeode, getDriver } from '../../db/neo4j' import createServer from '../../server' import { createTestClient } from 'apollo-server-testing' diff --git a/backend/src/middleware/notifications/notificationsMiddleware.spec.js b/backend/src/middleware/notifications/notificationsMiddleware.spec.js index ad7fd3f8c..a8a5d396b 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.spec.js +++ b/backend/src/middleware/notifications/notificationsMiddleware.spec.js @@ -1,4 +1,4 @@ -import { gql } from '../../helpers/jest' +import gql from 'graphql-tag' import { cleanDatabase } from '../../db/factories' import { createTestClient } from 'apollo-server-testing' import { getNeode, getDriver } from '../../db/neo4j' diff --git a/backend/src/middleware/orderByMiddleware.spec.js b/backend/src/middleware/orderByMiddleware.spec.js index 1aef1c52a..7453cf301 100644 --- a/backend/src/middleware/orderByMiddleware.spec.js +++ b/backend/src/middleware/orderByMiddleware.spec.js @@ -1,4 +1,4 @@ -import { gql } from '../helpers/jest' +import gql from 'graphql-tag' import { cleanDatabase } from '../db/factories' import { getNeode, getDriver } from '../db/neo4j' import { createTestClient } from 'apollo-server-testing' diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 9aef8646b..00a34f9ab 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -253,6 +253,42 @@ const isMemberOfGroup = rule({ } }) +const canRemoveUserFromGroup = rule({ + cache: 'no_cache', +})(async (_parent, args, { user, driver }) => { + if (!(user && user.id)) return false + const { groupId, userId } = args + const currentUserId = user.id + if (currentUserId === userId) return false + const session = driver.session() + const readTxPromise = session.readTransaction(async (transaction) => { + const transactionResponse = await transaction.run( + ` + MATCH (User {id: $currentUserId})-[currentUserMembership:MEMBER_OF]->(group:Group {id: $groupId}) + OPTIONAL MATCH (group)<-[userMembership:MEMBER_OF]-(user:User { id: $userId }) + RETURN currentUserMembership.role AS currentUserRole, userMembership.role AS userRole + `, + { currentUserId, groupId, userId }, + ) + return { + currentUserRole: transactionResponse.records.map((record) => + record.get('currentUserRole'), + )[0], + userRole: transactionResponse.records.map((record) => record.get('userRole'))[0], + } + }) + try { + const { currentUserRole, userRole } = await readTxPromise + return ( + currentUserRole && ['owner'].includes(currentUserRole) && userRole && userRole !== 'owner' + ) + } catch (error) { + throw new Error(error) + } finally { + session.close() + } +}) + const canCommentPost = rule({ cache: 'no_cache', })(async (_parent, args, { user, driver }) => { @@ -382,6 +418,7 @@ export default shield( JoinGroup: isAllowedToJoinGroup, LeaveGroup: isAllowedToLeaveGroup, ChangeGroupMemberRole: isAllowedToChangeGroupMemberRole, + RemoveUserFromGroup: canRemoveUserFromGroup, CreatePost: and(isAuthenticated, isMemberOfGroup), UpdatePost: isAuthor, DeletePost: isAuthor, diff --git a/backend/src/middleware/permissionsMiddleware.spec.js b/backend/src/middleware/permissionsMiddleware.spec.js index 35a2ab82b..667e74164 100644 --- a/backend/src/middleware/permissionsMiddleware.spec.js +++ b/backend/src/middleware/permissionsMiddleware.spec.js @@ -1,7 +1,7 @@ import { createTestClient } from 'apollo-server-testing' import createServer from '../server' import Factory, { cleanDatabase } from '../db/factories' -import { gql } from '../helpers/jest' +import gql from 'graphql-tag' import { getDriver, getNeode } from '../db/neo4j' import CONFIG from '../config' diff --git a/backend/src/middleware/softDelete/softDeleteMiddleware.spec.js b/backend/src/middleware/softDelete/softDeleteMiddleware.spec.js index 7e14cad1f..88d46a1c7 100644 --- a/backend/src/middleware/softDelete/softDeleteMiddleware.spec.js +++ b/backend/src/middleware/softDelete/softDeleteMiddleware.spec.js @@ -1,5 +1,5 @@ import Factory, { cleanDatabase } from '../../db/factories' -import { gql } from '../../helpers/jest' +import gql from 'graphql-tag' import { getNeode, getDriver } from '../../db/neo4j' import createServer from '../../server' import { createTestClient } from 'apollo-server-testing' diff --git a/backend/src/middleware/userInteractions.spec.js b/backend/src/middleware/userInteractions.spec.js index 2f97bda5a..94d1ff274 100644 --- a/backend/src/middleware/userInteractions.spec.js +++ b/backend/src/middleware/userInteractions.spec.js @@ -1,5 +1,5 @@ import Factory, { cleanDatabase } from '../db/factories' -import { gql } from '../helpers/jest' +import gql from 'graphql-tag' import { getNeode, getDriver } from '../db/neo4j' import createServer from '../server' import { createTestClient } from 'apollo-server-testing' diff --git a/backend/src/middleware/validation/validationMiddleware.spec.js b/backend/src/middleware/validation/validationMiddleware.spec.js index a887832c1..2e1cd6fa7 100644 --- a/backend/src/middleware/validation/validationMiddleware.spec.js +++ b/backend/src/middleware/validation/validationMiddleware.spec.js @@ -1,4 +1,4 @@ -import { gql } from '../../helpers/jest' +import gql from 'graphql-tag' import Factory, { cleanDatabase } from '../../db/factories' import { getNeode, getDriver } from '../../db/neo4j' import { createTestClient } from 'apollo-server-testing' diff --git a/backend/src/schema/resolvers/comments.spec.js b/backend/src/schema/resolvers/comments.spec.js index 09487082e..b2730dad4 100644 --- a/backend/src/schema/resolvers/comments.spec.js +++ b/backend/src/schema/resolvers/comments.spec.js @@ -1,5 +1,5 @@ import Factory, { cleanDatabase } from '../../db/factories' -import { gql } from '../../helpers/jest' +import gql from 'graphql-tag' import { createTestClient } from 'apollo-server-testing' import createServer from '../../server' import { getNeode, getDriver } from '../../db/neo4j' diff --git a/backend/src/schema/resolvers/donations.spec.js b/backend/src/schema/resolvers/donations.spec.js index 5a2bcfd7b..9fc010eca 100644 --- a/backend/src/schema/resolvers/donations.spec.js +++ b/backend/src/schema/resolvers/donations.spec.js @@ -1,6 +1,6 @@ import { createTestClient } from 'apollo-server-testing' import Factory, { cleanDatabase } from '../../db/factories' -import { gql } from '../../helpers/jest' +import gql from 'graphql-tag' import { getNeode, getDriver } from '../../db/neo4j' import createServer from '../../server' diff --git a/backend/src/schema/resolvers/emails.spec.js b/backend/src/schema/resolvers/emails.spec.js index 4778e3fcf..02a631495 100644 --- a/backend/src/schema/resolvers/emails.spec.js +++ b/backend/src/schema/resolvers/emails.spec.js @@ -1,5 +1,5 @@ import Factory, { cleanDatabase } from '../../db/factories' -import { gql } from '../../helpers/jest' +import gql from 'graphql-tag' import { getDriver, getNeode } from '../../db/neo4j' import createServer from '../../server' import { createTestClient } from 'apollo-server-testing' diff --git a/backend/src/schema/resolvers/embeds.spec.js b/backend/src/schema/resolvers/embeds.spec.js index 347825f3a..0f0d41357 100644 --- a/backend/src/schema/resolvers/embeds.spec.js +++ b/backend/src/schema/resolvers/embeds.spec.js @@ -3,7 +3,7 @@ import fs from 'fs' import path from 'path' import { createTestClient } from 'apollo-server-testing' import createServer from '../../server' -import { gql } from '../../helpers/jest' +import gql from 'graphql-tag' jest.mock('node-fetch') const { Response } = jest.requireActual('node-fetch') diff --git a/backend/src/schema/resolvers/follow.spec.js b/backend/src/schema/resolvers/follow.spec.js index 14d8a366d..0d9d524ff 100644 --- a/backend/src/schema/resolvers/follow.spec.js +++ b/backend/src/schema/resolvers/follow.spec.js @@ -2,7 +2,7 @@ import { createTestClient } from 'apollo-server-testing' import Factory, { cleanDatabase } from '../../db/factories' import { getDriver, getNeode } from '../../db/neo4j' import createServer from '../../server' -import { gql } from '../../helpers/jest' +import gql from 'graphql-tag' const driver = getDriver() const neode = getNeode() diff --git a/backend/src/schema/resolvers/groups.js b/backend/src/schema/resolvers/groups.js index 4ea588d28..4a13dcc88 100644 --- a/backend/src/schema/resolvers/groups.js +++ b/backend/src/schema/resolvers/groups.js @@ -295,25 +295,8 @@ export default { LeaveGroup: async (_parent, params, context, _resolveInfo) => { const { groupId, userId } = params const session = context.driver.session() - const writeTxResultPromise = session.writeTransaction(async (transaction) => { - const leaveGroupCypher = ` - MATCH (member:User {id: $userId})-[membership:MEMBER_OF]->(group:Group {id: $groupId}) - DELETE membership - WITH member, group - OPTIONAL MATCH (p:Post)-[:IN]->(group) - WHERE NOT group.groupType = 'public' - WITH member, group, collect(p) AS posts - FOREACH (post IN posts | - MERGE (member)-[:CANNOT_SEE]->(post)) - RETURN member {.*, myRoleInGroup: NULL} - ` - - const transactionResponse = await transaction.run(leaveGroupCypher, { groupId, userId }) - const [member] = await transactionResponse.records.map((record) => record.get('member')) - return member - }) try { - return await writeTxResultPromise + return await removeUserFromGroupWriteTxResultPromise(session, groupId, userId) } catch (error) { throw new Error(error) } finally { @@ -368,6 +351,17 @@ export default { session.close() } }, + RemoveUserFromGroup: async (_parent, params, context, _resolveInfo) => { + const { groupId, userId } = params + const session = context.driver.session() + try { + return await removeUserFromGroupWriteTxResultPromise(session, groupId, userId) + } catch (error) { + throw new Error(error) + } finally { + session.close() + } + }, }, Group: { ...Resolver('Group', { @@ -383,3 +377,27 @@ export default { }), }, } + +const removeUserFromGroupWriteTxResultPromise = async (session, groupId, userId) => { + return session.writeTransaction(async (transaction) => { + const removeUserFromGroupCypher = ` + MATCH (user:User {id: $userId})-[membership:MEMBER_OF]->(group:Group {id: $groupId}) + DELETE membership + WITH user, group + OPTIONAL MATCH (author:User)-[:WROTE]->(p:Post)-[:IN]->(group) + WHERE NOT group.groupType = 'public' + AND NOT author.id = $userId + WITH user, collect(p) AS posts + FOREACH (post IN posts | + MERGE (user)-[:CANNOT_SEE]->(post)) + RETURN user {.*, myRoleInGroup: NULL} + ` + + const transactionResponse = await transaction.run(removeUserFromGroupCypher, { + groupId, + userId, + }) + const [user] = await transactionResponse.records.map((record) => record.get('user')) + return user + }) +} diff --git a/backend/src/schema/resolvers/groups.spec.js b/backend/src/schema/resolvers/groups.spec.js index 3b84f4b42..13291383d 100644 --- a/backend/src/schema/resolvers/groups.spec.js +++ b/backend/src/schema/resolvers/groups.spec.js @@ -6,6 +6,7 @@ import { joinGroupMutation, leaveGroupMutation, changeGroupMemberRoleMutation, + removeUserFromGroupMutation, groupMembersQuery, groupQuery, } from '../../graphql/groups' @@ -196,7 +197,6 @@ const seedComplexScenarioAndClearAuthentication = async () => { }, }) // hidden-group - authenticatedUser = await adminMemberUser.toJson() await mutate({ mutation: createGroupMutation(), variables: { @@ -214,32 +214,17 @@ const seedComplexScenarioAndClearAuthentication = async () => { mutation: changeGroupMemberRoleMutation(), variables: { groupId: 'hidden-group', - userId: 'admin-member-user', - roleInGroup: 'usual', - }, - }) - await mutate({ - mutation: changeGroupMemberRoleMutation(), - variables: { - groupId: 'hidden-group', - userId: 'second-owner-member-user', + userId: 'usual-member-user', roleInGroup: 'usual', }, }) + await mutate({ mutation: changeGroupMemberRoleMutation(), variables: { groupId: 'hidden-group', userId: 'admin-member-user', - roleInGroup: 'usual', - }, - }) - await mutate({ - mutation: changeGroupMemberRoleMutation(), - variables: { - groupId: 'hidden-group', - userId: 'second-owner-member-user', - roleInGroup: 'usual', + roleInGroup: 'admin', }, }) @@ -2982,4 +2967,192 @@ describe('in mode', () => { }) }) }) + + describe('RemoveUserFromGroup', () => { + beforeAll(async () => { + await seedComplexScenarioAndClearAuthentication() + }) + + afterAll(async () => { + await cleanDatabase() + }) + + describe('unauthenticated', () => { + it('throws an error', async () => { + await expect( + mutate({ + mutation: removeUserFromGroupMutation(), + variables: { + groupId: 'hidden-group', + userId: 'usual-member-user', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([ + expect.objectContaining({ + message: 'Not Authorized!', + }), + ]), + }) + }) + }) + + describe('authenticated', () => { + describe('as usual member', () => { + it('throws an error', async () => { + authenticatedUser = await usualMemberUser.toJson() + await expect( + mutate({ + mutation: removeUserFromGroupMutation(), + variables: { + groupId: 'hidden-group', + userId: 'admin-member-user', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([ + expect.objectContaining({ + message: 'Not Authorized!', + }), + ]), + }) + }) + }) + + describe('as owner', () => { + beforeEach(async () => { + authenticatedUser = await ownerMemberUser.toJson() + }) + + it('removes the user from the group', async () => { + await expect( + mutate({ + mutation: removeUserFromGroupMutation(), + variables: { + groupId: 'hidden-group', + userId: 'usual-member-user', + }, + }), + ).resolves.toMatchObject({ + data: { + RemoveUserFromGroup: expect.objectContaining({ + id: 'usual-member-user', + myRoleInGroup: null, + }), + }, + errors: undefined, + }) + }) + + it('cannot remove self', async () => { + await expect( + mutate({ + mutation: removeUserFromGroupMutation(), + variables: { + groupId: 'hidden-group', + userId: 'owner-member-user', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([ + expect.objectContaining({ + message: 'Not Authorized!', + }), + ]), + }) + }) + }) + + describe('as admin', () => { + beforeEach(async () => { + authenticatedUser = await adminMemberUser.toJson() + await mutate({ + mutation: changeGroupMemberRoleMutation(), + variables: { + groupId: 'hidden-group', + userId: 'usual-member-user', + roleInGroup: 'usual', + }, + }) + }) + + it('throws an error', async () => { + authenticatedUser = await usualMemberUser.toJson() + await expect( + mutate({ + mutation: removeUserFromGroupMutation(), + variables: { + groupId: 'hidden-group', + userId: 'admin-member-user', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([ + expect.objectContaining({ + message: 'Not Authorized!', + }), + ]), + }) + }) + + /* + it('removes the user from the group', async () => { + await expect( + mutate({ + mutation: removeUserFromGroupMutation(), + variables: { + groupId: 'hidden-group', + userId: 'usual-member-user', + }, + }), + ).resolves.toMatchObject({ + data: { + RemoveUserFromGroup: expect.objectContaining({ + id: 'usual-member-user', + myRoleInGroup: null, + }), + }, + errors: undefined, + }) + }) + + it('cannot remove self', async () => { + await expect( + mutate({ + mutation: removeUserFromGroupMutation(), + variables: { + groupId: 'hidden-group', + userId: 'admin-member-user', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([ + expect.objectContaining({ + message: 'Not Authorized!', + }), + ]), + }) + }) + + it('cannot remove owner', async () => { + await expect( + mutate({ + mutation: removeUserFromGroupMutation(), + variables: { + groupId: 'hidden-group', + userId: 'owner-member-user', + }, + }), + ).resolves.toMatchObject({ + errors: expect.arrayContaining([ + expect.objectContaining({ + message: 'Not Authorized!', + }), + ]), + }) + }) + */ + }) + }) + }) }) diff --git a/backend/src/schema/resolvers/helpers/filterPostsOfMyGroups.js b/backend/src/schema/resolvers/helpers/filterPostsOfMyGroups.js new file mode 100644 index 000000000..a808a5582 --- /dev/null +++ b/backend/src/schema/resolvers/helpers/filterPostsOfMyGroups.js @@ -0,0 +1,40 @@ +import { mergeWith, isArray } from 'lodash' + +const getMyGroupIds = async (context) => { + const { user } = context + if (!(user && user.id)) return [] + const session = context.driver.session() + + const readTxResultPromise = await session.readTransaction(async (transaction) => { + const cypher = ` + MATCH (group:Group)<-[membership:MEMBER_OF]-(:User { id: $userId }) + WHERE membership.role IN ['usual', 'admin', 'owner'] + RETURN collect(group.id) AS myGroupIds` + const getMyGroupIdsResponse = await transaction.run(cypher, { userId: user.id }) + return getMyGroupIdsResponse.records.map((record) => record.get('myGroupIds')) + }) + try { + const [myGroupIds] = readTxResultPromise + return myGroupIds + } finally { + session.close() + } +} + +export const filterPostsOfMyGroups = async (params, context) => { + if (!(params.filter && params.filter.postsInMyGroups)) return params + delete params.filter.postsInMyGroups + const myGroupIds = await getMyGroupIds(context) + params.filter = mergeWith( + params.filter, + { + group: { id_in: myGroupIds }, + }, + (objValue, srcValue) => { + if (isArray(objValue)) { + return objValue.concat(srcValue) + } + }, + ) + return params +} diff --git a/backend/src/schema/resolvers/inviteCodes.spec.js b/backend/src/schema/resolvers/inviteCodes.spec.js index b55ab079b..1df791ba6 100644 --- a/backend/src/schema/resolvers/inviteCodes.spec.js +++ b/backend/src/schema/resolvers/inviteCodes.spec.js @@ -1,6 +1,6 @@ import Factory, { cleanDatabase } from '../../db/factories' import { getDriver } from '../../db/neo4j' -import { gql } from '../../helpers/jest' +import gql from 'graphql-tag' import createServer from '../../server' import { createTestClient } from 'apollo-server-testing' import CONSTANTS_REGISTRATION from './../../constants/registration' diff --git a/backend/src/schema/resolvers/locations.spec.js b/backend/src/schema/resolvers/locations.spec.js index 87542cf6d..82aebd441 100644 --- a/backend/src/schema/resolvers/locations.spec.js +++ b/backend/src/schema/resolvers/locations.spec.js @@ -1,5 +1,5 @@ import Factory, { cleanDatabase } from '../../db/factories' -import { gql } from '../../helpers/jest' +import gql from 'graphql-tag' import { getNeode, getDriver } from '../../db/neo4j' import createServer from '../../server' import { createTestClient } from 'apollo-server-testing' diff --git a/backend/src/schema/resolvers/moderation.spec.js b/backend/src/schema/resolvers/moderation.spec.js index 9f5014756..1665e9446 100644 --- a/backend/src/schema/resolvers/moderation.spec.js +++ b/backend/src/schema/resolvers/moderation.spec.js @@ -1,6 +1,6 @@ import { createTestClient } from 'apollo-server-testing' import Factory, { cleanDatabase } from '../../db/factories' -import { gql } from '../../helpers/jest' +import gql from 'graphql-tag' import { getNeode, getDriver } from '../../db/neo4j' import createServer from '../../server' diff --git a/backend/src/schema/resolvers/notifications.spec.js b/backend/src/schema/resolvers/notifications.spec.js index 82d6e6371..36bd530eb 100644 --- a/backend/src/schema/resolvers/notifications.spec.js +++ b/backend/src/schema/resolvers/notifications.spec.js @@ -1,5 +1,5 @@ import Factory, { cleanDatabase } from '../../db/factories' -import { gql } from '../../helpers/jest' +import gql from 'graphql-tag' import { getDriver } from '../../db/neo4j' import { createTestClient } from 'apollo-server-testing' import createServer from '../.././server' diff --git a/backend/src/schema/resolvers/passwordReset.spec.js b/backend/src/schema/resolvers/passwordReset.spec.js index 90c570f5a..fa5f26e58 100644 --- a/backend/src/schema/resolvers/passwordReset.spec.js +++ b/backend/src/schema/resolvers/passwordReset.spec.js @@ -1,5 +1,5 @@ import Factory, { cleanDatabase } from '../../db/factories' -import { gql } from '../../helpers/jest' +import gql from 'graphql-tag' import { getNeode, getDriver } from '../../db/neo4j' import CONSTANTS_REGISTRATION from './../../constants/registration' import createPasswordReset from './helpers/createPasswordReset' diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js index 78515e641..d806f3803 100644 --- a/backend/src/schema/resolvers/posts.js +++ b/backend/src/schema/resolvers/posts.js @@ -6,6 +6,7 @@ import { mergeImage, deleteImage } from './images/images' import Resolver from './helpers/Resolver' import { filterForMutedUsers } from './helpers/filterForMutedUsers' import { filterInvisiblePosts } from './helpers/filterInvisiblePosts' +import { filterPostsOfMyGroups } from './helpers/filterPostsOfMyGroups' import CONFIG from '../../config' const maintainPinnedPosts = (params) => { @@ -21,12 +22,14 @@ const maintainPinnedPosts = (params) => { export default { Query: { Post: async (object, params, context, resolveInfo) => { + params = await filterPostsOfMyGroups(params, context) params = await filterInvisiblePosts(params, context) params = await filterForMutedUsers(params, context) params = await maintainPinnedPosts(params) return neo4jgraphql(object, params, context, resolveInfo) }, profilePagePosts: async (object, params, context, resolveInfo) => { + params = await filterPostsOfMyGroups(params, context) params = await filterInvisiblePosts(params, context) params = await filterForMutedUsers(params, context) return neo4jgraphql(object, params, context, resolveInfo) diff --git a/backend/src/schema/resolvers/posts.spec.js b/backend/src/schema/resolvers/posts.spec.js index 7a0dc0176..9335c1313 100644 --- a/backend/src/schema/resolvers/posts.spec.js +++ b/backend/src/schema/resolvers/posts.spec.js @@ -1,6 +1,6 @@ import { createTestClient } from 'apollo-server-testing' import Factory, { cleanDatabase } from '../../db/factories' -import { gql } from '../../helpers/jest' +import gql from 'graphql-tag' import { getNeode, getDriver } from '../../db/neo4j' import createServer from '../../server' diff --git a/backend/src/schema/resolvers/postsInGroups.spec.js b/backend/src/schema/resolvers/postsInGroups.spec.js index 5bf5820f0..86a278207 100644 --- a/backend/src/schema/resolvers/postsInGroups.spec.js +++ b/backend/src/schema/resolvers/postsInGroups.spec.js @@ -1524,9 +1524,9 @@ describe('Posts in Groups', () => { }) }) - it('does not show the posts of the closed group anymore', async () => { + it('stil shows the posts of the closed group', async () => { const result = await query({ query: filterPosts(), variables: {} }) - expect(result.data.Post).toHaveLength(3) + expect(result.data.Post).toHaveLength(4) expect(result).toMatchObject({ data: { Post: expect.arrayContaining([ @@ -1540,6 +1540,11 @@ describe('Posts in Groups', () => { title: 'A post without a group', content: 'I am a user who does not belong to a group yet.', }, + { + id: 'post-to-closed-group', + title: 'A post to a closed group', + content: 'I am posting into a closed group as a member of the group', + }, { id: 'post-to-hidden-group', title: 'A post to a hidden group', @@ -1564,9 +1569,9 @@ describe('Posts in Groups', () => { }) }) - it('does only show the public posts', async () => { + it('still shows the post of the hidden group', async () => { const result = await query({ query: filterPosts(), variables: {} }) - expect(result.data.Post).toHaveLength(2) + expect(result.data.Post).toHaveLength(4) expect(result).toMatchObject({ data: { Post: expect.arrayContaining([ @@ -1580,6 +1585,16 @@ describe('Posts in Groups', () => { title: 'A post without a group', content: 'I am a user who does not belong to a group yet.', }, + { + id: 'post-to-closed-group', + title: 'A post to a closed group', + content: 'I am posting into a closed group as a member of the group', + }, + { + id: 'post-to-hidden-group', + title: 'A post to a hidden group', + content: 'I am posting into a hidden group as a member of the group', + }, ]), }, errors: undefined, @@ -1603,9 +1618,9 @@ describe('Posts in Groups', () => { authenticatedUser = await allGroupsUser.toJson() }) - it('does not show the posts of the closed group', async () => { + it('shows the posts of the closed group', async () => { const result = await query({ query: filterPosts(), variables: {} }) - expect(result.data.Post).toHaveLength(3) + expect(result.data.Post).toHaveLength(4) expect(result).toMatchObject({ data: { Post: expect.arrayContaining([ @@ -1624,6 +1639,11 @@ describe('Posts in Groups', () => { title: 'A post to a closed group', content: 'I am posting into a closed group as a member of the group', }, + { + id: 'post-to-hidden-group', + title: 'A post to a hidden group', + content: 'I am posting into a hidden group as a member of the group', + }, ]), }, errors: undefined, @@ -1678,5 +1698,59 @@ describe('Posts in Groups', () => { }) }) }) + + describe('filter posts in my groups', () => { + describe('without any posts in groups', () => { + beforeAll(async () => { + authenticatedUser = await anyUser.toJson() + }) + + it('finds no posts', async () => { + const result = await query({ + query: filterPosts(), + variables: { filter: { postsInMyGroups: true } }, + }) + expect(result.data.Post).toHaveLength(0) + expect(result).toMatchObject({ + data: { + Post: [], + }, + errors: undefined, + }) + }) + }) + + describe('with posts in groups', () => { + beforeAll(async () => { + // member of hidden-group and closed-group + authenticatedUser = await allGroupsUser.toJson() + }) + + it('finds two posts', async () => { + const result = await query({ + query: filterPosts(), + variables: { filter: { postsInMyGroups: true } }, + }) + expect(result.data.Post).toHaveLength(2) + expect(result).toMatchObject({ + data: { + Post: expect.arrayContaining([ + { + id: 'post-to-closed-group', + title: 'A post to a closed group', + content: 'I am posting into a closed group as a member of the group', + }, + { + id: 'post-to-hidden-group', + title: 'A post to a hidden group', + content: 'I am posting into a hidden group as a member of the group', + }, + ]), + }, + errors: undefined, + }) + }) + }) + }) }) }) diff --git a/backend/src/schema/resolvers/registration.spec.js b/backend/src/schema/resolvers/registration.spec.js index ae6bcb7d7..bfc6a5fa6 100644 --- a/backend/src/schema/resolvers/registration.spec.js +++ b/backend/src/schema/resolvers/registration.spec.js @@ -1,5 +1,5 @@ import Factory, { cleanDatabase } from '../../db/factories' -import { gql } from '../../helpers/jest' +import gql from 'graphql-tag' import { getDriver, getNeode } from '../../db/neo4j' import createServer from '../../server' import { createTestClient } from 'apollo-server-testing' diff --git a/backend/src/schema/resolvers/reports.spec.js b/backend/src/schema/resolvers/reports.spec.js index 449bdb3a7..96ef07d03 100644 --- a/backend/src/schema/resolvers/reports.spec.js +++ b/backend/src/schema/resolvers/reports.spec.js @@ -1,7 +1,7 @@ import { createTestClient } from 'apollo-server-testing' import createServer from '../.././server' import Factory, { cleanDatabase } from '../../db/factories' -import { gql } from '../../helpers/jest' +import gql from 'graphql-tag' import { getDriver, getNeode } from '../../db/neo4j' const instance = getNeode() diff --git a/backend/src/schema/resolvers/rewards.spec.js b/backend/src/schema/resolvers/rewards.spec.js index 108973c9c..06fe87ec0 100644 --- a/backend/src/schema/resolvers/rewards.spec.js +++ b/backend/src/schema/resolvers/rewards.spec.js @@ -1,6 +1,6 @@ import { createTestClient } from 'apollo-server-testing' import Factory, { cleanDatabase } from '../../db/factories' -import { gql } from '../../helpers/jest' +import gql from 'graphql-tag' import { getNeode, getDriver } from '../../db/neo4j' import createServer from '../../server' diff --git a/backend/src/schema/resolvers/searches.spec.js b/backend/src/schema/resolvers/searches.spec.js index 7c1a5a254..f889c2ac8 100644 --- a/backend/src/schema/resolvers/searches.spec.js +++ b/backend/src/schema/resolvers/searches.spec.js @@ -1,5 +1,5 @@ import Factory, { cleanDatabase } from '../../db/factories' -import { gql } from '../../helpers/jest' +import gql from 'graphql-tag' import { getNeode, getDriver } from '../../db/neo4j' import createServer from '../../server' import { createTestClient } from 'apollo-server-testing' diff --git a/backend/src/schema/resolvers/shout.spec.js b/backend/src/schema/resolvers/shout.spec.js index f774d7d0b..294a28a76 100644 --- a/backend/src/schema/resolvers/shout.spec.js +++ b/backend/src/schema/resolvers/shout.spec.js @@ -1,6 +1,6 @@ import { createTestClient } from 'apollo-server-testing' import Factory, { cleanDatabase } from '../../db/factories' -import { gql } from '../../helpers/jest' +import gql from 'graphql-tag' import { getNeode, getDriver } from '../../db/neo4j' import createServer from '../../server' diff --git a/backend/src/schema/resolvers/socialMedia.spec.js b/backend/src/schema/resolvers/socialMedia.spec.js index dafbed6d6..8265e8376 100644 --- a/backend/src/schema/resolvers/socialMedia.spec.js +++ b/backend/src/schema/resolvers/socialMedia.spec.js @@ -1,7 +1,7 @@ import { createTestClient } from 'apollo-server-testing' import createServer from '../../server' import Factory, { cleanDatabase } from '../../db/factories' -import { gql } from '../../helpers/jest' +import gql from 'graphql-tag' import { getDriver } from '../../db/neo4j' const driver = getDriver() diff --git a/backend/src/schema/resolvers/statistics.spec.js b/backend/src/schema/resolvers/statistics.spec.js index 46330ef9a..15aa2d449 100644 --- a/backend/src/schema/resolvers/statistics.spec.js +++ b/backend/src/schema/resolvers/statistics.spec.js @@ -1,6 +1,6 @@ import { createTestClient } from 'apollo-server-testing' import Factory, { cleanDatabase } from '../../db/factories' -import { gql } from '../../helpers/jest' +import gql from 'graphql-tag' import { getNeode, getDriver } from '../../db/neo4j' import createServer from '../../server' diff --git a/backend/src/schema/resolvers/userData.spec.js b/backend/src/schema/resolvers/userData.spec.js index ee4901f0a..3c521a4f1 100644 --- a/backend/src/schema/resolvers/userData.spec.js +++ b/backend/src/schema/resolvers/userData.spec.js @@ -1,5 +1,5 @@ import Factory, { cleanDatabase } from '../../db/factories' -import { gql } from '../../helpers/jest' +import gql from 'graphql-tag' import { getNeode, getDriver } from '../../db/neo4j' import createServer from '../../server' import { createTestClient } from 'apollo-server-testing' diff --git a/backend/src/schema/resolvers/user_management.spec.js b/backend/src/schema/resolvers/user_management.spec.js index a249d7500..546c7a748 100644 --- a/backend/src/schema/resolvers/user_management.spec.js +++ b/backend/src/schema/resolvers/user_management.spec.js @@ -1,7 +1,7 @@ import jwt from 'jsonwebtoken' import CONFIG from './../../config' import Factory, { cleanDatabase } from '../../db/factories' -import { gql } from '../../helpers/jest' +import gql from 'graphql-tag' import { loginMutation } from '../../graphql/userManagement' import { createTestClient } from 'apollo-server-testing' import createServer, { context } from '../../server' diff --git a/backend/src/schema/resolvers/users.spec.js b/backend/src/schema/resolvers/users.spec.js index 9b7699dc1..f256c6363 100644 --- a/backend/src/schema/resolvers/users.spec.js +++ b/backend/src/schema/resolvers/users.spec.js @@ -1,5 +1,5 @@ import Factory, { cleanDatabase } from '../../db/factories' -import { gql } from '../../helpers/jest' +import gql from 'graphql-tag' import { getNeode, getDriver } from '../../db/neo4j' import createServer from '../../server' import { createTestClient } from 'apollo-server-testing' diff --git a/backend/src/schema/resolvers/users/location.spec.js b/backend/src/schema/resolvers/users/location.spec.js index 73938c41a..7097002da 100644 --- a/backend/src/schema/resolvers/users/location.spec.js +++ b/backend/src/schema/resolvers/users/location.spec.js @@ -1,4 +1,4 @@ -import { gql } from '../../../helpers/jest' +import gql from 'graphql-tag' import Factory, { cleanDatabase } from '../../../db/factories' import { getNeode, getDriver } from '../../../db/neo4j' import { createTestClient } from 'apollo-server-testing' diff --git a/backend/src/schema/resolvers/users/mutedUsers.spec.js b/backend/src/schema/resolvers/users/mutedUsers.spec.js index 5c1a7dd5d..20e37f627 100644 --- a/backend/src/schema/resolvers/users/mutedUsers.spec.js +++ b/backend/src/schema/resolvers/users/mutedUsers.spec.js @@ -1,7 +1,7 @@ import { createTestClient } from 'apollo-server-testing' import createServer from '../../../server' import { cleanDatabase } from '../../../db/factories' -import { gql } from '../../../helpers/jest' +import gql from 'graphql-tag' import { getNeode, getDriver } from '../../../db/neo4j' const driver = getDriver() diff --git a/backend/src/schema/resolvers/viewedTeaserCount.spec.js b/backend/src/schema/resolvers/viewedTeaserCount.spec.js index d7437646b..ee90d1a08 100644 --- a/backend/src/schema/resolvers/viewedTeaserCount.spec.js +++ b/backend/src/schema/resolvers/viewedTeaserCount.spec.js @@ -1,6 +1,6 @@ import { createTestClient } from 'apollo-server-testing' import Factory, { cleanDatabase } from '../../db/factories' -import { gql } from '../../helpers/jest' +import gql from 'graphql-tag' import { getNeode, getDriver } from '../../db/neo4j' import createServer from '../../server' diff --git a/backend/src/schema/types/type/Group.gql b/backend/src/schema/types/type/Group.gql index ce90fad1d..acf585f71 100644 --- a/backend/src/schema/types/type/Group.gql +++ b/backend/src/schema/types/type/Group.gql @@ -132,4 +132,9 @@ type Mutation { userId: ID! roleInGroup: GroupMemberRole! ): User + + RemoveUserFromGroup( + groupId: ID! + userId: ID! + ): User } diff --git a/backend/src/schema/types/type/Post.gql b/backend/src/schema/types/type/Post.gql index 9eac00b0b..6fc7a3215 100644 --- a/backend/src/schema/types/type/Post.gql +++ b/backend/src/schema/types/type/Post.gql @@ -82,6 +82,7 @@ input _PostFilter { emotions_single: _PostEMOTEDFilter emotions_every: _PostEMOTEDFilter group: _GroupFilter + postsInMyGroups: Boolean } enum _PostOrdering { diff --git a/cypress/integration/Moderation.ReportContent/somebody_reported_the_following_posts.js b/cypress/integration/Moderation.ReportContent/somebody_reported_the_following_posts.js index ce876a081..3ca39e6e6 100644 --- a/cypress/integration/Moderation.ReportContent/somebody_reported_the_following_posts.js +++ b/cypress/integration/Moderation.ReportContent/somebody_reported_the_following_posts.js @@ -1,5 +1,4 @@ import { Given } from "cypress-cucumber-preprocessor/steps"; -import { gql } from '../../../backend/src/helpers/jest' Given('somebody reported the following posts:', table => { table.hashes().forEach(({ submitterEmail, resourceId, reasonCategory, reasonDescription }) => { @@ -10,7 +9,7 @@ Given('somebody reported the following posts:', table => { cy.factory() .build('user', {}, submitter) .authenticateAs(submitter) - .mutate(gql`mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) { + .mutate(`mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) { fileReport(resourceId: $resourceId, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription) { reportId } diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 335e00390..7d3738a3d 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -15,11 +15,10 @@ /* globals Cypress cy */ import "cypress-file-upload"; import { GraphQLClient, request } from 'graphql-request' -import { gql } from '../../backend/src/helpers/jest' import config from '../../backend/src/config' const authenticatedHeaders = (variables) => { - const mutation = gql` + const mutation = ` mutation($email: String!, $password: String!) { login(email: $email, password: $password) } diff --git a/deployment/DOCKER_MORE_CLOSELY.md b/deployment/DOCKER_MORE_CLOSELY.md deleted file mode 100644 index 113e3a4da..000000000 --- a/deployment/DOCKER_MORE_CLOSELY.md +++ /dev/null @@ -1,33 +0,0 @@ -# Docker - -## Apple M1 Platform - -***Attention:** For using Docker commands in Apple M1 environments!* - -```bash -# set env variable for your shell -$ export DOCKER_DEFAULT_PLATFORM=linux/amd64 -``` - -For even more informations, see [Docker More Closely](#docker-more-closely) - -### Docker Compose Override File For Apple M1 Platform - -For Docker compose `up` or `build` commands, you can use our Apple M1 override file that specifies the M1 platform: - -```bash -# in main folder - -# for production -$ docker compose -f docker-compose.yml -f docker-compose.apple-m1.override.yml up - -# for production testing Docker images from DockerHub -$ docker compose -f docker-compose.ocelotsocial-branded.yml -f docker-compose.apple-m1.override.yml up - -# only once: init admin user and create indexes and contraints in Neo4j database -$ docker compose exec backend /bin/sh -c "yarn prod:migrate init" -``` - -## Docker More Closely In Main Code - -To get more informations about the Apple M1 platform and to analyze the Docker builds etc. you find our documentation in our main code, [here](https://github.com/Ocelot-Social-Community/Ocelot-Social/blob/master/DOCKER_MORE_CLOSELY.md). diff --git a/deployment/docker-compose.apple-m1.override.yml b/deployment/docker-compose.apple-m1.override.yml deleted file mode 100644 index 80344e49f..000000000 --- a/deployment/docker-compose.apple-m1.override.yml +++ /dev/null @@ -1,36 +0,0 @@ -# This docker-compose file is just here for testing - -version: "3.4" - -services: - - ######################################################## - # WEBAPP ############################################### - ######################################################## - webapp: - platform: linux/amd64 - - ######################################################## - # BACKEND ############################################## - ######################################################## - backend: - platform: linux/amd64 - - ######################################################## - # MAINTENANCE ########################################## - ######################################################## - maintenance: - platform: linux/amd64 - - ######################################################## - # NEO4J ################################################ - ######################################################## - neo4j: - platform: linux/amd64 - - ######################################################## - # MAILSERVER TO FAKE SMTP ############################## - ######################################################## - # commented out, because otherwise override of production would error. and it seems unnecessary - # mailserver: - # platform: linux/amd64 diff --git a/deployment/docker-compose.yml b/deployment/docker-compose.yml index 0653d58f3..a0dda4dc0 100644 --- a/deployment/docker-compose.yml +++ b/deployment/docker-compose.yml @@ -7,6 +7,7 @@ services: dockerfile: ../webapp/Dockerfile context: ../webapp target: base + command: sleep 0 webapp-code: image: ocelotsocialnetwork/webapp:local-code @@ -14,10 +15,11 @@ services: dockerfile: ../webapp/Dockerfile context: ../webapp target: code + command: sleep 0 webapp: image: ocelotsocialnetwork/webapp-branded:local-${CONFIGURATION} - container_name: webapp + container_name: webapp-branded build: dockerfile: src/docker/webapp.Dockerfile target: branded @@ -51,6 +53,7 @@ services: dockerfile: ../backend/Dockerfile context: ../backend target: base + command: sleep 0 backend-code: image: ocelotsocialnetwork/backend:local-code @@ -58,13 +61,14 @@ services: dockerfile: ../backend/Dockerfile context: ../backend target: code + command: sleep 0 backend: image: ocelotsocialnetwork/backend-branded:local-${CONFIGURATION} container_name: backend build: dockerfile: src/docker/backend.Dockerfile - target: branded + target: branded-branded context: . args: - CONFIGURATION=$CONFIGURATION @@ -106,6 +110,7 @@ services: dockerfile: ../webapp/Dockerfile.maintenance context: ../webapp target: base + command: sleep 0 maintenance-code: image: ocelotsocialnetwork/maintenance:local-code @@ -113,11 +118,12 @@ services: dockerfile: ../webapp/Dockerfile.maintenance context: ../webapp target: code + command: sleep 0 maintenance: # 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: ocelotsocialnetwork/maintenance-branded:local-${CONFIGURATION} - container_name: maintenance + container_name: maintenance-branded build: # TODO: Separate from webapp, this must be independent dockerfile: src/docker/maintenance.Dockerfile @@ -152,7 +158,7 @@ services: mailserver: image: djfarrelly/maildev - container_name: mailserver + container_name: mailserver-branded ports: - 1080:80 networks: diff --git a/deployment/scripts/cluster.backup.sh b/deployment/scripts/cluster.backup.sh new file mode 100755 index 000000000..b83bab198 --- /dev/null +++ b/deployment/scripts/cluster.backup.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +# base setup +SCRIPT_PATH=$(realpath $0) +SCRIPT_DIR=$(dirname $SCRIPT_PATH) + +# configuration +CONFIGURATION=${CONFIGURATION:-"example"} +KUBECONFIG=${KUBECONFIG:-${SCRIPT_DIR}/../configurations/${CONFIGURATION}/kubeconfig.yaml} +BACKUP_DATE=$(date "+%F_%H-%M-%S") +BACKUP_FOLDER=${BACKUP_FOLDER:-${SCRIPT_DIR}/../configurations/${CONFIGURATION}/backup/${BACKUP_DATE}} + +# create backup fodler +mkdir -p ${BACKUP_FOLDER} + +# maintenance mode on +${SCRIPT_DIR}/cluster.maintenance.sh on + +# shutdown database +kubectl --kubeconfig=${KUBECONFIG} get deployment ocelot-neo4j -o json \ + | jq '.spec.template.spec.containers[] += {"command": ["tail", "-f", "/dev/null"]}' \ + | kubectl --kubeconfig=${KUBECONFIG} apply -f - + +# wait for the container to restart +sleep 60 + +# database backup +kubectl --kubeconfig=${KUBECONFIG} -n default exec -it \ + $(kubectl --kubeconfig=${KUBECONFIG} -n default get pods | grep ocelot-neo4j | awk '{ print $1 }') \ + -- neo4j-admin dump --to=/var/lib/neo4j/$BACKUP_DATE-neo4j-dump +# copy neo4j backup to local drive +kubectl --kubeconfig=${KUBECONFIG} cp \ + default/$(kubectl --kubeconfig=${KUBECONFIG} -n default get pods | grep ocelot-neo4j |awk '{ print $1 }'):/var/lib/neo4j/$BACKUP_DATE-neo4j-dump $BACKUP_FOLDER/neo4j-dump +# copy image data +kubectl --kubeconfig=${KUBECONFIG} cp \ + default/$(kubectl --kubeconfig=${KUBECONFIG} -n default get pods | grep ocelot-backend |awk '{ print $1 }'):/app/public/uploads $BACKUP_FOLDER/public-uploads + +# restart database +kubectl --kubeconfig=${KUBECONFIG} get deployment ocelot-neo4j -o json \ + | jq 'del(.spec.template.spec.containers[].command)' \ + | kubectl --kubeconfig=${KUBECONFIG} apply -f - + +# wait for the container to restart +sleep 60 + +# maintenance mode off +${SCRIPT_DIR}/cluster.maintenance.sh off \ No newline at end of file diff --git a/deployment/scripts/cluster.maintenance.sh b/deployment/scripts/cluster.maintenance.sh new file mode 100755 index 000000000..3a558d7bb --- /dev/null +++ b/deployment/scripts/cluster.maintenance.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# base setup +SCRIPT_PATH=$(realpath $0) +SCRIPT_DIR=$(dirname $SCRIPT_PATH) + +# configuration +CONFIGURATION=${CONFIGURATION:-"example"} +KUBECONFIG=${KUBECONFIG:-${SCRIPT_DIR}/../configurations/${CONFIGURATION}/kubeconfig.yaml} + +case $1 in + on) + kubectl --kubeconfig=${KUBECONFIG} patch ingress ingress-ocelot-webapp --type merge --patch-file ${SCRIPT_DIR}/../src/kubernetes/patches/patch.ingress.maintenance.on.yaml + ;; + off) + kubectl --kubeconfig=${KUBECONFIG} patch ingress ingress-ocelot-webapp --type merge --patch-file ${SCRIPT_DIR}/../src/kubernetes/patches/patch.ingress.maintenance.off.yaml + ;; + *) + echo -e "Run this script with first argument either 'on' or 'off'" + exit + ;; +esac diff --git a/deployment/src/docker/maintenance.Dockerfile b/deployment/src/docker/maintenance.Dockerfile index 1df6c551b..b699e7f20 100644 --- a/deployment/src/docker/maintenance.Dockerfile +++ b/deployment/src/docker/maintenance.Dockerfile @@ -14,7 +14,12 @@ ARG CONFIGURATION=example # copy public constants into the Docker image to brand it COPY configurations/${CONFIGURATION}/branding/static/ static/ COPY configurations/${CONFIGURATION}/branding/constants/ constants/ -COPY configurations/${CONFIGURATION}/branding/locales/ locales/ + +# locales +COPY configurations/${CONFIGURATION}/branding/locales/*.json locales/tmp/ +COPY src/tools/ tools/ +RUN apk add --no-cache bash jq +RUN tools/merge-locales.sh ################################################################################## # BUILD ########################################################################## diff --git a/deployment/src/docker/webapp.Dockerfile b/deployment/src/docker/webapp.Dockerfile index ad5b7c769..f0c75e392 100644 --- a/deployment/src/docker/webapp.Dockerfile +++ b/deployment/src/docker/webapp.Dockerfile @@ -12,17 +12,16 @@ FROM $APP_IMAGE_CODE as code ARG CONFIGURATION=example # copy public constants into the Docker image to brand it -COPY src/tools/ tools/ COPY configurations/${CONFIGURATION}/branding/static/ static/ COPY configurations/${CONFIGURATION}/branding/constants/ constants/ COPY configurations/${CONFIGURATION}/branding/locales/html/ locales/html/ -# COPY configurations/${CONFIGURATION}/branding/locales/index.js locales/index.js -COPY configurations/${CONFIGURATION}/branding/locales/*.json locales/tmp/ COPY configurations/${CONFIGURATION}/branding/assets/styles/imports/ assets/styles/imports/ COPY configurations/${CONFIGURATION}/branding/assets/fonts/ assets/fonts/ +# locales +COPY configurations/${CONFIGURATION}/branding/locales/*.json locales/tmp/ +COPY src/tools/ tools/ RUN apk add --no-cache bash jq - RUN tools/merge-locales.sh ################################################################################## diff --git a/deployment/src/kubernetes/patches/patch.ingress.maintenance.off.yaml b/deployment/src/kubernetes/patches/patch.ingress.maintenance.off.yaml new file mode 100644 index 000000000..c01745d0a --- /dev/null +++ b/deployment/src/kubernetes/patches/patch.ingress.maintenance.off.yaml @@ -0,0 +1,12 @@ +spec: + rules: + - host: + http: + paths: + - path: / + pathType: ImplementationSpecific + backend: + service: + name: ocelot-webapp + port: + number: 3000 \ No newline at end of file diff --git a/deployment/src/kubernetes/patches/patch.ingress.maintenance.on.yaml b/deployment/src/kubernetes/patches/patch.ingress.maintenance.on.yaml new file mode 100644 index 000000000..8a2c5d45f --- /dev/null +++ b/deployment/src/kubernetes/patches/patch.ingress.maintenance.on.yaml @@ -0,0 +1,12 @@ +spec: + rules: + - host: + http: + paths: + - path: / + pathType: ImplementationSpecific + backend: + service: + name: ocelot-maintenance + port: + number: 80 \ No newline at end of file diff --git a/docker-compose.apple-m1.override.yml b/docker-compose.apple-m1.override.yml deleted file mode 100644 index cbb041c51..000000000 --- a/docker-compose.apple-m1.override.yml +++ /dev/null @@ -1,34 +0,0 @@ -version: "3.4" - -services: - - ######################################################## - # WEBAPP ############################################### - ######################################################## - webapp: - platform: linux/amd64 - - ######################################################## - # BACKEND ############################################## - ######################################################## - backend: - platform: linux/amd64 - - ######################################################## - # MAINTENANCE ########################################## - ######################################################## - maintenance: - platform: linux/amd64 - - ######################################################## - # NEO4J ################################################ - ######################################################## - neo4j: - platform: linux/amd64 - - ######################################################## - # MAILSERVER TO FAKE SMTP ############################## - ######################################################## - # commented out, because otherwise override of production would error. and it seems unnecessary - # mailserver: - # platform: linux/amd64 diff --git a/webapp/README.md b/webapp/README.md index 0f377521d..4c56dcc75 100644 --- a/webapp/README.md +++ b/webapp/README.md @@ -10,8 +10,8 @@ between different local Node versions: ```bash # install Node $ cd webapp -$ nvm install v16.19.0 -$ nvm use v16.19.0 +$ nvm install v19.4.0 +$ nvm use v19.4.0 ``` Install node dependencies with [yarn](https://yarnpkg.com/en/): diff --git a/webapp/assets/_new/styles/tokens.scss b/webapp/assets/_new/styles/tokens.scss index 770a1a9c9..66bd03ecc 100644 --- a/webapp/assets/_new/styles/tokens.scss +++ b/webapp/assets/_new/styles/tokens.scss @@ -375,4 +375,21 @@ $color-footer-link: $color-primary; * @tokens Locale Menu Color */ -$color-locale-menu: $text-color-soft; \ No newline at end of file +$color-locale-menu: $text-color-soft; + +/** + * @tokens Donation Bar Color + */ + +$color-donation-bar: $color-primary; +$color-donation-bar-light: $color-primary-light; + +/** + * @tokens Toast Color + */ + +$color-toast-red: $color-danger; +$color-toast-orange: $color-warning; +$color-toast-yellow: $color-yellow; +$color-toast-blue: $color-secondary; +$color-toast-green: $color-success; diff --git a/webapp/assets/styles/imports/_toast.scss b/webapp/assets/styles/imports/_toast.scss index 0ef81a5b2..eb593dd6a 100644 --- a/webapp/assets/styles/imports/_toast.scss +++ b/webapp/assets/styles/imports/_toast.scss @@ -11,22 +11,22 @@ } .iziToast.iziToast-color-red { - background: $color-danger !important; - border-color: $color-danger !important; + background: $color-toast-red !important; + border-color: $color-toast-red !important; } .iziToast.iziToast-color-orange { - background: $color-warning !important; - border-color: $color-warning !important; + background: $color-toast-orange !important; + border-color: $color-toast-orange !important; } .iziToast.iziToast-color-yellow { - background: $color-yellow !important; - border-color: $color-yellow !important; + background: $color-toast-yellow !important; + border-color: $color-toast-yellow !important; } .iziToast.iziToast-color-blue { - background: $color-secondary !important; - border-color: $color-secondary !important; + background: $color-toast-blue !important; + border-color: $color-toast-blue !important; } .iziToast.iziToast-color-green { - background: $color-success !important; - border-color: $color-success !important; + background: $color-toast-green !important; + border-color: $color-toast-green !important; } diff --git a/webapp/components/AvatarMenu/AvatarMenu.spec.js b/webapp/components/AvatarMenu/AvatarMenu.spec.js index 5495c9ae6..be34c1804 100644 --- a/webapp/components/AvatarMenu/AvatarMenu.spec.js +++ b/webapp/components/AvatarMenu/AvatarMenu.spec.js @@ -1,11 +1,13 @@ -import { config, mount } from '@vue/test-utils' +import { mount } from '@vue/test-utils' import Vuex from 'vuex' import AvatarMenu from './AvatarMenu.vue' const localVue = global.localVue -config.stubs['nuxt-link'] = '' -config.stubs['router-link'] = '' +const stubs = { + 'nuxt-link': true, + 'router-link': true, +} describe('AvatarMenu.vue', () => { let propsData, getters, wrapper, mocks @@ -34,7 +36,7 @@ describe('AvatarMenu.vue', () => { const store = new Vuex.Store({ getters, }) - return mount(AvatarMenu, { propsData, localVue, store, mocks }) + return mount(AvatarMenu, { propsData, localVue, store, mocks, stubs }) } describe('mount', () => { diff --git a/webapp/components/Badges.spec.js b/webapp/components/Badges.spec.js index f81eaafb1..d19c2beb2 100644 --- a/webapp/components/Badges.spec.js +++ b/webapp/components/Badges.spec.js @@ -14,7 +14,7 @@ describe('Badges.vue', () => { } it('has class "hc-badges"', () => { - expect(Wrapper().contains('.hc-badges')).toBe(true) + expect(Wrapper().find('.hc-badges').exists()).toBe(true) }) describe('given a badge', () => { @@ -23,7 +23,7 @@ describe('Badges.vue', () => { }) it('proxies badge icon, which is just a URL without metadata', () => { - expect(Wrapper().contains('img[src="/api/path/to/some/icon"]')).toBe(true) + expect(Wrapper().find('img[src="/api/path/to/some/icon"]').exists()).toBe(true) }) }) }) diff --git a/webapp/components/Category/index.spec.js b/webapp/components/Category/index.spec.js index 9abb8a11d..22a85f6f1 100644 --- a/webapp/components/Category/index.spec.js +++ b/webapp/components/Category/index.spec.js @@ -1,4 +1,4 @@ -import { shallowMount } from '@vue/test-utils' +import { mount } from '@vue/test-utils' import Category from './index' @@ -9,7 +9,7 @@ describe('Category', () => { let name const Wrapper = () => { - return shallowMount(Category, { + return mount(Category, { localVue, propsData: { icon, @@ -27,8 +27,9 @@ describe('Category', () => { it('shows Name', () => { expect(Wrapper().text()).toContain('Peter') }) - it('shows Icon Svg', () => { - expect(Wrapper().contains('svg')) + + it('shows base icon', () => { + expect(Wrapper().find('.base-icon').exists()).toBe(true) }) }) }) diff --git a/webapp/components/CommentCard/CommentCard.spec.js b/webapp/components/CommentCard/CommentCard.spec.js index 0ebcb2649..7764afd1e 100644 --- a/webapp/components/CommentCard/CommentCard.spec.js +++ b/webapp/components/CommentCard/CommentCard.spec.js @@ -1,13 +1,10 @@ -import { config, mount } from '@vue/test-utils' +import { mount } from '@vue/test-utils' import CommentCard from './CommentCard.vue' import Vuex from 'vuex' const localVue = global.localVue localVue.directive('scrollTo', jest.fn()) -config.stubs['client-only'] = '' -config.stubs['nuxt-link'] = '' - describe('CommentCard.vue', () => { let propsData, mocks, stubs, getters, wrapper, Wrapper @@ -46,6 +43,8 @@ describe('CommentCard.vue', () => { } stubs = { ContentViewer: true, + 'client-only': true, + 'nuxt-link': true, } getters = { 'auth/user': () => { @@ -56,7 +55,9 @@ describe('CommentCard.vue', () => { }) describe('mount', () => { - beforeEach(jest.useFakeTimers) + beforeEach(() => { + jest.useFakeTimers() + }) Wrapper = () => { const store = new Vuex.Store({ diff --git a/webapp/components/CommentList/CommentList.spec.js b/webapp/components/CommentList/CommentList.spec.js index 39f76ff12..f4195aa41 100644 --- a/webapp/components/CommentList/CommentList.spec.js +++ b/webapp/components/CommentList/CommentList.spec.js @@ -1,4 +1,4 @@ -import { config, mount } from '@vue/test-utils' +import { mount } from '@vue/test-utils' import CommentList from './CommentList' import Vuex from 'vuex' import Vue from 'vue' @@ -8,10 +8,6 @@ const localVue = global.localVue localVue.filter('truncate', (string) => string) localVue.directive('scrollTo', jest.fn()) -config.stubs['v-popover'] = '' -config.stubs['nuxt-link'] = '' -config.stubs['client-only'] = '' - describe('CommentList.vue', () => { let mocks, store, wrapper, propsData, stubs @@ -72,7 +68,10 @@ describe('CommentList.vue', () => { }, } stubs = { - EditorContent: "
", + EditorContent: true, + 'v-popover': true, + 'nuxt-link': true, + 'client-only': true, } }) @@ -92,7 +91,9 @@ describe('CommentList.vue', () => { }) describe('scrollToAnchor mixin', () => { - beforeEach(jest.useFakeTimers) + beforeEach(() => { + jest.useFakeTimers() + }) describe('$route.hash !== `#comments`', () => { it('skips $scrollTo', () => { diff --git a/webapp/components/ComponentSlider/ComponentSlider.spec.js b/webapp/components/ComponentSlider/ComponentSlider.spec.js index 25bf3e7f4..0f1f345e9 100644 --- a/webapp/components/ComponentSlider/ComponentSlider.spec.js +++ b/webapp/components/ComponentSlider/ComponentSlider.spec.js @@ -52,7 +52,7 @@ describe('ComponentSlider.vue', () => { }) it('renders', () => { - expect(wrapper.is('div')).toBe(true) + expect(wrapper.element.tagName).toBe('DIV') }) it('click on next Button', async () => { diff --git a/webapp/components/ContentMenu/ContentMenu.spec.js b/webapp/components/ContentMenu/ContentMenu.spec.js index 37bef14f6..0bd398e41 100644 --- a/webapp/components/ContentMenu/ContentMenu.spec.js +++ b/webapp/components/ContentMenu/ContentMenu.spec.js @@ -1,4 +1,4 @@ -import { config, mount, createLocalVue } from '@vue/test-utils' +import { mount, createLocalVue } from '@vue/test-utils' import Vuex from 'vuex' import VTooltip from 'v-tooltip' import Styleguide from '@human-connection/styleguide' @@ -10,7 +10,11 @@ localVue.use(Styleguide) localVue.use(VTooltip) localVue.use(Vuex) -config.stubs['router-link'] = '' +const stubs = { + 'router-link': { + template: '', + }, +} let getters, mutations, mocks, menuToggle, openModalSpy @@ -36,7 +40,7 @@ describe('ContentMenu.vue', () => { 'auth/isAdmin': () => false, } - const openContentMenu = (values = {}) => { + const openContentMenu = async (values = {}) => { const store = new Vuex.Store({ mutations, getters }) const wrapper = mount(ContentMenu, { propsData: { @@ -45,16 +49,17 @@ describe('ContentMenu.vue', () => { mocks, store, localVue, + stubs, }) menuToggle = wrapper.find('[data-test="content-menu-button"]') - menuToggle.trigger('click') + await menuToggle.trigger('click') return wrapper } describe('owner of contribution', () => { let wrapper - beforeEach(() => { - wrapper = openContentMenu({ + beforeEach(async () => { + wrapper = await openContentMenu({ isOwner: true, resourceType: 'contribution', resource: { @@ -86,9 +91,9 @@ describe('ContentMenu.vue', () => { }) describe('admin can', () => { - it('pin unpinned post', () => { + it('pin unpinned post', async () => { getters['auth/isAdmin'] = () => true - const wrapper = openContentMenu({ + const wrapper = await openContentMenu({ isOwner: false, resourceType: 'contribution', resource: { @@ -111,8 +116,8 @@ describe('ContentMenu.vue', () => { ]) }) - it('unpin pinned post', () => { - const wrapper = openContentMenu({ + it('unpin pinned post', async () => { + const wrapper = await openContentMenu({ isOwner: false, resourceType: 'contribution', resource: { @@ -135,11 +140,11 @@ describe('ContentMenu.vue', () => { ]) }) - it('can delete another user', () => { + it('can delete another user', async () => { getters['auth/user'] = () => { return { id: 'some-user', slug: 'some-user' } } - const wrapper = openContentMenu({ + const wrapper = await openContentMenu({ resourceType: 'user', resource: { id: 'another-user', @@ -161,8 +166,8 @@ describe('ContentMenu.vue', () => { ]) }) - it('can not delete the own account', () => { - const wrapper = openContentMenu({ + it('can not delete the own account', async () => { + const wrapper = await openContentMenu({ resourceType: 'user', resource: { id: 'some-user', @@ -179,8 +184,8 @@ describe('ContentMenu.vue', () => { describe('owner of comment can', () => { let wrapper - beforeEach(() => { - wrapper = openContentMenu({ + beforeEach(async () => { + wrapper = await openContentMenu({ isOwner: true, resourceType: 'comment', resource: { @@ -208,10 +213,10 @@ describe('ContentMenu.vue', () => { }) describe('reporting', () => { - it('a post of another user is possible', () => { + it('a post of another user is possible', async () => { getters['auth/isAdmin'] = () => false getters['auth/isModerator'] = () => false - const wrapper = openContentMenu({ + const wrapper = await openContentMenu({ isOwner: false, resourceType: 'contribution', resource: { @@ -227,8 +232,8 @@ describe('ContentMenu.vue', () => { expect(openModalSpy).toHaveBeenCalledWith('report') }) - it('a comment of another user is possible', () => { - const wrapper = openContentMenu({ + it('a comment of another user is possible', async () => { + const wrapper = await openContentMenu({ isOwner: false, resourceType: 'comment', resource: { @@ -244,8 +249,8 @@ describe('ContentMenu.vue', () => { expect(openModalSpy).toHaveBeenCalledWith('report') }) - it('another user is possible', () => { - const wrapper = openContentMenu({ + it('another user is possible', async () => { + const wrapper = await openContentMenu({ isOwner: false, resourceType: 'user', resource: { @@ -261,8 +266,8 @@ describe('ContentMenu.vue', () => { expect(openModalSpy).toHaveBeenCalledWith('report') }) - it('another organization is possible', () => { - const wrapper = openContentMenu({ + it('another organization is possible', async () => { + const wrapper = await openContentMenu({ isOwner: false, resourceType: 'organization', resource: { @@ -280,10 +285,10 @@ describe('ContentMenu.vue', () => { }) describe('moderator', () => { - it('can disable posts', () => { + it('can disable posts', async () => { getters['auth/isAdmin'] = () => false getters['auth/isModerator'] = () => true - const wrapper = openContentMenu({ + const wrapper = await openContentMenu({ isOwner: false, resourceType: 'contribution', resource: { @@ -300,8 +305,8 @@ describe('ContentMenu.vue', () => { expect(openModalSpy).toHaveBeenCalledWith('disable') }) - it('can disable comments', () => { - const wrapper = openContentMenu({ + it('can disable comments', async () => { + const wrapper = await openContentMenu({ isOwner: false, resourceType: 'comment', resource: { @@ -318,8 +323,8 @@ describe('ContentMenu.vue', () => { expect(openModalSpy).toHaveBeenCalledWith('disable') }) - it('can disable users', () => { - const wrapper = openContentMenu({ + it('can disable users', async () => { + const wrapper = await openContentMenu({ isOwner: false, resourceType: 'user', resource: { @@ -336,8 +341,8 @@ describe('ContentMenu.vue', () => { expect(openModalSpy).toHaveBeenCalledWith('disable') }) - it('can disable organizations', () => { - const wrapper = openContentMenu({ + it('can disable organizations', async () => { + const wrapper = await openContentMenu({ isOwner: false, resourceType: 'organization', resource: { @@ -354,8 +359,8 @@ describe('ContentMenu.vue', () => { expect(openModalSpy).toHaveBeenCalledWith('disable') }) - it('can release posts', () => { - const wrapper = openContentMenu({ + it('can release posts', async () => { + const wrapper = await openContentMenu({ isOwner: false, resourceType: 'contribution', resource: { @@ -372,8 +377,8 @@ describe('ContentMenu.vue', () => { expect(openModalSpy).toHaveBeenCalledWith('release') }) - it('can release comments', () => { - const wrapper = openContentMenu({ + it('can release comments', async () => { + const wrapper = await openContentMenu({ isOwner: false, resourceType: 'comment', resource: { @@ -390,8 +395,8 @@ describe('ContentMenu.vue', () => { expect(openModalSpy).toHaveBeenCalledWith('release') }) - it('can release users', () => { - const wrapper = openContentMenu({ + it('can release users', async () => { + const wrapper = await openContentMenu({ isOwner: false, resourceType: 'user', resource: { @@ -408,8 +413,8 @@ describe('ContentMenu.vue', () => { expect(openModalSpy).toHaveBeenCalledWith('release') }) - it('can release organizations', () => { - const wrapper = openContentMenu({ + it('can release organizations', async () => { + const wrapper = await openContentMenu({ isOwner: false, resourceType: 'organization', resource: { @@ -428,10 +433,10 @@ describe('ContentMenu.vue', () => { }) describe('user', () => { - it('can access settings', () => { + it('can access settings', async () => { getters['auth/isAdmin'] = () => false getters['auth/isModerator'] = () => false - const wrapper = openContentMenu({ + const wrapper = await openContentMenu({ isOwner: true, resourceType: 'user', resource: { @@ -448,8 +453,8 @@ describe('ContentMenu.vue', () => { ).toBe('/settings') }) - it('can mute other users', () => { - const wrapper = openContentMenu({ + it('can mute other users', async () => { + const wrapper = await openContentMenu({ isOwner: false, resourceType: 'user', resource: { @@ -472,8 +477,8 @@ describe('ContentMenu.vue', () => { ]) }) - it('can unmute muted users', () => { - const wrapper = openContentMenu({ + it('can unmute muted users', async () => { + const wrapper = await openContentMenu({ isOwner: false, resourceType: 'user', resource: { diff --git a/webapp/components/ContentMenu/GroupContentMenu.spec.js b/webapp/components/ContentMenu/GroupContentMenu.spec.js index 8b17b24b1..49a66aaac 100644 --- a/webapp/components/ContentMenu/GroupContentMenu.spec.js +++ b/webapp/components/ContentMenu/GroupContentMenu.spec.js @@ -1,9 +1,13 @@ -import { config, mount } from '@vue/test-utils' +import { mount } from '@vue/test-utils' import GroupContentMenu from './GroupContentMenu.vue' const localVue = global.localVue -config.stubs['router-link'] = '' +const stubs = { + 'router-link': { + template: '', + }, +} const propsData = { usage: 'groupTeaser', @@ -24,7 +28,7 @@ describe('GroupContentMenu', () => { describe('mount', () => { const Wrapper = () => { - return mount(GroupContentMenu, { propsData, mocks, localVue }) + return mount(GroupContentMenu, { propsData, mocks, localVue, stubs }) } beforeEach(() => { diff --git a/webapp/components/ContributionForm/ContributionForm.spec.js b/webapp/components/ContributionForm/ContributionForm.spec.js index 1ef1777fe..ef3b47c37 100644 --- a/webapp/components/ContributionForm/ContributionForm.spec.js +++ b/webapp/components/ContributionForm/ContributionForm.spec.js @@ -1,4 +1,4 @@ -import { config, mount } from '@vue/test-utils' +import { mount } from '@vue/test-utils' import ContributionForm from './ContributionForm.vue' import Vuex from 'vuex' @@ -11,9 +11,11 @@ global.MutationObserver = MutationObserver const localVue = global.localVue -config.stubs['client-only'] = '' -config.stubs['nuxt-link'] = '' -config.stubs['v-popover'] = '' +const stubs = { + 'client-only': true, + 'nuxt-link': true, + 'v-popover': true, +} describe('ContributionForm.vue', () => { let wrapper, postTitleInput, expectedParams, cancelBtn, mocks, propsData @@ -88,6 +90,7 @@ describe('ContributionForm.vue', () => { localVue, store, propsData, + stubs, }) } @@ -163,7 +166,7 @@ describe('ContributionForm.vue', () => { .mockImplementation(function () { this.onload({ target: { result: 'someUrlToImage' } }) }) - wrapper.find(ImageUploader).vm.$emit('addHeroImage', imageUpload) + wrapper.findComponent(ImageUploader).vm.$emit('addHeroImage', imageUpload) await wrapper.find('form').trigger('submit') expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams)) expect(spy).toHaveBeenCalledWith(imageUpload) diff --git a/webapp/components/DeleteData/DeleteData.spec.js b/webapp/components/DeleteData/DeleteData.spec.js index c540c9832..f34b2fe6f 100644 --- a/webapp/components/DeleteData/DeleteData.spec.js +++ b/webapp/components/DeleteData/DeleteData.spec.js @@ -115,9 +115,9 @@ describe('DeleteData.vue', () => { enableContributionDeletionCheckbox = wrapper.find( '[data-test="contributions-deletion-checkbox"]', ) - enableContributionDeletionCheckbox.trigger('click') + enableContributionDeletionCheckbox.setChecked(true) enableCommentDeletionCheckbox = wrapper.find('[data-test="comments-deletion-checkbox"]') - enableCommentDeletionCheckbox.trigger('click') + enableCommentDeletionCheckbox.setChecked(true) deleteAccountBtn.trigger('click') expect(mocks.$apollo.mutate).toHaveBeenCalledWith( expect.objectContaining({ @@ -133,7 +133,7 @@ describe('DeleteData.vue', () => { enableContributionDeletionCheckbox = wrapper.find( '[data-test="contributions-deletion-checkbox"]', ) - enableContributionDeletionCheckbox.trigger('click') + enableContributionDeletionCheckbox.setChecked(true) deleteAccountBtn.trigger('click') expect(mocks.$apollo.mutate).toHaveBeenCalledWith( expect.objectContaining({ @@ -147,7 +147,7 @@ describe('DeleteData.vue', () => { it("deletes a user's comments if requested", () => { enableCommentDeletionCheckbox = wrapper.find('[data-test="comments-deletion-checkbox"]') - enableCommentDeletionCheckbox.trigger('click') + enableCommentDeletionCheckbox.setChecked(true) deleteAccountBtn.trigger('click') expect(mocks.$apollo.mutate).toHaveBeenCalledWith( expect.objectContaining({ diff --git a/webapp/components/Editor/Editor.spec.js b/webapp/components/Editor/Editor.spec.js index f51c5782f..98c287ced 100644 --- a/webapp/components/Editor/Editor.spec.js +++ b/webapp/components/Editor/Editor.spec.js @@ -35,7 +35,7 @@ describe('Editor.vue', () => { describe('mount', () => { it('renders', () => { - expect(Wrapper().is('div')).toBe(true) + expect(Wrapper().element.tagName).toBe('DIV') }) describe('given a piece of text', () => { diff --git a/webapp/components/Editor/nodes/Embed.spec.js b/webapp/components/Editor/nodes/Embed.spec.js index 372b189f9..05edb1296 100644 --- a/webapp/components/Editor/nodes/Embed.spec.js +++ b/webapp/components/Editor/nodes/Embed.spec.js @@ -42,7 +42,7 @@ describe('Embed.vue', () => { propsData.node = { attrs: { href: 'https://www.youtube.com/watch?v=qkdXAtO40Fo' } } const wrapper = Wrapper({ propsData }) await wrapper.html() - expect(wrapper.contains('embed-component-stub')).toBe(true) + expect(wrapper.find('embed-component-stub').exists()).toBe(true) }) }) diff --git a/webapp/components/Embed/EmbedComponent.spec.js b/webapp/components/Embed/EmbedComponent.spec.js index cd3526368..0d20f969f 100644 --- a/webapp/components/Embed/EmbedComponent.spec.js +++ b/webapp/components/Embed/EmbedComponent.spec.js @@ -160,7 +160,7 @@ describe('EmbedComponent.vue', () => { describe('sets permanently', () => { beforeEach(() => { - wrapper.find('input[type=checkbox]').trigger('click') + wrapper.find('input[type=checkbox]').setChecked(true) wrapper.find('[data-test="play-now-button"]').trigger('click') }) diff --git a/webapp/components/EnterNonce/EnterNonce.spec.js b/webapp/components/EnterNonce/EnterNonce.spec.js index a1f2e6b94..e75c8cf49 100644 --- a/webapp/components/EnterNonce/EnterNonce.spec.js +++ b/webapp/components/EnterNonce/EnterNonce.spec.js @@ -19,7 +19,9 @@ describe('EnterNonce ', () => { }) describe('mount', () => { - beforeEach(jest.useFakeTimers) + beforeEach(() => { + jest.useFakeTimers() + }) Wrapper = () => { return mount(EnterNonce, { diff --git a/webapp/components/FilterMenu/FollowingFilter.spec.js b/webapp/components/FilterMenu/FollowingFilter.spec.js index 4d4a827e5..0f51b305c 100644 --- a/webapp/components/FilterMenu/FollowingFilter.spec.js +++ b/webapp/components/FilterMenu/FollowingFilter.spec.js @@ -9,12 +9,14 @@ let wrapper describe('FollowingFilter', () => { const mutations = { 'posts/TOGGLE_FILTER_BY_FOLLOWED': jest.fn(), + 'posts/TOGGLE_FILTER_BY_MY_GROUPS': jest.fn(), } const getters = { 'auth/user': () => { return { id: 'u34' } }, 'posts/filteredByUsersFollowed': jest.fn(), + 'posts/filteredByPostsInMyGroups': jest.fn(), } const mocks = { @@ -34,12 +36,18 @@ describe('FollowingFilter', () => { describe('mount', () => { it('sets "filter-by-followed" button attribute `filled`', () => { getters['posts/filteredByUsersFollowed'] = jest.fn(() => true) + getters['posts/filteredByPostsInMyGroups'] = jest.fn(() => true) const wrapper = Wrapper() expect( wrapper .find('.following-filter .filter-list .follower-item .base-button') .classes('--filled'), ).toBe(true) + expect( + wrapper + .find('.following-filter .filter-list .posts-in-my-groups-item .base-button') + .classes('--filled'), + ).toBe(true) }) describe('click "filter-by-followed" button', () => { @@ -48,5 +56,14 @@ describe('FollowingFilter', () => { expect(mutations['posts/TOGGLE_FILTER_BY_FOLLOWED']).toHaveBeenCalledWith({}, 'u34') }) }) + + describe('click "filter-by-my-groups" button', () => { + it('calls TOGGLE_FILTER_BY_MY_GROUPS', () => { + wrapper + .find('.following-filter .filter-list .posts-in-my-groups-item .base-button') + .trigger('click') + expect(mutations['posts/TOGGLE_FILTER_BY_MY_GROUPS']).toHaveBeenCalled() + }) + }) }) }) diff --git a/webapp/components/FilterMenu/FollowingFilter.vue b/webapp/components/FilterMenu/FollowingFilter.vue index 7c2c2a282..9a488acd6 100644 --- a/webapp/components/FilterMenu/FollowingFilter.vue +++ b/webapp/components/FilterMenu/FollowingFilter.vue @@ -10,6 +10,15 @@ @click="toggleFilteredByFollowed(currentUser.id)" /> +
  • + +
  • @@ -28,12 +37,14 @@ export default { computed: { ...mapGetters({ filteredByUsersFollowed: 'posts/filteredByUsersFollowed', + filteredByPostsInMyGroups: 'posts/filteredByPostsInMyGroups', currentUser: 'auth/user', }), }, methods: { ...mapMutations({ toggleFilteredByFollowed: 'posts/TOGGLE_FILTER_BY_FOLLOWED', + toggleFilteredByMyGroups: 'posts/TOGGLE_FILTER_BY_MY_GROUPS', }), }, } diff --git a/webapp/components/Group/GroupForm.spec.js b/webapp/components/Group/GroupForm.spec.js index 0300bacd0..0eb503856 100644 --- a/webapp/components/Group/GroupForm.spec.js +++ b/webapp/components/Group/GroupForm.spec.js @@ -1,9 +1,11 @@ -import { config, mount } from '@vue/test-utils' +import { mount } from '@vue/test-utils' import GroupForm from './GroupForm.vue' const localVue = global.localVue -config.stubs['nuxt-link'] = '' +const stubs = { + 'nuxt-link': true, +} const propsData = { update: false, @@ -25,7 +27,7 @@ describe('GroupForm', () => { describe('mount', () => { const Wrapper = () => { - return mount(GroupForm, { propsData, mocks, localVue }) + return mount(GroupForm, { propsData, mocks, localVue, stubs }) } beforeEach(() => { diff --git a/webapp/components/Group/GroupMember.spec.js b/webapp/components/Group/GroupMember.spec.js index 0c0b46e43..ef8b96568 100644 --- a/webapp/components/Group/GroupMember.spec.js +++ b/webapp/components/Group/GroupMember.spec.js @@ -1,26 +1,65 @@ import { mount } from '@vue/test-utils' import GroupMember from './GroupMember.vue' +import { changeGroupMemberRoleMutation, removeUserFromGroupMutation } from '~/graphql/groups.js' const localVue = global.localVue const propsData = { - groupId: '', - groupMembers: [], + groupId: 'group-id', + groupMembers: [ + { + slug: 'owner', + id: 'owner', + myRoleInGroup: 'owner', + }, + { + slug: 'user', + id: 'user', + myRoleInGroup: 'usual', + }, + ], } +const stubs = { + 'nuxt-link': true, +} + +const apolloMock = jest + .fn() + .mockRejectedValueOnce({ message: 'Oh no!' }) + .mockResolvedValue({ + data: { + ChangeGroupMemberRole: { + slug: 'user', + id: 'user', + myRoleInGroup: 'admin', + }, + }, + }) + +const toastErrorMock = jest.fn() +const toastSuccessMock = jest.fn() + describe('GroupMember', () => { let wrapper let mocks beforeEach(() => { mocks = { - $t: jest.fn(), + $t: jest.fn((t) => t), + $apollo: { + mutate: apolloMock, + }, + $toast: { + error: toastErrorMock, + success: toastSuccessMock, + }, } }) describe('mount', () => { const Wrapper = () => { - return mount(GroupMember, { propsData, mocks, localVue }) + return mount(GroupMember, { propsData, mocks, localVue, stubs }) } beforeEach(() => { @@ -30,5 +69,120 @@ describe('GroupMember', () => { it('renders', () => { expect(wrapper.findAll('.group-member')).toHaveLength(1) }) + + it('has two users in table', () => { + expect(wrapper.find('tbody').findAll('tr')).toHaveLength(2) + }) + + it('has no modal', () => { + expect(wrapper.find('div.ds-modal-wrapper').exists()).toBe(false) + }) + + describe('change user role', () => { + beforeEach(() => { + jest.clearAllMocks() + wrapper + .find('tbody') + .findAll('tr') + .at(1) + .find('select') + .findAll('option') + .at(2) + .setSelected() + wrapper.find('tbody').findAll('tr').at(1).find('select').trigger('change') + }) + + describe('with server error', () => { + it('toasts an error message', () => { + expect(toastErrorMock).toBeCalledWith('Oh no!') + }) + }) + + describe('with server success', () => { + it('calls the API', () => { + expect(apolloMock).toBeCalledWith({ + mutation: changeGroupMemberRoleMutation(), + variables: { groupId: 'group-id', userId: 'user', roleInGroup: 'admin' }, + }) + }) + + it('toasts a success message', () => { + expect(toastSuccessMock).toBeCalledWith('group.changeMemberRole') + }) + }) + }) + + describe('click remove user', () => { + beforeAll(() => { + apolloMock.mockRejectedValueOnce({ message: 'Oh no!!' }).mockResolvedValue({ + data: { + RemoveUserFromGroup: { + slug: 'user', + id: 'user', + myRoleInGroup: null, + }, + }, + }) + }) + + beforeEach(() => { + wrapper = Wrapper() + wrapper.find('tbody').findAll('tr').at(1).find('button').trigger('click') + }) + + it('opens the modal', () => { + expect(wrapper.find('div.ds-modal-wrapper').isVisible()).toBe(true) + }) + + describe('click on cancel', () => { + beforeEach(() => { + wrapper.find('div.ds-modal-wrapper').find('button.ds-button-ghost').trigger('click') + }) + + it('closes the modal', () => { + expect(wrapper.find('div.ds-modal-wrapper').exists()).toBe(false) + }) + }) + + describe('click on confirm with server error', () => { + beforeEach(() => { + wrapper.find('div.ds-modal-wrapper').find('button.ds-button-primary').trigger('click') + }) + + it('toasts an error message', () => { + expect(toastErrorMock).toBeCalledWith('Oh no!!') + }) + + it('closes the modal', () => { + expect(wrapper.find('div.ds-modal-wrapper').exists()).toBe(false) + }) + }) + + describe('click on confirm with success', () => { + beforeEach(() => { + jest.clearAllMocks() + wrapper.find('div.ds-modal-wrapper').find('button.ds-button-primary').trigger('click') + }) + + it('calls the API', () => { + expect(apolloMock).toBeCalledWith({ + mutation: removeUserFromGroupMutation(), + variables: { groupId: 'group-id', userId: 'user' }, + }) + }) + + it('emits load group members', () => { + expect(wrapper.emitted('loadGroupMembers')).toBeTruthy() + }) + + it('toasts a success message', () => { + expect(toastSuccessMock).toBeCalledWith('group.memberRemoved') + }) + + it('closes the modal', () => { + expect(wrapper.find('div.ds-modal-wrapper').exists()).toBe(false) + }) + }) + }) }) }) diff --git a/webapp/components/Group/GroupMember.vue b/webapp/components/Group/GroupMember.vue index bacd0259d..d6c09746e 100644 --- a/webapp/components/Group/GroupMember.vue +++ b/webapp/components/Group/GroupMember.vue @@ -53,30 +53,33 @@ - - - + diff --git a/webapp/components/Hashtag/Hashtag.spec.js b/webapp/components/Hashtag/Hashtag.spec.js index 4a420d7d8..873919e20 100644 --- a/webapp/components/Hashtag/Hashtag.spec.js +++ b/webapp/components/Hashtag/Hashtag.spec.js @@ -1,10 +1,12 @@ -import { config, shallowMount } from '@vue/test-utils' +import { shallowMount } from '@vue/test-utils' import Hashtag from './Hashtag' const localVue = global.localVue -config.stubs['nuxt-link'] = '' +const stubs = { + 'nuxt-link': true, +} describe('Hashtag', () => { let id @@ -15,6 +17,7 @@ describe('Hashtag', () => { propsData: { id, }, + stubs, }) } diff --git a/webapp/components/HashtagsFilter/HashtagsFilter.spec.js b/webapp/components/HashtagsFilter/HashtagsFilter.spec.js index 3318ed7c0..07496c9fc 100644 --- a/webapp/components/HashtagsFilter/HashtagsFilter.spec.js +++ b/webapp/components/HashtagsFilter/HashtagsFilter.spec.js @@ -29,7 +29,7 @@ describe('HashtagsFilter.vue', () => { it('renders a card', () => { wrapper = Wrapper() - expect(wrapper.is('.base-card')).toBe(true) + expect(wrapper.classes('base-card')).toBe(true) }) describe('click clear search button', () => { diff --git a/webapp/components/InviteButton/InviteButton.spec.js b/webapp/components/InviteButton/InviteButton.spec.js index f28045612..cb73ecfd1 100644 --- a/webapp/components/InviteButton/InviteButton.spec.js +++ b/webapp/components/InviteButton/InviteButton.spec.js @@ -1,7 +1,11 @@ -import { config, mount } from '@vue/test-utils' +import { mount } from '@vue/test-utils' import InviteButton from './InviteButton.vue' -config.stubs['v-popover'] = '' +const stubs = { + 'v-popover': { + template: '', + }, +} describe('InviteButton.vue', () => { let wrapper @@ -22,7 +26,7 @@ describe('InviteButton.vue', () => { describe('mount', () => { const Wrapper = () => { - return mount(InviteButton, { mocks, propsData }) + return mount(InviteButton, { mocks, propsData, stubs }) } beforeEach(() => { @@ -30,12 +34,12 @@ describe('InviteButton.vue', () => { }) it('renders', () => { - expect(wrapper.contains('.invite-button')).toBe(true) + expect(wrapper.find('.invite-button').exists()).toBe(true) }) it('open popup', () => { wrapper.find('.base-button').trigger('click') - expect(wrapper.contains('.invite-button')).toBe(true) + expect(wrapper.find('.invite-button').exists()).toBe(true) }) it('invite codes not available', async () => { diff --git a/webapp/components/LocaleSwitch/LocaleSwitch.spec.js b/webapp/components/LocaleSwitch/LocaleSwitch.spec.js index 800309bfc..158624f18 100644 --- a/webapp/components/LocaleSwitch/LocaleSwitch.spec.js +++ b/webapp/components/LocaleSwitch/LocaleSwitch.spec.js @@ -1,11 +1,13 @@ -import { config, mount } from '@vue/test-utils' +import { mount } from '@vue/test-utils' import LocaleSwitch from './LocaleSwitch.vue' import Vuex from 'vuex' const localVue = global.localVue -config.stubs['client-only'] = '' +const stubs = { + 'client-only': true, +} describe('LocaleSwitch.vue', () => { let wrapper, mocks, computed, deutschLanguageItem, getters @@ -65,7 +67,7 @@ describe('LocaleSwitch.vue', () => { const store = new Vuex.Store({ getters, }) - return mount(LocaleSwitch, { mocks, localVue, computed, store }) + return mount(LocaleSwitch, { mocks, localVue, computed, store, stubs }) } describe('with current user', () => { diff --git a/webapp/components/LoginButton/LoginButton.spec.js b/webapp/components/LoginButton/LoginButton.spec.js index 62e663714..d529dbfb6 100644 --- a/webapp/components/LoginButton/LoginButton.spec.js +++ b/webapp/components/LoginButton/LoginButton.spec.js @@ -1,7 +1,10 @@ -import { config, mount } from '@vue/test-utils' +import { mount } from '@vue/test-utils' import LoginButton from './LoginButton.vue' -config.stubs['v-popover'] = '' +const stubs = { + 'v-popover': true, + 'nuxt-link': true, +} describe('LoginButton.vue', () => { let wrapper @@ -22,7 +25,7 @@ describe('LoginButton.vue', () => { describe('mount', () => { const Wrapper = () => { - return mount(LoginButton, { mocks, propsData }) + return mount(LoginButton, { mocks, propsData, stubs }) } beforeEach(() => { @@ -30,12 +33,12 @@ describe('LoginButton.vue', () => { }) it('renders', () => { - expect(wrapper.contains('.login-button')).toBe(true) + expect(wrapper.find('.login-button').exists()).toBe(true) }) it('open popup', () => { wrapper.find('.base-button').trigger('click') - expect(wrapper.contains('.login-button')).toBe(true) + expect(wrapper.find('.login-button').exists()).toBe(true) }) }) }) diff --git a/webapp/components/LoginForm/LoginForm.spec.js b/webapp/components/LoginForm/LoginForm.spec.js index 10fc2c622..e0972b453 100644 --- a/webapp/components/LoginForm/LoginForm.spec.js +++ b/webapp/components/LoginForm/LoginForm.spec.js @@ -2,15 +2,17 @@ import Vue from 'vue' import LoginForm from './LoginForm.vue' import Styleguide from '@human-connection/styleguide' import Vuex from 'vuex' -import { config, mount, createLocalVue } from '@vue/test-utils' +import { mount, createLocalVue } from '@vue/test-utils' const localVue = createLocalVue() localVue.use(Vuex) localVue.use(Styleguide) -config.stubs['nuxt-link'] = '' -config.stubs['locale-switch'] = '' -config.stubs['client-only'] = '' +const stubs = { + 'nuxt-link': true, + 'locale-switch': true, + 'client-only': true, +} const authUserMock = jest.fn().mockReturnValue({ activeCategories: [] }) @@ -46,7 +48,7 @@ describe('LoginForm', () => { error: jest.fn(), }, } - return mount(LoginForm, { mocks, localVue, propsData, store }) + return mount(LoginForm, { mocks, localVue, propsData, store, stubs }) } describe('fill in email and password and submit', () => { diff --git a/webapp/components/MasonryGrid/MasonryGridItem.spec.js b/webapp/components/MasonryGrid/MasonryGridItem.spec.js index 86233efc9..26309f00d 100644 --- a/webapp/components/MasonryGrid/MasonryGridItem.spec.js +++ b/webapp/components/MasonryGrid/MasonryGridItem.spec.js @@ -1,9 +1,11 @@ -import { config, mount } from '@vue/test-utils' +import { mount } from '@vue/test-utils' import MasonryGridItem from './MasonryGridItem' const localVue = global.localVue -config.stubs['ds-grid-item'] = '' +const stubs = { + 'ds-grid-item': true, +} describe('MasonryGridItem', () => { let wrapper @@ -11,34 +13,34 @@ describe('MasonryGridItem', () => { describe('given an imageAspectRatio', () => { it('sets the initial rowSpan to 13 when the ratio is higher than 1.3', () => { const propsData = { imageAspectRatio: 2 } - wrapper = mount(MasonryGridItem, { localVue, propsData }) + wrapper = mount(MasonryGridItem, { localVue, propsData, stubs }) expect(wrapper.vm.rowSpan).toBe(13) }) it('sets the initial rowSpan to 15 when the ratio is between 1.3 and 1', () => { const propsData = { imageAspectRatio: 1.1 } - wrapper = mount(MasonryGridItem, { localVue, propsData }) + wrapper = mount(MasonryGridItem, { localVue, propsData, stubs }) expect(wrapper.vm.rowSpan).toBe(15) }) it('sets the initial rowSpan to 18 when the ratio is between 1 and 0.7', () => { const propsData = { imageAspectRatio: 0.7 } - wrapper = mount(MasonryGridItem, { localVue, propsData }) + wrapper = mount(MasonryGridItem, { localVue, propsData, stubs }) expect(wrapper.vm.rowSpan).toBe(18) }) it('sets the initial rowSpan to 25 when the ratio is lower than 0.7', () => { const propsData = { imageAspectRatio: 0.3 } - wrapper = mount(MasonryGridItem, { localVue, propsData }) + wrapper = mount(MasonryGridItem, { localVue, propsData, stubs }) expect(wrapper.vm.rowSpan).toBe(25) }) describe('given no aspect ratio', () => { it('sets the initial rowSpan to 8 when not given an imageAspectRatio', () => { - wrapper = mount(MasonryGridItem, { localVue }) + wrapper = mount(MasonryGridItem, { localVue, stubs }) expect(wrapper.vm.rowSpan).toBe(8) }) }) diff --git a/webapp/components/Modal.spec.js b/webapp/components/Modal.spec.js index c08c90f51..037e84497 100644 --- a/webapp/components/Modal.spec.js +++ b/webapp/components/Modal.spec.js @@ -57,9 +57,9 @@ describe('Modal.vue', () => { it('initially empty', () => { wrapper = Wrapper() - expect(wrapper.contains(ConfirmModal)).toBe(false) - expect(wrapper.contains(DisableModal)).toBe(false) - expect(wrapper.contains(ReportModal)).toBe(false) + expect(wrapper.findComponent(ConfirmModal).exists()).toBe(false) + expect(wrapper.findComponent(DisableModal).exists()).toBe(false) + expect(wrapper.findComponent(ReportModal).exists()).toBe(false) }) describe('store/modal holds data to disable', () => { @@ -78,11 +78,11 @@ describe('Modal.vue', () => { }) it('renders disable modal', () => { - expect(wrapper.contains(DisableModal)).toBe(true) + expect(wrapper.findComponent(DisableModal).exists()).toBe(true) }) it('passes data to disable modal', () => { - expect(wrapper.find(DisableModal).props()).toEqual({ + expect(wrapper.findComponent(DisableModal).props()).toEqual({ type: 'contribution', name: 'some title', id: 'c456', @@ -91,9 +91,9 @@ describe('Modal.vue', () => { describe('child component emits close', () => { it('turns empty', async () => { - wrapper.find(DisableModal).vm.$emit('close') + wrapper.findComponent(DisableModal).vm.$emit('close') await Vue.nextTick() - expect(wrapper.contains(DisableModal)).toBe(false) + expect(wrapper.findComponent(DisableModal).exists()).toBe(false) }) }) @@ -109,7 +109,7 @@ describe('Modal.vue', () => { }, } wrapper = Wrapper() - expect(wrapper.find(DisableModal).props()).toEqual({ + expect(wrapper.findComponent(DisableModal).props()).toEqual({ type: 'comment', name: 'Author name', id: 'c456', @@ -124,7 +124,7 @@ describe('Modal.vue', () => { }, } wrapper = Wrapper() - expect(wrapper.find(DisableModal).props()).toEqual({ + expect(wrapper.findComponent(DisableModal).props()).toEqual({ type: 'comment', name: '', id: 'c456', @@ -142,7 +142,7 @@ describe('Modal.vue', () => { }, } wrapper = Wrapper() - expect(wrapper.find(DisableModal).props()).toEqual({ + expect(wrapper.findComponent(DisableModal).props()).toEqual({ type: 'user', name: 'Username', id: 'u456', @@ -160,7 +160,7 @@ describe('Modal.vue', () => { }, } wrapper = Wrapper() - expect(wrapper.find(DisableModal).props()).toEqual({ + expect(wrapper.findComponent(DisableModal).props()).toEqual({ type: 'something', name: null, id: 's456', diff --git a/webapp/components/Modal/ConfirmModal.spec.js b/webapp/components/Modal/ConfirmModal.spec.js index 6d7d0ccf6..28fef3058 100644 --- a/webapp/components/Modal/ConfirmModal.spec.js +++ b/webapp/components/Modal/ConfirmModal.spec.js @@ -1,11 +1,13 @@ -import { config, shallowMount, mount } from '@vue/test-utils' +import { shallowMount, mount } from '@vue/test-utils' import ConfirmModal from './ConfirmModal.vue' import { postMenuModalsData } from '~/components/utils/PostHelpers' const localVue = global.localVue -config.stubs['sweetalert-icon'] = '' +const stubs = { + 'sweetalert-icon': true, +} describe('ConfirmModal.vue', () => { let Wrapper @@ -41,6 +43,7 @@ describe('ConfirmModal.vue', () => { propsData, mocks, localVue, + stubs, }) } @@ -90,10 +93,13 @@ describe('ConfirmModal.vue', () => { propsData, mocks, localVue, + stubs, }) } - beforeEach(jest.useFakeTimers) + beforeEach(() => { + jest.useFakeTimers() + }) describe('given post id', () => { beforeEach(() => { diff --git a/webapp/components/Modal/DeleteUserModal.spec.js b/webapp/components/Modal/DeleteUserModal.spec.js index d2f28e8b0..1a1c152cd 100644 --- a/webapp/components/Modal/DeleteUserModal.spec.js +++ b/webapp/components/Modal/DeleteUserModal.spec.js @@ -1,9 +1,12 @@ -import { config, mount, shallowMount } from '@vue/test-utils' +import { mount, shallowMount } from '@vue/test-utils' import Vuex from 'vuex' import DeleteUserModal from './DeleteUserModal.vue' const localVue = global.localVue -config.stubs['sweetalert-icon'] = '' -config.stubs['nuxt-link'] = '' + +const stubs = { + 'sweetalert-icon': true, + 'nuxt-link': true, +} localVue.use(DeleteUserModal) @@ -49,6 +52,7 @@ describe('DeleteUserModal.vue', () => { mocks, store, localVue, + stubs, }) } @@ -74,9 +78,12 @@ describe('DeleteUserModal.vue', () => { mocks, store, localVue, + stubs, }) } - beforeEach(jest.useFakeTimers) + beforeEach(() => { + jest.useFakeTimers() + }) describe('given another user', () => { beforeEach(() => { diff --git a/webapp/components/Modal/DisableModal.spec.js b/webapp/components/Modal/DisableModal.spec.js index b7e52b5a0..0a7ffe25b 100644 --- a/webapp/components/Modal/DisableModal.spec.js +++ b/webapp/components/Modal/DisableModal.spec.js @@ -102,7 +102,9 @@ describe('DisableModal.vue', () => { localVue, }) } - beforeEach(jest.useFakeTimers) + beforeEach(() => { + jest.useFakeTimers() + }) describe('given id', () => { beforeEach(() => { diff --git a/webapp/components/Modal/ReportModal.spec.js b/webapp/components/Modal/ReportModal.spec.js index 999adacb4..d993e8103 100644 --- a/webapp/components/Modal/ReportModal.spec.js +++ b/webapp/components/Modal/ReportModal.spec.js @@ -1,10 +1,12 @@ -import { config, shallowMount, mount } from '@vue/test-utils' +import { shallowMount, mount } from '@vue/test-utils' import ReportModal from './ReportModal.vue' import Vue from 'vue' const localVue = global.localVue -config.stubs['sweetalert-icon'] = '' +const stubs = { + 'sweetalert-icon': true, +} describe('ReportModal.vue', () => { let wrapper @@ -39,6 +41,7 @@ describe('ReportModal.vue', () => { propsData, mocks, localVue, + stubs, }) } @@ -109,13 +112,16 @@ describe('ReportModal.vue', () => { propsData, mocks, localVue, + stubs, }) } - beforeEach(jest.useFakeTimers) + beforeEach(() => { + jest.useFakeTimers() + }) it('renders', () => { - expect(Wrapper().is('div')).toBe(true) + expect(Wrapper().element.tagName).toBe('DIV') }) describe('given id', () => { diff --git a/webapp/components/Notification/Notification.spec.js b/webapp/components/Notification/Notification.spec.js index 879bbfb7d..844f78e71 100644 --- a/webapp/components/Notification/Notification.spec.js +++ b/webapp/components/Notification/Notification.spec.js @@ -1,12 +1,10 @@ -import { config, mount, RouterLinkStub } from '@vue/test-utils' +import { mount, RouterLinkStub } from '@vue/test-utils' import Notification from './Notification.vue' import Vuex from 'vuex' const localVue = global.localVue -config.stubs['client-only'] = '' - describe('Notification', () => { let stubs let getters @@ -20,6 +18,7 @@ describe('Notification', () => { } stubs = { NuxtLink: RouterLinkStub, + 'client-only': true, } getters = { 'auth/user': () => { diff --git a/webapp/components/NotificationList/NotificationList.spec.js b/webapp/components/NotificationList/NotificationList.spec.js index 219c1fdbb..7f7038d59 100644 --- a/webapp/components/NotificationList/NotificationList.spec.js +++ b/webapp/components/NotificationList/NotificationList.spec.js @@ -1,4 +1,4 @@ -import { config, shallowMount, mount, RouterLinkStub } from '@vue/test-utils' +import { shallowMount, mount, RouterLinkStub } from '@vue/test-utils' import NotificationList from './NotificationList' import Notification from '../Notification/Notification' import Vuex from 'vuex' @@ -9,9 +9,6 @@ const localVue = global.localVue localVue.filter('truncate', (string) => string) -config.stubs['client-only'] = '' -config.stubs['v-popover'] = '' - describe('NotificationList.vue', () => { let wrapper let mocks @@ -33,6 +30,8 @@ describe('NotificationList.vue', () => { } stubs = { NuxtLink: RouterLinkStub, + 'client-only': true, + 'v-popover': true, } propsData = { notifications } }) @@ -44,6 +43,7 @@ describe('NotificationList.vue', () => { mocks, store, localVue, + stubs, }) } @@ -52,7 +52,7 @@ describe('NotificationList.vue', () => { }) it('renders Notification.vue for each notification of the user', () => { - expect(wrapper.findAll(Notification)).toHaveLength(2) + expect(wrapper.findAllComponents(Notification)).toHaveLength(2) }) }) @@ -97,7 +97,7 @@ describe('NotificationList.vue', () => { }) it('renders Notification.vue zero times', () => { - expect(wrapper.findAll(Notification)).toHaveLength(0) + expect(wrapper.findAllComponents(Notification)).toHaveLength(0) }) }) }) diff --git a/webapp/components/NotificationMenu/NotificationMenu.spec.js b/webapp/components/NotificationMenu/NotificationMenu.spec.js index 3986471fd..448ea37e4 100644 --- a/webapp/components/NotificationMenu/NotificationMenu.spec.js +++ b/webapp/components/NotificationMenu/NotificationMenu.spec.js @@ -1,12 +1,10 @@ -import { config, mount, RouterLinkStub } from '@vue/test-utils' +import { mount, RouterLinkStub } from '@vue/test-utils' import NotificationMenu from './NotificationMenu' const localVue = global.localVue localVue.filter('truncate', (string) => string) -config.stubs.dropdown = '' - describe('NotificationMenu.vue', () => { let wrapper let mocks @@ -23,6 +21,8 @@ describe('NotificationMenu.vue', () => { } stubs = { NuxtLink: RouterLinkStub, + UserTeaser: true, + 'client-only': true, } }) @@ -38,13 +38,13 @@ describe('NotificationMenu.vue', () => { it('renders as link without counter', () => { wrapper = Wrapper() - expect(wrapper.is('a.notifications-menu')).toBe(true) + expect(wrapper.classes('notifications-menu')).toBe(true) expect(() => wrapper.get('.count')).toThrow() }) it('no dropdown is rendered', () => { wrapper = Wrapper() - expect(wrapper.contains('.dropdown')).toBe(false) + expect(wrapper.find('.dropdown').exists()).toBe(false) }) describe('given only read notifications', () => { @@ -73,13 +73,13 @@ describe('NotificationMenu.vue', () => { it('renders as link without counter', () => { wrapper = Wrapper() - expect(wrapper.is('a.notifications-menu')).toBe(true) + expect(wrapper.classes('notifications-menu')).toBe(true) expect(() => wrapper.get('.count')).toThrow() }) it('no dropdown is rendered', () => { wrapper = Wrapper() - expect(wrapper.contains('.dropdown')).toBe(false) + expect(wrapper.find('.dropdown').exists()).toBe(false) }) }) @@ -101,6 +101,14 @@ describe('NotificationMenu.vue', () => { name: 'John Doe', }, }, + from: { + title: 'Title', + author: { + id: 'reporter', + slug: 'reporter', + name: 'reporter', + }, + }, }, { id: 'notification-42', @@ -115,6 +123,14 @@ describe('NotificationMenu.vue', () => { name: 'John Doe', }, }, + from: { + title: 'Title', + author: { + id: 'reporter', + slug: 'reporter', + name: 'reporter', + }, + }, }, { id: 'notification-43', @@ -129,6 +145,14 @@ describe('NotificationMenu.vue', () => { name: 'John Doe', }, }, + from: { + title: 'Title', + author: { + id: 'reporter', + slug: 'reporter', + name: 'reporter', + }, + }, }, ], } diff --git a/webapp/components/NotificationsTable/NotificationsTable.spec.js b/webapp/components/NotificationsTable/NotificationsTable.spec.js index b502b5982..e48610034 100644 --- a/webapp/components/NotificationsTable/NotificationsTable.spec.js +++ b/webapp/components/NotificationsTable/NotificationsTable.spec.js @@ -1,4 +1,4 @@ -import { config, mount, RouterLinkStub } from '@vue/test-utils' +import { mount, RouterLinkStub } from '@vue/test-utils' import Vuex from 'vuex' import NotificationsTable from './NotificationsTable' @@ -8,8 +8,6 @@ const localVue = global.localVue localVue.filter('truncate', (string) => string) -config.stubs['client-only'] = '' - describe('NotificationsTable.vue', () => { let wrapper, mocks, propsData, stubs const postNotification = notifications[0] @@ -21,6 +19,7 @@ describe('NotificationsTable.vue', () => { } stubs = { NuxtLink: RouterLinkStub, + 'client-only': true, } propsData = {} }) diff --git a/webapp/components/PageFooter/PageFooter.spec.js b/webapp/components/PageFooter/PageFooter.spec.js index 53302192a..b02541fbf 100644 --- a/webapp/components/PageFooter/PageFooter.spec.js +++ b/webapp/components/PageFooter/PageFooter.spec.js @@ -1,10 +1,14 @@ -import { config, mount } from '@vue/test-utils' +import { mount } from '@vue/test-utils' import PageFooter from './PageFooter.vue' import linksDefault from '~/constants/links.js' const localVue = global.localVue -config.stubs['nuxt-link'] = '' +const stubs = { + 'nuxt-link': { + template: '', + }, +} describe('PageFooter.vue', () => { let mocks @@ -21,7 +25,7 @@ describe('PageFooter.vue', () => { describe('mount', () => { const Wrapper = () => { - return mount(PageFooter, { mocks, localVue }) + return mount(PageFooter, { mocks, localVue, stubs }) } describe('links.js', () => { diff --git a/webapp/components/PageFooter/PageFooter.vue b/webapp/components/PageFooter/PageFooter.vue index c71fc4b5c..cd279766d 100644 --- a/webapp/components/PageFooter/PageFooter.vue +++ b/webapp/components/PageFooter/PageFooter.vue @@ -5,7 +5,7 @@ {{ $t(pageParams.internalPage.footerIdent) }} - - +   diff --git a/webapp/components/Password/Change.spec.js b/webapp/components/Password/Change.spec.js index 95b7c1a3a..ea5c392a9 100644 --- a/webapp/components/Password/Change.spec.js +++ b/webapp/components/Password/Change.spec.js @@ -90,9 +90,8 @@ describe('ChangePassword.vue', () => { }) describe('submit form', () => { - beforeEach(async (done) => { + beforeEach(async () => { await wrapper.find('form').trigger('submit') - done() }) it('calls changePassword mutation', () => { diff --git a/webapp/components/PasswordReset/ChangePassword.spec.js b/webapp/components/PasswordReset/ChangePassword.spec.js index cef110798..f885203c8 100644 --- a/webapp/components/PasswordReset/ChangePassword.spec.js +++ b/webapp/components/PasswordReset/ChangePassword.spec.js @@ -1,9 +1,11 @@ -import { config, mount } from '@vue/test-utils' +import { mount } from '@vue/test-utils' import ChangePassword from './ChangePassword' const localVue = global.localVue -config.stubs['sweetalert-icon'] = '' +const stubs = { + 'sweetalert-icon': true, +} describe('ChangePassword ', () => { let wrapper @@ -27,13 +29,16 @@ describe('ChangePassword ', () => { }) describe('mount', () => { - beforeEach(jest.useFakeTimers) + beforeEach(() => { + jest.useFakeTimers() + }) Wrapper = () => { return mount(ChangePassword, { mocks, propsData, localVue, + stubs, }) } diff --git a/webapp/components/PasswordReset/Request.spec.js b/webapp/components/PasswordReset/Request.spec.js index e601030c6..50d6495bd 100644 --- a/webapp/components/PasswordReset/Request.spec.js +++ b/webapp/components/PasswordReset/Request.spec.js @@ -1,12 +1,8 @@ -import { config, mount } from '@vue/test-utils' +import { mount } from '@vue/test-utils' import Request from './Request' const localVue = global.localVue -config.stubs['sweetalert-icon'] = '' -config.stubs['client-only'] = '' -config.stubs['nuxt-link'] = '' - describe('Request', () => { let wrapper, Wrapper, mocks, stubs @@ -26,12 +22,17 @@ describe('Request', () => { }, } stubs = { - LocaleSwitch: "
    ", + LocaleSwitch: true, + 'sweetalert-icon': true, + 'client-only': true, + 'nuxt-link': true, } }) describe('mount', () => { - beforeEach(jest.useFakeTimers) + beforeEach(() => { + jest.useFakeTimers() + }) Wrapper = () => { return mount(Request, { diff --git a/webapp/components/PostTeaser/PostTeaser.spec.js b/webapp/components/PostTeaser/PostTeaser.spec.js index 546c4581d..7791c62f0 100644 --- a/webapp/components/PostTeaser/PostTeaser.spec.js +++ b/webapp/components/PostTeaser/PostTeaser.spec.js @@ -1,4 +1,4 @@ -import { config, shallowMount, mount, RouterLinkStub } from '@vue/test-utils' +import { shallowMount, mount, RouterLinkStub } from '@vue/test-utils' import Vuex from 'vuex' @@ -6,9 +6,6 @@ import PostTeaser from './PostTeaser.vue' const localVue = global.localVue -config.stubs['client-only'] = '' -config.stubs['v-popover'] = '' - describe('PostTeaser', () => { let store let stubs @@ -35,6 +32,8 @@ describe('PostTeaser', () => { } stubs = { NuxtLink: RouterLinkStub, + 'client-only': true, + 'v-popover': true, } mocks = { $t: jest.fn(), @@ -77,7 +76,9 @@ describe('PostTeaser', () => { spy.mockReset() }) - beforeEach(jest.useFakeTimers) + beforeEach(() => { + jest.useFakeTimers() + }) describe('test Post callbacks', () => { beforeEach(() => { diff --git a/webapp/components/ProgressBar/ProgressBar.vue b/webapp/components/ProgressBar/ProgressBar.vue index 41183b798..09fb4b3a2 100644 --- a/webapp/components/ProgressBar/ProgressBar.vue +++ b/webapp/components/ProgressBar/ProgressBar.vue @@ -79,17 +79,17 @@ export default { border-radius: $border-radius-base; &.color-uni { - background: $color-primary-light; + background: $color-donation-bar; } &.color-repeating-linear-gradient { background: repeating-linear-gradient( 120deg, - $color-primary 0px, - $color-primary 30px, - $color-primary-light 50px, - $color-primary-light 75px, - $color-primary 95px + $color-donation-bar 0px, + $color-donation-bar 30px, + $color-donation-bar-light 50px, + $color-donation-bar-light 75px, + $color-donation-bar 95px ); } } diff --git a/webapp/components/Registration/Signup.spec.js b/webapp/components/Registration/Signup.spec.js index dda0cbb9d..7ef2dc994 100644 --- a/webapp/components/Registration/Signup.spec.js +++ b/webapp/components/Registration/Signup.spec.js @@ -1,10 +1,12 @@ -import { config, mount } from '@vue/test-utils' +import { mount } from '@vue/test-utils' import Signup, { SignupMutation } from './Signup' const localVue = global.localVue -config.stubs['sweetalert-icon'] = '' -config.stubs['nuxt-link'] = '' +const stubs = { + 'sweetalert-icon': true, + 'nuxt-link': true, +} describe('Signup', () => { let wrapper @@ -28,13 +30,16 @@ describe('Signup', () => { }) describe('mount', () => { - beforeEach(jest.useFakeTimers) + beforeEach(() => { + jest.useFakeTimers() + }) Wrapper = () => { return mount(Signup, { mocks, propsData, localVue, + stubs, }) } diff --git a/webapp/components/RelativeDateTime/spec.js b/webapp/components/RelativeDateTime/spec.js index 833baae3a..b0dd687b2 100644 --- a/webapp/components/RelativeDateTime/spec.js +++ b/webapp/components/RelativeDateTime/spec.js @@ -42,7 +42,7 @@ describe('RelativeDateTime', () => { }) it('renders', () => { - expect(Wrapper().is('span')).toBe(true) + expect(Wrapper().element.tagName).toBe('SPAN') }) describe("locale == 'en'", () => { diff --git a/webapp/components/ReleaseModal/ReleaseModal.spec.js b/webapp/components/ReleaseModal/ReleaseModal.spec.js index 9cded646f..a976e37e5 100644 --- a/webapp/components/ReleaseModal/ReleaseModal.spec.js +++ b/webapp/components/ReleaseModal/ReleaseModal.spec.js @@ -100,7 +100,9 @@ describe('ReleaseModal.vue', () => { }) } - beforeEach(jest.useFakeTimers) + beforeEach(() => { + jest.useFakeTimers() + }) describe('given id', () => { beforeEach(() => { diff --git a/webapp/components/SocialMedia/SocialMedia.spec.js b/webapp/components/SocialMedia/SocialMedia.spec.js index 420fbeaf2..5aa8220ae 100644 --- a/webapp/components/SocialMedia/SocialMedia.spec.js +++ b/webapp/components/SocialMedia/SocialMedia.spec.js @@ -1,8 +1,10 @@ -import { config, mount } from '@vue/test-utils' +import { mount } from '@vue/test-utils' import SocialMedia from './SocialMedia.vue' -config.stubs['ds-space'] = '' -config.stubs['ds-text'] = '' +const stubs = { + 'ds-space': true, + 'ds-text': true, +} describe('SocialMedia.vue', () => { let propsData @@ -18,7 +20,7 @@ describe('SocialMedia.vue', () => { describe('mount', () => { const Wrapper = () => { - return mount(SocialMedia, { propsData, mocks }) + return mount(SocialMedia, { propsData, mocks, stubs }) } describe('socialMedia card title', () => { diff --git a/webapp/components/_new/features/SearchResults/SearchResults.spec.js b/webapp/components/_new/features/SearchResults/SearchResults.spec.js index 19653fcac..ace02bd46 100644 --- a/webapp/components/_new/features/SearchResults/SearchResults.spec.js +++ b/webapp/components/_new/features/SearchResults/SearchResults.spec.js @@ -1,4 +1,4 @@ -import { config, mount } from '@vue/test-utils' +import { mount } from '@vue/test-utils' import Vuex from 'vuex' import SearchResults from './SearchResults' import helpers from '~/storybook/helpers' @@ -9,8 +9,10 @@ const localVue = global.localVue localVue.directive('scrollTo', jest.fn()) -config.stubs['client-only'] = '' -config.stubs['nuxt-link'] = '' +const stubs = { + 'client-only': true, + 'nuxt-link': true, +} describe('SearchResults', () => { let mocks, getters, propsData, wrapper @@ -18,7 +20,7 @@ describe('SearchResults', () => { const store = new Vuex.Store({ getters, }) - return mount(SearchResults, { mocks, localVue, propsData, store }) + return mount(SearchResults, { mocks, localVue, propsData, store, stubs }) } beforeEach(() => { diff --git a/webapp/components/_new/generic/CounterIcon/CounterIcon.spec.js b/webapp/components/_new/generic/CounterIcon/CounterIcon.spec.js index 49be22bde..584a971c4 100644 --- a/webapp/components/_new/generic/CounterIcon/CounterIcon.spec.js +++ b/webapp/components/_new/generic/CounterIcon/CounterIcon.spec.js @@ -19,7 +19,7 @@ describe('CounterIcon.vue', () => { }) it('renders the icon', () => { - expect(wrapper.find(BaseIcon).exists()).toBe(true) + expect(wrapper.findComponent(BaseIcon).exists()).toBe(true) }) it('renders the count', () => { @@ -35,7 +35,7 @@ describe('CounterIcon.vue', () => { }) it('renders the icon', () => { - expect(wrapper.find(BaseIcon).exists()).toBe(true) + expect(wrapper.findComponent(BaseIcon).exists()).toBe(true) }) it('renders the capped count with a plus', () => { diff --git a/webapp/components/_new/generic/ProfileAvatar/ProfileAvatar.spec.js b/webapp/components/_new/generic/ProfileAvatar/ProfileAvatar.spec.js index 44d35bd71..413af5047 100644 --- a/webapp/components/_new/generic/ProfileAvatar/ProfileAvatar.spec.js +++ b/webapp/components/_new/generic/ProfileAvatar/ProfileAvatar.spec.js @@ -20,7 +20,7 @@ describe('ProfileAvatar', () => { }) it('renders an icon', () => { - expect(wrapper.find(BaseIcon).exists()).toBe(true) + expect(wrapper.findComponent(BaseIcon).exists()).toBe(true) }) describe('given a profile', () => { @@ -38,7 +38,7 @@ describe('ProfileAvatar', () => { it('renders an icon', () => { propsData = { profile: { name: null } } wrapper = Wrapper() - expect(wrapper.find(BaseIcon).exists()).toBe(true) + expect(wrapper.findComponent(BaseIcon).exists()).toBe(true) }) }) @@ -46,7 +46,7 @@ describe('ProfileAvatar', () => { it('renders an icon', () => { propsData = { profile: { name: 'Anonymous' } } wrapper = Wrapper() - expect(wrapper.find(BaseIcon).exists()).toBe(true) + expect(wrapper.findComponent(BaseIcon).exists()).toBe(true) }) }) diff --git a/webapp/components/_new/generic/TabNavigation/TabNavigation.spec.js b/webapp/components/_new/generic/TabNavigation/TabNavigation.spec.js index 76ce9665a..f545839ef 100644 --- a/webapp/components/_new/generic/TabNavigation/TabNavigation.spec.js +++ b/webapp/components/_new/generic/TabNavigation/TabNavigation.spec.js @@ -1,14 +1,16 @@ -import { config, mount } from '@vue/test-utils' +import { mount } from '@vue/test-utils' import TabNavigation from './TabNavigation' const localVue = global.localVue -config.stubs['client-only'] = '' +const stubs = { + 'client-only': true, +} describe('TabNavigation', () => { let mocks, propsData, wrapper const Wrapper = () => { - return mount(TabNavigation, { mocks, localVue, propsData }) + return mount(TabNavigation, { mocks, localVue, propsData, stubs }) } beforeEach(() => { diff --git a/webapp/components/features/FiledReportsTable/FiledReportsTable.spec.js b/webapp/components/features/FiledReportsTable/FiledReportsTable.spec.js index 4931ea6c6..7a99e85aa 100644 --- a/webapp/components/features/FiledReportsTable/FiledReportsTable.spec.js +++ b/webapp/components/features/FiledReportsTable/FiledReportsTable.spec.js @@ -1,4 +1,4 @@ -import { config, mount, RouterLinkStub } from '@vue/test-utils' +import { mount, RouterLinkStub } from '@vue/test-utils' import Vuex from 'vuex' import FiledReportsTable from './FiledReportsTable' import { reports } from '~/components/features/ReportList/ReportList.story.js' @@ -7,8 +7,6 @@ const localVue = global.localVue localVue.filter('truncate', (string) => string) -config.stubs['client-only'] = '' - describe('FiledReportsTable.vue', () => { let wrapper, mocks, propsData, stubs, filed @@ -18,6 +16,7 @@ describe('FiledReportsTable.vue', () => { } stubs = { NuxtLink: RouterLinkStub, + 'client-only': true, } propsData = {} }) diff --git a/webapp/components/features/ProfileList/FollowList.spec.js b/webapp/components/features/ProfileList/FollowList.spec.js index 44a855eed..07214ae38 100644 --- a/webapp/components/features/ProfileList/FollowList.spec.js +++ b/webapp/components/features/ProfileList/FollowList.spec.js @@ -1,4 +1,4 @@ -import { config, mount } from '@vue/test-utils' +import { mount } from '@vue/test-utils' import Vuex from 'vuex' import helpers from '~/storybook/helpers' @@ -6,9 +6,11 @@ import FollowList from './FollowList.vue' const localVue = global.localVue -config.stubs['client-only'] = '' -config.stubs['ds-space'] = '' -config.stubs['nuxt-link'] = '' +const stubs = { + 'client-only': true, + 'ds-space': true, + 'nuxt-link': true, +} const user = { ...helpers.fakeUser()[0], @@ -45,6 +47,7 @@ describe('FollowList.vue', () => { $t: jest.fn((str) => str), }, localVue, + stubs, }) beforeAll(() => { @@ -138,11 +141,11 @@ describe('FollowList.vue', () => { }) it('renders the user-teasers as an overflowing list', () => { - expect(wrapper.find('.--overflow').is('ul')).toBe(true) + expect(wrapper.find('.--overflow').element.tagName).toBe('UL') }) it('renders a filter text input', () => { - expect(wrapper.find('[name="followingFilter"]').is('input')).toBe(true) + expect(wrapper.find('[name="followingFilter"]').element.tagName).toBe('INPUT') }) }) }) diff --git a/webapp/components/features/ReportList/ReportList.spec.js b/webapp/components/features/ReportList/ReportList.spec.js index d4dbc7132..17c609115 100644 --- a/webapp/components/features/ReportList/ReportList.spec.js +++ b/webapp/components/features/ReportList/ReportList.spec.js @@ -1,4 +1,4 @@ -import { config, mount } from '@vue/test-utils' +import { mount } from '@vue/test-utils' import Vuex from 'vuex' import ReportList from './ReportList' import { reports } from './ReportList.story' @@ -7,8 +7,10 @@ import DropdownFilter from '~/components/DropdownFilter/DropdownFilter' const localVue = global.localVue -config.stubs['client-only'] = '' -config.stubs['nuxt-link'] = '' +const stubs = { + 'client-only': true, + 'nuxt-link': true, +} describe('ReportList', () => { let mocks, mutations, getters, wrapper @@ -45,7 +47,7 @@ describe('ReportList', () => { mutations, getters, }) - return mount(ReportList, { mocks, localVue, store }) + return mount(ReportList, { mocks, localVue, store, stubs }) } describe('renders children components', () => { @@ -54,11 +56,11 @@ describe('ReportList', () => { }) it('renders DropdownFilter', () => { - expect(wrapper.find(DropdownFilter).exists()).toBe(true) + expect(wrapper.findComponent(DropdownFilter).exists()).toBe(true) }) it('renders ReportsTable', () => { - expect(wrapper.find(ReportsTable).exists()).toBe(true) + expect(wrapper.findComponent(ReportsTable).exists()).toBe(true) }) }) @@ -66,7 +68,7 @@ describe('ReportList', () => { beforeEach(async () => { wrapper = Wrapper() wrapper.setData({ reports }) - wrapper.find(ReportsTable).vm.$emit('confirm', reports[0]) + wrapper.findComponent(ReportsTable).vm.$emit('confirm', reports[0]) }) it('calls modal/SET_OPEN', () => { diff --git a/webapp/components/features/ReportRow/ReportRow.spec.js b/webapp/components/features/ReportRow/ReportRow.spec.js index b541a40d9..a1b405edc 100644 --- a/webapp/components/features/ReportRow/ReportRow.spec.js +++ b/webapp/components/features/ReportRow/ReportRow.spec.js @@ -1,4 +1,4 @@ -import { config, mount, RouterLinkStub } from '@vue/test-utils' +import { mount, RouterLinkStub } from '@vue/test-utils' import Vuex from 'vuex' import ReportRow from './ReportRow.vue' import BaseIcon from '~/components/_new/generic/BaseIcon/BaseIcon' @@ -6,8 +6,6 @@ import { reports } from '~/components/features/ReportList/ReportList.story.js' const localVue = global.localVue -config.stubs['client-only'] = '' - describe('ReportRow', () => { let propsData, mocks, stubs, getters, wrapper @@ -18,6 +16,7 @@ describe('ReportRow', () => { } stubs = { NuxtLink: RouterLinkStub, + 'client-only': true, } getters = { 'auth/user': () => { @@ -85,7 +84,9 @@ describe('ReportRow', () => { wrapper = Wrapper() }) it('renders the disabled icon', () => { - expect(wrapper.find('.status-line').find(BaseIcon).props().name).toEqual('eye-slash') + expect(wrapper.find('.status-line').findComponent(BaseIcon).props().name).toEqual( + 'eye-slash', + ) }) it('renders its current status', () => { @@ -99,7 +100,7 @@ describe('ReportRow', () => { wrapper = Wrapper() }) it('renders the enabled icon', () => { - expect(wrapper.find('.status-line').find(BaseIcon).props().name).toEqual('eye') + expect(wrapper.find('.status-line').findComponent(BaseIcon).props().name).toEqual('eye') }) it('renders its current status', () => { @@ -120,7 +121,7 @@ describe('ReportRow', () => { }) it('renders a comments icon', () => { - const commentsIcon = wrapper.find(BaseIcon).props().name + const commentsIcon = wrapper.findComponent(BaseIcon).props().name expect(commentsIcon).toEqual('comments') }) @@ -142,7 +143,7 @@ describe('ReportRow', () => { }) it('renders a bookmark icon', () => { - const postIcon = wrapper.find(BaseIcon).props().name + const postIcon = wrapper.findComponent(BaseIcon).props().name expect(postIcon).toEqual('bookmark') }) @@ -164,7 +165,7 @@ describe('ReportRow', () => { }) it('renders a user icon', () => { - const userIcon = wrapper.find(BaseIcon).props().name + const userIcon = wrapper.findComponent(BaseIcon).props().name expect(userIcon).toEqual('user') }) diff --git a/webapp/components/features/ReportsTable/ReportsTable.spec.js b/webapp/components/features/ReportsTable/ReportsTable.spec.js index c80e4fea5..688139eec 100644 --- a/webapp/components/features/ReportsTable/ReportsTable.spec.js +++ b/webapp/components/features/ReportsTable/ReportsTable.spec.js @@ -1,12 +1,14 @@ -import { config, mount } from '@vue/test-utils' +import { mount } from '@vue/test-utils' import Vuex from 'vuex' import ReportsTable from './ReportsTable.vue' import { reports } from '~/components/features/ReportList/ReportList.story.js' const localVue = global.localVue -config.stubs['client-only'] = '' -config.stubs['nuxt-link'] = '' +const stubs = { + 'client-only': true, + 'nuxt-link': true, +} describe('ReportsTable', () => { let propsData, mocks, getters, wrapper, reportsTable @@ -29,7 +31,7 @@ describe('ReportsTable', () => { const store = new Vuex.Store({ getters, }) - return mount(ReportsTable, { propsData, mocks, localVue, store }) + return mount(ReportsTable, { propsData, mocks, localVue, store, stubs }) } describe('given no reports', () => { diff --git a/webapp/components/features/SearchField/SearchField.spec.js b/webapp/components/features/SearchField/SearchField.spec.js index 140b55caa..e66c31938 100644 --- a/webapp/components/features/SearchField/SearchField.spec.js +++ b/webapp/components/features/SearchField/SearchField.spec.js @@ -1,4 +1,4 @@ -import { config, mount } from '@vue/test-utils' +import { mount } from '@vue/test-utils' import Vuex from 'vuex' import SearchField from './SearchField.vue' import SearchableInput from '~/components/generic/SearchableInput/SearchableInput' @@ -7,7 +7,10 @@ const localVue = global.localVue localVue.filter('truncate', () => 'truncated string') localVue.filter('dateTime', () => Date.now) -config.stubs['nuxt-link'] = '' + +const stubs = { + 'nuxt-link': true, +} describe('SearchField.vue', () => { let mocks, wrapper, getters @@ -26,14 +29,14 @@ describe('SearchField.vue', () => { const store = new Vuex.Store({ getters, }) - return mount(SearchField, { mocks, localVue, store }) + return mount(SearchField, { mocks, localVue, store, stubs }) } describe('mount', () => { describe('Emitted events', () => { let searchableInputComponent beforeEach(() => { - searchableInputComponent = wrapper.find(SearchableInput) + searchableInputComponent = wrapper.findComponent(SearchableInput) }) describe('query event', () => { diff --git a/webapp/components/generic/SearchPost/SearchPost.spec.js b/webapp/components/generic/SearchPost/SearchPost.spec.js index 3e05c9d74..3c690e39c 100644 --- a/webapp/components/generic/SearchPost/SearchPost.spec.js +++ b/webapp/components/generic/SearchPost/SearchPost.spec.js @@ -37,11 +37,11 @@ describe('SearchPost.vue', () => { }) it('renders post commentsCount', () => { - expect(counts.text()).toContain(propsData.option.commentsCount) + expect(counts.text()).toContain(propsData.option.commentsCount.toString()) }) it('renders post shoutedCount', () => { - expect(counts.text()).toContain(propsData.option.shoutedCount) + expect(counts.text()).toContain(propsData.option.shoutedCount.toString()) }) it('renders post author', () => { diff --git a/webapp/components/generic/SearchableInput/SearchableInput.spec.js b/webapp/components/generic/SearchableInput/SearchableInput.spec.js index e0e9f9831..cda9150b8 100644 --- a/webapp/components/generic/SearchableInput/SearchableInput.spec.js +++ b/webapp/components/generic/SearchableInput/SearchableInput.spec.js @@ -1,4 +1,4 @@ -import { config, mount } from '@vue/test-utils' +import { mount } from '@vue/test-utils' import Vuex from 'vuex' import Vue from 'vue' import SearchableInput from './SearchableInput' @@ -8,7 +8,10 @@ const localVue = global.localVue localVue.filter('truncate', () => 'truncated string') localVue.filter('dateTime', () => Date.now) -config.stubs['nuxt-link'] = '' + +const stubs = { + 'nuxt-link': true, +} describe('SearchableInput.vue', () => { let mocks, propsData, getters, wrapper @@ -28,7 +31,7 @@ describe('SearchableInput.vue', () => { const store = new Vuex.Store({ getters, }) - return mount(SearchableInput, { mocks, localVue, propsData, store }) + return mount(SearchableInput, { mocks, localVue, propsData, store, stubs }) } describe('mount', () => { diff --git a/webapp/graphql/groups.js b/webapp/graphql/groups.js index cf64c21b3..bb5292675 100644 --- a/webapp/graphql/groups.js +++ b/webapp/graphql/groups.js @@ -143,6 +143,19 @@ export const changeGroupMemberRoleMutation = () => { ` } +export const removeUserFromGroupMutation = () => { + return gql` + mutation ($groupId: ID!, $userId: ID!) { + RemoveUserFromGroup(groupId: $groupId, userId: $userId) { + id + name + slug + myRoleInGroup + } + } + ` +} + // ------ queries export const groupQuery = (i18n) => { diff --git a/webapp/jest.config.js b/webapp/jest.config.js index 9980c319c..538f72bc7 100644 --- a/webapp/jest.config.js +++ b/webapp/jest.config.js @@ -22,12 +22,14 @@ module.exports = { '^.+\\.js$': 'babel-jest', }, testMatch: ['**/?(*.)+(spec|test).js?(x)'], - modulePathIgnorePatterns: ['/build/'], + modulePathIgnorePatterns: ['/dist/'], moduleNameMapper: { '\\.(svg)$': '/test/fileMock.js', '\\.(css|less)$': 'identity-obj-proxy', + '@mapbox/mapbox-gl-geocoder': 'identity-obj-proxy', '^@/(.*)$': '/src/$1', '^~/(.*)$': '/$1', }, moduleFileExtensions: ['js', 'json', 'vue'], + testEnvironment: 'jest-environment-jsdom', } diff --git a/webapp/layouts/basic.spec.js b/webapp/layouts/basic.spec.js index f84195246..c87fc103b 100644 --- a/webapp/layouts/basic.spec.js +++ b/webapp/layouts/basic.spec.js @@ -1,10 +1,14 @@ import Vuex from 'vuex' -import { config, shallowMount } from '@vue/test-utils' +import { shallowMount } from '@vue/test-utils' import Basic from './basic.vue' const localVue = global.localVue -config.stubs.nuxt = '' +const stubs = { + nuxt: { + template: '', + }, +} describe('basic.vue', () => { let wrapper @@ -28,6 +32,7 @@ describe('basic.vue', () => { store, mocks, localVue, + stubs, }) } @@ -36,7 +41,7 @@ describe('basic.vue', () => { }) it('renders', () => { - expect(wrapper.is('.layout-blank')).toBe(true) + expect(wrapper.classes('layout-blank')).toBe(true) }) }) }) diff --git a/webapp/layouts/blank.spec.js b/webapp/layouts/blank.spec.js index a3ea3120c..f4c33cd9c 100644 --- a/webapp/layouts/blank.spec.js +++ b/webapp/layouts/blank.spec.js @@ -1,9 +1,13 @@ -import { config, shallowMount } from '@vue/test-utils' +import { shallowMount } from '@vue/test-utils' import Blank from './blank.vue' const localVue = global.localVue -config.stubs.nuxt = '' +const stubs = { + nuxt: { + template: '', + }, +} describe('blank.vue', () => { let wrapper @@ -20,6 +24,7 @@ describe('blank.vue', () => { return shallowMount(Blank, { mocks, localVue, + stubs, }) } @@ -28,7 +33,7 @@ describe('blank.vue', () => { }) it('renders', () => { - expect(wrapper.is('.layout-blank')).toBe(true) + expect(wrapper.classes('layout-blank')).toBe(true) }) }) }) diff --git a/webapp/layouts/default.spec.js b/webapp/layouts/default.spec.js index 3d465ce76..bf54d3581 100644 --- a/webapp/layouts/default.spec.js +++ b/webapp/layouts/default.spec.js @@ -1,13 +1,21 @@ import Vuex from 'vuex' -import { config, shallowMount } from '@vue/test-utils' +import { shallowMount } from '@vue/test-utils' import Default from './default.vue' const localVue = global.localVue localVue.directive('scrollTo', jest.fn()) -config.stubs.nuxt = '' -config.stubs['client-only'] = '' -config.stubs['nuxt-link'] = '' +const stubs = { + nuxt: { + template: '', + }, + 'client-only': { + template: '', + }, + 'nuxt-link': { + template: '', + }, +} describe('default.vue', () => { let wrapper @@ -38,6 +46,7 @@ describe('default.vue', () => { store, mocks, localVue, + stubs, }) } @@ -46,7 +55,7 @@ describe('default.vue', () => { }) it('renders', () => { - expect(wrapper.is('.layout-default')).toBe(true) + expect(wrapper.classes('layout-default')).toBe(true) }) }) }) diff --git a/webapp/layouts/error.spec.js b/webapp/layouts/error.spec.js index dccb5eb49..95b54c965 100644 --- a/webapp/layouts/error.spec.js +++ b/webapp/layouts/error.spec.js @@ -1,9 +1,13 @@ -import { config, shallowMount } from '@vue/test-utils' +import { shallowMount } from '@vue/test-utils' import Error from './error.vue' const localVue = global.localVue -config.stubs['nuxt-link'] = '' +const stubs = { + 'nuxt-link': { + template: '', + }, +} describe('error.vue', () => { let mocks, wrapper @@ -15,7 +19,7 @@ describe('error.vue', () => { }) const Wrapper = (propsData = {}) => { - return shallowMount(Error, { mocks, propsData, localVue }) + return shallowMount(Error, { mocks, propsData, localVue, stubs }) } describe('shallowMount', () => { diff --git a/webapp/locales/de.json b/webapp/locales/de.json index 3e0e7369d..c0cae5b08 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -24,7 +24,7 @@ "posts": "Beiträge", "projects": "Projekte", "shouts": "Empfehlungen", - "users": "Benutzer" + "users": "Nutzer" }, "donations": { "goal": "Monatlich benötigte Spenden", @@ -38,11 +38,11 @@ "nameOfHashtag": "Name", "number": "Nr.", "tagCount": "Beiträge", - "tagCountUnique": "Benutzer" + "tagCountUnique": "Nutzer" }, "invites": { "description": "Einladungen sind eine wunderbare Möglichkeit, Deine Freunde in Deinem Netzwerk zu haben …", - "name": "Benutzer einladen", + "name": "Nutzer einladen", "title": "Leute einladen" }, "name": "Systemverwaltung", @@ -59,11 +59,11 @@ "name": "Einstellungen" }, "users": { - "empty": "Keine Benutzer gefunden", + "empty": "Keine Nutzer gefunden", "form": { "placeholder": "E-Mail, Name oder Beschreibung" }, - "name": "Benutzer", + "name": "Nutzer", "roleChanged": "Rolle erfolgreich geändert!", "table": { "columns": { @@ -111,7 +111,7 @@ "shout": "Empfehlung ::: Empfehlungen", "tag": "Schlagwort ::: Schlagwörter", "takeAction": "Aktiv werden", - "user": "Benutzer ::: Benutzer", + "user": "Nutzer ::: Nutzer", "validations": { "categories": "es müssen eine bis drei Themen ausgewählt werden", "email": "muss eine gültige E-Mail-Adresse sein", @@ -138,12 +138,12 @@ "registration": { "create-user-account": { "buttonTitle": "Erstellen", - "error": "Es konnte kein Benutzerkonto erstellt werden!", + "error": "Es konnte kein Nutzerkonto erstellt werden!", "help": "Vielleicht war der Bestätigungscode falsch oder abgelaufen? Wenn das Problem weiterhin besteht, schicke uns gerne eine E-Mail an:", "recieveCommunicationAsEmailsEtcConfirmed": "Ich stimme auch dem Erhalt von E-Mails und anderen Formen der Kommunikation (z.B. Push-Benachrichtigungen) zu.", - "success": "Dein Benutzerkonto wurde erstellt!", + "success": "Dein Nutzerkonto wurde erstellt!", "termsAndCondsEtcConfirmed": "Ich habe folgendes gelesen, verstanden und stimme zu:", - "title": "Benutzerkonto anlegen" + "title": "Nutzerkonto anlegen" }, "email": { "buttonTitle": { @@ -197,14 +197,14 @@ "data-privacy": "Ich habe die Datenschutzerklärung gelesen und verstanden.", "description": "Um loszulegen, kannst Du Dich hier kostenfrei registrieren:", "errors": { - "email-exists": "Es gibt schon ein Benutzerkonto mit dieser E-Mail-Adresse!" + "email-exists": "Es gibt schon ein Nutzerkonto mit dieser E-Mail-Adresse!" }, "submit": "Konto erstellen", "success": "Eine E-Mail mit einem Link zum Abschließen Deiner Registrierung wurde an {email} geschickt", "terms-and-condition": "Ich stimme den
    Nutzungsbedingungen zu." }, "title": "Mach mit bei {APPLICATION_NAME}!", - "unavailable": "Leider ist die öffentliche Registrierung von Benutzerkonten auf diesem Server derzeit nicht möglich." + "unavailable": "Leider ist die öffentliche Registrierung von Nutzerkonten auf diesem Server derzeit nicht möglich." } } }, @@ -267,12 +267,14 @@ "happy": "Glücklich", "surprised": "Erstaunt" }, - "filterFollow": "Beiträge von Benutzern filtern, denen ich folge", + "filterFollow": "Beiträge von Nutzern filtern, denen ich folge", "filterMasonryGrid": { - "myFriends": "Benutzer denen ich folge", + "myFriends": "Nutzer denen ich folge", + "myGroups": "Meine Gruppen", "myTopics": "Meine Themen", "noFilter": "Beiträge filtern" }, + "filterMyGroups": "Beiträge in meinen Gruppen", "inappropriatePicture": "Dieses Bild kann für einige Menschen unangemessen sein.", "languageSelectLabel": "Sprache Deines Beitrags", "languageSelectText": "Sprache wählen", @@ -356,7 +358,7 @@ "unorderedList": "Ungeordnete Liste" }, "mention": { - "noUsersFound": "Keine Benutzer gefunden" + "noUsersFound": "Keine Nutzer gefunden" }, "placeholder": "Schreib etwas Inspirierendes …" }, @@ -378,8 +380,9 @@ "deleteFilter": "Filter löschen", "emotions": "Emotionen", "filter-by": "Filtern nach ...", - "following": "Benutzer denen ich folge", + "following": "Nutzer denen ich folge", "languages": "Sprachen", + "my-groups": "Meinen Gruppen", "order": { "newest": { "hint": "Sortiere die Neuesten nach vorn", @@ -411,9 +414,9 @@ "actionRadius": "Aktionsradius der Gruppe", "addMemberToGroup": "Zur Gruppe hinzufügen", "addMemberToGroupSuccess": "„{name}“ wurde der Gruppe mit der Rolle „{role}“ hinzugefügt!", - "addUser": "Benutzer hinzufügen", + "addUser": "Nutzer hinzufügen", "addUserNoOptions": "Keine Nutzer gefunden!", - "addUserPlaceholder": "Benutzername", + "addUserPlaceholder": "Nutzername", "allGroups": "Alle Gruppen", "button": { "tooltip": "Gruppen anzeigen" @@ -434,7 +437,7 @@ "title": "Meine Gruppe ändern" }, "errors": { - "userAlreadyMember": "Benutzer „{name}“ ist bereits Mitglied!" + "userAlreadyMember": "Nutzer „{name}“ ist bereits Mitglied!" }, "follow": "Folge", "foundation": "Gründung", @@ -452,6 +455,7 @@ "message": "Eine Gruppe zu verlassen ist möglicherweise nicht rückgängig zu machen!
    Gruppe „{name}“ verlassen!", "title": "Möchtest du wirklich die Gruppe verlassen?" }, + "memberRemoved": "Nutzer „{name}“ wurde aus der Gruppe entfernt!", "members": "Mitglieder", "membersAdministrationList": { "avatar": "Avatar", @@ -525,10 +529,10 @@ "logout": "Abmelden", "moreInfo": "Was ist {APPLICATION_NAME}?", "moreInfoHint": "zur Präsentationsseite", - "no-account": "Du hast noch kein Benutzerkonto?", + "no-account": "Du hast noch kein Nutzerkonto?", "no-cookie": "Es kann kein Cookie angelegt werden. Du must Cookies akzeptieren.", "password": "Dein Passwort", - "register": "Benutzerkonto erstellen", + "register": "Nutzerkonto erstellen", "success": "Du bist eingeloggt!" }, "maintenance": { @@ -544,7 +548,7 @@ "markerTypes": { "group": "Gruppe", "theUser": "deine Position", - "user": "Benutzer" + "user": "Nutzer" }, "pageTitle": "Landkarte", "styles": { @@ -591,12 +595,12 @@ "submit": "Bestätige Entscheidung", "User": { "disable": { - "message": "Möchtest Du den Benutzer „{name}“ wirklich gesperrt lassen?", - "title": "Sperre den Benutzer abschließend" + "message": "Möchtest Du den Nutzer „{name}“ wirklich gesperrt lassen?", + "title": "Sperre den Nutzer abschließend" }, "enable": { - "message": "Möchtest Du den Benutzer „{name}“ wirklich entsperrt lassen?", - "title": "Entsperre den Benutzer abschließend" + "message": "Möchtest Du den Nutzer „{name}“ wirklich entsperrt lassen?", + "title": "Entsperre den Nutzer abschließend" } } }, @@ -644,12 +648,12 @@ "mentioned_in_post": "Hat Dich in einem Beitrag erwähnt …" }, "title": "Benachrichtigungen", - "user": "Benutzer" + "user": "Nutzer" }, "position": { "group": "Gruppe", "my": "Meine Position", - "user": "Benutzer" + "user": "Nutzer" }, "post": { "comment": { @@ -740,7 +744,7 @@ "submit": "freigeben", "success": "Erfolgreich freigegeben!", "user": { - "error": "Den Benutzer hast Du schon gemeldet!", + "error": "Den Nutzer hast Du schon gemeldet!", "message": "Bist Du sicher, dass Du den Nutzer „{name}“ freigeben möchtest?", "title": "Nutzer freigeben", "type": "Nutzer" @@ -784,7 +788,7 @@ "submit": "Meldung senden", "success": "Vielen Dank für diese Meldung!", "user": { - "error": "Du hast den Benutzer bereits gemeldet!", + "error": "Du hast den Nutzer bereits gemeldet!", "message": "Bist Du sicher, dass Du den Nutzer „{name}“ melden möchtest?", "title": "Nutzer melden", "type": "Nutzer" @@ -797,7 +801,7 @@ "Group": "Gruppe ::: Gruppen", "Post": "Beitrag ::: Beiträge", "Tag": "Hashtag ::: Hashtags", - "User": "Benutzer ::: Benutzer" + "User": "Nutzer ::: Nutzer" }, "hint": "Wonach suchst Du? Nutze !… für Beiträge, @… für Mitglieder, &… für Gruppen, #… für Hashtags", "no-results": "Keine Ergebnisse für \"{search}\" gefunden. Versuch' es mit einem anderen Begriff!", @@ -816,16 +820,16 @@ }, "empty": "Bislang hast Du niemanden blockiert.", "explanation": { - "closing": "Das sollte fürs Erste genügen, damit blockierte Benutzer Dich nicht mehr länger belästigen können.", + "closing": "Das sollte fürs Erste genügen, damit blockierte Nutzer Dich nicht mehr länger belästigen können.", "commenting-disabled": "Du kannst den Beitrag derzeit nicht kommentieren.", "commenting-explanation": "Dafür kann es mehrere Gründe geben, bitte schau in unsere ", - "intro": "Wenn ein anderer Benutzer durch Dich blockiert wurde, dann passiert Folgendes:", - "notifications": "Von Dir blockierte Benutzer werden keine Benachrichtigungen mehr erhalten, falls sie in Deinen Beiträgen erwähnt werden.", + "intro": "Wenn ein anderer Nutzer durch Dich blockiert wurde, dann passiert Folgendes:", + "notifications": "Von Dir blockierte Nutzer werden keine Benachrichtigungen mehr erhalten, falls sie in Deinen Beiträgen erwähnt werden.", "their-perspective": "Umgekehrt das gleiche: Die blockierte Person bekommt auch in ihren Benachrichtigungen Deine Beiträge nicht mehr zu sehen.", "your-perspective": "In Deinen Benachrichtigungen tauchen keine Beiträge der blockierten Person mehr auf." }, - "how-to": "Du kannst andere Benutzer auf deren Profilseite über das Inhaltsmenü blockieren.", - "name": "Blockierte Benutzer", + "how-to": "Du kannst andere Nutzer auf deren Profilseite über das Inhaltsmenü blockieren.", + "name": "Blockierte Nutzer", "unblock": "Blockierten Nutzer freigeben", "unblocked": "{name} ist wieder entsperrt" }, @@ -834,7 +838,7 @@ "labelCity": "Deine Stadt oder Region", "labelCityHint": "(zeigt ungefähre Position auf der Landkarte)", "labelName": "Dein Name", - "labelSlug": "Dein eindeutiger Benutzername", + "labelSlug": "Dein eindeutiger Nutzername", "name": "Deine Daten", "namePlaceholder": "Petra Lustig", "success": "Deine Daten wurden erfolgreich aktualisiert!" @@ -843,12 +847,12 @@ "accountDescription": "Sei dir bewusst, dass deine Beiträge und Kommentare für unsere Community wichtig sind. Wenn du sie trotzdem löschen möchtest, musst du sie unten markieren.", "accountWarning": "Dein Konto, deine Beiträge oder Kommentare kannst du nach dem Löschen WEDER VERWALTEN NOCH WIEDERHERSTELLEN!", "accountWarningAdmin": "Das Konto, die Beiträge oder Kommentare können nach dem Löschen WEDER VERWALTET NOCH WIEDERHERGESTELLT WERDEN!", - "accountWarningIsAdmin": "Achtung! Du löschst jetzt ein Benutzerkonto!", + "accountWarningIsAdmin": "Achtung! Du löschst jetzt ein Nutzerkonto!", "commentedCount": "Meinen {count} Kommentar löschen ::: Meine {count} Kommentare löschen", - "confirmDeleting": "Benutzerkonto jetzt löschen", + "confirmDeleting": "Nutzerkonto jetzt löschen", "contributionsCount": "Meinen {count} Beitrag löschen ::: Meine {count} Beiträge löschen", "infoAdmin": "Alle Beiträge und Kommentare des Users werden zusätzlich gelöscht!", - "name": "Benutzerkonto löschen", + "name": "Nutzerkonto löschen", "pleaseConfirm": "Zerstörerische Aktion! Gib „{confirm}“ ein, um zu bestätigen.", "success": "Konto erfolgreich gelöscht!" }, @@ -908,13 +912,13 @@ }, "empty": "Bislang hast du niemanden stummgeschaltet.", "explanation": { - "intro": "Wenn ein anderer Benutzer von dir stummgeschaltet wurde, dann passiert folgendes:", + "intro": "Wenn ein anderer Nutzer von dir stummgeschaltet wurde, dann passiert folgendes:", "search": "Die Beiträge von stummgeschalteten Personen verschwinden aus deinen Suchergebnissen.", "your-perspective": "In deiner Beitragsübersicht tauchen keine Beiträge der stummgeschalteten Person mehr auf." }, - "how-to": "Du kannst andere Benutzer auf deren Profilseite über das Inhaltsmenü stummschalten.", + "how-to": "Du kannst andere Nutzer auf deren Profilseite über das Inhaltsmenü stummschalten.", "mute": "Stumm schalten", - "name": "Stummgeschaltete Benutzer", + "name": "Stummgeschaltete Nutzer", "unmute": "Stummschaltung aufheben", "unmuted": "{name} ist nicht mehr stummgeschaltet" }, @@ -969,7 +973,7 @@ }, "validation": { "slug": { - "alreadyTaken": "Dieser Benutzername ist schon vergeben.", + "alreadyTaken": "Dieser Nutzername ist schon vergeben.", "regex": "Es sind nur Kleinbuchstaben, Zahlen, Unterstriche oder Bindestriche erlaubt." } } diff --git a/webapp/locales/en.json b/webapp/locales/en.json index 9e45396b8..3a741a02b 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -270,9 +270,11 @@ "filterFollow": "Filter contributions from users I follow", "filterMasonryGrid": { "myFriends": "Users I follow", + "myGroups": "My groups", "myTopics": "My topics", "noFilter": "Filter posts" }, + "filterMyGroups": "Contributions in my groups", "inappropriatePicture": "This image may be inappropriate for some people.", "languageSelectLabel": "Language of your contribution", "languageSelectText": "Select Language", @@ -380,6 +382,7 @@ "filter-by": "Filter by ...", "following": "Users I follow", "languages": "Languages", + "my-groups": "My groups", "order": { "newest": { "hint": "Sort posts by the newest first", @@ -452,6 +455,7 @@ "message": "Leaving a group may be irreversible!
    Leave group “{name}”!", "title": "Do you really want to leave the group?" }, + "memberRemoved": "User “{name}” was removed from group!", "members": "Members", "membersAdministrationList": { "avatar": "Avatar", diff --git a/webapp/package.json b/webapp/package.json index bc3577400..9b9dbdf72 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -67,7 +67,7 @@ "devDependencies": { "@babel/core": "~7.12.3", "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/preset-env": "~7.9.0", + "@babel/preset-env": "^7.20.2", "@faker-js/faker": "5.1.0", "@nuxtjs/composition-api": "0.32.0", "@storybook/addon-a11y": "^6.3.6", @@ -77,11 +77,11 @@ "@vue/cli-shared-utils": "~4.3.1", "@vue/eslint-config-prettier": "~6.0.0", "@vue/server-test-utils": "~1.0.0-beta.31", - "@vue/test-utils": "~1.0.0-beta.31", + "@vue/test-utils": "1.3.4", "async-validator": "^3.2.4", "babel-core": "~7.0.0-bridge.0", "babel-eslint": "~10.1.0", - "babel-jest": "~25.3.0", + "babel-jest": "29.4", "babel-loader": "~8.1.0", "babel-plugin-require-context-hook": "^1.0.0", "babel-preset-vue": "~2.0.2", @@ -100,7 +100,8 @@ "eslint-plugin-vue": "~6.2.2", "flush-promises": "^1.0.2", "identity-obj-proxy": "^3.0.0", - "jest": "~26.6.3", + "jest": "29.4", + "jest-environment-jsdom": "^29.4.2", "mutation-observer": "^1.0.3", "prettier": "~2.7.1", "sass-loader": "~10.1.1", diff --git a/webapp/pages/admin.spec.js b/webapp/pages/admin.spec.js index fc3849fc4..aa6eceab1 100644 --- a/webapp/pages/admin.spec.js +++ b/webapp/pages/admin.spec.js @@ -1,7 +1,9 @@ -import { config, mount } from '@vue/test-utils' +import { mount } from '@vue/test-utils' import admin from './admin.vue' -config.stubs['nuxt-child'] = '' +const stubs = { + 'nuxt-child': true, +} const localVue = global.localVue @@ -20,6 +22,7 @@ describe('admin.vue', () => { return mount(admin, { mocks, localVue, + stubs, }) } @@ -28,7 +31,7 @@ describe('admin.vue', () => { }) it('renders', () => { - expect(wrapper.is('div')).toBe(true) + expect(wrapper.element.tagName).toBe('DIV') }) }) }) diff --git a/webapp/pages/admin/categories.spec.js b/webapp/pages/admin/categories.spec.js index 55715e74b..f293dacf2 100644 --- a/webapp/pages/admin/categories.spec.js +++ b/webapp/pages/admin/categories.spec.js @@ -26,7 +26,7 @@ describe('categories.vue', () => { }) it('renders', () => { - expect(wrapper.is('.base-card')).toBe(true) + expect(wrapper.classes('base-card')).toBe(true) }) }) }) diff --git a/webapp/pages/admin/donations.spec.js b/webapp/pages/admin/donations.spec.js index 57e28ff41..37a20393f 100644 --- a/webapp/pages/admin/donations.spec.js +++ b/webapp/pages/admin/donations.spec.js @@ -57,7 +57,7 @@ describe('donations.vue', () => { }) it('renders', () => { - expect(wrapper.is('.base-card')).toBe(true) + expect(wrapper.classes('base-card')).toBe(true) }) describe('displays', () => { @@ -88,24 +88,18 @@ describe('donations.vue', () => { describe('form component click', () => { it('on #showDonations checkbox changes "showDonations" to true', async () => { - // starts with false - wrapper.find('#showDonations').trigger('click') // set to true - await wrapper.vm.$nextTick() + await wrapper.find('#showDonations').setChecked(true) expect(wrapper.vm.showDonations).toBe(true) }) it('on #showDonations checkbox twice changes "showDonations" back to false', async () => { - // starts with false - wrapper.find('#showDonations').trigger('click') // set to true - wrapper.find('#showDonations').trigger('click') // set to false - await wrapper.vm.$nextTick() + await wrapper.find('#showDonations').setChecked(true) + await wrapper.find('#showDonations').setChecked(false) expect(wrapper.vm.showDonations).toBe(false) }) it.skip('on donations-goal and enter value XXX', async () => { - wrapper.find('#donations-goal').setValue('20000') - await wrapper.vm.$nextTick() - // console.log(wrapper.find('#donations-goal').element.value) + await wrapper.find('#donations-goal').setValue('20000') expect(wrapper.vm.formData.goal).toBe('20000') }) }) @@ -141,7 +135,7 @@ describe('donations.vue', () => { }) it('calls mutation with input values once', async () => { - wrapper.find('#showDonations').trigger('click') // set to true + wrapper.find('#showDonations').setChecked(true) await wrapper.vm.$nextTick() wrapper.find('#donations-goal').setValue('20000') await wrapper.vm.$nextTick() diff --git a/webapp/pages/admin/hashtags.spec.js b/webapp/pages/admin/hashtags.spec.js index cd2d308d1..b1a44aebf 100644 --- a/webapp/pages/admin/hashtags.spec.js +++ b/webapp/pages/admin/hashtags.spec.js @@ -26,7 +26,7 @@ describe('hashtags.vue', () => { }) it('renders', () => { - expect(wrapper.is('.base-card')).toBe(true) + expect(wrapper.classes('base-card')).toBe(true) }) }) }) diff --git a/webapp/pages/admin/invite.spec.js b/webapp/pages/admin/invite.spec.js index e3e882119..da0f743a2 100644 --- a/webapp/pages/admin/invite.spec.js +++ b/webapp/pages/admin/invite.spec.js @@ -29,7 +29,7 @@ describe('invite.vue', () => { }) it('renders', () => { - expect(wrapper.is('.ds-section')).toBe(true) + expect(wrapper.classes('ds-section')).toBe(true) }) }) }) diff --git a/webapp/pages/admin/notifications.spec.js b/webapp/pages/admin/notifications.spec.js index c9acf81a6..34773058e 100644 --- a/webapp/pages/admin/notifications.spec.js +++ b/webapp/pages/admin/notifications.spec.js @@ -29,7 +29,7 @@ describe('notifications.vue', () => { }) it('renders', () => { - expect(wrapper.is('.base-card')).toBe(true) + expect(wrapper.classes('base-card')).toBe(true) }) }) }) diff --git a/webapp/pages/admin/organizations.spec.js b/webapp/pages/admin/organizations.spec.js index d019d9485..2045a09b3 100644 --- a/webapp/pages/admin/organizations.spec.js +++ b/webapp/pages/admin/organizations.spec.js @@ -29,7 +29,7 @@ describe('organizations.vue', () => { }) it('renders', () => { - expect(wrapper.is('.base-card')).toBe(true) + expect(wrapper.classes('base-card')).toBe(true) }) }) }) diff --git a/webapp/pages/admin/pages.spec.js b/webapp/pages/admin/pages.spec.js index e0c3c9fb4..91103b756 100644 --- a/webapp/pages/admin/pages.spec.js +++ b/webapp/pages/admin/pages.spec.js @@ -29,7 +29,7 @@ describe('pages.vue', () => { }) it('renders', () => { - expect(wrapper.is('.base-card')).toBe(true) + expect(wrapper.classes('base-card')).toBe(true) }) }) }) diff --git a/webapp/pages/admin/settings.spec.js b/webapp/pages/admin/settings.spec.js index 78a5beb94..0f83a37d5 100644 --- a/webapp/pages/admin/settings.spec.js +++ b/webapp/pages/admin/settings.spec.js @@ -29,7 +29,7 @@ describe('settings.vue', () => { }) it('renders', () => { - expect(wrapper.is('.base-card')).toBe(true) + expect(wrapper.classes('base-card')).toBe(true) }) }) }) diff --git a/webapp/pages/admin/users.spec.js b/webapp/pages/admin/users.spec.js index e8e624cc1..43c51fb52 100644 --- a/webapp/pages/admin/users.spec.js +++ b/webapp/pages/admin/users.spec.js @@ -1,9 +1,12 @@ -import { config, mount } from '@vue/test-utils' +import { mount } from '@vue/test-utils' import Vuex from 'vuex' import Users from './users.vue' const localVue = global.localVue -config.stubs['nuxt-link'] = '' + +const stubs = { + 'nuxt-link': true, +} describe('Users', () => { let wrapper @@ -49,12 +52,13 @@ describe('Users', () => { mocks, localVue, store, + stubs, }) } it('renders', () => { wrapper = Wrapper() - expect(wrapper.is('div')).toBe(true) + expect(wrapper.element.tagName).toBe('DIV') }) describe('search', () => { diff --git a/webapp/pages/code-of-conduct.spec.js b/webapp/pages/code-of-conduct.spec.js index c050f65ba..e914ceaee 100644 --- a/webapp/pages/code-of-conduct.spec.js +++ b/webapp/pages/code-of-conduct.spec.js @@ -33,7 +33,7 @@ describe('code-of-conduct.vue', () => { }) it('renders', () => { - expect(wrapper.is('div')).toBeTruthy() + expect(wrapper.element.tagName).toBe('DIV') }) it('has correct content', () => { diff --git a/webapp/pages/data-privacy.spec.js b/webapp/pages/data-privacy.spec.js index c5e4e4d62..a1fdf4afc 100644 --- a/webapp/pages/data-privacy.spec.js +++ b/webapp/pages/data-privacy.spec.js @@ -33,7 +33,7 @@ describe('data-privacy.vue', () => { }) it('renders', () => { - expect(wrapper.is('div')).toBeTruthy() + expect(wrapper.element.tagName).toBe('DIV') }) it('has correct content', () => { diff --git a/webapp/pages/donate.spec.js b/webapp/pages/donate.spec.js index 719cdbf77..2eff7d8d0 100644 --- a/webapp/pages/donate.spec.js +++ b/webapp/pages/donate.spec.js @@ -37,7 +37,7 @@ describe('donate.vue', () => { }) it('renders', () => { - expect(wrapper.is('div')).toBeTruthy() + expect(wrapper.element.tagName).toBe('DIV') }) it('has correct content', () => { diff --git a/webapp/pages/faq.spec.js b/webapp/pages/faq.spec.js index 3e92d3f7f..9d3c776cc 100644 --- a/webapp/pages/faq.spec.js +++ b/webapp/pages/faq.spec.js @@ -33,7 +33,7 @@ describe('faq.vue', () => { }) it('renders', () => { - expect(wrapper.is('div')).toBeTruthy() + expect(wrapper.element.tagName).toBe('DIV') }) it('has correct content', () => { diff --git a/webapp/pages/group/_id.spec.js b/webapp/pages/group/_id.spec.js index bafd5c392..1a9a7203a 100644 --- a/webapp/pages/group/_id.spec.js +++ b/webapp/pages/group/_id.spec.js @@ -1,9 +1,11 @@ -import { config, mount } from '@vue/test-utils' +import { mount } from '@vue/test-utils' import _id from './_id.vue' const localVue = global.localVue -config.stubs['nuxt-child'] = '' +const stubs = { + 'nuxt-child': true, +} describe('Group profile _id.vue', () => { let wrapper @@ -19,6 +21,7 @@ describe('Group profile _id.vue', () => { return mount(_id, { mocks, localVue, + stubs, }) } @@ -27,7 +30,7 @@ describe('Group profile _id.vue', () => { }) it('renders', () => { - expect(wrapper.findAll('.nuxt-child')).toHaveLength(1) + expect(wrapper.findComponent({ name: 'nuxt-child' }).exists()).toBe(true) }) }) }) diff --git a/webapp/pages/group/_id/_slug.spec.js b/webapp/pages/group/_id/_slug.spec.js index 963952f5e..bb9279bce 100644 --- a/webapp/pages/group/_id/_slug.spec.js +++ b/webapp/pages/group/_id/_slug.spec.js @@ -1,15 +1,18 @@ -import { config, mount } from '@vue/test-utils' +import { mount } from '@vue/test-utils' import GroupProfileSlug from './_slug.vue' const localVue = global.localVue localVue.filter('date', (d) => d) -config.stubs['client-only'] = '' -config.stubs['v-popover'] = '' -config.stubs['nuxt-link'] = '' -config.stubs['infinite-loading'] = '' -config.stubs['follow-list'] = '' +const stubs = { + 'client-only': true, + 'v-popover': true, + 'nuxt-link': true, + 'router-link': true, + 'infinite-loading': true, + 'follow-list': true, +} describe('GroupProfileSlug', () => { let wrapper @@ -201,6 +204,7 @@ describe('GroupProfileSlug', () => { mocks, localVue, data, + stubs, }) } diff --git a/webapp/pages/group/_id/_slug.vue b/webapp/pages/group/_id/_slug.vue index 1b6d66033..641b8802e 100644 --- a/webapp/pages/group/_id/_slug.vue +++ b/webapp/pages/group/_id/_slug.vue @@ -1,5 +1,5 @@