Merge branch 'master' into hendrik-e2e

This commit is contained in:
mahula 2025-07-18 15:43:16 +02:00 committed by GitHub
commit a50a837be4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
98 changed files with 4170 additions and 5323 deletions

View File

@ -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@e58f0e551cf92535579bb196c65d215dc5bbdbc2 # v3.0.0
# uses: peter-evans/repository-dispatch@6846232b0e1bfd17c14dce7ac13fd3fcefe22c0c # 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@e58f0e551cf92535579bb196c65d215dc5bbdbc2 # v3.0.0
uses: peter-evans/repository-dispatch@6846232b0e1bfd17c14dce7ac13fd3fcefe22c0c # 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@e58f0e551cf92535579bb196c65d215dc5bbdbc2 # v3.0.0
uses: peter-evans/repository-dispatch@6846232b0e1bfd17c14dce7ac13fd3fcefe22c0c # 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

View File

@ -32,8 +32,8 @@ jobs:
- name: Neo4J | Build 'community' image
run: |
docker build --target community -t "ocelotsocialnetwork/neo4j-community:test" neo4j/
docker save "ocelotsocialnetwork/neo4j-community:test" > /tmp/neo4j.tar
docker compose -f docker-compose.yml -f docker-compose.test.yml build neo4j
docker save "ghcr.io/ocelot-social-community/ocelot-social/neo4j:community" > /tmp/neo4j.tar
- name: Cache docker images
id: cache-neo4j
@ -53,8 +53,8 @@ jobs:
- name: backend | Build 'test' image
run: |
docker build --target test -t "ocelotsocialnetwork/backend:test" backend/
docker save "ocelotsocialnetwork/backend:test" > /tmp/backend.tar
docker compose -f docker-compose.yml -f docker-compose.test.yml build backend
docker save "ghcr.io/ocelot-social-community/ocelot-social/backend:test" > /tmp/backend.tar
- name: Cache docker images
id: cache-backend
@ -112,8 +112,7 @@ jobs:
cp backend/.env.template backend/.env
- name: backend | docker compose
# doesn't work without the --build flag - this either means we should not load the cached images or cache the correct image
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach backend --build
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach backend
- name: backend | Initialize Database
run: docker compose exec -T backend yarn db:migrate init

View File

@ -4,8 +4,52 @@ 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.11.0](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.10.1...3.11.0)
- remove expect package [`#8738`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8738)
- build(deps-dev): bump eslint-plugin-import in /backend [`#8705`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8705)
- build(deps): bump the metascraper group in /backend with 12 updates [`#8717`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8717)
- build(deps-dev): bump dotenv from 16.5.0 to 17.0.0 [`#8720`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8720)
- build(deps-dev): bump eslint-plugin-jest in /backend [`#8706`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8706)
- build(deps): bump @aws-sdk/lib-storage in /backend [`#8723`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8723)
- build(deps): bump @aws-sdk/client-s3 from 3.832.0 to 3.839.0 in /backend [`#8722`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8722)
- refactor(backend): put config into context [`#8603`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8603)
- build(deps): bump dotenv from 16.5.0 to 17.0.0 in /backend [`#8726`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8726)
- build(deps-dev): bump @types/node from 24.0.3 to 24.0.6 in /backend [`#8721`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8721)
- fix(webapp): added option for slug [`#8659`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8659)
- build(deps-dev): bump prettier from 3.5.3 to 3.6.2 in /backend [`#8729`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8729)
- build(deps-dev): bump eslint-import-resolver-typescript in /backend [`#8725`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8725)
- build(deps-dev): bump @types/lodash from 4.17.18 to 4.17.19 in /backend [`#8727`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8727)
- build(deps-dev): bump eslint-plugin-prettier in /backend [`#8728`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8728)
- build(deps): bump node from 24.2.0-alpine to 24.3.0-alpine in /backend [`#8730`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8730)
- build(deps): bump peter-evans/repository-dispatch [`#8731`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8731)
- fix(backend): mask jwt token in log [`#8737`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8737)
- refactor(backend): fix tests for #8714 [`#8716`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8716)
- fix(backend): refactor S3 usage and always apply protocol fix [`#8714`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8714)
- refactor(docker): neo4j image naming inconsistency in docker compose files [`#8736`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8736)
- feat(backend): all db node properties [`#8635`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8635)
- fix(webapp): catch possibe errors on request geolocation [`#8640`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8640)
- Fix video player in Safari [`#8711`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8711)
- fix(webapp): fix property access of possibly undefined objects [`#8639`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8639)
- Build source maps [`#8695`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8695)
- feat(devops): tool versions [`#8709`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8709)
- build(deps-dev): bump eslint-plugin-prettier in /webapp [`#8699`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8699)
- build(deps): bump peter-evans/repository-dispatch [`#8701`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8701)
- build(deps): bump the metascraper group across 1 directory with 12 updates [`#8572`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8572)
- build(deps): bump @aws-sdk/client-s3 from 3.828.0 to 3.832.0 in /backend [`#8708`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8708)
- build(deps): bump linkifyjs from 4.2.0 to 4.3.1 in /backend [`#8531`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8531)
- build(deps-dev): bump @types/node from 24.0.1 to 24.0.3 in /backend [`#8707`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8707)
- build(deps-dev): bump @types/lodash from 4.17.17 to 4.17.18 in /backend [`#8702`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8702)
- build(deps-dev): bump the cypress group across 1 directory with 3 updates [`#8698`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8698)
- chore(other): set some 'nvm' versions to '24.2.0' [`#8691`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8691)
- Put message creation in a transaction with file uploads to avoid empty messages [`#8694`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8694)
- fix(webapp): better chat upload ui [`#8693`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8693)
#### [3.10.1](https://github.com/Ocelot-Social-Community/Ocelot-Social/compare/3.10.0...3.10.1)
> 19 June 2025
- Release v3.10.1 [`#8692`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8692)
- feat(backend): logger [`#8655`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8655)
- fix(webapp): show hint that message is being saved [`#8690`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8690)
- fix(webapp): added timer [`#8658`](https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8658)

View File

@ -1,4 +1,4 @@
DEBUG=true
DEBUG=neo4j-graphql-js
NEO4J_URI=bolt://localhost:7687
NEO4J_USERNAME=neo4j

View File

@ -1,4 +1,4 @@
FROM node:24.2.0-alpine AS base
FROM node:24.4.0-alpine AS base
LABEL org.label-schema.name="ocelot.social:backend"
LABEL org.label-schema.description="Backend of the Social Network Software ocelot.social"
LABEL org.label-schema.usage="https://github.com/Ocelot-Social-Community/Ocelot-Social/blob/master/README.md"

View File

