From 66b5e61c154ab96d86deecf59d2bd82d639a43b7 Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Wed, 10 Sep 2025 17:26:28 +0100 Subject: [PATCH] refactor(backend): test block & unblock user (#8879) * queries * test block users & small refactor on the resolver * require 92% coverage (+2%) * update according to review * use cypher instead of neode --- backend/jest.config.cjs | 2 +- backend/src/graphql/queries/blockUser.ts | 11 + backend/src/graphql/queries/blockedUsers.ts | 11 + backend/src/graphql/queries/unblockUser.ts | 11 + backend/src/graphql/resolvers/users.ts | 55 +-- .../resolvers/users/blockedUsers.spec.ts | 406 ++++++++++++++++++ 6 files changed, 461 insertions(+), 35 deletions(-) create mode 100644 backend/src/graphql/queries/blockUser.ts create mode 100644 backend/src/graphql/queries/blockedUsers.ts create mode 100644 backend/src/graphql/queries/unblockUser.ts create mode 100644 backend/src/graphql/resolvers/users/blockedUsers.spec.ts diff --git a/backend/jest.config.cjs b/backend/jest.config.cjs index 3441db428..c78de9155 100644 --- a/backend/jest.config.cjs +++ b/backend/jest.config.cjs @@ -18,7 +18,7 @@ module.exports = { ], coverageThreshold: { global: { - lines: 90, + lines: 92, }, }, testMatch: ['**/src/**/?(*.)+(spec|test).ts?(x)'], diff --git a/backend/src/graphql/queries/blockUser.ts b/backend/src/graphql/queries/blockUser.ts new file mode 100644 index 000000000..4bf878774 --- /dev/null +++ b/backend/src/graphql/queries/blockUser.ts @@ -0,0 +1,11 @@ +import gql from 'graphql-tag' + +export const blockUser = gql` + mutation ($id: ID!) { + blockUser(id: $id) { + id + name + isBlocked + } + } +` diff --git a/backend/src/graphql/queries/blockedUsers.ts b/backend/src/graphql/queries/blockedUsers.ts new file mode 100644 index 000000000..d7f358f80 --- /dev/null +++ b/backend/src/graphql/queries/blockedUsers.ts @@ -0,0 +1,11 @@ +import gql from 'graphql-tag' + +export const blockedUsers = gql` + query { + blockedUsers { + id + name + isBlocked + } + } +` diff --git a/backend/src/graphql/queries/unblockUser.ts b/backend/src/graphql/queries/unblockUser.ts new file mode 100644 index 000000000..7a65babdc --- /dev/null +++ b/backend/src/graphql/queries/unblockUser.ts @@ -0,0 +1,11 @@ +import gql from 'graphql-tag' + +export const unblockUser = gql` + mutation ($id: ID!) { + unblockUser(id: $id) { + id + name + isBlocked + } + } +` diff --git a/backend/src/graphql/resolvers/users.ts b/backend/src/graphql/resolvers/users.ts index 096dfe521..bb8f85434 100644 --- a/backend/src/graphql/resolvers/users.ts +++ b/backend/src/graphql/resolvers/users.ts @@ -35,36 +35,17 @@ export const getMutedUsers = async (context) => { return mutedUsers } -export const getBlockedUsers = async (context) => { - const { neode } = context - const userModel = neode.model('User') - let blockedUsers = neode - .query() - .match('user', userModel) - .where('user.id', context.user.id) - .relationship(userModel.relationships().get('blocked')) - .to('blocked', userModel) - .return('blocked') - blockedUsers = await blockedUsers.execute() - blockedUsers = blockedUsers.records.map((r) => r.get('blocked').properties) - return blockedUsers -} - export default { Query: { - mutedUsers: async (_object, _args, context, _resolveInfo) => { - try { - return getMutedUsers(context) - } catch (e) { - throw new UserInputError(e.message) - } - }, - blockedUsers: async (_object, _args, context, _resolveInfo) => { - try { - return getBlockedUsers(context) - } catch (e) { - throw new UserInputError(e.message) - } + mutedUsers: async (_object, _args, context, _resolveInfo) => getMutedUsers(context), + blockedUsers: async (_object, _args, context: Context, _resolveInfo) => { + return ( + await context.database.query({ + query: `MATCH (user:User{ id: $user.id})-[:BLOCKED]->(blocked:User) + RETURN blocked {.*}`, + variables: { user: context.user }, + }) + ).records.map((r) => r.get('blocked')) }, User: async (object, args, context, resolveInfo) => { if (args.email) { @@ -140,9 +121,11 @@ export default { return unBlockUserTransactionResponse.records.map((record) => record.get('blockedUser'))[0] }) try { - return await writeTxResultPromise - } catch (error) { - throw new UserInputError(error.message) + const blockedUser = await writeTxResultPromise + if (!blockedUser) { + throw new UserInputError('Could not find User') + } + return blockedUser } finally { session.close() } @@ -164,9 +147,13 @@ export default { return unBlockUserTransactionResponse.records.map((record) => record.get('blockedUser'))[0] }) try { - return await writeTxResultPromise - } catch (error) { - throw new UserInputError(error.message) + const unblockedUser = await writeTxResultPromise + if (!unblockedUser) { + throw new Error('Could not find blocked User') + } + return unblockedUser + } catch { + throw new UserInputError('Could not find blocked User') } finally { await session.close() } diff --git a/backend/src/graphql/resolvers/users/blockedUsers.spec.ts b/backend/src/graphql/resolvers/users/blockedUsers.spec.ts new file mode 100644 index 000000000..06f79ee76 --- /dev/null +++ b/backend/src/graphql/resolvers/users/blockedUsers.spec.ts @@ -0,0 +1,406 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +import gql from 'graphql-tag' + +import { cleanDatabase } from '@db/factories' +import { blockedUsers } from '@graphql/queries/blockedUsers' +import { blockUser } from '@graphql/queries/blockUser' +import { unblockUser } from '@graphql/queries/unblockUser' +import { createApolloTestSetup } from '@root/test/helpers' +import type { ApolloTestSetup } from '@root/test/helpers' +import type { Context } from '@src/context' + +let currentUser +let blockedUser + +let authenticatedUser: Context['user'] +const context = () => ({ authenticatedUser }) +let mutate: ApolloTestSetup['mutate'] +let query: ApolloTestSetup['query'] +let database: ApolloTestSetup['database'] +let server: ApolloTestSetup['server'] + +beforeAll(async () => { + await cleanDatabase() + const apolloSetup = createApolloTestSetup({ context }) + mutate = apolloSetup.mutate + query = apolloSetup.query + database = apolloSetup.database + server = apolloSetup.server +}) + +afterAll(async () => { + await cleanDatabase() + void server.stop() + void database.driver.close() + database.neode.close() +}) + +afterEach(async () => { + await cleanDatabase() +}) + +describe('blockedUsers', () => { + describe('unauthenticated', () => { + beforeEach(() => { + authenticatedUser = null + }) + + it('throws authorization error', async () => { + await expect(query({ query: blockedUsers })).resolves.toMatchObject({ + data: { blockedUsers: null }, + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) + + describe('authenticated and given a blocked user', () => { + beforeEach(async () => { + currentUser = await database.neode.create('User', { + name: 'Current User', + id: 'u1', + }) + blockedUser = await database.neode.create('User', { + name: 'Blocked User', + id: 'u2', + }) + await currentUser.relateTo(blockedUser, 'blocked') + authenticatedUser = await currentUser.toJson() + }) + + it('returns a list of blocked users', async () => { + await expect(query({ query: blockedUsers })).resolves.toMatchObject({ + data: { + blockedUsers: [ + { + name: 'Blocked User', + id: 'u2', + isBlocked: true, + }, + ], + }, + }) + }) + }) +}) + +describe('blockUser', () => { + describe('unauthenticated', () => { + beforeEach(() => { + authenticatedUser = null + }) + + it('throws authorization error', async () => { + await expect(mutate({ mutation: blockUser, variables: { id: 'u2' } })).resolves.toMatchObject( + { + data: { blockUser: null }, + errors: [{ message: 'Not Authorized!' }], + }, + ) + }) + }) + + describe('authenticated', () => { + beforeEach(async () => { + currentUser = await database.neode.create('User', { + name: 'Current User', + id: 'u1', + }) + authenticatedUser = await currentUser.toJson() + }) + + describe('block yourself', () => { + it('returns null', async () => { + await expect( + mutate({ mutation: blockUser, variables: { id: 'u1' } }), + ).resolves.toMatchObject({ + data: { blockUser: null }, + }) + }) + }) + + describe('block not existing user', () => { + it('throws an error', async () => { + await expect( + mutate({ mutation: blockUser, variables: { id: 'u2' } }), + ).resolves.toMatchObject({ + errors: [ + { + message: 'Could not find User', + }, + ], + }) + }) + }) + + describe('given a to-be-blocked user', () => { + beforeEach(async () => { + blockedUser = await database.neode.create('User', { + name: 'Blocked User', + id: 'u2', + }) + }) + + it('blocks a user', async () => { + await expect( + mutate({ mutation: blockUser, variables: { id: 'u2' } }), + ).resolves.toMatchObject({ + data: { + blockUser: { id: 'u2', name: 'Blocked User', isBlocked: true }, + }, + }) + }) + + it('unfollows the user when blocking', async () => { + await currentUser.relateTo(blockedUser, 'following') + const queryUser = gql` + query { + User(id: "u2") { + id + isBlocked + followedByCurrentUser + } + } + ` + await expect(query({ query: queryUser })).resolves.toMatchObject({ + data: { User: [{ id: 'u2', isBlocked: false, followedByCurrentUser: true }] }, + }) + await mutate({ mutation: blockUser, variables: { id: 'u2' } }) + await expect(query({ query: queryUser })).resolves.toMatchObject({ + data: { User: [{ id: 'u2', isBlocked: true, followedByCurrentUser: false }] }, + }) + }) + + describe('given both the current user and the to-be-blocked user write a post', () => { + let postQuery + + beforeEach(async () => { + const post1 = await database.neode.create('Post', { + id: 'p12', + title: 'A post written by the current user', + }) + const post2 = await database.neode.create('Post', { + id: 'p23', + title: 'A post written by the blocked user', + }) + await Promise.all([ + post1.relateTo(currentUser, 'author'), + post2.relateTo(blockedUser, 'author'), + ]) + postQuery = gql` + query { + Post(orderBy: createdAt_asc) { + id + title + author { + id + name + } + } + } + ` + }) + + const bothPostsAreInTheNewsfeed = async () => { + await expect(query({ query: postQuery })).resolves.toMatchObject({ + data: { + Post: [ + { + id: 'p12', + title: 'A post written by the current user', + author: { + name: 'Current User', + id: 'u1', + }, + }, + { + id: 'p23', + title: 'A post written by the blocked user', + author: { + name: 'Blocked User', + id: 'u2', + }, + }, + ], + }, + }) + } + + describe('from the perspective of the current user', () => { + it('both posts are in the newsfeed', bothPostsAreInTheNewsfeed) + + describe('but if the current user blocks the other user', () => { + beforeEach(async () => { + await currentUser.relateTo(blockedUser, 'blocked') + }) + + // TODO: clarify proper behaviour + it("the blocked user's post still shows up in the newsfeed of the current user", async () => { + await expect(query({ query: postQuery })).resolves.toMatchObject({ + data: { + Post: [ + { + id: 'p12', + title: 'A post written by the current user', + author: { + name: 'Current User', + id: 'u1', + }, + }, + { + id: 'p23', + title: 'A post written by the blocked user', + author: { + name: 'Blocked User', + id: 'u2', + }, + }, + ], + }, + }) + }) + }) + }) + + describe('from the perspective of the blocked user', () => { + beforeEach(async () => { + authenticatedUser = await blockedUser.toJson() + }) + + it('both posts are in the newsfeed', bothPostsAreInTheNewsfeed) + describe('but if the current user blocks the other user', () => { + beforeEach(async () => { + await currentUser.relateTo(blockedUser, 'blocked') + }) + + it("the current user's post will show up in the newsfeed of the blocked user", async () => { + await expect(query({ query: postQuery })).resolves.toMatchObject({ + data: { + Post: expect.arrayContaining([ + { + id: 'p23', + title: 'A post written by the blocked user', + author: { name: 'Blocked User', id: 'u2' }, + }, + { + id: 'p12', + title: 'A post written by the current user', + author: { name: 'Current User', id: 'u1' }, + }, + ]), + }, + }) + }) + }) + }) + }) + }) + }) +}) + +describe('unblockUser', () => { + describe('unauthenticated', () => { + beforeEach(() => { + authenticatedUser = null + }) + + it('throws authorization error', async () => { + await expect( + mutate({ mutation: unblockUser, variables: { id: 'u2' } }), + ).resolves.toMatchObject({ + data: { unblockUser: null }, + errors: [{ message: 'Not Authorized!' }], + }) + }) + }) + + describe('authenticated', () => { + beforeEach(async () => { + currentUser = await database.neode.create('User', { + name: 'Current User', + id: 'u1', + }) + authenticatedUser = await currentUser.toJson() + }) + + describe('unblock yourself', () => { + it('returns null', async () => { + await expect( + mutate({ mutation: unblockUser, variables: { id: 'u1' } }), + ).resolves.toMatchObject({ + data: { unblockUser: null }, + }) + }) + }) + + describe('unblock not-existing user', () => { + it('throws an error', async () => { + await expect( + mutate({ mutation: unblockUser, variables: { id: 'lksjdflksfdj' } }), + ).resolves.toMatchObject({ + errors: [ + { + message: 'Could not find blocked User', + }, + ], + }) + }) + }) + + describe('given another user', () => { + beforeEach(async () => { + blockedUser = await database.neode.create('User', { + name: 'Blocked User', + id: 'u2', + }) + }) + + describe('unblocking a not yet blocked user', () => { + it('throws an error', async () => { + await expect( + mutate({ mutation: unblockUser, variables: { id: 'u2' } }), + ).resolves.toMatchObject({ + errors: [ + { + message: 'Could not find blocked User', + }, + ], + }) + }) + }) + + describe('given a blocked user', () => { + beforeEach(async () => { + await currentUser.relateTo(blockedUser, 'blocked') + }) + + it('unblocks a user', async () => { + await expect( + mutate({ mutation: unblockUser, variables: { id: 'u2' } }), + ).resolves.toMatchObject({ + data: { + unblockUser: { id: 'u2', name: 'Blocked User', isBlocked: false }, + }, + }) + }) + + describe('unblocking twice', () => { + it('throws an error on second unblock', async () => { + await mutate({ mutation: unblockUser, variables: { id: 'u2' } }) + await expect( + mutate({ mutation: unblockUser, variables: { id: 'u2' } }), + ).resolves.toMatchObject({ + errors: [ + { + message: 'Could not find blocked User', + }, + ], + }) + }) + }) + }) + }) + }) +})