diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 1d2aef77e..595c9d584 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -1,25 +1,9 @@
---
-name: 🐛 Bug report
-about: Create a report to help us improve
+name: 🐛 Bug Report
+about: Create a report to help us to improve.
labels: bug
title: 🐛 [Bug]
---
-## :bug: Bugreport
+## :bug: Bug Report
-
-### Steps to reproduce the behavior
-1.
-2.
-3.
-4. ...
-5. Profit
-
-### Expected behavior
-
-
-### Version & Environment
-
-
-### Additional context
-
diff --git a/.github/ISSUE_TEMPLATE/devops_ticket.md b/.github/ISSUE_TEMPLATE/devops_ticket.md
index 77355d1cf..115664911 100644
--- a/.github/ISSUE_TEMPLATE/devops_ticket.md
+++ b/.github/ISSUE_TEMPLATE/devops_ticket.md
@@ -1,24 +1,9 @@
---
-name: 💥 DevOps ticket
-about: Help us manage our deployed App.
+name: 💥 DevOps Ticket
+about: Help us manage our deployed app.
labels: devops
title: 💥 [DevOps]
---
-## 💥 DevOps ticket
+## 💥 DevOps Ticket
-
-### Motive
-
-
-### Related issues
-
-
-### Implementation
-
-
-### Validation
-
-
-### Additional context
-
diff --git a/.github/ISSUE_TEMPLATE/epic.md b/.github/ISSUE_TEMPLATE/epic.md
new file mode 100644
index 000000000..cf72cd673
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/epic.md
@@ -0,0 +1,12 @@
+---
+name: 🌟 Epic
+about: Define a big development step.
+labels: epic
+title: 🌟 [EPIC]
+---
+
+
+
+## 🌟 EPIC
+
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index ef3b30be2..beae80901 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -1,24 +1,9 @@
---
-name: 🚀 Feature request
-about: Suggest an idea for this project
+name: 🚀 Feature Request
+about: Suggest an idea for this project.
labels: feature
title: 🚀 [Feature]
---
-## :rocket: Feature
+## :rocket: Feature Request
-
-### User Problem
-
-
-### Implementation
-
-
-### Design & Layout
-
-
-### Validation
-
-
-### Additional context
-
diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md
index aabbc0f0a..40e6e381b 100644
--- a/.github/ISSUE_TEMPLATE/question.md
+++ b/.github/ISSUE_TEMPLATE/question.md
@@ -1,12 +1,12 @@
---
name: 💬 Question
-about: If you need help understanding HumanConnection.
+about: If you need help understanding ocelot.social.
labels: question
title: 💬 [Question]
---
-
-
+
+
-## :speech_balloon: Question
+## 💬 Question
diff --git a/.github/ISSUE_TEMPLATE/refactor_tickets.md b/.github/ISSUE_TEMPLATE/refactor_tickets.md
index b595abd5d..d1841e35e 100644
--- a/.github/ISSUE_TEMPLATE/refactor_tickets.md
+++ b/.github/ISSUE_TEMPLATE/refactor_tickets.md
@@ -1,21 +1,10 @@
---
-name: 🔧 Refactor ticket
+name: 🔧 Refactor
about: Help us improve our code by refactoring it.
labels: refactor
title: 🔧 [Refactor]
---
-## :zap: Refactor ticket
+## 🔧 Refactor
-### Motive
-
-
-### Related issues
-
-
-### Implementation
-
-
-### Additional context
-
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 000000000..c5433c921
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,179 @@
+version: 2
+updates:
+- package-ecosystem: npm
+ directory: "/"
+ schedule:
+ interval: daily
+ time: "04:00"
+ open-pull-requests-limit: 10
+ ignore:
+ - dependency-name: cypress
+ versions:
+ - 6.3.0
+ - 6.4.0
+ - 6.5.0
+ - 6.6.0
+ - 6.7.1
+ - 6.8.0
+ - 7.0.0
+ - 7.0.1
+ - 7.1.0
+ - dependency-name: cypress-cucumber-preprocessor
+ versions:
+ - 4.0.0
+ - 4.0.1
+ - 4.0.3
+ - dependency-name: date-fns
+ versions:
+ - 2.16.1
+ - 2.17.0
+ - 2.18.0
+ - 2.19.0
+ - 2.20.0
+ - 2.20.1
+ - 2.20.2
+ - 2.20.3
+ - 2.21.0
+ - dependency-name: cypress-file-upload
+ versions:
+ - 5.0.2
+ - 5.0.3
+ - 5.0.4
+ - 5.0.5
+ - dependency-name: neo4j-driver
+ versions:
+ - 4.2.2
+- package-ecosystem: npm
+ directory: "/backend"
+ schedule:
+ interval: daily
+ time: "04:00"
+ open-pull-requests-limit: 10
+ ignore:
+ - dependency-name: y18n
+ versions:
+ - 4.0.1
+ - 4.0.2
+ - dependency-name: metascraper-publisher
+ versions:
+ - 5.16.16
+ - 5.18.1
+ - 5.18.12
+ - 5.18.2
+ - 5.18.4
+ - 5.18.5
+ - 5.18.6
+ - 5.18.9
+ - 5.20.0
+ - 5.21.0
+ - 5.21.2
+ - 5.21.3
+ - 5.21.4
+ - 5.21.5
+ - dependency-name: metascraper-author
+ versions:
+ - 5.16.16
+ - 5.18.1
+ - 5.18.12
+ - 5.18.2
+ - 5.18.4
+ - 5.18.5
+ - 5.18.6
+ - 5.18.9
+ - 5.20.0
+ - 5.21.0
+ - 5.21.2
+ - 5.21.3
+ - 5.21.4
+ - 5.21.5
+ - dependency-name: neo4j-driver
+ versions:
+ - 4.2.2
+ - dependency-name: neo4j-graphql-js
+ versions:
+ - 2.19.1
+ - dependency-name: mustache
+ versions:
+ - 4.1.0
+- package-ecosystem: npm
+ directory: "/webapp"
+ schedule:
+ interval: daily
+ time: "04:00"
+ open-pull-requests-limit: 10
+ ignore:
+ - dependency-name: nuxt
+ versions:
+ - 2.14.12
+ - 2.15.0
+ - 2.15.1
+ - 2.15.2
+ - 2.15.3
+ - dependency-name: v-tooltip
+ versions:
+ - 2.1.2
+ - dependency-name: "@vue/server-test-utils"
+ versions:
+ - 1.1.2
+ - 1.1.3
+ - dependency-name: node-notifier
+ versions:
+ - 8.0.1
+- package-ecosystem: docker
+ directory: "/webapp"
+ schedule:
+ interval: daily
+ time: "04:00"
+ open-pull-requests-limit: 10
+ ignore:
+ - dependency-name: node
+ versions:
+ - ">= 15.5.a, < 15.6"
+ - dependency-name: node
+ versions:
+ - 15.10.0.pre.alpine3.10
+ - 15.11.0.pre.alpine3.10
+ - 15.12.0.pre.alpine3.10
+ - 15.13.0.pre.alpine3.10
+ - 15.7.0.pre.alpine3.10
+ - 15.8.0.pre.alpine3.10
+ - 15.9.0.pre.alpine3.10
+- package-ecosystem: docker
+ directory: "/backend"
+ schedule:
+ interval: daily
+ time: "04:00"
+ open-pull-requests-limit: 10
+ ignore:
+ - dependency-name: node
+ versions:
+ - ">= 15.4.a, < 15.5"
+ - dependency-name: node
+ versions:
+ - ">= 15.5.a, < 15.6"
+ - dependency-name: node
+ versions:
+ - 15.10.0.pre.alpine3.10
+ - 15.11.0.pre.alpine3.10
+ - 15.12.0.pre.alpine3.10
+ - 15.13.0.pre.alpine3.10
+ - 15.7.0.pre.alpine3.10
+ - 15.8.0.pre.alpine3.10
+ - 15.9.0.pre.alpine3.10
+- package-ecosystem: docker
+ directory: "/neo4j"
+ schedule:
+ interval: daily
+ time: "04:00"
+ open-pull-requests-limit: 10
+ ignore:
+ - dependency-name: neo4j
+ versions:
+ - 4.2.3
+ - 4.2.4
+- package-ecosystem: docker
+ directory: "/deployment/legacy-migration/maintenance-worker"
+ schedule:
+ interval: daily
+ time: "04:00"
+ open-pull-requests-limit: 10
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 24350a81a..1c7927665 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -4,6 +4,7 @@ on:
push:
branches:
- master
+ # - 4451-new-deployment-with-base-and-code # for testing while developing
jobs:
##############################################################################
@@ -55,9 +56,9 @@ jobs:
# NEO4J ##################################################################
##########################################################################
- name: Neo4J | Build `community` image
- run: |
- docker build --target community -t "ocelotsocialnetwork/neo4j:latest" -t "ocelotsocialnetwork/neo4j:community" -t "ocelotsocialnetwork/neo4j:${VERSION}" -t "ocelotsocialnetwork/neo4j:${BUILD_VERSION}" neo4j/
- docker save "ocelotsocialnetwork/neo4j" > /tmp/neo4j.tar
+ run: docker build --target community -t "ocelotsocialnetwork/neo4j:latest" -t "ocelotsocialnetwork/neo4j:community" -t "ocelotsocialnetwork/neo4j:${VERSION}" -t "ocelotsocialnetwork/neo4j:${BUILD_VERSION}" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT neo4j/
+ - name: Neo4J | Save docker image
+ run: docker save "ocelotsocialnetwork/neo4j" > /tmp/neo4j.tar
- name: Upload Artifact
uses: actions/upload-artifact@v2
with:
@@ -91,10 +92,13 @@ jobs:
##########################################################################
# BUILD BACKEND DOCKER IMAGE (production) ################################
##########################################################################
- - name: backend | Build `production` image
+ - name: Backend | Build `production` image
run: |
- docker build --target production -t "ocelotsocialnetwork/backend:latest" -t "ocelotsocialnetwork/backend:${VERSION}" -t "ocelotsocialnetwork/backend:${BUILD_VERSION}" backend/
- docker save "ocelotsocialnetwork/backend" > /tmp/backend.tar
+ docker build --target base -t "ocelotsocialnetwork/backend:latest-base" -t "ocelotsocialnetwork/backend:${VERSION}-base" -t "ocelotsocialnetwork/backend:${BUILD_VERSION}-base" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT backend/
+ docker build --target code -t "ocelotsocialnetwork/backend:latest-code" -t "ocelotsocialnetwork/backend:${VERSION}-code" -t "ocelotsocialnetwork/backend:${BUILD_VERSION}-code" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT backend/
+ docker build --target production -t "ocelotsocialnetwork/backend:latest" -t "ocelotsocialnetwork/backend:${VERSION}" -t "ocelotsocialnetwork/backend:${BUILD_VERSION}" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT backend/
+ - name: Backend | Save docker image
+ run: docker save "ocelotsocialnetwork/backend" > /tmp/backend.tar
- name: Upload Artifact
uses: actions/upload-artifact@v2
with:
@@ -128,10 +132,13 @@ jobs:
##########################################################################
# BUILD WEBAPP DOCKER IMAGE (build) ######################################
##########################################################################
- - name: webapp | Build `production` image
+ - name: Webapp | Build `production` image
run: |
- docker build --target production -t "ocelotsocialnetwork/webapp:latest" -t "ocelotsocialnetwork/webapp:${VERSION}" -t "ocelotsocialnetwork/webapp:${BUILD_VERSION}" webapp/
- docker save "ocelotsocialnetwork/webapp" > /tmp/webapp.tar
+ docker build --target base -t "ocelotsocialnetwork/webapp:latest-base" -t "ocelotsocialnetwork/webapp:${VERSION}-base" -t "ocelotsocialnetwork/webapp:${BUILD_VERSION}-base" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT webapp/
+ docker build --target code -t "ocelotsocialnetwork/webapp:latest-code" -t "ocelotsocialnetwork/webapp:${VERSION}-code" -t "ocelotsocialnetwork/webapp:${BUILD_VERSION}-code" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT webapp/
+ docker build --target production -t "ocelotsocialnetwork/webapp:latest" -t "ocelotsocialnetwork/webapp:${VERSION}" -t "ocelotsocialnetwork/webapp:${BUILD_VERSION}" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT webapp/
+ - name: Webapp | Save docker image
+ run: docker save "ocelotsocialnetwork/webapp" > /tmp/webapp.tar
- name: Upload Artifact
uses: actions/upload-artifact@v2
with:
@@ -165,11 +172,13 @@ jobs:
##########################################################################
# BUILD MAINTENANCE DOCKER IMAGE (build) #################################
##########################################################################
- - name: maintenance | Build `production` image
- # TODO: --target production
+ - name: Maintenance | Build `production` image
run: |
- docker build -t "ocelotsocialnetwork/maintenance:latest" -t "ocelotsocialnetwork/maintenance:${VERSION}" -t "ocelotsocialnetwork/maintenance:${BUILD_VERSION}" webapp/ -f webapp/Dockerfile.maintenance
- docker save "ocelotsocialnetwork/maintenance" > /tmp/maintenance.tar
+ docker build --target base -t "ocelotsocialnetwork/maintenance:latest-base" -t "ocelotsocialnetwork/maintenance:${VERSION}-base" -t "ocelotsocialnetwork/maintenance:${BUILD_VERSION}-base" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT webapp/ -f webapp/Dockerfile.maintenance
+ docker build --target code -t "ocelotsocialnetwork/maintenance:latest-code" -t "ocelotsocialnetwork/maintenance:${VERSION}-code" -t "ocelotsocialnetwork/maintenance:${BUILD_VERSION}-code" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT webapp/ -f webapp/Dockerfile.maintenance
+ docker build --target production -t "ocelotsocialnetwork/maintenance:latest" -t "ocelotsocialnetwork/maintenance:${VERSION}" -t "ocelotsocialnetwork/maintenance:${BUILD_VERSION}" --build-arg BBUILD_DATE=$BUILD_DATE --build-arg BBUILD_VERSION=$BUILD_VERSION --build-arg BBUILD_COMMIT=$BUILD_COMMIT webapp/ -f webapp/Dockerfile.maintenance
+ - name: Maintenance | Save docker image
+ run: docker save "ocelotsocialnetwork/maintenance" > /tmp/maintenance.tar
- name: Upload Artifact
uses: actions/upload-artifact@v2
with:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 276589dc0..2bec6cebb 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -194,7 +194,6 @@ jobs:
##########################################################################
# UNIT TESTS BACKEND #####################################################
##########################################################################
- # TODO: Why do we need those .envs?
- name: backend | copy env files webapp
run: cp webapp/.env.template webapp/.env
- name: backend | copy env files backend
@@ -205,6 +204,17 @@ jobs:
run: docker-compose exec -T backend yarn db:migrate init
- name: backend | Unit test
run: docker-compose exec -T backend yarn test
+ ##########################################################################
+ # COVERAGE CHECK BACKEND #################################################
+ ##########################################################################
+ - name: backend | Coverage check
+ uses: webcraftmedia/coverage-check-action@master
+ with:
+ report_name: Coverage Backend
+ type: lcov
+ result_path: ./coverage/lcov.info
+ min_coverage: 58
+ token: ${{ github.token }}
##############################################################################
# JOB: UNIT TEST WEBAPP ######################################################
@@ -230,9 +240,8 @@ jobs:
- name: Load Docker Image
run: docker load < /tmp/webapp.tar
##########################################################################
- # UNIT TESTS WEBAPP #####################################################
+ # UNIT TESTS WEBAPP ######################################################
##########################################################################
- # TODO: Why do we need those .envs?
- name: backend | copy env files webapp
run: cp webapp/.env.template webapp/.env
- name: backend | copy env files backend
@@ -240,5 +249,93 @@ jobs:
- name: backend | docker-compose
run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp
- name: webapp | Unit tests
- #run: docker run --rm ocelotsocialnetwork/webapp:build yarn run test
- run: docker-compose exec -T webapp yarn test
\ No newline at end of file
+ run: docker-compose exec -T webapp yarn test
+ ##########################################################################
+ # COVERAGE REPORT FRONTEND ################################################
+ ##########################################################################
+ #- name: frontend | Coverage report
+ # uses: romeovs/lcov-reporter-action@v0.2.21
+ # with:
+ # github-token: ${{ secrets.GITHUB_TOKEN }}
+ # lcov-file: ./coverage/lcov.info
+ ##########################################################################
+ # COVERAGE CHECK WEBAPP ##################################################
+ ##########################################################################
+ - name: webapp | Coverage check
+ uses: webcraftmedia/coverage-check-action@master
+ with:
+ report_name: Coverage Webapp
+ type: lcov
+ result_path: ./coverage/lcov.info
+ min_coverage: 65
+ token: ${{ github.token }}
+
+ ##############################################################################
+ # JOB: FULLSTACK TESTS #######################################################
+ ##############################################################################
+ fullstack_tests:
+ name: Fullstack tests
+ runs-on: ubuntu-latest
+ needs: [build_test_webapp, build_test_backend, build_test_neo4j]
+ env:
+ jobs: 8
+ strategy:
+ matrix:
+ # run copies of the current job in parallel
+ job: [1, 2, 3, 4, 5, 6, 7, 8]
+ steps:
+ ##########################################################################
+ # CHECKOUT CODE ##########################################################
+ ##########################################################################
+ - name: Checkout code
+ uses: actions/checkout@v2
+ ##########################################################################
+ # DOWNLOAD DOCKER IMAGES #################################################
+ ##########################################################################
+ - name: Download Docker Image (Neo4J)
+ uses: actions/download-artifact@v2
+ with:
+ name: docker-neo4j-image
+ path: /tmp
+ - name: Load Docker Image
+ run: docker load < /tmp/neo4j.tar
+ - name: Download Docker Image (Backend)
+ uses: actions/download-artifact@v2
+ with:
+ name: docker-backend-test
+ path: /tmp
+ - name: Load Docker Image
+ run: docker load < /tmp/backend.tar
+ - name: Download Docker Image (Webapp)
+ uses: actions/download-artifact@v2
+ with:
+ name: docker-webapp-test
+ path: /tmp
+ - name: Load Docker Image
+ run: docker load < /tmp/webapp.tar
+ ##########################################################################
+ # FULLSTACK TESTS CYPRESS ################################################
+ ##########################################################################
+ - name: webapp | copy env files webapp
+ run: cp webapp/.env.template webapp/.env
+ - name: backend | copy env files backend
+ run: cp backend/.env.template backend/.env
+ - name: backend | docker-compose
+ run: docker-compose -f docker-compose.yml -f docker-compose.test.yml up --detach --no-deps webapp neo4j backend
+ - name: cypress | Fullstack tests
+ run: |
+ yarn install
+ yarn run cypress:run --spec $(cypress/parallel-features.sh ${{ matrix.job }} ${{ env.jobs }} )
+ ##########################################################################
+ # UPLOAD SCREENSHOTS & VIDEO #############################################
+ ##########################################################################
+ - name: Upload Artifact
+ uses: actions/upload-artifact@v2
+ with:
+ name: cypress-screenshots
+ path: cypress/screenshots/
+ - name: Upload Artifact
+ uses: actions/upload-artifact@v2
+ with:
+ name: cypress-videos
+ path: cypress/videos/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 09fbb1478..e7e69342a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,16 +4,85 @@ All notable changes to this project will be documented in this file. Dates are d
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
-#### [v0.6.15](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.14...v0.6.15)
+#### [v1.0.4](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/1.0.3...v1.0.4)
+- fixed wrong env variable [`#4474`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4474)
+- chore: [WIP] 🍰 New Deployment With 'base' And 'code' Docker Images [`#4452`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4452)
+- feat: 🍰 Flexible Footer Links [`#4468`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4468)
+- docs: 🍰 Correct 'Contribution.md' [`#4466`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4466)
+- docs: 🍰 Correct Discord Links And Divers [`#4461`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4461)
+- Implement flexible page footer links [`1bd4af6`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/1bd4af6fd3b5db167575910948a0a72461a1129a)
+- Implement tests for flexible page footer links [`627a20f`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/627a20f66a65450996a5fe3128fd37769fdfd629)
+- Correct Discord links and divers [`0318910`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/0318910488e245c4a1d09181265de63d05a89cf1)
+
+#### [1.0.3](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/1.0.2...1.0.3)
+
+> 19 May 2021
+
+- chore: 🍰 Release v1.0.3 [`#4435`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4435)
+- chore: 🍰 Refactor Logos [`#4433`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4433)
+- feat: 🍰 Show Password Component [`#4370`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4370)
+- chore: 🍰 Replace Ocelot Logos 619x593 With 600x570 [`#4428`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4428)
+- Coverage [`#4393`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4393)
+- correct_docker_tagging [`#4391`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4391)
+- 🍰 Get Cypress Tests Running Again [`#4338`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4338)
+- frontend + backend coverage tests [`#4367`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4367)
+- refactor: 🍰 Refactor E-Mail Templates [`#4350`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4350)
+- feat: 🍰 Remove More-Info Of Post [`#4316`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4316)
+- have cypress running locally - the tests still fail [`e3e0341`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/e3e03415e1bd9e0be88f33930a52e63d8af64ee1)
+- have cypress running locally - the tests still fail [`0ec0574`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/0ec05743751cfef0cb86c17b87e9e4ef9a2c9e47)
+- Refactor logos, first step [`ff6cc30`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/ff6cc306aff6150a924f1a647387e498d050ea9f)
+
+#### [1.0.2](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/1.0.1...1.0.2)
+
+> 6 April 2021
+
+- fix: Email Confirmation-Link When An Invite-Code Is Given [`#4336`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4336)
+- release v1.0.2 [`e583010`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/e5830101e4b449905fe2d0018627d75af62b2a20)
+- slider jumps to enter-nonce when link contains invite-code, email and nonce and method is invite-code. Thanks @tirokk [`c80b3a2`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/c80b3a212835f76cfc2f11542345d6c3b226995b)
+- fix enail confirmation link when an invite-code is given [`27f0de9`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/27f0de9464685c8f960bde9d07986fdc5b20f8f9)
+
+#### [1.0.1](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/1.0.0...1.0.1)
+
+> 4 April 2021
+
+- readme_update [`#4331`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4331)
+- jwt_expiretime [`#4330`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4330)
+- Clean env, docker & workflow [`#4337`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4337)
+- fix: 🍰 Fixing The Avatars unwanted Border [`#4320`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4320)
+- Change background color of avatar image to white [`e48a99a`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/e48a99afe63e96cffafe16db7bf5ae35cfdebd7a)
+- removed config warning [`f6c070a`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/f6c070a3cb36c5593123b27b4d1b6a5b7a10aba3)
+- include env files in build process [`dfe6f67`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/dfe6f679b3bb0f440aee986d6d12925cccee9050)
+
+### [1.0.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.15...1.0.0)
+
+> 31 March 2021
+
+- v1.0.0 [`#4321`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4321)
+- fix: 🐛 Fix Wrong Truncation Of Hashtags In Admin List Via Adding 'truncateStr' [`#4314`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4314)
+- feat: 🍰Implement Registration Slider [`#4270`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4270)
+- feat: 🍰 Invite Button [`#4301`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4301)
+- fix: 🍰 Suggestion List Filter [`#4296`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4296)
+- fix: JWT Expires In 2 Years [`#4278`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4278)
+- Deployment [`#4263`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4263)
+- feat: 🍰 Redesign Registration Process Frontend [`#4168`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4168)
+- Delete unnecessary code [`3fa7e04`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/3fa7e04d4895161db1f764ccad58e35188c9d065)
+- basic invite button in frontend [`356f026`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/356f02622655a57561fcdecfcc8a735d23cac79f)
+- setting up invite button [`e6dc3f4`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/e6dc3f42cedaf9953d737cf30cf7ed317b634be7)
+
+#### [0.6.15](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.14...0.6.15)
+
+> 1 March 2021
+
+- feat: Count Post Teaser Views [`#4255`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4255)
- feat: 🍰 Count Post Clicks [`#4248`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4248)
- Correct version style [`#4218`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4218)
- dashboard restructuring image [`#4266`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4266)
- fix: 🐛 Adapted Editor List Styles [`#4239`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4239)
- fix: Scrolling On Profile Page [`#4234`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4234)
+- release 0.6.15 [`ef4265d`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/ef4265d9387d94aa09db2b80461c2ec90b4623ae)
- count views of post teaser [`1c3f628`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/1c3f628fb2e161400319b32da274952c1b57836e)
- tests fixed for clickedCount [`96066ea`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/96066eae02e659a00bce280f9f97a28ac1446ce0)
-- add clickedCount to PostTeaser [`d3eafc9`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/d3eafc9b69c70e31021c6e2723bd5bcfa6b9d17f)
#### [0.6.14](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.13...0.6.14)
@@ -99,21 +168,11 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- file upload: refactored [`650e83f`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/650e83f4c250389477933a2e7d21d8245b0ce882)
- change user role: tests are working [`14dfe2a`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/14dfe2ae2cd4a24c06c9229893b33586dfceae4f)
-#### [0.6.5](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/v0.6.4...0.6.5)
+#### [0.6.5](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.4...0.6.5)
> 8 February 2021
-- - adjusted changelog to ocelot-social repo [`9603882`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/9603882edebf8967e05abfa94e4e1ebf452d4e24)
-- - first steps towards docker image deployment & github autotagging [`5503216`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/5503216ad4a0230ac533042e4a69806590fc2a5a)
-- - deploy structure image [`a60400b`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/a60400b4fe6f59bbb80e1073db4def3ba205e1a7)
-
-#### [v0.6.4](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.4...v0.6.4)
-
-> 9 February 2021
-
-- chore(release): 0.6.4 [`8b7570d`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/8b7570dc35d0ea431f673a711ac051f1e1320acb)
-- change user roles is working, test fails [`8c3310a`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/8c3310abaf87c0e5597fec4f93fb37d27122c9e7)
-- change user role: tests are working [`f10da4b`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/f10da4b09388fe1e2b85abd53f6ffc67c785d4c1)
+- updated CHANGELOG.md [`9d9075f`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/9d9075f2117b2eb4b607e7d59ab18c7e655c6ea7)
#### [0.6.4](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.3...0.6.4)
@@ -123,15 +182,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- fetch full history [`5ecee4d`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/5ecee4d73a92d2e5c5ae971d79848ed27f65a72c)
- don't fail if tag exists (release) [`39c82fc`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/39c82fcb37d5c8e7e78a79288e1ef6280f8d0892)
-#### [0.6.3](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/v0.6.3...0.6.3)
-
-> 8 February 2021
-
-- - adjusted changelog to ocelot-social repo [`9603882`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/9603882edebf8967e05abfa94e4e1ebf452d4e24)
-- - fixed changelog [`cf70b12`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/cf70b12ed74011924ea788ab932fc9d7ac0e6bd9)
-- - yarn install to allow yarn auto-changelog [`fc496aa`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/fc496aa04cb7e804da4335da0cb5cda26f874ea2)
-
-#### [v0.6.3](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.0...v0.6.3)
+#### [0.6.3](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/0.6.0...0.6.3)
> 8 February 2021
@@ -140,9 +191,9 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
- feat: 🍰 Allow Only Supported Image File Formats [`#3928`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/3928)
- refactor: Disbale Emoji, Language And Catgeory Filter [`#4193`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4193)
- refactor: Remove Catgeories From Post Teaser [`#4191`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/4191)
+- - adjusted changelog to ocelot-social repo [`9603882`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/9603882edebf8967e05abfa94e4e1ebf452d4e24)
- - first steps towards docker image deployment & github autotagging [`5503216`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/5503216ad4a0230ac533042e4a69806590fc2a5a)
- - lots of additional tests [`0ba37aa`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/0ba37aab18f537d722aede7b87fa0b8e79f80e66)
-- - deploy structure image [`a60400b`](https://github.com/Ocelot-Social-Community/Ocelot-Social/commit/a60400b4fe6f59bbb80e1073db4def3ba205e1a7)
#### [0.6.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/v0.6.0...0.6.0)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b5f74d879..50d3721d1 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -4,31 +4,33 @@ Thank you so much for thinking of contributing to the Human Connection project!
## Getting Set Up
-Instructions for how to install all the necessary software and some code guidelines can be found in our [documentation](https://docs.human-connection.org/human-connection/).
+Instructions for how to install all the necessary software and some code guidelines can be found in our main [Readme](/README.md) or in our [documentation](https://docs.human-connection.org/human-connection/).
-To get you started we recommend that you join forces with a regular contributor. Please join [our discord instance](https://human-connection.org/discord) to chat with developers or just get in touch directly on an issue on either [Github](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues) or [Zenhub](https://app.zenhub.com/workspaces/ocelotsocial-5fb21ff922cb410015dd6535/board?filterLogic=any&repos=301151089):
+To get you started we recommend that you join forces with a regular contributor. Please join [our Discord instance](https://discord.gg/AJSX9DCSUA) to chat with developers or just get in touch directly on an issue on either [Github](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues) or [Zenhub](https://app.zenhub.com/workspaces/ocelotsocial-5fb21ff922cb410015dd6535/board?filterLogic=any&repos=301151089):

-We also have regular pair programming sessions that you are very welcome to join! We feel this is often the best way to get to know both the project and the team. Most developers are also available for spontaneous sessions if the times listed below don't work for you – just ping us on discord.
+We also have regular pair programming sessions that you are very welcome to join! We feel this is often the best way to get to know both the project and the team. Most developers are also available for spontaneous sessions if the times listed below don't work for you – just ping us on Discord.
## Development Flow
We operate in two week sprints that are planned, estimated and prioritised on [Zenhub](https://app.zenhub.com/workspaces/ocelotsocial-5fb21ff922cb410015dd6535/board?filterLogic=any&repos=301151089). All issues are also linked to and synced with [Github](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues). Look for the `good first issue` label if you're not sure where to start!
-We try to discuss all questions directly related to a feature or bug in the respective issue, in order to preserve it for the future and for other developers. We use discord for real-time communication.
+We try to discuss all questions directly related to a feature or bug in the respective issue, in order to preserve it for the future and for other developers. We use Discord for real-time communication.
This is how we solve bugs and implement features, step by step:
+
1. We find an issue we want to work on, usually during the sprint planning but as an open source contributor this can happen at any time.
2. We communicate with the team to see if the issue is still available. (When you comment on an issue but don't get an answer there within 1-2 days try to mention @Human-Connection/hc-dev-team to make sure we check in.)
3. We make sure we understand the issue in detail – what problem is it solving and how should it be implemented?
-4. We assign ourselves to the issue and move it to `In Progress` on [Zenhub](https://app.zenhub.com/workspaces/human-connection-nitro-5c0154ecc699f60fc92cf11f).
+4. We assign ourselves to the issue and move it to `In Progress` on [Zenhub](https://app.zenhub.com/workspaces/ocelotsocial-5fb21ff922cb410015dd6535/board?filterLogic=any&repos=301151089).
5. We start working on it in a `new branch` and open a `pull request` prefixed with `[WIP]` (work in progress) to which we regularly push our changes.
6. When questions come up we clarify them with the team (directly in the issue on Github).
7. When we are happy with our work and our PR is passing all tests we remove the `[WIP]` from the PR description and ask for reviews (if you're not sure who to ask there is @Human-Connection/hc-dev-team which pings all core developers).
8. We then incorporate the suggestions from the reviews into our work and once it has been approved it can be merged into master!
Every pull request needs to:
+
* fix an issue (if there is something you want to work on but there is no issue for it, create one first and discuss it with the team)
* include tests for the code that is added or changed
* pass all tests (linter, backend, frontend, end-to-end)
@@ -38,37 +40,46 @@ Every pull request needs to:
There are many volunteers all around the world helping us build this network and without their contributions we wouldn't be where we are today. Big thank you to all of you!
-You can see the core team behind Human Connection [on our website](https://human-connection.org/en/the-team/). On Github you will mostly run into our developers:
-* Robert (@roschaefer)
-* Matt (@mattwr18)
+You can talk to our core team on [Discord](https://discord.gg/AJSX9DCSUA). And on Github you will mostly run into our core developers:
+
+* Ulf (@ulfgebhardt)
+* Moriz (@Mogge)
* Wolle (@Tirokk)
* Alex (@ogerly)
+
+
## Meetings and Pair Programming Sessions
Times below refer to **German Time** – that's CET (GMT+1) in winter and CEST (GMT+2) in summer – because most Human Connection core team members are living in Germany.
Daily standup
-* every Monday–Friday 11:30
-* in the discord `Conference Room`
+
+* every Monday–Thursday 11:30 am (german time see above 👆🏼)
+* in our [Discord](https://discord.gg/AJSX9DCSUA) `Office Cube`
* all contributors welcome!
* everybody shares what they are working on and asks for help if they are blocked
+
## Philosophy
@@ -102,10 +115,9 @@ We use pair programming sessions as a tool for knowledge sharing. We can learn a
As a volunteeer you have no commitment except your own self development and your awesomeness by contributing to this free and open-source software project. Cheers to you!
-
## Open-Source Bounties
-There are so many good reasons to contribute to Human Connection
+There are so many good reasons to contribute to ocelot.social
* You learn state-of-the-art technologies
* You build your portfolio
@@ -121,7 +133,7 @@ pull request approved and merged for free**. You can choose something really
quick and easy. What's important is starting a working relationship with the
team, learning the workflow, and understanding this contribution guide. You can
filter issues by 'good first issue', to get an idea where to start. Please join
-our our [community chat](https://human-connection.org/discord), too.
+our our [Discord community chat](https://discord.gg/AJSX9DCSUA), too.
You can filter Github issues with label [bounty](https://github.com/Ocelot-Social-Community/Ocelot-Social/issues?q=is%3Aopen+is%3Aissue+label%3Abounty). These issues should have a second label `€`
which indicate their respective financial compensation in Euros.
diff --git a/README.md b/README.md
index 8b24a8388..416fcaefb 100644
--- a/README.md
+++ b/README.md
@@ -12,11 +12,13 @@ ocelot.social is a nonprofit social, action and knowledge network that connects
* **Knowledge**: Read articles about interesting topics and find related posts in the **More Info** tab or by **Filtering** based on **Categories** and **Tagging** or by using the **Fulltext Search**.
* **Action**: Don't just read about how to make the world a better place, but come into **Action** by following provided suggestions on the **Action** tab provided by other people or **Organisations**.
- [](https://ocelot.social)
+
+
+
## Live demo
-Try out our deployed [development environment](https://develop.human-connection.org/).
+__Try out our deployed [development environment](https://develop.human-connection.org/).__
Logins:
@@ -29,6 +31,7 @@ Logins:
## Directory Layout
There are four important directories:
+
* [Backend](./backend) runs on the server and is a middleware between database and frontend
* [Frontend](./webapp) is a server-side-rendered and client-side-rendered web frontend
* [Cypress](./cypress) contains end-to-end tests and executable feature specifications
@@ -44,16 +47,19 @@ There are two approaches:
## Installation
### Clone the Repository
+
Clone the repository, this will create a new folder called `Ocelot-Social`:
Using HTTPS:
+
```bash
$ git clone https://github.com/Ocelot-Social-Community/Ocelot-Social.git
```
Using SSH:
+
```bash
-$ git clone git@github.com:Human-Connection/Human-Connection.git
+$ git clone git@github.com:Ocelot-Social-Community/Ocelot-Social.git
```
Change into the new folder.
@@ -86,13 +92,15 @@ docker-compose version 1.23.2
#### Start Ocelot-Social via Docker-Compose
For Development:
+
```bash
-docker-compose up
+$ docker-compose up
```
-For Production
+For Production:
+
```bash
-docker-compose -f docker-compose.yml up
+$ docker-compose -f docker-compose.yml up
```
This will start all required Docker containers
@@ -105,12 +113,12 @@ The only deployment method in this repository is `docker-compose` for developmen
## Developer Chat
-Join our friendly open-source community on [Discord](https://discordapp.com/invite/DFSjPaX) :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 `@@Mentor` to get you onboard :neckbeard:
Check out the [contribution guideline](./CONTRIBUTING.md), too!
We give write permissions to every developer who asks for it. Just text us on
-[Discord](https://discord.gg/6ub73U3).
+[Discord](https://discord.gg/AJSX9DCSUA).
## Technology Stack
@@ -129,4 +137,5 @@ Browser compatibility testing with [BrowserStack](https://www.browserstack.com/)
## License
+
See the [LICENSE](LICENSE.md) file for license rights and limitations (MIT).
diff --git a/backend/.env.template b/backend/.env.template
index fc9766478..5858a5d1e 100644
--- a/backend/.env.template
+++ b/backend/.env.template
@@ -10,6 +10,7 @@ SMTP_USERNAME=
SMTP_PASSWORD=
JWT_SECRET="b/&&7b78BF&fv/Vd"
+JWT_EXPIRES="2y"
MAPBOX_TOKEN="pk.eyJ1IjoiYnVzZmFrdG9yIiwiYSI6ImNraDNiM3JxcDBhaWQydG1uczhpZWtpOW4ifQ.7TNRTO-o9aK1Y6MyW_Nd4g"
PRIVATE_KEY_PASSPHRASE="a7dsf78sadg87ad87sfagsadg78"
@@ -17,6 +18,7 @@ PRIVATE_KEY_PASSPHRASE="a7dsf78sadg87ad87sfagsadg78"
SENTRY_DSN_BACKEND=
COMMIT=
PUBLIC_REGISTRATION=false
+INVITE_REGISTRATION=true
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
diff --git a/backend/Dockerfile b/backend/Dockerfile
index 2f2b70f04..0ebdfb1eb 100644
--- a/backend/Dockerfile
+++ b/backend/Dockerfile
@@ -1,17 +1,20 @@
##################################################################################
-# BASE ###########################################################################
+# BASE (Is pushed to DockerHub for rebranding) ###################################
##################################################################################
FROM node:12.19.0-alpine3.10 as base
-# ENVs (available in production aswell, can be overwritten by commandline or env file)
+# ENVs
## DOCKER_WORKDIR would be a classical ARG, but that is not multi layer persistent - shame
ENV DOCKER_WORKDIR="/app"
## We Cannot do `$(date -u +'%Y-%m-%dT%H:%M:%SZ')` here so we use unix timestamp=0
-ENV BUILD_DATE="1970-01-01T00:00:00.00Z"
+ARG BBUILD_DATE="1970-01-01T00:00:00.00Z"
+ENV BUILD_DATE=$BBUILD_DATE
## We cannot do $(yarn run version)-${BUILD_NUMBER} here so we default to 0.0.0-0
-ENV BUILD_VERSION="0.0.0-0"
+ARG BBUILD_VERSION="0.0.0-0"
+ENV BUILD_VERSION=$BBUILD_VERSION
## We cannot do `$(git rev-parse --short HEAD)` here so we default to 0000000
-ENV BUILD_COMMIT="0000000"
+ARG BBUILD_COMMIT="0000000"
+ENV BUILD_COMMIT=$BBUILD_COMMIT
## SET NODE_ENV
ENV NODE_ENV="production"
## App relevant Envs
@@ -56,12 +59,18 @@ FROM base as development
CMD /bin/sh -c "yarn install && yarn run dev"
##################################################################################
-# BUILD (Does contain all files and is therefore bloated) ########################
+# CODE (Does contain all code files and is pushed to DockerHub for rebranding) ###
##################################################################################
-FROM base as build
+FROM base as code
-# Copy everything
+# copy everything, but do not build.
COPY . .
+
+##################################################################################
+# BUILD (Does contain all files and the compilate and is therefore bloated) ######
+##################################################################################
+FROM code as build
+
# yarn install
RUN yarn install --production=false --frozen-lockfile --non-interactive
# yarn build
@@ -82,7 +91,6 @@ FROM base as production
# Copy "binary"-files from build image
COPY --from=build ${DOCKER_WORKDIR}/dist ./dist
-COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules
# Copy static files
# TODO - externalize the uploads so we can copy the whole folder
COPY --from=build ${DOCKER_WORKDIR}/public/img/ ./public/img/
@@ -91,4 +99,4 @@ COPY --from=build ${DOCKER_WORKDIR}/public/providers.json ./public/providers.jso
COPY --from=build ${DOCKER_WORKDIR}/package.json ./package.json
# Run command
-CMD /bin/sh -c "yarn run start"
\ No newline at end of file
+CMD /bin/sh -c "yarn run start"
diff --git a/backend/package.json b/backend/package.json
index ff448e293..ac5c1d2f8 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -1,6 +1,6 @@
{
"name": "ocelot-social-backend",
- "version": "0.6.15",
+ "version": "1.0.4",
"description": "GraphQL Backend for ocelot.social",
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
"author": "ocelot.social Community",
@@ -15,7 +15,7 @@
"dev": "nodemon --exec babel-node src/ -e js,gql",
"dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/ -e js,gql",
"lint": "eslint src --config .eslintrc.js",
- "test": "cross-env NODE_ENV=test jest --forceExit --detectOpenHandles --runInBand",
+ "test": "cross-env NODE_ENV=test jest --forceExit --detectOpenHandles --runInBand --coverage",
"db:clean": "babel-node src/db/clean.js",
"db:reset": "yarn run db:clean",
"db:seed": "babel-node src/db/seed.js",
@@ -39,6 +39,12 @@
]
},
"dependencies": {
+ "@babel/cli": "~7.8.4",
+ "@babel/core": "~7.9.0",
+ "@babel/node": "~7.8.7",
+ "@babel/plugin-proposal-throw-expressions": "^7.8.3",
+ "@babel/preset-env": "~7.9.5",
+ "@babel/register": "^7.9.0",
"@hapi/joi": "^17.1.1",
"@sentry/node": "^5.15.4",
"apollo-cache-inmemory": "~1.6.5",
@@ -48,12 +54,15 @@
"apollo-server": "~2.14.2",
"apollo-server-express": "^2.14.2",
"aws-sdk": "^2.652.0",
+ "babel-core": "~7.0.0-0",
+ "babel-eslint": "~10.1.0",
+ "babel-jest": "~25.2.6",
"babel-plugin-transform-runtime": "^6.23.0",
"bcryptjs": "~2.4.3",
"cheerio": "~1.0.0-rc.3",
"cors": "~2.8.5",
"cross-env": "~7.0.2",
- "date-fns": "2.11.1",
+ "date-fns": "2.22.1",
"debug": "~4.1.1",
"dotenv": "~8.2.0",
"express": "^4.17.1",
@@ -72,7 +81,7 @@
"languagedetect": "^2.0.0",
"linkifyjs": "~2.1.8",
"lodash": "~4.17.14",
- "merge-graphql-schemas": "^1.7.7",
+ "merge-graphql-schemas": "^1.7.8",
"metascraper": "^5.11.8",
"metascraper-audio": "^5.14.26",
"metascraper-author": "^5.14.22",
@@ -92,7 +101,7 @@
"migrate": "^1.7.0",
"mime-types": "^2.1.26",
"minimatch": "^3.0.4",
- "mustache": "^4.0.1",
+ "mustache": "^4.2.0",
"neo4j-driver": "^4.0.2",
"neo4j-graphql-js": "^2.11.5",
"neode": "^0.3.7",
@@ -111,16 +120,7 @@
"xregexp": "^4.3.0"
},
"devDependencies": {
- "@babel/cli": "~7.8.4",
- "@babel/core": "~7.9.0",
- "@babel/node": "~7.8.7",
- "@babel/plugin-proposal-throw-expressions": "^7.8.3",
- "@babel/preset-env": "~7.9.5",
- "@babel/register": "^7.9.0",
"apollo-server-testing": "~2.11.0",
- "babel-core": "~7.0.0-0",
- "babel-eslint": "~10.1.0",
- "babel-jest": "~25.2.6",
"chai": "~4.2.0",
"cucumber": "~6.0.5",
"eslint": "~6.8.0",
@@ -134,7 +134,7 @@
"eslint-plugin-standard": "~4.0.1",
"jest": "~25.3.0",
"nodemon": "~2.0.2",
- "prettier": "~2.2.0",
+ "prettier": "~2.3.2",
"rosie": "^2.0.1",
"supertest": "~4.0.2"
},
diff --git a/backend/src/config/index.js b/backend/src/config/index.js
index 47771029b..f3a8fb63c 100644
--- a/backend/src/config/index.js
+++ b/backend/src/config/index.js
@@ -7,9 +7,8 @@ if (require.resolve) {
try {
dotenv.config({ path: require.resolve('../../.env') })
} catch (error) {
- if (error.code === 'MODULE_NOT_FOUND') {
- console.log('WARN: No `.env` file found in `/app` (docker) or `/backend` (no docker)') // eslint-disable-line no-console
- } else {
+ // This error is thrown when the .env is not found
+ if (error.code !== 'MODULE_NOT_FOUND') {
throw error
}
}
@@ -35,13 +34,14 @@ const required = {
const server = {
CLIENT_URI: env.CLIENT_URI || 'http://localhost:3000',
GRAPHQL_URI: env.GRAPHQL_URI || 'http://localhost:4000',
+ JWT_EXPIRES: env.JWT_EXPIRES || '2y',
}
const smtp = {
SMTP_HOST: env.SMTP_HOST,
SMTP_PORT: env.SMTP_PORT,
- SMTP_IGNORE_TLS: env.SMTP_IGNORE_TLS === 'true' || true,
- SMTP_SECURE: env.SMTP_IGNORE_TLS === 'true' || false,
+ SMTP_IGNORE_TLS: env.SMTP_IGNORE_TLS !== 'false', // default = true
+ SMTP_SECURE: env.SMTP_SECURE === 'true',
SMTP_USERNAME: env.SMTP_USERNAME,
SMTP_PASSWORD: env.SMTP_PASSWORD,
}
@@ -82,7 +82,8 @@ const options = {
SUPPORT_URL: links.SUPPORT,
APPLICATION_NAME: metadata.APPLICATION_NAME,
ORGANIZATION_URL: links.ORGANIZATION,
- PUBLIC_REGISTRATION: env.PUBLIC_REGISTRATION === 'true',
+ PUBLIC_REGISTRATION: env.PUBLIC_REGISTRATION === 'true' || false,
+ INVITE_REGISTRATION: env.INVITE_REGISTRATION !== 'false', // default = true
}
// Check if all required configs are present
diff --git a/backend/src/config/links.js b/backend/src/config/links.js
index 494e449ba..b111726f3 100644
--- a/backend/src/config/links.js
+++ b/backend/src/config/links.js
@@ -1,6 +1,17 @@
+// this file is duplicated in `backend/src/config/links.js` and `webapp/constants/links.js` and replaced on rebranding by https://github.com/Ocelot-Social-Community/Ocelot-Social-Deploy-Rebranding/tree/master/branding/constants/
export default {
+ LANDING_PAGE: '/login', // examples: '/login', '/registration', '/organization', or external 'https://ocelot.social'
+
+ // you can find and store templates at https://github.com/Ocelot-Social-Community/Ocelot-Social-Deploy-Rebranding/tree/master/branding/templates/
+
+ SUPPORT: 'https://ocelot.social', // example for internal support page: 'https://staging.ocelot.social/support'. set a full URL please, because it is used in e-mails as well!
+
+ // on null or empty strings internal pages are used, see 'webapp/locales/html/'
ORGANIZATION: 'https://ocelot.social',
- DONATE: 'https://ocelot-social.herokuapp.com/donations',
+ DONATE: 'https://ocelot-social.herokuapp.com/donations', // we use 'ocelot-social.herokuapp.com' at the moment, because redirections of 'ocelot.social' subpages are not working correctly
+ IMPRINT: 'https://ocelot-social.herokuapp.com/imprint', // we use 'ocelot-social.herokuapp.com' at the moment, because redirections of 'ocelot.social' subpages are not working correctly
+ TERMS_AND_CONDITIONS: null,
+ CODE_OF_CONDUCT: null,
+ DATA_PRIVACY: null,
FAQ: 'https://ocelot.social',
- SUPPORT: 'https://ocelot.social',
}
diff --git a/backend/src/config/logos.js b/backend/src/config/logos.js
new file mode 100644
index 000000000..d093c7b46
--- /dev/null
+++ b/backend/src/config/logos.js
@@ -0,0 +1,10 @@
+// this file is duplicated in `backend/src/config/logos.js` and `webapp/constants/logos.js` and replaced on rebranding
+// this are the paths in the webapp
+export default {
+ LOGO_HEADER_PATH: '/img/custom/logo-horizontal.svg',
+ LOGO_SIGNUP_PATH: '/img/custom/logo-squared.svg',
+ LOGO_WELCOME_PATH: '/img/custom/logo-squared.svg',
+ LOGO_LOGOUT_PATH: '/img/custom/logo-squared.svg',
+ LOGO_PASSWORD_RESET_PATH: '/img/custom/logo-squared.svg',
+ LOGO_MAINTENACE_RESET_PATH: '/img/custom/logo-squared.svg',
+}
diff --git a/backend/src/config/metadata.js b/backend/src/config/metadata.js
index 68d353eea..d40308e80 100644
--- a/backend/src/config/metadata.js
+++ b/backend/src/config/metadata.js
@@ -1,7 +1,9 @@
+// this file is duplicated in `backend/src/config/metadata.js` and `webapp/constants/metadata.js` and replaced on rebranding
export default {
APPLICATION_NAME: 'ocelot.social',
APPLICATION_SHORT_NAME: 'ocelot',
APPLICATION_DESCRIPTION: 'ocelot.social Community Network',
+ COOKIE_NAME: 'ocelot-social-token',
ORGANIZATION_NAME: 'ocelot.social Community',
ORGANIZATION_JURISDICTION: 'City of Angels',
}
diff --git a/backend/src/db/factories.js b/backend/src/db/factories.js
index 717926413..bedf592ed 100644
--- a/backend/src/db/factories.js
+++ b/backend/src/db/factories.js
@@ -105,12 +105,12 @@ Factory.define('user')
})
Factory.define('post')
- .option('categoryIds', [])
+ /* .option('categoryIds', [])
.option('categories', ['categoryIds'], (categoryIds) => {
if (categoryIds.length) return Promise.all(categoryIds.map((id) => neode.find('Category', id)))
// there must be at least one category
return Promise.all([Factory.build('category')])
- })
+ }) */
.option('tagIds', [])
.option('tags', ['tagIds'], (tagIds) => {
return Promise.all(tagIds.map((id) => neode.find('Tag', id)))
@@ -147,16 +147,16 @@ Factory.define('post')
return language || 'en'
})
.after(async (buildObject, options) => {
- const [post, author, image, categories, tags] = await Promise.all([
+ const [post, author, image, /* categories, */ tags] = await Promise.all([
neode.create('Post', buildObject),
options.author,
options.image,
- options.categories,
+ // options.categories,
options.tags,
])
await Promise.all([
post.relateTo(author, 'author'),
- Promise.all(categories.map((c) => c.relateTo(post, 'post'))),
+ // Promise.all(categories.map((c) => c.relateTo(post, 'post'))),
Promise.all(tags.map((t) => t.relateTo(post, 'post'))),
])
if (image) await post.relateTo(image, 'image')
diff --git a/backend/src/db/seed.js b/backend/src/db/seed.js
index 713a03142..7d9b5e954 100644
--- a/backend/src/db/seed.js
+++ b/backend/src/db/seed.js
@@ -137,100 +137,93 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
}),
])
- const [
- peterLustig,
- bobDerBaumeister,
- jennyRostock,
- huey,
- dewey,
- louie,
- dagobert,
- ] = await Promise.all([
- Factory.build(
- 'user',
- {
- id: 'u1',
- name: 'Peter Lustig',
- slug: 'peter-lustig',
- role: 'admin',
- },
- {
- email: 'admin@example.org',
- },
- ),
- Factory.build(
- 'user',
- {
- id: 'u2',
- name: 'Bob der Baumeister',
- slug: 'bob-der-baumeister',
- role: 'moderator',
- },
- {
- email: 'moderator@example.org',
- },
- ),
- Factory.build(
- 'user',
- {
- id: 'u3',
- name: 'Jenny Rostock',
- slug: 'jenny-rostock',
- role: 'user',
- },
- {
- email: 'user@example.org',
- },
- ),
- Factory.build(
- 'user',
- {
- id: 'u4',
- name: 'Huey',
- slug: 'huey',
- role: 'user',
- },
- {
- email: 'huey@example.org',
- },
- ),
- Factory.build(
- 'user',
- {
- id: 'u5',
- name: 'Dewey',
- slug: 'dewey',
- role: 'user',
- },
- {
- email: 'dewey@example.org',
- },
- ),
- Factory.build(
- 'user',
- {
- id: 'u6',
- name: 'Louie',
- slug: 'louie',
- role: 'user',
- },
- {
- email: 'louie@example.org',
- },
- ),
- Factory.build(
- 'user',
- {
- id: 'u7',
- name: 'Dagobert',
- slug: 'dagobert',
- role: 'user',
- },
- {
- email: 'dagobert@example.org',
- },
- ),
- ])
+ const [peterLustig, bobDerBaumeister, jennyRostock, huey, dewey, louie, dagobert] =
+ await Promise.all([
+ Factory.build(
+ 'user',
+ {
+ id: 'u1',
+ name: 'Peter Lustig',
+ slug: 'peter-lustig',
+ role: 'admin',
+ },
+ {
+ email: 'admin@example.org',
+ },
+ ),
+ Factory.build(
+ 'user',
+ {
+ id: 'u2',
+ name: 'Bob der Baumeister',
+ slug: 'bob-der-baumeister',
+ role: 'moderator',
+ },
+ {
+ email: 'moderator@example.org',
+ },
+ ),
+ Factory.build(
+ 'user',
+ {
+ id: 'u3',
+ name: 'Jenny Rostock',
+ slug: 'jenny-rostock',
+ role: 'user',
+ },
+ {
+ email: 'user@example.org',
+ },
+ ),
+ Factory.build(
+ 'user',
+ {
+ id: 'u4',
+ name: 'Huey',
+ slug: 'huey',
+ role: 'user',
+ },
+ {
+ email: 'huey@example.org',
+ },
+ ),
+ Factory.build(
+ 'user',
+ {
+ id: 'u5',
+ name: 'Dewey',
+ slug: 'dewey',
+ role: 'user',
+ },
+ {
+ email: 'dewey@example.org',
+ },
+ ),
+ Factory.build(
+ 'user',
+ {
+ id: 'u6',
+ name: 'Louie',
+ slug: 'louie',
+ role: 'user',
+ },
+ {
+ email: 'louie@example.org',
+ },
+ ),
+ Factory.build(
+ 'user',
+ {
+ id: 'u7',
+ name: 'Dagobert',
+ slug: 'dagobert',
+ role: 'user',
+ },
+ {
+ email: 'dagobert@example.org',
+ },
+ ),
+ ])
await Promise.all([
peterLustig.relateTo(Berlin, 'isIn'),
@@ -561,7 +554,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
const hashtagAndMention1 =
'The new physics of #QuantenFlussTheorie can explain #QuantumGravity! @peter-lustig got that already. ;-)'
const createPostMutation = gql`
- mutation($id: ID, $title: String!, $content: String!, $categoryIds: [ID]) {
+ mutation ($id: ID, $title: String!, $content: String!, $categoryIds: [ID]) {
CreatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) {
id
}
@@ -618,7 +611,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
const mentionInComment2 =
'Did @peter-lustig tell you?'
const createCommentMutation = gql`
- mutation($id: ID, $postId: ID!, $content: String!) {
+ mutation ($id: ID, $postId: ID!, $content: String!) {
CreateComment(id: $id, postId: $postId, content: $content) {
id
}
diff --git a/backend/src/jwt/encode.js b/backend/src/jwt/encode.js
index 50e439474..baeb62d3d 100644
--- a/backend/src/jwt/encode.js
+++ b/backend/src/jwt/encode.js
@@ -5,7 +5,7 @@ import CONFIG from './../config'
export default function encode(user) {
const { id, name, slug } = user
const token = jwt.sign({ id, name, slug }, CONFIG.JWT_SECRET, {
- expiresIn: '2y',
+ expiresIn: CONFIG.JWT_EXPIRES,
issuer: CONFIG.GRAPHQL_URI,
audience: CONFIG.CLIENT_URI,
subject: user.id.toString(),
diff --git a/backend/src/middleware/email/emailMiddleware.js b/backend/src/middleware/email/emailMiddleware.js
index 1143e3d0f..571b733d5 100644
--- a/backend/src/middleware/email/emailMiddleware.js
+++ b/backend/src/middleware/email/emailMiddleware.js
@@ -43,9 +43,14 @@ if (!hasEmailConfig) {
}
const sendSignupMail = async (resolve, root, args, context, resolveInfo) => {
+ const { inviteCode } = args
const response = await resolve(root, args, context, resolveInfo)
const { email, nonce } = response
- await sendMail(signupTemplate({ email, nonce }))
+ if (inviteCode) {
+ await sendMail(signupTemplate({ email, nonce, inviteCode }))
+ } else {
+ await sendMail(signupTemplate({ email, nonce }))
+ }
delete response.nonce
return response
}
@@ -71,6 +76,5 @@ export default {
AddEmailAddress: sendEmailVerificationMail,
requestPasswordReset: sendPasswordResetMail,
Signup: sendSignupMail,
- SignupByInvitation: sendSignupMail,
},
}
diff --git a/backend/src/middleware/email/templateBuilder.js b/backend/src/middleware/email/templateBuilder.js
index 6e147d752..872b86b29 100644
--- a/backend/src/middleware/email/templateBuilder.js
+++ b/backend/src/middleware/email/templateBuilder.js
@@ -1,10 +1,11 @@
import mustache from 'mustache'
import CONFIG from '../../config'
+import logosWebapp from '../../config/logos.js'
import * as templates from './templates'
const from = CONFIG.EMAIL_DEFAULT_SENDER
-const welcomeImageUrl = new URL(`/img/custom/welcome.svg`, CONFIG.CLIENT_URI)
+const welcomeImageUrl = new URL(logosWebapp.LOGO_WELCOME_PATH, CONFIG.CLIENT_URI)
const defaultParams = {
supportUrl: CONFIG.SUPPORT_URL,
@@ -13,11 +14,18 @@ const defaultParams = {
welcomeImageUrl,
}
-export const signupTemplate = ({ email, nonce }) => {
+export const signupTemplate = ({ email, nonce, inviteCode = null }) => {
const subject = `Willkommen, Bienvenue, Welcome to ${CONFIG.APPLICATION_NAME}!`
- const actionUrl = new URL('/registration/create-user-account', CONFIG.CLIENT_URI)
- actionUrl.searchParams.set('nonce', nonce)
+ // dev format example: http://localhost:3000/registration?method=invite-mail&email=wolle.huss%40pjannto.com&nonce=64853
+ const actionUrl = new URL('/registration', CONFIG.CLIENT_URI)
actionUrl.searchParams.set('email', email)
+ actionUrl.searchParams.set('nonce', nonce)
+ if (inviteCode) {
+ actionUrl.searchParams.set('inviteCode', inviteCode)
+ actionUrl.searchParams.set('method', 'invite-code')
+ } else {
+ actionUrl.searchParams.set('method', 'invite-mail')
+ }
return {
from,
@@ -34,8 +42,8 @@ export const signupTemplate = ({ email, nonce }) => {
export const emailVerificationTemplate = ({ email, nonce, name }) => {
const subject = 'Neue E-Mail Adresse | New E-Mail Address'
const actionUrl = new URL('/settings/my-email-address/verify', CONFIG.CLIENT_URI)
- actionUrl.searchParams.set('nonce', nonce)
actionUrl.searchParams.set('email', email)
+ actionUrl.searchParams.set('nonce', nonce)
return {
from,
@@ -77,7 +85,7 @@ export const wrongAccountTemplate = ({ email }) => {
subject,
html: mustache.render(
templates.layout,
- { actionUrl, supportUrl: CONFIG.SUPPORT_URL, welcomeImageUrl },
+ { ...defaultParams, actionUrl, supportUrl: CONFIG.SUPPORT_URL, welcomeImageUrl },
{ content: templates.wrongAccount },
),
}
diff --git a/backend/src/middleware/email/templates/emailVerification.html b/backend/src/middleware/email/templates/emailVerification.html
index 3fc421dc5..35ce27e5a 100644
--- a/backend/src/middleware/email/templates/emailVerification.html
+++ b/backend/src/middleware/email/templates/emailVerification.html
@@ -7,8 +7,8 @@
`
const postWithHastagsQuery = gql`
- query($id: ID) {
+ query ($id: ID) {
Post(id: $id) {
tags {
id
diff --git a/backend/src/middleware/languages/languages.spec.js b/backend/src/middleware/languages/languages.spec.js
index 1448e4e4b..9bba45c0c 100644
--- a/backend/src/middleware/languages/languages.spec.js
+++ b/backend/src/middleware/languages/languages.spec.js
@@ -29,7 +29,7 @@ afterAll(async () => {
})
const createPostMutation = gql`
- mutation($title: String!, $content: String!, $categoryIds: [ID]) {
+ mutation ($title: String!, $content: String!, $categoryIds: [ID]) {
CreatePost(title: $title, content: $content, categoryIds: $categoryIds) {
language
}
diff --git a/backend/src/middleware/notifications/notificationsMiddleware.spec.js b/backend/src/middleware/notifications/notificationsMiddleware.spec.js
index af4ed9693..7583a6727 100644
--- a/backend/src/middleware/notifications/notificationsMiddleware.spec.js
+++ b/backend/src/middleware/notifications/notificationsMiddleware.spec.js
@@ -10,7 +10,7 @@ const driver = getDriver()
const neode = getNeode()
const categoryIds = ['cat9']
const createPostMutation = gql`
- mutation($id: ID, $title: String!, $postContent: String!, $categoryIds: [ID]!) {
+ mutation ($id: ID, $title: String!, $postContent: String!, $categoryIds: [ID]!) {
CreatePost(id: $id, title: $title, content: $postContent, categoryIds: $categoryIds) {
id
title
@@ -19,7 +19,7 @@ const createPostMutation = gql`
}
`
const updatePostMutation = gql`
- mutation($id: ID!, $title: String!, $postContent: String!, $categoryIds: [ID]!) {
+ mutation ($id: ID!, $title: String!, $postContent: String!, $categoryIds: [ID]!) {
UpdatePost(id: $id, content: $postContent, title: $title, categoryIds: $categoryIds) {
title
content
@@ -27,7 +27,7 @@ const updatePostMutation = gql`
}
`
const createCommentMutation = gql`
- mutation($id: ID, $postId: ID!, $commentContent: String!) {
+ mutation ($id: ID, $postId: ID!, $commentContent: String!) {
CreateComment(id: $id, postId: $postId, content: $commentContent) {
id
content
@@ -80,7 +80,7 @@ afterEach(async () => {
describe('notifications', () => {
const notificationQuery = gql`
- query($read: Boolean) {
+ query ($read: Boolean) {
notifications(read: $read, orderBy: updatedAt_desc) {
read
reason
@@ -367,7 +367,7 @@ describe('notifications', () => {
describe('if the notification was marked as read earlier', () => {
const markAsReadAction = async () => {
const mutation = gql`
- mutation($id: ID!) {
+ mutation ($id: ID!) {
markAsRead(id: $id) {
read
}
diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js
index 7aeb7252a..b10389f50 100644
--- a/backend/src/middleware/permissionsMiddleware.js
+++ b/backend/src/middleware/permissionsMiddleware.js
@@ -1,6 +1,7 @@
import { rule, shield, deny, allow, or } from 'graphql-shield'
import { getNeode } from '../db/neo4j'
import CONFIG from '../config'
+import { validateInviteCode } from '../schema/resolvers/transactions/inviteCodes'
const debug = !!CONFIG.DEBUG
const allowExternalErrors = true
@@ -87,7 +88,14 @@ const noEmailFilter = rule({
return !('email' in args)
})
-const publicRegistration = rule()(() => !!CONFIG.PUBLIC_REGISTRATION)
+const publicRegistration = rule()(() => CONFIG.PUBLIC_REGISTRATION)
+
+const inviteRegistration = rule()(async (_parent, args, { user, driver }) => {
+ if (!CONFIG.INVITE_REGISTRATION) return false
+ const { inviteCode } = args
+ const session = driver.session()
+ return validateInviteCode(session, inviteCode)
+})
// Permissions
export default shield(
@@ -121,14 +129,15 @@ export default shield(
userData: isAuthenticated,
MyInviteCodes: isAuthenticated,
isValidInviteCode: allow,
+ VerifyNonce: allow,
queryLocations: isAuthenticated,
availableRoles: isAdmin,
+ getInviteCode: isAuthenticated, // and inviteRegistration
},
Mutation: {
'*': deny,
login: allow,
- SignupByInvitation: allow,
- Signup: or(publicRegistration, isAdmin),
+ Signup: or(publicRegistration, inviteRegistration, isAdmin),
SignupVerification: allow,
UpdateUser: onlyYourself,
CreatePost: isAuthenticated,
diff --git a/backend/src/middleware/permissionsMiddleware.spec.js b/backend/src/middleware/permissionsMiddleware.spec.js
index 775533867..5fa4a8f01 100644
--- a/backend/src/middleware/permissionsMiddleware.spec.js
+++ b/backend/src/middleware/permissionsMiddleware.spec.js
@@ -3,11 +3,13 @@ import createServer from '../server'
import Factory, { cleanDatabase } from '../db/factories'
import { gql } from '../helpers/jest'
import { getDriver, getNeode } from '../db/neo4j'
+import CONFIG from '../config'
const instance = getNeode()
const driver = getDriver()
-let query, authenticatedUser, owner, anotherRegularUser, administrator, variables, moderator
+let query, mutate, variables
+let authenticatedUser, owner, anotherRegularUser, administrator, moderator
describe('authorization', () => {
beforeAll(async () => {
@@ -20,6 +22,7 @@ describe('authorization', () => {
}),
})
query = createTestClient(server).query
+ mutate = createTestClient(server).mutate
})
afterEach(async () => {
@@ -77,7 +80,7 @@ describe('authorization', () => {
describe('access email address', () => {
const userQuery = gql`
- query($name: String) {
+ query ($name: String) {
User(name: $name) {
email
}
@@ -159,5 +162,132 @@ describe('authorization', () => {
})
})
})
+
+ describe('access Signup', () => {
+ const signupMutation = gql`
+ mutation ($email: String!, $inviteCode: String) {
+ Signup(email: $email, inviteCode: $inviteCode) {
+ email
+ }
+ }
+ `
+
+ describe('admin invite only', () => {
+ beforeEach(async () => {
+ variables = {
+ email: 'some@email.org',
+ inviteCode: 'AAAAAA',
+ }
+ CONFIG.INVITE_REGISTRATION = false
+ CONFIG.PUBLIC_REGISTRATION = false
+ await Factory.build('inviteCode', {
+ code: 'AAAAAA',
+ })
+ })
+
+ describe('as user', () => {
+ beforeEach(async () => {
+ authenticatedUser = await anotherRegularUser.toJson()
+ })
+
+ it('denies permission', async () => {
+ await expect(mutate({ mutation: signupMutation, variables })).resolves.toMatchObject({
+ errors: [{ message: 'Not Authorised!' }],
+ data: { Signup: null },
+ })
+ })
+ })
+
+ describe('as admin', () => {
+ beforeEach(async () => {
+ authenticatedUser = await administrator.toJson()
+ })
+
+ it('returns an email', async () => {
+ await expect(mutate({ mutation: signupMutation, variables })).resolves.toMatchObject({
+ errors: undefined,
+ data: {
+ Signup: { email: 'some@email.org' },
+ },
+ })
+ })
+ })
+ })
+
+ describe('public registration', () => {
+ beforeEach(async () => {
+ variables = {
+ email: 'some@email.org',
+ inviteCode: 'AAAAAA',
+ }
+ CONFIG.INVITE_REGISTRATION = false
+ CONFIG.PUBLIC_REGISTRATION = true
+ await Factory.build('inviteCode', {
+ code: 'AAAAAA',
+ })
+ })
+
+ describe('as anyone', () => {
+ beforeEach(async () => {
+ authenticatedUser = null
+ })
+
+ it('returns an email', async () => {
+ await expect(mutate({ mutation: signupMutation, variables })).resolves.toMatchObject({
+ errors: undefined,
+ data: {
+ Signup: { email: 'some@email.org' },
+ },
+ })
+ })
+ })
+ })
+
+ describe('invite registration', () => {
+ beforeEach(async () => {
+ CONFIG.INVITE_REGISTRATION = true
+ CONFIG.PUBLIC_REGISTRATION = false
+ await Factory.build('inviteCode', {
+ code: 'AAAAAA',
+ })
+ })
+
+ describe('as anyone with valid invite code', () => {
+ beforeEach(async () => {
+ variables = {
+ email: 'some@email.org',
+ inviteCode: 'AAAAAA',
+ }
+ authenticatedUser = null
+ })
+
+ it('returns an email', async () => {
+ await expect(mutate({ mutation: signupMutation, variables })).resolves.toMatchObject({
+ errors: undefined,
+ data: {
+ Signup: { email: 'some@email.org' },
+ },
+ })
+ })
+ })
+
+ describe('as anyone without valid invite', () => {
+ beforeEach(async () => {
+ variables = {
+ email: 'some@email.org',
+ inviteCode: 'no valid invite code',
+ }
+ authenticatedUser = null
+ })
+
+ it('denies permission', async () => {
+ await expect(mutate({ mutation: signupMutation, variables })).resolves.toMatchObject({
+ errors: [{ message: 'Not Authorised!' }],
+ data: { Signup: null },
+ })
+ })
+ })
+ })
+ })
})
})
diff --git a/backend/src/middleware/slugifyMiddleware.spec.js b/backend/src/middleware/slugifyMiddleware.spec.js
index 97503851a..1f45c29df 100644
--- a/backend/src/middleware/slugifyMiddleware.spec.js
+++ b/backend/src/middleware/slugifyMiddleware.spec.js
@@ -54,7 +54,7 @@ describe('slugifyMiddleware', () => {
describe('CreatePost', () => {
const categoryIds = ['cat9']
const createPostMutation = gql`
- mutation($title: String!, $content: String!, $categoryIds: [ID]!, $slug: String) {
+ mutation ($title: String!, $content: String!, $categoryIds: [ID]!, $slug: String) {
CreatePost(title: $title, content: $content, categoryIds: $categoryIds, slug: $slug) {
slug
}
@@ -164,7 +164,7 @@ describe('slugifyMiddleware', () => {
describe('SignupVerification', () => {
const mutation = gql`
- mutation(
+ mutation (
$password: String!
$email: String!
$name: String!
diff --git a/backend/src/middleware/userInteractions.spec.js b/backend/src/middleware/userInteractions.spec.js
index 77c9fbd1d..27bee87c8 100644
--- a/backend/src/middleware/userInteractions.spec.js
+++ b/backend/src/middleware/userInteractions.spec.js
@@ -10,7 +10,7 @@ const driver = getDriver()
const neode = getNeode()
const postQuery = gql`
- query($id: ID) {
+ query ($id: ID) {
Post(id: $id) {
clickedCount
}
diff --git a/backend/src/middleware/validation/validationMiddleware.spec.js b/backend/src/middleware/validation/validationMiddleware.spec.js
index c3d518256..530a80b1e 100644
--- a/backend/src/middleware/validation/validationMiddleware.spec.js
+++ b/backend/src/middleware/validation/validationMiddleware.spec.js
@@ -17,14 +17,14 @@ let authenticatedUser,
commentingUser
const createCommentMutation = gql`
- mutation($id: ID, $postId: ID!, $content: String!) {
+ mutation ($id: ID, $postId: ID!, $content: String!) {
CreateComment(id: $id, postId: $postId, content: $content) {
id
}
}
`
const updateCommentMutation = gql`
- mutation($content: String!, $id: ID!) {
+ mutation ($content: String!, $id: ID!) {
UpdateComment(content: $content, id: $id) {
id
}
@@ -32,7 +32,7 @@ const updateCommentMutation = gql`
`
const reportMutation = gql`
- mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
+ mutation ($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
fileReport(
resourceId: $resourceId
reasonCategory: $reasonCategory
@@ -43,7 +43,7 @@ const reportMutation = gql`
}
`
const reviewMutation = gql`
- mutation($resourceId: ID!, $disable: Boolean, $closed: Boolean) {
+ mutation ($resourceId: ID!, $disable: Boolean, $closed: Boolean) {
review(resourceId: $resourceId, disable: $disable, closed: $closed) {
createdAt
updatedAt
@@ -52,7 +52,7 @@ const reviewMutation = gql`
`
const updateUserMutation = gql`
- mutation($id: ID!, $name: String) {
+ mutation ($id: ID!, $name: String) {
UpdateUser(id: $id, name: $name) {
name
}
diff --git a/backend/src/schema/resolvers/comments.spec.js b/backend/src/schema/resolvers/comments.spec.js
index 9f633c8b0..63a5538d0 100644
--- a/backend/src/schema/resolvers/comments.spec.js
+++ b/backend/src/schema/resolvers/comments.spec.js
@@ -36,7 +36,7 @@ afterEach(async () => {
})
const createCommentMutation = gql`
- mutation($id: ID, $postId: ID!, $content: String!) {
+ mutation ($id: ID, $postId: ID!, $content: String!) {
CreateComment(id: $id, postId: $postId, content: $content) {
id
content
@@ -128,7 +128,7 @@ describe('CreateComment', () => {
describe('UpdateComment', () => {
const updateCommentMutation = gql`
- mutation($content: String!, $id: ID!) {
+ mutation ($content: String!, $id: ID!) {
UpdateComment(content: $content, id: $id) {
id
content
@@ -220,7 +220,7 @@ describe('UpdateComment', () => {
describe('DeleteComment', () => {
const deleteCommentMutation = gql`
- mutation($id: ID!) {
+ mutation ($id: ID!) {
DeleteComment(id: $id) {
id
content
diff --git a/backend/src/schema/resolvers/donations.spec.js b/backend/src/schema/resolvers/donations.spec.js
index ea5ee4e09..5bbbd60e0 100644
--- a/backend/src/schema/resolvers/donations.spec.js
+++ b/backend/src/schema/resolvers/donations.spec.js
@@ -9,7 +9,7 @@ const instance = getNeode()
const driver = getDriver()
const updateDonationsMutation = gql`
- mutation($goal: Int, $progress: Int) {
+ mutation ($goal: Int, $progress: Int) {
UpdateDonations(goal: $goal, progress: $progress) {
id
goal
diff --git a/backend/src/schema/resolvers/emails.js b/backend/src/schema/resolvers/emails.js
index 7986f2613..8f6b1c651 100644
--- a/backend/src/schema/resolvers/emails.js
+++ b/backend/src/schema/resolvers/emails.js
@@ -6,6 +6,27 @@ import Validator from 'neode/build/Services/Validator.js'
import normalizeEmail from './helpers/normalizeEmail'
export default {
+ Query: {
+ VerifyNonce: async (_parent, args, context, _resolveInfo) => {
+ const session = context.driver.session()
+ const readTxResultPromise = session.readTransaction(async (txc) => {
+ const result = await txc.run(
+ `
+ MATCH (email:EmailAddress {email: $email, nonce: $nonce})
+ RETURN count(email) > 0 AS result
+ `,
+ { email: args.email, nonce: args.nonce },
+ )
+ return result
+ })
+ try {
+ const txResult = await readTxResultPromise
+ return txResult.records[0].get('result')
+ } finally {
+ session.close()
+ }
+ },
+ },
Mutation: {
AddEmailAddress: async (_parent, args, context, _resolveInfo) => {
let response
diff --git a/backend/src/schema/resolvers/emails.spec.js b/backend/src/schema/resolvers/emails.spec.js
index 94e7ede31..c5596cc27 100644
--- a/backend/src/schema/resolvers/emails.spec.js
+++ b/backend/src/schema/resolvers/emails.spec.js
@@ -6,7 +6,7 @@ import { createTestClient } from 'apollo-server-testing'
const neode = getNeode()
-let mutate
+let mutate, query
let authenticatedUser
let user
let variables
@@ -16,7 +16,8 @@ beforeEach(async () => {
variables = {}
})
-beforeAll(() => {
+beforeAll(async () => {
+ await cleanDatabase()
const { server } = createServer({
context: () => {
return {
@@ -27,6 +28,7 @@ beforeAll(() => {
},
})
mutate = createTestClient(server).mutate
+ query = createTestClient(server).query
})
afterEach(async () => {
@@ -35,7 +37,7 @@ afterEach(async () => {
describe('AddEmailAddress', () => {
const mutation = gql`
- mutation($email: String!) {
+ mutation ($email: String!) {
AddEmailAddress(email: $email) {
email
verifiedAt
@@ -140,7 +142,7 @@ describe('AddEmailAddress', () => {
describe('VerifyEmailAddress', () => {
const mutation = gql`
- mutation($email: String!, $nonce: String!) {
+ mutation ($email: String!, $nonce: String!) {
VerifyEmailAddress(email: $email, nonce: $nonce) {
email
createdAt
@@ -185,7 +187,7 @@ describe('VerifyEmailAddress', () => {
let emailAddress
beforeEach(async () => {
emailAddress = await Factory.build('unverifiedEmailAddress', {
- nonce: 'abcdef',
+ nonce: '12345',
verifiedAt: null,
createdAt: new Date().toISOString(),
email: 'to-be-verified@example.org',
@@ -204,7 +206,7 @@ describe('VerifyEmailAddress', () => {
describe('given valid nonce for `UnverifiedEmailAddress` node', () => {
beforeEach(() => {
- variables = { ...variables, nonce: 'abcdef' }
+ variables = { ...variables, nonce: '12345' }
})
describe('but the address does not belong to the authenticated user', () => {
@@ -295,3 +297,40 @@ describe('VerifyEmailAddress', () => {
})
})
})
+
+describe('VerifyNonce', () => {
+ beforeEach(async () => {
+ await Factory.build('emailAddress', {
+ nonce: '12345',
+ verifiedAt: null,
+ createdAt: new Date().toISOString(),
+ email: 'to-be-verified@example.org',
+ })
+ })
+
+ const verifyNonceQuery = gql`
+ query ($email: String!, $nonce: String!) {
+ VerifyNonce(email: $email, nonce: $nonce)
+ }
+ `
+
+ it('returns true when nonce and email match', async () => {
+ variables = {
+ email: 'to-be-verified@example.org',
+ nonce: '12345',
+ }
+ await expect(query({ query: verifyNonceQuery, variables })).resolves.toMatchObject({
+ data: { VerifyNonce: true },
+ })
+ })
+
+ it('returns false when nonce and email do not match', async () => {
+ variables = {
+ email: 'to-be-verified@example.org',
+ nonce: '---',
+ }
+ await expect(query({ query: verifyNonceQuery, variables })).resolves.toMatchObject({
+ data: { VerifyNonce: false },
+ })
+ })
+})
diff --git a/backend/src/schema/resolvers/embeds.spec.js b/backend/src/schema/resolvers/embeds.spec.js
index 6c034acf7..b7ce7ab63 100644
--- a/backend/src/schema/resolvers/embeds.spec.js
+++ b/backend/src/schema/resolvers/embeds.spec.js
@@ -37,8 +37,7 @@ const babyLovesCatEmbedResponse = new Response(
thumbnail_height: 360,
provider_url: 'https://www.youtube.com/',
thumbnail_width: 480,
- html:
- '',
+ html: '',
thumbnail_url: 'https://i.ytimg.com/vi/qkdXAtO40Fo/hqdefault.jpg',
version: '1.0',
author_name: 'Merkley Family',
@@ -57,7 +56,7 @@ describe('Query', () => {
})
const { query } = createTestClient(server)
const embed = gql`
- query($url: String!) {
+ query ($url: String!) {
embed(url: $url) {
type
title
@@ -204,8 +203,7 @@ Have all the information for the brand in separate config files. Set these defau
video: null,
lang: 'de',
sources: ['resource', 'oembed'],
- html:
- '',
+ html: '',
},
},
errors: undefined,
diff --git a/backend/src/schema/resolvers/follow.spec.js b/backend/src/schema/resolvers/follow.spec.js
index f35795991..6d8db982a 100644
--- a/backend/src/schema/resolvers/follow.spec.js
+++ b/backend/src/schema/resolvers/follow.spec.js
@@ -16,7 +16,7 @@ let user2
let variables
const mutationFollowUser = gql`
- mutation($id: ID!) {
+ mutation ($id: ID!) {
followUser(id: $id) {
name
followedBy {
@@ -29,7 +29,7 @@ const mutationFollowUser = gql`
`
const mutationUnfollowUser = gql`
- mutation($id: ID!) {
+ mutation ($id: ID!) {
unfollowUser(id: $id) {
name
followedBy {
@@ -42,7 +42,7 @@ const mutationUnfollowUser = gql`
`
const userQuery = gql`
- query($id: ID) {
+ query ($id: ID) {
User(id: $id) {
followedBy {
id
diff --git a/backend/src/schema/resolvers/helpers/generateNonce.js b/backend/src/schema/resolvers/helpers/generateNonce.js
index e9b758774..6da40b5c2 100644
--- a/backend/src/schema/resolvers/helpers/generateNonce.js
+++ b/backend/src/schema/resolvers/helpers/generateNonce.js
@@ -1,4 +1,5 @@
-import { v4 as uuid } from 'uuid'
export default function generateNonce() {
- return uuid().substring(0, 6)
+ return Array.from({ length: 5 }, (n = Math.floor(Math.random() * 10)) => {
+ return String.fromCharCode(n + 48)
+ }).join('')
}
diff --git a/backend/src/schema/resolvers/inviteCodes.js b/backend/src/schema/resolvers/inviteCodes.js
index 91148a08d..442ff17b1 100644
--- a/backend/src/schema/resolvers/inviteCodes.js
+++ b/backend/src/schema/resolvers/inviteCodes.js
@@ -1,5 +1,6 @@
import generateInviteCode from './helpers/generateInviteCode'
import Resolver from './helpers/Resolver'
+import { validateInviteCode } from './transactions/inviteCodes'
const uniqueInviteCode = async (session, code) => {
return session.readTransaction(async (txc) => {
@@ -12,6 +13,52 @@ const uniqueInviteCode = async (session, code) => {
export default {
Query: {
+ getInviteCode: async (_parent, args, context, _resolveInfo) => {
+ const {
+ user: { id: userId },
+ } = context
+ const session = context.driver.session()
+ const readTxResultPromise = session.readTransaction(async (txc) => {
+ const result = await txc.run(
+ `MATCH (user:User {id: $userId})-[:GENERATED]->(ic:InviteCode)
+ WHERE ic.expiresAt IS NULL
+ OR datetime(ic.expiresAt) >= datetime()
+ RETURN properties(ic) AS inviteCodes`,
+ {
+ userId,
+ },
+ )
+ return result.records.map((record) => record.get('inviteCodes'))
+ })
+ try {
+ const inviteCode = await readTxResultPromise
+ if (inviteCode && inviteCode.length > 0) return inviteCode[0]
+ let code = generateInviteCode()
+ while (!(await uniqueInviteCode(session, code))) {
+ code = generateInviteCode()
+ }
+ const writeTxResultPromise = session.writeTransaction(async (txc) => {
+ const result = await txc.run(
+ `MATCH (user:User {id: $userId})
+ MERGE (user)-[:GENERATED]->(ic:InviteCode { code: $code })
+ ON CREATE SET
+ ic.createdAt = toString(datetime()),
+ ic.expiresAt = $expiresAt
+ RETURN ic AS inviteCode`,
+ {
+ userId,
+ code,
+ expiresAt: null,
+ },
+ )
+ return result.records.map((record) => record.get('inviteCode').properties)
+ })
+ const txResult = await writeTxResultPromise
+ return txResult[0]
+ } finally {
+ session.close()
+ }
+ },
MyInviteCodes: async (_parent, args, context, _resolveInfo) => {
const {
user: { id: userId },
@@ -36,28 +83,9 @@ export default {
},
isValidInviteCode: async (_parent, args, context, _resolveInfo) => {
const { code } = args
- if (!code) return false
const session = context.driver.session()
- const readTxResultPromise = session.readTransaction(async (txc) => {
- const result = await txc.run(
- `MATCH (ic:InviteCode { code: toUpper($code) })
- RETURN
- CASE
- WHEN ic.expiresAt IS NULL THEN true
- WHEN datetime(ic.expiresAt) >= datetime() THEN true
- ELSE false END AS result`,
- {
- code,
- },
- )
- return result.records.map((record) => record.get('result'))
- })
- try {
- const txResult = await readTxResultPromise
- return !!txResult[0]
- } finally {
- session.close()
- }
+ if (!code) return false
+ return validateInviteCode(session, code)
},
},
Mutation: {
diff --git a/backend/src/schema/resolvers/inviteCodes.spec.js b/backend/src/schema/resolvers/inviteCodes.spec.js
index 19f021437..940aa8403 100644
--- a/backend/src/schema/resolvers/inviteCodes.spec.js
+++ b/backend/src/schema/resolvers/inviteCodes.spec.js
@@ -11,7 +11,7 @@ let mutate
const driver = getDriver()
const generateInviteCodeMutation = gql`
- mutation($expiresAt: String = null) {
+ mutation ($expiresAt: String = null) {
GenerateInviteCode(expiresAt: $expiresAt) {
code
createdAt
@@ -31,7 +31,7 @@ const myInviteCodesQuery = gql`
`
const isValidInviteCodeQuery = gql`
- query($code: ID!) {
+ query ($code: ID!) {
isValidInviteCode(code: $code)
}
`
diff --git a/backend/src/schema/resolvers/locations.spec.js b/backend/src/schema/resolvers/locations.spec.js
index 34d0f2e9d..fa562cbfd 100644
--- a/backend/src/schema/resolvers/locations.spec.js
+++ b/backend/src/schema/resolvers/locations.spec.js
@@ -31,7 +31,7 @@ describe('resolvers', () => {
describe('custom mutation, not handled by neo4j-graphql-js', () => {
let variables
const updateUserMutation = gql`
- mutation($id: ID!, $name: String) {
+ mutation ($id: ID!, $name: String) {
UpdateUser(id: $id, name: $name) {
name
location {
diff --git a/backend/src/schema/resolvers/moderation.spec.js b/backend/src/schema/resolvers/moderation.spec.js
index b62d35ee8..a579c62da 100644
--- a/backend/src/schema/resolvers/moderation.spec.js
+++ b/backend/src/schema/resolvers/moderation.spec.js
@@ -16,7 +16,7 @@ let mutate,
closeReportVariables
const reviewMutation = gql`
- mutation($resourceId: ID!, $disable: Boolean, $closed: Boolean) {
+ mutation ($resourceId: ID!, $disable: Boolean, $closed: Boolean) {
review(resourceId: $resourceId, disable: $disable, closed: $closed) {
createdAt
updatedAt
diff --git a/backend/src/schema/resolvers/notifications.spec.js b/backend/src/schema/resolvers/notifications.spec.js
index 9d7795dd4..f600e7d1c 100644
--- a/backend/src/schema/resolvers/notifications.spec.js
+++ b/backend/src/schema/resolvers/notifications.spec.js
@@ -139,7 +139,7 @@ describe('given some notifications', () => {
describe('notifications', () => {
const notificationQuery = gql`
- query($read: Boolean, $orderBy: NotificationOrdering) {
+ query ($read: Boolean, $orderBy: NotificationOrdering) {
notifications(read: $read, orderBy: $orderBy) {
from {
__typename
@@ -249,7 +249,7 @@ describe('given some notifications', () => {
const deletePostAction = async () => {
authenticatedUser = await author.toJson()
const deletePostMutation = gql`
- mutation($id: ID!) {
+ mutation ($id: ID!) {
DeletePost(id: $id) {
id
deleted
@@ -284,7 +284,7 @@ describe('given some notifications', () => {
describe('markAsRead', () => {
const markAsReadMutation = gql`
- mutation($id: ID!) {
+ mutation ($id: ID!) {
markAsRead(id: $id) {
from {
__typename
diff --git a/backend/src/schema/resolvers/passwordReset.spec.js b/backend/src/schema/resolvers/passwordReset.spec.js
index fd1395c57..40a18f5d9 100644
--- a/backend/src/schema/resolvers/passwordReset.spec.js
+++ b/backend/src/schema/resolvers/passwordReset.spec.js
@@ -55,7 +55,7 @@ describe('passwordReset', () => {
describe('requestPasswordReset', () => {
const mutation = gql`
- mutation($email: String!) {
+ mutation ($email: String!) {
requestPasswordReset(email: $email)
}
`
@@ -116,7 +116,7 @@ describe('resetPassword', () => {
}
const mutation = gql`
- mutation($nonce: String!, $email: String!, $newPassword: String!) {
+ mutation ($nonce: String!, $email: String!, $newPassword: String!) {
resetPassword(nonce: $nonce, email: $email, newPassword: $newPassword)
}
`
@@ -196,7 +196,7 @@ describe('resetPassword', () => {
it('updates password of the user', async () => {
await mutate({ mutation, variables })
const checkLoginMutation = gql`
- mutation($email: String!, $password: String!) {
+ mutation ($email: String!, $password: String!) {
login(email: $email, password: $password)
}
`
diff --git a/backend/src/schema/resolvers/posts.js b/backend/src/schema/resolvers/posts.js
index b7b77bbd5..d199b6f09 100644
--- a/backend/src/schema/resolvers/posts.js
+++ b/backend/src/schema/resolvers/posts.js
@@ -348,7 +348,7 @@ export default {
undefinedToNull: ['activityId', 'objectId', 'language', 'pinnedAt', 'pinned'],
hasMany: {
tags: '-[:TAGGED]->(related:Tag)',
- categories: '-[:CATEGORIZED]->(related:Category)',
+ // categories: '-[:CATEGORIZED]->(related:Category)',
comments: '<-[:COMMENTS]-(related:Comment)',
shoutedBy: '<-[:SHOUTED]-(related:User)',
emotions: '<-[related:EMOTED]',
diff --git a/backend/src/schema/resolvers/posts.spec.js b/backend/src/schema/resolvers/posts.spec.js
index f0c57b8fb..20dbecfb6 100644
--- a/backend/src/schema/resolvers/posts.spec.js
+++ b/backend/src/schema/resolvers/posts.spec.js
@@ -16,7 +16,7 @@ const categoryIds = ['cat9', 'cat4', 'cat15']
let variables
const createPostMutation = gql`
- mutation($id: ID, $title: String!, $content: String!, $language: String, $categoryIds: [ID]) {
+ mutation ($id: ID, $title: String!, $content: String!, $language: String, $categoryIds: [ID]) {
CreatePost(
id: $id
title: $title
@@ -147,7 +147,7 @@ describe('Post', () => {
})
})
- it('by categories', async () => {
+ /* it('by categories', async () => {
const postQueryFilteredByCategories = gql`
query Post($filter: _PostFilter) {
Post(filter: $filter) {
@@ -172,7 +172,7 @@ describe('Post', () => {
await expect(
query({ query: postQueryFilteredByCategories, variables }),
).resolves.toMatchObject(expected)
- })
+ }) */
describe('by emotions', () => {
const postQueryFilteredByEmotions = gql`
@@ -323,14 +323,8 @@ describe('CreatePost', () => {
describe('UpdatePost', () => {
let author, newlyCreatedPost
const updatePostMutation = gql`
- mutation($id: ID!, $title: String!, $content: String!, $categoryIds: [ID], $image: ImageInput) {
- UpdatePost(
- id: $id
- title: $title
- content: $content
- categoryIds: $categoryIds
- image: $image
- ) {
+ mutation ($id: ID!, $title: String!, $content: String!, $image: ImageInput) {
+ UpdatePost(id: $id, title: $title, content: $content, image: $image) {
id
title
content
@@ -338,9 +332,6 @@ describe('UpdatePost', () => {
name
slug
}
- categories {
- id
- }
createdAt
updatedAt
}
@@ -428,7 +419,7 @@ describe('UpdatePost', () => {
expect(newlyCreatedPost.updatedAt).not.toEqual(UpdatePost.updatedAt)
})
- describe('no new category ids provided for update', () => {
+ /* describe('no new category ids provided for update', () => {
it('resolves and keeps current categories', async () => {
const expected = {
data: {
@@ -443,9 +434,9 @@ describe('UpdatePost', () => {
expected,
)
})
- })
+ }) */
- describe('given category ids', () => {
+ /* describe('given category ids', () => {
beforeEach(() => {
variables = { ...variables, categoryIds: ['cat27'] }
})
@@ -464,7 +455,7 @@ describe('UpdatePost', () => {
expected,
)
})
- })
+ }) */
describe('params.image', () => {
describe('is object', () => {
@@ -506,7 +497,7 @@ describe('UpdatePost', () => {
describe('pin posts', () => {
let author
const pinPostMutation = gql`
- mutation($id: ID!) {
+ mutation ($id: ID!) {
pinPost(id: $id) {
id
title
@@ -782,7 +773,7 @@ describe('pin posts', () => {
it('pinned post appear first even when created before other posts', async () => {
const postOrderingQuery = gql`
- query($orderBy: [_PostOrdering]) {
+ query ($orderBy: [_PostOrdering]) {
Post(orderBy: $orderBy) {
id
pinned
@@ -825,7 +816,7 @@ describe('pin posts', () => {
describe('unpin posts', () => {
let pinnedPost
const unpinPostMutation = gql`
- mutation($id: ID!) {
+ mutation ($id: ID!) {
unpinPost(id: $id) {
id
title
@@ -937,7 +928,7 @@ describe('unpin posts', () => {
describe('DeletePost', () => {
let author
const deletePostMutation = gql`
- mutation($id: ID!) {
+ mutation ($id: ID!) {
DeletePost(id: $id) {
id
deleted
@@ -1061,14 +1052,14 @@ describe('DeletePost', () => {
describe('emotions', () => {
let author, postToEmote
const PostsEmotionsCountQuery = gql`
- query($id: ID!) {
+ query ($id: ID!) {
Post(id: $id) {
emotionsCount
}
}
`
const PostsEmotionsQuery = gql`
- query($id: ID!) {
+ query ($id: ID!) {
Post(id: $id) {
emotions {
emotion
@@ -1102,7 +1093,7 @@ describe('emotions', () => {
describe('AddPostEmotions', () => {
const addPostEmotionsMutation = gql`
- mutation($to: _PostInput!, $data: _EMOTEDInput!) {
+ mutation ($to: _PostInput!, $data: _EMOTEDInput!) {
AddPostEmotions(to: $to, data: $data) {
from {
id
@@ -1219,7 +1210,7 @@ describe('emotions', () => {
describe('RemovePostEmotions', () => {
let removePostEmotionsVariables, postsEmotionsQueryVariables
const removePostEmotionsMutation = gql`
- mutation($to: _PostInput!, $data: _EMOTEDInput!) {
+ mutation ($to: _PostInput!, $data: _EMOTEDInput!) {
RemovePostEmotions(to: $to, data: $data) {
from {
id
@@ -1318,13 +1309,13 @@ describe('emotions', () => {
let PostsEmotionsByCurrentUserVariables
const PostsEmotionsCountByEmotionQuery = gql`
- query($postId: ID!, $data: _EMOTEDInput!) {
+ query ($postId: ID!, $data: _EMOTEDInput!) {
PostsEmotionsCountByEmotion(postId: $postId, data: $data)
}
`
const PostsEmotionsByCurrentUserQuery = gql`
- query($postId: ID!) {
+ query ($postId: ID!) {
PostsEmotionsByCurrentUser(postId: $postId)
}
`
diff --git a/backend/src/schema/resolvers/registration.js b/backend/src/schema/resolvers/registration.js
index f1c43be21..2796028fe 100644
--- a/backend/src/schema/resolvers/registration.js
+++ b/backend/src/schema/resolvers/registration.js
@@ -29,34 +29,22 @@ export default {
}
args.termsAndConditionsAgreedAt = new Date().toISOString()
- let { nonce, email } = args
+ let { nonce, email, inviteCode } = args
email = normalizeEmail(email)
delete args.nonce
delete args.email
+ delete args.inviteCode
args = encryptPassword(args)
const { driver } = context
const session = driver.session()
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
- const createUserTransactionResponse = await transaction.run(
- `
- MATCH(email:EmailAddress {nonce: $nonce, email: $email})
- WHERE NOT (email)-[:BELONGS_TO]->()
- CREATE (user:User)
- MERGE(user)-[:PRIMARY_EMAIL]->(email)
- MERGE(user)<-[:BELONGS_TO]-(email)
- SET user += $args
- SET user.id = randomUUID()
- SET user.role = 'user'
- SET user.createdAt = toString(datetime())
- SET user.updatedAt = toString(datetime())
- SET user.allowEmbedIframes = FALSE
- SET user.showShoutsPublicly = FALSE
- SET email.verifiedAt = toString(datetime())
- RETURN user {.*}
- `,
- { args, nonce, email },
- )
+ const createUserTransactionResponse = await transaction.run(signupCypher(inviteCode), {
+ args,
+ nonce,
+ email,
+ inviteCode,
+ })
const [user] = createUserTransactionResponse.records.map((record) => record.get('user'))
if (!user) throw new UserInputError('Invalid email or nonce')
return user
@@ -74,3 +62,39 @@ export default {
},
},
}
+
+const signupCypher = (inviteCode) => {
+ let optionalMatch = ''
+ let optionalMerge = ''
+ if (inviteCode) {
+ optionalMatch = `
+ OPTIONAL MATCH
+ (inviteCode:InviteCode {code: $inviteCode})<-[:GENERATED]-(host:User)
+ `
+ optionalMerge = `
+ MERGE(user)-[:REDEEMED { createdAt: toString(datetime()) }]->(inviteCode)
+ MERGE(host)-[:INVITED { createdAt: toString(datetime()) }]->(user)
+ MERGE(user)-[:FOLLOWS { createdAt: toString(datetime()) }]->(host)
+ MERGE(host)-[:FOLLOWS { createdAt: toString(datetime()) }]->(user)
+ `
+ }
+ const cypher = `
+ MATCH(email:EmailAddress {nonce: $nonce, email: $email})
+ WHERE NOT (email)-[:BELONGS_TO]->()
+ ${optionalMatch}
+ CREATE (user:User)
+ MERGE(user)-[:PRIMARY_EMAIL]->(email)
+ MERGE(user)<-[:BELONGS_TO]-(email)
+ ${optionalMerge}
+ SET user += $args
+ SET user.id = randomUUID()
+ SET user.role = 'user'
+ SET user.createdAt = toString(datetime())
+ SET user.updatedAt = toString(datetime())
+ SET user.allowEmbedIframes = FALSE
+ SET user.showShoutsPublicly = FALSE
+ SET email.verifiedAt = toString(datetime())
+ RETURN user {.*}
+ `
+ return cypher
+}
diff --git a/backend/src/schema/resolvers/registration.spec.js b/backend/src/schema/resolvers/registration.spec.js
index 63dc35519..25e8fd1c8 100644
--- a/backend/src/schema/resolvers/registration.spec.js
+++ b/backend/src/schema/resolvers/registration.spec.js
@@ -3,6 +3,7 @@ import { gql } from '../../helpers/jest'
import { getDriver, getNeode } from '../../db/neo4j'
import createServer from '../../server'
import { createTestClient } from 'apollo-server-testing'
+import CONFIG from '../../config'
const neode = getNeode()
@@ -15,7 +16,8 @@ beforeEach(async () => {
variables = {}
})
-beforeAll(() => {
+beforeAll(async () => {
+ await cleanDatabase()
const { server } = createServer({
context: () => {
return {
@@ -34,8 +36,8 @@ afterEach(async () => {
describe('Signup', () => {
const mutation = gql`
- mutation($email: String!) {
- Signup(email: $email) {
+ mutation ($email: String!, $inviteCode: String) {
+ Signup(email: $email, inviteCode: $inviteCode) {
email
}
}
@@ -50,6 +52,8 @@ describe('Signup', () => {
})
it('throws AuthorizationError', async () => {
+ CONFIG.INVITE_REGISTRATION = false
+ CONFIG.PUBLIC_REGISTRATION = false
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
errors: [{ message: 'Not Authorised!' }],
})
@@ -141,7 +145,7 @@ describe('Signup', () => {
describe('SignupVerification', () => {
const mutation = gql`
- mutation(
+ mutation (
$name: String!
$password: String!
$email: String!
diff --git a/backend/src/schema/resolvers/reports.spec.js b/backend/src/schema/resolvers/reports.spec.js
index 2ecccfc23..1208db7fe 100644
--- a/backend/src/schema/resolvers/reports.spec.js
+++ b/backend/src/schema/resolvers/reports.spec.js
@@ -11,7 +11,7 @@ describe('file a report on a resource', () => {
let authenticatedUser, currentUser, mutate, query, moderator, abusiveUser, otherReportingUser
const categoryIds = ['cat9']
const fileReportMutation = gql`
- mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
+ mutation ($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
fileReport(
resourceId: $resourceId
reasonCategory: $reasonCategory
@@ -42,7 +42,7 @@ describe('file a report on a resource', () => {
reasonDescription: 'Violates code of conduct !!!',
}
const reportsQuery = gql`
- query($closed: Boolean) {
+ query ($closed: Boolean) {
reports(orderBy: createdAt_desc, closed: $closed) {
id
createdAt
@@ -74,7 +74,7 @@ describe('file a report on a resource', () => {
}
`
const reviewMutation = gql`
- mutation($resourceId: ID!, $disable: Boolean, $closed: Boolean) {
+ mutation ($resourceId: ID!, $disable: Boolean, $closed: Boolean) {
review(resourceId: $resourceId, disable: $disable, closed: $closed) {
createdAt
resource {
diff --git a/backend/src/schema/resolvers/rewards.spec.js b/backend/src/schema/resolvers/rewards.spec.js
index eaaa852ec..0f76e1ed1 100644
--- a/backend/src/schema/resolvers/rewards.spec.js
+++ b/backend/src/schema/resolvers/rewards.spec.js
@@ -76,7 +76,7 @@ describe('rewards', () => {
describe('reward', () => {
const rewardMutation = gql`
- mutation($from: ID!, $to: ID!) {
+ mutation ($from: ID!, $to: ID!) {
reward(badgeKey: $from, userId: $to) {
id
badges {
@@ -266,7 +266,7 @@ describe('rewards', () => {
}
const unrewardMutation = gql`
- mutation($from: ID!, $to: ID!) {
+ mutation ($from: ID!, $to: ID!) {
unreward(badgeKey: $from, userId: $to) {
id
badges {
diff --git a/backend/src/schema/resolvers/searches.spec.js b/backend/src/schema/resolvers/searches.spec.js
index a859bf296..ceb63304e 100644
--- a/backend/src/schema/resolvers/searches.spec.js
+++ b/backend/src/schema/resolvers/searches.spec.js
@@ -28,7 +28,7 @@ afterAll(async () => {
})
const searchQuery = gql`
- query($query: String!) {
+ query ($query: String!) {
searchResults(query: $query, limit: 5) {
__typename
... on Post {
@@ -49,7 +49,7 @@ const searchQuery = gql`
`
const searchPostQuery = gql`
- query($query: String!, $firstPosts: Int, $postsOffset: Int) {
+ query ($query: String!, $firstPosts: Int, $postsOffset: Int) {
searchPosts(query: $query, firstPosts: $firstPosts, postsOffset: $postsOffset) {
postCount
posts {
diff --git a/backend/src/schema/resolvers/shout.spec.js b/backend/src/schema/resolvers/shout.spec.js
index 574907180..383bd83f0 100644
--- a/backend/src/schema/resolvers/shout.spec.js
+++ b/backend/src/schema/resolvers/shout.spec.js
@@ -9,17 +9,17 @@ const instance = getNeode()
const driver = getDriver()
const mutationShoutPost = gql`
- mutation($id: ID!) {
+ mutation ($id: ID!) {
shout(id: $id, type: Post)
}
`
const mutationUnshoutPost = gql`
- mutation($id: ID!) {
+ mutation ($id: ID!) {
unshout(id: $id, type: Post)
}
`
const queryPost = gql`
- query($id: ID!) {
+ query ($id: ID!) {
Post(id: $id) {
id
shoutedBy {
diff --git a/backend/src/schema/resolvers/socialMedia.spec.js b/backend/src/schema/resolvers/socialMedia.spec.js
index 898174199..cff20185b 100644
--- a/backend/src/schema/resolvers/socialMedia.spec.js
+++ b/backend/src/schema/resolvers/socialMedia.spec.js
@@ -70,7 +70,7 @@ describe('SocialMedia', () => {
beforeEach(() => {
mutation = gql`
- mutation($url: String!) {
+ mutation ($url: String!) {
CreateSocialMedia(url: $url) {
id
url
@@ -131,7 +131,7 @@ describe('SocialMedia', () => {
describe('ownedBy', () => {
beforeEach(() => {
mutation = gql`
- mutation($url: String!) {
+ mutation ($url: String!) {
CreateSocialMedia(url: $url) {
url
ownedBy {
@@ -162,7 +162,7 @@ describe('SocialMedia', () => {
const socialMedia = await setUpSocialMedia()
mutation = gql`
- mutation($id: ID!, $url: String!) {
+ mutation ($id: ID!, $url: String!) {
UpdateSocialMedia(id: $id, url: $url) {
id
url
@@ -225,7 +225,7 @@ describe('SocialMedia', () => {
const socialMedia = await setUpSocialMedia()
mutation = gql`
- mutation($id: ID!) {
+ mutation ($id: ID!) {
DeleteSocialMedia(id: $id) {
id
url
diff --git a/backend/src/schema/resolvers/transactions/inviteCodes.js b/backend/src/schema/resolvers/transactions/inviteCodes.js
new file mode 100644
index 000000000..554b15f86
--- /dev/null
+++ b/backend/src/schema/resolvers/transactions/inviteCodes.js
@@ -0,0 +1,22 @@
+export async function validateInviteCode(session, inviteCode) {
+ const readTxResultPromise = session.readTransaction(async (txc) => {
+ const result = await txc.run(
+ `MATCH (ic:InviteCode { code: toUpper($inviteCode) })
+ RETURN
+ CASE
+ WHEN ic.expiresAt IS NULL THEN true
+ WHEN datetime(ic.expiresAt) >= datetime() THEN true
+ ELSE false END AS result`,
+ {
+ inviteCode,
+ },
+ )
+ return result.records.map((record) => record.get('result'))
+ })
+ try {
+ const txResult = await readTxResultPromise
+ return !!txResult[0]
+ } finally {
+ session.close()
+ }
+}
diff --git a/backend/src/schema/resolvers/userData.spec.js b/backend/src/schema/resolvers/userData.spec.js
index 972248d50..0a4e54295 100644
--- a/backend/src/schema/resolvers/userData.spec.js
+++ b/backend/src/schema/resolvers/userData.spec.js
@@ -39,7 +39,7 @@ afterAll(async () => {
})
const userDataQuery = gql`
- query($id: ID!) {
+ query ($id: ID!) {
userData(id: $id) {
user {
id
diff --git a/backend/src/schema/resolvers/user_management.spec.js b/backend/src/schema/resolvers/user_management.spec.js
index b434ea628..57fe4a7f4 100644
--- a/backend/src/schema/resolvers/user_management.spec.js
+++ b/backend/src/schema/resolvers/user_management.spec.js
@@ -171,7 +171,7 @@ describe('currentUser', () => {
describe('login', () => {
const loginMutation = gql`
- mutation($email: String!, $password: String!) {
+ mutation ($email: String!, $password: String!) {
login(email: $email, password: $password)
}
`
@@ -287,7 +287,7 @@ describe('login', () => {
describe('change password', () => {
const changePasswordMutation = gql`
- mutation($oldPassword: String!, $newPassword: String!) {
+ mutation ($oldPassword: String!, $newPassword: String!) {
changePassword(oldPassword: $oldPassword, newPassword: $newPassword)
}
`
diff --git a/backend/src/schema/resolvers/users.spec.js b/backend/src/schema/resolvers/users.spec.js
index e5e818040..6fd893240 100644
--- a/backend/src/schema/resolvers/users.spec.js
+++ b/backend/src/schema/resolvers/users.spec.js
@@ -17,7 +17,7 @@ const driver = getDriver()
const neode = getNeode()
const deleteUserMutation = gql`
- mutation($id: ID!, $resource: [Deletable]) {
+ mutation ($id: ID!, $resource: [Deletable]) {
DeleteUser(id: $id, resource: $resource) {
id
name
@@ -46,7 +46,7 @@ const deleteUserMutation = gql`
`
const switchUserRoleMutation = gql`
- mutation($role: UserGroup!, $id: ID!) {
+ mutation ($role: UserGroup!, $id: ID!) {
switchUserRole(role: $role, id: $id) {
name
role
@@ -81,7 +81,7 @@ describe('User', () => {
beforeEach(async () => {
userQuery = gql`
- query($email: String) {
+ query ($email: String) {
User(email: $email) {
name
}
@@ -144,7 +144,7 @@ describe('UpdateUser', () => {
beforeEach(async () => {
updateUserMutation = gql`
- mutation(
+ mutation (
$id: ID!
$name: String
$termsAndConditionsAgreedVersion: String
diff --git a/backend/src/schema/resolvers/users/location.spec.js b/backend/src/schema/resolvers/users/location.spec.js
index 41b784249..c966fe720 100644
--- a/backend/src/schema/resolvers/users/location.spec.js
+++ b/backend/src/schema/resolvers/users/location.spec.js
@@ -9,7 +9,7 @@ const driver = getDriver()
let authenticatedUser, mutate, query, variables
const updateUserMutation = gql`
- mutation($id: ID!, $name: String!, $locationName: String) {
+ mutation ($id: ID!, $name: String!, $locationName: String) {
UpdateUser(id: $id, name: $name, locationName: $locationName) {
locationName
}
@@ -17,7 +17,7 @@ const updateUserMutation = gql`
`
const queryLocations = gql`
- query($place: String!, $lang: String!) {
+ query ($place: String!, $lang: String!) {
queryLocations(place: $place, lang: $lang) {
place_name
id
@@ -114,10 +114,22 @@ describe('Location Service', () => {
const result = await query({ query: queryLocations, variables })
expect(result.data.queryLocations).toEqual([
{ id: 'place.14094307404564380', place_name: 'Berlin, Germany' },
- { id: 'place.15095411613564380', place_name: 'Berlin, Maryland, United States' },
- { id: 'place.5225018734564380', place_name: 'Berlin, Connecticut, United States' },
- { id: 'place.16922023226564380', place_name: 'Berlin, New Jersey, United States' },
- { id: 'place.4035845612564380', place_name: 'Berlin Township, New Jersey, United States' },
+ {
+ id: expect.stringMatching(/^place\.[0-9]+$/),
+ place_name: 'Berlin, Maryland, United States',
+ },
+ {
+ id: expect.stringMatching(/^place\.[0-9]+$/),
+ place_name: 'Berlin, Connecticut, United States',
+ },
+ {
+ id: expect.stringMatching(/^place\.[0-9]+$/),
+ place_name: 'Berlin, New Jersey, United States',
+ },
+ {
+ id: expect.stringMatching(/^place\.[0-9]+$/),
+ place_name: 'Berlin Township, New Jersey, United States',
+ },
])
})
@@ -128,11 +140,23 @@ describe('Location Service', () => {
}
const result = await query({ query: queryLocations, variables })
expect(result.data.queryLocations).toEqual([
- { id: 'place.14094307404564380', place_name: 'Berlin, Deutschland' },
- { id: 'place.15095411613564380', place_name: 'Berlin, Maryland, Vereinigte Staaten' },
- { id: 'place.16922023226564380', place_name: 'Berlin, New Jersey, Vereinigte Staaten' },
- { id: 'place.10735893248465990', place_name: 'Berlin Heights, Ohio, Vereinigte Staaten' },
- { id: 'place.1165756679564380', place_name: 'Berlin, Massachusetts, Vereinigte Staaten' },
+ { id: expect.stringMatching(/^place\.[0-9]+$/), place_name: 'Berlin, Deutschland' },
+ {
+ id: expect.stringMatching(/^place\.[0-9]+$/),
+ place_name: 'Berlin, Maryland, Vereinigte Staaten',
+ },
+ {
+ id: expect.stringMatching(/^place\.[0-9]+$/),
+ place_name: 'Berlin, New Jersey, Vereinigte Staaten',
+ },
+ {
+ id: expect.stringMatching(/^place\.[0-9]+$/),
+ place_name: 'Berlin Heights, Ohio, Vereinigte Staaten',
+ },
+ {
+ id: expect.stringMatching(/^place\.[0-9]+$/),
+ place_name: 'Berlin, Massachusetts, Vereinigte Staaten',
+ },
])
})
diff --git a/backend/src/schema/resolvers/users/mutedUsers.spec.js b/backend/src/schema/resolvers/users/mutedUsers.spec.js
index 345b435f5..c86cf2eb1 100644
--- a/backend/src/schema/resolvers/users/mutedUsers.spec.js
+++ b/backend/src/schema/resolvers/users/mutedUsers.spec.js
@@ -93,7 +93,7 @@ describe('muteUser', () => {
muteAction = (variables) => {
const { mutate } = createTestClient(server)
const muteUserMutation = gql`
- mutation($id: ID!) {
+ mutation ($id: ID!) {
muteUser(id: $id) {
id
name
@@ -310,7 +310,7 @@ describe('unmuteUser', () => {
unmuteAction = (variables) => {
const { mutate } = createTestClient(server)
const unmuteUserMutation = gql`
- mutation($id: ID!) {
+ mutation ($id: ID!) {
unmuteUser(id: $id) {
id
name
diff --git a/backend/src/schema/resolvers/viewedTeaserCount.spec.js b/backend/src/schema/resolvers/viewedTeaserCount.spec.js
index c81e272ec..ceb95c0ed 100644
--- a/backend/src/schema/resolvers/viewedTeaserCount.spec.js
+++ b/backend/src/schema/resolvers/viewedTeaserCount.spec.js
@@ -32,7 +32,7 @@ afterAll(async () => {
describe('count post teaser views', () => {
let aUser, bUser
const markTeaserAsViewed = gql`
- mutation($id: ID!) {
+ mutation ($id: ID!) {
markTeaserAsViewed(id: $id) {
id
viewedTeaserCount
diff --git a/backend/src/schema/types/type/EmailAddress.gql b/backend/src/schema/types/type/EmailAddress.gql
index e09ec9e63..b2e65eafa 100644
--- a/backend/src/schema/types/type/EmailAddress.gql
+++ b/backend/src/schema/types/type/EmailAddress.gql
@@ -4,12 +4,16 @@ type EmailAddress {
createdAt: String
}
+type Query {
+ VerifyNonce(email: String!, nonce: String!): Boolean!
+}
+
type Mutation {
- Signup(email: String!): EmailAddress
- SignupByInvitation(email: String!, token: String!): EmailAddress
+ Signup(email: String!, inviteCode: String = null): EmailAddress
SignupVerification(
nonce: String!
email: String!
+ inviteCode: String = null
name: String!
password: String!
slug: String
diff --git a/backend/src/schema/types/type/InviteCode.gql b/backend/src/schema/types/type/InviteCode.gql
index 8ad7851a2..3293c735b 100644
--- a/backend/src/schema/types/type/InviteCode.gql
+++ b/backend/src/schema/types/type/InviteCode.gql
@@ -14,4 +14,5 @@ type Mutation {
type Query {
MyInviteCodes: [InviteCode]
isValidInviteCode(code: ID!): Boolean
+ getInviteCode: InviteCode
}
diff --git a/backend/yarn.lock b/backend/yarn.lock
index 7d6558da0..e01df5c33 100644
--- a/backend/yarn.lock
+++ b/backend/yarn.lock
@@ -910,6 +910,13 @@
dependencies:
regenerator-runtime "^0.13.4"
+"@babel/runtime@^7.9.2":
+ version "7.12.5"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.12.5.tgz#410e7e487441e1b360c29be715d870d9b985882e"
+ integrity sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==
+ dependencies:
+ regenerator-runtime "^0.13.4"
+
"@babel/template@^7.7.4", "@babel/template@^7.8.3", "@babel/template@^7.8.6":
version "7.8.6"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b"
@@ -956,32 +963,32 @@
exec-sh "^0.3.2"
minimist "^1.2.0"
-"@graphql-toolkit/common@0.9.12":
- version "0.9.12"
- resolved "https://registry.yarnpkg.com/@graphql-toolkit/common/-/common-0.9.12.tgz#a3bf91d00130100cfa0a72be718b16cc1011ab45"
- integrity sha512-NjBVxeM1GB0bldiBm4UQoxKtbu/hjRfcqDEt1JPia+uXgFde3qTxtF5IZkworRFNCp+8KPSVrwcreQHSfFW63w==
+"@graphql-toolkit/common@0.10.4":
+ version "0.10.4"
+ resolved "https://registry.yarnpkg.com/@graphql-toolkit/common/-/common-0.10.4.tgz#7785f2a3f14559d0778859c49f4442078c196695"
+ integrity sha512-HQ3HaxCqX+UE8y/0h7LMDBBGSIKJxY/gaQesaksvE2Y+N4NpSWdiW6HpOcgXfC2HGf9yM0hEdsERzzL8z3mbHQ==
dependencies:
aggregate-error "3.0.1"
camel-case "4.1.1"
- graphql-tools-fork "9.0.1"
+ graphql-tools "5.0.0"
lodash "4.17.15"
-"@graphql-toolkit/file-loading@0.9.12":
- version "0.9.12"
- resolved "https://registry.yarnpkg.com/@graphql-toolkit/file-loading/-/file-loading-0.9.12.tgz#f7d5d7e042df59c4e6c8fe3417c67dd1cddf9043"
- integrity sha512-xEmKkbWI5FXQARTN3dbPzshwcmduOJUvx1QGsfcuryVDdQJZynUEaPF7IxYaFpvQF7vwxGIRdff/jC/a5ypDrA==
+"@graphql-toolkit/file-loading@0.10.4":
+ version "0.10.4"
+ resolved "https://registry.yarnpkg.com/@graphql-toolkit/file-loading/-/file-loading-0.10.4.tgz#50e8933e44b17853544c1fe63350df93f33a5e80"
+ integrity sha512-oUmy/sO3BJfax85pVKI7FZ6TWrViNuWXoJkRM293YV9bKGuYU9TgqZoHyM+oEqWO5ruXCL/nCdw3cIBau+rSNA==
dependencies:
globby "11.0.0"
unixify "1.0.0"
-"@graphql-toolkit/schema-merging@0.9.12":
- version "0.9.12"
- resolved "https://registry.yarnpkg.com/@graphql-toolkit/schema-merging/-/schema-merging-0.9.12.tgz#f058a7d256a4ed0e61e9874f90ae03326b81668e"
- integrity sha512-ciqxLeMw7KPbJcq/xgnPbGyJGRO6bO1zQcdWCUSssyw8VDRHj5PFqEOAzT88eZQkEtg3qLN/wQEypeyFyNTHzw==
+"@graphql-toolkit/schema-merging@0.10.4":
+ version "0.10.4"
+ resolved "https://registry.yarnpkg.com/@graphql-toolkit/schema-merging/-/schema-merging-0.10.4.tgz#2428590a531a33e9fe03be27cce9030f1c4c044b"
+ integrity sha512-naL6reYBuILLMrkMfKz0lOLL0kl6gGYnaaywnO/Dgp9F4NeAxDdAs5CV6Fy9NO5OzePFP58Dnc4sh2RyYrrFJg==
dependencies:
- "@graphql-toolkit/common" "0.9.12"
+ "@graphql-toolkit/common" "0.10.4"
deepmerge "4.2.2"
- graphql-tools-fork "9.0.1"
+ graphql-tools "5.0.0"
tslib "1.11.1"
"@hapi/address@2.x.x":
@@ -2036,7 +2043,7 @@ apollo-link-context@~1.0.20:
apollo-link "^1.2.14"
tslib "^1.9.3"
-apollo-link-http-common@^0.2.15, apollo-link-http-common@^0.2.16:
+apollo-link-http-common@^0.2.14, apollo-link-http-common@^0.2.16:
version "0.2.16"
resolved "https://registry.yarnpkg.com/apollo-link-http-common/-/apollo-link-http-common-0.2.16.tgz#756749dafc732792c8ca0923f9a40564b7c59ecc"
integrity sha512-2tIhOIrnaF4UbQHf7kjeQA/EmSorB7+HyJIIrUjJOKBgnXwuexi8aMecRlqTIDWcyVXCeqLhUnztMa6bOH/jTg==
@@ -2054,7 +2061,7 @@ apollo-link-http@~1.5.17:
apollo-link-http-common "^0.2.16"
tslib "^1.9.3"
-apollo-link@^1.0.0, apollo-link@^1.2.13, apollo-link@^1.2.14:
+apollo-link@^1.0.0, apollo-link@^1.2.12, apollo-link@^1.2.14:
version "1.2.14"
resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.14.tgz#3feda4b47f9ebba7f4160bef8b977ba725b684d9"
integrity sha512-p67CMEFP7kOG1JZ0ZkYZwRDa369w5PIjtMjvrQd/HnIV8FRsHRqLqK+oAZQnFa1DDdZtOtHTi+aMIW6EatC2jg==
@@ -2211,6 +2218,16 @@ apollo-tracing@^0.12.0:
apollo-server-env "^2.4.5"
apollo-server-plugin-base "^0.10.2"
+apollo-upload-client@^13.0.0:
+ version "13.0.0"
+ resolved "https://registry.yarnpkg.com/apollo-upload-client/-/apollo-upload-client-13.0.0.tgz#146d1ddd85d711fcac8ca97a72d3ca6787f2b71b"
+ integrity sha512-lJ9/bk1BH1lD15WhWRha2J3+LrXrPIX5LP5EwiOUHv8PCORp4EUrcujrA3rI5hZeZygrTX8bshcuMdpqpSrvtA==
+ dependencies:
+ "@babel/runtime" "^7.9.2"
+ apollo-link "^1.2.12"
+ apollo-link-http-common "^0.2.14"
+ extract-files "^8.0.0"
+
apollo-utilities@1.3.3, apollo-utilities@^1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.3.tgz#f1854715a7be80cd810bc3ac95df085815c0787c"
@@ -2688,13 +2705,15 @@ browser-resolve@^1.11.3:
resolve "1.1.7"
browserslist@^4.8.3, browserslist@^4.9.1:
- version "4.9.1"
- resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.9.1.tgz#01ffb9ca31a1aef7678128fc6a2253316aa7287c"
- integrity sha512-Q0DnKq20End3raFulq6Vfp1ecB9fh8yUNV55s8sekaDDeqBaCtWlRHCUdaWyUeSSBJM7IbM6HcsyaeYqgeDhnw==
+ version "4.16.6"
+ resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.6.tgz#d7901277a5a88e554ed305b183ec9b0c08f66fa2"
+ integrity sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==
dependencies:
- caniuse-lite "^1.0.30001030"
- electron-to-chromium "^1.3.363"
- node-releases "^1.1.50"
+ caniuse-lite "^1.0.30001219"
+ colorette "^1.2.2"
+ electron-to-chromium "^1.3.723"
+ escalade "^3.1.1"
+ node-releases "^1.1.71"
bser@^2.0.0:
version "2.1.0"
@@ -2790,10 +2809,10 @@ camelize@1.0.0:
resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b"
integrity sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs=
-caniuse-lite@^1.0.30001030:
- version "1.0.30001032"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001032.tgz#b8d224914e2cd7f507085583d4e38144c652bce4"
- integrity sha512-8joOm7BwcpEN4BfVHtfh0hBXSAPVYk+eUIcNntGtMkUWy/6AKRCDZINCLe3kB1vHhT2vBxBF85Hh9VlPXi/qjA==
+caniuse-lite@^1.0.30001219:
+ version "1.0.30001230"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001230.tgz#8135c57459854b2240b57a4a6786044bdc5a9f71"
+ integrity sha512-5yBd5nWCBS+jWKTcHOzXwo5xzcj4ePE/yjtkZyUV1BTUmrBaA9MRGC+e7mxnqXSA90CmCA8L3eKLaSUkt099IQ==
capture-exit@^2.0.0:
version "2.0.0"
@@ -3068,6 +3087,11 @@ color-name@~1.1.4:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+colorette@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94"
+ integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==
+
colors@^1.1.2:
version "1.3.3"
resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.3.tgz#39e005d546afe01e01f9c4ca8fa50f686a01205d"
@@ -3392,10 +3416,10 @@ data-urls@^1.1.0:
whatwg-mimetype "^2.2.0"
whatwg-url "^7.0.0"
-date-fns@2.11.1:
- version "2.11.1"
- resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.11.1.tgz#197b8be1bbf5c5e6fe8bea817f0fe111820e7a12"
- integrity sha512-3RdUoinZ43URd2MJcquzBbDQo+J87cSzB8NkXdZiN5ia1UNyep0oCyitfiL88+R7clGTeq/RniXAc16gWyAu1w==
+date-fns@2.22.1:
+ version "2.22.1"
+ resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.22.1.tgz#1e5af959831ebb1d82992bf67b765052d8f0efc4"
+ integrity sha512-yUFPQjrxEmIsMqlHhAhmxkuH769baF21Kk+nZwZGyrMoyLA+LugaQtC0+Tqf9CBUUULWwUJt6Q5ySI3LJDDCGg==
dateformat@^3.0.3:
version "3.0.3"
@@ -3730,10 +3754,10 @@ ee-first@1.1.1:
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
-electron-to-chromium@^1.3.363:
- version "1.3.368"
- resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.368.tgz#d7597e04339f7ca70762031ec473d38eb2df6acb"
- integrity sha512-fqzDipW3p+uDkHUHFPrdW3wINRKcJsbnJwBD7hgaQEQwcuLSvNLw6SeUp5gKDpTbmTl7zri7IZfhsdTUTnygJg==
+electron-to-chromium@^1.3.723:
+ version "1.3.739"
+ resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.739.tgz#f07756aa92cabd5a6eec6f491525a64fe62f98b9"
+ integrity sha512-+LPJVRsN7hGZ9EIUUiWCpO7l4E3qBYHNadazlucBfsXBbccDFNKUBAgzE68FnkWGJPwD/AfKhSzL+G+Iqb8A4A==
emoji-regex@^7.0.1:
version "7.0.3"
@@ -3880,6 +3904,11 @@ es6-symbol@^3.1.1, es6-symbol@~3.1.1:
d "1"
es5-ext "~0.10.14"
+escalade@^3.1.1:
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
+ integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
+
escape-html@~1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
@@ -4299,10 +4328,10 @@ extglob@^2.0.4:
snapdragon "^0.8.1"
to-regex "^3.0.1"
-extract-files@^7.0.0:
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/extract-files/-/extract-files-7.0.0.tgz#3dc7853320ff7876ec62d6e98f2f4e6f3e6282f6"
- integrity sha512-3AUlT7TD+DbQXNe3t70QrgJU6Wgcp7rk1Zm0vqWz8OYnw4vxihgG0TgZ2SIGrVqScc4WfOu7B4a0BezGJ0YqvQ==
+extract-files@^8.0.0:
+ version "8.1.0"
+ resolved "https://registry.yarnpkg.com/extract-files/-/extract-files-8.1.0.tgz#46a0690d0fe77411a2e3804852adeaa65cd59288"
+ integrity sha512-PTGtfthZK79WUMk+avLmwx3NGdU8+iVFXC2NMGxKsn0MnihOG2lvumj+AZo8CTwTrwjXDgZ5tztbRlEdRjBonQ==
extsprintf@1.3.0:
version "1.3.0"
@@ -4849,19 +4878,19 @@ graphql-tag@^2.9.2, graphql-tag@~2.10.3:
resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.3.tgz#ea1baba5eb8fc6339e4c4cf049dabe522b0edf03"
integrity sha512-4FOv3ZKfA4WdOKJeHdz6B3F/vxBLSgmBcGeAFPf4n1F64ltJUvOOerNj0rsJxONQGdhUMynQIvd6LzB+1J5oKA==
-graphql-tools-fork@9.0.1:
- version "9.0.1"
- resolved "https://registry.yarnpkg.com/graphql-tools-fork/-/graphql-tools-fork-9.0.1.tgz#fc8df40c108bdba3268999dea355cc614c765038"
- integrity sha512-kM6mUNVekgnWKtVqLGQ9HvQqQ3zZVPZRg1esltBoohsbUMaChl+9QkjBjoMxnZPnbTGOOGGagopNBQALIBysNg==
+graphql-tools@5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-5.0.0.tgz#67281c834a0e29f458adba8018f424816fa627e9"
+ integrity sha512-5zn3vtn//382b7G3Wzz3d5q/sh+f7tVrnxeuhTMTJ7pWJijNqLxH7VEzv8VwXCq19zAzHYEosFHfXiK7qzvk7w==
dependencies:
- apollo-link "^1.2.13"
- apollo-link-http-common "^0.2.15"
+ apollo-link "^1.2.14"
+ apollo-upload-client "^13.0.0"
deprecated-decorator "^0.1.6"
- extract-files "^7.0.0"
form-data "^3.0.0"
iterall "^1.3.0"
node-fetch "^2.6.0"
- uuid "^7.0.2"
+ tslib "^1.11.1"
+ uuid "^7.0.3"
graphql-tools@^4.0.0, graphql-tools@^4.0.4, graphql-tools@^4.0.5:
version "4.0.8"
@@ -6606,13 +6635,13 @@ merge-descriptors@1.0.1:
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
-merge-graphql-schemas@^1.7.7:
- version "1.7.7"
- resolved "https://registry.yarnpkg.com/merge-graphql-schemas/-/merge-graphql-schemas-1.7.7.tgz#673898f97c384d88fd66e49359b2f604e5864441"
- integrity sha512-3mjFHXpsF+bnsnSy/FlZiWCcNo7AULuG8kEBE+YVX8qbzNrEPJVhSg7ANr5LjQZbGtSlTz7fa3Wqw0bfbQ5R5Q==
+merge-graphql-schemas@^1.7.8:
+ version "1.7.8"
+ resolved "https://registry.yarnpkg.com/merge-graphql-schemas/-/merge-graphql-schemas-1.7.8.tgz#11a0a672a38a61d988c09ffdebe1bd4f8418de48"
+ integrity sha512-C3EJ1i86OjmbcCT524wVPRl17M5VZzgyh9kIGYAlYnAILX+7xfh8cCbMKfehh9n4opZg6CtcPogCiVZ6PB2NyQ==
dependencies:
- "@graphql-toolkit/file-loading" "0.9.12"
- "@graphql-toolkit/schema-merging" "0.9.12"
+ "@graphql-toolkit/file-loading" "0.10.4"
+ "@graphql-toolkit/schema-merging" "0.10.4"
tslib "1.11.1"
merge-stream@^2.0.0:
@@ -6942,10 +6971,10 @@ ms@^2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
-mustache@^4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.0.1.tgz#d99beb031701ad433338e7ea65e0489416c854a2"
- integrity sha512-yL5VE97+OXn4+Er3THSmTdCFCtx5hHWzrolvH+JObZnUYwuaG7XV+Ch4fR2cIrcYI0tFHxS7iyFYl14bW8y2sA==
+mustache@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/mustache/-/mustache-4.2.0.tgz#e5892324d60a12ec9c2a73359edca52972bf6f64"
+ integrity sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==
mute-stream@0.0.8:
version "0.0.8"
@@ -7150,12 +7179,10 @@ node-pre-gyp@^0.12.0:
semver "^5.3.0"
tar "^4"
-node-releases@^1.1.50:
- version "1.1.50"
- resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.50.tgz#803c40d2c45db172d0410e4efec83aa8c6ad0592"
- integrity sha512-lgAmPv9eYZ0bGwUYAKlr8MG6K4CvWliWqnkcT2P8mMAgVrH3lqfBPorFlxiG1pHQnqmavJZ9vbMXUTNyMLbrgQ==
- dependencies:
- semver "^6.3.0"
+node-releases@^1.1.71:
+ version "1.1.72"
+ resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.72.tgz#14802ab6b1039a79a0c7d662b610a5bbd76eacbe"
+ integrity sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw==
nodemailer-html-to-text@^3.1.0:
version "3.1.0"
@@ -7816,10 +7843,10 @@ prettier-linter-helpers@^1.0.0:
dependencies:
fast-diff "^1.1.2"
-prettier@~2.2.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.0.tgz#8a03c7777883b29b37fb2c4348c66a78e980418b"
- integrity sha512-yYerpkvseM4iKD/BXLYUkQV5aKt4tQPqaGW6EsZjzyu0r7sVZZNPJW4Y8MyKmicp6t42XUPcBVA+H6sB3gqndw==
+prettier@~2.3.2:
+ version "2.3.2"
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.2.tgz#ef280a05ec253712e486233db5c6f23441e7342d"
+ integrity sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==
pretty-format@^25.3.0:
version "25.3.0"
@@ -9406,15 +9433,15 @@ ts-invariant@^0.4.0:
dependencies:
tslib "^1.9.3"
-tslib@1.11.1, tslib@^1.9.0:
+tslib@1.11.1:
version "1.11.1"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35"
integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==
-tslib@^1.10.0, tslib@^1.9.3:
- version "1.13.0"
- resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043"
- integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==
+tslib@^1.10.0, tslib@^1.11.1, tslib@^1.9.0, tslib@^1.9.3:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
+ integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tunnel-agent@^0.6.0:
version "0.6.0"
@@ -9689,7 +9716,7 @@ uuid@^3.1.0, uuid@^3.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
-uuid@^7.0.2:
+uuid@^7.0.3:
version "7.0.3"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b"
integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==
@@ -9972,9 +9999,9 @@ xtend@^4.0.1:
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
y18n@^4.0.0:
- version "4.0.0"
- resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b"
- integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==
+ version "4.0.3"
+ resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
+ integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
yallist@^2.1.2:
version "2.1.2"
@@ -9997,9 +10024,9 @@ yallist@^4.0.0:
integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
yargs-parser@^18.1.1:
- version "18.1.1"
- resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.1.tgz#bf7407b915427fc760fcbbccc6c82b4f0ffcbd37"
- integrity sha512-KRHEsOM16IX7XuLnMOqImcPNbLVXMNHYAoFc3BKR8Ortl5gzDbtXvvEoGx9imk5E+X1VeNKNlcHr8B8vi+7ipA==
+ version "18.1.3"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
+ integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
dependencies:
camelcase "^5.0.0"
decamelize "^1.2.0"
diff --git a/cypress/README.md b/cypress/README.md
index fb94cc522..d9e235786 100644
--- a/cypress/README.md
+++ b/cypress/README.md
@@ -13,16 +13,13 @@ $ docker-compose up
## Setup without docker
-First, you have to tell cypress how to connect to your local neo4j database
-among other things. You can copy our template configuration and change the new
-file according to your needs.
+To start the services that are required for cypress testing manually. You basically need the whole setup to run:
-To start the services that are required for cypress testing, run:
+- backend
+- webapp
+- neo4j
-```bash
-# in the top level folder Ocelot-Social/
-$ yarn cypress:setup
-```
+Navigate to the corresponding folders and start the services.
## Install cypress
@@ -35,21 +32,11 @@ without docker, you would have to install cypress and its dependencies first:
$ yarn install
```
-## Run cypress
-
-After verifying that there are no errors with the servers starting, open another tab in your terminal and run the following command:
-
-```bash
-$ yarn cypress:run
-```
-
-
-
### Open Interactive Test Console
-If you are like me, you might want to see some visual output. The interactive cypress environment also helps at debugging your tests, you can even time travel between individual steps and see the exact state of the app.
+The interactive cypress test console allows to run tests and have visual feedback on that. The interactive cypress environment also helps at debugging the tests, you can even time travel between individual steps and see the exact state of the app.
-To use this feature, instead of `yarn cypress:run` you would run the following command:
+To use this feature run:
```bash
$ yarn cypress:open
@@ -57,7 +44,19 @@ $ yarn cypress:open

+## Run cypress
+
+To run cypress without the user interface:
+
+```bash
+$ yarn cypress:run
+```
+
+This is used to run cypress in CI or in console
+
+
+
## Write some Tests
Check out the Cypress documentation for further information on how to write tests:
-[https://docs.cypress.io/guides/getting-started/writing-your-first-test.html\#Write-a-simple-test](https://docs.cypress.io/guides/getting-started/writing-your-first-test.html#Write-a-simple-test)
+[Write-a-simple-test](https://docs.cypress.io/guides/getting-started/writing-your-first-test.html#Write-a-simple-test)
diff --git a/cypress/constants/terms-and-conditions-version.js b/cypress/constants/terms-and-conditions-version.js
deleted file mode 100644
index 7b2a8fb5d..000000000
--- a/cypress/constants/terms-and-conditions-version.js
+++ /dev/null
@@ -1,2 +0,0 @@
-// please change also version in file "webapp/constants/terms-and-conditions-version.js"
-export const VERSION = '0.0.4'
\ No newline at end of file
diff --git a/cypress.json b/cypress/cypress.json
similarity index 55%
rename from cypress.json
rename to cypress/cypress.json
index 284bdbd34..3f41ca3e5 100644
--- a/cypress.json
+++ b/cypress/cypress.json
@@ -1,8 +1,10 @@
{
"projectId": "qa7fe2",
"ignoreTestFiles": "*.js",
+ "chromeWebSecurity": false,
"baseUrl": "http://localhost:3000",
- "env": {
- "RETRIES": 2
+ "retries": {
+ "runMode": 2,
+ "openMode": 0
}
}
diff --git a/cypress/integration/Admin.PinPost.feature b/cypress/integration/Admin.PinPost.feature
new file mode 100644
index 000000000..a5297d894
--- /dev/null
+++ b/cypress/integration/Admin.PinPost.feature
@@ -0,0 +1,43 @@
+Feature: Admin pins a post
+ As an admin
+ I want to pin a post so that it always appears at the top
+ In order to make sure all network users read it
+ e.g. notify people about security incidents, maintenance downtimes
+
+ Background:
+ Given the following "users" are in the database:
+ | slug | email | password | id | name | role | termsAndConditionsAgreedVersion |
+ | user | user@example.org | abcd | user | User-Chad | user | 0.0.4 |
+ | admin | admin@example.org | 1234 | admin | Admin-Man | admin | 0.0.4 |
+ Given the following "posts" are in the database:
+ | id | title | pinned | createdAt |
+ | p1 | Some other post | | 2020-01-21 |
+ | p2 | Houston we have a problem | x | 2020-01-20 |
+ | p3 | Yet another post | | 2020-01-19 |
+
+ Scenario: Pinned post always appears on the top of the newsfeed
+ When I am logged in as "user"
+ And I navigate to page "/"
+ Then the first post on the newsfeed has the title:
+ """
+ Houston we have a problem
+ """
+ And the post with title "Houston we have a problem" has a ribbon for pinned posts
+
+ Scenario: Ordinary users cannot pin a post
+ When I am logged in as "user"
+ And I navigate to page "/"
+ And I open the content menu of post "Yet another post"
+ Then there is no button to pin a post
+
+ Scenario: Admins are allowed to pin a post
+ When I am logged in as "admin"
+ And I navigate to page "/"
+ And I open the content menu of post "Yet another post"
+ And I click on "pin post"
+ Then I see a toaster with "Post pinned successfully"
+ And the first post on the newsfeed has the title:
+ """
+ Yet another post
+ """
+ And the post with title "Yet another post" has a ribbon for pinned posts
diff --git a/cypress/integration/Admin.PinPost/I_open_the_content_menu_of_post_{string}.js b/cypress/integration/Admin.PinPost/I_open_the_content_menu_of_post_{string}.js
new file mode 100644
index 000000000..78e9ab1ea
--- /dev/null
+++ b/cypress/integration/Admin.PinPost/I_open_the_content_menu_of_post_{string}.js
@@ -0,0 +1,7 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When("I open the content menu of post {string}", (title)=> {
+ cy.contains('.post-teaser', title)
+ .find('.content-menu .base-button')
+ .click()
+})
\ No newline at end of file
diff --git a/cypress/integration/Admin.PinPost/the_post_with_title_{string}_has_a_ribbon_for_pinned_posts.js b/cypress/integration/Admin.PinPost/the_post_with_title_{string}_has_a_ribbon_for_pinned_posts.js
new file mode 100644
index 000000000..1db51d2b0
--- /dev/null
+++ b/cypress/integration/Admin.PinPost/the_post_with_title_{string}_has_a_ribbon_for_pinned_posts.js
@@ -0,0 +1,9 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("the post with title {string} has a ribbon for pinned posts", (title) => {
+ cy.get(".post-teaser").contains(title)
+ .parent()
+ .parent()
+ .find(".ribbon.--pinned")
+ .should("contain", "Announcement")
+})
\ No newline at end of file
diff --git a/cypress/integration/Admin.PinPost/there_is_no_button_to_pin_a_post.js b/cypress/integration/Admin.PinPost/there_is_no_button_to_pin_a_post.js
new file mode 100644
index 000000000..859b9faf1
--- /dev/null
+++ b/cypress/integration/Admin.PinPost/there_is_no_button_to_pin_a_post.js
@@ -0,0 +1,7 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("there is no button to pin a post", () => {
+ cy.get("a.ds-menu-item-link")
+ .should('contain', "Report Post") // sanity check
+ .should('not.contain', "Pin post")
+})
\ No newline at end of file
diff --git a/cypress/integration/Admin.TagOverview.feature b/cypress/integration/Admin.TagOverview.feature
new file mode 100644
index 000000000..fcec638ec
--- /dev/null
+++ b/cypress/integration/Admin.TagOverview.feature
@@ -0,0 +1,31 @@
+Feature: Admin tag overview
+ As a database administrator
+ I would like to see a overview of all tags and their usage
+ In order to be able to decide which tags are popular or not
+
+ Background:
+ Given the following "users" are in the database:
+ | slug | email | password | id | name | role | termsAndConditionsAgreedVersion |
+ | admin | admin@example.org | 1234 | admin | Admin-Man | admin | 0.0.4 |
+ | u1 | u1@example.org | 1234 | u1 | User1 | user | 0.0.4 |
+ | u2 | u2@example.org | 1234 | u2 | User2 | user | 0.0.4 |
+ | u3 | u3@example.org | 1234 | u3 | User3 | user | 0.0.4 |
+ And the following "tags" are in the database:
+ | id |
+ | Ecology |
+ | Nature |
+ | Democracy |
+ And the following "posts" are in the database:
+ | id | title | authorId | tagIds |
+ | p1 | P1 from U1 | u1 | Nature, Democracy |
+ | p2 | P2 from U2 | u2 | Ecology, Democracy |
+ | p3 | P3 from U3 | u3 | Nature, Democracy |
+ And I am logged in as "admin"
+
+ Scenario: See an overview of tags
+ When I navigate to page "/admin/hashtags"
+ Then I can see the following table:
+ | No. | Hashtags | Users | Posts |
+ | 1 | #Democracy | 3 | 3 |
+ | 2 | #Nature | 2 | 2 |
+ | 3 | #Ecology | 1 | 1 |
\ No newline at end of file
diff --git a/cypress/integration/internationalization/Internationalization.feature b/cypress/integration/Internationalization.feature
similarity index 89%
rename from cypress/integration/internationalization/Internationalization.feature
rename to cypress/integration/Internationalization.feature
index 18070d888..5eb6bbc3f 100644
--- a/cypress/integration/internationalization/Internationalization.feature
+++ b/cypress/integration/Internationalization.feature
@@ -4,7 +4,7 @@ Feature: Internationalization
In order to be able to understand the interface
Background:
- Given I am on the "login" page
+ Given I navigate to page "/login"
Scenario Outline: I select "" in the language menu and see ""
When I select "" in the language menu
@@ -18,6 +18,6 @@ Feature: Internationalization
| English | Login |
Scenario: Keep preferred language after refresh
- Given I previously switched the language to "Français"
+ When I select "Français" in the language menu
And I refresh the page
Then the whole user interface appears in "Français"
diff --git a/cypress/integration/Internationalization/I_see_a_button_with_the_label_{string}.js b/cypress/integration/Internationalization/I_see_a_button_with_the_label_{string}.js
new file mode 100644
index 000000000..a67f9d7df
--- /dev/null
+++ b/cypress/integration/Internationalization/I_see_a_button_with_the_label_{string}.js
@@ -0,0 +1,5 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("I see a button with the label {string}", label => {
+ cy.contains("button", label);
+});
\ No newline at end of file
diff --git a/cypress/integration/Internationalization/I_select_{string}_in_the_language_menu.js b/cypress/integration/Internationalization/I_select_{string}_in_the_language_menu.js
new file mode 100644
index 000000000..b850a7573
--- /dev/null
+++ b/cypress/integration/Internationalization/I_select_{string}_in_the_language_menu.js
@@ -0,0 +1,8 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When("I select {string} in the language menu", language => {
+ cy.get(".locale-menu")
+ .click();
+ cy.contains(".locale-menu-popover a", language)
+ .click();
+});
\ No newline at end of file
diff --git a/cypress/integration/Internationalization/the_whole_user_interface_appears_in_{string}.js b/cypress/integration/Internationalization/the_whole_user_interface_appears_in_{string}.js
new file mode 100644
index 000000000..4d80b8a0d
--- /dev/null
+++ b/cypress/integration/Internationalization/the_whole_user_interface_appears_in_{string}.js
@@ -0,0 +1,8 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+import locales from '../../../webapp/locales'
+
+Then("the whole user interface appears in {string}", language => {
+ const { code } = locales.find((entry) => entry.name === language);
+ cy.get(`html[lang=${code}]`);
+ cy.getCookie("locale").should("have.property", "value", code);
+});
\ No newline at end of file
diff --git a/cypress/integration/Moderation.HidePost.feature b/cypress/integration/Moderation.HidePost.feature
new file mode 100644
index 000000000..0ef802267
--- /dev/null
+++ b/cypress/integration/Moderation.HidePost.feature
@@ -0,0 +1,40 @@
+Feature: Hide Posts
+ As a moderator
+ I would like to be able to hide posts from the public
+ to enforce our network's code of conduct and/or legal regulations
+
+ Background:
+ Given the following "users" are in the database:
+ | slug | email | password | id | name | role | termsAndConditionsAgreedVersion |
+ | user | user@example.org | abcd | user | User-Chad | user | 0.0.4 |
+ | moderator | moderator@example.org | 1234 | moderator | Mod-Man | moderator | 0.0.4 |
+ Given the following "posts" are in the database:
+ | id | title | deleted | disabled |
+ | p1 | This post should be visible | | |
+ | p2 | This post is disabled | | x |
+ | p3 | This post is deleted | x | |
+
+ Scenario: Disabled posts don't show up on the newsfeed as user
+ When I am logged in as "user"
+ And I navigate to page "/"
+ Then I should see only 1 posts on the newsfeed
+ And the first post on the newsfeed has the title:
+ """
+ This post should be visible
+ """
+
+ Scenario: Disabled posts show up on the newsfeed as moderator
+ When I am logged in as "moderator"
+ And I navigate to page "/"
+ Then I should see only 2 posts on the newsfeed
+ And the first post on the newsfeed has the title:
+ """
+ This post is disabled
+ """
+
+ Scenario: Visiting a disabled post's page should return 404
+ Given I am logged in as "user"
+ Then the page "/post/this-post-is-disabled" returns a 404 error with a message:
+ """
+ This post could not be found
+ """
diff --git a/cypress/integration/Moderation.HidePost/I_should_see_only_{int}_posts_on_the_newsfeed.js b/cypress/integration/Moderation.HidePost/I_should_see_only_{int}_posts_on_the_newsfeed.js
new file mode 100644
index 000000000..611365bb0
--- /dev/null
+++ b/cypress/integration/Moderation.HidePost/I_should_see_only_{int}_posts_on_the_newsfeed.js
@@ -0,0 +1,7 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("I should see only {int} posts on the newsfeed", posts => {
+ cy.get(".post-teaser")
+ .should("have.length", posts);
+});
+
\ No newline at end of file
diff --git a/cypress/integration/Moderation.HidePost/the_page_{string}_returns_a_404_error_with_a_message.js b/cypress/integration/Moderation.HidePost/the_page_{string}_returns_a_404_error_with_a_message.js
new file mode 100644
index 000000000..6d9cfb2ef
--- /dev/null
+++ b/cypress/integration/Moderation.HidePost/the_page_{string}_returns_a_404_error_with_a_message.js
@@ -0,0 +1,14 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("the page {string} returns a 404 error with a message:", (route, message) => {
+ cy.request({
+ url: route,
+ failOnStatusCode: false
+ })
+ .its("status")
+ .should("eq", 404);
+ cy.visit(route, {
+ failOnStatusCode: false
+ });
+ cy.get(".error-message").should("contain", message);
+});
\ No newline at end of file
diff --git a/cypress/integration/moderation/ReportContent.feature b/cypress/integration/Moderation.ReportContent.feature
similarity index 61%
rename from cypress/integration/moderation/ReportContent.feature
rename to cypress/integration/Moderation.ReportContent.feature
index be1a07786..518020bd0 100644
--- a/cypress/integration/moderation/ReportContent.feature
+++ b/cypress/integration/Moderation.ReportContent.feature
@@ -8,51 +8,47 @@ Feature: Report and Moderate
So I can look into it and decide what to do
Background:
- Given we have the following user accounts:
- | id | name |
- | u67 | David Irving |
- | annoying-user | I'm gonna mute Moderators and Admins HA HA HA |
-
- Given we have the following posts in our database:
+ Given the following "users" are in the database:
+ | slug | email | password | id | name | role | termsAndConditionsAgreedVersion |
+ | user | user@example.org | abcd | user | User-Chad | user | 0.0.4 |
+ | moderator | moderator@example.org | 1234 | moderator | Mod-Man | moderator | 0.0.4 |
+ | annoying | annoying@example.org | 1234 | annoying-user | I'm gonna mute Moderators and Admins HA HA HA | user | 0.0.4 |
+ And the following "posts" are in the database:
| authorId | id | title | content |
- | u67 | p1 | The Truth about the Holocaust | It never existed! |
+ | annoying-user | p1 | The Truth about the Holocaust | It never existed! |
| annoying-user | p2 | Fake news | This content is demonstratably infactual in some way |
+
Scenario Outline: Report a post from various pages
- Given I am logged in with a "user" role
- When I see David Irving's post on the
+ When I am logged in as "user"
+ And I navigate to page ""
And I click on "Report Post" from the content menu of the post
And I confirm the reporting dialog because it is a criminal act under German law:
"""
Do you really want to report the contribution "The Truth about the Holocaust"?
"""
- Then I see a success message:
- """
- Thanks for reporting!
- """
+ Then I see a toaster with "Thanks for reporting!"
Examples:
- | Page |
- | landing page |
- | post page |
+ | Page |
+ | / |
+ | /post/p1 |
Scenario: Report user
- Given I am logged in with a "user" role
- And I see David Irving's post on the post page
+ Given I am logged in as "user"
+ And I navigate to page "/post/the-truth-about-the-holocaust"
When I click on the author
And I click on "Report User" from the content menu in the user info box
And I confirm the reporting dialog because he is a holocaust denier:
"""
- Do you really want to report the user "David Irving"?
- """
- Then I see a success message:
- """
- Thanks for reporting!
+ Do you really want to report the user "I'm gonna mute Moderators and …"?
"""
+ Then I see a toaster with "Thanks for reporting!"
Scenario: Review reported content
Given somebody reported the following posts:
| submitterEmail | resourceId | reasonCategory | reasonDescription |
| p1.submitter@example.org | p1 | discrimination_etc | Offensive content |
- And I am logged in with a "moderator" role
+ And I am logged in as "moderator"
+ And I navigate to page "/"
When I click on the avatar menu in the top right corner
And I click on "Moderation"
Then I see all the reported posts including the one from above
@@ -62,7 +58,8 @@ Feature: Report and Moderate
Given somebody reported the following posts:
| submitterEmail | resourceId | reasonCategory | reasonDescription |
| p2.submitter@example.org | p2 | other | Offensive content |
- And I am logged in with a "moderator" role
+ And I am logged in as "moderator"
+ And I navigate to page "/"
And there is an annoying user who has muted me
When I click on the avatar menu in the top right corner
And I click on "Moderation"
@@ -70,6 +67,7 @@ Feature: Report and Moderate
And I can visit the post page
Scenario: Normal user can't see the moderation page
- Given I am logged in with a "user" role
+ Given I am logged in as "user"
+ And I navigate to page "/"
When I click on the avatar menu in the top right corner
Then I can't see the moderation menu item
diff --git a/cypress/integration/Moderation.ReportContent/I_can_t_see_the_moderation_menu_item.js b/cypress/integration/Moderation.ReportContent/I_can_t_see_the_moderation_menu_item.js
new file mode 100644
index 000000000..96706281a
--- /dev/null
+++ b/cypress/integration/Moderation.ReportContent/I_can_t_see_the_moderation_menu_item.js
@@ -0,0 +1,11 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then(`I can't see the moderation menu item`, () => {
+ cy.get('.avatar-menu-popover')
+ .find('a[href="/settings"]', 'Settings')
+ .should('exist') // OK, the dropdown is actually open
+
+ cy.get('.avatar-menu-popover')
+ .find('a[href="/moderation"]', 'Moderation')
+ .should('not.exist')
+})
\ No newline at end of file
diff --git a/cypress/integration/Moderation.ReportContent/I_can_visit_the_post_page.js b/cypress/integration/Moderation.ReportContent/I_can_visit_the_post_page.js
new file mode 100644
index 000000000..8ca69da50
--- /dev/null
+++ b/cypress/integration/Moderation.ReportContent/I_can_visit_the_post_page.js
@@ -0,0 +1,7 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then('I can visit the post page', () => {
+ cy.contains('Fake news').click()
+ cy.location('pathname').should('contain', '/post')
+ .get('.base-card .title').should('contain', 'Fake news')
+})
\ No newline at end of file
diff --git a/cypress/integration/Moderation.ReportContent/I_click_on_Report_Post_from_the_content_menu_of_the_post.js b/cypress/integration/Moderation.ReportContent/I_click_on_Report_Post_from_the_content_menu_of_the_post.js
new file mode 100644
index 000000000..30682b009
--- /dev/null
+++ b/cypress/integration/Moderation.ReportContent/I_click_on_Report_Post_from_the_content_menu_of_the_post.js
@@ -0,0 +1,11 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When('I click on "Report Post" from the content menu of the post', () => {
+ cy.contains('.base-card', 'The Truth about the Holocaust')
+ .find('.content-menu .base-button')
+ .click({force: true})
+
+ cy.get('.popover .ds-menu-item-link')
+ .contains('Report Post')
+ .click()
+})
\ No newline at end of file
diff --git a/cypress/integration/Moderation.ReportContent/I_click_on_the_author.js b/cypress/integration/Moderation.ReportContent/I_click_on_the_author.js
new file mode 100644
index 000000000..3a6600ff6
--- /dev/null
+++ b/cypress/integration/Moderation.ReportContent/I_click_on_the_author.js
@@ -0,0 +1,7 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When('I click on the author', () => {
+ cy.get('.user-teaser')
+ .click()
+ .url().should('include', '/profile/')
+})
\ No newline at end of file
diff --git a/cypress/integration/Moderation.ReportContent/I_click_on_the_avatar_menu_in_the_top_right_corner.js b/cypress/integration/Moderation.ReportContent/I_click_on_the_avatar_menu_in_the_top_right_corner.js
new file mode 100644
index 000000000..27830b239
--- /dev/null
+++ b/cypress/integration/Moderation.ReportContent/I_click_on_the_avatar_menu_in_the_top_right_corner.js
@@ -0,0 +1,5 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When("I click on the avatar menu in the top right corner", () => {
+ cy.get(".avatar-menu").click();
+});
\ No newline at end of file
diff --git a/cypress/integration/Moderation.ReportContent/I_confirm_the_reporting_dialog.js b/cypress/integration/Moderation.ReportContent/I_confirm_the_reporting_dialog.js
new file mode 100644
index 000000000..4009fa4e8
--- /dev/null
+++ b/cypress/integration/Moderation.ReportContent/I_confirm_the_reporting_dialog.js
@@ -0,0 +1,16 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When(/^I confirm the reporting dialog .*:$/, message => {
+ cy.contains(message) // wait for element to become visible
+ cy.get('.ds-modal')
+ .within(() => {
+ cy.get('.ds-radio-option-label')
+ .first()
+ .click({
+ force: true
+ })
+ cy.get('button')
+ .contains('Report')
+ .click()
+ })
+})
\ No newline at end of file
diff --git a/cypress/integration/Moderation.ReportContent/I_see_all_the_reported_posts_including_from_the_user_who_muted_me.js b/cypress/integration/Moderation.ReportContent/I_see_all_the_reported_posts_including_from_the_user_who_muted_me.js
new file mode 100644
index 000000000..522cd6c78
--- /dev/null
+++ b/cypress/integration/Moderation.ReportContent/I_see_all_the_reported_posts_including_from_the_user_who_muted_me.js
@@ -0,0 +1,7 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then('I see all the reported posts including from the user who muted me', () => {
+ cy.get('table tbody').within(() => {
+ cy.contains('tr', 'Fake news')
+ })
+})
\ No newline at end of file
diff --git a/cypress/integration/Moderation.ReportContent/I_see_all_the_reported_posts_including_the_one_from_above.js b/cypress/integration/Moderation.ReportContent/I_see_all_the_reported_posts_including_the_one_from_above.js
new file mode 100644
index 000000000..66c9baf61
--- /dev/null
+++ b/cypress/integration/Moderation.ReportContent/I_see_all_the_reported_posts_including_the_one_from_above.js
@@ -0,0 +1,7 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then('I see all the reported posts including the one from above', () => {
+ cy.get('table tbody').within(() => {
+ cy.contains('tr', 'The Truth about the Holocaust')
+ })
+})
\ No newline at end of file
diff --git a/cypress/integration/Moderation.ReportContent/each_list_item_links_to_the_post_page.js b/cypress/integration/Moderation.ReportContent/each_list_item_links_to_the_post_page.js
new file mode 100644
index 000000000..9ce69d6de
--- /dev/null
+++ b/cypress/integration/Moderation.ReportContent/each_list_item_links_to_the_post_page.js
@@ -0,0 +1,6 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then('each list item links to the post page', () => {
+ cy.contains('The Truth about the Holocaust').click();
+ cy.location('pathname').should('contain', '/post')
+})
\ No newline at end of file
diff --git a/cypress/integration/Moderation.ReportContent/somebody_reported_the_following_posts.js b/cypress/integration/Moderation.ReportContent/somebody_reported_the_following_posts.js
new file mode 100644
index 000000000..ce876a081
--- /dev/null
+++ b/cypress/integration/Moderation.ReportContent/somebody_reported_the_following_posts.js
@@ -0,0 +1,23 @@
+import { Given } from "cypress-cucumber-preprocessor/steps";
+import { gql } from '../../../backend/src/helpers/jest'
+
+Given('somebody reported the following posts:', table => {
+ table.hashes().forEach(({ submitterEmail, resourceId, reasonCategory, reasonDescription }) => {
+ const submitter = {
+ email: submitterEmail,
+ password: '1234'
+ }
+ cy.factory()
+ .build('user', {}, submitter)
+ .authenticateAs(submitter)
+ .mutate(gql`mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
+ fileReport(resourceId: $resourceId, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription) {
+ reportId
+ }
+ }`, {
+ resourceId,
+ reasonCategory,
+ reasonDescription
+ })
+ })
+})
\ No newline at end of file
diff --git a/cypress/integration/Moderation.ReportContent/there_is_an_annoying_user_who_has_muted_me.js b/cypress/integration/Moderation.ReportContent/there_is_an_annoying_user_who_has_muted_me.js
new file mode 100644
index 000000000..8d475ee43
--- /dev/null
+++ b/cypress/integration/Moderation.ReportContent/there_is_an_annoying_user_who_has_muted_me.js
@@ -0,0 +1,13 @@
+Given("there is an annoying user who has muted me", () => {
+ cy.neode()
+ .first("User", {
+ role: 'moderator'
+ })
+ .then(mutedUser => {
+ cy.neode()
+ .first("User", {
+ id: 'user'
+ })
+ .relateTo(mutedUser, "muted");
+ });
+});
\ No newline at end of file
diff --git a/cypress/integration/Notification.Mention.feature b/cypress/integration/Notification.Mention.feature
new file mode 100644
index 000000000..cadfe11dd
--- /dev/null
+++ b/cypress/integration/Notification.Mention.feature
@@ -0,0 +1,29 @@
+Feature: Notification for a mention
+ As a user
+ I want to be notified if somebody mentions me in a post or comment
+ In order join conversations about or related to me
+
+ Background:
+ Given the following "users" are in the database:
+ | slug | email | password | id | name | termsAndConditionsAgreedVersion |
+ | wolle-aus-hamburg | wolle@example.org | 1234 | wolle | Wolle aus Hamburg | 0.0.4 |
+ | matt-rider | matt@example.org | 4321 | matt | Matt Rider | 0.0.4 |
+
+ Scenario: Mention another user, re-login as this user and see notifications
+ Given I am logged in as "wolle-aus-hamburg"
+ And I navigate to page "/"
+ And I navigate to page "/post/create"
+ And I start to write a new post with the title "Hey Matt" beginning with:
+ """
+ Big shout to our fellow contributor
+ """
+ And mention "@matt-rider" in the text
+ And I click on "save button"
+ And I am logged in as "matt-rider"
+ And I navigate to page "/"
+ And see 1 unread notifications in the top menu
+ And open the notification menu and click on the first item
+ And I wait for 750 milliseconds
+ Then I am on page "/post/.*/hey-matt"
+ And the unread counter is removed
+ And the notification menu button links to the all notifications page
\ No newline at end of file
diff --git a/cypress/integration/Notification.Mention/I_start_to_write_a_new_post_with_the_title_{string}_beginning_with.js b/cypress/integration/Notification.Mention/I_start_to_write_a_new_post_with_the_title_{string}_beginning_with.js
new file mode 100644
index 000000000..fde5042c1
--- /dev/null
+++ b/cypress/integration/Notification.Mention/I_start_to_write_a_new_post_with_the_title_{string}_beginning_with.js
@@ -0,0 +1,8 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When("I start to write a new post with the title {string} beginning with:", (title, intro) => {
+ cy.get('input[name="title"]')
+ .type(title);
+ cy.get(".ProseMirror")
+ .type(intro);
+});
\ No newline at end of file
diff --git a/cypress/integration/Notification.Mention/mention_{string}_in_the_text.js b/cypress/integration/Notification.Mention/mention_{string}_in_the_text.js
new file mode 100644
index 000000000..fa8a29d4a
--- /dev/null
+++ b/cypress/integration/Notification.Mention/mention_{string}_in_the_text.js
@@ -0,0 +1,9 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When("mention {string} in the text", mention => {
+ cy.get(".ProseMirror")
+ .type(" @");
+ cy.get(".suggestion-list__item")
+ .contains(mention)
+ .click();
+});
\ No newline at end of file
diff --git a/cypress/integration/Notification.Mention/open_the_notification_menu_and_click_on_the_first_item.js b/cypress/integration/Notification.Mention/open_the_notification_menu_and_click_on_the_first_item.js
new file mode 100644
index 000000000..0d3917f38
--- /dev/null
+++ b/cypress/integration/Notification.Mention/open_the_notification_menu_and_click_on_the_first_item.js
@@ -0,0 +1,10 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When("open the notification menu and click on the first item", () => {
+ cy.get(".notifications-menu")
+ .invoke('show')
+ .click(); // "invoke('show')" because of the delay for show the menu
+ cy.get(".notification .link")
+ .first()
+ .click({force: true});
+});
\ No newline at end of file
diff --git a/cypress/integration/Notification.Mention/see_{int}_unread_notifications_in_the_top_menu.js b/cypress/integration/Notification.Mention/see_{int}_unread_notifications_in_the_top_menu.js
new file mode 100644
index 000000000..124b61873
--- /dev/null
+++ b/cypress/integration/Notification.Mention/see_{int}_unread_notifications_in_the_top_menu.js
@@ -0,0 +1,6 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("see {int} unread notifications in the top menu", count => {
+ cy.get(".notifications-menu")
+ .should("contain", count);
+});
\ No newline at end of file
diff --git a/cypress/integration/Notification.Mention/the_notification_menu_button_links_to_the_all_notifications_page.js b/cypress/integration/Notification.Mention/the_notification_menu_button_links_to_the_all_notifications_page.js
new file mode 100644
index 000000000..e40827a16
--- /dev/null
+++ b/cypress/integration/Notification.Mention/the_notification_menu_button_links_to_the_all_notifications_page.js
@@ -0,0 +1,8 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("the notification menu button links to the all notifications page", () => {
+ cy.get(".notifications-menu")
+ .click();
+ cy.location("pathname")
+ .should("contain", "/notifications");
+});
\ No newline at end of file
diff --git a/cypress/integration/Notification.Mention/the_unread_counter_is_removed.js b/cypress/integration/Notification.Mention/the_unread_counter_is_removed.js
new file mode 100644
index 000000000..3859103e8
--- /dev/null
+++ b/cypress/integration/Notification.Mention/the_unread_counter_is_removed.js
@@ -0,0 +1,6 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("the unread counter is removed", () => {
+ cy.get('.notifications-menu .counter-icon')
+ .should('not.exist');
+});
\ No newline at end of file
diff --git a/cypress/integration/PersistentLinks.feature b/cypress/integration/PersistentLinks.feature
new file mode 100644
index 000000000..89f9d9654
--- /dev/null
+++ b/cypress/integration/PersistentLinks.feature
@@ -0,0 +1,31 @@
+Feature: Persistent Links
+ As a user
+ I want all links to carry permanent information that identifies the linked resource
+ In order to have persistent links even if a part of the URL might change
+ | | Modifiable | Referenceable | Unique | Purpose |
+ | -- | -- | -- | -- | -- |
+ | ID | no | yes | yes | Identity, Traceability, Links |
+ | Slug | yes | yes | yes | @-Mentions, SEO-friendly URL |
+ | Name | yes | no | no | Search, self-description |
+
+ Background:
+ Given the following "users" are in the database:
+ | slug | email | password | id | name | termsAndConditionsAgreedVersion |
+ | thehawk | hawk@example.org | abcd | MHNqce98y1 | Stephen Hawking | 0.0.4 |
+ | narrator | narrator@example.org | 1234 | narrator | Nathan Narrator | 0.0.4 |
+ And the following "posts" are in the database:
+ | id | title | slug |
+ | bWBjpkTKZp | 101 Essays that will change the way you think | 101-essays |
+ And I am logged in as "narrator"
+
+ Scenario Outline: Link with healable information is valid and gets auto-completed
+ When I navigate to page ""
+ Then I am on page ""
+ Examples:
+ | url | redirectUrl | reason |
+ | /profile/thehawk | /profile/MHNqce98y1/thehawk | Identifiable user slug |
+ | /post/101-essays | /post/bWBjpkTKZp/101-essays | Identifiable post slug |
+ | /profile/MHNqce98y1 | /profile/MHNqce98y1/thehawk | Identifiable user ID |
+ | /post/bWBjpkTKZp | /post/bWBjpkTKZp/101-essays | Identifiable post ID |
+ | /profile/MHNqce98y1/stephen-hawking | /profile/MHNqce98y1/thehawk | Identifiable user ID takes precedence over slug |
+ | /post/bWBjpkTKZp/the-way-you-think | /post/bWBjpkTKZp/101-essays | Identifiable post ID takes precedence over slug |
diff --git a/cypress/integration/Post.Comment.feature b/cypress/integration/Post.Comment.feature
new file mode 100644
index 000000000..1ec0c602a
--- /dev/null
+++ b/cypress/integration/Post.Comment.feature
@@ -0,0 +1,49 @@
+Feature: Comments on post
+ As a user
+ I want to comment and see comments on contributions of others
+ To be able to express my thoughts and emotions about these, discuss, and add give further information.
+
+ Background:
+ Given the following "users" are in the database:
+ | slug | email | password | id | name | termsAndConditionsAgreedVersion |
+ | peter-pan| peter@pan.com | abcd | id-of-peter-pan| Peter Pan | 0.0.4 |
+ | narrator | narrator@example.org | 1234 | narrator | Nathan Narrator | 0.0.4 |
+ And the following "posts" are in the database:
+ | id | title | slug | authorId |
+ | bWBjpkTKZp | 101 Essays that will change the way you think | 101-essays | id-of-peter-pan |
+ And the following "comments" are in the database:
+ | postId | content | authorId |
+ | bWBjpkTKZp | @peter-pan reply to me | id-of-peter-pan |
+ And I am logged in as "narrator"
+
+ Scenario: Comment creation
+ Given I navigate to page "/post/bWBjpkTKZp/101-essays"
+ And I comment the following:
+ """
+ Ocelot.social rocks
+ """
+ And I click on "comment button"
+ Then my comment should be successfully created
+ And I should see my comment
+ And the editor should be cleared
+
+ Scenario: View medium length comments
+ Given I navigate to page "/post/bWBjpkTKZp/101-essays"
+ And I type in a comment with 305 characters
+ And I click on "comment button"
+ Then my comment should be successfully created
+ And I should see the entirety of my comment
+ And the editor should be cleared
+
+ Scenario: View long comments
+ Given I navigate to page "/post/bWBjpkTKZp/101-essays"
+ And I type in a comment with 1205 characters
+ And I click on "comment button"
+ Then my comment should be successfully created
+ And I should see an abbreviated version of my comment
+ And the editor should be cleared
+
+ Scenario: Direct reply to Comment
+ Given I navigate to page "/post/bWBjpkTKZp/101-essays"
+ And I click on "reply button"
+ Then it should create a mention in the CommentForm
diff --git a/cypress/integration/Post.Comment/I_comment_the_following.js b/cypress/integration/Post.Comment/I_comment_the_following.js
new file mode 100644
index 000000000..0f5a5049c
--- /dev/null
+++ b/cypress/integration/Post.Comment/I_comment_the_following.js
@@ -0,0 +1,7 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When("I comment the following:", async text => {
+ const comment = text.replace("\n", " ")
+ cy.task('pushValue', { name: 'lastComment', value: comment })
+ cy.get(".editor .ProseMirror").type(comment);
+});
\ No newline at end of file
diff --git a/cypress/integration/Post.Comment/I_should_see_an_abbreviated_version_of_my_comment.js b/cypress/integration/Post.Comment/I_should_see_an_abbreviated_version_of_my_comment.js
new file mode 100644
index 000000000..d0b7940f0
--- /dev/null
+++ b/cypress/integration/Post.Comment/I_should_see_an_abbreviated_version_of_my_comment.js
@@ -0,0 +1,6 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("I should see an abbreviated version of my comment", () => {
+ cy.get("article.comment-card")
+ .should("contain", "show more")
+});
\ No newline at end of file
diff --git a/cypress/integration/Post.Comment/I_should_see_my_comment.js b/cypress/integration/Post.Comment/I_should_see_my_comment.js
new file mode 100644
index 000000000..356593f9c
--- /dev/null
+++ b/cypress/integration/Post.Comment/I_should_see_my_comment.js
@@ -0,0 +1,13 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("I should see my comment", () => {
+ cy.get("article.comment-card p")
+ .should("contain", "Ocelot.social rocks")
+ .get(".user-teaser span.slug")
+ .should("contain", "@peter-pan") // specific enough
+ .get(".user-avatar img")
+ .should("have.attr", "src")
+ .and("contain", 'https://') // some url
+ .get(".user-teaser > .info > .text")
+ .should("contain", "today at");
+});
\ No newline at end of file
diff --git a/cypress/integration/Post.Comment/I_should_see_the_entirety_of_my_comment.js b/cypress/integration/Post.Comment/I_should_see_the_entirety_of_my_comment.js
new file mode 100644
index 000000000..a903fa4d0
--- /dev/null
+++ b/cypress/integration/Post.Comment/I_should_see_the_entirety_of_my_comment.js
@@ -0,0 +1,6 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("I should see the entirety of my comment", () => {
+ cy.get("article.comment-card")
+ .should("not.contain", "show more")
+});
\ No newline at end of file
diff --git a/cypress/integration/Post.Comment/I_type_in_a_comment_with_{int}_characters.js b/cypress/integration/Post.Comment/I_type_in_a_comment_with_{int}_characters.js
new file mode 100644
index 000000000..1522c0e64
--- /dev/null
+++ b/cypress/integration/Post.Comment/I_type_in_a_comment_with_{int}_characters.js
@@ -0,0 +1,9 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When("I type in a comment with {int} characters", size => {
+ var c="";
+ for (var i = 0; i < size; i++) {
+ c += "c"
+ }
+ cy.get(".editor .ProseMirror").type(c);
+});
\ No newline at end of file
diff --git a/cypress/integration/Post.Comment/it_should_create_a_mention_in_the_CommentForm.js b/cypress/integration/Post.Comment/it_should_create_a_mention_in_the_CommentForm.js
new file mode 100644
index 000000000..3468badad
--- /dev/null
+++ b/cypress/integration/Post.Comment/it_should_create_a_mention_in_the_CommentForm.js
@@ -0,0 +1,7 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("it should create a mention in the CommentForm", () => {
+ cy.get(".ProseMirror a")
+ .should('have.class', 'mention')
+ .should('contain', '@peter-pan')
+})
\ No newline at end of file
diff --git a/cypress/integration/Post.Comment/my_comment_should_be_successfully_created.js b/cypress/integration/Post.Comment/my_comment_should_be_successfully_created.js
new file mode 100644
index 000000000..766106ddf
--- /dev/null
+++ b/cypress/integration/Post.Comment/my_comment_should_be_successfully_created.js
@@ -0,0 +1,5 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("my comment should be successfully created", () => {
+ cy.get(".iziToast-message").contains("Comment submitted!");
+});
\ No newline at end of file
diff --git a/cypress/integration/Post.Comment/the_editor_should_be_cleared.js b/cypress/integration/Post.Comment/the_editor_should_be_cleared.js
new file mode 100644
index 000000000..579fc2ca9
--- /dev/null
+++ b/cypress/integration/Post.Comment/the_editor_should_be_cleared.js
@@ -0,0 +1,5 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("the editor should be cleared", () => {
+ cy.get(".ProseMirror p").should("have.class", "is-empty");
+});
\ No newline at end of file
diff --git a/cypress/integration/Post.Create.feature b/cypress/integration/Post.Create.feature
new file mode 100644
index 000000000..cdb3e1008
--- /dev/null
+++ b/cypress/integration/Post.Create.feature
@@ -0,0 +1,24 @@
+Feature: Create a post
+ As an logged in user
+ I would like to create a post
+ To say something to everyone in the community
+
+ Background:
+ Given the following "users" are in the database:
+ | slug | email | password | id | name | termsAndConditionsAgreedVersion |
+ | narrator | narrator@example.org | 1234 | narrator | Nathan Narrator | 0.0.4 |
+ And I am logged in as "narrator"
+ And I navigate to page "/"
+
+ Scenario: Create a post
+ When I click on "create post button"
+ Then I am on page "post/create"
+ When I choose "My first post" as the title
+ And I choose the following text as content:
+ """
+ Ocelot.social is a free and open-source social network
+ for active citizenship.
+ """
+ And I click on "save button"
+ Then I am on page "/post/.*/my-first-post"
+ And the post was saved successfully
diff --git a/cypress/integration/Post.Create/I_choose_{string}_as_the_title.js b/cypress/integration/Post.Create/I_choose_{string}_as_the_title.js
new file mode 100644
index 000000000..9fbf8e58f
--- /dev/null
+++ b/cypress/integration/Post.Create/I_choose_{string}_as_the_title.js
@@ -0,0 +1,8 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When("I choose {string} as the title", async title => {
+ const lastPost = {}
+ lastPost.title = title.replace("\n", " ");
+ cy.task('pushValue', { name: 'lastPost', value: lastPost })
+ cy.get('input[name="title"]').type(lastPost.title);
+});
\ No newline at end of file
diff --git a/cypress/integration/Post.Create/the_post_was_saved_successfully.js b/cypress/integration/Post.Create/the_post_was_saved_successfully.js
new file mode 100644
index 000000000..eec2f819b
--- /dev/null
+++ b/cypress/integration/Post.Create/the_post_was_saved_successfully.js
@@ -0,0 +1,8 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("the post was saved successfully", async () => {
+ cy.task('getValue', 'lastPost').then(lastPost => {
+ cy.get(".base-card > .title").should("contain", lastPost.title);
+ cy.get(".content").should("contain", lastPost.content);
+ })
+});
\ No newline at end of file
diff --git a/cypress/integration/Post.Images.feature b/cypress/integration/Post.Images.feature
new file mode 100644
index 000000000..68c223394
--- /dev/null
+++ b/cypress/integration/Post.Images.feature
@@ -0,0 +1,66 @@
+Feature: Upload/Delete images on posts
+ As a user
+ I would like to be able to add/delete an image to/from my Post
+ So that I can personalize my posts
+
+ Background:
+ Given the following "users" are in the database:
+ | slug | email | password | id | name | termsAndConditionsAgreedVersion |
+ | narrator | narrator@example.org | 1234 | narrator | Nathan Narrator | 0.0.4 |
+ And the following "posts" are in the database:
+ | authorId | id | title | content |
+ | narrator | p1 | Post to be updated | successfully updated |
+ And I am logged in as "narrator"
+ And I navigate to page "/"
+
+ Scenario: Create a Post with a Teaser Image
+ When I click on "create post button"
+ Then I wait for 750 milliseconds
+ Then I should be able to "add" a teaser image
+ And I add all required fields
+ And I click on "save button"
+ And I wait for 750 milliseconds
+ Then I am on page "/post/.*/new-post"
+ And I wait for 750 milliseconds
+ And the post was saved successfully with the "new" teaser image
+
+ Scenario: Update a Post to add an image
+ Given I navigate to page "/post/edit/p1"
+ Then I wait for 750 milliseconds
+ And I should be able to "change" a teaser image
+ And I click on "save button"
+ Then I see a toaster with "Saved!"
+ And I wait for 750 milliseconds
+ And I am on page "/post/.*/post-to-be-updated"
+ And I wait for 750 milliseconds
+ Then the post was saved successfully with the "updated" teaser image
+
+ Scenario: Add image, then add a different image
+ When I click on "create post button"
+ Then I wait for 750 milliseconds
+ Then I should be able to "add" a teaser image
+ And I should be able to "change" a teaser image
+ And the first image should not be displayed anymore
+
+ Scenario: Add image, then delete it
+ When I click on "create post button"
+ Then I wait for 750 milliseconds
+ Then I should be able to "add" a teaser image
+ Then I should be able to "remove" a teaser image
+ And I add all required fields
+ And I click on "save button"
+ And I wait for 750 milliseconds
+ Then I am on page "/post/.*/new-post"
+ And I wait for 750 milliseconds
+ And the "new" post was saved successfully without a teaser image
+
+ Scenario: Delete existing image
+ Given I navigate to page "/post/edit/p1"
+ Then I wait for 750 milliseconds
+ And my post has a teaser image
+ Then I should be able to "remove" a teaser image
+ And I click on "save button"
+ And I wait for 750 milliseconds
+ Then I am on page "/post/.*/post-to-be-updated"
+ And I wait for 750 milliseconds
+ And the "updated" post was saved successfully without a teaser image
\ No newline at end of file
diff --git a/cypress/integration/Post.Images/I_add_all_required_fields.js b/cypress/integration/Post.Images/I_add_all_required_fields.js
new file mode 100644
index 000000000..52a95ab52
--- /dev/null
+++ b/cypress/integration/Post.Images/I_add_all_required_fields.js
@@ -0,0 +1,8 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("I add all required fields", () => {
+ cy.get('input[name="title"]')
+ .type('new post')
+ .get(".editor .ProseMirror")
+ .type('new post content')
+ })
\ No newline at end of file
diff --git a/cypress/integration/Post.Images/I_should_be_able_to_{string}_a_teaser_image.js b/cypress/integration/Post.Images/I_should_be_able_to_{string}_a_teaser_image.js
new file mode 100644
index 000000000..ce5b54f25
--- /dev/null
+++ b/cypress/integration/Post.Images/I_should_be_able_to_{string}_a_teaser_image.js
@@ -0,0 +1,30 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("I should be able to {string} a teaser image", condition => {
+ // cy.reload()
+ switch(condition){
+ case 'change':
+ cy.get('.delete-image-button')
+ .click()
+ cy.fixture('humanconnection.png').as('postTeaserImage').then(function() {
+ cy.get("#postdropzone").upload(
+ { fileContent: this.postTeaserImage, fileName: 'humanconnection.png', mimeType: "image/png" },
+ { subjectType: "drag-n-drop", force: true }
+ ).wait(750);
+ })
+ break;
+ case 'add':
+ cy.fixture('onourjourney.png').as('postTeaserImage').then(function() {
+ cy.get("#postdropzone").upload(
+ { fileContent: this.postTeaserImage, fileName: 'onourjourney.png', mimeType: "image/png" },
+ { subjectType: "drag-n-drop", force: true }
+ ).wait(750);
+ })
+ break;
+ case 'remove':
+ cy.get('.delete-image-button')
+ .click()
+ break;
+ }
+
+})
\ No newline at end of file
diff --git a/cypress/integration/Post.Images/my_post_has_a_teaser_image.js b/cypress/integration/Post.Images/my_post_has_a_teaser_image.js
new file mode 100644
index 000000000..66ff3c31d
--- /dev/null
+++ b/cypress/integration/Post.Images/my_post_has_a_teaser_image.js
@@ -0,0 +1,7 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When('my post has a teaser image', () => {
+ cy.get('.contribution-form .image')
+ .should('exist')
+ .and('have.attr', 'src')
+})
\ No newline at end of file
diff --git a/cypress/integration/Post.Images/the_first_image_should_not_be_displayed_anymore.js b/cypress/integration/Post.Images/the_first_image_should_not_be_displayed_anymore.js
new file mode 100644
index 000000000..867c97fdf
--- /dev/null
+++ b/cypress/integration/Post.Images/the_first_image_should_not_be_displayed_anymore.js
@@ -0,0 +1,9 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("the first image should not be displayed anymore", () => {
+ cy.get(".hero-image")
+ .children()
+ .get('.hero-image > .image')
+ .should('have.length', 1)
+ .and('have.attr', 'src')
+})
\ No newline at end of file
diff --git a/cypress/integration/Post.Images/the_post_was_saved_successfully_with_the_{string}_teaser_image.js b/cypress/integration/Post.Images/the_post_was_saved_successfully_with_the_{string}_teaser_image.js
new file mode 100644
index 000000000..ece83d878
--- /dev/null
+++ b/cypress/integration/Post.Images/the_post_was_saved_successfully_with_the_{string}_teaser_image.js
@@ -0,0 +1,11 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("the post was saved successfully with the {string} teaser image", condition => {
+ cy.get(".base-card > .title")
+ .should("contain", condition === 'updated' ? 'to be updated' : 'new post')
+ .get(".content")
+ .should("contain", condition === 'updated' ? 'successfully updated' : 'new post content')
+ .get('.post-page img')
+ .should("have.attr", "src")
+ .and("contains", condition === 'updated' ? 'humanconnection' : 'onourjourney')
+})
\ No newline at end of file
diff --git a/cypress/integration/Post.Images/the_{string}_post_was_saved_successfully_without_a_teaser_image.js b/cypress/integration/Post.Images/the_{string}_post_was_saved_successfully_without_a_teaser_image.js
new file mode 100644
index 000000000..abafcf0cc
--- /dev/null
+++ b/cypress/integration/Post.Images/the_{string}_post_was_saved_successfully_without_a_teaser_image.js
@@ -0,0 +1,12 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then('the {string} post was saved successfully without a teaser image', condition => {
+ cy.get(".base-card > .title")
+ .should("contain", condition === 'updated' ? 'to be updated' : 'new post')
+ .get(".content")
+ .should("contain", condition === 'updated' ? 'successfully updated' : 'new post content')
+ .get('.post-page')
+ .should('exist')
+ .get('.hero-image > .image')
+ .should('not.exist')
+})
\ No newline at end of file
diff --git a/cypress/integration/Post.feature b/cypress/integration/Post.feature
new file mode 100644
index 000000000..7a572b955
--- /dev/null
+++ b/cypress/integration/Post.feature
@@ -0,0 +1,23 @@
+Feature: See a post
+ As an logged in user
+ I would like to see a post
+ And to see the whole content of it
+
+ Background:
+ Given the following "users" are in the database:
+ | slug | email | password | id | name | termsAndConditionsAgreedVersion |
+ | peter-pan| peter@pan.com | abcd | id-of-peter-pan| Peter Pan | 0.0.4 |
+ | narrator | narrator@example.org | 1234 | narrator | Nathan Narrator | 0.0.4 |
+ And the following "posts" are in the database:
+ | id | title | slug | authorId | content |
+ | aBcDeFgHiJ | previously created post | previously-created-post | id-of-peter-pan | with some content |
+ And I am logged in as "narrator"
+
+ Scenario: See a post on the newsfeed
+ When I navigate to page "/"
+ Then the post shows up on the newsfeed at position 1
+
+ Scenario: Navigate to the Post Page
+ When I navigate to page "/"
+ And I click on "the first post"
+ Then I am on page "/post/.*"
diff --git a/cypress/integration/Post/the_post_shows_up_on_the_newsfeed_at_position_{int}.js b/cypress/integration/Post/the_post_shows_up_on_the_newsfeed_at_position_{int}.js
new file mode 100644
index 000000000..3b42ea58e
--- /dev/null
+++ b/cypress/integration/Post/the_post_shows_up_on_the_newsfeed_at_position_{int}.js
@@ -0,0 +1,8 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("the post shows up on the newsfeed at position {int}", index => {
+ const selector = `.post-teaser:nth-child(${index}) > .base-card`;
+ cy.get(selector).should("contain", 'previously created post');
+ cy.get(selector).should("contain", 'with some content');
+
+});
\ No newline at end of file
diff --git a/cypress/integration/search/Search.feature b/cypress/integration/Search.feature.broken
similarity index 59%
rename from cypress/integration/search/Search.feature
rename to cypress/integration/Search.feature.broken
index d128838f3..a770c757c 100644
--- a/cypress/integration/search/Search.feature
+++ b/cypress/integration/Search.feature.broken
@@ -4,20 +4,21 @@ Feature: Search
In order to find related content
Background:
- Given I have a user account
- And we have the following posts in our database:
+ Given the following "users" are in the database:
+ | slug | email | password | id | name | termsAndConditionsAgreedVersion |
+ | narrator | narrator@example.org | 1234 | narrator | Nathan Narrator | 0.0.4 |
+ | search-for-me | u1@example.org | 1234 | user-for-search | Search for me | 0.0.4 |
+ | not-to-be-found | u2@example.org | 1234 | just-an-id | Not to be found | 0.0.4 |
+ And the following "posts" are in the database:
| id | title | content |
| p1 | 101 Essays that will change the way you think | 101 Essays, of course (PR)! |
- | p2 | No content | will be found in this post, I guarantee |
- And we have the following user accounts:
- | slug | name | id |
- | search-for-me | Search for me | user-for-search |
- | not-to-be-found | Not to be found | just-an-id |
-
- Given I am logged in
+ | p2 | No content | will be found in this post, I guarantee |
+ And I am logged in as "narrator"
+ And I navigate to page "/"
Scenario: Search for specific words
When I search for "Essays"
+ And I wait for 3000 milliseconds
Then I should have one item in the select dropdown
Then I should see the following posts in the select dropdown:
| title |
@@ -25,8 +26,9 @@ Feature: Search
Scenario: Press enter opens search page
When I type "PR" and press Enter
- Then I should see the search results page
- Then I should see the following posts on the search results page
+ Then I am on page "/search/search-results"
+ And the search parameter equals "?search=PR"
+ Then I should see the following posts on the search results page:
| title |
| 101 Essays that will change the way you think |
@@ -36,8 +38,9 @@ Feature: Search
Scenario: Select entry goes to post
When I search for "Essays"
+ And I wait for 3000 milliseconds
And I select a post entry
- Then I should be on the post's page
+ Then I am on page "/post/p1/101-essays-that-will-change-the-way-you-think"
Scenario: Select dropdown content
When I search for "Essays"
@@ -52,4 +55,4 @@ Feature: Search
| slug |
| search-for-me |
And I select a user entry
- Then I should be on the user's profile
\ No newline at end of file
+ Then I am on page "/profile/user-for-search/search-for-me"
\ No newline at end of file
diff --git a/cypress/integration/Search/I_select_a_post_entry.js b/cypress/integration/Search/I_select_a_post_entry.js
new file mode 100644
index 000000000..25611f91e
--- /dev/null
+++ b/cypress/integration/Search/I_select_a_post_entry.js
@@ -0,0 +1,7 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When("I select a post entry", () => {
+ cy.get(".searchable-input .search-post")
+ .first()
+ .trigger("click");
+});
\ No newline at end of file
diff --git a/cypress/integration/Search/I_select_a_user_entry.js b/cypress/integration/Search/I_select_a_user_entry.js
new file mode 100644
index 000000000..b7222b2fb
--- /dev/null
+++ b/cypress/integration/Search/I_select_a_user_entry.js
@@ -0,0 +1,7 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("I select a user entry", () => {
+ cy.get(".searchable-input .user-teaser")
+ .first()
+ .trigger("click");
+})
\ No newline at end of file
diff --git a/cypress/integration/Search/I_should_have_one_item_in_the_select_dropdown.js b/cypress/integration/Search/I_should_have_one_item_in_the_select_dropdown.js
new file mode 100644
index 000000000..7e5188ab6
--- /dev/null
+++ b/cypress/integration/Search/I_should_have_one_item_in_the_select_dropdown.js
@@ -0,0 +1,7 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("I should have one item in the select dropdown", () => {
+ cy.get(".searchable-input .ds-select-dropdown").should($li => {
+ expect($li).to.have.length(1);
+ });
+});
\ No newline at end of file
diff --git a/cypress/integration/Search/I_should_not_see_posts_without_the_searched-for_term_in_the_select_dropdown.js b/cypress/integration/Search/I_should_not_see_posts_without_the_searched-for_term_in_the_select_dropdown.js
new file mode 100644
index 000000000..a76ed6a5d
--- /dev/null
+++ b/cypress/integration/Search/I_should_not_see_posts_without_the_searched-for_term_in_the_select_dropdown.js
@@ -0,0 +1,6 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("I should not see posts without the searched-for term in the select dropdown", () => {
+ cy.get(".ds-select-dropdown")
+ .should("not.contain","No searched for content");
+});
\ No newline at end of file
diff --git a/cypress/integration/Search/I_should_see_posts_with_the_searched-for_term_in_the_select_dropdown.js b/cypress/integration/Search/I_should_see_posts_with_the_searched-for_term_in_the_select_dropdown.js
new file mode 100644
index 000000000..ce755abb0
--- /dev/null
+++ b/cypress/integration/Search/I_should_see_posts_with_the_searched-for_term_in_the_select_dropdown.js
@@ -0,0 +1,6 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("I should see posts with the searched-for term in the select dropdown", () => {
+ cy.get(".ds-select-dropdown")
+ .should("contain","101 Essays that will change the way you think");
+});
\ No newline at end of file
diff --git a/cypress/integration/Search/I_should_see_the_following_posts_on_the_search_results_page.js b/cypress/integration/Search/I_should_see_the_following_posts_on_the_search_results_page.js
new file mode 100644
index 000000000..f703a04f5
--- /dev/null
+++ b/cypress/integration/Search/I_should_see_the_following_posts_on_the_search_results_page.js
@@ -0,0 +1,8 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("I should see the following posts on the search results page:", table => {
+ table.hashes().forEach(({ title }) => {
+ cy.get(".post-teaser")
+ .should("contain",title)
+ });
+});
\ No newline at end of file
diff --git a/cypress/integration/Search/I_should_see_the_following_users_in_the_select_dropdown.js b/cypress/integration/Search/I_should_see_the_following_users_in_the_select_dropdown.js
new file mode 100644
index 000000000..3e5e14043
--- /dev/null
+++ b/cypress/integration/Search/I_should_see_the_following_users_in_the_select_dropdown.js
@@ -0,0 +1,8 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("I should see the following users in the select dropdown:", table => {
+ cy.get(".search-heading").should("contain", "Users");
+ table.hashes().forEach(({ slug }) => {
+ cy.get(".ds-select-dropdown").should("contain", slug);
+ });
+});
\ No newline at end of file
diff --git a/cypress/integration/Search/I_type_{string}_and_press_Enter.js b/cypress/integration/Search/I_type_{string}_and_press_Enter.js
new file mode 100644
index 000000000..1a0fc6d42
--- /dev/null
+++ b/cypress/integration/Search/I_type_{string}_and_press_Enter.js
@@ -0,0 +1,8 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When("I type {string} and press Enter", value => {
+ cy.get(".searchable-input .ds-select input")
+ .focus()
+ .type(value)
+ .type("{enter}", { force: true });
+});
\ No newline at end of file
diff --git a/cypress/integration/Search/I_type_{string}_and_press_escape.js b/cypress/integration/Search/I_type_{string}_and_press_escape.js
new file mode 100644
index 000000000..a3cde6cda
--- /dev/null
+++ b/cypress/integration/Search/I_type_{string}_and_press_escape.js
@@ -0,0 +1,8 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When("I type {string} and press escape", value => {
+ cy.get(".searchable-input .ds-select input")
+ .focus()
+ .type(value)
+ .type("{esc}");
+});
\ No newline at end of file
diff --git a/cypress/integration/Search/the_search_field_should_clear.js b/cypress/integration/Search/the_search_field_should_clear.js
new file mode 100644
index 000000000..f571cdbd4
--- /dev/null
+++ b/cypress/integration/Search/the_search_field_should_clear.js
@@ -0,0 +1,6 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("the search field should clear", () => {
+ cy.get(".searchable-input .ds-select input")
+ .should("have.text", "");
+});
\ No newline at end of file
diff --git a/cypress/integration/Search/the_search_parameter_equals_{string}.js b/cypress/integration/Search/the_search_parameter_equals_{string}.js
new file mode 100644
index 000000000..b8473584c
--- /dev/null
+++ b/cypress/integration/Search/the_search_parameter_equals_{string}.js
@@ -0,0 +1,6 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("the search parameter equals {string}", search => {
+ cy.location("search")
+ .should("eq", search);
+});
\ No newline at end of file
diff --git a/cypress/integration/User.Authentication.feature b/cypress/integration/User.Authentication.feature
new file mode 100644
index 000000000..db7680bd4
--- /dev/null
+++ b/cypress/integration/User.Authentication.feature
@@ -0,0 +1,26 @@
+Feature: User authentication
+ As an user
+ I want to sign in
+ In order to be able to posts and do other contributions as myself
+ Furthermore I want to be able to stay logged in and logout again
+
+ Background:
+ Given the following "users" are in the database:
+ | email | password | id | name | slug | termsAndConditionsAgreedVersion |
+ | peterpan@example.org | 1234 | id-of-peter-pan | Peter Pan | peter-pan | 0.0.4 |
+
+ Scenario: Log in
+ When I navigate to page "/login"
+ And I fill in my credentials "peterpan@example.org" "1234"
+ And I click on "submit button"
+ Then I am logged in with username "Peter Pan"
+
+ Scenario: Refresh and stay logged in
+ Given I am logged in as "peter-pan"
+ When I refresh the page
+ Then I am logged in with username "Peter Pan"
+
+ Scenario: Log out
+ Given I am logged in as "peter-pan"
+ When I log out
+ Then I am on page "login"
diff --git a/cypress/integration/User.Authentication/I_am_logged_in_with_username_{string}.js b/cypress/integration/User.Authentication/I_am_logged_in_with_username_{string}.js
new file mode 100644
index 000000000..4383282bd
--- /dev/null
+++ b/cypress/integration/User.Authentication/I_am_logged_in_with_username_{string}.js
@@ -0,0 +1,7 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("I am logged in with username {string}", name => {
+ cy.get(".avatar-menu").click();
+ cy.get(".avatar-menu-popover").contains(name);
+ cy.get(".avatar-menu").click(); // Close menu again
+});
\ No newline at end of file
diff --git a/cypress/integration/user_profile/BlockUser.feature b/cypress/integration/User.Block.feature.broken
similarity index 57%
rename from cypress/integration/user_profile/BlockUser.feature
rename to cypress/integration/User.Block.feature.broken
index b5c510286..3d58c3c27 100644
--- a/cypress/integration/user_profile/BlockUser.feature
+++ b/cypress/integration/User.Block.feature.broken
@@ -1,16 +1,21 @@
-Feature: Block a User
+Feature: User - block an user
As a user
I'd like to have a button to block another user
To prevent him from seeing and interacting with my contributions
Background:
- Given I have a user account
- And there is an annoying user called "Harassing User"
- And I am logged in
+ Given the following "users" are in the database:
+ | email | password | id | name | slug | termsAndConditionsAgreedVersion |
+ | peterpan@example.org | 123 | id-of-peter-pan | Peter Pan | peter-pan | 0.0.4 |
+ | user@example.org | 123 | harassing-user | Harassing User | harassing-user | 0.0.4 |
+ And the following "posts" are in the database:
+ | id | title | slug | authorId |
+ | bWBjpkTKZp | previously created post | previously-created-post | id-of-peter-pan |
+ And I am logged in as "peter-pan"
Scenario: Block a user
- Given I am on the profile page of the annoying user
- When I click on "Block user" from the content menu in the user info box
+ When I navigate to page "profile/harassing-user"
+ And I click on "Block user" from the content menu in the user info box
And I "should" see "Unblock user" from the content menu in the user info box
And I navigate to my "Blocked users" settings page
Then I can see the following table:
@@ -19,42 +24,46 @@ Feature: Block a User
Scenario: Blocked user cannot interact with my contributions
Given I block the user "Harassing User"
- And I previously created a post
- And a blocked user visits the post page of one of my authored posts
+ And I am logged in as "harassing-user"
+ And I navigate to page "/post/previously-created-post"
Then they should see a text explaining why commenting is not possible
And they should not see the comment form
Scenario: Block a previously followed user
Given I follow the user "Harassing User"
- When I visit the profile page of the annoying user
+ When I navigate to page "/profile/harassing-user"
And I click on "Block user" from the content menu in the user info box
And I get removed from his follower collection
And I "should" see "Unblock user" from the content menu in the user info box
Scenario: Posts of blocked users are not filtered from search results
- Given "Harassing User" wrote a post "You can still see my posts"
+ Given "harassing-user" wrote a post "You can still see my posts"
And I block the user "Harassing User"
When I search for "see"
+ And I wait for 3000 milliseconds
Then I should see the following posts in the select dropdown:
| title |
| You can still see my posts |
Scenario: Blocked users can still see my posts
- Given I previously created a post
- And I block the user "Harassing User"
- And the "blocked" user searches for "previously created"
+ When I block the user "Harassing User"
+ And I am logged in as "harassing-user"
+ And I navigate to page "/"
+ And I search for "previously created"
+ And I wait for 3000 milliseconds
Then I should see the following posts in the select dropdown:
| title |
| previously created post |
Scenario: Blocked users cannot see they are blocked in their list
Given a user has blocked me
+ And I navigate to page "/"
And I navigate to my "Blocked users" settings page
Then I should see no users in my blocked users list
Scenario: Blocked users should not see link or button to unblock, only blocking users
Given a user has blocked me
- When I visit the profile page of the annoying user
+ When I navigate to page "/profile/harassing-user"
And I should see the "Follow" button
And I should not see "Unblock user" button
And I "should not" see "Unblock user" from the content menu in the user info box
diff --git a/cypress/integration/User.Block/I_block_the_user_{string}.js b/cypress/integration/User.Block/I_block_the_user_{string}.js
new file mode 100644
index 000000000..cde1d96b9
--- /dev/null
+++ b/cypress/integration/User.Block/I_block_the_user_{string}.js
@@ -0,0 +1,11 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When("I block the user {string}", name => {
+ cy.neode()
+ .first("User", { name })
+ .then(blockedUser => {
+ cy.neode()
+ .first("User", {id: "id-of-peter-pan"})
+ .relateTo(blockedUser, "blocked");
+ });
+});
\ No newline at end of file
diff --git a/cypress/integration/User.Block/I_should_not_see_{string}_button.js b/cypress/integration/User.Block/I_should_not_see_{string}_button.js
new file mode 100644
index 000000000..5bf0b7a68
--- /dev/null
+++ b/cypress/integration/User.Block/I_should_not_see_{string}_button.js
@@ -0,0 +1,6 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then('I should not see {string} button', button => {
+ cy.get('.base-card .action-buttons')
+ .should('have.length', 1)
+})
\ No newline at end of file
diff --git a/cypress/integration/User.Block/I_should_see_no_users_in_my_blocked_users_list.js b/cypress/integration/User.Block/I_should_see_no_users_in_my_blocked_users_list.js
new file mode 100644
index 000000000..11161ef2f
--- /dev/null
+++ b/cypress/integration/User.Block/I_should_see_no_users_in_my_blocked_users_list.js
@@ -0,0 +1,6 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("I should see no users in my blocked users list", () => {
+ cy.get('.ds-placeholder')
+ .should('contain', "So far, you have not blocked anybody.")
+})
\ No newline at end of file
diff --git a/cypress/integration/User.Block/I_should_see_the_{string}_button.js b/cypress/integration/User.Block/I_should_see_the_{string}_button.js
new file mode 100644
index 000000000..373800870
--- /dev/null
+++ b/cypress/integration/User.Block/I_should_see_the_{string}_button.js
@@ -0,0 +1,6 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then('I should see the {string} button', button => {
+ cy.get('.base-card .action-buttons .base-button')
+ .should('contain', button)
+})
\ No newline at end of file
diff --git a/cypress/integration/User.Block/I_{string}_see_{string}_from_the_content_menu_in_the_user_info_box.js b/cypress/integration/User.Block/I_{string}_see_{string}_from_the_content_menu_in_the_user_info_box.js
new file mode 100644
index 000000000..0f44b5192
--- /dev/null
+++ b/cypress/integration/User.Block/I_{string}_see_{string}_from_the_content_menu_in_the_user_info_box.js
@@ -0,0 +1,7 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("I {string} see {string} from the content menu in the user info box", (condition, link) => {
+ cy.get(".user-content-menu .base-button").click()
+ cy.get(".popover .ds-menu-item-link")
+ .should(condition === 'should' ? 'contain' : 'not.contain', link)
+})
\ No newline at end of file
diff --git a/cypress/integration/User.Block/a_user_has_blocked_me.js b/cypress/integration/User.Block/a_user_has_blocked_me.js
new file mode 100644
index 000000000..d1703407f
--- /dev/null
+++ b/cypress/integration/User.Block/a_user_has_blocked_me.js
@@ -0,0 +1,15 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When("a user has blocked me", () => {
+ cy.neode()
+ .first("User", {
+ name: "Peter Pan"
+ })
+ .then(blockedUser => {
+ cy.neode()
+ .first("User", {
+ name: 'Harassing User'
+ })
+ .relateTo(blockedUser, "blocked");
+ });
+});
\ No newline at end of file
diff --git a/cypress/integration/User.Block/they_should_not_see_the_comment_form.js b/cypress/integration/User.Block/they_should_not_see_the_comment_form.js
new file mode 100644
index 000000000..962934994
--- /dev/null
+++ b/cypress/integration/User.Block/they_should_not_see_the_comment_form.js
@@ -0,0 +1,5 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("they should not see the comment form", () => {
+ cy.get(".base-card").children().should('not.have.class', 'comment-form')
+})
\ No newline at end of file
diff --git a/cypress/integration/User.Block/they_should_see_a_text_explaining_why_commenting_is_not_possible.js b/cypress/integration/User.Block/they_should_see_a_text_explaining_why_commenting_is_not_possible.js
new file mode 100644
index 000000000..d95f3229c
--- /dev/null
+++ b/cypress/integration/User.Block/they_should_see_a_text_explaining_why_commenting_is_not_possible.js
@@ -0,0 +1,5 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("they should see a text explaining why commenting is not possible", () => {
+ cy.get('.ds-placeholder').should('contain', "Commenting is not possible at this time on this post.")
+})
\ No newline at end of file
diff --git a/cypress/integration/User.Mute.feature.broken b/cypress/integration/User.Mute.feature.broken
new file mode 100644
index 000000000..1390063f7
--- /dev/null
+++ b/cypress/integration/User.Mute.feature.broken
@@ -0,0 +1,60 @@
+Feature: Mute a User
+ As a user
+ I'd like to have a button to mute another user
+ To prevent him from seeing and interacting with my contributions
+
+ Background:
+ Given the following "users" are in the database:
+ | email | password | id | name | slug | termsAndConditionsAgreedVersion |
+ | peterpan@example.org | 123 | id-of-peter-pan | Peter Pan | peter-pan | 0.0.4 |
+ | user@example.org | 123 | annoying-user | Annoying User | annoying-user | 0.0.4 |
+ Given the following "posts" are in the database:
+ | id | title | content | authorId |
+ | im-not-muted | Post that should be seen | cause I'm not muted | id-of-peter-pan |
+ | bWBjpkTKZp | previously created post | previously-created-post | id-of-peter-pan |
+ And I am logged in as "peter-pan"
+
+ Scenario: Mute a user
+ Given I navigate to page "/profile/annoying-user"
+ When I click on "Mute user" from the content menu in the user info box
+ And I navigate to my "Muted users" settings page
+ Then I can see the following table:
+ | Avatar | Name |
+ | | Annoying User |
+
+ Scenario: Mute a previously followed user
+ Given I follow the user "Annoying User"
+ And "annoying-user" wrote a post "Spam Spam Spam"
+ When I navigate to page "/profile/annoying-user"
+ And I click on "Mute user" from the content menu in the user info box
+ Then the list of posts of this user is empty
+ And I get removed from his follower collection
+
+ Scenario: Posts of muted users are filtered from search results, users are not
+ Given "annoying-user" wrote a post "Spam Spam Spam"
+ When I search for "Spam"
+ And I wait for 3000 milliseconds
+ Then I should see the following posts in the select dropdown:
+ | title |
+ | Spam Spam Spam |
+ When I mute the user "Annoying User"
+ And I refresh the page
+ And I search for "Anno"
+ And I wait for 3000 milliseconds
+ Then the search should not contain posts by the annoying user
+ But the search should contain the annoying user
+ But I search for "not muted"
+ And I wait for 3000 milliseconds
+ Then I should see the following posts in the select dropdown:
+ | title |
+ | Post that should be seen |
+
+ Scenario: Muted users can still see my posts
+ And I mute the user "Annoying User"
+ And I am logged in as "annoying-user"
+ And I navigate to page "/"
+ And I search for "previously created"
+ And I wait for 3000 milliseconds
+ Then I should see the following posts in the select dropdown:
+ | title |
+ | previously created post |
diff --git a/cypress/integration/User.Mute/I_mute_the_user_{string}.js b/cypress/integration/User.Mute/I_mute_the_user_{string}.js
new file mode 100644
index 000000000..e0ed382cb
--- /dev/null
+++ b/cypress/integration/User.Mute/I_mute_the_user_{string}.js
@@ -0,0 +1,13 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When("I mute the user {string}", name => {
+ cy.neode()
+ .first("User", { name })
+ .then(mutedUser => {
+ cy.neode()
+ .first("User", {
+ name: "Peter Pan"
+ })
+ .relateTo(mutedUser, "muted");
+ });
+});
\ No newline at end of file
diff --git a/cypress/integration/User.Mute/the_list_of_posts_of_this_user_is_empty.js b/cypress/integration/User.Mute/the_list_of_posts_of_this_user_is_empty.js
new file mode 100644
index 000000000..038ca2168
--- /dev/null
+++ b/cypress/integration/User.Mute/the_list_of_posts_of_this_user_is_empty.js
@@ -0,0 +1,6 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("the list of posts of this user is empty", () => {
+ cy.get(".base-card").not(".post-link");
+ cy.get(".main-container").find(".ds-space.hc-empty");
+});
\ No newline at end of file
diff --git a/cypress/integration/User.Mute/the_search_should_contain_the_annoying_user.js b/cypress/integration/User.Mute/the_search_should_contain_the_annoying_user.js
new file mode 100644
index 000000000..d29eafc2f
--- /dev/null
+++ b/cypress/integration/User.Mute/the_search_should_contain_the_annoying_user.js
@@ -0,0 +1,13 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("the search should contain the annoying user", () => {
+ cy.get(".searchable-input .ds-select-dropdown")
+ .should($li => {
+ expect($li).to.have.length(1);
+ })
+ cy.get(".ds-select-dropdown .user-teaser .slug")
+ .should("contain", '@annoying-user');
+ cy.get(".searchable-input .ds-select input")
+ .focus()
+ .type("{esc}");
+})
\ No newline at end of file
diff --git a/cypress/integration/User.Mute/the_search_should_not_contain_posts_by_the_annoying_user.js b/cypress/integration/User.Mute/the_search_should_not_contain_posts_by_the_annoying_user.js
new file mode 100644
index 000000000..a2f0a01d7
--- /dev/null
+++ b/cypress/integration/User.Mute/the_search_should_not_contain_posts_by_the_annoying_user.js
@@ -0,0 +1,10 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("the search should not contain posts by the annoying user", () => {
+ cy.get(".searchable-input .ds-select-dropdown").should($li => {
+ expect($li).to.have.length(1);
+ })
+ cy.get(".ds-select-dropdown")
+ .should("not.have.class", '.search-post')
+ .should("not.contain", 'Spam')
+});
\ No newline at end of file
diff --git a/cypress/integration/UserProfile.Avatar.feature b/cypress/integration/UserProfile.Avatar.feature
new file mode 100644
index 000000000..abb3fea63
--- /dev/null
+++ b/cypress/integration/UserProfile.Avatar.feature
@@ -0,0 +1,20 @@
+Feature: User profile - Upload avatar image
+ As a user
+ I would like to be able to add an avatar image to my profile
+ So that I can personalize my profile
+
+ Background:
+ Given the following "users" are in the database:
+ | email | password | id | name | slug | termsAndConditionsAgreedVersion |
+ | peterpan@example.org | 123 | id-of-peter-pan | Peter Pan | peter-pan | 0.0.4 |
+ | user@example.org | 123 | user | User | user | 0.0.4 |
+ And I am logged in as "peter-pan"
+
+ Scenario: Change my UserProfile Image
+ And I navigate to page "/profile/peter-pan"
+ Then I should be able to change my profile picture
+
+ Scenario: Unable to change another user's avatar
+ Given I am logged in as "user"
+ And I navigate to page "/profile/peter-pan"
+ Then I cannot upload a picture
\ No newline at end of file
diff --git a/cypress/integration/UserProfile.Avatar/I_cannot_upload_a_picture.js b/cypress/integration/UserProfile.Avatar/I_cannot_upload_a_picture.js
new file mode 100644
index 000000000..d20a181f2
--- /dev/null
+++ b/cypress/integration/UserProfile.Avatar/I_cannot_upload_a_picture.js
@@ -0,0 +1,8 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("I cannot upload a picture", () => {
+ cy.get(".base-card")
+ .children()
+ .should("not.have.id", "customdropzone")
+ .should("have.class", "user-avatar");
+});
\ No newline at end of file
diff --git a/cypress/integration/UserProfile.Avatar/I_should_be_able_to_change_my_profile_picture.js b/cypress/integration/UserProfile.Avatar/I_should_be_able_to_change_my_profile_picture.js
new file mode 100644
index 000000000..f92789ef8
--- /dev/null
+++ b/cypress/integration/UserProfile.Avatar/I_should_be_able_to_change_my_profile_picture.js
@@ -0,0 +1,17 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("I should be able to change my profile picture", () => {
+ const avatarUpload = "onourjourney.png";
+
+ cy.fixture(avatarUpload, "base64").then(fileContent => {
+ cy.get("#customdropzone").upload(
+ { fileContent, fileName: avatarUpload, mimeType: "image/png" },
+ { subjectType: "drag-n-drop", force: true }
+ );
+ });
+ cy.get(".profile-avatar img")
+ .should("have.attr", "src")
+ .and("contains", "onourjourney");
+ cy.contains(".iziToast-message", "Upload successful")
+ .should("have.length",1);
+});
\ No newline at end of file
diff --git a/cypress/integration/UserProfile.ChangePassword.feature b/cypress/integration/UserProfile.ChangePassword.feature
new file mode 100644
index 000000000..a7eec1cce
--- /dev/null
+++ b/cypress/integration/UserProfile.ChangePassword.feature
@@ -0,0 +1,55 @@
+Feature: User profile - change password
+ As a user
+ I want to change my password in my settings
+ For security, e.g. if I exposed my password by accident
+
+ Login via email and password is a well-known authentication procedure and you
+ can assure to the server that you are who you claim to be. Either if you
+ exposed your password by acccident and you want to invalidate the exposed
+ password or just out of an good habit, you want to change your password.
+
+ Background:
+ Given the following "users" are in the database:
+ | email | password | id | name | slug | termsAndConditionsAgreedVersion |
+ | peterpan@example.org | exposed | id-of-peter-pan | Peter Pan | peter-pan | 0.0.4 |
+ And I am logged in as "peter-pan"
+ And I navigate to page "/settings"
+ And I click on "security menu"
+
+ Scenario: Incorrect Old Password
+ When I fill the password form with:
+ | Your old password | incorrect |
+ | Your new password | secure |
+ | Confirm new password | secure |
+ And I submit the form
+ And I see a "failure toaster" message:
+ """
+ Old password is not correct
+ """
+
+ Scenario: Incorrect Password Repeat
+ When I fill the password form with:
+ | Your old password | exposed |
+ | Your new password | secure |
+ | Confirm new password | eruces |
+ And I cannot submit the form
+
+ Scenario: Change my password
+ Given I navigate to page "/settings"
+ And I click on "security menu"
+ When I fill the password form with:
+ | Your old password | exposed |
+ | Your new password | secure |
+ | Confirm new password | secure |
+ And I submit the form
+ And I see a "success toaster" message:
+ """
+ Password successfully changed!
+ """
+ And I log out
+ Then I fill in my credentials "peterpan@example.org" "exposed"
+ And I click on "submit button"
+ And I cannot login anymore
+ But I fill in my credentials "peterpan@example.org" "secure"
+ And I click on "submit button"
+ And I can login successfully
diff --git a/cypress/integration/UserProfile.ChangePassword/I_can_login_successfully.js b/cypress/integration/UserProfile.ChangePassword/I_can_login_successfully.js
new file mode 100644
index 000000000..d1a62cc4d
--- /dev/null
+++ b/cypress/integration/UserProfile.ChangePassword/I_can_login_successfully.js
@@ -0,0 +1,7 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("I can login successfully", () => {
+ // cy.reload();
+ cy.get(".iziToast-wrapper")
+ .should("contain", "You are logged in!");
+});
\ No newline at end of file
diff --git a/cypress/integration/UserProfile.ChangePassword/I_cannot_login_anymore.js b/cypress/integration/UserProfile.ChangePassword/I_cannot_login_anymore.js
new file mode 100644
index 000000000..ff381d891
--- /dev/null
+++ b/cypress/integration/UserProfile.ChangePassword/I_cannot_login_anymore.js
@@ -0,0 +1,7 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("I cannot login anymore", password => {
+ //cy.reload();
+ cy.get(".iziToast-wrapper")
+ .should("contain", "Incorrect email address or password.");
+});
\ No newline at end of file
diff --git a/cypress/integration/UserProfile.ChangePassword/I_cannot_submit_the_form.js b/cypress/integration/UserProfile.ChangePassword/I_cannot_submit_the_form.js
new file mode 100644
index 000000000..657d38bd8
--- /dev/null
+++ b/cypress/integration/UserProfile.ChangePassword/I_cannot_submit_the_form.js
@@ -0,0 +1,6 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When("I cannot submit the form", () => {
+ cy.get("button[type=submit]")
+ .should('be.disabled');
+});
\ No newline at end of file
diff --git a/cypress/integration/UserProfile.ChangePassword/I_fill_the_password_form_with.js b/cypress/integration/UserProfile.ChangePassword/I_fill_the_password_form_with.js
new file mode 100644
index 000000000..69345ecc6
--- /dev/null
+++ b/cypress/integration/UserProfile.ChangePassword/I_fill_the_password_form_with.js
@@ -0,0 +1,11 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When("I fill the password form with:", table => {
+ table = table.rowsHash();
+ cy.get("input[id=oldPassword]")
+ .type(table["Your old password"])
+ .get("input[id=password]")
+ .type(table["Your new password"])
+ .get("input[id=passwordConfirmation]")
+ .type(table["Confirm new password"]);
+});
\ No newline at end of file
diff --git a/cypress/integration/UserProfile.ChangePassword/I_see_a_{string}_message.js b/cypress/integration/UserProfile.ChangePassword/I_see_a_{string}_message.js
new file mode 100644
index 000000000..90ddf0bd3
--- /dev/null
+++ b/cypress/integration/UserProfile.ChangePassword/I_see_a_{string}_message.js
@@ -0,0 +1,5 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("I see a {string} message:", (type, message) => {
+ cy.contains(message);
+});
\ No newline at end of file
diff --git a/cypress/integration/UserProfile.ChangePassword/I_submit_the_form copy.js b/cypress/integration/UserProfile.ChangePassword/I_submit_the_form copy.js
new file mode 100644
index 000000000..18349cff8
--- /dev/null
+++ b/cypress/integration/UserProfile.ChangePassword/I_submit_the_form copy.js
@@ -0,0 +1,5 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When("I submit the form", () => {
+ cy.get("form").submit();
+});
\ No newline at end of file
diff --git a/cypress/integration/UserProfile.NameDescriptionLocation.feature b/cypress/integration/UserProfile.NameDescriptionLocation.feature
new file mode 100644
index 000000000..891d98748
--- /dev/null
+++ b/cypress/integration/UserProfile.NameDescriptionLocation.feature
@@ -0,0 +1,38 @@
+Feature: User profile - name, description and location
+ As a user
+ I would like to change my name, add a description and a location
+ So others can see my name, get some info about me and my location
+
+ Background:
+ Given the following "users" are in the database:
+ | email | password | id | name | slug | termsAndConditionsAgreedVersion |
+ | peterpan@example.org | 123 | id-of-peter-pan | Peter Pan | peter-pan | 0.0.4 |
+ And I am logged in as "peter-pan"
+ And I navigate to page "settings"
+
+ Scenario: Change username
+ When I save "Hansi" as my new name
+ Then I can see my new name "Hansi" when I click on my profile picture in the top right
+ When I refresh the page
+ Then I can see my new name "Hansi" when I click on my profile picture in the top right
+
+ Scenario Outline: I set my location to ""
+ When I save "" as my location
+ And I navigate to page "/profile/peter-pan"
+ Then they can see "" in the info box below my avatar
+ Examples: Location
+ | location | type |
+ | Paris | City |
+ | Saxony-Anhalt | Region |
+ | Germany | Country |
+
+ Scenario: Display a description on profile page
+ Given I have the following self-description:
+ """
+ Ich lebe fettlos, fleischlos, fischlos dahin, fühle mich aber ganz wohl dabei
+ """
+ When I navigate to page "/profile/peter-pan"
+ Then they can see the following text in the info box below my avatar:
+ """
+ Ich lebe fettlos, fleischlos, fischlos dahin, fühle mich aber ganz wohl dabei
+ """
diff --git a/cypress/integration/UserProfile.NameDescriptionLocation/I_can_see_my_new_name_{string}_when_I_click_on_my_profile_picture_in_the_top_right.js b/cypress/integration/UserProfile.NameDescriptionLocation/I_can_see_my_new_name_{string}_when_I_click_on_my_profile_picture_in_the_top_right.js
new file mode 100644
index 000000000..b9e97a304
--- /dev/null
+++ b/cypress/integration/UserProfile.NameDescriptionLocation/I_can_see_my_new_name_{string}_when_I_click_on_my_profile_picture_in_the_top_right.js
@@ -0,0 +1,7 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then('I can see my new name {string} when I click on my profile picture in the top right', name => {
+ cy.get('.avatar-menu').click() // open
+ cy.get('.avatar-menu-popover').contains(name)
+ cy.get('.avatar-menu').click() // close again
+})
\ No newline at end of file
diff --git a/cypress/integration/UserProfile.NameDescriptionLocation/I_have_the_following_self-description.js b/cypress/integration/UserProfile.NameDescriptionLocation/I_have_the_following_self-description.js
new file mode 100644
index 000000000..a1bc1c524
--- /dev/null
+++ b/cypress/integration/UserProfile.NameDescriptionLocation/I_have_the_following_self-description.js
@@ -0,0 +1,12 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When('I have the following self-description:', text => {
+ cy.get('textarea[id=about]')
+ .clear()
+ .type(text)
+ cy.get('[type=submit]')
+ .click()
+ .not('[disabled]')
+ cy.get('.iziToast-message')
+ .should('contain', 'Your data was successfully updated')
+ })
\ No newline at end of file
diff --git a/cypress/integration/UserProfile.NameDescriptionLocation/I_save_{string}_as_my_location.js b/cypress/integration/UserProfile.NameDescriptionLocation/I_save_{string}_as_my_location.js
new file mode 100644
index 000000000..de5143b9f
--- /dev/null
+++ b/cypress/integration/UserProfile.NameDescriptionLocation/I_save_{string}_as_my_location.js
@@ -0,0 +1,13 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When('I save {string} as my location', location => {
+ cy.get('input[id=city]').type(location)
+ cy.get('.ds-select-option')
+ .contains(location)
+ .click()
+ cy.get('[type=submit]')
+ .click()
+ .not('[disabled]')
+ cy.get('.iziToast-message')
+ .should('contain', 'Your data was successfully updated')
+ })
\ No newline at end of file
diff --git a/cypress/integration/UserProfile.NameDescriptionLocation/I_save_{string}_as_my_new_name.js b/cypress/integration/UserProfile.NameDescriptionLocation/I_save_{string}_as_my_new_name.js
new file mode 100644
index 000000000..22e26cbc5
--- /dev/null
+++ b/cypress/integration/UserProfile.NameDescriptionLocation/I_save_{string}_as_my_new_name.js
@@ -0,0 +1,12 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When('I save {string} as my new name', name => {
+ cy.get('input[id=name]')
+ .clear()
+ .type(name)
+ cy.get('[type=submit]')
+ .click()
+ .not('[disabled]')
+ cy.get('.iziToast-message')
+ .should('contain', 'Your data was successfully updated')
+})
\ No newline at end of file
diff --git a/cypress/integration/UserProfile.NameDescriptionLocation/they_can_see_the_following_text_in_the_info_box_below_my_avatar.js b/cypress/integration/UserProfile.NameDescriptionLocation/they_can_see_the_following_text_in_the_info_box_below_my_avatar.js
new file mode 100644
index 000000000..6d375a406
--- /dev/null
+++ b/cypress/integration/UserProfile.NameDescriptionLocation/they_can_see_the_following_text_in_the_info_box_below_my_avatar.js
@@ -0,0 +1,5 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When('they can see the following text in the info box below my avatar:', text => {
+ cy.contains(text)
+})
\ No newline at end of file
diff --git a/cypress/integration/UserProfile.NameDescriptionLocation/they_can_see_{string}_in_the_info_box_below_my_avatar.js b/cypress/integration/UserProfile.NameDescriptionLocation/they_can_see_{string}_in_the_info_box_below_my_avatar.js
new file mode 100644
index 000000000..ea328f441
--- /dev/null
+++ b/cypress/integration/UserProfile.NameDescriptionLocation/they_can_see_{string}_in_the_info_box_below_my_avatar.js
@@ -0,0 +1,5 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then('they can see {string} in the info box below my avatar', location => {
+ cy.contains(location)
+})
\ No newline at end of file
diff --git a/cypress/integration/UserProfile.SocialMedia.feature b/cypress/integration/UserProfile.SocialMedia.feature
new file mode 100644
index 000000000..5ab1feb25
--- /dev/null
+++ b/cypress/integration/UserProfile.SocialMedia.feature
@@ -0,0 +1,41 @@
+Feature: User profile - list social media accounts
+ As a User
+ I'd like to enter my social media
+ So I can show them to other users to get in contact
+
+ Background:
+ Given the following "users" are in the database:
+ | email | password | id | name | slug | termsAndConditionsAgreedVersion |
+ | peterpan@example.org | 123 | id-of-peter-pan | Peter Pan | peter-pan | 0.0.4 |
+ And I am logged in as "peter-pan"
+
+ Scenario: Adding Social Media
+ When I navigate to page "/settings/my-social-media"
+ Then I am on page "/settings/my-social-media"
+ When I add a social media link
+ Then I see a toaster with "Added social media"
+ And the new social media link shows up on the page
+
+ Scenario: Other users viewing my Social Media
+ Given I have added a social media link
+ When I navigate to page "/profile/peter-pan"
+ Then they should be able to see my social media links
+
+ Scenario: Deleting Social Media
+ When I navigate to page "/settings/my-social-media"
+ Then I am on page "/settings/my-social-media"
+ Given I have added a social media link
+ When I delete a social media link
+ Then I see a toaster with "Deleted social media"
+
+ Scenario: Editing Social Media
+ When I navigate to page "/settings/my-social-media"
+ Then I am on page "/settings/my-social-media"
+ Given I have added a social media link
+ When I start editing a social media link
+ Then I can cancel editing
+ When I start editing a social media link
+ And I edit and save the link
+ Then I see a toaster with "Added social media"
+ And the new url is displayed
+ But the old url is not displayed
diff --git a/cypress/integration/UserProfile.SocialMedia/I_add_a_social_media_link.js b/cypress/integration/UserProfile.SocialMedia/I_add_a_social_media_link.js
new file mode 100644
index 000000000..9253709f9
--- /dev/null
+++ b/cypress/integration/UserProfile.SocialMedia/I_add_a_social_media_link.js
@@ -0,0 +1,9 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When('I add a social media link', () => {
+ cy.get('input#addSocialMedia')
+ .type('https://freeradical.zone/peter-pan')
+ .get('button')
+ .contains('Add link')
+ .click()
+})
\ No newline at end of file
diff --git a/cypress/integration/UserProfile.SocialMedia/I_can_cancel_editing.js b/cypress/integration/UserProfile.SocialMedia/I_can_cancel_editing.js
new file mode 100644
index 000000000..03d60c44a
--- /dev/null
+++ b/cypress/integration/UserProfile.SocialMedia/I_can_cancel_editing.js
@@ -0,0 +1,8 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then('I can cancel editing', () => {
+ cy.get('button#cancel')
+ .click()
+ .get('input#editSocialMedia')
+ .should('have.length', 0)
+})
\ No newline at end of file
diff --git a/cypress/integration/UserProfile.SocialMedia/I_delete_a_social_media_link.js b/cypress/integration/UserProfile.SocialMedia/I_delete_a_social_media_link.js
new file mode 100644
index 000000000..10daffca1
--- /dev/null
+++ b/cypress/integration/UserProfile.SocialMedia/I_delete_a_social_media_link.js
@@ -0,0 +1,6 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When('I delete a social media link', () => {
+ cy.get(".base-button[title='Delete']")
+ .click()
+})
\ No newline at end of file
diff --git a/cypress/integration/UserProfile.SocialMedia/I_edit_and_save_the_link.js b/cypress/integration/UserProfile.SocialMedia/I_edit_and_save_the_link.js
new file mode 100644
index 000000000..714e6b701
--- /dev/null
+++ b/cypress/integration/UserProfile.SocialMedia/I_edit_and_save_the_link.js
@@ -0,0 +1,10 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When('I edit and save the link', () => {
+ cy.get('input#editSocialMedia')
+ .clear()
+ .type('https://freeradical.zone/tinkerbell')
+ .get('button')
+ .contains('Save')
+ .click()
+})
\ No newline at end of file
diff --git a/cypress/integration/UserProfile.SocialMedia/I_have_added_a_social_media_link.js b/cypress/integration/UserProfile.SocialMedia/I_have_added_a_social_media_link.js
new file mode 100644
index 000000000..203b97032
--- /dev/null
+++ b/cypress/integration/UserProfile.SocialMedia/I_have_added_a_social_media_link.js
@@ -0,0 +1,10 @@
+import { Given } from "cypress-cucumber-preprocessor/steps";
+
+Given('I have added a social media link', () => {
+ cy.visit('/settings/my-social-media')
+ .get('input#addSocialMedia')
+ .type('https://freeradical.zone/peter-pan')
+ .get('button')
+ .contains('Add link')
+ .click()
+})
\ No newline at end of file
diff --git a/cypress/integration/UserProfile.SocialMedia/I_start_editing_a_social_media_link.js b/cypress/integration/UserProfile.SocialMedia/I_start_editing_a_social_media_link.js
new file mode 100644
index 000000000..1da05cfa5
--- /dev/null
+++ b/cypress/integration/UserProfile.SocialMedia/I_start_editing_a_social_media_link.js
@@ -0,0 +1,6 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When('I start editing a social media link', () => {
+ cy.get(".base-button[title='Edit']")
+ .click()
+})
\ No newline at end of file
diff --git a/cypress/integration/UserProfile.SocialMedia/the_new_social_media_link_shows_up_on_the_page.js b/cypress/integration/UserProfile.SocialMedia/the_new_social_media_link_shows_up_on_the_page.js
new file mode 100644
index 000000000..e72546f2a
--- /dev/null
+++ b/cypress/integration/UserProfile.SocialMedia/the_new_social_media_link_shows_up_on_the_page.js
@@ -0,0 +1,6 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then('the new social media link shows up on the page', () => {
+ cy.get('a[href="https://freeradical.zone/peter-pan"]')
+ .should('have.length', 1)
+})
\ No newline at end of file
diff --git a/cypress/integration/UserProfile.SocialMedia/the_new_url_is_displayed.js b/cypress/integration/UserProfile.SocialMedia/the_new_url_is_displayed.js
new file mode 100644
index 000000000..c25e6f0bb
--- /dev/null
+++ b/cypress/integration/UserProfile.SocialMedia/the_new_url_is_displayed.js
@@ -0,0 +1,6 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then('the new url is displayed', () => {
+ cy.get("a[href='https://freeradical.zone/tinkerbell']")
+ .should('have.length', 1)
+})
\ No newline at end of file
diff --git a/cypress/integration/UserProfile.SocialMedia/the_old_url_is_not_displayed.js b/cypress/integration/UserProfile.SocialMedia/the_old_url_is_not_displayed.js
new file mode 100644
index 000000000..b3e804124
--- /dev/null
+++ b/cypress/integration/UserProfile.SocialMedia/the_old_url_is_not_displayed.js
@@ -0,0 +1,7 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then('the old url is not displayed', () => {
+ cy.get("a[href='https://freeradical.zone/peter-pan']")
+ .should('have.length', 0)
+})
+
\ No newline at end of file
diff --git a/cypress/integration/UserProfile.SocialMedia/they_should_be_able_to_see_my_social_media_links.js b/cypress/integration/UserProfile.SocialMedia/they_should_be_able_to_see_my_social_media_links.js
new file mode 100644
index 000000000..249e4f420
--- /dev/null
+++ b/cypress/integration/UserProfile.SocialMedia/they_should_be_able_to_see_my_social_media_links.js
@@ -0,0 +1,8 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then('they should be able to see my social media links', () => {
+ cy.get('.base-card')
+ .contains('Where else can I find Peter Pan?')
+ .get('a[href="https://freeradical.zone/peter-pan"]')
+ .should('have.length', 1)
+})
\ No newline at end of file
diff --git a/cypress/integration/administration/PinPost.feature b/cypress/integration/administration/PinPost.feature
deleted file mode 100644
index 40ff9cda5..000000000
--- a/cypress/integration/administration/PinPost.feature
+++ /dev/null
@@ -1,36 +0,0 @@
-Feature: Pin a post
- As an admin
- I want to pin a post so that it always appears at the top
- In order to make sure all network users read it - e.g. notify people about security incidents, maintenance downtimes
-
-
- Background:
- Given we have the following posts in our database:
- | id | title | pinned | createdAt |
- | p1 | Some other post | | 2020-01-21 |
- | p2 | Houston we have a problem | x | 2020-01-20 |
- | p3 | Yet another post | | 2020-01-19 |
-
- Scenario: Pinned post always appears on the top of the newsfeed
- Given I am logged in with a "user" role
- Then the first post on the landing page has the title:
- """
- Houston we have a problem
- """
- And the post with title "Houston we have a problem" has a ribbon for pinned posts
-
- Scenario: Ordinary users cannot pin a post
- Given I am logged in with a "user" role
- When I open the content menu of post "Yet another post"
- Then there is no button to pin a post
-
- Scenario: Admins are allowed to pin a post
- Given I am logged in with a "admin" role
- And I open the content menu of post "Yet another post"
- When I click on 'Pin post'
- Then I see a toaster with "Post pinned successfully"
- And the first post on the landing page has the title:
- """
- Yet another post
- """
- And the post with title "Yet another post" has a ribbon for pinned posts
diff --git a/cypress/integration/administration/TagsAndCategories.feature b/cypress/integration/administration/TagsAndCategories.feature
deleted file mode 100644
index 516966c6b..000000000
--- a/cypress/integration/administration/TagsAndCategories.feature
+++ /dev/null
@@ -1,36 +0,0 @@
-Feature: Tags and Categories
- As a database administrator
- I would like to see a summary of all tags and categories and their usage
- In order to be able to decide which tags and categories are popular or not
-
- The currently deployed application, codename "Alpha", distinguishes between
- categories and tags. Each post can have a number of categories and/or tags.
- A few categories are required for each post, tags are completely optional.
- Both help to find relevant posts in the database, e.g. users can filter for
- categories.
-
- If administrators summary of all tags and categories and how often they are
- used, they learn what new category might be convenient for users, e.g. by
- looking at the popularity of a tag.
-
- Background:
- Given I am logged in with a "admin" role
- And we have a selection of tags and categories as well as posts
-
- Scenario: See an overview of categories
- When I navigate to the administration dashboard
- And I click on the menu item "Categories"
- Then I can see the following table:
- | | Name | Posts |
- | | Just For Fun | 2 |
- | | Happiness & Values | 1 |
- | | Health & Wellbeing | 1 |
-
- Scenario: See an overview of tags
- When I navigate to the administration dashboard
- And I click on the menu item "Hashtags"
- Then I can see the following table:
- | No. | Hashtags | Users | Posts |
- | 1 | #Democracy | 3 | 4 |
- | 2 | #Nature | 2 | 3 |
- | 3 | #Ecology | 1 | 1 |
diff --git a/cypress/integration/common/I_am_logged_in_as_{string}.js b/cypress/integration/common/I_am_logged_in_as_{string}.js
new file mode 100644
index 000000000..96d1c28ab
--- /dev/null
+++ b/cypress/integration/common/I_am_logged_in_as_{string}.js
@@ -0,0 +1,18 @@
+import { Given } from "cypress-cucumber-preprocessor/steps";
+import encode from '../../../backend/src/jwt/encode'
+
+Given("I am logged in as {string}", slug => {
+ cy.neode()
+ .first("User", { slug })
+ .then(user => {
+ return new Cypress.Promise((resolve, reject) => {
+ if(!user) {
+ return reject(`User ${email} not found in database`)
+ }
+ return user.toJson().then((user) => resolve(user))
+ })
+ })
+ .then(user => {
+ cy.setCookie('ocelot-social-token', encode(user))
+ })
+});
\ No newline at end of file
diff --git a/cypress/integration/common/I_am_on_page_{string}.js b/cypress/integration/common/I_am_on_page_{string}.js
new file mode 100644
index 000000000..5ef1b9852
--- /dev/null
+++ b/cypress/integration/common/I_am_on_page_{string}.js
@@ -0,0 +1,6 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("I am on page {string}", page => {
+ cy.location("pathname")
+ .should("match", new RegExp(page));
+});
\ No newline at end of file
diff --git a/cypress/integration/common/I_can_see_the_following_table.js b/cypress/integration/common/I_can_see_the_following_table.js
new file mode 100644
index 000000000..9ebe1208c
--- /dev/null
+++ b/cypress/integration/common/I_can_see_the_following_table.js
@@ -0,0 +1,16 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then('I can see the following table:', table => {
+ const headers = table.raw()[0]
+ headers.forEach((expected, i) => {
+ cy.get('thead th')
+ .eq(i)
+ .should('contain', expected)
+ })
+ const flattened = [].concat.apply([], table.rows())
+ flattened.forEach((expected, i) => {
+ cy.get('tbody td')
+ .eq(i)
+ .should('contain', expected)
+ })
+})
\ No newline at end of file
diff --git a/cypress/integration/common/I_choose_the_following_text_as_content.js b/cypress/integration/common/I_choose_the_following_text_as_content.js
new file mode 100644
index 000000000..62b5426d5
--- /dev/null
+++ b/cypress/integration/common/I_choose_the_following_text_as_content.js
@@ -0,0 +1,9 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When("I choose the following text as content:", async text => {
+ cy.task('getValue', 'lastPost').then(lastPost => {
+ lastPost.content = text.replace("\n", " ");
+ cy.task('pushValue', { name: 'lastPost', value: lastPost })
+ cy.get(".editor .ProseMirror").type(lastPost.content);
+ })
+});
\ No newline at end of file
diff --git a/cypress/integration/common/I_click_on_{string}.js b/cypress/integration/common/I_click_on_{string}.js
new file mode 100644
index 000000000..5f43eb912
--- /dev/null
+++ b/cypress/integration/common/I_click_on_{string}.js
@@ -0,0 +1,19 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When("I click on {string}", element => {
+ const elementSelectors = {
+ 'submit button': 'button[name=submit]',
+ 'create post button': '.post-add-button',
+ 'save button': 'button[type=submit]',
+ 'the first post': '.post-teaser:first-child',
+ 'comment button': 'button[type=submit]',
+ 'reply button': '.reply-button',
+ 'security menu': 'a[href="/settings/security"]',
+ 'pin post': '.ds-menu-item:first-child',
+ 'Moderation': 'a[href="/moderation"]',
+ }
+
+ cy.get(elementSelectors[element])
+ .click()
+ .wait(750);
+});
\ No newline at end of file
diff --git a/cypress/integration/common/I_click_on_{string}_from_the_content_menu_in_the_user_info_box.js b/cypress/integration/common/I_click_on_{string}_from_the_content_menu_in_the_user_info_box.js
new file mode 100644
index 000000000..f1a859bfe
--- /dev/null
+++ b/cypress/integration/common/I_click_on_{string}_from_the_content_menu_in_the_user_info_box.js
@@ -0,0 +1,12 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When("I click on {string} from the content menu in the user info box",
+ button => {
+ cy.get(".user-content-menu .base-button").click();
+ cy.get(".popover .ds-menu-item-link")
+ .contains(button)
+ .click({
+ force: true
+ });
+ }
+);
\ No newline at end of file
diff --git a/cypress/integration/common/I_fill_in_my_credentials_{string}_{string}.js b/cypress/integration/common/I_fill_in_my_credentials_{string}_{string}.js
new file mode 100644
index 000000000..e2227f454
--- /dev/null
+++ b/cypress/integration/common/I_fill_in_my_credentials_{string}_{string}.js
@@ -0,0 +1,12 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When("I fill in my credentials {string} {string}", (email,password) => {
+ cy.get("input[name=email]")
+ .trigger("focus")
+ .type('{selectall}{backspace}')
+ .type(email)
+ .get("input[name=password]")
+ .trigger("focus")
+ .type('{selectall}{backspace}')
+ .type(password);
+});
\ No newline at end of file
diff --git a/cypress/integration/common/I_follow_the_user_{string}.js b/cypress/integration/common/I_follow_the_user_{string}.js
new file mode 100644
index 000000000..56d50a5ae
--- /dev/null
+++ b/cypress/integration/common/I_follow_the_user_{string}.js
@@ -0,0 +1,11 @@
+Given("I follow the user {string}", name => {
+ cy.neode()
+ .first("User", {name})
+ .then(followed => {
+ cy.neode()
+ .first("User", {
+ name: "Peter Pan"
+ })
+ .relateTo(followed, "following");
+ });
+});
\ No newline at end of file
diff --git a/cypress/integration/common/I_get_removed_from_his_follower_collection.js b/cypress/integration/common/I_get_removed_from_his_follower_collection.js
new file mode 100644
index 000000000..b32ca5961
--- /dev/null
+++ b/cypress/integration/common/I_get_removed_from_his_follower_collection.js
@@ -0,0 +1,8 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("I get removed from his follower collection", () => {
+ cy.get(".base-card")
+ .not(".post-link");
+ cy.get(".main-container")
+ .contains(".base-card","is not followed by anyone");
+ });
\ No newline at end of file
diff --git a/cypress/integration/common/I_log_out.js b/cypress/integration/common/I_log_out.js
new file mode 100644
index 000000000..51605f17e
--- /dev/null
+++ b/cypress/integration/common/I_log_out.js
@@ -0,0 +1,9 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When("I log out", () => {
+ cy.get(".avatar-menu")
+ .click();
+ cy.get(".avatar-menu-popover")
+ .find('a[href="/logout"]')
+ .click();
+});
\ No newline at end of file
diff --git a/cypress/integration/common/I_navigate_to_my_{string}_settings_page.js b/cypress/integration/common/I_navigate_to_my_{string}_settings_page.js
new file mode 100644
index 000000000..4d369eab2
--- /dev/null
+++ b/cypress/integration/common/I_navigate_to_my_{string}_settings_page.js
@@ -0,0 +1,10 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When("I navigate to my {string} settings page", settingsPage => {
+ cy.get(".avatar-menu-trigger").click();
+ cy.get(".avatar-menu-popover")
+ .find("a[href]")
+ .contains("Settings")
+ .click();
+ cy.contains(".ds-menu-item-link", settingsPage).click();
+});
\ No newline at end of file
diff --git a/cypress/integration/common/I_navigate_to_page_{string}.js b/cypress/integration/common/I_navigate_to_page_{string}.js
new file mode 100644
index 000000000..aa929c80a
--- /dev/null
+++ b/cypress/integration/common/I_navigate_to_page_{string}.js
@@ -0,0 +1,5 @@
+import { Given } from "cypress-cucumber-preprocessor/steps";
+
+Given("I navigate to page {string}", page => {
+ cy.visit(page);
+});
\ No newline at end of file
diff --git a/cypress/integration/common/I_refresh_the_page.js b/cypress/integration/common/I_refresh_the_page.js
new file mode 100644
index 000000000..1ac655cb4
--- /dev/null
+++ b/cypress/integration/common/I_refresh_the_page.js
@@ -0,0 +1,6 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When('I refresh the page', () => {
+ cy.visit('/')
+ .reload();
+});
\ No newline at end of file
diff --git a/cypress/integration/common/I_search_for_{string}.js b/cypress/integration/common/I_search_for_{string}.js
new file mode 100644
index 000000000..eaad481f7
--- /dev/null
+++ b/cypress/integration/common/I_search_for_{string}.js
@@ -0,0 +1,12 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When("I search for {string}", value => {
+ cy.intercept({
+ method: "POST",
+ url: "http://localhost:3000/api",
+ }).as("graphqlRequest");
+ cy.get(".searchable-input .ds-select input")
+ .focus()
+ .type(value);
+ cy.wait("@graphqlRequest");
+});
\ No newline at end of file
diff --git a/cypress/integration/common/I_see_a_toaster_with_{string}.js b/cypress/integration/common/I_see_a_toaster_with_{string}.js
new file mode 100644
index 000000000..1cf7da285
--- /dev/null
+++ b/cypress/integration/common/I_see_a_toaster_with_{string}.js
@@ -0,0 +1,5 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("I see a toaster with {string}", (title) => {
+ cy.get(".iziToast-message").should("contain", title);
+})
\ No newline at end of file
diff --git a/cypress/integration/common/I_should_see_the_following_posts_in_the_select_dropdown.js b/cypress/integration/common/I_should_see_the_following_posts_in_the_select_dropdown.js
new file mode 100644
index 000000000..420c3376a
--- /dev/null
+++ b/cypress/integration/common/I_should_see_the_following_posts_in_the_select_dropdown.js
@@ -0,0 +1,8 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("I should see the following posts in the select dropdown:", table => {
+ table.hashes().forEach(({ title }) => {
+ cy.get(".ds-select-dropdown")
+ .should("contain", title);
+ });
+});
\ No newline at end of file
diff --git a/cypress/integration/common/I_wait_for_{int}_milliseconds.js b/cypress/integration/common/I_wait_for_{int}_milliseconds.js
new file mode 100644
index 000000000..bc8ef906a
--- /dev/null
+++ b/cypress/integration/common/I_wait_for_{int}_milliseconds.js
@@ -0,0 +1,5 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When("I wait for {int} milliseconds", time => {
+ cy.wait(time)
+});
\ No newline at end of file
diff --git a/cypress/integration/common/admin.js b/cypress/integration/common/admin.js
deleted file mode 100644
index 346fe64fb..000000000
--- a/cypress/integration/common/admin.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import { When, Then } from 'cypress-cucumber-preprocessor/steps'
-
-/* global cy */
-
-When('I navigate to the administration dashboard', () => {
- cy.get('.avatar-menu').click()
- cy.get('.avatar-menu-popover')
- .find('a[href="/admin"]')
- .click()
-})
-
-Then('I can see the following table:', table => {
- const headers = table.raw()[0]
- headers.forEach((expected, i) => {
- cy.get('thead th').eq(i).should('contain', expected)
- })
- const flattened = [].concat.apply([], table.rows())
- flattened.forEach((expected, i) => {
- cy.get('tbody td').eq(i).should('contain', expected)
- })
-})
diff --git a/cypress/integration/common/post.js b/cypress/integration/common/post.js
deleted file mode 100644
index cba238a63..000000000
--- a/cypress/integration/common/post.js
+++ /dev/null
@@ -1,165 +0,0 @@
-import { When, Then } from "cypress-cucumber-preprocessor/steps";
-import locales from '../../../webapp/locales'
-import orderBy from 'lodash/orderBy'
-
-const languages = orderBy(locales, 'name')
-
-When("I type in a comment with {int} characters", size => {
- var c="";
- for (var i = 0; i < size; i++) {
- c += "c"
- }
- cy.get(".editor .ProseMirror").type(c);
-});
-
-Then("I click on the {string} button", text => {
- cy.get("button")
- .contains(text)
- .click();
-});
-
-Then("I click on the reply button", () => {
- cy.get(".reply-button")
- .click();
-});
-
-Then("my comment should be successfully created", () => {
- cy.get(".iziToast-message").contains("Comment submitted!");
-});
-
-Then("I should see my comment", () => {
- cy.get("article.comment-card p")
- .should("contain", "Human Connection rocks")
- .get(".user-teaser span.slug")
- .should("contain", "@peter-pan") // specific enough
- .get(".user-avatar img")
- .should("have.attr", "src")
- .and("contain", 'https://') // some url
- .get(".user-teaser > .info > .text")
- .should("contain", "today at");
-});
-
-Then("I should see the entirety of my comment", () => {
- cy.get("article.comment-card")
- .should("not.contain", "show more")
-});
-
-Then("I should see an abreviated version of my comment", () => {
- cy.get("article.comment-card")
- .should("contain", "show more")
-});
-
-Then("the editor should be cleared", () => {
- cy.get(".ProseMirror p").should("have.class", "is-empty");
-});
-
-Then("it should create a mention in the CommentForm", () => {
- cy.get(".ProseMirror a")
- .should('have.class', 'mention')
- .should('contain', '@peter-pan')
-})
-
-When("I open the content menu of post {string}", (title)=> {
- cy.contains('.post-teaser', title)
- .find('.content-menu .base-button')
- .click()
-})
-
-When("I click on 'Pin post'", (string)=> {
- cy.get("a.ds-menu-item-link").contains("Pin post")
- .click()
-})
-
-Then("there is no button to pin a post", () => {
- cy.get("a.ds-menu-item-link")
- .should('contain', "Report Post") // sanity check
- .should('not.contain', "Pin post")
-})
-
-And("the post with title {string} has a ribbon for pinned posts", (title) => {
- cy.get(".post-teaser").contains(title)
- .parent()
- .parent()
- .find(".ribbon.--pinned")
- .should("contain", "Announcement")
-})
-
-Then("I see a toaster with {string}", (title) => {
- cy.get(".iziToast-message").should("contain", title);
-})
-
-Then("I should be able to {string} a teaser image", condition => {
- cy.reload()
- const teaserImageUpload = (condition === 'change') ? "humanconnection.png" : "onourjourney.png";
- cy.fixture(teaserImageUpload).as('postTeaserImage').then(function() {
- cy.get("#postdropzone").upload(
- { fileContent: this.postTeaserImage, fileName: teaserImageUpload, mimeType: "image/png" },
- { subjectType: "drag-n-drop", force: true }
- );
- })
-})
-
-Then('confirm crop', () => {
- cy.get('.crop-confirm')
- .click()
-})
-
-Then("I add all required fields", () => {
- cy.get('input[name="title"]')
- .type('new post')
- .get(".editor .ProseMirror")
- .type('new post content')
- .get(".categories-select .base-button")
- .first()
- .click()
- .get('.base-card > .select-field input')
- .click()
- .get('.ds-select-option')
- .eq(languages.findIndex(l => l.code === 'en'))
- .click()
-})
-
-Then("the post was saved successfully with the {string} teaser image", condition => {
- cy.get(".base-card > .title")
- .should("contain", condition === 'updated' ? 'to be updated' : 'new post')
- .get(".content")
- .should("contain", condition === 'updated' ? 'successfully updated' : 'new post content')
- .get('.post-page img')
- .should("have.attr", "src")
- .and("contains", condition === 'updated' ? 'humanconnection' : 'onourjourney')
-})
-
-Then("the first image should not be displayed anymore", () => {
- cy.get(".hero-image")
- .children()
- .get('.hero-image > .image')
- .should('have.length', 1)
- .and('have.attr', 'src')
-})
-
-Then('the {string} post was saved successfully without a teaser image', condition => {
- cy.get(".base-card > .title")
- .should("contain", condition === 'updated' ? 'to be updated' : 'new post')
- .get(".content")
- .should("contain", condition === 'updated' ? 'successfully updated' : 'new post content')
- .get('.post-page')
- .should('exist')
- .get('.hero-image > .image')
- .should('not.exist')
-})
-
-Then('I should be able to remove it', () => {
- cy.get('.crop-cancel')
- .click()
-})
-
-When('my post has a teaser image', () => {
- cy.get('.contribution-form .image')
- .should('exist')
- .and('have.attr', 'src')
-})
-
-Then('I should be able to remove the image', () => {
- cy.get('.dz-message > .base-button')
- .click()
-})
diff --git a/cypress/integration/common/profile.js b/cypress/integration/common/profile.js
deleted file mode 100644
index a0be8a2cf..000000000
--- a/cypress/integration/common/profile.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import { When, Then } from "cypress-cucumber-preprocessor/steps";
-
-/* global cy */
-
-When("I visit my profile page", () => {
- cy.openPage("profile/peter-pan");
-});
-
-Then("I should be able to change my profile picture", () => {
- const avatarUpload = "onourjourney.png";
-
- cy.fixture(avatarUpload, "base64").then(fileContent => {
- cy.get("#customdropzone").upload(
- { fileContent, fileName: avatarUpload, mimeType: "image/png" },
- { subjectType: "drag-n-drop", force: true }
- );
- });
- cy.get(".profile-avatar img")
- .should("have.attr", "src")
- .and("contains", "onourjourney");
- cy.contains(".iziToast-message", "Upload successful").should(
- "have.length",
- 1
- );
-});
-
-When("I visit another user's profile page", () => {
- cy.openPage("profile/peter-pan");
-});
-
-Then("I cannot upload a picture", () => {
- cy.get(".base-card")
- .children()
- .should("not.have.id", "customdropzone")
- .should("have.class", "user-avatar");
-});
diff --git a/cypress/integration/common/report.js b/cypress/integration/common/report.js
deleted file mode 100644
index 4c6d2f6c3..000000000
--- a/cypress/integration/common/report.js
+++ /dev/null
@@ -1,182 +0,0 @@
-import { Given, When, Then } from 'cypress-cucumber-preprocessor/steps'
-import { VERSION } from '../../constants/terms-and-conditions-version.js'
-import { gql } from '../../../backend/src/helpers/jest'
-
-/* global cy */
-
-let lastReportTitle
-let davidIrvingPostTitle = 'The Truth about the Holocaust'
-let davidIrvingPostSlug = 'the-truth-about-the-holocaust'
-let annoyingUserWhoMutedModeratorTitle = 'Fake news'
-
-const savePostTitle = $post => {
- return $post
- .first()
- .find('.title')
- .first()
- .invoke('text')
- .then(title => {
- lastReportTitle = title
- })
-}
-
-Given("I see David Irving's post on the landing page", page => {
- cy.openPage('landing')
-})
-
-Given("I see David Irving's post on the post page", page => {
- cy.visit(`/post/${davidIrvingPostSlug}`)
- cy.contains(davidIrvingPostTitle) // wait
-})
-
-Given('I am logged in with a {string} role', role => {
- cy.factory().build('user', {
- termsAndConditionsAgreedVersion: VERSION,
- role,
- name: `${role} is my name`
- }, {
- email: `${role}@example.org`,
- password: '1234',
- })
- cy.neode()
- .first("User", {
- name: `${role} is my name`,
- })
- .then(user => {
- return new Cypress.Promise((resolve, reject) => {
- return user.toJson().then((user) => resolve(user))
- })
- })
- .then(user => cy.login(user))
-})
-
-When('I click on "Report Post" from the content menu of the post', () => {
- cy.contains('.base-card', davidIrvingPostTitle)
- .find('.content-menu .base-button')
- .click({force: true})
-
- cy.get('.popover .ds-menu-item-link')
- .contains('Report Post')
- .click()
-})
-
-When('I click on "Report User" from the content menu in the user info box', () => {
- cy.contains('.base-card', davidIrvingPostTitle)
- .get('.user-content-menu .base-button')
- .click({ force: true })
-
- cy.get('.popover .ds-menu-item-link')
- .contains('Report User')
- .click()
-})
-
-When('I click on the author', () => {
- cy.get('.user-teaser')
- .click()
- .url().should('include', '/profile/')
-})
-
-When('I report the author', () => {
- cy.get('.page-name-profile-id-slug').then(() => {
- invokeReportOnElement('.base-card').then(() => {
- cy.get('button')
- .contains('Send')
- .click()
- })
- })
-})
-
-When('I click on send in the confirmation dialog', () => {
- cy.get('button')
- .contains('Send')
- .click()
-})
-
-Then('I get a success message', () => {
- cy.get('.iziToast-message').contains('Thanks')
-})
-
-Then('I see my reported user', () => {
- cy.get('table').then(() => {
- cy.get('tbody tr')
- .first()
- .contains(lastReportTitle.trim())
- })
-})
-
-Then(`I can't see the moderation menu item`, () => {
- cy.get('.avatar-menu-popover')
- .find('a[href="/settings"]', 'Settings')
- .should('exist') // OK, the dropdown is actually open
-
- cy.get('.avatar-menu-popover')
- .find('a[href="/moderation"]', 'Moderation')
- .should('not.exist')
-})
-
-When(/^I confirm the reporting dialog .*:$/, message => {
- cy.contains(message) // wait for element to become visible
- cy.get('.ds-modal').within(() => {
- cy.get('.ds-radio-option-label')
- .first()
- .click({
- force: true
- })
- cy.get('button')
- .contains('Report')
- .click()
- })
-})
-
-Given('somebody reported the following posts:', table => {
- table.hashes().forEach(({ submitterEmail, resourceId, reasonCategory, reasonDescription }) => {
- const submitter = {
- email: submitterEmail,
- password: '1234'
- }
- cy.factory()
- .build('user', {}, submitter)
- .authenticateAs(submitter)
- .mutate(gql`mutation($resourceId: ID!, $reasonCategory: ReasonCategory!, $reasonDescription: String!) {
- fileReport(resourceId: $resourceId, reasonCategory: $reasonCategory, reasonDescription: $reasonDescription) {
- reportId
- }
- }`, {
- resourceId,
- reasonCategory,
- reasonDescription
- })
- })
-})
-
-Then('I see all the reported posts including the one from above', () => {
- cy.get('table tbody').within(() => {
- cy.contains('tr', davidIrvingPostTitle)
- })
-})
-
-Then('I see all the reported posts including from the user who muted me', () => {
- cy.get('table tbody').within(() => {
- cy.contains('tr', annoyingUserWhoMutedModeratorTitle)
- })
-})
-
-Then('each list item links to the post page', () => {
- cy.contains(davidIrvingPostTitle).click()
- cy.location('pathname').should('contain', '/post')
-})
-
-Then('I can visit the post page', () => {
- cy.contains(annoyingUserWhoMutedModeratorTitle).click()
- cy.location('pathname').should('contain', '/post')
- .get('.base-card .title').should('contain', annoyingUserWhoMutedModeratorTitle)
-})
-
-When("they have a post someone has reported", () => {
- cy.factory()
- .build("post", {
- title,
- }, {
- authorId: 'annnoying-user',
- });
-})
diff --git a/cypress/integration/common/search.js b/cypress/integration/common/search.js
deleted file mode 100644
index 5eae20a22..000000000
--- a/cypress/integration/common/search.js
+++ /dev/null
@@ -1,126 +0,0 @@
-import { When, Then } from "cypress-cucumber-preprocessor/steps";
-When("I search for {string}", value => {
- cy.get(".searchable-input .ds-select input")
- .focus()
- .type(value);
-});
-
-Then("I should have one item in the select dropdown", () => {
- cy.get(".searchable-input .ds-select-dropdown").should($li => {
- expect($li).to.have.length(1);
- });
-});
-
-Then("the search should not contain posts by the annoying user", () => {
- cy.get(".searchable-input .ds-select-dropdown").should($li => {
- expect($li).to.have.length(1);
- })
- cy.get(".ds-select-dropdown")
- .should("not.have.class", '.search-post')
- .should("not.contain", 'Spam')
-});
-
-Then("the search should contain the annoying user", () => {
- cy.get(".searchable-input .ds-select-dropdown").should($li => {
- expect($li).to.have.length(1);
- })
- cy.get(".ds-select-dropdown .user-teaser .slug").should("contain", '@spammy-spammer');
- cy.get(".searchable-input .ds-select input")
- .focus()
- .type("{esc}");
-})
-
-Then("I should see the following posts in the select dropdown:", table => {
- table.hashes().forEach(({ title }) => {
- cy.get(".ds-select-dropdown").should("contain", title);
- });
-});
-
-Then("I should see the following users in the select dropdown:", table => {
- cy.get(".search-heading").should("contain", "Users");
- table.hashes().forEach(({ slug }) => {
- cy.get(".ds-select-dropdown").should("contain", slug);
- });
-});
-
-When("I type {string} and press Enter", value => {
- cy.get(".searchable-input .ds-select input")
- .focus()
- .type(value)
- .type("{enter}", { force: true });
-});
-
-When("I type {string} and press escape", value => {
- cy.get(".searchable-input .ds-select input")
- .focus()
- .type(value)
- .type("{esc}");
-});
-
-Then("the search field should clear", () => {
- cy.get(".searchable-input .ds-select input").should("have.text", "");
-});
-
-When("I select a post entry", () => {
- cy.get(".searchable-input .search-post")
- .first()
- .trigger("click");
-});
-
-Then("I should be on the post's page", () => {
- cy.location("pathname").should("contain", "/post/");
- cy.location("pathname").should(
- "eq",
- "/post/p1/101-essays-that-will-change-the-way-you-think"
- );
-});
-
-Then(
- "I should see posts with the searched-for term in the select dropdown",
- () => {
- cy.get(".ds-select-dropdown").should(
- "contain",
- "101 Essays that will change the way you think"
- );
- }
-);
-
-Then("I should see the search results page", () => {
- cy.location("pathname").should(
- "eq",
- "/search/search-results"
- );
- cy.location("search").should(
- "eq",
- "?search=PR"
- );
-});
-
-Then("I should see the following posts on the search results page",
- () => {
- cy.get(".post-teaser").should(
- "contain",
- "101 Essays that will change the way you think"
- );
- }
-);
-
-Then(
- "I should not see posts without the searched-for term in the select dropdown",
- () => {
- cy.get(".ds-select-dropdown").should(
- "not.contain",
- "No searched for content"
- );
- }
-);
-
-Then("I select a user entry", () => {
- cy.get(".searchable-input .user-teaser")
- .first()
- .trigger("click");
-})
-
-Then("I should be on the user's profile", () => {
- cy.location("pathname").should("eq", "/profile/user-for-search/search-for-me")
-})
diff --git a/cypress/integration/common/settings.js b/cypress/integration/common/settings.js
deleted file mode 100644
index 3dcff141d..000000000
--- a/cypress/integration/common/settings.js
+++ /dev/null
@@ -1,160 +0,0 @@
-import { When, Then } from 'cypress-cucumber-preprocessor/steps'
-
-/* global cy */
-
-let aboutMeText
-let myLocation
-
-const matchNameInUserMenu = name => {
- cy.get('.avatar-menu').click() // open
- cy.get('.avatar-menu-popover').contains(name)
- cy.get('.avatar-menu').click() // close again
-}
-
-When('I save {string} as my new name', name => {
- cy.get('input[id=name]')
- .clear()
- .type(name)
- cy.get('[type=submit]')
- .click()
- .not('[disabled]')
- cy.get('.iziToast-message')
- .should('contain', 'Your data was successfully updated')
-})
-
-When('I save {string} as my location', location => {
- cy.get('input[id=city]').type(location)
- cy.get('.ds-select-option')
- .contains(location)
- .click()
- cy.get('[type=submit]')
- .click()
- .not('[disabled]')
- cy.get('.iziToast-message')
- .should('contain', 'Your data was successfully updated')
- myLocation = location
-})
-
-When('I have the following self-description:', text => {
- cy.get('textarea[id=bio]')
- .clear()
- .type(text)
- cy.get('[type=submit]')
- .click()
- .not('[disabled]')
- cy.get('.iziToast-message')
- .should('contain', 'Your data was successfully updated')
- aboutMeText = text
-})
-
-When('people visit my profile page', url => {
- cy.openPage('/profile/peter-pan')
-})
-
-
-When('they can see the text in the info box below my avatar', () => {
- cy.contains(aboutMeText)
-})
-
-Then('they can see the location in the info box below my avatar', () => {
- cy.contains(myLocation)
-})
-
-Then('the name {string} is still there', name => {
- matchNameInUserMenu(name)
-})
-
-Then(
- 'I can see my new name {string} when I click on my profile picture in the top right',
- name => matchNameInUserMenu(name)
-)
-
-When('I click on the {string} link', link => {
- cy.get('a')
- .contains(link)
- .click()
-})
-
-Then('I should be on the {string} page', page => {
- cy.location()
- .should(loc => {
- expect(loc.pathname).to.eq(page)
- })
- .get('h2')
- .should('contain', 'Social media')
-})
-
-When('I add a social media link', () => {
- cy.get('input#addSocialMedia')
- .type('https://freeradical.zone/peter-pan')
- .get('button')
- .contains('Add link')
- .click()
-})
-
-Then('it gets saved successfully', () => {
- cy.get('.iziToast-message')
- .should('contain', 'Added social media')
-})
-
-Then('the new social media link shows up on the page', () => {
- cy.get('a[href="https://freeradical.zone/peter-pan"]')
- .should('have.length', 1)
-})
-
-Given('I have added a social media link', () => {
- cy.openPage('/settings/my-social-media')
- .get('input#addSocialMedia')
- .type('https://freeradical.zone/peter-pan')
- .get('button')
- .contains('Add link')
- .click()
-})
-
-Then('they should be able to see my social media links', () => {
- cy.get('.base-card')
- .contains('Where else can I find Peter Pan?')
- .get('a[href="https://freeradical.zone/peter-pan"]')
- .should('have.length', 1)
-})
-
-When('I delete a social media link', () => {
- cy.get(".base-button[title='Delete']")
- .click()
-})
-
-Then('it gets deleted successfully', () => {
- cy.get('.iziToast-message')
- .should('contain', 'Deleted social media')
-})
-
-When('I start editing a social media link', () => {
- cy.get(".base-button[title='Edit']")
- .click()
-})
-
-Then('I can cancel editing', () => {
- cy.get('button#cancel')
- .click()
- .get('input#editSocialMedia')
- .should('have.length', 0)
-})
-
-When('I edit and save the link', () => {
- cy.get('input#editSocialMedia')
- .clear()
- .type('https://freeradical.zone/tinkerbell')
- .get('button')
- .contains('Save')
- .click()
-})
-
-Then('the new url is displayed', () => {
- cy.get("a[href='https://freeradical.zone/tinkerbell']")
- .should('have.length', 1)
-})
-
-Then('the old url is not displayed', () => {
- cy.get("a[href='https://freeradical.zone/peter-pan']")
- .should('have.length', 0)
-})
diff --git a/cypress/integration/common/steps.js b/cypress/integration/common/steps.js
deleted file mode 100644
index 22a9d016e..000000000
--- a/cypress/integration/common/steps.js
+++ /dev/null
@@ -1,609 +0,0 @@
-import {
- Given,
- When,
- Then
-} from "cypress-cucumber-preprocessor/steps";
-import helpers from "../../support/helpers";
-import { VERSION } from '../../constants/terms-and-conditions-version.js'
-import locales from '../../../webapp/locales'
-import orderBy from 'lodash/orderBy'
-
-/* global cy */
-
-const languages = orderBy(locales, 'name')
-let lastPost = {};
-
-let loginCredentials = {
- email: "peterpan@example.org",
- password: "1234"
-};
-const termsAndConditionsAgreedVersion = {
- termsAndConditionsAgreedVersion: VERSION
-};
-const narratorParams = {
- id: 'id-of-peter-pan',
- name: "Peter Pan",
- slug: "peter-pan",
- ...termsAndConditionsAgreedVersion,
-};
-
-const annoyingParams = {
- email: "spammy-spammer@example.org",
- slug: 'spammy-spammer',
- password: "1234",
-};
-
-Given("I am logged in", () => {
- cy.neode()
- .first("User", {
- name: narratorParams.name
- })
- .then(user => {
- return new Cypress.Promise((resolve, reject) => {
- return user.toJson().then((user) => resolve(user))
- })
- })
- .then(user => cy.login(user))
-});
-
-Given("I log in as {string}", name => {
- cy.logout()
- cy.neode()
- .first("User", {
- name
- })
- .then(user => {
- return new Cypress.Promise((resolve, reject) => {
- return user.toJson().then((user) => resolve(user))
- })
- })
- .then(user => cy.login(user))
-})
-
-Given("the {string} user searches for {string}", (_, postTitle) => {
- cy.logout()
- cy.neode()
- .first("User", {
- id: "annoying-user"
- })
- .then(user => {
- return new Cypress.Promise((resolve, reject) => {
- return user.toJson().then((user) => resolve(user))
- })
- })
- .then(user => cy.login(user))
- cy.get(".searchable-input .ds-select input")
- .focus()
- .type(postTitle);
-});
-
-Given("we have a selection of categories", () => {
- cy.factory().build('category', { id: "cat0", slug: "just-for-fun" });
-});
-
-Given("we have a selection of tags and categories as well as posts", () => {
- cy.factory()
- .build('category', { id: 'cat12', name: "Just For Fun", icon: "smile", })
- .build('category', { id: 'cat121', name: "Happiness & Values", icon: "heart-o"})
- .build('category', { id: 'cat122', name: "Health & Wellbeing", icon: "medkit"})
- .build("tag", { id: "Ecology" })
- .build("tag", { id: "Nature" })
- .build("tag", { id: "Democracy" })
- .build("user", { id: 'a1' })
- .build("post", {}, {
- authorId: 'a1',
- tagIds: ["Ecology", "Nature", "Democracy"],
- categoryIds: ["cat12"]
- })
- .build("post", {}, {
- authorId: 'a1',
- tagIds: ["Nature", "Democracy"],
- categoryIds: ["cat121"]
- })
- .build("user", { id: 'a2' })
- .build("post", {}, {
- authorId: 'a2',
- tagIds: ['Nature', 'Democracy'],
- categoryIds: ["cat12"]
- })
- .build("post", {}, {
- tagIds: ['Democracy'],
- categoryIds: ["cat122"]
- })
-});
-
-Given("we have the following user accounts:", table => {
- table.hashes().forEach(params => {
- cy.factory().build("user", {
- ...params,
- ...termsAndConditionsAgreedVersion
- }, params);
- });
-});
-
-Given("I have a user account", () => {
- cy.factory().build("user", narratorParams, loginCredentials);
-});
-
-Given("my user account has the role {string}", role => {
- cy.factory().build("user", {
- role,
- ...termsAndConditionsAgreedVersion,
- }, loginCredentials);
-});
-
-When("I log out", cy.logout);
-
-When("I visit {string}", page => {
- cy.openPage(page);
-});
-
-When("I visit the {string} page", page => {
- cy.openPage(page);
-});
-
-When("a blocked user visits the post page of one of my authored posts", () => {
- cy.logout()
- cy.neode()
- .first("User", {
- name: 'Harassing User'
- })
- .then(user => {
- return new Cypress.Promise((resolve, reject) => {
- return user.toJson().then((user) => resolve(user))
- })
- })
- .then(user => cy.login(user))
- cy.openPage('post/previously-created-post')
-})
-
-Given("I am on the {string} page", page => {
- cy.openPage(page);
-});
-
-When("I fill in my email and password combination and click submit", () => {
- cy.manualLogin(loginCredentials);
-});
-
-When(/(?:when )?I refresh the page/, () => {
- cy.visit('/')
- .reload();
-});
-
-When("I log out through the menu in the top right corner", () => {
- cy.get(".avatar-menu").click();
- cy.get(".avatar-menu-popover")
- .find('a[href="/logout"]')
- .click();
-});
-
-Then("I can see my name {string} in the dropdown menu", () => {
- cy.get(".avatar-menu-popover").should("contain", narratorParams.name);
-});
-
-Then("I see the login screen again", () => {
- cy.location("pathname").should("contain", "/login");
-});
-
-Then("I can click on my profile picture in the top right corner", () => {
- cy.get(".avatar-menu").click();
- cy.get(".avatar-menu-popover");
-});
-
-Then("I am still logged in", () => {
- cy.get(".avatar-menu").click();
- cy.get(".avatar-menu-popover").contains(narratorParams.name);
-});
-
-When("I select {string} in the language menu", name => {
- cy.switchLanguage(name, true);
-});
-
-Given("I previously switched the language to {string}", name => {
- cy.switchLanguage(name, true);
-});
-
-Then("the whole user interface appears in {string}", name => {
- const {
- code
- } = helpers.getLangByName(name);
- cy.get(`html[lang=${code}]`);
- cy.getCookie("locale").should("have.property", "value", code);
-});
-
-Then("I see a button with the label {string}", label => {
- cy.contains("button", label);
-});
-
-When(`I click on {string}`, linkOrButton => {
- cy.contains(linkOrButton).click();
-});
-
-When(`I click on the menu item {string}`, linkOrButton => {
- cy.contains(".ds-menu-item", linkOrButton).click();
-});
-
-When("I press {string}", label => {
- cy.contains(label).click();
-});
-
-Given("we have the following comments in our database:", table => {
- table.hashes().forEach((attributesOrOptions, i) => {
- cy.factory().build("comment", {
- ...attributesOrOptions,
- }, {
- ...attributesOrOptions,
- });
- })
-});
-
-Given("we have the following posts in our database:", table => {
- table.hashes().forEach((attributesOrOptions, i) => {
- cy.factory().build("post", {
- ...attributesOrOptions,
- deleted: Boolean(attributesOrOptions.deleted),
- disabled: Boolean(attributesOrOptions.disabled),
- pinned: Boolean(attributesOrOptions.pinned),
- }, {
- ...attributesOrOptions,
- });
- })
-})
-
-Then("I see a success message:", message => {
- cy.contains(message);
-});
-
-When("I click on the avatar menu in the top right corner", () => {
- cy.get(".avatar-menu").click();
-});
-
-When(
- "I click on the big plus icon in the bottom right corner to create post",
- () => {
- cy.get(".post-add-button").click();
- cy.location("pathname").should('eq', '/post/create')
- }
-);
-
-Given("I previously created a post", () => {
- lastPost = {
- lastPost,
- title: "previously created post",
- content: "with some content",
- };
- cy.factory()
- .build("post", lastPost, {
- authorId: narratorParams.id
- });
-});
-
-When("I choose {string} as the title of the post", title => {
- lastPost.title = title.replace("\n", " ");
- cy.get('input[name="title"]').type(lastPost.title);
-});
-
-When("I type in the following text:", text => {
- lastPost.content = text.replace("\n", " ");
- cy.get(".editor .ProseMirror").type(lastPost.content);
-});
-
-Then("I select a category", () => {
- cy.get(".base-button")
- .contains("Just for Fun")
- .click();
-});
-
-When("I choose {string} as the language for the post", (languageCode) => {
- cy.get('.contribution-form .ds-select')
- .click().get('.ds-select-option')
- .eq(languages.findIndex(l => l.code === languageCode)).click()
-})
-
-Then("the post shows up on the landing page at position {int}", index => {
- cy.openPage("landing");
- const selector = `.post-teaser:nth-child(${index}) > .base-card`;
- cy.get(selector).should("contain", lastPost.title);
- cy.get(selector).should("contain", lastPost.content);
-});
-
-Then("I get redirected to {string}", route => {
- cy.location("pathname").should("contain", route.replace("...", ""));
-});
-
-Then("the post was saved successfully", () => {
- cy.get(".base-card > .title").should("contain", lastPost.title);
- cy.get(".content").should("contain", lastPost.content);
-});
-
-Then(/^I should see only ([0-9]+) posts? on the landing page/, postCount => {
- cy.get(".post-teaser").should("have.length", postCount);
-});
-
-Then("the first post on the landing page has the title:", title => {
- cy.get(".post-teaser:first").should("contain", title);
-});
-
-Then(
- "the page {string} returns a 404 error with a message:",
- (route, message) => {
- cy.request({
- url: route,
- failOnStatusCode: false
- })
- .its("status")
- .should("eq", 404);
- cy.visit(route, {
- failOnStatusCode: false
- });
- cy.get(".error-message").should("contain", message);
- }
-);
-
-Given("I am logged in with these credentials:", table => {
- loginCredentials = table.hashes()[0];
- cy.factory().build("user", {
- ...termsAndConditionsAgreedVersion,
- name: loginCredentials.email,
- }, loginCredentials);
- cy.neode()
- .first("User", {
- name: loginCredentials.email,
- })
- .then(user => {
- return new Cypress.Promise((resolve, reject) => {
- return user.toJson().then((user) => resolve(user))
- })
- })
- .then(user => cy.login(user))
-});
-
-When("I fill the password form with:", table => {
- table = table.rowsHash();
- cy.get("input[id=oldPassword]")
- .type(table["Your old password"])
- .get("input[id=password]")
- .type(table["Your new passsword"])
- .get("input[id=passwordConfirmation]")
- .type(table["Confirm new password"]);
-});
-
-When("submit the form", () => {
- cy.get("form").submit();
-});
-
-Then("I cannot login anymore with password {string}", password => {
- cy.reload();
- const { email } = loginCredentials
- cy.manualLogin({ email, password })
- .get(".iziToast-wrapper").should("contain", "Incorrect email address or password.");
-});
-
-Then("I can login successfully with password {string}", password => {
- cy.reload();
- const { email } = loginCredentials
- cy.manualLogin({ email, password })
- .get(".iziToast-wrapper").should("contain", "You are logged in!");
-});
-
-When("open the notification menu and click on the first item", () => {
- cy.get(".notifications-menu").invoke('show').click(); // "invoke('show')" because of the delay for show the menu
- cy.get(".notification .link")
- .first()
- .click({
- force: true
- });
-});
-
-Then("see {int} unread notifications in the top menu", count => {
- cy.get(".notifications-menu").should("contain", count);
-});
-
-Then("I get to the post page of {string}", path => {
- path = path.replace("...", "");
- cy.url().should("contain", "/post/");
- cy.url().should("contain", path);
-});
-
-When(
- "I start to write a new post with the title {string} beginning with:",
- (title, intro) => {
- cy.get(".post-add-button").click();
- cy.get('input[name="title"]').type(title);
- cy.get(".ProseMirror").type(intro);
- }
-);
-
-When("mention {string} in the text", mention => {
- cy.get(".ProseMirror").type(" @");
- cy.get(".suggestion-list__item")
- .contains(mention)
- .click();
-});
-
-Then("the unread counter is removed", () => {
- cy.get('.notifications-menu .counter-icon').should('not.exist');
-});
-
-Then("the notification menu button links to the all notifications page", () => {
- cy.get(".notifications-menu").click();
- cy.location("pathname").should("contain", "/notifications");
-});
-
-Given("there is an annoying user called {string}", name => {
- cy.factory().build("user", {
- id: "annoying-user",
- name,
- ...termsAndConditionsAgreedVersion,
- }, annoyingParams);
-});
-
-Given("there is an annoying user who has muted me", () => {
- cy.neode()
- .first("User", {
- role: 'moderator'
- })
- .then(mutedUser => {
- cy.neode()
- .first("User", {
- id: 'annoying-user'
- })
- .relateTo(mutedUser, "muted");
- });
-});
-
-Given("I am on the profile page of the annoying user", name => {
- cy.openPage("profile/annoying-user/spammy-spammer");
-});
-
-When("I visit the profile page of the annoying user", name => {
- cy.openPage("profile/annoying-user");
-});
-
-When("I ", name => {
- cy.openPage("profile/annoying-user");
-});
-
-When(
- "I click on {string} from the content menu in the user info box",
- button => {
- cy.get(".user-content-menu .base-button").click();
- cy.get(".popover .ds-menu-item-link")
- .contains(button)
- .click({
- force: true
- });
- }
-);
-
-When("I navigate to my {string} settings page", settingsPage => {
- cy.get(".avatar-menu-trigger").click();
- cy.get(".avatar-menu-popover")
- .find("a[href]")
- .contains("Settings")
- .click();
- cy.contains(".ds-menu-item-link", settingsPage).click();
-});
-
-Given("I follow the user {string}", name => {
- cy.neode()
- .first("User", {
- name
- })
- .then(followed => {
- cy.neode()
- .first("User", {
- name: narratorParams.name
- })
- .relateTo(followed, "following");
- });
-});
-
-Given('{string} wrote a post {string}', (_, title) => {
- cy.factory()
- .build("post", {
- title,
- }, {
- authorId: 'annoying-user',
- });
-});
-
-Then("the list of posts of this user is empty", () => {
- cy.get(".base-card").not(".post-link");
- cy.get(".main-container").find(".ds-space.hc-empty");
-});
-
-Then("I get removed from his follower collection", () => {
- cy.get(".base-card").not(".post-link");
- cy.get(".main-container").contains(
- ".base-card",
- "is not followed by anyone"
- );
-});
-
-Given("I wrote a post {string}", title => {
- cy.factory()
- .build("post", {
- title,
- }, {
- authorId: narratorParams.id,
- });
-});
-
-When("I mute the user {string}", name => {
- cy.neode()
- .first("User", {
- name
- })
- .then(mutedUser => {
- cy.neode()
- .first("User", {
- name: narratorParams.name
- })
- .relateTo(mutedUser, "muted");
- });
-});
-
-When("I block the user {string}", name => {
- cy.neode()
- .first("User", {
- name
- })
- .then(blockedUser => {
- cy.neode()
- .first("User", {
- id: narratorParams.id
- })
- .relateTo(blockedUser, "blocked");
- });
-});
-
-When("a user has blocked me", () => {
- cy.neode()
- .first("User", {
- name: narratorParams.name
- })
- .then(blockedUser => {
- cy.neode()
- .first("User", {
- name: 'Harassing User'
- })
- .relateTo(blockedUser, "blocked");
- });
-});
-
-Then("I see only one post with the title {string}", title => {
- cy.get(".main-container")
- .find(".post-link")
- .should("have.length", 1);
- cy.get(".main-container").contains(".post-link", title);
-});
-
-Then("they should not see the comment form", () => {
- cy.get(".base-card").children().should('not.have.class', 'comment-form')
-})
-
-Then("they should see a text explaining why commenting is not possible", () => {
- cy.get('.ds-placeholder').should('contain', "Commenting is not possible at this time on this post.")
-})
-
-Then("I should see no users in my blocked users list", () => {
- cy.get('.ds-placeholder')
- .should('contain', "So far, you have not blocked anybody.")
-})
-
-Then("I {string} see {string} from the content menu in the user info box", (condition, link) => {
- cy.get(".user-content-menu .base-button").click()
- cy.get(".popover .ds-menu-item-link")
- .should(condition === 'should' ? 'contain' : 'not.contain', link)
-})
-
-Then('I should not see {string} button', button => {
- cy.get('.base-card .action-buttons')
- .should('have.length', 1)
-})
-
-Then('I should see the {string} button', button => {
- cy.get('.base-card .action-buttons .base-button')
- .should('contain', button)
-})
diff --git a/cypress/integration/common/the_first_post_on_the_newsfeed_has_the_title.js b/cypress/integration/common/the_first_post_on_the_newsfeed_has_the_title.js
new file mode 100644
index 000000000..afe370e90
--- /dev/null
+++ b/cypress/integration/common/the_first_post_on_the_newsfeed_has_the_title.js
@@ -0,0 +1,6 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("the first post on the newsfeed has the title:", title => {
+ cy.get(".post-teaser:first")
+ .should("contain", title);
+});
\ No newline at end of file
diff --git a/cypress/integration/common/the_following_{string}_are_in_the_database.js b/cypress/integration/common/the_following_{string}_are_in_the_database.js
new file mode 100644
index 000000000..1d17ec686
--- /dev/null
+++ b/cypress/integration/common/the_following_{string}_are_in_the_database.js
@@ -0,0 +1,35 @@
+import { Given } from "cypress-cucumber-preprocessor/steps";
+
+Given("the following {string} are in the database:", (table,data) => {
+ switch(table){
+ case "posts":
+ data.hashes().forEach( entry => {
+ cy.factory().build("post", {
+ ...entry,
+ deleted: Boolean(entry.deleted),
+ disabled: Boolean(entry.disabled),
+ pinned: Boolean(entry.pinned),
+ },{
+ ...entry,
+ tagIds: entry.tagIds ? entry.tagIds.split(',').map(item => item.trim()) : [],
+ });
+ })
+ break
+ case "comments":
+ data.hashes().forEach( entry => {
+ cy.factory()
+ .build("comment",entry,entry);
+ })
+ break
+ case "users":
+ data.hashes().forEach( entry => {
+ cy.factory().build("user",entry,entry);
+ });
+ break
+ case "tags":
+ data.hashes().forEach( entry => {
+ cy.factory().build("tag", entry, entry)
+ });
+ break
+ }
+})
\ No newline at end of file
diff --git a/cypress/integration/common/{string}_wrote_a_post_{string}.js b/cypress/integration/common/{string}_wrote_a_post_{string}.js
new file mode 100644
index 000000000..42ac98028
--- /dev/null
+++ b/cypress/integration/common/{string}_wrote_a_post_{string}.js
@@ -0,0 +1,10 @@
+import { Given } from "cypress-cucumber-preprocessor/steps";
+
+Given('{string} wrote a post {string}', (author, title) => {
+ cy.factory()
+ .build("post", {
+ title,
+ }, {
+ authorId: author,
+ });
+});
\ No newline at end of file
diff --git a/cypress/integration/moderation/HidePosts.feature b/cypress/integration/moderation/HidePosts.feature
deleted file mode 100644
index bb82c7188..000000000
--- a/cypress/integration/moderation/HidePosts.feature
+++ /dev/null
@@ -1,26 +0,0 @@
-Feature: Hide Posts
- As the moderator team
- we'd like to be able to hide posts from the public
- to enforce our network's code of conduct and/or legal regulations
-
- Background:
- Given we have the following posts in our database:
- | id | title | deleted | disabled |
- | p1 | This post should be visible | | |
- | p2 | This post is disabled | | x |
- | p3 | This post is deleted | x | |
-
- Scenario: Disabled posts don't show up on the landing page
- Given I am logged in with a "user" role
- Then I should see only 1 post on the landing page
- And the first post on the landing page has the title:
- """
- This post should be visible
- """
-
- Scenario: Visiting a disabled post's page should return 404
- Given I am logged in with a "user" role
- Then the page "/post/this-post-is-disabled" returns a 404 error with a message:
- """
- This post could not be found
- """
diff --git a/cypress/integration/notifications/Mentions.feature b/cypress/integration/notifications/Mentions.feature
deleted file mode 100644
index 1cf265624..000000000
--- a/cypress/integration/notifications/Mentions.feature
+++ /dev/null
@@ -1,29 +0,0 @@
-Feature: Notification for a mention
- As a user
- I want to be notified if sb. mentions me in a post or comment
- In order join conversations about or related to me
-
- Background:
- Given we have a selection of categories
- And we have the following user accounts:
- | name | slug | email | password |
- | Wolle aus Hamburg | wolle-aus-hamburg | wolle@example.org | 1234 |
- | Matt Rider | matt-rider | matt@example.org | 4321 |
-
- Scenario: Mention another user, re-login as this user and see notifications
- Given I log in as "Wolle aus Hamburg"
- And I start to write a new post with the title "Hey Matt" beginning with:
- """
- Big shout to our fellow contributor
- """
- And mention "@matt-rider" in the text
- And I select a category
- And I choose "en" as the language for the post
- And I click on "Save"
- And I log in as "Matt Rider"
- And see 1 unread notifications in the top menu
- And open the notification menu and click on the first item
- Then I get to the post page of ".../hey-matt"
- And the unread counter is removed
- And the notification menu button links to the all notifications page
-
diff --git a/cypress/integration/post/Comment.feature b/cypress/integration/post/Comment.feature
deleted file mode 100644
index da261726b..000000000
--- a/cypress/integration/post/Comment.feature
+++ /dev/null
@@ -1,46 +0,0 @@
-Feature: Post Comment
- As a user
- I want to comment on contributions of others
- To be able to express my thoughts and emotions about these, discuss, and add give further information.
-
- Background:
- Given I have a user account
- And we have the following posts in our database:
- | id | title | slug | authorId |
- | bWBjpkTKZp | 101 Essays that will change the way you think | 101-essays | id-of-peter-pan |
- And we have the following comments in our database:
- | postId | content | authorId |
- | bWBjpkTKZp | @peter-pan reply to me | id-of-peter-pan |
- And I am logged in
-
- Scenario: Comment creation
- Given I visit "post/bWBjpkTKZp/101-essays"
- And I type in the following text:
- """
- Human Connection rocks
- """
- And I click on the "Comment" button
- Then my comment should be successfully created
- And I should see my comment
- And the editor should be cleared
-
- Scenario: View medium length comments
- Given I visit "post/bWBjpkTKZp/101-essays"
- And I type in a comment with 305 characters
- And I click on the "Comment" button
- Then my comment should be successfully created
- And I should see the entirety of my comment
- And the editor should be cleared
-
- Scenario: View long comments
- Given I visit "post/bWBjpkTKZp/101-essays"
- And I type in a comment with 1205 characters
- And I click on the "Comment" button
- Then my comment should be successfully created
- And I should see an abreviated version of my comment
- And the editor should be cleared
-
- Scenario: Direct reply to Comment
- Given I visit "post/bWBjpkTKZp/101-essays"
- And I click on the reply button
- Then it should create a mention in the CommentForm
diff --git a/cypress/integration/post/DeleteImage.feature b/cypress/integration/post/DeleteImage.feature
deleted file mode 100644
index 07bfe43b1..000000000
--- a/cypress/integration/post/DeleteImage.feature
+++ /dev/null
@@ -1,19 +0,0 @@
-Feature: Delete Teaser Image
- As a user
- I would like to be able to remove an image I have previously added to my Post
- So that I have control over the content of my Post
-
- Background:
- Given I have a user account
- Given I am logged in
- Given we have the following posts in our database:
- | authorId | id | title | content |
- | id-of-peter-pan | p1 | Post to be updated | successfully updated |
-
- Scenario: Delete existing image
- Given I am on the 'post/edit/p1' page
- And my post has a teaser image
- Then I should be able to remove the image
- And I click on "Save"
- Then I get redirected to ".../post-to-be-updated"
- And the "updated" post was saved successfully without a teaser image
diff --git a/cypress/integration/post/ImageUploader.feature b/cypress/integration/post/ImageUploader.feature
deleted file mode 100644
index 1bbd80c78..000000000
--- a/cypress/integration/post/ImageUploader.feature
+++ /dev/null
@@ -1,47 +0,0 @@
-Feature: Upload Teaser Image
- As a user
- I would like to be able to add a teaser image to my Post
- So that I can personalize my posts
-
-
- Background:
- Given I have a user account
- Given I am logged in
- Given we have the following posts in our database:
- | authorId | id | title | content |
- | id-of-peter-pan | p1 | Post to be updated | successfully updated |
-
- Scenario: Create a Post with a Teaser Image
- When I click on the big plus icon in the bottom right corner to create post
- Then I should be able to "add" a teaser image
- And confirm crop
- And I add all required fields
- And I click on "Save"
- Then I get redirected to ".../new-post"
- And the post was saved successfully with the "new" teaser image
-
- Scenario: Update a Post to add an image
- Given I am on the 'post/edit/p1' page
- And I should be able to "change" a teaser image
- And confirm crop
- And I click on "Save"
- Then I see a toaster with "Saved!"
- And I get redirected to ".../post-to-be-updated"
- Then the post was saved successfully with the "updated" teaser image
-
- Scenario: Add image, then add a different image
- When I click on the big plus icon in the bottom right corner to create post
- Then I should be able to "add" a teaser image
- And confirm crop
- And I should be able to "change" a teaser image
- And confirm crop
- And the first image should not be displayed anymore
-
- Scenario: Add image, then delete it
- When I click on the big plus icon in the bottom right corner to create post
- Then I should be able to "add" a teaser image
- And I should be able to remove it
- And I add all required fields
- And I click on "Save"
- Then I get redirected to ".../new-post"
- And the "new" post was saved successfully without a teaser image
diff --git a/cypress/integration/post/PersistentLinks.feature b/cypress/integration/post/PersistentLinks.feature
deleted file mode 100644
index 5ea48ef6a..000000000
--- a/cypress/integration/post/PersistentLinks.feature
+++ /dev/null
@@ -1,41 +0,0 @@
-Feature: Persistent Links
- As a user
- I want all links to carry permanent information that identifies the linked resource
- In order to have persistent links even if a part of the URL might change
-
- | | Modifiable | Referenceable | Unique | Purpose |
- | -- | -- | -- | -- | -- |
- | ID | no | yes | yes | Identity, Traceability, Links |
- | Slug | yes | yes | yes | @-Mentions, SEO-friendly URL |
- | Name | yes | no | no | Search, self-description |
-
-
- Background:
- Given we have the following user accounts:
- | id | name | slug |
- | MHNqce98y1 | Stephen Hawking | thehawk |
- And we have the following posts in our database:
- | id | title | slug |
- | bWBjpkTKZp | 101 Essays that will change the way you think | 101-essays |
- And I have a user account
- And I am logged in
-
- Scenario Outline: Link with slug only is valid and gets auto-completed
- When I visit ""
- Then I get redirected to ""
- Examples:
- | url | redirectUrl |
- | /profile/thehawk | /profile/MHNqce98y1/thehawk |
- | /post/101-essays | /post/bWBjpkTKZp/101-essays |
-
- Scenario: Link with id only will always point to the same user
- When I visit "/profile/MHNqce98y1"
- Then I get redirected to "/profile/MHNqce98y1/thehawk"
-
- Scenario Outline: ID takes precedence over slug
- When I visit ""
- Then I get redirected to ""
- Examples:
- | url | redirectUrl |
- | /profile/MHNqce98y1/stephen-hawking | /profile/MHNqce98y1/thehawk |
- | /post/bWBjpkTKZp/the-way-you-think | /post/bWBjpkTKZp/101-essays |
diff --git a/cypress/integration/post/WritePost.feature b/cypress/integration/post/WritePost.feature
deleted file mode 100644
index 0d74606ad..000000000
--- a/cypress/integration/post/WritePost.feature
+++ /dev/null
@@ -1,28 +0,0 @@
-Feature: Create a post
- As a user
- I would like to create a post
- To say something to everyone in the community
-
- Background:
- Given I have a user account
- And I am logged in
- And we have a selection of categories
- And I am on the "landing" page
-
- Scenario: Create a post
- When I click on the big plus icon in the bottom right corner to create post
- And I choose "My first post" as the title of the post
- And I type in the following text:
- """
- Human Connection is a free and open-source social network
- for active citizenship.
- """
- And I select a category
- And I choose "en" as the language for the post
- And I click on "Save"
- Then I get redirected to ".../my-first-post"
- And the post was saved successfully
-
- Scenario: See a post on the landing page
- Given I previously created a post
- Then the post shows up on the landing page at position 1
diff --git a/cypress/integration/user_account/ChangePassword.feature b/cypress/integration/user_account/ChangePassword.feature
deleted file mode 100644
index dbdf724f7..000000000
--- a/cypress/integration/user_account/ChangePassword.feature
+++ /dev/null
@@ -1,30 +0,0 @@
-Feature: Change password
- As a user
- I want to change my password in my settings
- For security, e.g. if I exposed my password by accident
-
- Login via email and password is a well-known authentication procedure and you
- can assure to the server that you are who you claim to be. Either if you
- exposed your password by acccident and you want to invalidate the exposed
- password or just out of an good habit, you want to change your password.
-
- Background:
- Given I am logged in with these credentials:
- | email | password |
- | user@example.org | exposed |
-
- Scenario: Change my password
- Given I am on the "settings" page
- And I click on "Security"
- When I fill the password form with:
- | Your old password | exposed |
- | Your new passsword | secure |
- | Confirm new password | secure |
- And submit the form
- And I see a success message:
- """
- Password successfully changed!
- """
- And I log out through the menu in the top right corner
- Then I cannot login anymore with password "exposed"
- But I can login successfully with password "secure"
diff --git a/cypress/integration/user_account/Login.feature b/cypress/integration/user_account/Login.feature
deleted file mode 100644
index 6e8f60a56..000000000
--- a/cypress/integration/user_account/Login.feature
+++ /dev/null
@@ -1,23 +0,0 @@
-Feature: Authentication
- As a database administrator
- I want users to sign in
- In order to attribute posts and other contributions to their authors
-
- Background:
- Given I have a user account
-
- Scenario: Log in
- When I visit the "login" page
- And I fill in my email and password combination and click submit
- Then I can click on my profile picture in the top right corner
- And I can see my name "Peter Lustig" in the dropdown menu
-
- Scenario: Refresh and stay logged in
- Given I am logged in
- When I refresh the page
- Then I am still logged in
-
- Scenario: Log out
- Given I am logged in
- When I log out through the menu in the top right corner
- Then I see the login screen again
diff --git a/cypress/integration/user_profile/AboutMeAndLocation.feature b/cypress/integration/user_profile/AboutMeAndLocation.feature
deleted file mode 100644
index 2a512bf3f..000000000
--- a/cypress/integration/user_profile/AboutMeAndLocation.feature
+++ /dev/null
@@ -1,37 +0,0 @@
-Feature: About me and location
- As a user
- I would like to add some about me text and a location
- So others can get some info about me and my location
-
- The location and about me are displayed on the user profile. Later it will be possible
- to search for users by location.
-
- Background:
- Given I have a user account
- And I am logged in
- And I am on the "settings" page
-
- Scenario: Change username
- When I save "Hansi" as my new name
- Then I can see my new name "Hansi" when I click on my profile picture in the top right
- And when I refresh the page
- Then the name "Hansi" is still there
-
- Scenario Outline: I set my location to ""
- When I save "" as my location
- When people visit my profile page
- Then they can see the location in the info box below my avatar
-
- Examples: Location
- | location | type |
- | Paris | City |
- | Saxony-Anhalt | Region |
- | Germany | Country |
-
- Scenario: Display a description on profile page
- Given I have the following self-description:
- """
- Ich lebe fettlos, fleischlos, fischlos dahin, fühle mich aber ganz wohl dabei
- """
- When people visit my profile page
- Then they can see the text in the info box below my avatar
diff --git a/cypress/integration/user_profile/SocialMedia.feature b/cypress/integration/user_profile/SocialMedia.feature
deleted file mode 100644
index e6090a0a4..000000000
--- a/cypress/integration/user_profile/SocialMedia.feature
+++ /dev/null
@@ -1,42 +0,0 @@
-Feature: List Social Media Accounts
- As a User
- I'd like to enter my social media
- So I can show them to other users to get in contact
-
- Background:
- Given I have a user account
- And I am logged in
-
- Scenario: Adding Social Media
- Given I am on the "settings" page
- And I click on the "Social media" link
- Then I should be on the "/settings/my-social-media" page
- When I add a social media link
- Then it gets saved successfully
- And the new social media link shows up on the page
-
- Scenario: Other users viewing my Social Media
- Given I have added a social media link
- When people visit my profile page
- Then they should be able to see my social media links
-
- Scenario: Deleting Social Media
- Given I am on the "settings" page
- And I click on the "Social media" link
- Then I should be on the "/settings/my-social-media" page
- Given I have added a social media link
- When I delete a social media link
- Then it gets deleted successfully
-
- Scenario: Editing Social Media
- Given I am on the "settings" page
- And I click on the "Social media" link
- Then I should be on the "/settings/my-social-media" page
- Given I have added a social media link
- When I start editing a social media link
- Then I can cancel editing
- When I start editing a social media link
- And I edit and save the link
- Then it gets saved successfully
- And the new url is displayed
- But the old url is not displayed
diff --git a/cypress/integration/user_profile/UploadUserProfileImage.feature b/cypress/integration/user_profile/UploadUserProfileImage.feature
deleted file mode 100644
index b46a31de8..000000000
--- a/cypress/integration/user_profile/UploadUserProfileImage.feature
+++ /dev/null
@@ -1,18 +0,0 @@
-Feature: Upload UserProfile Image
- As a user
- I would like to be able to add an avatar/profile pic to my profile
- So that I can personalize my profile
-
-
- Background:
- Given I have a user account
-
- Scenario: Change my UserProfile Image
- Given I am logged in
- And I visit my profile page
- Then I should be able to change my profile picture
-
- Scenario: Unable to change another user's avatar
- Given I am logged in with a "user" role
- And I visit another user's profile page
- Then I cannot upload a picture
\ No newline at end of file
diff --git a/cypress/integration/user_profile/mute-users/Mute.feature b/cypress/integration/user_profile/mute-users/Mute.feature
deleted file mode 100644
index 03ac4370b..000000000
--- a/cypress/integration/user_profile/mute-users/Mute.feature
+++ /dev/null
@@ -1,51 +0,0 @@
-Feature: Mute a User
- As a user
- I'd like to have a button to mute another user
- To prevent him from seeing and interacting with my contributions
- Background:
- Given I have a user account
- And there is an annoying user called "Spammy Spammer"
- And I am logged in
-
- Scenario: Mute a user
- Given I am on the profile page of the annoying user
- When I click on "Mute user" from the content menu in the user info box
- And I navigate to my "Muted users" settings page
- Then I can see the following table:
- | Avatar | Name |
- | | Spammy Spammer |
-
- Scenario: Mute a previously followed user
- Given I follow the user "Spammy Spammer"
- And "Spammy Spammer" wrote a post "Spam Spam Spam"
- When I visit the profile page of the annoying user
- And I click on "Mute user" from the content menu in the user info box
- Then the list of posts of this user is empty
- And I get removed from his follower collection
-
- Scenario: Posts of muted users are filtered from search results, users are not
- Given we have the following posts in our database:
- | id | title | content |
- | im-not-muted | Post that should be seen | cause I'm not muted |
- Given "Spammy Spammer" wrote a post "Spam Spam Spam"
- When I search for "Spam"
- Then I should see the following posts in the select dropdown:
- | title |
- | Spam Spam Spam |
- When I mute the user "Spammy Spammer"
- And I refresh the page
- And I search for "Spam"
- Then the search should not contain posts by the annoying user
- But the search should contain the annoying user
- But I search for "not muted"
- Then I should see the following posts in the select dropdown:
- | title |
- | Post that should be seen |
-
- Scenario: Muted users can still see my posts
- Given I previously created a post
- And I mute the user "Spammy Spammer"
- And the "muted" user searches for "previously created"
- Then I should see the following posts in the select dropdown:
- | title |
- | previously created post |
diff --git a/cypress/parallel-features.sh b/cypress/parallel-features.sh
new file mode 100755
index 000000000..a234b1d0e
--- /dev/null
+++ b/cypress/parallel-features.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+# Functions
+function join_by { local IFS="$1"; shift; echo "$*"; }
+
+# Arguments:
+CUR_JOB=$1
+MAX_JOBS=$2
+
+# Features
+FEATURE_LIST=( $(find cypress/integration/ -maxdepth 1 -name "*.feature") )
+
+# Calculation
+MAX_FEATURES=$(find cypress/integration/ -maxdepth 1 -name "*.feature" -printf '.' | wc -m)
+FEATURES_PER_JOB=$(expr $(expr ${MAX_FEATURES} + ${MAX_JOBS} - 1) / ${MAX_JOBS} )
+FEATURES_SKIP=$(expr $(expr ${CUR_JOB} - 1 ) \* ${FEATURES_PER_JOB} )
+
+# Comma separated list
+echo $(join_by , ${FEATURE_LIST[@]:${FEATURES_SKIP}:${FEATURES_PER_JOB}})
\ No newline at end of file
diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js
index cc6ac0e91..4e6b440ef 100644
--- a/cypress/plugins/index.js
+++ b/cypress/plugins/index.js
@@ -15,15 +15,27 @@
const cucumber = require('cypress-cucumber-preprocessor').default
const dotenv = require('dotenv')
+// Import backend .env (smart)?
+const { parsed } = dotenv.config({ path: require.resolve('../../backend/.env') })
+
+// Test persistent(between commands) store
+const testStore = {}
+
module.exports = (on, config) => {
- // (on, config) => {
- // `on` is used to hook into various events Cypress emits
- // `config` is the resolved Cypress config
- const { parsed } = dotenv.config({ path: require.resolve('../../backend/.env') })
config.env.NEO4J_URI = parsed.NEO4J_URI
config.env.NEO4J_USERNAME = parsed.NEO4J_USERNAME
config.env.NEO4J_PASSWORD = parsed.NEO4J_PASSWORD
config.env.JWT_SECRET = parsed.JWT_SECRET
on('file:preprocessor', cucumber())
+ on('task', {
+ pushValue({ name, value }) {
+ testStore[name] = value
+ return true
+ },
+ getValue(name) {
+ console.log("getValue",name,testStore)
+ return testStore[name]
+ },
+ })
return config
-}
+}
\ No newline at end of file
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
index a15e57007..335e00390 100644
--- a/cypress/support/commands.js
+++ b/cypress/support/commands.js
@@ -14,16 +14,9 @@
/* globals Cypress cy */
import "cypress-file-upload";
-import helpers from "./helpers";
import { GraphQLClient, request } from 'graphql-request'
import { gql } from '../../backend/src/helpers/jest'
import config from '../../backend/src/config'
-import encode from '../../backend/src/jwt/encode'
-
-const switchLang = name => {
- cy.get(".locale-menu").click();
- cy.contains(".locale-menu-popover a", name).click();
-};
const authenticatedHeaders = (variables) => {
const mutation = gql`
@@ -38,50 +31,11 @@ const authenticatedHeaders = (variables) => {
})
}
-Cypress.Commands.add("switchLanguage", (name, force) => {
- const { code } = helpers.getLangByName(name);
- if (force) {
- switchLang(name);
- } else {
- cy.get("html").then($html => {
- if ($html && $html.attr("lang") !== code) {
- switchLang(name);
- }
- });
- }
-});
-
-Cypress.Commands.add("login", user => {
- const token = encode(user)
- cy.setCookie('human-connection-token', token)
- .visit("/")
-});
-
-Cypress.Commands.add("manualLogin", ({ email, password }) => {
- cy.visit(`/login`)
- .get("input[name=email]")
- .trigger("focus")
- .type(email)
- .get("input[name=password]")
- .trigger("focus")
- .type(password)
- .get("button[name=submit]")
- .as("submitButton")
- .click();
-});
-
Cypress.Commands.add("logout", () => {
cy.visit(`/logout`);
cy.location("pathname").should("contain", "/login"); // we're out
});
-Cypress.Commands.add("openPage", page => {
- if (page === "landing") {
- page = "";
- }
- cy.visit(`/${page}`);
-});
-
Cypress.Commands.add(
'authenticateAs',
({email, password}) => {
diff --git a/cypress/support/helpers.js b/cypress/support/helpers.js
deleted file mode 100644
index 7d66af5d6..000000000
--- a/cypress/support/helpers.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import find from 'lodash/find'
-import locales from '../../webapp/locales'
-
-export default {
- getLangByName(name) {
- return find(locales, { name })
- }
-}
diff --git a/cypress/support/index.js b/cypress/support/index.js
index 3290d2a5a..453c8476f 100644
--- a/cypress/support/index.js
+++ b/cypress/support/index.js
@@ -19,7 +19,7 @@ import './commands'
import './factories'
// intermittent failing tests
-import 'cypress-plugin-retries'
+// import 'cypress-plugin-retries'
// Alternatively you can use CommonJS syntax:
// require('./commands')
diff --git a/docker-compose.override.yml b/docker-compose.override.yml
index 8d1bb96f2..5fceb2776 100644
--- a/docker-compose.override.yml
+++ b/docker-compose.override.yml
@@ -18,7 +18,6 @@ services:
- webapp_node_modules:/app/node_modules
# bind the local folder to the docker to allow live reload
- ./webapp:/app
-
########################################################
# BACKEND ##############################################
########################################################
@@ -35,7 +34,6 @@ services:
- backend_node_modules:/app/node_modules
# bind the local folder to the docker to allow live reload
- ./backend:/app
-
########################################################
# NEO4J ################################################
########################################################
@@ -47,13 +45,11 @@ services:
networks:
# So we can access the neo4j query browser from our host machine
- external-net
-
########################################################
# MAINTENANCE ##########################################
########################################################
maintenance:
image: ocelotsocialnetwork/maintenance:development
-
########################################################
# MAILSERVER TO FAKE SMTP ##############################
########################################################
@@ -63,6 +59,7 @@ services:
- 1080:80
networks:
- external-net
+
volumes:
webapp_node_modules:
backend_node_modules:
diff --git a/docker-compose.test.yml b/docker-compose.test.yml
index 2d382d606..973cf87cf 100644
--- a/docker-compose.test.yml
+++ b/docker-compose.test.yml
@@ -10,7 +10,8 @@ services:
target: test
environment:
- NODE_ENV="test"
-
+ volumes:
+ - ./coverage:/app/coverage
########################################################
# BACKEND ##############################################
########################################################
@@ -20,19 +21,29 @@ services:
target: test
environment:
- NODE_ENV="test"
-
+ volumes:
+ - ./coverage:/app/coverage
########################################################
# NEO4J ################################################
########################################################
neo4j:
image: ocelotsocialnetwork/neo4j:community
-
+ #environment:
+ # - NEO4J_dbms_connector_bolt_enabled=true
+ # - NEO4J_dbms_connector_bolt_tls__level=OPTIONAL
+ # - NEO4J_dbms_connector_bolt_listen__address=0.0.0.0:7687
+ # - NEO4J_auth=none
+ # - NEO4J_dbms_connectors_default__listen__address=0.0.0.0
+ # - NEO4J_dbms_connector_http_listen__address=0.0.0.0:7474
+ # - NEO4J_dbms_connector_https_listen__address=0.0.0.0:7473
+ networks:
+ # So we can access the neo4j query browser from our host machine
+ - external-net
########################################################
# MAINTENANCE ##########################################
########################################################
maintenance:
image: ocelotsocialnetwork/maintenance:test
-
########################################################
# MAILSERVER TO FAKE SMTP ##############################
########################################################
@@ -42,6 +53,7 @@ services:
- 1080:80
networks:
- external-net
+
volumes:
webapp_node_modules:
backend_node_modules:
diff --git a/docker-compose.yml b/docker-compose.yml
index 392447f61..d20bb6aec 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -35,7 +35,6 @@ services:
- GRAPHQL_URI=http://backend:4000
env_file:
- ./webapp/.env
-
########################################################
# BACKEND ##############################################
########################################################
@@ -68,7 +67,6 @@ services:
- CLIENT_URI=http://webapp:3000
env_file:
- ./backend/.env
-
########################################################
# NEO4J ################################################
########################################################
@@ -92,7 +90,6 @@ services:
# TODO: clarify if that is the only thing needed to unlock the Enterprise version
# - NEO4J_ACCEPT_LICENSE_AGREEMENT=yes
# TODO: Remove the playground from production
-
########################################################
# MAINTENANCE ##########################################
########################################################
diff --git a/neo4j/Dockerfile b/neo4j/Dockerfile
index e7931378a..4bdc4ef1f 100644
--- a/neo4j/Dockerfile
+++ b/neo4j/Dockerfile
@@ -3,13 +3,16 @@
##################################################################################
FROM neo4j:3.5.14 as community
-# ENVs (available in production aswell, can be overwritten by commandline or env file)
+# ENVs
## We Cannot do `$(date -u +'%Y-%m-%dT%H:%M:%SZ')` here so we use unix timestamp=0
-ENV BUILD_DATE="1970-01-01T00:00:00.00Z"
+ARG BBUILD_DATE="1970-01-01T00:00:00.00Z"
+ENV BUILD_DATE=$BBUILD_DATE
## We cannot do $(yarn run version)-${BUILD_NUMBER} here so we default to 0.0.0-0
-ENV BUILD_VERSION="0.0.0-0"
+ARG BBUILD_VERSION="0.0.0-0"
+ENV BUILD_VERSION=$BBUILD_VERSION
## We cannot do `$(git rev-parse --short HEAD)` here so we default to 0000000
-ENV BUILD_COMMIT="0000000"
+ARG BBUILD_COMMIT="0000000"
+ENV BUILD_COMMIT=$BBUILD_COMMIT
# Labels
LABEL org.label-schema.build-date="${BUILD_DATE}"
diff --git a/package.json b/package.json
index fbf890306..9c30b2c48 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "ocelot-social",
- "version": "0.6.15",
+ "version": "1.0.4",
"description": "Fullstack and API tests with cypress and cucumber for ocelot.social",
"author": "ocelot.social Community",
"license": "MIT",
@@ -13,14 +13,10 @@
"nonGlobalStepDefinitions": true
},
"scripts": {
- "install:all": "yarn install && cd backend && yarn install && cd ../webapp && yarn install",
"db:seed": "cd backend && yarn run db:seed",
"db:reset": "cd backend && yarn run db:reset",
- "cypress:backend": "cd backend && yarn run dev",
- "cypress:webapp": "cd webapp && yarn run dev",
- "cypress:setup": "run-p cypress:backend cypress:webapp",
- "cypress:run": "cross-env cypress run --browser firefox",
- "cypress:open": "cross-env cypress open --browser firefox",
+ "cypress:run": "cypress run --browser electron --config-file ./cypress/cypress.json",
+ "cypress:open": "cypress open --browser electron --config-file ./cypress/cypress.json",
"cucumber:setup": "cd backend && yarn run dev",
"cucumber": "wait-on tcp:4000 && cucumber-js --require-module @babel/register --exit",
"release": "yarn version --no-git-tag-version --no-commit-hooks --no-commit && auto-changelog --latest-version $(node -p -e \"require('./package.json').version\") && cd backend && yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version $(node -p -e \"require('./../package.json').version\") && cd ../webapp && yarn version --no-git-tag-version --no-commit-hooks --no-commit --new-version $(node -p -e \"require('./../package.json').version\")"
@@ -29,30 +25,30 @@
"@babel/core": "^7.9.0",
"@babel/preset-env": "^7.12.7",
"@babel/register": "^7.12.10",
- "auto-changelog": "^2.2.1",
+ "auto-changelog": "^2.3.0",
"bcryptjs": "^2.4.3",
"codecov": "^3.7.1",
- "cross-env": "^7.0.2",
+ "cross-env": "^7.0.3",
"cucumber": "^6.0.5",
- "cypress": "^4.2.0",
+ "cypress": "^7.0.1",
"cypress-cucumber-preprocessor": "^2.2.1",
"cypress-file-upload": "^3.5.3",
- "cypress-plugin-retries": "^1.5.2",
- "date-fns": "^2.12.0",
+ "date-fns": "^2.22.1",
"dotenv": "^8.2.0",
"expect": "^25.3.0",
"faker": "Marak/faker.js#master",
- "graphql-request": "^1.8.2",
+ "graphql-request": "^2.0.0",
"import": "^0.0.6",
"jsonwebtoken": "^8.5.1",
"mock-socket": "^9.0.3",
"neo4j-driver": "^4.0.2",
- "neode": "^0.3.7",
+ "neode": "^0.4.7",
"npm-run-all": "^4.1.5",
"rosie": "^2.0.1",
"slug": "^2.1.1"
},
"resolutions": {
"set-value": "^2.0.1"
- }
+ },
+ "dependencies": {}
}
diff --git a/webapp/.env.template b/webapp/.env.template
index 1acad49b4..7373255a9 100644
--- a/webapp/.env.template
+++ b/webapp/.env.template
@@ -1,5 +1,6 @@
SENTRY_DSN_WEBAPP=
COMMIT=
PUBLIC_REGISTRATION=false
+INVITE_REGISTRATION=true
WEBSOCKETS_URI=ws://localhost:3000/api/graphql
GRAPHQL_URI=http://localhost:4000/
diff --git a/webapp/Dockerfile b/webapp/Dockerfile
index eda437920..8d830a9d5 100644
--- a/webapp/Dockerfile
+++ b/webapp/Dockerfile
@@ -1,17 +1,20 @@
##################################################################################
-# BASE ###########################################################################
+# BASE (Is pushed to DockerHub for rebranding) ###################################
##################################################################################
FROM node:12.19.0-alpine3.10 as base
-# ENVs (available in production aswell, can be overwritten by commandline or env file)
+# ENVs
## DOCKER_WORKDIR would be a classical ARG, but that is not multi layer persistent - shame
ENV DOCKER_WORKDIR="/app"
## We Cannot do `$(date -u +'%Y-%m-%dT%H:%M:%SZ')` here so we use unix timestamp=0
-ENV BUILD_DATE="1970-01-01T00:00:00.00Z"
+ARG BBUILD_DATE="1970-01-01T00:00:00.00Z"
+ENV BUILD_DATE=$BBUILD_DATE
## We cannot do $(yarn run version)-${BUILD_NUMBER} here so we default to 0.0.0-0
-ENV BUILD_VERSION="0.0.0-0"
+ARG BBUILD_VERSION="0.0.0-0"
+ENV BUILD_VERSION=$BBUILD_VERSION
## We cannot do `$(git rev-parse --short HEAD)` here so we default to 0000000
-ENV BUILD_COMMIT="0000000"
+ARG BBUILD_COMMIT="0000000"
+ENV BUILD_COMMIT=$BBUILD_COMMIT
## SET NODE_ENV
ENV NODE_ENV="production"
## App relevant Envs
@@ -56,12 +59,18 @@ FROM base as development
CMD /bin/sh -c "yarn install && yarn run dev"
##################################################################################
-# BUILD (Does contain all files and is therefore bloated) ########################
+# CODE (Does contain all code files and is pushed to DockerHub for rebranding) ###
##################################################################################
-FROM base as build
+FROM base as code
-# Copy everything
+# copy everything, but do not build.
COPY . .
+
+##################################################################################
+# BUILD (Does contain all files and the compilate and is therefore bloated) ######
+##################################################################################
+FROM code as build
+
# yarn install
RUN yarn install --production=false --frozen-lockfile --non-interactive
# yarn build
@@ -80,18 +89,13 @@ CMD /bin/sh -c "yarn run dev"
##################################################################################
FROM base as production
+# TODO - do all copying with one COPY command to have one layer
# Copy "binary"-files from build image
COPY --from=build ${DOCKER_WORKDIR}/.nuxt ./.nuxt
COPY --from=build ${DOCKER_WORKDIR}/node_modules ./node_modules
COPY --from=build ${DOCKER_WORKDIR}/nuxt.config.js ./nuxt.config.js
-# Copy static files
-# TODO - this should be one Folder containign all stuff needed to be copied
-COPY --from=build ${DOCKER_WORKDIR}/config/ ./config/
-COPY --from=build ${DOCKER_WORKDIR}/constants ./constants
-COPY --from=build ${DOCKER_WORKDIR}/static ./static
-COPY --from=build ${DOCKER_WORKDIR}/locales ./locales
# Copy package.json for script definitions (lock file should not be needed)
COPY --from=build ${DOCKER_WORKDIR}/package.json ./package.json
# Run command
-CMD /bin/sh -c "yarn run start"
\ No newline at end of file
+CMD /bin/sh -c "yarn run start"
diff --git a/webapp/Dockerfile.maintenance b/webapp/Dockerfile.maintenance
index b02fe352b..dcc06ad61 100644
--- a/webapp/Dockerfile.maintenance
+++ b/webapp/Dockerfile.maintenance
@@ -1,20 +1,59 @@
-FROM node:12.19.0-alpine3.10 as build
-LABEL Description="Maintenance page of the Social Network ocelot.social" Vendor="ocelot.social Community" Version="0.0.1" Maintainer="ocelot.social Community (devops@ocelot.social)"
+##################################################################################
+# BASE ###########################################################################
+##################################################################################
+FROM node:12.19.0-alpine3.10 as base
-EXPOSE 3000
-CMD ["yarn", "run", "start"]
+# ENVs
+## DOCKER_WORKDIR would be a classical ARG, but that is not multi layer persistent - shame
+ENV DOCKER_WORKDIR="/app"
+## We Cannot do `$(date -u +'%Y-%m-%dT%H:%M:%SZ')` here so we use unix timestamp=0
+ARG BBUILD_DATE="1970-01-01T00:00:00.00Z"
+ENV BUILD_DATE=$BBUILD_DATE
+## We cannot do $(yarn run version)-${BUILD_NUMBER} here so we default to 0.0.0-0
+ARG BBUILD_VERSION="0.0.0-0"
+ENV BUILD_VERSION=$BBUILD_VERSION
+## We cannot do `$(git rev-parse --short HEAD)` here so we default to 0000000
+ARG BBUILD_COMMIT="0000000"
+ENV BUILD_COMMIT=$BBUILD_COMMIT
+## SET NODE_ENV
+ENV NODE_ENV="production"
+## App relevant Envs
+ENV PORT="3000"
-# Expose the app port
-ARG BUILD_COMMIT
-ENV BUILD_COMMIT=$BUILD_COMMIT
-ARG WORKDIR=/develop-webapp
-RUN mkdir -p $WORKDIR
-WORKDIR $WORKDIR
+# Labels
+LABEL org.label-schema.build-date="${BUILD_DATE}"
+LABEL org.label-schema.name="ocelot.social:backend"
+LABEL org.label-schema.description="Maintenance page of the Social Network Software ocelot.social"
+LABEL org.label-schema.usage="https://github.com/Ocelot-Social-Community/Ocelot-Social/blob/master/README.md"
+LABEL org.label-schema.url="https://ocelot.social"
+LABEL org.label-schema.vcs-url="https://github.com/Ocelot-Social-Community/Ocelot-Social/tree/master/backend"
+LABEL org.label-schema.vcs-ref="${BUILD_COMMIT}"
+LABEL org.label-schema.vendor="ocelot.social Community"
+LABEL org.label-schema.version="${BUILD_VERSION}"
+LABEL org.label-schema.schema-version="1.0"
+LABEL maintainer="devops@ocelot.social"
-# See: https://github.com/nodejs/docker-node/pull/367#issuecomment-430807898
+# Install Additional Software
+## install: git
RUN apk --no-cache add git
+# Settings
+## Expose Container Port
+EXPOSE ${PORT}
+
+## Workdir
+RUN mkdir -p ${DOCKER_WORKDIR}
+WORKDIR ${DOCKER_WORKDIR}
+
+CMD ["yarn", "run", "start"]
+
+##################################################################################
+# CODE (Does contain all code files and is pushed to DockerHub for rebranding) ###
+##################################################################################
+FROM base as code
+
COPY package.json yarn.lock ./
+# yarn install
RUN yarn install --production=false --frozen-lockfile --non-interactive
COPY assets assets
@@ -29,14 +68,24 @@ COPY constants constants
COPY nuxt.config.js nuxt.config.js
COPY config/ config/
+# this is needed in rebranding
+COPY maintenance/nginx maintenance/nginx
# this will also ovewrite the existing package.json
COPY maintenance/source ./
+##################################################################################
+# BUILD ### TODO # TODO # TODO # TODO # TODO # TODO # TODO # TODO # TODO # TODO ##
+##################################################################################
+FROM code as build
+# yarn generate
RUN yarn run generate
+##################################################################################
+# PRODUCTION ### TODO # TODO # TODO # TODO # TODO # TODO # TODO # TODO # TODO ####
+##################################################################################
+FROM nginx:alpine as production
-FROM nginx:alpine
-COPY --from=build ./develop-webapp/dist/ /usr/share/nginx/html/
+COPY --from=build ./app/dist/ /usr/share/nginx/html/
RUN rm /etc/nginx/conf.d/default.conf
COPY maintenance/nginx/custom.conf /etc/nginx/conf.d/
diff --git a/webapp/assets/_new/icons/svgs/copy.svg b/webapp/assets/_new/icons/svgs/copy.svg
new file mode 100644
index 000000000..1792f2002
--- /dev/null
+++ b/webapp/assets/_new/icons/svgs/copy.svg
@@ -0,0 +1,5 @@
+
+
diff --git a/webapp/components/AvatarMenu/AvatarMenu.vue b/webapp/components/AvatarMenu/AvatarMenu.vue
index f65c6f6cf..d47eb2d68 100644
--- a/webapp/components/AvatarMenu/AvatarMenu.vue
+++ b/webapp/components/AvatarMenu/AvatarMenu.vue
@@ -46,6 +46,7 @@
+
+
diff --git a/webapp/components/Editor/MenuBarButton.vue b/webapp/components/Editor/MenuBarButton.vue
index 2543352ca..e4f11e46d 100644
--- a/webapp/components/Editor/MenuBarButton.vue
+++ b/webapp/components/Editor/MenuBarButton.vue
@@ -10,7 +10,7 @@ export default {
isActive: Boolean,
icon: String,
label: String,
- onClick: Function,
+ onClick: { type: Function, default: () => {} },
},
}
diff --git a/webapp/components/Editor/MenuLegend.vue b/webapp/components/Editor/MenuLegend.vue
new file mode 100644
index 000000000..afc137b1f
--- /dev/null
+++ b/webapp/components/Editor/MenuLegend.vue
@@ -0,0 +1,115 @@
+
+
+
+
+
+
+
+
+
+
{{ userInitials }} .initials,
> .base-icon {
position: absolute;
@@ -77,6 +86,7 @@ export default {
height: 100%;
object-fit: cover;
object-position: center;
+ background-color: $background-color-base;
}
}
diff --git a/webapp/components/_new/generic/UserAvatar/storybook/critical-avatar-white-background.png b/webapp/components/_new/generic/UserAvatar/storybook/critical-avatar-white-background.png
new file mode 100755
index 000000000..adfaa5a86
Binary files /dev/null and b/webapp/components/_new/generic/UserAvatar/storybook/critical-avatar-white-background.png differ
diff --git a/webapp/components/features/ReportsTable/ReportsTable.spec.js b/webapp/components/features/ReportsTable/ReportsTable.spec.js
index a9baeea4f..c80e4fea5 100644
--- a/webapp/components/features/ReportsTable/ReportsTable.spec.js
+++ b/webapp/components/features/ReportsTable/ReportsTable.spec.js
@@ -34,7 +34,7 @@ describe('ReportsTable', () => {
describe('given no reports', () => {
beforeEach(() => {
- propsData = { ...propsData, reports: [] }
+ propsData = { ...propsData }
wrapper = Wrapper()
})
diff --git a/webapp/components/generic/SearchableInput/SearchableInput.spec.js b/webapp/components/generic/SearchableInput/SearchableInput.spec.js
index 53c361997..e0e9f9831 100644
--- a/webapp/components/generic/SearchableInput/SearchableInput.spec.js
+++ b/webapp/components/generic/SearchableInput/SearchableInput.spec.js
@@ -120,5 +120,15 @@ describe('SearchableInput.vue', () => {
query: { search: 'ab' },
})
})
+
+ it('replaces irregular whitespace with a single space', async () => {
+ select.element.value = 'peter lustig'
+ select.trigger('input')
+ select.trigger('keyup.enter')
+ expect(mocks.$router.push).toHaveBeenCalledWith({
+ path: '/search/search-results',
+ query: { search: 'peter lustig' },
+ })
+ })
})
})
diff --git a/webapp/components/utils/TranslateErrorMessage.js b/webapp/components/utils/TranslateErrorMessage.js
new file mode 100644
index 000000000..3597f3358
--- /dev/null
+++ b/webapp/components/utils/TranslateErrorMessage.js
@@ -0,0 +1,12 @@
+export default (message, mapping, translate) => {
+ let translatedMessage = null
+ for (const [pattern, ident] of Object.entries(mapping)) {
+ if (message.includes(pattern)) {
+ translatedMessage = translate(ident)
+ }
+ }
+ if (!translatedMessage) {
+ translatedMessage = message
+ }
+ return translatedMessage
+}
diff --git a/webapp/config/index.js b/webapp/config/index.js
index fd564f350..00df85bac 100644
--- a/webapp/config/index.js
+++ b/webapp/config/index.js
@@ -12,13 +12,12 @@ const environment = {
PRODUCTION: process.env.NODE_ENV === 'production' || false,
NUXT_BUILD: process.env.NUXT_BUILD || '.nuxt',
STYLEGUIDE_DEV: process.env.STYLEGUIDE_DEV || false,
- RELEASE: process.env.release,
}
const server = {
GRAPHQL_URI: process.env.GRAPHQL_URI || 'http://localhost:4000',
BACKEND_TOKEN: process.env.BACKEND_TOKEN || 'NULL',
- WEBSOCKETS_URI: process.env.WEBSOCKETS_URI || 'ws://localhost:4000/graphql',
+ WEBSOCKETS_URI: process.env.WEBSOCKETS_URI || 'ws://localhost:3000/api/graphql',
}
const sentry = {
@@ -29,6 +28,8 @@ const sentry = {
const options = {
VERSION: process.env.VERSION || pkg.version,
DESCRIPTION: process.env.DESCRIPTION || pkg.description,
+ PUBLIC_REGISTRATION: process.env.PUBLIC_REGISTRATION === 'true' || false,
+ INVITE_REGISTRATION: process.env.INVITE_REGISTRATION !== 'false', // default = true
// Cookies
COOKIE_EXPIRE_TIME: process.env.COOKIE_EXPIRE_TIME || 730, // Two years by default
COOKIE_HTTPS_ONLY: process.env.COOKIE_HTTPS_ONLY || process.env.NODE_ENV === 'production', // ensure true in production if not set explicitly
diff --git a/webapp/constants/links.js b/webapp/constants/links.js
index 494e449ba..b111726f3 100644
--- a/webapp/constants/links.js
+++ b/webapp/constants/links.js
@@ -1,6 +1,17 @@
+// this file is duplicated in `backend/src/config/links.js` and `webapp/constants/links.js` and replaced on rebranding by https://github.com/Ocelot-Social-Community/Ocelot-Social-Deploy-Rebranding/tree/master/branding/constants/
export default {
+ LANDING_PAGE: '/login', // examples: '/login', '/registration', '/organization', or external 'https://ocelot.social'
+
+ // you can find and store templates at https://github.com/Ocelot-Social-Community/Ocelot-Social-Deploy-Rebranding/tree/master/branding/templates/
+
+ SUPPORT: 'https://ocelot.social', // example for internal support page: 'https://staging.ocelot.social/support'. set a full URL please, because it is used in e-mails as well!
+
+ // on null or empty strings internal pages are used, see 'webapp/locales/html/'
ORGANIZATION: 'https://ocelot.social',
- DONATE: 'https://ocelot-social.herokuapp.com/donations',
+ DONATE: 'https://ocelot-social.herokuapp.com/donations', // we use 'ocelot-social.herokuapp.com' at the moment, because redirections of 'ocelot.social' subpages are not working correctly
+ IMPRINT: 'https://ocelot-social.herokuapp.com/imprint', // we use 'ocelot-social.herokuapp.com' at the moment, because redirections of 'ocelot.social' subpages are not working correctly
+ TERMS_AND_CONDITIONS: null,
+ CODE_OF_CONDUCT: null,
+ DATA_PRIVACY: null,
FAQ: 'https://ocelot.social',
- SUPPORT: 'https://ocelot.social',
}
diff --git a/webapp/constants/logos.js b/webapp/constants/logos.js
new file mode 100644
index 000000000..d093c7b46
--- /dev/null
+++ b/webapp/constants/logos.js
@@ -0,0 +1,10 @@
+// this file is duplicated in `backend/src/config/logos.js` and `webapp/constants/logos.js` and replaced on rebranding
+// this are the paths in the webapp
+export default {
+ LOGO_HEADER_PATH: '/img/custom/logo-horizontal.svg',
+ LOGO_SIGNUP_PATH: '/img/custom/logo-squared.svg',
+ LOGO_WELCOME_PATH: '/img/custom/logo-squared.svg',
+ LOGO_LOGOUT_PATH: '/img/custom/logo-squared.svg',
+ LOGO_PASSWORD_RESET_PATH: '/img/custom/logo-squared.svg',
+ LOGO_MAINTENACE_RESET_PATH: '/img/custom/logo-squared.svg',
+}
diff --git a/webapp/constants/metadata.js b/webapp/constants/metadata.js
index 20b9a5896..da313c7fb 100644
--- a/webapp/constants/metadata.js
+++ b/webapp/constants/metadata.js
@@ -1,8 +1,9 @@
+// this file is duplicated in `backend/src/config/metadata.js` and `webapp/constants/metadata.js` and replaced on rebranding
export default {
APPLICATION_NAME: 'ocelot.social',
- APPLICATION_SHORT_NAME: 'ocelot',
+ APPLICATION_SHORT_NAME: 'ocelot.social',
APPLICATION_DESCRIPTION: 'ocelot.social Community Network',
+ COOKIE_NAME: 'ocelot-social-token',
ORGANIZATION_NAME: 'ocelot.social Community',
ORGANIZATION_JURISDICTION: 'City of Angels',
- COOKIE_NAME: 'ocelot-social-token',
}
diff --git a/webapp/graphql/Fragments.js b/webapp/graphql/Fragments.js
index b67851873..7b05e2369 100644
--- a/webapp/graphql/Fragments.js
+++ b/webapp/graphql/Fragments.js
@@ -78,12 +78,6 @@ export const tagsCategoriesAndPinnedFragment = gql`
tags {
id
}
- categories {
- id
- slug
- name
- icon
- }
pinnedBy {
id
name
diff --git a/webapp/graphql/Registration.js b/webapp/graphql/Registration.js
index 873c855db..fcf9c52a7 100644
--- a/webapp/graphql/Registration.js
+++ b/webapp/graphql/Registration.js
@@ -4,6 +4,7 @@ export const SignupVerificationMutation = gql`
$nonce: String!
$name: String!
$email: String!
+ $inviteCode: String
$password: String!
$about: String
$termsAndConditionsAgreedVersion: String!
@@ -12,6 +13,7 @@ export const SignupVerificationMutation = gql`
SignupVerification(
nonce: $nonce
email: $email
+ inviteCode: $inviteCode
name: $name
password: $password
about: $about
diff --git a/webapp/layouts/basic.spec.js b/webapp/layouts/basic.spec.js
new file mode 100644
index 000000000..5094a970b
--- /dev/null
+++ b/webapp/layouts/basic.spec.js
@@ -0,0 +1,34 @@
+import { config, shallowMount } from '@vue/test-utils'
+import Basic from './basic.vue'
+
+const localVue = global.localVue
+
+config.stubs.nuxt = ''
+
+describe('basic.vue', () => {
+ let wrapper
+ let mocks
+
+ beforeEach(() => {
+ mocks = {
+ $t: jest.fn(),
+ }
+ })
+
+ describe('shallow mount', () => {
+ const Wrapper = () => {
+ return shallowMount(Basic, {
+ mocks,
+ localVue,
+ })
+ }
+
+ beforeEach(() => {
+ wrapper = Wrapper()
+ })
+
+ it('renders', () => {
+ expect(wrapper.is('.layout-blank')).toBe(true)
+ })
+ })
+})
diff --git a/webapp/layouts/basic.vue b/webapp/layouts/basic.vue
index 527d5239c..01872868e 100644
--- a/webapp/layouts/basic.vue
+++ b/webapp/layouts/basic.vue
@@ -6,7 +6,7 @@
-
+
diff --git a/webapp/layouts/blank.spec.js b/webapp/layouts/blank.spec.js
new file mode 100644
index 000000000..a3ea3120c
--- /dev/null
+++ b/webapp/layouts/blank.spec.js
@@ -0,0 +1,34 @@
+import { config, shallowMount } from '@vue/test-utils'
+import Blank from './blank.vue'
+
+const localVue = global.localVue
+
+config.stubs.nuxt = ''
+
+describe('blank.vue', () => {
+ let wrapper
+ let mocks
+
+ beforeEach(() => {
+ mocks = {
+ $t: jest.fn(),
+ }
+ })
+
+ describe('shallow mount', () => {
+ const Wrapper = () => {
+ return shallowMount(Blank, {
+ mocks,
+ localVue,
+ })
+ }
+
+ beforeEach(() => {
+ wrapper = Wrapper()
+ })
+
+ it('renders', () => {
+ expect(wrapper.is('.layout-blank')).toBe(true)
+ })
+ })
+})
diff --git a/webapp/layouts/default.spec.js b/webapp/layouts/default.spec.js
new file mode 100644
index 000000000..3d465ce76
--- /dev/null
+++ b/webapp/layouts/default.spec.js
@@ -0,0 +1,52 @@
+import Vuex from 'vuex'
+import { config, shallowMount } from '@vue/test-utils'
+import Default from './default.vue'
+
+const localVue = global.localVue
+localVue.directive('scrollTo', jest.fn())
+
+config.stubs.nuxt = ''
+config.stubs['client-only'] = ''
+config.stubs['nuxt-link'] = ''
+
+describe('default.vue', () => {
+ let wrapper
+ let mocks
+ let store
+
+ beforeEach(() => {
+ mocks = {
+ $route: {
+ matched: [{ name: 'index' }],
+ },
+ $scrollTo: jest.fn(),
+ $t: jest.fn(),
+ $env: {
+ INVITE_REGISTRATION: true,
+ },
+ }
+ store = new Vuex.Store({
+ getters: {
+ 'auth/isLoggedIn': () => true,
+ },
+ })
+ })
+
+ describe('shallow mount', () => {
+ const Wrapper = () => {
+ return shallowMount(Default, {
+ store,
+ mocks,
+ localVue,
+ })
+ }
+
+ beforeEach(() => {
+ wrapper = Wrapper()
+ })
+
+ it('renders', () => {
+ expect(wrapper.is('.layout-default')).toBe(true)
+ })
+ })
+})
diff --git a/webapp/layouts/default.vue b/webapp/layouts/default.vue
index 0bf98967e..0dfd752bf 100644
--- a/webapp/layouts/default.vue
+++ b/webapp/layouts/default.vue
@@ -6,7 +6,7 @@
-
+
+
+
+
+
+
@@ -84,6 +89,7 @@ import seo from '~/mixins/seo'
import FilterMenu from '~/components/FilterMenu/FilterMenu.vue'
import PageFooter from '~/components/PageFooter/PageFooter'
import AvatarMenu from '~/components/AvatarMenu/AvatarMenu'
+import InviteButton from '~/components/InviteButton/InviteButton'
export default {
components: {
@@ -95,12 +101,14 @@ export default {
AvatarMenu,
FilterMenu,
PageFooter,
+ InviteButton,
},
mixins: [seo],
data() {
return {
mobileSearchVisible: false,
toggleMobileMenu: false,
+ inviteRegistration: this.$env.INVITE_REGISTRATION === true, // for 'false' in .env INVITE_REGISTRATION is of type undefined and not(!) boolean false, because of internal handling,
}
},
computed: {
diff --git a/webapp/locales/de.json b/webapp/locales/de.json
index e02e4f6b0..de8e98888 100644
--- a/webapp/locales/de.json
+++ b/webapp/locales/de.json
@@ -102,7 +102,6 @@
"letsTalk": "Miteinander reden",
"loading": "wird geladen",
"loadMore": "mehr laden",
- "moreInfo": "Mehr Info",
"name": "Name",
"organization": "Organisation ::: Organisationen",
"post": "Beitrag ::: Beiträge",
@@ -120,26 +119,6 @@
"versus": "Versus"
},
"components": {
- "enter-invite": {
- "form": {
- "description": "Gib den Einladungs-Code ein, den du bekommen hast.",
- "invite-code": "Einladungs-Code eingeben",
- "next": "Weiter",
- "validations": {
- "length": "muss genau 6 Buchstaben lang sein"
- }
- }
- },
- "enter-nonce": {
- "form": {
- "description": "Öffne Dein E-Mail Postfach und gib den Code ein, den wir geschickt haben.",
- "next": "Weiter",
- "nonce": "Code eingeben",
- "validations": {
- "length": "muss genau 6 Buchstaben lang sein"
- }
- }
- },
"password-reset": {
"change-password": {
"error": "Passwort Änderung fehlgeschlagen. Möglicherweise falscher Sicherheitscode?",
@@ -157,33 +136,68 @@
},
"registration": {
"create-user-account": {
+ "buttonTitle": "Erstellen",
"error": "Es konnte kein Benutzerkonto erstellt werden!",
"help": "Vielleicht war der Bestätigungscode falsch oder abgelaufen? Wenn das Problem weiterhin besteht, schicke uns gerne eine E-Mail an:",
+ "recieveCommunicationAsEmailsEtcConfirmed": "Ich stimme auch dem Erhalt von E-Mails und anderen Formen der Kommunikation (z.B. Push-Benachrichtigungen) zu.",
"success": "Dein Benutzerkonto wurde erstellt!",
+ "termsAndCondsEtcConfirmed": "Ich habe folgendes gelesen, verstanden und stimme zu:",
"title": "Benutzerkonto anlegen"
},
"email": {
+ "buttonTitle": {
+ "resend": "Erneut senden",
+ "send": "Sende E-Mail",
+ "skipResend": "Nicht senden"
+ },
"form": {
+ "sendEmailAgain": "E-Mail erneut senden",
"success": "Verifikations-E-Mail gesendet an {email}!"
- }
+ },
+ "title": "E-Mail"
+ },
+ "email-display": {
+ "warningFormat": "⚠️ E-Mail hat ein ungültiges Format!",
+ "warningUndef": "⚠️ Keine E-Mail definiert!",
+ "yourEmail": "Deine E-Mail-Adresse:"
+ },
+ "email-nonce": {
+ "buttonTitle": "Bestätigen",
+ "form": {
+ "description": "Öffne Dein E-Mail Postfach und gib den Code ein, den wir geschickt haben.",
+ "next": "Weiter",
+ "nonce": "E-Mail-Code: 32143",
+ "validations": {
+ "error": "Ungültiger Bestätigungs-Code {nonce} für E-Mail {email}!",
+ "length": "muss genau 5 Buchstaben lang sein",
+ "success": "Gültiger Bestätigungs-Code {nonce} für E-Mail {email}!"
+ }
+ },
+ "title": "E-Mail Bestätigung"
},
"invite-code": {
+ "buttonTitle": "Weiter",
"form": {
- "success": "Gültiger Einladungs-Code {inviteCode}!"
+ "description": "Gib den Einladungs-Code ein, den du bekommen hast.",
+ "invite-code": "Einladungs-Code: ACJERB",
+ "next": "Weiter",
+ "validations": {
+ "error": "Ungültiger Einladungs-Code {inviteCode}!",
+ "length": "muss genau 6 Buchstaben lang sein",
+ "success": "Gültiger Einladungs-Code {inviteCode}!"
+ }
}
},
+ "no-public-registrstion": {
+ "title": "Keine öffentliche Registrierung möglich"
+ },
"signup": {
"form": {
"data-privacy": "Ich habe die Datenschutzerklärung gelesen und verstanden.",
"description": "Um loszulegen, kannst Du Dich hier kostenfrei registrieren:",
"errors": {
- "email-exists": "Es gibt schon ein Benutzerkonto mit dieser E-Mail-Adresse!",
- "invalid-invitation-token": "Es sieht so aus, als ob der Einladungscode schon eingelöst wurde. Jeder Code kann nur einmalig benutzt werden."
+ "email-exists": "Es gibt schon ein Benutzerkonto mit dieser E-Mail-Adresse!"
},
- "invitation-code": "Dein Einladungscode lautet: {code}",
- "minimum-age": "Ich bin 18 Jahre oder älter.",
- "no-commercial": "Ich habe keine kommerziellen Absichten und ich repräsentiere kein kommerzielles Unternehmen oder Organisation.",
- "no-political": "Ich bin nicht im Auftrag einer Partei oder politischen Organisation im Netzwerk.",
"submit": "Konto erstellen",
"success": "Eine E-Mail mit einem Link zum Abschließen Deiner Registrierung wurde an {email} geschickt",
"terms-and-condition": "Ich stimme den Nutzungsbedingungen zu."
@@ -299,6 +313,20 @@
"addLetter": "Tippe einen Buchstaben",
"noHashtagsFound": "Keine Hashtags gefunden"
},
+ "legend": {
+ "bold": "Fett",
+ "heading3": "Überschrift 3",
+ "heading4": "Überschrift 4",
+ "italic": "Kursiv",
+ "legendTitle": "Tastaturkürzel und Markdown-Code",
+ "link": "Verlinkung",
+ "orderedList": "Geordnete Liste",
+ "paragraph": "Absatz",
+ "quote": "Zitat",
+ "ruler": "Linie",
+ "underline": "Unterstrichen",
+ "unorderedList": "Ungeordnete Liste"
+ },
"mention": {
"noUsersFound": "Keine Benutzer gefunden"
},
@@ -336,6 +364,12 @@
"change-filter-settings": "Verändere die Filter-Einstellungen, um mehr Ergebnisse zu erhalten.",
"no-results": "Keine Beiträge gefunden."
},
+ "invite-codes": {
+ "copy-code": "Code:",
+ "copy-success": "Einladungscode erfolgreich in die Zwischenablage kopiert",
+ "not-available": "Du hast keinen Einladungscode zur Verfügung!",
+ "your-code": "Kopiere deinen Einladungscode in die Ablage:"
+ },
"login": {
"email": "Deine E-Mail",
"failure": "Fehlerhafte E-Mail-Adresse oder Passwort.",
@@ -464,14 +498,6 @@
"unpin": "Beitrag loslösen",
"unpinnedSuccessfully": "Angehefteten Beitrag erfolgreich losgelöst!"
},
- "moreInfo": {
- "description": "Hier findest Du weitere Infos zum Thema.",
- "name": "Mehr Info",
- "title": "Mehr Informationen",
- "titleOfCategoriesSection": "Kategorien",
- "titleOfHashtagsSection": "Hashtags",
- "titleOfRelatedContributionsSection": "Verwandte Beiträge"
- },
"name": "Beitrag",
"pinned": "Meldung",
"takeAction": {
@@ -759,9 +785,10 @@
"faq": "FAQ",
"germany": "Deutschland",
"imprint": "Impressum",
- "made": "Mit ❤ gemacht",
+ "made": "Mit ❤️ gemacht",
"register": "Registernummer",
"responsible": "Verantwortlich für Inhalte dieser Seite (§ 55 Abs. 2 RStV)",
+ "support": "Support",
"taxident": "Umsatzsteuer-Identifikationsnummer gemäß § 27 a Umsatzsteuergesetz (Deutschland)",
"termsAndConditions": "Nutzungsbedingungen",
"thanks": "Danke!",
@@ -780,9 +807,7 @@
}
},
"termsAndConditions": {
- "agree": "Ich stimme zu!",
"newTermsAndConditions": "Neue Nutzungsbedingungen",
- "termsAndConditionsConfirmed": "Ich habe die Nutzungsbedingungen durchgelesen und stimme ihnen zu.",
"termsAndConditionsNewConfirm": "Ich habe die neuen Nutzungsbedingungen durchgelesen und stimme zu.",
"termsAndConditionsNewConfirmText": "Bitte lies Dir die neuen Nutzungsbedingungen jetzt durch!"
},
diff --git a/webapp/locales/en.json b/webapp/locales/en.json
index e195f542e..0ad660272 100644
--- a/webapp/locales/en.json
+++ b/webapp/locales/en.json
@@ -102,7 +102,6 @@
"letsTalk": "Let`s Talk",
"loading": "loading",
"loadMore": "load more",
- "moreInfo": "More Info",
"name": "Name",
"organization": "Organization ::: Organizations",
"post": "Post ::: Posts",
@@ -120,26 +119,6 @@
"versus": "Versus"
},
"components": {
- "enter-invite": {
- "form": {
- "description": "Enter the invitation code you received.",
- "invite-code": "Enter your invite code",
- "next": "Continue",
- "validations": {
- "length": "must be 6 characters long"
- }
- }
- },
- "enter-nonce": {
- "form": {
- "description": "Open your inbox and enter the code that we've sent to you.",
- "next": "Continue",
- "nonce": "Enter your code",
- "validations": {
- "length": "must be 6 characters long"
- }
- }
- },
"password-reset": {
"change-password": {
"error": "Changing your password failed. Maybe the security code was not correct?",
@@ -157,33 +136,68 @@
},
"registration": {
"create-user-account": {
+ "buttonTitle": "Create",
"error": "No user account could be created!",
"help": " Maybe the confirmation was invalid? In case of problems, feel free to ask for help by sending us a mail to:",
+ "recieveCommunicationAsEmailsEtcConfirmed": "I also agree to receive e-mails and other forms of communication (e.g. push notifications).",
"success": "Your account has been created!",
+ "termsAndCondsEtcConfirmed": "I have read, understand and agree to the following:",
"title": "Create user account"
},
"email": {
+ "buttonTitle": {
+ "resend": "Resend e-mail",
+ "send": "Send e-mail",
+ "skipResend": "Skip resend"
+ },
"form": {
+ "sendEmailAgain": "Send e-mail again",
"success": "Verification e-mail send to {email}!"
- }
+ },
+ "title": "E-Mail"
+ },
+ "email-display": {
+ "warningFormat": "⚠️ E-mail has wrong format!",
+ "warningUndef": "⚠️ No e-mail defined!",
+ "yourEmail": "Your e-mail address:"
+ },
+ "email-nonce": {
+ "buttonTitle": "Confirm",
+ "form": {
+ "description": "Open your inbox and enter the code that we've sent to you.",
+ "next": "Continue",
+ "nonce": "E-mail code: 32143",
+ "validations": {
+ "error": "Invalid verification code {nonce} for e-mail {email}!",
+ "length": "must be 5 characters long",
+ "success": "Valid verification code {nonce} for e-mail {email}!"
+ }
+ },
+ "title": "E-Mail Confirmation"
},
"invite-code": {
+ "buttonTitle": "Next",
"form": {
- "success": "Valid invite code {inviteCode}!"
+ "description": "Enter the invitation code you received.",
+ "invite-code": "Invite code: ACJERB",
+ "next": "Continue",
+ "validations": {
+ "error": "Invalid invite code {inviteCode}!",
+ "length": "must be 6 characters long",
+ "success": "Valid invite code {inviteCode}!"
+ }
}
},
+ "no-public-registrstion": {
+ "title": "No Public Registration"
+ },
"signup": {
"form": {
"data-privacy": "I have read and understood the privacy statement.",
"description": "To get started, you can register here for free:",
"errors": {
- "email-exists": "There is already a user account with this e-mail address!",
- "invalid-invitation-token": "It looks like as if the invitation has been used already. Invitation links can only be used once."
+ "email-exists": "There is already a user account with this e-mail address!"
},
- "invitation-code": "Your invitation code is: {code}",
- "minimum-age": "I'm 18 years or older.",
- "no-commercial": "I have no commercial interests and I am not representing a company or any other commercial organisation on the network.",
- "no-political": "I am not on behalf of a party or political organization in the network.",
"submit": "Create an account",
"success": "A mail with a link to complete your registration has been sent to {email}",
"terms-and-condition": "I confirm to the Terms and conditions."
@@ -299,6 +313,20 @@
"addLetter": "Type a letter",
"noHashtagsFound": "No hashtags found"
},
+ "legend": {
+ "bold": "Bold",
+ "heading3": "Heading 3",
+ "heading4": "Heading 4",
+ "italic": "Italic",
+ "legendTitle": "Keyboard shortcuts and markdown code",
+ "link": "Link",
+ "orderedList": "Ordered list",
+ "paragraph": "Paragraph",
+ "quote": "Quote",
+ "ruler": "Ruler",
+ "underline": "Underline",
+ "unorderedList": "Unordered list"
+ },
"mention": {
"noUsersFound": "No users found"
},
@@ -336,6 +364,12 @@
"change-filter-settings": "Change your filter settings to get more results.",
"no-results": "No contributions found."
},
+ "invite-codes": {
+ "copy-code": "Code:",
+ "copy-success": "Invite code copied to clipboard",
+ "not-available": "You have no valid invite code available!",
+ "your-code": "Copy your invite code to the clipboard:"
+ },
"login": {
"email": "Your E-mail",
"failure": "Incorrect email address or password.",
@@ -464,14 +498,6 @@
"unpin": "Unpin post",
"unpinnedSuccessfully": "Post unpinned successfully!"
},
- "moreInfo": {
- "description": "Here you can find more information about this topic.",
- "name": "More info",
- "title": "More information",
- "titleOfCategoriesSection": "Categories",
- "titleOfHashtagsSection": "Hashtags",
- "titleOfRelatedContributionsSection": "Related posts"
- },
"name": "Post",
"pinned": "Announcement",
"takeAction": {
@@ -759,9 +785,10 @@
"faq": "FAQ",
"germany": "Germany",
"imprint": "Imprint",
- "made": "Made with ❤",
+ "made": "Made with ❤️",
"register": "Registry number",
"responsible": "responsible for contents of this page (§ 55 Abs. 2 RStV)",
+ "support": "Support",
"taxident": "USt-ID. according to §27a of the German Sales Tax Law:",
"termsAndConditions": "Terms and conditions",
"thanks": "Thanks!",
@@ -780,9 +807,7 @@
}
},
"termsAndConditions": {
- "agree": "I agree!",
"newTermsAndConditions": "New Terms and Conditions",
- "termsAndConditionsConfirmed": "I have read and confirmed the terms and conditions.",
"termsAndConditionsNewConfirm": "I have read and agree to the new terms of conditions.",
"termsAndConditionsNewConfirmText": "Please read the new terms of use now!"
},
diff --git a/webapp/locales/es.json b/webapp/locales/es.json
index 659fd1995..1ab09e011 100644
--- a/webapp/locales/es.json
+++ b/webapp/locales/es.json
@@ -98,7 +98,6 @@
"letsTalk": "Hablemos",
"loading": "cargando",
"loadMore": "cargar más",
- "moreInfo": "Más información",
"name": "Nombre",
"organization": "Organización ::: Organizaciones",
"post": "Mensaje ::: Mensajes",
@@ -116,16 +115,6 @@
"versus": "Versus"
},
"components": {
- "enter-nonce": {
- "form": {
- "description": "Abra su buzón de correo e introduzca el código que le enviamos.",
- "next": "Continuar",
- "nonce": "Introduzca el código",
- "validations": {
- "length": "debe tener exactamente 6 letras"
- }
- }
- },
"password-reset": {
"change-password": {
"error": "Error al cambiar la contraseña. ¿Posiblemente un código de seguridad incorrecto?",
@@ -148,18 +137,23 @@
"success": "¡Su cuenta de usuario ha sido creada!",
"title": "Crear una cuenta de usuario"
},
+ "email-nonce": {
+ "form": {
+ "description": "Abra su buzón de correo e introduzca el código que le enviamos.",
+ "next": "Continuar",
+ "nonce": "Introduzca el código",
+ "validations": {
+ "length": "debe tener exactamente 5 letras"
+ }
+ }
+ },
"signup": {
"form": {
"data-privacy": "He leido y entendido la declaración de protección de datos.",
"description": "Para empezar, introduzca su dirección de correo electrónico:",
"errors": {
- "email-exists": "¡Ya hay una cuenta de usuario con esta dirección de correo electrónico!",
- "invalid-invitation-token": "Parece que el código de invitación ya ha sido canjeado. Cada código sólo se puede utilizar una vez."
+ "email-exists": "¡Ya hay una cuenta de usuario con esta dirección de correo electrónico!"
},
- "invitation-code": "Su código de invitación es: {code}",
- "minimum-age": "Tengo 18 años o más.",
- "no-commercial": "No tengo intensiones comerciales y no represento una empresa u organización comercial.",
- "no-political": "No estoy en la red en nombre de un partido o una organización política.",
"submit": "Crear una cuenta",
"success": "Se ha enviado un correo electrónico con un enlace de confirmación para el registro a {email}.",
"terms-and-condition": "Estoy de acuerdo con los términos de uso."
@@ -425,14 +419,6 @@
"unpin": "Desanclar contribución",
"unpinnedSuccessfully": "¡Contribución desanclado con éxito!"
},
- "moreInfo": {
- "description": "Aquí puede encontrar más información sobre este tema.",
- "name": "Más info",
- "title": "Más información",
- "titleOfCategoriesSection": "Categorías",
- "titleOfHashtagsSection": "Hashtags",
- "titleOfRelatedContributionsSection": "Contribuciones relacionadas"
- },
"name": "Contribución",
"pinned": "Anuncio",
"takeAction": {
@@ -708,7 +694,7 @@
"faq": "Preguntas más frecuentes",
"germany": "Alemania",
"imprint": "Pie de imprenta",
- "made": "Hecho con ❤",
+ "made": "Hecho con ❤️",
"register": "Número de registro",
"responsible": "Responsable según § 55 Abs. 2 RStV (Alemania)",
"taxident": "Número de identificación del impuesto sobre el valor añadido según el § 27 a de la Ley del Impuesto sobre el Valor Añadido (Alemania)",
@@ -729,9 +715,7 @@
}
},
"termsAndConditions": {
- "agree": "¡Estoy de acuerdo!",
"newTermsAndConditions": "Nuevos términos de uso",
- "termsAndConditionsConfirmed": "He leído y acepto los términos de uso.",
"termsAndConditionsNewConfirm": "He leído y acepto los nuevos términos de uso.",
"termsAndConditionsNewConfirmText": "¡Por favor, lea los nuevos términos de uso ahora!"
},
diff --git a/webapp/locales/fr.json b/webapp/locales/fr.json
index d43007cf7..cb4e96bdc 100644
--- a/webapp/locales/fr.json
+++ b/webapp/locales/fr.json
@@ -98,7 +98,6 @@
"letsTalk": "Parlons-en",
"loading": "chargement",
"loadMore": "charger plus",
- "moreInfo": "Plus d'infos",
"name": "Nom",
"organization": "Organisation ::: Organisations",
"post": "Message ::: Messages",
@@ -116,16 +115,6 @@
"versus": "Versus"
},
"components": {
- "enter-nonce": {
- "form": {
- "description": "Ouvrez votre boîte de réception et entrez le code que nous vous avons envoyé.",
- "next": "Continuer",
- "nonce": "Entrez votre code",
- "validations": {
- "length": "doit comporter 6 caractères"
- }
- }
- },
"password-reset": {
"change-password": {
"error": "La modification de votre mot de passe a échoué. Peut-être que le code de sécurité n'était pas correct ?",
@@ -148,18 +137,23 @@
"success": "Votre compte a été créé!",
"title": "Créer un compte utilisateur"
},
+ "email-nonce": {
+ "form": {
+ "description": "Ouvrez votre boîte de réception et entrez le code que nous vous avons envoyé.",
+ "next": "Continuer",
+ "nonce": "Entrez votre code",
+ "validations": {
+ "length": "doit comporter 5 caractères"
+ }
+ }
+ },
"signup": {
"form": {
"data-privacy": "J'ai lu et compris la Déclaration de confidentialité.",
"description": "Pour commencer, entrez votre adresse mail :",
"errors": {
- "email-exists": "Il existe déjà un compte utilisateur avec cette adresse mail!",
- "invalid-invitation-token": "On dirait que l'invitation a déjà été utilisée. Les liens d'invitation ne peuvent être utilisés qu'une seule fois."
+ "email-exists": "Il existe déjà un compte utilisateur avec cette adresse mail!"
},
- "invitation-code": "Votre code d'invitation est: {code} ",
- "minimum-age": "J'ai 18 ans ou plus.",
- "no-commercial": "Je n'ai aucun intérêt commercial et je ne représente pas d'entreprise ou toute autre organisation commerciale sur le réseau.",
- "no-political": "Je ne parle pas au nom d'un parti ou d'une organisation politique sur le réseau.",
"submit": "Créer un compte",
"success": "Un mail avec un lien pour compléter votre inscription a été envoyé à {email}",
"terms-and-condition": "Je confirme les Conditions générales."
@@ -413,14 +407,6 @@
"unpin": "Retirer l'épingle du poste",
"unpinnedSuccessfully": "Épingle retirer du Post avec succès!"
},
- "moreInfo": {
- "description": "Ici vous pouvez trouver plus d'informations à ce sujet.",
- "name": "Plus d'infos",
- "title": "Plus d'information",
- "titleOfCategoriesSection": "Catégories",
- "titleOfHashtagsSection": "Hashtags",
- "titleOfRelatedContributionsSection": "Articles Similaires"
- },
"name": "Post",
"pinned": "Annonce",
"takeAction": {
@@ -676,7 +662,7 @@
"faq": "FAQ",
"germany": "Allemagne",
"imprint": "Mentions légales",
- "made": "Fabriqué avec ❤",
+ "made": "Fabriqué avec ❤️",
"register": "Numéro de registre",
"responsible": "Responsable selon § 55 Abs. 2 RStV (Allemagne)",
"taxident": "Numéro d'identification à la taxe sur la valeur ajoutée selon § 27 a de la loi sur la taxe sur la valeur ajoutée (Allemagne)",
@@ -697,9 +683,7 @@
}
},
"termsAndConditions": {
- "agree": "J'accepte!",
"newTermsAndConditions": "Nouvelles conditions générales",
- "termsAndConditionsConfirmed": "J'ai lu et accepte les conditions générales.",
"termsAndConditionsNewConfirm": "J'ai lu et accepté les nouvelles conditions générales.",
"termsAndConditionsNewConfirmText": "Veuillez lire les nouvelles conditions d'utilisation dès maintenant !"
},
diff --git a/webapp/locales/html/de/code-of-conduct.html b/webapp/locales/html/de/code-of-conduct.html
index 2ab747555..96ac29482 100644
--- a/webapp/locales/html/de/code-of-conduct.html
+++ b/webapp/locales/html/de/code-of-conduct.html
@@ -1 +1,5 @@
-