@ -1,6 +1,6 @@
{
"name": "ocelot-social-backend",
"version": "3.10.1",
"version": "3.11.0",
"description": "GraphQL Backend for ocelot.social",
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
"author": "ocelot.social Community",
@ -28,8 +28,8 @@
"prod:db:data:categories": "node build/src/db/categories.js"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.832.0",
"@aws-sdk/lib-storage": "^3.828.0",
"@aws-sdk/client-s3": "^3.844.0",
"@aws-sdk/lib-storage": "^3.842.0",
"@sentry/node": "^5.15.4",
"@types/mime-types": "^3.0.1",
"apollo-server": "~2.14.2",
@ -38,7 +38,7 @@
"body-parser": "^1.20.3",
"cheerio": "~1.1.0",
"cross-env": "~7.0.3",
"dotenv": "~16.5.0",
"dotenv": "~17.0.1",
"email-templates": "^12.0.3",
"express": "^5.1.0",
"graphql": "^14.6.0",
@ -57,20 +57,20 @@
"linkifyjs": "^4.3.1",
"lodash": "~4.17.21",
"merge-graphql-schemas": "^1.7.8",
"metascraper": "^5.47.1",
"metascraper-author": "^5.47.1",
"metascraper-date": "^5.47.1",
"metascraper-description": "^5.47.1",
"metascraper-image": "^5.47.1",
"metascraper-lang": "^5.47.1",
"metascraper": "^5.49.1",
"metascraper-author": "^5.49.1",
"metascraper-date": "^5.49.1",
"metascraper-description": "^5.49.1",
"metascraper-image": "^5.49.1",
"metascraper-lang": "^5.49.1",
"metascraper-lang-detector": "^4.10.2",
"metascraper-logo": "^5.47.1",
"metascraper-publisher": "^5.47.1",
"metascraper-logo": "^5.49.1",
"metascraper-publisher": "^5.49.1",
"metascraper-soundcloud": "^5.34.4",
"metascraper-title": "^5.47.1",
"metascraper-url": "^5.47.1",
"metascraper-video": "^5.47.1",
"metascraper-youtube": "^5.47.1",
"metascraper-title": "^5.49.1",
"metascraper-url": "^5.49.1",
"metascraper-video": "^5.49.1",
"metascraper-youtube": "^5.49.1",
"migrate": "^2.1.0",
"mime-types": "^3.0.1",
"minimatch": "^10.0.3",
@ -79,7 +79,7 @@
"neo4j-graphql-js": "^2.11.5",
"neode": "^0.4.9",
"node-fetch": "^2.7.0",
"nodemailer": "^7.0.3",
"nodemailer": "^7.0.5",
"nodemailer-html-to-text": "^3.2.0",
"preview-email": "^3.1.0",
"pug": "^3.0.3",
@ -93,11 +93,13 @@
},
"devDependencies": {
"@eslint-community/eslint-plugin-eslint-comments": "^4.5.0",
"@faker-js/faker": "9.8.0",
"@faker-js/faker": "9.9.0",
"@types/email-templates": "^10.0.4",
"@types/jest": "^29.5.14",
"@types/lodash": "^4.17.18",
"@types/node": "^24.0.3",
"@types/jsonwebtoken": "~8.5.1",
"@types/lodash": "^4.17.20",
"@types/node": "^24.0.14",
"@types/request": "^2.48.12",
"@types/slug": "^5.0.9",
"@types/uuid": "~9.0.1",
"@typescript-eslint/eslint-plugin": "^5.62.0",
@ -106,18 +108,18 @@
"eslint": "^8.57.1",
"eslint-config-prettier": "^10.1.5",
"eslint-config-standard": "^17.1.0",
"eslint-import-resolver-typescript": "^4.4.3",
"eslint-plugin-import": "^2.31.0",
"eslint-plugin-jest": "^28.13.5",
"eslint-import-resolver-typescript": "^4.4.4",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-jest": "^29.0.1",
"eslint-plugin-jsonc": "^2.20.1",
"eslint-plugin-n": "^17.20.0",
"eslint-plugin-n": "^17.21.0",
"eslint-plugin-no-catch-all": "^1.1.0",
"eslint-plugin-prettier": "^5.4.1",
"eslint-plugin-prettier": "^5.5.1",
"eslint-plugin-promise": "^7.2.1",
"eslint-plugin-security": "^3.0.1",
"jest": "^29.7.0",
"nodemon": "~3.1.10",
"prettier": "^3.5.3",
"prettier": "^3.6.2",
"require-json5": "^1.3.0",
"rosie": "^2.1.1",
"ts-jest": "^29.4.0",

View File

@ -32,12 +32,6 @@ const environment = {
LOG_LEVEL: 'DEBUG',
}
const required = {
MAPBOX_TOKEN: env.MAPBOX_TOKEN,
JWT_SECRET: env.JWT_SECRET,
PRIVATE_KEY_PASSPHRASE: env.PRIVATE_KEY_PASSPHRASE,
}
const server = {
CLIENT_URI: env.CLIENT_URI ?? 'http://localhost:3000',
GRAPHQL_URI: env.GRAPHQL_URI ?? 'http://localhost:4000',
@ -97,33 +91,34 @@ const redis = {
REDIS_PASSWORD: env.REDIS_PASSWORD,
}
const s3 = {
const required = {
AWS_ACCESS_KEY_ID: env.AWS_ACCESS_KEY_ID,
AWS_SECRET_ACCESS_KEY: env.AWS_SECRET_ACCESS_KEY,
AWS_ENDPOINT: env.AWS_ENDPOINT,
AWS_REGION: env.AWS_REGION,
AWS_BUCKET: env.AWS_BUCKET,
S3_PUBLIC_GATEWAY: env.S3_PUBLIC_GATEWAY,
MAPBOX_TOKEN: env.MAPBOX_TOKEN,
JWT_SECRET: env.JWT_SECRET,
PRIVATE_KEY_PASSPHRASE: env.PRIVATE_KEY_PASSPHRASE,
}
export interface S3Configured {
AWS_ACCESS_KEY_ID: string
AWS_SECRET_ACCESS_KEY: string
AWS_ENDPOINT: string
AWS_REGION: string
AWS_BUCKET: string
S3_PUBLIC_GATEWAY: string | undefined
const S3_PUBLIC_GATEWAY = env.S3_PUBLIC_GATEWAY
// https://stackoverflow.com/a/53050575
type NoUndefinedField<T> = { [P in keyof T]-?: NoUndefinedField<NonNullable<T[P]>> }
function assertRequiredConfig(
conf: typeof required,
): asserts conf is NoUndefinedField<typeof required> {
Object.entries(conf).forEach(([key, value]) => {
if (!value) {
throw new Error(`ERROR: "${key}" env variable is missing.`)
}
})
}
export const isS3configured = (config: typeof s3): config is S3Configured => {
return !!(
config.AWS_ACCESS_KEY_ID &&
config.AWS_SECRET_ACCESS_KEY &&
config.AWS_ENDPOINT &&
config.AWS_REGION &&
config.AWS_BUCKET
)
}
assertRequiredConfig(required)
const options = {
EMAIL_DEFAULT_SENDER: env.EMAIL_DEFAULT_SENDER,
@ -147,24 +142,28 @@ const language = {
LANGUAGE_DEFAULT: process.env.LANGUAGE_DEFAULT ?? 'en',
}
// Check if all required configs are present
Object.entries(required).map((entry) => {
if (!entry[1]) {
throw new Error(`ERROR: "${entry[0]}" env variable is missing.`)
}
return entry
})
export default {
const CONFIG = {
...environment,
...server,
...required,
...neo4j,
...sentry,
...redis,
...s3,
...options,
...language,
S3_PUBLIC_GATEWAY,
}
export type Config = typeof CONFIG
export type S3Config = Pick<
Config,
| 'AWS_ACCESS_KEY_ID'
| 'AWS_SECRET_ACCESS_KEY'
| 'AWS_ENDPOINT'
| 'AWS_REGION'
| 'AWS_BUCKET'
| 'S3_PUBLIC_GATEWAY'
>
export default CONFIG
export { nodemailerTransportOptions }

View File

@ -0,0 +1,61 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
import databaseContext from '@context/database'
import pubsubContext from '@context/pubsub'
import CONFIG from '@src/config'
import type { DecodedUser } from '@src/jwt/decode'
import { decode } from '@src/jwt/decode'
import ocelotLogger from '@src/logger'
import type OcelotLogger from '@src/logger'
import type { ApolloServerExpressConfig } from 'apollo-server-express'
const serverDatabase = databaseContext()
const serverPubsub = pubsubContext()
export const getContext =
(opts?: {
database?: ReturnType<typeof databaseContext>
pubsub?: ReturnType<typeof pubsubContext>
authenticatedUser: DecodedUser | null | undefined
logger?: typeof OcelotLogger
config: typeof CONFIG
}) =>
async (req: { headers: { authorization?: string } }) => {
const {
database = serverDatabase,
pubsub = serverPubsub,
authenticatedUser = undefined,
logger = ocelotLogger,
config = CONFIG,
} = opts ?? {}
const { driver } = database
const user =
authenticatedUser === null
? null
: (authenticatedUser ?? (await decode({ driver, config })(req.headers.authorization)))
const result = {
database,
driver,
neode: database.neode,
pubsub,
logger,
user,
req,
cypherParams: {
currentUserId: user ? user.id : null,
},
config,
}
return result
}
export const context: ApolloServerExpressConfig['context'] = async (options) => {
const { connection, req } = options
if (connection) {
return connection.context
} else {
return getContext()(req)
}
}
export type Context = Awaited<ReturnType<ReturnType<typeof getContext>>>

View File

@ -13,7 +13,7 @@ import { v4 as uuid } from 'uuid'
import { generateInviteCode } from '@graphql/resolvers/inviteCodes'
import { isUniqueFor } from '@middleware/sluggifyMiddleware'
import uniqueSlug from '@middleware/slugify/uniqueSlug'
import { Context } from '@src/server'
import { Context } from '@src/context'
import { getDriver, getNeode } from './neo4j'

View File

@ -4,7 +4,6 @@
/* eslint-disable @typescript-eslint/no-floating-promises */
/* eslint-disable n/no-process-exit */
import { faker } from '@faker-js/faker'
import { createTestClient } from 'apollo-server-testing'
import sample from 'lodash/sample'
import CONFIG from '@config/index'
@ -16,10 +15,9 @@ import { CreateMessage } from '@graphql/queries/CreateMessage'
import { createPostMutation } from '@graphql/queries/createPostMutation'
import { createRoomMutation } from '@graphql/queries/createRoomMutation'
import { joinGroupMutation } from '@graphql/queries/joinGroupMutation'
import createServer from '@src/server'
import { createApolloTestSetup } from '@root/test/helpers'
import Factory from './factories'
import { getNeode, getDriver } from './neo4j'
import { trophies, verification } from './seed/badges'
if (CONFIG.PRODUCTION && !CONFIG.PRODUCTION_DB_CLEAN_ALLOW) {
@ -35,22 +33,21 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
console.log('Seeded Data...')
let authenticatedUser = null
const driver = getDriver()
const neode = getNeode()
const { server } = createServer({
context: () => {
return {
driver,
neode,
user: authenticatedUser,
}
},
// locations
const context = () => ({
authenticatedUser,
config: CONFIG,
})
const { mutate } = createTestClient(server)
const apolloSetup = createApolloTestSetup({ context })
const { mutate, server, database } = apolloSetup
const { neode } = database
try {
// eslint-disable-next-line no-console
console.log('seed', 'locations')
// locations
const Hamburg = await Factory.build('location', {
id: 'region.5127278006398860',
name: 'Hamburg',
@ -1618,7 +1615,7 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl']
throw err
} finally {
await server.stop()
await driver.close()
await database.driver.close()
// eslint-disable-next-line @typescript-eslint/await-thenable
await neode.close()
process.exit(0)

View File

@ -9,15 +9,14 @@ import { Readable } from 'node:stream'
import { S3Client } from '@aws-sdk/client-s3'
import { Upload } from '@aws-sdk/lib-storage'
import { UserInputError } from 'apollo-server'
import { createTestClient } from 'apollo-server-testing'
import databaseContext from '@context/database'
import Factory, { cleanDatabase } from '@db/factories'
import File from '@db/models/File'
import { CreateMessage } from '@graphql/queries/CreateMessage'
import { createRoomMutation } from '@graphql/queries/createRoomMutation'
import type { S3Configured } from '@src/config'
import createServer, { getContext } from '@src/server'
import type { ApolloTestSetup } from '@root/test/helpers'
import { createApolloTestSetup } from '@root/test/helpers'
import type { S3Config } from '@src/config'
import { attachments } from './attachments'
@ -38,7 +37,7 @@ const UploadMock = {
;(Upload as unknown as jest.Mock).mockImplementation(() => UploadMock)
const config: S3Configured = {
const config: S3Config = {
AWS_ACCESS_KEY_ID: 'AWS_ACCESS_KEY_ID',
AWS_SECRET_ACCESS_KEY: 'AWS_SECRET_ACCESS_KEY',
AWS_BUCKET: 'AWS_BUCKET',
@ -47,20 +46,19 @@ const config: S3Configured = {
S3_PUBLIC_GATEWAY: undefined,
}
const database = databaseContext()
let authenticatedUser, server, mutate
let authenticatedUser
const context = () => ({ authenticatedUser, config })
let mutate: ApolloTestSetup['mutate']
let database: ApolloTestSetup['database']
let server: ApolloTestSetup['server']
beforeAll(async () => {
await cleanDatabase()
const contextUser = async (_req) => authenticatedUser
const context = getContext({ user: contextUser, database })
server = createServer({ context }).server
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
mutate = createTestClient(server).mutate
const apolloSetup = createApolloTestSetup({ context })
mutate = apolloSetup.mutate
database = apolloSetup.database
server = apolloSetup.server
})
afterAll(async () => {
@ -115,7 +113,7 @@ describe('delete Attachment', () => {
},
})
message = m.data.CreateMessage
message = (m.data as any).CreateMessage // eslint-disable-line @typescript-eslint/no-explicit-any
await database.write({
query: `

View File

@ -1,13 +1,12 @@
import path from 'node:path'
import { DeleteObjectCommand, ObjectCannedACL, S3Client } from '@aws-sdk/client-s3'
import { Upload } from '@aws-sdk/lib-storage'
import { UserInputError } from 'apollo-server-express'
import slug from 'slug'
import { v4 as uuid } from 'uuid'
import { isS3configured, S3Configured } from '@config/index'
import type { S3Config } from '@config/index'
import { wrapTransaction } from '@graphql/resolvers/images/wrapTransaction'
import { s3Service } from '@src/uploads/s3Service'
import type { FileUpload } from 'graphql-upload'
import type { Transaction } from 'neo4j-driver'
@ -55,22 +54,8 @@ export interface Attachments {
) => Promise<any>
}
export const attachments = (config: S3Configured) => {
if (!isS3configured(config)) {
throw new Error('S3 not configured')
}
const { AWS_BUCKET: Bucket, S3_PUBLIC_GATEWAY } = config
const { AWS_ENDPOINT, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY } = config
const s3 = new S3Client({
credentials: {
accessKeyId: AWS_ACCESS_KEY_ID,
secretAccessKey: AWS_SECRET_ACCESS_KEY,
},
endpoint: AWS_ENDPOINT,
forcePathStyle: true,
})
export const attachments = (config: S3Config) => {
const s3 = s3Service(config, 'attachments')
const del: Attachments['del'] = async (resource, relationshipType, opts = {}) => {
const { transaction } = opts
@ -86,17 +71,7 @@ export const attachments = (config: S3Configured) => {
)
const [file] = txResult.records.map((record) => record.get('fileProps') as File)
if (file) {
let { pathname } = new URL(file.url, 'http://example.org') // dummy domain to avoid invalid URL error
pathname = pathname.substring(1) // remove first character '/'
const prefix = `${Bucket}/`
if (pathname.startsWith(prefix)) {
pathname = pathname.slice(prefix.length)
}
const params = {
Bucket,
Key: pathname,
}
await s3.send(new DeleteObjectCommand(params))
await s3.deleteFile(file.url)
}
return file
}
@ -119,34 +94,10 @@ export const attachments = (config: S3Configured) => {
const { name: fileName, ext } = path.parse(uploadFile.filename)
const uniqueFilename = `${uuid()}-${slug(fileName)}${ext}`
const s3Location = `attachments/${uniqueFilename}`
const params = {
Bucket,
Key: s3Location,
ACL: ObjectCannedACL.public_read,
ContentType: uploadFile.mimetype,
Body: uploadFile.createReadStream(),
}
const command = new Upload({ client: s3, params })
const data = await command.done()
let { Location: location } = data
if (!location) {
throw new Error('File upload did not return `Location`')
}
if (!location.startsWith('https://') && !location.startsWith('http://')) {
// Ensure the location has a protocol. Hetzner does not return a protocol in the location.
location = `https://${location}`
}
let url = ''
if (!S3_PUBLIC_GATEWAY) {
url = location
} else {
const publicLocation = new URL(S3_PUBLIC_GATEWAY)
publicLocation.pathname = new URL(location).pathname
url = publicLocation.href
}
const url = await s3.uploadFile({
...uploadFile,
uniqueFilename,
})
const { name, type } = fileInput
const file = { url, name, type, ...fileAttributes }

View File

@ -1,37 +1,32 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { ApolloServer } from 'apollo-server-express'
import { createTestClient } from 'apollo-server-testing'
import gql from 'graphql-tag'
import { TROPHY_BADGES_SELECTED_MAX } from '@constants/badges'
import databaseContext from '@context/database'
import Factory, { cleanDatabase } from '@db/factories'
import { rewardTrophyBadge } from '@graphql/queries/rewardTrophyBadge'
import { setTrophyBadgeSelected } from '@graphql/queries/setTrophyBadgeSelected'
import createServer, { getContext } from '@src/server'
import type { ApolloTestSetup } from '@root/test/helpers'
import { createApolloTestSetup } from '@root/test/helpers'
import type { Context } from '@src/context'
let regularUser, administrator, moderator, badge, verification
const database = databaseContext()
let server: ApolloServer
let authenticatedUser
let query, mutate
let authenticatedUser: Context['user']
const context = () => ({ authenticatedUser })
let mutate: ApolloTestSetup['mutate']
let query: ApolloTestSetup['query']
let database: ApolloTestSetup['database']
let server: ApolloTestSetup['server']
beforeAll(async () => {
await cleanDatabase()
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/require-await
const contextUser = async (_req) => authenticatedUser
const context = getContext({ user: contextUser, database })
server = createServer({ context }).server
const createTestClientResult = createTestClient(server)
query = createTestClientResult.query
mutate = createTestClientResult.mutate
const apolloSetup = createApolloTestSetup({ context })
mutate = apolloSetup.mutate
query = apolloSetup.query
database = apolloSetup.database
server = apolloSetup.server
})
afterAll(() => {
@ -838,7 +833,7 @@ describe('Badges', () => {
describe('check test setup', () => {
it('user has one badge and has it selected', async () => {
authenticatedUser = regularUser.toJson()
authenticatedUser = await regularUser.toJson()
const userQuery = gql`
{
User(id: "regular-user-id") {

View File

@ -7,7 +7,7 @@
import { neo4jgraphql } from 'neo4j-graphql-js'
import { TROPHY_BADGES_SELECTED_MAX } from '@constants/badges'
import { Context } from '@src/server'
import { Context } from '@src/context'
export const defaultTrophyBadge = {
id: 'default_trophy',
@ -32,7 +32,10 @@ export default {
},
Mutation: {
setVerificationBadge: async (_object, args, context, _resolveInfo) => {
setVerificationBadge: async (_object, args, context: Context, _resolveInfo) => {
if (!context.user) {
throw new Error('Missing authenticated user.')
}
const {
user: { id: currentUserId },
} = context
@ -70,11 +73,14 @@ export default {
} catch (error) {
throw new Error(error)
} finally {
session.close()
await session.close()
}
},
rewardTrophyBadge: async (_object, args, context: Context, _resolveInfo) => {
if (!context.user) {
throw new Error('Missing authenticated user.')
}
const {
user: { id: currentUserId },
} = context

View File

@ -2,29 +2,26 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { ApolloServer } from 'apollo-server-express'
import { createTestClient } from 'apollo-server-testing'
import gql from 'graphql-tag'
import databaseContext from '@context/database'
import Factory, { cleanDatabase } from '@db/factories'
import createServer, { getContext } from '@src/server'
import type { ApolloTestSetup } from '@root/test/helpers'
import { createApolloTestSetup } from '@root/test/helpers'
import type { Context } from '@src/context'
const database = databaseContext()
let variables, commentAuthor, newlyCreatedComment
let authenticatedUser: Context['user']
const context = () => ({ authenticatedUser })
let mutate: ApolloTestSetup['mutate']
let database: ApolloTestSetup['database']
let server: ApolloTestSetup['server']
let variables, mutate, authenticatedUser, commentAuthor, newlyCreatedComment
let server: ApolloServer
beforeAll(async () => {
await cleanDatabase()
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/require-await
const contextUser = async (_req) => authenticatedUser
const context = getContext({ user: contextUser, database })
server = createServer({ context }).server
mutate = createTestClient(server).mutate
const apolloSetup = createApolloTestSetup({ context })
mutate = apolloSetup.mutate
database = apolloSetup.database
server = apolloSetup.server
})
afterAll(async () => {
@ -35,6 +32,7 @@ afterAll(async () => {
})
beforeEach(async () => {
authenticatedUser = null
variables = {}
await database.neode.create('Category', {
id: 'cat9',
@ -98,14 +96,14 @@ describe('CreateComment', () => {
content: "I'm not authorized to comment",
}
const { errors } = await mutate({ mutation: createCommentMutation, variables })
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
describe('authenticated', () => {
beforeEach(async () => {
const user = await database.neode.create('User', { name: 'Author' })
authenticatedUser = await user.toJson()
authenticatedUser = (await user.toJson()) as Context['user']
})
describe('given a post', () => {
@ -157,7 +155,7 @@ describe('UpdateComment', () => {
describe('unauthenticated', () => {
it('throws authorization error', async () => {
const { errors } = await mutate({ mutation: updateCommentMutation, variables })
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
@ -169,7 +167,7 @@ describe('UpdateComment', () => {
it('throws authorization error', async () => {
const { errors } = await mutate({ mutation: updateCommentMutation, variables })
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
@ -208,7 +206,7 @@ describe('UpdateComment', () => {
newlyCreatedComment = await newlyCreatedComment.toJson()
const {
data: { UpdateComment },
} = await mutate({ mutation: updateCommentMutation, variables })
} = (await mutate({ mutation: updateCommentMutation, variables })) as any // eslint-disable-line @typescript-eslint/no-explicit-any
expect(newlyCreatedComment.updatedAt).toBeTruthy()
expect(Date.parse(newlyCreatedComment.updatedAt)).toEqual(expect.any(Number))
expect(UpdateComment.updatedAt).toBeTruthy()
@ -224,7 +222,7 @@ describe('UpdateComment', () => {
it('returns null', async () => {
const { data, errors } = await mutate({ mutation: updateCommentMutation, variables })
expect(data).toMatchObject({ UpdateComment: null })
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
})
@ -249,7 +247,7 @@ describe('DeleteComment', () => {
describe('unauthenticated', () => {
it('throws authorization error', async () => {
const result = await mutate({ mutation: deleteCommentMutation, variables })
expect(result.errors[0]).toHaveProperty('message', 'Not Authorized!')
expect(result.errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
@ -261,7 +259,7 @@ describe('DeleteComment', () => {
it('throws authorization error', async () => {
const { errors } = await mutate({ mutation: deleteCommentMutation, variables })
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})

View File

@ -1,44 +1,37 @@
/* 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 CONFIG from '@config/index'
/* eslint-disable @typescript-eslint/no-explicit-any */
import Factory, { cleanDatabase } from '@db/factories'
import { getNeode, getDriver } from '@db/neo4j'
import { createPostMutation } from '@graphql/queries/createPostMutation'
import { filterPosts } from '@graphql/queries/filterPosts'
import createServer from '@src/server'
import type { ApolloTestSetup } from '@root/test/helpers'
import { createApolloTestSetup } from '@root/test/helpers'
import type { Context } from '@src/context'
CONFIG.CATEGORIES_ACTIVE = false
const driver = getDriver()
const neode = getNeode()
let query
let mutate
let authenticatedUser
let user
let authenticatedUser: Context['user']
const config = { CATEGORIES_ACTIVE: false }
const context = () => ({ authenticatedUser, config })
let mutate: ApolloTestSetup['mutate']
let query: ApolloTestSetup['query']
let database: ApolloTestSetup['database']
let server: ApolloTestSetup['server']
beforeAll(async () => {
await cleanDatabase()
const { server } = createServer({
context: () => {
return {
driver,
neode,
user: authenticatedUser,
}
},
})
query = createTestClient(server).query
mutate = createTestClient(server).mutate
const apolloSetup = createApolloTestSetup({ context })
mutate = apolloSetup.mutate
query = apolloSetup.query
database = apolloSetup.database
server = apolloSetup.server
})
afterAll(async () => {
await cleanDatabase()
await driver.close()
void server.stop()
void database.driver.close()
database.neode.close()
})
describe('Filter Posts', () => {
@ -99,7 +92,7 @@ describe('Filter Posts', () => {
it('finds all posts', async () => {
const {
data: { Post: result },
} = await query({ query: filterPosts() })
} = (await query({ query: filterPosts() })) as any
expect(result).toHaveLength(4)
expect(result).toEqual(
expect.arrayContaining([
@ -116,7 +109,10 @@ describe('Filter Posts', () => {
it('finds the articles', async () => {
const {
data: { Post: result },
} = await query({ query: filterPosts(), variables: { filter: { postType_in: ['Article'] } } })
} = (await query({
query: filterPosts(),
variables: { filter: { postType_in: ['Article'] } },
})) as any
expect(result).toHaveLength(2)
expect(result).toEqual(
expect.arrayContaining([
@ -131,7 +127,10 @@ describe('Filter Posts', () => {
it('finds the articles', async () => {
const {
data: { Post: result },
} = await query({ query: filterPosts(), variables: { filter: { postType_in: ['Event'] } } })
} = (await query({
query: filterPosts(),
variables: { filter: { postType_in: ['Event'] } },
})) as any
expect(result).toHaveLength(2)
expect(result).toEqual(
expect.arrayContaining([
@ -146,10 +145,10 @@ describe('Filter Posts', () => {
it('finds all posts', async () => {
const {
data: { Post: result },
} = await query({
} = (await query({
query: filterPosts(),
variables: { filter: { postType_in: ['Article', 'Event'] } },
})
})) as any
expect(result).toHaveLength(4)
expect(result).toEqual(
expect.arrayContaining([
@ -166,10 +165,10 @@ describe('Filter Posts', () => {
it('finds the events ordered accordingly', async () => {
const {
data: { Post: result },
} = await query({
} = (await query({
query: filterPosts(),
variables: { filter: { postType_in: ['Event'] }, orderBy: ['eventStart_desc'] },
})
})) as any
expect(result).toHaveLength(2)
expect(result).toEqual([
expect.objectContaining({
@ -190,10 +189,10 @@ describe('Filter Posts', () => {
it('finds the events ordered accordingly', async () => {
const {
data: { Post: result },
} = await query({
} = (await query({
query: filterPosts(),
variables: { filter: { postType_in: ['Event'] }, orderBy: ['eventStart_asc'] },
})
})) as any
expect(result).toHaveLength(2)
expect(result).toEqual([
expect.objectContaining({
@ -214,7 +213,7 @@ describe('Filter Posts', () => {
it('finds only events after given date', async () => {
const {
data: { Post: result },
} = await query({
} = (await query({
query: filterPosts(),
variables: {
filter: {
@ -226,7 +225,7 @@ describe('Filter Posts', () => {
).toISOString(),
},
},
})
})) as any
expect(result).toHaveLength(1)
expect(result).toEqual([
expect.objectContaining({

View File

@ -3,10 +3,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { createTestClient } from 'apollo-server-testing'
import CONFIG from '@config/index'
import databaseContext from '@context/database'
import Factory, { cleanDatabase } from '@db/factories'
import { changeGroupMemberRoleMutation } from '@graphql/queries/changeGroupMemberRoleMutation'
import { createGroupMutation } from '@graphql/queries/createGroupMutation'
@ -16,9 +12,11 @@ import { joinGroupMutation } from '@graphql/queries/joinGroupMutation'
import { leaveGroupMutation } from '@graphql/queries/leaveGroupMutation'
import { removeUserFromGroupMutation } from '@graphql/queries/removeUserFromGroupMutation'
import { updateGroupMutation } from '@graphql/queries/updateGroupMutation'
import createServer, { getContext } from '@src/server'
import type { ApolloTestSetup } from '@root/test/helpers'
import { createApolloTestSetup } from '@root/test/helpers'
import type { Context } from '@src/context'
// import CONFIG from '@src/config'
let authenticatedUser
let user
let noMemberUser
let pendingMemberUser
@ -27,18 +25,21 @@ let adminMemberUser
let ownerMemberUser
let secondOwnerMemberUser
let authenticatedUser: Context['user']
const context = () => ({ authenticatedUser, config })
let mutate: ApolloTestSetup['mutate']
let query: ApolloTestSetup['query']
let database: ApolloTestSetup['database']
let server: ApolloTestSetup['server']
const categoryIds = ['cat9', 'cat4', 'cat15']
const descriptionAdditional100 =
' 123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789'
let variables = {}
const database = databaseContext()
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
const contextUser = async (_req) => authenticatedUser
const context = getContext({ user: contextUser, database })
const { server } = createServer({ context })
const { mutate, query } = createTestClient(server)
const config = {
CATEGORIES_ACTIVE: true,
// MAPBOX_TOKEN: CONFIG.MAPBOX_TOKEN,
}
const seedBasicsAndClearAuthentication = async () => {
variables = {}
@ -230,7 +231,11 @@ const seedComplexScenarioAndClearAuthentication = async () => {
}
beforeAll(async () => {
await cleanDatabase()
const apolloSetup = createApolloTestSetup({ context })
mutate = apolloSetup.mutate
query = apolloSetup.query
database = apolloSetup.database
server = apolloSetup.server
})
afterAll(async () => {
@ -270,7 +275,7 @@ describe('in mode', () => {
describe('unauthenticated', () => {
it('throws authorization error', async () => {
const { errors } = await mutate({ mutation: createGroupMutation(), variables })
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
@ -339,17 +344,13 @@ describe('in mode', () => {
'<a href="https://domain.org/0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789">0</a>',
},
})
expect(errors![0]).toHaveProperty('message', 'Description too short!')
expect(errors?.[0]).toHaveProperty('message', 'Description too short!')
})
})
})
})
describe('categories', () => {
beforeEach(() => {
CONFIG.CATEGORIES_ACTIVE = true
})
describe('with matching amount of categories', () => {
it('has new categories', async () => {
await expect(
@ -382,7 +383,7 @@ describe('in mode', () => {
mutation: createGroupMutation(),
variables: { ...variables, categoryIds: null },
})
expect(errors![0]).toHaveProperty('message', 'Too few categories!')
expect(errors?.[0]).toHaveProperty('message', 'Too few categories!')
})
})
@ -392,7 +393,7 @@ describe('in mode', () => {
mutation: createGroupMutation(),
variables: { ...variables, categoryIds: [] },
})
expect(errors![0]).toHaveProperty('message', 'Too few categories!')
expect(errors?.[0]).toHaveProperty('message', 'Too few categories!')
})
})
})
@ -403,7 +404,7 @@ describe('in mode', () => {
mutation: createGroupMutation(),
variables: { ...variables, categoryIds: ['cat9', 'cat4', 'cat15', 'cat27'] },
})
expect(errors![0]).toHaveProperty('message', 'Too many categories!')
expect(errors?.[0]).toHaveProperty('message', 'Too many categories!')
})
})
})
@ -581,10 +582,6 @@ describe('in mode', () => {
})
describe('categories', () => {
beforeEach(() => {
CONFIG.CATEGORIES_ACTIVE = true
})
it('has set categories', async () => {
await expect(query({ query: groupQuery, variables: {} })).resolves.toMatchObject({
data: {
@ -811,7 +808,7 @@ describe('in mode', () => {
userId: 'current-user',
},
})
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
@ -1566,7 +1563,7 @@ describe('in mode', () => {
roleInGroup: 'pending',
},
})
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
@ -1721,7 +1718,7 @@ describe('in mode', () => {
mutation: changeGroupMemberRoleMutation(),
variables,
})
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
})
@ -1747,7 +1744,7 @@ describe('in mode', () => {
mutation: changeGroupMemberRoleMutation(),
variables,
})
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
@ -1796,7 +1793,7 @@ describe('in mode', () => {
mutation: changeGroupMemberRoleMutation(),
variables,
})
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
})
@ -1819,7 +1816,7 @@ describe('in mode', () => {
mutation: changeGroupMemberRoleMutation(),
variables,
})
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
})
@ -1842,7 +1839,7 @@ describe('in mode', () => {
mutation: changeGroupMemberRoleMutation(),
variables,
})
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
})
@ -1900,7 +1897,7 @@ describe('in mode', () => {
mutation: changeGroupMemberRoleMutation(),
variables,
})
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
})
@ -1923,7 +1920,7 @@ describe('in mode', () => {
mutation: changeGroupMemberRoleMutation(),
variables,
})
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
@ -1940,7 +1937,7 @@ describe('in mode', () => {
mutation: changeGroupMemberRoleMutation(),
variables,
})
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
})
@ -1963,7 +1960,7 @@ describe('in mode', () => {
mutation: changeGroupMemberRoleMutation(),
variables,
})
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
@ -1980,7 +1977,7 @@ describe('in mode', () => {
mutation: changeGroupMemberRoleMutation(),
variables,
})
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
})
@ -2003,7 +2000,7 @@ describe('in mode', () => {
mutation: changeGroupMemberRoleMutation(),
variables,
})
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
@ -2020,7 +2017,7 @@ describe('in mode', () => {
mutation: changeGroupMemberRoleMutation(),
variables,
})
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
})
@ -2110,7 +2107,7 @@ describe('in mode', () => {
mutation: changeGroupMemberRoleMutation(),
variables,
})
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
@ -2127,7 +2124,7 @@ describe('in mode', () => {
mutation: changeGroupMemberRoleMutation(),
variables,
})
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
})
@ -2150,7 +2147,7 @@ describe('in mode', () => {
mutation: changeGroupMemberRoleMutation(),
variables,
})
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
@ -2167,7 +2164,7 @@ describe('in mode', () => {
mutation: changeGroupMemberRoleMutation(),
variables,
})
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
})
@ -2190,7 +2187,7 @@ describe('in mode', () => {
mutation: changeGroupMemberRoleMutation(),
variables,
})
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
@ -2207,7 +2204,7 @@ describe('in mode', () => {
mutation: changeGroupMemberRoleMutation(),
variables,
})
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
})
@ -2297,7 +2294,7 @@ describe('in mode', () => {
mutation: changeGroupMemberRoleMutation(),
variables,
})
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
})
@ -2320,7 +2317,7 @@ describe('in mode', () => {
mutation: changeGroupMemberRoleMutation(),
variables,
})
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
})
@ -2343,7 +2340,7 @@ describe('in mode', () => {
mutation: changeGroupMemberRoleMutation(),
variables,
})
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
})
@ -2407,7 +2404,7 @@ describe('in mode', () => {
userId: 'current-user',
},
})
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
@ -2524,7 +2521,7 @@ describe('in mode', () => {
userId: 'owner-member-user',
},
})
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
@ -2538,7 +2535,7 @@ describe('in mode', () => {
userId: 'second-owner-member-user',
},
})
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
@ -2552,7 +2549,7 @@ describe('in mode', () => {
userId: 'none-member-user',
},
})
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
@ -2566,7 +2563,7 @@ describe('in mode', () => {
userId: 'usual-member-user',
},
})
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
@ -2580,7 +2577,7 @@ describe('in mode', () => {
userId: 'admin-member-user',
},
})
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
})
@ -2606,7 +2603,7 @@ describe('in mode', () => {
slug: 'my-best-group',
},
})
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
@ -2859,17 +2856,13 @@ describe('in mode', () => {
'<a href="https://domain.org/0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789">0</a>',
},
})
expect(errors![0]).toHaveProperty('message', 'Description too short!')
expect(errors?.[0]).toHaveProperty('message', 'Description too short!')
})
})
})
})
describe('categories', () => {
beforeEach(async () => {
CONFIG.CATEGORIES_ACTIVE = true
})
describe('with matching amount of categories', () => {
it('has new categories', async () => {
await expect(
@ -2906,7 +2899,7 @@ describe('in mode', () => {
categoryIds: [],
},
})
expect(errors![0]).toHaveProperty('message', 'Too few categories!')
expect(errors?.[0]).toHaveProperty('message', 'Too few categories!')
})
})
})
@ -2920,7 +2913,7 @@ describe('in mode', () => {
categoryIds: ['cat9', 'cat4', 'cat15', 'cat27'],
},
})
expect(errors![0]).toHaveProperty('message', 'Too many categories!')
expect(errors?.[0]).toHaveProperty('message', 'Too many categories!')
})
})
})
@ -2940,7 +2933,7 @@ describe('in mode', () => {
categoryIds: ['cat4', 'cat27'],
},
})
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
@ -2958,7 +2951,7 @@ describe('in mode', () => {
categoryIds: ['cat4', 'cat27'],
},
})
expect(errors![0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
})

View File

@ -8,11 +8,10 @@
import { UserInputError } from 'apollo-server'
import { v4 as uuid } from 'uuid'
import CONFIG from '@config/index'
import { CATEGORIES_MIN, CATEGORIES_MAX } from '@constants/categories'
import { DESCRIPTION_WITHOUT_HTML_LENGTH_MIN } from '@constants/groups'
import { removeHtmlTags } from '@middleware/helpers/cleanHtml'
import type { Context } from '@src/server'
import type { Context } from '@src/context'
import Resolver, {
removeUndefinedNullValuesFromObject,
@ -32,6 +31,9 @@ export default {
removeUndefinedNullValuesFromObject(matchParams)
const session = context.driver.session()
const readTxResultPromise = session.readTransaction(async (txc) => {
if (!context.user) {
throw new Error('Missing authenticated user.')
}
const groupMatchParamsCypher = convertObjectToCypherMapLiteral(matchParams, true)
let groupCypher
if (isMember === true) {
@ -139,13 +141,14 @@ export default {
},
Mutation: {
CreateGroup: async (_parent, params, context: Context, _resolveInfo) => {
const { config } = context
const { categoryIds } = params
delete params.categoryIds
params.locationName = params.locationName === '' ? null : params.locationName
if (CONFIG.CATEGORIES_ACTIVE && (!categoryIds || categoryIds.length < CATEGORIES_MIN)) {
if (config.CATEGORIES_ACTIVE && (!categoryIds || categoryIds.length < CATEGORIES_MIN)) {
throw new UserInputError('Too few categories!')
}
if (CONFIG.CATEGORIES_ACTIVE && categoryIds && categoryIds.length > CATEGORIES_MAX) {
if (config.CATEGORIES_ACTIVE && categoryIds && categoryIds.length > CATEGORIES_MAX) {
throw new UserInputError('Too many categories!')
}
if (
@ -158,8 +161,11 @@ export default {
params.id = params.id || uuid()
const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
if (!context.user) {
throw new Error('Missing authenticated user.')
}
const categoriesCypher =
CONFIG.CATEGORIES_ACTIVE && categoryIds
config.CATEGORIES_ACTIVE && categoryIds
? `
WITH group, membership
UNWIND $categoryIds AS categoryId
@ -194,7 +200,7 @@ export default {
try {
const group = await writeTxResultPromise
// TODO: put in a middleware, see "UpdateGroup", "UpdateUser"
await createOrUpdateLocations('Group', params.id, params.locationName, session)
await createOrUpdateLocations('Group', params.id, params.locationName, session, context)
return group
} catch (error) {
if (error.code === 'Neo.ClientError.Schema.ConstraintValidationFailed')
@ -205,13 +211,14 @@ export default {
}
},
UpdateGroup: async (_parent, params, context: Context, _resolveInfo) => {
const { config } = context
const { categoryIds } = params
delete params.categoryIds
const { id: groupId, avatar: avatarInput } = params
delete params.avatar
params.locationName = params.locationName === '' ? null : params.locationName
if (CONFIG.CATEGORIES_ACTIVE && categoryIds) {
if (config.CATEGORIES_ACTIVE && categoryIds) {
if (categoryIds.length < CATEGORIES_MIN) {
throw new UserInputError('Too few categories!')
}
@ -226,7 +233,7 @@ export default {
throw new UserInputError('Description too short!')
}
const session = context.driver.session()
if (CONFIG.CATEGORIES_ACTIVE && categoryIds && categoryIds.length) {
if (config.CATEGORIES_ACTIVE && categoryIds && categoryIds.length) {
const cypherDeletePreviousRelations = `
MATCH (group:Group {id: $groupId})-[previousRelations:CATEGORIZED]->(category:Category)
DELETE previousRelations
@ -237,13 +244,16 @@ export default {
})
}
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
if (!context.user) {
throw new Error('Missing authenticated user.')
}
let updateGroupCypher = `
MATCH (group:Group {id: $groupId})
SET group += $params
SET group.updatedAt = toString(datetime())
WITH group
`
if (CONFIG.CATEGORIES_ACTIVE && categoryIds && categoryIds.length) {
if (config.CATEGORIES_ACTIVE && categoryIds && categoryIds.length) {
updateGroupCypher += `
UNWIND $categoryIds AS categoryId
MATCH (category:Category {id: categoryId})
@ -263,14 +273,16 @@ export default {
})
const [group] = transactionResponse.records.map((record) => record.get('group'))
if (avatarInput) {
await images.mergeImage(group, 'AVATAR_IMAGE', avatarInput, { transaction })
await images(context.config).mergeImage(group, 'AVATAR_IMAGE', avatarInput, {
transaction,
})
}
return group
})
try {
const group = await writeTxResultPromise
// TODO: put in a middleware, see "CreateGroup", "UpdateUser"
await createOrUpdateLocations('Group', params.id, params.locationName, session)
await createOrUpdateLocations('Group', params.id, params.locationName, session, context)
return group
} catch (error) {
if (error.code === 'Neo.ClientError.Schema.ConstraintValidationFailed')
@ -380,10 +392,16 @@ export default {
}
},
muteGroup: async (_parent, params, context: Context, _resolveInfo) => {
if (!context.user) {
throw new Error('Missing authenticated user.')
}
const { groupId } = params
const userId = context.user.id
const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
if (!context.user) {
throw new Error('Missing authenticated user.')
}
const transactionResponse = await transaction.run(
`
MATCH (group:Group { id: $groupId })
@ -409,6 +427,9 @@ export default {
}
},
unmuteGroup: async (_parent, params, context: Context, _resolveInfo) => {
if (!context.user) {
throw new Error('Missing authenticated user.')
}
const { groupId } = params
const userId = context.user.id
const session = context.driver.session()

View File

@ -1,10 +1,15 @@
/* 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 type { Context } from '@src/context'
import normalizeEmail from './normalizeEmail'
export default async function createPasswordReset(options) {
export default async function createPasswordReset(options: {
driver: Context['driver']
nonce: string
email: string
issuedAt?: Date
}) {
const { driver, nonce, email, issuedAt = new Date() } = options
const normalizedEmail = normalizeEmail(email)
const session = driver.session()
@ -33,6 +38,6 @@ export default async function createPasswordReset(options) {
const [records] = await createPasswordResetTxPromise
return records || {}
} finally {
session.close()
await session.close()
}
}

View File

@ -1,16 +1,11 @@
import CONFIG, { isS3configured } from '@config/index'
import type { Context } from '@src/context'
import type { FileDeleteCallback, FileUploadCallback } from '@src/uploads/types'
import { images as imagesLocal } from './imagesLocal'
import { images as imagesS3 } from './imagesS3'
import type { FileUpload } from 'graphql-upload'
import type { Transaction } from 'neo4j-driver'
export type FileDeleteCallback = (url: string) => Promise<void>
export type FileUploadCallback = (
upload: Pick<FileUpload, 'createReadStream' | 'mimetype'> & { uniqueFilename: string },
) => Promise<string>
export interface DeleteImageOpts {
transaction?: Transaction
deleteCallback?: FileDeleteCallback
@ -55,4 +50,4 @@ export interface Images {
) => Promise<any>
}
export const images = isS3configured(CONFIG) ? imagesS3(CONFIG) : imagesLocal
export const images = (config: Context['config']) => imagesS3(config)

View File

@ -1,364 +0,0 @@
/* eslint-disable @typescript-eslint/require-await */
/* 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 promise/prefer-await-to-callbacks */
import { UserInputError } from 'apollo-server'
import Factory, { cleanDatabase } from '@db/factories'
import { getNeode, getDriver } from '@db/neo4j'
import { images } from './imagesLocal'
import type { ImageInput } from './images'
import type { FileUpload } from 'graphql-upload'
const driver = getDriver()
const neode = getNeode()
const uuid = '[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}'
let uploadCallback
let deleteCallback
beforeAll(async () => {
await cleanDatabase()
})
afterAll(async () => {
await cleanDatabase()
await driver.close()
})
beforeEach(async () => {
uploadCallback = jest.fn(({ uniqueFilename }) => `/uploads/${uniqueFilename}`)
deleteCallback = jest.fn()
})
// 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
afterEach(async () => {
await cleanDatabase()
})
describe('deleteImage', () => {
const { deleteImage } = images
describe('given a resource with an image', () => {
let user: { id: string }
beforeEach(async () => {
const u = await Factory.build(
'user',
{},
{
avatar: Factory.build('image', {
url: 'http://localhost/some/avatar/url/',
alt: 'This is the avatar image of a user',
}),
},
)
user = await u.toJson()
})
it('deletes `Image` node', async () => {
await expect(neode.all('Image')).resolves.toHaveLength(1)
await deleteImage(user, 'AVATAR_IMAGE', { deleteCallback })
await expect(neode.all('Image')).resolves.toHaveLength(0)
})
it('calls deleteCallback', async () => {
const u = await Factory.build('user')
user = await u.toJson()
await deleteImage(user, 'AVATAR_IMAGE', { deleteCallback })
expect(deleteCallback).toHaveBeenCalled()
})
describe('given a transaction parameter', () => {
it('executes cypher statements within the transaction', async () => {
const session = driver.session()
let someString: string
try {
someString = await session.writeTransaction(async (transaction) => {
await deleteImage(user, 'AVATAR_IMAGE', {
deleteCallback,
transaction,
})
const txResult = await transaction.run('RETURN "Hello" as result')
const [result] = txResult.records.map((record) => record.get('result'))
return result
})
} finally {
await session.close()
}
await expect(neode.all('Image')).resolves.toHaveLength(0)
expect(someString).toEqual('Hello')
})
it('rolls back the transaction in case of errors', async () => {
await expect(neode.all('Image')).resolves.toHaveLength(1)
const session = driver.session()
try {
await session.writeTransaction(async (transaction) => {
await deleteImage(user, 'AVATAR_IMAGE', {
deleteCallback,
transaction,
})
throw new Error('Ouch!')
})
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (err) {
// nothing has been deleted
await expect(neode.all('Image')).resolves.toHaveLength(1)
// all good
} finally {
await session.close()
}
})
})
})
})
describe('mergeImage', () => {
const { mergeImage } = images
let imageInput: ImageInput
let post: { id: string }
beforeEach(() => {
imageInput = {
alt: 'A description of the new image',
}
})
describe('given image.upload', () => {
beforeEach(() => {
const createReadStream: FileUpload['createReadStream'] = (() => ({
pipe: () => ({
on: (_, callback) => callback(),
}),
})) as unknown as FileUpload['createReadStream']
imageInput = {
...imageInput,
upload: Promise.resolve({
filename: 'image.jpg',
mimetype: 'image/jpeg',
encoding: '7bit',
createReadStream,
}),
}
})
describe('on existing resource', () => {
beforeEach(async () => {
const p = await Factory.build(
'post',
{ id: 'p99' },
{
author: Factory.build('user', {}, { avatar: null }),
image: null,
},
)
post = await p.toJson()
})
it('returns new image', async () => {
await expect(
mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }),
).resolves.toMatchObject({
url: expect.any(String),
alt: 'A description of the new image',
})
})
it('calls upload callback', async () => {
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
expect(uploadCallback).toHaveBeenCalled()
})
it('creates `:Image` node', async () => {
await expect(neode.all('Image')).resolves.toHaveLength(0)
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
await expect(neode.all('Image')).resolves.toHaveLength(1)
})
it('creates a url safe name', async () => {
if (!imageInput.upload) {
throw new Error('Test imageInput was not setup correctly.')
}
const upload = await imageInput.upload
upload.filename = '/path/to/awkward?/ file-location/?foo- bar-avatar.jpg'
imageInput.upload = Promise.resolve(upload)
await expect(
mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }),
).resolves.toMatchObject({
url: expect.stringMatching(new RegExp(`^/uploads/${uuid}-foo-bar-avatar.jpg`)),
})
})
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`,
{},
)
post = neode.hydrateFirst<{ id: string }>(result, 'p', neode.model('Post')).properties()
const image = neode.hydrateFirst(result, 'i', neode.model('Image'))
expect(post).toBeTruthy()
expect(image).toBeTruthy()
})
it('sets metadata', async () => {
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
const image = await neode.first<typeof Image>('Image', {}, undefined)
await expect(image.toJson()).resolves.toMatchObject({
alt: 'A description of the new image',
createdAt: expect.any(String),
url: expect.any(String),
})
})
describe('given a transaction parameter', () => {
it('executes cypher statements within the transaction', async () => {
const session = driver.session()
try {
await session.writeTransaction(async (transaction) => {
const image = await mergeImage(post, 'HERO_IMAGE', imageInput, {
uploadCallback,
deleteCallback,
transaction,
})
return transaction.run(
`
MATCH(image:Image {url: $image.url})
SET image.alt = 'This alt text gets overwritten'
RETURN image {.*}
`,
{ image },
)
})
} finally {
await session.close()
}
const image = await neode.first<typeof Image>(
'Image',
{ alt: 'This alt text gets overwritten' },
undefined,
)
await expect(image.toJson()).resolves.toMatchObject({
alt: 'This alt text gets overwritten',
})
})
it('rolls back the transaction in case of errors', async () => {
const session = driver.session()
try {
await session.writeTransaction(async (transaction) => {
const image = await mergeImage(post, 'HERO_IMAGE', imageInput, {
uploadCallback,
deleteCallback,
transaction,
})
return transaction.run('Ooops invalid cypher!', { image })
})
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (err) {
// nothing has been created
await expect(neode.all('Image')).resolves.toHaveLength(0)
// all good
} finally {
await session.close()
}
})
})
describe('if resource has an image already', () => {
beforeEach(async () => {
const [post, image] = await Promise.all([
neode.find('Post', 'p99'),
Factory.build('image'),
])
await post.relateTo(image, 'image')
})
it('calls deleteCallback', async () => {
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
expect(deleteCallback).toHaveBeenCalled()
})
it('calls uploadCallback', async () => {
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
expect(uploadCallback).toHaveBeenCalled()
})
it('updates metadata of existing image node', async () => {
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<typeof Image>('Image', {}, undefined)
await expect(image.toJson()).resolves.toMatchObject({
alt: 'A description of the new image',
createdAt: expect.any(String),
url: expect.any(String),
// TODO
// width:
// height:
})
})
})
})
})
describe('without image.upload', () => {
it('throws UserInputError', async () => {
const p = await Factory.build('post', { id: 'p99' }, { image: null })
post = await p.toJson()
await expect(mergeImage(post, 'HERO_IMAGE', imageInput)).rejects.toEqual(
new UserInputError('Cannot find image for given resource'),
)
})
describe('if resource has an image already', () => {
beforeEach(async () => {
const p = await Factory.build(
'post',
{
id: 'p99',
},
{
author: Factory.build(
'user',
{},
{
avatar: null,
},
),
image: Factory.build('image', {
alt: 'This is the previous, not updated image',
url: 'http://localhost/some/original/url',
}),
},
)
post = await p.toJson()
})
it('does not call deleteCallback', async () => {
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
expect(deleteCallback).not.toHaveBeenCalled()
})
it('does not call uploadCallback', async () => {
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
expect(uploadCallback).not.toHaveBeenCalled()
})
it('updates metadata', async () => {
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
const images = await neode.all('Image')
expect(images).toHaveLength(1)
await expect(images.first().toJson()).resolves.toMatchObject({
createdAt: expect.any(String),
url: expect.any(String),
alt: 'A description of the new image',
})
})
})
})
})

View File

@ -1,131 +0,0 @@
/* eslint-disable @typescript-eslint/require-await */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable promise/avoid-new */
/* eslint-disable security/detect-non-literal-fs-filename */
import { existsSync, unlinkSync, createWriteStream } from 'node:fs'
import path from 'node:path'
import { UserInputError } from 'apollo-server'
import slug from 'slug'
import { v4 as uuid } from 'uuid'
import { wrapTransaction } from './wrapTransaction'
import type { Images, FileDeleteCallback, FileUploadCallback } from './images'
import type { FileUpload } from 'graphql-upload'
const deleteImage: Images['deleteImage'] = async (resource, relationshipType, opts = {}) => {
const { transaction, deleteCallback } = opts
if (!transaction) return wrapTransaction(deleteImage, [resource, relationshipType], opts)
const txResult = await transaction.run(
`
MATCH (resource {id: $resource.id})-[rel:${relationshipType}]->(image:Image)
WITH image, image {.*} as imageProps
DETACH DELETE image
RETURN imageProps
`,
{ resource },
)
const [image] = txResult.records.map((record) => record.get('imageProps'))
// This behaviour differs from `mergeImage`. If you call `mergeImage`
// with metadata for an image that does not exist, it's an indicator
// of an error (so throw an error). If we bulk delete an image, it
// could very well be that there is no image for the resource.
if (image) deleteImageFile(image, deleteCallback)
return image
}
const mergeImage: Images['mergeImage'] = async (
resource,
relationshipType,
imageInput,
opts = {},
) => {
if (typeof imageInput === 'undefined') return
if (imageInput === null) return deleteImage(resource, relationshipType, opts)
const { transaction, uploadCallback, deleteCallback } = opts
if (!transaction)
return wrapTransaction(mergeImage, [resource, relationshipType, imageInput], opts)
let txResult
txResult = await transaction.run(
`
MATCH (resource {id: $resource.id})-[:${relationshipType}]->(image:Image)
RETURN image {.*}
`,
{ resource },
)
const [existingImage] = txResult.records.map((record) => record.get('image'))
const { upload } = imageInput
if (!(existingImage || upload)) throw new UserInputError('Cannot find image for given resource')
if (existingImage && upload) deleteImageFile(existingImage, deleteCallback)
const url = await uploadImageFile(upload, uploadCallback)
const { alt, sensitive, aspectRatio, type } = imageInput
const image = { alt, sensitive, aspectRatio, url, type }
txResult = await transaction.run(
`
MATCH (resource {id: $resource.id})
MERGE (resource)-[:${relationshipType}]->(image:Image)
ON CREATE SET image.createdAt = toString(datetime())
ON MATCH SET image.updatedAt = toString(datetime())
SET image += $image
RETURN image {.*}
`,
{ resource, image },
)
const [mergedImage] = txResult.records.map((record) => record.get('image'))
return mergedImage
}
const localFileDelete: FileDeleteCallback = async (url) => {
const location = `public${url}`
// eslint-disable-next-line n/no-sync
if (existsSync(location)) unlinkSync(location)
}
const deleteImageFile = (image, deleteCallback: FileDeleteCallback | undefined) => {
if (!deleteCallback) {
deleteCallback = localFileDelete
}
const { url } = image
// eslint-disable-next-line @typescript-eslint/no-floating-promises
deleteCallback(url)
return url
}
const uploadImageFile = async (
upload: Promise<FileUpload> | undefined,
uploadCallback: FileUploadCallback | undefined,
) => {
if (!upload) return undefined
if (!uploadCallback) {
uploadCallback = localFileUpload
}
// eslint-disable-next-line @typescript-eslint/unbound-method
const { createReadStream, filename, mimetype } = await upload
const { name, ext } = path.parse(filename)
const uniqueFilename = `${uuid()}-${slug(name)}${ext}`
return uploadCallback({ createReadStream, uniqueFilename, mimetype })
}
const localFileUpload: FileUploadCallback = ({ createReadStream, uniqueFilename }) => {
const destination = `/uploads/${uniqueFilename}`
return new Promise((resolve, reject) =>
createReadStream().pipe(
createWriteStream(`public${destination}`)
.on('finish', () => resolve(destination))
.on('error', (error) => reject(error)),
),
)
}
export const images: Images = {
deleteImage,
mergeImage,
}

View File

@ -1,29 +1,47 @@
/* eslint-disable @typescript-eslint/require-await */
/* 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 promise/prefer-await-to-callbacks */
import { DeleteObjectCommand } from '@aws-sdk/client-s3'
import { Upload } from '@aws-sdk/lib-storage'
import { UserInputError } from 'apollo-server'
import Factory, { cleanDatabase } from '@db/factories'
import { getNeode, getDriver } from '@db/neo4j'
import type { S3Configured } from '@src/config'
import type { S3Config } from '@src/config'
import { images } from './imagesS3'
import type { ImageInput } from './images'
import type { FileUpload } from 'graphql-upload'
jest.mock('@aws-sdk/client-s3', () => {
return {
S3Client: jest.fn().mockImplementation(() => ({
send: jest.fn(),
})),
ObjectCannedACL: { public_read: 'public_read' },
DeleteObjectCommand: jest.fn().mockImplementation(() => ({})),
}
})
jest.mock('@aws-sdk/lib-storage', () => {
return {
Upload: jest.fn().mockImplementation(({ params: { Key } }: { params: { Key: string } }) => ({
done: () => Promise.resolve({ Location: `http://your-objectstorage.com/bucket/${Key}` }),
})),
}
})
const mockUpload = jest.mocked(Upload)
const mockDeleteObjectCommand = jest.mocked(DeleteObjectCommand)
const driver = getDriver()
const neode = getNeode()
const uuid = '[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}'
let uploadCallback
let deleteCallback
const config: S3Configured = {
const config: S3Config = {
AWS_ACCESS_KEY_ID: 'AWS_ACCESS_KEY_ID',
AWS_SECRET_ACCESS_KEY: 'AWS_SECRET_ACCESS_KEY',
AWS_BUCKET: 'AWS_BUCKET',
@ -41,16 +59,10 @@ afterAll(async () => {
await driver.close()
})
beforeEach(async () => {
uploadCallback = jest.fn(
({ uniqueFilename }) => `http://your-objectstorage.com/bucket/${uniqueFilename}`,
)
deleteCallback = jest.fn()
})
// 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
afterEach(async () => {
await cleanDatabase()
jest.clearAllMocks()
})
describe('deleteImage', () => {
@ -73,13 +85,13 @@ describe('deleteImage', () => {
it('deletes `Image` node', async () => {
await expect(neode.all('Image')).resolves.toHaveLength(1)
await deleteImage(user, 'AVATAR_IMAGE', { deleteCallback })
await deleteImage(user, 'AVATAR_IMAGE')
await expect(neode.all('Image')).resolves.toHaveLength(0)
})
it('calls deleteCallback', async () => {
await deleteImage(user, 'AVATAR_IMAGE', { deleteCallback })
expect(deleteCallback).toHaveBeenCalled()
await deleteImage(user, 'AVATAR_IMAGE')
expect(mockDeleteObjectCommand).toHaveBeenCalled()
})
describe('given a transaction parameter', () => {
@ -89,7 +101,6 @@ describe('deleteImage', () => {
try {
someString = await session.writeTransaction(async (transaction) => {
await deleteImage(user, 'AVATAR_IMAGE', {
deleteCallback,
transaction,
})
const txResult = await transaction.run('RETURN "Hello" as result')
@ -109,7 +120,6 @@ describe('deleteImage', () => {
try {
await session.writeTransaction(async (transaction) => {
await deleteImage(user, 'AVATAR_IMAGE', {
deleteCallback,
transaction,
})
throw new Error('Ouch!')
@ -169,22 +179,20 @@ describe('mergeImage', () => {
})
it('returns new image', async () => {
await expect(
mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }),
).resolves.toMatchObject({
await expect(mergeImage(post, 'HERO_IMAGE', imageInput)).resolves.toMatchObject({
url: expect.any(String),
alt: 'A description of the new image',
})
})
it('calls upload callback', async () => {
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
expect(uploadCallback).toHaveBeenCalled()
await mergeImage(post, 'HERO_IMAGE', imageInput)
expect(mockUpload).toHaveBeenCalled()
})
it('creates `:Image` node', async () => {
await expect(neode.all('Image')).resolves.toHaveLength(0)
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
await mergeImage(post, 'HERO_IMAGE', imageInput)
await expect(neode.all('Image')).resolves.toHaveLength(1)
})
@ -195,11 +203,9 @@ describe('mergeImage', () => {
const upload = await imageInput.upload
upload.filename = '/path/to/awkward?/ file-location/?foo- bar-avatar.jpg'
imageInput.upload = Promise.resolve(upload)
await expect(
mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }),
).resolves.toMatchObject({
await expect(mergeImage(post, 'HERO_IMAGE', imageInput)).resolves.toMatchObject({
url: expect.stringMatching(
new RegExp(`^http://your-objectstorage.com/bucket/${uuid}-foo-bar-avatar.jpg`),
new RegExp(`^http://your-objectstorage.com/bucket/original/${uuid}-foo-bar-avatar.jpg`),
),
})
})
@ -217,18 +223,18 @@ describe('mergeImage', () => {
const upload = await imageInput.upload
upload.filename = '/path/to/file-location/foo-bar-avatar.jpg'
imageInput.upload = Promise.resolve(upload)
await expect(
mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback }),
).resolves.toMatchObject({
await expect(mergeImage(post, 'HERO_IMAGE', imageInput)).resolves.toMatchObject({
url: expect.stringMatching(
new RegExp(`^http://s3-public-gateway.com/bucket/${uuid}-foo-bar-avatar.jpg`),
new RegExp(
`^http://s3-public-gateway.com/bucket/original/${uuid}-foo-bar-avatar.jpg`,
),
),
})
})
})
it('connects resource with image via given image type', async () => {
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
await mergeImage(post, 'HERO_IMAGE', imageInput)
const result = await neode.cypher(
`MATCH(p:Post {id: "p99"})-[:HERO_IMAGE]->(i:Image) RETURN i,p`,
{},
@ -240,7 +246,7 @@ describe('mergeImage', () => {
})
it('sets metadata', async () => {
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
await mergeImage(post, 'HERO_IMAGE', imageInput)
const image = await neode.first<typeof Image>('Image', {}, undefined)
await expect(image.toJson()).resolves.toMatchObject({
alt: 'A description of the new image',
@ -255,8 +261,6 @@ describe('mergeImage', () => {
try {
await session.writeTransaction(async (transaction) => {
const image = await mergeImage(post, 'HERO_IMAGE', imageInput, {
uploadCallback,
deleteCallback,
transaction,
})
return transaction.run(
@ -286,8 +290,6 @@ describe('mergeImage', () => {
try {
await session.writeTransaction(async (transaction) => {
const image = await mergeImage(post, 'HERO_IMAGE', imageInput, {
uploadCallback,
deleteCallback,
transaction,
})
return transaction.run('Ooops invalid cypher!', { image })
@ -313,18 +315,18 @@ describe('mergeImage', () => {
})
it('calls deleteCallback', async () => {
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
expect(deleteCallback).toHaveBeenCalled()
await mergeImage(post, 'HERO_IMAGE', imageInput)
expect(mockDeleteObjectCommand).toHaveBeenCalled()
})
it('calls uploadCallback', async () => {
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
expect(uploadCallback).toHaveBeenCalled()
it('calls Upload', async () => {
await mergeImage(post, 'HERO_IMAGE', imageInput)
expect(mockUpload).toHaveBeenCalled()
})
it('updates metadata of existing image node', async () => {
await expect(neode.all('Image')).resolves.toHaveLength(1)
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
await mergeImage(post, 'HERO_IMAGE', imageInput)
await expect(neode.all('Image')).resolves.toHaveLength(1)
const image = await neode.first<typeof Image>('Image', {}, undefined)
await expect(image.toJson()).resolves.toMatchObject({
@ -374,17 +376,17 @@ describe('mergeImage', () => {
})
it('does not call deleteCallback', async () => {
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
expect(deleteCallback).not.toHaveBeenCalled()
await mergeImage(post, 'HERO_IMAGE', imageInput)
expect(mockDeleteObjectCommand).not.toHaveBeenCalled()
})
it('does not call uploadCallback', async () => {
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
expect(uploadCallback).not.toHaveBeenCalled()
it('does not call Upload', async () => {
await mergeImage(post, 'HERO_IMAGE', imageInput)
expect(mockUpload).not.toHaveBeenCalled()
})
it('updates metadata', async () => {
await mergeImage(post, 'HERO_IMAGE', imageInput, { uploadCallback, deleteCallback })
await mergeImage(post, 'HERO_IMAGE', imageInput)
const images = await neode.all('Image')
expect(images).toHaveLength(1)
await expect(images.first().toJson()).resolves.toMatchObject({

View File

@ -1,34 +1,22 @@
import path from 'node:path'
import { S3Client, DeleteObjectCommand, ObjectCannedACL } from '@aws-sdk/client-s3'
import { Upload } from '@aws-sdk/lib-storage'
import { UserInputError } from 'apollo-server'
import { FileUpload } from 'graphql-upload'
import slug from 'slug'
import { v4 as uuid } from 'uuid'
import { S3Configured } from '@config/index'
import type { S3Config } from '@config/index'
import { s3Service } from '@src/uploads/s3Service'
import { wrapTransaction } from './wrapTransaction'
import type { Image, Images, FileDeleteCallback, FileUploadCallback } from './images'
import type { FileUpload } from 'graphql-upload'
import type { Image, Images } from './images'
export const images = (config: S3Configured) => {
// const widths = [34, 160, 320, 640, 1024]
const { AWS_BUCKET: Bucket, S3_PUBLIC_GATEWAY } = config
const { AWS_ENDPOINT, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY } = config
const s3 = new S3Client({
credentials: {
accessKeyId: AWS_ACCESS_KEY_ID,
secretAccessKey: AWS_SECRET_ACCESS_KEY,
},
endpoint: AWS_ENDPOINT,
forcePathStyle: true,
})
export const images = (config: S3Config) => {
const s3 = s3Service(config, 'original')
const deleteImage: Images['deleteImage'] = async (resource, relationshipType, opts = {}) => {
const { transaction, deleteCallback = s3Delete } = opts
const { transaction } = opts
if (!transaction) return wrapTransaction(deleteImage, [resource, relationshipType], opts)
const txResult = await transaction.run(
`
@ -45,7 +33,7 @@ export const images = (config: S3Configured) => {
// of an error (so throw an error). If we bulk delete an image, it
// could very well be that there is no image for the resource.
if (image) {
await deleteCallback(image.url)
await s3.deleteFile(image.url)
}
return image
}
@ -58,7 +46,7 @@ export const images = (config: S3Configured) => {
) => {
if (typeof imageInput === 'undefined') return
if (imageInput === null) return deleteImage(resource, relationshipType, opts)
const { transaction, uploadCallback, deleteCallback = s3Delete } = opts
const { transaction } = opts
if (!transaction)
return wrapTransaction(mergeImage, [resource, relationshipType, imageInput], opts)
@ -73,9 +61,9 @@ export const images = (config: S3Configured) => {
const { upload } = imageInput
if (!(existingImage || upload)) throw new UserInputError('Cannot find image for given resource')
if (existingImage && upload) {
await deleteCallback(existingImage.url)
await s3.deleteFile(existingImage.url)
}
const url = await uploadImageFile(upload, uploadCallback)
const url = await uploadImageFile(upload)
const { alt, sensitive, aspectRatio, type } = imageInput
const image = { alt, sensitive, aspectRatio, url, type }
txResult = await transaction.run(
@ -93,58 +81,17 @@ export const images = (config: S3Configured) => {
return mergedImage
}
const uploadImageFile = async (
uploadPromise: Promise<FileUpload> | undefined,
uploadCallback: FileUploadCallback | undefined = s3Upload,
) => {
const uploadImageFile = async (uploadPromise: Promise<FileUpload> | undefined) => {
if (!uploadPromise) return undefined
const upload = await uploadPromise
const { name, ext } = path.parse(upload.filename)
const uniqueFilename = `${uuid()}-${slug(name)}${ext}`
const Location = await uploadCallback({ ...upload, uniqueFilename })
if (!S3_PUBLIC_GATEWAY) {
return Location
}
const publicLocation = new URL(S3_PUBLIC_GATEWAY)
publicLocation.pathname = new URL(Location).pathname
return publicLocation.href
return await s3.uploadFile({ ...upload, uniqueFilename })
}
const s3Upload: FileUploadCallback = async ({ createReadStream, uniqueFilename, mimetype }) => {
const s3Location = `original/${uniqueFilename}`
const params = {
Bucket,
Key: s3Location,
ACL: ObjectCannedACL.public_read,
ContentType: mimetype,
Body: createReadStream(),
}
const command = new Upload({ client: s3, params })
const data = await command.done()
const { Location } = data
if (!Location) {
throw new Error('File upload did not return `Location`')
}
return Location
}
const s3Delete: FileDeleteCallback = async (url) => {
let { pathname } = new URL(url, 'http://example.org') // dummy domain to avoid invalid URL error
pathname = pathname.substring(1) // remove first character '/'
const prefix = `${Bucket}/`
if (pathname.startsWith(prefix)) {
pathname = pathname.slice(prefix.length)
}
const params = {
Bucket,
Key: pathname,
}
await s3.send(new DeleteObjectCommand(params))
}
const images: Images = {
const images = {
deleteImage,
mergeImage,
}
} satisfies Images
return images
}

View File

@ -1,11 +1,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { ApolloServer } from 'apollo-server-express'
import { createTestClient } from 'apollo-server-testing'
import CONFIG from '@config/index'
import databaseContext from '@context/database'
import Factory, { cleanDatabase } from '@db/factories'
import { createGroupMutation } from '@graphql/queries/createGroupMutation'
import { currentUser } from '@graphql/queries/currentUser'
@ -20,26 +16,24 @@ import {
authenticatedValidateInviteCode,
unauthenticatedValidateInviteCode,
} from '@graphql/queries/validateInviteCode'
import createServer, { getContext } from '@src/server'
import type { ApolloTestSetup } from '@root/test/helpers'
import { createApolloTestSetup, TEST_CONFIG } from '@root/test/helpers'
import type { Context } from '@src/context'
const database = databaseContext()
let server: ApolloServer
let authenticatedUser
let query, mutate
let authenticatedUser: Context['user']
const context = () => ({ authenticatedUser })
let mutate: ApolloTestSetup['mutate']
let query: ApolloTestSetup['query']
let database: ApolloTestSetup['database']
let server: ApolloTestSetup['server']
beforeAll(async () => {
await cleanDatabase()
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/require-await
const contextUser = async (_req) => authenticatedUser
const context = getContext({ user: contextUser, database })
server = createServer({ context }).server
const createTestClientResult = createTestClient(server)
query = createTestClientResult.query
mutate = createTestClientResult.mutate
const apolloSetup = createApolloTestSetup({ context })
mutate = apolloSetup.mutate
query = apolloSetup.query
database = apolloSetup.database
server = apolloSetup.server
})
afterAll(() => {
@ -479,7 +473,7 @@ describe('generatePersonalInviteCode', () => {
it('throws an error when the max amount of invite links was reached', async () => {
let lastCode
for (let i = 0; i < CONFIG.INVITE_CODES_PERSONAL_PER_USER; i++) {
for (let i = 0; i < TEST_CONFIG.INVITE_CODES_PERSONAL_PER_USER; i++) {
lastCode = await mutate({ mutation: generatePersonalInviteCode })
expect(lastCode).toMatchObject({
errors: undefined,
@ -740,7 +734,7 @@ describe('generateGroupInviteCode', () => {
it('throws an error when the max amount of invite links was reached', async () => {
let lastCode
for (let i = 0; i < CONFIG.INVITE_CODES_GROUP_PER_USER; i++) {
for (let i = 0; i < TEST_CONFIG.INVITE_CODES_GROUP_PER_USER; i++) {
lastCode = await mutate({
mutation: generateGroupInviteCode,
variables: { groupId: 'public-group' },

View File

@ -1,10 +1,8 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import CONFIG from '@config/index'
import registrationConstants from '@constants/registrationBranded'
// eslint-disable-next-line import/no-cycle
import { Context } from '@src/server'
import { Context } from '@src/context'
import Resolver from './helpers/Resolver'
@ -53,6 +51,9 @@ export const validateInviteCode = async (context: Context, inviteCode) => {
}
export const redeemInviteCode = async (context: Context, code, newUser = false) => {
if (!context.user) {
throw new Error('Missing authenticated user.')
}
const result = (
await context.database.query({
query: `
@ -159,7 +160,9 @@ export default {
})
).records[0].get('count')
if (parseInt(userInviteCodeAmount as string) >= CONFIG.INVITE_CODES_PERSONAL_PER_USER) {
if (
parseInt(userInviteCodeAmount as string) >= context.config.INVITE_CODES_PERSONAL_PER_USER
) {
throw new Error('You have reached the maximum of Invite Codes you can generate')
}
@ -198,7 +201,7 @@ export default {
})
).records[0].get('count')
if (parseInt(userInviteCodeAmount as string) >= CONFIG.INVITE_CODES_GROUP_PER_USER) {
if (parseInt(userInviteCodeAmount as string) >= context.config.INVITE_CODES_GROUP_PER_USER) {
throw new Error(
'You have reached the maximum of Invite Codes you can generate for this group',
)

View File

@ -1,10 +1,11 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-argument */
import { UserInputError } from 'apollo-server'
import type { Context } from '@src/context'
import Resolver from './helpers/Resolver'
import { queryLocations } from './users/location'
@ -23,7 +24,7 @@ export default {
'nameRU',
],
}),
distanceToMe: async (parent, _params, context, _resolveInfo) => {
distanceToMe: async (parent, _params, context: Context, _resolveInfo) => {
if (!parent.id) {
throw new Error('Can not identify selected Location!')
}
@ -53,9 +54,9 @@ export default {
},
},
Query: {
queryLocations: async (_object, args, _context, _resolveInfo) => {
queryLocations: async (_object, args, context: Context, _resolveInfo) => {
try {
return queryLocations(args)
return queryLocations(args, context)
} catch (e) {
throw new UserInputError(e.message)
}

View File

@ -5,11 +5,8 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { Readable } from 'node:stream'
import { ApolloServer } from 'apollo-server-express'
import { createTestClient } from 'apollo-server-testing'
import { Upload } from 'graphql-upload/public/index'
import databaseContext from '@context/database'
import pubsubContext from '@context/pubsub'
import Factory, { cleanDatabase } from '@db/factories'
import { CreateMessage } from '@graphql/queries/CreateMessage'
@ -17,29 +14,28 @@ import { createRoomMutation } from '@graphql/queries/createRoomMutation'
import { MarkMessagesAsSeen } from '@graphql/queries/MarkMessagesAsSeen'
import { Message } from '@graphql/queries/Message'
import { roomQuery } from '@graphql/queries/roomQuery'
import createServer, { getContext } from '@src/server'
import type { ApolloTestSetup } from '@root/test/helpers'
import { createApolloTestSetup } from '@root/test/helpers'
import type { Context } from '@src/context'
let query
let mutate
let authenticatedUser
let authenticatedUser: Context['user']
const context = () => ({ authenticatedUser, pubsub })
let mutate: ApolloTestSetup['mutate']
let query: ApolloTestSetup['query']
let database: ApolloTestSetup['database']
let server: ApolloTestSetup['server']
let chattingUser, otherChattingUser, notChattingUser
const database = databaseContext()
const pubsub = pubsubContext()
const pubsubSpy = jest.spyOn(pubsub, 'publish')
let server: ApolloServer
beforeAll(async () => {
await cleanDatabase()
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/require-await
const contextUser = async (_req) => authenticatedUser
const context = getContext({ user: contextUser, database, pubsub })
server = createServer({ context }).server
query = createTestClient(server).query
mutate = createTestClient(server).mutate
const apolloSetup = createApolloTestSetup({ context })
mutate = apolloSetup.mutate
query = apolloSetup.query
database = apolloSetup.database
server = apolloSetup.server
})
beforeEach(async () => {
@ -79,6 +75,10 @@ describe('Message', () => {
})
describe('unauthenticated', () => {
beforeAll(() => {
authenticatedUser = null
})
it('throws authorization error', async () => {
await expect(
mutate({
@ -128,7 +128,7 @@ describe('Message', () => {
userId: 'other-chatting-user',
},
})
roomId = room.data.CreateRoom.id
roomId = (room.data as any).CreateRoom.id // eslint-disable-line @typescript-eslint/no-explicit-any
})
describe('user chats in room', () => {
@ -180,7 +180,7 @@ describe('Message', () => {
lastMessageAt: expect.any(String),
unreadCount: 0,
lastMessage: expect.objectContaining({
_id: result.data.Room[0].lastMessage.id,
_id: result.data?.Room[0].lastMessage.id,
id: expect.any(String),
content: 'Some nice message to other chatting user',
senderId: 'chatting-user',
@ -410,7 +410,7 @@ describe('Message', () => {
userId: 'other-chatting-user',
},
})
roomId = room.data.CreateRoom.id
roomId = (room.data as any).CreateRoom.id // eslint-disable-line @typescript-eslint/no-explicit-any
await mutate({
mutation: CreateMessage,
@ -434,7 +434,7 @@ describe('Message', () => {
Message: [
{
id: expect.any(String),
_id: result.data.Message[0].id,
_id: result.data?.Message[0].id,
indexId: 0,
content: 'Some nice message to other chatting user',
senderId: 'chatting-user',
@ -642,7 +642,7 @@ describe('Message', () => {
userId: 'other-chatting-user',
},
})
roomId = room.data.CreateRoom.id
roomId = (room.data as any).CreateRoom.id // eslint-disable-line @typescript-eslint/no-explicit-any
await mutate({
mutation: CreateMessage,
variables: {
@ -673,7 +673,7 @@ describe('Message', () => {
roomId,
},
})
msgs.data.Message.forEach((m) => messageIds.push(m.id))
msgs.data?.Message.forEach((m) => messageIds.push(m.id))
})
it('returns true', async () => {

View File

@ -7,7 +7,7 @@
import { withFilter } from 'graphql-subscriptions'
import { neo4jgraphql } from 'neo4j-graphql-js'
import CONFIG, { isS3configured } from '@config/index'
import CONFIG from '@config/index'
import { CHAT_MESSAGE_ADDED } from '@constants/subscriptions'
import { attachments } from './attachments/attachments'
@ -125,19 +125,17 @@ export default {
const atns: File[] = []
if (isS3configured(CONFIG)) {
for await (const file of files) {
const atn = await attachments(CONFIG).add(
message,
'ATTACHMENT',
file,
{},
{
transaction,
},
)
atns.push(atn)
}
for await (const file of files) {
const atn = await attachments(CONFIG).add(
message,
'ATTACHMENT',
file,
{},
{
transaction,
},
)
atns.push(atn)
}
return { ...message, files: atns }

View File

@ -3,42 +3,40 @@
/* 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'
import Factory, { cleanDatabase } from '@db/factories'
import { getDriver } from '@db/neo4j'
import { markAllAsReadMutation } from '@graphql/queries/markAllAsReadMutation'
import { markAsReadMutation } from '@graphql/queries/markAsReadMutation'
import { notificationQuery } from '@graphql/queries/notificationQuery'
import createServer from '@src/server'
import type { ApolloTestSetup } from '@root/test/helpers'
import { createApolloTestSetup } from '@root/test/helpers'
import type { Context } from '@src/context'
const driver = getDriver()
let authenticatedUser
let user
let author
let variables
let query
let mutate
let authenticatedUser: Context['user']
const context = () => ({ authenticatedUser })
let query: ApolloTestSetup['query']
let mutate: ApolloTestSetup['mutate']
let database: ApolloTestSetup['database']
let server: ApolloTestSetup['server']
beforeAll(async () => {
await cleanDatabase()
const { server } = createServer({
context: () => {
return {
driver,
user: authenticatedUser,
}
},
})
query = createTestClient(server).query
mutate = createTestClient(server).mutate
const apolloSetup = createApolloTestSetup({ context })
query = apolloSetup.query
mutate = apolloSetup.mutate
database = apolloSetup.database
server = apolloSetup.server
})
afterAll(async () => {
await cleanDatabase()
await driver.close()
void server.stop()
void database.driver.close()
database.neode.close()
})
beforeEach(async () => {
@ -157,7 +155,7 @@ describe('given some notifications', () => {
describe('unauthenticated', () => {
it('throws authorization error', async () => {
const { errors } = await query({ query: notificationQuery() })
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
@ -241,7 +239,7 @@ describe('given some notifications', () => {
variables: { ...variables, read: false },
})
await expect(response).toMatchObject(expected)
await expect(response.data.notifications).toHaveLength(2) // double-check
await expect(response.data?.notifications).toHaveLength(2) // double-check
})
describe('if a resource gets deleted', () => {
@ -288,7 +286,7 @@ describe('given some notifications', () => {
mutation: markAsReadMutation(),
variables: { ...variables, id: 'p1' },
})
expect(result.errors[0]).toHaveProperty('message', 'Not Authorized!')
expect(result.errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
@ -307,7 +305,7 @@ describe('given some notifications', () => {
it('returns null', async () => {
const response = await mutate({ mutation: markAsReadMutation(), variables })
expect(response.data.markAsRead).toEqual(null)
expect(response.data?.markAsRead).toEqual(null)
expect(response.errors).toBeUndefined()
})
})
@ -344,7 +342,7 @@ describe('given some notifications', () => {
})
it('returns null', async () => {
const response = await mutate({ mutation: markAsReadMutation(), variables })
expect(response.data.markAsRead).toEqual(null)
expect(response.data?.markAsRead).toEqual(null)
expect(response.errors).toBeUndefined()
})
})
@ -382,7 +380,7 @@ describe('given some notifications', () => {
const result = await mutate({
mutation: markAllAsReadMutation(),
})
expect(result.errors[0]).toHaveProperty('message', 'Not Authorized!')
expect(result.errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
@ -400,7 +398,7 @@ describe('given some notifications', () => {
it('returns all as read', async () => {
const response = await mutate({ mutation: markAllAsReadMutation(), variables })
expect(response.data.markAllAsRead).toEqual(
expect(response.data?.markAllAsRead).toEqual(
expect.arrayContaining([
{
createdAt: '2019-08-30T19:33:48.651Z',

View File

@ -1,23 +1,23 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { ApolloServer } from 'apollo-server-express'
import { createTestClient } from 'apollo-server-testing'
import gql from 'graphql-tag'
import CONFIG from '@config/index'
import databaseContext from '@context/database'
import Factory, { cleanDatabase } from '@db/factories'
import { createPostMutation } from '@graphql/queries/createPostMutation'
import createServer, { getContext } from '@src/server'
import type { ApolloTestSetup } from '@root/test/helpers'
import { createApolloTestSetup } from '@root/test/helpers'
import type { Context } from '@src/context'
CONFIG.CATEGORIES_ACTIVE = false
let query
let mutate
let authenticatedUser
let user
let otherUser
let authenticatedUser: Context['user']
const config = { CATEGORIES_ACTIVE: true }
const context = () => ({ authenticatedUser, config })
let mutate: ApolloTestSetup['mutate']
let query: ApolloTestSetup['query']
let database: ApolloTestSetup['database']
let server: ApolloTestSetup['server']
const createCommentMutation = gql`
mutation ($id: ID, $postId: ID!, $content: String!) {
@ -38,20 +38,13 @@ const postQuery = gql`
}
`
const database = databaseContext()
let server: ApolloServer
beforeAll(async () => {
await cleanDatabase()
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/require-await
const contextUser = async (_req) => authenticatedUser
const context = getContext({ user: contextUser, database })
server = createServer({ context }).server
query = createTestClient(server).query
mutate = createTestClient(server).mutate
const apolloSetup = createApolloTestSetup({ context })
mutate = apolloSetup.mutate
query = apolloSetup.query
database = apolloSetup.database
server = apolloSetup.server
})
afterAll(async () => {

View File

@ -1,51 +1,45 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call */
import { createTestClient } from 'apollo-server-testing'
import gql from 'graphql-tag'
import registrationConstants from '@constants/registrationBranded'
import Factory, { cleanDatabase } from '@db/factories'
import { getNeode, getDriver } from '@db/neo4j'
import createServer from '@src/server'
import type { ApolloTestSetup } from '@root/test/helpers'
import { createApolloTestSetup } from '@root/test/helpers'
import createPasswordReset from './helpers/createPasswordReset'
const neode = getNeode()
const driver = getDriver()
let mutate
let authenticatedUser
let variables
let mutate: ApolloTestSetup['mutate']
let database: ApolloTestSetup['database']
let server: ApolloTestSetup['server']
const getAllPasswordResets = async () => {
const passwordResetQuery = await neode.cypher(
const passwordResetQuery = await database.neode.cypher(
'MATCH (passwordReset:PasswordReset) RETURN passwordReset',
{},
)
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
const resets = passwordResetQuery.records.map((record) => record.get('passwordReset'))
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return resets
}
beforeAll(async () => {
await cleanDatabase()
const { server } = createServer({
context: () => {
return {
driver,
neode,
user: authenticatedUser,
}
},
})
mutate = createTestClient(server).mutate
const apolloSetup = createApolloTestSetup()
mutate = apolloSetup.mutate
database = apolloSetup.database
server = apolloSetup.server
})
afterAll(async () => {
await cleanDatabase()
await driver.close()
void server.stop()
void database.driver.close()
database.neode.close()
})
beforeEach(() => {
@ -129,7 +123,7 @@ describe('resetPassword', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const setup = async (options: any = {}) => {
const { email = 'user@example.org', issuedAt = new Date(), nonce = '12345' } = options
await createPasswordReset({ driver, email, issuedAt, nonce })
await createPasswordReset({ driver: database.driver, email, issuedAt, nonce })
}
const mutation = gql`

View File

@ -8,13 +8,15 @@ import bcrypt from 'bcryptjs'
import { v4 as uuid } from 'uuid'
import registrationConstants from '@constants/registrationBranded'
import type { Context } from '@src/context'
import createPasswordReset from './helpers/createPasswordReset'
import normalizeEmail from './helpers/normalizeEmail'
export default {
Mutation: {
requestPasswordReset: async (_parent, { email }, { driver }) => {
requestPasswordReset: async (_parent, { email }, context: Context) => {
const { driver } = context
email = normalizeEmail(email)
// TODO: why this is generated differntly from 'backend/src/schema/resolvers/helpers/generateNonce.js'?
const nonce = uuid().substring(0, registrationConstants.NONCE_LENGTH)

View File

@ -2,12 +2,8 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { ApolloServer } from 'apollo-server-express'
import { createTestClient } from 'apollo-server-testing'
import gql from 'graphql-tag'
import CONFIG from '@config/index'
import databaseContext from '@context/database'
import Factory, { cleanDatabase } from '@db/factories'
import Image from '@db/models/Image'
import { createGroupMutation } from '@graphql/queries/createGroupMutation'
@ -15,30 +11,32 @@ import { createPostMutation } from '@graphql/queries/createPostMutation'
import { Post } from '@graphql/queries/Post'
import { pushPost } from '@graphql/queries/pushPost'
import { unpushPost } from '@graphql/queries/unpushPost'
import createServer, { getContext } from '@src/server'
CONFIG.CATEGORIES_ACTIVE = true
import type { ApolloTestSetup } from '@root/test/helpers'
import { createApolloTestSetup } from '@root/test/helpers'
import type { Context } from '@src/context'
let user
const database = databaseContext()
let authenticatedUser: Context['user']
const context = () => ({ authenticatedUser, config })
let mutate: ApolloTestSetup['mutate']
let query: ApolloTestSetup['query']
let database: ApolloTestSetup['database']
let server: ApolloTestSetup['server']
let server: ApolloServer
let authenticatedUser
let query, mutate
const defaultConfig = {
CATEGORIES_ACTIVE: true,
// MAPBOX_TOKEN: CONFIG.MAPBOX_TOKEN,
}
let config: Partial<Context['config']>
beforeAll(async () => {
await cleanDatabase()
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/require-await
const contextUser = async (_req) => authenticatedUser
const context = getContext({ user: contextUser, database })
server = createServer({ context }).server
const createTestClientResult = createTestClient(server)
mutate = createTestClientResult.mutate
query = createTestClientResult.query
const apolloSetup = createApolloTestSetup({ context })
mutate = apolloSetup.mutate
query = apolloSetup.query
database = apolloSetup.database
server = apolloSetup.server
})
afterAll(() => {
@ -51,6 +49,7 @@ const categoryIds = ['cat9', 'cat4', 'cat15']
let variables
beforeEach(async () => {
config = { ...defaultConfig }
variables = {}
user = await Factory.build(
'user',
@ -271,7 +270,7 @@ describe('CreatePost', () => {
describe('unauthenticated', () => {
it('throws authorization error', async () => {
const { errors } = await mutate({ mutation: createPostMutation(), variables })
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
@ -708,7 +707,7 @@ describe('UpdatePost', () => {
categoryIds,
},
})
newlyCreatedPost = data.CreatePost
newlyCreatedPost = (data as any).CreatePost // eslint-disable-line @typescript-eslint/no-explicit-any
variables = {
id: newlyCreatedPost.id,
title: 'New title',
@ -733,7 +732,7 @@ describe('UpdatePost', () => {
it('throws authorization error', async () => {
const { errors } = await mutate({ mutation: updatePostMutation, variables })
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
@ -771,7 +770,7 @@ describe('UpdatePost', () => {
it('updates the updatedAt attribute', async () => {
const {
data: { UpdatePost },
} = await mutate({ mutation: updatePostMutation, variables })
} = (await mutate({ mutation: updatePostMutation, variables })) as any // eslint-disable-line @typescript-eslint/no-explicit-any
expect(UpdatePost.updatedAt).toBeTruthy()
expect(Date.parse(UpdatePost.updatedAt)).toEqual(expect.any(Number))
expect(newlyCreatedPost.updatedAt).not.toEqual(UpdatePost.updatedAt)
@ -1377,7 +1376,8 @@ describe('pin posts', () => {
describe('MAX_PINNED_POSTS is 0', () => {
beforeEach(async () => {
CONFIG.MAX_PINNED_POSTS = 0
config = { ...defaultConfig, MAX_PINNED_POSTS: 0 }
await Factory.build(
'post',
{
@ -1400,7 +1400,7 @@ describe('pin posts', () => {
describe('MAX_PINNED_POSTS is 1', () => {
beforeEach(() => {
CONFIG.MAX_PINNED_POSTS = 1
config = { ...defaultConfig, MAX_PINNED_POSTS: 1 }
})
describe('are allowed to pin posts', () => {
@ -1752,7 +1752,8 @@ describe('pin posts', () => {
const postsPinnedCountsQuery = `query { PostsPinnedCounts { maxPinnedPosts, currentlyPinnedPosts } }`
beforeEach(async () => {
CONFIG.MAX_PINNED_POSTS = 3
config = { ...defaultConfig, MAX_PINNED_POSTS: 3 }
await Factory.build(
'post',
{
@ -2127,7 +2128,7 @@ describe('DeletePost', () => {
describe('unauthenticated', () => {
it('throws authorization error', async () => {
const { errors } = await mutate({ mutation: deletePostMutation, variables })
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
@ -2138,7 +2139,7 @@ describe('DeletePost', () => {
it('throws authorization error', async () => {
const { errors } = await mutate({ mutation: deletePostMutation, variables })
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
@ -2280,7 +2281,7 @@ describe('emotions', () => {
variables,
})
expect(addPostEmotions.errors[0]).toHaveProperty('message', 'Not Authorized!')
expect(addPostEmotions.errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
@ -2401,7 +2402,7 @@ describe('emotions', () => {
mutation: removePostEmotionsMutation,
variables: removePostEmotionsVariables,
})
expect(removePostEmotions.errors[0]).toHaveProperty('message', 'Not Authorized!')
expect(removePostEmotions.errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})

View File

@ -9,8 +9,7 @@ import { isEmpty } from 'lodash'
import { neo4jgraphql } from 'neo4j-graphql-js'
import { v4 as uuid } from 'uuid'
import CONFIG from '@config/index'
import { Context } from '@src/server'
import { Context } from '@src/context'
import { validateEventParams } from './helpers/events'
import { filterForMutedUsers } from './helpers/filterForMutedUsers'
@ -41,7 +40,7 @@ const filterEventDates = (params) => {
export default {
Query: {
Post: async (object, params, context, resolveInfo) => {
Post: async (object, params, context: Context, resolveInfo) => {
params = await filterPostsOfMyGroups(params, context)
params = await filterInvisiblePosts(params, context)
params = await filterForMutedUsers(params, context)
@ -77,10 +76,13 @@ export default {
session.close()
}
},
PostsEmotionsByCurrentUser: async (_object, params, context, _resolveInfo) => {
PostsEmotionsByCurrentUser: async (_object, params, context: Context, _resolveInfo) => {
const { postId } = params
const session = context.driver.session()
const readTxResultPromise = session.readTransaction(async (transaction) => {
if (!context.user) {
throw new Error('Missing authenticated user.')
}
const emotionsTransactionResponse = await transaction.run(
`
MATCH (user:User {id: $userId})-[emoted:EMOTED]->(post:Post {id: $postId})
@ -94,23 +96,29 @@ export default {
const [emotions] = await readTxResultPromise
return emotions
} finally {
session.close()
await session.close()
}
},
PostsPinnedCounts: async (_object, params, context: Context, _resolveInfo) => {
const { config } = context
const [postsPinnedCount] = (
await context.database.query({
query: 'MATCH (p:Post { pinned: true }) RETURN COUNT (p) AS count',
})
).records.map((r) => Number(r.get('count').toString()))
return {
maxPinnedPosts: CONFIG.MAX_PINNED_POSTS,
maxPinnedPosts: config.MAX_PINNED_POSTS,
currentlyPinnedPosts: postsPinnedCount,
}
},
},
Mutation: {
CreatePost: async (_parent, params, context, _resolveInfo) => {
CreatePost: async (_parent, params, context: Context, _resolveInfo) => {
const { user } = context
if (!user) {
throw new Error('Missing authenticated user.')
}
const { config } = context
const { categoryIds, groupId } = params
const { image: imageInput } = params
@ -146,7 +154,7 @@ export default {
)`
}
const categoriesCypher =
CONFIG.CATEGORIES_ACTIVE && categoryIds
config.CATEGORIES_ACTIVE && categoryIds
? `WITH post
UNWIND $categoryIds AS categoryId
MATCH (category:Category {id: categoryId})
@ -173,18 +181,18 @@ export default {
${groupCypher}
RETURN post {.*, postType: [l IN labels(post) WHERE NOT l = 'Post'] }
`,
{ userId: context.user.id, categoryIds, groupId, params },
{ userId: user.id, categoryIds, groupId, params },
)
const [post] = createPostTransactionResponse.records.map((record) => record.get('post'))
if (imageInput) {
await images.mergeImage(post, 'HERO_IMAGE', imageInput, { transaction })
await images(context.config).mergeImage(post, 'HERO_IMAGE', imageInput, { transaction })
}
return post
})
try {
const post = await writeTxResultPromise
if (locationName) {
await createOrUpdateLocations('Post', post.id, locationName, session)
await createOrUpdateLocations('Post', post.id, locationName, session, context)
}
return post
} catch (e) {
@ -192,10 +200,11 @@ export default {
throw new UserInputError('Post with this slug already exists!')
throw new Error(e)
} finally {
session.close()
await session.close()
}
},
UpdatePost: async (_parent, params, context, _resolveInfo) => {
UpdatePost: async (_parent, params, context: Context, _resolveInfo) => {
const { config } = context
const { categoryIds } = params
const { image: imageInput } = params
@ -211,7 +220,7 @@ export default {
WITH post
`
if (CONFIG.CATEGORIES_ACTIVE && categoryIds && categoryIds.length) {
if (config.CATEGORIES_ACTIVE && categoryIds && categoryIds.length) {
const cypherDeletePreviousRelations = `
MATCH (post:Post { id: $params.id })-[previousRelations:CATEGORIZED]->(category:Category)
DELETE previousRelations
@ -248,20 +257,20 @@ export default {
updatePostVariables,
)
const [post] = updatePostTransactionResponse.records.map((record) => record.get('post'))
await images.mergeImage(post, 'HERO_IMAGE', imageInput, { transaction })
await images(context.config).mergeImage(post, 'HERO_IMAGE', imageInput, { transaction })
return post
})
const post = await writeTxResultPromise
if (locationName) {
await createOrUpdateLocations('Post', post.id, locationName, session)
await createOrUpdateLocations('Post', post.id, locationName, session, context)
}
return post
} finally {
session.close()
await session.close()
}
},
DeletePost: async (_object, args, context, _resolveInfo) => {
DeletePost: async (_object, args, context: Context, _resolveInfo) => {
const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
const deletePostTransactionResponse = await transaction.run(
@ -278,17 +287,17 @@ export default {
{ postId: args.id },
)
const [post] = deletePostTransactionResponse.records.map((record) => record.get('post'))
await images.deleteImage(post, 'HERO_IMAGE', { transaction })
await images(context.config).deleteImage(post, 'HERO_IMAGE', { transaction })
return post
})
try {
const post = await writeTxResultPromise
return post
} finally {
session.close()
await session.close()
}
},
AddPostEmotions: async (_object, params, context, _resolveInfo) => {
AddPostEmotions: async (_object, params, context: Context, _resolveInfo) => {
const { to, data } = params
const { user } = context
const session = context.driver.session()
@ -312,7 +321,7 @@ export default {
const [emoted] = await writeTxResultPromise
return emoted
} finally {
session.close()
await session.close()
}
},
RemovePostEmotions: async (_object, params, context, _resolveInfo) => {
@ -344,7 +353,11 @@ export default {
}
},
pinPost: async (_parent, params, context: Context, _resolveInfo) => {
if (CONFIG.MAX_PINNED_POSTS === 0) throw new Error('Pinned posts are not allowed!')
if (!context.user) {
throw new Error('Missing authenticated user.')
}
const { config } = context
if (config.MAX_PINNED_POSTS === 0) throw new Error('Pinned posts are not allowed!')
let pinnedPostWithNestedAttributes
const { driver, user } = context
const session = driver.session()
@ -358,7 +371,7 @@ export default {
SET post.pinned = true
RETURN post, pinned.createdAt as pinnedAt`
if (CONFIG.MAX_PINNED_POSTS === 1) {
if (config.MAX_PINNED_POSTS === 1) {
let writeTxResultPromise = session.writeTransaction(async (transaction) => {
const deletePreviousRelationsResponse = await transaction.run(
`
@ -403,7 +416,7 @@ export default {
query: `MATCH (:User)-[:PINNED]->(post:Post { pinned: true }) RETURN COUNT(post) AS count`,
})
).records.map((r) => Number(r.get('count').toString()))
if (currentPinnedPostCount >= CONFIG.MAX_PINNED_POSTS) {
if (currentPinnedPostCount >= config.MAX_PINNED_POSTS) {
throw new Error('Max number of pinned posts is reached!')
}
const [pinPostResult] = (

View File

@ -2,11 +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 { ApolloServer } from 'apollo-server-express'
import { createTestClient } from 'apollo-server-testing'
import CONFIG from '@config/index'
import databaseContext from '@context/database'
import Factory, { cleanDatabase } from '@db/factories'
import { changeGroupMemberRoleMutation } from '@graphql/queries/changeGroupMemberRoleMutation'
import { createCommentMutation } from '@graphql/queries/createCommentMutation'
@ -18,9 +13,9 @@ import { postQuery } from '@graphql/queries/postQuery'
import { profilePagePosts } from '@graphql/queries/profilePagePosts'
import { searchPosts } from '@graphql/queries/searchPosts'
import { signupVerificationMutation } from '@graphql/queries/signupVerificationMutation'
import createServer, { getContext } from '@src/server'
CONFIG.CATEGORIES_ACTIVE = false
import type { ApolloTestSetup } from '@root/test/helpers'
import { createApolloTestSetup } from '@root/test/helpers'
import type { Context } from '@src/context'
jest.mock('@constants/groups', () => {
return {
@ -29,30 +24,28 @@ jest.mock('@constants/groups', () => {
}
})
let query
let mutate
let anyUser
let allGroupsUser
let pendingUser
let publicUser
let closedUser
let hiddenUser
let authenticatedUser
let newUser
let authenticatedUser: Context['user']
const config = { CATEGORIES_ACTIVE: false }
const context = () => ({ authenticatedUser, config })
let mutate: ApolloTestSetup['mutate']
let query: ApolloTestSetup['query']
let database: ApolloTestSetup['database']
let server: ApolloTestSetup['server']
const database = databaseContext()
let server: ApolloServer
beforeAll(async () => {
await cleanDatabase()
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
const contextUser = async (_req) => authenticatedUser
const context = getContext({ user: contextUser, database })
server = createServer({ context }).server
query = createTestClient(server).query
mutate = createTestClient(server).mutate
const apolloSetup = createApolloTestSetup({ context })
mutate = apolloSetup.mutate
query = apolloSetup.query
database = apolloSetup.database
server = apolloSetup.server
})
afterAll(async () => {
@ -545,7 +538,7 @@ describe('Posts in Groups', () => {
describe('visibility of posts', () => {
describe('query post by ID', () => {
describe('without authentication', () => {
beforeAll(async () => {
beforeEach(() => {
authenticatedUser = null
})
@ -608,7 +601,7 @@ describe('Posts in Groups', () => {
termsAndConditionsAgreedVersion: '0.0.1',
},
})
newUser = result.data.SignupVerification
newUser = result.data?.SignupVerification
authenticatedUser = newUser
})
@ -802,13 +795,13 @@ describe('Posts in Groups', () => {
describe('filter posts', () => {
describe('without authentication', () => {
beforeAll(async () => {
beforeEach(() => {
authenticatedUser = null
})
it('shows the post of the public group and the post without group', async () => {
const result = await query({ query: filterPosts(), variables: {} })
expect(result.data.Post).toHaveLength(2)
expect(result.data?.Post).toHaveLength(2)
expect(result).toMatchObject({
data: {
Post: expect.arrayContaining([
@ -838,7 +831,7 @@ describe('Posts in Groups', () => {
it('shows the post of the public group and the post without group', async () => {
const result = await query({ query: filterPosts(), variables: {} })
expect(result.data.Post).toHaveLength(2)
expect(result.data?.Post).toHaveLength(2)
expect(result).toMatchObject({
data: {
Post: expect.arrayContaining([
@ -868,7 +861,7 @@ describe('Posts in Groups', () => {
it('shows the post of the public group and the post without group', async () => {
const result = await query({ query: filterPosts(), variables: {} })
expect(result.data.Post).toHaveLength(2)
expect(result.data?.Post).toHaveLength(2)
expect(result).toMatchObject({
data: {
Post: expect.arrayContaining([
@ -898,7 +891,7 @@ describe('Posts in Groups', () => {
it('shows the post of the public group and the post without group', async () => {
const result = await query({ query: filterPosts(), variables: {} })
expect(result.data.Post).toHaveLength(2)
expect(result.data?.Post).toHaveLength(2)
expect(result).toMatchObject({
data: {
Post: expect.arrayContaining([
@ -928,7 +921,7 @@ describe('Posts in Groups', () => {
it('shows all posts', async () => {
const result = await query({ query: filterPosts(), variables: {} })
expect(result.data.Post).toHaveLength(4)
expect(result.data?.Post).toHaveLength(4)
expect(result).toMatchObject({
data: {
Post: expect.arrayContaining([
@ -966,13 +959,13 @@ describe('Posts in Groups', () => {
describe('profile page posts', () => {
describe('without authentication', () => {
beforeAll(async () => {
beforeEach(() => {
authenticatedUser = null
})
it('shows the post of the public group and the post without group', async () => {
const result = await query({ query: profilePagePosts(), variables: {} })
expect(result.data.profilePagePosts).toHaveLength(2)
expect(result.data?.profilePagePosts).toHaveLength(2)
expect(result).toMatchObject({
data: {
profilePagePosts: expect.arrayContaining([
@ -1000,7 +993,7 @@ describe('Posts in Groups', () => {
it('shows the post of the public group and the post without group', async () => {
const result = await query({ query: profilePagePosts(), variables: {} })
expect(result.data.profilePagePosts).toHaveLength(2)
expect(result.data?.profilePagePosts).toHaveLength(2)
expect(result).toMatchObject({
data: {
profilePagePosts: expect.arrayContaining([
@ -1028,7 +1021,7 @@ describe('Posts in Groups', () => {
it('shows the post of the public group and the post without group', async () => {
const result = await query({ query: profilePagePosts(), variables: {} })
expect(result.data.profilePagePosts).toHaveLength(2)
expect(result.data?.profilePagePosts).toHaveLength(2)
expect(result).toMatchObject({
data: {
profilePagePosts: expect.arrayContaining([
@ -1056,7 +1049,7 @@ describe('Posts in Groups', () => {
it('shows the post of the public group and the post without group', async () => {
const result = await query({ query: profilePagePosts(), variables: {} })
expect(result.data.profilePagePosts).toHaveLength(2)
expect(result.data?.profilePagePosts).toHaveLength(2)
expect(result).toMatchObject({
data: {
profilePagePosts: expect.arrayContaining([
@ -1084,7 +1077,7 @@ describe('Posts in Groups', () => {
it('shows all posts', async () => {
const result = await query({ query: profilePagePosts(), variables: {} })
expect(result.data.profilePagePosts).toHaveLength(4)
expect(result.data?.profilePagePosts).toHaveLength(4)
expect(result).toMatchObject({
data: {
profilePagePosts: expect.arrayContaining([
@ -1118,7 +1111,7 @@ describe('Posts in Groups', () => {
describe('searchPosts', () => {
describe('without authentication', () => {
beforeAll(async () => {
beforeEach(() => {
authenticatedUser = null
})
@ -1131,7 +1124,7 @@ describe('Posts in Groups', () => {
firstPosts: 25,
},
})
expect(result.data.searchPosts.posts).toHaveLength(0)
expect(result.data?.searchPosts.posts).toHaveLength(0)
expect(result).toMatchObject({
data: {
searchPosts: {
@ -1157,7 +1150,7 @@ describe('Posts in Groups', () => {
firstPosts: 25,
},
})
expect(result.data.searchPosts.posts).toHaveLength(2)
expect(result.data?.searchPosts.posts).toHaveLength(2)
expect(result).toMatchObject({
data: {
searchPosts: {
@ -1194,7 +1187,7 @@ describe('Posts in Groups', () => {
firstPosts: 25,
},
})
expect(result.data.searchPosts.posts).toHaveLength(2)
expect(result.data?.searchPosts.posts).toHaveLength(2)
expect(result).toMatchObject({
data: {
searchPosts: {
@ -1231,7 +1224,7 @@ describe('Posts in Groups', () => {
firstPosts: 25,
},
})
expect(result.data.searchPosts.posts).toHaveLength(2)
expect(result.data?.searchPosts.posts).toHaveLength(2)
expect(result).toMatchObject({
data: {
searchPosts: {
@ -1268,7 +1261,7 @@ describe('Posts in Groups', () => {
firstPosts: 25,
},
})
expect(result.data.searchPosts.posts).toHaveLength(4)
expect(result.data?.searchPosts.posts).toHaveLength(4)
expect(result).toMatchObject({
data: {
searchPosts: {
@ -1321,7 +1314,7 @@ describe('Posts in Groups', () => {
it('shows the posts of the closed group', async () => {
const result = await query({ query: filterPosts(), variables: {} })
expect(result.data.Post).toHaveLength(3)
expect(result.data?.Post).toHaveLength(3)
expect(result).toMatchObject({
data: {
Post: expect.arrayContaining([
@ -1366,7 +1359,7 @@ describe('Posts in Groups', () => {
it('shows all the posts', async () => {
const result = await query({ query: filterPosts(), variables: {} })
expect(result.data.Post).toHaveLength(4)
expect(result.data?.Post).toHaveLength(4)
expect(result).toMatchObject({
data: {
Post: expect.arrayContaining([
@ -1419,7 +1412,7 @@ describe('Posts in Groups', () => {
it('does not show the posts of the closed group anymore', async () => {
const result = await query({ query: filterPosts(), variables: {} })
expect(result.data.Post).toHaveLength(3)
expect(result.data?.Post).toHaveLength(3)
expect(result).toMatchObject({
data: {
Post: expect.arrayContaining([
@ -1464,7 +1457,7 @@ describe('Posts in Groups', () => {
it('shows only the public posts', async () => {
const result = await query({ query: filterPosts(), variables: {} })
expect(result.data.Post).toHaveLength(2)
expect(result.data?.Post).toHaveLength(2)
expect(result).toMatchObject({
data: {
Post: expect.arrayContaining([
@ -1503,7 +1496,7 @@ describe('Posts in Groups', () => {
it('still shows the posts of the public group', async () => {
const result = await query({ query: filterPosts(), variables: {} })
expect(result.data.Post).toHaveLength(4)
expect(result.data?.Post).toHaveLength(4)
expect(result).toMatchObject({
data: {
Post: expect.arrayContaining([
@ -1552,7 +1545,7 @@ describe('Posts in Groups', () => {
it('stil shows the posts of the closed group', async () => {
const result = await query({ query: filterPosts(), variables: {} })
expect(result.data.Post).toHaveLength(4)
expect(result.data?.Post).toHaveLength(4)
expect(result).toMatchObject({
data: {
Post: expect.arrayContaining([
@ -1601,7 +1594,7 @@ describe('Posts in Groups', () => {
it('still shows the post of the hidden group', async () => {
const result = await query({ query: filterPosts(), variables: {} })
expect(result.data.Post).toHaveLength(4)
expect(result.data?.Post).toHaveLength(4)
expect(result).toMatchObject({
data: {
Post: expect.arrayContaining([
@ -1654,7 +1647,7 @@ describe('Posts in Groups', () => {
it('shows the posts of the closed group', async () => {
const result = await query({ query: filterPosts(), variables: {} })
expect(result.data.Post).toHaveLength(4)
expect(result.data?.Post).toHaveLength(4)
expect(result).toMatchObject({
data: {
Post: expect.arrayContaining([
@ -1705,7 +1698,7 @@ describe('Posts in Groups', () => {
it('shows all posts', async () => {
const result = await query({ query: filterPosts(), variables: {} })
expect(result.data.Post).toHaveLength(4)
expect(result.data?.Post).toHaveLength(4)
expect(result).toMatchObject({
data: {
Post: expect.arrayContaining([
@ -1752,7 +1745,7 @@ describe('Posts in Groups', () => {
query: filterPosts(),
variables: { filter: { postsInMyGroups: true } },
})
expect(result.data.Post).toHaveLength(0)
expect(result.data?.Post).toHaveLength(0)
expect(result).toMatchObject({
data: {
Post: [],
@ -1773,7 +1766,7 @@ describe('Posts in Groups', () => {
query: filterPosts(),
variables: { filter: { postsInMyGroups: true } },
})
expect(result.data.Post).toHaveLength(2)
expect(result.data?.Post).toHaveLength(2)
expect(result).toMatchObject({
data: {
Post: expect.arrayContaining([

View File

@ -1,36 +1,30 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { ApolloServer } from 'apollo-server-express'
import { createTestClient } from 'apollo-server-testing'
import gql from 'graphql-tag'
import CONFIG from '@config/index'
import databaseContext from '@context/database'
import Factory, { cleanDatabase } from '@db/factories'
import EmailAddress from '@db/models/EmailAddress'
import User from '@db/models/User'
import createServer, { getContext } from '@src/server'
import type { ApolloTestSetup } from '@root/test/helpers'
import { createApolloTestSetup } from '@root/test/helpers'
import type { Context } from '@src/context'
let variables
const database = databaseContext()
let server: ApolloServer
let authenticatedUser
let mutate
let authenticatedUser: Context['user']
const context = () => ({ authenticatedUser, config })
let mutate: ApolloTestSetup['mutate']
let database: ApolloTestSetup['database']
let server: ApolloTestSetup['server']
let config: Partial<Context['config']> = {}
beforeAll(async () => {
await cleanDatabase()
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/require-await
const contextUser = async (_req) => authenticatedUser
const context = getContext({ user: contextUser, database })
server = createServer({ context }).server
const createTestClientResult = createTestClient(server)
mutate = createTestClientResult.mutate
const apolloSetup = createApolloTestSetup({ context })
mutate = apolloSetup.mutate
database = apolloSetup.database
server = apolloSetup.server
})
afterAll(() => {
@ -40,6 +34,7 @@ afterAll(() => {
})
beforeEach(() => {
config = {}
variables = {}
})
@ -62,11 +57,13 @@ describe('Signup', () => {
describe('unauthenticated', () => {
beforeEach(() => {
authenticatedUser = null
config = {
INVITE_REGISTRATION: false,
PUBLIC_REGISTRATION: false,
}
})
it('throws AuthorizationError', async () => {
CONFIG.INVITE_REGISTRATION = false
CONFIG.PUBLIC_REGISTRATION = false
await expect(mutate({ mutation, variables })).resolves.toMatchObject({
errors: [{ message: 'Not Authorized!' }],
})

View File

@ -7,7 +7,7 @@ import { UserInputError } from 'apollo-server'
import { hash } from 'bcryptjs'
import { getNeode } from '@db/neo4j'
import { Context } from '@src/server'
import { Context } from '@src/context'
import existingEmailAddress from './helpers/existingEmailAddress'
import generateNonce from './helpers/generateNonce'
@ -106,7 +106,7 @@ export default {
await redeemInviteCode(context, inviteCode, true)
}
await createOrUpdateLocations('User', user.id, locationName, session)
await createOrUpdateLocations('User', user.id, locationName, session, context)
return user
} catch (e) {
if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed')

View File

@ -1,46 +1,38 @@
/* 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'
/* eslint-disable @typescript-eslint/no-explicit-any */
import Factory, { cleanDatabase } from '@db/factories'
import { getNeode, getDriver } from '@db/neo4j'
import { CreateMessage } from '@graphql/queries/CreateMessage'
import { createRoomMutation } from '@graphql/queries/createRoomMutation'
import { roomQuery } from '@graphql/queries/roomQuery'
import { unreadRoomsQuery } from '@graphql/queries/unreadRoomsQuery'
import createServer from '@src/server'
import type { ApolloTestSetup } from '@root/test/helpers'
import { createApolloTestSetup } from '@root/test/helpers'
import type { Context } from '@src/context'
const driver = getDriver()
const neode = getNeode()
let query
let mutate
let authenticatedUser
let chattingUser, otherChattingUser, notChattingUser
let authenticatedUser: Context['user']
const context = () => ({ authenticatedUser })
let mutate: ApolloTestSetup['mutate']
let query: ApolloTestSetup['query']
let database: ApolloTestSetup['database']
let server: ApolloTestSetup['server']
beforeAll(async () => {
await cleanDatabase()
const { server } = createServer({
context: () => {
return {
driver,
neode,
user: authenticatedUser,
cypherParams: {
currentUserId: authenticatedUser ? authenticatedUser.id : null,
},
}
},
})
query = createTestClient(server).query
mutate = createTestClient(server).mutate
const apolloSetup = createApolloTestSetup({ context })
mutate = apolloSetup.mutate
query = apolloSetup.query
database = apolloSetup.database
server = apolloSetup.server
})
afterAll(async () => {
await cleanDatabase()
await driver.close()
void server.stop()
void database.driver.close()
database.neode.close()
})
describe('Room', () => {
@ -73,6 +65,10 @@ describe('Room', () => {
describe('create room', () => {
describe('unauthenticated', () => {
beforeAll(() => {
authenticatedUser = null
})
it('throws authorization error', async () => {
await expect(
mutate({
@ -133,13 +129,13 @@ describe('Room', () => {
userId: 'other-chatting-user',
},
})
roomId = result.data.CreateRoom.id
roomId = (result.data as any).CreateRoom.id
expect(result).toMatchObject({
errors: undefined,
data: {
CreateRoom: {
id: expect.any(String),
roomId: result.data.CreateRoom.id,
roomId: (result.data as any).CreateRoom.id,
roomName: 'Other Chatting User',
unreadCount: 0,
users: expect.arrayContaining([
@ -215,7 +211,7 @@ describe('Room', () => {
Room: [
{
id: expect.any(String),
roomId: result.data.Room[0].id,
roomId: (result.data as any).Room[0].id,
roomName: 'Other Chatting User',
users: expect.arrayContaining([
{
@ -255,7 +251,7 @@ describe('Room', () => {
Room: [
{
id: expect.any(String),
roomId: result.data.Room[0].id,
roomId: (result.data as any).Room[0].id,
roomName: 'Chatting User',
unreadCount: 0,
users: expect.arrayContaining([
@ -325,7 +321,7 @@ describe('Room', () => {
userId: 'not-chatting-user',
},
})
otherRoomId = result.data.CreateRoom.roomId
otherRoomId = (result.data as any).CreateRoom.roomId
await mutate({
mutation: CreateMessage,
variables: {
@ -354,7 +350,7 @@ describe('Room', () => {
userId: 'not-chatting-user',
},
})
otherRoomId = result2.data.CreateRoom.roomId
otherRoomId = (result2.data as any).CreateRoom.roomId
await mutate({
mutation: CreateMessage,
variables: {
@ -591,7 +587,6 @@ describe('Room', () => {
})
describe('query single room', () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let result: any = null
beforeAll(async () => {

View File

@ -2,30 +2,21 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { ApolloServer } from 'apollo-server-express'
import { createTestClient } from 'apollo-server-testing'
import databaseContext from '@context/database'
import Factory, { cleanDatabase } from '@db/factories'
import { statistics } from '@graphql/queries/statistics'
import createServer, { getContext } from '@src/server'
import type { ApolloTestSetup } from '@root/test/helpers'
import { createApolloTestSetup } from '@root/test/helpers'
const database = databaseContext()
let server: ApolloServer
let query, authenticatedUser
let database: ApolloTestSetup['database']
let server: ApolloTestSetup['server']
let query: ApolloTestSetup['query']
beforeAll(async () => {
await cleanDatabase()
// eslint-disable-next-line @typescript-eslint/require-await
const contextUser = async (_req) => authenticatedUser
const context = getContext({ user: contextUser, database })
server = createServer({ context }).server
const createTestClientResult = createTestClient(server)
query = createTestClientResult.query
const apolloSetup = createApolloTestSetup()
query = apolloSetup.query
database = apolloSetup.database
server = apolloSetup.server
})
afterAll(async () => {

View File

@ -2,7 +2,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/dot-notation */
import { Context } from '@src/server'
import { Context } from '@src/context'
export default {
Query: {

View File

@ -1,31 +1,32 @@
/* eslint-disable @typescript-eslint/require-await */
/* 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 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'
import { verify } from 'jsonwebtoken'
import CONFIG from '@config/index'
import { categories } from '@constants/categories'
import Factory, { cleanDatabase } from '@db/factories'
import { getNeode, getDriver } from '@db/neo4j'
import { loginMutation } from '@graphql/queries/loginMutation'
import encode from '@jwt/encode'
import createServer, { context } from '@src/server'
import { decode } from '@jwt/decode'
import { encode } from '@jwt/encode'
import type { ApolloTestSetup } from '@root/test/helpers'
import { createApolloTestSetup, TEST_CONFIG } from '@root/test/helpers'
const neode = getNeode()
const driver = getDriver()
let query, mutate, variables, req, user
const jwt = { verify }
let variables, req, user
let mutate: ApolloTestSetup['mutate']
let query: ApolloTestSetup['query']
let database: ApolloTestSetup['database']
let server: ApolloTestSetup['server']
const disable = async (id) => {
const moderator = await Factory.build('user', { id: 'u2', role: 'moderator' })
const user = await neode.find('User', id)
const user = await database.neode.find('User', id)
const reportAgainstUser = await Factory.build('report')
await Promise.all([
reportAgainstUser.relateTo(moderator, 'filed', {
@ -42,23 +43,34 @@ const disable = async (id) => {
])
}
const config = {
JWT_SECRET: 'I am the JWT secret',
JWT_EXPIRES: TEST_CONFIG.JWT_EXPIRES,
CLIENT_URI: TEST_CONFIG.CLIENT_URI,
GRAPHQL_URI: TEST_CONFIG.GRAPHQL_URI,
}
const context = { config }
beforeAll(async () => {
await cleanDatabase()
const { server } = createServer({
context: () => {
// One of the rare occasions where we test
// the actual `context` implementation here
return context({ req })
},
})
query = createTestClient(server).query
mutate = createTestClient(server).mutate
const context = async () => {
const authenticatedUser = await decode({ driver: database.driver, config })(
req.headers.authorization,
)
return { authenticatedUser, config }
}
const apolloSetup = createApolloTestSetup({ context })
mutate = apolloSetup.mutate
query = apolloSetup.query
database = apolloSetup.database
server = apolloSetup.server
})
afterAll(async () => {
await cleanDatabase()
await driver.close()
void server.stop()
void database.driver.close()
database.neode.close()
})
beforeEach(() => {
@ -120,7 +132,7 @@ describe('currentUser', () => {
avatar,
},
)
const userBearerToken = encode({ id: 'u3' })
const userBearerToken = encode(context)({ id: 'u3' })
req = { headers: { authorization: `Bearer ${userBearerToken}` } }
})
@ -203,11 +215,11 @@ describe('currentUser', () => {
it('returns only the saved active categories', async () => {
const result = await query({ query: currentUserQuery, variables })
expect(result.data.currentUser.activeCategories).toHaveLength(4)
expect(result.data.currentUser.activeCategories).toContain('cat1')
expect(result.data.currentUser.activeCategories).toContain('cat3')
expect(result.data.currentUser.activeCategories).toContain('cat5')
expect(result.data.currentUser.activeCategories).toContain('cat7')
expect(result.data?.currentUser.activeCategories).toHaveLength(4)
expect(result.data?.currentUser.activeCategories).toContain('cat1')
expect(result.data?.currentUser.activeCategories).toContain('cat3')
expect(result.data?.currentUser.activeCategories).toContain('cat5')
expect(result.data?.currentUser.activeCategories).toContain('cat7')
})
})
})
@ -236,8 +248,8 @@ describe('login', () => {
it('responds with a JWT bearer token', async () => {
const {
data: { login: token },
} = await mutate({ mutation: loginMutation, variables })
jwt.verify(token, CONFIG.JWT_SECRET, (err, data) => {
} = (await mutate({ mutation: loginMutation, variables })) as any // eslint-disable-line @typescript-eslint/no-explicit-any
jwt.verify(token, config.JWT_SECRET, (err, data) => {
expect(data).toMatchObject({
id: 'acb2d923-f3af-479e-9f00-61b12e864666',
})
@ -274,7 +286,7 @@ describe('login', () => {
describe('normalization', () => {
describe('email address is a gmail address ', () => {
beforeEach(async () => {
const email = await neode.first(
const email = await database.neode.first(
'EmailAddress',
{ email: 'test@example.org' },
undefined,
@ -354,7 +366,7 @@ describe('change password', () => {
describe('authenticated', () => {
beforeEach(async () => {
await Factory.build('user', { id: 'u3' })
const userBearerToken = encode({ id: 'u3' })
const userBearerToken = encode(context)({ id: 'u3' })
req = { headers: { authorization: `Bearer ${userBearerToken}` } }
})
describe('old password === new password', () => {

View File

@ -9,7 +9,8 @@ import bcrypt from 'bcryptjs'
import { neo4jgraphql } from 'neo4j-graphql-js'
import { getNeode } from '@db/neo4j'
import encode from '@jwt/encode'
import { encode } from '@jwt/encode'
import type { Context } from '@src/context'
import normalizeEmail from './helpers/normalizeEmail'
@ -21,7 +22,8 @@ export default {
neo4jgraphql(object, { id: context.user.id }, context, resolveInfo),
},
Mutation: {
login: async (_, { email, password }, { driver }) => {
login: async (_, { email, password }, context: Context) => {
const { driver } = context
// if (user && user.id) {
// throw new Error('Already logged in.')
// }
@ -45,17 +47,21 @@ export default {
!currentUser.disabled
) {
delete currentUser.encryptedPassword
return encode(currentUser)
return encode(context)(currentUser)
} else if (currentUser?.disabled) {
throw new AuthenticationError('Your account has been disabled.')
} else {
throw new AuthenticationError('Incorrect email address or password.')
}
} finally {
session.close()
await session.close()
}
},
changePassword: async (_, { oldPassword, newPassword }, { user }) => {
changePassword: async (_, { oldPassword, newPassword }, context: Context) => {
if (!context.user) {
throw new Error('Missing authenticated user.')
}
const { user } = context
const currentUser = await neode.find('User', user.id)
const encryptedPassword = currentUser.get<string>('encryptedPassword')
@ -73,7 +79,7 @@ export default {
updatedAt: new Date().toISOString(),
})
return encode(await currentUser.toJson())
return encode(context)(await currentUser.toJson())
},
},
}

View File

@ -3,29 +3,34 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/require-await */
/* eslint-disable @typescript-eslint/no-unsafe-call */
import { ApolloServer } from 'apollo-server-express'
import { createTestClient } from 'apollo-server-testing'
import gql from 'graphql-tag'
import { categories } from '@constants/categories'
import databaseContext from '@context/database'
import pubsubContext from '@context/pubsub'
import Factory, { cleanDatabase } from '@db/factories'
import User from '@db/models/User'
import { setTrophyBadgeSelected } from '@graphql/queries/setTrophyBadgeSelected'
import createServer, { getContext } from '@src/server'
import type { ApolloTestSetup } from '@root/test/helpers'
import { createApolloTestSetup } from '@root/test/helpers'
import type { Context } from '@src/context'
import type { DecodedUser } from '@src/jwt/decode'
// import CONFIG from '@src/config'
const categoryIds = ['cat9']
let user
let admin
let authenticatedUser
let query
let mutate
let variables
const pubsub = pubsubContext()
let authenticatedUser: Context['user']
const context = () => ({ authenticatedUser, pubsub })
let mutate: ApolloTestSetup['mutate']
let query: ApolloTestSetup['query']
let database: ApolloTestSetup['database']
let server: ApolloTestSetup['server']
const deleteUserMutation = gql`
mutation ($id: ID!, $resource: [Deletable]) {
DeleteUser(id: $id, resource: $resource) {
@ -94,21 +99,13 @@ const resetTrophyBadgesSelected = gql`
}
`
const database = databaseContext()
let server: ApolloServer
beforeAll(async () => {
await cleanDatabase()
const contextUser = async (_req) => authenticatedUser
const context = getContext({ user: contextUser, database, pubsub })
server = createServer({ context }).server
const createTestClientResult = createTestClient(server)
query = createTestClientResult.query
mutate = createTestClientResult.mutate
const apolloSetup = createApolloTestSetup({ context })
mutate = apolloSetup.mutate
query = apolloSetup.query
database = apolloSetup.database
server = apolloSetup.server
})
afterAll(async () => {
@ -118,6 +115,10 @@ afterAll(async () => {
database.neode.close()
})
beforeEach(async () => {
authenticatedUser = null
})
// 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
afterEach(async () => {
await cleanDatabase()
@ -128,6 +129,11 @@ describe('User', () => {
let userQuery
beforeEach(async () => {
const user = await Factory.build('user', {
id: 'user',
role: 'user',
})
authenticatedUser = await user.toJson()
userQuery = gql`
query ($email: String) {
User(email: $email) {
@ -254,7 +260,7 @@ describe('UpdateUser', () => {
it('is not allowed to change other user accounts', async () => {
const { errors } = await mutate({ mutation: updateUserMutation, variables })
expect(errors[0]).toHaveProperty('message', 'Not Authorized!')
expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!')
})
})
@ -326,7 +332,7 @@ describe('UpdateUser', () => {
termsAndConditionsAgreedVersion: 'invalid version format',
}
const { errors } = await mutate({ mutation: updateUserMutation, variables })
expect(errors[0]).toHaveProperty('message', 'Invalid version format!')
expect(errors?.[0]).toHaveProperty('message', 'Invalid version format!')
})
describe('supports updating location', () => {
@ -684,7 +690,10 @@ describe('emailNotificationSettings', () => {
it('returns the emailNotificationSettings', async () => {
authenticatedUser = await user.toJson()
await expect(
query({ query: emailNotificationSettingsQuery, variables: { id: authenticatedUser.id } }),
query({
query: emailNotificationSettingsQuery,
variables: { id: authenticatedUser?.id },
}),
).resolves.toEqual(
expect.objectContaining({
data: {
@ -778,7 +787,7 @@ describe('emailNotificationSettings', () => {
describe('as self', () => {
it('updates the emailNotificationSettings', async () => {
authenticatedUser = await user.toJson()
authenticatedUser = (await user.toJson()) as DecodedUser
await expect(
mutate({
mutation: emailNotificationSettingsMutation,
@ -876,7 +885,7 @@ describe('save category settings', () => {
describe('not authenticated', () => {
beforeEach(async () => {
authenticatedUser = undefined
authenticatedUser = null
})
it('throws an error', async () => {
@ -921,7 +930,7 @@ describe('save category settings', () => {
it('returns the active categories when user is queried', async () => {
await expect(
query({ query: userQuery, variables: { id: authenticatedUser.id } }),
query({ query: userQuery, variables: { id: authenticatedUser?.id } }),
).resolves.toEqual(
expect.objectContaining({
data: {
@ -963,7 +972,7 @@ describe('save category settings', () => {
it('returns the new active categories when user is queried', async () => {
await expect(
query({ query: userQuery, variables: { id: authenticatedUser.id } }),
query({ query: userQuery, variables: { id: authenticatedUser?.id } }),
).resolves.toEqual(
expect.objectContaining({
data: {
@ -1000,7 +1009,7 @@ describe('updateOnlineStatus', () => {
describe('not authenticated', () => {
beforeEach(async () => {
authenticatedUser = undefined
authenticatedUser = null
})
it('throws an error', async () => {
@ -1030,7 +1039,7 @@ describe('updateOnlineStatus', () => {
)
const cypher = 'MATCH (u:User {id: $id}) RETURN u'
const result = await database.neode.cypher(cypher, { id: authenticatedUser.id })
const result = await database.neode.cypher(cypher, { id: authenticatedUser?.id })
const dbUser = database.neode.hydrateFirst(result, 'u', database.neode.model('User'))
await expect(dbUser.toJson()).resolves.toMatchObject({
lastOnlineStatus: 'online',
@ -1056,7 +1065,7 @@ describe('updateOnlineStatus', () => {
)
const cypher = 'MATCH (u:User {id: $id}) RETURN u'
const result = await database.neode.cypher(cypher, { id: authenticatedUser.id })
const result = await database.neode.cypher(cypher, { id: authenticatedUser?.id })
const dbUser = database.neode.hydrateFirst(result, 'u', database.neode.model('User'))
await expect(dbUser.toJson()).resolves.toMatchObject({
lastOnlineStatus: 'away',
@ -1072,7 +1081,7 @@ describe('updateOnlineStatus', () => {
)
const cypher = 'MATCH (u:User {id: $id}) RETURN u'
const result = await database.neode.cypher(cypher, { id: authenticatedUser.id })
const result = await database.neode.cypher(cypher, { id: authenticatedUser?.id })
const dbUser = database.neode.hydrateFirst<typeof User>(
result,
'u',
@ -1091,7 +1100,7 @@ describe('updateOnlineStatus', () => {
}),
)
const result2 = await database.neode.cypher(cypher, { id: authenticatedUser.id })
const result2 = await database.neode.cypher(cypher, { id: authenticatedUser?.id })
const dbUser2 = database.neode.hydrateFirst(result2, 'u', database.neode.model('User'))
await expect(dbUser2.toJson()).resolves.toMatchObject({
lastOnlineStatus: 'away',
@ -1133,7 +1142,7 @@ describe('setTrophyBadgeSelected', () => {
describe('not authenticated', () => {
beforeEach(async () => {
authenticatedUser = undefined
authenticatedUser = null
})
it('throws an error', async () => {
@ -1515,8 +1524,8 @@ describe('resetTrophyBadgesSelected', () => {
})
describe('not authenticated', () => {
beforeEach(async () => {
authenticatedUser = undefined
beforeEach(() => {
authenticatedUser = null
})
it('throws an error', async () => {

View File

@ -10,7 +10,7 @@ import { neo4jgraphql } from 'neo4j-graphql-js'
import { TROPHY_BADGES_SELECTED_MAX } from '@constants/badges'
import { getNeode } from '@db/neo4j'
import { Context } from '@src/server'
import { Context } from '@src/context'
import { defaultTrophyBadge, defaultVerificationBadge } from './badges'
import normalizeEmail from './helpers/normalizeEmail'
@ -168,10 +168,10 @@ export default {
} catch (error) {
throw new UserInputError(error.message)
} finally {
session.close()
await session.close()
}
},
UpdateUser: async (_parent, params, context, _resolveInfo) => {
UpdateUser: async (_parent, params, context: Context, _resolveInfo) => {
const { avatar: avatarInput } = params
delete params.avatar
params.locationName = params.locationName === '' ? null : params.locationName
@ -210,22 +210,24 @@ export default {
)
const [user] = updateUserTransactionResponse.records.map((record) => record.get('user'))
if (avatarInput) {
await images.mergeImage(user, 'AVATAR_IMAGE', avatarInput, { transaction })
await images(context.config).mergeImage(user, 'AVATAR_IMAGE', avatarInput, {
transaction,
})
}
return user
})
try {
const user = await writeTxResultPromise
// TODO: put in a middleware, see "CreateGroup", "UpdateGroup"
await createOrUpdateLocations('User', params.id, params.locationName, session)
await createOrUpdateLocations('User', params.id, params.locationName, session, context)
return user
} catch (error) {
throw new UserInputError(error.message)
} finally {
session.close()
await session.close()
}
},
DeleteUser: async (_object, params, context, _resolveInfo) => {
DeleteUser: async (_object, params, context: Context, _resolveInfo) => {
const { resource, id: userId } = params
const session = context.driver.session()
@ -253,7 +255,9 @@ export default {
return Promise.all(
txResult.records
.map((record) => record.get('resource'))
.map((resource) => images.deleteImage(resource, 'HERO_IMAGE', { transaction })),
.map((resource) =>
images(context.config).deleteImage(resource, 'HERO_IMAGE', { transaction }),
),
)
}),
)
@ -281,14 +285,14 @@ export default {
{ userId },
)
const [user] = deleteUserTransactionResponse.records.map((record) => record.get('user'))
await images.deleteImage(user, 'AVATAR_IMAGE', { transaction })
await images(context.config).deleteImage(user, 'AVATAR_IMAGE', { transaction })
return user
})
try {
const user = await deleteUserTxResultPromise
return user
} finally {
session.close()
await session.close()
}
},
switchUserRole: async (_object, args, context, _resolveInfo) => {

View File

@ -1,16 +1,22 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
import { createTestClient } from 'apollo-server-testing'
import gql from 'graphql-tag'
import Factory, { cleanDatabase } from '@db/factories'
import { getNeode, getDriver } from '@db/neo4j'
import createServer from '@src/server'
import type { ApolloTestSetup } from '@root/test/helpers'
import { createApolloTestSetup } from '@root/test/helpers'
import type { Context } from '@src/context'
const neode = getNeode()
const driver = getDriver()
let authenticatedUser, mutate, query, variables
let variables
let authenticatedUser: Context['user']
const context = () => ({
authenticatedUser,
})
let mutate: ApolloTestSetup['mutate']
let query: any // eslint-disable-line @typescript-eslint/no-explicit-any
let database: ApolloTestSetup['database']
let server: ApolloTestSetup['server']
const updateUserMutation = gql`
mutation ($id: ID!, $name: String!, $locationName: String) {
@ -78,23 +84,19 @@ const newlyCreatedNodesWithLocales = [
beforeAll(async () => {
await cleanDatabase()
const { server } = createServer({
context: () => {
return {
user: authenticatedUser,
neode,
driver,
}
},
const apolloSetup = createApolloTestSetup({
context,
})
mutate = createTestClient(server).mutate
query = createTestClient(server).query
mutate = apolloSetup.mutate
query = apolloSetup.query
database = apolloSetup.database
server = apolloSetup.server
})
afterAll(async () => {
await cleanDatabase()
await driver.close()
afterAll(() => {
void server.stop()
void database.driver.close()
database.neode.close()
})
beforeEach(() => {
@ -110,9 +112,8 @@ afterEach(async () => {
describe('Location Service', () => {
// Authentication
// TODO: unify, externalize, simplify, wtf?
let user
beforeEach(async () => {
user = await Factory.build('user', {
const user = await Factory.build('user', {
id: 'location-user',
})
authenticatedUser = await user.toJson()
@ -195,9 +196,8 @@ describe('Location Service', () => {
describe('userMiddleware', () => {
describe('UpdateUser', () => {
let user
beforeEach(async () => {
user = await Factory.build('user', {
const user = await Factory.build('user', {
id: 'updating-user',
})
authenticatedUser = await user.toJson()
@ -211,7 +211,7 @@ describe('userMiddleware', () => {
locationName: 'Welzheim, Baden-Württemberg, Germany',
}
await mutate({ mutation: updateUserMutation, variables })
const locations = await neode.cypher(
const locations = await database.neode.cypher(
`MATCH (city:Location)-[:IS_IN]->(district:Location)-[:IS_IN]->(state:Location)-[:IS_IN]->(country:Location) return city {.*}, state {.*}, country {.*}`,
{},
)

View File

@ -9,7 +9,7 @@
/* eslint-disable n/no-unsupported-features/node-builtins */
import { UserInputError } from 'apollo-server'
import CONFIG from '@config/index'
import type { Context } from '@src/context'
const locales = ['en', 'de', 'fr', 'nl', 'it', 'es', 'pt', 'pl', 'ru']
@ -61,7 +61,13 @@ const createLocation = async (session, mapboxData) => {
})
}
export const createOrUpdateLocations = async (nodeLabel, nodeId, locationName, session) => {
export const createOrUpdateLocations = async (
nodeLabel,
nodeId,
locationName,
session,
context: Context,
) => {
if (locationName === undefined) return
let locationId
@ -72,7 +78,7 @@ export const createOrUpdateLocations = async (nodeLabel, nodeId, locationName, s
`https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent(
locationName,
)}.json?access_token=${
CONFIG.MAPBOX_TOKEN
context.config.MAPBOX_TOKEN
}&types=region,place,country,address&language=${locales.join(',')}`,
{
signal: AbortSignal.timeout(REQUEST_TIMEOUT),
@ -156,10 +162,10 @@ export const createOrUpdateLocations = async (nodeLabel, nodeId, locationName, s
}
}
export const queryLocations = async ({ place, lang }) => {
export const queryLocations = async ({ place, lang }, context: Context) => {
try {
const res: any = await fetch(
`https://api.mapbox.com/geocoding/v5/mapbox.places/${place}.json?access_token=${CONFIG.MAPBOX_TOKEN}&types=region,place,country&language=${lang}`,
`https://api.mapbox.com/geocoding/v5/mapbox.places/${place}.json?access_token=${context.config.MAPBOX_TOKEN}&types=region,place,country&language=${lang}`,
{
signal: AbortSignal.timeout(REQUEST_TIMEOUT),
},

View File

@ -4,12 +4,20 @@
import Factory, { cleanDatabase } from '@db/factories'
import User from '@db/models/User'
import { getDriver, getNeode } from '@db/neo4j'
import { TEST_CONFIG } from '@root/test/helpers'
import decode from './decode'
import encode from './encode'
import { decode } from './decode'
import { encode } from './encode'
const driver = getDriver()
const neode = getNeode()
const config = {
JWT_SECRET: 'supersecret',
JWT_EXPIRES: TEST_CONFIG.JWT_EXPIRES,
CLIENT_URI: TEST_CONFIG.CLIENT_URI,
GRAPHQL_URI: TEST_CONFIG.GRAPHQL_URI,
}
const context = { driver, config }
beforeAll(async () => {
await cleanDatabase()
@ -26,9 +34,9 @@ afterEach(async () => {
})
describe('decode', () => {
let authorizationHeader
let authorizationHeader: string | undefined | null
const returnsNull = async () => {
await expect(decode(driver, authorizationHeader)).resolves.toBeNull()
await expect(decode(context)(authorizationHeader)).resolves.toBeNull()
}
describe('given `null` as JWT Bearer token', () => {
@ -57,7 +65,8 @@ describe('decode', () => {
describe('given valid JWT Bearer token', () => {
describe('and corresponding user in the database', () => {
let user, validAuthorizationHeader
let user
let validAuthorizationHeader: string
beforeEach(async () => {
user = await Factory.build(
'user',
@ -74,11 +83,11 @@ describe('decode', () => {
email: 'user@example.org',
},
)
validAuthorizationHeader = encode(await user.toJson())
validAuthorizationHeader = encode(context)(await user.toJson())
})
it('returns user object without email', async () => {
await expect(decode(driver, validAuthorizationHeader)).resolves.toMatchObject({
await expect(decode(context)(validAuthorizationHeader)).resolves.toMatchObject({
role: 'user',
name: 'Jenny Rostock',
id: 'u3',
@ -89,7 +98,7 @@ describe('decode', () => {
it('sets `lastActiveAt`', async () => {
let user = await neode.first<typeof User>('User', { id: 'u3' }, undefined)
await expect(user.toJson()).resolves.not.toHaveProperty('lastActiveAt')
await decode(driver, validAuthorizationHeader)
await decode(context)(validAuthorizationHeader)
user = await neode.first<typeof User>('User', { id: 'u3' }, undefined)
await expect(user.toJson()).resolves.toMatchObject({
lastActiveAt: expect.any(String),
@ -107,7 +116,7 @@ describe('decode', () => {
await expect(user.toJson()).resolves.toMatchObject({
lastActiveAt: '2019-10-03T23:33:08.598Z',
})
await decode(driver, validAuthorizationHeader)
await decode(context)(validAuthorizationHeader)
user = await neode.first<typeof User>('User', { id: 'u3' }, undefined)
await expect(user.toJson()).resolves.toMatchObject({
// should be a different time by now ;)

View File

@ -1,44 +1,56 @@
/* 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 jwt from 'jsonwebtoken'
import CONFIG from '@config/index'
import { verify } from 'jsonwebtoken'
export default async (driver, authorizationHeader) => {
if (!authorizationHeader) return null
const token = authorizationHeader.replace('Bearer ', '')
let id = null
try {
const decoded = await jwt.verify(token, CONFIG.JWT_SECRET)
id = decoded.sub
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (err) {
return null
}
const session = driver.session()
import type CONFIG from '@src/config'
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
const updateUserLastActiveTransactionResponse = await transaction.run(
`
import type { JwtPayload } from 'jsonwebtoken'
import type { Driver } from 'neo4j-driver'
export interface DecodedUser {
id: string
slug: string
name: string
role: string
disabled: boolean
}
const jwt = { verify }
export const decode =
(context: { config: Pick<typeof CONFIG, 'JWT_SECRET'>; driver: Driver }) =>
async (authorizationHeader: string | undefined | null) => {
if (!authorizationHeader) return null
const token = authorizationHeader.replace('Bearer ', '')
let id: null | string = null
try {
const decoded = jwt.verify(token, context.config.JWT_SECRET) as JwtPayload
id = decoded.sub ?? null
// eslint-disable-next-line no-catch-all/no-catch-all
} catch (err) {
return null
}
const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction<DecodedUser[]>(async (transaction) => {
const updateUserLastActiveTransactionResponse = await transaction.run(
`
MATCH (user:User {id: $id, deleted: false, disabled: false })
SET user.lastActiveAt = toString(datetime())
RETURN user {.id, .slug, .name, .role, .disabled, .actorId}
LIMIT 1
`,
{ id },
)
return updateUserLastActiveTransactionResponse.records.map((record) => record.get('user'))
})
try {
const [currentUser] = await writeTxResultPromise
if (!currentUser) return null
return {
token,
...currentUser,
{ id },
)
return updateUserLastActiveTransactionResponse.records.map((record) => record.get('user'))
})
try {
const [currentUser] = await writeTxResultPromise
if (!currentUser) return null
return {
token,
...currentUser,
}
} finally {
await session.close()
}
} finally {
session.close()
}
}

View File

@ -1,11 +1,18 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import jwt from 'jsonwebtoken'
import { verify } from 'jsonwebtoken'
import CONFIG from '@config/index'
import { TEST_CONFIG } from '@root/test/helpers'
import encode from './encode'
import { encode } from './encode'
const jwt = { verify }
const config = {
JWT_SECRET: 'supersecret',
JWT_EXPIRES: TEST_CONFIG.JWT_EXPIRES,
CLIENT_URI: TEST_CONFIG.CLIENT_URI,
GRAPHQL_URI: TEST_CONFIG.GRAPHQL_URI,
}
const context = { config }
describe('encode', () => {
let payload
@ -18,9 +25,9 @@ describe('encode', () => {
})
it('encodes a valided JWT bearer token', () => {
const token = encode(payload)
const token = encode(context)(payload)
expect(token.split('.')).toHaveLength(3)
const decoded = jwt.verify(token, CONFIG.JWT_SECRET)
const decoded = jwt.verify(token, context.config.JWT_SECRET)
expect(decoded).toEqual({
name: 'Some body',
slug: 'some-body',
@ -43,7 +50,7 @@ describe('encode', () => {
})
it('does not encode sensitive data', () => {
const token = encode(payload)
const token = encode(context)(payload)
expect(payload).toEqual({
email: 'none-of-your-business@example.org',
password: 'topsecret',
@ -51,7 +58,7 @@ describe('encode', () => {
slug: 'some-body',
id: 'some-id',
})
const decoded = jwt.verify(token, CONFIG.JWT_SECRET)
const decoded = jwt.verify(token, context.config.JWT_SECRET)
expect(decoded).toEqual({
name: 'Some body',
slug: 'some-body',

View File

@ -1,19 +1,23 @@
/* 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-assignment */
import jwt from 'jsonwebtoken'
import { sign } from 'jsonwebtoken'
import CONFIG from '@config/index'
import type CONFIG from '@src/config'
const jwt = { sign }
// Generate an Access Token for the given User ID
export default function encode(user) {
const { id, name, slug } = user
const token = jwt.sign({ id, name, slug }, CONFIG.JWT_SECRET, {
expiresIn: CONFIG.JWT_EXPIRES,
issuer: CONFIG.GRAPHQL_URI,
audience: CONFIG.CLIENT_URI,
subject: user.id.toString(),
})
return token
}
export const encode =
(context: {
config: Pick<typeof CONFIG, 'JWT_SECRET' | 'JWT_EXPIRES' | 'GRAPHQL_URI' | 'CLIENT_URI'>
}) =>
(user) => {
const { id, name, slug } = user
const token: string = jwt.sign({ id, name, slug }, context.config.JWT_SECRET, {
expiresIn: context.config.JWT_EXPIRES,
issuer: context.config.GRAPHQL_URI,
audience: context.config.CLIENT_URI,
subject: user.id.toString(),
})
return token
}

View File

@ -1,33 +1,29 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
import { ApolloServer } from 'apollo-server-express'
import { createTestClient } from 'apollo-server-testing'
import gql from 'graphql-tag'
import databaseContext from '@context/database'
import Factory, { cleanDatabase } from '@db/factories'
import CONFIG from '@src/config'
import type { ApolloTestSetup } from '@root/test/helpers'
import { createApolloTestSetup } from '@root/test/helpers'
import { categories } from '@src/constants/categories'
import createServer, { getContext } from '@src/server'
import type { Context } from '@src/context'
const database = databaseContext()
let config: Partial<Context['config']>
let query: ApolloTestSetup['query']
let database: ApolloTestSetup['database']
let server: ApolloTestSetup['server']
let server: ApolloServer
let query
beforeEach(() => {
config = {}
})
beforeAll(async () => {
await cleanDatabase()
const authenticatedUser = null
// eslint-disable-next-line @typescript-eslint/require-await
const contextUser = async (_req) => authenticatedUser
const context = getContext({ user: contextUser, database })
server = createServer({ context }).server
const createTestClientResult = createTestClient(server)
query = createTestClientResult.query
const context = () => ({ config, authenticatedUser: null })
const apolloSetup = createApolloTestSetup({ context })
query = apolloSetup.query
database = apolloSetup.database
server = apolloSetup.server
for (const category of categories) {
await Factory.build('category', {
id: category.id,
@ -55,10 +51,10 @@ const categoriesQuery = gql`
}
`
describe('categroeis middleware', () => {
describe('categories middleware', () => {
describe('categories are active', () => {
beforeEach(() => {
CONFIG.CATEGORIES_ACTIVE = true
config = { ...config, CATEGORIES_ACTIVE: true }
})
it('returns the categories', async () => {
@ -78,7 +74,7 @@ describe('categroeis middleware', () => {
describe('categories are not active', () => {
beforeEach(() => {
CONFIG.CATEGORIES_ACTIVE = false
config = { ...config, CATEGORIES_ACTIVE: false }
})
it('returns an empty array though there are categories in the db', async () => {

View File

@ -1,9 +1,19 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-call */
import CONFIG from '@src/config'
import type { Context } from '@src/context'
const checkCategoriesActive = (resolve, root, args, context, resolveInfo) => {
if (CONFIG.CATEGORIES_ACTIVE) {
type Resolver = (
root: unknown,
args: unknown,
context: Context,
resolveInfo: unknown,
) => Promise<unknown>
const checkCategoriesActive = (
resolve: Resolver,
root: unknown,
args: unknown,
context: Context,
resolveInfo: unknown,
) => {
if (context.config.CATEGORIES_ACTIVE) {
return resolve(root, args, context, resolveInfo)
}
return []

View File

@ -1,21 +1,20 @@
/* 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-assignment */
import { createTestClient } from 'apollo-server-testing'
import gql from 'graphql-tag'
import { cleanDatabase } from '@db/factories'
import { getNeode, getDriver } from '@db/neo4j'
import createServer from '@src/server'
import type { ApolloTestSetup } from '@root/test/helpers'
import { createApolloTestSetup } from '@root/test/helpers'
import type { Context } from '@src/context'
let server
let query
let mutate
let hashtagingUser
let authenticatedUser
const driver = getDriver()
const neode = getNeode()
let authenticatedUser: Context['user']
const context = () => ({ authenticatedUser })
let mutate: ApolloTestSetup['mutate']
let query: any // eslint-disable-line @typescript-eslint/no-explicit-any
let database: ApolloTestSetup['database']
let server: ApolloTestSetup['server']
const categoryIds = ['cat9']
const createPostMutation = gql`
mutation ($id: ID, $title: String!, $postContent: String!, $categoryIds: [ID]!) {
@ -37,34 +36,27 @@ const updatePostMutation = gql`
beforeAll(async () => {
await cleanDatabase()
const createServerResult = createServer({
context: () => {
return {
user: authenticatedUser,
neode,
driver,
}
},
})
server = createServerResult.server
const createTestClientResult = createTestClient(server)
query = createTestClientResult.query
mutate = createTestClientResult.mutate
const apolloSetup = createApolloTestSetup({ context })
mutate = apolloSetup.mutate
query = apolloSetup.query
database = apolloSetup.database
server = apolloSetup.server
})
afterAll(async () => {
await cleanDatabase()
await driver.close()
void server.stop()
void database.driver.close()
database.neode.close()
})
beforeEach(async () => {
hashtagingUser = await neode.create('User', {
hashtagingUser = await database.neode.create('User', {
id: 'you',
name: 'Al Capone',
slug: 'al-capone',
})
await neode.create('Category', {
await database.neode.create('Category', {
id: 'cat9',
name: 'Democracy & Politics',
icon: 'university',

View File

@ -16,7 +16,6 @@ import languages from './languages/languages'
import login from './login/loginMiddleware'
import notifications from './notifications/notificationsMiddleware'
import orderBy from './orderByMiddleware'
// eslint-disable-next-line import/no-cycle
import permissions from './permissionsMiddleware'
import sentry from './sentryMiddleware'
import sluggify from './sluggifyMiddleware'

View File

@ -1,38 +1,33 @@
/* 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'
import Factory, { cleanDatabase } from '@db/factories'
import { getNeode, getDriver } from '@db/neo4j'
import createServer from '@src/server'
import type { ApolloTestSetup } from '@root/test/helpers'
import { createApolloTestSetup } from '@root/test/helpers'
import type { Context } from '@src/context'
let mutate
let authenticatedUser
let authenticatedUser: Context['user']
const context = () => ({ authenticatedUser })
let variables
const driver = getDriver()
const neode = getNeode()
let mutate: ApolloTestSetup['mutate']
let database: ApolloTestSetup['database']
let server: ApolloTestSetup['server']
beforeAll(async () => {
await cleanDatabase()
const { server } = createServer({
context: () => {
return {
driver,
neode,
user: authenticatedUser,
}
},
})
mutate = createTestClient(server).mutate
const apolloSetup = createApolloTestSetup({ context })
mutate = apolloSetup.mutate
database = apolloSetup.database
server = apolloSetup.server
})
afterAll(async () => {
await cleanDatabase()
await driver.close()
void server.stop()
void database.driver.close()
database.neode.close()
})
const createPostMutation = gql`

View File

@ -1,27 +1,29 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/require-await */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import { ApolloServer } from 'apollo-server-express'
import { createTestClient } from 'apollo-server-testing'
import gql from 'graphql-tag'
import databaseContext from '@context/database'
import Factory, { cleanDatabase } from '@db/factories'
import { createGroupMutation } from '@graphql/queries/createGroupMutation'
import { joinGroupMutation } from '@graphql/queries/joinGroupMutation'
import CONFIG from '@src/config'
import createServer, { getContext } from '@src/server'
CONFIG.CATEGORIES_ACTIVE = false
import type { ApolloTestSetup } from '@root/test/helpers'
import { createApolloTestSetup } from '@root/test/helpers'
import type { Context } from '@src/context'
const sendNotificationMailMock: (notification) => void = jest.fn()
jest.mock('@src/emails/sendEmail', () => ({
sendNotificationMail: (notification) => sendNotificationMailMock(notification),
}))
let query, mutate, authenticatedUser, emaillessMember
let emaillessMember
let authenticatedUser: Context['user']
const config = { CATEGORIES_ACTIVE: false }
const context = () => ({ authenticatedUser, config })
let mutate: ApolloTestSetup['mutate']
let query: ApolloTestSetup['query']
let database: ApolloTestSetup['database']
let server: ApolloTestSetup['server']
let postAuthor, groupMember
@ -94,21 +96,13 @@ const markAllAsRead = async () =>
`,
})
const database = databaseContext()
let server: ApolloServer
beforeAll(async () => {
await cleanDatabase()
const contextUser = async (_req) => authenticatedUser
const context = getContext({ user: contextUser, database })
server = createServer({ context }).server
const createTestClientResult = createTestClient(server)
query = createTestClientResult.query
mutate = createTestClientResult.mutate
const apolloSetup = createApolloTestSetup({ context })
mutate = apolloSetup.mutate
query = apolloSetup.query
database = apolloSetup.database
server = apolloSetup.server
})
afterAll(async () => {

View File

@ -1,25 +1,27 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import { ApolloServer } from 'apollo-server-express'
import { createTestClient } from 'apollo-server-testing'
import gql from 'graphql-tag'
import databaseContext from '@context/database'
import Factory, { cleanDatabase } from '@db/factories'
import { createGroupMutation } from '@graphql/queries/createGroupMutation'
import CONFIG from '@src/config'
import createServer, { getContext } from '@src/server'
CONFIG.CATEGORIES_ACTIVE = false
import type { ApolloTestSetup } from '@root/test/helpers'
import { createApolloTestSetup } from '@root/test/helpers'
import type { Context } from '@src/context'
const sendNotificationMailMock: (notification) => void = jest.fn()
jest.mock('@src/emails/sendEmail', () => ({
sendNotificationMail: (notification) => sendNotificationMailMock(notification),
}))
let query, mutate, authenticatedUser
let authenticatedUser: Context['user']
const config = { CATEGORIES_ACTIVE: false }
const context = () => ({ authenticatedUser, config })
let mutate: ApolloTestSetup['mutate']
let query: ApolloTestSetup['query']
let database: ApolloTestSetup['database']
let server: ApolloTestSetup['server']
let postAuthor, firstFollower, secondFollower, thirdFollower, emaillessFollower
@ -68,22 +70,13 @@ const followUserMutation = gql`
}
`
const database = databaseContext()
let server: ApolloServer
beforeAll(async () => {
await cleanDatabase()
// eslint-disable-next-line @typescript-eslint/require-await
const contextUser = async (_req) => authenticatedUser
const context = getContext({ user: contextUser, database })
server = createServer({ context }).server
const createTestClientResult = createTestClient(server)
query = createTestClientResult.query
mutate = createTestClientResult.mutate
const apolloSetup = createApolloTestSetup({ context })
mutate = apolloSetup.mutate
query = apolloSetup.query
database = apolloSetup.database
server = apolloSetup.server
})
afterAll(async () => {

View File

@ -1,28 +1,29 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/require-await */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import { ApolloServer } from 'apollo-server-express'
import { createTestClient } from 'apollo-server-testing'
import gql from 'graphql-tag'
import databaseContext from '@context/database'
import Factory, { cleanDatabase } from '@db/factories'
import { changeGroupMemberRoleMutation } from '@graphql/queries/changeGroupMemberRoleMutation'
import { createGroupMutation } from '@graphql/queries/createGroupMutation'
import { joinGroupMutation } from '@graphql/queries/joinGroupMutation'
import CONFIG from '@src/config'
import createServer, { getContext } from '@src/server'
CONFIG.CATEGORIES_ACTIVE = false
import type { ApolloTestSetup } from '@root/test/helpers'
import { createApolloTestSetup } from '@root/test/helpers'
import type { Context } from '@src/context'
const sendNotificationMailMock: (notification) => void = jest.fn()
jest.mock('@src/emails/sendEmail', () => ({
sendNotificationMail: (notification) => sendNotificationMailMock(notification),
}))
let query, mutate, authenticatedUser
let authenticatedUser: Context['user']
const config = { CATEGORIES_ACTIVE: false }
const context = () => ({ authenticatedUser, config })
let mutate: ApolloTestSetup['mutate']
let query: ApolloTestSetup['query']
let database: ApolloTestSetup['database']
let server: ApolloTestSetup['server']
let postAuthor, groupMember, pendingMember, noMember, emaillessMember
@ -90,21 +91,13 @@ const markAllAsRead = async () =>
`,
})
const database = databaseContext()
let server: ApolloServer
beforeAll(async () => {
await cleanDatabase()
const contextUser = async (_req) => authenticatedUser
const context = getContext({ user: contextUser, database })
server = createServer({ context }).server
const createTestClientResult = createTestClient(server)
query = createTestClientResult.query
mutate = createTestClientResult.mutate
const apolloSetup = createApolloTestSetup({ context })
mutate = apolloSetup.mutate
query = apolloSetup.query
database = apolloSetup.database
server = apolloSetup.server
})
afterAll(async () => {

View File

@ -1,25 +1,25 @@
/* eslint-disable @typescript-eslint/require-await */
/* 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 { ApolloServer } from 'apollo-server-express'
import { createTestClient } from 'apollo-server-testing'
import gql from 'graphql-tag'
import CONFIG from '@config/index'
import databaseContext from '@context/database'
import Factory, { cleanDatabase } from '@db/factories'
import createServer, { getContext } from '@src/server'
CONFIG.CATEGORIES_ACTIVE = false
import type { ApolloTestSetup } from '@root/test/helpers'
import { createApolloTestSetup } from '@root/test/helpers'
import type { Context } from '@src/context'
const sendNotificationMailMock: (notification) => void = jest.fn()
jest.mock('@src/emails/sendEmail', () => ({
sendNotificationMail: (notification) => sendNotificationMailMock(notification),
}))
let query, mutate, authenticatedUser
let authenticatedUser: Context['user']
const config = { CATEGORIES_ACTIVE: false }
const context = () => ({ authenticatedUser, config })
let mutate: ApolloTestSetup['mutate']
let query: ApolloTestSetup['query']
let database: ApolloTestSetup['database']
let server: ApolloTestSetup['server']
let postAuthor, firstCommenter, secondCommenter, emaillessObserver
@ -77,21 +77,13 @@ const toggleObservePostMutation = gql`
}
}
`
const database = databaseContext()
let server: ApolloServer
beforeAll(async () => {
await cleanDatabase()
const contextUser = async (_req) => authenticatedUser
const context = getContext({ user: contextUser, database })
server = createServer({ context }).server
const createTestClientResult = createTestClient(server)
query = createTestClientResult.query
mutate = createTestClientResult.mutate
const apolloSetup = createApolloTestSetup({ context })
mutate = apolloSetup.mutate
query = apolloSetup.query
database = apolloSetup.database
server = apolloSetup.server
})
afterAll(async () => {

View File

@ -2,15 +2,12 @@
/* 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'
import databaseContext from '@context/database'
import Factory, { cleanDatabase } from '@db/factories'
import CONFIG from '@src/config'
import createServer, { getContext } from '@src/server'
CONFIG.CATEGORIES_ACTIVE = false
import type { ApolloTestSetup } from '@root/test/helpers'
import { createApolloTestSetup } from '@root/test/helpers'
import type { Context } from '@src/context'
const sendNotificationMailMock: (notification) => void = jest.fn()
jest.mock('@src/emails/sendEmail', () => ({
@ -22,7 +19,12 @@ jest.mock('../helpers/isUserOnline', () => ({
isUserOnline: () => isUserOnlineMock(),
}))
let mutate, authenticatedUser
let authenticatedUser: Context['user']
const config = { CATEGORIES_ACTIVE: false }
const context = () => ({ authenticatedUser, config })
let mutate: ApolloTestSetup['mutate']
let database: ApolloTestSetup['database']
let server: ApolloTestSetup['server']
let postAuthor
@ -36,23 +38,17 @@ const createPostMutation = gql`
}
`
const database = databaseContext()
beforeAll(async () => {
await cleanDatabase()
// eslint-disable-next-line @typescript-eslint/require-await
const contextUser = async (_req) => authenticatedUser
const context = getContext({ user: contextUser, database })
const { server } = createServer({ context })
const createTestClientResult = createTestClient(server)
mutate = createTestClientResult.mutate
const apolloSetup = createApolloTestSetup({ context })
mutate = apolloSetup.mutate
database = apolloSetup.database
server = apolloSetup.server
})
afterAll(async () => {
await cleanDatabase()
void server.stop()
await database.driver.close()
})

View File

@ -1,28 +1,29 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/require-await */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import { ApolloServer } from 'apollo-server-express'
import { createTestClient } from 'apollo-server-testing'
import gql from 'graphql-tag'
import databaseContext from '@context/database'
import Factory, { cleanDatabase } from '@db/factories'
import { changeGroupMemberRoleMutation } from '@graphql/queries/changeGroupMemberRoleMutation'
import { createGroupMutation } from '@graphql/queries/createGroupMutation'
import { joinGroupMutation } from '@graphql/queries/joinGroupMutation'
import CONFIG from '@src/config'
import createServer, { getContext } from '@src/server'
CONFIG.CATEGORIES_ACTIVE = false
import type { ApolloTestSetup } from '@root/test/helpers'
import { createApolloTestSetup } from '@root/test/helpers'
import type { Context } from '@src/context'
const sendNotificationMailMock: (notification) => void = jest.fn()
jest.mock('@src/emails/sendEmail', () => ({
sendNotificationMail: (notification) => sendNotificationMailMock(notification),
}))
let query, mutate, authenticatedUser
let authenticatedUser: Context['user']
const config = { CATEGORIES_ACTIVE: false }
const context = () => ({ authenticatedUser, config })
let mutate: ApolloTestSetup['mutate']
let query: ApolloTestSetup['query']
let database: ApolloTestSetup['database']
let server: ApolloTestSetup['server']
let postAuthor, groupMember, pendingMember, emaillessMember
@ -92,20 +93,13 @@ const markAllAsRead = async () =>
`,
})
const database = databaseContext()
let server: ApolloServer
beforeAll(async () => {
await cleanDatabase()
const contextUser = async (_req) => authenticatedUser
const context = getContext({ user: contextUser, database })
server = createServer({ context }).server
const createTestClientResult = createTestClient(server)
query = createTestClientResult.query
mutate = createTestClientResult.mutate
const apolloSetup = createApolloTestSetup({ context })
mutate = apolloSetup.mutate
query = apolloSetup.query
database = apolloSetup.database
server = apolloSetup.server
})
afterAll(async () => {

View File

@ -4,11 +4,8 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import { ApolloServer } from 'apollo-server-express'
import { createTestClient } from 'apollo-server-testing'
import gql from 'graphql-tag'
import databaseContext from '@context/database'
import pubsubContext from '@context/pubsub'
import Factory, { cleanDatabase } from '@db/factories'
import { changeGroupMemberRoleMutation } from '@graphql/queries/changeGroupMemberRoleMutation'
@ -18,7 +15,10 @@ import { createRoomMutation } from '@graphql/queries/createRoomMutation'
import { joinGroupMutation } from '@graphql/queries/joinGroupMutation'
import { leaveGroupMutation } from '@graphql/queries/leaveGroupMutation'
import { removeUserFromGroupMutation } from '@graphql/queries/removeUserFromGroupMutation'
import createServer, { getContext } from '@src/server'
import type { ApolloTestSetup } from '@root/test/helpers'
import { createApolloTestSetup } from '@root/test/helpers'
import type { Context } from '@src/context'
import type { DecodedUser } from '@src/jwt/decode'
const sendChatMessageMailMock: (notification) => void = jest.fn()
const sendNotificationMailMock: (notification) => void = jest.fn()
@ -32,11 +32,17 @@ jest.mock('../helpers/isUserOnline', () => ({
isUserOnline: () => isUserOnlineMock(),
}))
const database = databaseContext()
const pubsub = pubsubContext()
const pubsubSpy = jest.spyOn(pubsub, 'publish')
let query, mutate, notifiedUser, authenticatedUser
let notifiedUser
let authenticatedUser: Context['user']
const context = () => ({ authenticatedUser, pubsub })
let mutate: ApolloTestSetup['mutate']
let query: any // eslint-disable-line @typescript-eslint/no-explicit-any
let database: ApolloTestSetup['database']
let server: ApolloTestSetup['server']
const categoryIds = ['cat9']
const createPostMutation = gql`
@ -65,19 +71,13 @@ const createCommentMutation = gql`
}
`
let server: ApolloServer
beforeAll(async () => {
await cleanDatabase()
const contextUser = async (_req) => authenticatedUser
const context = getContext({ user: contextUser, database, pubsub })
server = createServer({ context }).server
const createTestClientResult = createTestClient(server)
query = createTestClientResult.query
mutate = createTestClientResult.mutate
const apolloSetup = createApolloTestSetup({ context })
mutate = apolloSetup.mutate
query = apolloSetup.query
database = apolloSetup.database
server = apolloSetup.server
})
afterAll(async () => {
@ -910,7 +910,7 @@ describe('notifications', () => {
userId: 'chatReceiver',
},
})
roomId = room.data.CreateRoom.id
roomId = (room.data as any).CreateRoom.id // eslint-disable-line @typescript-eslint/no-explicit-any
})
describe('if the chatReceiver is online', () => {
@ -1106,7 +1106,7 @@ describe('notifications', () => {
describe('user joins group', () => {
const joinGroupAction = async () => {
authenticatedUser = await notifiedUser.toJson()
authenticatedUser = (await notifiedUser.toJson()) as DecodedUser
await mutate({
mutation: joinGroupMutation(),
variables: {
@ -1193,7 +1193,7 @@ describe('notifications', () => {
describe('user joins and leaves group', () => {
const leaveGroupAction = async () => {
authenticatedUser = await notifiedUser.toJson()
authenticatedUser = (await notifiedUser.toJson()) as DecodedUser
await mutate({
mutation: leaveGroupMutation(),
variables: {
@ -1206,7 +1206,7 @@ describe('notifications', () => {
beforeEach(async () => {
jest.clearAllMocks()
authenticatedUser = await notifiedUser.toJson()
authenticatedUser = (await notifiedUser.toJson()) as DecodedUser
await mutate({
mutation: joinGroupMutation(),
variables: {
@ -1318,7 +1318,7 @@ describe('notifications', () => {
describe('user role in group changes', () => {
const changeGroupMemberRoleAction = async () => {
authenticatedUser = await groupOwner.toJson()
authenticatedUser = (await groupOwner.toJson()) as DecodedUser
await mutate({
mutation: changeGroupMemberRoleMutation(),
variables: {
@ -1331,7 +1331,7 @@ describe('notifications', () => {
}
beforeEach(async () => {
authenticatedUser = await notifiedUser.toJson()
authenticatedUser = (await notifiedUser.toJson()) as DecodedUser
await mutate({
mutation: joinGroupMutation(),
variables: {
@ -1427,7 +1427,7 @@ describe('notifications', () => {
}
beforeEach(async () => {
authenticatedUser = await notifiedUser.toJson()
authenticatedUser = (await notifiedUser.toJson()) as DecodedUser
await mutate({
mutation: joinGroupMutation(),
variables: {

View File

@ -1,36 +1,35 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { ApolloServer } from 'apollo-server-express'
import { createTestClient } from 'apollo-server-testing'
import gql from 'graphql-tag'
import CONFIG from '@config/index'
import databaseContext from '@context/database'
import Factory, { cleanDatabase } from '@db/factories'
import createServer, { getContext } from '@src/server'
import type { ApolloTestSetup } from '@root/test/helpers'
import { createApolloTestSetup } from '@root/test/helpers'
import type { Context } from '@src/context'
let variables
let owner, anotherRegularUser, administrator, moderator
const database = databaseContext()
let authenticatedUser: Context['user']
let config: Partial<Context['config']>
const context = () => ({ authenticatedUser, config })
let mutate: ApolloTestSetup['mutate']
let query: ApolloTestSetup['query']
let database: ApolloTestSetup['database']
let server: ApolloTestSetup['server']
let server: ApolloServer
let authenticatedUser
let query, mutate
beforeEach(() => {
config = { CATEGORIES_ACTIVE: true }
})
beforeAll(async () => {
await cleanDatabase()
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/require-await
const contextUser = async (_req) => authenticatedUser
const context = getContext({ user: contextUser, database })
server = createServer({ context }).server
const createTestClientResult = createTestClient(server)
query = createTestClientResult.query
mutate = createTestClientResult.mutate
const apolloSetup = createApolloTestSetup({ context })
mutate = apolloSetup.mutate
query = apolloSetup.query
database = apolloSetup.database
server = apolloSetup.server
})
afterAll(() => {
@ -194,11 +193,16 @@ describe('authorization', () => {
inviteCode: 'ABCDEF',
locale: 'de',
}
CONFIG.INVITE_REGISTRATION = false
CONFIG.PUBLIC_REGISTRATION = false
await Factory.build('inviteCode', {
code: 'ABCDEF',
})
config = {
...config,
CATEGORIES_ACTIVE: true,
INVITE_REGISTRATION: false,
PUBLIC_REGISTRATION: false,
}
})
describe('as user', () => {
@ -237,11 +241,15 @@ describe('authorization', () => {
inviteCode: 'ABCDEF',
locale: 'de',
}
CONFIG.INVITE_REGISTRATION = false
CONFIG.PUBLIC_REGISTRATION = true
await Factory.build('inviteCode', {
code: 'ABCDEF',
})
config = {
...config,
CATEGORIES_ACTIVE: true,
INVITE_REGISTRATION: false,
PUBLIC_REGISTRATION: true,
}
})
describe('as anyone', () => {
@ -262,11 +270,15 @@ describe('authorization', () => {
describe('invite registration', () => {
beforeEach(async () => {
CONFIG.INVITE_REGISTRATION = true
CONFIG.PUBLIC_REGISTRATION = false
await Factory.build('inviteCode', {
code: 'ABCDEF',
})
config = {
...config,
CATEGORIES_ACTIVE: true,
INVITE_REGISTRATION: true,
PUBLIC_REGISTRATION: false,
}
})
describe('as anyone with valid invite code', () => {

View File

@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-unsafe-argument */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/require-await */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
@ -9,9 +9,8 @@ import { rule, shield, deny, allow, or, and } from 'graphql-shield'
import CONFIG from '@config/index'
import SocialMedia from '@db/models/SocialMedia'
import { getNeode } from '@db/neo4j'
// eslint-disable-next-line import/no-cycle
import { validateInviteCode } from '@graphql/resolvers/inviteCodes'
import { Context } from '@src/server'
import type { Context } from '@src/context'
const debug = !!CONFIG.DEBUG
const allowExternalErrors = true
@ -24,29 +23,29 @@ const isAuthenticated = rule({
return !!ctx?.user?.id
})
const isModerator = rule()(async (_parent, _args, { user }, _info) => {
return user && (user.role === 'moderator' || user.role === 'admin')
const isModerator = rule()(async (_parent, _args, { user }: Context, _info) => {
return !!(user && (user.role === 'moderator' || user.role === 'admin'))
})
const isAdmin = rule()(async (_parent, _args, { user }, _info) => {
return user && user.role === 'admin'
const isAdmin = rule()(async (_parent, _args, { user }: Context, _info) => {
return !!(user && user.role === 'admin')
})
const onlyYourself = rule({
cache: 'no_cache',
})(async (_parent, args, context, _info) => {
return context.user.id === args.id
})(async (_parent, args, context: Context, _info) => {
return context.user?.id === args.id
})
const isMyOwn = rule({
cache: 'no_cache',
})(async (parent, _args, { user }, _info) => {
return user && user.id === parent.id
})(async (parent, _args, { user }: Context, _info) => {
return !!(user && user.id === parent.id)
})
const isMySocialMedia = rule({
cache: 'no_cache',
})(async (_, args, { user }) => {
})(async (_, args, { user }: Context) => {
// We need a User
if (!user) {
return false
@ -65,7 +64,7 @@ const isMySocialMedia = rule({
const isAllowedToChangeGroupSettings = rule({
cache: 'no_cache',
})(async (_parent, args, { user, driver }) => {
})(async (_parent, args, { user, driver }: Context) => {
if (!user?.id) return false
const ownerId = user.id
const { id: groupId } = args
@ -89,13 +88,13 @@ const isAllowedToChangeGroupSettings = rule({
} catch (error) {
throw new Error(error)
} finally {
session.close()
await session.close()
}
})
const isAllowedSeeingGroupMembers = rule({
cache: 'no_cache',
})(async (_parent, args, { user, driver }) => {
})(async (_parent, args, { user, driver }: Context) => {
if (!user?.id) return false
const { id: groupId } = args
const session = driver.session()
@ -125,13 +124,13 @@ const isAllowedSeeingGroupMembers = rule({
} catch (error) {
throw new Error(error)
} finally {
session.close()
await session.close()
}
})
const isAllowedToChangeGroupMemberRole = rule({
cache: 'no_cache',
})(async (_parent, args, { user, driver }) => {
})(async (_parent, args, { user, driver }: Context) => {
if (!user?.id) return false
const currentUserId = user.id
const { groupId, userId, roleInGroup } = args
@ -172,13 +171,13 @@ const isAllowedToChangeGroupMemberRole = rule({
} catch (error) {
throw new Error(error)
} finally {
session.close()
await session.close()
}
})
const isAllowedToJoinGroup = rule({
cache: 'no_cache',
})(async (_parent, args, { user, driver }) => {
})(async (_parent, args, { user, driver }: Context) => {
if (!user?.id) return false
const { groupId, userId } = args
const session = driver.session()
@ -202,13 +201,13 @@ const isAllowedToJoinGroup = rule({
} catch (error) {
throw new Error(error)
} finally {
session.close()
await session.close()
}
})
const isAllowedToLeaveGroup = rule({
cache: 'no_cache',
})(async (_parent, args, { user, driver }) => {
})(async (_parent, args, { user, driver }: Context) => {
if (!user?.id) return false
const { groupId, userId } = args
if (user.id !== userId) return false
@ -232,13 +231,13 @@ const isAllowedToLeaveGroup = rule({
} catch (error) {
throw new Error(error)
} finally {
session.close()
await session.close()
}
})
const isMemberOfGroup = rule({
cache: 'no_cache',
})(async (_parent, args, { user, driver }) => {
})(async (_parent, args, { user, driver }: Context) => {
if (!user?.id) return false
const { groupId } = args
if (!groupId) return true
@ -260,13 +259,13 @@ const isMemberOfGroup = rule({
} catch (error) {
throw new Error(error)
} finally {
session.close()
await session.close()
}
})
const canRemoveUserFromGroup = rule({
cache: 'no_cache',
})(async (_parent, args, { user, driver }) => {
})(async (_parent, args, { user, driver }: Context) => {
if (!user?.id) return false
const { groupId, userId } = args
const currentUserId = user.id
@ -296,13 +295,13 @@ const canRemoveUserFromGroup = rule({
} catch (error) {
throw new Error(error)
} finally {
session.close()
await session.close()
}
})
const canCommentPost = rule({
cache: 'no_cache',
})(async (_parent, args, { user, driver }) => {
})(async (_parent, args, { user, driver }: Context) => {
if (!user?.id) return false
const { postId } = args
const userId = user.id
@ -330,13 +329,13 @@ const canCommentPost = rule({
} catch (error) {
throw new Error(error)
} finally {
session.close()
await session.close()
}
})
const isAuthor = rule({
cache: 'no_cache',
})(async (_parent, args, { user, driver }) => {
})(async (_parent, args, { user, driver }: Context) => {
if (!user) return false
const { id: resourceId } = args
const session = driver.session()
@ -354,14 +353,14 @@ const isAuthor = rule({
const [author] = await authorReadTxPromise
return !!author
} finally {
session.close()
await session.close()
}
})
const isDeletingOwnAccount = rule({
cache: 'no_cache',
})(async (_parent, args, context, _info) => {
return context.user.id === args.id
})(async (_parent, args, context: Context, _info) => {
return context.user?.id === args.id
})
const noEmailFilter = rule({
@ -370,10 +369,12 @@ const noEmailFilter = rule({
return !('email' in args)
})
const publicRegistration = rule()(() => CONFIG.PUBLIC_REGISTRATION)
const publicRegistration = rule()(
async (_parent, _args, context: Context) => context.config.PUBLIC_REGISTRATION,
)
const inviteRegistration = rule()(async (_parent, args, context: Context) => {
if (!CONFIG.INVITE_REGISTRATION) return false
if (!context.config.INVITE_REGISTRATION) return false
const { inviteCode } = args
return validateInviteCode(context, inviteCode)
})

View File

@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import type { Context } from '@src/server'
import type { Context } from '@src/context'
import uniqueSlug from './slugify/uniqueSlug'

View File

@ -2,16 +2,14 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { ApolloServer } from 'apollo-server-express'
import { createTestClient } from 'apollo-server-testing'
import databaseContext from '@context/database'
import Factory, { cleanDatabase } from '@db/factories'
import { createGroupMutation } from '@graphql/queries/createGroupMutation'
import { createPostMutation } from '@graphql/queries/createPostMutation'
import { signupVerificationMutation } from '@graphql/queries/signupVerificationMutation'
import { updateGroupMutation } from '@graphql/queries/updateGroupMutation'
import createServer, { getContext } from '@src/server'
import type { ApolloTestSetup } from '@root/test/helpers'
import { createApolloTestSetup } from '@root/test/helpers'
import type { Context } from '@src/context'
let variables
const categoryIds = ['cat9']
@ -19,23 +17,18 @@ const categoryIds = ['cat9']
const descriptionAdditional100 =
' 123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789-123456789'
const database = databaseContext()
let server: ApolloServer
let authenticatedUser
let mutate
let authenticatedUser: Context['user']
const context = () => ({ authenticatedUser })
let mutate: ApolloTestSetup['mutate']
let database: ApolloTestSetup['database']
let server: ApolloTestSetup['server']
beforeAll(async () => {
await cleanDatabase()
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/require-await
const contextUser = async (_req) => authenticatedUser
const context = getContext({ user: contextUser, database })
server = createServer({ context }).server
const createTestClientResult = createTestClient(server)
mutate = createTestClientResult.mutate
const testServer = createApolloTestSetup({ context })
mutate = testServer.mutate
database = testServer.database
server = testServer.server
})
afterAll(() => {

View File

@ -1,21 +1,25 @@
/* eslint-disable @typescript-eslint/await-thenable */
/* 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-return */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { createTestClient } from 'apollo-server-testing'
import gql from 'graphql-tag'
import Factory, { cleanDatabase } from '@db/factories'
import { getNeode, getDriver } from '@db/neo4j'
import createServer from '@src/server'
const neode = getNeode()
const driver = getDriver()
import type { ApolloTestSetup } from '@root/test/helpers'
import { createApolloTestSetup } from '@root/test/helpers'
import type { Context } from '@src/context'
const categoryIds = ['cat9']
let query, graphqlQuery, authenticatedUser, user, moderator, troll
let graphqlQuery
let moderator
let user
let troll
let authenticatedUser: Context['user']
const context = () => ({ authenticatedUser })
let query: ApolloTestSetup['query']
let database: ApolloTestSetup['database']
let server: ApolloTestSetup['server']
const action = () => {
return query({ query: graphqlQuery })
@ -23,8 +27,15 @@ const action = () => {
beforeAll(async () => {
await cleanDatabase()
const apolloSetup = createApolloTestSetup({ context })
query = apolloSetup.query
database = apolloSetup.database
server = apolloSetup.server
// For performance reasons we do this only once
const avatar = await Factory.build('image', {
url: 'http://localhost/some/offensive/avatar.jpg',
})
const users = await Promise.all([
Factory.build('user', { id: 'u1', role: 'user' }),
Factory.build(
@ -47,12 +58,10 @@ beforeAll(async () => {
about: 'This self description is very offensive',
},
{
avatar: Factory.build('image', {
url: 'http://localhost/some/offensive/avatar.jpg',
}),
avatar,
},
),
neode.create('Category', {
database.neode.create('Category', {
id: 'cat9',
name: 'Democracy & Politics',
icon: 'university',
@ -136,18 +145,6 @@ beforeAll(async () => {
),
])
const { server } = createServer({
context: () => {
return {
driver,
neode,
user: authenticatedUser,
}
},
})
const client = createTestClient(server)
query = client.query
const trollingPost = resources[1]
const trollingComment = resources[2]
@ -202,7 +199,9 @@ beforeAll(async () => {
afterAll(async () => {
await cleanDatabase()
await driver.close()
void server.stop()
void database.driver.close()
database.neode.close()
})
describe('softDeleteMiddleware', () => {
@ -222,7 +221,7 @@ describe('softDeleteMiddleware', () => {
}
`
const { data } = await action()
subject = data.User[0].following[0].comments[0]
subject = (data as any).User[0].following[0].comments[0]
}
const beforeUser = async () => {
graphqlQuery = gql`
@ -240,7 +239,7 @@ describe('softDeleteMiddleware', () => {
}
`
const { data } = await action()
subject = data.User[0].following[0]
subject = (data as any).User[0].following[0]
}
const beforePost = async () => {
graphqlQuery = gql`
@ -261,7 +260,7 @@ describe('softDeleteMiddleware', () => {
}
`
const { data } = await action()
subject = data.User[0].following[0].contributions[0]
subject = (data as any).User[0].following[0].contributions[0]
}
describe('as moderator', () => {
@ -276,10 +275,11 @@ describe('softDeleteMiddleware', () => {
it('displays slug', () => expect(subject.slug).toEqual('offensive-name'))
it('displays about', () =>
expect(subject.about).toEqual('This self description is very offensive'))
it('displays avatar', () =>
it('displays avatar', async () => {
expect(subject.avatar).toEqual({
url: expect.stringMatching('http://localhost/some/offensive/avatar.jpg'),
}))
})
})
})
describe('Post', () => {
@ -369,10 +369,9 @@ describe('softDeleteMiddleware', () => {
it('shows disabled but hides deleted posts', async () => {
const expected = [{ title: 'Disabled post' }, { title: 'Publicly visible post' }]
const {
data: { Post },
} = await action()
await expect(Post).toEqual(expect.arrayContaining(expected))
const { data } = await action()
const { Post } = data as any
expect(Post).toEqual(expect.arrayContaining(expected))
})
})
@ -400,12 +399,11 @@ describe('softDeleteMiddleware', () => {
{ content: 'Enabled comment on public post' },
{ content: 'UNAVAILABLE' },
]
const { data } = await action()
const {
data: {
Post: [{ comments }],
},
} = await action()
await expect(comments).toEqual(expect.arrayContaining(expected))
Post: [{ comments }],
} = data as any
expect(comments).toEqual(expect.arrayContaining(expected))
})
})
@ -419,12 +417,11 @@ describe('softDeleteMiddleware', () => {
{ content: 'Enabled comment on public post' },
{ content: 'Disabled comment' },
]
const { data } = await action()
const {
data: {
Post: [{ comments }],
},
} = await action()
await expect(comments).toEqual(expect.arrayContaining(expected))
Post: [{ comments }],
} = data as any
expect(comments).toEqual(expect.arrayContaining(expected))
})
})
})

View File

@ -1,33 +1,29 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-return */
import { ApolloServer } from 'apollo-server-express'
import { createTestClient } from 'apollo-server-testing'
import databaseContext from '@context/database'
import { ApolloServer } from 'apollo-server-express'
import Factory, { cleanDatabase } from '@db/factories'
import type { ApolloTestSetup } from '@root/test/helpers'
import { createApolloTestSetup } from '@root/test/helpers'
import type { Context } from '@src/context'
import { loginMutation } from '@src/graphql/queries/loginMutation'
import ocelotLogger from '@src/logger'
import { loggerPlugin } from '@src/plugins/apolloLogger'
import createServer, { getContext } from '@src/server'
const database = databaseContext()
let server: ApolloServer
let mutate, authenticatedUser
const authenticatedUser: Context['user'] = null
let mutate: ApolloTestSetup['mutate']
let database: ApolloTestSetup['database']
const context = () => ({ authenticatedUser })
beforeAll(async () => {
await cleanDatabase()
// eslint-disable-next-line @typescript-eslint/require-await
const contextUser = async (_req) => authenticatedUser
const context = getContext({ user: contextUser, database })
server = createServer({ context, plugins: [loggerPlugin] }).server
const createTestClientResult = createTestClient(server)
mutate = createTestClientResult.mutate
const apolloSetup = createApolloTestSetup({ context, plugins: [loggerPlugin] })
mutate = apolloSetup.mutate
database = apolloSetup.database
server = apolloSetup.server
})
afterAll(async () => {
@ -61,7 +57,7 @@ describe('apollo logger', () => {
})
describe('login mutation', () => {
it('logs the request and response', async () => {
it('logs the request and response, masking password and token', async () => {
await mutate({
mutation: loginMutation,
variables: {
@ -81,7 +77,7 @@ describe('apollo logger', () => {
}),
)
expect(loggerSpy).toBeCalledWith('Apollo Response', expect.any(String), expect.any(String))
expect(loggerSpy).toBeCalledWith('Apollo Response', expect.any(String), '{"login":"token"}')
expect(consoleSpy).toBeCalledTimes(2)
})

View File

@ -30,7 +30,14 @@ export const loggerPlugin = {
ocelotLogger.error(...logResponse, JSON.stringify(requestContext.errors))
return
}
logResponse.push(JSON.stringify(requestContext.response.data))
if (requestContext.response.data.login) {
// mask the token
const data = cloneDeep(requestContext.response.data)
data.login = 'token'
logResponse.push(JSON.stringify(data))
} else {
logResponse.push(JSON.stringify(requestContext.response.data))
}
ocelotLogger.debug(...logResponse)
}
},

View File

@ -1,8 +1,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* 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-explicit-any */
/* eslint-disable import/no-named-as-default-member */
import http from 'node:http'
@ -13,86 +12,31 @@ import express from 'express'
import { graphqlUploadExpress } from 'graphql-upload'
import helmet from 'helmet'
import databaseContext from '@context/database'
import pubsubContext from '@context/pubsub'
import CONFIG from './config'
import { context, getContext } from './context'
import schema from './graphql/schema'
import decode from './jwt/decode'
import ocelotLogger from './logger'
// eslint-disable-next-line import/no-cycle
import middleware from './middleware'
import type OcelotLogger from './logger'
import type { ApolloServerExpressConfig } from 'apollo-server-express'
const serverDatabase = databaseContext()
const serverPubsub = pubsubContext()
const databaseUser = async (req) => decode(serverDatabase.driver, req.headers.authorization)
export const getContext =
(
{
database = serverDatabase,
pubsub = serverPubsub,
user = databaseUser,
logger = ocelotLogger,
}: {
database?: ReturnType<typeof databaseContext>
pubsub?: ReturnType<typeof pubsubContext>
user?: (any) => Promise<any>
logger?: typeof OcelotLogger
} = {
database: serverDatabase,
pubsub: serverPubsub,
user: databaseUser,
logger: ocelotLogger,
},
) =>
async (req) => {
const u = await user(req)
return {
database,
driver: database.driver,
neode: database.neode,
pubsub,
logger,
user: u,
req,
cypherParams: {
currentUserId: u ? u.id : null,
},
}
}
export type Context = Awaited<ReturnType<ReturnType<typeof getContext>>>
export const context = async (options) => {
const { connection, req } = options
if (connection) {
return connection.context
} else {
return getContext()(req)
}
}
const createServer = (options?) => {
const defaults = {
const createServer = (options?: ApolloServerExpressConfig) => {
const defaults: ApolloServerExpressConfig = {
context,
schema: middleware(schema),
subscriptions: {
onConnect: (connectionParams) => getContext()(connectionParams),
onConnect: (connectionParams) =>
getContext()(connectionParams as { headers: { authorization?: string } }),
},
debug: !!CONFIG.DEBUG,
uploads: false,
tracing: !!CONFIG.DEBUG,
formatError: (error) => {
// console.log(error.originalError)
if (error.message === 'ERROR_VALIDATION') {
return new Error(error.originalError.details.map((d) => d.message))
return new Error((error.originalError as any).details.map((d) => d.message))
}
return error
},
plugins: [],
}
const server = new ApolloServer(Object.assign(defaults, options))

View File

@ -0,0 +1,70 @@
import { S3Client, DeleteObjectCommand, ObjectCannedACL } from '@aws-sdk/client-s3'
import { Upload } from '@aws-sdk/lib-storage'
import type { S3Config } from '@config/index'
import { FileUploadCallback, FileDeleteCallback } from './types'
export const s3Service = (config: S3Config, prefix: string) => {
const { AWS_BUCKET: Bucket } = config
const { AWS_ENDPOINT, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, S3_PUBLIC_GATEWAY } = config
const s3 = new S3Client({
credentials: {
accessKeyId: AWS_ACCESS_KEY_ID,
secretAccessKey: AWS_SECRET_ACCESS_KEY,
},
endpoint: AWS_ENDPOINT,
forcePathStyle: true,
})
const uploadFile: FileUploadCallback = async ({ createReadStream, uniqueFilename, mimetype }) => {
const s3Location = prefix.length > 0 ? `${prefix}/${uniqueFilename}` : uniqueFilename
const params = {
Bucket,
Key: s3Location,
ACL: ObjectCannedACL.public_read,
ContentType: mimetype,
Body: createReadStream(),
}
const command = new Upload({ client: s3, params })
const data = await command.done()
let { Location: location } = data
if (!location) {
throw new Error('File upload did not return `Location`')
}
if (!location.startsWith('https://') && !location.startsWith('http://')) {
// Ensure the location has a protocol. Hetzner sometimes does not return a protocol in the location.
location = `https://${location}`
}
if (!S3_PUBLIC_GATEWAY) {
return location
}
const publicLocation = new URL(S3_PUBLIC_GATEWAY)
publicLocation.pathname = new URL(location).pathname
return publicLocation.href
}
const deleteFile: FileDeleteCallback = async (url) => {
let { pathname } = new URL(url, 'http://example.org') // dummy domain to avoid invalid URL error
pathname = pathname.substring(1) // remove first character '/'
const prefix = `${Bucket}/`
if (pathname.startsWith(prefix)) {
pathname = pathname.slice(prefix.length)
}
const params = {
Bucket,
Key: pathname,
}
await s3.send(new DeleteObjectCommand(params))
}
return {
uploadFile,
deleteFile,
}
}

View File

@ -0,0 +1,7 @@
import type { FileUpload } from 'graphql-upload'
export type FileDeleteCallback = (url: string) => Promise<void>
export type FileUploadCallback = (
upload: Pick<FileUpload, 'createReadStream' | 'mimetype'> & { uniqueFilename: string },
) => Promise<string>

101
backend/test/helpers.ts Normal file
View File

@ -0,0 +1,101 @@
import { createTestClient } from 'apollo-server-testing'
import databaseContext from '@context/database'
import type CONFIG from '@src/config'
import type { Context } from '@src/context'
import { getContext } from '@src/context'
import createServer from '@src/server'
import type { ApolloServerExpressConfig } from 'apollo-server-express'
export const TEST_CONFIG = {
NODE_ENV: 'test',
DEBUG: undefined,
TEST: true,
PRODUCTION: false,
PRODUCTION_DB_CLEAN_ALLOW: false,
DISABLED_MIDDLEWARES: [],
SEND_MAIL: false,
CLIENT_URI: 'http://webapp:3000',
GRAPHQL_URI: 'http://localhost:4000',
JWT_EXPIRES: '2y',
MAPBOX_TOKEN:
'pk.eyJ1IjoiYnVzZmFrdG9yIiwiYSI6ImNraDNiM3JxcDBhaWQydG1uczhpZWtpOW4ifQ.7TNRTO-o9aK1Y6MyW_Nd4g',
JWT_SECRET: 'JWT_SECRET',
PRIVATE_KEY_PASSPHRASE: 'PRIVATE_KEY_PASSPHRASE',
NEO4J_URI: 'bolt://localhost:7687',
NEO4J_USERNAME: 'neo4j',
NEO4J_PASSWORD: 'neo4j',
SENTRY_DSN_BACKEND: undefined,
COMMIT: undefined,
REDIS_DOMAIN: undefined,
REDIS_PORT: undefined,
REDIS_PASSWORD: undefined,
AWS_ACCESS_KEY_ID: 'minio',
AWS_SECRET_ACCESS_KEY: '12341234',
AWS_ENDPOINT: 'http:/minio:9000',
AWS_REGION: 'local',
AWS_BUCKET: 'ocelot',
S3_PUBLIC_GATEWAY: undefined,
EMAIL_DEFAULT_SENDER: '',
SUPPORT_EMAIL: '',
SUPPORT_URL: '',
APPLICATION_NAME: '',
ORGANIZATION_URL: '',
PUBLIC_REGISTRATION: false,
INVITE_REGISTRATION: true,
INVITE_CODES_PERSONAL_PER_USER: 7,
INVITE_CODES_GROUP_PER_USER: 7,
CATEGORIES_ACTIVE: false,
MAX_PINNED_POSTS: 1,
LANGUAGE_DEFAULT: 'en',
LOG_LEVEL: 'DEBUG',
} as const satisfies typeof CONFIG
interface OverwritableContextParams {
authenticatedUser?: Context['user']
config?: Partial<typeof CONFIG>
pubsub?: Context['pubsub']
}
interface CreateTestServerOptions {
context: () => OverwritableContextParams | Promise<OverwritableContextParams>
plugins?: ApolloServerExpressConfig['plugins']
}
export const createApolloTestSetup = (opts?: CreateTestServerOptions) => {
const defaultOpts: CreateTestServerOptions = { context: () => ({ authenticatedUser: null }) }
const { context: testContext, plugins } = opts ?? defaultOpts
const database = databaseContext()
const context = async (req: { headers: { authorization?: string } }) => {
const { authenticatedUser, config = {}, pubsub } = await testContext()
return getContext({
authenticatedUser,
database,
pubsub,
config: { ...TEST_CONFIG, ...config },
})(req)
}
const server = createServer({
context,
plugins,
}).server
const { mutate, query } = createTestClient(server)
return {
server,
query,
mutate,
database,
}
}
export type ApolloTestSetup = ReturnType<typeof createApolloTestSetup>

File diff suppressed because it is too large Load Diff

View File

@ -3,8 +3,8 @@ import { defineStep } from '@badeball/cypress-cucumber-preprocessor'
defineStep('I should see my comment', () => {
cy.get('article.comment-card p')
.should('contain', 'Ocelot.social rocks')
.get('.user-teaser span.slug')
.should('contain', '@peter-pan') // specific enough
.get('.user-teaser span.name')
.should('contain', 'Peter Pan') // specific enough
.get('.profile-avatar img')
.should('have.attr', 'src')
.and('contain', 'https://') // some url

View File

@ -1,5 +1,6 @@
import { defineStep } from '@badeball/cypress-cucumber-preprocessor'
import encode from '../../../../backend/build/src/jwt/encode'
import CONFIG from '../../../../backend/build/src/config/index'
import { encode } from '../../../../backend/build/src/jwt/encode'
defineStep('I am logged in as {string}', slug => {
cy.neode()
@ -13,6 +14,6 @@ defineStep('I am logged in as {string}', slug => {
})
})
.then(user => {
cy.setCookie('ocelot-social-token', encode(user))
cy.setCookie('ocelot-social-token', encode({ config: CONFIG })(user))
})
})

View File

@ -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.10.1"
appVersion: "3.11.0"

View File

@ -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.10.1"
appVersion: "3.11.0"

View File

@ -32,7 +32,6 @@ services:
target: development
environment:
- NODE_ENV="development"
- DEBUG=true
- SMTP_PORT=1025
- SMTP_HOST=mailserver
- AWS_ACCESS_KEY_ID=minio
@ -41,7 +40,6 @@ services:
- AWS_REGION=local
- AWS_BUCKET=ocelot
- S3_PUBLIC_GATEWAY=http:/localhost:9000
- DEBUG=neo4j-graphql-js
volumes:
- ./backend:/app

View File

@ -26,6 +26,7 @@ services:
- AWS_ENDPOINT=http:/minio:9000
- AWS_REGION=local
- AWS_BUCKET=ocelot
- DEBUG=
volumes:
- ./coverage:/app/coverage
@ -35,7 +36,10 @@ services:
neo4j:
# name the image so that it cannot be found in a DockerHub repository, otherwise it will not be built locally from the 'dockerfile' but pulled from there
image: ghcr.io/ocelot-social-community/ocelot-social/neo4j-community:test
image: ghcr.io/ocelot-social-community/ocelot-social/neo4j:community
build:
context: ./neo4j
target: community
#environment:
# - NEO4J_dbms_connector_bolt_enabled=true
# - NEO4J_dbms_connector_bolt_tls__level=OPTIONAL

View File

@ -59,7 +59,6 @@ services:
# - PORT="4000"
- NODE_ENV="production"
# Application only envs
- DEBUG=false
- NEO4J_URI=bolt://neo4j:7687
- GRAPHQL_URI=http://backend:4000
- CLIENT_URI=http://webapp:3000
@ -75,7 +74,7 @@ services:
- 3001:80
neo4j:
image: ghcr.io/ocelot-social-community/ocelot-social/neo4j
image: ghcr.io/ocelot-social-community/ocelot-social/neo4j:community
build:
context: ./neo4j
# community edition 👆🏼, because we have no enterprise licence 👇🏼 at the moment

View File

@ -1,12 +1,12 @@
{
"name": "ocelot-social-frontend",
"version": "3.10.0",
"version": "3.11.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ocelot-social-frontend",
"version": "3.10.0",
"version": "3.11.0",
"license": "Apache-2.0",
"dependencies": {
"@intlify/unplugin-vue-i18n": "^2.0.0",

View File

@ -1,6 +1,6 @@
{
"name": "ocelot-social-frontend",
"version": "3.10.1",
"version": "3.11.0",
"description": "ocelot.social new Frontend (in development and not fully implemented) by IT4C Boilerplate for frontends",
"main": "build/index.js",
"type": "module",

508
package-lock.json generated
View File

@ -1,29 +1,28 @@
{
"name": "ocelot-social",
"version": "3.10.1",
"version": "3.11.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "ocelot-social",
"version": "3.10.1",
"version": "3.11.0",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.27.1",
"@babel/preset-env": "^7.27.1",
"@babel/register": "^7.27.1",
"@badeball/cypress-cucumber-preprocessor": "^22.1.0",
"@badeball/cypress-cucumber-preprocessor": "^22.2.0",
"@cucumber/cucumber": "11.3.0",
"@cypress/browserify-preprocessor": "^3.0.2",
"@faker-js/faker": "9.8.0",
"@faker-js/faker": "9.9.0",
"auto-changelog": "^2.5.0",
"bcryptjs": "^3.0.2",
"cross-env": "^7.0.3",
"cypress": "^14.5.0",
"cypress": "^14.5.1",
"cypress-network-idle": "^1.15.0",
"date-fns": "^3.6.0",
"dotenv": "^16.5.0",
"expect": "^29.6.4",
"dotenv": "^17.2.0",
"graphql-request": "^2.0.0",
"import": "^0.0.6",
"jsonwebtoken": "^9.0.2",
@ -1825,9 +1824,9 @@
}
},
"node_modules/@badeball/cypress-cucumber-preprocessor": {
"version": "22.1.0",
"resolved": "https://registry.npmjs.org/@badeball/cypress-cucumber-preprocessor/-/cypress-cucumber-preprocessor-22.1.0.tgz",
"integrity": "sha512-6hZdi7krImbUKp8lVqwMcW5cGxMmyZahPpkWL5D3wfTYRRhVgeOx9Y7liyCqKcOZkyCoufJEx8iGtTNYKiR3SQ==",
"version": "22.2.0",
"resolved": "https://registry.npmjs.org/@badeball/cypress-cucumber-preprocessor/-/cypress-cucumber-preprocessor-22.2.0.tgz",
"integrity": "sha512-od4a1k5VeptXSr1AI2gi5iHMmrKQhwXeLouiuv1yF6Th/FoDstaukdPy6lvwqAuEgb4wx0H1eFVi5/rlSD+1pA==",
"dev": true,
"funding": [
{
@ -1836,7 +1835,6 @@
}
],
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@cucumber/ci-environment": "^10.0.1",
"@cucumber/cucumber": "^11.0.0",
@ -2848,9 +2846,9 @@
}
},
"node_modules/@faker-js/faker": {
"version": "9.8.0",
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.8.0.tgz",
"integrity": "sha512-U9wpuSrJC93jZBxx/Qq2wPjCuYISBueyVUGK7qqdmj7r/nxaxwW8AQDCLeRO7wZnjj94sh3p246cAYjUKuqgfg==",
"version": "9.9.0",
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.9.0.tgz",
"integrity": "sha512-OEl393iCOoo/z8bMezRlJu+GlRGlsKbUAN7jKB6LhnKoqKve5DXRpalbItIIcwnCjs1k/FOPjFzcA6Qn+H+YbA==",
"dev": true,
"funding": [
{
@ -2858,7 +2856,6 @@
"url": "https://opencollective.com/fakerjs"
}
],
"license": "MIT",
"engines": {
"node": ">=18.0.0",
"npm": ">=9.0.0"
@ -2984,90 +2981,6 @@
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"node_modules/@jest/expect-utils": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz",
"integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==",
"dev": true,
"dependencies": {
"jest-get-type": "^29.6.3"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/@jest/schemas": {
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz",
"integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==",
"dev": true,
"dependencies": {
"@sinclair/typebox": "^0.27.8"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/@jest/types": {
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz",
"integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==",
"dev": true,
"dependencies": {
"@jest/schemas": "^29.6.3",
"@types/istanbul-lib-coverage": "^2.0.0",
"@types/istanbul-reports": "^3.0.0",
"@types/node": "*",
"@types/yargs": "^17.0.8",
"chalk": "^4.0.0"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/@jest/types/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/@jest/types/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/@jest/types/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
@ -4249,12 +4162,6 @@
"integrity": "sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==",
"optional": true
},
"node_modules/@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
"integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==",
"dev": true
},
"node_modules/@sindresorhus/merge-streams": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz",
@ -4322,30 +4229,6 @@
"@types/unist": "*"
}
},
"node_modules/@types/istanbul-lib-coverage": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
"integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
"dev": true
},
"node_modules/@types/istanbul-lib-report": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz",
"integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==",
"dev": true,
"dependencies": {
"@types/istanbul-lib-coverage": "*"
}
},
"node_modules/@types/istanbul-reports": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz",
"integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==",
"dev": true,
"dependencies": {
"@types/istanbul-lib-report": "*"
}
},
"node_modules/@types/jsonfile": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz",
@ -4411,7 +4294,7 @@
"version": "18.18.9",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.9.tgz",
"integrity": "sha512-0f5klcuImLnG4Qreu9hPj/rEfFq6YRc5n2mAjSsH+ec/mJL+3voBH0+8T7o8RpFjH7ovc+TRsL/c7OYIQsPTfQ==",
"devOptional": true,
"optional": true,
"dependencies": {
"undici-types": "~5.26.4"
}
@ -4444,12 +4327,6 @@
"integrity": "sha512-m04Om5Gz6kbjUwAQ7XJJQ30OdEFsSmAVsvn4NYwcTRyMVpKKa1aPuESw1n2CxS5fYkOQv3nHgDKeNa8e76fUkw==",
"dev": true
},
"node_modules/@types/stack-utils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz",
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
"dev": true
},
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
@ -4474,21 +4351,6 @@
"integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==",
"optional": true
},
"node_modules/@types/yargs": {
"version": "17.0.31",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.31.tgz",
"integrity": "sha512-bocYSx4DI8TmdlvxqGpVNXOgCNR1Jj0gNPhhAY+iz1rgKDAaYrAYdFYnhDV1IFuiuVc9HkOwyDcFxaTElF3/wg==",
"dev": true,
"dependencies": {
"@types/yargs-parser": "*"
}
},
"node_modules/@types/yargs-parser": {
"version": "21.0.3",
"resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz",
"integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==",
"dev": true
},
"node_modules/@types/yauzl": {
"version": "2.10.3",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
@ -7237,21 +7099,6 @@
"fsevents": "~2.3.2"
}
},
"node_modules/ci-info": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz",
"integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/sibiraj-s"
}
],
"engines": {
"node": ">=8"
}
},
"node_modules/cipher-base": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
@ -7825,12 +7672,11 @@
"optional": true
},
"node_modules/cypress": {
"version": "14.5.0",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-14.5.0.tgz",
"integrity": "sha512-1HOnKvWep0LkWuFwPeWkZ0TDl7ivi2/Mz+WNU4dfkeLJaFndS3Ow6TXT7YjuTqLFI2peJKzPKljVUFdymI2K5g==",
"version": "14.5.1",
"resolved": "https://registry.npmjs.org/cypress/-/cypress-14.5.1.tgz",
"integrity": "sha512-vYBeZKW3UAtxwv5mFuSlOBCYhyO0H86TeDKRJ7TgARyHiREIaiDjeHtqjzrXRFrdz9KnNavqlm+z+hklC7v8XQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@cypress/request": "^3.0.8",
"@cypress/xvfb": "^1.2.4",
@ -8446,15 +8292,6 @@
"node": ">=0.3.1"
}
},
"node_modules/diff-sequences": {
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz",
"integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==",
"dev": true,
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/diffie-hellman": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
@ -8556,9 +8393,9 @@
}
},
"node_modules/dotenv": {
"version": "16.5.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
"integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==",
"version": "17.2.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.0.tgz",
"integrity": "sha512-Q4sgBT60gzd0BB0lSyYD3xM4YxrXA9y4uBDof1JNYGzOXrQdQ6yX+7XIAqoFOGQFOTK1D3Hts5OllpxMDZFONQ==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
@ -9021,22 +8858,6 @@
"node": ">=4"
}
},
"node_modules/expect": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz",
"integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==",
"dev": true,
"dependencies": {
"@jest/expect-utils": "^29.7.0",
"jest-get-type": "^29.6.3",
"jest-matcher-utils": "^29.7.0",
"jest-message-util": "^29.7.0",
"jest-util": "^29.7.0"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@ -10921,254 +10742,6 @@
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/jest-diff": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz",
"integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==",
"dev": true,
"dependencies": {
"chalk": "^4.0.0",
"diff-sequences": "^29.6.3",
"jest-get-type": "^29.6.3",
"pretty-format": "^29.7.0"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/jest-diff/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/jest-diff/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/jest-diff/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/jest-get-type": {
"version": "29.6.3",
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz",
"integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==",
"dev": true,
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/jest-matcher-utils": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz",
"integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==",
"dev": true,
"dependencies": {
"chalk": "^4.0.0",
"jest-diff": "^29.7.0",
"jest-get-type": "^29.6.3",
"pretty-format": "^29.7.0"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/jest-matcher-utils/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/jest-matcher-utils/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/jest-matcher-utils/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/jest-message-util": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz",
"integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==",
"dev": true,
"dependencies": {
"@babel/code-frame": "^7.12.13",
"@jest/types": "^29.6.3",
"@types/stack-utils": "^2.0.0",
"chalk": "^4.0.0",
"graceful-fs": "^4.2.9",
"micromatch": "^4.0.4",
"pretty-format": "^29.7.0",
"slash": "^3.0.0",
"stack-utils": "^2.0.3"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/jest-message-util/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/jest-message-util/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/jest-message-util/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/jest-util": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz",
"integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==",
"dev": true,
"dependencies": {
"@jest/types": "^29.6.3",
"@types/node": "*",
"chalk": "^4.0.0",
"ci-info": "^3.2.0",
"graceful-fs": "^4.2.9",
"picomatch": "^2.2.3"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/jest-util/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/jest-util/node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/jest-util/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -13590,20 +13163,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pretty-format": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz",
"integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==",
"dev": true,
"dependencies": {
"@jest/schemas": "^29.6.3",
"ansi-styles": "^5.0.0",
"react-is": "^18.0.0"
},
"engines": {
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
"node_modules/pretty-ms": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.1.0.tgz",
@ -13949,12 +13508,6 @@
"safe-buffer": "^5.1.0"
}
},
"node_modules/react-is": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
"dev": true
},
"node_modules/read-only-stream": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz",
@ -15123,27 +14676,6 @@
"node": ">=0.10.0"
}
},
"node_modules/stack-utils": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
"integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==",
"dev": true,
"dependencies": {
"escape-string-regexp": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/stack-utils/node_modules/escape-string-regexp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
"integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/stackframe": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz",
@ -15854,7 +15386,7 @@
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"devOptional": true
"optional": true
},
"node_modules/unicode-canonical-property-names-ecmascript": {
"version": "2.0.1",

View File

@ -1,6 +1,6 @@
{
"name": "ocelot-social",
"version": "3.10.1",
"version": "3.11.0",
"description": "Free and open source software program code available to run social networks.",
"author": "ocelot.social Community",
"license": "MIT",
@ -36,18 +36,17 @@
"@babel/core": "^7.27.1",
"@babel/preset-env": "^7.27.1",
"@babel/register": "^7.27.1",
"@badeball/cypress-cucumber-preprocessor": "^22.1.0",
"@badeball/cypress-cucumber-preprocessor": "^22.2.0",
"@cucumber/cucumber": "11.3.0",
"@cypress/browserify-preprocessor": "^3.0.2",
"@faker-js/faker": "9.8.0",
"@faker-js/faker": "9.9.0",
"auto-changelog": "^2.5.0",
"bcryptjs": "^3.0.2",
"cross-env": "^7.0.3",
"cypress": "^14.5.0",
"cypress": "^14.5.1",
"cypress-network-idle": "^1.15.0",
"date-fns": "^3.6.0",
"dotenv": "^16.5.0",
"expect": "^29.6.4",
"dotenv": "^17.2.0",
"graphql-request": "^2.0.0",
"import": "^0.0.6",
"jsonwebtoken": "^9.0.2",

View File

@ -56,6 +56,7 @@ describe('UserTeaser', () => {
withLinkToProfile = true,
onTouchScreen = false,
withAvatar = true,
showSlug = true,
user = userTilda,
withPopoverEnabled = true,
}) => {
@ -76,6 +77,7 @@ describe('UserTeaser', () => {
user,
linkToProfile: withLinkToProfile,
showAvatar: withAvatar,
showSlug: showSlug,
showPopover: withPopoverEnabled,
},
stubs: {

View File

@ -101,7 +101,7 @@ storiesOf('UserTeaser', module)
data: () => ({
user,
}),
template: '<user-teaser :user="user" />',
template: '<user-teaser :user="user" :show-slug="true" />',
}))
.add('with date', () => ({
components: { UserTeaser },
@ -109,7 +109,7 @@ storiesOf('UserTeaser', module)
data: () => ({
user,
}),
template: '<user-teaser :user="user" :date-time="new Date()" />',
template: '<user-teaser :user="user" :show-slug="true" :date-time="new Date()" />',
}))
.add('has edited something', () => ({
components: { UserTeaser },
@ -118,7 +118,7 @@ storiesOf('UserTeaser', module)
user,
}),
template: `
<user-teaser :user="user" :date-time="new Date()">
<user-teaser :user="user" :show-slug="true" :date-time="new Date()">
<template #dateTime>
- HEY! I'm edited
</template>
@ -131,7 +131,7 @@ storiesOf('UserTeaser', module)
data: () => ({
user: null,
}),
template: '<user-teaser :user="user" :date-time="new Date()" />',
template: '<user-teaser :user="user" :show-slug="true" :date-time="new Date()" />',
}))
.add('with group and date', () => ({
components: { UserTeaser },
@ -140,7 +140,8 @@ storiesOf('UserTeaser', module)
user,
group,
}),
template: '<user-teaser :user="user" :group="group" :date-time="new Date()" />',
template:
'<user-teaser :user="user" :show-slug="true" :group="group" :date-time="new Date()" />',
}))
.add('with group and date wide', () => ({
components: { UserTeaser },
@ -149,5 +150,6 @@ storiesOf('UserTeaser', module)
user,
group,
}),
template: '<user-teaser :user="user" :group="group" wide :date-time="new Date()" />',
template:
'<user-teaser :user="user" :show-slug="true" :group="group" wide :date-time="new Date()" />',
}))

View File

@ -13,6 +13,7 @@
:group="group"
:wide="wide"
:show-avatar="showAvatar"
:show-slug="showSlug"
:date-time="dateTime"
:show-popover="showPopover"
:injected-text="injectedText"
@ -42,6 +43,7 @@ export default {
group: { type: Object, default: null },
wide: { type: Boolean, default: false },
showAvatar: { type: Boolean, default: true },
showSlug: { type: Boolean, default: false },
dateTime: { type: [Date, String], default: null },
showPopover: { type: Boolean, default: true },
injectedText: { type: String, default: null },

View File

@ -23,7 +23,7 @@
@open-menu="loadPopover(openMenu)"
@close-menu="closeMenu(false)"
>
<span class="slug">{{ userSlug }}</span>
<span v-if="showSlug" class="slug">{{ userSlug }}</span>
<span class="name">{{ userName }}</span>
</user-teaser-helper>
<span v-if="wide">&nbsp;</span>
@ -83,6 +83,7 @@ export default {
group: { type: Object, default: null },
wide: { type: Boolean, default: false },
showAvatar: { type: Boolean, default: true },
showSlug: { type: Boolean, default: false },
dateTime: { type: [Date, String], default: null },
showPopover: { type: Boolean, default: true },
injectedText: { type: String, default: null },

View File

@ -62,7 +62,7 @@ describe('FiledReportsTable.vue', () => {
describe('FiledReport', () => {
it('renders the reporting user', () => {
const userSlug = wrapper.find('[data-test="filing-user"]')
expect(userSlug.text()).toContain('@community-moderator')
expect(userSlug.text()).toContain('Community moderator')
})
it('renders the reported date', () => {

View File

@ -109,7 +109,7 @@ describe('ReportRow', () => {
it('renders the moderator who reviewed the resource', () => {
const username = wrapper.find('[data-test="report-reviewer"]')
expect(username.text()).toContain('@moderator')
expect(username.text()).toContain('Moderator')
})
})
})
@ -132,7 +132,7 @@ describe('ReportRow', () => {
it('renders the author', () => {
const userSlug = wrapper.find('[data-test="report-author"]')
expect(userSlug.text()).toContain('@louie')
expect(userSlug.text()).toContain('Louie')
})
})
@ -154,7 +154,7 @@ describe('ReportRow', () => {
it('renders the author', () => {
const username = wrapper.find('[data-test="report-author"]')
expect(username.text()).toContain('@dagobert')
expect(username.text()).toContain('Dagobert')
})
})
@ -171,7 +171,7 @@ describe('ReportRow', () => {
it('renders a link to the user profile', () => {
const userLink = wrapper.find('[data-test="report-content"]')
expect(userLink.text()).toContain('@abusive-user')
expect(userLink.text()).toContain('Abusive user')
})
})
})

View File

@ -87,8 +87,8 @@ describe('SearchableInput.vue', () => {
it("pushes to user's profile", async () => {
select.element.value = 'Bob'
select.trigger('input')
const users = wrapper.findAll('.slug')
const bob = users.filter((item) => item.text().match(/@bob-der-baumeister/))
const users = wrapper.findAll('.name')
const bob = users.filter((item) => item.text().match(/Bob der Baumeister/))
bob.trigger('click')
await Vue.nextTick()
expect(mocks.$router.push).toHaveBeenCalledWith({

View File

@ -1,6 +1,6 @@
{
"name": "@ocelot-social/maintenance",
"version": "3.10.1",
"version": "3.11.0",
"description": "Maintenance page for ocelot.social",
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
"author": "ocelot.social Community",

View File

@ -1,6 +1,6 @@
{
"name": "ocelot-social-webapp",
"version": "3.10.1",
"version": "3.11.0",
"description": "ocelot.social Frontend",
"repository": "https://github.com/Ocelot-Social-Community/Ocelot-Social",
"author": "ocelot.social Community",
@ -71,7 +71,7 @@
"@babel/core": "^7.25.8",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/preset-env": "^7.25.8",
"@faker-js/faker": "9.8.0",
"@faker-js/faker": "9.9.0",
"@storybook/addon-a11y": "^8.0.8",
"@storybook/addon-actions": "^5.3.21",
"@storybook/addon-notes": "^5.3.18",

View File

@ -561,11 +561,7 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a close
<nuxt-link-stub
to="[object Object]"
>
<span
class="slug"
>
@peter-lustig
</span>
<!---->
<span
class="name"
@ -646,11 +642,7 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a close
<nuxt-link-stub
to="[object Object]"
>
<span
class="slug"
>
@jenny-rostock
</span>
<!---->
<span
class="name"
@ -731,11 +723,7 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a close
<nuxt-link-stub
to="[object Object]"
>
<span
class="slug"
>
@bob-der-baumeister
</span>
<!---->
<span
class="name"
@ -816,11 +804,7 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a close
<nuxt-link-stub
to="[object Object]"
>
<span
class="slug"
>
@huey
</span>
<!---->
<span
class="name"
@ -2452,11 +2436,7 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a close
<nuxt-link-stub
to="[object Object]"
>
<span
class="slug"
>
@peter-lustig
</span>
<!---->
<span
class="name"
@ -2537,11 +2517,7 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a close
<nuxt-link-stub
to="[object Object]"
>
<span
class="slug"
>
@jenny-rostock
</span>
<!---->
<span
class="name"
@ -2622,11 +2598,7 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a close
<nuxt-link-stub
to="[object Object]"
>
<span
class="slug"
>
@bob-der-baumeister
</span>
<!---->
<span
class="name"
@ -2707,11 +2679,7 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a close
<nuxt-link-stub
to="[object Object]"
>
<span
class="slug"
>
@huey
</span>
<!---->
<span
class="name"
@ -3446,11 +3414,7 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a curre
<nuxt-link-stub
to="[object Object]"
>
<span
class="slug"
>
@peter-lustig
</span>
<!---->
<span
class="name"
@ -3531,11 +3495,7 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a curre
<nuxt-link-stub
to="[object Object]"
>
<span
class="slug"
>
@jenny-rostock
</span>
<!---->
<span
class="name"
@ -3616,11 +3576,7 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a curre
<nuxt-link-stub
to="[object Object]"
>
<span
class="slug"
>
@bob-der-baumeister
</span>
<!---->
<span
class="name"
@ -3701,11 +3657,7 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a curre
<nuxt-link-stub
to="[object Object]"
>
<span
class="slug"
>
@huey
</span>
<!---->
<span
class="name"
@ -4285,11 +4237,7 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a curre
<nuxt-link-stub
to="[object Object]"
>
<span
class="slug"
>
@peter-lustig
</span>
<!---->
<span
class="name"
@ -4370,11 +4318,7 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a curre
<nuxt-link-stub
to="[object Object]"
>
<span
class="slug"
>
@jenny-rostock
</span>
<!---->
<span
class="name"
@ -4455,11 +4399,7 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a curre
<nuxt-link-stub
to="[object Object]"
>
<span
class="slug"
>
@bob-der-baumeister
</span>
<!---->
<span
class="name"
@ -4540,11 +4480,7 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a curre
<nuxt-link-stub
to="[object Object]"
>
<span
class="slug"
>
@huey
</span>
<!---->
<span
class="name"
@ -5122,11 +5058,7 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a curre
<nuxt-link-stub
to="[object Object]"
>
<span
class="slug"
>
@peter-lustig
</span>
<!---->
<span
class="name"
@ -5207,11 +5139,7 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a curre
<nuxt-link-stub
to="[object Object]"
>
<span
class="slug"
>
@jenny-rostock
</span>
<!---->
<span
class="name"
@ -5292,11 +5220,7 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a curre
<nuxt-link-stub
to="[object Object]"
>
<span
class="slug"
>
@bob-der-baumeister
</span>
<!---->
<span
class="name"
@ -5377,11 +5301,7 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a curre
<nuxt-link-stub
to="[object Object]"
>
<span
class="slug"
>
@huey
</span>
<!---->
<span
class="name"
@ -6028,11 +5948,7 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a curre
<nuxt-link-stub
to="[object Object]"
>
<span
class="slug"
>
@peter-lustig
</span>
<!---->
<span
class="name"
@ -6113,11 +6029,7 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a curre
<nuxt-link-stub
to="[object Object]"
>
<span
class="slug"
>
@jenny-rostock
</span>
<!---->
<span
class="name"
@ -6198,11 +6110,7 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a curre
<nuxt-link-stub
to="[object Object]"
>
<span
class="slug"
>
@bob-der-baumeister
</span>
<!---->
<span
class="name"
@ -6283,11 +6191,7 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a curre
<nuxt-link-stub
to="[object Object]"
>
<span
class="slug"
>
@huey
</span>
<!---->
<span
class="name"
@ -7069,11 +6973,7 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a hidde
<nuxt-link-stub
to="[object Object]"
>
<span
class="slug"
>
@peter-lustig
</span>
<!---->
<span
class="name"
@ -7154,11 +7054,7 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a hidde
<nuxt-link-stub
to="[object Object]"
>
<span
class="slug"
>
@jenny-rostock
</span>
<!---->
<span
class="name"
@ -7239,11 +7135,7 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a hidde
<nuxt-link-stub
to="[object Object]"
>
<span
class="slug"
>
@bob-der-baumeister
</span>
<!---->
<span
class="name"
@ -7324,11 +7216,7 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a hidde
<nuxt-link-stub
to="[object Object]"
>
<span
class="slug"
>
@huey
</span>
<!---->
<span
class="name"
@ -8063,11 +7951,7 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a hidde
<nuxt-link-stub
to="[object Object]"
>
<span
class="slug"
>
@peter-lustig
</span>
<!---->
<span
class="name"
@ -8148,11 +8032,7 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a hidde
<nuxt-link-stub
to="[object Object]"
>
<span
class="slug"
>
@jenny-rostock
</span>
<!---->
<span
class="name"
@ -8233,11 +8113,7 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a hidde
<nuxt-link-stub
to="[object Object]"
>
<span
class="slug"
>
@bob-der-baumeister
</span>
<!---->
<span
class="name"
@ -8318,11 +8194,7 @@ exports[`GroupProfileSlug given a puplic group "yoga-practice" given a hidde
<nuxt-link-stub
to="[object Object]"
>
<span
class="slug"
>
@huey
</span>
<!---->
<span
class="name"

View File

@ -2526,10 +2526,10 @@
minimatch "^3.0.4"
strip-json-comments "^3.1.1"
"@faker-js/faker@9.8.0":
version "9.8.0"
resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-9.8.0.tgz#3344284028d1c9dc98dee2479f82939310370d88"
integrity sha512-U9wpuSrJC93jZBxx/Qq2wPjCuYISBueyVUGK7qqdmj7r/nxaxwW8AQDCLeRO7wZnjj94sh3p246cAYjUKuqgfg==
"@faker-js/faker@9.9.0":
version "9.9.0"
resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-9.9.0.tgz#3ad015fbbaaae7af3149555e0f22b4b30134c69d"
integrity sha512-OEl393iCOoo/z8bMezRlJu+GlRGlsKbUAN7jKB6LhnKoqKve5DXRpalbItIIcwnCjs1k/FOPjFzcA6Qn+H+YbA==
"@human-connection/styleguide@0.5.22":
version "0.5.22"

3224
yarn.lock

File diff suppressed because it is too large Load Diff