From 3c1c2d4dcbdaf09b4fb137185d0d01e474c20dd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Wed, 28 May 2025 18:16:09 +0800 Subject: [PATCH] refactor(backend): put config into context This is a side quest of #8558. The motivation is to be able to do dependency injection in the tests without overwriting global data. I saw the first merge conflict from #8551 and voila: It seems @Mogge could have used this already. refactor: follow @Mogge's review See: https://github.com/Ocelot-Social-Community/Ocelot-Social/pull/8603#pullrequestreview-2880714796 refactor: better test helper methods wip: continue refactoring wip: continue posts continue wip: continue groups continue registration registration continue messages continue observeposts continue categories continue posts in groups continue invite codes refactor: continue notificationsMiddleware continue statistics spec followed-users online-status mentions-in-groups posts-in-groups email spec finish all tests improve typescript missed one test remove one more reference of CONFIG eliminate one more global import of CONFIG fix language spec test fix two more test suites refactor: completely mock out 3rd part API request refactor test fixed user_management spec fixed more locatoin specs install types for jsonwebtoken one more fetchmock fixed one more suite fix one more spec yet another spec fix spec delete whitespaces remove beforeAll that the same as the default fix merge conflict fix e2e test refactor: use single callback function for `context` setup refactor: display logs from backend during CI Because why not? fix seeds fix login refactor: one unnecessary naming refactor: better editor support refactor: fail early Interestingly, I've had to destructure `context.user` in order to make typescript happy. Weird. refactor: undo changes to workflows - no effect We're running in `--detached` mode on CI, so I guess we won't be able to see the logs anyways. --- backend/package.json | 2 + backend/src/config/index.ts | 33 +- backend/src/context/fetch.ts | 19 + backend/src/context/index.ts | 61 ++ backend/src/db/seed.ts | 19 +- backend/src/graphql/resolvers/badges.spec.ts | 35 +- backend/src/graphql/resolvers/badges.ts | 12 +- .../src/graphql/resolvers/comments.spec.ts | 45 +- .../graphql/resolvers/filter-posts.spec.ts | 73 +- backend/src/graphql/resolvers/groups.spec.ts | 129 ++- backend/src/graphql/resolvers/groups.ts | 43 +- .../resolvers/helpers/createPasswordReset.ts | 13 +- .../src/graphql/resolvers/images/images.ts | 6 +- .../src/graphql/resolvers/inviteCodes.spec.ts | 38 +- backend/src/graphql/resolvers/inviteCodes.ts | 13 +- backend/src/graphql/resolvers/locations.ts | 9 +- .../src/graphql/resolvers/messages.spec.ts | 40 +- .../graphql/resolvers/notifications.spec.ts | 50 +- .../graphql/resolvers/observePosts.spec.ts | 37 +- .../graphql/resolvers/passwordReset.spec.ts | 40 +- .../src/graphql/resolvers/passwordReset.ts | 4 +- backend/src/graphql/resolvers/posts.spec.ts | 66 +- backend/src/graphql/resolvers/posts.ts | 63 +- .../graphql/resolvers/postsInGroups.spec.ts | 99 +-- .../graphql/resolvers/registration.spec.ts | 39 +- backend/src/graphql/resolvers/registration.ts | 4 +- .../src/graphql/resolvers/statistics.spec.ts | 27 +- backend/src/graphql/resolvers/statistics.ts | 2 +- .../graphql/resolvers/user_management.spec.ts | 78 +- .../src/graphql/resolvers/user_management.ts | 18 +- backend/src/graphql/resolvers/users.spec.ts | 72 +- backend/src/graphql/resolvers/users.ts | 24 +- .../graphql/resolvers/users/location.spec.ts | 54 +- .../src/graphql/resolvers/users/location.ts | 46 +- backend/src/jwt/decode.spec.ts | 27 +- backend/src/jwt/decode.ts | 78 +- backend/src/jwt/encode.spec.ts | 25 +- backend/src/jwt/encode.ts | 30 +- backend/src/middleware/categories.spec.ts | 40 +- backend/src/middleware/categories.ts | 20 +- .../hashtags/hashtagsMiddleware.spec.ts | 46 +- backend/src/middleware/index.ts | 1 - .../middleware/languages/languages.spec.ts | 35 +- .../notificationsMiddleware.emails.spec.ts | 40 +- ...ficationsMiddleware.followed-users.spec.ts | 39 +- ...tionsMiddleware.mentions-in-groups.spec.ts | 39 +- ...icationsMiddleware.observing-posts.spec.ts | 38 +- ...ificationsMiddleware.online-status.spec.ts | 32 +- ...icationsMiddleware.posts-in-groups.spec.ts | 38 +- .../notificationsMiddleware.spec.ts | 48 +- .../middleware/permissionsMiddleware.spec.ts | 62 +- .../src/middleware/permissionsMiddleware.ts | 69 +- backend/src/middleware/sluggifyMiddleware.ts | 2 +- .../src/middleware/slugifyMiddleware.spec.ts | 31 +- backend/src/server.ts | 61 +- backend/test/fetchMock/berlinDe.ts | 255 ++++++ backend/test/fetchMock/berlinEn.ts | 254 ++++++ backend/test/fetchMock/berlinGermany.ts | 742 ++++++++++++++++ backend/test/fetchMock/empty.ts | 7 + backend/test/fetchMock/hamburg.ts | 661 ++++++++++++++ backend/test/fetchMock/hamburgNY.ts | 813 ++++++++++++++++++ backend/test/fetchMock/index.ts | 11 + backend/test/fetchMock/leipzig.ts | 653 ++++++++++++++ backend/test/fetchMock/mapboxResponses.ts | 32 + backend/test/fetchMock/paris.ts | 615 +++++++++++++ backend/test/fetchMock/welzheim.ts | 686 +++++++++++++++ backend/test/helpers.ts | 100 +++ backend/yarn.lock | 42 +- .../common/I_am_logged_in_as_{string}.js | 5 +- 69 files changed, 5958 insertions(+), 1032 deletions(-) create mode 100644 backend/src/context/fetch.ts create mode 100644 backend/src/context/index.ts create mode 100644 backend/test/fetchMock/berlinDe.ts create mode 100644 backend/test/fetchMock/berlinEn.ts create mode 100644 backend/test/fetchMock/berlinGermany.ts create mode 100644 backend/test/fetchMock/empty.ts create mode 100644 backend/test/fetchMock/hamburg.ts create mode 100644 backend/test/fetchMock/hamburgNY.ts create mode 100644 backend/test/fetchMock/index.ts create mode 100644 backend/test/fetchMock/leipzig.ts create mode 100644 backend/test/fetchMock/mapboxResponses.ts create mode 100644 backend/test/fetchMock/paris.ts create mode 100644 backend/test/fetchMock/welzheim.ts create mode 100644 backend/test/helpers.ts diff --git a/backend/package.json b/backend/package.json index 34bd99ebf..762a1c0ab 100644 --- a/backend/package.json +++ b/backend/package.json @@ -96,8 +96,10 @@ "@faker-js/faker": "9.8.0", "@types/email-templates": "^10.0.4", "@types/jest": "^29.5.14", + "@types/jsonwebtoken": "~8.5.1", "@types/lodash": "^4.17.17", "@types/node": "^22.15.30", + "@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 0aee79626..d517b3e23 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -37,6 +37,26 @@ const required = { PRIVATE_KEY_PASSPHRASE: env.PRIVATE_KEY_PASSPHRASE, } +export type NonNullableCollection = T extends (infer U)[] + ? Exclude[] + : { + [K in keyof T as NonNullable extends never ? never : K]-?: Exclude< + T[K], + null | undefined + > + } +function assertRequiredConfig( + conf: typeof required, +): asserts conf is NonNullableCollection { + 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', @@ -146,15 +166,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, @@ -166,4 +178,7 @@ export default { ...language, } +export type Config = typeof CONFIG +export default CONFIG + export { nodemailerTransportOptions } diff --git a/backend/src/context/fetch.ts b/backend/src/context/fetch.ts new file mode 100644 index 000000000..697690213 --- /dev/null +++ b/backend/src/context/fetch.ts @@ -0,0 +1,19 @@ +import request from 'request' + +export const fetch = (url: string) => { + // eslint-disable-next-line promise/avoid-new + return new Promise((resolve, reject) => { + // eslint-disable-next-line promise/prefer-await-to-callbacks + request(url, function (error, response, body) { + if (error) { + reject(error) + } else { + const response = JSON.parse(body) // eslint-disable-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-argument + // console.log(url) + // console.log(JSON.stringify(response, null, 2)) + resolve(response) + } + }) + }) +} +export type Fetch = typeof fetch diff --git a/backend/src/context/index.ts b/backend/src/context/index.ts new file mode 100644 index 000000000..6e9611094 --- /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 type { Fetch } from '@context/fetch' +import { fetch as defaultFetch } from '@context/fetch' +import pubsubContext from '@context/pubsub' +import CONFIG from '@src/config' +import type { DecodedUser } from '@src/jwt/decode' +import { decode } from '@src/jwt/decode' + +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 + config: typeof CONFIG + fetch: Fetch + }) => + async (req: { headers: { authorization?: string } }) => { + const { + authenticatedUser = undefined, + database = serverDatabase, + pubsub = serverPubsub, + config = CONFIG, + fetch = defaultFetch, + } = 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, + user, + req, + cypherParams: { + currentUserId: user ? user.id : null, + }, + config, + fetch, + } + 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/seed.ts b/backend/src/db/seed.ts index e7f5b23c5..e2e2c11f4 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,7 +15,8 @@ import { createMessageMutation } from '@graphql/queries/createMessageMutation' 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 { fetch as actualFetch } from '@src/context/fetch' import Factory from './factories' import { getNeode, getDriver } from './neo4j' @@ -36,16 +36,13 @@ const languages = ['de', 'en', 'es', 'fr', 'it', 'pt', 'pl'] const neode = getNeode() try { - const { server } = createServer({ - context: () => { - return { - driver, - neode, - user: authenticatedUser, - } - }, + const context = () => ({ + authenticatedUser, + config: CONFIG, + fetch: actualFetch, }) - const { mutate } = createTestClient(server) + const apolloSetup = createApolloTestSetup({ context }) + const { mutate } = apolloSetup // locations const Hamburg = await Factory.build('location', { 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..44a1acd27 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 () => { @@ -98,14 +95,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 +154,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 +166,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 +205,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 +221,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 +246,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 +258,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..b9b97d9cc 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,13 @@ 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 { fetchMock } from '@root/test/fetchMock' +import type { ApolloTestSetup } from '@root/test/helpers' +import { createApolloTestSetup } from '@root/test/helpers' +import type { Context } from '@src/context' +// import CONFIG from '@src/config' +// import { fetch as fetchMock } from '@src/context/fetch' -let authenticatedUser let user let noMemberUser let pendingMemberUser @@ -27,18 +27,21 @@ let adminMemberUser let ownerMemberUser let secondOwnerMemberUser +let authenticatedUser: Context['user'] +const context = () => ({ authenticatedUser, config, fetch: fetchMock }) +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 +233,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 +277,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 +346,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 +385,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 +395,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 +406,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 +584,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 +810,7 @@ describe('in mode', () => { userId: 'current-user', }, }) - expect(errors![0]).toHaveProperty('message', 'Not Authorized!') + expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -1566,7 +1565,7 @@ describe('in mode', () => { roleInGroup: 'pending', }, }) - expect(errors![0]).toHaveProperty('message', 'Not Authorized!') + expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -1721,7 +1720,7 @@ describe('in mode', () => { mutation: changeGroupMemberRoleMutation(), variables, }) - expect(errors![0]).toHaveProperty('message', 'Not Authorized!') + expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!') }) }) }) @@ -1747,7 +1746,7 @@ describe('in mode', () => { mutation: changeGroupMemberRoleMutation(), variables, }) - expect(errors![0]).toHaveProperty('message', 'Not Authorized!') + expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -1796,7 +1795,7 @@ describe('in mode', () => { mutation: changeGroupMemberRoleMutation(), variables, }) - expect(errors![0]).toHaveProperty('message', 'Not Authorized!') + expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!') }) }) }) @@ -1819,7 +1818,7 @@ describe('in mode', () => { mutation: changeGroupMemberRoleMutation(), variables, }) - expect(errors![0]).toHaveProperty('message', 'Not Authorized!') + expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!') }) }) }) @@ -1842,7 +1841,7 @@ describe('in mode', () => { mutation: changeGroupMemberRoleMutation(), variables, }) - expect(errors![0]).toHaveProperty('message', 'Not Authorized!') + expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!') }) }) }) @@ -1900,7 +1899,7 @@ describe('in mode', () => { mutation: changeGroupMemberRoleMutation(), variables, }) - expect(errors![0]).toHaveProperty('message', 'Not Authorized!') + expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!') }) }) }) @@ -1923,7 +1922,7 @@ describe('in mode', () => { mutation: changeGroupMemberRoleMutation(), variables, }) - expect(errors![0]).toHaveProperty('message', 'Not Authorized!') + expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -1940,7 +1939,7 @@ describe('in mode', () => { mutation: changeGroupMemberRoleMutation(), variables, }) - expect(errors![0]).toHaveProperty('message', 'Not Authorized!') + expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!') }) }) }) @@ -1963,7 +1962,7 @@ describe('in mode', () => { mutation: changeGroupMemberRoleMutation(), variables, }) - expect(errors![0]).toHaveProperty('message', 'Not Authorized!') + expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -1980,7 +1979,7 @@ describe('in mode', () => { mutation: changeGroupMemberRoleMutation(), variables, }) - expect(errors![0]).toHaveProperty('message', 'Not Authorized!') + expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!') }) }) }) @@ -2003,7 +2002,7 @@ describe('in mode', () => { mutation: changeGroupMemberRoleMutation(), variables, }) - expect(errors![0]).toHaveProperty('message', 'Not Authorized!') + expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -2020,7 +2019,7 @@ describe('in mode', () => { mutation: changeGroupMemberRoleMutation(), variables, }) - expect(errors![0]).toHaveProperty('message', 'Not Authorized!') + expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!') }) }) }) @@ -2110,7 +2109,7 @@ describe('in mode', () => { mutation: changeGroupMemberRoleMutation(), variables, }) - expect(errors![0]).toHaveProperty('message', 'Not Authorized!') + expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -2127,7 +2126,7 @@ describe('in mode', () => { mutation: changeGroupMemberRoleMutation(), variables, }) - expect(errors![0]).toHaveProperty('message', 'Not Authorized!') + expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!') }) }) }) @@ -2150,7 +2149,7 @@ describe('in mode', () => { mutation: changeGroupMemberRoleMutation(), variables, }) - expect(errors![0]).toHaveProperty('message', 'Not Authorized!') + expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -2167,7 +2166,7 @@ describe('in mode', () => { mutation: changeGroupMemberRoleMutation(), variables, }) - expect(errors![0]).toHaveProperty('message', 'Not Authorized!') + expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!') }) }) }) @@ -2190,7 +2189,7 @@ describe('in mode', () => { mutation: changeGroupMemberRoleMutation(), variables, }) - expect(errors![0]).toHaveProperty('message', 'Not Authorized!') + expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -2207,7 +2206,7 @@ describe('in mode', () => { mutation: changeGroupMemberRoleMutation(), variables, }) - expect(errors![0]).toHaveProperty('message', 'Not Authorized!') + expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!') }) }) }) @@ -2297,7 +2296,7 @@ describe('in mode', () => { mutation: changeGroupMemberRoleMutation(), variables, }) - expect(errors![0]).toHaveProperty('message', 'Not Authorized!') + expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!') }) }) }) @@ -2320,7 +2319,7 @@ describe('in mode', () => { mutation: changeGroupMemberRoleMutation(), variables, }) - expect(errors![0]).toHaveProperty('message', 'Not Authorized!') + expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!') }) }) }) @@ -2343,7 +2342,7 @@ describe('in mode', () => { mutation: changeGroupMemberRoleMutation(), variables, }) - expect(errors![0]).toHaveProperty('message', 'Not Authorized!') + expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!') }) }) }) @@ -2407,7 +2406,7 @@ describe('in mode', () => { userId: 'current-user', }, }) - expect(errors![0]).toHaveProperty('message', 'Not Authorized!') + expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -2524,7 +2523,7 @@ describe('in mode', () => { userId: 'owner-member-user', }, }) - expect(errors![0]).toHaveProperty('message', 'Not Authorized!') + expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -2538,7 +2537,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 +2551,7 @@ describe('in mode', () => { userId: 'none-member-user', }, }) - expect(errors![0]).toHaveProperty('message', 'Not Authorized!') + expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -2566,7 +2565,7 @@ describe('in mode', () => { userId: 'usual-member-user', }, }) - expect(errors![0]).toHaveProperty('message', 'Not Authorized!') + expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -2580,7 +2579,7 @@ describe('in mode', () => { userId: 'admin-member-user', }, }) - expect(errors![0]).toHaveProperty('message', 'Not Authorized!') + expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!') }) }) }) @@ -2606,7 +2605,7 @@ describe('in mode', () => { slug: 'my-best-group', }, }) - expect(errors![0]).toHaveProperty('message', 'Not Authorized!') + expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -2859,17 +2858,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 +2901,7 @@ describe('in mode', () => { categoryIds: [], }, }) - expect(errors![0]).toHaveProperty('message', 'Too few categories!') + expect(errors?.[0]).toHaveProperty('message', 'Too few categories!') }) }) }) @@ -2920,7 +2915,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 +2935,7 @@ describe('in mode', () => { categoryIds: ['cat4', 'cat27'], }, }) - expect(errors![0]).toHaveProperty('message', 'Not Authorized!') + expect(errors?.[0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -2958,7 +2953,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 9e330bade..4024e59ed 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) { @@ -136,13 +138,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 ( @@ -155,8 +158,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 @@ -191,7 +197,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') @@ -202,13 +208,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!') } @@ -223,7 +230,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 @@ -234,13 +241,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}) @@ -260,14 +270,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') @@ -377,10 +389,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 }) @@ -406,6 +424,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 6c2fa8b3a..c3819edd4 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 { images as imagesLocal } from './imagesLocal' import { images as imagesS3 } from './imagesS3' @@ -55,4 +56,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 81799fdf1..49687754b 100644 --- a/backend/src/graphql/resolvers/messages.spec.ts +++ b/backend/src/graphql/resolvers/messages.spec.ts @@ -3,10 +3,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 databaseContext from '@context/database' import pubsubContext from '@context/pubsub' import Factory, { cleanDatabase } from '@db/factories' import { createMessageMutation } from '@graphql/queries/createMessageMutation' @@ -14,29 +11,28 @@ import { createRoomMutation } from '@graphql/queries/createRoomMutation' import { markMessagesAsSeen } from '@graphql/queries/markMessagesAsSeen' import { messageQuery } from '@graphql/queries/messageQuery' 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 }) afterAll(async () => { @@ -120,7 +116,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', () => { @@ -162,7 +158,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', @@ -293,7 +289,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', @@ -500,7 +496,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..6669f120d 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,33 @@ 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 { fetchMock } from '@root/test/fetchMock' +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, fetch: fetchMock }) +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 +50,7 @@ const categoryIds = ['cat9', 'cat4', 'cat15'] let variables beforeEach(async () => { + config = { ...defaultConfig } variables = {} user = await Factory.build( 'user', @@ -271,7 +271,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 +708,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 +733,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 +771,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 +1377,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 +1401,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 +1753,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 +2129,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 +2140,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 +2282,7 @@ describe('emotions', () => { variables, }) - expect(addPostEmotions.errors[0]).toHaveProperty('message', 'Not Authorized!') + expect(addPostEmotions.errors?.[0]).toHaveProperty('message', 'Not Authorized!') }) }) @@ -2401,7 +2403,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..262db2d61 100644 --- a/backend/src/graphql/resolvers/users.spec.ts +++ b/backend/src/graphql/resolvers/users.spec.ts @@ -3,29 +3,36 @@ /* 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 { fetchMock } from '@root/test/fetchMock' +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' +// import { fetch as fetchMock } from '@src/context/fetch' 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, fetch: fetchMock }) +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 +101,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 () => { @@ -254,7 +253,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 +325,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 +683,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 +780,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 +878,7 @@ describe('save category settings', () => { describe('not authenticated', () => { beforeEach(async () => { - authenticatedUser = undefined + authenticatedUser = null }) it('throws an error', async () => { @@ -921,7 +923,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 +965,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 +1002,7 @@ describe('updateOnlineStatus', () => { describe('not authenticated', () => { beforeEach(async () => { - authenticatedUser = undefined + authenticatedUser = null }) it('throws an error', async () => { @@ -1030,7 +1032,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 +1058,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 +1074,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 +1093,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 +1135,7 @@ describe('setTrophyBadgeSelected', () => { describe('not authenticated', () => { beforeEach(async () => { - authenticatedUser = undefined + authenticatedUser = null }) it('throws an error', async () => { @@ -1515,8 +1517,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..ccbc06cb8 100644 --- a/backend/src/graphql/resolvers/users/location.spec.ts +++ b/backend/src/graphql/resolvers/users/location.spec.ts @@ -1,16 +1,26 @@ /* 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 { fetchMock } from '@root/test/fetchMock' +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, + // config: { MAPBOX_TOKEN: CONFIG.MAPBOX_TOKEN }, + // fetch: defaultFetch, + fetch: fetchMock, +}) +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 +88,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 +116,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 +200,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 +215,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 dc515e70d..5d8925daa 100644 --- a/backend/src/graphql/resolvers/users/location.ts +++ b/backend/src/graphql/resolvers/users/location.ts @@ -6,24 +6,10 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-return */ /* eslint-disable @typescript-eslint/no-explicit-any */ -/* eslint-disable promise/avoid-new */ -/* eslint-disable promise/prefer-await-to-callbacks */ + import { UserInputError } from 'apollo-server' -import request from 'request' -import CONFIG from '@config/index' - -const fetch = (url) => { - return new Promise((resolve, reject) => { - request(url, function (error, response, body) { - if (error) { - reject(error) - } else { - resolve(JSON.parse(body)) - } - }) - }) -} +import type { Context } from '@src/context' const locales = ['en', 'de', 'fr', 'nl', 'it', 'es', 'pt', 'pl', 'ru'] @@ -73,19 +59,24 @@ 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 if (locationName !== null) { - const res: any = await fetch( - `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent( - locationName, - )}.json?access_token=${ - CONFIG.MAPBOX_TOKEN - }&types=region,place,country,address&language=${locales.join(',')}`, - ) + const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/${encodeURIComponent( + locationName, + )}.json?access_token=${ + context.config.MAPBOX_TOKEN + }&types=region,place,country,address&language=${locales.join(',')}` + const res: any = await context.fetch(url) if (!res?.features?.[0]) { throw new UserInputError('locationName is invalid') @@ -159,10 +150,9 @@ export const createOrUpdateLocations = async (nodeLabel, nodeId, locationName, s }) } -export const queryLocations = async ({ place, lang }) => { - 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}`, - ) +export const queryLocations = async ({ place, lang }, context: Context) => { + const url = `https://api.mapbox.com/geocoding/v5/mapbox.places/${place}.json?access_token=${context.config.MAPBOX_TOKEN}&types=region,place,country&language=${lang}` + const res: any = await context.fetch(url) // Return empty array if no location found or error occurred if (!res?.features) { return [] 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..5ee6bbb78 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 }) + 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 7b51cec25..f189aa2c0 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 0a45521f0..6a2d16385 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/server.ts b/backend/src/server.ts index f56b01f34..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,69 +12,28 @@ 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' -// eslint-disable-next-line import/no-cycle import middleware from './middleware' -const serverDatabase = databaseContext() -const serverPubsub = pubsubContext() +import type { ApolloServerExpressConfig } from 'apollo-server-express' -const databaseUser = async (req) => decode(serverDatabase.driver, req.headers.authorization) - -export const getContext = - ( - { - database = serverDatabase, - pubsub = serverPubsub, - user = databaseUser, - }: { - database?: ReturnType - pubsub?: ReturnType - user?: (any) => Promise - } = { database: serverDatabase, pubsub: serverPubsub, user: databaseUser }, - ) => - async (req) => { - const u = await user(req) - return { - database, - driver: database.driver, - neode: database.neode, - pubsub, - user: u, - req, - cypherParams: { - currentUserId: u ? u.id : null, - }, - } - } - -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 }, @@ -103,4 +61,3 @@ const createServer = (options?) => { } export default createServer -export type Context = Awaited>> diff --git a/backend/test/fetchMock/berlinDe.ts b/backend/test/fetchMock/berlinDe.ts new file mode 100644 index 000000000..4ba38488b --- /dev/null +++ b/backend/test/fetchMock/berlinDe.ts @@ -0,0 +1,255 @@ +export const berlinDe = { + type: 'FeatureCollection', + query: ['berlin'], + features: [ + { + id: 'place.115770', + type: 'Feature', + place_type: ['region', 'place'], + relevance: 1, + properties: { + mapbox_id: 'dXJuOm1ieHBsYzpBY1E2', + wikidata: 'Q64', + short_code: 'DE-BE', + }, + text_de: 'Berlin', + language_de: 'de', + place_name_de: 'Berlin, Deutschland', + text: 'Berlin', + language: 'de', + place_name: 'Berlin, Deutschland', + bbox: [13.08836, 52.338261, 13.760906, 52.675502], + center: [13.38886, 52.517037], + geometry: { + type: 'Point', + coordinates: [13.38886, 52.517037], + }, + context: [ + { + id: 'country.8762', + mapbox_id: 'dXJuOm1ieHBsYzpJam8', + wikidata: 'Q183', + short_code: 'de', + text_de: 'Deutschland', + language_de: 'de', + text: 'Deutschland', + language: 'de', + }, + ], + }, + { + id: 'place.25995500', + type: 'Feature', + place_type: ['place'], + relevance: 1, + properties: { + mapbox_id: 'dXJuOm1ieHBsYzpBWXlvN0E', + wikidata: 'Q614184', + }, + text_de: 'Berlin', + language_de: 'de', + place_name_de: 'Berlin, Maryland, Vereinigte Staaten', + text: 'Berlin', + language: 'de', + place_name: 'Berlin, Maryland, Vereinigte Staaten', + bbox: [-75.364989, 38.141747, -75.087625, 38.4078], + center: [-75.219004, 38.324728], + geometry: { + type: 'Point', + coordinates: [-75.219004, 38.324728], + }, + context: [ + { + id: 'district.25396972', + mapbox_id: 'dXJuOm1ieHBsYzpBWU9HN0E', + wikidata: 'Q494072', + text_de: 'Worcester County', + language_de: 'de', + text: 'Worcester County', + language: 'de', + }, + { + id: 'region.124140', + mapbox_id: 'dXJuOm1ieHBsYzpBZVRz', + wikidata: 'Q1391', + short_code: 'US-MD', + text_de: 'Maryland', + language_de: 'de', + text: 'Maryland', + language: 'de', + }, + { + id: 'country.8940', + mapbox_id: 'dXJuOm1ieHBsYzpJdXc', + wikidata: 'Q30', + short_code: 'us', + text_de: 'Vereinigte Staaten', + language_de: 'de', + text: 'Vereinigte Staaten', + language: 'de', + }, + ], + }, + { + id: 'place.25970924', + type: 'Feature', + place_type: ['place'], + relevance: 1, + properties: { + mapbox_id: 'dXJuOm1ieHBsYzpBWXhJN0E', + wikidata: 'Q1086827', + }, + text_de: 'Berlin', + language_de: 'de', + place_name_de: 'Berlin, New Jersey, Vereinigte Staaten', + text: 'Berlin', + language: 'de', + place_name: 'Berlin, New Jersey, Vereinigte Staaten', + bbox: [-74.974491, 39.70904, -74.878605, 39.809363], + center: [-74.929456, 39.791432], + geometry: { + type: 'Point', + coordinates: [-74.929456, 39.791432], + }, + context: [ + { + id: 'district.3016428', + mapbox_id: 'dXJuOm1ieHBsYzpMZ2Jz', + wikidata: 'Q497810', + text_de: 'Camden County', + language_de: 'de', + text: 'Camden County', + language: 'de', + }, + { + id: 'region.156908', + mapbox_id: 'dXJuOm1ieHBsYzpBbVRz', + wikidata: 'Q1408', + short_code: 'US-NJ', + text_de: 'New Jersey', + language_de: 'de', + text: 'New Jersey', + language: 'de', + }, + { + id: 'country.8940', + mapbox_id: 'dXJuOm1ieHBsYzpJdXc', + wikidata: 'Q30', + short_code: 'us', + text_de: 'Vereinigte Staaten', + language_de: 'de', + text: 'Vereinigte Staaten', + language: 'de', + }, + ], + }, + { + id: 'place.26118380', + type: 'Feature', + place_type: ['place'], + relevance: 1, + properties: { + mapbox_id: 'dXJuOm1ieHBsYzpBWTZJN0E', + wikidata: 'Q2219095', + }, + text_de: 'Berlin Heights', + language_de: 'de', + place_name_de: 'Berlin Heights, Ohio, Vereinigte Staaten', + text: 'Berlin Heights', + language: 'de', + place_name: 'Berlin Heights, Ohio, Vereinigte Staaten', + bbox: [-82.54353, 41.285345, -82.382139, 41.364821], + center: [-82.493433, 41.325154], + geometry: { + type: 'Point', + coordinates: [-82.493433, 41.325154], + }, + context: [ + { + id: 'district.7235308', + mapbox_id: 'dXJuOm1ieHBsYzpibWJz', + wikidata: 'Q111310', + text_de: 'Erie County', + language_de: 'de', + text: 'Erie County', + language: 'de', + }, + { + id: 'region.140524', + mapbox_id: 'dXJuOm1ieHBsYzpBaVRz', + wikidata: 'Q1397', + short_code: 'US-OH', + text_de: 'Ohio', + language_de: 'de', + text: 'Ohio', + language: 'de', + }, + { + id: 'country.8940', + mapbox_id: 'dXJuOm1ieHBsYzpJdXc', + wikidata: 'Q30', + short_code: 'us', + text_de: 'Vereinigte Staaten', + language_de: 'de', + text: 'Vereinigte Staaten', + language: 'de', + }, + ], + }, + { + id: 'place.26044652', + type: 'Feature', + place_type: ['place'], + relevance: 1, + properties: { + mapbox_id: 'dXJuOm1ieHBsYzpBWTFvN0E', + wikidata: 'Q524646', + }, + text_de: 'Berlin', + language_de: 'de', + place_name_de: 'Berlin, Massachusetts, Vereinigte Staaten', + text: 'Berlin', + language: 'de', + place_name: 'Berlin, Massachusetts, Vereinigte Staaten', + bbox: [-71.678766, 42.350413, -71.58015, 42.418232], + center: [-71.638302, 42.381333], + geometry: { + type: 'Point', + coordinates: [-71.638302, 42.381333], + }, + context: [ + { + id: 'district.25405164', + mapbox_id: 'dXJuOm1ieHBsYzpBWU9tN0E', + wikidata: 'Q54093', + text_de: 'Worcester County', + language_de: 'de', + text: 'Worcester County', + language: 'de', + }, + { + id: 'region.353516', + mapbox_id: 'dXJuOm1ieHBsYzpCV1Rz', + wikidata: 'Q771', + short_code: 'US-MA', + text_de: 'Massachusetts', + language_de: 'de', + text: 'Massachusetts', + language: 'de', + }, + { + id: 'country.8940', + mapbox_id: 'dXJuOm1ieHBsYzpJdXc', + wikidata: 'Q30', + short_code: 'us', + text_de: 'Vereinigte Staaten', + language_de: 'de', + text: 'Vereinigte Staaten', + language: 'de', + }, + ], + }, + ], + attribution: + 'NOTICE: © 2025 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained.', +} diff --git a/backend/test/fetchMock/berlinEn.ts b/backend/test/fetchMock/berlinEn.ts new file mode 100644 index 000000000..10712fbb5 --- /dev/null +++ b/backend/test/fetchMock/berlinEn.ts @@ -0,0 +1,254 @@ +export const berlinEn = { + type: 'FeatureCollection', + query: ['berlin'], + features: [ + { + id: 'place.115770', + type: 'Feature', + place_type: ['region', 'place'], + relevance: 1, + properties: { + mapbox_id: 'dXJuOm1ieHBsYzpBY1E2', + wikidata: 'Q64', + short_code: 'DE-BE', + }, + text_en: 'Berlin', + language_en: 'en', + place_name_en: 'Berlin, Germany', + text: 'Berlin', + language: 'en', + place_name: 'Berlin, Germany', + bbox: [13.08836, 52.338261, 13.760906, 52.675502], + center: [13.38886, 52.517037], + geometry: { + type: 'Point', + coordinates: [13.38886, 52.517037], + }, + context: [ + { + id: 'country.8762', + mapbox_id: 'dXJuOm1ieHBsYzpJam8', + wikidata: 'Q183', + short_code: 'de', + text_en: 'Germany', + language_en: 'en', + text: 'Germany', + language: 'en', + }, + ], + }, + { + id: 'place.25995500', + type: 'Feature', + place_type: ['place'], + relevance: 1, + properties: { + mapbox_id: 'dXJuOm1ieHBsYzpBWXlvN0E', + wikidata: 'Q614184', + }, + text_en: 'Berlin', + language_en: 'en', + place_name_en: 'Berlin, Maryland, United States', + text: 'Berlin', + language: 'en', + place_name: 'Berlin, Maryland, United States', + bbox: [-75.364989, 38.141747, -75.087625, 38.4078], + center: [-75.219004, 38.324728], + geometry: { + type: 'Point', + coordinates: [-75.219004, 38.324728], + }, + context: [ + { + id: 'district.25396972', + mapbox_id: 'dXJuOm1ieHBsYzpBWU9HN0E', + wikidata: 'Q494072', + text_en: 'Worcester County', + language_en: 'en', + text: 'Worcester County', + language: 'en', + }, + { + id: 'region.124140', + mapbox_id: 'dXJuOm1ieHBsYzpBZVRz', + wikidata: 'Q1391', + short_code: 'US-MD', + text_en: 'Maryland', + language_en: 'en', + text: 'Maryland', + language: 'en', + }, + { + id: 'country.8940', + mapbox_id: 'dXJuOm1ieHBsYzpJdXc', + wikidata: 'Q30', + short_code: 'us', + text_en: 'United States', + language_en: 'en', + text: 'United States', + language: 'en', + }, + ], + }, + { + id: 'place.25970924', + type: 'Feature', + place_type: ['place'], + relevance: 1, + properties: { + mapbox_id: 'dXJuOm1ieHBsYzpBWXhJN0E', + wikidata: 'Q1086827', + }, + text_en: 'Berlin', + language_en: 'en', + place_name_en: 'Berlin, New Jersey, United States', + text: 'Berlin', + language: 'en', + place_name: 'Berlin, New Jersey, United States', + bbox: [-74.974491, 39.70904, -74.878605, 39.809363], + center: [-74.929456, 39.791432], + geometry: { + type: 'Point', + coordinates: [-74.929456, 39.791432], + }, + context: [ + { + id: 'district.3016428', + mapbox_id: 'dXJuOm1ieHBsYzpMZ2Jz', + wikidata: 'Q497810', + text_en: 'Camden County', + language_en: 'en', + text: 'Camden County', + language: 'en', + }, + { + id: 'region.156908', + mapbox_id: 'dXJuOm1ieHBsYzpBbVRz', + wikidata: 'Q1408', + short_code: 'US-NJ', + text_en: 'New Jersey', + language_en: 'en', + text: 'New Jersey', + language: 'en', + }, + { + id: 'country.8940', + mapbox_id: 'dXJuOm1ieHBsYzpJdXc', + wikidata: 'Q30', + short_code: 'us', + text_en: 'United States', + language_en: 'en', + text: 'United States', + language: 'en', + }, + ], + }, + { + id: 'place.26036460', + type: 'Feature', + place_type: ['place'], + relevance: 1, + properties: { + mapbox_id: 'dXJuOm1ieHBsYzpBWTFJN0E', + }, + text_en: 'Berlin', + language_en: 'en', + place_name_en: 'Berlin, Connecticut, United States', + text: 'Berlin', + language: 'en', + place_name: 'Berlin, Connecticut, United States', + bbox: [-72.840244, 41.554195, -72.712381, 41.652706], + center: [-72.73954, 41.628325], + geometry: { + type: 'Point', + coordinates: [-72.73954, 41.628325], + }, + context: [ + { + id: 'district.10020588', + mapbox_id: 'dXJuOm1ieHBsYzptT2Jz', + wikidata: 'Q54236', + text_en: 'Hartford County', + language_en: 'en', + text: 'Hartford County', + language: 'en', + }, + { + id: 'region.361708', + mapbox_id: 'dXJuOm1ieHBsYzpCWVRz', + wikidata: 'Q779', + short_code: 'US-CT', + text_en: 'Connecticut', + language_en: 'en', + text: 'Connecticut', + language: 'en', + }, + { + id: 'country.8940', + mapbox_id: 'dXJuOm1ieHBsYzpJdXc', + wikidata: 'Q30', + short_code: 'us', + text_en: 'United States', + language_en: 'en', + text: 'United States', + language: 'en', + }, + ], + }, + { + id: 'place.26118380', + type: 'Feature', + place_type: ['place'], + relevance: 1, + properties: { + mapbox_id: 'dXJuOm1ieHBsYzpBWTZJN0E', + wikidata: 'Q2219095', + }, + text_en: 'Berlin Heights', + language_en: 'en', + place_name_en: 'Berlin Heights, Ohio, United States', + text: 'Berlin Heights', + language: 'en', + place_name: 'Berlin Heights, Ohio, United States', + bbox: [-82.54353, 41.285345, -82.382139, 41.364821], + center: [-82.493433, 41.325154], + geometry: { + type: 'Point', + coordinates: [-82.493433, 41.325154], + }, + context: [ + { + id: 'district.7235308', + mapbox_id: 'dXJuOm1ieHBsYzpibWJz', + wikidata: 'Q111310', + text_en: 'Erie County', + language_en: 'en', + text: 'Erie County', + language: 'en', + }, + { + id: 'region.140524', + mapbox_id: 'dXJuOm1ieHBsYzpBaVRz', + wikidata: 'Q1397', + short_code: 'US-OH', + text_en: 'Ohio', + language_en: 'en', + text: 'Ohio', + language: 'en', + }, + { + id: 'country.8940', + mapbox_id: 'dXJuOm1ieHBsYzpJdXc', + wikidata: 'Q30', + short_code: 'us', + text_en: 'United States', + language_en: 'en', + text: 'United States', + language: 'en', + }, + ], + }, + ], + attribution: + 'NOTICE: © 2025 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained.', +} diff --git a/backend/test/fetchMock/berlinGermany.ts b/backend/test/fetchMock/berlinGermany.ts new file mode 100644 index 000000000..09e01f30e --- /dev/null +++ b/backend/test/fetchMock/berlinGermany.ts @@ -0,0 +1,742 @@ +export const berlinGermany = { + type: 'FeatureCollection', + query: ['berlin', 'germany'], + features: [ + { + id: 'place.115770', + type: 'Feature', + place_type: ['region', 'place'], + relevance: 1, + properties: { + mapbox_id: 'dXJuOm1ieHBsYzpBY1E2', + wikidata: 'Q64', + short_code: 'DE-BE', + }, + text_en: 'Berlin', + language_en: 'en', + place_name_en: 'Berlin, Germany', + text: 'Berlin', + language: 'en', + place_name: 'Berlin, Germany', + text_de: 'Berlin', + language_de: 'de', + place_name_de: 'Berlin, Deutschland', + text_fr: 'Berlin', + language_fr: 'fr', + place_name_fr: 'Berlin, Allemagne', + text_nl: 'Berlijn', + language_nl: 'nl', + place_name_nl: 'Berlijn, Duitsland', + text_it: 'Berlino', + language_it: 'it', + place_name_it: 'Berlino, Germania', + text_es: 'Berlín', + language_es: 'es', + place_name_es: 'Berlín, Alemania', + text_pt: 'Berlim', + language_pt: 'pt', + place_name_pt: 'Berlim, Alemanha', + text_pl: 'Berlin', + language_pl: 'pl', + place_name_pl: 'Berlin, Niemcy', + text_ru: 'Берлин', + language_ru: 'ru', + place_name_ru: 'Берлин, Германия', + bbox: [13.08836, 52.338261, 13.760906, 52.675502], + center: [13.38886, 52.517037], + geometry: { + type: 'Point', + coordinates: [13.38886, 52.517037], + }, + context: [ + { + id: 'country.8762', + mapbox_id: 'dXJuOm1ieHBsYzpJam8', + wikidata: 'Q183', + short_code: 'de', + text_en: 'Germany', + language_en: 'en', + text: 'Germany', + language: 'en', + text_de: 'Deutschland', + language_de: 'de', + text_fr: 'Allemagne', + language_fr: 'fr', + text_nl: 'Duitsland', + language_nl: 'nl', + text_it: 'Germania', + language_it: 'it', + text_es: 'Alemania', + language_es: 'es', + text_pt: 'Alemanha', + language_pt: 'pt', + text_pl: 'Niemcy', + language_pl: 'pl', + text_ru: 'Германия', + language_ru: 'ru', + }, + ], + }, + { + id: 'address.1882189389165686', + type: 'Feature', + place_type: ['address'], + relevance: 1, + properties: { + accuracy: 'street', + mapbox_id: 'dXJuOm1ieGFkci1zdHI6Y2EyZjMxNmMtZjBhMy00ZmYwLWEwODgtZDYxMzg4ZmUwNTVh', + }, + text_en: 'Berlin', + place_name_en: 'Berlin, 24848 Klein Bennebek, Germany', + text: 'Berlin', + place_name: 'Berlin, 24848 Klein Bennebek, Germany', + text_de: 'Berlin', + place_name_de: 'Berlin, 24848 Klein Bennebek, Deutschland', + text_fr: 'Berlin', + place_name_fr: 'Berlin, 24848 Klein Bennebek, Allemagne', + text_nl: 'Berlin', + place_name_nl: 'Berlin, 24848 Klein Bennebek, Duitsland', + text_it: 'Berlin', + place_name_it: 'Berlin, 24848 Klein Bennebek, Germania', + text_es: 'Berlin', + place_name_es: 'Berlin, 24848 Klein Bennebek, Alemania', + text_pt: 'Berlin', + place_name_pt: 'Berlin, 24848 Klein Bennebek, Alemanha', + text_pl: 'Berlin', + place_name_pl: 'Berlin, 24848 Klein Bennebek, Niemcy', + text_ru: 'Berlin', + place_name_ru: 'Berlin, 24848 Клайн-Беннебек, Германия', + center: [9.422626, 54.404248], + geometry: { + type: 'Point', + coordinates: [9.422626, 54.404248], + }, + context: [ + { + id: 'postcode.13266490', + mapbox_id: 'dXJuOm1ieHBsYzp5bTQ2', + text_en: '24848', + text: '24848', + text_de: '24848', + text_fr: '24848', + text_nl: '24848', + text_it: '24848', + text_es: '24848', + text_pt: '24848', + text_pl: '24848', + text_ru: '24848', + }, + { + id: 'place.40798266', + mapbox_id: 'dXJuOm1ieHBsYzpBbTZJT2c', + wikidata: 'Q556639', + text_en: 'Klein Bennebek', + language_en: 'en', + text: 'Klein Bennebek', + language: 'en', + text_de: 'Klein Bennebek', + language_de: 'de', + text_fr: 'Klein Bennebek', + language_fr: 'fr', + text_nl: 'Klein Bennebek', + language_nl: 'nl', + text_it: 'Klein Bennebek', + language_it: 'it', + text_es: 'Klein Bennebek', + language_es: 'es', + text_pt: 'Klein Bennebek', + language_pt: 'pt', + text_pl: 'Klein Bennebek', + language_pl: 'pl', + text_ru: 'Клайн-Беннебек', + language_ru: 'ru', + }, + { + id: 'district.1885754', + mapbox_id: 'dXJuOm1ieHBsYzpITVk2', + wikidata: 'Q2941', + text_en: 'Distrito de Schleswig-Flensburg', + language_en: 'es', + text: 'Distrito de Schleswig-Flensburg', + language: 'es', + text_de: 'Kreis Schleswig-Flensburg', + language_de: 'de', + text_fr: 'Kreis Schleswig-Flensburg', + text_nl: 'Kreis Schleswig-Flensburg', + text_it: 'Kreis Schleswig-Flensburg', + text_es: 'Distrito de Schleswig-Flensburg', + language_es: 'es', + text_pt: 'Distrito de Schleswig-Flensburg', + language_pt: 'es', + text_pl: 'Powiat Schleswig-Flensburg', + language_pl: 'pl', + text_ru: 'Шлезвиг-Фленсбург', + language_ru: 'ru', + }, + { + id: 'region.17466', + mapbox_id: 'dXJuOm1ieHBsYzpSRG8', + wikidata: 'Q1194', + short_code: 'DE-SH', + text_en: 'Schleswig-Holstein', + language_en: 'en', + text: 'Schleswig-Holstein', + language: 'en', + text_de: 'Schleswig-Holstein', + language_de: 'de', + text_fr: 'Schleswig-Holstein', + language_fr: 'fr', + text_nl: 'Sleeswijk-Holstein', + language_nl: 'nl', + text_it: 'Schleswig-Holstein', + language_it: 'it', + text_es: 'Schleswig-Holstein', + language_es: 'es', + text_pt: 'Schleswig-Holstein', + language_pt: 'pt', + text_pl: 'Szlezwik-Holsztyn', + language_pl: 'pl', + text_ru: 'Шлезвиг-Гольштейн', + language_ru: 'ru', + }, + { + id: 'country.8762', + mapbox_id: 'dXJuOm1ieHBsYzpJam8', + wikidata: 'Q183', + short_code: 'de', + text_en: 'Germany', + language_en: 'en', + text: 'Germany', + language: 'en', + text_de: 'Deutschland', + language_de: 'de', + text_fr: 'Allemagne', + language_fr: 'fr', + text_nl: 'Duitsland', + language_nl: 'nl', + text_it: 'Germania', + language_it: 'it', + text_es: 'Alemania', + language_es: 'es', + text_pt: 'Alemanha', + language_pt: 'pt', + text_pl: 'Niemcy', + language_pl: 'pl', + text_ru: 'Германия', + language_ru: 'ru', + }, + ], + }, + { + id: 'address.7517935193989996', + type: 'Feature', + place_type: ['address'], + relevance: 0.958333, + properties: { + accuracy: 'street', + mapbox_id: 'dXJuOm1ieGFkci1zdHI6ZTY0MTBlYTgtNzBhZS00MmViLTk0ZWItNThlM2U4YmUzNDcz', + }, + text_en: 'Barlin', + place_name_en: 'Barlin, 17159 Dargun, Germany', + text: 'Barlin', + place_name: 'Barlin, 17159 Dargun, Germany', + text_de: 'Barlin', + place_name_de: 'Barlin, 17159 Dargun, Deutschland', + text_fr: 'Barlin', + place_name_fr: 'Barlin, 17159 Dargun, Allemagne', + text_nl: 'Barlin', + place_name_nl: 'Barlin, 17159 Dargun, Duitsland', + text_it: 'Barlin', + place_name_it: 'Barlin, 17159 Dargun, Germania', + text_es: 'Barlin', + place_name_es: 'Barlin, 17159 Dargun, Alemania', + text_pt: 'Barlin', + place_name_pt: 'Barlin, 17159 Dargun, Alemanha', + text_pl: 'Barlin', + place_name_pl: 'Barlin, 17159 Dargun, Niemcy', + text_ru: 'Barlin', + place_name_ru: 'Barlin, 17159 Даргун, Германия', + center: [12.883232, 53.922631], + geometry: { + type: 'Point', + coordinates: [12.883232, 53.922631], + }, + context: [ + { + id: 'postcode.8072762', + mapbox_id: 'dXJuOm1ieHBsYzpleTQ2', + text_en: '17159', + text: '17159', + text_de: '17159', + text_fr: '17159', + text_nl: '17159', + text_it: '17159', + text_es: '17159', + text_pt: '17159', + text_pl: '17159', + text_ru: '17159', + }, + { + id: 'locality.19302970', + mapbox_id: 'dXJuOm1ieHBsYzpBU2FLT2c', + text_en: 'Barlin', + language_en: 'de', + text: 'Barlin', + language: 'de', + text_de: 'Barlin', + language_de: 'de', + text_fr: 'Barlin', + text_nl: 'Barlin', + text_it: 'Barlin', + text_es: 'Barlin', + text_pt: 'Barlin', + text_pl: 'Barlin', + text_ru: 'Barlin', + }, + { + id: 'place.14256186', + mapbox_id: 'dXJuOm1ieHBsYzoyWWc2', + wikidata: 'Q50959', + text_en: 'Dargun', + language_en: 'en', + text: 'Dargun', + language: 'en', + text_de: 'Dargun', + language_de: 'de', + text_fr: 'Dargun', + language_fr: 'fr', + text_nl: 'Dargun', + language_nl: 'nl', + text_it: 'Dargun', + language_it: 'it', + text_es: 'Dargun', + language_es: 'es', + text_pt: 'Dargun', + language_pt: 'pt', + text_pl: 'Dargun', + language_pl: 'pl', + text_ru: 'Даргун', + language_ru: 'ru', + }, + { + id: 'district.1214010', + mapbox_id: 'dXJuOm1ieHBsYzpFb1k2', + wikidata: 'Q2902', + text_en: 'Mecklenburgische Seenplatte District', + language_en: 'en', + text: 'Mecklenburgische Seenplatte District', + language: 'en', + text_de: 'Kreis Mecklenburgische Seenplatte', + language_de: 'de', + text_fr: 'Plateau des lacs mecklembourgeois', + language_fr: 'fr', + text_nl: 'Landkreis Mecklenburgische Seenplatte', + language_nl: 'nl', + text_it: 'circondario della Terra dei Laghi del Meclemburgo', + language_it: 'it', + text_es: 'Lagos de Mecklemburgo', + language_es: 'es', + text_pt: 'Lagos de Mecklemburgo', + language_pt: 'es', + text_pl: 'Landkreis Mecklenburgische Seenplatte', + language_pl: 'pl', + text_ru: 'Мекленбург-Зеенплате', + language_ru: 'ru', + }, + { + id: 'region.25658', + mapbox_id: 'dXJuOm1ieHBsYzpaRG8', + wikidata: 'Q1196', + short_code: 'DE-MV', + text_en: 'Mecklenburg-Western Pomerania', + language_en: 'en', + text: 'Mecklenburg-Western Pomerania', + language: 'en', + text_de: 'Mecklenburg-Vorpommern', + language_de: 'de', + text_fr: 'Mecklembourg-Poméranie-Occidentale', + language_fr: 'fr', + text_nl: 'Mecklenburg-Voor-Pommeren', + language_nl: 'nl', + text_it: 'Meclemburgo-Pomerania Anteriore', + language_it: 'it', + text_es: 'Mecklemburgo-Pomerania Occidental', + language_es: 'es', + text_pt: 'Mecklemburgo-Pomerânia Ocidental', + language_pt: 'pt', + text_pl: 'Meklemburgia-Pomorze Przednie', + language_pl: 'pl', + text_ru: 'Мекленбург — Передняя Померания', + language_ru: 'ru', + }, + { + id: 'country.8762', + mapbox_id: 'dXJuOm1ieHBsYzpJam8', + wikidata: 'Q183', + short_code: 'de', + text_en: 'Germany', + language_en: 'en', + text: 'Germany', + language: 'en', + text_de: 'Deutschland', + language_de: 'de', + text_fr: 'Allemagne', + language_fr: 'fr', + text_nl: 'Duitsland', + language_nl: 'nl', + text_it: 'Germania', + language_it: 'it', + text_es: 'Alemania', + language_es: 'es', + text_pt: 'Alemanha', + language_pt: 'pt', + text_pl: 'Niemcy', + language_pl: 'pl', + text_ru: 'Германия', + language_ru: 'ru', + }, + ], + }, + { + id: 'address.4752874377841546', + type: 'Feature', + place_type: ['address'], + relevance: 0.958333, + properties: { + accuracy: 'street', + mapbox_id: 'dXJuOm1ieGFkci1zdHI6ODljMjJmMWItNTUyMy00ODIwLWFiNDYtNjU0YTRiZTc4NzJm', + }, + text_en: 'Ferliner Straße', + place_name_en: 'Ferliner Straße, 84332 Hebertsfelden, Germany', + text: 'Ferliner Straße', + place_name: 'Ferliner Straße, 84332 Hebertsfelden, Germany', + text_de: 'Ferliner Straße', + place_name_de: 'Ferliner Straße, 84332 Hebertsfelden, Deutschland', + text_fr: 'Ferliner Straße', + place_name_fr: 'Ferliner Straße, 84332 Hebertsfelden, Allemagne', + text_nl: 'Ferliner Straße', + place_name_nl: 'Ferliner Straße, 84332 Hebertsfelden, Duitsland', + text_it: 'Ferliner Straße', + place_name_it: 'Ferliner Straße, 84332 Hebertsfelden, Germania', + text_es: 'Ferliner Straße', + place_name_es: 'Ferliner Straße, 84332 Hebertsfelden, Alemania', + text_pt: 'Ferliner Straße', + place_name_pt: 'Ferliner Straße, 84332 Hebertsfelden, Alemanha', + text_pl: 'Ferliner Straße', + place_name_pl: 'Ferliner Straße, 84332 Hebertsfelden, Niemcy', + text_ru: 'Ferliner Straße', + place_name_ru: 'Ferliner Straße, 84332 Хебертсфельден, Германия', + center: [12.81704, 48.43586], + geometry: { + type: 'Point', + coordinates: [12.81704, 48.43586], + }, + context: [ + { + id: 'postcode.51138106', + mapbox_id: 'dXJuOm1ieHBsYzpBd3hPT2c', + text_en: '84332', + text: '84332', + text_de: '84332', + text_fr: '84332', + text_nl: '84332', + text_it: '84332', + text_es: '84332', + text_pt: '84332', + text_pl: '84332', + text_ru: '84332', + }, + { + id: 'locality.267905594', + mapbox_id: 'dXJuOm1ieHBsYzpEL2ZxT2c', + text_en: 'Prienbach', + language_en: 'de', + text: 'Prienbach', + language: 'de', + text_de: 'Prienbach', + language_de: 'de', + text_fr: 'Prienbach', + text_nl: 'Prienbach', + text_it: 'Prienbach', + text_es: 'Prienbach', + text_pt: 'Prienbach', + text_pl: 'Prienbach', + text_ru: 'Prienbach', + }, + { + id: 'place.31664186', + mapbox_id: 'dXJuOm1ieHBsYzpBZU1vT2c', + wikidata: 'Q553780', + text_en: 'Hebertsfelden', + language_en: 'en', + text: 'Hebertsfelden', + language: 'en', + text_de: 'Hebertsfelden', + language_de: 'de', + text_fr: 'Hebertsfelden', + language_fr: 'fr', + text_nl: 'Hebertsfelden', + language_nl: 'nl', + text_it: 'Hebertsfelden', + language_it: 'it', + text_es: 'Hebertsfelden', + language_es: 'es', + text_pt: 'Hebertsfelden', + language_pt: 'pt', + text_pl: 'Hebertsfelden', + language_pl: 'pl', + text_ru: 'Хебертсфельден', + language_ru: 'ru', + }, + { + id: 'district.1795642', + mapbox_id: 'dXJuOm1ieHBsYzpHMlk2', + wikidata: 'Q10477', + text_en: 'arrondissement de Rottal-Inn', + language_en: 'fr', + text: 'arrondissement de Rottal-Inn', + language: 'fr', + text_de: 'Kreis Rottal-Inn', + language_de: 'de', + text_fr: 'arrondissement de Rottal-Inn', + language_fr: 'fr', + text_nl: 'arrondissement de Rottal-Inn', + language_nl: 'fr', + text_it: 'Districtul Rottal-Inn', + language_it: 'ro', + text_es: 'arrondissement de Rottal-Inn', + language_es: 'fr', + text_pt: 'Kreis Rottal-Inn', + text_pl: 'Powiat Rottal-Inn', + language_pl: 'pl', + text_ru: 'Ротталь-Инн', + language_ru: 'ru', + }, + { + id: 'region.123962', + mapbox_id: 'dXJuOm1ieHBsYzpBZVE2', + wikidata: 'Q980', + short_code: 'DE-BY', + text_en: 'Bavaria', + language_en: 'en', + text: 'Bavaria', + language: 'en', + text_de: 'Bayern', + language_de: 'de', + text_fr: 'Bavière', + language_fr: 'fr', + text_nl: 'Beieren', + language_nl: 'nl', + text_it: 'Baviera', + language_it: 'it', + text_es: 'Baviera', + language_es: 'es', + text_pt: 'Baviera', + language_pt: 'pt', + text_pl: 'Bawaria', + language_pl: 'pl', + text_ru: 'Бавария', + language_ru: 'ru', + }, + { + id: 'country.8762', + mapbox_id: 'dXJuOm1ieHBsYzpJam8', + wikidata: 'Q183', + short_code: 'de', + text_en: 'Germany', + language_en: 'en', + text: 'Germany', + language: 'en', + text_de: 'Deutschland', + language_de: 'de', + text_fr: 'Allemagne', + language_fr: 'fr', + text_nl: 'Duitsland', + language_nl: 'nl', + text_it: 'Germania', + language_it: 'it', + text_es: 'Alemania', + language_es: 'es', + text_pt: 'Alemanha', + language_pt: 'pt', + text_pl: 'Niemcy', + language_pl: 'pl', + text_ru: 'Германия', + language_ru: 'ru', + }, + ], + }, + { + id: 'address.7246440478456020', + type: 'Feature', + place_type: ['address'], + relevance: 0.958333, + properties: { + accuracy: 'street', + mapbox_id: 'dXJuOm1ieGFkci1zdHI6MTUyNjZlZjItOTIxZC00OTc0LWIyZmMtOWM2NTQyOWE3MDEx', + }, + text_en: 'Erlin', + place_name_en: 'Erlin, 74545 Michelfeld, Germany', + text: 'Erlin', + place_name: 'Erlin, 74545 Michelfeld, Germany', + text_de: 'Erlin', + place_name_de: 'Erlin, 74545 Michelfeld, Deutschland', + text_fr: 'Erlin', + place_name_fr: 'Erlin, 74545 Michelfeld, Allemagne', + text_nl: 'Erlin', + place_name_nl: 'Erlin, 74545 Michelfeld, Duitsland', + text_it: 'Erlin', + place_name_it: 'Erlin, 74545 Michelfeld, Germania', + text_es: 'Erlin', + place_name_es: 'Erlin, 74545 Michelfeld, Alemania', + text_pt: 'Erlin', + place_name_pt: 'Erlin, 74545 Michelfeld, Alemanha', + text_pl: 'Erlin', + place_name_pl: 'Erlin, 74545 Michelfeld, Niemcy', + text_ru: 'Erlin', + place_name_ru: 'Erlin, 74545 Михельфельд, Германия', + center: [9.658099, 49.105636], + geometry: { + type: 'Point', + coordinates: [9.658099, 49.105636], + }, + context: [ + { + id: 'postcode.43716154', + mapbox_id: 'dXJuOm1ieHBsYzpBcHNPT2c', + text_en: '74545', + text: '74545', + text_de: '74545', + text_fr: '74545', + text_nl: '74545', + text_it: '74545', + text_es: '74545', + text_pt: '74545', + text_pl: '74545', + text_ru: '74545', + }, + { + id: 'locality.166046266', + mapbox_id: 'dXJuOm1ieHBsYzpDZVdxT2c', + text_en: 'Kiesberg', + language_en: 'de', + text: 'Kiesberg', + language: 'de', + text_de: 'Kiesberg', + language_de: 'de', + text_fr: 'Kiesberg', + text_nl: 'Kiesberg', + text_it: 'Kiesberg', + text_es: 'Kiesberg', + text_pt: 'Kiesberg', + text_pl: 'Kiesberg', + text_ru: 'Kiesberg', + }, + { + id: 'place.50980922', + mapbox_id: 'dXJuOm1ieHBsYzpBd25vT2c', + wikidata: 'Q81048', + text_en: 'Michelfeld', + language_en: 'en', + text: 'Michelfeld', + language: 'en', + text_de: 'Michelfeld', + language_de: 'de', + text_fr: 'Michelfeld', + language_fr: 'fr', + text_nl: 'Michelfeld', + language_nl: 'nl', + text_it: 'Michelfeld', + language_it: 'it', + text_es: 'Michelfeld', + language_es: 'es', + text_pt: 'Michelfeld', + language_pt: 'pt', + text_pl: 'Michelfeld', + language_pl: 'pl', + text_ru: 'Михельфельд', + language_ru: 'ru', + }, + { + id: 'district.1902138', + mapbox_id: 'dXJuOm1ieHBsYzpIUVk2', + wikidata: 'Q8520', + text_en: 'Landkreis Schwäbisch Hall', + language_en: 'en', + text: 'Landkreis Schwäbisch Hall', + language: 'en', + text_de: 'Kreis Schwäbisch Hall', + language_de: 'de', + text_fr: 'arrondissement de Schwäbisch Hall', + language_fr: 'fr', + text_nl: 'arrondissement de Schwäbisch Hall', + language_nl: 'fr', + text_it: 'circondario di Schwäbisch Hall', + language_it: 'it', + text_es: 'arrondissement de Schwäbisch Hall', + language_es: 'fr', + text_pt: 'circondario di Schwäbisch Hall', + language_pt: 'it', + text_pl: 'Districtul Schwäbisch Hall', + language_pl: 'ro', + text_ru: 'Швебиш-Халль', + language_ru: 'ru', + }, + { + id: 'region.132154', + mapbox_id: 'dXJuOm1ieHBsYzpBZ1E2', + wikidata: 'Q985', + short_code: 'DE-BW', + text_en: 'Baden-Württemberg', + language_en: 'en', + text: 'Baden-Württemberg', + language: 'en', + text_de: 'Baden-Württemberg', + language_de: 'de', + text_fr: 'Bade-Wurtemberg', + language_fr: 'fr', + text_nl: 'Baden-Württemberg', + language_nl: 'nl', + text_it: 'Baden-Württemberg', + language_it: 'it', + text_es: 'Baden-Wurtemberg', + language_es: 'es', + text_pt: 'Baden-Württemberg', + language_pt: 'pt', + text_pl: 'Badenia-Wirtembergia', + language_pl: 'pl', + text_ru: 'Баден-Вюртемберг', + language_ru: 'ru', + }, + { + id: 'country.8762', + mapbox_id: 'dXJuOm1ieHBsYzpJam8', + wikidata: 'Q183', + short_code: 'de', + text_en: 'Germany', + language_en: 'en', + text: 'Germany', + language: 'en', + text_de: 'Deutschland', + language_de: 'de', + text_fr: 'Allemagne', + language_fr: 'fr', + text_nl: 'Duitsland', + language_nl: 'nl', + text_it: 'Germania', + language_it: 'it', + text_es: 'Alemania', + language_es: 'es', + text_pt: 'Alemanha', + language_pt: 'pt', + text_pl: 'Niemcy', + language_pl: 'pl', + text_ru: 'Германия', + language_ru: 'ru', + }, + ], + }, + ], + attribution: + 'NOTICE: © 2025 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained.', +} diff --git a/backend/test/fetchMock/empty.ts b/backend/test/fetchMock/empty.ts new file mode 100644 index 000000000..9ae6b3afa --- /dev/null +++ b/backend/test/fetchMock/empty.ts @@ -0,0 +1,7 @@ +export const empty = { + type: 'FeatureCollection', + query: ['gbhtsd4sdha'], + features: [], + attribution: + 'NOTICE: © 2025 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained.', +} diff --git a/backend/test/fetchMock/hamburg.ts b/backend/test/fetchMock/hamburg.ts new file mode 100644 index 000000000..65f742926 --- /dev/null +++ b/backend/test/fetchMock/hamburg.ts @@ -0,0 +1,661 @@ +export const hamburg = { + type: 'FeatureCollection', + query: ['hamburg', 'germany'], + features: [ + { + id: 'place.9274', + type: 'Feature', + place_type: ['region', 'place'], + relevance: 1, + properties: { + mapbox_id: 'dXJuOm1ieHBsYzpKRG8', + wikidata: 'Q1055', + short_code: 'DE-HH', + }, + text_en: 'Hamburg', + language_en: 'en', + place_name_en: 'Hamburg, Germany', + text: 'Hamburg', + language: 'en', + place_name: 'Hamburg, Germany', + text_de: 'Hamburg', + language_de: 'de', + place_name_de: 'Hamburg, Deutschland', + text_fr: 'Hambourg', + language_fr: 'fr', + place_name_fr: 'Hambourg, Allemagne', + text_nl: 'Hamburg', + language_nl: 'nl', + place_name_nl: 'Hamburg, Duitsland', + text_it: 'Amburgo', + language_it: 'it', + place_name_it: 'Amburgo, Germania', + text_es: 'Hamburgo', + language_es: 'es', + place_name_es: 'Hamburgo, Alemania', + text_pt: 'Hamburgo', + language_pt: 'pt', + place_name_pt: 'Hamburgo, Alemanha', + text_pl: 'Hamburg', + language_pl: 'pl', + place_name_pl: 'Hamburg, Niemcy', + text_ru: 'Гамбург', + language_ru: 'ru', + place_name_ru: 'Гамбург, Германия', + bbox: [8.345049, 53.395085, 10.325247, 54.010088], + center: [10.000654, 53.550341], + geometry: { + type: 'Point', + coordinates: [10.000654, 53.550341], + }, + context: [ + { + id: 'country.8762', + mapbox_id: 'dXJuOm1ieHBsYzpJam8', + wikidata: 'Q183', + short_code: 'de', + text_en: 'Germany', + language_en: 'en', + text: 'Germany', + language: 'en', + text_de: 'Deutschland', + language_de: 'de', + text_fr: 'Allemagne', + language_fr: 'fr', + text_nl: 'Duitsland', + language_nl: 'nl', + text_it: 'Germania', + language_it: 'it', + text_es: 'Alemania', + language_es: 'es', + text_pt: 'Alemanha', + language_pt: 'pt', + text_pl: 'Niemcy', + language_pl: 'pl', + text_ru: 'Германия', + language_ru: 'ru', + }, + ], + }, + { + id: 'address.2262168808051568', + type: 'Feature', + place_type: ['address'], + relevance: 1, + properties: { + accuracy: 'street', + mapbox_id: 'dXJuOm1ieGFkci1zdHI6MGM5NzRmMmYtNTA0Yi00MDk1LWJjNGItNTA3NmRhY2Y4YzQ5', + }, + text_en: 'Hamburg', + place_name_en: 'Hamburg, 38889 Oberharz am Brocken, Germany', + text: 'Hamburg', + place_name: 'Hamburg, 38889 Oberharz am Brocken, Germany', + text_de: 'Hamburg', + place_name_de: 'Hamburg, 38889 Oberharz am Brocken, Deutschland', + text_fr: 'Hamburg', + place_name_fr: 'Hamburg, 38889 Oberharz am Brocken, Allemagne', + text_nl: 'Hamburg', + place_name_nl: 'Hamburg, 38889 Oberharz am Brocken, Duitsland', + text_it: 'Hamburg', + place_name_it: 'Hamburg, 38889 Oberharz am Brocken, Germania', + text_es: 'Hamburg', + place_name_es: 'Hamburg, 38889 Oberharz am Brocken, Alemania', + text_pt: 'Hamburg', + place_name_pt: 'Hamburg, 38889 Oberharz am Brocken, Alemanha', + text_pl: 'Hamburg', + place_name_pl: 'Hamburg, 38889 Oberharz am Brocken, Niemcy', + text_ru: 'Hamburg', + place_name_ru: 'Hamburg, 38889 Оберхарц-ам-Броккен, Германия', + center: [10.872935, 51.752737], + geometry: { + type: 'Point', + coordinates: [10.872935, 51.752737], + }, + context: [ + { + id: 'postcode.23424570', + mapbox_id: 'dXJuOm1ieHBsYzpBV1Z1T2c', + text_en: '38889', + text: '38889', + text_de: '38889', + text_fr: '38889', + text_nl: '38889', + text_it: '38889', + text_es: '38889', + text_pt: '38889', + text_pl: '38889', + text_ru: '38889', + }, + { + id: 'locality.232909370', + mapbox_id: 'dXJuOm1ieHBsYzpEZUhxT2c', + wikidata: 'Q1982027', + text_en: 'Neuwerk', + language_en: 'en', + text: 'Neuwerk', + language: 'en', + text_de: 'Neuwerk', + language_de: 'de', + text_fr: 'Neuwerk', + language_fr: 'fr', + text_nl: 'Neuwerk', + language_nl: 'nl', + text_it: 'Neuwerk', + language_it: 'it', + text_es: 'Neuwerk', + language_es: 'es', + text_pt: 'Neuwerk', + language_pt: 'pt', + text_pl: 'Neuwerk', + language_pl: 'pl', + text_ru: 'Neuwerk', + }, + { + id: 'place.57780282', + mapbox_id: 'dXJuOm1ieHBsYzpBM0dvT2c', + wikidata: 'Q703001', + text_en: 'Oberharz am Brocken', + language_en: 'en', + text: 'Oberharz am Brocken', + language: 'en', + text_de: 'Oberharz am Brocken', + language_de: 'de', + text_fr: 'Oberharz am Brocken', + language_fr: 'fr', + text_nl: 'Oberharz am Brocken', + language_nl: 'nl', + text_it: 'Oberharz am Brocken', + language_it: 'it', + text_es: 'Oberharz am Brocken', + language_es: 'es', + text_pt: 'Oberharz am Brocken', + language_pt: 'pt', + text_pl: 'Oberharz am Brocken', + language_pl: 'pl', + text_ru: 'Оберхарц-ам-Броккен', + language_ru: 'ru', + }, + { + id: 'district.755258', + mapbox_id: 'dXJuOm1ieHBsYzpDNFk2', + wikidata: 'Q6087', + text_en: 'Harz District', + language_en: 'en', + text: 'Harz District', + language: 'en', + text_de: 'Kreis Harz', + language_de: 'de', + text_fr: 'Arrondissement de Harz', + language_fr: 'fr', + text_nl: 'Landkreis Harz', + language_nl: 'nl', + text_it: 'circondario dello Harz', + language_it: 'it', + text_es: 'Distrito de Harz', + language_es: 'es', + text_pt: 'Distrito de Harz', + language_pt: 'es', + text_pl: 'Landkreis Harz', + language_pl: 'pl', + text_ru: 'Гарц', + language_ru: 'ru', + }, + { + id: 'region.91194', + mapbox_id: 'dXJuOm1ieHBsYzpBV1E2', + wikidata: 'Q1206', + short_code: 'DE-ST', + text_en: 'Saxony-Anhalt', + language_en: 'en', + text: 'Saxony-Anhalt', + language: 'en', + text_de: 'Sachsen-Anhalt', + language_de: 'de', + text_fr: 'Saxe-Anhalt', + language_fr: 'fr', + text_nl: 'Saksen-Anhalt', + language_nl: 'nl', + text_it: 'Sassonia-Anhalt', + language_it: 'it', + text_es: 'Sajonia-Anhalt', + language_es: 'es', + text_pt: 'Saxónia-Anhalt', + language_pt: 'pt', + text_pl: 'Saksonia-Anhalt', + language_pl: 'pl', + text_ru: 'Саксония-Анхальт', + language_ru: 'ru', + }, + { + id: 'country.8762', + mapbox_id: 'dXJuOm1ieHBsYzpJam8', + wikidata: 'Q183', + short_code: 'de', + text_en: 'Germany', + language_en: 'en', + text: 'Germany', + language: 'en', + text_de: 'Deutschland', + language_de: 'de', + text_fr: 'Allemagne', + language_fr: 'fr', + text_nl: 'Duitsland', + language_nl: 'nl', + text_it: 'Germania', + language_it: 'it', + text_es: 'Alemania', + language_es: 'es', + text_pt: 'Alemanha', + language_pt: 'pt', + text_pl: 'Niemcy', + language_pl: 'pl', + text_ru: 'Германия', + language_ru: 'ru', + }, + ], + }, + { + id: 'address.8952604554248206', + type: 'Feature', + place_type: ['address'], + relevance: 1, + properties: { + accuracy: 'street', + mapbox_id: 'dXJuOm1ieGFkci1zdHI6NGNmMGExNGUtMjExMy00Yzk1LTllYjQtYTllOTRmNDIxZmEz', + }, + text_en: 'Hamburg', + place_name_en: 'Hamburg, 54608 Bleialf, Germany', + text: 'Hamburg', + place_name: 'Hamburg, 54608 Bleialf, Germany', + text_de: 'Hamburg', + place_name_de: 'Hamburg, 54608 Bleialf, Deutschland', + text_fr: 'Hamburg', + place_name_fr: 'Hamburg, 54608 Bleialf, Allemagne', + text_nl: 'Hamburg', + place_name_nl: 'Hamburg, 54608 Bleialf, Duitsland', + text_it: 'Hamburg', + place_name_it: 'Hamburg, 54608 Bleialf, Germania', + text_es: 'Hamburg', + place_name_es: 'Hamburg, 54608 Bleialf, Alemania', + text_pt: 'Hamburg', + place_name_pt: 'Hamburg, 54608 Bleialf, Alemanha', + text_pl: 'Hamburg', + place_name_pl: 'Hamburg, 54608 Bleialf, Niemcy', + text_ru: 'Hamburg', + place_name_ru: 'Hamburg, 54608 Блайальф, Германия', + center: [6.286919, 50.231471], + geometry: { + type: 'Point', + coordinates: [6.286919, 50.231471], + }, + context: [ + { + id: 'postcode.30887482', + mapbox_id: 'dXJuOm1ieHBsYzpBZGRPT2c', + text_en: '54608', + text: '54608', + text_de: '54608', + text_fr: '54608', + text_nl: '54608', + text_it: '54608', + text_es: '54608', + text_pt: '54608', + text_pl: '54608', + text_ru: '54608', + }, + { + id: 'place.8742970', + mapbox_id: 'dXJuOm1ieHBsYzpoV2c2', + wikidata: 'Q553361', + text_en: 'Bleialf', + language_en: 'en', + text: 'Bleialf', + language: 'en', + text_de: 'Bleialf', + language_de: 'de', + text_fr: 'Bleialf', + language_fr: 'fr', + text_nl: 'Bleialf', + language_nl: 'nl', + text_it: 'Bleialf', + language_it: 'it', + text_es: 'Bleialf', + language_es: 'es', + text_pt: 'Bleialf', + language_pt: 'pt', + text_pl: 'Bleialf', + language_pl: 'pl', + text_ru: 'Блайальф', + language_ru: 'ru', + }, + { + id: 'district.2434618', + mapbox_id: 'dXJuOm1ieHBsYzpKU1k2', + wikidata: 'Q8580', + text_en: 'Eifelkreis Bitburg-Prüm', + language_en: 'en', + text: 'Eifelkreis Bitburg-Prüm', + language: 'en', + text_de: 'Eifelkreis Bitburg-Prüm', + language_de: 'de', + text_fr: 'Eifel-Bitburg-Prüm', + language_fr: 'fr', + text_nl: 'Eifelkreis Bitburg-Prüm', + language_nl: 'nl', + text_it: 'Eifelkreis Bitburg-Prüm', + language_it: 'it', + text_es: 'Eifel-Bitburg-Prüm', + language_es: 'es', + text_pt: 'Bitburg-Prüm', + language_pt: 'pt', + text_pl: 'Eifelkreis Bitburg-Prüm', + language_pl: 'pl', + text_ru: 'Айфель-Битбург-Прюм', + language_ru: 'ru', + }, + { + id: 'region.58426', + mapbox_id: 'dXJuOm1ieHBsYzo1RG8', + wikidata: 'Q1200', + short_code: 'DE-RP', + text_en: 'Rhineland-Palatinate', + language_en: 'en', + text: 'Rhineland-Palatinate', + language: 'en', + text_de: 'Rheinland-Pfalz', + language_de: 'de', + text_fr: 'Rhénanie-Palatinat', + language_fr: 'fr', + text_nl: 'Rijnland-Palts', + language_nl: 'nl', + text_it: 'Renania-Palatinato', + language_it: 'it', + text_es: 'Renania-Palatinado', + language_es: 'es', + text_pt: 'Renânia-Palatinado', + language_pt: 'pt', + text_pl: 'Nadrenia-Palatynat', + language_pl: 'pl', + text_ru: 'Рейнланд-Пфальц', + language_ru: 'ru', + }, + { + id: 'country.8762', + mapbox_id: 'dXJuOm1ieHBsYzpJam8', + wikidata: 'Q183', + short_code: 'de', + text_en: 'Germany', + language_en: 'en', + text: 'Germany', + language: 'en', + text_de: 'Deutschland', + language_de: 'de', + text_fr: 'Allemagne', + language_fr: 'fr', + text_nl: 'Duitsland', + language_nl: 'nl', + text_it: 'Germania', + language_it: 'it', + text_es: 'Alemania', + language_es: 'es', + text_pt: 'Alemanha', + language_pt: 'pt', + text_pl: 'Niemcy', + language_pl: 'pl', + text_ru: 'Германия', + language_ru: 'ru', + }, + ], + }, + { + id: 'place.35727418', + type: 'Feature', + place_type: ['place'], + relevance: 0.964286, + properties: { + mapbox_id: 'dXJuOm1ieHBsYzpBaUVvT2c', + wikidata: 'Q7040', + }, + text_en: 'Homburg', + language_en: 'en', + place_name_en: 'Homburg, Saarpfalz-Kreis, Saarland, Germany', + text: 'Homburg', + language: 'en', + place_name: 'Homburg, Saarpfalz-Kreis, Saarland, Germany', + text_de: 'Homburg', + language_de: 'de', + place_name_de: 'Homburg, Saarpfalz-Kreis, Saarland, Deutschland', + text_fr: 'Hombourg', + language_fr: 'fr', + place_name_fr: 'Hombourg, circondario del Saarpfalz, Sarre, Allemagne', + text_nl: 'Homburg', + language_nl: 'nl', + place_name_nl: 'Homburg, Saarpfalz-Kreis, Saarland, Duitsland', + text_it: 'Homburg', + language_it: 'it', + place_name_it: 'Homburg, circondario del Saarpfalz, Saarland, Germania', + text_es: 'Homburg', + language_es: 'es', + place_name_es: 'Homburg, Saarpfalz-Kreis, Sarre, Alemania', + text_pt: 'Homburg', + language_pt: 'pt', + place_name_pt: 'Homburg, circondario del Saarpfalz, Sarre, Alemanha', + text_pl: 'Homburg', + language_pl: 'pl', + place_name_pl: 'Homburg, Powiat Saarpfalz, Saara, Niemcy', + text_ru: 'Хомбург', + language_ru: 'ru', + place_name_ru: 'Хомбург, Саарпфальц, Саар, Германия', + bbox: [7.274109, 49.247602, 7.404584, 49.386381], + center: [7.340504, 49.321426], + geometry: { + type: 'Point', + coordinates: [7.340504, 49.321426], + }, + context: [ + { + id: 'district.2704954', + mapbox_id: 'dXJuOm1ieHBsYzpLVVk2', + wikidata: 'Q6793', + text_en: 'Saarpfalz-Kreis', + language_en: 'de', + text: 'Saarpfalz-Kreis', + language: 'de', + text_de: 'Saarpfalz-Kreis', + language_de: 'de', + text_fr: 'circondario del Saarpfalz', + language_fr: 'it', + text_nl: 'Saarpfalz-Kreis', + text_it: 'circondario del Saarpfalz', + language_it: 'it', + text_es: 'Saarpfalz-Kreis', + text_pt: 'circondario del Saarpfalz', + language_pt: 'it', + text_pl: 'Powiat Saarpfalz', + language_pl: 'pl', + text_ru: 'Саарпфальц', + language_ru: 'ru', + }, + { + id: 'region.66618', + mapbox_id: 'dXJuOm1ieHBsYzpBUVE2', + wikidata: 'Q1201', + short_code: 'DE-SL', + text_en: 'Saarland', + language_en: 'en', + text: 'Saarland', + language: 'en', + text_de: 'Saarland', + language_de: 'de', + text_fr: 'Sarre', + language_fr: 'fr', + text_nl: 'Saarland', + language_nl: 'nl', + text_it: 'Saarland', + language_it: 'it', + text_es: 'Sarre', + language_es: 'es', + text_pt: 'Sarre', + language_pt: 'pt', + text_pl: 'Saara', + language_pl: 'pl', + text_ru: 'Саар', + language_ru: 'ru', + }, + { + id: 'country.8762', + mapbox_id: 'dXJuOm1ieHBsYzpJam8', + wikidata: 'Q183', + short_code: 'de', + text_en: 'Germany', + language_en: 'en', + text: 'Germany', + language: 'en', + text_de: 'Deutschland', + language_de: 'de', + text_fr: 'Allemagne', + language_fr: 'fr', + text_nl: 'Duitsland', + language_nl: 'nl', + text_it: 'Germania', + language_it: 'it', + text_es: 'Alemania', + language_es: 'es', + text_pt: 'Alemanha', + language_pt: 'pt', + text_pl: 'Niemcy', + language_pl: 'pl', + text_ru: 'Германия', + language_ru: 'ru', + }, + ], + }, + { + id: 'place.30623802', + type: 'Feature', + place_type: ['place'], + relevance: 0.964286, + properties: { + mapbox_id: 'dXJuOm1ieHBsYzpBZE5JT2c', + wikidata: 'Q503226', + }, + text_en: 'Harburg', + language_en: 'en', + place_name_en: 'Harburg, arrondissement de Danube-Ries, Bavaria, Germany', + text: 'Harburg', + language: 'en', + place_name: 'Harburg, arrondissement de Danube-Ries, Bavaria, Germany', + text_de: 'Harburg', + language_de: 'de', + place_name_de: 'Harburg, Kreis Donau-Ries, Bayern, Deutschland', + text_fr: 'Harbourg', + language_fr: 'fr', + place_name_fr: 'Harbourg, arrondissement de Danube-Ries, Bavière, Allemagne', + text_nl: 'Harburg', + language_nl: 'nl', + place_name_nl: 'Harburg, arrondissement de Danube-Ries, Beieren, Duitsland', + text_it: 'Harburg', + language_it: 'it', + place_name_it: 'Harburg, Circondario del Danubio-Ries, Baviera, Germania', + text_es: 'Harburg', + language_es: 'es', + place_name_es: 'Harburg, arrondissement de Danube-Ries, Baviera, Alemania', + text_pt: 'Harburg', + language_pt: 'pt', + place_name_pt: 'Harburg, Circondario del Danubio-Ries, Baviera, Alemanha', + text_pl: 'Harburg', + language_pl: 'pl', + place_name_pl: 'Harburg, Landkreis Donau-Ries, Bawaria, Niemcy', + text_ru: 'Харбург', + language_ru: 'ru', + place_name_ru: 'Харбург, Донау-Рис, Бавария, Германия', + bbox: [10.617754, 48.726549, 10.770464, 48.83036], + center: [10.688789, 48.787212], + geometry: { + type: 'Point', + coordinates: [10.688789, 48.787212], + }, + context: [ + { + id: 'district.419386', + mapbox_id: 'dXJuOm1ieHBsYzpCbVk2', + wikidata: 'Q10418', + text_en: 'arrondissement de Danube-Ries', + language_en: 'fr', + text: 'arrondissement de Danube-Ries', + language: 'fr', + text_de: 'Kreis Donau-Ries', + language_de: 'de', + text_fr: 'arrondissement de Danube-Ries', + language_fr: 'fr', + text_nl: 'arrondissement de Danube-Ries', + language_nl: 'fr', + text_it: 'Circondario del Danubio-Ries', + language_it: 'it', + text_es: 'arrondissement de Danube-Ries', + language_es: 'fr', + text_pt: 'Circondario del Danubio-Ries', + language_pt: 'it', + text_pl: 'Landkreis Donau-Ries', + language_pl: 'pl', + text_ru: 'Донау-Рис', + language_ru: 'ru', + }, + { + id: 'region.123962', + mapbox_id: 'dXJuOm1ieHBsYzpBZVE2', + wikidata: 'Q980', + short_code: 'DE-BY', + text_en: 'Bavaria', + language_en: 'en', + text: 'Bavaria', + language: 'en', + text_de: 'Bayern', + language_de: 'de', + text_fr: 'Bavière', + language_fr: 'fr', + text_nl: 'Beieren', + language_nl: 'nl', + text_it: 'Baviera', + language_it: 'it', + text_es: 'Baviera', + language_es: 'es', + text_pt: 'Baviera', + language_pt: 'pt', + text_pl: 'Bawaria', + language_pl: 'pl', + text_ru: 'Бавария', + language_ru: 'ru', + }, + { + id: 'country.8762', + mapbox_id: 'dXJuOm1ieHBsYzpJam8', + wikidata: 'Q183', + short_code: 'de', + text_en: 'Germany', + language_en: 'en', + text: 'Germany', + language: 'en', + text_de: 'Deutschland', + language_de: 'de', + text_fr: 'Allemagne', + language_fr: 'fr', + text_nl: 'Duitsland', + language_nl: 'nl', + text_it: 'Germania', + language_it: 'it', + text_es: 'Alemania', + language_es: 'es', + text_pt: 'Alemanha', + language_pt: 'pt', + text_pl: 'Niemcy', + language_pl: 'pl', + text_ru: 'Германия', + language_ru: 'ru', + }, + ], + }, + ], + attribution: + 'NOTICE: © 2025 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained.', +} diff --git a/backend/test/fetchMock/hamburgNY.ts b/backend/test/fetchMock/hamburgNY.ts new file mode 100644 index 000000000..8f920c99c --- /dev/null +++ b/backend/test/fetchMock/hamburgNY.ts @@ -0,0 +1,813 @@ +export const hamburgNY = { + type: 'FeatureCollection', + query: ['hamburg', 'new', 'jersey'], + features: [ + { + id: 'place.138242284', + type: 'Feature', + place_type: ['place'], + relevance: 1, + properties: { + mapbox_id: 'dXJuOm1ieHBsYzpDRDFvN0E', + wikidata: 'Q1082931', + }, + text_en: 'Hamburg', + language_en: 'en', + place_name_en: 'Hamburg, New Jersey, United States', + text: 'Hamburg', + language: 'en', + place_name: 'Hamburg, New Jersey, United States', + text_de: 'Hamburg', + language_de: 'de', + place_name_de: 'Hamburg, New Jersey, Vereinigte Staaten', + text_fr: 'Hamburg', + language_fr: 'fr', + place_name_fr: 'Hamburg, New Jersey, États-Unis', + text_nl: 'Hamburg', + language_nl: 'nl', + place_name_nl: 'Hamburg, New Jersey, Verenigde Staten van Amerika', + text_it: 'Hamburg', + language_it: 'it', + place_name_it: "Hamburg, New Jersey, Stati Uniti d'America", + text_es: 'Hamburg', + language_es: 'es', + place_name_es: 'Hamburg, Nueva Jersey, Estados Unidos', + text_pt: 'Hamburg', + language_pt: 'pt', + place_name_pt: 'Hamburg, Nova Jérsia, Estados Unidos', + text_pl: 'Hamburg', + language_pl: 'nl', + place_name_pl: 'Hamburg, New Jersey, Stany Zjednoczone', + text_ru: 'Хамбург', + language_ru: 'kk', + place_name_ru: 'Хамбург, Нью-Джерси, США', + bbox: [-74.632296, 41.101134, -74.510997, 41.194008], + center: [-74.576227, 41.153431], + geometry: { + type: 'Point', + coordinates: [-74.576227, 41.153431], + }, + context: [ + { + id: 'district.22456044', + mapbox_id: 'dXJuOm1ieHBsYzpBVmFtN0E', + wikidata: 'Q495998', + text_en: 'Sussex County', + language_en: 'en', + text: 'Sussex County', + language: 'en', + text_de: 'Sussex County', + language_de: 'de', + text_fr: 'comté de Sussex', + language_fr: 'fr', + text_nl: 'Sussex County', + language_nl: 'nl', + text_it: 'contea di Sussex', + language_it: 'it', + text_es: 'Condado de Sussex', + language_es: 'es', + text_pt: 'Condado de Sussex', + language_pt: 'pt', + text_pl: 'Hrabstwo Sussex', + language_pl: 'pl', + text_ru: 'Сассекс', + language_ru: 'ru', + }, + { + id: 'region.156908', + mapbox_id: 'dXJuOm1ieHBsYzpBbVRz', + wikidata: 'Q1408', + short_code: 'US-NJ', + text_en: 'New Jersey', + language_en: 'en', + text: 'New Jersey', + language: 'en', + text_de: 'New Jersey', + language_de: 'de', + text_fr: 'New Jersey', + language_fr: 'fr', + text_nl: 'New Jersey', + language_nl: 'nl', + text_it: 'New Jersey', + language_it: 'it', + text_es: 'Nueva Jersey', + language_es: 'es', + text_pt: 'Nova Jérsia', + language_pt: 'pt', + text_pl: 'New Jersey', + language_pl: 'pl', + text_ru: 'Нью-Джерси', + language_ru: 'ru', + }, + { + id: 'country.8940', + mapbox_id: 'dXJuOm1ieHBsYzpJdXc', + wikidata: 'Q30', + short_code: 'us', + text_en: 'United States', + language_en: 'en', + text: 'United States', + language: 'en', + text_de: 'Vereinigte Staaten', + language_de: 'de', + text_fr: 'États-Unis', + language_fr: 'fr', + text_nl: 'Verenigde Staten van Amerika', + language_nl: 'nl', + text_it: "Stati Uniti d'America", + language_it: 'it', + text_es: 'Estados Unidos', + language_es: 'es', + text_pt: 'Estados Unidos', + language_pt: 'pt', + text_pl: 'Stany Zjednoczone', + language_pl: 'pl', + text_ru: 'США', + language_ru: 'ru', + }, + ], + }, + { + id: 'address.4892302912777302', + type: 'Feature', + place_type: ['address'], + relevance: 1, + properties: { + accuracy: 'street', + 'override:postcode': '', + mapbox_id: 'dXJuOm1ieGFkci1zdHI6YTVmNjA1ZDItOGJhYS00MWRlLWE0MmUtYzYzYThjY2JlYzE2', + }, + text_en: 'Hamburg Turnpike', + place_name_en: 'Hamburg Turnpike, Stockholm, New Jersey 07460, United States', + text: 'Hamburg Turnpike', + place_name: 'Hamburg Turnpike, Stockholm, New Jersey 07460, United States', + text_de: 'Hamburg Turnpike', + place_name_de: 'Hamburg Turnpike, Stockholm, New Jersey 07460, Vereinigte Staaten', + text_fr: 'Hamburg Turnpike', + place_name_fr: 'Hamburg Turnpike, Stockholm, New Jersey 07460, États-Unis', + text_nl: 'Hamburg Turnpike', + place_name_nl: 'Hamburg Turnpike, Stockholm, New Jersey 07460, Verenigde Staten van Amerika', + text_it: 'Hamburg Turnpike', + place_name_it: "Hamburg Turnpike, Stockholm, New Jersey 07460, Stati Uniti d'America", + text_es: 'Hamburg Turnpike', + place_name_es: 'Hamburg Turnpike, Stockholm, Nueva Jersey 07460, Estados Unidos', + text_pt: 'Hamburg Turnpike', + place_name_pt: 'Hamburg Turnpike, Stockholm, Nova Jérsia 07460, Estados Unidos', + text_pl: 'Hamburg Turnpike', + place_name_pl: 'Hamburg Turnpike, Stockholm, New Jersey 07460, Stany Zjednoczone', + text_ru: 'Hamburg Turnpike', + place_name_ru: 'Hamburg Turnpike, Stockholm, Нью-Джерси 07460, США', + center: [-74.52295, 41.11971], + geometry: { + type: 'Point', + coordinates: [-74.52295, 41.11971], + }, + context: [ + { + id: 'postcode.20008684', + mapbox_id: 'dXJuOm1ieHBsYzpBVEZPN0E', + text_en: '07460', + text: '07460', + text_de: '07460', + text_fr: '07460', + text_nl: '07460', + text_it: '07460', + text_es: '07460', + text_pt: '07460', + text_pl: '07460', + text_ru: '07460', + }, + { + id: 'locality.237284076', + mapbox_id: 'dXJuOm1ieHBsYzpEaVNxN0E', + text_en: 'Hardyston Township', + language_en: 'en', + text: 'Hardyston Township', + language: 'en', + text_de: 'Hardyston Township', + language_de: 'en', + text_fr: 'Hardyston Township', + language_fr: 'en', + text_nl: 'Hardyston Township', + language_nl: 'en', + text_it: 'Hardyston Township', + language_it: 'en', + text_es: 'Hardyston Township', + language_es: 'en', + text_pt: 'Hardyston Township', + text_pl: 'Hardyston Township', + language_pl: 'en', + text_ru: 'Hardyston Township', + }, + { + id: 'place.315435244', + mapbox_id: 'dXJuOm1ieHBsYzpFczBvN0E', + wikidata: 'Q7617988', + text_en: 'Stockholm', + language_en: 'en', + text: 'Stockholm', + language: 'en', + text_de: 'Stockholm', + language_de: 'en', + text_fr: 'Stockholm', + language_fr: 'fr', + text_nl: 'Stockholm', + language_nl: 'fr', + text_it: 'Stockholm', + language_it: 'fr', + text_es: 'Stockholm', + language_es: 'fr', + text_pt: 'Stockholm', + text_pl: 'Stockholm', + language_pl: 'en', + text_ru: 'Stockholm', + }, + { + id: 'district.22456044', + mapbox_id: 'dXJuOm1ieHBsYzpBVmFtN0E', + wikidata: 'Q495998', + text_en: 'Sussex County', + language_en: 'en', + text: 'Sussex County', + language: 'en', + text_de: 'Sussex County', + language_de: 'de', + text_fr: 'comté de Sussex', + language_fr: 'fr', + text_nl: 'Sussex County', + language_nl: 'nl', + text_it: 'contea di Sussex', + language_it: 'it', + text_es: 'Condado de Sussex', + language_es: 'es', + text_pt: 'Condado de Sussex', + language_pt: 'pt', + text_pl: 'Hrabstwo Sussex', + language_pl: 'pl', + text_ru: 'Сассекс', + language_ru: 'ru', + }, + { + id: 'region.156908', + mapbox_id: 'dXJuOm1ieHBsYzpBbVRz', + wikidata: 'Q1408', + short_code: 'US-NJ', + text_en: 'New Jersey', + language_en: 'en', + text: 'New Jersey', + language: 'en', + text_de: 'New Jersey', + language_de: 'de', + text_fr: 'New Jersey', + language_fr: 'fr', + text_nl: 'New Jersey', + language_nl: 'nl', + text_it: 'New Jersey', + language_it: 'it', + text_es: 'Nueva Jersey', + language_es: 'es', + text_pt: 'Nova Jérsia', + language_pt: 'pt', + text_pl: 'New Jersey', + language_pl: 'pl', + text_ru: 'Нью-Джерси', + language_ru: 'ru', + }, + { + id: 'country.8940', + mapbox_id: 'dXJuOm1ieHBsYzpJdXc', + wikidata: 'Q30', + short_code: 'us', + text_en: 'United States', + language_en: 'en', + text: 'United States', + language: 'en', + text_de: 'Vereinigte Staaten', + language_de: 'de', + text_fr: 'États-Unis', + language_fr: 'fr', + text_nl: 'Verenigde Staten van Amerika', + language_nl: 'nl', + text_it: "Stati Uniti d'America", + language_it: 'it', + text_es: 'Estados Unidos', + language_es: 'es', + text_pt: 'Estados Unidos', + language_pt: 'pt', + text_pl: 'Stany Zjednoczone', + language_pl: 'pl', + text_ru: 'США', + language_ru: 'ru', + }, + ], + }, + { + id: 'address.5578007523305814', + type: 'Feature', + place_type: ['address'], + relevance: 1, + properties: { + accuracy: 'street', + mapbox_id: 'dXJuOm1ieGFkci1zdHI6YTY5NzVmZjAtMWI1MC00ZTZlLTlkNTctZTIzYWQ3N2Y5NGU5', + }, + text_en: 'Hamburg Pike', + place_name_en: 'Hamburg Pike, Hamburg, New Jersey 07419, United States', + text: 'Hamburg Pike', + place_name: 'Hamburg Pike, Hamburg, New Jersey 07419, United States', + text_de: 'Hamburg Pike', + place_name_de: 'Hamburg Pike, Hamburg, New Jersey 07419, Vereinigte Staaten', + text_fr: 'Hamburg Pike', + place_name_fr: 'Hamburg Pike, Hamburg, New Jersey 07419, États-Unis', + text_nl: 'Hamburg Pike', + place_name_nl: 'Hamburg Pike, Hamburg, New Jersey 07419, Verenigde Staten van Amerika', + text_it: 'Hamburg Pike', + place_name_it: "Hamburg Pike, Hamburg, New Jersey 07419, Stati Uniti d'America", + text_es: 'Hamburg Pike', + place_name_es: 'Hamburg Pike, Hamburg, Nueva Jersey 07419, Estados Unidos', + text_pt: 'Hamburg Pike', + place_name_pt: 'Hamburg Pike, Hamburg, Nova Jérsia 07419, Estados Unidos', + text_pl: 'Hamburg Pike', + place_name_pl: 'Hamburg Pike, Hamburg, New Jersey 07419, Stany Zjednoczone', + text_ru: 'Hamburg Pike', + place_name_ru: 'Hamburg Pike, Хамбург, Нью-Джерси 07419, США', + center: [-74.541928, 41.125463], + geometry: { + type: 'Point', + coordinates: [-74.541928, 41.125463], + }, + context: [ + { + id: 'postcode.19820268', + mapbox_id: 'dXJuOm1ieHBsYzpBUzV1N0E', + text_en: '07419', + text: '07419', + text_de: '07419', + text_fr: '07419', + text_nl: '07419', + text_it: '07419', + text_es: '07419', + text_pt: '07419', + text_pl: '07419', + text_ru: '07419', + }, + { + id: 'locality.237284076', + mapbox_id: 'dXJuOm1ieHBsYzpEaVNxN0E', + text_en: 'Hardyston Township', + language_en: 'en', + text: 'Hardyston Township', + language: 'en', + text_de: 'Hardyston Township', + language_de: 'en', + text_fr: 'Hardyston Township', + language_fr: 'en', + text_nl: 'Hardyston Township', + language_nl: 'en', + text_it: 'Hardyston Township', + language_it: 'en', + text_es: 'Hardyston Township', + language_es: 'en', + text_pt: 'Hardyston Township', + text_pl: 'Hardyston Township', + language_pl: 'en', + text_ru: 'Hardyston Township', + }, + { + id: 'place.138242284', + mapbox_id: 'dXJuOm1ieHBsYzpDRDFvN0E', + wikidata: 'Q1082931', + text_en: 'Hamburg', + language_en: 'en', + text: 'Hamburg', + language: 'en', + text_de: 'Hamburg', + language_de: 'de', + text_fr: 'Hamburg', + language_fr: 'fr', + text_nl: 'Hamburg', + language_nl: 'nl', + text_it: 'Hamburg', + language_it: 'it', + text_es: 'Hamburg', + language_es: 'es', + text_pt: 'Hamburg', + language_pt: 'pt', + text_pl: 'Hamburg', + language_pl: 'nl', + text_ru: 'Хамбург', + language_ru: 'kk', + }, + { + id: 'district.22456044', + mapbox_id: 'dXJuOm1ieHBsYzpBVmFtN0E', + wikidata: 'Q495998', + text_en: 'Sussex County', + language_en: 'en', + text: 'Sussex County', + language: 'en', + text_de: 'Sussex County', + language_de: 'de', + text_fr: 'comté de Sussex', + language_fr: 'fr', + text_nl: 'Sussex County', + language_nl: 'nl', + text_it: 'contea di Sussex', + language_it: 'it', + text_es: 'Condado de Sussex', + language_es: 'es', + text_pt: 'Condado de Sussex', + language_pt: 'pt', + text_pl: 'Hrabstwo Sussex', + language_pl: 'pl', + text_ru: 'Сассекс', + language_ru: 'ru', + }, + { + id: 'region.156908', + mapbox_id: 'dXJuOm1ieHBsYzpBbVRz', + wikidata: 'Q1408', + short_code: 'US-NJ', + text_en: 'New Jersey', + language_en: 'en', + text: 'New Jersey', + language: 'en', + text_de: 'New Jersey', + language_de: 'de', + text_fr: 'New Jersey', + language_fr: 'fr', + text_nl: 'New Jersey', + language_nl: 'nl', + text_it: 'New Jersey', + language_it: 'it', + text_es: 'Nueva Jersey', + language_es: 'es', + text_pt: 'Nova Jérsia', + language_pt: 'pt', + text_pl: 'New Jersey', + language_pl: 'pl', + text_ru: 'Нью-Джерси', + language_ru: 'ru', + }, + { + id: 'country.8940', + mapbox_id: 'dXJuOm1ieHBsYzpJdXc', + wikidata: 'Q30', + short_code: 'us', + text_en: 'United States', + language_en: 'en', + text: 'United States', + language: 'en', + text_de: 'Vereinigte Staaten', + language_de: 'de', + text_fr: 'États-Unis', + language_fr: 'fr', + text_nl: 'Verenigde Staten van Amerika', + language_nl: 'nl', + text_it: "Stati Uniti d'America", + language_it: 'it', + text_es: 'Estados Unidos', + language_es: 'es', + text_pt: 'Estados Unidos', + language_pt: 'pt', + text_pl: 'Stany Zjednoczone', + language_pl: 'pl', + text_ru: 'США', + language_ru: 'ru', + }, + ], + }, + { + id: 'address.4994460186306076', + type: 'Feature', + place_type: ['address'], + relevance: 0.914815, + properties: { + accuracy: 'street', + mapbox_id: 'dXJuOm1ieGFkci1zdHI6MzIyNTIzODItMTM4My00YjAyLThiNTUtOTAwMGFiMDNiYmQy', + }, + text_en: 'Hamburg Road', + place_name_en: 'Hamburg Road, Parsippany, New Jersey 07054, United States', + text: 'Hamburg Road', + place_name: 'Hamburg Road, Parsippany, New Jersey 07054, United States', + text_de: 'Hamburg Road', + place_name_de: + 'Hamburg Road, Parsippany-Troy Hills Township, New Jersey 07054, Vereinigte Staaten', + text_fr: 'Hamburg Road', + place_name_fr: 'Hamburg Road, Parsippany-Troy Hills, New Jersey 07054, États-Unis', + text_nl: 'Hamburg Road', + place_name_nl: + 'Hamburg Road, Parsippany-Troy Hills, New Jersey 07054, Verenigde Staten van Amerika', + text_it: 'Hamburg Road', + place_name_it: "Hamburg Road, Parsippany-Troy Hills, New Jersey 07054, Stati Uniti d'America", + text_es: 'Hamburg Road', + place_name_es: 'Hamburg Road, Parsippany-Troy Hills, Nueva Jersey 07054, Estados Unidos', + text_pt: 'Hamburg Road', + place_name_pt: 'Hamburg Road, Parsippany-Troy Hills, Nova Jérsia 07054, Estados Unidos', + text_pl: 'Hamburg Road', + place_name_pl: 'Hamburg Road, Parsippany-Troy Hills, New Jersey 07054, Stany Zjednoczone', + text_ru: 'Hamburg Road', + place_name_ru: 'Hamburg Road, Парсиппани-Трой-Хилс, Нью-Джерси 07054, США', + center: [-74.436622, 40.859052], + geometry: { + type: 'Point', + coordinates: [-74.436622, 40.859052], + }, + context: [ + { + id: 'postcode.19074796', + mapbox_id: 'dXJuOm1ieHBsYzpBU01PN0E', + text_en: '07054', + text: '07054', + text_de: '07054', + text_fr: '07054', + text_nl: '07054', + text_it: '07054', + text_es: '07054', + text_pt: '07054', + text_pl: '07054', + text_ru: '07054', + }, + { + id: 'place.252618988', + mapbox_id: 'dXJuOm1ieHBsYzpEdzZvN0E', + wikidata: 'Q532344', + text_en: 'Parsippany', + language_en: 'en', + text: 'Parsippany', + language: 'en', + text_de: 'Parsippany-Troy Hills Township', + language_de: 'de', + text_fr: 'Parsippany-Troy Hills', + language_fr: 'fr', + text_nl: 'Parsippany-Troy Hills', + language_nl: 'nl', + text_it: 'Parsippany-Troy Hills', + language_it: 'it', + text_es: 'Parsippany-Troy Hills', + language_es: 'es', + text_pt: 'Parsippany-Troy Hills', + language_pt: 'es', + text_pl: 'Parsippany-Troy Hills', + language_pl: 'nl', + text_ru: 'Парсиппани-Трой-Хилс', + language_ru: 'ru', + }, + { + id: 'district.16525036', + mapbox_id: 'dXJuOm1ieHBsYzovQ2Jz', + wikidata: 'Q498163', + text_en: 'Morris County', + language_en: 'en', + text: 'Morris County', + language: 'en', + text_de: 'Morris County', + language_de: 'de', + text_fr: 'comté de Morris', + language_fr: 'fr', + text_nl: 'Morris County', + language_nl: 'nl', + text_it: 'contea di Morris', + language_it: 'it', + text_es: 'Condado de Morris', + language_es: 'es', + text_pt: 'Condado de Morris', + language_pt: 'pt', + text_pl: 'Hrabstwo Morris', + language_pl: 'pl', + text_ru: 'Моррис', + language_ru: 'ru', + }, + { + id: 'region.156908', + mapbox_id: 'dXJuOm1ieHBsYzpBbVRz', + wikidata: 'Q1408', + short_code: 'US-NJ', + text_en: 'New Jersey', + language_en: 'en', + text: 'New Jersey', + language: 'en', + text_de: 'New Jersey', + language_de: 'de', + text_fr: 'New Jersey', + language_fr: 'fr', + text_nl: 'New Jersey', + language_nl: 'nl', + text_it: 'New Jersey', + language_it: 'it', + text_es: 'Nueva Jersey', + language_es: 'es', + text_pt: 'Nova Jérsia', + language_pt: 'pt', + text_pl: 'New Jersey', + language_pl: 'pl', + text_ru: 'Нью-Джерси', + language_ru: 'ru', + }, + { + id: 'country.8940', + mapbox_id: 'dXJuOm1ieHBsYzpJdXc', + wikidata: 'Q30', + short_code: 'us', + text_en: 'United States', + language_en: 'en', + text: 'United States', + language: 'en', + text_de: 'Vereinigte Staaten', + language_de: 'de', + text_fr: 'États-Unis', + language_fr: 'fr', + text_nl: 'Verenigde Staten van Amerika', + language_nl: 'nl', + text_it: "Stati Uniti d'America", + language_it: 'it', + text_es: 'Estados Unidos', + language_es: 'es', + text_pt: 'Estados Unidos', + language_pt: 'pt', + text_pl: 'Stany Zjednoczone', + language_pl: 'pl', + text_ru: 'США', + language_ru: 'ru', + }, + ], + }, + { + id: 'address.7206046822993882', + type: 'Feature', + place_type: ['address'], + relevance: 0.914815, + properties: { + accuracy: 'street', + mapbox_id: 'dXJuOm1ieGFkci1zdHI6OGNiMjYzOWMtZDAxYS00OWY3LThmNTEtZTllMmJjMDY4MWU2', + }, + text_en: 'Hamburg Avenue', + place_name_en: 'Hamburg Avenue, Egg Harbor City, New Jersey 08215, United States', + text: 'Hamburg Avenue', + place_name: 'Hamburg Avenue, Egg Harbor City, New Jersey 08215, United States', + text_de: 'Hamburg Avenue', + place_name_de: 'Hamburg Avenue, Egg Harbor City, New Jersey 08215, Vereinigte Staaten', + text_fr: 'Hamburg Avenue', + place_name_fr: 'Hamburg Avenue, Egg Harbor City, New Jersey 08215, États-Unis', + text_nl: 'Hamburg Avenue', + place_name_nl: + 'Hamburg Avenue, Egg Harbor City, New Jersey 08215, Verenigde Staten van Amerika', + text_it: 'Hamburg Avenue', + place_name_it: "Hamburg Avenue, Egg Harbor City, New Jersey 08215, Stati Uniti d'America", + text_es: 'Hamburg Avenue', + place_name_es: 'Hamburg Avenue, Egg Harbor City, Nueva Jersey 08215, Estados Unidos', + text_pt: 'Hamburg Avenue', + place_name_pt: 'Hamburg Avenue, Egg Harbor City, Nova Jérsia 08215, Estados Unidos', + text_pl: 'Hamburg Avenue', + place_name_pl: 'Hamburg Avenue, Egg Harbor City, New Jersey 08215, Stany Zjednoczone', + text_ru: 'Hamburg Avenue', + place_name_ru: 'Hamburg Avenue, Эгг Харбор Сити, Нью-Джерси 08215, США', + center: [-74.634456, 39.551661], + geometry: { + type: 'Point', + coordinates: [-74.634456, 39.551661], + }, + context: [ + { + id: 'postcode.22638316', + mapbox_id: 'dXJuOm1ieHBsYzpBVmx1N0E', + text_en: '08215', + text: '08215', + text_de: '08215', + text_fr: '08215', + text_nl: '08215', + text_it: '08215', + text_es: '08215', + text_pt: '08215', + text_pl: '08215', + text_ru: '08215', + }, + { + id: 'locality.378645228', + mapbox_id: 'dXJuOm1ieHBsYzpGcEdxN0E', + text_en: 'Mullica Township', + language_en: 'en', + text: 'Mullica Township', + language: 'en', + text_de: 'Mullica Township', + language_de: 'en', + text_fr: 'Mullica Township', + language_fr: 'en', + text_nl: 'Mullica Township', + language_nl: 'en', + text_it: 'Mullica Township', + language_it: 'en', + text_es: 'Mullica Township', + language_es: 'en', + text_pt: 'Mullica Township', + text_pl: 'Mullica Township', + language_pl: 'en', + text_ru: 'Mullica Township', + }, + { + id: 'place.97724652', + mapbox_id: 'dXJuOm1ieHBsYzpCZE1vN0E', + wikidata: 'Q1082961', + text_en: 'Egg Harbor City', + language_en: 'en', + text: 'Egg Harbor City', + language: 'en', + text_de: 'Egg Harbor City', + language_de: 'de', + text_fr: 'Egg Harbor City', + language_fr: 'fr', + text_nl: 'Egg Harbor City', + language_nl: 'nl', + text_it: 'Egg Harbor City', + language_it: 'it', + text_es: 'Egg Harbor City', + language_es: 'es', + text_pt: 'Egg Harbor City', + language_pt: 'pt', + text_pl: 'Egg Harbor City', + language_pl: 'pl', + text_ru: 'Эгг Харбор Сити', + language_ru: 'kk', + }, + { + id: 'district.837356', + mapbox_id: 'dXJuOm1ieHBsYzpETWJz', + wikidata: 'Q497928', + text_en: 'Atlantic County', + language_en: 'en', + text: 'Atlantic County', + language: 'en', + text_de: 'Atlantic County', + language_de: 'de', + text_fr: "comté d'Atlantic", + language_fr: 'fr', + text_nl: 'Atlantic County', + language_nl: 'nl', + text_it: 'contea di Atlantic', + language_it: 'it', + text_es: 'Condado de Atlantic', + language_es: 'es', + text_pt: 'Condado de Atlantic', + language_pt: 'pt', + text_pl: 'Hrabstwo Atlantic', + language_pl: 'pl', + text_ru: 'Атлантик', + language_ru: 'ru', + }, + { + id: 'region.156908', + mapbox_id: 'dXJuOm1ieHBsYzpBbVRz', + wikidata: 'Q1408', + short_code: 'US-NJ', + text_en: 'New Jersey', + language_en: 'en', + text: 'New Jersey', + language: 'en', + text_de: 'New Jersey', + language_de: 'de', + text_fr: 'New Jersey', + language_fr: 'fr', + text_nl: 'New Jersey', + language_nl: 'nl', + text_it: 'New Jersey', + language_it: 'it', + text_es: 'Nueva Jersey', + language_es: 'es', + text_pt: 'Nova Jérsia', + language_pt: 'pt', + text_pl: 'New Jersey', + language_pl: 'pl', + text_ru: 'Нью-Джерси', + language_ru: 'ru', + }, + { + id: 'country.8940', + mapbox_id: 'dXJuOm1ieHBsYzpJdXc', + wikidata: 'Q30', + short_code: 'us', + text_en: 'United States', + language_en: 'en', + text: 'United States', + language: 'en', + text_de: 'Vereinigte Staaten', + language_de: 'de', + text_fr: 'États-Unis', + language_fr: 'fr', + text_nl: 'Verenigde Staten van Amerika', + language_nl: 'nl', + text_it: "Stati Uniti d'America", + language_it: 'it', + text_es: 'Estados Unidos', + language_es: 'es', + text_pt: 'Estados Unidos', + language_pt: 'pt', + text_pl: 'Stany Zjednoczone', + language_pl: 'pl', + text_ru: 'США', + language_ru: 'ru', + }, + ], + }, + ], + attribution: + 'NOTICE: © 2025 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained.', +} diff --git a/backend/test/fetchMock/index.ts b/backend/test/fetchMock/index.ts new file mode 100644 index 000000000..e5915b648 --- /dev/null +++ b/backend/test/fetchMock/index.ts @@ -0,0 +1,11 @@ +import type { Context } from '@src/context' + +import { mapboxResponses } from './mapboxResponses' + +export const fetchMock: Context['fetch'] = (url) => { + const response: unknown = mapboxResponses[url] // eslint-disable-line security/detect-object-injection + if (!response) { + throw new Error(`Missing response for url: ${url}`) + } + return Promise.resolve(response) +} diff --git a/backend/test/fetchMock/leipzig.ts b/backend/test/fetchMock/leipzig.ts new file mode 100644 index 000000000..419cbc256 --- /dev/null +++ b/backend/test/fetchMock/leipzig.ts @@ -0,0 +1,653 @@ +export const leipzig = { + type: 'FeatureCollection', + query: ['leipzig'], + features: [ + { + id: 'place.45697082', + type: 'Feature', + place_type: ['place'], + relevance: 1, + properties: { + mapbox_id: 'dXJuOm1ieHBsYzpBcmxJT2c', + wikidata: 'Q2079', + }, + text_en: 'Leipzig', + language_en: 'en', + place_name_en: 'Leipzig, Saxony, Germany', + text: 'Leipzig', + language: 'en', + place_name: 'Leipzig, Saxony, Germany', + text_de: 'Leipzig', + language_de: 'de', + place_name_de: 'Leipzig, Sachsen, Deutschland', + text_fr: 'Leipzig', + language_fr: 'fr', + place_name_fr: 'Leipzig, Saxe, Allemagne', + text_nl: 'Leipzig', + language_nl: 'nl', + place_name_nl: 'Leipzig, Saksen, Duitsland', + text_it: 'Lipsia', + language_it: 'it', + place_name_it: 'Lipsia, Sassonia, Germania', + text_es: 'Leipzig', + language_es: 'es', + place_name_es: 'Leipzig, Sajonia, Alemania', + text_pt: 'Leipzig', + language_pt: 'pt', + place_name_pt: 'Leipzig, Saxónia, Alemanha', + text_pl: 'Lipsk', + language_pl: 'pl', + place_name_pl: 'Lipsk, Saksonia, Niemcy', + text_ru: 'Лейпциг', + language_ru: 'ru', + place_name_ru: 'Лейпциг, Саксония, Германия', + bbox: [12.236476, 51.23808, 12.542577, 51.447065], + center: [12.375101, 51.34083], + geometry: { + type: 'Point', + coordinates: [12.375101, 51.34083], + }, + context: [ + { + id: 'region.74810', + mapbox_id: 'dXJuOm1ieHBsYzpBU1E2', + wikidata: 'Q1202', + short_code: 'DE-SN', + text_en: 'Saxony', + language_en: 'en', + text: 'Saxony', + language: 'en', + text_de: 'Sachsen', + language_de: 'de', + text_fr: 'Saxe', + language_fr: 'fr', + text_nl: 'Saksen', + language_nl: 'nl', + text_it: 'Sassonia', + language_it: 'it', + text_es: 'Sajonia', + language_es: 'es', + text_pt: 'Saxónia', + language_pt: 'pt', + text_pl: 'Saksonia', + language_pl: 'pl', + text_ru: 'Саксония', + language_ru: 'ru', + }, + { + id: 'country.8762', + mapbox_id: 'dXJuOm1ieHBsYzpJam8', + wikidata: 'Q183', + short_code: 'de', + text_en: 'Germany', + language_en: 'en', + text: 'Germany', + language: 'en', + text_de: 'Deutschland', + language_de: 'de', + text_fr: 'Allemagne', + language_fr: 'fr', + text_nl: 'Duitsland', + language_nl: 'nl', + text_it: 'Germania', + language_it: 'it', + text_es: 'Alemania', + language_es: 'es', + text_pt: 'Alemanha', + language_pt: 'pt', + text_pl: 'Niemcy', + language_pl: 'pl', + text_ru: 'Германия', + language_ru: 'ru', + }, + ], + }, + { + id: 'place.18655359', + type: 'Feature', + place_type: ['place'], + relevance: 1, + properties: { + mapbox_id: 'dXJuOm1ieHBsYzpBUnlvZnc', + wikidata: 'Q4257515', + }, + text_en: 'Leipzig', + language_en: 'de', + place_name_en: 'Leipzig, Varnensky District, Chelyabinsk, Russia', + text: 'Leipzig', + language: 'de', + place_name: 'Leipzig, Varnensky District, Chelyabinsk, Russia', + text_de: 'Leipzig', + language_de: 'de', + place_name_de: 'Leipzig, Warnenski rajon, Oblast Tscheljabinsk, Russland', + text_fr: 'Leipzig', + language_fr: 'nl', + place_name_fr: 'Leipzig, Raïon de Varna, oblast de Tcheliabinsk, Russie', + text_nl: 'Leipzig', + language_nl: 'nl', + place_name_nl: 'Leipzig, Varnenskiy rayon, Oblast Tsjeljabinsk, Rusland', + text_it: 'Лейпциг', + place_name_it: "Лейпциг, Varnenskij rajon, oblast' di Čeljabinsk, Russia", + text_es: 'Leipzig', + language_es: 'nl', + place_name_es: 'Leipzig, Raïon de Varna, Cheliábinsk, Rusia', + text_pt: 'Лейпциг', + place_name_pt: 'Лейпциг, Varnenskij rajon, Oblast de Cheliabinsk, Rússia', + text_pl: 'Leipzig', + language_pl: 'nl', + place_name_pl: 'Leipzig, Varnenskiy rayon, Obwód czelabiński, Rosja', + text_ru: 'Лейпциг', + language_ru: 'ru', + place_name_ru: 'Лейпциг, Варненский район, Челябинская область, Россия', + center: [61.049587, 53.568829], + geometry: { + type: 'Point', + coordinates: [61.049587, 53.568829], + }, + context: [ + { + id: 'district.1967810', + mapbox_id: 'dXJuOm1ieHBsYzpIZ2JD', + wikidata: 'Q1658354', + text_en: 'Varnensky District', + language_en: 'en', + text: 'Varnensky District', + language: 'en', + text_de: 'Warnenski rajon', + language_de: 'de', + text_fr: 'Raïon de Varna', + language_fr: 'fr', + text_nl: 'Varnenskiy rayon', + language_nl: 'nl', + text_it: 'Varnenskij rajon', + language_it: 'it', + text_es: 'Raïon de Varna', + language_es: 'fr', + text_pt: 'Varnenskij rajon', + language_pt: 'it', + text_pl: 'Varnenskiy rayon', + language_pl: 'nl', + text_ru: 'Варненский район', + language_ru: 'ru', + }, + { + id: 'region.468162', + mapbox_id: 'dXJuOm1ieHBsYzpCeVRD', + wikidata: 'Q5714', + short_code: 'RU-CHE', + text_en: 'Chelyabinsk', + language_en: 'en', + text: 'Chelyabinsk', + language: 'en', + text_de: 'Oblast Tscheljabinsk', + language_de: 'de', + text_fr: 'oblast de Tcheliabinsk', + language_fr: 'fr', + text_nl: 'Oblast Tsjeljabinsk', + language_nl: 'nl', + text_it: "oblast' di Čeljabinsk", + language_it: 'it', + text_es: 'Cheliábinsk', + language_es: 'es', + text_pt: 'Oblast de Cheliabinsk', + language_pt: 'pt', + text_pl: 'Obwód czelabiński', + language_pl: 'pl', + text_ru: 'Челябинская область', + language_ru: 'ru', + }, + { + id: 'country.8898', + mapbox_id: 'dXJuOm1ieHBsYzpJc0k', + wikidata: 'Q159', + short_code: 'ru', + text_en: 'Russia', + language_en: 'en', + text: 'Russia', + language: 'en', + text_de: 'Russland', + language_de: 'de', + text_fr: 'Russie', + language_fr: 'fr', + text_nl: 'Rusland', + language_nl: 'nl', + text_it: 'Russia', + language_it: 'it', + text_es: 'Rusia', + language_es: 'es', + text_pt: 'Rússia', + language_pt: 'pt', + text_pl: 'Rosja', + language_pl: 'pl', + text_ru: 'Россия', + language_ru: 'ru', + }, + ], + }, + { + id: 'address.7058830234246124', + type: 'Feature', + place_type: ['address'], + relevance: 1, + properties: { + accuracy: 'street', + mapbox_id: 'dXJuOm1ieGFkci1zdHI6OGQ4MmQ0YmYtNWViYy00OTY2LWE1MTMtNDlkYzNiNDBhZjhm', + }, + text_en: 'Leipzig Way', + place_name_en: 'Leipzig Way, Greenwith South Australia 5125, Australia', + text: 'Leipzig Way', + place_name: 'Leipzig Way, Greenwith South Australia 5125, Australia', + text_de: 'Leipzig Way', + place_name_de: 'Leipzig Way, Greenwith South Australia 5125, Australien', + text_fr: 'Leipzig Way', + place_name_fr: 'Leipzig Way, Greenwith Australie-Méridionale 5125, Australie', + text_nl: 'Leipzig Way', + place_name_nl: 'Leipzig Way, Greenwith Zuid-Australië 5125, Australië', + text_it: 'Leipzig Way', + place_name_it: 'Leipzig Way, Greenwith Australia Meridionale 5125, Australia', + text_es: 'Leipzig Way', + place_name_es: 'Leipzig Way, Greenwith Australia Meridional 5125, Australia', + text_pt: 'Leipzig Way', + place_name_pt: 'Leipzig Way, Greenwith Austrália Meridional 5125, Austrália', + text_pl: 'Leipzig Way', + place_name_pl: 'Leipzig Way, Greenwith Australia Południowa 5125, Australia', + text_ru: 'Leipzig Way', + place_name_ru: 'Leipzig Way, Greenwith Южная Австралия 5125, Австралия', + center: [138.709402, -34.765821], + geometry: { + type: 'Point', + coordinates: [138.709402, -34.765821], + }, + context: [ + { + id: 'postcode.15560206', + mapbox_id: 'dXJuOm1ieHBsYzo3VzRP', + text_en: '5125', + text: '5125', + text_de: '5125', + text_fr: '5125', + text_nl: '5125', + text_it: '5125', + text_es: '5125', + text_pt: '5125', + text_pl: '5125', + text_ru: '5125', + }, + { + id: 'locality.265447950', + mapbox_id: 'dXJuOm1ieHBsYzpEOUpxRGc', + wikidata: 'Q5604921', + text_en: 'Greenwith', + language_en: 'en', + text: 'Greenwith', + language: 'en', + text_de: 'Greenwith', + language_de: 'en', + text_fr: 'Greenwith', + language_fr: 'fr', + text_nl: 'Greenwith', + language_nl: 'fr', + text_it: 'Greenwith', + language_it: 'fr', + text_es: 'Greenwith', + language_es: 'fr', + text_pt: 'Greenwith', + text_pl: 'Greenwith', + language_pl: 'en', + text_ru: 'Greenwith', + }, + { + id: 'place.51214', + mapbox_id: 'dXJuOm1ieHBsYzp5QTQ', + wikidata: 'Q5112', + text_en: 'Adelaide', + language_en: 'en', + text: 'Adelaide', + language: 'en', + text_de: 'Adelaide', + language_de: 'de', + text_fr: 'Adélaïde', + language_fr: 'fr', + text_nl: 'Adelaide', + language_nl: 'nl', + text_it: 'Adelaide', + language_it: 'it', + text_es: 'Adelaida', + language_es: 'es', + text_pt: 'Adelaide', + language_pt: 'pt', + text_pl: 'Adelaide', + language_pl: 'pl', + text_ru: 'Аделаида', + language_ru: 'ru', + }, + { + id: 'region.66574', + mapbox_id: 'dXJuOm1ieHBsYzpBUVFP', + wikidata: 'Q35715', + short_code: 'AU-SA', + text_en: 'South Australia', + language_en: 'en', + text: 'South Australia', + language: 'en', + text_de: 'South Australia', + language_de: 'de', + text_fr: 'Australie-Méridionale', + language_fr: 'fr', + text_nl: 'Zuid-Australië', + language_nl: 'nl', + text_it: 'Australia Meridionale', + language_it: 'it', + text_es: 'Australia Meridional', + language_es: 'es', + text_pt: 'Austrália Meridional', + language_pt: 'pt', + text_pl: 'Australia Południowa', + language_pl: 'pl', + text_ru: 'Южная Австралия', + language_ru: 'ru', + }, + { + id: 'country.8718', + mapbox_id: 'dXJuOm1ieHBsYzpJZzQ', + wikidata: 'Q408', + short_code: 'au', + text_en: 'Australia', + language_en: 'en', + text: 'Australia', + language: 'en', + text_de: 'Australien', + language_de: 'de', + text_fr: 'Australie', + language_fr: 'fr', + text_nl: 'Australië', + language_nl: 'nl', + text_it: 'Australia', + language_it: 'it', + text_es: 'Australia', + language_es: 'es', + text_pt: 'Austrália', + language_pt: 'pt', + text_pl: 'Australia', + language_pl: 'pl', + text_ru: 'Австралия', + language_ru: 'ru', + }, + ], + }, + { + id: 'address.3203988662803706', + type: 'Feature', + place_type: ['address'], + relevance: 1, + properties: { + accuracy: 'street', + mapbox_id: 'dXJuOm1ieGFkci1zdHI6NGUzNzNkZjItNmI4Ny00ODdkLWI3MzQtZjVkMGJlMjI1NjA5', + }, + text_en: 'Leipzig', + place_name_en: 'Leipzig, Osorno, Los Lagos 5290000, Chile', + text: 'Leipzig', + place_name: 'Leipzig, Osorno, Los Lagos 5290000, Chile', + text_de: 'Leipzig', + place_name_de: 'Leipzig, Osorno, Región de Los Lagos 5290000, Chile', + text_fr: 'Leipzig', + place_name_fr: 'Leipzig, Osorno, Région des Lacs 5290000, Chili', + text_nl: 'Leipzig', + place_name_nl: 'Leipzig, Osorno, Los Lagos 5290000, Chili', + text_it: 'Leipzig', + place_name_it: 'Leipzig, Osorno, regione di Los Lagos 5290000, Cile', + text_es: 'Leipzig', + place_name_es: 'Leipzig, Osorno, Región de Los Lagos 5290000, Chile', + text_pt: 'Leipzig', + place_name_pt: 'Leipzig, Osorno, Região de Los Lagos 5290000, Chile', + text_pl: 'Leipzig', + place_name_pl: 'Leipzig, Osorno, Los Lagos 5290000, Chile', + text_ru: 'Leipzig', + place_name_ru: 'Leipzig, Осорно, Лос-Лагос 5290000, Чили', + center: [-73.1087, -40.570176], + geometry: { + type: 'Point', + coordinates: [-73.1087, -40.570176], + }, + context: [ + { + id: 'postcode.2002479', + mapbox_id: 'dXJuOm1ieHBsYzpIbzR2', + text_en: '5290000', + text: '5290000', + text_de: '5290000', + text_fr: '5290000', + text_nl: '5290000', + text_it: '5290000', + text_es: '5290000', + text_pt: '5290000', + text_pl: '5290000', + text_ru: '5290000', + }, + { + id: 'locality.2656815', + mapbox_id: 'dXJuOm1ieHBsYzpLSW92', + text_en: 'Cementerio Municipal', + text: 'Cementerio Municipal', + text_de: 'Cementerio Municipal', + text_fr: 'Cementerio Municipal', + text_nl: 'Cementerio Municipal', + text_it: 'Cementerio Municipal', + text_es: 'Cementerio Municipal', + text_pt: 'Cementerio Municipal', + text_pl: 'Cementerio Municipal', + text_ru: 'Cementerio Municipal', + }, + { + id: 'place.1574959', + mapbox_id: 'dXJuOm1ieHBsYzpHQWd2', + wikidata: 'Q51059', + text_en: 'Osorno', + language_en: 'en', + text: 'Osorno', + language: 'en', + text_de: 'Osorno', + language_de: 'de', + text_fr: 'Osorno', + language_fr: 'fr', + text_nl: 'Osorno', + language_nl: 'nl', + text_it: 'Osorno', + language_it: 'it', + text_es: 'Osorno', + language_es: 'es', + text_pt: 'Osorno', + language_pt: 'pt', + text_pl: 'Osorno', + language_pl: 'pl', + text_ru: 'Осорно', + language_ru: 'ru', + }, + { + id: 'region.99375', + mapbox_id: 'dXJuOm1ieHBsYzpBWVF2', + wikidata: 'Q2178', + short_code: 'CL-LL', + text_en: 'Los Lagos', + language_en: 'en', + text: 'Los Lagos', + language: 'en', + text_de: 'Región de Los Lagos', + language_de: 'de', + text_fr: 'Région des Lacs', + language_fr: 'fr', + text_nl: 'Los Lagos', + language_nl: 'nl', + text_it: 'regione di Los Lagos', + language_it: 'it', + text_es: 'Región de Los Lagos', + language_es: 'es', + text_pt: 'Região de Los Lagos', + language_pt: 'pt', + text_pl: 'Los Lagos', + language_pl: 'pl', + text_ru: 'Лос-Лагос', + language_ru: 'ru', + }, + { + id: 'country.8751', + mapbox_id: 'dXJuOm1ieHBsYzpJaTg', + wikidata: 'Q298', + short_code: 'cl', + text_en: 'Chile', + language_en: 'en', + text: 'Chile', + language: 'en', + text_de: 'Chile', + language_de: 'de', + text_fr: 'Chili', + language_fr: 'fr', + text_nl: 'Chili', + language_nl: 'nl', + text_it: 'Cile', + language_it: 'it', + text_es: 'Chile', + language_es: 'es', + text_pt: 'Chile', + language_pt: 'pt', + text_pl: 'Chile', + language_pl: 'pl', + text_ru: 'Чили', + language_ru: 'ru', + }, + ], + }, + { + id: 'address.7521078080010634', + type: 'Feature', + place_type: ['address'], + relevance: 1, + properties: { + accuracy: 'street', + mapbox_id: 'dXJuOm1ieGFkci1zdHI6OTFkZjliYjctNWJiZC00YTk3LTk1ZjktOTZjMGVkNzU3MjY3', + }, + text_en: 'Leipziger Straße', + language_en: 'de', + place_name_en: 'Leipziger Straße, 43003 Chomutov, Ústí nad Labem, Czech Republic', + text: 'Leipziger Straße', + language: 'de', + place_name: 'Leipziger Straße, 43003 Chomutov, Ústí nad Labem, Czech Republic', + text_de: 'Leipziger Straße', + language_de: 'de', + place_name_de: 'Leipziger Straße, 43003 Chomutov, Ústecký kraj, Tschechien', + text_fr: 'Lipská', + place_name_fr: "Lipská, 43003 Chomutov, région d'Ústí nad Labem, Tchéquie", + text_nl: 'Lipská', + place_name_nl: 'Lipská, 43003 Chomutov, Ústí nad Labem, Tsjechië', + text_it: 'Lipská', + place_name_it: 'Lipská, 43003 Chomutov, regione di Ústí nad Labem, Repubblica Ceca', + text_es: 'Lipská', + place_name_es: 'Lipská, 43003 Chomutov, Región de Ústí nad Labem, República Checa', + text_pt: 'Lipská', + place_name_pt: 'Lipská, 43003 Chomutov, Ústí nad Labem, Chéquia', + text_pl: 'Lipská', + place_name_pl: 'Lipská, 43003 Chomutov, Kraj ustecki, Czechy', + text_ru: 'Lipská', + place_name_ru: 'Lipská, 43003 Хомутов, Устецкий край, Чехия', + center: [13.385229, 50.472246], + geometry: { + type: 'Point', + coordinates: [13.385229, 50.472246], + }, + context: [ + { + id: 'postcode.7521078080010634', + text_en: '43003', + text: '43003', + text_de: '43003', + text_fr: '43003', + text_nl: '43003', + text_it: '43003', + text_es: '43003', + text_pt: '43003', + text_pl: '43003', + text_ru: '43003', + }, + { + id: 'place.6162489', + mapbox_id: 'dXJuOm1ieHBsYzpYZ2c1', + wikidata: 'Q146356', + text_en: 'Chomutov', + language_en: 'en', + text: 'Chomutov', + language: 'en', + text_de: 'Chomutov', + language_de: 'de', + text_fr: 'Chomutov', + language_fr: 'fr', + text_nl: 'Chomutov', + language_nl: 'nl', + text_it: 'Chomutov', + language_it: 'it', + text_es: 'Chomutov', + language_es: 'es', + text_pt: 'Chomutov', + language_pt: 'pt', + text_pl: 'Chomutov', + language_pl: 'pl', + text_ru: 'Хомутов', + language_ru: 'ru', + }, + { + id: 'region.74809', + mapbox_id: 'dXJuOm1ieHBsYzpBU1E1', + wikidata: 'Q192702', + short_code: 'CZ-42', + text_en: 'Ústí nad Labem', + language_en: 'en', + text: 'Ústí nad Labem', + language: 'en', + text_de: 'Ústecký kraj', + language_de: 'de', + text_fr: "région d'Ústí nad Labem", + language_fr: 'fr', + text_nl: 'Ústí nad Labem', + language_nl: 'nl', + text_it: 'regione di Ústí nad Labem', + language_it: 'it', + text_es: 'Región de Ústí nad Labem', + language_es: 'es', + text_pt: 'Ústí nad Labem', + language_pt: 'pt', + text_pl: 'Kraj ustecki', + language_pl: 'pl', + text_ru: 'Устецкий край', + language_ru: 'ru', + }, + { + id: 'country.8761', + mapbox_id: 'dXJuOm1ieHBsYzpJams', + wikidata: 'Q213', + short_code: 'cz', + text_en: 'Czech Republic', + language_en: 'en', + text: 'Czech Republic', + language: 'en', + text_de: 'Tschechien', + language_de: 'de', + text_fr: 'Tchéquie', + language_fr: 'fr', + text_nl: 'Tsjechië', + language_nl: 'nl', + text_it: 'Repubblica Ceca', + language_it: 'it', + text_es: 'República Checa', + language_es: 'es', + text_pt: 'Chéquia', + language_pt: 'pt', + text_pl: 'Czechy', + language_pl: 'pl', + text_ru: 'Чехия', + language_ru: 'ru', + }, + ], + }, + ], + attribution: + 'NOTICE: © 2025 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained.', +} diff --git a/backend/test/fetchMock/mapboxResponses.ts b/backend/test/fetchMock/mapboxResponses.ts new file mode 100644 index 000000000..b813c38e8 --- /dev/null +++ b/backend/test/fetchMock/mapboxResponses.ts @@ -0,0 +1,32 @@ +import { berlinDe } from './berlinDe' +import { berlinEn } from './berlinEn' +import { berlinGermany } from './berlinGermany' +import { empty } from './empty' +import { hamburg } from './hamburg' +import { hamburgNY } from './hamburgNY' +import { leipzig } from './leipzig' +import { paris } from './paris' +import { welzheim } from './welzheim' + +export const mapboxResponses = { + 'https://api.mapbox.com/geocoding/v5/mapbox.places/Berlin.json?access_token=MAPBOX_TOKEN&types=region,place,country&language=en': + berlinEn, + 'https://api.mapbox.com/geocoding/v5/mapbox.places/Berlin.json?access_token=MAPBOX_TOKEN&types=region,place,country&language=de': + berlinDe, + 'https://api.mapbox.com/geocoding/v5/mapbox.places/Berlin%2C%20Germany.json?access_token=MAPBOX_TOKEN&types=region,place,country,address&language=en,de,fr,nl,it,es,pt,pl,ru': + berlinGermany, + 'https://api.mapbox.com/geocoding/v5/mapbox.places/GbHtsd4sdHa.json?access_token=MAPBOX_TOKEN&types=region,place,country&language=en': + empty, + 'https://api.mapbox.com/geocoding/v5/mapbox.places/.json?access_token=MAPBOX_TOKEN&types=region,place,country&language=en': + empty, + 'https://api.mapbox.com/geocoding/v5/mapbox.places/Welzheim%2C%20Baden-W%C3%BCrttemberg%2C%20Germany.json?access_token=MAPBOX_TOKEN&types=region,place,country,address&language=en,de,fr,nl,it,es,pt,pl,ru': + welzheim, + 'https://api.mapbox.com/geocoding/v5/mapbox.places/Hamburg%2C%20Germany.json?access_token=MAPBOX_TOKEN&types=region,place,country,address&language=en,de,fr,nl,it,es,pt,pl,ru': + hamburg, + 'https://api.mapbox.com/geocoding/v5/mapbox.places/Hamburg%2C%20New%20Jersey%2C%20United%20States.json?access_token=MAPBOX_TOKEN&types=region,place,country,address&language=en,de,fr,nl,it,es,pt,pl,ru': + hamburgNY, + 'https://api.mapbox.com/geocoding/v5/mapbox.places/Leipzig.json?access_token=MAPBOX_TOKEN&types=region,place,country,address&language=en,de,fr,nl,it,es,pt,pl,ru': + leipzig, + 'https://api.mapbox.com/geocoding/v5/mapbox.places/Paris%2C%20France.json?access_token=MAPBOX_TOKEN&types=region,place,country,address&language=en,de,fr,nl,it,es,pt,pl,ru': + paris, +} as const diff --git a/backend/test/fetchMock/paris.ts b/backend/test/fetchMock/paris.ts new file mode 100644 index 000000000..b331059f0 --- /dev/null +++ b/backend/test/fetchMock/paris.ts @@ -0,0 +1,615 @@ +export const paris = { + type: 'FeatureCollection', + query: ['paris', 'france'], + features: [ + { + id: 'place.894029', + type: 'Feature', + place_type: ['region', 'place'], + relevance: 1, + properties: { + mapbox_id: 'dXJuOm1ieHBsYzpEYVJO', + wikidata: 'Q90', + short_code: 'FR-75', + }, + text_en: 'Paris', + language_en: 'en', + place_name_en: 'Paris, France', + text: 'Paris', + language: 'en', + place_name: 'Paris, France', + text_de: 'Paris', + language_de: 'de', + place_name_de: 'Paris, Frankreich', + text_fr: 'Paris', + language_fr: 'fr', + place_name_fr: 'Paris, France', + text_nl: 'Parijs', + language_nl: 'nl', + place_name_nl: 'Parijs, Frankrijk', + text_it: 'Parigi', + language_it: 'it', + place_name_it: 'Parigi, Francia', + text_es: 'París', + language_es: 'es', + place_name_es: 'París, Francia', + text_pt: 'Paris', + language_pt: 'pt', + place_name_pt: 'Paris, França', + text_pl: 'Paryż', + language_pl: 'pl', + place_name_pl: 'Paryż, Francja', + text_ru: 'Париж', + language_ru: 'ru', + place_name_ru: 'Париж, Франция', + bbox: [2.224229, 48.815562, 2.469851, 48.902148], + center: [2.348392, 48.853495], + geometry: { + type: 'Point', + coordinates: [2.348392, 48.853495], + }, + context: [ + { + id: 'country.8781', + mapbox_id: 'dXJuOm1ieHBsYzpJazA', + wikidata: 'Q142', + short_code: 'fr', + text_en: 'France', + language_en: 'en', + text: 'France', + language: 'en', + text_de: 'Frankreich', + language_de: 'de', + text_fr: 'France', + language_fr: 'fr', + text_nl: 'Frankrijk', + language_nl: 'nl', + text_it: 'Francia', + language_it: 'it', + text_es: 'Francia', + language_es: 'es', + text_pt: 'França', + language_pt: 'pt', + text_pl: 'Francja', + language_pl: 'pl', + text_ru: 'Франция', + language_ru: 'ru', + }, + ], + }, + { + id: 'address.7942827143743488', + type: 'Feature', + place_type: ['address'], + relevance: 1, + properties: { + accuracy: 'street', + mapbox_id: 'dXJuOm1ieGFkci1zdHI6ZGUyMmI5NjYtODFjMi00YjA4LWJkODgtMGIxNWE2YWE0Y2I2', + }, + text_en: 'Paris', + place_name_en: 'Paris, 83170 Brignoles, France', + text: 'Paris', + place_name: 'Paris, 83170 Brignoles, France', + text_de: 'Paris', + place_name_de: 'Paris, 83170 Brignoles, Frankreich', + text_fr: 'Paris', + place_name_fr: 'Paris, 83170 Brignoles, France', + text_nl: 'Paris', + place_name_nl: 'Paris, 83170 Brignoles, Frankrijk', + text_it: 'Paris', + place_name_it: 'Paris, 83170 Brignoles, Francia', + text_es: 'Paris', + place_name_es: 'Paris, 83170 Brignoles, Francia', + text_pt: 'Paris', + place_name_pt: 'Paris, 83170 Brignoles, França', + text_pl: 'Paris', + place_name_pl: 'Paris, 83170 Brignoles, Francja', + text_ru: 'Paris', + place_name_ru: 'Paris, 83170 Бриньоль, Франция', + center: [6.069561, 43.415211], + geometry: { + type: 'Point', + coordinates: [6.069561, 43.415211], + }, + context: [ + { + id: 'neighborhood.23768141', + mapbox_id: 'dXJuOm1ieHBsYzpBV3FzVFE', + text_en: 'Extension Annees 80-', + language_en: 'fr', + text: 'Extension Annees 80-', + language: 'fr', + text_de: 'Extension Annees 80-', + text_fr: 'Extension Annees 80-', + language_fr: 'fr', + text_nl: 'Extension Annees 80-', + language_nl: 'fr', + text_it: 'Extension Annees 80-', + language_it: 'fr', + text_es: 'Extension Annees 80-', + language_es: 'fr', + text_pt: 'Extension Annees 80-', + text_pl: 'Extension Annees 80-', + text_ru: 'Extension Annees 80-', + }, + { + id: 'postcode.43822669', + mapbox_id: 'dXJuOm1ieHBsYzpBcHl1VFE', + text_en: '83170', + text: '83170', + text_de: '83170', + text_fr: '83170', + text_nl: '83170', + text_it: '83170', + text_es: '83170', + text_pt: '83170', + text_pl: '83170', + text_ru: '83170', + }, + { + id: 'place.37988429', + mapbox_id: 'dXJuOm1ieHBsYzpBa09vVFE', + wikidata: 'Q207584', + text_en: 'Brignoles', + language_en: 'en', + text: 'Brignoles', + language: 'en', + text_de: 'Brignoles', + language_de: 'de', + text_fr: 'Brignoles', + language_fr: 'fr', + text_nl: 'Brignoles', + language_nl: 'nl', + text_it: 'Brignoles', + language_it: 'it', + text_es: 'Brignoles', + language_es: 'es', + text_pt: 'Brignoles', + language_pt: 'pt', + text_pl: 'Brignoles', + language_pl: 'pl', + text_ru: 'Бриньоль', + language_ru: 'ru', + }, + { + id: 'region.451661', + mapbox_id: 'dXJuOm1ieHBsYzpCdVJO', + wikidata: 'Q12789', + short_code: 'FR-83', + text_en: 'Var', + language_en: 'en', + text: 'Var', + language: 'en', + text_de: 'Département Var', + language_de: 'de', + text_fr: 'Var', + language_fr: 'fr', + text_nl: 'Var', + language_nl: 'nl', + text_it: 'Varo', + language_it: 'it', + text_es: 'Var', + language_es: 'es', + text_pt: 'Var', + language_pt: 'pt', + text_pl: 'Var', + language_pl: 'pl', + text_ru: 'Вар', + language_ru: 'ru', + }, + { + id: 'country.8781', + mapbox_id: 'dXJuOm1ieHBsYzpJazA', + wikidata: 'Q142', + short_code: 'fr', + text_en: 'France', + language_en: 'en', + text: 'France', + language: 'en', + text_de: 'Frankreich', + language_de: 'de', + text_fr: 'France', + language_fr: 'fr', + text_nl: 'Frankrijk', + language_nl: 'nl', + text_it: 'Francia', + language_it: 'it', + text_es: 'Francia', + language_es: 'es', + text_pt: 'França', + language_pt: 'pt', + text_pl: 'Francja', + language_pl: 'pl', + text_ru: 'Франция', + language_ru: 'ru', + }, + ], + }, + { + id: 'address.1606141678135750', + type: 'Feature', + place_type: ['address'], + relevance: 1, + properties: { + accuracy: 'street', + mapbox_id: 'dXJuOm1ieGFkci1zdHI6YTI3YjI5ODYtNGI1ZS00MTBiLThkOTEtOWJjOGUzZmE4NjA0', + }, + text_en: 'Paris', + place_name_en: "Paris, 11230 Sainte-Colombe-sur-l'Hers, France", + text: 'Paris', + place_name: "Paris, 11230 Sainte-Colombe-sur-l'Hers, France", + text_de: 'Paris', + place_name_de: "Paris, 11230 Sainte-Colombe-sur-l'Hers, Frankreich", + text_fr: 'Paris', + place_name_fr: "Paris, 11230 Sainte-Colombe-sur-l'Hers, France", + text_nl: 'Paris', + place_name_nl: "Paris, 11230 Sainte-Colombe-sur-l'Hers, Frankrijk", + text_it: 'Paris', + place_name_it: "Paris, 11230 Sainte-Colombe-sur-l'Hers, Francia", + text_es: 'Paris', + place_name_es: "Paris, 11230 Sainte-Colombe-sur-l'Hers, Francia", + text_pt: 'Paris', + place_name_pt: "Paris, 11230 Sainte-Colombe-sur-l'Hers, França", + text_pl: 'Paris', + place_name_pl: "Paris, 11230 Sainte-Colombe-sur-l'Hers, Francja", + text_ru: 'Paris', + place_name_ru: 'Paris, 11230 Сент-Коломб-сюр-л’Эр, Франция', + center: [1.957948, 42.947813], + geometry: { + type: 'Point', + coordinates: [1.957948, 42.947813], + }, + context: [ + { + id: 'postcode.3993165', + mapbox_id: 'dXJuOm1ieHBsYzpQTzVO', + text_en: '11230', + text: '11230', + text_de: '11230', + text_fr: '11230', + text_nl: '11230', + text_it: '11230', + text_es: '11230', + text_pt: '11230', + text_pl: '11230', + text_ru: '11230', + }, + { + id: 'place.218073165', + mapbox_id: 'dXJuOm1ieHBsYzpEUCtJVFE', + wikidata: 'Q1081836', + text_en: "Sainte-Colombe-sur-l'Hers", + language_en: 'en', + text: "Sainte-Colombe-sur-l'Hers", + language: 'en', + text_de: "Sainte-Colombe-sur-l'Hers", + language_de: 'de', + text_fr: "Sainte-Colombe-sur-l'Hers", + language_fr: 'fr', + text_nl: "Sainte-Colombe-sur-l'Hers", + language_nl: 'nl', + text_it: "Sainte-Colombe-sur-l'Hers", + language_it: 'it', + text_es: "Sainte-Colombe-sur-l'Hers", + language_es: 'es', + text_pt: "Sainte-Colombe-sur-l'Hers", + language_pt: 'pt', + text_pl: "Sainte-Colombe-sur-l'Hers", + language_pl: 'pl', + text_ru: 'Сент-Коломб-сюр-л’Эр', + language_ru: 'ru', + }, + { + id: 'region.713805', + mapbox_id: 'dXJuOm1ieHBsYzpDdVJO', + wikidata: 'Q3207', + short_code: 'FR-11', + text_en: 'Aude', + language_en: 'en', + text: 'Aude', + language: 'en', + text_de: 'Département Aude', + language_de: 'de', + text_fr: 'Aude', + language_fr: 'fr', + text_nl: 'Aude', + language_nl: 'nl', + text_it: 'Aude', + language_it: 'it', + text_es: 'Aude', + language_es: 'es', + text_pt: 'Aude', + language_pt: 'pt', + text_pl: 'Aude', + language_pl: 'pl', + text_ru: 'Од', + language_ru: 'ru', + }, + { + id: 'country.8781', + mapbox_id: 'dXJuOm1ieHBsYzpJazA', + wikidata: 'Q142', + short_code: 'fr', + text_en: 'France', + language_en: 'en', + text: 'France', + language: 'en', + text_de: 'Frankreich', + language_de: 'de', + text_fr: 'France', + language_fr: 'fr', + text_nl: 'Frankrijk', + language_nl: 'nl', + text_it: 'Francia', + language_it: 'it', + text_es: 'Francia', + language_es: 'es', + text_pt: 'França', + language_pt: 'pt', + text_pl: 'Francja', + language_pl: 'pl', + text_ru: 'Франция', + language_ru: 'ru', + }, + ], + }, + { + id: 'address.8644476532166668', + type: 'Feature', + place_type: ['address'], + relevance: 1, + properties: { + accuracy: 'street', + mapbox_id: 'dXJuOm1ieGFkci1zdHI6MTY1YmU5ZTAtMWZiMS00MjhhLThlZmQtN2NkMDVhZjZkNzE3', + }, + text_en: 'Paris', + place_name_en: 'Paris, 36120 Jeu-les-Bois, France', + text: 'Paris', + place_name: 'Paris, 36120 Jeu-les-Bois, France', + text_de: 'Paris', + place_name_de: 'Paris, 36120 Jeu-les-Bois, Frankreich', + text_fr: 'Paris', + place_name_fr: 'Paris, 36120 Jeu-les-Bois, France', + text_nl: 'Paris', + place_name_nl: 'Paris, 36120 Jeu-les-Bois, Frankrijk', + text_it: 'Paris', + place_name_it: 'Paris, 36120 Jeu-les-Bois, Francia', + text_es: 'Paris', + place_name_es: 'Paris, 36120 Jeu-les-Bois, Francia', + text_pt: 'Paris', + place_name_pt: 'Paris, 36120 Jeu-les-Bois, França', + text_pl: 'Paris', + place_name_pl: 'Paris, 36120 Jeu-les-Bois, Francja', + text_ru: 'Paris', + place_name_ru: 'Paris, 36120 Же-ле-Буа, Франция', + center: [1.811749, 46.694191], + geometry: { + type: 'Point', + coordinates: [1.811749, 46.694191], + }, + context: [ + { + id: 'postcode.17755725', + mapbox_id: 'dXJuOm1ieHBsYzpBUTd1VFE', + text_en: '36120', + text: '36120', + text_de: '36120', + text_fr: '36120', + text_nl: '36120', + text_it: '36120', + text_es: '36120', + text_pt: '36120', + text_pl: '36120', + text_ru: '36120', + }, + { + id: 'place.111659085', + mapbox_id: 'dXJuOm1ieHBsYzpCcWZJVFE', + wikidata: 'Q596397', + text_en: 'Jeu-les-Bois', + language_en: 'en', + text: 'Jeu-les-Bois', + language: 'en', + text_de: 'Jeu-les-Bois', + language_de: 'de', + text_fr: 'Jeu-les-Bois', + language_fr: 'fr', + text_nl: 'Jeu-les-Bois', + language_nl: 'nl', + text_it: 'Jeu-les-Bois', + language_it: 'it', + text_es: 'Jeu-les-Bois', + language_es: 'es', + text_pt: 'Jeu-les-Bois', + language_pt: 'pt', + text_pl: 'Jeu-les-Bois', + language_pl: 'pl', + text_ru: 'Же-ле-Буа', + language_ru: 'ru', + }, + { + id: 'region.74829', + mapbox_id: 'dXJuOm1ieHBsYzpBU1JO', + wikidata: 'Q12553', + short_code: 'FR-36', + text_en: 'Indre', + language_en: 'en', + text: 'Indre', + language: 'en', + text_de: 'Département Indre', + language_de: 'de', + text_fr: 'Indre', + language_fr: 'fr', + text_nl: 'Indre', + language_nl: 'nl', + text_it: 'Indre', + language_it: 'it', + text_es: 'Indre', + language_es: 'es', + text_pt: 'Indre', + language_pt: 'pt', + text_pl: 'Indre', + language_pl: 'pl', + text_ru: 'Эндр', + language_ru: 'ru', + }, + { + id: 'country.8781', + mapbox_id: 'dXJuOm1ieHBsYzpJazA', + wikidata: 'Q142', + short_code: 'fr', + text_en: 'France', + language_en: 'en', + text: 'France', + language: 'en', + text_de: 'Frankreich', + language_de: 'de', + text_fr: 'France', + language_fr: 'fr', + text_nl: 'Frankrijk', + language_nl: 'nl', + text_it: 'Francia', + language_it: 'it', + text_es: 'Francia', + language_es: 'es', + text_pt: 'França', + language_pt: 'pt', + text_pl: 'Francja', + language_pl: 'pl', + text_ru: 'Франция', + language_ru: 'ru', + }, + ], + }, + { + id: 'address.8865196537941870', + type: 'Feature', + place_type: ['address'], + relevance: 1, + properties: { + accuracy: 'street', + mapbox_id: 'dXJuOm1ieGFkci1zdHI6NjU2N2E5ODgtN2I4Yi00ZjU2LWEzZWUtYjk4Y2YwMjQzY2Ey', + }, + text_en: 'Paris', + place_name_en: 'Paris, 31560 Saint-Léon, France', + text: 'Paris', + place_name: 'Paris, 31560 Saint-Léon, France', + text_de: 'Paris', + place_name_de: 'Paris, 31560 Saint-Léon, Frankreich', + text_fr: 'Paris', + place_name_fr: 'Paris, 31560 Saint-Léon, France', + text_nl: 'Paris', + place_name_nl: 'Paris, 31560 Saint-Léon, Frankrijk', + text_it: 'Paris', + place_name_it: 'Paris, 31560 Saint-Léon, Francia', + text_es: 'Paris', + place_name_es: 'Paris, 31560 Saint-Léon, Francia', + text_pt: 'Paris', + place_name_pt: 'Paris, 31560 Saint-Léon, França', + text_pl: 'Paris', + place_name_pl: 'Paris, 31560 Saint-Léon, Francja', + text_ru: 'Paris', + place_name_ru: 'Paris, 31560 Сен-Леон, Франция', + center: [1.549305, 43.419569], + geometry: { + type: 'Point', + coordinates: [1.549305, 43.419569], + }, + context: [ + { + id: 'postcode.15044173', + mapbox_id: 'dXJuOm1ieHBsYzo1WTVO', + text_en: '31560', + text: '31560', + text_de: '31560', + text_fr: '31560', + text_nl: '31560', + text_it: '31560', + text_es: '31560', + text_pt: '31560', + text_pl: '31560', + text_ru: '31560', + }, + { + id: 'place.229107789', + mapbox_id: 'dXJuOm1ieHBsYzpEYWZvVFE', + wikidata: 'Q1362198', + text_en: 'Saint-Léon', + language_en: 'en', + text: 'Saint-Léon', + language: 'en', + text_de: 'Saint-Léon', + language_de: 'de', + text_fr: 'Saint-Léon', + language_fr: 'fr', + text_nl: 'Saint-Léon', + language_nl: 'nl', + text_it: 'Saint-Léon', + language_it: 'it', + text_es: 'Saint-Léon', + language_es: 'es', + text_pt: 'Saint-Léon', + language_pt: 'pt', + text_pl: 'Saint-Léon', + language_pl: 'pl', + text_ru: 'Сен-Леон', + language_ru: 'ru', + }, + { + id: 'region.42061', + mapbox_id: 'dXJuOm1ieHBsYzpwRTA', + wikidata: 'Q12538', + short_code: 'FR-31', + text_en: 'Haute-Garonne', + language_en: 'en', + text: 'Haute-Garonne', + language: 'en', + text_de: 'Haute-Garonne', + language_de: 'de', + text_fr: 'Haute-Garonne', + language_fr: 'fr', + text_nl: 'Haute-Garonne', + language_nl: 'nl', + text_it: 'Alta Garonna', + language_it: 'it', + text_es: 'Alto Garona', + language_es: 'es', + text_pt: 'Alta Garona', + language_pt: 'pt', + text_pl: 'Górna Garonna', + language_pl: 'pl', + text_ru: 'Верхняя Гаронна', + language_ru: 'ru', + }, + { + id: 'country.8781', + mapbox_id: 'dXJuOm1ieHBsYzpJazA', + wikidata: 'Q142', + short_code: 'fr', + text_en: 'France', + language_en: 'en', + text: 'France', + language: 'en', + text_de: 'Frankreich', + language_de: 'de', + text_fr: 'France', + language_fr: 'fr', + text_nl: 'Frankrijk', + language_nl: 'nl', + text_it: 'Francia', + language_it: 'it', + text_es: 'Francia', + language_es: 'es', + text_pt: 'França', + language_pt: 'pt', + text_pl: 'Francja', + language_pl: 'pl', + text_ru: 'Франция', + language_ru: 'ru', + }, + ], + }, + ], + attribution: + 'NOTICE: © 2025 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained.', +} diff --git a/backend/test/fetchMock/welzheim.ts b/backend/test/fetchMock/welzheim.ts new file mode 100644 index 000000000..17384146a --- /dev/null +++ b/backend/test/fetchMock/welzheim.ts @@ -0,0 +1,686 @@ +export const welzheim = { + type: 'FeatureCollection', + query: ['welzheim', 'baden', 'württemberg', 'germany'], + features: [ + { + id: 'place.85084218', + type: 'Feature', + place_type: ['place'], + relevance: 1, + properties: { + mapbox_id: 'dXJuOm1ieHBsYzpCUkpJT2c', + wikidata: 'Q82421', + }, + text_en: 'Welzheim', + language_en: 'en', + place_name_en: 'Welzheim, arrondissement de Rems-Murr, Baden-Württemberg, Germany', + text: 'Welzheim', + language: 'en', + place_name: 'Welzheim, arrondissement de Rems-Murr, Baden-Württemberg, Germany', + text_de: 'Welzheim', + language_de: 'de', + place_name_de: 'Welzheim, Rems-Murr-Kreis, Baden-Württemberg, Deutschland', + text_fr: 'Welzheim', + language_fr: 'fr', + place_name_fr: 'Welzheim, arrondissement de Rems-Murr, Bade-Wurtemberg, Allemagne', + text_nl: 'Welzheim', + language_nl: 'nl', + place_name_nl: 'Welzheim, arrondissement de Rems-Murr, Baden-Württemberg, Duitsland', + text_it: 'Welzheim', + language_it: 'it', + place_name_it: 'Welzheim, circondario del Rems-Murr, Baden-Württemberg, Germania', + text_es: 'Welzheim', + language_es: 'es', + place_name_es: 'Welzheim, arrondissement de Rems-Murr, Baden-Wurtemberg, Alemania', + text_pt: 'Welzheim', + language_pt: 'pt', + place_name_pt: 'Welzheim, circondario del Rems-Murr, Baden-Württemberg, Alemanha', + text_pl: 'Welzheim', + language_pl: 'pl', + place_name_pl: 'Welzheim, Powiat Rems-Murr, Badenia-Wirtembergia, Niemcy', + text_ru: 'Вельцхайм', + language_ru: 'ru', + place_name_ru: 'Вельцхайм, Ремс-Мур, Баден-Вюртемберг, Германия', + bbox: [9.555711, 48.840845, 9.69537, 48.923906], + center: [9.634301, 48.874393], + geometry: { + type: 'Point', + coordinates: [9.634301, 48.874393], + }, + context: [ + { + id: 'district.2598458', + mapbox_id: 'dXJuOm1ieHBsYzpKNlk2', + wikidata: 'Q8528', + text_en: 'arrondissement de Rems-Murr', + language_en: 'fr', + text: 'arrondissement de Rems-Murr', + language: 'fr', + text_de: 'Rems-Murr-Kreis', + language_de: 'de', + text_fr: 'arrondissement de Rems-Murr', + language_fr: 'fr', + text_nl: 'arrondissement de Rems-Murr', + language_nl: 'fr', + text_it: 'circondario del Rems-Murr', + language_it: 'it', + text_es: 'arrondissement de Rems-Murr', + language_es: 'fr', + text_pt: 'circondario del Rems-Murr', + language_pt: 'it', + text_pl: 'Powiat Rems-Murr', + language_pl: 'pl', + text_ru: 'Ремс-Мур', + language_ru: 'ru', + }, + { + id: 'region.132154', + mapbox_id: 'dXJuOm1ieHBsYzpBZ1E2', + wikidata: 'Q985', + short_code: 'DE-BW', + text_en: 'Baden-Württemberg', + language_en: 'en', + text: 'Baden-Württemberg', + language: 'en', + text_de: 'Baden-Württemberg', + language_de: 'de', + text_fr: 'Bade-Wurtemberg', + language_fr: 'fr', + text_nl: 'Baden-Württemberg', + language_nl: 'nl', + text_it: 'Baden-Württemberg', + language_it: 'it', + text_es: 'Baden-Wurtemberg', + language_es: 'es', + text_pt: 'Baden-Württemberg', + language_pt: 'pt', + text_pl: 'Badenia-Wirtembergia', + language_pl: 'pl', + text_ru: 'Баден-Вюртемберг', + language_ru: 'ru', + }, + { + id: 'country.8762', + mapbox_id: 'dXJuOm1ieHBsYzpJam8', + wikidata: 'Q183', + short_code: 'de', + text_en: 'Germany', + language_en: 'en', + text: 'Germany', + language: 'en', + text_de: 'Deutschland', + language_de: 'de', + text_fr: 'Allemagne', + language_fr: 'fr', + text_nl: 'Duitsland', + language_nl: 'nl', + text_it: 'Germania', + language_it: 'it', + text_es: 'Alemania', + language_es: 'es', + text_pt: 'Alemanha', + language_pt: 'pt', + text_pl: 'Niemcy', + language_pl: 'pl', + text_ru: 'Германия', + language_ru: 'ru', + }, + ], + }, + { + id: 'region.132154', + type: 'Feature', + place_type: ['region'], + relevance: 0.703704, + properties: { + mapbox_id: 'dXJuOm1ieHBsYzpBZ1E2', + wikidata: 'Q985', + short_code: 'DE-BW', + }, + text_en: 'Baden-Württemberg', + language_en: 'en', + place_name_en: 'Baden-Württemberg, Germany', + text: 'Baden-Württemberg', + language: 'en', + place_name: 'Baden-Württemberg, Germany', + text_de: 'Baden-Württemberg', + language_de: 'de', + place_name_de: 'Baden-Württemberg, Deutschland', + text_fr: 'Bade-Wurtemberg', + language_fr: 'fr', + place_name_fr: 'Bade-Wurtemberg, Allemagne', + text_nl: 'Baden-Württemberg', + language_nl: 'nl', + place_name_nl: 'Baden-Württemberg, Duitsland', + text_it: 'Baden-Württemberg', + language_it: 'it', + place_name_it: 'Baden-Württemberg, Germania', + text_es: 'Baden-Wurtemberg', + language_es: 'es', + place_name_es: 'Baden-Wurtemberg, Alemania', + text_pt: 'Baden-Württemberg', + language_pt: 'pt', + place_name_pt: 'Baden-Württemberg, Alemanha', + text_pl: 'Badenia-Wirtembergia', + language_pl: 'pl', + place_name_pl: 'Badenia-Wirtembergia, Niemcy', + text_ru: 'Баден-Вюртемберг', + language_ru: 'ru', + place_name_ru: 'Баден-Вюртемберг, Германия', + bbox: [7.511749, 47.532404, 10.495575, 49.791327], + center: [9.180013, 48.778449], + geometry: { + type: 'Point', + coordinates: [9.180013, 48.778449], + }, + context: [ + { + id: 'country.8762', + mapbox_id: 'dXJuOm1ieHBsYzpJam8', + wikidata: 'Q183', + short_code: 'de', + text_en: 'Germany', + language_en: 'en', + text: 'Germany', + language: 'en', + text_de: 'Deutschland', + language_de: 'de', + text_fr: 'Allemagne', + language_fr: 'fr', + text_nl: 'Duitsland', + language_nl: 'nl', + text_it: 'Germania', + language_it: 'it', + text_es: 'Alemania', + language_es: 'es', + text_pt: 'Alemanha', + language_pt: 'pt', + text_pl: 'Niemcy', + language_pl: 'pl', + text_ru: 'Германия', + language_ru: 'ru', + }, + ], + }, + { + id: 'address.7757118904863240', + type: 'Feature', + place_type: ['address'], + relevance: 0.67254, + properties: { + accuracy: 'street', + mapbox_id: 'dXJuOm1ieGFkci1zdHI6YTZkMmM2YTYtZDY3ZC00MjE1LWIyZmYtOTUwODQ4MTk3MmIz', + }, + text_en: 'German-Götz-Straße', + place_name_en: 'German-Götz-Straße, 72469 Meßstetten, Germany', + text: 'German-Götz-Straße', + place_name: 'German-Götz-Straße, 72469 Meßstetten, Germany', + text_de: 'German-Götz-Straße', + place_name_de: 'German-Götz-Straße, 72469 Meßstetten, Deutschland', + text_fr: 'German-Götz-Straße', + place_name_fr: 'German-Götz-Straße, 72469 Meßstetten, Allemagne', + text_nl: 'German-Götz-Straße', + place_name_nl: 'German-Götz-Straße, 72469 Meßstetten, Duitsland', + text_it: 'German-Götz-Straße', + place_name_it: 'German-Götz-Straße, 72469 Meßstetten, Germania', + text_es: 'German-Götz-Straße', + place_name_es: 'German-Götz-Straße, 72469 Meßstetten, Alemania', + text_pt: 'German-Götz-Straße', + place_name_pt: 'German-Götz-Straße, 72469 Meßstetten, Alemanha', + text_pl: 'German-Götz-Straße', + place_name_pl: 'German-Götz-Straße, 72469 Meßstetten, Niemcy', + text_ru: 'German-Götz-Straße', + place_name_ru: 'German-Götz-Straße, 72469 Месштеттен, Германия', + center: [8.920287, 48.189669], + geometry: { + type: 'Point', + coordinates: [8.920287, 48.189669], + }, + context: [ + { + id: 'postcode.41586234', + mapbox_id: 'dXJuOm1ieHBsYzpBbnFPT2c', + text_en: '72469', + text: '72469', + text_de: '72469', + text_fr: '72469', + text_nl: '72469', + text_it: '72469', + text_es: '72469', + text_pt: '72469', + text_pl: '72469', + text_ru: '72469', + }, + { + id: 'locality.150301242', + mapbox_id: 'dXJuOm1ieHBsYzpDUFZxT2c', + wikidata: 'Q18337859', + text_en: 'Hossingen', + language_en: 'en', + text: 'Hossingen', + language: 'en', + text_de: 'Hossingen', + language_de: 'de', + text_fr: 'Hossingen', + language_fr: 'fr', + text_nl: 'Hossingen', + language_nl: 'nl', + text_it: 'Hossingen', + language_it: 'fr', + text_es: 'Hossingen', + language_es: 'fr', + text_pt: 'Hossingen', + text_pl: 'Hossingen', + language_pl: 'nl', + text_ru: 'Hossingen', + }, + { + id: 'place.50759738', + mapbox_id: 'dXJuOm1ieHBsYzpBd2FJT2c', + wikidata: 'Q515661', + text_en: 'Meßstetten', + language_en: 'en', + text: 'Meßstetten', + language: 'en', + text_de: 'Meßstetten', + language_de: 'de', + text_fr: 'Meßstetten', + language_fr: 'fr', + text_nl: 'Meßstetten', + language_nl: 'nl', + text_it: 'Meßstetten', + language_it: 'it', + text_es: 'Meßstetten', + language_es: 'es', + text_pt: 'Meßstetten', + language_pt: 'pt', + text_pl: 'Meßstetten', + language_pl: 'pl', + text_ru: 'Месштеттен', + language_ru: 'ru', + }, + { + id: 'district.2795066', + mapbox_id: 'dXJuOm1ieHBsYzpLcVk2', + wikidata: 'Q8233', + text_en: 'arrondissement de Zollernalb', + language_en: 'fr', + text: 'arrondissement de Zollernalb', + language: 'fr', + text_de: 'Zollernalbkreis', + language_de: 'de', + text_fr: 'arrondissement de Zollernalb', + language_fr: 'fr', + text_nl: 'arrondissement de Zollernalb', + language_nl: 'fr', + text_it: 'circondario dello Zollernalb', + language_it: 'it', + text_es: 'arrondissement de Zollernalb', + language_es: 'fr', + text_pt: 'circondario dello Zollernalb', + language_pt: 'it', + text_pl: 'Powiat Zollernalb', + language_pl: 'pl', + text_ru: 'Цоллернальб', + language_ru: 'ru', + }, + { + id: 'region.132154', + mapbox_id: 'dXJuOm1ieHBsYzpBZ1E2', + wikidata: 'Q985', + short_code: 'DE-BW', + text_en: 'Baden-Württemberg', + language_en: 'en', + text: 'Baden-Württemberg', + language: 'en', + text_de: 'Baden-Württemberg', + language_de: 'de', + text_fr: 'Bade-Wurtemberg', + language_fr: 'fr', + text_nl: 'Baden-Württemberg', + language_nl: 'nl', + text_it: 'Baden-Württemberg', + language_it: 'it', + text_es: 'Baden-Wurtemberg', + language_es: 'es', + text_pt: 'Baden-Württemberg', + language_pt: 'pt', + text_pl: 'Badenia-Wirtembergia', + language_pl: 'pl', + text_ru: 'Баден-Вюртемберг', + language_ru: 'ru', + }, + { + id: 'country.8762', + mapbox_id: 'dXJuOm1ieHBsYzpJam8', + wikidata: 'Q183', + short_code: 'de', + text_en: 'Germany', + language_en: 'en', + text: 'Germany', + language: 'en', + text_de: 'Deutschland', + language_de: 'de', + text_fr: 'Allemagne', + language_fr: 'fr', + text_nl: 'Duitsland', + language_nl: 'nl', + text_it: 'Germania', + language_it: 'it', + text_es: 'Alemania', + language_es: 'es', + text_pt: 'Alemanha', + language_pt: 'pt', + text_pl: 'Niemcy', + language_pl: 'pl', + text_ru: 'Германия', + language_ru: 'ru', + }, + ], + }, + { + id: 'address.7729037509260214', + type: 'Feature', + place_type: ['address'], + relevance: 0.67254, + properties: { + accuracy: 'street', + mapbox_id: 'dXJuOm1ieGFkci1zdHI6ZTFhMjYyZTEtN2I4Ny00YTYxLThlYmMtNTQxYzQyYzljMzQ2', + }, + text_en: 'Germanstraße', + place_name_en: 'Germanstraße, 78048 Villingen-Schwenningen, Germany', + text: 'Germanstraße', + place_name: 'Germanstraße, 78048 Villingen-Schwenningen, Germany', + text_de: 'Germanstraße', + place_name_de: 'Germanstraße, 78048 Villingen-Schwenningen, Deutschland', + text_fr: 'Germanstraße', + place_name_fr: 'Germanstraße, 78048 Villingen-Schwenningen, Allemagne', + text_nl: 'Germanstraße', + place_name_nl: 'Germanstraße, 78048 Villingen-Schwenningen, Duitsland', + text_it: 'Germanstraße', + place_name_it: 'Germanstraße, 78048 Villingen-Schwenningen, Germania', + text_es: 'Germanstraße', + place_name_es: 'Germanstraße, 78048 Villingen-Schwenningen, Alemania', + text_pt: 'Germanstraße', + place_name_pt: 'Germanstraße, 78048 Villingen-Schwenningen, Alemanha', + text_pl: 'Germanstraße', + place_name_pl: 'Germanstraße, 78048 Villingen-Schwenningen, Niemcy', + text_ru: 'Germanstraße', + place_name_ru: 'Germanstraße, 78048 Филлинген-Швеннинген, Германия', + center: [8.430963, 48.073201], + geometry: { + type: 'Point', + coordinates: [8.430963, 48.073201], + }, + context: [ + { + id: 'postcode.45977146', + mapbox_id: 'dXJuOm1ieHBsYzpBcjJPT2c', + text_en: '78048', + text: '78048', + text_de: '78048', + text_fr: '78048', + text_nl: '78048', + text_it: '78048', + text_es: '78048', + text_pt: '78048', + text_pl: '78048', + text_ru: '78048', + }, + { + id: 'locality.351808058', + mapbox_id: 'dXJuOm1ieHBsYzpGUGdxT2c', + wikidata: 'Q47501154', + text_en: 'Villingen', + language_en: 'en', + text: 'Villingen', + language: 'en', + text_de: 'Villingen', + language_de: 'de', + text_fr: 'Villingen', + language_fr: 'nl', + text_nl: 'Villingen', + language_nl: 'nl', + text_it: 'Villingen', + language_it: 'it', + text_es: 'Villingen', + language_es: 'nl', + text_pt: 'Villingen', + language_pt: 'it', + text_pl: 'Villingen', + language_pl: 'nl', + text_ru: 'Villingen', + }, + { + id: 'place.81537082', + mapbox_id: 'dXJuOm1ieHBsYzpCTndvT2c', + wikidata: 'Q3865', + text_en: 'Villingen-Schwenningen', + language_en: 'en', + text: 'Villingen-Schwenningen', + language: 'en', + text_de: 'Villingen-Schwenningen', + language_de: 'de', + text_fr: 'Villingen-Schwenningen', + language_fr: 'fr', + text_nl: 'Villingen-Schwenningen', + language_nl: 'nl', + text_it: 'Villingen-Schwenningen', + language_it: 'it', + text_es: 'Villingen-Schwenningen', + language_es: 'es', + text_pt: 'Villingen-Schwenningen', + language_pt: 'pt', + text_pl: 'Villingen-Schwenningen', + language_pl: 'pl', + text_ru: 'Филлинген-Швеннинген', + language_ru: 'ru', + }, + { + id: 'district.2729530', + mapbox_id: 'dXJuOm1ieHBsYzpLYVk2', + wikidata: 'Q8203', + text_en: 'Schwarzwald-Baar district', + language_en: 'en', + text: 'Schwarzwald-Baar district', + language: 'en', + text_de: 'Schwarzwald-Baar-Kreis', + language_de: 'de', + text_fr: 'arrondissement de Forêt-Noire-Baar', + language_fr: 'fr', + text_nl: 'arrondissement de Forêt-Noire-Baar', + language_nl: 'fr', + text_it: 'circondario della Foresta Nera-Baar', + language_it: 'it', + text_es: 'arrondissement de Forêt-Noire-Baar', + language_es: 'fr', + text_pt: 'circondario della Foresta Nera-Baar', + language_pt: 'it', + text_pl: 'Powiat Schwarzwald-Baar', + language_pl: 'pl', + text_ru: 'Шварцвальд-Бар', + language_ru: 'ru', + }, + { + id: 'region.132154', + mapbox_id: 'dXJuOm1ieHBsYzpBZ1E2', + wikidata: 'Q985', + short_code: 'DE-BW', + text_en: 'Baden-Württemberg', + language_en: 'en', + text: 'Baden-Württemberg', + language: 'en', + text_de: 'Baden-Württemberg', + language_de: 'de', + text_fr: 'Bade-Wurtemberg', + language_fr: 'fr', + text_nl: 'Baden-Württemberg', + language_nl: 'nl', + text_it: 'Baden-Württemberg', + language_it: 'it', + text_es: 'Baden-Wurtemberg', + language_es: 'es', + text_pt: 'Baden-Württemberg', + language_pt: 'pt', + text_pl: 'Badenia-Wirtembergia', + language_pl: 'pl', + text_ru: 'Баден-Вюртемберг', + language_ru: 'ru', + }, + { + id: 'country.8762', + mapbox_id: 'dXJuOm1ieHBsYzpJam8', + wikidata: 'Q183', + short_code: 'de', + text_en: 'Germany', + language_en: 'en', + text: 'Germany', + language: 'en', + text_de: 'Deutschland', + language_de: 'de', + text_fr: 'Allemagne', + language_fr: 'fr', + text_nl: 'Duitsland', + language_nl: 'nl', + text_it: 'Germania', + language_it: 'it', + text_es: 'Alemania', + language_es: 'es', + text_pt: 'Alemanha', + language_pt: 'pt', + text_pl: 'Niemcy', + language_pl: 'pl', + text_ru: 'Германия', + language_ru: 'ru', + }, + ], + }, + { + id: 'place.85002298', + type: 'Feature', + place_type: ['place'], + relevance: 0.574074, + properties: { + mapbox_id: 'dXJuOm1ieHBsYzpCUkVJT2c', + wikidata: 'Q509998', + }, + text_en: 'Wellheim', + language_en: 'en', + place_name_en: "Wellheim, arrondissement d'Eichstätt, Bavaria, Germany", + text: 'Wellheim', + language: 'en', + place_name: "Wellheim, arrondissement d'Eichstätt, Bavaria, Germany", + text_de: 'Wellheim', + language_de: 'de', + place_name_de: 'Wellheim, Kreis Eichstätt, Bayern, Deutschland', + text_fr: 'Wellheim', + language_fr: 'fr', + place_name_fr: "Wellheim, arrondissement d'Eichstätt, Bavière, Allemagne", + text_nl: 'Wellheim', + language_nl: 'nl', + place_name_nl: 'Wellheim, Landkreis Eichstätt, Beieren, Duitsland', + text_it: 'Wellheim', + language_it: 'it', + place_name_it: 'Wellheim, circondario di Eichstätt, Baviera, Germania', + text_es: 'Wellheim', + language_es: 'es', + place_name_es: "Wellheim, arrondissement d'Eichstätt, Baviera, Alemania", + text_pt: 'Wellheim', + language_pt: 'pt', + place_name_pt: 'Wellheim, circondario di Eichstätt, Baviera, Alemanha', + text_pl: 'Wellheim', + language_pl: 'pl', + place_name_pl: 'Wellheim, Powiat Eichstätt, Bawaria, Niemcy', + text_ru: 'Велльхайм', + language_ru: 'ru', + place_name_ru: 'Велльхайм, Айхштет, Бавария, Германия', + bbox: [11.032347, 48.79008, 11.173361, 48.846386], + center: [11.080854, 48.817787], + geometry: { + type: 'Point', + coordinates: [11.080854, 48.817787], + }, + context: [ + { + id: 'district.460346', + mapbox_id: 'dXJuOm1ieHBsYzpCd1k2', + wikidata: 'Q10491', + text_en: "arrondissement d'Eichstätt", + language_en: 'fr', + text: "arrondissement d'Eichstätt", + language: 'fr', + text_de: 'Kreis Eichstätt', + language_de: 'de', + text_fr: "arrondissement d'Eichstätt", + language_fr: 'fr', + text_nl: 'Landkreis Eichstätt', + language_nl: 'nl', + text_it: 'circondario di Eichstätt', + language_it: 'it', + text_es: "arrondissement d'Eichstätt", + language_es: 'fr', + text_pt: 'circondario di Eichstätt', + language_pt: 'it', + text_pl: 'Powiat Eichstätt', + language_pl: 'pl', + text_ru: 'Айхштет', + language_ru: 'ru', + }, + { + id: 'region.123962', + mapbox_id: 'dXJuOm1ieHBsYzpBZVE2', + wikidata: 'Q980', + short_code: 'DE-BY', + text_en: 'Bavaria', + language_en: 'en', + text: 'Bavaria', + language: 'en', + text_de: 'Bayern', + language_de: 'de', + text_fr: 'Bavière', + language_fr: 'fr', + text_nl: 'Beieren', + language_nl: 'nl', + text_it: 'Baviera', + language_it: 'it', + text_es: 'Baviera', + language_es: 'es', + text_pt: 'Baviera', + language_pt: 'pt', + text_pl: 'Bawaria', + language_pl: 'pl', + text_ru: 'Бавария', + language_ru: 'ru', + }, + { + id: 'country.8762', + mapbox_id: 'dXJuOm1ieHBsYzpJam8', + wikidata: 'Q183', + short_code: 'de', + text_en: 'Germany', + language_en: 'en', + text: 'Germany', + language: 'en', + text_de: 'Deutschland', + language_de: 'de', + text_fr: 'Allemagne', + language_fr: 'fr', + text_nl: 'Duitsland', + language_nl: 'nl', + text_it: 'Germania', + language_it: 'it', + text_es: 'Alemania', + language_es: 'es', + text_pt: 'Alemanha', + language_pt: 'pt', + text_pl: 'Niemcy', + language_pl: 'pl', + text_ru: 'Германия', + language_ru: 'ru', + }, + ], + }, + ], + attribution: + 'NOTICE: © 2025 Mapbox and its suppliers. All rights reserved. Use of this data is subject to the Mapbox Terms of Service (https://www.mapbox.com/about/maps/). This response and the information it contains may not be retained.', +} diff --git a/backend/test/helpers.ts b/backend/test/helpers.ts new file mode 100644 index 000000000..60ebe0f91 --- /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' + +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://localhost:3000', + GRAPHQL_URI: 'http://localhost:4000', + JWT_EXPIRES: '2y', + + MAPBOX_TOKEN: 'MAPBOX_TOKEN', + 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', +} as const satisfies typeof CONFIG + +interface OverwritableContextParams { + authenticatedUser?: Context['user'] + config?: Partial + pubsub?: Context['pubsub'] + fetch?: Context['fetch'] +} +interface CreateTestServerOptions { + context: () => OverwritableContextParams | Promise +} + +const crash = () => { + throw new Error('Mock me in your test!') +} + +export const createApolloTestSetup = (opts?: CreateTestServerOptions) => { + const defaultOpts: CreateTestServerOptions = { context: () => ({ authenticatedUser: null }) } + const { context: testContext } = opts ?? defaultOpts + const database = databaseContext() + const context = async (req: { headers: { authorization?: string } }) => { + const { authenticatedUser, config = {}, pubsub, fetch = crash } = await testContext() + return getContext({ + authenticatedUser, + database, + pubsub, + config: { ...TEST_CONFIG, ...config }, + fetch, + })(req) + } + + const server = createServer({ + context, + }).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 f5164b1a9..70d2c76f7 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -2348,6 +2348,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" @@ -2491,6 +2496,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" @@ -2585,6 +2597,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" @@ -2615,6 +2637,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": version "9.0.8" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba" @@ -5789,6 +5816,17 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw== +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" @@ -8247,7 +8285,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.19, 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.19, 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== @@ -9711,7 +9749,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.1.2, safe-buffer@~5.2.0: +safe-buffer@5.2.1, safe-buffer@^5.0.1, safe-buffer@^5.1.2, 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)) }) })