diff --git a/.github/workflows/docker-push.yml b/.github/workflows/docker-push.yml index 406d8304b..cc84c6e3e 100644 --- a/.github/workflows/docker-push.yml +++ b/.github/workflows/docker-push.yml @@ -81,7 +81,7 @@ jobs: type=sha - name: Build and push Docker images id: push - uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 + uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 with: context: ${{ matrix.app.context }} target: ${{ matrix.app.target }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b66413f22..425da269b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -64,7 +64,7 @@ jobs: echo "BUILD_COMMIT=${GITHUB_SHA}" >> $GITHUB_ENV - run: echo "BUILD_VERSION=${VERSION}-${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV #- name: Repository Dispatch - # uses: peter-evans/repository-dispatch@7d980a9b9f8ecf8955ea90507b3ed89122f53215 # v3.0.0 + # uses: peter-evans/repository-dispatch@44966f0098fd4ab26380bb099e1edf6d57eb2c90 # v3.0.0 # with: # token: ${{ github.token }} # event-type: trigger-ocelot-build-success @@ -72,7 +72,7 @@ jobs: # client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}", "VERSION": "${VERSION}", "BUILD_DATE": "${BUILD_DATE}", "BUILD_COMMIT": "${BUILD_COMMIT}", "BUILD_VERSION": "${BUILD_VERSION}"}' - name: Repository Dispatch stage.ocelot.social - uses: peter-evans/repository-dispatch@7d980a9b9f8ecf8955ea90507b3ed89122f53215 # v3.0.0 + uses: peter-evans/repository-dispatch@44966f0098fd4ab26380bb099e1edf6d57eb2c90 # v3.0.0 with: token: ${{ secrets.OCELOT_PUBLISH_EVENT_PAT }} # this token is required to access the other repository event-type: trigger-ocelot-build-success @@ -80,7 +80,7 @@ jobs: client-payload: '{"ref": "${{ github.ref }}", "sha": "${{ github.sha }}", "GITHUB_RUN_NUMBER": "${{ env.GITHUB_RUN_NUMBER }}", "VERSION": "${VERSION}", "BUILD_DATE": "${BUILD_DATE}", "BUILD_COMMIT": "${BUILD_COMMIT}", "BUILD_VERSION": "${BUILD_VERSION}"}' - name: Repository Dispatch stage.yunite.me - uses: peter-evans/repository-dispatch@7d980a9b9f8ecf8955ea90507b3ed89122f53215 # v3.0.0 + uses: peter-evans/repository-dispatch@44966f0098fd4ab26380bb099e1edf6d57eb2c90 # v3.0.0 with: token: ${{ secrets.OCELOT_PUBLISH_EVENT_PAT }} # this token is required to access the other repository event-type: trigger-ocelot-build-success diff --git a/CHANGELOG.md b/CHANGELOG.md index 361c5df32..e3b1a955a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,80 @@ 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). +#### [3.4.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.3.0...3.4.0) + +- fix(webapp): fix badge focus [`#8452`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8452) +- feat(backend): branding middlewares [`#8429`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8429) +- refactor(webapp): make login, registration, password-reset layout brandable [`#8440`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8440) +- fix(backend): fixes for branding [`#8449`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8449) +- Replace edit link by pencil button [`#8453`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8453) +- fix(webapp): refine little things [`#8382`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8382) +- fix(webapp): fix admin badges settings [`#8438`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8438) +- build(deps): bump peter-evans/repository-dispatch [`#8443`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8443) +- build(deps-dev): bump nodemon from 3.1.9 to 3.1.10 in /backend [`#8447`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8447) +- build(deps-dev): bump @types/node from 22.14.1 to 22.15.2 in /backend [`#8446`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8446) +- build(deps): bump docker/build-push-action from 6.15.0 to 6.16.0 [`#8444`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8444) +- build(deps-dev): bump cypress from 14.3.1 to 14.3.2 in the cypress group [`#8442`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8442) +- build(deps-dev): bump eslint-import-resolver-typescript in /backend [`#8445`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8445) +- build(deps-dev): bump eslint-config-prettier in /backend [`#8370`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8370) +- revokeBadge also removes selection [`#8437`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8437) +- feat(webapp): badges UI [`#8426`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8426) +- feat(backend): lint - detect unused typescript disables [`#8425`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8425) +- fix(docu): remove required but missing `frontend/.env` [`#8431`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8431) +- refactor(backend): types for neo4j & neode [`#8409`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8409) +- lint everything, disable some setup steps for jest [`#8423`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8423) +- lint n/no-sync [`#8405`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8405) +- fix(backend): fix notification emails with different name [`#8419`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8419) +- refactor(backend): default badges, always return a badge [`#8430`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8430) +- refactor(backend): allow to set selected badge-slot to null [`#8421`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8421) +- chore(frontend): run npm install [`#8432`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8432) +- refactor(webapp): refactor branding diverse v2 [`#8427`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8427) +- feat(webapp): badges admin settings [`#8401`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8401) +- move graphql types into graphql folder [`#8420`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8420) +- fix faker image seed [`#8422`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8422) +- build(deps-dev): bump @faker-js/faker from 9.6.0 to 9.7.0 in /webapp [`#8411`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8411) +- build(deps-dev): bump @faker-js/faker from 9.6.0 to 9.7.0 [`#8414`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8414) +- build(deps): bump sanitize-html from 2.15.0 to 2.16.0 in /backend [`#8418`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8418) +- build(deps-dev): bump cypress from 14.3.0 to 14.3.1 in the cypress group [`#8413`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8413) +- build(deps): bump actions/setup-node from 4.3.0 to 4.4.0 [`#8412`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8412) +- refactor(backend): separate queries [`#8358`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8358) +- refactor(backend): lint @typescript-eslint/strict [`#8408`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8408) +- refactor(backend): lint @typescript-eslint/recommended-requiring-type-checking [`#8407`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8407) +- lint @typescript-eslint/recommended [`#8406`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8406) +- build(deps): bump nodemailer from 6.10.0 to 6.10.1 in /backend [`#8417`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8417) +- build(deps-dev): bump @eslint-community/eslint-plugin-eslint-comments [`#8415`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8415) +- feat(backend): badges [`#8391`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8391) +- feat(backend): do not notify blocked or muted users [`#8403`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8403) +- feat(backend): only one email is sent although more notifications are triggered [`#8400`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8400) +- fix(backend): flaky notifications on mention in group unit test [`#8404`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8404) +- refactor(webapp): refactor branding of post ribbons and chat etc. [`#8395`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8395) +- downgrade sass to 1.77.6 [`#8399`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8399) +- mentiioned users in posts and comments of groups [`#8392`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8392) +- feat(backend): no notification mails to users online [`#8397`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8397) +- Add .nuxt to gitignore [`#8393`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8393) +- fix migrations [`#8390`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8390) +- chore(frontend): add '.nvmrc' file to new frontend [`#7112`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/7112) +- refactor(backend): fix is muted by me query [`#8365`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8365) +- fix(backend): block/mute chat [`#8364`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8364) +- build(deps): bump graphql-upload from 11.0.0 to 13.0.0 in /backend [`#8375`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8375) +- build(deps-dev): bump the typescript group across 1 directory with 2 updates [`#8383`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8383) +- Bump graphql from 14.7.0 to 15.10.1 in /webapp [`#8157`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8157) +- fix(webapp): better settings ux [`#8347`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8347) +- Bump bcryptjs from 2.4.3 to 3.0.2 [`#8218`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8218) +- Bump bcryptjs from 2.4.3 to 3.0.2 in /backend [`#8224`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8224) +- build(deps-dev): bump cypress from 14.2.1 to 14.3.0 in the cypress group [`#8366`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8366) +- build(deps-dev): bump eslint-import-resolver-typescript in /backend [`#8369`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8369) +- build(deps-dev): bump dotenv from 16.4.7 to 16.5.0 [`#8367`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8367) +- build(deps): bump ioredis from 4.16.1 to 5.6.1 in /backend [`#8371`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8371) +- build(deps): bump dotenv from 16.4.7 to 16.5.0 in /backend [`#8372`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8372) +- build(deps-dev): bump eslint-config-prettier in /webapp [`#8377`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8377) +- build(deps-dev): bump @types/node from 22.14.0 to 22.14.1 in /backend [`#8374`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8374) + #### [3.3.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.2.1...3.3.0) +> 12 April 2025 + +- v3.3.0 [`#8380`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8380) - fix(webapp): refine group muting locales [`#8378`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8378) - chore(backend): add e-mail setting for our new 'mailserver' to our backend .env.template [`#8359`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8359) - refactor(backend): set up smtp pooling for nodemailer [`#8167`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8167) diff --git a/README.md b/README.md index b1fe0ea14..910eae5a4 100644 --- a/README.md +++ b/README.md @@ -187,10 +187,6 @@ $ cp .env.template .env # in folder backend/ $ cp .env.template .env -# in folder frontend/ -$ cp .env.template .env -``` - For Development: ```bash diff --git a/backend/.eslintignore b/backend/.eslintignore new file mode 100644 index 000000000..e19e2338d --- /dev/null +++ b/backend/.eslintignore @@ -0,0 +1,3 @@ +node_modules/ +build/ +coverage/ \ No newline at end of file diff --git a/backend/.eslintrc.cjs b/backend/.eslintrc.cjs index 51adb6831..1fe6b8779 100644 --- a/backend/.eslintrc.cjs +++ b/backend/.eslintrc.cjs @@ -115,7 +115,7 @@ module.exports = { 'n/no-callback-literal': 'error', // 'n/no-deprecated-api': 'error', // part of n/recommended // 'n/no-exports-assign': 'error', // part of n/recommended - 'n/no-extraneous-import': 'off', // TODO // part of n/recommended + 'n/no-extraneous-import': 'off', // duplicate of import/no-extraneous-dependencies // part of n/recommended // 'n/no-extraneous-require': 'error', // part of n/recommended 'n/no-hide-core-modules': 'error', 'n/no-missing-import': 'off', // not compatible with typescript // part of n/recommended @@ -127,7 +127,7 @@ module.exports = { // 'n/no-process-exit': 'error', // part of n/recommended 'n/no-restricted-import': 'error', 'n/no-restricted-require': 'error', - // 'n/no-sync': 'error', + 'n/no-sync': 'error', // 'n/no-unpublished-bin': 'error', // part of n/recommended 'n/no-unpublished-import': [ 'error', diff --git a/backend/Dockerfile b/backend/Dockerfile index e1c244069..f89fa2d3d 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -23,12 +23,14 @@ COPY . . ONBUILD COPY ./branding/constants/ src/config/tmp ONBUILD RUN tools/replace-constants.sh ONBUILD COPY ./branding/email/ src/middleware/helpers/email/ +ONBUILD COPY ./branding/middlewares/ src/middleware/branding/ ONBUILD COPY ./branding/data/ src/db/data +ONBUILD COPY ./branding/public/ public/ ONBUILD RUN yarn install --production=false --frozen-lockfile --non-interactive ONBUILD RUN yarn run build ONBUILD RUN mkdir /build ONBUILD RUN cp -r ./build /build -ONBUILD RUN cp -r ./public /build/build +ONBUILD RUN cp -r ./public /build ONBUILD RUN cp -r ./package.json yarn.lock /build ONBUILD RUN cd /build && yarn install --production=true --frozen-lockfile --non-interactive diff --git a/backend/branding/middlewares/.gitkeep b/backend/branding/middlewares/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/backend/branding/public/.gitkeep b/backend/branding/public/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/backend/package.json b/backend/package.json index 60ecba12e..6d12190ed 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "ocelot-social-backend", - "version": "3.3.0", + "version": "3.4.0", "description": "GraphQL Backend for ocelot.social", "repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social", "author": "ocelot.social Community", @@ -12,18 +12,19 @@ "build": "tsc && tsc-alias && ./scripts/build.copy.files.sh", "dev": "nodemon --exec ts-node --require tsconfig-paths/register src/ -e js,ts,gql", "dev:debug": "nodemon --exec babel-node --inspect=0.0.0.0:9229 src/ -e js,ts,gql", - "lint": "eslint --max-warnings=0 --ext .js,.ts ./src", + "lint": "eslint --max-warnings=0 --report-unused-disable-directives --ext .js,.ts .", "test": "cross-env NODE_ENV=test NODE_OPTIONS=--max-old-space-size=8192 jest --runInBand --coverage --forceExit --detectOpenHandles", "db:reset": "ts-node --require tsconfig-paths/register src/db/reset.ts", "db:reset:withmigrations": "ts-node --require tsconfig-paths/register src/db/reset-with-migrations.ts", "db:seed": "ts-node --require tsconfig-paths/register src/db/seed.ts", "db:data:admin": "ts-node --require tsconfig-paths/register src/db/admin.ts", "db:data:badges": "ts-node --require tsconfig-paths/register src/db/badges.ts", - "db:data:branding": "ts-node --require tsconfig-paths/register src/db/data-production.ts", + "db:data:branding": "ts-node --require tsconfig-paths/register src/db/data-branding.ts", "db:data:categories": "ts-node --require tsconfig-paths/register src/db/categories.ts", "db:migrate": "migrate --compiler 'ts:./src/db/compiler.ts' --migrations-dir ./src/db/migrations --store ./src/db/migrate/store.ts", "db:migrate:create": "migrate --compiler 'ts:./src/db/compiler.ts' --migrations-dir ./src/db/migrations --template-file ./src/db/migrate/template.ts --date-format 'yyyymmddHHmmss' create", - "prod:migrate": "migrate --migrations-dir ./build/src/db/migrations --store ./build/src/db/migrate/store.js" + "prod:migrate": "migrate --migrations-dir ./build/src/db/migrations --store ./build/src/db/migrate/store.js", + "prod:db:data:branding": "node build/src/db/data-branding.js" }, "dependencies": { "@babel/cli": "~7.27.0", @@ -104,14 +105,16 @@ "@eslint-community/eslint-plugin-eslint-comments": "^4.5.0", "@faker-js/faker": "9.7.0", "@types/jest": "^29.5.14", - "@types/node": "^22.14.1", + "@types/lodash": "^4.17.16", + "@types/node": "^22.15.2", + "@types/uuid": "~9.0.1", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0", "apollo-server-testing": "~2.11.0", "eslint": "^8.57.1", - "eslint-config-prettier": "^10.1.1", + "eslint-config-prettier": "^10.1.2", "eslint-config-standard": "^17.1.0", - "eslint-import-resolver-typescript": "^4.3.2", + "eslint-import-resolver-typescript": "^4.3.4", "eslint-plugin-import": "^2.31.0", "eslint-plugin-jest": "^28.11.0", "eslint-plugin-n": "^17.17.0", @@ -120,7 +123,7 @@ "eslint-plugin-promise": "^7.2.1", "eslint-plugin-security": "^3.0.1", "jest": "^29.7.0", - "nodemon": "~3.1.9", + "nodemon": "~3.1.10", "prettier": "^3.5.3", "require-json5": "^1.3.0", "rosie": "^2.1.1", diff --git a/backend/public/img/badges/default_trophy.svg b/backend/public/img/badges/default_trophy.svg new file mode 100644 index 000000000..b203cdfc6 --- /dev/null +++ b/backend/public/img/badges/default_trophy.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/backend/public/img/badges/default_verification.svg b/backend/public/img/badges/default_verification.svg new file mode 100644 index 000000000..7bde29f35 --- /dev/null +++ b/backend/public/img/badges/default_verification.svg @@ -0,0 +1,28 @@ + + + + + + + + + + diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index cf07297df..9b82299ae 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -1,35 +1,19 @@ -/* eslint-disable @typescript-eslint/require-await */ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable security/detect-non-literal-fs-filename */ /* eslint-disable n/no-process-env */ -/* eslint-disable n/no-unpublished-require */ -/* eslint-disable n/no-missing-require */ import { config } from 'dotenv' import emails from './emails' import metadata from './metadata' // Load env file -if (require.resolve) { - try { - config({ path: require.resolve('../../.env') }) - } catch (error) { - // This error is thrown when the .env is not found - if (error.code !== 'MODULE_NOT_FOUND') { - throw error - } - } -} +config() // Use Cypress env or process.env // eslint-disable-next-line @typescript-eslint/no-explicit-any declare let Cypress: any | undefined -const env = typeof Cypress !== 'undefined' ? Cypress.env() : process.env // eslint-disable-line no-undef +const env = typeof Cypress !== 'undefined' ? Cypress.env() : process.env const environment = { NODE_ENV: env.NODE_ENV || process.env.NODE_ENV, @@ -38,7 +22,9 @@ const environment = { PRODUCTION: env.NODE_ENV === 'production', // used for staging enviroments if 'PRODUCTION=true' and 'PRODUCTION_DB_CLEAN_ALLOW=true' PRODUCTION_DB_CLEAN_ALLOW: env.PRODUCTION_DB_CLEAN_ALLOW === 'true' || false, // default = false - DISABLED_MIDDLEWARES: (env.NODE_ENV !== 'production' && env.DISABLED_MIDDLEWARES) || false, + DISABLED_MIDDLEWARES: ['test', 'development'].includes(env.NODE_ENV as string) + ? (env.DISABLED_MIDDLEWARES?.split(',') ?? []) + : [], } const required = { diff --git a/backend/src/db/admin.ts b/backend/src/db/admin.ts index ae6bca49e..1f62c8733 100644 --- a/backend/src/db/admin.ts +++ b/backend/src/db/admin.ts @@ -1,9 +1,6 @@ /* eslint-disable @typescript-eslint/no-floating-promises */ /* eslint-disable @typescript-eslint/require-await */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ + import { hashSync } from 'bcryptjs' import { v4 as uuid } from 'uuid' @@ -11,6 +8,7 @@ import { getDriver } from './neo4j' const defaultAdmin = { email: 'admin@example.org', + // eslint-disable-next-line n/no-sync password: hashSync('1234', 10), name: 'admin', id: uuid(), diff --git a/backend/src/db/badges.ts b/backend/src/db/badges.ts index b4e879357..cbad0b004 100644 --- a/backend/src/db/badges.ts +++ b/backend/src/db/badges.ts @@ -1,6 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-floating-promises */ import { getNeode } from './neo4j' import { trophies, verification } from './seed/badges' @@ -12,6 +9,6 @@ import { trophies, verification } from './seed/badges' await trophies() await verification() } finally { - await neode.close() + neode.close() } })() diff --git a/backend/src/db/categories.ts b/backend/src/db/categories.ts index b3774e7b9..a007b25ae 100644 --- a/backend/src/db/categories.ts +++ b/backend/src/db/categories.ts @@ -1,9 +1,7 @@ /* eslint-disable @typescript-eslint/no-floating-promises */ /* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable @typescript-eslint/require-await */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + import { categories } from '@constants/categories' import { getDriver } from './neo4j' diff --git a/backend/src/db/compiler.ts b/backend/src/db/compiler.ts index 9c2140f2a..1b364f919 100644 --- a/backend/src/db/compiler.ts +++ b/backend/src/db/compiler.ts @@ -3,7 +3,7 @@ /* eslint-disable import/no-commonjs */ // eslint-disable-next-line n/no-unpublished-require, @typescript-eslint/no-var-requires const tsNode = require('ts-node') -// eslint-disable-next-line import/no-unassigned-import, import/no-extraneous-dependencies, n/no-unpublished-require +// eslint-disable-next-line import/no-unassigned-import, n/no-unpublished-require require('tsconfig-paths/register') module.exports = tsNode.register diff --git a/backend/src/db/data-branding.ts b/backend/src/db/data-branding.ts index e9af41840..eceaf391b 100644 --- a/backend/src/db/data-branding.ts +++ b/backend/src/db/data-branding.ts @@ -1,16 +1,18 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-misused-promises */ /* eslint-disable @typescript-eslint/no-floating-promises */ import { readdir } from 'node:fs/promises' import path from 'node:path' +import { getNeode } from './neo4j' + const dataFolder = path.join(__dirname, 'data/') +const neode = getNeode() ;(async function () { const files = await readdir(dataFolder) - files.forEach(async (file) => { + for await (const file of files) { if (file.slice(0, -3).endsWith('-branding')) { const importedModule = await import(path.join(dataFolder, file)) if (!importedModule.default) { @@ -18,5 +20,8 @@ const dataFolder = path.join(__dirname, 'data/') } await importedModule.default() } - }) + } + + // close database connection + neode.close() })() diff --git a/backend/src/db/factories.ts b/backend/src/db/factories.ts index a0230a467..90a666205 100644 --- a/backend/src/db/factories.ts +++ b/backend/src/db/factories.ts @@ -1,11 +1,9 @@ -/* eslint-disable @typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/unbound-method */ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ import { faker } from '@faker-js/faker' import { hashSync } from 'bcryptjs' import { Factory } from 'rosie' @@ -39,7 +37,7 @@ export const cleanDatabase = async ({ withMigrations } = { withMigrations: false return transaction.run(clean) }) } finally { - session.close() + await session.close() } } @@ -95,6 +93,7 @@ Factory.define('basicUser') return slug || slugify(name, { lower: true }) }) .attr('encryptedPassword', ['password'], (password) => { + // eslint-disable-next-line n/no-sync return hashSync(password, 10) }) diff --git a/backend/src/db/migrate/store.ts b/backend/src/db/migrate/store.ts index 947364e4d..9976be8b4 100644 --- a/backend/src/db/migrate/store.ts +++ b/backend/src/db/migrate/store.ts @@ -1,17 +1,13 @@ -/* eslint-disable @typescript-eslint/require-await */ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable security/detect-non-literal-fs-filename */ import { getDriver, getNeode } from '@db/neo4j' class Store { async init(errFn) { const neode = getNeode() - const session = neode.driver.session() + const session = neode.session() const txFreshIndicesConstrains = session.writeTransaction(async (txc) => { // drop all indices and constraints await txc.run('CALL apoc.schema.assert({},{},true)') @@ -38,6 +34,9 @@ class Store { // we need to have all constraints and indexes defined here. They can not be properly migrated await txFreshIndicesConstrains + // You have to wait for the schema to install, else the constraints will not be present. + // This is a type error of the library + // eslint-disable-next-line @typescript-eslint/await-thenable await getNeode().schema.install() // eslint-disable-next-line no-console console.log('Successfully created database indices and constraints!') @@ -46,8 +45,8 @@ class Store { console.log(error) // eslint-disable-line no-console errFn(error) } finally { - session.close() - neode.driver.close() + await session.close() + neode.close() } } @@ -76,7 +75,7 @@ class Store { console.log(error) // eslint-disable-line no-console next(error) } finally { - session.close() + await session.close() } } @@ -112,7 +111,7 @@ class Store { console.log(error) // eslint-disable-line no-console next(error) } finally { - session.close() + await session.close() } } } diff --git a/backend/src/db/migrate/template.ts b/backend/src/db/migrate/template.ts index ce538f260..a7287dd42 100644 --- a/backend/src/db/migrate/template.ts +++ b/backend/src/db/migrate/template.ts @@ -1,11 +1,5 @@ -/* eslint-disable @typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable security/detect-non-literal-fs-filename */ + import { getDriver } from '@db/neo4j' export const description = '' @@ -27,7 +21,7 @@ export async function up(_next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } @@ -48,6 +42,6 @@ export async function down(_next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations-examples/20200123150105-merge_duplicate_user_accounts.ts b/backend/src/db/migrations-examples/20200123150105-merge_duplicate_user_accounts.ts index d13eeecf9..eda3206b4 100644 --- a/backend/src/db/migrations-examples/20200123150105-merge_duplicate_user_accounts.ts +++ b/backend/src/db/migrations-examples/20200123150105-merge_duplicate_user_accounts.ts @@ -1,11 +1,9 @@ -/* eslint-disable @typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable security/detect-non-literal-fs-filename */ /* eslint-disable import/no-extraneous-dependencies */ /* eslint-disable promise/prefer-await-to-callbacks */ import { throwError, concat } from 'rxjs' @@ -25,7 +23,8 @@ export const description = ` ` export function up(next) { const driver = getDriver() - const rxSession = driver.rxSession() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const rxSession = driver.rxSession() as any rxSession .beginTransaction() .pipe( diff --git a/backend/src/db/migrations-examples/20200123150110-merge_duplicate_location_nodes.ts b/backend/src/db/migrations-examples/20200123150110-merge_duplicate_location_nodes.ts index 249464257..de73bdaae 100644 --- a/backend/src/db/migrations-examples/20200123150110-merge_duplicate_location_nodes.ts +++ b/backend/src/db/migrations-examples/20200123150110-merge_duplicate_location_nodes.ts @@ -1,11 +1,9 @@ -/* eslint-disable @typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable security/detect-non-literal-fs-filename */ /* eslint-disable import/no-extraneous-dependencies */ /* eslint-disable promise/prefer-await-to-callbacks */ import { throwError, concat } from 'rxjs' @@ -19,7 +17,8 @@ export const description = ` ` export function up(next) { const driver = getDriver() - const rxSession = driver.rxSession() + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const rxSession = driver.rxSession() as any rxSession .beginTransaction() .pipe( diff --git a/backend/src/db/migrations-examples/20200127110135-create_muted_relationship_between_existing_blocked_relationships.ts b/backend/src/db/migrations-examples/20200127110135-create_muted_relationship_between_existing_blocked_relationships.ts index cfc00fcfe..8be7bad07 100644 --- a/backend/src/db/migrations-examples/20200127110135-create_muted_relationship_between_existing_blocked_relationships.ts +++ b/backend/src/db/migrations-examples/20200127110135-create_muted_relationship_between_existing_blocked_relationships.ts @@ -1,11 +1,5 @@ -/* eslint-disable @typescript-eslint/require-await */ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable security/detect-non-literal-fs-filename */ + import { getDriver } from '@db/neo4j' export const description = ` @@ -37,20 +31,20 @@ export async function up(_next) { // eslint-disable-next-line no-console console.log('rolled back') } finally { - session.close() + await session.close() } } -export function down(next) { +export async function down(next) { const driver = getDriver() const session = driver.session() try { // Rollback your migration here. - next() + // next() // eslint-disable-next-line no-catch-all/no-catch-all } catch (err) { next(err) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations-examples/20200206190233-swap_latitude_with_longitude.ts b/backend/src/db/migrations-examples/20200206190233-swap_latitude_with_longitude.ts index a1eab53fa..f63be216d 100644 --- a/backend/src/db/migrations-examples/20200206190233-swap_latitude_with_longitude.ts +++ b/backend/src/db/migrations-examples/20200206190233-swap_latitude_with_longitude.ts @@ -1,12 +1,8 @@ /* eslint-disable @typescript-eslint/no-floating-promises */ /* eslint-disable @typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable security/detect-non-literal-fs-filename */ + import { getDriver } from '@db/neo4j' export const description = ` diff --git a/backend/src/db/migrations-examples/20200207080200-fulltext_index_for_tags.ts b/backend/src/db/migrations-examples/20200207080200-fulltext_index_for_tags.ts index f7bcb0810..f2e32d6f8 100644 --- a/backend/src/db/migrations-examples/20200207080200-fulltext_index_for_tags.ts +++ b/backend/src/db/migrations-examples/20200207080200-fulltext_index_for_tags.ts @@ -1,11 +1,7 @@ -/* eslint-disable @typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable security/detect-non-literal-fs-filename */ import { getDriver } from '@db/neo4j' export const description = @@ -39,7 +35,7 @@ export async function up(next) { throw new Error(error) } } finally { - session.close() + await session.close() } } @@ -66,6 +62,6 @@ export async function down(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations-examples/20200213230248-add_unique_index_to_image_url.ts b/backend/src/db/migrations-examples/20200213230248-add_unique_index_to_image_url.ts index a22a38127..81d96f68c 100644 --- a/backend/src/db/migrations-examples/20200213230248-add_unique_index_to_image_url.ts +++ b/backend/src/db/migrations-examples/20200213230248-add_unique_index_to_image_url.ts @@ -1,11 +1,7 @@ -/* eslint-disable @typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable security/detect-non-literal-fs-filename */ import { getDriver } from '@db/neo4j' export const description = ` @@ -40,7 +36,7 @@ export async function up(next) { throw new Error(error) } } finally { - session.close() + await session.close() } } @@ -64,6 +60,6 @@ export async function down(next) { // eslint-disable-next-line no-console console.log('rolled back') } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations-examples/20200312140328-bulk_upload_to_s3.ts b/backend/src/db/migrations-examples/20200312140328-bulk_upload_to_s3.ts index 9d97aab7e..0307a2e6e 100644 --- a/backend/src/db/migrations-examples/20200312140328-bulk_upload_to_s3.ts +++ b/backend/src/db/migrations-examples/20200312140328-bulk_upload_to_s3.ts @@ -1,6 +1,4 @@ -/* eslint-disable @typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ @@ -55,6 +53,7 @@ export async function up(next) { const { pathname } = new URL(url, 'http://example.org') const fileLocation = path.join(__dirname, `../../../public/${pathname}`) const s3Location = `original${pathname}` + // eslint-disable-next-line n/no-sync if (existsSync(fileLocation)) { const mimeType = mime.lookup(fileLocation) const params = { @@ -91,7 +90,7 @@ export async function up(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } @@ -113,6 +112,6 @@ export async function down(next) { // eslint-disable-next-line no-console console.log('rolled back') } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations-examples/20200320200315-refactor_all_images_to_separate_type.ts b/backend/src/db/migrations-examples/20200320200315-refactor_all_images_to_separate_type.ts index 61be45099..1d24bd141 100644 --- a/backend/src/db/migrations-examples/20200320200315-refactor_all_images_to_separate_type.ts +++ b/backend/src/db/migrations-examples/20200320200315-refactor_all_images_to_separate_type.ts @@ -1,11 +1,6 @@ -/* eslint-disable @typescript-eslint/require-await */ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable security/detect-non-literal-fs-filename */ + /* eslint-disable no-console */ import { getDriver } from '@db/neo4j' @@ -66,7 +61,7 @@ export async function up() { console.log('Created image nodes from all user avatars and post images.') printSummaries(stats) } finally { - session.close() + await session.close() } } @@ -104,6 +99,6 @@ export async function down() { console.log('UNDO: Split images from users and posts.') printSummaries(stats) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations-examples/20200323140300-remove_deleted_users_obsolete_attributes.ts b/backend/src/db/migrations-examples/20200323140300-remove_deleted_users_obsolete_attributes.ts index 36b29f477..b75324a78 100644 --- a/backend/src/db/migrations-examples/20200323140300-remove_deleted_users_obsolete_attributes.ts +++ b/backend/src/db/migrations-examples/20200323140300-remove_deleted_users_obsolete_attributes.ts @@ -1,11 +1,8 @@ /* eslint-disable @typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable security/detect-non-literal-fs-filename */ + import { getDriver } from '@db/neo4j' export const description = @@ -28,7 +25,7 @@ export async function up(next) { `) try { // Implement your migration here. - const users = await updateDeletedUserAttributes.records.map((record) => record.get('user')) + const users = updateDeletedUserAttributes.records.map((record) => record.get('user')) // eslint-disable-next-line no-console console.log(users) await transaction.commit() @@ -41,7 +38,7 @@ export async function up(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations-examples/20200323160336-remove_deleted_posts_obsolete_attributes.ts b/backend/src/db/migrations-examples/20200323160336-remove_deleted_posts_obsolete_attributes.ts index 1a3e97dba..597eb1d83 100644 --- a/backend/src/db/migrations-examples/20200323160336-remove_deleted_posts_obsolete_attributes.ts +++ b/backend/src/db/migrations-examples/20200323160336-remove_deleted_posts_obsolete_attributes.ts @@ -1,11 +1,8 @@ /* eslint-disable @typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable security/detect-non-literal-fs-filename */ + import { getDriver } from '@db/neo4j' export const description = @@ -30,7 +27,7 @@ export async function up(next) { `) try { // Implement your migration here. - const posts = await updateDeletedPostsAttributes.records.map((record) => record.get('post')) + const posts = updateDeletedPostsAttributes.records.map((record) => record.get('post')) // eslint-disable-next-line no-console console.log(posts) await transaction.commit() @@ -43,7 +40,7 @@ export async function up(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations-examples/20200326160326-remove_dangling_image_urls.ts b/backend/src/db/migrations-examples/20200326160326-remove_dangling_image_urls.ts index f5fbd24de..1109ac623 100644 --- a/backend/src/db/migrations-examples/20200326160326-remove_dangling_image_urls.ts +++ b/backend/src/db/migrations-examples/20200326160326-remove_dangling_image_urls.ts @@ -3,8 +3,7 @@ /* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + /* eslint-disable security/detect-non-literal-fs-filename */ import { existsSync } from 'node:fs' @@ -33,6 +32,7 @@ export async function up(next) { const urls = records.map((record) => record.get('url')) const danglingUrls = urls.filter((url) => { const fileLocation = `public${url}` + // eslint-disable-next-line n/no-sync return !existsSync(fileLocation) }) await transaction.run( @@ -61,7 +61,7 @@ export async function up(next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/1614023644903-add-clickedCount-to-posts.ts b/backend/src/db/migrations/1614023644903-add-clickedCount-to-posts.ts index 9bb7ab996..7443b4749 100644 --- a/backend/src/db/migrations/1614023644903-add-clickedCount-to-posts.ts +++ b/backend/src/db/migrations/1614023644903-add-clickedCount-to-posts.ts @@ -1,7 +1,5 @@ /* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + import { getDriver } from '@db/neo4j' export const description = ` @@ -27,7 +25,7 @@ export async function up(_next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } @@ -50,6 +48,6 @@ export async function down(_next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/1614177130817-add-viewedTeaserCount-to-posts.ts b/backend/src/db/migrations/1614177130817-add-viewedTeaserCount-to-posts.ts index 9ebfee85c..b23bf96bf 100644 --- a/backend/src/db/migrations/1614177130817-add-viewedTeaserCount-to-posts.ts +++ b/backend/src/db/migrations/1614177130817-add-viewedTeaserCount-to-posts.ts @@ -1,7 +1,5 @@ /* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + import { getDriver } from '@db/neo4j' export const description = ` @@ -27,7 +25,7 @@ export async function up(_next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } @@ -50,6 +48,6 @@ export async function down(_next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/20210506150512-add-donations-node.ts b/backend/src/db/migrations/20210506150512-add-donations-node.ts index 78919d46e..90f00e26f 100644 --- a/backend/src/db/migrations/20210506150512-add-donations-node.ts +++ b/backend/src/db/migrations/20210506150512-add-donations-node.ts @@ -1,11 +1,5 @@ -/* eslint-disable @typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable security/detect-non-literal-fs-filename */ + import { v4 as uuid } from 'uuid' import { getDriver } from '@db/neo4j' @@ -43,7 +37,7 @@ export async function up(_next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } @@ -68,6 +62,6 @@ export async function down(_next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/20210923140939-add-sendNotificationEmails-property-to-all-users.ts b/backend/src/db/migrations/20210923140939-add-sendNotificationEmails-property-to-all-users.ts index e4ed26095..5bc6aed17 100644 --- a/backend/src/db/migrations/20210923140939-add-sendNotificationEmails-property-to-all-users.ts +++ b/backend/src/db/migrations/20210923140939-add-sendNotificationEmails-property-to-all-users.ts @@ -1,11 +1,5 @@ -/* eslint-disable @typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable security/detect-non-literal-fs-filename */ + import { getDriver } from '@db/neo4j' export const description = '' @@ -33,7 +27,7 @@ export async function up(_next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } @@ -60,6 +54,6 @@ export async function down(_next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/20220803060819-create_fulltext_indices_and_unique_keys_for_groups.ts b/backend/src/db/migrations/20220803060819-create_fulltext_indices_and_unique_keys_for_groups.ts index 9fa0ffcd2..f06c10984 100644 --- a/backend/src/db/migrations/20220803060819-create_fulltext_indices_and_unique_keys_for_groups.ts +++ b/backend/src/db/migrations/20220803060819-create_fulltext_indices_and_unique_keys_for_groups.ts @@ -1,11 +1,5 @@ -/* eslint-disable @typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable security/detect-non-literal-fs-filename */ + import { getDriver } from '@db/neo4j' export const description = ` @@ -42,7 +36,7 @@ export async function up(_next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } @@ -74,6 +68,6 @@ export async function down(_next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/20230320130345-fulltext-search-indexes.ts b/backend/src/db/migrations/20230320130345-fulltext-search-indexes.ts index bf3541e7c..686d221de 100644 --- a/backend/src/db/migrations/20230320130345-fulltext-search-indexes.ts +++ b/backend/src/db/migrations/20230320130345-fulltext-search-indexes.ts @@ -1,11 +1,5 @@ -/* eslint-disable @typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable security/detect-non-literal-fs-filename */ + import { getDriver } from '@db/neo4j' export const description = '' @@ -50,7 +44,7 @@ export async function up(_next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } @@ -75,6 +69,6 @@ export async function down(_next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/20230329150329-article-label-for-posts.ts b/backend/src/db/migrations/20230329150329-article-label-for-posts.ts index 1c1259ca0..44433e56b 100644 --- a/backend/src/db/migrations/20230329150329-article-label-for-posts.ts +++ b/backend/src/db/migrations/20230329150329-article-label-for-posts.ts @@ -1,11 +1,5 @@ -/* eslint-disable @typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable security/detect-non-literal-fs-filename */ + import { getDriver } from '@db/neo4j' export const description = 'Add to all existing posts the Article label' @@ -30,7 +24,7 @@ export async function up(_next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } @@ -54,6 +48,6 @@ export async function down(_next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/20230608130637-add-postType-property.ts b/backend/src/db/migrations/20230608130637-add-postType-property.ts index e4f1033b5..1e5474064 100644 --- a/backend/src/db/migrations/20230608130637-add-postType-property.ts +++ b/backend/src/db/migrations/20230608130637-add-postType-property.ts @@ -1,11 +1,5 @@ -/* eslint-disable @typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable security/detect-non-literal-fs-filename */ + import { getDriver } from '@db/neo4j' export const description = 'Add postType property Article to all posts' @@ -30,7 +24,7 @@ export async function up(_next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } @@ -54,6 +48,6 @@ export async function down(_next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/20231017141022-fix-event-dates.ts b/backend/src/db/migrations/20231017141022-fix-event-dates.ts index b3d7d14bd..259e3ff65 100644 --- a/backend/src/db/migrations/20231017141022-fix-event-dates.ts +++ b/backend/src/db/migrations/20231017141022-fix-event-dates.ts @@ -1,11 +1,7 @@ -/* eslint-disable @typescript-eslint/require-await */ +/* eslint-disable @typescript-eslint/no-base-to-string */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/restrict-template-expressions */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable security/detect-non-literal-fs-filename */ + import { getDriver } from '@db/neo4j' export const description = ` @@ -26,11 +22,11 @@ export async function up(_next) { `) for (const event of events.records) { let [id, eventStart, eventEnd] = event - let date = new Date(eventStart) + let date = new Date(eventStart as string) date.setHours(date.getHours() - 1) eventStart = date.toISOString() if (eventEnd) { - date = new Date(eventEnd) + date = new Date(eventEnd as string) date.setHours(date.getHours() - 1) eventEnd = date.toISOString() } @@ -50,7 +46,7 @@ export async function up(_next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } @@ -69,6 +65,6 @@ export async function down(_next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/20250331130323-author-observes-own-post.ts b/backend/src/db/migrations/20250331130323-author-observes-own-post.ts index e088e12c1..df6eebf23 100644 --- a/backend/src/db/migrations/20250331130323-author-observes-own-post.ts +++ b/backend/src/db/migrations/20250331130323-author-observes-own-post.ts @@ -1,11 +1,5 @@ -/* eslint-disable @typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable security/detect-non-literal-fs-filename */ + import { getDriver } from '@db/neo4j' export const description = ` @@ -37,7 +31,7 @@ export async function up(_next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } @@ -62,6 +56,6 @@ export async function down(_next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/20250331140313-commenter-observes-post.ts b/backend/src/db/migrations/20250331140313-commenter-observes-post.ts index f3f358f20..ce1d32bc0 100644 --- a/backend/src/db/migrations/20250331140313-commenter-observes-post.ts +++ b/backend/src/db/migrations/20250331140313-commenter-observes-post.ts @@ -1,11 +1,5 @@ -/* eslint-disable @typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable security/detect-non-literal-fs-filename */ + import { getDriver } from '@db/neo4j' export const description = ` @@ -37,7 +31,7 @@ export async function up(_next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } @@ -63,6 +57,6 @@ export async function down(_next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/20250405030454-email-notification-settings.ts b/backend/src/db/migrations/20250405030454-email-notification-settings.ts index f17c88fa9..eaa9a7a6e 100644 --- a/backend/src/db/migrations/20250405030454-email-notification-settings.ts +++ b/backend/src/db/migrations/20250405030454-email-notification-settings.ts @@ -1,11 +1,5 @@ -/* eslint-disable @typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable security/detect-non-literal-fs-filename */ + import { getDriver } from '@db/neo4j' export const description = @@ -38,7 +32,7 @@ export async function up(_next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } @@ -69,6 +63,6 @@ export async function down(_next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/migrations/20250414220436-delete-old-badges.ts b/backend/src/db/migrations/20250414220436-delete-old-badges.ts index 9c1d2b8e1..d03e14619 100644 --- a/backend/src/db/migrations/20250414220436-delete-old-badges.ts +++ b/backend/src/db/migrations/20250414220436-delete-old-badges.ts @@ -1,11 +1,5 @@ -/* eslint-disable @typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/restrict-template-expressions */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable security/detect-non-literal-fs-filename */ + import { getDriver } from '@db/neo4j' export const description = '' @@ -30,7 +24,7 @@ export async function up(_next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } @@ -52,6 +46,6 @@ export async function down(_next) { console.log('rolled back') throw new Error(error) } finally { - session.close() + await session.close() } } diff --git a/backend/src/db/neo4j.ts b/backend/src/db/neo4j.ts index b7c0eec56..dcd19a0ea 100644 --- a/backend/src/db/neo4j.ts +++ b/backend/src/db/neo4j.ts @@ -1,16 +1,14 @@ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ + /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable import/no-named-as-default-member */ -import neo4j from 'neo4j-driver' +import neo4j, { Driver } from 'neo4j-driver' import Neode from 'neode' import CONFIG from '@config/index' import models from '@models/index' -let driver +let driver: Driver const defaultOptions = { uri: CONFIG.NEO4J_URI, username: CONFIG.NEO4J_USERNAME, @@ -25,7 +23,7 @@ export function getDriver(options = {}) { return driver } -let neodeInstance +let neodeInstance: Neode export function getNeode(options = {}) { if (!neodeInstance) { const { uri, username, password } = { ...defaultOptions, ...options } diff --git a/backend/src/db/seed.ts b/backend/src/db/seed.ts index 08594d1b4..0e2c2c61d 100644 --- a/backend/src/db/seed.ts +++ b/backend/src/db/seed.ts @@ -28,7 +28,6 @@ if (CONFIG.PRODUCTION && !CONFIG.PRODUCTION_DB_CLEAN_ALLOW) { const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] -/* eslint-disable no-multi-spaces */ ;(async function () { let authenticatedUser = null const driver = getDriver() @@ -1585,7 +1584,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] /* eslint-disable-next-line no-console */ console.log('Seeded Data...') await driver.close() - await neode.close() + neode.close() process.exit(0) // eslint-disable-next-line no-catch-all/no-catch-all } catch (err) { @@ -1594,4 +1593,3 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] process.exit(1) } })() -/* eslint-enable no-multi-spaces */ diff --git a/backend/src/graphql/types/type/Badge.gql b/backend/src/graphql/types/type/Badge.gql index cbfe0193d..8cdad2ee7 100644 --- a/backend/src/graphql/types/type/Badge.gql +++ b/backend/src/graphql/types/type/Badge.gql @@ -4,6 +4,7 @@ type Badge { icon: String! createdAt: String description: String! + isDefault: Boolean! rewarded: [User]! @relation(name: "REWARDED", direction: "OUT") verifies: [User]! @relation(name: "VERIFIES", direction: "OUT") diff --git a/backend/src/graphql/types/type/User.gql b/backend/src/graphql/types/type/User.gql index f1a2bcc15..81dd9cf5b 100644 --- a/backend/src/graphql/types/type/User.gql +++ b/backend/src/graphql/types/type/User.gql @@ -125,10 +125,10 @@ type User { categories: [Category] @relation(name: "CATEGORIZED", direction: "OUT") - badgeVerification: Badge @relation(name: "VERIFIES", direction: "IN") + badgeVerification: Badge! @neo4j_ignore badgeTrophies: [Badge]! @relation(name: "REWARDED", direction: "IN") badgeTrophiesCount: Int! @cypher(statement: "MATCH (this)<-[:REWARDED]-(r:Badge) RETURN COUNT(r)") - badgeTrophiesSelected: [Badge]! @neo4j_ignore + badgeTrophiesSelected: [Badge!]! @neo4j_ignore badgeTrophiesUnused: [Badge]! @neo4j_ignore badgeTrophiesUnusedCount: Int! @neo4j_ignore @@ -252,6 +252,6 @@ type Mutation { # Get a JWT Token for the given Email and password login(email: String!, password: String!): String! - setTrophyBadgeSelected(slot: Int!, badgeId: ID!): User + setTrophyBadgeSelected(slot: Int!, badgeId: ID): User resetTrophyBadgesSelected: User } diff --git a/backend/src/helpers/encryptPassword.ts b/backend/src/helpers/encryptPassword.ts index d8d08c23c..1d12556ea 100644 --- a/backend/src/helpers/encryptPassword.ts +++ b/backend/src/helpers/encryptPassword.ts @@ -4,6 +4,7 @@ import { hashSync } from 'bcryptjs' export default function (args) { + // eslint-disable-next-line n/no-sync args.encryptedPassword = hashSync(args.password, 10) delete args.password return args diff --git a/backend/src/jwt/decode.spec.ts b/backend/src/jwt/decode.spec.ts index 34dd86d68..0cd52a5d5 100644 --- a/backend/src/jwt/decode.spec.ts +++ b/backend/src/jwt/decode.spec.ts @@ -3,6 +3,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import Factory, { cleanDatabase } from '@db/factories' import { getDriver, getNeode } from '@db/neo4j' +import User from '@models/User' import decode from './decode' import encode from './encode' @@ -16,7 +17,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) // TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 @@ -86,26 +87,28 @@ describe('decode', () => { }) it('sets `lastActiveAt`', async () => { - let user = await neode.first('User', { id: 'u3' }) + let user = await neode.first('User', { id: 'u3' }, undefined) await expect(user.toJson()).resolves.not.toHaveProperty('lastActiveAt') await decode(driver, validAuthorizationHeader) - user = await neode.first('User', { id: 'u3' }) + user = await neode.first('User', { id: 'u3' }, undefined) await expect(user.toJson()).resolves.toMatchObject({ lastActiveAt: expect.any(String), }) }) it('updates `lastActiveAt` for every authenticated request', async () => { - let user = await neode.first('User', { id: 'u3' }) + let user = await neode.first('User', { id: 'u3' }, undefined) await user.update({ - updatedAt: new Date().toISOString(), - lastActiveAt: '2019-10-03T23:33:08.598Z', + // eslint-disable-next-line @typescript-eslint/no-explicit-any + updatedAt: new Date().toISOString() as any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + lastActiveAt: '2019-10-03T23:33:08.598Z' as any, }) await expect(user.toJson()).resolves.toMatchObject({ lastActiveAt: '2019-10-03T23:33:08.598Z', }) await decode(driver, validAuthorizationHeader) - user = await neode.first('User', { id: 'u3' }) + user = await neode.first('User', { id: 'u3' }, undefined) await expect(user.toJson()).resolves.toMatchObject({ // should be a different time by now ;) lastActiveAt: expect.not.stringContaining('2019-10-03T23:33'), diff --git a/backend/src/middleware/branding/brandingMiddlewares.ts b/backend/src/middleware/branding/brandingMiddlewares.ts new file mode 100644 index 000000000..8d47043e8 --- /dev/null +++ b/backend/src/middleware/branding/brandingMiddlewares.ts @@ -0,0 +1,6 @@ +// eslint-disable-next-line import/no-cycle +import { MiddlewareOrder } from '@middleware/index' + +export default (): MiddlewareOrder[] => { + return [] +} diff --git a/backend/src/middleware/hashtags/hashtagsMiddleware.spec.ts b/backend/src/middleware/hashtags/hashtagsMiddleware.spec.ts index 4f0d57303..bc3b96594 100644 --- a/backend/src/middleware/hashtags/hashtagsMiddleware.spec.ts +++ b/backend/src/middleware/hashtags/hashtagsMiddleware.spec.ts @@ -55,22 +55,15 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { - hashtagingUser = await neode.create( - 'User', - { - id: 'you', - name: 'Al Capone', - slug: 'al-capone', - }, - { - password: '1234', - email: 'test@example.org', - }, - ) + hashtagingUser = await neode.create('User', { + id: 'you', + name: 'Al Capone', + slug: 'al-capone', + }) await neode.create('Category', { id: 'cat9', name: 'Democracy & Politics', diff --git a/backend/src/middleware/helpers/email/templates/de/index.ts b/backend/src/middleware/helpers/email/templates/de/index.ts index 408bbb34d..4aa323b9f 100644 --- a/backend/src/middleware/helpers/email/templates/de/index.ts +++ b/backend/src/middleware/helpers/email/templates/de/index.ts @@ -3,6 +3,7 @@ import fs from 'node:fs' import path from 'node:path' +// eslint-disable-next-line n/no-sync const readFile = (fileName) => fs.readFileSync(path.join(__dirname, fileName), 'utf-8') export const notification = readFile('./notification.html') diff --git a/backend/src/middleware/helpers/email/templates/en/index.ts b/backend/src/middleware/helpers/email/templates/en/index.ts index 408bbb34d..4aa323b9f 100644 --- a/backend/src/middleware/helpers/email/templates/en/index.ts +++ b/backend/src/middleware/helpers/email/templates/en/index.ts @@ -3,6 +3,7 @@ import fs from 'node:fs' import path from 'node:path' +// eslint-disable-next-line n/no-sync const readFile = (fileName) => fs.readFileSync(path.join(__dirname, fileName), 'utf-8') export const notification = readFile('./notification.html') diff --git a/backend/src/middleware/helpers/email/templates/index.ts b/backend/src/middleware/helpers/email/templates/index.ts index f481516db..9a64192ce 100644 --- a/backend/src/middleware/helpers/email/templates/index.ts +++ b/backend/src/middleware/helpers/email/templates/index.ts @@ -3,6 +3,7 @@ import fs from 'node:fs' import path from 'node:path' +// eslint-disable-next-line n/no-sync const readFile = (fileName) => fs.readFileSync(path.join(__dirname, fileName), 'utf-8') export const signup = readFile('./signup.html') diff --git a/backend/src/middleware/index.ts b/backend/src/middleware/index.ts index 7ce53663c..37fd33ef9 100644 --- a/backend/src/middleware/index.ts +++ b/backend/src/middleware/index.ts @@ -1,14 +1,14 @@ /* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable security/detect-object-injection */ -import { applyMiddleware } from 'graphql-middleware' +import { applyMiddleware, IMiddleware } from 'graphql-middleware' import CONFIG from '@config/index' +// eslint-disable-next-line import/no-cycle +import brandingMiddlewares from './branding/brandingMiddlewares' import chatMiddleware from './chatMiddleware' import excerpt from './excerptMiddleware' import hashtags from './hashtags/hashtagsMiddleware' @@ -26,56 +26,44 @@ import userInteractions from './userInteractions' import validation from './validation/validationMiddleware' import xss from './xssMiddleware' -export default (schema) => { - const middlewares = { - sentry, - permissions, - xss, - validation, - sluggify, - excerpt, - login, - notifications, - hashtags, - softDelete, - includedFields, - orderBy, - languages, - userInteractions, - chatMiddleware, - } - - let order = [ - 'sentry', - 'permissions', - 'xss', - // 'activityPub', disabled temporarily - 'validation', - 'userInteractions', - 'sluggify', - 'languages', - 'excerpt', - 'login', - 'notifications', - 'hashtags', - 'softDelete', - 'includedFields', - 'orderBy', - 'chatMiddleware', - ] - - // add permisions middleware at the first position (unless we're seeding) - if (CONFIG.DISABLED_MIDDLEWARES) { - const disabledMiddlewares = CONFIG.DISABLED_MIDDLEWARES.split(',') - order = order.filter((key) => { - if (disabledMiddlewares.includes(key)) { - /* eslint-disable-next-line no-console */ - console.log(`Warning: Disabled "${disabledMiddlewares}" middleware.`) - } - return !disabledMiddlewares.includes(key) - }) - } - - const appliedMiddlewares = order.map((key) => middlewares[key]) - return applyMiddleware(schema, ...appliedMiddlewares) +export interface MiddlewareOrder { + order: number + name: string + middleware: IMiddleware +} + +const ocelotMiddlewares: MiddlewareOrder[] = [ + { order: -200, name: 'sentry', middleware: sentry }, + { order: -190, name: 'permissions', middleware: permissions }, + { order: -180, name: 'xss', middleware: xss }, + { order: -170, name: 'validation', middleware: validation }, + { order: -160, name: 'userInteractions', middleware: userInteractions }, + { order: -150, name: 'sluggify', middleware: sluggify }, + { order: -140, name: 'languages', middleware: languages }, + { order: -130, name: 'excerpt', middleware: excerpt }, + { order: -120, name: 'login', middleware: login }, + { order: -110, name: 'notifications', middleware: notifications }, + { order: -100, name: 'hashtags', middleware: hashtags }, + { order: -90, name: 'softDelete', middleware: softDelete }, + { order: -80, name: 'includedFields', middleware: includedFields }, + { order: -70, name: 'orderBy', middleware: orderBy }, + { order: -60, name: 'chatMiddleware', middleware: chatMiddleware }, +] + +export default (schema) => { + const middlewares = ocelotMiddlewares + .concat(brandingMiddlewares()) + .sort((a, b) => a.order - b.order) + + const filteredMiddlewares = middlewares.filter( + (middleware) => !CONFIG.DISABLED_MIDDLEWARES.includes(middleware.name), + ) + + // Warn if we filtered + if (middlewares.length < filteredMiddlewares.length) { + // eslint-disable-next-line no-console + console.log(`Warning: Disabled "${CONFIG.DISABLED_MIDDLEWARES.join(', ')}" middleware.`) + } + + return applyMiddleware(schema, ...filteredMiddlewares.map((middleware) => middleware.middleware)) } diff --git a/backend/src/middleware/languages/languages.spec.ts b/backend/src/middleware/languages/languages.spec.ts index 11ebf3a41..50e3a028f 100644 --- a/backend/src/middleware/languages/languages.spec.ts +++ b/backend/src/middleware/languages/languages.spec.ts @@ -32,7 +32,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) const createPostMutation = gql` diff --git a/backend/src/middleware/notifications/notificationsMiddleware.emails.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.emails.spec.ts index 78c95b454..79d95e43e 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.emails.spec.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.emails.spec.ts @@ -16,20 +16,21 @@ import createServer from '@src/server' CONFIG.CATEGORIES_ACTIVE = false -const sendMailMock = jest.fn() -jest.mock('../helpers/email/sendMail', () => ({ - sendMail: () => sendMailMock(), +const sendMailMock: (notification) => void = jest.fn() +jest.mock('@middleware/helpers/email/sendMail', () => ({ + sendMail: (notification) => sendMailMock(notification), })) -let server, query, mutate, authenticatedUser +let server, query, mutate, authenticatedUser, emaillessMember let postAuthor, groupMember const driver = getDriver() const neode = getNeode() -const mentionString = - '@group-member' +const mentionString = ` + @group-member + @email-less-member` const createPostMutation = gql` mutation ($id: ID, $title: String!, $content: String!, $groupId: ID) { @@ -119,7 +120,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('emails sent for notifications', () => { @@ -148,6 +149,11 @@ describe('emails sent for notifications', () => { password: '1234', }, ) + emaillessMember = await neode.create('User', { + id: 'email-less-member', + name: 'Email-less Member', + slug: 'email-less-member', + }) authenticatedUser = await postAuthor.toJson() await mutate({ mutation: createGroupMutation(), @@ -171,6 +177,18 @@ describe('emails sent for notifications', () => { mutation: followUserMutation, variables: { id: 'post-author' }, }) + authenticatedUser = await emaillessMember.toJson() + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'public-group', + userId: 'group-member', + }, + }) + await mutate({ + mutation: followUserMutation, + variables: { id: 'post-author' }, + }) }) afterEach(async () => { @@ -188,7 +206,7 @@ describe('emails sent for notifications', () => { variables: { id: 'post', title: 'This is the post', - content: `Hello, ${mentionString}, my trusty follower.`, + content: `Hello, ${mentionString}, my trusty followers.`, groupId: 'public-group', }, }) @@ -213,7 +231,7 @@ describe('emails sent for notifications', () => { __typename: 'Post', id: 'post', content: - 'Hello, @group-member, my trusty follower.', + 'Hello,
@group-member
@email-less-member, my trusty followers.', }, read: false, reason: 'post_in_group', @@ -225,7 +243,7 @@ describe('emails sent for notifications', () => { __typename: 'Post', id: 'post', content: - 'Hello, @group-member, my trusty follower.', + 'Hello,
@group-member
@email-less-member, my trusty followers.', }, read: false, reason: 'followed_user_posted', @@ -237,7 +255,7 @@ describe('emails sent for notifications', () => { __typename: 'Post', id: 'post', content: - 'Hello, @group-member, my trusty follower.', + 'Hello,
@group-member
@email-less-member, my trusty followers.', }, read: false, reason: 'mentioned_in_post', @@ -260,7 +278,7 @@ describe('emails sent for notifications', () => { variables: { id: 'post', title: 'This is the post', - content: `Hello, ${mentionString}, my trusty follower.`, + content: `Hello, ${mentionString}, my trusty followers.`, groupId: 'public-group', }, }) @@ -285,7 +303,7 @@ describe('emails sent for notifications', () => { __typename: 'Post', id: 'post', content: - 'Hello, @group-member, my trusty follower.', + 'Hello,
@group-member
@email-less-member, my trusty followers.', }, read: false, reason: 'post_in_group', @@ -297,7 +315,7 @@ describe('emails sent for notifications', () => { __typename: 'Post', id: 'post', content: - 'Hello, @group-member, my trusty follower.', + 'Hello,
@group-member
@email-less-member, my trusty followers.', }, read: false, reason: 'followed_user_posted', @@ -309,7 +327,7 @@ describe('emails sent for notifications', () => { __typename: 'Post', id: 'post', content: - 'Hello, @group-member, my trusty follower.', + 'Hello,
@group-member
@email-less-member, my trusty followers.', }, read: false, reason: 'mentioned_in_post', @@ -333,7 +351,7 @@ describe('emails sent for notifications', () => { variables: { id: 'post', title: 'This is the post', - content: `Hello, ${mentionString}, my trusty follower.`, + content: `Hello, ${mentionString}, my trusty followers.`, groupId: 'public-group', }, }) @@ -358,7 +376,7 @@ describe('emails sent for notifications', () => { __typename: 'Post', id: 'post', content: - 'Hello, @group-member, my trusty follower.', + 'Hello,
@group-member
@email-less-member, my trusty followers.', }, read: false, reason: 'post_in_group', @@ -370,7 +388,7 @@ describe('emails sent for notifications', () => { __typename: 'Post', id: 'post', content: - 'Hello, @group-member, my trusty follower.', + 'Hello,
@group-member
@email-less-member, my trusty followers.', }, read: false, reason: 'followed_user_posted', @@ -382,7 +400,7 @@ describe('emails sent for notifications', () => { __typename: 'Post', id: 'post', content: - 'Hello, @group-member, my trusty follower.', + 'Hello,
@group-member
@email-less-member, my trusty followers.', }, read: false, reason: 'mentioned_in_post', @@ -407,7 +425,7 @@ describe('emails sent for notifications', () => { variables: { id: 'post', title: 'This is the post', - content: `Hello, ${mentionString}, my trusty follower.`, + content: `Hello, ${mentionString}, my trusty followers.`, groupId: 'public-group', }, }) @@ -432,7 +450,7 @@ describe('emails sent for notifications', () => { __typename: 'Post', id: 'post', content: - 'Hello, @group-member, my trusty follower.', + 'Hello,
@group-member
@email-less-member, my trusty followers.', }, read: false, reason: 'post_in_group', @@ -444,7 +462,7 @@ describe('emails sent for notifications', () => { __typename: 'Post', id: 'post', content: - 'Hello, @group-member, my trusty follower.', + 'Hello,
@group-member
@email-less-member, my trusty followers.', }, read: false, reason: 'followed_user_posted', @@ -456,7 +474,7 @@ describe('emails sent for notifications', () => { __typename: 'Post', id: 'post', content: - 'Hello, @group-member, my trusty follower.', + 'Hello,
@group-member
@email-less-member, my trusty followers.', }, read: false, reason: 'mentioned_in_post', @@ -481,7 +499,7 @@ describe('emails sent for notifications', () => { variables: { id: 'post', title: 'This is the post', - content: `Hello, ${mentionString}, my trusty follower.`, + content: `Hello, ${mentionString}, my trusty followers.`, groupId: 'public-group', }, }) @@ -501,7 +519,7 @@ describe('emails sent for notifications', () => { mutation: createCommentMutation, variables: { id: 'comment-2', - content: `Hello, ${mentionString}, my beloved follower.`, + content: `Hello, ${mentionString}, my trusty followers.`, postId: 'post', }, }) @@ -529,7 +547,7 @@ describe('emails sent for notifications', () => { __typename: 'Comment', id: 'comment-2', content: - 'Hello, @group-member, my beloved follower.', + 'Hello,
@group-member
@email-less-member, my trusty followers.', }, read: false, reason: 'commented_on_post', @@ -541,7 +559,7 @@ describe('emails sent for notifications', () => { __typename: 'Comment', id: 'comment-2', content: - 'Hello, @group-member, my beloved follower.', + 'Hello,
@group-member
@email-less-member, my trusty followers.', }, read: false, reason: 'mentioned_in_comment', @@ -563,7 +581,7 @@ describe('emails sent for notifications', () => { variables: { id: 'post', title: 'This is the post', - content: `Hello, ${mentionString}, my trusty follower.`, + content: `Hello, ${mentionString}, my trusty followers.`, groupId: 'public-group', }, }) @@ -583,7 +601,7 @@ describe('emails sent for notifications', () => { mutation: createCommentMutation, variables: { id: 'comment-2', - content: `Hello, ${mentionString}, my beloved follower.`, + content: `Hello, ${mentionString}, my trusty followers.`, postId: 'post', }, }) @@ -611,7 +629,7 @@ describe('emails sent for notifications', () => { __typename: 'Comment', id: 'comment-2', content: - 'Hello, @group-member, my beloved follower.', + 'Hello,
@group-member
@email-less-member, my trusty followers.', }, read: false, reason: 'commented_on_post', @@ -623,7 +641,7 @@ describe('emails sent for notifications', () => { __typename: 'Comment', id: 'comment-2', content: - 'Hello, @group-member, my beloved follower.', + 'Hello,
@group-member
@email-less-member, my trusty followers.', }, read: false, reason: 'mentioned_in_comment', @@ -646,7 +664,7 @@ describe('emails sent for notifications', () => { variables: { id: 'post', title: 'This is the post', - content: `Hello, ${mentionString}, my trusty follower.`, + content: `Hello, ${mentionString}, my trusty followers.`, groupId: 'public-group', }, }) @@ -666,7 +684,7 @@ describe('emails sent for notifications', () => { mutation: createCommentMutation, variables: { id: 'comment-2', - content: `Hello, ${mentionString}, my beloved follower.`, + content: `Hello, ${mentionString}, my trusty followers.`, postId: 'post', }, }) @@ -694,7 +712,7 @@ describe('emails sent for notifications', () => { __typename: 'Comment', id: 'comment-2', content: - 'Hello, @group-member, my beloved follower.', + 'Hello,
@group-member
@email-less-member, my trusty followers.', }, read: false, reason: 'commented_on_post', @@ -706,7 +724,7 @@ describe('emails sent for notifications', () => { __typename: 'Comment', id: 'comment-2', content: - 'Hello, @group-member, my beloved follower.', + 'Hello,
@group-member
@email-less-member, my trusty followers.', }, read: false, reason: 'mentioned_in_comment', diff --git a/backend/src/middleware/notifications/notificationsMiddleware.followed-users.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.followed-users.spec.ts index 5be4ea5b5..21d4a14a0 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.followed-users.spec.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.followed-users.spec.ts @@ -2,7 +2,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ + import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' @@ -14,14 +14,14 @@ import createServer from '@src/server' CONFIG.CATEGORIES_ACTIVE = false -const sendMailMock = jest.fn() -jest.mock('../helpers/email/sendMail', () => ({ - sendMail: () => sendMailMock(), +const sendMailMock: (notification) => void = jest.fn() +jest.mock('@middleware/helpers/email/sendMail', () => ({ + sendMail: (notification) => sendMailMock(notification), })) let server, query, mutate, authenticatedUser -let postAuthor, firstFollower, secondFollower +let postAuthor, firstFollower, secondFollower, thirdFollower, emaillessFollower const driver = getDriver() const neode = getNeode() @@ -94,7 +94,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('following users notifications', () => { @@ -107,7 +107,7 @@ describe('following users notifications', () => { slug: 'post-author', }, { - email: 'test@example.org', + email: 'post-author@example.org', password: '1234', }, ) @@ -119,7 +119,7 @@ describe('following users notifications', () => { slug: 'first-follower', }, { - email: 'test2@example.org', + email: 'first-follower@example.org', password: '1234', }, ) @@ -131,10 +131,27 @@ describe('following users notifications', () => { slug: 'second-follower', }, { - email: 'test3@example.org', + email: 'second-follower@example.org', password: '1234', }, ) + thirdFollower = await Factory.build( + 'user', + { + id: 'third-follower', + name: 'Third Follower', + slug: 'third-follower', + }, + { + email: 'third-follower@example.org', + password: '1234', + }, + ) + emaillessFollower = await neode.create('User', { + id: 'email-less-follower', + name: 'Email-less Follower', + slug: 'email-less-follower', + }) await secondFollower.update({ emailNotificationsFollowingUsers: false }) authenticatedUser = await firstFollower.toJson() await mutate({ @@ -146,6 +163,16 @@ describe('following users notifications', () => { mutation: followUserMutation, variables: { id: 'post-author' }, }) + authenticatedUser = await thirdFollower.toJson() + await mutate({ + mutation: followUserMutation, + variables: { id: 'post-author' }, + }) + authenticatedUser = await emaillessFollower.toJson() + await mutate({ + mutation: followUserMutation, + variables: { id: 'post-author' }, + }) jest.clearAllMocks() }) @@ -221,8 +248,43 @@ describe('following users notifications', () => { }) }) - it('sends only one email, as second follower has emails disabled', () => { - expect(sendMailMock).toHaveBeenCalledTimes(1) + it('sends notification to the email-less follower', async () => { + authenticatedUser = await emaillessFollower.toJson() + await expect( + query({ + query: notificationQuery, + }), + ).resolves.toMatchObject({ + data: { + notifications: [ + { + from: { + __typename: 'Post', + id: 'post', + }, + read: false, + reason: 'followed_user_posted', + }, + ], + }, + errors: undefined, + }) + }) + + it('sends only two emails, as second follower has emails disabled and email-less follower has no email', () => { + expect(sendMailMock).toHaveBeenCalledTimes(2) + expect(sendMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + html: expect.stringContaining('Hello First Follower'), + to: 'first-follower@example.org', + }), + ) + expect(sendMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + html: expect.stringContaining('Hello Third Follower'), + to: 'third-follower@example.org', + }), + ) }) }) diff --git a/backend/src/middleware/notifications/notificationsMiddleware.mentions-in-groups.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.mentions-in-groups.spec.ts index 7058efd25..96c7e9d18 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.mentions-in-groups.spec.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.mentions-in-groups.spec.ts @@ -17,22 +17,23 @@ import createServer from '@src/server' CONFIG.CATEGORIES_ACTIVE = false -const sendMailMock = jest.fn() -jest.mock('../helpers/email/sendMail', () => ({ - sendMail: () => sendMailMock(), +const sendMailMock: (notification) => void = jest.fn() +jest.mock('@middleware/helpers/email/sendMail', () => ({ + sendMail: (notification) => sendMailMock(notification), })) let server, query, mutate, authenticatedUser -let postAuthor, groupMember, pendingMember, noMember +let postAuthor, groupMember, pendingMember, noMember, emaillessMember const driver = getDriver() const neode = getNeode() const mentionString = ` - @no-meber + @no-member @pending-member @group-member. + @email-less-member. ` const createPostMutation = gql` @@ -115,7 +116,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('mentions in groups', () => { @@ -168,6 +169,12 @@ describe('mentions in groups', () => { password: '1234', }, ) + emaillessMember = await neode.create('User', { + id: 'email-less-member', + name: 'Email-less Member', + slug: 'email-less-member', + }) + authenticatedUser = await postAuthor.toJson() await mutate({ mutation: createGroupMutation(), @@ -243,6 +250,28 @@ describe('mentions in groups', () => { userId: 'pending-member', }, }) + authenticatedUser = await emaillessMember.toJson() + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'public-group', + userId: 'group-member', + }, + }) + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'closed-group', + userId: 'group-member', + }, + }) + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'hidden-group', + userId: 'group-member', + }, + }) authenticatedUser = await postAuthor.toJson() await mutate({ mutation: changeGroupMemberRoleMutation(), @@ -260,8 +289,26 @@ describe('mentions in groups', () => { roleInGroup: 'usual', }, }) + await mutate({ + mutation: changeGroupMemberRoleMutation(), + variables: { + groupId: 'closed-group', + userId: 'email-less-member', + roleInGroup: 'usual', + }, + }) + await mutate({ + mutation: changeGroupMemberRoleMutation(), + variables: { + groupId: 'hidden-group', + userId: 'email-less-member', + roleInGroup: 'usual', + }, + }) authenticatedUser = await groupMember.toJson() await markAllAsRead() + authenticatedUser = await emaillessMember.toJson() + await markAllAsRead() }) afterEach(async () => { @@ -327,7 +374,7 @@ describe('mentions in groups', () => { __typename: 'Post', id: 'public-post', content: - 'Hey
@no-meber
@pending-member
@group-member.
! Please read this', + 'Hey
@no-member
@pending-member
@group-member.
@email-less-member.
! Please read this', }, read: false, reason: 'post_in_group', @@ -339,7 +386,7 @@ describe('mentions in groups', () => { __typename: 'Post', id: 'public-post', content: - 'Hey
@no-meber
@pending-member
@group-member.
! Please read this', + 'Hey
@no-member
@pending-member
@group-member.
@email-less-member.
! Please read this', }, read: false, reason: 'mentioned_in_post', @@ -351,7 +398,7 @@ describe('mentions in groups', () => { }) }) - it('sends 3 emails, one for each user', () => { + it('sends only 3 emails, one for each user with an email', () => { expect(sendMailMock).toHaveBeenCalledTimes(3) }) }) @@ -423,7 +470,7 @@ describe('mentions in groups', () => { __typename: 'Post', id: 'closed-post', content: - 'Hey members
@no-meber
@pending-member
@group-member.
! Please read this', + 'Hey members
@no-member
@pending-member
@group-member.
@email-less-member.
! Please read this', }, read: false, reason: 'post_in_group', @@ -435,7 +482,7 @@ describe('mentions in groups', () => { __typename: 'Post', id: 'closed-post', content: - 'Hey members
@no-meber
@pending-member
@group-member.
! Please read this', + 'Hey members
@no-member
@pending-member
@group-member.
@email-less-member.
! Please read this', }, read: false, reason: 'mentioned_in_post', @@ -519,7 +566,7 @@ describe('mentions in groups', () => { __typename: 'Post', id: 'hidden-post', content: - 'Hey hiders
@no-meber
@pending-member
@group-member.
! Please read this', + 'Hey hiders
@no-member
@pending-member
@group-member.
@email-less-member.
! Please read this', }, read: false, reason: 'post_in_group', @@ -531,7 +578,7 @@ describe('mentions in groups', () => { __typename: 'Post', id: 'hidden-post', content: - 'Hey hiders
@no-meber
@pending-member
@group-member.
! Please read this', + 'Hey hiders
@no-member
@pending-member
@group-member.
@email-less-member.
! Please read this', }, read: false, reason: 'mentioned_in_post', diff --git a/backend/src/middleware/notifications/notificationsMiddleware.observing-posts.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.observing-posts.spec.ts index 2c73d2beb..a0864fe07 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.observing-posts.spec.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.observing-posts.spec.ts @@ -6,15 +6,20 @@ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' import CONFIG from '@config/index' -import { cleanDatabase } from '@db/factories' +import Factory, { cleanDatabase } from '@db/factories' import { getNeode, getDriver } from '@db/neo4j' import createServer from '@src/server' CONFIG.CATEGORIES_ACTIVE = false +const sendMailMock: (notification) => void = jest.fn() +jest.mock('@middleware/helpers/email/sendMail', () => ({ + sendMail: (notification) => sendMailMock(notification), +})) + let server, query, mutate, authenticatedUser -let postAuthor, firstCommenter, secondCommenter +let postAuthor, firstCommenter, secondCommenter, emaillessObserver const driver = getDriver() const neode = getNeode() @@ -97,47 +102,52 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('notifications for users that observe a post', () => { beforeAll(async () => { - postAuthor = await neode.create( - 'User', + postAuthor = await Factory.build( + 'user', { id: 'post-author', name: 'Post Author', slug: 'post-author', }, { - email: 'test@example.org', + email: 'post-author@example.org', password: '1234', }, ) - firstCommenter = await neode.create( - 'User', + firstCommenter = await Factory.build( + 'user', { id: 'first-commenter', name: 'First Commenter', slug: 'first-commenter', }, { - email: 'test2@example.org', + email: 'first-commenter@example.org', password: '1234', }, ) - secondCommenter = await neode.create( - 'User', + secondCommenter = await Factory.build( + 'user', { id: 'second-commenter', name: 'Second Commenter', slug: 'second-commenter', }, { - email: 'test3@example.org', + email: 'second-commenter@example.org', password: '1234', }, ) + emaillessObserver = await neode.create('User', { + id: 'email-less-observer', + name: 'Email-less Observer', + slug: 'email-less-observer', + }) authenticatedUser = await postAuthor.toJson() await mutate({ mutation: createPostMutation, @@ -147,6 +157,14 @@ describe('notifications for users that observe a post', () => { content: 'This is the content of the post', }, }) + authenticatedUser = await emaillessObserver.toJson() + await mutate({ + mutation: toggleObservePostMutation, + variables: { + id: 'post', + value: true, + }, + }) }) describe('first comment on the post', () => { @@ -198,8 +216,18 @@ describe('notifications for users that observe a post', () => { }) }) + it('sends one email', () => { + expect(sendMailMock).toHaveBeenCalledTimes(1) + expect(sendMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + to: 'post-author@example.org', + }), + ) + }) + describe('second comment on post', () => { beforeAll(async () => { + jest.clearAllMocks() authenticatedUser = await secondCommenter.toJson() await mutate({ mutation: createCommentMutation, @@ -277,10 +305,25 @@ describe('notifications for users that observe a post', () => { errors: undefined, }) }) + + it('sends two emails', () => { + expect(sendMailMock).toHaveBeenCalledTimes(2) + expect(sendMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + to: 'post-author@example.org', + }), + ) + expect(sendMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + to: 'first-commenter@example.org', + }), + ) + }) }) describe('first commenter unfollows the post and post author comments post', () => { beforeAll(async () => { + jest.clearAllMocks() authenticatedUser = await firstCommenter.toJson() await mutate({ mutation: toggleObservePostMutation, @@ -376,6 +419,15 @@ describe('notifications for users that observe a post', () => { errors: undefined, }) }) + + it('sends one email', () => { + expect(sendMailMock).toHaveBeenCalledTimes(1) + expect(sendMailMock).toHaveBeenCalledWith( + expect.objectContaining({ + to: 'second-commenter@example.org', + }), + ) + }) }) }) }) diff --git a/backend/src/middleware/notifications/notificationsMiddleware.online-status.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.online-status.spec.ts index da9a6b250..3a47d376d 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.online-status.spec.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.online-status.spec.ts @@ -13,9 +13,9 @@ import createServer from '@src/server' CONFIG.CATEGORIES_ACTIVE = false -const sendMailMock = jest.fn() -jest.mock('../helpers/email/sendMail', () => ({ - sendMail: () => sendMailMock(), +const sendMailMock: (notification) => void = jest.fn() +jest.mock('@middleware/helpers/email/sendMail', () => ({ + sendMail: (notification) => sendMailMock(notification), })) let isUserOnlineMock = jest.fn().mockReturnValue(false) @@ -62,7 +62,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) afterEach(async () => { diff --git a/backend/src/middleware/notifications/notificationsMiddleware.posts-in-groups.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.posts-in-groups.spec.ts index 9ca4ae7ab..25aef2e2b 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.posts-in-groups.spec.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.posts-in-groups.spec.ts @@ -17,14 +17,14 @@ import createServer from '@src/server' CONFIG.CATEGORIES_ACTIVE = false -const sendMailMock = jest.fn() -jest.mock('../helpers/email/sendMail', () => ({ - sendMail: () => sendMailMock(), +const sendMailMock: (notification) => void = jest.fn() +jest.mock('@middleware/helpers/email/sendMail', () => ({ + sendMail: (notification) => sendMailMock(notification), })) let server, query, mutate, authenticatedUser -let postAuthor, groupMember, pendingMember +let postAuthor, groupMember, pendingMember, emaillessMember const driver = getDriver() const neode = getNeode() @@ -118,7 +118,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('notify group members of new posts in group', () => { @@ -159,6 +159,12 @@ describe('notify group members of new posts in group', () => { password: '1234', }, ) + emaillessMember = await neode.create('User', { + id: 'email-less-member', + name: 'Email-less Member', + slug: 'email-less-member', + }) + authenticatedUser = await postAuthor.toJson() await mutate({ mutation: createGroupMutation(), @@ -186,6 +192,14 @@ describe('notify group members of new posts in group', () => { userId: 'pending-member', }, }) + authenticatedUser = await emaillessMember.toJson() + await mutate({ + mutation: joinGroupMutation(), + variables: { + groupId: 'g-1', + userId: 'group-member', + }, + }) authenticatedUser = await postAuthor.toJson() await mutate({ mutation: changeGroupMemberRoleMutation(), @@ -195,6 +209,14 @@ describe('notify group members of new posts in group', () => { roleInGroup: 'usual', }, }) + await mutate({ + mutation: changeGroupMemberRoleMutation(), + variables: { + groupId: 'g-1', + userId: 'email-less-member', + roleInGroup: 'usual', + }, + }) }) afterEach(async () => { diff --git a/backend/src/middleware/notifications/notificationsMiddleware.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.spec.ts index 908ccac22..ab0a6a5b2 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.spec.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.spec.ts @@ -18,9 +18,9 @@ import { leaveGroupMutation } from '@graphql/queries/leaveGroupMutation' import { removeUserFromGroupMutation } from '@graphql/queries/removeUserFromGroupMutation' import createServer, { pubsub } from '@src/server' -const sendMailMock = jest.fn() -jest.mock('../helpers/email/sendMail', () => ({ - sendMail: () => sendMailMock(), +const sendMailMock: (notification) => void = jest.fn() +jest.mock('@middleware/helpers/email/sendMail', () => ({ + sendMail: (notification) => sendMailMock(notification), })) const chatMessageTemplateMock = jest.fn() @@ -88,7 +88,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { @@ -195,8 +195,8 @@ describe('notifications', () => { beforeEach(async () => { jest.clearAllMocks() commentContent = 'Commenters comment.' - commentAuthor = await neode.create( - 'User', + commentAuthor = await Factory.build( + 'user', { id: 'commentAuthor', name: 'Mrs Comment', @@ -345,8 +345,8 @@ describe('notifications', () => { beforeEach(async () => { jest.clearAllMocks() - postAuthor = await neode.create( - 'User', + postAuthor = await Factory.build( + 'user', { id: 'postAuthor', name: 'Mrs Post', @@ -658,8 +658,8 @@ describe('notifications', () => { beforeEach(async () => { commentContent = 'One mention about me with @al-capone.' - commentAuthor = await neode.create( - 'User', + commentAuthor = await Factory.build( + 'user', { id: 'commentAuthor', name: 'Mrs Comment', @@ -673,15 +673,15 @@ describe('notifications', () => { }) it('sends only one notification with reason mentioned_in_comment', async () => { - postAuthor = await neode.create( - 'User', + postAuthor = await Factory.build( + 'user', { id: 'MrPostAuthor', name: 'Mr Author', slug: 'mr-author', }, { - email: 'post-author@example.org', + email: 'post-author2@example.org', password: '1234', }, ) @@ -756,8 +756,8 @@ describe('notifications', () => { await postAuthor.relateTo(notifiedUser, 'blocked') commentContent = 'One mention about me with @al-capone.' - commentAuthor = await neode.create( - 'User', + commentAuthor = await Factory.build( + 'user', { id: 'commentAuthor', name: 'Mrs Comment', @@ -807,8 +807,8 @@ describe('notifications', () => { await postAuthor.relateTo(notifiedUser, 'muted') commentContent = 'One mention about me with @al-capone.' - commentAuthor = await neode.create( - 'User', + commentAuthor = await Factory.build( + 'user', { id: 'commentAuthor', name: 'Mrs Comment', @@ -879,8 +879,8 @@ describe('notifications', () => { beforeEach(async () => { jest.clearAllMocks() - chatSender = await neode.create( - 'User', + chatSender = await Factory.build( + 'user', { id: 'chatSender', name: 'chatSender', @@ -931,7 +931,7 @@ describe('notifications', () => { content: 'Some nice message to chatReceiver', senderId: 'chatSender', username: 'chatSender', - avatar: null, + avatar: expect.any(String), date: expect.any(String), saved: true, distributed: false, @@ -967,7 +967,7 @@ describe('notifications', () => { content: 'Some nice message to chatReceiver', senderId: 'chatSender', username: 'chatSender', - avatar: null, + avatar: expect.any(String), date: expect.any(String), saved: true, distributed: false, @@ -1046,7 +1046,7 @@ describe('notifications', () => { content: 'Some nice message to chatReceiver', senderId: 'chatSender', username: 'chatSender', - avatar: null, + avatar: expect.any(String), date: expect.any(String), saved: true, distributed: false, diff --git a/backend/src/middleware/notifications/notificationsMiddleware.ts b/backend/src/middleware/notifications/notificationsMiddleware.ts index f7be031c8..4926dc94e 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.ts @@ -13,64 +13,32 @@ import { isUserOnline } from '@middleware/helpers/isUserOnline' import { validateNotifyUsers } from '@middleware/validation/validationMiddleware' // eslint-disable-next-line import/no-cycle import { getUnreadRoomsCount } from '@schema/resolvers/rooms' -// eslint-disable-next-line import/no-cycle import { pubsub, NOTIFICATION_ADDED, ROOM_COUNT_UPDATED, CHAT_MESSAGE_ADDED } from '@src/server' import extractMentionedUsers from './mentions/extractMentionedUsers' -const queryNotificationEmails = async (context, notificationUserIds) => { - if (!notificationUserIds?.length) return [] - const userEmailCypher = ` - MATCH (user: User) - // blocked users are filtered out from notifications already - WHERE user.id in $notificationUserIds - WITH user - MATCH (user)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress) - RETURN emailAddress {.email} - ` - const session = context.driver.session() - const writeTxResultPromise = session.readTransaction(async (transaction) => { - const emailAddressTransactionResponse = await transaction.run(userEmailCypher, { - notificationUserIds, - }) - return emailAddressTransactionResponse.records.map((record) => record.get('emailAddress')) - }) - try { - const emailAddresses = await writeTxResultPromise - return emailAddresses - } catch (error) { - throw new Error(error) - } finally { - session.close() - } -} - const publishNotifications = async ( context, - promises, + notificationsPromise, emailNotificationSetting: string, emailsSent: string[] = [], ): Promise => { - let notifications = await Promise.all(promises) - notifications = notifications.flat() - const notificationsEmailAddresses = await queryNotificationEmails( - context, - notifications.map((notification) => notification.to.id), - ) - notifications.forEach((notificationAdded, index) => { + const notifications = await notificationsPromise + notifications.forEach((notificationAdded) => { pubsub.publish(NOTIFICATION_ADDED, { notificationAdded }) if ( + notificationAdded.email && // no primary email was found (notificationAdded.to[emailNotificationSetting] ?? true) && !isUserOnline(notificationAdded.to) && - !emailsSent.includes(notificationsEmailAddresses[index].email) + !emailsSent.includes(notificationAdded.email) ) { sendMail( notificationTemplate({ - email: notificationsEmailAddresses[index].email, + email: notificationAdded.email, variables: { notification: notificationAdded }, }), ) - emailsSent.push(notificationsEmailAddresses[index].email) + emailsSent.push(notificationAdded.email) } }) return emailsSent @@ -82,7 +50,7 @@ const handleJoinGroup = async (resolve, root, args, context, resolveInfo) => { if (user) { await publishNotifications( context, - [notifyOwnersOfGroup(groupId, userId, 'user_joined_group', context)], + notifyOwnersOfGroup(groupId, userId, 'user_joined_group', context), 'emailNotificationsGroupMemberJoined', ) } @@ -95,7 +63,7 @@ const handleLeaveGroup = async (resolve, root, args, context, resolveInfo) => { if (user) { await publishNotifications( context, - [notifyOwnersOfGroup(groupId, userId, 'user_left_group', context)], + notifyOwnersOfGroup(groupId, userId, 'user_left_group', context), 'emailNotificationsGroupMemberLeft', ) } @@ -108,7 +76,7 @@ const handleChangeGroupMemberRole = async (resolve, root, args, context, resolve if (user) { await publishNotifications( context, - [notifyMemberOfGroup(groupId, userId, 'changed_group_member_role', context)], + notifyMemberOfGroup(groupId, userId, 'changed_group_member_role', context), 'emailNotificationsGroupMemberRoleChanged', ) } @@ -121,7 +89,7 @@ const handleRemoveUserFromGroup = async (resolve, root, args, context, resolveIn if (user) { await publishNotifications( context, - [notifyMemberOfGroup(groupId, userId, 'removed_user_from_group', context)], + notifyMemberOfGroup(groupId, userId, 'removed_user_from_group', context), 'emailNotificationsGroupMemberRemoved', ) } @@ -135,20 +103,20 @@ const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo if (post) { const sentEmails: string[] = await publishNotifications( context, - [notifyUsersOfMention('Post', post.id, idsOfUsers, 'mentioned_in_post', context)], + notifyUsersOfMention('Post', post.id, idsOfUsers, 'mentioned_in_post', context), 'emailNotificationsMention', ) sentEmails.concat( await publishNotifications( context, - [notifyFollowingUsers(post.id, groupId, context)], + notifyFollowingUsers(post.id, groupId, context), 'emailNotificationsFollowingUsers', sentEmails, ), ) await publishNotifications( context, - [notifyGroupMembersOfNewPost(post.id, groupId, context)], + notifyGroupMembersOfNewPost(post.id, groupId, context), 'emailNotificationsPostInGroup', sentEmails, ) @@ -164,20 +132,18 @@ const handleContentDataOfComment = async (resolve, root, args, context, resolveI idsOfMentionedUsers = idsOfMentionedUsers.filter((id) => id !== postAuthor.id) const sentEmails: string[] = await publishNotifications( context, - [ - notifyUsersOfMention( - 'Comment', - comment.id, - idsOfMentionedUsers, - 'mentioned_in_comment', - context, - ), - ], + notifyUsersOfMention( + 'Comment', + comment.id, + idsOfMentionedUsers, + 'mentioned_in_comment', + context, + ), 'emailNotificationsMention', ) await publishNotifications( context, - [notifyUsersOfComment('Comment', comment.id, 'commented_on_post', context)], + notifyUsersOfComment('Comment', comment.id, 'commented_on_post', context), 'emailNotificationsCommentOnObservedPost', sentEmails, ) @@ -208,17 +174,20 @@ const notifyFollowingUsers = async (postId, groupId, context) => { const cypher = ` MATCH (post:Post { id: $postId })<-[:WROTE]-(author:User { id: $userId })<-[:FOLLOWS]-(user:User) OPTIONAL MATCH (post)-[:IN]->(group:Group { id: $groupId }) - WITH post, author, user, group WHERE group IS NULL OR group.groupType = 'public' + OPTIONAL MATCH (user)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress) + WITH post, author, user, emailAddress, group + WHERE group IS NULL OR group.groupType = 'public' MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user) SET notification.read = FALSE SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime())) SET notification.updatedAt = toString(datetime()) - WITH notification, author, user, + WITH notification, author, user, emailAddress.email as email, post {.*, author: properties(author) } AS finalResource RETURN notification { .*, from: finalResource, to: properties(user), + email: email, relatedUser: properties(author) } ` @@ -233,8 +202,7 @@ const notifyFollowingUsers = async (postId, groupId, context) => { return notificationTransactionResponse.records.map((record) => record.get('notification')) }) try { - const notifications = await writeTxResultPromise - return notifications + return await writeTxResultPromise } catch (error) { throw new Error(error) } finally { @@ -247,23 +215,25 @@ const notifyGroupMembersOfNewPost = async (postId, groupId, context) => { const reason = 'post_in_group' const cypher = ` MATCH (post:Post { id: $postId })<-[:WROTE]-(author:User { id: $userId }) + OPTIONAL MATCH (user)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress) MATCH (post)-[:IN]->(group:Group { id: $groupId })<-[membership:MEMBER_OF]-(user:User) WHERE NOT membership.role = 'pending' AND NOT (user)-[:MUTED]->(group) AND NOT (user)-[:MUTED]->(author) AND NOT (user)-[:BLOCKED]-(author) AND NOT user.id = $userId - WITH post, author, user + WITH post, author, user, emailAddress MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user) SET notification.read = FALSE SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime())) SET notification.updatedAt = toString(datetime()) - WITH notification, author, user, + WITH notification, author, user, emailAddress.email as email, post {.*, author: properties(author) } AS finalResource RETURN notification { .*, from: finalResource, to: properties(user), + email: email, relatedUser: properties(author) } ` @@ -278,8 +248,7 @@ const notifyGroupMembersOfNewPost = async (postId, groupId, context) => { return notificationTransactionResponse.records.map((record) => record.get('notification')) }) try { - const notifications = await writeTxResultPromise - return notifications + return await writeTxResultPromise } catch (error) { throw new Error(error) } finally { @@ -295,12 +264,13 @@ const notifyOwnersOfGroup = async (groupId, userId, reason, context) => { WITH owner, group, user, membership MERGE (group)-[notification:NOTIFIED {reason: $reason}]->(owner) WITH group, owner, notification, user, membership + OPTIONAL MATCH (owner)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress) SET notification.read = FALSE SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime())) SET notification.updatedAt = toString(datetime()) SET notification.relatedUserId = $userId - WITH owner, group { __typename: 'Group', .*, myRole: membership.roleInGroup } AS finalGroup, user, notification - RETURN notification {.*, from: finalGroup, to: properties(owner), relatedUser: properties(user) } + WITH owner, emailAddress.email as email, group { __typename: 'Group', .*, myRole: membership.roleInGroup } AS finalGroup, user, notification + RETURN notification {.*, from: finalGroup, to: properties(owner), email: email, relatedUser: properties(user) } ` const session = context.driver.session() const writeTxResultPromise = session.writeTransaction(async (transaction) => { @@ -312,8 +282,7 @@ const notifyOwnersOfGroup = async (groupId, userId, reason, context) => { return notificationTransactionResponse.records.map((record) => record.get('notification')) }) try { - const notifications = await writeTxResultPromise - return notifications + return await writeTxResultPromise } catch (error) { throw new Error(error) } finally { @@ -327,17 +296,18 @@ const notifyMemberOfGroup = async (groupId, userId, reason, context) => { MATCH (owner:User { id: $ownerId }) MATCH (user:User { id: $userId }) MATCH (group:Group { id: $groupId }) + OPTIONAL MATCH (user)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress) OPTIONAL MATCH (user)-[membership:MEMBER_OF]->(group) - WITH user, group, owner, membership + WITH user, group, owner, membership, emailAddress MERGE (group)-[notification:NOTIFIED {reason: $reason}]->(user) - WITH group, user, notification, owner, membership + WITH group, user, notification, owner, membership, emailAddress SET notification.read = FALSE SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime())) SET notification.updatedAt = toString(datetime()) SET notification.relatedUserId = $ownerId WITH group { __typename: 'Group', .*, myRole: membership.roleInGroup } AS finalGroup, - notification, user, owner - RETURN notification {.*, from: finalGroup, to: properties(user), relatedUser: properties(owner) } + notification, user, emailAddress.email as email, owner + RETURN notification {.*, from: finalGroup, to: properties(user), email: email, relatedUser: properties(owner) } ` const session = context.driver.session() const writeTxResultPromise = session.writeTransaction(async (transaction) => { @@ -350,8 +320,7 @@ const notifyMemberOfGroup = async (groupId, userId, reason, context) => { return notificationTransactionResponse.records.map((record) => record.get('notification')) }) try { - const notifications = await writeTxResultPromise - return notifications + return await writeTxResultPromise } catch (error) { throw new Error(error) } finally { @@ -371,11 +340,13 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => { WHERE user.id in $idsOfUsers AND NOT (user)-[:BLOCKED]-(author) AND NOT (user)-[:MUTED]->(author) + OPTIONAL MATCH (user)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress) OPTIONAL MATCH (post)-[:IN]->(group:Group) OPTIONAL MATCH (group)<-[membership:MEMBER_OF]-(user) - WITH post, author, user, group WHERE group IS NULL OR group.groupType = 'public' OR membership.role IN ['usual', 'admin', 'owner'] + WITH post, author, user, group, emailAddress + WHERE group IS NULL OR group.groupType = 'public' OR membership.role IN ['usual', 'admin', 'owner'] MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user) - WITH post AS resource, notification, user + WITH post AS resource, notification, user, emailAddress ` break } @@ -388,25 +359,27 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => { AND NOT (user)-[:BLOCKED]-(postAuthor) AND NOT (user)-[:MUTED]->(commenter) AND NOT (user)-[:MUTED]->(postAuthor) + OPTIONAL MATCH (user)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress) OPTIONAL MATCH (post)-[:IN]->(group:Group) OPTIONAL MATCH (group)<-[membership:MEMBER_OF]-(user) - WITH comment, user, group WHERE group IS NULL OR group.groupType = 'public' OR membership.role IN ['usual', 'admin', 'owner'] + WITH comment, user, group, emailAddress + WHERE group IS NULL OR group.groupType = 'public' OR membership.role IN ['usual', 'admin', 'owner'] MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user) - WITH comment AS resource, notification, user + WITH comment AS resource, notification, user, emailAddress ` break } } mentionedCypher += ` - WITH notification, user, resource, + WITH notification, user, resource, emailAddress, [(resource)<-[:WROTE]-(author:User) | author {.*}] AS authors, [(resource)-[:COMMENTS]->(post:Post)<-[:WROTE]-(author:User) | post{.*, author: properties(author)} ] AS posts - WITH resource, user, notification, authors, posts, + WITH resource, user, emailAddress.email as email, notification, authors, posts, resource {.*, __typename: [l IN labels(resource) WHERE l IN ['Post', 'Comment', 'Group']][0], author: authors[0], post: posts[0]} AS finalResource SET notification.read = FALSE SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime())) SET notification.updatedAt = toString(datetime()) - RETURN notification {.*, from: finalResource, to: properties(user), relatedUser: properties(user) } + RETURN notification {.*, from: finalResource, to: properties(user), email: email, relatedUser: properties(user) } ` const session = context.driver.session() const writeTxResultPromise = session.writeTransaction(async (transaction) => { @@ -418,8 +391,7 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => { return notificationTransactionResponse.records.map((record) => record.get('notification')) }) try { - const notifications = await writeTxResultPromise - return notifications + return await writeTxResultPromise } catch (error) { throw new Error(error) } finally { @@ -437,18 +409,20 @@ const notifyUsersOfComment = async (label, commentId, reason, context) => { WHERE NOT (observingUser)-[:BLOCKED]-(commenter) AND NOT (observingUser)-[:MUTED]->(commenter) AND NOT observingUser.id = $userId - WITH observingUser, post, comment, commenter + OPTIONAL MATCH (observingUser)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress) + WITH observingUser, emailAddress, post, comment, commenter MATCH (postAuthor:User)-[:WROTE]->(post) MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(observingUser) SET notification.read = FALSE SET notification.createdAt = COALESCE(notification.createdAt, toString(datetime())) SET notification.updatedAt = toString(datetime()) - WITH notification, observingUser, post, commenter, postAuthor, + WITH notification, observingUser, emailAddress.email as email, post, commenter, postAuthor, comment {.*, __typename: labels(comment)[0], author: properties(commenter), post: post {.*, author: properties(postAuthor) } } AS finalResource RETURN notification { .*, from: finalResource, to: properties(observingUser), + email: email, relatedUser: properties(commenter) } `, @@ -461,8 +435,7 @@ const notifyUsersOfComment = async (label, commentId, reason, context) => { return notificationTransactionResponse.records.map((record) => record.get('notification')) }) try { - const notifications = await writeTxResultPromise - return notifications + return await writeTxResultPromise } finally { session.close() } diff --git a/backend/src/middleware/orderByMiddleware.spec.ts b/backend/src/middleware/orderByMiddleware.spec.ts index 1ae15b26a..b98c21c0b 100644 --- a/backend/src/middleware/orderByMiddleware.spec.ts +++ b/backend/src/middleware/orderByMiddleware.spec.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' @@ -28,7 +26,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { diff --git a/backend/src/middleware/permissionsMiddleware.spec.ts b/backend/src/middleware/permissionsMiddleware.spec.ts index 834e9888a..ca45005fe 100644 --- a/backend/src/middleware/permissionsMiddleware.spec.ts +++ b/backend/src/middleware/permissionsMiddleware.spec.ts @@ -33,7 +33,7 @@ describe('authorization', () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) // TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 diff --git a/backend/src/middleware/permissionsMiddleware.ts b/backend/src/middleware/permissionsMiddleware.ts index 20063de11..3897a61e9 100644 --- a/backend/src/middleware/permissionsMiddleware.ts +++ b/backend/src/middleware/permissionsMiddleware.ts @@ -8,6 +8,7 @@ import { rule, shield, deny, allow, or, and } from 'graphql-shield' import CONFIG from '@config/index' import { getNeode } from '@db/neo4j' +import SocialMedia from '@models/SocialMedia' import { validateInviteCode } from '@schema/resolvers/transactions/inviteCodes' const debug = !!CONFIG.DEBUG @@ -48,15 +49,16 @@ const isMySocialMedia = rule({ if (!user) { return false } - let socialMedia = await neode.find('SocialMedia', args.id) + const socialMedia = await neode.find('SocialMedia', args.id) // Did we find a social media node? if (!socialMedia) { return false } - socialMedia = await socialMedia.toJson() // whats this for? + const socialMediaJson = await socialMedia.toJson() // whats this for? // Is it my social media entry? - return socialMedia.ownedBy.node.id === user.id + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (socialMediaJson.ownedBy as any).node.id === user.id }) const isAllowedToChangeGroupSettings = rule({ diff --git a/backend/src/middleware/slugifyMiddleware.spec.ts b/backend/src/middleware/slugifyMiddleware.spec.ts index 35247471c..75a52e4cf 100644 --- a/backend/src/middleware/slugifyMiddleware.spec.ts +++ b/backend/src/middleware/slugifyMiddleware.spec.ts @@ -42,7 +42,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { diff --git a/backend/src/middleware/softDelete/softDeleteMiddleware.spec.ts b/backend/src/middleware/softDelete/softDeleteMiddleware.spec.ts index 0264dedb9..ed9dcbf37 100644 --- a/backend/src/middleware/softDelete/softDeleteMiddleware.spec.ts +++ b/backend/src/middleware/softDelete/softDeleteMiddleware.spec.ts @@ -202,7 +202,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('softDeleteMiddleware', () => { diff --git a/backend/src/middleware/userInteractions.spec.ts b/backend/src/middleware/userInteractions.spec.ts index fc096c2b7..61d92ff83 100644 --- a/backend/src/middleware/userInteractions.spec.ts +++ b/backend/src/middleware/userInteractions.spec.ts @@ -46,7 +46,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('middleware/userInteractions', () => { diff --git a/backend/src/middleware/validation/validationMiddleware.spec.ts b/backend/src/middleware/validation/validationMiddleware.spec.ts index 3d3cd9bda..ea4f6ec54 100644 --- a/backend/src/middleware/validation/validationMiddleware.spec.ts +++ b/backend/src/middleware/validation/validationMiddleware.spec.ts @@ -79,7 +79,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { diff --git a/backend/src/models/Category.ts b/backend/src/models/Category.ts index f61d5aaab..9a3f47fd0 100644 --- a/backend/src/models/Category.ts +++ b/backend/src/models/Category.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { v4 as uuid } from 'uuid' export default { diff --git a/backend/src/models/Comment.ts b/backend/src/models/Comment.ts index f05cb7ccc..f4548f0c2 100644 --- a/backend/src/models/Comment.ts +++ b/backend/src/models/Comment.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { v4 as uuid } from 'uuid' export default { diff --git a/backend/src/models/Donations.ts b/backend/src/models/Donations.ts index 61113702d..742bfb569 100644 --- a/backend/src/models/Donations.ts +++ b/backend/src/models/Donations.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { v4 as uuid } from 'uuid' export default { diff --git a/backend/src/models/Group.ts b/backend/src/models/Group.ts index cff388a0a..a75ad518f 100644 --- a/backend/src/models/Group.ts +++ b/backend/src/models/Group.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { v4 as uuid } from 'uuid' export default { diff --git a/backend/src/models/Post.ts b/backend/src/models/Post.ts index 466e8a21d..75081b728 100644 --- a/backend/src/models/Post.ts +++ b/backend/src/models/Post.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { v4 as uuid } from 'uuid' export default { diff --git a/backend/src/models/Report.ts b/backend/src/models/Report.ts index 07e8a79c1..3e001746b 100644 --- a/backend/src/models/Report.ts +++ b/backend/src/models/Report.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { v4 as uuid } from 'uuid' export default { diff --git a/backend/src/models/SocialMedia.ts b/backend/src/models/SocialMedia.ts index 86f2f90be..6010c97bb 100644 --- a/backend/src/models/SocialMedia.ts +++ b/backend/src/models/SocialMedia.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { v4 as uuid } from 'uuid' export default { diff --git a/backend/src/models/User.spec.ts b/backend/src/models/User.spec.ts index 7d2c584b5..cea2d4db0 100644 --- a/backend/src/models/User.spec.ts +++ b/backend/src/models/User.spec.ts @@ -1,8 +1,6 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/restrict-template-expressions */ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ + import { cleanDatabase } from '@db/factories' import { getNeode, getDriver } from '@db/neo4j' @@ -15,7 +13,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) // TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 diff --git a/backend/src/models/User.ts b/backend/src/models/User.ts index 611b6a984..77a37c3c1 100644 --- a/backend/src/models/User.ts +++ b/backend/src/models/User.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { v4 as uuid } from 'uuid' export default { diff --git a/backend/src/schema/resolvers/badges.spec.ts b/backend/src/schema/resolvers/badges.spec.ts index 17fe46c99..e6b5173a9 100644 --- a/backend/src/schema/resolvers/badges.spec.ts +++ b/backend/src/schema/resolvers/badges.spec.ts @@ -33,7 +33,7 @@ describe('Badges', () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { @@ -100,6 +100,7 @@ describe('Badges', () => { id badgeVerification { id + isDefault } badgeTrophies { id @@ -204,7 +205,7 @@ describe('Badges', () => { data: { setVerificationBadge: { id: 'regular-user-id', - badgeVerification: { id: 'verification_moderator' }, + badgeVerification: { id: 'verification_moderator', isDefault: false }, badgeTrophies: [], }, }, @@ -226,7 +227,7 @@ describe('Badges', () => { data: { setVerificationBadge: { id: 'regular-user-id', - badgeVerification: { id: 'verification_admin' }, + badgeVerification: { id: 'verification_admin', isDefault: false }, badgeTrophies: [], }, }, @@ -255,7 +256,7 @@ describe('Badges', () => { data: { setVerificationBadge: { id: 'regular-user-2-id', - badgeVerification: { id: 'verification_moderator' }, + badgeVerification: { id: 'verification_moderator', isDefault: false }, badgeTrophies: [], }, }, @@ -299,6 +300,7 @@ describe('Badges', () => { id badgeVerification { id + isDefault } badgeTrophies { id @@ -403,7 +405,7 @@ describe('Badges', () => { data: { rewardTrophyBadge: { id: 'regular-user-id', - badgeVerification: null, + badgeVerification: { id: 'default_verification', isDefault: true }, badgeTrophies: [{ id: 'trophy_rhino' }], }, }, @@ -522,24 +524,30 @@ describe('Badges', () => { beforeEach(async () => { await regularUser.relateTo(badge, 'rewarded') await regularUser.relateTo(verification, 'verifies') + await regularUser.relateTo(badge, 'selected', { slot: 6 }) }) const revokeBadgeMutation = gql` mutation ($badgeId: ID!, $userId: ID!) { revokeBadge(badgeId: $badgeId, userId: $userId) { id - badgeVerification { - id - } badgeTrophies { id } + badgeVerification { + id + isDefault + } + badgeTrophiesSelected { + id + isDefault + } } } ` describe('check test setup', () => { - it('user has one badge', async () => { + it('user has one badge and has it selected', async () => { authenticatedUser = regularUser.toJson() const userQuery = gql` { @@ -548,11 +556,68 @@ describe('Badges', () => { badgeTrophies { id } + badgeVerification { + id + isDefault + } + badgeTrophiesSelected { + id + isDefault + } } } ` const expected = { - data: { User: [{ badgeTrophiesCount: 1, badgeTrophies: [{ id: 'trophy_rhino' }] }] }, + data: { + User: [ + { + badgeTrophiesCount: 1, + badgeTrophies: [{ id: 'trophy_rhino' }], + badgeVerification: { + id: 'verification_moderator', + isDefault: false, + }, + badgeTrophiesSelected: [ + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'trophy_rhino', + isDefault: false, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + ], + }, + ], + }, errors: undefined, } await expect(query({ query: userQuery })).resolves.toMatchObject(expected) @@ -596,8 +661,46 @@ describe('Badges', () => { data: { revokeBadge: { id: 'regular-user-id', - badgeVerification: { id: 'verification_moderator' }, + badgeVerification: { id: 'verification_moderator', isDefault: false }, badgeTrophies: [], + badgeTrophiesSelected: [ + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + ], }, }, errors: undefined, @@ -610,8 +713,46 @@ describe('Badges', () => { data: { revokeBadge: { id: 'regular-user-id', - badgeVerification: { id: 'verification_moderator' }, + badgeVerification: { id: 'verification_moderator', isDefault: false }, badgeTrophies: [], + badgeTrophiesSelected: [ + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + ], }, }, errors: undefined, @@ -631,8 +772,46 @@ describe('Badges', () => { data: { revokeBadge: { id: 'regular-user-id', - badgeVerification: null, + badgeVerification: { id: 'default_verification', isDefault: true }, badgeTrophies: [{ id: 'trophy_rhino' }], + badgeTrophiesSelected: [ + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'trophy_rhino', + isDefault: false, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + ], }, }, errors: undefined, @@ -659,8 +838,46 @@ describe('Badges', () => { data: { revokeBadge: { id: 'regular-user-id', - badgeVerification: null, + badgeVerification: { id: 'default_verification', isDefault: true }, badgeTrophies: [{ id: 'trophy_rhino' }], + badgeTrophiesSelected: [ + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'trophy_rhino', + isDefault: false, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + ], }, }, errors: undefined, diff --git a/backend/src/schema/resolvers/badges.ts b/backend/src/schema/resolvers/badges.ts index 430e3bf75..7c107e42c 100644 --- a/backend/src/schema/resolvers/badges.ts +++ b/backend/src/schema/resolvers/badges.ts @@ -6,6 +6,22 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { neo4jgraphql } from 'neo4j-graphql-js' +export const defaultTrophyBadge = { + id: 'default_trophy', + type: 'trophy', + icon: '/img/badges/default_trophy.svg', + description: '', + createdAt: '', +} + +export const defaultVerificationBadge = { + id: 'default_verification', + type: 'verification', + icon: '/img/badges/default_verification.svg', + description: '', + createdAt: '', +} + export default { Query: { Badge: async (object, args, context, resolveInfo) => @@ -103,8 +119,10 @@ export default { const response = await transaction.run( ` MATCH (user:User {id: $userId}) - OPTIONAL MATCH (badge:Badge {id: $badgeId})-[relation:REWARDED|VERIFIES]->(user) - DELETE relation + OPTIONAL MATCH (badge:Badge {id: $badgeId})-[rewarded:REWARDED|VERIFIES]->(user) + OPTIONAL MATCH (user)-[selected:SELECTED]->(badge) + DELETE rewarded + DELETE selected RETURN user {.*} `, { @@ -123,4 +141,8 @@ export default { } }, }, + Badge: { + isDefault: async (parent, _params, _context, _resolveInfo) => + [defaultTrophyBadge.id, defaultVerificationBadge.id].includes(parent.id), + }, } diff --git a/backend/src/schema/resolvers/comments.spec.ts b/backend/src/schema/resolvers/comments.spec.ts index 432723bae..a7177d754 100644 --- a/backend/src/schema/resolvers/comments.spec.ts +++ b/backend/src/schema/resolvers/comments.spec.ts @@ -30,7 +30,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { diff --git a/backend/src/schema/resolvers/donations.spec.ts b/backend/src/schema/resolvers/donations.spec.ts index 8e58153c1..8fc23d4e9 100644 --- a/backend/src/schema/resolvers/donations.spec.ts +++ b/backend/src/schema/resolvers/donations.spec.ts @@ -42,7 +42,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('donations', () => { diff --git a/backend/src/schema/resolvers/emails.spec.ts b/backend/src/schema/resolvers/emails.spec.ts index d16adbf5d..f77602463 100644 --- a/backend/src/schema/resolvers/emails.spec.ts +++ b/backend/src/schema/resolvers/emails.spec.ts @@ -36,7 +36,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { @@ -110,11 +110,14 @@ describe('AddEmailAddress', () => { it('connects `UnverifiedEmailAddress` to the authenticated user', async () => { await mutate({ mutation, variables }) - const result = await neode.cypher(` + const result = await neode.cypher( + ` MATCH(u:User)-[:PRIMARY_EMAIL]->(:EmailAddress {email: "user@example.org"}) MATCH(u:User)<-[:BELONGS_TO]-(e:UnverifiedEmailAddress {email: "new-email@example.org"}) RETURN e - `) + `, + {}, + ) const email = neode.hydrateFirst(result, 'e', neode.model('UnverifiedEmailAddress')) await expect(email.toJson()).resolves.toMatchObject({ email: 'new-email@example.org', @@ -257,10 +260,13 @@ describe('VerifyEmailAddress', () => { it('connects the new `EmailAddress` as PRIMARY', async () => { await mutate({ mutation, variables }) - const result = await neode.cypher(` + const result = await neode.cypher( + ` MATCH(u:User {id: "567"})-[:PRIMARY_EMAIL]->(e:EmailAddress {email: "to-be-verified@example.org"}) RETURN e - `) + `, + {}, + ) const email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress')) await expect(email.toJson()).resolves.toMatchObject({ email: 'to-be-verified@example.org', @@ -272,13 +278,13 @@ describe('VerifyEmailAddress', () => { MATCH(u:User {id: "567"})-[:PRIMARY_EMAIL]->(e:EmailAddress {email: "user@example.org"}) RETURN e ` - let result = await neode.cypher(cypherStatement) + let result = await neode.cypher(cypherStatement, {}) let email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress')) await expect(email.toJson()).resolves.toMatchObject({ email: 'user@example.org', }) await mutate({ mutation, variables }) - result = await neode.cypher(cypherStatement) + result = await neode.cypher(cypherStatement, {}) email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress')) await expect(email).toBe(false) }) @@ -288,13 +294,13 @@ describe('VerifyEmailAddress', () => { MATCH(u:User {id: "567"})<-[:BELONGS_TO]-(e:EmailAddress {email: "user@example.org"}) RETURN e ` - let result = await neode.cypher(cypherStatement) + let result = await neode.cypher(cypherStatement, {}) let email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress')) await expect(email.toJson()).resolves.toMatchObject({ email: 'user@example.org', }) await mutate({ mutation, variables }) - result = await neode.cypher(cypherStatement) + result = await neode.cypher(cypherStatement, {}) email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress')) await expect(email).toBe(false) }) @@ -319,10 +325,13 @@ describe('VerifyEmailAddress', () => { it('connects the new `EmailAddress` as PRIMARY', async () => { await mutate({ mutation, variables }) - const result = await neode.cypher(` + const result = await neode.cypher( + ` MATCH(u:User {id: "567"})-[:PRIMARY_EMAIL]->(e:EmailAddress {email: "to-be-verified@example.org"}) RETURN e - `) + `, + {}, + ) const email = neode.hydrateFirst(result, 'e', neode.model('EmailAddress')) await expect(email.toJson()).resolves.toMatchObject({ email: 'to-be-verified@example.org', diff --git a/backend/src/schema/resolvers/embeds.spec.ts b/backend/src/schema/resolvers/embeds.spec.ts index a3b33f81f..f6de4d13e 100644 --- a/backend/src/schema/resolvers/embeds.spec.ts +++ b/backend/src/schema/resolvers/embeds.spec.ts @@ -20,14 +20,17 @@ afterEach(() => { let variables = {} +// eslint-disable-next-line n/no-sync const HumanConnectionOrg = fs.readFileSync( path.join(__dirname, '../../../snapshots/embeds/HumanConnectionOrg.html'), 'utf8', ) +// eslint-disable-next-line n/no-sync const pr3934 = fs.readFileSync( path.join(__dirname, '../../../snapshots/embeds/pr3934.html'), 'utf8', ) +// eslint-disable-next-line n/no-sync const babyLovesCat = fs.readFileSync( path.join(__dirname, '../../../snapshots/embeds/babyLovesCat.html'), 'utf8', diff --git a/backend/src/schema/resolvers/embeds/findProvider.ts b/backend/src/schema/resolvers/embeds/findProvider.ts index 9be9732bf..6f5e0df90 100644 --- a/backend/src/schema/resolvers/embeds/findProvider.ts +++ b/backend/src/schema/resolvers/embeds/findProvider.ts @@ -8,6 +8,7 @@ import path from 'node:path' import { minimatch } from 'minimatch' +// eslint-disable-next-line n/no-sync let oEmbedProvidersFile = fs.readFileSync( path.join(__dirname, '../../../../public/providers.json'), 'utf8', diff --git a/backend/src/schema/resolvers/embeds/scraper.ts b/backend/src/schema/resolvers/embeds/scraper.ts index bcd25046b..a8cd07a76 100644 --- a/backend/src/schema/resolvers/embeds/scraper.ts +++ b/backend/src/schema/resolvers/embeds/scraper.ts @@ -87,8 +87,9 @@ export default async function scrape(url) { throw new ApolloError('Not found', 'NOT_FOUND') } - return { - type: 'link', - ...output, + if (!output.type) { + output.type = 'link' } + + return output } diff --git a/backend/src/schema/resolvers/filter-posts.spec.ts b/backend/src/schema/resolvers/filter-posts.spec.ts index 87ba2a8e5..c29b98365 100644 --- a/backend/src/schema/resolvers/filter-posts.spec.ts +++ b/backend/src/schema/resolvers/filter-posts.spec.ts @@ -38,7 +38,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('Filter Posts', () => { diff --git a/backend/src/schema/resolvers/follow.spec.ts b/backend/src/schema/resolvers/follow.spec.ts index 437b4e160..e846eb56f 100644 --- a/backend/src/schema/resolvers/follow.spec.ts +++ b/backend/src/schema/resolvers/follow.spec.ts @@ -76,7 +76,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { diff --git a/backend/src/schema/resolvers/follow.ts b/backend/src/schema/resolvers/follow.ts index d3c1a9081..d08f243b1 100644 --- a/backend/src/schema/resolvers/follow.ts +++ b/backend/src/schema/resolvers/follow.ts @@ -1,6 +1,7 @@ -/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ + /* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { getNeode } from '@db/neo4j' diff --git a/backend/src/schema/resolvers/groups.spec.ts b/backend/src/schema/resolvers/groups.spec.ts index 39ab87dd4..664f57397 100644 --- a/backend/src/schema/resolvers/groups.spec.ts +++ b/backend/src/schema/resolvers/groups.spec.ts @@ -241,7 +241,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('in mode', () => { diff --git a/backend/src/schema/resolvers/helpers/Resolver.ts b/backend/src/schema/resolvers/helpers/Resolver.ts index 2d7faa7b7..71d7602a4 100644 --- a/backend/src/schema/resolvers/helpers/Resolver.ts +++ b/backend/src/schema/resolvers/helpers/Resolver.ts @@ -7,8 +7,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable security/detect-object-injection */ -import log from './databaseLogger' - export const undefinedToNullResolver = (list) => { const resolvers = {} list.forEach((key) => { @@ -41,7 +39,6 @@ export default function Resolver(type, options: any = {}) { RETURN related {.*} as related ` const result = await txc.run(cypher, { id, cypherParams }) - log(result) return result.records.map((r) => r.get('related')) }) try { @@ -66,7 +63,6 @@ export default function Resolver(type, options: any = {}) { const nodeCondition = condition.replace('this', 'this {id: $id}') const cypher = `${nodeCondition} as ${key}` const result = await txc.run(cypher, { id, cypherParams }) - log(result) const [response] = result.records.map((r) => r.get(key)) return response }) @@ -93,7 +89,6 @@ export default function Resolver(type, options: any = {}) { RETURN COUNT(DISTINCT(related)) as count ` const result = await txc.run(cypher, { id, cypherParams }) - log(result) const [response] = result.records.map((r) => r.get('count').toNumber()) return response }) diff --git a/backend/src/schema/resolvers/helpers/databaseLogger.ts b/backend/src/schema/resolvers/helpers/databaseLogger.ts deleted file mode 100644 index ad1de5828..000000000 --- a/backend/src/schema/resolvers/helpers/databaseLogger.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable import/no-named-as-default */ -// eslint-disable-next-line import/no-extraneous-dependencies -import Debug from 'debug' - -const debugCypher = Debug('human-connection:neo4j:cypher') -const debugStats = Debug('human-connection:neo4j:stats') - -export default function log(response) { - const { counters, resultConsumedAfter, resultAvailableAfter, query } = response.summary - const { text, parameters } = query - debugCypher('%s', text) - debugCypher('%o', parameters) - debugStats('%o', counters) - debugStats('%o', { - resultConsumedAfter: resultConsumedAfter.toNumber(), - resultAvailableAfter: resultAvailableAfter.toNumber(), - }) -} diff --git a/backend/src/schema/resolvers/images/images.spec.ts b/backend/src/schema/resolvers/images/images.spec.ts index 0a2cbadbd..938571126 100644 --- a/backend/src/schema/resolvers/images/images.spec.ts +++ b/backend/src/schema/resolvers/images/images.spec.ts @@ -25,7 +25,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { @@ -83,7 +83,7 @@ describe('deleteImage', () => { return result }) } finally { - session.close() + await session.close() } await expect(neode.all('Image')).resolves.toHaveLength(0) await expect(someString).toEqual('Hello') @@ -106,7 +106,7 @@ describe('deleteImage', () => { await expect(neode.all('Image')).resolves.toHaveLength(1) // all good } finally { - session.close() + await session.close() } }) }) @@ -198,9 +198,10 @@ describe('mergeImage', () => { it('connects resource with image via given image type', async () => { await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }) - const result = await neode.cypher(` - MATCH(p:Post {id: "p99"})-[:HERO_IMAGE]->(i:Image) RETURN i,p - `) + const result = await neode.cypher( + `MATCH(p:Post {id: "p99"})-[:HERO_IMAGE]->(i:Image) RETURN i,p`, + {}, + ) post = neode.hydrateFirst(result, 'p', neode.model('Post')) const image = neode.hydrateFirst(result, 'i', neode.model('Image')) expect(post).toBeTruthy() @@ -215,7 +216,7 @@ describe('mergeImage', () => { it('sets metadata', async () => { await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }) - const image = await neode.first('Image', {}) + const image = await neode.first('Image', {}, undefined) await expect(image.toJson()).resolves.toMatchObject({ alt: 'A description of the new image', createdAt: expect.any(String), @@ -243,9 +244,13 @@ describe('mergeImage', () => { ) }) } finally { - session.close() + await session.close() } - const image = await neode.first('Image', { alt: 'This alt text gets overwritten' }) + const image = await neode.first( + 'Image', + { alt: 'This alt text gets overwritten' }, + undefined, + ) await expect(image.toJson()).resolves.toMatchObject({ alt: 'This alt text gets overwritten', }) @@ -268,7 +273,7 @@ describe('mergeImage', () => { await expect(neode.all('Image')).resolves.toHaveLength(0) // all good } finally { - session.close() + await session.close() } }) }) @@ -296,7 +301,7 @@ describe('mergeImage', () => { await expect(neode.all('Image')).resolves.toHaveLength(1) await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }) await expect(neode.all('Image')).resolves.toHaveLength(1) - const image = await neode.first('Image', {}) + const image = await neode.first('Image', {}, undefined) await expect(image.toJson()).resolves.toMatchObject({ alt: 'A description of the new image', createdAt: expect.any(String), diff --git a/backend/src/schema/resolvers/images/images.ts b/backend/src/schema/resolvers/images/images.ts index d8ce03758..2e76a7889 100644 --- a/backend/src/schema/resolvers/images/images.ts +++ b/backend/src/schema/resolvers/images/images.ts @@ -91,7 +91,7 @@ const wrapTransaction = async (wrappedCallback, args, opts) => { }) return result } finally { - session.close() + await session.close() } } @@ -152,6 +152,7 @@ const s3Upload = async ({ createReadStream, uniqueFilename, mimetype }) => { const localFileDelete = async (url) => { const location = `public${url}` + // eslint-disable-next-line n/no-sync if (existsSync(location)) unlinkSync(location) } diff --git a/backend/src/schema/resolvers/inviteCodes.spec.ts b/backend/src/schema/resolvers/inviteCodes.spec.ts index 13512a86c..f44721cc9 100644 --- a/backend/src/schema/resolvers/inviteCodes.spec.ts +++ b/backend/src/schema/resolvers/inviteCodes.spec.ts @@ -57,7 +57,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('inviteCodes', () => { diff --git a/backend/src/schema/resolvers/locations.spec.ts b/backend/src/schema/resolvers/locations.spec.ts index d4510284c..aed85da54 100644 --- a/backend/src/schema/resolvers/locations.spec.ts +++ b/backend/src/schema/resolvers/locations.spec.ts @@ -30,7 +30,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) // TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 diff --git a/backend/src/schema/resolvers/messages.spec.ts b/backend/src/schema/resolvers/messages.spec.ts index 7b46e0205..8061cf460 100644 --- a/backend/src/schema/resolvers/messages.spec.ts +++ b/backend/src/schema/resolvers/messages.spec.ts @@ -45,7 +45,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('Message', () => { diff --git a/backend/src/schema/resolvers/moderation.spec.ts b/backend/src/schema/resolvers/moderation.spec.ts index 67d070893..f3224421e 100644 --- a/backend/src/schema/resolvers/moderation.spec.ts +++ b/backend/src/schema/resolvers/moderation.spec.ts @@ -75,7 +75,7 @@ describe('moderate resources', () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { @@ -194,7 +194,7 @@ describe('moderate resources', () => { ]) const cypher = 'MATCH (:Report)<-[review:REVIEWED]-(moderator:User {id: "moderator-id"}) RETURN review' - const reviews = await neode.cypher(cypher) + const reviews = await neode.cypher(cypher, {}) expect(reviews.records).toHaveLength(1) }) diff --git a/backend/src/schema/resolvers/moderation.ts b/backend/src/schema/resolvers/moderation.ts index 6fe8637c6..bcdb3992a 100644 --- a/backend/src/schema/resolvers/moderation.ts +++ b/backend/src/schema/resolvers/moderation.ts @@ -2,8 +2,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import log from './helpers/databaseLogger' - export default { Mutation: { review: async (_object, params, context, _resolveInfo) => { @@ -31,7 +29,6 @@ export default { moderatorId: moderator.id, dateTime: new Date().toISOString(), }) - log(reviewTransactionResponse) return reviewTransactionResponse.records.map((record) => record.get('review')) }) const [reviewed] = await reviewWriteTxResultPromise diff --git a/backend/src/schema/resolvers/notifications.spec.ts b/backend/src/schema/resolvers/notifications.spec.ts index 2aebe4c24..d6d22e459 100644 --- a/backend/src/schema/resolvers/notifications.spec.ts +++ b/backend/src/schema/resolvers/notifications.spec.ts @@ -38,7 +38,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { diff --git a/backend/src/schema/resolvers/notifications.ts b/backend/src/schema/resolvers/notifications.ts index 6151d305e..0168558c3 100644 --- a/backend/src/schema/resolvers/notifications.ts +++ b/backend/src/schema/resolvers/notifications.ts @@ -8,8 +8,6 @@ import { withFilter } from 'graphql-subscriptions' import { pubsub, NOTIFICATION_ADDED } from '@src/server' -import log from './helpers/databaseLogger' - export default { Subscription: { notificationAdded: { @@ -76,7 +74,6 @@ export default { `, { id: currentUser.id }, ) - log(notificationsTransactionResponse) return notificationsTransactionResponse.records.map((record) => record.get('notification')) }) try { @@ -106,7 +103,6 @@ export default { `, { resourceId: args.id, id: currentUser.id }, ) - log(markNotificationAsReadTransactionResponse) return markNotificationAsReadTransactionResponse.records.map((record) => record.get('notification'), ) @@ -136,7 +132,6 @@ export default { `, { id: currentUser.id }, ) - log(markAllNotificationAsReadTransactionResponse) return markAllNotificationAsReadTransactionResponse.records.map((record) => record.get('notification'), ) diff --git a/backend/src/schema/resolvers/observePosts.spec.ts b/backend/src/schema/resolvers/observePosts.spec.ts index 9176d424e..76ad5b058 100644 --- a/backend/src/schema/resolvers/observePosts.spec.ts +++ b/backend/src/schema/resolvers/observePosts.spec.ts @@ -61,7 +61,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('observing posts', () => { diff --git a/backend/src/schema/resolvers/passwordReset.spec.ts b/backend/src/schema/resolvers/passwordReset.spec.ts index 59cf1cf4a..d5d08265c 100644 --- a/backend/src/schema/resolvers/passwordReset.spec.ts +++ b/backend/src/schema/resolvers/passwordReset.spec.ts @@ -22,6 +22,7 @@ let variables const getAllPasswordResets = async () => { const passwordResetQuery = await neode.cypher( 'MATCH (passwordReset:PasswordReset) RETURN passwordReset', + {}, ) const resets = passwordResetQuery.records.map((record) => record.get('passwordReset')) return resets @@ -44,7 +45,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(() => { diff --git a/backend/src/schema/resolvers/passwordReset.ts b/backend/src/schema/resolvers/passwordReset.ts index 44f3f51bd..f806f7249 100644 --- a/backend/src/schema/resolvers/passwordReset.ts +++ b/backend/src/schema/resolvers/passwordReset.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/await-thenable */ + /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ @@ -21,7 +21,7 @@ export default { resetPassword: async (_parent, { email, nonce, newPassword }, { driver }) => { const stillValid = new Date() stillValid.setDate(stillValid.getDate() - 1) - const encryptedNewPassword = await bcrypt.hashSync(newPassword, 10) + const encryptedNewPassword = await bcrypt.hash(newPassword, 10) const session = driver.session() try { const passwordResetTxPromise = session.writeTransaction(async (transaction) => { diff --git a/backend/src/schema/resolvers/posts.spec.ts b/backend/src/schema/resolvers/posts.spec.ts index 403db3950..0a05200fd 100644 --- a/backend/src/schema/resolvers/posts.spec.ts +++ b/backend/src/schema/resolvers/posts.spec.ts @@ -10,6 +10,7 @@ import CONFIG from '@config/index' import Factory, { cleanDatabase } from '@db/factories' import { getNeode, getDriver } from '@db/neo4j' import { createPostMutation } from '@graphql/queries/createPostMutation' +import Image from '@models/Image' import createServer from '@src/server' CONFIG.CATEGORIES_ACTIVE = true @@ -46,7 +47,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { @@ -974,9 +975,13 @@ describe('UpdatePost', () => { variables = { ...variables, image: { sensitive: true } } }) it('updates the image', async () => { - await expect(neode.first('Image', { sensitive: true })).resolves.toBeFalsy() + await expect( + neode.first('Image', { sensitive: true }, undefined), + ).resolves.toBeFalsy() await mutate({ mutation: updatePostMutation, variables }) - await expect(neode.first('Image', { sensitive: true })).resolves.toBeTruthy() + await expect( + neode.first('Image', { sensitive: true }, undefined), + ).resolves.toBeTruthy() }) }) @@ -996,9 +1001,13 @@ describe('UpdatePost', () => { delete variables.image }) it('keeps the image unchanged', async () => { - await expect(neode.first('Image', { sensitive: true })).resolves.toBeFalsy() + await expect( + neode.first('Image', { sensitive: true }, undefined), + ).resolves.toBeFalsy() await mutate({ mutation: updatePostMutation, variables }) - await expect(neode.first('Image', { sensitive: true })).resolves.toBeFalsy() + await expect( + neode.first('Image', { sensitive: true }, undefined), + ).resolves.toBeFalsy() }) }) }) @@ -1244,11 +1253,11 @@ describe('pin posts', () => { it('removes previous `pinned` attribute', async () => { const cypher = 'MATCH (post:Post) WHERE post.pinned IS NOT NULL RETURN post' - pinnedPost = await neode.cypher(cypher) + pinnedPost = await neode.cypher(cypher, {}) expect(pinnedPost.records).toHaveLength(1) variables = { ...variables, id: 'only-pinned-post' } await mutate({ mutation: pinPostMutation, variables }) - pinnedPost = await neode.cypher(cypher) + pinnedPost = await neode.cypher(cypher, {}) expect(pinnedPost.records).toHaveLength(1) }) @@ -1257,6 +1266,7 @@ describe('pin posts', () => { await mutate({ mutation: pinPostMutation, variables }) pinnedPost = await neode.cypher( `MATCH (:User)-[pinned:PINNED]->(post:Post) RETURN post, pinned`, + {}, ) expect(pinnedPost.records).toHaveLength(1) }) diff --git a/backend/src/schema/resolvers/postsInGroups.spec.ts b/backend/src/schema/resolvers/postsInGroups.spec.ts index 664a64b9f..7cb0bdc76 100644 --- a/backend/src/schema/resolvers/postsInGroups.spec.ts +++ b/backend/src/schema/resolvers/postsInGroups.spec.ts @@ -63,7 +63,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('Posts in Groups', () => { diff --git a/backend/src/schema/resolvers/registration.spec.ts b/backend/src/schema/resolvers/registration.spec.ts index 9a0e578cd..f19c6bf01 100644 --- a/backend/src/schema/resolvers/registration.spec.ts +++ b/backend/src/schema/resolvers/registration.spec.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ @@ -9,6 +8,8 @@ import gql from 'graphql-tag' import CONFIG from '@config/index' import Factory, { cleanDatabase } from '@db/factories' import { getDriver, getNeode } from '@db/neo4j' +import EmailAddress from '@models/EmailAddress' +import User from '@models/User' import createServer from '@src/server' const neode = getNeode() @@ -35,7 +36,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { @@ -97,17 +98,27 @@ describe('Signup', () => { describe('creates a EmailAddress node', () => { it('with `createdAt` attribute', async () => { await mutate({ mutation, variables }) - let emailAddress = await neode.first('EmailAddress', { email: 'someuser@example.org' }) - emailAddress = await emailAddress.toJson() - expect(emailAddress.createdAt).toBeTruthy() - expect(Date.parse(emailAddress.createdAt)).toEqual(expect.any(Number)) + const emailAddress = await neode.first( + 'EmailAddress', + { email: 'someuser@example.org' }, + undefined, + ) + const emailAddressJson = await emailAddress.toJson() + expect(emailAddressJson.createdAt).toBeTruthy() + expect(Date.parse(emailAddressJson.createdAt as unknown as string)).toEqual( + expect.any(Number), + ) }) it('with a cryptographic `nonce`', async () => { await mutate({ mutation, variables }) - let emailAddress = await neode.first('EmailAddress', { email: 'someuser@example.org' }) - emailAddress = await emailAddress.toJson() - expect(emailAddress.nonce).toEqual(expect.any(String)) + const emailAddress = await neode.first( + 'EmailAddress', + { email: 'someuser@example.org' }, + undefined, + ) + const emailAddressJson = await emailAddress.toJson() + expect(emailAddressJson.nonce).toEqual(expect.any(String)) }) describe('if the email already exists', () => { @@ -247,7 +258,11 @@ describe('SignupVerification', () => { it('sets `verifiedAt` attribute of EmailAddress', async () => { await mutate({ mutation, variables }) - const email = await neode.first('EmailAddress', { email: 'john@example.org' }) + const email = await neode.first( + 'EmailAddress', + { email: 'john@example.org' }, + undefined, + ) await expect(email.toJson()).resolves.toEqual( expect.objectContaining({ verifiedAt: expect.any(String), @@ -268,7 +283,7 @@ describe('SignupVerification', () => { it('sets `about` attribute of User', async () => { variables = { ...variables, about: 'Find this description in the user profile' } await mutate({ mutation, variables }) - const user = await neode.first('User', { name: 'John Doe' }) + const user = await neode.first('User', { name: 'John Doe' }, undefined) await expect(user.toJson()).resolves.toMatchObject({ about: 'Find this description in the user profile', }) diff --git a/backend/src/schema/resolvers/reports.spec.ts b/backend/src/schema/resolvers/reports.spec.ts index 4401329cb..bcbe1df4e 100644 --- a/backend/src/schema/resolvers/reports.spec.ts +++ b/backend/src/schema/resolvers/reports.spec.ts @@ -122,7 +122,7 @@ describe('file a report on a resource', () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) // TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 diff --git a/backend/src/schema/resolvers/reports.ts b/backend/src/schema/resolvers/reports.ts index 35e250f48..b8886c48f 100644 --- a/backend/src/schema/resolvers/reports.ts +++ b/backend/src/schema/resolvers/reports.ts @@ -3,8 +3,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import log from './helpers/databaseLogger' - export default { Mutation: { fileReport: async (_parent, params, context, _resolveInfo) => { @@ -33,7 +31,6 @@ export default { reasonDescription, }, ) - log(fileReportTransactionResponse) return fileReportTransactionResponse.records.map((record) => record.get('filedReport')) }) try { @@ -106,7 +103,6 @@ export default { ${offset} ${limit} `, ) - log(reportsTransactionResponse) return reportsTransactionResponse.records.map((record) => record.get('report')) }) try { @@ -131,7 +127,6 @@ export default { `, { id }, ) - log(filedReportsTransactionResponse) return filedReportsTransactionResponse.records.map((record) => ({ submitter: record.get('submitter').properties, filed: record.get('filed').properties, @@ -166,7 +161,6 @@ export default { `, { id }, ) - log(reviewedReportsTransactionResponse) return reviewedReportsTransactionResponse.records.map((record) => ({ review: record.get('review').properties, moderator: record.get('moderator').properties, diff --git a/backend/src/schema/resolvers/rooms.spec.ts b/backend/src/schema/resolvers/rooms.spec.ts index f374285e1..9a226a2f8 100644 --- a/backend/src/schema/resolvers/rooms.spec.ts +++ b/backend/src/schema/resolvers/rooms.spec.ts @@ -40,7 +40,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('Room', () => { diff --git a/backend/src/schema/resolvers/searches.spec.ts b/backend/src/schema/resolvers/searches.spec.ts index cb774cad5..8a94fbf21 100644 --- a/backend/src/schema/resolvers/searches.spec.ts +++ b/backend/src/schema/resolvers/searches.spec.ts @@ -30,7 +30,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() neode.close() }) diff --git a/backend/src/schema/resolvers/searches.ts b/backend/src/schema/resolvers/searches.ts index 845a070a5..34fc11709 100644 --- a/backend/src/schema/resolvers/searches.ts +++ b/backend/src/schema/resolvers/searches.ts @@ -4,7 +4,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/restrict-template-expressions */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ -import log from './helpers/databaseLogger' import { queryString } from './searches/queryString' // see http://lucene.apache.org/core/8_3_1/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#package.description @@ -133,7 +132,6 @@ const getSearchResults = async (context, setup, params, resultCallback = searchR const session = context.driver.session() try { const results = await searchResultPromise(session, setup, params) - log(results) return resultCallback(results) } finally { session.close() diff --git a/backend/src/schema/resolvers/shout.spec.ts b/backend/src/schema/resolvers/shout.spec.ts index 4ec6189bd..9023284c6 100644 --- a/backend/src/schema/resolvers/shout.spec.ts +++ b/backend/src/schema/resolvers/shout.spec.ts @@ -56,7 +56,7 @@ describe('shout and unshout posts', () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(async () => { diff --git a/backend/src/schema/resolvers/shout.ts b/backend/src/schema/resolvers/shout.ts index 0a7ec6a39..f0b5885eb 100644 --- a/backend/src/schema/resolvers/shout.ts +++ b/backend/src/schema/resolvers/shout.ts @@ -2,8 +2,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import log from './helpers/databaseLogger' - export default { Mutation: { shout: async (_object, params, context, _resolveInfo) => { @@ -25,7 +23,6 @@ export default { userId: context.user.id, }, ) - log(shoutTransactionResponse) return shoutTransactionResponse.records.map((record) => record.get('isShouted')) }) const [isShouted] = await shoutWriteTxResultPromise @@ -53,7 +50,6 @@ export default { userId: context.user.id, }, ) - log(unshoutTransactionResponse) return unshoutTransactionResponse.records.map((record) => record.get('isShouted')) }) const [isShouted] = await unshoutWriteTxResultPromise diff --git a/backend/src/schema/resolvers/socialMedia.spec.ts b/backend/src/schema/resolvers/socialMedia.spec.ts index 584e64cfb..168360a3b 100644 --- a/backend/src/schema/resolvers/socialMedia.spec.ts +++ b/backend/src/schema/resolvers/socialMedia.spec.ts @@ -18,7 +18,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('SocialMedia', () => { diff --git a/backend/src/schema/resolvers/socialMedia.ts b/backend/src/schema/resolvers/socialMedia.ts index d3a563d2c..952e4a27e 100644 --- a/backend/src/schema/resolvers/socialMedia.ts +++ b/backend/src/schema/resolvers/socialMedia.ts @@ -1,6 +1,7 @@ -/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ + /* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ + /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { getNeode } from '@db/neo4j' diff --git a/backend/src/schema/resolvers/statistics.spec.ts b/backend/src/schema/resolvers/statistics.spec.ts index 9d68b611f..50f124ac9 100644 --- a/backend/src/schema/resolvers/statistics.spec.ts +++ b/backend/src/schema/resolvers/statistics.spec.ts @@ -44,7 +44,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) // TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 diff --git a/backend/src/schema/resolvers/statistics.ts b/backend/src/schema/resolvers/statistics.ts index e2b93bbea..f7af390bf 100644 --- a/backend/src/schema/resolvers/statistics.ts +++ b/backend/src/schema/resolvers/statistics.ts @@ -3,8 +3,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable security/detect-object-injection */ -import log from './helpers/databaseLogger' - export default { Query: { statistics: async (_parent, _args, { driver }) => { @@ -28,7 +26,6 @@ export default { RETURN labels, relTypesCount `, ) - log(statisticsTransactionResponse) return statisticsTransactionResponse.records.map((record) => { return { ...record.get('labels'), diff --git a/backend/src/schema/resolvers/userData.spec.ts b/backend/src/schema/resolvers/userData.spec.ts index b3cd75694..17f1f4446 100644 --- a/backend/src/schema/resolvers/userData.spec.ts +++ b/backend/src/schema/resolvers/userData.spec.ts @@ -64,7 +64,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('resolvers/userData', () => { diff --git a/backend/src/schema/resolvers/user_management.spec.ts b/backend/src/schema/resolvers/user_management.spec.ts index 92832baa2..1029ab2b1 100644 --- a/backend/src/schema/resolvers/user_management.spec.ts +++ b/backend/src/schema/resolvers/user_management.spec.ts @@ -4,6 +4,8 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable promise/prefer-await-to-callbacks */ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable jest/unbound-method */ import { createTestClient } from 'apollo-server-testing' import gql from 'graphql-tag' import jwt from 'jsonwebtoken' @@ -56,7 +58,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(() => { @@ -160,8 +162,16 @@ describe('currentUser', () => { await respondsWith({ data: { currentUser: expect.objectContaining({ - activeCategories: [ + activeCategories: expect.arrayContaining([ 'cat1', + 'cat2', + 'cat3', + 'cat4', + 'cat5', + 'cat6', + 'cat7', + 'cat8', + 'cat9', 'cat10', 'cat11', 'cat12', @@ -172,15 +182,7 @@ describe('currentUser', () => { 'cat17', 'cat18', 'cat19', - 'cat2', - 'cat3', - 'cat4', - 'cat5', - 'cat6', - 'cat7', - 'cat8', - 'cat9', - ], + ]), }), }, }) @@ -272,7 +274,11 @@ describe('login', () => { describe('normalization', () => { describe('email address is a gmail address ', () => { beforeEach(async () => { - const email = await neode.first('EmailAddress', { email: 'test@example.org' }) + const email = await neode.first( + 'EmailAddress', + { email: 'test@example.org' }, + undefined, + ) await email.update({ email: 'someuser@gmail.com' }) }) diff --git a/backend/src/schema/resolvers/user_management.ts b/backend/src/schema/resolvers/user_management.ts index 13437e815..7bea1f53c 100644 --- a/backend/src/schema/resolvers/user_management.ts +++ b/backend/src/schema/resolvers/user_management.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/require-await */ /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/await-thenable */ + /* eslint-disable @typescript-eslint/no-unsafe-call */ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ @@ -12,7 +12,6 @@ import { neo4jgraphql } from 'neo4j-graphql-js' import { getNeode } from '@db/neo4j' import encode from '@jwt/encode' -import log from './helpers/databaseLogger' import normalizeEmail from './helpers/normalizeEmail' const neode = getNeode() @@ -38,13 +37,12 @@ export default { `, { userEmail: email }, ) - log(loginTransactionResponse) return loginTransactionResponse.records.map((record) => record.get('user')) }) const [currentUser] = await loginReadTxResultPromise if ( currentUser && - (await bcrypt.compareSync(password, currentUser.encryptedPassword)) && + (await bcrypt.compare(password, currentUser.encryptedPassword)) && !currentUser.disabled ) { delete currentUser.encryptedPassword @@ -61,16 +59,16 @@ export default { changePassword: async (_, { oldPassword, newPassword }, { user }) => { const currentUser = await neode.find('User', user.id) - const encryptedPassword = currentUser.get('encryptedPassword') - if (!(await bcrypt.compareSync(oldPassword, encryptedPassword))) { + const encryptedPassword = currentUser.get('encryptedPassword') + if (!(await bcrypt.compare(oldPassword, encryptedPassword))) { throw new AuthenticationError('Old password is not correct') } - if (await bcrypt.compareSync(newPassword, encryptedPassword)) { + if (await bcrypt.compare(newPassword, encryptedPassword)) { throw new AuthenticationError('Old password and new password should be different') } - const newEncryptedPassword = await bcrypt.hashSync(newPassword, 10) + const newEncryptedPassword = await bcrypt.hash(newPassword, 10) await currentUser.update({ encryptedPassword: newEncryptedPassword, updatedAt: new Date().toISOString(), diff --git a/backend/src/schema/resolvers/users.spec.ts b/backend/src/schema/resolvers/users.spec.ts index 0a74d46d3..ad37e2024 100644 --- a/backend/src/schema/resolvers/users.spec.ts +++ b/backend/src/schema/resolvers/users.spec.ts @@ -9,6 +9,7 @@ import gql from 'graphql-tag' import { categories } from '@constants/categories' import Factory, { cleanDatabase } from '@db/factories' import { getNeode, getDriver } from '@db/neo4j' +import User from '@models/User' import createServer from '@src/server' const categoryIds = ['cat9'] @@ -76,11 +77,12 @@ const updateOnlineStatus = gql` ` const setTrophyBadgeSelected = gql` - mutation ($slot: Int!, $badgeId: ID!) { + mutation ($slot: Int!, $badgeId: ID) { setTrophyBadgeSelected(slot: $slot, badgeId: $badgeId) { badgeTrophiesCount badgeTrophiesSelected { id + isDefault } badgeTrophiesUnused { id @@ -96,6 +98,7 @@ const resetTrophyBadgesSelected = gql` badgeTrophiesCount badgeTrophiesSelected { id + isDefault } badgeTrophiesUnused { id @@ -123,7 +126,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) // TODO: avoid database clean after each test in the future if possible for performance and flakyness reasons by filling the database step by step, see issue https://github.com/Ocelot-Social-Community/Ocelot-Social/issues/4543 @@ -1081,7 +1084,7 @@ describe('updateOnlineStatus', () => { const cypher = 'MATCH (u:User {id: $id}) RETURN u' const result = await neode.cypher(cypher, { id: authenticatedUser.id }) - const dbUser = neode.hydrateFirst(result, 'u', neode.model('User')) + const dbUser = neode.hydrateFirst(result, 'u', neode.model('User')) await expect(dbUser.toJson()).resolves.toMatchObject({ lastOnlineStatus: 'away', awaySince: expect.any(String), @@ -1242,15 +1245,40 @@ describe('setTrophyBadgeSelected', () => { badgeTrophiesSelected: [ { id: 'trophy_bear', + isDefault: false, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, }, - null, - null, - null, - null, - null, - null, - null, - null, ], badgeTrophiesUnused: [ { @@ -1275,17 +1303,40 @@ describe('setTrophyBadgeSelected', () => { badgeTrophiesSelected: [ { id: 'trophy_bear', + isDefault: false, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, }, - null, - null, - null, - null, { id: 'trophy_panda', + isDefault: false, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, }, - null, - null, - null, ], badgeTrophiesUnused: [], badgeTrophiesUnusedCount: 0, @@ -1294,6 +1345,141 @@ describe('setTrophyBadgeSelected', () => { }), ) }) + + describe('set badge to null or default', () => { + beforeEach(async () => { + await mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 0, badgeId: 'trophy_bear' }, + }) + await mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 5, badgeId: 'trophy_panda' }, + }) + }) + + it('returns the user with no badge set on the selected slot when sending null', async () => { + await expect( + mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 5, badgeId: null }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + setTrophyBadgeSelected: { + badgeTrophiesCount: 2, + badgeTrophiesSelected: [ + { + id: 'trophy_bear', + isDefault: false, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + ], + badgeTrophiesUnused: [ + { + id: 'trophy_panda', + }, + ], + badgeTrophiesUnusedCount: 1, + }, + }, + }), + ) + }) + + it('returns the user with no badge set on the selected slot when sending default_trophy', async () => { + await expect( + mutate({ + mutation: setTrophyBadgeSelected, + variables: { slot: 5, badgeId: 'default_trophy' }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + setTrophyBadgeSelected: { + badgeTrophiesCount: 2, + badgeTrophiesSelected: [ + { + id: 'trophy_bear', + isDefault: false, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + ], + badgeTrophiesUnused: [ + { + id: 'trophy_panda', + }, + ], + badgeTrophiesUnusedCount: 1, + }, + }, + }), + ) + }) + }) }) }) @@ -1364,15 +1550,52 @@ describe('resetTrophyBadgesSelected', () => { data: { resetTrophyBadgesSelected: { badgeTrophiesCount: 2, - badgeTrophiesSelected: [null, null, null, null, null, null, null, null, null], - badgeTrophiesUnused: [ + badgeTrophiesSelected: [ + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + { + id: 'default_trophy', + isDefault: true, + }, + ], + badgeTrophiesUnused: expect.arrayContaining([ { id: 'trophy_panda', }, { id: 'trophy_bear', }, - ], + ]), badgeTrophiesUnusedCount: 2, }, }, diff --git a/backend/src/schema/resolvers/users.ts b/backend/src/schema/resolvers/users.ts index 4f1fb6d5b..f549e79a3 100644 --- a/backend/src/schema/resolvers/users.ts +++ b/backend/src/schema/resolvers/users.ts @@ -11,7 +11,7 @@ import { neo4jgraphql } from 'neo4j-graphql-js' import { TROPHY_BADGES_SELECTED_MAX } from '@constants/badges' import { getNeode } from '@db/neo4j' -import log from './helpers/databaseLogger' +import { defaultTrophyBadge, defaultVerificationBadge } from './badges' import Resolver from './helpers/Resolver' import { mergeImage, deleteImage } from './images/images' import { createOrUpdateLocations } from './users/location' @@ -278,7 +278,6 @@ export default { `, { userId }, ) - log(deleteUserTransactionResponse) const [user] = deleteUserTransactionResponse.records.map((record) => record.get('user')) await deleteImage(user, 'AVATAR_IMAGE', { transaction }) return user @@ -404,17 +403,27 @@ export default { const session = context.driver.session() const query = session.writeTransaction(async (transaction) => { - const result = await transaction.run( - ` + const queryBadge = ` MATCH (user:User {id: $userId})<-[:REWARDED]-(badge:Badge {id: $badgeId}) OPTIONAL MATCH (user)-[badgeRelation:SELECTED]->(badge) OPTIONAL MATCH (user)-[slotRelation:SELECTED{slot: $slot}]->(:Badge) DELETE badgeRelation, slotRelation MERGE (user)-[:SELECTED{slot: toInteger($slot)}]->(badge) RETURN user {.*} - `, - { userId, badgeId, slot }, - ) + ` + const queryEmpty = ` + MATCH (user:User {id: $userId}) + OPTIONAL MATCH (user)-[slotRelation:SELECTED {slot: $slot}]->(:Badge) + DELETE slotRelation + RETURN user {.*} + ` + const isDefault = !badgeId || badgeId === defaultTrophyBadge.id + + const result = await transaction.run(isDefault ? queryEmpty : queryBadge, { + userId, + badgeId, + slot, + }) return result.records.map((record) => record.get('user'))[0] }) try { @@ -530,7 +539,7 @@ export default { }) try { const badgesSelected = await query - const result = Array(TROPHY_BADGES_SELECTED_MAX).fill(null) + const result = Array(TROPHY_BADGES_SELECTED_MAX).fill(defaultTrophyBadge) badgesSelected.map((record) => { result[record.get('slot')] = record.get('badge') return true @@ -542,21 +551,17 @@ export default { session.close() } }, - badgeTrophiesUnused: async (_parent, _params, context, _resolveInfo) => { - const { - user: { id: userId }, - } = context - + badgeTrophiesUnused: async (parent, _params, context, _resolveInfo) => { const session = context.driver.session() - const query = session.writeTransaction(async (transaction) => { + const query = session.readTransaction(async (transaction) => { const result = await transaction.run( ` - MATCH (user:User {id: $userId})<-[:REWARDED]-(badge:Badge) + MATCH (user:User {id: $parent.id})<-[:REWARDED]-(badge:Badge) WHERE NOT (user)-[:SELECTED]-(badge) RETURN badge {.*} `, - { userId }, + { parent }, ) return result.records.map((record) => record.get('badge')) }) @@ -568,21 +573,17 @@ export default { session.close() } }, - badgeTrophiesUnusedCount: async (_parent, _params, context, _resolveInfo) => { - const { - user: { id: userId }, - } = context - + badgeTrophiesUnusedCount: async (parent, _params, context, _resolveInfo) => { const session = context.driver.session() - const query = session.writeTransaction(async (transaction) => { + const query = session.readTransaction(async (transaction) => { const result = await transaction.run( ` - MATCH (user:User {id: $userId})<-[:REWARDED]-(badge:Badge) + MATCH (user:User {id: $parent.id})<-[:REWARDED]-(badge:Badge) WHERE NOT (user)-[:SELECTED]-(badge) RETURN toString(COUNT(badge)) as count `, - { userId }, + { parent }, ) return result.records.map((record) => record.get('count'))[0] }) @@ -594,6 +595,28 @@ export default { session.close() } }, + badgeVerification: async (parent, _params, context, _resolveInfo) => { + const session = context.driver.session() + + const query = session.writeTransaction(async (transaction) => { + const result = await transaction.run( + ` + MATCH (user:User {id: $parent.id})<-[:VERIFIES]-(verification:Badge) + RETURN verification {.*} + `, + { parent }, + ) + return result.records.map((record) => record.get('verification'))[0] + }) + try { + const result = await query + return result ?? defaultVerificationBadge + } catch (error) { + throw new Error(error) + } finally { + session.close() + } + }, ...Resolver('User', { undefinedToNull: [ 'actorId', @@ -634,7 +657,6 @@ export default { invitedBy: '<-[:INVITED]-(related:User)', location: '-[:IS_IN]->(related:Location)', redeemedInviteCode: '-[:REDEEMED]->(related:InviteCode)', - badgeVerification: '<-[:VERIFIES]-(related:Badge)', }, hasMany: { followedBy: '<-[:FOLLOWS]-(related:User)', diff --git a/backend/src/schema/resolvers/users/location.spec.ts b/backend/src/schema/resolvers/users/location.spec.ts index 9c3791e35..659c126dd 100644 --- a/backend/src/schema/resolvers/users/location.spec.ts +++ b/backend/src/schema/resolvers/users/location.spec.ts @@ -94,7 +94,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(() => { @@ -213,6 +213,7 @@ describe('userMiddleware', () => { await mutate({ mutation: updateUserMutation, variables }) const locations = await neode.cypher( `MATCH (city:Location)-[:IS_IN]->(district:Location)-[:IS_IN]->(state:Location)-[:IS_IN]->(country:Location) return city {.*}, state {.*}, country {.*}`, + {}, ) expect( locations.records.map((record) => { diff --git a/backend/src/schema/resolvers/users/location.ts b/backend/src/schema/resolvers/users/location.ts index d32c03cd2..6dfaede4e 100644 --- a/backend/src/schema/resolvers/users/location.ts +++ b/backend/src/schema/resolvers/users/location.ts @@ -8,17 +8,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable promise/avoid-new */ /* eslint-disable promise/prefer-await-to-callbacks */ -/* eslint-disable import/no-named-as-default */ import { UserInputError } from 'apollo-server' -// eslint-disable-next-line import/no-extraneous-dependencies -import Debug from 'debug' import request from 'request' import CONFIG from '@config/index' import asyncForEach from '@helpers/asyncForEach' -const debug = Debug('human-connection:location') - const fetch = (url) => { return new Promise((resolve, reject) => { request(url, function (error, response, body) { @@ -93,8 +88,6 @@ export const createOrUpdateLocations = async (nodeLabel, nodeId, locationName, s }&types=region,place,country,address&language=${locales.join(',')}`, ) - debug(res) - if (!res?.features?.[0]) { throw new UserInputError('locationName is invalid') } diff --git a/backend/src/schema/resolvers/users/mutedUsers.spec.ts b/backend/src/schema/resolvers/users/mutedUsers.spec.ts index 455672199..ccb6d2a87 100644 --- a/backend/src/schema/resolvers/users/mutedUsers.spec.ts +++ b/backend/src/schema/resolvers/users/mutedUsers.spec.ts @@ -23,7 +23,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) beforeEach(() => { diff --git a/backend/src/schema/resolvers/viewedTeaserCount.spec.ts b/backend/src/schema/resolvers/viewedTeaserCount.spec.ts index 9fb8a7eb9..f4fba31f8 100644 --- a/backend/src/schema/resolvers/viewedTeaserCount.spec.ts +++ b/backend/src/schema/resolvers/viewedTeaserCount.spec.ts @@ -32,7 +32,7 @@ beforeAll(async () => { afterAll(async () => { await cleanDatabase() - driver.close() + await driver.close() }) describe('count post teaser views', () => { diff --git a/backend/test/setup.ts b/backend/test/setup.ts index d2f24bd40..d1d32be5b 100644 --- a/backend/test/setup.ts +++ b/backend/test/setup.ts @@ -1,8 +1,2 @@ -// Polyfill missing encoders in jsdom -// https://stackoverflow.com/questions/68468203/why-am-i-getting-textencoder-is-not-defined-in-jest -import { TextEncoder, TextDecoder } from 'util' -global.TextEncoder = TextEncoder -global.TextDecoder = TextDecoder as any - // Metascraper takes longer nowadays, double time -jest.setTimeout(10000) \ No newline at end of file +jest.setTimeout(10000) diff --git a/backend/yarn.lock b/backend/yarn.lock index 3f0aa3990..c702695e1 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -1579,10 +1579,10 @@ url-regex "~4.1.1" video-extensions "~1.1.0" -"@napi-rs/wasm-runtime@^0.2.8": - version "0.2.8" - resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.8.tgz#642e8390ee78ed21d6b79c467aa610e249224ed6" - integrity sha512-OBlgKdX7gin7OIq4fadsjpg+cp2ZphvAIKucHsNfTdJiqdOmOEwQd/bHi0VwNrcw5xpBJyUw6cK/QilCqy1BSg== +"@napi-rs/wasm-runtime@^0.2.9": + version "0.2.9" + resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.9.tgz#7278122cf94f3b36d8170a8eee7d85356dfa6a96" + integrity sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg== dependencies: "@emnapi/core" "^1.4.0" "@emnapi/runtime" "^1.4.0" @@ -2061,6 +2061,11 @@ "@types/koa-compose" "*" "@types/node" "*" +"@types/lodash@^4.17.16": + version "4.17.16" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.16.tgz#94ae78fab4a38d73086e962d0b65c30d816bfb0a" + integrity sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g== + "@types/long@^4.0.0": version "4.0.1" resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" @@ -2079,10 +2084,10 @@ "@types/node" "*" form-data "^3.0.0" -"@types/node@*", "@types/node@>=6", "@types/node@^22.14.1": - version "22.14.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.14.1.tgz#53b54585cec81c21eee3697521e31312d6ca1e6f" - integrity sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw== +"@types/node@*", "@types/node@>=6", "@types/node@^22.15.2": + version "22.15.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.15.2.tgz#1db55aa64618ee93a58c8912f74beefe44aca905" + integrity sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A== dependencies: undici-types "~6.21.0" @@ -2126,6 +2131,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/uuid@~9.0.1": + version "9.0.8" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba" + integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA== + "@types/ws@^7.0.0": version "7.2.5" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-7.2.5.tgz#513f28b04a1ea1aa9dc2cad3f26e8e37c88aae49" @@ -2289,87 +2299,92 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== -"@unrs/resolver-binding-darwin-arm64@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.5.0.tgz#0c64ebe422a3d05ada91d8ba84e037383742c955" - integrity sha512-YmocNlEcX/AgJv8gI41bhjMOTcKcea4D2nRIbZj+MhRtSH5+vEU8r/pFuTuoF+JjVplLsBueU+CILfBPVISyGQ== +"@unrs/resolver-binding-darwin-arm64@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.7.0.tgz#04fe2253f2b6366ae993b1565c6495e563ad8a4c" + integrity sha512-vIWAU56r2lZAmUsljp6m9+hrTlwNkZH6pqnSPff2WxzofV+jWRSHLmZRUS+g+VE+LlyPByifmGGHpJmhWetatg== -"@unrs/resolver-binding-darwin-x64@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.5.0.tgz#57210874eca22ec3a07039c97c028fb19c0c6d57" - integrity sha512-qpUrXgH4e/0xu1LOhPEdfgSY3vIXOxDQv370NEL8npN8h40HcQDA+Pl2r4HBW6tTXezWIjxUFcP7tj529RZtDw== +"@unrs/resolver-binding-darwin-x64@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.7.0.tgz#8d74ee589f1c379b9b75880ea85871bdaf89766e" + integrity sha512-+bShFLgtdwuNteQbKq3X230754AouNMXSLDZ56EssgDyckDt6Ld7wRaJjZF0pY671HnY2pk9/amO4amAFzfN1A== -"@unrs/resolver-binding-freebsd-x64@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.5.0.tgz#4519371d0ad8e557a86623d8497e3abcdcb5ae43" - integrity sha512-3tX8r8vgjvZzaJZB4jvxUaaFCDCb3aWDCpZN3EjhGnnwhztslI05KSG5NY/jNjlcZ5QWZ7dEZZ/rNBFsmTaSPw== +"@unrs/resolver-binding-freebsd-x64@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.7.0.tgz#d0bcea8e240d54d048aa45a6c7bd7e4d4824abfb" + integrity sha512-HJjXb3aIptDZQ0saSmk2S4W1pWNVZ2iNpAbNGZOfsUXbi8xwCmHdVjErNS92hRp7djuDLup1OLrzOMtTdw5BmA== -"@unrs/resolver-binding-linux-arm-gnueabihf@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.5.0.tgz#4fc05aec9e65a6478003a0b9034a06ac0da886ab" - integrity sha512-FH+ixzBKaUU9fWOj3TYO+Yn/eO6kYvMLV9eNJlJlkU7OgrxkCmiMS6wUbyT0KA3FOZGxnEQ2z3/BHgYm2jqeLA== +"@unrs/resolver-binding-linux-arm-gnueabihf@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.7.0.tgz#ae56292948a47a876d894da740b8001a14c88bc3" + integrity sha512-NF3lk7KHulLD97UE+MHjH0mrOjeZG8Hz10h48YcFz2V0rlxBdRSRcMbGer8iH/1mIlLqxtvXJfGLUr4SMj0XZg== -"@unrs/resolver-binding-linux-arm-musleabihf@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.5.0.tgz#c24b35dd5818fcd25569425b1dc1a98a883e248b" - integrity sha512-pxCgXMgwB/4PfqFQg73lMhmWwcC0j5L+dNXhZoz/0ek0iS/oAWl65fxZeT/OnU7fVs52MgdP2q02EipqJJXHSg== +"@unrs/resolver-binding-linux-arm-musleabihf@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.7.0.tgz#4a32424660d2f0ed328297b24f46e64f4c2990d8" + integrity sha512-Gn1c/t24irDgU8yYj4vVG6qHplwUM42ti9/zYWgfmFjoXCH6L4Ab9hh6HuO7bfDSvGDRGWQt1IVaBpgbKHdh3Q== -"@unrs/resolver-binding-linux-arm64-gnu@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.5.0.tgz#07dc8478a0a356d343790208dc557d6d053689af" - integrity sha512-FX2FV7vpLE/+Z0NZX9/1pwWud5Wocm/2PgpUXbT5aSV3QEB10kBPJAzssOQylvdj8mOHoKl5pVkXpbCwww/T2g== +"@unrs/resolver-binding-linux-arm64-gnu@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.7.0.tgz#7b9d73558a2d85911c82314784edb89dcd0b274d" + integrity sha512-XRrVXRIUP++qyqAqgiXUpOv0GP3cHx7aA7NrzVFf6Cc8FoYuwtnmT+vctfSo4wRZN71MNU4xq2BEFxI4qvSerg== -"@unrs/resolver-binding-linux-arm64-musl@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.5.0.tgz#169e531731f7e462dffa410034a1d06a7a921aa8" - integrity sha512-+gF97xst1BZb28T3nwwzEtq2ewCoMDGKsenYsZuvpmNrW0019G1iUAunZN+FG55L21y+uP7zsGX06OXDQ/viKw== +"@unrs/resolver-binding-linux-arm64-musl@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.7.0.tgz#7d7305c5f5610744ef7a373d2a9022c922113568" + integrity sha512-Sligg+vTDAYTXkUtgviPjGEFIh57pkvlfdyRw21i9gkjp/eCNOAi2o5e7qLGTkoYdJHZJs5wVMViPEmAbw2/Tg== -"@unrs/resolver-binding-linux-ppc64-gnu@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.5.0.tgz#f6ad2ff47d74c8158b28a18536a71a8ecf84a17f" - integrity sha512-5bEmVcQw9js8JYM2LkUBw5SeELSIxX+qKf9bFrfFINKAp4noZ//hUxLpbF7u/3gTBN1GsER6xOzIZlw/VTdXtA== +"@unrs/resolver-binding-linux-ppc64-gnu@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.7.0.tgz#280e4846c3bd9b81fdda25ac3cdda203da9bfd20" + integrity sha512-Apek8/x+7Rg33zUJlQV44Bvq8/t1brfulk0veNJrk9wprF89bCYFMUHF7zQYcpf2u+m1+qs3mYQrBd43fGXhMA== -"@unrs/resolver-binding-linux-riscv64-gnu@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.5.0.tgz#2f3986cb44f285f90d27e87cee8b4059de3ffbdd" - integrity sha512-GGk/8TPUsf1Q99F+lzMdjE6sGL26uJCwQ9TlvBs8zR3cLQNw/MIumPN7zrs3GFGySjnwXc8gA6J3HKbejywmqA== +"@unrs/resolver-binding-linux-riscv64-gnu@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.7.0.tgz#63301787af18d158ab4e99ec5041f507da228721" + integrity sha512-kBale8CFX5clfV9VmI9EwKw2ZACMEx1ecjV92F9SeWTUoxl9d+LGzS6zMSX3kGYqcfJB3NXMwLCTwIDBLG1y4g== -"@unrs/resolver-binding-linux-s390x-gnu@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.5.0.tgz#813ea07833012bc34ecc59f023e422b421138761" - integrity sha512-5uRkFYYVNAeVaA4W/CwugjFN3iDOHCPqsBLCCOoJiMfFMMz4evBRsg+498OFa9w6VcTn2bD5aI+RRayaIgk2Sw== +"@unrs/resolver-binding-linux-riscv64-musl@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.7.0.tgz#b85d66b2c4d73fe335d448322c708448c4487c44" + integrity sha512-s/Q33xQjeFHSCvGl1sZztFZF6xhv7coMvFz6wa/x/ZlEArjiQoMMwGa/Aieq1Kp/6+S13iU3/IJF0ga6/451ow== -"@unrs/resolver-binding-linux-x64-gnu@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.5.0.tgz#18b0d7553268fa490db92be578ac4b0fd8cae049" - integrity sha512-j905CZH3nehYy6NimNqC2B14pxn4Ltd7guKMyPTzKehbFXTUgihQS/ZfHQTdojkMzbSwBOSgq1dOrY+IpgxDsA== +"@unrs/resolver-binding-linux-s390x-gnu@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.7.0.tgz#f4202f7bebd823e0744a785ac1426f07129a2f81" + integrity sha512-7PuNXAo97ydaxVNrIYJzPipvINJafDpB8pt5CoZHfu8BmqcU6d7kl6/SABTnqNffNkd6Cfhuo70jvGB2P7oJ/Q== -"@unrs/resolver-binding-linux-x64-musl@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.5.0.tgz#04541e98d16e358c695393251e365bc3d802dfa4" - integrity sha512-dmLevQTuzQRwu5A+mvj54R5aye5I4PVKiWqGxg8tTaYP2k2oTs/3Mo8mgnhPk28VoYCi0fdFYpgzCd4AJndQvQ== +"@unrs/resolver-binding-linux-x64-gnu@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.7.0.tgz#c3fa31d0b4cc49d54c956dec43bead5a0c4127cf" + integrity sha512-fNosEzDMYItA4It+R0tioHwKlEfx/3TkkJdP2x9B5o9R946NDC4ZZj5ZjA+Y4NQD2V/imB3QPAKmeh3vHQGQyA== -"@unrs/resolver-binding-wasm32-wasi@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.5.0.tgz#7a2ae7467c4c52d53c20ad7fc2bace1b23de8168" - integrity sha512-LtJMhwu7avhoi+kKfAZOKN773RtzLBVVF90YJbB0wyMpUj9yQPeA+mteVUI9P70OG/opH47FeV5AWeaNWWgqJg== +"@unrs/resolver-binding-linux-x64-musl@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.7.0.tgz#a447f7261958f688950be70a26b79a7955fb10d3" + integrity sha512-gHIw42dmnVcw7osjNPRybaXhONhggWkkzqiOZzXco1q3OKkn4KsbDylATeemnq3TP+L1BrzSqzl0H9UTJ6ji+w== + +"@unrs/resolver-binding-wasm32-wasi@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.7.0.tgz#fc9c486ffddef353daef71488f8f77e4de44dd8b" + integrity sha512-yq7POusv63/yTkNTaNsnXU/SAcBzckHyk1oYrDXqjS1m/goaWAaU9J9HrsovgTHkljxTcDd6PMAsJ5WZVBuGEQ== dependencies: - "@napi-rs/wasm-runtime" "^0.2.8" + "@napi-rs/wasm-runtime" "^0.2.9" -"@unrs/resolver-binding-win32-arm64-msvc@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.5.0.tgz#11deb282b8ce73fab26f1d04df0fa4d6363752c2" - integrity sha512-FTZBxLL4SO1mgIM86KykzJmPeTPisBDHQV6xtfDXbTMrentuZ6SdQKJUV5BWaoUK3p8kIULlrCcucqdCnk8Npg== +"@unrs/resolver-binding-win32-arm64-msvc@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.7.0.tgz#c316d889d29293faab926d1260b16a2d4c430ed4" + integrity sha512-/IPZPbdri9jglHonwB3F7EpQZvBK3ObH+g4ma/KDrqTEAECwvgE10Unvo0ox3LQFR/iMMAkVY+sGNMrMiIV/QQ== -"@unrs/resolver-binding-win32-ia32-msvc@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.5.0.tgz#2a5d414912379425bd395ea15901a5dd5febc7c1" - integrity sha512-i5bB7vJ1waUsFciU/FKLd4Zw0VnAkvhiJ4//jYQXyDUuiLKodmtQZVTcOPU7pp97RrNgCFtXfC1gnvj/DHPJTw== +"@unrs/resolver-binding-win32-ia32-msvc@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.7.0.tgz#14c9e08990dd0cf10d4962c40e9b368ea06b9789" + integrity sha512-NGVKbHEdrLuJdpcuGqV5zXO3v8t4CWOs0qeCGjO47RiwwufOi/yYcrtxtCzZAaMPBrffHL7c6tJ1Hxr17cPUGg== -"@unrs/resolver-binding-win32-x64-msvc@1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.5.0.tgz#5768c6bba4a27833a48a8a77e50eb01b520d0962" - integrity sha512-wAvXp4k7jhioi4SebXW/yfzzYwsUCr9kIX4gCsUFKpCTUf8Mi7vScJXI3S+kupSUf0LbVHudR8qBbe2wFMSNUw== +"@unrs/resolver-binding-win32-x64-msvc@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.7.0.tgz#c130ae8c0ce56dd1fe952d44fe95a6f9a91cccb6" + integrity sha512-Jf14pKofg58DIwcZv4Wt9AyVVe7bSJP8ODz+EP9nG/rho08FQzan0VOJk1g6/BNE1RkoYd+lRTWK+/BgH12qoQ== "@wry/context@^0.4.0": version "0.4.4" @@ -4581,10 +4596,10 @@ eslint-compat-utils@^0.5.1: dependencies: semver "^7.5.4" -eslint-config-prettier@^10.1.1: - version "10.1.1" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.1.tgz#cf0ff6e5c4e7e15f129f1f1ce2a5ecba92dec132" - integrity sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw== +eslint-config-prettier@^10.1.2: + version "10.1.2" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-10.1.2.tgz#31a4b393c40c4180202c27e829af43323bf85276" + integrity sha512-Epgp/EofAUeEpIdZkW60MHKvPyru1ruQJxPL+WIycnaPApuseK0Zpkrh/FwL9oIpQvIhJwV7ptOy0DWUjTlCiA== eslint-config-standard@^17.1.0: version "17.1.0" @@ -4600,17 +4615,17 @@ eslint-import-resolver-node@^0.3.9: is-core-module "^2.13.0" resolve "^1.22.4" -eslint-import-resolver-typescript@^4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.3.2.tgz#1d2371be6d073bade177ee04a4548dbacdc334c0" - integrity sha512-T2LqBXj87ndEC9t1LrDiPkzalSFzD4rrXr6BTzGdgMx1jdQM4T972guQvg7Ih+LNO51GURXI/qMHS5GF3h1ilw== +eslint-import-resolver-typescript@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.3.4.tgz#3d04161698925b5dc9c297966442c2761a319de4" + integrity sha512-buzw5z5VtiQMysYLH9iW9BV04YyZebsw+gPi+c4FCjfS9i6COYOrEWw9t3m3wA9PFBfqcBCqWf32qrXLbwafDw== dependencies: debug "^4.4.0" get-tsconfig "^4.10.0" is-bun-module "^2.0.0" stable-hash "^0.0.5" - tinyglobby "^0.2.12" - unrs-resolver "^1.4.1" + tinyglobby "^0.2.13" + unrs-resolver "^1.6.3" eslint-module-utils@^2.12.0: version "2.12.0" @@ -5043,10 +5058,10 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" -fdir@^6.4.3: - version "6.4.3" - resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.3.tgz#011cdacf837eca9b811c89dbb902df714273db72" - integrity sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw== +fdir@^6.4.4: + version "6.4.4" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.4.tgz#1cfcf86f875a883e19a8fab53622cfe992e8d2f9" + integrity sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg== file-entry-cache@^6.0.1: version "6.0.1" @@ -7556,14 +7571,7 @@ minimatch@^5.0.1: dependencies: brace-expansion "^2.0.1" -minimatch@^9.0.1, minimatch@^9.0.4: - version "9.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51" - integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw== - dependencies: - brace-expansion "^2.0.1" - -minimatch@^9.0.5: +minimatch@^9.0.1, minimatch@^9.0.4, minimatch@^9.0.5: version "9.0.5" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== @@ -7696,6 +7704,11 @@ nanoid@^3.3.6: resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== +napi-postinstall@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/napi-postinstall/-/napi-postinstall-0.1.6.tgz#7682101f43fc66c233b625ee8ebf07826c6eedde" + integrity sha512-w1bClprmjwpybo+7M1Rd0N4QK5Ein8kH/1CQ0Wv8Q9vrLbDMakxc4rZpv8zYc8RVErUELJlFhM8UzOF3IqlYKw== + natural-compare-lite@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz#17b09581988979fddafe0201e931ba933c96cbb4" @@ -7858,10 +7871,10 @@ nodemailer@^6.10.1: resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.10.1.tgz#cbc434c54238f83a51c07eabd04e2b3e832da623" integrity sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA== -nodemon@~3.1.9: - version "3.1.9" - resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.9.tgz#df502cdc3b120e1c3c0c6e4152349019efa7387b" - integrity sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg== +nodemon@~3.1.10: + version "3.1.10" + resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-3.1.10.tgz#5015c5eb4fffcb24d98cf9454df14f4fecec9bc1" + integrity sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw== dependencies: chokidar "^3.5.2" debug "^4" @@ -9573,12 +9586,12 @@ timers-ext@^0.1.7: es5-ext "^0.10.64" next-tick "^1.1.0" -tinyglobby@^0.2.12: - version "0.2.12" - resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.12.tgz#ac941a42e0c5773bd0b5d08f32de82e74a1a61b5" - integrity sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww== +tinyglobby@^0.2.13: + version "0.2.13" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.13.tgz#a0e46515ce6cbcd65331537e57484af5a7b2ff7e" + integrity sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw== dependencies: - fdir "^6.4.3" + fdir "^6.4.4" picomatch "^4.0.2" title@~3.4.1: @@ -10050,27 +10063,30 @@ unpipe@1.0.0, unpipe@~1.0.0: resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= -unrs-resolver@^1.4.1: - version "1.5.0" - resolved "https://registry.yarnpkg.com/unrs-resolver/-/unrs-resolver-1.5.0.tgz#d0a608f08321d8e90ba8eb10a3240e7995997275" - integrity sha512-6aia3Oy7SEe0MuUGQm2nsyob0L2+g57w178K5SE/3pvSGAIp28BB2O921fKx424Ahc/gQ6v0DXFbhcpyhGZdOA== +unrs-resolver@^1.6.3: + version "1.7.0" + resolved "https://registry.yarnpkg.com/unrs-resolver/-/unrs-resolver-1.7.0.tgz#2d1523d0a9c9271d0dc5b400520b776b947893ea" + integrity sha512-b76tVoT9KPniDY1GoYghDUQX20gjzXm/TONfHfgayLaiuo+oGyT9CsQkGCEJs+1/uryVBEOGOt3yYWDXbJhL7g== + dependencies: + napi-postinstall "^0.1.6" optionalDependencies: - "@unrs/resolver-binding-darwin-arm64" "1.5.0" - "@unrs/resolver-binding-darwin-x64" "1.5.0" - "@unrs/resolver-binding-freebsd-x64" "1.5.0" - "@unrs/resolver-binding-linux-arm-gnueabihf" "1.5.0" - "@unrs/resolver-binding-linux-arm-musleabihf" "1.5.0" - "@unrs/resolver-binding-linux-arm64-gnu" "1.5.0" - "@unrs/resolver-binding-linux-arm64-musl" "1.5.0" - "@unrs/resolver-binding-linux-ppc64-gnu" "1.5.0" - "@unrs/resolver-binding-linux-riscv64-gnu" "1.5.0" - "@unrs/resolver-binding-linux-s390x-gnu" "1.5.0" - "@unrs/resolver-binding-linux-x64-gnu" "1.5.0" - "@unrs/resolver-binding-linux-x64-musl" "1.5.0" - "@unrs/resolver-binding-wasm32-wasi" "1.5.0" - "@unrs/resolver-binding-win32-arm64-msvc" "1.5.0" - "@unrs/resolver-binding-win32-ia32-msvc" "1.5.0" - "@unrs/resolver-binding-win32-x64-msvc" "1.5.0" + "@unrs/resolver-binding-darwin-arm64" "1.7.0" + "@unrs/resolver-binding-darwin-x64" "1.7.0" + "@unrs/resolver-binding-freebsd-x64" "1.7.0" + "@unrs/resolver-binding-linux-arm-gnueabihf" "1.7.0" + "@unrs/resolver-binding-linux-arm-musleabihf" "1.7.0" + "@unrs/resolver-binding-linux-arm64-gnu" "1.7.0" + "@unrs/resolver-binding-linux-arm64-musl" "1.7.0" + "@unrs/resolver-binding-linux-ppc64-gnu" "1.7.0" + "@unrs/resolver-binding-linux-riscv64-gnu" "1.7.0" + "@unrs/resolver-binding-linux-riscv64-musl" "1.7.0" + "@unrs/resolver-binding-linux-s390x-gnu" "1.7.0" + "@unrs/resolver-binding-linux-x64-gnu" "1.7.0" + "@unrs/resolver-binding-linux-x64-musl" "1.7.0" + "@unrs/resolver-binding-wasm32-wasi" "1.7.0" + "@unrs/resolver-binding-win32-arm64-msvc" "1.7.0" + "@unrs/resolver-binding-win32-ia32-msvc" "1.7.0" + "@unrs/resolver-binding-win32-x64-msvc" "1.7.0" update-browserslist-db@^1.1.0: version "1.1.0" diff --git a/deployment/helm/charts/ocelot-neo4j/Chart.yaml b/deployment/helm/charts/ocelot-neo4j/Chart.yaml index 83fb561dc..e9f8e2354 100644 --- a/deployment/helm/charts/ocelot-neo4j/Chart.yaml +++ b/deployment/helm/charts/ocelot-neo4j/Chart.yaml @@ -21,4 +21,4 @@ version: 0.1.0 # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "3.3.0" +appVersion: "3.4.0" diff --git a/deployment/helm/charts/ocelot-social/Chart.yaml b/deployment/helm/charts/ocelot-social/Chart.yaml index d15c18a4a..2a872480e 100644 --- a/deployment/helm/charts/ocelot-social/Chart.yaml +++ b/deployment/helm/charts/ocelot-social/Chart.yaml @@ -21,4 +21,4 @@ version: 0.1.0 # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "3.3.0" +appVersion: "3.4.0" diff --git a/docker-compose.yml b/docker-compose.yml index d46b5cd29..8397c4e47 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -43,8 +43,6 @@ services: # Application only envs #- HOST=0.0.0.0 # This is nuxt specific, alternative value is HOST=webapp #- GRAPHQL_URI=http://backend:4000 - env_file: - - ./frontend/.env backend: image: ghcr.io/ocelot-social-community/ocelot-social/backend:${OCELOT_VERSION:-latest} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 63b54a127..4a8a0442c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "ocelot-social-frontend", - "version": "3.2.1", + "version": "3.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ocelot-social-frontend", - "version": "3.2.1", + "version": "3.4.0", "license": "Apache-2.0", "dependencies": { "@intlify/unplugin-vue-i18n": "^2.0.0", diff --git a/frontend/package.json b/frontend/package.json index b4e46da30..5e2235be5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "ocelot-social-frontend", - "version": "3.3.0", + "version": "3.4.0", "description": "ocelot.social new Frontend (in development and not fully implemented) by IT4C Boilerplate for frontends", "main": "build/index.js", "type": "module", diff --git a/package-lock.json b/package-lock.json index 4174bf891..17de00757 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "auto-changelog": "^2.5.0", "bcryptjs": "^3.0.2", "cross-env": "^7.0.3", - "cypress": "^14.3.1", + "cypress": "^14.3.2", "cypress-network-idle": "^1.15.0", "date-fns": "^3.6.0", "dotenv": "^16.5.0", @@ -7762,9 +7762,9 @@ "optional": true }, "node_modules/cypress": { - "version": "14.3.1", - "resolved": "https://registry.npmjs.org/cypress/-/cypress-14.3.1.tgz", - "integrity": "sha512-/2q06qvHMK3PNiadnRW1Je0lJ43gAFPQJUAK2zIxjr22kugtWxVQznTBLVu1AvRH+RP3oWZhCdWqiEi+0NuqCg==", + "version": "14.3.2", + "resolved": "https://registry.npmjs.org/cypress/-/cypress-14.3.2.tgz", + "integrity": "sha512-n+yGD2ZFFKgy7I3YtVpZ7BcFYrrDMcKj713eOZdtxPttpBjCyw/R8dLlFSsJPouneGN7A/HOSRyPJ5+3/gKDoA==", "dev": true, "hasInstallScript": true, "license": "MIT", diff --git a/package.json b/package.json index e86458b61..58d4fd768 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ocelot-social", - "version": "3.3.0", + "version": "3.4.0", "description": "Free and open source software program code available to run social networks.", "author": "ocelot.social Community", "license": "MIT", @@ -43,7 +43,7 @@ "auto-changelog": "^2.5.0", "bcryptjs": "^3.0.2", "cross-env": "^7.0.3", - "cypress": "^14.3.1", + "cypress": "^14.3.2", "cypress-network-idle": "^1.15.0", "date-fns": "^3.6.0", "dotenv": "^16.5.0", diff --git a/webapp/components/BadgeSelection.spec.js b/webapp/components/BadgeSelection.spec.js new file mode 100644 index 000000000..78f00b87a --- /dev/null +++ b/webapp/components/BadgeSelection.spec.js @@ -0,0 +1,76 @@ +import { render, screen, fireEvent } from '@testing-library/vue' +import BadgeSelection from './BadgeSelection.vue' + +const localVue = global.localVue + +describe('Badges.vue', () => { + const Wrapper = (propsData) => { + return render(BadgeSelection, { + propsData, + localVue, + }) + } + + describe('without badges', () => { + it('renders', () => { + const wrapper = Wrapper({ badges: [] }) + expect(wrapper.container).toMatchSnapshot() + }) + }) + + describe('with badges', () => { + const badges = [ + { + id: '1', + icon: '/path/to/some/icon', + isDefault: false, + description: 'Some description', + }, + { + id: '2', + icon: '/path/to/another/icon', + isDefault: true, + description: 'Another description', + }, + { + id: '3', + icon: '/path/to/third/icon', + isDefault: false, + description: 'Third description', + }, + ] + + let wrapper + + beforeEach(() => { + wrapper = Wrapper({ badges }) + }) + + it('renders', () => { + expect(wrapper.container).toMatchSnapshot() + }) + + describe('clicking on a badge', () => { + beforeEach(async () => { + const badge = screen.getByText(badges[1].description) + await fireEvent.click(badge) + }) + + it('emits badge-selected with badge', async () => { + expect(wrapper.emitted()['badge-selected']).toEqual([[badges[1]]]) + }) + }) + + describe('clicking twice on a badge', () => { + beforeEach(async () => { + const badge = screen.getByText(badges[1].description) + await fireEvent.click(badge) + await fireEvent.click(badge) + }) + + it('emits badge-selected with null', async () => { + expect(wrapper.emitted()['badge-selected']).toEqual([[badges[1]], [null]]) + }) + }) + }) +}) diff --git a/webapp/components/BadgeSelection.vue b/webapp/components/BadgeSelection.vue new file mode 100644 index 000000000..a6554d779 --- /dev/null +++ b/webapp/components/BadgeSelection.vue @@ -0,0 +1,102 @@ + + + + + diff --git a/webapp/components/Badges.spec.js b/webapp/components/Badges.spec.js index d19c2beb2..ae15e0b0a 100644 --- a/webapp/components/Badges.spec.js +++ b/webapp/components/Badges.spec.js @@ -1,29 +1,114 @@ -import { shallowMount } from '@vue/test-utils' +import { render, screen, fireEvent } from '@testing-library/vue' import Badges from './Badges.vue' +const localVue = global.localVue + describe('Badges.vue', () => { - let propsData + const Wrapper = (propsData) => { + return render(Badges, { + propsData, + localVue, + }) + } - beforeEach(() => { - propsData = {} - }) - - describe('shallowMount', () => { - const Wrapper = () => { - return shallowMount(Badges, { propsData }) - } - - it('has class "hc-badges"', () => { - expect(Wrapper().find('.hc-badges').exists()).toBe(true) + describe('without badges', () => { + it('renders in presentation mode', () => { + const wrapper = Wrapper({ badges: [], selectionMode: false }) + expect(wrapper.container).toMatchSnapshot() }) - describe('given a badge', () => { + it('renders in selection mode', () => { + const wrapper = Wrapper({ badges: [], selectionMode: true }) + expect(wrapper.container).toMatchSnapshot() + }) + }) + + describe('with badges', () => { + const badges = [ + { + id: '1', + icon: '/path/to/some/icon', + isDefault: false, + description: 'Some description', + }, + { + id: '2', + icon: '/path/to/another/icon', + isDefault: true, + description: 'Another description', + }, + { + id: '3', + icon: '/path/to/third/icon', + isDefault: false, + description: 'Third description', + }, + ] + + describe('in presentation mode', () => { + let wrapper + beforeEach(() => { - propsData.badges = [{ id: '1', icon: '/path/to/some/icon' }] + wrapper = Wrapper({ badges, scale: 1.2, selectionMode: false }) }) - it('proxies badge icon, which is just a URL without metadata', () => { - expect(Wrapper().find('img[src="/api/path/to/some/icon"]').exists()).toBe(true) + it('renders', () => { + expect(wrapper.container).toMatchSnapshot() + }) + + it('clicking on second badge does nothing', async () => { + const badge = screen.getByTitle(badges[1].description) + await fireEvent.click(badge) + expect(wrapper.emitted()).toEqual({}) + }) + }) + + describe('in selection mode', () => { + let wrapper + + beforeEach(() => { + wrapper = Wrapper({ badges, scale: 1.2, selectionMode: true }) + }) + + it('renders', () => { + expect(wrapper.container).toMatchSnapshot() + }) + + it('clicking on first badge does nothing', async () => { + const badge = screen.getByTitle(badges[0].description) + await fireEvent.click(badge) + expect(wrapper.emitted()).toEqual({}) + }) + + describe('clicking on second badge', () => { + beforeEach(async () => { + const badge = screen.getByTitle(badges[1].description) + await fireEvent.click(badge) + }) + + it('selects badge', () => { + expect(wrapper.container).toMatchSnapshot() + }) + + it('emits badge-selected with index', async () => { + expect(wrapper.emitted()['badge-selected']).toEqual([[1]]) + }) + }) + + describe('clicking twice on second badge', () => { + beforeEach(async () => { + const badge = screen.getByTitle(badges[1].description) + await fireEvent.click(badge) + await fireEvent.click(badge) + }) + + it('deselects badge', () => { + expect(wrapper.container).toMatchSnapshot() + }) + + it('emits badge-selected with null', async () => { + expect(wrapper.emitted()['badge-selected']).toEqual([[1], [null]]) + }) }) }) }) diff --git a/webapp/components/Badges.vue b/webapp/components/Badges.vue index d569452c7..0fdf065c3 100644 --- a/webapp/components/Badges.vue +++ b/webapp/components/Badges.vue @@ -1,69 +1,173 @@ diff --git a/webapp/components/__snapshots__/BadgeSelection.spec.js.snap b/webapp/components/__snapshots__/BadgeSelection.spec.js.snap new file mode 100644 index 000000000..a31d547c9 --- /dev/null +++ b/webapp/components/__snapshots__/BadgeSelection.spec.js.snap @@ -0,0 +1,84 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Badges.vue with badges renders 1`] = ` +
+
+ + + +
+
+`; + +exports[`Badges.vue without badges renders 1`] = ` +
+
+
+`; diff --git a/webapp/components/__snapshots__/Badges.spec.js.snap b/webapp/components/__snapshots__/Badges.spec.js.snap new file mode 100644 index 000000000..425d2ace7 --- /dev/null +++ b/webapp/components/__snapshots__/Badges.spec.js.snap @@ -0,0 +1,165 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Badges.vue with badges in presentation mode renders 1`] = ` +
+
+
+ +
+
+ +
+
+ +
+
+
+`; + +exports[`Badges.vue with badges in selection mode clicking on second badge selects badge 1`] = ` +
+
+
+ +
+ + +
+
+`; + +exports[`Badges.vue with badges in selection mode clicking twice on second badge deselects badge 1`] = ` +
+
+
+ +
+ + +
+
+`; + +exports[`Badges.vue with badges in selection mode renders 1`] = ` +
+
+
+ +
+ + +
+
+`; + +exports[`Badges.vue without badges renders in presentation mode 1`] = ` +
+
+
+`; + +exports[`Badges.vue without badges renders in selection mode 1`] = ` +
+
+
+`; diff --git a/webapp/components/_new/features/Admin/Badges/BadgesSection.spec.js b/webapp/components/_new/features/Admin/Badges/BadgesSection.spec.js index 8baddc692..8abf8d679 100644 --- a/webapp/components/_new/features/Admin/Badges/BadgesSection.spec.js +++ b/webapp/components/_new/features/Admin/Badges/BadgesSection.spec.js @@ -21,26 +21,41 @@ const badge2 = { describe('Admin/BadgesSection', () => { let wrapper - const Wrapper = () => { + const Wrapper = (withBadges = true) => { return render(BadgesSection, { localVue, propsData: { - badges: [badge1, badge2], + badges: withBadges ? [badge1, badge2] : [], + }, + mocks: { + $t: jest.fn((t) => t), }, }) } - beforeEach(() => { - wrapper = Wrapper() + describe('without badges', () => { + beforeEach(() => { + wrapper = Wrapper(false) + }) + + it('renders', () => { + expect(wrapper.baseElement).toMatchSnapshot() + }) }) - it('renders', () => { - expect(wrapper.baseElement).toMatchSnapshot() - }) + describe('with badges', () => { + beforeEach(() => { + wrapper = Wrapper(true) + }) - it('emits toggleButton', async () => { - const button = screen.getByAltText(badge1.description) - await fireEvent.click(button) - expect(wrapper.emitted().toggleBadge[0][0]).toEqual(badge1) + it('renders', () => { + expect(wrapper.baseElement).toMatchSnapshot() + }) + + it('emits toggleButton', async () => { + const button = screen.getByAltText(badge1.description) + await fireEvent.click(button) + expect(wrapper.emitted().toggleBadge[0][0]).toEqual(badge1) + }) }) }) diff --git a/webapp/components/_new/features/Admin/Badges/BadgesSection.vue b/webapp/components/_new/features/Admin/Badges/BadgesSection.vue index 8ff9da7ed..fc89d2a50 100644 --- a/webapp/components/_new/features/Admin/Badges/BadgesSection.vue +++ b/webapp/components/_new/features/Admin/Badges/BadgesSection.vue @@ -1,16 +1,19 @@ diff --git a/webapp/components/_new/features/Admin/Badges/__snapshots__/BadgesSection.spec.js.snap b/webapp/components/_new/features/Admin/Badges/__snapshots__/BadgesSection.spec.js.snap index c09a50725..a78f44edc 100644 --- a/webapp/components/_new/features/Admin/Badges/__snapshots__/BadgesSection.spec.js.snap +++ b/webapp/components/_new/features/Admin/Badges/__snapshots__/BadgesSection.spec.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Admin/BadgesSection renders 1`] = ` +exports[`Admin/BadgesSection with badges renders 1`] = `
`; + +exports[`Admin/BadgesSection without badges renders 1`] = ` + +
+
+

+ +

+ +
+ + admin.badges.noBadges + +
+
+
+ +`; diff --git a/webapp/config/index.js b/webapp/config/index.js index 5da17010b..fb275a8ec 100644 --- a/webapp/config/index.js +++ b/webapp/config/index.js @@ -35,6 +35,7 @@ const options = { 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 CATEGORIES_ACTIVE: process.env.CATEGORIES_ACTIVE === 'true' || false, + BADGES_ENABLED: process.env.BADGES_ENABLED === 'true' || false, } const CONFIG = { diff --git a/webapp/graphql/Fragments.js b/webapp/graphql/Fragments.js index d0ad8a0fe..77af830e8 100644 --- a/webapp/graphql/Fragments.js +++ b/webapp/graphql/Fragments.js @@ -26,9 +26,15 @@ export const locationFragment = (lang) => gql` export const badgesFragment = gql` fragment badges on User { - badgeTrophies { + badgeTrophiesSelected { id icon + description + } + badgeVerification { + id + icon + description } } ` diff --git a/webapp/graphql/User.js b/webapp/graphql/User.js index 147e93c6f..75342ef2a 100644 --- a/webapp/graphql/User.js +++ b/webapp/graphql/User.js @@ -405,6 +405,22 @@ export const currentUserQuery = gql` query { currentUser { ...user + badgeTrophiesSelected { + id + icon + description + isDefault + } + badgeTrophiesUnused { + id + icon + description + } + badgeVerification { + id + icon + description + } email role about @@ -466,3 +482,43 @@ export const userDataQuery = (i18n) => { } ` } + +export const setTrophyBadgeSelected = gql` + mutation ($slot: Int!, $badgeId: ID) { + setTrophyBadgeSelected(slot: $slot, badgeId: $badgeId) { + badgeTrophiesCount + badgeTrophiesSelected { + id + icon + description + isDefault + } + badgeTrophiesUnused { + id + icon + description + } + badgeTrophiesUnusedCount + } + } +` + +export const resetTrophyBadgesSelected = gql` + mutation { + resetTrophyBadgesSelected { + badgeTrophiesCount + badgeTrophiesSelected { + id + icon + description + isDefault + } + badgeTrophiesUnused { + id + icon + description + } + badgeTrophiesUnusedCount + } + } +` diff --git a/webapp/locales/de.json b/webapp/locales/de.json index 21590e556..71e792aa0 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -12,6 +12,7 @@ "admin": { "badges": { "description": "Stelle die verfügbaren Auszeichnungen für diesen Nutzer ein.", + "noBadges": "Keine Auszeichnungen vorhanden.", "revokeTrophy": { "error": "Trophäe konnte nicht widerrufen werden!", "success": "Trophäe erfolgreich widerrufen" @@ -97,8 +98,7 @@ "number": "Nr.", "role": "Rolle", "slug": "Alias" - }, - "edit": "Bearbeiten" + } } } }, @@ -957,6 +957,16 @@ "title": "Suchergebnisse" }, "settings": { + "badges": { + "click-to-select": "Klicke auf einen freien Platz, um eine Badge hinzufügen.", + "click-to-use": "Klicke auf eine Badge, um sie zu platzieren.", + "description": "Hier hast du die Möglichkeit zu entscheiden, wie deine bereits erworbenen Badges in deinem Profil gezeigt werden sollen.", + "name": "Badges", + "no-badges-available": "Im Moment stehen dir keine Badges zur Verfügung, die du hinzufügen könntest.", + "remove": "Badge entfernen", + "success-update": "Deine Badges wurden erfolgreich gespeichert.", + "verification": "Dies ist deine Verifikations-Badge und kann nicht geändert werden." + }, "blocked-users": { "block": "Nutzer blockieren", "columns": { diff --git a/webapp/locales/en.json b/webapp/locales/en.json index 8d270349e..6b0de56c9 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -12,6 +12,7 @@ "admin": { "badges": { "description": "Configure the available badges for this user", + "noBadges": "There are no badges available", "revokeTrophy": { "error": "Trophy could not be revoked!", "success": "Trophy successfully revoked!" @@ -97,8 +98,7 @@ "number": "No.", "role": "Role", "slug": "Slug" - }, - "edit": "Edit" + } } } }, @@ -957,6 +957,16 @@ "title": "Search Results" }, "settings": { + "badges": { + "click-to-select": "Click on an empty space to add a badge.", + "click-to-use": "Click on a badge to use it in the selected slot.", + "description": "Here you can choose how to display your earned badges on your profile.", + "name": "Badges", + "no-badges-available": "You currently don't have any badges available to add.", + "remove": "Remove Badge", + "success-update": "Your badges have been updated successfully.", + "verification": "This is your verification badge and cannot be changed." + }, "blocked-users": { "block": "Block user", "columns": { diff --git a/webapp/locales/es.json b/webapp/locales/es.json index 31f2cc5f4..15096b9d8 100644 --- a/webapp/locales/es.json +++ b/webapp/locales/es.json @@ -12,6 +12,7 @@ "admin": { "badges": { "description": null, + "noBadges": null, "revokeTrophy": { "error": null, "success": null @@ -97,8 +98,7 @@ "number": "No.", "role": "Rol", "slug": "Alias" - }, - "edit": null + } } } }, @@ -957,6 +957,16 @@ "title": null }, "settings": { + "badges": { + "click-to-select": null, + "click-to-use": null, + "description": null, + "name": null, + "no-badges-available": null, + "remove": null, + "success-update": null, + "verification": null + }, "blocked-users": { "block": "Bloquear usuario", "columns": { diff --git a/webapp/locales/fr.json b/webapp/locales/fr.json index 4bbca2b82..2da2a9801 100644 --- a/webapp/locales/fr.json +++ b/webapp/locales/fr.json @@ -12,6 +12,7 @@ "admin": { "badges": { "description": null, + "noBadges": null, "revokeTrophy": { "error": null, "success": null @@ -97,8 +98,7 @@ "number": "Num.", "role": "Rôle", "slug": "Slug" - }, - "edit": null + } } } }, @@ -957,6 +957,16 @@ "title": null }, "settings": { + "badges": { + "click-to-select": null, + "click-to-use": null, + "description": null, + "name": null, + "no-badges-available": null, + "remove": null, + "success-update": null, + "verification": null + }, "blocked-users": { "block": "Bloquer l'utilisateur", "columns": { diff --git a/webapp/locales/it.json b/webapp/locales/it.json index 21bfaa859..485abff3a 100644 --- a/webapp/locales/it.json +++ b/webapp/locales/it.json @@ -12,6 +12,7 @@ "admin": { "badges": { "description": null, + "noBadges": null, "revokeTrophy": { "error": null, "success": null @@ -97,8 +98,7 @@ "number": null, "role": null, "slug": null - }, - "edit": null + } } } }, @@ -957,6 +957,16 @@ "title": null }, "settings": { + "badges": { + "click-to-select": null, + "click-to-use": null, + "description": null, + "name": null, + "no-badges-available": null, + "remove": null, + "success-update": null, + "verification": null + }, "blocked-users": { "block": null, "columns": { diff --git a/webapp/locales/nl.json b/webapp/locales/nl.json index f67518c21..40f9aca2e 100644 --- a/webapp/locales/nl.json +++ b/webapp/locales/nl.json @@ -12,6 +12,7 @@ "admin": { "badges": { "description": null, + "noBadges": null, "revokeTrophy": { "error": null, "success": null @@ -97,8 +98,7 @@ "number": null, "role": null, "slug": null - }, - "edit": null + } } } }, @@ -957,6 +957,16 @@ "title": null }, "settings": { + "badges": { + "click-to-select": null, + "click-to-use": null, + "description": null, + "name": null, + "no-badges-available": null, + "remove": null, + "success-update": null, + "verification": null + }, "blocked-users": { "block": null, "columns": { diff --git a/webapp/locales/pl.json b/webapp/locales/pl.json index 4c6a96a5f..ee332b84b 100644 --- a/webapp/locales/pl.json +++ b/webapp/locales/pl.json @@ -12,6 +12,7 @@ "admin": { "badges": { "description": null, + "noBadges": null, "revokeTrophy": { "error": null, "success": null @@ -97,8 +98,7 @@ "number": null, "role": null, "slug": null - }, - "edit": null + } } } }, @@ -957,6 +957,16 @@ "title": null }, "settings": { + "badges": { + "click-to-select": null, + "click-to-use": null, + "description": null, + "name": null, + "no-badges-available": null, + "remove": null, + "success-update": null, + "verification": null + }, "blocked-users": { "block": null, "columns": { diff --git a/webapp/locales/pt.json b/webapp/locales/pt.json index 7d5ad52c1..54f9b5d99 100644 --- a/webapp/locales/pt.json +++ b/webapp/locales/pt.json @@ -12,6 +12,7 @@ "admin": { "badges": { "description": null, + "noBadges": null, "revokeTrophy": { "error": null, "success": null @@ -97,8 +98,7 @@ "number": "N.º", "role": "Função", "slug": "Slug" - }, - "edit": null + } } } }, @@ -957,6 +957,16 @@ "title": null }, "settings": { + "badges": { + "click-to-select": null, + "click-to-use": null, + "description": null, + "name": null, + "no-badges-available": null, + "remove": null, + "success-update": null, + "verification": null + }, "blocked-users": { "block": "Bloquear usuário", "columns": { diff --git a/webapp/locales/ru.json b/webapp/locales/ru.json index 3a394d6ff..4d2e2a357 100644 --- a/webapp/locales/ru.json +++ b/webapp/locales/ru.json @@ -12,6 +12,7 @@ "admin": { "badges": { "description": null, + "noBadges": null, "revokeTrophy": { "error": null, "success": null @@ -97,8 +98,7 @@ "number": "№", "role": "Роль", "slug": "Алиас" - }, - "edit": null + } } } }, @@ -957,6 +957,16 @@ "title": null }, "settings": { + "badges": { + "click-to-select": null, + "click-to-use": null, + "description": null, + "name": null, + "no-badges-available": null, + "remove": null, + "success-update": null, + "verification": null + }, "blocked-users": { "block": "Блокировать", "columns": { diff --git a/webapp/maintenance/source/package.json b/webapp/maintenance/source/package.json index 1aa029eb4..df76a77ca 100644 --- a/webapp/maintenance/source/package.json +++ b/webapp/maintenance/source/package.json @@ -1,6 +1,6 @@ { "name": "@ocelot-social/maintenance", - "version": "3.3.0", + "version": "3.4.0", "description": "Maintenance page for ocelot.social", "repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social", "author": "ocelot.social Community", diff --git a/webapp/nuxt.config.js b/webapp/nuxt.config.js index 07cfa6bc4..263c3f149 100644 --- a/webapp/nuxt.config.js +++ b/webapp/nuxt.config.js @@ -207,15 +207,6 @@ export default { 'X-API-TOKEN': CONFIG.BACKEND_TOKEN, }, }, - '/img': { - // make this configurable (nuxt-dotenv) - target: CONFIG.GRAPHQL_URI, - toProxy: true, // cloudflare needs that - headers: { - 'X-UI-Request': true, - 'X-API-TOKEN': CONFIG.BACKEND_TOKEN, - }, - }, }, // Give apollo module options diff --git a/webapp/package.json b/webapp/package.json index d18a88408..574c94378 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -1,6 +1,6 @@ { "name": "ocelot-social-webapp", - "version": "3.3.0", + "version": "3.4.0", "description": "ocelot.social Frontend", "repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social", "author": "ocelot.social Community", @@ -79,6 +79,7 @@ "@storybook/addon-actions": "^5.3.21", "@storybook/addon-notes": "^5.3.18", "@storybook/vue": "~7.4.0", + "@testing-library/jest-dom": "^6.6.3", "@testing-library/vue": "5", "@vue/cli-shared-utils": "~4.3.1", "@vue/eslint-config-prettier": "~6.0.0", diff --git a/webapp/pages/__snapshots__/settings.spec.js.snap b/webapp/pages/__snapshots__/settings.spec.js.snap new file mode 100644 index 000000000..6672e41af --- /dev/null +++ b/webapp/pages/__snapshots__/settings.spec.js.snap @@ -0,0 +1,427 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`settings.vue given badges are disabled renders 1`] = ` +
+
+
+

+ +

+
+ +
+ +
+ + +
+ + + +
+
+
+
+`; + +exports[`settings.vue given badges are enabled renders 1`] = ` +
+
+
+

+ +

+
+ +
+ +
+ + +
+ + + +
+
+
+
+`; diff --git a/webapp/pages/admin/users/__snapshots__/index.spec.js.snap b/webapp/pages/admin/users/__snapshots__/index.spec.js.snap new file mode 100644 index 000000000..0fff4016b --- /dev/null +++ b/webapp/pages/admin/users/__snapshots__/index.spec.js.snap @@ -0,0 +1,867 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Users given badges are disabled renders 1`] = ` +
+
+

+ admin.users.name +

+ +
+
+
+
+ +
+
+ + + + + +
+ + +
+ + + +
+
+ +
+ +
+
+
+ + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + admin.users.table.columns.number + + + + admin.users.table.columns.name + + + + admin.users.table.columns.email + + + + admin.users.table.columns.slug + + + + admin.users.table.columns.createdAt + + + + 🖉 + + + + 🗨 + + + + ❤ + + + + admin.users.table.columns.role + +
+ NaN. + + + + User + + + + + + user@example.org + + + + + + user + + + + + + + + + + + + + + + + + + + +
+ NaN. + + + + User + + + + + + user2@example.org + + + + + + user + + + + + + + + + + + + + + + + + + + +
+
+ +
+ + + + + +
+ + +
+
+`; + +exports[`Users given badges are enabled renders 1`] = ` +
+
+

+ admin.users.name +

+ +
+
+
+
+ +
+
+ + + + + +
+ + +
+ + + +
+
+ +
+ +
+
+
+ + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + admin.users.table.columns.number + + + + admin.users.table.columns.name + + + + admin.users.table.columns.email + + + + admin.users.table.columns.slug + + + + admin.users.table.columns.createdAt + + + + 🖉 + + + + 🗨 + + + + ❤ + + + + admin.users.table.columns.role + + + + admin.users.table.columns.badges + +
+ NaN. + + + + User + + + + + + user@example.org + + + + + + user + + + + + + + + + + + + + + + + + + + + + + + +
+ NaN. + + + + User + + + + + + user2@example.org + + + + + + user + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ + + + + +
+ + +
+
+`; diff --git a/webapp/pages/admin/users/_id.spec.js b/webapp/pages/admin/users/_id.spec.js index 933de58de..d38b13022 100644 --- a/webapp/pages/admin/users/_id.spec.js +++ b/webapp/pages/admin/users/_id.spec.js @@ -58,6 +58,11 @@ describe('.vue', () => { query: jest.fn(), }, mutate: jest.fn(), + queries: { + Badge: { + loading: false, + }, + }, }, $toast: { success: jest.fn(), diff --git a/webapp/pages/admin/users/_id.vue b/webapp/pages/admin/users/_id.vue index 808e1653a..a6c4dafaa 100644 --- a/webapp/pages/admin/users/_id.vue +++ b/webapp/pages/admin/users/_id.vue @@ -8,7 +8,7 @@ {{ $t('admin.badges.description') }} - + userBadge.id === badge.id), })) }, + isLoadingBadges() { + return this.$apollo.queries.Badge.loading + }, }, methods: { toggleBadge(badge) { diff --git a/webapp/pages/admin/users/index.spec.js b/webapp/pages/admin/users/index.spec.js index 8d6b923c5..85e8789b8 100644 --- a/webapp/pages/admin/users/index.spec.js +++ b/webapp/pages/admin/users/index.spec.js @@ -10,11 +10,9 @@ const stubs = { describe('Users', () => { let wrapper - let Wrapper - let getters const mocks = { - $t: jest.fn(), + $t: jest.fn((t) => t), $apollo: { loading: false, mutate: jest @@ -38,116 +36,154 @@ describe('Users', () => { }, } - describe('mount', () => { - getters = { - 'auth/isAdmin': () => true, - 'auth/user': () => { - return { id: 'admin' } - }, - } + const getters = { + 'auth/isAdmin': () => true, + 'auth/user': () => { + return { id: 'admin' } + }, + } - Wrapper = () => { - const store = new Vuex.Store({ getters }) - return mount(Users, { - mocks, - localVue, - store, - stubs, - }) - } + const Wrapper = () => { + const store = new Vuex.Store({ getters }) + return mount(Users, { + mocks, + localVue, + store, + stubs, + data: () => ({ + User: [ + { + id: 'user', + email: 'user@example.org', + name: 'User', + role: 'moderator', + slug: 'user', + }, + { + id: 'user2', + email: 'user2@example.org', + name: 'User', + role: 'moderator', + slug: 'user', + }, + ], + }), + }) + } + + describe('given badges are enabled', () => { + beforeEach(() => { + mocks.$env = { + BADGES_ENABLED: true, + } + wrapper = Wrapper() + }) it('renders', () => { + expect(wrapper.element).toMatchSnapshot() + }) + }) + + describe('given badges are disabled', () => { + beforeEach(() => { + mocks.$env = { + BADGES_ENABLED: false, + } wrapper = Wrapper() - expect(wrapper.element.tagName).toBe('DIV') }) - describe('search', () => { - let searchAction - beforeEach(() => { - searchAction = (wrapper, { query }) => { - wrapper.find('input').setValue(query) - wrapper.find('form').trigger('submit') - return wrapper - } + it('renders', () => { + expect(wrapper.element).toMatchSnapshot() + }) + }) + + describe('search', () => { + let searchAction + beforeEach(() => { + wrapper = Wrapper() + searchAction = (wrapper, { query }) => { + wrapper.find('input').setValue(query) + wrapper.find('form').trigger('submit') + return wrapper + } + }) + + describe('query looks like an email address', () => { + it('searches users for exact email address', async () => { + const wrapper = await searchAction(Wrapper(), { query: 'email@example.org' }) + expect(wrapper.vm.email).toEqual('email@example.org') + expect(wrapper.vm.filter).toBe(null) }) - describe('query looks like an email address', () => { - it('searches users for exact email address', async () => { - const wrapper = await searchAction(Wrapper(), { query: 'email@example.org' }) - expect(wrapper.vm.email).toEqual('email@example.org') - expect(wrapper.vm.filter).toBe(null) - }) - - it('email address is case-insensitive', async () => { - const wrapper = await searchAction(Wrapper(), { query: 'eMaiL@example.org' }) - expect(wrapper.vm.email).toEqual('email@example.org') - expect(wrapper.vm.filter).toBe(null) - }) - }) - - describe('query is just text', () => { - it('tries to find matching users by `name`, `slug` or `about`', async () => { - const wrapper = await searchAction(await Wrapper(), { query: 'Find me' }) - const expected = { - OR: [ - { name_contains: 'Find me' }, - { slug_contains: 'Find me' }, - { about_contains: 'Find me' }, - ], - } - expect(wrapper.vm.email).toBe(null) - expect(wrapper.vm.filter).toEqual(expected) - }) + it('email address is case-insensitive', async () => { + const wrapper = await searchAction(Wrapper(), { query: 'eMaiL@example.org' }) + expect(wrapper.vm.email).toEqual('email@example.org') + expect(wrapper.vm.filter).toBe(null) }) }) - describe('change roles', () => { - beforeAll(() => { - wrapper = Wrapper() - wrapper.setData({ - User: [ - { - id: 'admin', - email: 'admin@example.org', - name: 'Admin', - role: 'admin', - slug: 'admin', - }, - { - id: 'user', - email: 'user@example.org', - name: 'User', - role: 'user', - slug: 'user', - }, + describe('query is just text', () => { + it('tries to find matching users by `name`, `slug` or `about`', async () => { + const wrapper = await searchAction(await Wrapper(), { query: 'Find me' }) + const expected = { + OR: [ + { name_contains: 'Find me' }, + { slug_contains: 'Find me' }, + { about_contains: 'Find me' }, ], - userRoles: ['user', 'moderator', 'admin'], - }) - }) - - it('cannot change own role', () => { - const adminRow = wrapper.findAll('tr').at(1) - expect(adminRow.find('select').exists()).toBe(false) - }) - - it('changes the role of another user', () => { - const userRow = wrapper.findAll('tr').at(2) - userRow.findAll('option').at(1).setSelected() - expect(mocks.$apollo.mutate).toHaveBeenCalledWith( - expect.objectContaining({ - variables: { - id: 'user', - role: 'moderator', - }, - }), - ) - }) - - it('toasts a success message after role has changed', () => { - const userRow = wrapper.findAll('tr').at(2) - userRow.findAll('option').at(1).setSelected() - expect(mocks.$toast.success).toHaveBeenCalled() + } + expect(wrapper.vm.email).toBe(null) + expect(wrapper.vm.filter).toEqual(expected) }) }) }) + + describe('change roles', () => { + beforeAll(() => { + wrapper = Wrapper() + wrapper.setData({ + User: [ + { + id: 'admin', + email: 'admin@example.org', + name: 'Admin', + role: 'admin', + slug: 'admin', + }, + { + id: 'user', + email: 'user@example.org', + name: 'User', + role: 'user', + slug: 'user', + }, + ], + userRoles: ['user', 'moderator', 'admin'], + }) + }) + + it('cannot change own role', () => { + const adminRow = wrapper.findAll('tr').at(1) + expect(adminRow.find('select').exists()).toBe(false) + }) + + it('changes the role of another user', () => { + const userRow = wrapper.findAll('tr').at(2) + userRow.findAll('option').at(1).setSelected() + expect(mocks.$apollo.mutate).toHaveBeenCalledWith( + expect.objectContaining({ + variables: { + id: 'user', + role: 'moderator', + }, + }), + ) + }) + + it('toasts a success message after role has changed', () => { + const userRow = wrapper.findAll('tr').at(2) + userRow.findAll('option').at(1).setSelected() + expect(mocks.$toast.success).toHaveBeenCalled() + }) + }) }) diff --git a/webapp/pages/admin/users/index.vue b/webapp/pages/admin/users/index.vue index 24258a57f..0bd592bad 100644 --- a/webapp/pages/admin/users/index.vue +++ b/webapp/pages/admin/users/index.vue @@ -70,7 +70,7 @@ params: { id: scope.row.id }, }" > - {{ $t('admin.users.table.edit') }} + @@ -120,7 +120,7 @@ export default { currentUser: 'auth/user', }), fields() { - return { + const fields = { index: this.$t('admin.users.table.columns.number'), name: this.$t('admin.users.table.columns.name'), email: this.$t('admin.users.table.columns.email'), @@ -142,11 +142,16 @@ export default { label: this.$t('admin.users.table.columns.role'), align: 'right', }, - badges: { + } + + if (this.$env.BADGES_ENABLED) { + fields.badges = { label: this.$t('admin.users.table.columns.badges'), align: 'right', - }, + } } + + return fields }, }, apollo: { @@ -219,4 +224,8 @@ export default { .admin-users > .base-card:first-child { margin-bottom: $space-small; } + +.ds-table-col { + vertical-align: middle; +} diff --git a/webapp/pages/profile/_id/__snapshots__/_slug.spec.js.snap b/webapp/pages/profile/_id/__snapshots__/_slug.spec.js.snap new file mode 100644 index 000000000..9eec4e96a --- /dev/null +++ b/webapp/pages/profile/_id/__snapshots__/_slug.spec.js.snap @@ -0,0 +1,2442 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ProfileSlug given an authenticated user given another profile user and badges are disabled renders 1`] = ` +
+
+
+ +
+
+
+
+ + BTB + + + + + +
+ + + + + +
+
+ +
+
+
+
+ +
+

+ + Bob the builder + +

+ +

+ + @undefined + +

+ + + +

+ + profile.memberSince + +

+
+ + + +
+
+ +
+

+ + + + + 0 + + + + +

+

+ + profile.followers + +

+
+
+
+ +
+ +
+

+ + + + + 0 + + + + +

+

+ + profile.following + +

+
+
+
+
+ +
+ + + + + + + +
+ + + + +
+ +
+ +

+ + profile.network.title + +

+ + + +
+ + + + +
+ +
+ + + +
+ + + + + + + +
+
+
+
+
+
+`; + +exports[`ProfileSlug given an authenticated user given another profile user and badges are enabled renders 1`] = ` +
+
+
+ +
+
+
+
+ + BTB + + + + + +
+ + + + + +
+
+ +
+
+
+
+ +
+

+ + Bob the builder + +

+ +

+ + @undefined + +

+ + + +

+ + profile.memberSince + +

+
+ +
+ + +
+
+ +
+
+ +
+
+ +
+
+
+ +
+
+ +
+

+ + + + + 0 + + + + +

+

+ + profile.followers + +

+
+
+
+ +
+ +
+

+ + + + + 0 + + + + +

+

+ + profile.following + +

+
+
+
+
+ +
+ + + + + + + +
+ + + + +
+ +
+ +

+ + profile.network.title + +

+ + + +
+ + + + +
+ +
+ + + +
+ + + + + + + +
+
+
+
+
+
+`; + +exports[`ProfileSlug given an authenticated user given the logged in user as profile user and badges are disabled renders 1`] = ` +
+
+
+ +
+
+
+
+
+
+
+
+ + BTB + + + + + +
+ +
+
+ +
+
+
+
+
+
+ + + + + +
+
+ +
+
+
+
+ +
+

+ + Bob the builder + +

+ +

+ + @undefined + +

+ + + +

+ + profile.memberSince + +

+
+ + + +
+
+ +
+

+ + + + + 0 + + + + +

+

+ + profile.followers + +

+
+
+
+ +
+ +
+

+ + + + + 0 + + + + +

+

+ + profile.following + +

+
+
+
+
+ + + + + + +
+ +
+ +

+ + profile.network.title + +

+ + + +
+ + + + +
+ +
+ + + +
+ + + + + + + +
+
+
+
+
+
+`; + +exports[`ProfileSlug given an authenticated user given the logged in user as profile user and badges are enabled renders 1`] = ` +
+
+
+ +
+
+
+
+
+
+
+
+ + BTB + + + + + +
+ +
+
+ +
+
+
+
+
+
+ + + + + +
+
+ +
+
+
+
+ +
+

+ + Bob the builder + +

+ +

+ + @undefined + +

+ + + +

+ + profile.memberSince + +

+
+ + + +
+
+ +
+

+ + + + + 0 + + + + +

+

+ + profile.followers + +

+
+
+
+ +
+ +
+

+ + + + + 0 + + + + +

+

+ + profile.following + +

+
+
+
+
+ + + + + + +
+ +
+ +

+ + profile.network.title + +

+ + + +
+ + + + +
+ +
+ + + +
+ + + + + + + +
+
+
+
+
+
+`; diff --git a/webapp/pages/profile/_id/_slug.spec.js b/webapp/pages/profile/_id/_slug.spec.js index 5ab87ad3a..a4cc473c3 100644 --- a/webapp/pages/profile/_id/_slug.spec.js +++ b/webapp/pages/profile/_id/_slug.spec.js @@ -1,22 +1,25 @@ -import { mount } from '@vue/test-utils' +import { render } from '@testing-library/vue' import ProfileSlug from './_slug.vue' const localVue = global.localVue localVue.filter('date', (d) => d) +// Mock Math.random, used in Dropdown +Object.assign(Math, { + random: () => 0, +}) + const stubs = { 'client-only': true, 'v-popover': true, 'nuxt-link': true, - 'infinite-loading': true, 'follow-list': true, 'router-link': true, } describe('ProfileSlug', () => { let wrapper - let Wrapper let mocks beforeEach(() => { @@ -25,7 +28,7 @@ describe('ProfileSlug', () => { id: 'p23', name: 'It is a post', }, - $t: jest.fn(), + $t: jest.fn((t) => t), // If you're mocking router, then don't use VueRouter with localVue: https://vue-test-utils.vuejs.org/guides/using-with-vue-router.html $route: { params: { @@ -49,49 +52,144 @@ describe('ProfileSlug', () => { } }) - describe('mount', () => { - Wrapper = () => { - return mount(ProfileSlug, { - mocks, - localVue, - stubs, - }) - } + const Wrapper = (badgesEnabled, data) => { + return render(ProfileSlug, { + localVue, + stubs, + data: () => data, + mocks: { + ...mocks, + $env: { + BADGES_ENABLED: badgesEnabled, + }, + }, + }) + } - describe('given an authenticated user', () => { - beforeEach(() => { - mocks.$filters = { - removeLinks: (c) => c, - truncate: (a) => a, - } - mocks.$store = { - getters: { - 'auth/isModerator': () => false, - 'auth/user': { - id: 'u23', - }, + describe('given an authenticated user', () => { + beforeEach(() => { + mocks.$filters = { + removeLinks: (c) => c, + truncate: (a) => a, + } + mocks.$store = { + getters: { + 'auth/isModerator': () => false, + 'auth/user': { + id: 'u23', }, - } - }) + }, + } + }) - describe('given a user for the profile', () => { - beforeEach(() => { - wrapper = Wrapper() - wrapper.setData({ - User: [ + describe('given another profile user', () => { + const user = { + User: [ + { + id: 'u3', + name: 'Bob the builder', + contributionsCount: 6, + shoutedCount: 7, + commentedCount: 8, + badgeVerification: { + id: 'bv1', + icon: '/path/to/icon-bv1', + description: 'verified', + isDefault: false, + }, + badgeTrophiesSelected: [ { - id: 'u3', - name: 'Bob the builder', - contributionsCount: 6, - shoutedCount: 7, - commentedCount: 8, + id: 'bt1', + icon: '/path/to/icon-bt1', + description: 'a trophy', + isDefault: false, + }, + { + id: 'bt2', + icon: '/path/to/icon-bt2', + description: 'no trophy', + isDefault: true, }, ], - }) + }, + ], + } + + describe('and badges are enabled', () => { + beforeEach(() => { + wrapper = Wrapper(true, user) }) - it('displays name of the user', () => { - expect(wrapper.text()).toContain('Bob the builder') + it('renders', () => { + expect(wrapper.container).toMatchSnapshot() + }) + }) + + describe('and badges are disabled', () => { + beforeEach(() => { + wrapper = Wrapper(false, user) + }) + + it('renders', () => { + expect(wrapper.container).toMatchSnapshot() + }) + }) + }) + + describe('given the logged in user as profile user', () => { + beforeEach(() => { + mocks.$route.params.id = 'u23' + }) + + const user = { + User: [ + { + id: 'u23', + name: 'Bob the builder', + contributionsCount: 6, + shoutedCount: 7, + commentedCount: 8, + badgeVerification: { + id: 'bv1', + icon: '/path/to/icon-bv1', + description: 'verified', + isDefault: false, + }, + badgeTrophiesSelected: [ + { + id: 'bt1', + icon: '/path/to/icon-bt1', + description: 'a trophy', + isDefault: false, + }, + { + id: 'bt2', + icon: '/path/to/icon-bt2', + description: 'no trophy', + isDefault: true, + }, + ], + }, + ], + } + + describe('and badges are enabled', () => { + beforeEach(() => { + wrapper = Wrapper(true, user) + }) + + it('renders', () => { + expect(wrapper.container).toMatchSnapshot() + }) + }) + + describe('and badges are disabled', () => { + beforeEach(() => { + wrapper = Wrapper(false, user) + }) + + it('renders', () => { + expect(wrapper.container).toMatchSnapshot() }) }) }) diff --git a/webapp/pages/profile/_id/_slug.vue b/webapp/pages/profile/_id/_slug.vue index 382350faf..38035e217 100644 --- a/webapp/pages/profile/_id/_slug.vue +++ b/webapp/pages/profile/_id/_slug.vue @@ -42,8 +42,11 @@ {{ $t('profile.memberSince') }} {{ user.createdAt | date('MMMM yyyy') }} - - + + + + + @@ -266,6 +269,10 @@ export default { user() { return this.User ? this.User[0] : {} }, + userBadges() { + if (!this.$env.BADGES_ENABLED) return null + return [this.user.badgeVerification, ...(this.user.badgeTrophiesSelected || [])] + }, userName() { const { name } = this.user || {} return name || this.$t('profile.userAnonym') @@ -456,6 +463,12 @@ export default { margin: auto; margin-top: -60px; } +.badge-edit-link { + transition: all 0.2s ease-out; + &:hover { + opacity: 0.7; + } +} .page-name-profile-id-slug { .ds-flex-item:first-child .content-menu { position: absolute; diff --git a/webapp/pages/registration.spec.js b/webapp/pages/registration.spec.js index b34ba3ba7..8be08fa5e 100644 --- a/webapp/pages/registration.spec.js +++ b/webapp/pages/registration.spec.js @@ -313,7 +313,7 @@ describe('Registration', () => { it('renders', async () => { wrapper = await Wrapper() - expect(wrapper.classes('registration-slider')).toBe(true) + expect(wrapper.find('.registration-slider')).toBeTruthy() }) // The asyncTests must go last diff --git a/webapp/pages/settings.spec.js b/webapp/pages/settings.spec.js index 0f3c6e22c..8c2917c90 100644 --- a/webapp/pages/settings.spec.js +++ b/webapp/pages/settings.spec.js @@ -1,4 +1,4 @@ -import { mount } from '@vue/test-utils' +import { render } from '@testing-library/vue' import settings from './settings.vue' const localVue = global.localVue @@ -17,21 +17,37 @@ describe('settings.vue', () => { } }) - describe('mount', () => { - const Wrapper = () => { - return mount(settings, { - mocks, - localVue, - stubs, - }) - } + const Wrapper = () => { + return render(settings, { + mocks, + localVue, + stubs, + }) + } + describe('given badges are enabled', () => { beforeEach(() => { + mocks.$env = { + BADGES_ENABLED: true, + } wrapper = Wrapper() }) it('renders', () => { - expect(wrapper.element.tagName).toBe('DIV') + expect(wrapper.container).toMatchSnapshot() + }) + }) + + describe('given badges are disabled', () => { + beforeEach(() => { + mocks.$env = { + BADGES_ENABLED: false, + } + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.container).toMatchSnapshot() }) }) }) diff --git a/webapp/pages/settings.vue b/webapp/pages/settings.vue index 5d526c3cc..e1181650e 100644 --- a/webapp/pages/settings.vue +++ b/webapp/pages/settings.vue @@ -21,7 +21,7 @@ export default { computed: { routes() { - return [ + const routes = [ { name: this.$t('settings.data.name'), path: `/settings`, @@ -83,6 +83,15 @@ export default { }, } */ ] + + if (this.$env.BADGES_ENABLED) { + routes.splice(2, 0, { + name: this.$t('settings.badges.name'), + path: `/settings/badges`, + }) + } + + return routes }, }, } diff --git a/webapp/pages/settings/__snapshots__/badges.spec.js.snap b/webapp/pages/settings/__snapshots__/badges.spec.js.snap new file mode 100644 index 000000000..4f80bc37e --- /dev/null +++ b/webapp/pages/settings/__snapshots__/badges.spec.js.snap @@ -0,0 +1,429 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`badge settings with badges more badges available selecting an empty slot shows list with available badges 1`] = ` +
+
+

+ settings.badges.name +

+ +

+ settings.badges.description +

+ +
+
+
+
+ +
+ + + +
+
+ + + +
+ + + settings.badges.click-to-use + + +
+ + + +
+
+ + +
+
+
+ + +
+
+`; + +exports[`badge settings with badges no more badges available selecting an empty slot shows no more badges available message 1`] = ` +
+
+

+ settings.badges.name +

+ +

+ settings.badges.description +

+ +
+
+
+
+ +
+ + + +
+
+ +

+ + settings.badges.no-badges-available + +

+ + + + + + +
+ + +
+
+`; + +exports[`badge settings with badges renders 1`] = ` +
+
+

+ settings.badges.name +

+ +

+ settings.badges.description +

+ +
+
+
+
+ +
+ + + +
+
+ + + +
+ + + settings.badges.click-to-select + + +
+ + + + +
+ + +
+
+`; + +exports[`badge settings with badges selecting a used badge clicking remove badge button with successful server request removes the badge 1`] = ` +
+
+

+ settings.badges.name +

+ +

+ settings.badges.description +

+ +
+
+
+ + + + +
+
+ + + + + + +
+ + +
+
+`; + +exports[`badge settings without badges renders 1`] = ` +
+
+

+ settings.badges.name +

+ +

+ settings.badges.description +

+ +
+
+
+
+ +
+
+
+ + + + + + + + +
+ + +
+
+`; diff --git a/webapp/pages/settings/__snapshots__/notifications.spec.js.snap b/webapp/pages/settings/__snapshots__/notifications.spec.js.snap index 0b70393ee..fd2c2f56c 100644 --- a/webapp/pages/settings/__snapshots__/notifications.spec.js.snap +++ b/webapp/pages/settings/__snapshots__/notifications.spec.js.snap @@ -12,11 +12,11 @@ exports[`notifications.vue mount renders 1`] = `

settings.notifications.post @@ -66,11 +66,11 @@ exports[`notifications.vue mount renders 1`] = `

settings.notifications.group @@ -155,42 +155,47 @@ exports[`notifications.vue mount renders 1`] = `

- - + - settings.notifications.checkAll - - - - - - + +
diff --git a/webapp/pages/settings/badges.spec.js b/webapp/pages/settings/badges.spec.js new file mode 100644 index 000000000..291fd75d6 --- /dev/null +++ b/webapp/pages/settings/badges.spec.js @@ -0,0 +1,302 @@ +import { render, screen, fireEvent } from '@testing-library/vue' +import '@testing-library/jest-dom' +import badges from './badges.vue' + +const localVue = global.localVue + +describe('badge settings', () => { + let mocks + + const apolloMutateMock = jest.fn() + + const Wrapper = () => { + return render(badges, { + localVue, + mocks, + }) + } + + beforeEach(() => { + mocks = { + $t: jest.fn((t) => t), + $toast: { + success: jest.fn(), + error: jest.fn(), + }, + $apollo: { + mutate: apolloMutateMock, + }, + } + }) + + describe('without badges', () => { + beforeEach(() => { + mocks.$store = { + getters: { + 'auth/isModerator': () => false, + 'auth/user': { + id: 'u23', + badgeVerification: { + id: 'bv1', + icon: '/verification/icon', + description: 'Verification description', + isDefault: true, + }, + badgeTrophiesSelected: [], + badgeTrophiesUnused: [], + }, + }, + } + }) + + it('renders', () => { + const wrapper = Wrapper() + expect(wrapper.container).toMatchSnapshot() + }) + }) + + describe('with badges', () => { + const badgeTrophiesSelected = [ + { + id: '1', + icon: '/path/to/some/icon', + isDefault: false, + description: 'Some description', + }, + { + id: '2', + icon: '/path/to/empty/icon', + isDefault: true, + description: 'Empty', + }, + { + id: '3', + icon: '/path/to/third/icon', + isDefault: false, + description: 'Third description', + }, + ] + + const badgeTrophiesUnused = [ + { + id: '4', + icon: '/path/to/fourth/icon', + description: 'Fourth description', + }, + { + id: '5', + icon: '/path/to/fifth/icon', + description: 'Fifth description', + }, + ] + + let wrapper + + beforeEach(() => { + mocks.$store = { + getters: { + 'auth/isModerator': () => false, + 'auth/user': { + id: 'u23', + badgeVerification: { + id: 'bv1', + icon: '/verification/icon', + description: 'Verification description', + isDefault: false, + }, + badgeTrophiesSelected, + badgeTrophiesUnused, + }, + }, + } + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.container).toMatchSnapshot() + }) + + describe('selecting a used badge', () => { + beforeEach(async () => { + const badge = screen.getByTitle(badgeTrophiesSelected[0].description) + await fireEvent.click(badge) + }) + + it('shows remove badge button', () => { + expect(screen.getByText('settings.badges.remove')).toBeInTheDocument() + }) + + describe('clicking remove badge button', () => { + const clickButton = async () => { + const removeButton = screen.getByText('settings.badges.remove') + await fireEvent.click(removeButton) + } + + describe('with successful server request', () => { + beforeEach(() => { + apolloMutateMock.mockResolvedValue({ + data: { + setTrophyBadgeSelected: { + id: 'u23', + badgeTrophiesSelected: [ + { + id: '2', + icon: '/path/to/empty/icon', + isDefault: true, + description: 'Empty', + }, + { + id: '2', + icon: '/path/to/empty/icon', + isDefault: true, + description: 'Empty', + }, + { + id: '3', + icon: '/path/to/third/icon', + isDefault: false, + description: 'Third description', + }, + ], + }, + }, + }) + clickButton() + }) + + it('calls the server', () => { + expect(apolloMutateMock).toHaveBeenCalledWith({ + mutation: expect.anything(), + update: expect.anything(), + variables: { + badgeId: null, + slot: 0, + }, + }) + }) + + /* To test this, we would need a better apollo mock */ + it.skip('removes the badge', async () => { + expect(wrapper.container).toMatchSnapshot() + }) + + it('shows a success message', () => { + expect(mocks.$toast.success).toHaveBeenCalledWith('settings.badges.success-update') + }) + }) + + describe('with failed server request', () => { + beforeEach(() => { + apolloMutateMock.mockRejectedValue({ message: 'Ouch!' }) + clickButton() + }) + + it('shows an error message', () => { + expect(mocks.$toast.error).toHaveBeenCalledWith('settings.badges.error-update') + }) + }) + }) + }) + + describe('no more badges available', () => { + beforeEach(async () => { + mocks.$store.getters['auth/user'].badgeTrophiesUnused = [] + }) + + describe('selecting an empty slot', () => { + beforeEach(async () => { + const emptySlot = screen.getAllByTitle('Empty')[0] + await fireEvent.click(emptySlot) + }) + + it('shows no more badges available message', () => { + expect(wrapper.container).toMatchSnapshot() + }) + }) + }) + + describe('more badges available', () => { + describe('selecting an empty slot', () => { + beforeEach(async () => { + const emptySlot = screen.getAllByTitle('Empty')[0] + await fireEvent.click(emptySlot) + }) + + it('shows list with available badges', () => { + expect(wrapper.container).toMatchSnapshot() + }) + + describe('clicking on an available badge', () => { + const clickBadge = async () => { + const badge = screen.getByText(badgeTrophiesUnused[0].description) + await fireEvent.click(badge) + } + + describe('with successful server request', () => { + beforeEach(() => { + apolloMutateMock.mockResolvedValue({ + data: { + setTrophyBadgeSelected: { + id: 'u23', + badgeTrophiesSelected: [ + { + id: '4', + icon: '/path/to/fourth/icon', + description: 'Fourth description', + isDefault: false, + }, + { + id: '2', + icon: '/path/to/empty/icon', + isDefault: true, + description: 'Empty', + }, + { + id: '3', + icon: '/path/to/third/icon', + isDefault: false, + description: 'Third description', + }, + ], + }, + }, + }) + clickBadge() + }) + + it('calls the server', () => { + expect(apolloMutateMock).toHaveBeenCalledWith({ + mutation: expect.anything(), + update: expect.anything(), + variables: { + badgeId: '4', + slot: 1, + }, + }) + }) + + /* To test this, we would need a better apollo mock */ + it.skip('adds the badge', async () => { + expect(wrapper.container).toMatchSnapshot() + }) + + it('shows a success message', () => { + expect(mocks.$toast.success).toHaveBeenCalledWith('settings.badges.success-update') + }) + }) + + describe('with failed server request', () => { + beforeEach(() => { + apolloMutateMock.mockRejectedValue({ message: 'Ouch!' }) + clickBadge() + }) + + it('shows an error message', () => { + expect(mocks.$toast.error).toHaveBeenCalledWith('settings.badges.error-update') + }) + }) + }) + }) + }) + }) +}) diff --git a/webapp/pages/settings/badges.vue b/webapp/pages/settings/badges.vue new file mode 100644 index 000000000..3f0e7c7e7 --- /dev/null +++ b/webapp/pages/settings/badges.vue @@ -0,0 +1,176 @@ + + + + + diff --git a/webapp/static/img/badges/stars.svg b/webapp/static/img/badges/stars.svg new file mode 100644 index 000000000..44d64a5f4 --- /dev/null +++ b/webapp/static/img/badges/stars.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 1ef19363e..e17834008 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -7,6 +7,11 @@ resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== +"@adobe/css-tools@^4.4.0": + version "4.4.2" + resolved "https://registry.yarnpkg.com/@adobe/css-tools/-/css-tools-4.4.2.tgz#c836b1bd81e6d62cd6cdf3ee4948bcdce8ea79c8" + integrity sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A== + "@ampproject/remapping@^2.2.0": version "2.2.1" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" @@ -4253,6 +4258,19 @@ lz-string "^1.5.0" pretty-format "^27.0.2" +"@testing-library/jest-dom@^6.6.3": + version "6.6.3" + resolved "https://registry.yarnpkg.com/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz#26ba906cf928c0f8172e182c6fe214eb4f9f2bd2" + integrity sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA== + dependencies: + "@adobe/css-tools" "^4.4.0" + aria-query "^5.0.0" + chalk "^3.0.0" + css.escape "^1.5.1" + dom-accessibility-api "^0.6.3" + lodash "^4.17.21" + redent "^3.0.0" + "@testing-library/vue@5": version "5.9.0" resolved "https://registry.yarnpkg.com/@testing-library/vue/-/vue-5.9.0.tgz#d33c52ae89e076808abe622f70dcbccb1b5d080c" @@ -6058,6 +6076,11 @@ aria-query@5.1.3: dependencies: deep-equal "^2.0.5" +aria-query@^5.0.0: + version "5.3.2" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.2.tgz#93f81a43480e33a338f19163a3d10a50c01dcd59" + integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw== + arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" @@ -8230,6 +8253,11 @@ css-what@2.1, css-what@^2.1.2: resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== +css.escape@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" + integrity sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg== + csscolorparser@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/csscolorparser/-/csscolorparser-1.0.3.tgz#b34f391eea4da8f3e98231e2ccd8df9c041f171b" @@ -8793,6 +8821,11 @@ dom-accessibility-api@^0.5.9: resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453" integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== +dom-accessibility-api@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz#993e925cc1d73f2c662e7d75dd5a5445259a8fd8" + integrity sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w== + dom-converter@^0.2: version "0.2.0" resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" diff --git a/yarn.lock b/yarn.lock index a5a09d086..1e3983be3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4074,10 +4074,10 @@ cypress-network-idle@^1.15.0: resolved "https://registry.yarnpkg.com/cypress-network-idle/-/cypress-network-idle-1.15.0.tgz#e249f08695a46f1ddce18a95d5293937f277cbb3" integrity sha512-8zU16zhc7S3nMl1NTEEcNsZYlJy/ZzP2zPTTrngGxyXH32Ipake/xfHLZsgrzeWCieiS2AVhQsakhWqFzO3hpw== -cypress@^14.3.1: - version "14.3.1" - resolved "https://registry.yarnpkg.com/cypress/-/cypress-14.3.1.tgz#b0570c0e5b198d930a2c0f640d099e777bec2d2f" - integrity sha512-/2q06qvHMK3PNiadnRW1Je0lJ43gAFPQJUAK2zIxjr22kugtWxVQznTBLVu1AvRH+RP3oWZhCdWqiEi+0NuqCg== +cypress@^14.3.2: + version "14.3.2" + resolved "https://registry.yarnpkg.com/cypress/-/cypress-14.3.2.tgz#04a6ea66c1715119ef41dda5851d75801cc1e226" + integrity sha512-n+yGD2ZFFKgy7I3YtVpZ7BcFYrrDMcKj713eOZdtxPttpBjCyw/R8dLlFSsJPouneGN7A/HOSRyPJ5+3/gKDoA== dependencies: "@cypress/request" "^3.0.8" "@cypress/xvfb" "^1.2.4"