refactor(backend): reports query parameterization and resolver cleanup with test coverage (#9156)

This commit is contained in:
Ulf Gebhardt 2026-02-09 20:05:59 +01:00 committed by GitHub
parent 7162f3bd4e
commit c33ec0bd11
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 224 additions and 43 deletions

View File

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

View File

@ -1,8 +1,20 @@
import gql from 'graphql-tag'
export const reports = gql`
query ($closed: Boolean) {
reports(orderBy: createdAt_desc, closed: $closed) {
query (
$orderBy: ReportOrdering
$reviewed: Boolean
$closed: Boolean
$first: Int
$offset: Int
) {
reports(
orderBy: $orderBy
reviewed: $reviewed
closed: $closed
first: $first
offset: $offset
) {
id
createdAt
updatedAt

View File

@ -620,32 +620,31 @@ describe('file a report on a resource', () => {
),
])
authenticatedUser = await currentUser.toJson()
await Promise.all([
mutate({
mutation: fileReport,
variables: {
resourceId: 'abusive-post-1',
reasonCategory: 'other',
reasonDescription: 'This comment is bigoted',
},
}),
mutate({
mutation: fileReport,
variables: {
resourceId: 'abusive-comment-1',
reasonCategory: 'discrimination_etc',
reasonDescription: 'This post is bigoted',
},
}),
mutate({
mutation: fileReport,
variables: {
resourceId: 'abusive-user-1',
reasonCategory: 'doxing',
reasonDescription: 'This user is harassing me with bigoted remarks',
},
}),
])
// Sequential to ensure distinct createdAt values for orderBy tests
await mutate({
mutation: fileReport,
variables: {
resourceId: 'abusive-post-1',
reasonCategory: 'other',
reasonDescription: 'This post is bigoted',
},
})
await mutate({
mutation: fileReport,
variables: {
resourceId: 'abusive-comment-1',
reasonCategory: 'discrimination_etc',
reasonDescription: 'This comment is bigoted',
},
})
await mutate({
mutation: fileReport,
variables: {
resourceId: 'abusive-user-1',
reasonCategory: 'doxing',
reasonDescription: 'This user is harassing me with bigoted remarks',
},
})
authenticatedUser = null
})
@ -707,7 +706,7 @@ describe('file a report on a resource', () => {
}),
createdAt: expect.any(String),
reasonCategory: 'other',
reasonDescription: 'This comment is bigoted',
reasonDescription: 'This post is bigoted',
}),
]),
}),
@ -727,7 +726,7 @@ describe('file a report on a resource', () => {
}),
createdAt: expect.any(String),
reasonCategory: 'discrimination_etc',
reasonDescription: 'This post is bigoted',
reasonDescription: 'This comment is bigoted',
}),
]),
}),
@ -737,6 +736,176 @@ describe('file a report on a resource', () => {
const { data } = await query({ query: reports })
expect(data).toEqual(expected)
})
describe('orderBy', () => {
it('createdAt_asc returns reports in ascending order', async () => {
authenticatedUser = await moderator.toJson()
const { data } = await query({
query: reports,
variables: { orderBy: 'createdAt_asc' },
})
const sorted = [...data.reports].sort((a, b) => (a.createdAt > b.createdAt ? 1 : -1))
expect(data.reports).toEqual(sorted)
})
it('createdAt_desc returns reports in descending order', async () => {
authenticatedUser = await moderator.toJson()
const { data } = await query({
query: reports,
variables: { orderBy: 'createdAt_desc' },
})
const sorted = [...data.reports].sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1))
expect(data.reports).toEqual(sorted)
})
})
describe('reviewed filter', () => {
it('reviewed: false returns only unreviewed reports', async () => {
authenticatedUser = await moderator.toJson()
const { data } = await query({
query: reports,
variables: { reviewed: false },
})
expect(data.reports).toHaveLength(3)
})
it('reviewed: true returns only reviewed reports', async () => {
authenticatedUser = await moderator.toJson()
// review one report
await mutate({
mutation: review,
variables: { resourceId: 'abusive-post-1', disable: false, closed: false },
})
const { data } = await query({
query: reports,
variables: { reviewed: true },
})
expect(data.reports).toHaveLength(1)
expect(data.reports[0].resource.id).toBe('abusive-post-1')
})
})
describe('closed filter', () => {
it('closed: false returns only open reports', async () => {
authenticatedUser = await moderator.toJson()
const { data } = await query({
query: reports,
variables: { closed: false },
})
expect(data.reports).toHaveLength(3)
data.reports.forEach((report) => {
expect(report.closed).toBe(false)
})
})
it('closed: true returns only closed reports', async () => {
authenticatedUser = await moderator.toJson()
// close one report via review
await mutate({
mutation: review,
variables: { resourceId: 'abusive-post-1', disable: false, closed: true },
})
const { data } = await query({
query: reports,
variables: { closed: true },
})
expect(data.reports).toHaveLength(1)
expect(data.reports[0].resource.id).toBe('abusive-post-1')
expect(data.reports[0].closed).toBe(true)
})
})
describe('combined reviewed and closed filter', () => {
it('returns only reports matching both filters', async () => {
authenticatedUser = await moderator.toJson()
// review and close one report
await mutate({
mutation: review,
variables: { resourceId: 'abusive-post-1', disable: false, closed: true },
})
// review but keep open another report
await mutate({
mutation: review,
variables: { resourceId: 'abusive-user-1', disable: false, closed: false },
})
const { data } = await query({
query: reports,
variables: { reviewed: true, closed: true },
})
expect(data.reports).toHaveLength(1)
expect(data.reports[0].resource.id).toBe('abusive-post-1')
expect(data.reports[0].closed).toBe(true)
})
it('reviewed: true, closed: false returns reviewed but open reports', async () => {
authenticatedUser = await moderator.toJson()
// review and close one report
await mutate({
mutation: review,
variables: { resourceId: 'abusive-post-1', disable: false, closed: true },
})
// review but keep open another report
await mutate({
mutation: review,
variables: { resourceId: 'abusive-user-1', disable: false, closed: false },
})
const { data } = await query({
query: reports,
variables: { reviewed: true, closed: false },
})
expect(data.reports).toHaveLength(1)
expect(data.reports[0].resource.id).toBe('abusive-user-1')
expect(data.reports[0].closed).toBe(false)
})
})
describe('pagination', () => {
it('first: 2 returns only 2 reports', async () => {
authenticatedUser = await moderator.toJson()
const { data } = await query({
query: reports,
variables: { first: 2 },
})
expect(data.reports).toHaveLength(2)
})
it('first: 1 returns only 1 report', async () => {
authenticatedUser = await moderator.toJson()
const { data } = await query({
query: reports,
variables: { first: 1 },
})
expect(data.reports).toHaveLength(1)
})
it('offset: 1 skips the first report', async () => {
authenticatedUser = await moderator.toJson()
const { data: allData } = await query({
query: reports,
variables: { orderBy: 'createdAt_asc' },
})
const { data: offsetData } = await query({
query: reports,
variables: { orderBy: 'createdAt_asc', offset: 1 },
})
expect(offsetData.reports).toHaveLength(allData.reports.length - 1)
expect(offsetData.reports[0].id).toBe(allData.reports[1].id)
})
it('first and offset combined for paging', async () => {
authenticatedUser = await moderator.toJson()
const { data: allData } = await query({
query: reports,
variables: { orderBy: 'createdAt_asc' },
})
const { data: pageData } = await query({
query: reports,
variables: { orderBy: 'createdAt_asc', first: 1, offset: 1 },
})
expect(pageData.reports).toHaveLength(1)
expect(pageData.reports[0].id).toBe(allData.reports[1].id)
})
})
})
})
})

View File

@ -45,7 +45,8 @@ export default {
reports: async (_parent, params, context, _resolveInfo) => {
const { driver } = context
const session = driver.session()
let orderByClause, filterClause
let orderByClause
const filterClauses: string[] = []
switch (params.orderBy) {
case 'createdAt_asc':
orderByClause = 'ORDER BY report.createdAt ASC'
@ -59,26 +60,24 @@ export default {
switch (params.reviewed) {
case true:
filterClause = 'AND ((report)<-[:REVIEWED]-(:User))'
filterClauses.push('AND ((report)<-[:REVIEWED]-(:User))')
break
case false:
filterClause = 'AND NOT ((report)<-[:REVIEWED]-(:User))'
filterClauses.push('AND NOT ((report)<-[:REVIEWED]-(:User))')
break
default:
filterClause = ''
}
switch (params.closed) {
case true:
filterClause = 'AND report.closed = true'
filterClauses.push('AND report.closed = true')
break
case false:
filterClause = 'AND report.closed = false'
break
default:
filterClauses.push('AND report.closed = false')
break
}
const filterClause = filterClauses.join(' ')
const offset =
params.offset && typeof params.offset === 'number' ? `SKIP ${params.offset}` : ''
const limit = params.first && typeof params.first === 'number' ? `LIMIT ${params.first}` : ''
@ -114,7 +113,8 @@ export default {
},
},
Report: {
filed: async (parent, _params, context, _resolveInfo) => {
// This field is inline queried in the cypher statement above
/* filed: async (parent, _params, context, _resolveInfo) => {
if (typeof parent.filed !== 'undefined') return parent.filed
const session = context.driver.session()
const { id } = parent
@ -146,9 +146,9 @@ export default {
session.close()
}
return filed
},
}, */
reviewed: async (parent, _params, context, _resolveInfo) => {
if (typeof parent.reviewed !== 'undefined') return parent.reviewed
// if (typeof parent.reviewed !== 'undefined') return parent.reviewed
const session = context.driver.session()
const { id } = parent
let reviewed