fix(backend): fix backend tests & increase coverage (#9514)

This commit is contained in:
Ulf Gebhardt 2026-04-05 21:43:38 +02:00 committed by GitHub
parent 700aaaf0b2
commit 306f21cc20
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 354 additions and 110 deletions

View File

@ -27,7 +27,7 @@ export default {
],
coverageThreshold: {
global: {
lines: 93,
lines: 94,
},
},
testMatch: ['**/src/**/?(*.)+(spec|test).ts?(x)'],

View File

@ -7,6 +7,8 @@ mutation ChangeGroupMemberRole($groupId: ID!, $userId: ID!, $roleInGroup: GroupM
}
membership {
role
createdAt
updatedAt
}
}
}

View File

@ -27,6 +27,7 @@ query Group($isMember: Boolean, $id: ID, $slug: String) {
name
}
myRole
currentlyPinnedPostsCount
inviteCodes {
code
redeemedByCount

View File

@ -0,0 +1,3 @@
query GroupCount($isMember: Boolean) {
GroupCount(isMember: $isMember)
}

View File

@ -1,6 +1,7 @@
mutation UpdatePost(
$id: ID!
$title: String!
$slug: String
$content: String!
$image: ImageInput
$categoryIds: [ID]
@ -10,6 +11,7 @@ mutation UpdatePost(
UpdatePost(
id: $id
title: $title
slug: $slug
content: $content
image: $image
categoryIds: $categoryIds
@ -18,6 +20,7 @@ mutation UpdatePost(
) {
id
title
slug
content
author {
id

View File

@ -10,10 +10,13 @@ import Factory, { cleanDatabase } from '@db/factories'
import ChangeGroupMemberRole from '@graphql/queries/groups/ChangeGroupMemberRole.gql'
import CreateGroup from '@graphql/queries/groups/CreateGroup.gql'
import groupQuery from '@graphql/queries/groups/Group.gql'
import GroupCount from '@graphql/queries/groups/GroupCount.gql'
import groupMembersQuery from '@graphql/queries/groups/GroupMembers.gql'
import JoinGroup from '@graphql/queries/groups/JoinGroup.gql'
import LeaveGroup from '@graphql/queries/groups/LeaveGroup.gql'
import muteGroupMutation from '@graphql/queries/groups/muteGroup.gql'
import RemoveUserFromGroup from '@graphql/queries/groups/RemoveUserFromGroup.gql'
import unmuteGroupMutation from '@graphql/queries/groups/unmuteGroup.gql'
import UpdateGroup from '@graphql/queries/groups/UpdateGroup.gql'
import { createApolloTestSetup } from '@root/test/helpers'
@ -1740,8 +1743,17 @@ describe('in mode', () => {
})
})
// the GQL mutation needs this fields in the result for testing
it.todo('has "updatedAt" newer as "createdAt"')
it('has "updatedAt" newer than or equal to "createdAt"', async () => {
const result = await mutate({
mutation: ChangeGroupMemberRole,
variables,
})
const { createdAt, updatedAt }: { createdAt: string; updatedAt: string } =
result.data.ChangeGroupMemberRole.membership
expect(new Date(updatedAt).getTime()).toBeGreaterThanOrEqual(
new Date(createdAt).getTime(),
)
})
})
})
@ -3308,4 +3320,200 @@ describe('in mode', () => {
})
})
})
describe('clean db after each additional coverage', () => {
beforeEach(async () => {
await seedBasicsAndClearAuthentication()
})
afterEach(async () => {
await cleanDatabase()
})
describe('GroupCount', () => {
beforeEach(async () => {
authenticatedUser = await user.toJson()
await mutate({
mutation: CreateGroup,
variables: {
name: 'Count Test Group',
about: 'A group for counting',
description:
'This is a test group for counting purposes, with enough description length to pass validation',
groupType: 'public',
actionRadius: 'national',
categoryIds: ['cat9'],
},
})
})
it('returns count of all visible groups when isMember is not set', async () => {
const result = await query({ query: GroupCount })
expect(result).toMatchObject({
data: { GroupCount: 1 },
errors: undefined,
})
})
it('returns count of groups the user is a member of', async () => {
const result = await query({ query: GroupCount, variables: { isMember: true } })
expect(result).toMatchObject({
data: { GroupCount: 1 },
errors: undefined,
})
})
})
describe('muteGroup and unmuteGroup', () => {
let groupId: string
beforeEach(async () => {
authenticatedUser = await user.toJson()
const result = await mutate({
mutation: CreateGroup,
variables: {
name: 'Mute Test Group',
about: 'A group for muting',
description:
'This is a test group for muting purposes, with enough description length to pass validation',
groupType: 'public',
actionRadius: 'national',
categoryIds: ['cat9'],
},
})
groupId = result.data.CreateGroup.id
})
it('mutes a group', async () => {
await expect(
mutate({ mutation: muteGroupMutation, variables: { groupId } }),
).resolves.toMatchObject({
data: {
muteGroup: {
id: groupId,
isMutedByMe: true,
},
},
errors: undefined,
})
})
it('unmutes a group', async () => {
await mutate({ mutation: muteGroupMutation, variables: { groupId } })
await expect(
mutate({ mutation: unmuteGroupMutation, variables: { groupId } }),
).resolves.toMatchObject({
data: {
unmuteGroup: {
id: groupId,
isMutedByMe: false,
},
},
errors: undefined,
})
})
it('muteGroup throws for unauthenticated user', async () => {
authenticatedUser = null
await expect(
mutate({ mutation: muteGroupMutation, variables: { groupId } }),
).resolves.toMatchObject({
errors: [expect.objectContaining({ message: 'Not Authorized!' })],
})
})
it('unmuteGroup throws for unauthenticated user', async () => {
authenticatedUser = null
await expect(
mutate({ mutation: unmuteGroupMutation, variables: { groupId } }),
).resolves.toMatchObject({
errors: [expect.objectContaining({ message: 'Not Authorized!' })],
})
})
})
describe('UpdateGroup slug conflict', () => {
beforeEach(async () => {
authenticatedUser = await user.toJson()
await mutate({
mutation: CreateGroup,
variables: {
id: 'group-a',
name: 'Group A',
slug: 'group-a',
about: 'About A',
description:
'A test group with enough description length to pass the validation requirement check',
groupType: 'public',
actionRadius: 'national',
categoryIds: ['cat9'],
},
})
await mutate({
mutation: CreateGroup,
variables: {
id: 'group-b',
name: 'Group B',
slug: 'group-b',
about: 'About B',
description:
'A test group with enough description length to pass the validation requirement check',
groupType: 'public',
actionRadius: 'national',
categoryIds: ['cat9'],
},
})
})
it('throws error when updating slug to conflict with existing group', async () => {
await expect(
mutate({
mutation: UpdateGroup,
variables: { id: 'group-b', slug: 'group-a' },
}),
).resolves.toMatchObject({
errors: [expect.objectContaining({ message: 'Group with this slug already exists!' })],
})
})
})
describe('CreateGroup slug conflict', () => {
beforeEach(async () => {
authenticatedUser = await user.toJson()
await mutate({
mutation: CreateGroup,
variables: {
name: 'Existing Group',
slug: 'existing-group',
about: 'About',
description:
'A test group with enough description length to pass the validation requirement check',
groupType: 'public',
actionRadius: 'national',
categoryIds: ['cat9'],
},
})
})
it('throws error when creating group with duplicate slug', async () => {
await expect(
mutate({
mutation: CreateGroup,
variables: {
name: 'Another Group',
slug: 'existing-group',
about: 'About',
description:
'A test group with enough description length to pass the validation requirement check',
groupType: 'public',
actionRadius: 'national',
categoryIds: ['cat9'],
},
}),
).resolves.toMatchObject({
errors: [expect.objectContaining({ message: 'Group with this slug already exists!' })],
})
})
})
})
})

