Merge branch 'master' of github.com:Ocelot-Social-Community/Ocelot-Social

This commit is contained in:
Markus 2023-03-09 13:31:06 +01:00
commit 277654d9c5
193 changed files with 4280 additions and 2717 deletions

209
.github/dependabot.yml vendored
View File

@ -1,179 +1,68 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: "github-actions"
directory: "/"
rebase-strategy: "disabled"
schedule:
interval: weekly
day: "saturday"
timezone: "Europe/Berlin"
time: "03:00"
- package-ecosystem: npm - package-ecosystem: npm
directory: "/" directory: "/"
rebase-strategy: "disabled"
schedule: schedule:
interval: daily interval: weekly
time: "04:00" day: "saturday"
open-pull-requests-limit: 10 timezone: "Europe/Berlin"
ignore: time: "03:00"
- 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
- package-ecosystem: npm - package-ecosystem: npm
directory: "/backend" directory: "/backend"
rebase-strategy: "disabled"
schedule: schedule:
interval: daily interval: weekly
time: "04:00" day: "saturday"
open-pull-requests-limit: 10 timezone: "Europe/Berlin"
ignore: time: "03:00"
- 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
- package-ecosystem: npm - package-ecosystem: npm
directory: "/webapp" directory: "/webapp"
rebase-strategy: "disabled"
schedule: schedule:
interval: daily interval: weekly
time: "04:00" day: "saturday"
open-pull-requests-limit: 10 timezone: "Europe/Berlin"
ignore: time: "03:00"
- 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
- package-ecosystem: docker - package-ecosystem: docker
directory: "/backend" directory: "/backend"
rebase-strategy: "disabled"
schedule: schedule:
interval: daily interval: weekly
time: "04:00" day: "saturday"
open-pull-requests-limit: 10 timezone: "Europe/Berlin"
ignore: time: "03:00"
- dependency-name: node - package-ecosystem: docker
versions: directory: "/webapp"
- ">= 15.4.a, < 15.5" rebase-strategy: "disabled"
- dependency-name: node schedule:
versions: interval: weekly
- ">= 15.5.a, < 15.6" day: "saturday"
- dependency-name: node timezone: "Europe/Berlin"
versions: time: "03:00"
- 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
- package-ecosystem: docker - package-ecosystem: docker
directory: "/neo4j" directory: "/neo4j"
rebase-strategy: "disabled"
schedule: schedule:
interval: daily interval: weekly
time: "04:00" day: "saturday"
open-pull-requests-limit: 10 timezone: "Europe/Berlin"
ignore: time: "03:00"
- dependency-name: neo4j
versions:
- 4.2.3
- 4.2.4
- package-ecosystem: docker - package-ecosystem: docker
directory: "/deployment/legacy-migration/maintenance-worker" directory: "/deployment/src/docker"
rebase-strategy: "disabled"
schedule: schedule:
interval: daily interval: weekly
time: "04:00" day: "saturday"
open-pull-requests-limit: 10 timezone: "Europe/Berlin"
time: "03:00"

View File

@ -1,7 +1,6 @@
name: "ocelot.social lint pull request CI" name: "ocelot.social lint pull request CI"
on: on:
pull_request:
pull_request_target: pull_request_target:
types: types:
- opened - opened
@ -12,6 +11,7 @@ jobs:
main: main:
name: Validate PR title name: Validate PR title
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.actor != 'dependabot[bot]' }}
steps: steps:
- uses: amannn/action-semantic-pull-request@v5 - uses: amannn/action-semantic-pull-request@v5
env: env:

View File

@ -1,9 +1,7 @@
name: ocelot.social publish branded CI name: publish-branded
on: on:
push: repository_dispatch:
branches: types: [trigger-build-success]
- master
jobs: jobs:
build_branded: build_branded:
name: Docker Build Branded name: Docker Build Branded
@ -11,7 +9,10 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 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 - name: Build branded images
run: | run: |
deployment/scripts/branded-images.build.sh deployment/scripts/branded-images.build.sh
@ -47,6 +48,8 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
with:
ref: ${{ github.event.client_payload.ref }}
- name: Download Docker Image (Backend) - name: Download Docker Image (Backend)
uses: actions/download-artifact@v2 uses: actions/download-artifact@v2
@ -74,12 +77,3 @@ jobs:
- name: Upload to dockerhub - name: Upload to dockerhub
run: deployment/scripts/branded-images.upload.sh 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

View File

@ -1,64 +1,36 @@
name: ocelot.social publish CI name: publish
on: on:
push: push:
branches: branches:
- master - master
# - 5059-epic-groups # for testing while developing
# template branches in repo
# - template--separate-branch-auto-deployment--5059-epic-groups
jobs: 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 ########################################## # JOB: DOCKER BUILD COMMUNITY NEO4J ##########################################
############################################################################## ##############################################################################
build_production_neo4j: build_production_neo4j:
name: Docker Build Production - Neo4J name: Docker Build Production - Neo4J
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [prepare]
steps: steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v3
########################################################################## - name: Setup env
# SET ENVS ############################################################### run: |
########################################################################## echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
- name: ENV - VERSION echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
run: echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
- name: ENV - BUILD_DATE - run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
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: Neo4J | Build `community` image - 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 - name: Neo4J | Save docker image
run: docker save "ocelotsocialnetwork/neo4j-community" > /tmp/neo4j.tar run: docker save "ocelotsocialnetwork/neo4j-community" > /tmp/neo4j.tar
- name: Upload Artifact - name: Upload Artifact
@ -73,32 +45,41 @@ jobs:
build_production_backend: build_production_backend:
name: Docker Build Production - Backend name: Docker Build Production - Backend
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [prepare]
steps: steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v3
########################################################################## - name: Setup env
# SET ENVS ############################################################### run: |
########################################################################## echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
- name: ENV - VERSION echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
run: echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
- name: ENV - BUILD_DATE - run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
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: Backend | Build `production` image - name: Backend | Build `production` image
run: | 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 base \
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/ --tag "ocelotsocialnetwork/backend:latest-base" \
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/ --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 - name: Backend | Save docker image
run: docker save "ocelotsocialnetwork/backend" > /tmp/backend.tar run: docker save "ocelotsocialnetwork/backend" > /tmp/backend.tar
- name: Upload Artifact - name: Upload Artifact
@ -113,32 +94,41 @@ jobs:
build_production_webapp: build_production_webapp:
name: Docker Build Production - WebApp name: Docker Build Production - WebApp
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [prepare]
steps: steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v3
########################################################################## - name: Setup env
# SET ENVS ############################################################### run: |
########################################################################## echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
- name: ENV - VERSION echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
run: echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
- name: ENV - BUILD_DATE - run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
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: Webapp | Build `production` image - name: Webapp | Build `production` image
run: | 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 base \
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/ --tag "ocelotsocialnetwork/webapp:latest-base" \
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/ --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 - name: Webapp | Save docker image
run: docker save "ocelotsocialnetwork/webapp" > /tmp/webapp.tar run: docker save "ocelotsocialnetwork/webapp" > /tmp/webapp.tar
- name: Upload Artifact - name: Upload Artifact
@ -153,32 +143,44 @@ jobs:
build_production_maintenance: build_production_maintenance:
name: Docker Build Production - Maintenance name: Docker Build Production - Maintenance
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [prepare]
steps: steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v3
########################################################################## - name: Setup env
# SET ENVS ############################################################### run: |
########################################################################## echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
- name: ENV - VERSION echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
run: echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
- name: ENV - BUILD_DATE - run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
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: Maintenance | Build `production` image - name: Maintenance | Build `production` image
run: | 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 base \
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 --tag "ocelotsocialnetwork/maintenance:latest-base" \
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 --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 - name: Maintenance | Save docker image
run: docker save "ocelotsocialnetwork/maintenance" > /tmp/maintenance.tar run: docker save "ocelotsocialnetwork/maintenance" > /tmp/maintenance.tar
- name: Upload Artifact - name: Upload Artifact
@ -198,130 +200,39 @@ jobs:
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }} DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
steps: steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code
uses: actions/checkout@v3
##########################################################################
# DOWNLOAD DOCKER IMAGES #################################################
##########################################################################
- name: Download Docker Image (Neo4J) - name: Download Docker Image (Neo4J)
uses: actions/download-artifact@v3 uses: actions/download-artifact@v3
with: with:
name: docker-neo4j-community name: docker-neo4j-community
path: /tmp path: /tmp
- name: Load Docker Image - run: docker load < /tmp/neo4j.tar
run: docker load < /tmp/neo4j.tar
- name: Download Docker Image (Backend) - name: Download Docker Image (Backend)
uses: actions/download-artifact@v3 uses: actions/download-artifact@v3
with: with:
name: docker-backend-production name: docker-backend-production
path: /tmp path: /tmp
- name: Load Docker Image - run: docker load < /tmp/backend.tar
run: docker load < /tmp/backend.tar
- name: Download Docker Image (WebApp) - name: Download Docker Image (WebApp)
uses: actions/download-artifact@v3 uses: actions/download-artifact@v3
with: with:
name: docker-webapp-production name: docker-webapp-production
path: /tmp path: /tmp
- name: Load Docker Image - run: docker load < /tmp/webapp.tar
run: docker load < /tmp/webapp.tar
- name: Download Docker Image (Maintenance) - name: Download Docker Image (Maintenance)
uses: actions/download-artifact@v3 uses: actions/download-artifact@v3
with: with:
name: docker-maintenance-production name: docker-maintenance-production
path: /tmp path: /tmp
- name: Load Docker Image - run: docker load < /tmp/maintenance.tar
run: docker load < /tmp/maintenance.tar
##########################################################################
# Upload #################################################################
##########################################################################
- name: login to dockerhub - name: login to dockerhub
run: echo "${DOCKERHUB_TOKEN}" | docker login -u "${DOCKERHUB_USERNAME}" --password-stdin run: echo "${DOCKERHUB_TOKEN}" | docker login -u "${DOCKERHUB_USERNAME}" --password-stdin
- name: Push neo4j - name: Push images
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
run: | run: |
kubectl -n default set image deployment/ocelot-webapp container-ocelot-webapp=ocelotsocialnetwork/webapp:$BUILD_VERSION docker push --all-tags ocelotsocialnetwork/neo4j-community
kubectl -n default rollout restart deployment/ocelot-webapp docker push --all-tags ocelotsocialnetwork/backend
kubectl -n default set image deployment/ocelot-backend container-ocelot-backend=ocelotsocialnetwork/backend:$BUILD_VERSION docker push --all-tags ocelotsocialnetwork/webapp
kubectl -n default rollout restart deployment/ocelot-backend docker push --all-tags ocelotsocialnetwork/maintenance
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"
############################################################################## ##############################################################################
# JOB: GITHUB TAG LATEST VERSION ############################################# # JOB: GITHUB TAG LATEST VERSION #############################################
@ -331,39 +242,16 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [upload_to_dockerhub] needs: [upload_to_dockerhub]
steps: steps:
##########################################################################
# CHECKOUT CODE ##########################################################
##########################################################################
- name: Checkout code - name: Checkout code
uses: actions/checkout@v3 uses: actions/checkout@v3
with: with:
fetch-depth: 0 # Fetch full History for changelog fetch-depth: 0 # Fetch full History for changelog
########################################################################## - name: Setup env
# SET ENVS ############################################################### run: |
########################################################################## echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV
- name: ENV - VERSION echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV
run: echo "VERSION=$(node -p -e "require('./package.json').version")" >> $GITHUB_ENV echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV
- name: ENV - BUILD_DATE - run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV
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: package-version-to-git-tag + build number - name: package-version-to-git-tag + build number
uses: pkgdeps/git-tag-action@v2 uses: pkgdeps/git-tag-action@v2
with: with:
@ -372,13 +260,10 @@ jobs:
version: ${{ env.BUILD_VERSION }} version: ${{ env.BUILD_VERSION }}
git_commit_sha: ${{ github.sha }} git_commit_sha: ${{ github.sha }}
git_tag_prefix: "b" git_tag_prefix: "b"
########################################################################## - name: Generate changelog
# Push release tag to GitHub ############################################# run: |
########################################################################## yarn install
- name: yarn install yarn auto-changelog --latest-version ${{ env.VERSION }} --unreleased-only
run: yarn install
- name: generate changelog
run: yarn auto-changelog --latest-version ${{ env.VERSION }} --unreleased-only
- name: package-version-to-git-release - name: package-version-to-git-release
continue-on-error: true # Will fail if tag exists continue-on-error: true # Will fail if tag exists
id: create_release id: create_release
@ -391,3 +276,101 @@ jobs:
body_path: ./CHANGELOG.md body_path: ./CHANGELOG.md
draft: false draft: false
prerelease: 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"

