From c33ec0bd1159da5b325461e4e4ab4cc1b365ea5c Mon Sep 17 00:00:00 2001 From: Ulf Gebhardt Date: Mon, 9 Feb 2026 20:05:59 +0100 Subject: [PATCH] refactor(backend): reports query parameterization and resolver cleanup with test coverage (#9156) --- backend/jest.config.cjs | 2 +- backend/src/graphql/queries/reports.ts | 16 +- backend/src/graphql/resolvers/reports.spec.ts | 225 +++++++++++++++--- backend/src/graphql/resolvers/reports.ts | 24 +- 4 files changed, 224 insertions(+), 43 deletions(-) diff --git a/backend/jest.config.cjs b/backend/jest.config.cjs index c78de9155..285654e3a 100644 --- a/backend/jest.config.cjs +++ b/backend/jest.config.cjs @@ -18,7 +18,7 @@ module.exports = { ], coverageThreshold: { global: { - lines: 92, + lines: 93, }, }, testMatch: ['**/src/**/?(*.)+(spec|test).ts?(x)'], diff --git a/backend/src/graphql/queries/reports.ts b/backend/src/graphql/queries/reports.ts index 13849d3e4..f4549bb00 100644 --- a/backend/src/graphql/queries/reports.ts +++ b/backend/src/graphql/queries/reports.ts @@ -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 diff --git a/backend/src/graphql/resolvers/reports.spec.ts b/backend/src/graphql/resolvers/reports.spec.ts index 5b51b1073..75d227ed0 100644 --- a/backend/src/graphql/resolvers/reports.spec.ts +++ b/backend/src/graphql/resolvers/reports.spec.ts @@ -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) + }) + }) }) }) }) diff --git a/backend/src/graphql/resolvers/reports.ts b/backend/src/graphql/resolvers/reports.ts index b8886c48f..fb619e44b 100644 --- a/backend/src/graphql/resolvers/reports.ts +++ b/backend/src/graphql/resolvers/reports.ts @@ -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