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
This commit is contained in:
Ulf Gebhardt 2025-09-10 17:26:28 +01:00 committed by GitHub
parent 380d3401c0
commit 66b5e61c15
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 461 additions and 35 deletions

View File

@ -18,7 +18,7 @@ module.exports = {
],
coverageThreshold: {
global: {
lines: 90,
lines: 92,
},
},
testMatch: ['**/src/**/?(*.)+(spec|test).ts?(x)'],

View File

@ -0,0 +1,11 @@
import gql from 'graphql-tag'
export const blockUser = gql`
mutation ($id: ID!) {
blockUser(id: $id) {
id
name
isBlocked
}
}
`

View File

@ -0,0 +1,11 @@
import gql from 'graphql-tag'
export const blockedUsers = gql`
query {
blockedUsers {
id
name
isBlocked
}
}
`

View File

@ -0,0 +1,11 @@
import gql from 'graphql-tag'
export const unblockUser = gql`
mutation ($id: ID!) {
unblockUser(id: $id) {
id
name
isBlocked
}
}
`

View File

@ -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()
}

View File

@ -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',
},
],
})
})
})
})
})
})
})