diff --git a/backend/package.json b/backend/package.json index 5e3462f17..fcd05edaa 100644 --- a/backend/package.json +++ b/backend/package.json @@ -98,6 +98,8 @@ "@types/jest": "^29.5.14", "@types/lodash": "^4.17.19", "@types/node": "^24.0.10", + "@types/jsonwebtoken": "~8.5.1", + "@types/request": "^2.48.12", "@types/slug": "^5.0.9", "@types/uuid": "~9.0.1", "@typescript-eslint/eslint-plugin": "^5.62.0", diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index b8ccc382a..f6ea44fa6 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -38,6 +38,21 @@ const required = { PRIVATE_KEY_PASSPHRASE: env.PRIVATE_KEY_PASSPHRASE, } +// https://stackoverflow.com/a/53050575 +type NoUndefinedField = { [P in keyof T]-?: NoUndefinedField> } + +function assertRequiredConfig( + conf: typeof required, +): asserts conf is NoUndefinedField { + Object.entries(conf).forEach(([key, value]) => { + if (!value) { + throw new Error(`ERROR: "${key}" env variable is missing.`) + } + }) +} + +assertRequiredConfig(required) + const server = { CLIENT_URI: env.CLIENT_URI ?? 'http://localhost:3000', GRAPHQL_URI: env.GRAPHQL_URI ?? 'http://localhost:4000', @@ -147,15 +162,7 @@ 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, @@ -167,4 +174,7 @@ export default { ...language, } +export type Config = typeof CONFIG +export default CONFIG + export { nodemailerTransportOptions } diff --git a/backend/src/context/index.ts b/backend/src/context/index.ts new file mode 100644 index 000000000..51b14f74f --- /dev/null +++ b/backend/src/context/index.ts @@ -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 + pubsub?: ReturnType + 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>> diff --git a/backend/src/db/factories.ts b/backend/src/db/factories.ts index ebdc4a868..d1b93b7e5 100644 --- a/backend/src/db/factories.ts +++ b/backend/src/db/factories.ts @@ -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' diff --git a/backend/src/db/seed.ts b/backend/src/db/seed.ts index 558425ec9..d9db21b54 100644 --- a/backend/src/db/seed.ts +++ b/backend/src/db/seed.ts @@ -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) diff --git a/backend/src/graphql/resolvers/attachments/attachments.spec.ts b/backend/src/graphql/resolvers/attachments/attachments.spec.ts index 68321423f..79b78ad9c 100644 --- a/backend/src/graphql/resolvers/attachments/attachments.spec.ts +++ b/backend/src/graphql/resolvers/attachments/attachments.spec.ts @@ -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 { ApolloTestSetup } from '@root/test/helpers' +import { createApolloTestSetup } from '@root/test/helpers' import type { S3Configured } from '@src/config' -import createServer, { getContext } from '@src/server' import { attachments } from './attachments' @@ -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: ` diff --git a/backend/src/graphql/resolvers/badges.spec.ts b/backend/src/graphql/resolvers/badges.spec.ts index 6ebed7990..6303ce35b 100644 --- a/backend/src/graphql/resolvers/badges.spec.ts +++ b/backend/src/graphql/resolvers/badges.spec.ts @@ -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") { diff --git a/backend/src/graphql/resolvers/badges.ts b/backend/src/graphql/resolvers/badges.ts index 700e18d89..9c147ab3c 100644 --- a/backend/src/graphql/resolvers/badges.ts +++ b/backend/src/graphql/resolvers/badges.ts @@ -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 diff --git a/backend/src/graphql/resolvers/comments.spec.ts b/backend/src/graphql/resolvers/comments.spec.ts index 9681abe9a..08d18c67c 100644 --- a/backend/src/graphql/resolvers/comments.spec.ts +++ b/backend/src/graphql/resolvers/comments.spec.ts @@ -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!') }) }) diff --git a/backend/src/graphql/resolvers/filter-posts.spec.ts b/backend/src/graphql/resolvers/filter-posts.spec.ts index c29b98365..9c1d0a304 100644 --- a/backend/src/graphql/resolvers/filter-posts.spec.ts +++ b/backend/src/graphql/resolvers/filter-posts.spec.ts @@ -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({ diff --git a/backend/src/graphql/resolvers/groups.spec.ts b/backend/src/graphql/resolvers/groups.spec.ts index 333bc03c1..744503a9d 100644 --- a/backend/src/graphql/resolvers/groups.spec.ts +++ b/backend/src/graphql/resolvers/groups.spec.ts @@ -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', () => { '0', }, }) - 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', () => { '0', }, }) - 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!') }) }) }) diff --git a/backend/src/graphql/resolvers/groups.ts b/backend/src/graphql/resolvers/groups.ts index 9efa8e6af..1213c15a9 100644 --- a/backend/src/graphql/resolvers/groups.ts +++ b/backend/src/graphql/resolvers/groups.ts @@ -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() diff --git a/backend/src/graphql/resolvers/helpers/createPasswordReset.ts b/backend/src/graphql/resolvers/helpers/createPasswordReset.ts index 0727c5d4e..5add52762 100644 --- a/backend/src/graphql/resolvers/helpers/createPasswordReset.ts +++ b/backend/src/graphql/resolvers/helpers/createPasswordReset.ts @@ -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() } } diff --git a/backend/src/graphql/resolvers/images/images.ts b/backend/src/graphql/resolvers/images/images.ts index f4f7bdccd..dae73bd25 100644 --- a/backend/src/graphql/resolvers/images/images.ts +++ b/backend/src/graphql/resolvers/images/images.ts @@ -1,4 +1,5 @@ -import CONFIG, { isS3configured } from '@config/index' +import { isS3configured } from '@config/index' +import type { Context } from '@src/context' import type { FileDeleteCallback, FileUploadCallback } from '@src/uploads/types' import { images as imagesLocal } from './imagesLocal' @@ -51,4 +52,5 @@ export interface Images { ) => Promise } -export const images = isS3configured(CONFIG) ? imagesS3(CONFIG) : imagesLocal +export const images = (config: Context['config']) => + isS3configured(config) ? imagesS3(config) : imagesLocal diff --git a/backend/src/graphql/resolvers/inviteCodes.spec.ts b/backend/src/graphql/resolvers/inviteCodes.spec.ts index a2f43ecb6..fba67147f 100644 --- a/backend/src/graphql/resolvers/inviteCodes.spec.ts +++ b/backend/src/graphql/resolvers/inviteCodes.spec.ts @@ -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' }, diff --git a/backend/src/graphql/resolvers/inviteCodes.ts b/backend/src/graphql/resolvers/inviteCodes.ts index b17d32dd8..217df869a 100644 --- a/backend/src/graphql/resolvers/inviteCodes.ts +++ b/backend/src/graphql/resolvers/inviteCodes.ts @@ -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', ) diff --git a/backend/src/graphql/resolvers/locations.ts b/backend/src/graphql/resolvers/locations.ts index fc69fab94..0222e0baf 100644 --- a/backend/src/graphql/resolvers/locations.ts +++ b/backend/src/graphql/resolvers/locations.ts @@ -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) } diff --git a/backend/src/graphql/resolvers/messages.spec.ts b/backend/src/graphql/resolvers/messages.spec.ts index d51a29993..0630ee9e4 100644 --- a/backend/src/graphql/resolvers/messages.spec.ts +++ b/backend/src/graphql/resolvers/messages.spec.ts @@ -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 () => { diff --git a/backend/src/graphql/resolvers/notifications.spec.ts b/backend/src/graphql/resolvers/notifications.spec.ts index d6d22e459..cda75bbbc 100644 --- a/backend/src/graphql/resolvers/notifications.spec.ts +++ b/backend/src/graphql/resolvers/notifications.spec.ts @@ -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', diff --git a/backend/src/graphql/resolvers/observePosts.spec.ts b/backend/src/graphql/resolvers/observePosts.spec.ts index fd2786fc9..c4c31d8ea 100644 --- a/backend/src/graphql/resolvers/observePosts.spec.ts +++ b/backend/src/graphql/resolvers/observePosts.spec.ts @@ -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 () => { diff --git a/backend/src/graphql/resolvers/passwordReset.spec.ts b/backend/src/graphql/resolvers/passwordReset.spec.ts index 3bc4d53ba..e583fc5c5 100644 --- a/backend/src/graphql/resolvers/passwordReset.spec.ts +++ b/backend/src/graphql/resolvers/passwordReset.spec.ts @@ -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` diff --git a/backend/src/graphql/resolvers/passwordReset.ts b/backend/src/graphql/resolvers/passwordReset.ts index ac437a555..fb602f276 100644 --- a/backend/src/graphql/resolvers/passwordReset.ts +++ b/backend/src/graphql/resolvers/passwordReset.ts @@ -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) diff --git a/backend/src/graphql/resolvers/posts.spec.ts b/backend/src/graphql/resolvers/posts.spec.ts index 7f679d2b9..e72581119 100644 --- a/backend/src/graphql/resolvers/posts.spec.ts +++ b/backend/src/graphql/resolvers/posts.spec.ts @@ -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 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!') }) }) diff --git a/backend/src/graphql/resolvers/posts.ts b/backend/src/graphql/resolvers/posts.ts index cef255634..af807b041 100644 --- a/backend/src/graphql/resolvers/posts.ts +++ b/backend/src/graphql/resolvers/posts.ts @@ -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] = ( diff --git a/backend/src/graphql/resolvers/postsInGroups.spec.ts b/backend/src/graphql/resolvers/postsInGroups.spec.ts index d50451468..9222d4e75 100644 --- a/backend/src/graphql/resolvers/postsInGroups.spec.ts +++ b/backend/src/graphql/resolvers/postsInGroups.spec.ts @@ -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([ diff --git a/backend/src/graphql/resolvers/registration.spec.ts b/backend/src/graphql/resolvers/registration.spec.ts index fe8dc40e0..2f8fbfd4d 100644 --- a/backend/src/graphql/resolvers/registration.spec.ts +++ b/backend/src/graphql/resolvers/registration.spec.ts @@ -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 = {} 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!' }], }) diff --git a/backend/src/graphql/resolvers/registration.ts b/backend/src/graphql/resolvers/registration.ts index db24ed7d0..949aedf6c 100644 --- a/backend/src/graphql/resolvers/registration.ts +++ b/backend/src/graphql/resolvers/registration.ts @@ -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') diff --git a/backend/src/graphql/resolvers/statistics.spec.ts b/backend/src/graphql/resolvers/statistics.spec.ts index f67552f39..dba7bcf57 100644 --- a/backend/src/graphql/resolvers/statistics.spec.ts +++ b/backend/src/graphql/resolvers/statistics.spec.ts @@ -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 () => { diff --git a/backend/src/graphql/resolvers/statistics.ts b/backend/src/graphql/resolvers/statistics.ts index 00ead1eb2..d07a9ba62 100644 --- a/backend/src/graphql/resolvers/statistics.ts +++ b/backend/src/graphql/resolvers/statistics.ts @@ -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: { diff --git a/backend/src/graphql/resolvers/user_management.spec.ts b/backend/src/graphql/resolvers/user_management.spec.ts index 1029ab2b1..8712685a8 100644 --- a/backend/src/graphql/resolvers/user_management.spec.ts +++ b/backend/src/graphql/resolvers/user_management.spec.ts @@ -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', () => { diff --git a/backend/src/graphql/resolvers/user_management.ts b/backend/src/graphql/resolvers/user_management.ts index 140a8d53c..294206527 100644 --- a/backend/src/graphql/resolvers/user_management.ts +++ b/backend/src/graphql/resolvers/user_management.ts @@ -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('encryptedPassword') @@ -73,7 +79,7 @@ export default { updatedAt: new Date().toISOString(), }) - return encode(await currentUser.toJson()) + return encode(context)(await currentUser.toJson()) }, }, } diff --git a/backend/src/graphql/resolvers/users.spec.ts b/backend/src/graphql/resolvers/users.spec.ts index 2576c1f15..e870c7cd6 100644 --- a/backend/src/graphql/resolvers/users.spec.ts +++ b/backend/src/graphql/resolvers/users.spec.ts @@ -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( 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 () => { diff --git a/backend/src/graphql/resolvers/users.ts b/backend/src/graphql/resolvers/users.ts index 9418ef3e6..ff512416b 100644 --- a/backend/src/graphql/resolvers/users.ts +++ b/backend/src/graphql/resolvers/users.ts @@ -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) => { diff --git a/backend/src/graphql/resolvers/users/location.spec.ts b/backend/src/graphql/resolvers/users/location.spec.ts index 659c126dd..a8349c2e8 100644 --- a/backend/src/graphql/resolvers/users/location.spec.ts +++ b/backend/src/graphql/resolvers/users/location.spec.ts @@ -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 {.*}`, {}, ) diff --git a/backend/src/graphql/resolvers/users/location.ts b/backend/src/graphql/resolvers/users/location.ts index 4f72f2b9e..343503a70 100644 --- a/backend/src/graphql/resolvers/users/location.ts +++ b/backend/src/graphql/resolvers/users/location.ts @@ -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), }, diff --git a/backend/src/jwt/decode.spec.ts b/backend/src/jwt/decode.spec.ts index cbb220b5b..fa074aa82 100644 --- a/backend/src/jwt/decode.spec.ts +++ b/backend/src/jwt/decode.spec.ts @@ -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('User', { id: 'u3' }, undefined) await expect(user.toJson()).resolves.not.toHaveProperty('lastActiveAt') - await decode(driver, validAuthorizationHeader) + await decode(context)(validAuthorizationHeader) user = await neode.first('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('User', { id: 'u3' }, undefined) await expect(user.toJson()).resolves.toMatchObject({ // should be a different time by now ;) diff --git a/backend/src/jwt/decode.ts b/backend/src/jwt/decode.ts index 0a433d38f..93cc64275 100644 --- a/backend/src/jwt/decode.ts +++ b/backend/src/jwt/decode.ts @@ -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; 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(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() } -} diff --git a/backend/src/jwt/encode.spec.ts b/backend/src/jwt/encode.spec.ts index 8121118f3..9ef922598 100644 --- a/backend/src/jwt/encode.spec.ts +++ b/backend/src/jwt/encode.spec.ts @@ -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', diff --git a/backend/src/jwt/encode.ts b/backend/src/jwt/encode.ts index 742bf438b..e1d818eba 100644 --- a/backend/src/jwt/encode.ts +++ b/backend/src/jwt/encode.ts @@ -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 + }) => + (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 + } diff --git a/backend/src/middleware/categories.spec.ts b/backend/src/middleware/categories.spec.ts index 3afda82a6..ed82bdef6 100644 --- a/backend/src/middleware/categories.spec.ts +++ b/backend/src/middleware/categories.spec.ts @@ -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 +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 () => { diff --git a/backend/src/middleware/categories.ts b/backend/src/middleware/categories.ts index 759a3938f..7d9f2a71e 100644 --- a/backend/src/middleware/categories.ts +++ b/backend/src/middleware/categories.ts @@ -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 +const checkCategoriesActive = ( + resolve: Resolver, + root: unknown, + args: unknown, + context: Context, + resolveInfo: unknown, +) => { + if (context.config.CATEGORIES_ACTIVE) { return resolve(root, args, context, resolveInfo) } return [] diff --git a/backend/src/middleware/hashtags/hashtagsMiddleware.spec.ts b/backend/src/middleware/hashtags/hashtagsMiddleware.spec.ts index bc3b96594..42b9ccc05 100644 --- a/backend/src/middleware/hashtags/hashtagsMiddleware.spec.ts +++ b/backend/src/middleware/hashtags/hashtagsMiddleware.spec.ts @@ -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', diff --git a/backend/src/middleware/index.ts b/backend/src/middleware/index.ts index b4824ae0e..6b5ca7654 100644 --- a/backend/src/middleware/index.ts +++ b/backend/src/middleware/index.ts @@ -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' diff --git a/backend/src/middleware/languages/languages.spec.ts b/backend/src/middleware/languages/languages.spec.ts index 50e3a028f..2f0214446 100644 --- a/backend/src/middleware/languages/languages.spec.ts +++ b/backend/src/middleware/languages/languages.spec.ts @@ -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` diff --git a/backend/src/middleware/notifications/notificationsMiddleware.emails.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.emails.spec.ts index 27aeb8cf4..d7cf11d34 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.emails.spec.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.emails.spec.ts @@ -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 () => { diff --git a/backend/src/middleware/notifications/notificationsMiddleware.followed-users.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.followed-users.spec.ts index 3bb0d48e3..6bef70a39 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.followed-users.spec.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.followed-users.spec.ts @@ -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 () => { diff --git a/backend/src/middleware/notifications/notificationsMiddleware.mentions-in-groups.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.mentions-in-groups.spec.ts index 9eb26e57f..95833ad82 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.mentions-in-groups.spec.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.mentions-in-groups.spec.ts @@ -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 () => { diff --git a/backend/src/middleware/notifications/notificationsMiddleware.observing-posts.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.observing-posts.spec.ts index e8c25a16f..9b8e0e1e6 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.observing-posts.spec.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.observing-posts.spec.ts @@ -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 () => { diff --git a/backend/src/middleware/notifications/notificationsMiddleware.online-status.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.online-status.spec.ts index 1cbb6a2a1..d4659f918 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.online-status.spec.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.online-status.spec.ts @@ -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() }) diff --git a/backend/src/middleware/notifications/notificationsMiddleware.posts-in-groups.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.posts-in-groups.spec.ts index 9a7e830ef..fdece8b58 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.posts-in-groups.spec.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.posts-in-groups.spec.ts @@ -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 () => { diff --git a/backend/src/middleware/notifications/notificationsMiddleware.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.spec.ts index cf004ea52..a34233775 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.spec.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.spec.ts @@ -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: { diff --git a/backend/src/middleware/permissionsMiddleware.spec.ts b/backend/src/middleware/permissionsMiddleware.spec.ts index f7422f59f..cac4d45b5 100644 --- a/backend/src/middleware/permissionsMiddleware.spec.ts +++ b/backend/src/middleware/permissionsMiddleware.spec.ts @@ -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 +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', () => { diff --git a/backend/src/middleware/permissionsMiddleware.ts b/backend/src/middleware/permissionsMiddleware.ts index cc70fc00b..cd3112278 100644 --- a/backend/src/middleware/permissionsMiddleware.ts +++ b/backend/src/middleware/permissionsMiddleware.ts @@ -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) }) diff --git a/backend/src/middleware/sluggifyMiddleware.ts b/backend/src/middleware/sluggifyMiddleware.ts index fc38a5bfb..b940f4851 100644 --- a/backend/src/middleware/sluggifyMiddleware.ts +++ b/backend/src/middleware/sluggifyMiddleware.ts @@ -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' diff --git a/backend/src/middleware/slugifyMiddleware.spec.ts b/backend/src/middleware/slugifyMiddleware.spec.ts index f40c2064a..085badb15 100644 --- a/backend/src/middleware/slugifyMiddleware.spec.ts +++ b/backend/src/middleware/slugifyMiddleware.spec.ts @@ -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(() => { diff --git a/backend/src/plugins/apolloLogger.spec.ts b/backend/src/plugins/apolloLogger.spec.ts index 878eb2c2e..876eb2f51 100644 --- a/backend/src/plugins/apolloLogger.spec.ts +++ b/backend/src/plugins/apolloLogger.spec.ts @@ -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 () => { diff --git a/backend/src/server.ts b/backend/src/server.ts index b5f183e32..5826a746c 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -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 - pubsub?: ReturnType - user?: (any) => Promise - 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>> - -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)) diff --git a/backend/test/helpers.ts b/backend/test/helpers.ts new file mode 100644 index 000000000..75a20fc76 --- /dev/null +++ b/backend/test/helpers.ts @@ -0,0 +1,100 @@ +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: '', + AWS_SECRET_ACCESS_KEY: '', + AWS_ENDPOINT: '', + AWS_REGION: '', + AWS_BUCKET: '', + 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 + pubsub?: Context['pubsub'] +} +interface CreateTestServerOptions { + context: () => OverwritableContextParams | Promise + 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 diff --git a/backend/yarn.lock b/backend/yarn.lock index e76e18350..73c82312f 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -2361,6 +2361,11 @@ "@types/node" "*" "@types/responselike" "*" +"@types/caseless@*": + version "0.12.5" + resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.5.tgz#db9468cb1b1b5a925b8f34822f1669df0c5472f5" + integrity sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg== + "@types/connect@*": version "3.4.33" resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546" @@ -2504,6 +2509,13 @@ resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== +"@types/jsonwebtoken@~8.5.1": + version "8.5.9" + resolved "https://registry.yarnpkg.com/@types/jsonwebtoken/-/jsonwebtoken-8.5.9.tgz#2c064ecb0b3128d837d2764aa0b117b0ff6e4586" + integrity sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg== + dependencies: + "@types/node" "*" + "@types/keygrip@*": version "1.0.2" resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72" @@ -2598,6 +2610,16 @@ resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== +"@types/request@^2.48.12": + version "2.48.12" + resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.12.tgz#0f590f615a10f87da18e9790ac94c29ec4c5ef30" + integrity sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw== + dependencies: + "@types/caseless" "*" + "@types/node" "*" + "@types/tough-cookie" "*" + form-data "^2.5.0" + "@types/responselike@*", "@types/responselike@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" @@ -2628,6 +2650,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== +"@types/tough-cookie@*": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz#cb6e2a691b70cb177c6e3ae9c1d2e8b2ea8cd304" + integrity sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA== + "@types/uuid@^9.0.1", "@types/uuid@~9.0.1": version "9.0.8" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba" @@ -4946,6 +4973,16 @@ es-set-tostringtag@^2.0.3: has-tostringtag "^1.0.2" hasown "^2.0.1" +es-set-tostringtag@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" + integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== + dependencies: + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + es-shim-unscopables@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz#702e632193201e3edf8713635d083d378e510241" @@ -5712,6 +5749,17 @@ foreground-child@^3.1.0: cross-spawn "^7.0.6" signal-exit "^4.0.1" +form-data@^2.5.0: + version "2.5.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.3.tgz#f9bcf87418ce748513c0c3494bb48ec270c97acc" + integrity sha512-XHIrMD0NpDrNM/Ckf7XJiBbLl57KEhT3+i3yY+eWm+cqYZJQTZrKo8Y8AWKnuV5GT4scfuUGt9LzNoIx3dU1nQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + es-set-tostringtag "^2.1.0" + mime-types "^2.1.35" + safe-buffer "^5.2.1" + form-data@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682" @@ -5836,7 +5884,7 @@ get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: has-symbols "^1.0.3" hasown "^2.0.0" -get-intrinsic@^1.2.5, get-intrinsic@^1.3.0: +get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== @@ -8078,7 +8126,7 @@ mime-db@^1.54.0: resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5" integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ== -mime-types@^2.1.12, mime-types@~2.1.22, mime-types@~2.1.24, mime-types@~2.1.34: +mime-types@^2.1.12, mime-types@^2.1.35, mime-types@~2.1.22, mime-types@~2.1.24, mime-types@~2.1.34: version "2.1.35" resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== @@ -9481,7 +9529,7 @@ safe-array-concat@^1.1.2: has-symbols "^1.0.3" isarray "^2.0.5" -safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.2.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== diff --git a/cypress/support/step_definitions/common/I_am_logged_in_as_{string}.js b/cypress/support/step_definitions/common/I_am_logged_in_as_{string}.js index b8153190c..f0a62ee7c 100644 --- a/cypress/support/step_definitions/common/I_am_logged_in_as_{string}.js +++ b/cypress/support/step_definitions/common/I_am_logged_in_as_{string}.js @@ -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)) }) })