View File

@ -167,7 +167,7 @@ export default {
MERGE (owner)-[membership:MEMBER_OF]->(group)
SET
membership.createdAt = toString(datetime()),
membership.updatedAt = null,
membership.updatedAt = toString(datetime()),
membership.role = 'owner'
${categoriesCypher}
RETURN group {.*, myRole: membership.role}
@ -280,7 +280,7 @@ export default {
MERGE (user)-[membership:MEMBER_OF]->(group)
ON CREATE SET
membership.createdAt = toString(datetime()),
membership.updatedAt = null,
membership.updatedAt = toString(datetime()),
membership.role =
CASE WHEN group.groupType = 'public'
THEN 'usual'
@ -339,7 +339,7 @@ export default {
MERGE (member)-[membership:MEMBER_OF]->(group)
ON CREATE SET
membership.createdAt = toString(datetime()),
membership.updatedAt = null,
membership.updatedAt = toString(datetime()),
membership.role = $roleInGroup
ON MATCH SET
membership.updatedAt = toString(datetime()),

View File

@ -1,7 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable jest/expect-expect */
import Factory, { cleanDatabase } from '@db/factories'
import currentUser from '@graphql/queries/auth/currentUser.gql'
@ -234,23 +233,6 @@ describe('validateInviteCode', () => {
}),
)
})
// eslint-disable-next-line jest/no-disabled-tests
it.skip('throws authorization error when querying extended fields', async () => {
await expect(
query({ query: authenticatedValidateInviteCode, variables: { code: 'PERSNL' } }),
).resolves.toMatchObject({
data: {
validateInviteCode: {
code: 'PERSNL',
generatedBy: null,
invitedTo: null,
isValid: true,
},
},
errors: [{ message: 'Not Authorized!' }],
})
})
})
describe('as authenticated user', () => {
@ -307,27 +289,6 @@ describe('validateInviteCode', () => {
errors: undefined,
})
})
// This doesn't work because group permissions are fucked
// eslint-disable-next-line jest/no-disabled-tests
it.skip('throws authorization error when querying extended hidden group fields', async () => {
await expect(
query({ query: authenticatedValidateInviteCode, variables: { code: 'GRPHDN' } }),
).resolves.toMatchObject({
data: {
validateInviteCode: {
code: 'GRPHDN',
generatedBy: null,
invitedTo: null,
isValid: true,
},
},
errors: [{ message: 'Not Authorized!' }],
})
})
// eslint-disable-next-line jest/no-disabled-tests
it.skip('throws no authorization error when querying extended hidden group fields as member', async () => {})
})
})
@ -494,9 +455,6 @@ describe('generatePersonalInviteCode', () => {
errors: undefined,
})
})
// eslint-disable-next-line jest/no-disabled-tests
it.skip('returns a new invite code when colliding with an existing one', () => {})
})
})
@ -762,9 +720,6 @@ describe('generateGroupInviteCode', () => {
errors: undefined,
})
})
// eslint-disable-next-line jest/no-disabled-tests
it.skip('returns a new group invite code when colliding with an existing one', () => {})
})
describe('as authenticated not-member', () => {

View File

@ -118,7 +118,7 @@ export const redeemInviteCode = async (context: Context, code, newUser = false)
MERGE (user)-[membership:MEMBER_OF]->(group)
ON CREATE SET
membership.createdAt = toString(datetime()),
membership.updatedAt = null,
membership.updatedAt = toString(datetime()),
membership.role = $role
`,
variables: { user: context.user, code, role },

View File

@ -69,7 +69,7 @@ describe('Filter Posts', () => {
content: 'Elli wird fünf. Wir feiern ihren Geburtstag.',
postType: 'Event',
eventInput: {
eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(),
eventStart: new Date(now.getFullYear(), now.getMonth() + 1, 15).toISOString(),
eventVenue: 'Garten der Familie Maier',
},
},
@ -174,7 +174,7 @@ describe('Filter Posts', () => {
expect(result).toEqual([
expect.objectContaining({
id: 'e1',
eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(),
eventStart: new Date(now.getFullYear(), now.getMonth() + 1, 15).toISOString(),
}),
expect.objectContaining({
id: 'e2',
@ -184,9 +184,7 @@ describe('Filter Posts', () => {
})
})
// Does not work on months end
// eslint-disable-next-line jest/no-disabled-tests
describe.skip('order events by event start ascending', () => {
describe('order events by event start ascending', () => {
it('finds the events ordered accordingly', async () => {
const {
data: { Post: result },
@ -202,15 +200,13 @@ describe('Filter Posts', () => {
}),
expect.objectContaining({
id: 'e1',
eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(),
eventStart: new Date(now.getFullYear(), now.getMonth() + 1, 15).toISOString(),
}),
])
})
})
// Does not work on months end
// eslint-disable-next-line jest/no-disabled-tests
describe.skip('filter events by event start date', () => {
describe('filter events by event start date', () => {
it('finds only events after given date', async () => {
const {
data: { Post: result },
@ -231,7 +227,7 @@ describe('Filter Posts', () => {
expect(result).toEqual([
expect.objectContaining({
id: 'e1',
eventStart: new Date(now.getFullYear(), now.getMonth() + 1).toISOString(),
eventStart: new Date(now.getFullYear(), now.getMonth() + 1, 15).toISOString(),
}),
])
})

View File

@ -20,7 +20,6 @@ import unpushPost from '@graphql/queries/posts/unpushPost.gql'
import UpdatePost from '@graphql/queries/posts/UpdatePost.gql'
import { createApolloTestSetup } from '@root/test/helpers'
import type Image from '@db/models/Image'
import type { ApolloTestSetup } from '@root/test/helpers'
import type { Context } from '@src/context'
@ -885,50 +884,6 @@ describe('UpdatePost', () => {
})
})
})
// eslint-disable-next-line jest/no-disabled-tests
describe.skip('params.image', () => {
describe('is object', () => {
beforeEach(() => {
variables = { ...variables, image: { sensitive: true } }
})
it('updates the image', async () => {
await expect(
database.neode.first<typeof Image>('Image', { sensitive: true }, undefined),
).resolves.toBeFalsy()
await mutate({ mutation: UpdatePost, variables })
await expect(
database.neode.first<typeof Image>('Image', { sensitive: true }, undefined),
).resolves.toBeTruthy()
})
})
describe('is null', () => {
beforeEach(() => {
variables = { ...variables, image: null }
})
it('deletes the image', async () => {
await expect(database.neode.all('Image')).resolves.toHaveLength(6)
await mutate({ mutation: UpdatePost, variables })
await expect(database.neode.all('Image')).resolves.toHaveLength(5)
})
})
describe('is undefined', () => {
beforeEach(() => {
delete variables.image
})
it('keeps the image unchanged', async () => {
await expect(
database.neode.first<typeof Image>('Image', { sensitive: true }, undefined),
).resolves.toBeFalsy()
await mutate({ mutation: UpdatePost, variables })
await expect(
database.neode.first<typeof Image>('Image', { sensitive: true }, undefined),
).resolves.toBeFalsy()
})
})
})
})
})

View File

@ -219,7 +219,7 @@ export default {
} catch (e) {
if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed')
throw new UserInputError('Post with this slug already exists!')
throw new Error(e)
throw e
} finally {
await session.close()
}
@ -286,6 +286,10 @@ export default {
await createOrUpdateLocations('Post', post.id, locationName, session, context)
}
return post
} catch (e) {
if (e.code === 'Neo.ClientError.Schema.ConstraintValidationFailed')
throw new UserInputError('Post with this slug already exists!')
throw e
} finally {
await session.close()
}

View File

@ -239,7 +239,30 @@ describe('reports', () => {
})
})
it.todo('creates multiple filed reports')
it('creates separate reports for different resources', async () => {
await Factory.build(
'user',
{
id: 'second-abusive-user-id',
role: 'user',
name: 'second-abusive-user',
},
{
email: 'second-abusive@example.org',
},
)
const firstReport = await mutate({
mutation: fileReport,
variables: { ...variables, resourceId: 'abusive-user-id' },
})
const secondReport = await mutate({
mutation: fileReport,
variables: { ...variables, resourceId: 'second-abusive-user-id' },
})
expect(firstReport.data.fileReport.reportId).not.toEqual(
secondReport.data.fileReport.reportId,
)
})
})
describe('reported resource is a user', () => {

View File

@ -7,6 +7,7 @@ import SignupVerification from '@graphql/queries/auth/SignupVerification.gql'
import CreateGroup from '@graphql/queries/groups/CreateGroup.gql'
import UpdateGroup from '@graphql/queries/groups/UpdateGroup.gql'
import CreatePost from '@graphql/queries/posts/CreatePost.gql'
import UpdatePost from '@graphql/queries/posts/UpdatePost.gql'
import { createApolloTestSetup } from '@root/test/helpers'
import type { ApolloTestSetup } from '@root/test/helpers'
@ -424,7 +425,100 @@ describe('slugifyMiddleware', () => {
})
})
it.todo('UpdatePost')
describe('UpdatePost', () => {
let createPostResult
beforeEach(async () => {
createPostResult = await mutate({
mutation: CreatePost,
variables: {
title: 'I am a brand new post',
content: 'Some content',
categoryIds,
},
})
})
describe('if new slug not exists', () => {
describe('setting slug explicitly', () => {
it('has the new slug', async () => {
await expect(
mutate({
mutation: UpdatePost,
variables: {
id: createPostResult.data.CreatePost.id,
title: 'I am a brand new post',
content: 'Some content',
slug: 'my-brand-new-post',
},
}),
).resolves.toMatchObject({
data: {
UpdatePost: {
slug: 'my-brand-new-post',
},
},
errors: undefined,
})
})
})
})
describe('if new slug exists in another post', () => {
beforeEach(async () => {
await Factory.build(
'post',
{
title: 'Pre-existing post',
slug: 'pre-existing-post',
content: 'Someone else content',
},
{
categoryIds,
},
)
})
describe('setting slug explicitly', () => {
it('rejects UpdatePost', async () => {
try {
await expect(
mutate({
mutation: UpdatePost,
variables: {
id: createPostResult.data.CreatePost.id,
title: 'I am a brand new post',
content: 'Some content',
slug: 'pre-existing-post',
},
}),
).resolves.toMatchObject({
errors: [
{
message: 'Post with this slug already exists!',
},
],
})
} catch (error) {
throw new Error(`
${error}
Probably your database has no unique constraints!
To see all constraints go to http://localhost:7474/browser/ and
paste the following:
\`\`\`
CALL db.constraints();
\`\`\`
Learn how to setup the database here:
https://github.com/Ocelot-Social-Community/Ocelot-Social/blob/master/backend/README.md#database-indices-and-constraints
`)
}
})
})
})
})
describe('SignupVerification', () => {
beforeEach(() => {