View File

@ -168,6 +168,8 @@ jobs:
name: Unit tests - backend name: Unit tests - backend
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build_test_neo4j,build_test_backend] needs: [build_test_neo4j,build_test_backend]
permissions:
checks: write
steps: steps:
########################################################################## ##########################################################################
# CHECKOUT CODE ########################################################## # CHECKOUT CODE ##########################################################
@ -225,6 +227,8 @@ jobs:
name: Unit tests - webapp name: Unit tests - webapp
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build_test_webapp] needs: [build_test_webapp]
permissions:
checks: write
steps: steps:
########################################################################## ##########################################################################
# CHECKOUT CODE ########################################################## # CHECKOUT CODE ##########################################################
@ -269,7 +273,7 @@ jobs:
report_name: Coverage Webapp report_name: Coverage Webapp
type: lcov type: lcov
result_path: ./coverage/lcov.info result_path: ./coverage/lcov.info
min_coverage: 63 min_coverage: 83
token: ${{ github.token }} token: ${{ github.token }}
############################################################################## ##############################################################################

View File

@ -174,34 +174,14 @@ Please copy and paste the following quotes for the languages:
#### Environment Variable For Apple M1 Platform #### 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 ```bash
# set env variable for your shell # set env variable for your shell
$ export DOCKER_DEFAULT_PLATFORM=linux/amd64 $ export DOCKER_DEFAULT_PLATFORM=linux/amd64
``` ```
#### Docker Compose Override File For Apple M1 Platform Or alternatively use a `YAML` docker compose overwrite file with `platform: linux/amd64`.
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"
```
### Analyzing Docker Builds ### Analyzing Docker Builds

View File

@ -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. 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
<img src="https://user-images.githubusercontent.com/17728384/218597429-554e4082-3906-4721-8f68-0c13146fc218.png" alt="Post feed" title="Post feed" />
Check out more screenshots [here](/wiki/en:Screenshots).
## Features
Ocelot.social networks feature:
* <strong>user accounts</strong>
* <strong>user roles</strong>
* <strong>posts</strong> and <strong>comments</strong>
* <strong>groups</strong>
* <strong>maps</strong>
* <strong>search</strong>
* <strong>filters</strong>
* 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: 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) [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: 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. 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. 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`: Clone the repository, this will create a new folder called `Ocelot-Social`:
@ -65,7 +124,7 @@ Change into the new folder.
$ cd Ocelot-Social $ cd Ocelot-Social
``` ```
## Live Demo And Developer Logins ### Live Demo And Developer Logins
**Try out our deployed [development environment](https://stage.ocelot.social).** **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) * central staging network: [stage.ocelot.social](https://stage.ocelot.social)
<!-- - rebranded staging network: [rebrand.ocelot.social](https://stage.ocelot.social). --> <!-- - rebranded staging network: [rebrand.ocelot.social](https://stage.ocelot.social). -->
### Login #### Login
Logins for the live demos and developers (local developers after the following installations) in the browser: 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 | | `moderator@example.org` | 1234 | moderator |
| `admin@example.org` | 1234 | admin | | `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. 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. 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 docker-compose version 1.23.2
``` ```
#### Start Ocelot-Social via Docker-Compose ##### Start Ocelot-Social via Docker-Compose
Prepare ENVs once beforehand: 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 closer description see [backend README.md](./backend/README.md).
For a full documentation see [SUMMARY](./SUMMARY.md). For a full documentation see [SUMMARY](./SUMMARY.md).
### Local Installation #### Local Installation
For a full documentation see [SUMMARY](./SUMMARY.md). 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. 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. 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! 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: 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: 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 We give write permissions to every developer who asks for it. Just text us on
[Discord](https://discord.gg/AJSX9DCSUA). [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. 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`. The only deployment method in this repository for development purposes as described above is `docker-compose`.
## Technology Stack ### Technology Stack
* [VueJS](https://vuejs.org/) * [VueJS](https://vuejs.org/)
* [NuxtJS](https://nuxtjs.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/) * [NodeJS](https://nodejs.org/en/)
* [Neo4J](https://neo4j.com/) * [Neo4J](https://neo4j.com/)
### For Testing #### For Testing
* [Cypress](https://docs.cypress.io/) * [Cypress](https://docs.cypress.io/)
* [Storybook](https://storybook.js.org/) * [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/) * [Vue Test Utils](https://vue-test-utils.vuejs.org/)
* [ESLint](https://eslint.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/). 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/)
<img alt="BrowserStack Logo" src=".gitbook/assets/browserstack-logo.svg" width="256"> <img alt="BrowserStack Logo" src=".gitbook/assets/browserstack-logo.svg" width="256">
## License ### License
See the [LICENSE](LICENSE.md) file for license rights and limitations (MIT). See the [LICENSE](LICENSE.md) file for license rights and limitations (MIT).

View File

@ -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) ![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: backend is running:
{% tabs %} {% tabs %}
@ -90,6 +90,12 @@ backend is running:
```bash ```bash
# in main folder while docker-compose is running # in main folder while docker-compose is running
$ docker exec backend yarn run db:migrate init $ 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 %} {% endtab %}
@ -126,7 +132,7 @@ To reset the database run:
$ docker exec backend yarn run db:reset $ docker exec backend yarn run db:reset
# you could also wipe out your neo4j database and delete all volumes with: # you could also wipe out your neo4j database and delete all volumes with:
$ docker-compose down -v $ 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 $ docker exec backend yarn run db:migrate init
``` ```

View File

@ -65,7 +65,6 @@ Factory.define('basicUser')
name: faker.name.findName, name: faker.name.findName,
password: '1234', password: '1234',
role: 'user', role: 'user',
about: faker.lorem.paragraph,
termsAndConditionsAgreedVersion: '0.0.1', termsAndConditionsAgreedVersion: '0.0.1',
termsAndConditionsAgreedAt: '2019-08-01T10:47:19.212Z', termsAndConditionsAgreedAt: '2019-08-01T10:47:19.212Z',
allowEmbedIframes: false, allowEmbedIframes: false,
@ -82,12 +81,28 @@ Factory.define('basicUser')
Factory.define('userWithoutEmailAddress') Factory.define('userWithoutEmailAddress')
.extend('basicUser') .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) => { .after(async (buildObject, options) => {
return neode.create('User', buildObject) return neode.create('User', buildObject)
}) })
Factory.define('user') Factory.define('user')
.extend('basicUser') .extend('basicUser')
.option('about', faker.lorem.paragraph)
.option('email', faker.internet.exampleEmail) .option('email', faker.internet.exampleEmail)
.option('avatar', () => .option('avatar', () =>
Factory.build('image', { Factory.build('image', {

View File

@ -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 // ------ queries
export const groupQuery = () => { export const groupQuery = () => {

View File

@ -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 // 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 // or we wait a bit to check if we missed to set an await somewhere
// see: https://www.sitepoint.com/delay-sleep-pause-wait/ // see: https://www.sitepoint.com/delay-sleep-pause-wait/

View File

@ -1,4 +1,4 @@
import { gql } from '../../helpers/jest' import gql from 'graphql-tag'
import { cleanDatabase } from '../../db/factories' import { cleanDatabase } from '../../db/factories'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import { getNeode, getDriver } from '../../db/neo4j' import { getNeode, getDriver } from '../../db/neo4j'

View File

@ -1,5 +1,5 @@
import Factory, { cleanDatabase } from '../../db/factories' import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest' import gql from 'graphql-tag'
import { getNeode, getDriver } from '../../db/neo4j' import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server' import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'

View File

@ -1,4 +1,4 @@
import { gql } from '../../helpers/jest' import gql from 'graphql-tag'
import { cleanDatabase } from '../../db/factories' import { cleanDatabase } from '../../db/factories'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import { getNeode, getDriver } from '../../db/neo4j' import { getNeode, getDriver } from '../../db/neo4j'

View File

@ -1,4 +1,4 @@
import { gql } from '../helpers/jest' import gql from 'graphql-tag'
import { cleanDatabase } from '../db/factories' import { cleanDatabase } from '../db/factories'
import { getNeode, getDriver } from '../db/neo4j' import { getNeode, getDriver } from '../db/neo4j'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'

View File

@ -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({ const canCommentPost = rule({
cache: 'no_cache', cache: 'no_cache',
})(async (_parent, args, { user, driver }) => { })(async (_parent, args, { user, driver }) => {
@ -382,6 +418,7 @@ export default shield(
JoinGroup: isAllowedToJoinGroup, JoinGroup: isAllowedToJoinGroup,
LeaveGroup: isAllowedToLeaveGroup, LeaveGroup: isAllowedToLeaveGroup,
ChangeGroupMemberRole: isAllowedToChangeGroupMemberRole, ChangeGroupMemberRole: isAllowedToChangeGroupMemberRole,
RemoveUserFromGroup: canRemoveUserFromGroup,
CreatePost: and(isAuthenticated, isMemberOfGroup), CreatePost: and(isAuthenticated, isMemberOfGroup),
UpdatePost: isAuthor, UpdatePost: isAuthor,
DeletePost: isAuthor, DeletePost: isAuthor,

View File

@ -1,7 +1,7 @@
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import createServer from '../server' import createServer from '../server'
import Factory, { cleanDatabase } from '../db/factories' import Factory, { cleanDatabase } from '../db/factories'
import { gql } from '../helpers/jest' import gql from 'graphql-tag'
import { getDriver, getNeode } from '../db/neo4j' import { getDriver, getNeode } from '../db/neo4j'
import CONFIG from '../config' import CONFIG from '../config'

View File

@ -1,5 +1,5 @@
import Factory, { cleanDatabase } from '../../db/factories' import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest' import gql from 'graphql-tag'
import { getNeode, getDriver } from '../../db/neo4j' import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server' import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'

View File

@ -1,5 +1,5 @@
import Factory, { cleanDatabase } from '../db/factories' import Factory, { cleanDatabase } from '../db/factories'
import { gql } from '../helpers/jest' import gql from 'graphql-tag'
import { getNeode, getDriver } from '../db/neo4j' import { getNeode, getDriver } from '../db/neo4j'
import createServer from '../server' import createServer from '../server'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'

View File

@ -1,4 +1,4 @@
import { gql } from '../../helpers/jest' import gql from 'graphql-tag'
import Factory, { cleanDatabase } from '../../db/factories' import Factory, { cleanDatabase } from '../../db/factories'
import { getNeode, getDriver } from '../../db/neo4j' import { getNeode, getDriver } from '../../db/neo4j'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'

View File

@ -1,5 +1,5 @@
import Factory, { cleanDatabase } from '../../db/factories' import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest' import gql from 'graphql-tag'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import createServer from '../../server' import createServer from '../../server'
import { getNeode, getDriver } from '../../db/neo4j' import { getNeode, getDriver } from '../../db/neo4j'

View File

@ -1,6 +1,6 @@
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import Factory, { cleanDatabase } from '../../db/factories' import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest' import gql from 'graphql-tag'
import { getNeode, getDriver } from '../../db/neo4j' import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server' import createServer from '../../server'

View File

@ -1,5 +1,5 @@
import Factory, { cleanDatabase } from '../../db/factories' import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest' import gql from 'graphql-tag'
import { getDriver, getNeode } from '../../db/neo4j' import { getDriver, getNeode } from '../../db/neo4j'
import createServer from '../../server' import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'

View File

@ -3,7 +3,7 @@ import fs from 'fs'
import path from 'path' import path from 'path'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import createServer from '../../server' import createServer from '../../server'
import { gql } from '../../helpers/jest' import gql from 'graphql-tag'
jest.mock('node-fetch') jest.mock('node-fetch')
const { Response } = jest.requireActual('node-fetch') const { Response } = jest.requireActual('node-fetch')

View File

@ -2,7 +2,7 @@ import { createTestClient } from 'apollo-server-testing'
import Factory, { cleanDatabase } from '../../db/factories' import Factory, { cleanDatabase } from '../../db/factories'
import { getDriver, getNeode } from '../../db/neo4j' import { getDriver, getNeode } from '../../db/neo4j'
import createServer from '../../server' import createServer from '../../server'
import { gql } from '../../helpers/jest' import gql from 'graphql-tag'
const driver = getDriver() const driver = getDriver()
const neode = getNeode() const neode = getNeode()

View File

@ -295,25 +295,8 @@ export default {
LeaveGroup: async (_parent, params, context, _resolveInfo) => { LeaveGroup: async (_parent, params, context, _resolveInfo) => {
const { groupId, userId } = params const { groupId, userId } = params
const session = context.driver.session() 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 { try {
return await writeTxResultPromise return await removeUserFromGroupWriteTxResultPromise(session, groupId, userId)
} catch (error) { } catch (error) {
throw new Error(error) throw new Error(error)
} finally { } finally {
@ -368,6 +351,17 @@ export default {
session.close() 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: { Group: {
...Resolver('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
})
}

View File

@ -6,6 +6,7 @@ import {
joinGroupMutation, joinGroupMutation,
leaveGroupMutation, leaveGroupMutation,
changeGroupMemberRoleMutation, changeGroupMemberRoleMutation,
removeUserFromGroupMutation,
groupMembersQuery, groupMembersQuery,
groupQuery, groupQuery,
} from '../../graphql/groups' } from '../../graphql/groups'
@ -196,7 +197,6 @@ const seedComplexScenarioAndClearAuthentication = async () => {
}, },
}) })
// hidden-group // hidden-group
authenticatedUser = await adminMemberUser.toJson()
await mutate({ await mutate({
mutation: createGroupMutation(), mutation: createGroupMutation(),
variables: { variables: {
@ -214,32 +214,17 @@ const seedComplexScenarioAndClearAuthentication = async () => {
mutation: changeGroupMemberRoleMutation(), mutation: changeGroupMemberRoleMutation(),
variables: { variables: {
groupId: 'hidden-group', groupId: 'hidden-group',
userId: 'admin-member-user', userId: 'usual-member-user',
roleInGroup: 'usual',
},
})
await mutate({
mutation: changeGroupMemberRoleMutation(),
variables: {
groupId: 'hidden-group',
userId: 'second-owner-member-user',
roleInGroup: 'usual', roleInGroup: 'usual',
}, },
}) })
await mutate({ await mutate({
mutation: changeGroupMemberRoleMutation(), mutation: changeGroupMemberRoleMutation(),
variables: { variables: {
groupId: 'hidden-group', groupId: 'hidden-group',
userId: 'admin-member-user', userId: 'admin-member-user',
roleInGroup: 'usual', roleInGroup: 'admin',
},
})
await mutate({
mutation: changeGroupMemberRoleMutation(),
variables: {
groupId: 'hidden-group',
userId: 'second-owner-member-user',
roleInGroup: 'usual',
}, },
}) })
@ -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!',
}),
]),
})
})
*/
})
})
})
}) })

View File

@ -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
}

View File

@ -1,6 +1,6 @@
import Factory, { cleanDatabase } from '../../db/factories' import Factory, { cleanDatabase } from '../../db/factories'
import { getDriver } from '../../db/neo4j' import { getDriver } from '../../db/neo4j'
import { gql } from '../../helpers/jest' import gql from 'graphql-tag'
import createServer from '../../server' import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import CONSTANTS_REGISTRATION from './../../constants/registration' import CONSTANTS_REGISTRATION from './../../constants/registration'

View File

@ -1,5 +1,5 @@
import Factory, { cleanDatabase } from '../../db/factories' import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest' import gql from 'graphql-tag'
import { getNeode, getDriver } from '../../db/neo4j' import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server' import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'

View File

@ -1,6 +1,6 @@
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import Factory, { cleanDatabase } from '../../db/factories' import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest' import gql from 'graphql-tag'
import { getNeode, getDriver } from '../../db/neo4j' import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server' import createServer from '../../server'

View File

@ -1,5 +1,5 @@
import Factory, { cleanDatabase } from '../../db/factories' import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest' import gql from 'graphql-tag'
import { getDriver } from '../../db/neo4j' import { getDriver } from '../../db/neo4j'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import createServer from '../.././server' import createServer from '../.././server'

View File

@ -1,5 +1,5 @@
import Factory, { cleanDatabase } from '../../db/factories' import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest' import gql from 'graphql-tag'
import { getNeode, getDriver } from '../../db/neo4j' import { getNeode, getDriver } from '../../db/neo4j'
import CONSTANTS_REGISTRATION from './../../constants/registration' import CONSTANTS_REGISTRATION from './../../constants/registration'
import createPasswordReset from './helpers/createPasswordReset' import createPasswordReset from './helpers/createPasswordReset'

View File

@ -6,6 +6,7 @@ import { mergeImage, deleteImage } from './images/images'
import Resolver from './helpers/Resolver' import Resolver from './helpers/Resolver'
import { filterForMutedUsers } from './helpers/filterForMutedUsers' import { filterForMutedUsers } from './helpers/filterForMutedUsers'
import { filterInvisiblePosts } from './helpers/filterInvisiblePosts' import { filterInvisiblePosts } from './helpers/filterInvisiblePosts'
import { filterPostsOfMyGroups } from './helpers/filterPostsOfMyGroups'
import CONFIG from '../../config' import CONFIG from '../../config'
const maintainPinnedPosts = (params) => { const maintainPinnedPosts = (params) => {
@ -21,12 +22,14 @@ const maintainPinnedPosts = (params) => {
export default { export default {
Query: { Query: {
Post: async (object, params, context, resolveInfo) => { Post: async (object, params, context, resolveInfo) => {
params = await filterPostsOfMyGroups(params, context)
params = await filterInvisiblePosts(params, context) params = await filterInvisiblePosts(params, context)
params = await filterForMutedUsers(params, context) params = await filterForMutedUsers(params, context)
params = await maintainPinnedPosts(params) params = await maintainPinnedPosts(params)
return neo4jgraphql(object, params, context, resolveInfo) return neo4jgraphql(object, params, context, resolveInfo)
}, },
profilePagePosts: async (object, params, context, resolveInfo) => { profilePagePosts: async (object, params, context, resolveInfo) => {
params = await filterPostsOfMyGroups(params, context)
params = await filterInvisiblePosts(params, context) params = await filterInvisiblePosts(params, context)
params = await filterForMutedUsers(params, context) params = await filterForMutedUsers(params, context)
return neo4jgraphql(object, params, context, resolveInfo) return neo4jgraphql(object, params, context, resolveInfo)

View File

@ -1,6 +1,6 @@
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import Factory, { cleanDatabase } from '../../db/factories' import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest' import gql from 'graphql-tag'
import { getNeode, getDriver } from '../../db/neo4j' import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server' import createServer from '../../server'

View File

@ -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: {} }) const result = await query({ query: filterPosts(), variables: {} })
expect(result.data.Post).toHaveLength(3) expect(result.data.Post).toHaveLength(4)
expect(result).toMatchObject({ expect(result).toMatchObject({
data: { data: {
Post: expect.arrayContaining([ Post: expect.arrayContaining([
@ -1540,6 +1540,11 @@ describe('Posts in Groups', () => {
title: 'A post without a group', title: 'A post without a group',
content: 'I am a user who does not belong to a group yet.', 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', id: 'post-to-hidden-group',
title: 'A post to a 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: {} }) const result = await query({ query: filterPosts(), variables: {} })
expect(result.data.Post).toHaveLength(2) expect(result.data.Post).toHaveLength(4)
expect(result).toMatchObject({ expect(result).toMatchObject({
data: { data: {
Post: expect.arrayContaining([ Post: expect.arrayContaining([
@ -1580,6 +1585,16 @@ describe('Posts in Groups', () => {
title: 'A post without a group', title: 'A post without a group',
content: 'I am a user who does not belong to a group yet.', 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, errors: undefined,
@ -1603,9 +1618,9 @@ describe('Posts in Groups', () => {
authenticatedUser = await allGroupsUser.toJson() 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: {} }) const result = await query({ query: filterPosts(), variables: {} })
expect(result.data.Post).toHaveLength(3) expect(result.data.Post).toHaveLength(4)
expect(result).toMatchObject({ expect(result).toMatchObject({
data: { data: {
Post: expect.arrayContaining([ Post: expect.arrayContaining([
@ -1624,6 +1639,11 @@ describe('Posts in Groups', () => {
title: 'A post to a closed group', title: 'A post to a closed group',
content: 'I am posting into a closed group as a member of the 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, 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,
})
})
})
})
}) })
}) })

View File

@ -1,5 +1,5 @@
import Factory, { cleanDatabase } from '../../db/factories' import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest' import gql from 'graphql-tag'
import { getDriver, getNeode } from '../../db/neo4j' import { getDriver, getNeode } from '../../db/neo4j'
import createServer from '../../server' import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'

View File

@ -1,7 +1,7 @@
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import createServer from '../.././server' import createServer from '../.././server'
import Factory, { cleanDatabase } from '../../db/factories' import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest' import gql from 'graphql-tag'
import { getDriver, getNeode } from '../../db/neo4j' import { getDriver, getNeode } from '../../db/neo4j'
const instance = getNeode() const instance = getNeode()

View File

@ -1,6 +1,6 @@
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import Factory, { cleanDatabase } from '../../db/factories' import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest' import gql from 'graphql-tag'
import { getNeode, getDriver } from '../../db/neo4j' import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server' import createServer from '../../server'

View File

@ -1,5 +1,5 @@
import Factory, { cleanDatabase } from '../../db/factories' import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest' import gql from 'graphql-tag'
import { getNeode, getDriver } from '../../db/neo4j' import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server' import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'

View File

@ -1,6 +1,6 @@
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import Factory, { cleanDatabase } from '../../db/factories' import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest' import gql from 'graphql-tag'
import { getNeode, getDriver } from '../../db/neo4j' import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server' import createServer from '../../server'

View File

@ -1,7 +1,7 @@
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import createServer from '../../server' import createServer from '../../server'
import Factory, { cleanDatabase } from '../../db/factories' import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest' import gql from 'graphql-tag'
import { getDriver } from '../../db/neo4j' import { getDriver } from '../../db/neo4j'
const driver = getDriver() const driver = getDriver()

View File

@ -1,6 +1,6 @@
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import Factory, { cleanDatabase } from '../../db/factories' import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest' import gql from 'graphql-tag'
import { getNeode, getDriver } from '../../db/neo4j' import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server' import createServer from '../../server'

View File

@ -1,5 +1,5 @@
import Factory, { cleanDatabase } from '../../db/factories' import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest' import gql from 'graphql-tag'
import { getNeode, getDriver } from '../../db/neo4j' import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server' import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'

View File

@ -1,7 +1,7 @@
import jwt from 'jsonwebtoken' import jwt from 'jsonwebtoken'
import CONFIG from './../../config' import CONFIG from './../../config'
import Factory, { cleanDatabase } from '../../db/factories' import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest' import gql from 'graphql-tag'
import { loginMutation } from '../../graphql/userManagement' import { loginMutation } from '../../graphql/userManagement'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import createServer, { context } from '../../server' import createServer, { context } from '../../server'

View File

@ -1,5 +1,5 @@
import Factory, { cleanDatabase } from '../../db/factories' import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest' import gql from 'graphql-tag'
import { getNeode, getDriver } from '../../db/neo4j' import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server' import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'

View File

@ -1,4 +1,4 @@
import { gql } from '../../../helpers/jest' import gql from 'graphql-tag'
import Factory, { cleanDatabase } from '../../../db/factories' import Factory, { cleanDatabase } from '../../../db/factories'
import { getNeode, getDriver } from '../../../db/neo4j' import { getNeode, getDriver } from '../../../db/neo4j'
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'

View File

@ -1,7 +1,7 @@
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import createServer from '../../../server' import createServer from '../../../server'
import { cleanDatabase } from '../../../db/factories' import { cleanDatabase } from '../../../db/factories'
import { gql } from '../../../helpers/jest' import gql from 'graphql-tag'
import { getNeode, getDriver } from '../../../db/neo4j' import { getNeode, getDriver } from '../../../db/neo4j'
const driver = getDriver() const driver = getDriver()

View File

@ -1,6 +1,6 @@
import { createTestClient } from 'apollo-server-testing' import { createTestClient } from 'apollo-server-testing'
import Factory, { cleanDatabase } from '../../db/factories' import Factory, { cleanDatabase } from '../../db/factories'
import { gql } from '../../helpers/jest' import gql from 'graphql-tag'
import { getNeode, getDriver } from '../../db/neo4j' import { getNeode, getDriver } from '../../db/neo4j'
import createServer from '../../server' import createServer from '../../server'

View File

@ -132,4 +132,9 @@ type Mutation {
userId: ID! userId: ID!
roleInGroup: GroupMemberRole! roleInGroup: GroupMemberRole!
): User ): User
RemoveUserFromGroup(
groupId: ID!
userId: ID!
): User
} }

View File

@ -82,6 +82,7 @@ input _PostFilter {
emotions_single: _PostEMOTEDFilter emotions_single: _PostEMOTEDFilter
emotions_every: _PostEMOTEDFilter emotions_every: _PostEMOTEDFilter
group: _GroupFilter group: _GroupFilter
postsInMyGroups: Boolean
} }
enum _PostOrdering { enum _PostOrdering {

View File

@ -1,5 +1,4 @@
import { Given } from "cypress-cucumber-preprocessor/steps"; import { Given } from "cypress-cucumber-preprocessor/steps";
import { gql } from '../../../backend/src/helpers/jest'
Given('somebody reported the following posts:', table => { Given('somebody reported the following posts:', table => {
table.hashes().forEach(({ submitterEmail, resourceId, reasonCategory, reasonDescription }) => { table.hashes().forEach(({ submitterEmail, resourceId, reasonCategory, reasonDescription }) => {
@ -10,7 +9,7 @@ Given('somebody reported the following posts:', table => {
cy.factory() cy.factory()
.build('user', {}, submitter) .build('user', {}, submitter)
.authenticateAs(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) { fileReport(resourceId: $resourceId, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription) {
reportId reportId
} }

View File

@ -15,11 +15,10 @@
/* globals Cypress cy */ /* globals Cypress cy */
import "cypress-file-upload"; import "cypress-file-upload";
import { GraphQLClient, request } from 'graphql-request' import { GraphQLClient, request } from 'graphql-request'
import { gql } from '../../backend/src/helpers/jest'
import config from '../../backend/src/config' import config from '../../backend/src/config'
const authenticatedHeaders = (variables) => { const authenticatedHeaders = (variables) => {
const mutation = gql` const mutation = `
mutation($email: String!, $password: String!) { mutation($email: String!, $password: String!) {
login(email: $email, password: $password) login(email: $email, password: $password)
} }

View File

@ -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).

View File

@ -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

View File

@ -7,6 +7,7 @@ services:
dockerfile: ../webapp/Dockerfile dockerfile: ../webapp/Dockerfile
context: ../webapp context: ../webapp
target: base target: base
command: sleep 0
webapp-code: webapp-code:
image: ocelotsocialnetwork/webapp:local-code image: ocelotsocialnetwork/webapp:local-code
@ -14,10 +15,11 @@ services:
dockerfile: ../webapp/Dockerfile dockerfile: ../webapp/Dockerfile
context: ../webapp context: ../webapp
target: code target: code
command: sleep 0
webapp: webapp:
image: ocelotsocialnetwork/webapp-branded:local-${CONFIGURATION} image: ocelotsocialnetwork/webapp-branded:local-${CONFIGURATION}
container_name: webapp container_name: webapp-branded
build: build:
dockerfile: src/docker/webapp.Dockerfile dockerfile: src/docker/webapp.Dockerfile
target: branded target: branded
@ -51,6 +53,7 @@ services:
dockerfile: ../backend/Dockerfile dockerfile: ../backend/Dockerfile
context: ../backend context: ../backend
target: base target: base
command: sleep 0
backend-code: backend-code:
image: ocelotsocialnetwork/backend:local-code image: ocelotsocialnetwork/backend:local-code
@ -58,13 +61,14 @@ services:
dockerfile: ../backend/Dockerfile dockerfile: ../backend/Dockerfile
context: ../backend context: ../backend
target: code target: code
command: sleep 0
backend: backend:
image: ocelotsocialnetwork/backend-branded:local-${CONFIGURATION} image: ocelotsocialnetwork/backend-branded:local-${CONFIGURATION}
container_name: backend container_name: backend
build: build:
dockerfile: src/docker/backend.Dockerfile dockerfile: src/docker/backend.Dockerfile
target: branded target: branded-branded
context: . context: .
args: args:
- CONFIGURATION=$CONFIGURATION - CONFIGURATION=$CONFIGURATION
@ -106,6 +110,7 @@ services:
dockerfile: ../webapp/Dockerfile.maintenance dockerfile: ../webapp/Dockerfile.maintenance
context: ../webapp context: ../webapp
target: base target: base
command: sleep 0
maintenance-code: maintenance-code:
image: ocelotsocialnetwork/maintenance:local-code image: ocelotsocialnetwork/maintenance:local-code
@ -113,11 +118,12 @@ services:
dockerfile: ../webapp/Dockerfile.maintenance dockerfile: ../webapp/Dockerfile.maintenance
context: ../webapp context: ../webapp
target: code target: code
command: sleep 0
maintenance: 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 # 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} image: ocelotsocialnetwork/maintenance-branded:local-${CONFIGURATION}
container_name: maintenance container_name: maintenance-branded
build: build:
# TODO: Separate from webapp, this must be independent # TODO: Separate from webapp, this must be independent
dockerfile: src/docker/maintenance.Dockerfile dockerfile: src/docker/maintenance.Dockerfile
@ -152,7 +158,7 @@ services:
mailserver: mailserver:
image: djfarrelly/maildev image: djfarrelly/maildev
container_name: mailserver container_name: mailserver-branded
ports: ports:
- 1080:80 - 1080:80
networks: networks:

View File

@ -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

View File

@ -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

View File

@ -14,7 +14,12 @@ ARG CONFIGURATION=example
# copy public constants into the Docker image to brand it # copy public constants into the Docker image to brand it
COPY configurations/${CONFIGURATION}/branding/static/ static/ COPY configurations/${CONFIGURATION}/branding/static/ static/
COPY configurations/${CONFIGURATION}/branding/constants/ constants/ 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 ########################################################################## # BUILD ##########################################################################

View File

@ -12,17 +12,16 @@ FROM $APP_IMAGE_CODE as code
ARG CONFIGURATION=example ARG CONFIGURATION=example
# copy public constants into the Docker image to brand it # copy public constants into the Docker image to brand it
COPY src/tools/ tools/
COPY configurations/${CONFIGURATION}/branding/static/ static/ COPY configurations/${CONFIGURATION}/branding/static/ static/
COPY configurations/${CONFIGURATION}/branding/constants/ constants/ COPY configurations/${CONFIGURATION}/branding/constants/ constants/
COPY configurations/${CONFIGURATION}/branding/locales/html/ locales/html/ 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/styles/imports/ assets/styles/imports/
COPY configurations/${CONFIGURATION}/branding/assets/fonts/ assets/fonts/ 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 apk add --no-cache bash jq
RUN tools/merge-locales.sh RUN tools/merge-locales.sh
################################################################################## ##################################################################################

View File

@ -0,0 +1,12 @@
spec:
rules:
- host:
http:
paths:
- path: /
pathType: ImplementationSpecific
backend:
service:
name: ocelot-webapp
port:
number: 3000

View File

@ -0,0 +1,12 @@
spec:
rules:
- host:
http:
paths:
- path: /
pathType: ImplementationSpecific
backend:
service:
name: ocelot-maintenance
port:
number: 80

View File

@ -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

View File

@ -10,8 +10,8 @@ between different local Node versions:
```bash ```bash
# install Node # install Node
$ cd webapp $ cd webapp
$ nvm install v16.19.0 $ nvm install v19.4.0
$ nvm use v16.19.0 $ nvm use v19.4.0
``` ```
Install node dependencies with [yarn](https://yarnpkg.com/en/): Install node dependencies with [yarn](https://yarnpkg.com/en/):

View File

@ -376,3 +376,20 @@ $color-footer-link: $color-primary;
*/ */
$color-locale-menu: $text-color-soft; $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;

View File

@ -11,22 +11,22 @@
} }
.iziToast.iziToast-color-red { .iziToast.iziToast-color-red {
background: $color-danger !important; background: $color-toast-red !important;
border-color: $color-danger !important; border-color: $color-toast-red !important;
} }
.iziToast.iziToast-color-orange { .iziToast.iziToast-color-orange {
background: $color-warning !important; background: $color-toast-orange !important;
border-color: $color-warning !important; border-color: $color-toast-orange !important;
} }
.iziToast.iziToast-color-yellow { .iziToast.iziToast-color-yellow {
background: $color-yellow !important; background: $color-toast-yellow !important;
border-color: $color-yellow !important; border-color: $color-toast-yellow !important;
} }
.iziToast.iziToast-color-blue { .iziToast.iziToast-color-blue {
background: $color-secondary !important; background: $color-toast-blue !important;
border-color: $color-secondary !important; border-color: $color-toast-blue !important;
} }
.iziToast.iziToast-color-green { .iziToast.iziToast-color-green {
background: $color-success !important; background: $color-toast-green !important;
border-color: $color-success !important; border-color: $color-toast-green !important;
} }

View File

@ -1,11 +1,13 @@
import { config, mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import Vuex from 'vuex' import Vuex from 'vuex'
import AvatarMenu from './AvatarMenu.vue' import AvatarMenu from './AvatarMenu.vue'
const localVue = global.localVue const localVue = global.localVue
config.stubs['nuxt-link'] = '<span><slot /></span>' const stubs = {
config.stubs['router-link'] = '<span><slot /></span>' 'nuxt-link': true,
'router-link': true,
}
describe('AvatarMenu.vue', () => { describe('AvatarMenu.vue', () => {
let propsData, getters, wrapper, mocks let propsData, getters, wrapper, mocks
@ -34,7 +36,7 @@ describe('AvatarMenu.vue', () => {
const store = new Vuex.Store({ const store = new Vuex.Store({
getters, getters,
}) })
return mount(AvatarMenu, { propsData, localVue, store, mocks }) return mount(AvatarMenu, { propsData, localVue, store, mocks, stubs })
} }
describe('mount', () => { describe('mount', () => {

View File

@ -14,7 +14,7 @@ describe('Badges.vue', () => {
} }
it('has class "hc-badges"', () => { it('has class "hc-badges"', () => {
expect(Wrapper().contains('.hc-badges')).toBe(true) expect(Wrapper().find('.hc-badges').exists()).toBe(true)
}) })
describe('given a badge', () => { describe('given a badge', () => {
@ -23,7 +23,7 @@ describe('Badges.vue', () => {
}) })
it('proxies badge icon, which is just a URL without metadata', () => { 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)
}) })
}) })
}) })

View File

@ -1,4 +1,4 @@
import { shallowMount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import Category from './index' import Category from './index'
@ -9,7 +9,7 @@ describe('Category', () => {
let name let name
const Wrapper = () => { const Wrapper = () => {
return shallowMount(Category, { return mount(Category, {
localVue, localVue,
propsData: { propsData: {
icon, icon,
@ -27,8 +27,9 @@ describe('Category', () => {
it('shows Name', () => { it('shows Name', () => {
expect(Wrapper().text()).toContain('Peter') 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)
}) })
}) })
}) })

View File

@ -1,13 +1,10 @@
import { config, mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import CommentCard from './CommentCard.vue' import CommentCard from './CommentCard.vue'
import Vuex from 'vuex' import Vuex from 'vuex'
const localVue = global.localVue const localVue = global.localVue
localVue.directive('scrollTo', jest.fn()) localVue.directive('scrollTo', jest.fn())
config.stubs['client-only'] = '<span><slot /></span>'
config.stubs['nuxt-link'] = '<span><slot /></span>'
describe('CommentCard.vue', () => { describe('CommentCard.vue', () => {
let propsData, mocks, stubs, getters, wrapper, Wrapper let propsData, mocks, stubs, getters, wrapper, Wrapper
@ -46,6 +43,8 @@ describe('CommentCard.vue', () => {
} }
stubs = { stubs = {
ContentViewer: true, ContentViewer: true,
'client-only': true,
'nuxt-link': true,
} }
getters = { getters = {
'auth/user': () => { 'auth/user': () => {
@ -56,7 +55,9 @@ describe('CommentCard.vue', () => {
}) })
describe('mount', () => { describe('mount', () => {
beforeEach(jest.useFakeTimers) beforeEach(() => {
jest.useFakeTimers()
})
Wrapper = () => { Wrapper = () => {
const store = new Vuex.Store({ const store = new Vuex.Store({

View File

@ -1,4 +1,4 @@
import { config, mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import CommentList from './CommentList' import CommentList from './CommentList'
import Vuex from 'vuex' import Vuex from 'vuex'
import Vue from 'vue' import Vue from 'vue'
@ -8,10 +8,6 @@ const localVue = global.localVue
localVue.filter('truncate', (string) => string) localVue.filter('truncate', (string) => string)
localVue.directive('scrollTo', jest.fn()) localVue.directive('scrollTo', jest.fn())
config.stubs['v-popover'] = '<span><slot /></span>'
config.stubs['nuxt-link'] = '<span><slot /></span>'
config.stubs['client-only'] = '<span><slot /></span>'
describe('CommentList.vue', () => { describe('CommentList.vue', () => {
let mocks, store, wrapper, propsData, stubs let mocks, store, wrapper, propsData, stubs
@ -72,7 +68,10 @@ describe('CommentList.vue', () => {
}, },
} }
stubs = { stubs = {
EditorContent: "<div class='stub'></div>", EditorContent: true,
'v-popover': true,
'nuxt-link': true,
'client-only': true,
} }
}) })
@ -92,7 +91,9 @@ describe('CommentList.vue', () => {
}) })
describe('scrollToAnchor mixin', () => { describe('scrollToAnchor mixin', () => {
beforeEach(jest.useFakeTimers) beforeEach(() => {
jest.useFakeTimers()
})
describe('$route.hash !== `#comments`', () => { describe('$route.hash !== `#comments`', () => {
it('skips $scrollTo', () => { it('skips $scrollTo', () => {

View File

@ -52,7 +52,7 @@ describe('ComponentSlider.vue', () => {
}) })
it('renders', () => { it('renders', () => {
expect(wrapper.is('div')).toBe(true) expect(wrapper.element.tagName).toBe('DIV')
}) })
it('click on next Button', async () => { it('click on next Button', async () => {

View File

@ -1,4 +1,4 @@
import { config, mount, createLocalVue } from '@vue/test-utils' import { mount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex' import Vuex from 'vuex'
import VTooltip from 'v-tooltip' import VTooltip from 'v-tooltip'
import Styleguide from '@human-connection/styleguide' import Styleguide from '@human-connection/styleguide'
@ -10,7 +10,11 @@ localVue.use(Styleguide)
localVue.use(VTooltip) localVue.use(VTooltip)
localVue.use(Vuex) localVue.use(Vuex)
config.stubs['router-link'] = '<span><slot /></span>' const stubs = {
'router-link': {
template: '<span><slot /></span>',
},
}
let getters, mutations, mocks, menuToggle, openModalSpy let getters, mutations, mocks, menuToggle, openModalSpy
@ -36,7 +40,7 @@ describe('ContentMenu.vue', () => {
'auth/isAdmin': () => false, 'auth/isAdmin': () => false,
} }
const openContentMenu = (values = {}) => { const openContentMenu = async (values = {}) => {
const store = new Vuex.Store({ mutations, getters }) const store = new Vuex.Store({ mutations, getters })
const wrapper = mount(ContentMenu, { const wrapper = mount(ContentMenu, {
propsData: { propsData: {
@ -45,16 +49,17 @@ describe('ContentMenu.vue', () => {
mocks, mocks,
store, store,
localVue, localVue,
stubs,
}) })
menuToggle = wrapper.find('[data-test="content-menu-button"]') menuToggle = wrapper.find('[data-test="content-menu-button"]')
menuToggle.trigger('click') await menuToggle.trigger('click')
return wrapper return wrapper
} }
describe('owner of contribution', () => { describe('owner of contribution', () => {
let wrapper let wrapper
beforeEach(() => { beforeEach(async () => {
wrapper = openContentMenu({ wrapper = await openContentMenu({
isOwner: true, isOwner: true,
resourceType: 'contribution', resourceType: 'contribution',
resource: { resource: {
@ -86,9 +91,9 @@ describe('ContentMenu.vue', () => {
}) })
describe('admin can', () => { describe('admin can', () => {
it('pin unpinned post', () => { it('pin unpinned post', async () => {
getters['auth/isAdmin'] = () => true getters['auth/isAdmin'] = () => true
const wrapper = openContentMenu({ const wrapper = await openContentMenu({
isOwner: false, isOwner: false,
resourceType: 'contribution', resourceType: 'contribution',
resource: { resource: {
@ -111,8 +116,8 @@ describe('ContentMenu.vue', () => {
]) ])
}) })
it('unpin pinned post', () => { it('unpin pinned post', async () => {
const wrapper = openContentMenu({ const wrapper = await openContentMenu({
isOwner: false, isOwner: false,
resourceType: 'contribution', resourceType: 'contribution',
resource: { resource: {
@ -135,11 +140,11 @@ describe('ContentMenu.vue', () => {
]) ])
}) })
it('can delete another user', () => { it('can delete another user', async () => {
getters['auth/user'] = () => { getters['auth/user'] = () => {
return { id: 'some-user', slug: 'some-user' } return { id: 'some-user', slug: 'some-user' }
} }
const wrapper = openContentMenu({ const wrapper = await openContentMenu({
resourceType: 'user', resourceType: 'user',
resource: { resource: {
id: 'another-user', id: 'another-user',
@ -161,8 +166,8 @@ describe('ContentMenu.vue', () => {
]) ])
}) })
it('can not delete the own account', () => { it('can not delete the own account', async () => {
const wrapper = openContentMenu({ const wrapper = await openContentMenu({
resourceType: 'user', resourceType: 'user',
resource: { resource: {
id: 'some-user', id: 'some-user',
@ -179,8 +184,8 @@ describe('ContentMenu.vue', () => {
describe('owner of comment can', () => { describe('owner of comment can', () => {
let wrapper let wrapper
beforeEach(() => { beforeEach(async () => {
wrapper = openContentMenu({ wrapper = await openContentMenu({
isOwner: true, isOwner: true,
resourceType: 'comment', resourceType: 'comment',
resource: { resource: {
@ -208,10 +213,10 @@ describe('ContentMenu.vue', () => {
}) })
describe('reporting', () => { 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/isAdmin'] = () => false
getters['auth/isModerator'] = () => false getters['auth/isModerator'] = () => false
const wrapper = openContentMenu({ const wrapper = await openContentMenu({
isOwner: false, isOwner: false,
resourceType: 'contribution', resourceType: 'contribution',
resource: { resource: {
@ -227,8 +232,8 @@ describe('ContentMenu.vue', () => {
expect(openModalSpy).toHaveBeenCalledWith('report') expect(openModalSpy).toHaveBeenCalledWith('report')
}) })
it('a comment of another user is possible', () => { it('a comment of another user is possible', async () => {
const wrapper = openContentMenu({ const wrapper = await openContentMenu({
isOwner: false, isOwner: false,
resourceType: 'comment', resourceType: 'comment',
resource: { resource: {
@ -244,8 +249,8 @@ describe('ContentMenu.vue', () => {
expect(openModalSpy).toHaveBeenCalledWith('report') expect(openModalSpy).toHaveBeenCalledWith('report')
}) })
it('another user is possible', () => { it('another user is possible', async () => {
const wrapper = openContentMenu({ const wrapper = await openContentMenu({
isOwner: false, isOwner: false,
resourceType: 'user', resourceType: 'user',
resource: { resource: {
@ -261,8 +266,8 @@ describe('ContentMenu.vue', () => {
expect(openModalSpy).toHaveBeenCalledWith('report') expect(openModalSpy).toHaveBeenCalledWith('report')
}) })
it('another organization is possible', () => { it('another organization is possible', async () => {
const wrapper = openContentMenu({ const wrapper = await openContentMenu({
isOwner: false, isOwner: false,
resourceType: 'organization', resourceType: 'organization',
resource: { resource: {
@ -280,10 +285,10 @@ describe('ContentMenu.vue', () => {
}) })
describe('moderator', () => { describe('moderator', () => {
it('can disable posts', () => { it('can disable posts', async () => {
getters['auth/isAdmin'] = () => false getters['auth/isAdmin'] = () => false
getters['auth/isModerator'] = () => true getters['auth/isModerator'] = () => true
const wrapper = openContentMenu({ const wrapper = await openContentMenu({
isOwner: false, isOwner: false,
resourceType: 'contribution', resourceType: 'contribution',
resource: { resource: {
@ -300,8 +305,8 @@ describe('ContentMenu.vue', () => {
expect(openModalSpy).toHaveBeenCalledWith('disable') expect(openModalSpy).toHaveBeenCalledWith('disable')
}) })
it('can disable comments', () => { it('can disable comments', async () => {
const wrapper = openContentMenu({ const wrapper = await openContentMenu({
isOwner: false, isOwner: false,
resourceType: 'comment', resourceType: 'comment',
resource: { resource: {
@ -318,8 +323,8 @@ describe('ContentMenu.vue', () => {
expect(openModalSpy).toHaveBeenCalledWith('disable') expect(openModalSpy).toHaveBeenCalledWith('disable')
}) })
it('can disable users', () => { it('can disable users', async () => {
const wrapper = openContentMenu({ const wrapper = await openContentMenu({
isOwner: false, isOwner: false,
resourceType: 'user', resourceType: 'user',
resource: { resource: {
@ -336,8 +341,8 @@ describe('ContentMenu.vue', () => {
expect(openModalSpy).toHaveBeenCalledWith('disable') expect(openModalSpy).toHaveBeenCalledWith('disable')
}) })
it('can disable organizations', () => { it('can disable organizations', async () => {
const wrapper = openContentMenu({ const wrapper = await openContentMenu({
isOwner: false, isOwner: false,
resourceType: 'organization', resourceType: 'organization',
resource: { resource: {
@ -354,8 +359,8 @@ describe('ContentMenu.vue', () => {
expect(openModalSpy).toHaveBeenCalledWith('disable') expect(openModalSpy).toHaveBeenCalledWith('disable')
}) })
it('can release posts', () => { it('can release posts', async () => {
const wrapper = openContentMenu({ const wrapper = await openContentMenu({
isOwner: false, isOwner: false,
resourceType: 'contribution', resourceType: 'contribution',
resource: { resource: {
@ -372,8 +377,8 @@ describe('ContentMenu.vue', () => {
expect(openModalSpy).toHaveBeenCalledWith('release') expect(openModalSpy).toHaveBeenCalledWith('release')
}) })
it('can release comments', () => { it('can release comments', async () => {
const wrapper = openContentMenu({ const wrapper = await openContentMenu({
isOwner: false, isOwner: false,
resourceType: 'comment', resourceType: 'comment',
resource: { resource: {
@ -390,8 +395,8 @@ describe('ContentMenu.vue', () => {
expect(openModalSpy).toHaveBeenCalledWith('release') expect(openModalSpy).toHaveBeenCalledWith('release')
}) })
it('can release users', () => { it('can release users', async () => {
const wrapper = openContentMenu({ const wrapper = await openContentMenu({
isOwner: false, isOwner: false,
resourceType: 'user', resourceType: 'user',
resource: { resource: {
@ -408,8 +413,8 @@ describe('ContentMenu.vue', () => {
expect(openModalSpy).toHaveBeenCalledWith('release') expect(openModalSpy).toHaveBeenCalledWith('release')
}) })
it('can release organizations', () => { it('can release organizations', async () => {
const wrapper = openContentMenu({ const wrapper = await openContentMenu({
isOwner: false, isOwner: false,
resourceType: 'organization', resourceType: 'organization',
resource: { resource: {
@ -428,10 +433,10 @@ describe('ContentMenu.vue', () => {
}) })
describe('user', () => { describe('user', () => {
it('can access settings', () => { it('can access settings', async () => {
getters['auth/isAdmin'] = () => false getters['auth/isAdmin'] = () => false
getters['auth/isModerator'] = () => false getters['auth/isModerator'] = () => false
const wrapper = openContentMenu({ const wrapper = await openContentMenu({
isOwner: true, isOwner: true,
resourceType: 'user', resourceType: 'user',
resource: { resource: {
@ -448,8 +453,8 @@ describe('ContentMenu.vue', () => {
).toBe('/settings') ).toBe('/settings')
}) })
it('can mute other users', () => { it('can mute other users', async () => {
const wrapper = openContentMenu({ const wrapper = await openContentMenu({
isOwner: false, isOwner: false,
resourceType: 'user', resourceType: 'user',
resource: { resource: {
@ -472,8 +477,8 @@ describe('ContentMenu.vue', () => {
]) ])
}) })
it('can unmute muted users', () => { it('can unmute muted users', async () => {
const wrapper = openContentMenu({ const wrapper = await openContentMenu({
isOwner: false, isOwner: false,
resourceType: 'user', resourceType: 'user',
resource: { resource: {

View File

@ -1,9 +1,13 @@
import { config, mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import GroupContentMenu from './GroupContentMenu.vue' import GroupContentMenu from './GroupContentMenu.vue'
const localVue = global.localVue const localVue = global.localVue
config.stubs['router-link'] = '<span><slot /></span>' const stubs = {
'router-link': {
template: '<span><slot /></span>',
},
}
const propsData = { const propsData = {
usage: 'groupTeaser', usage: 'groupTeaser',
@ -24,7 +28,7 @@ describe('GroupContentMenu', () => {
describe('mount', () => { describe('mount', () => {
const Wrapper = () => { const Wrapper = () => {
return mount(GroupContentMenu, { propsData, mocks, localVue }) return mount(GroupContentMenu, { propsData, mocks, localVue, stubs })
} }
beforeEach(() => { beforeEach(() => {

View File

@ -1,4 +1,4 @@
import { config, mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import ContributionForm from './ContributionForm.vue' import ContributionForm from './ContributionForm.vue'
import Vuex from 'vuex' import Vuex from 'vuex'
@ -11,9 +11,11 @@ global.MutationObserver = MutationObserver
const localVue = global.localVue const localVue = global.localVue
config.stubs['client-only'] = '<span><slot /></span>' const stubs = {
config.stubs['nuxt-link'] = '<span><slot /></span>' 'client-only': true,
config.stubs['v-popover'] = '<span><slot /></span>' 'nuxt-link': true,
'v-popover': true,
}
describe('ContributionForm.vue', () => { describe('ContributionForm.vue', () => {
let wrapper, postTitleInput, expectedParams, cancelBtn, mocks, propsData let wrapper, postTitleInput, expectedParams, cancelBtn, mocks, propsData
@ -88,6 +90,7 @@ describe('ContributionForm.vue', () => {
localVue, localVue,
store, store,
propsData, propsData,
stubs,
}) })
} }
@ -163,7 +166,7 @@ describe('ContributionForm.vue', () => {
.mockImplementation(function () { .mockImplementation(function () {
this.onload({ target: { result: 'someUrlToImage' } }) 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') await wrapper.find('form').trigger('submit')
expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams)) expect(mocks.$apollo.mutate).toHaveBeenCalledWith(expect.objectContaining(expectedParams))
expect(spy).toHaveBeenCalledWith(imageUpload) expect(spy).toHaveBeenCalledWith(imageUpload)

View File

@ -115,9 +115,9 @@ describe('DeleteData.vue', () => {
enableContributionDeletionCheckbox = wrapper.find( enableContributionDeletionCheckbox = wrapper.find(
'[data-test="contributions-deletion-checkbox"]', '[data-test="contributions-deletion-checkbox"]',
) )
enableContributionDeletionCheckbox.trigger('click') enableContributionDeletionCheckbox.setChecked(true)
enableCommentDeletionCheckbox = wrapper.find('[data-test="comments-deletion-checkbox"]') enableCommentDeletionCheckbox = wrapper.find('[data-test="comments-deletion-checkbox"]')
enableCommentDeletionCheckbox.trigger('click') enableCommentDeletionCheckbox.setChecked(true)
deleteAccountBtn.trigger('click') deleteAccountBtn.trigger('click')
expect(mocks.$apollo.mutate).toHaveBeenCalledWith( expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
@ -133,7 +133,7 @@ describe('DeleteData.vue', () => {
enableContributionDeletionCheckbox = wrapper.find( enableContributionDeletionCheckbox = wrapper.find(
'[data-test="contributions-deletion-checkbox"]', '[data-test="contributions-deletion-checkbox"]',
) )
enableContributionDeletionCheckbox.trigger('click') enableContributionDeletionCheckbox.setChecked(true)
deleteAccountBtn.trigger('click') deleteAccountBtn.trigger('click')
expect(mocks.$apollo.mutate).toHaveBeenCalledWith( expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
@ -147,7 +147,7 @@ describe('DeleteData.vue', () => {
it("deletes a user's comments if requested", () => { it("deletes a user's comments if requested", () => {
enableCommentDeletionCheckbox = wrapper.find('[data-test="comments-deletion-checkbox"]') enableCommentDeletionCheckbox = wrapper.find('[data-test="comments-deletion-checkbox"]')
enableCommentDeletionCheckbox.trigger('click') enableCommentDeletionCheckbox.setChecked(true)
deleteAccountBtn.trigger('click') deleteAccountBtn.trigger('click')
expect(mocks.$apollo.mutate).toHaveBeenCalledWith( expect(mocks.$apollo.mutate).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({

View File

@ -35,7 +35,7 @@ describe('Editor.vue', () => {
describe('mount', () => { describe('mount', () => {
it('renders', () => { it('renders', () => {
expect(Wrapper().is('div')).toBe(true) expect(Wrapper().element.tagName).toBe('DIV')
}) })
describe('given a piece of text', () => { describe('given a piece of text', () => {

View File

@ -42,7 +42,7 @@ describe('Embed.vue', () => {
propsData.node = { attrs: { href: 'https://www.youtube.com/watch?v=qkdXAtO40Fo' } } propsData.node = { attrs: { href: 'https://www.youtube.com/watch?v=qkdXAtO40Fo' } }
const wrapper = Wrapper({ propsData }) const wrapper = Wrapper({ propsData })
await wrapper.html() await wrapper.html()
expect(wrapper.contains('embed-component-stub')).toBe(true) expect(wrapper.find('embed-component-stub').exists()).toBe(true)
}) })
}) })

View File

@ -160,7 +160,7 @@ describe('EmbedComponent.vue', () => {
describe('sets permanently', () => { describe('sets permanently', () => {
beforeEach(() => { beforeEach(() => {
wrapper.find('input[type=checkbox]').trigger('click') wrapper.find('input[type=checkbox]').setChecked(true)
wrapper.find('[data-test="play-now-button"]').trigger('click') wrapper.find('[data-test="play-now-button"]').trigger('click')
}) })

View File

@ -19,7 +19,9 @@ describe('EnterNonce ', () => {
}) })
describe('mount', () => { describe('mount', () => {
beforeEach(jest.useFakeTimers) beforeEach(() => {
jest.useFakeTimers()
})
Wrapper = () => { Wrapper = () => {
return mount(EnterNonce, { return mount(EnterNonce, {

View File

@ -9,12 +9,14 @@ let wrapper
describe('FollowingFilter', () => { describe('FollowingFilter', () => {
const mutations = { const mutations = {
'posts/TOGGLE_FILTER_BY_FOLLOWED': jest.fn(), 'posts/TOGGLE_FILTER_BY_FOLLOWED': jest.fn(),
'posts/TOGGLE_FILTER_BY_MY_GROUPS': jest.fn(),
} }
const getters = { const getters = {
'auth/user': () => { 'auth/user': () => {
return { id: 'u34' } return { id: 'u34' }
}, },
'posts/filteredByUsersFollowed': jest.fn(), 'posts/filteredByUsersFollowed': jest.fn(),
'posts/filteredByPostsInMyGroups': jest.fn(),
} }
const mocks = { const mocks = {
@ -34,12 +36,18 @@ describe('FollowingFilter', () => {
describe('mount', () => { describe('mount', () => {
it('sets "filter-by-followed" button attribute `filled`', () => { it('sets "filter-by-followed" button attribute `filled`', () => {
getters['posts/filteredByUsersFollowed'] = jest.fn(() => true) getters['posts/filteredByUsersFollowed'] = jest.fn(() => true)
getters['posts/filteredByPostsInMyGroups'] = jest.fn(() => true)
const wrapper = Wrapper() const wrapper = Wrapper()
expect( expect(
wrapper wrapper
.find('.following-filter .filter-list .follower-item .base-button') .find('.following-filter .filter-list .follower-item .base-button')
.classes('--filled'), .classes('--filled'),
).toBe(true) ).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', () => { describe('click "filter-by-followed" button', () => {
@ -48,5 +56,14 @@ describe('FollowingFilter', () => {
expect(mutations['posts/TOGGLE_FILTER_BY_FOLLOWED']).toHaveBeenCalledWith({}, 'u34') 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()
})
})
}) })
}) })

View File

@ -10,6 +10,15 @@
@click="toggleFilteredByFollowed(currentUser.id)" @click="toggleFilteredByFollowed(currentUser.id)"
/> />
</li> </li>
<li class="item posts-in-my-groups-item">
<labeled-button
icon="users"
:label="$t('filter-menu.my-groups')"
:filled="filteredByPostsInMyGroups"
:title="$t('contribution.filterMyGroups')"
@click="toggleFilteredByMyGroups()"
/>
</li>
</template> </template>
</filter-menu-section> </filter-menu-section>
</template> </template>
@ -28,12 +37,14 @@ export default {
computed: { computed: {
...mapGetters({ ...mapGetters({
filteredByUsersFollowed: 'posts/filteredByUsersFollowed', filteredByUsersFollowed: 'posts/filteredByUsersFollowed',
filteredByPostsInMyGroups: 'posts/filteredByPostsInMyGroups',
currentUser: 'auth/user', currentUser: 'auth/user',
}), }),
}, },
methods: { methods: {
...mapMutations({ ...mapMutations({
toggleFilteredByFollowed: 'posts/TOGGLE_FILTER_BY_FOLLOWED', toggleFilteredByFollowed: 'posts/TOGGLE_FILTER_BY_FOLLOWED',
toggleFilteredByMyGroups: 'posts/TOGGLE_FILTER_BY_MY_GROUPS',
}), }),
}, },
} }

View File

@ -1,9 +1,11 @@
import { config, mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import GroupForm from './GroupForm.vue' import GroupForm from './GroupForm.vue'
const localVue = global.localVue const localVue = global.localVue
config.stubs['nuxt-link'] = '<span><slot /></span>' const stubs = {
'nuxt-link': true,
}
const propsData = { const propsData = {
update: false, update: false,
@ -25,7 +27,7 @@ describe('GroupForm', () => {
describe('mount', () => { describe('mount', () => {
const Wrapper = () => { const Wrapper = () => {
return mount(GroupForm, { propsData, mocks, localVue }) return mount(GroupForm, { propsData, mocks, localVue, stubs })
} }
beforeEach(() => { beforeEach(() => {

View File

@ -1,26 +1,65 @@
import { mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import GroupMember from './GroupMember.vue' import GroupMember from './GroupMember.vue'
import { changeGroupMemberRoleMutation, removeUserFromGroupMutation } from '~/graphql/groups.js'
const localVue = global.localVue const localVue = global.localVue
const propsData = { const propsData = {
groupId: '', groupId: 'group-id',
groupMembers: [], 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', () => { describe('GroupMember', () => {
let wrapper let wrapper
let mocks let mocks
beforeEach(() => { beforeEach(() => {
mocks = { mocks = {
$t: jest.fn(), $t: jest.fn((t) => t),
$apollo: {
mutate: apolloMock,
},
$toast: {
error: toastErrorMock,
success: toastSuccessMock,
},
} }
}) })
describe('mount', () => { describe('mount', () => {
const Wrapper = () => { const Wrapper = () => {
return mount(GroupMember, { propsData, mocks, localVue }) return mount(GroupMember, { propsData, mocks, localVue, stubs })
} }
beforeEach(() => { beforeEach(() => {
@ -30,5 +69,120 @@ describe('GroupMember', () => {
it('renders', () => { it('renders', () => {
expect(wrapper.findAll('.group-member')).toHaveLength(1) 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)
})
})
})
}) })
}) })

View File

@ -53,17 +53,20 @@
</ds-chip> </ds-chip>
</template> </template>
<template #edit="scope"> <template #edit="scope">
<ds-button v-if="scope.row.myRoleInGroup !== 'owner'" size="small" primary disabled> <base-button
<!-- TODO: implement removal of group members --> v-if="scope.row.myRoleInGroup !== 'owner'"
<!-- :disabled="scope.row.myRoleInGroup === 'owner'" size="small"
--> primary
@click="
isOpen = true
userId = scope.row.id
"
>
{{ $t('group.removeMemberButton') }} {{ $t('group.removeMemberButton') }}
</ds-button> </base-button>
</template> </template>
</ds-table> </ds-table>
<!-- TODO: implement removal of group members --> <ds-modal
<!-- TODO: change to ocelot.social modal -->
<!-- <ds-modal
v-if="isOpen" v-if="isOpen"
v-model="isOpen" v-model="isOpen"
:title="`${$t('group.removeMember')}`" :title="`${$t('group.removeMember')}`"
@ -71,12 +74,12 @@
extended extended
:confirm-label="$t('group.removeMember')" :confirm-label="$t('group.removeMember')"
:cancel-label="$t('actions.cancel')" :cancel-label="$t('actions.cancel')"
@confirm="deleteMember(memberId)" @confirm="removeUser()"
/> --> />
</div> </div>
</template> </template>
<script> <script>
import { changeGroupMemberRoleMutation } from '~/graphql/groups.js' import { changeGroupMemberRoleMutation, removeUserFromGroupMutation } from '~/graphql/groups.js'
export default { export default {
name: 'GroupMember', name: 'GroupMember',
@ -96,6 +99,8 @@ export default {
query: '', query: '',
searchProcess: null, searchProcess: null,
user: {}, user: {},
isOpen: false,
userId: null,
} }
}, },
computed: { computed: {
@ -139,6 +144,25 @@ export default {
this.$toast.error(error.message) this.$toast.error(error.message)
} }
}, },
removeUser() {
this.$apollo
.mutate({
mutation: removeUserFromGroupMutation(),
variables: { groupId: this.groupId, userId: this.userId },
})
.then(({ data }) => {
this.$emit('loadGroupMembers')
this.$toast.success(
this.$t('group.memberRemoved', { name: data.RemoveUserFromGroup.slug }),
)
})
.catch((error) => {
this.$toast.error(error.message)
})
.finally(() => {
this.userId = null
})
},
}, },
} }
</script> </script>

View File

@ -1,10 +1,12 @@
import { config, shallowMount } from '@vue/test-utils' import { shallowMount } from '@vue/test-utils'
import Hashtag from './Hashtag' import Hashtag from './Hashtag'
const localVue = global.localVue const localVue = global.localVue
config.stubs['nuxt-link'] = '<span><slot /></span>' const stubs = {
'nuxt-link': true,
}
describe('Hashtag', () => { describe('Hashtag', () => {
let id let id
@ -15,6 +17,7 @@ describe('Hashtag', () => {
propsData: { propsData: {
id, id,
}, },
stubs,
}) })
} }

View File

@ -29,7 +29,7 @@ describe('HashtagsFilter.vue', () => {
it('renders a card', () => { it('renders a card', () => {
wrapper = Wrapper() wrapper = Wrapper()
expect(wrapper.is('.base-card')).toBe(true) expect(wrapper.classes('base-card')).toBe(true)
}) })
describe('click clear search button', () => { describe('click clear search button', () => {

View File

@ -1,7 +1,11 @@
import { config, mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import InviteButton from './InviteButton.vue' import InviteButton from './InviteButton.vue'
config.stubs['v-popover'] = '<span><slot /></span>' const stubs = {
'v-popover': {
template: '<span><slot /></span>',
},
}
describe('InviteButton.vue', () => { describe('InviteButton.vue', () => {
let wrapper let wrapper
@ -22,7 +26,7 @@ describe('InviteButton.vue', () => {
describe('mount', () => { describe('mount', () => {
const Wrapper = () => { const Wrapper = () => {
return mount(InviteButton, { mocks, propsData }) return mount(InviteButton, { mocks, propsData, stubs })
} }
beforeEach(() => { beforeEach(() => {
@ -30,12 +34,12 @@ describe('InviteButton.vue', () => {
}) })
it('renders', () => { it('renders', () => {
expect(wrapper.contains('.invite-button')).toBe(true) expect(wrapper.find('.invite-button').exists()).toBe(true)
}) })
it('open popup', () => { it('open popup', () => {
wrapper.find('.base-button').trigger('click') 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 () => { it('invite codes not available', async () => {

View File

@ -1,11 +1,13 @@
import { config, mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import LocaleSwitch from './LocaleSwitch.vue' import LocaleSwitch from './LocaleSwitch.vue'
import Vuex from 'vuex' import Vuex from 'vuex'
const localVue = global.localVue const localVue = global.localVue
config.stubs['client-only'] = '<span><slot /></span>' const stubs = {
'client-only': true,
}
describe('LocaleSwitch.vue', () => { describe('LocaleSwitch.vue', () => {
let wrapper, mocks, computed, deutschLanguageItem, getters let wrapper, mocks, computed, deutschLanguageItem, getters
@ -65,7 +67,7 @@ describe('LocaleSwitch.vue', () => {
const store = new Vuex.Store({ const store = new Vuex.Store({
getters, getters,
}) })
return mount(LocaleSwitch, { mocks, localVue, computed, store }) return mount(LocaleSwitch, { mocks, localVue, computed, store, stubs })
} }
describe('with current user', () => { describe('with current user', () => {

View File

@ -1,7 +1,10 @@
import { config, mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import LoginButton from './LoginButton.vue' import LoginButton from './LoginButton.vue'
config.stubs['v-popover'] = '<span><slot /></span>' const stubs = {
'v-popover': true,
'nuxt-link': true,
}
describe('LoginButton.vue', () => { describe('LoginButton.vue', () => {
let wrapper let wrapper
@ -22,7 +25,7 @@ describe('LoginButton.vue', () => {
describe('mount', () => { describe('mount', () => {
const Wrapper = () => { const Wrapper = () => {
return mount(LoginButton, { mocks, propsData }) return mount(LoginButton, { mocks, propsData, stubs })
} }
beforeEach(() => { beforeEach(() => {
@ -30,12 +33,12 @@ describe('LoginButton.vue', () => {
}) })
it('renders', () => { it('renders', () => {
expect(wrapper.contains('.login-button')).toBe(true) expect(wrapper.find('.login-button').exists()).toBe(true)
}) })
it('open popup', () => { it('open popup', () => {
wrapper.find('.base-button').trigger('click') wrapper.find('.base-button').trigger('click')
expect(wrapper.contains('.login-button')).toBe(true) expect(wrapper.find('.login-button').exists()).toBe(true)
}) })
}) })
}) })

View File

@ -2,15 +2,17 @@ import Vue from 'vue'
import LoginForm from './LoginForm.vue' import LoginForm from './LoginForm.vue'
import Styleguide from '@human-connection/styleguide' import Styleguide from '@human-connection/styleguide'
import Vuex from 'vuex' import Vuex from 'vuex'
import { config, mount, createLocalVue } from '@vue/test-utils' import { mount, createLocalVue } from '@vue/test-utils'
const localVue = createLocalVue() const localVue = createLocalVue()
localVue.use(Vuex) localVue.use(Vuex)
localVue.use(Styleguide) localVue.use(Styleguide)
config.stubs['nuxt-link'] = '<span><slot /></span>' const stubs = {
config.stubs['locale-switch'] = '<span><slot /></span>' 'nuxt-link': true,
config.stubs['client-only'] = '<span><slot /></span>' 'locale-switch': true,
'client-only': true,
}
const authUserMock = jest.fn().mockReturnValue({ activeCategories: [] }) const authUserMock = jest.fn().mockReturnValue({ activeCategories: [] })
@ -46,7 +48,7 @@ describe('LoginForm', () => {
error: jest.fn(), 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', () => { describe('fill in email and password and submit', () => {

View File

@ -1,9 +1,11 @@
import { config, mount } from '@vue/test-utils' import { mount } from '@vue/test-utils'
import MasonryGridItem from './MasonryGridItem' import MasonryGridItem from './MasonryGridItem'
const localVue = global.localVue const localVue = global.localVue
config.stubs['ds-grid-item'] = '<span><slot /></span>' const stubs = {
'ds-grid-item': true,
}
describe('MasonryGridItem', () => { describe('MasonryGridItem', () => {
let wrapper let wrapper
@ -11,34 +13,34 @@ describe('MasonryGridItem', () => {
describe('given an imageAspectRatio', () => { describe('given an imageAspectRatio', () => {
it('sets the initial rowSpan to 13 when the ratio is higher than 1.3', () => { it('sets the initial rowSpan to 13 when the ratio is higher than 1.3', () => {
const propsData = { imageAspectRatio: 2 } const propsData = { imageAspectRatio: 2 }
wrapper = mount(MasonryGridItem, { localVue, propsData }) wrapper = mount(MasonryGridItem, { localVue, propsData, stubs })
expect(wrapper.vm.rowSpan).toBe(13) expect(wrapper.vm.rowSpan).toBe(13)
}) })
it('sets the initial rowSpan to 15 when the ratio is between 1.3 and 1', () => { it('sets the initial rowSpan to 15 when the ratio is between 1.3 and 1', () => {
const propsData = { imageAspectRatio: 1.1 } const propsData = { imageAspectRatio: 1.1 }
wrapper = mount(MasonryGridItem, { localVue, propsData }) wrapper = mount(MasonryGridItem, { localVue, propsData, stubs })
expect(wrapper.vm.rowSpan).toBe(15) expect(wrapper.vm.rowSpan).toBe(15)
}) })
it('sets the initial rowSpan to 18 when the ratio is between 1 and 0.7', () => { it('sets the initial rowSpan to 18 when the ratio is between 1 and 0.7', () => {
const propsData = { imageAspectRatio: 0.7 } const propsData = { imageAspectRatio: 0.7 }
wrapper = mount(MasonryGridItem, { localVue, propsData }) wrapper = mount(MasonryGridItem, { localVue, propsData, stubs })
expect(wrapper.vm.rowSpan).toBe(18) expect(wrapper.vm.rowSpan).toBe(18)
}) })
it('sets the initial rowSpan to 25 when the ratio is lower than 0.7', () => { it('sets the initial rowSpan to 25 when the ratio is lower than 0.7', () => {
const propsData = { imageAspectRatio: 0.3 } const propsData = { imageAspectRatio: 0.3 }
wrapper = mount(MasonryGridItem, { localVue, propsData }) wrapper = mount(MasonryGridItem, { localVue, propsData, stubs })
expect(wrapper.vm.rowSpan).toBe(25) expect(wrapper.vm.rowSpan).toBe(25)
}) })
describe('given no aspect ratio', () => { describe('given no aspect ratio', () => {
it('sets the initial rowSpan to 8 when not given an imageAspectRatio', () => { 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) expect(wrapper.vm.rowSpan).toBe(8)
}) })
}) })

View File

@ -57,9 +57,9 @@ describe('Modal.vue', () => {
it('initially empty', () => { it('initially empty', () => {
wrapper = Wrapper() wrapper = Wrapper()
expect(wrapper.contains(ConfirmModal)).toBe(false) expect(wrapper.findComponent(ConfirmModal).exists()).toBe(false)
expect(wrapper.contains(DisableModal)).toBe(false) expect(wrapper.findComponent(DisableModal).exists()).toBe(false)
expect(wrapper.contains(ReportModal)).toBe(false) expect(wrapper.findComponent(ReportModal).exists()).toBe(false)
}) })
describe('store/modal holds data to disable', () => { describe('store/modal holds data to disable', () => {
@ -78,11 +78,11 @@ describe('Modal.vue', () => {
}) })
it('renders disable modal', () => { it('renders disable modal', () => {
expect(wrapper.contains(DisableModal)).toBe(true) expect(wrapper.findComponent(DisableModal).exists()).toBe(true)
}) })
it('passes data to disable modal', () => { it('passes data to disable modal', () => {
expect(wrapper.find(DisableModal).props()).toEqual({ expect(wrapper.findComponent(DisableModal).props()).toEqual({
type: 'contribution', type: 'contribution',
name: 'some title', name: 'some title',
id: 'c456', id: 'c456',
@ -91,9 +91,9 @@ describe('Modal.vue', () => {
describe('child component emits close', () => { describe('child component emits close', () => {
it('turns empty', async () => { it('turns empty', async () => {
wrapper.find(DisableModal).vm.$emit('close') wrapper.findComponent(DisableModal).vm.$emit('close')
await Vue.nextTick() 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() wrapper = Wrapper()
expect(wrapper.find(DisableModal).props()).toEqual({ expect(wrapper.findComponent(DisableModal).props()).toEqual({
type: 'comment', type: 'comment',
name: 'Author name', name: 'Author name',
id: 'c456', id: 'c456',
@ -124,7 +124,7 @@ describe('Modal.vue', () => {
}, },
} }
wrapper = Wrapper() wrapper = Wrapper()
expect(wrapper.find(DisableModal).props()).toEqual({ expect(wrapper.findComponent(DisableModal).props()).toEqual({
type: 'comment', type: 'comment',
name: '', name: '',
id: 'c456', id: 'c456',
@ -142,7 +142,7 @@ describe('Modal.vue', () => {
}, },
} }
wrapper = Wrapper() wrapper = Wrapper()
expect(wrapper.find(DisableModal).props()).toEqual({ expect(wrapper.findComponent(DisableModal).props()).toEqual({
type: 'user', type: 'user',
name: 'Username', name: 'Username',
id: 'u456', id: 'u456',
@ -160,7 +160,7 @@ describe('Modal.vue', () => {
}, },
} }
wrapper = Wrapper() wrapper = Wrapper()
expect(wrapper.find(DisableModal).props()).toEqual({ expect(wrapper.findComponent(DisableModal).props()).toEqual({
type: 'something', type: 'something',
name: null, name: null,
id: 's456', id: 's456',

View File

@ -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 ConfirmModal from './ConfirmModal.vue'
import { postMenuModalsData } from '~/components/utils/PostHelpers' import { postMenuModalsData } from '~/components/utils/PostHelpers'
const localVue = global.localVue const localVue = global.localVue
config.stubs['sweetalert-icon'] = '<span><slot /></span>' const stubs = {
'sweetalert-icon': true,
}
describe('ConfirmModal.vue', () => { describe('ConfirmModal.vue', () => {
let Wrapper let Wrapper
@ -41,6 +43,7 @@ describe('ConfirmModal.vue', () => {
propsData, propsData,
mocks, mocks,
localVue, localVue,
stubs,
}) })
} }
@ -90,10 +93,13 @@ describe('ConfirmModal.vue', () => {
propsData, propsData,
mocks, mocks,
localVue, localVue,
stubs,
}) })
} }
beforeEach(jest.useFakeTimers) beforeEach(() => {
jest.useFakeTimers()
})
describe('given post id', () => { describe('given post id', () => {
beforeEach(() => { beforeEach(() => {

View File

@ -1,9 +1,12 @@
import { config, mount, shallowMount } from '@vue/test-utils' import { mount, shallowMount } from '@vue/test-utils'
import Vuex from 'vuex' import Vuex from 'vuex'
import DeleteUserModal from './DeleteUserModal.vue' import DeleteUserModal from './DeleteUserModal.vue'
const localVue = global.localVue const localVue = global.localVue
config.stubs['sweetalert-icon'] = '<span><slot /></span>'
config.stubs['nuxt-link'] = '<span><slot /></span>' const stubs = {
'sweetalert-icon': true,
'nuxt-link': true,
}
localVue.use(DeleteUserModal) localVue.use(DeleteUserModal)
@ -49,6 +52,7 @@ describe('DeleteUserModal.vue', () => {
mocks, mocks,
store, store,
localVue, localVue,
stubs,
}) })
} }
@ -74,9 +78,12 @@ describe('DeleteUserModal.vue', () => {
mocks, mocks,
store, store,
localVue, localVue,
stubs,
}) })
} }
beforeEach(jest.useFakeTimers) beforeEach(() => {
jest.useFakeTimers()
})
describe('given another user', () => { describe('given another user', () => {
beforeEach(() => { beforeEach(() => {

View File

@ -102,7 +102,9 @@ describe('DisableModal.vue', () => {
localVue, localVue,
}) })
} }
beforeEach(jest.useFakeTimers) beforeEach(() => {
jest.useFakeTimers()
})
describe('given id', () => { describe('given id', () => {
beforeEach(() => { beforeEach(() => {

View File

@ -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 ReportModal from './ReportModal.vue'
import Vue from 'vue' import Vue from 'vue'
const localVue = global.localVue const localVue = global.localVue
config.stubs['sweetalert-icon'] = '<span><slot /></span>' const stubs = {
'sweetalert-icon': true,
}
describe('ReportModal.vue', () => { describe('ReportModal.vue', () => {
let wrapper let wrapper
@ -39,6 +41,7 @@ describe('ReportModal.vue', () => {
propsData, propsData,
mocks, mocks,
localVue, localVue,
stubs,
}) })
} }
@ -109,13 +112,16 @@ describe('ReportModal.vue', () => {
propsData, propsData,
mocks, mocks,
localVue, localVue,
stubs,
}) })
} }
beforeEach(jest.useFakeTimers) beforeEach(() => {
jest.useFakeTimers()
})
it('renders', () => { it('renders', () => {
expect(Wrapper().is('div')).toBe(true) expect(Wrapper().element.tagName).toBe('DIV')
}) })
describe('given id', () => { describe('given id', () => {

View File

@ -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 Notification from './Notification.vue'
import Vuex from 'vuex' import Vuex from 'vuex'
const localVue = global.localVue const localVue = global.localVue
config.stubs['client-only'] = '<span><slot /></span>'
describe('Notification', () => { describe('Notification', () => {
let stubs let stubs
let getters let getters
@ -20,6 +18,7 @@ describe('Notification', () => {
} }
stubs = { stubs = {
NuxtLink: RouterLinkStub, NuxtLink: RouterLinkStub,
'client-only': true,
} }
getters = { getters = {
'auth/user': () => { 'auth/user': () => {

View File

@ -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 NotificationList from './NotificationList'
import Notification from '../Notification/Notification' import Notification from '../Notification/Notification'
import Vuex from 'vuex' import Vuex from 'vuex'
@ -9,9 +9,6 @@ const localVue = global.localVue
localVue.filter('truncate', (string) => string) localVue.filter('truncate', (string) => string)
config.stubs['client-only'] = '<span><slot /></span>'
config.stubs['v-popover'] = '<span><slot /></span>'
describe('NotificationList.vue', () => { describe('NotificationList.vue', () => {
let wrapper let wrapper
let mocks let mocks
@ -33,6 +30,8 @@ describe('NotificationList.vue', () => {
} }
stubs = { stubs = {
NuxtLink: RouterLinkStub, NuxtLink: RouterLinkStub,
'client-only': true,
'v-popover': true,
} }
propsData = { notifications } propsData = { notifications }
}) })
@ -44,6 +43,7 @@ describe('NotificationList.vue', () => {
mocks, mocks,
store, store,
localVue, localVue,
stubs,
}) })
} }
@ -52,7 +52,7 @@ describe('NotificationList.vue', () => {
}) })
it('renders Notification.vue for each notification of the user', () => { 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', () => { it('renders Notification.vue zero times', () => {
expect(wrapper.findAll(Notification)).toHaveLength(0) expect(wrapper.findAllComponents(Notification)).toHaveLength(0)
}) })
}) })
}) })

View File

@ -1,12 +1,10 @@
import { config, mount, RouterLinkStub } from '@vue/test-utils' import { mount, RouterLinkStub } from '@vue/test-utils'
import NotificationMenu from './NotificationMenu' import NotificationMenu from './NotificationMenu'
const localVue = global.localVue const localVue = global.localVue
localVue.filter('truncate', (string) => string) localVue.filter('truncate', (string) => string)
config.stubs.dropdown = '<span class="dropdown"><slot :toggleMenu="() => null" /></span>'
describe('NotificationMenu.vue', () => { describe('NotificationMenu.vue', () => {
let wrapper let wrapper
let mocks let mocks
@ -23,6 +21,8 @@ describe('NotificationMenu.vue', () => {
} }
stubs = { stubs = {
NuxtLink: RouterLinkStub, NuxtLink: RouterLinkStub,
UserTeaser: true,
'client-only': true,
} }
}) })
@ -38,13 +38,13 @@ describe('NotificationMenu.vue', () => {
it('renders as link without counter', () => { it('renders as link without counter', () => {
wrapper = Wrapper() wrapper = Wrapper()
expect(wrapper.is('a.notifications-menu')).toBe(true) expect(wrapper.classes('notifications-menu')).toBe(true)
expect(() => wrapper.get('.count')).toThrow() expect(() => wrapper.get('.count')).toThrow()
}) })
it('no dropdown is rendered', () => { it('no dropdown is rendered', () => {
wrapper = Wrapper() wrapper = Wrapper()
expect(wrapper.contains('.dropdown')).toBe(false) expect(wrapper.find('.dropdown').exists()).toBe(false)
}) })
describe('given only read notifications', () => { describe('given only read notifications', () => {
@ -73,13 +73,13 @@ describe('NotificationMenu.vue', () => {
it('renders as link without counter', () => { it('renders as link without counter', () => {
wrapper = Wrapper() wrapper = Wrapper()
expect(wrapper.is('a.notifications-menu')).toBe(true) expect(wrapper.classes('notifications-menu')).toBe(true)
expect(() => wrapper.get('.count')).toThrow() expect(() => wrapper.get('.count')).toThrow()
}) })
it('no dropdown is rendered', () => { it('no dropdown is rendered', () => {
wrapper = Wrapper() 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', name: 'John Doe',
}, },
}, },
from: {
title: 'Title',
author: {
id: 'reporter',
slug: 'reporter',
name: 'reporter',
},
},
}, },
{ {
id: 'notification-42', id: 'notification-42',
@ -115,6 +123,14 @@ describe('NotificationMenu.vue', () => {
name: 'John Doe', name: 'John Doe',
}, },
}, },
from: {
title: 'Title',
author: {
id: 'reporter',
slug: 'reporter',
name: 'reporter',
},
},
}, },
{ {
id: 'notification-43', id: 'notification-43',
@ -129,6 +145,14 @@ describe('NotificationMenu.vue', () => {
name: 'John Doe', name: 'John Doe',
}, },
}, },
from: {
title: 'Title',
author: {
id: 'reporter',
slug: 'reporter',
name: 'reporter',
},
},
}, },
], ],
} }

Some files were not shown because too many files have changed in this diff Show More