From a69f2a5dac7466e8297c2f324fc32cdf4b7ae81b Mon Sep 17 00:00:00 2001 From: Devin Zuczek Date: Wed, 29 Jul 2026 12:47:17 -0400 Subject: [PATCH] update moderationblockdetails to work on both clients for now --- apps/api/src/openapi.ts | 5 ++- apps/api/src/routes/moderation.ts | 23 ++++++----- apps/api/src/test/integration/api.test.ts | 47 +++++++++++++---------- 3 files changed, 43 insertions(+), 32 deletions(-) diff --git a/apps/api/src/openapi.ts b/apps/api/src/openapi.ts index 7efda26..b6f4556 100644 --- a/apps/api/src/openapi.ts +++ b/apps/api/src/openapi.ts @@ -391,10 +391,11 @@ export const SubscriptionResponse = z.object({ // ---- Moderation ------------------------------------------------------------ /** - * `GET /api/PlayerReporting/v1/moderationBlockDetails` — always the "not blocked" + * `GET|POST /api/PlayerReporting/v1/moderationBlockDetails` — always the "not blocked" * answer (no ban storage yet), mirroring the reference server's stub * `ReturnModerationBlockDetails()`. `ReportCategory` is `Unknown` (-1) rather than 0, - * which is a real category, and `Message` is the empty string the reference sends. + * which is a real category, and `Message` is null — the client distinguishes "no + * message" from a blank one, so we send null where the reference sends an empty string. * `IsVoiceModAutoban`/`TimeoutStartedAt` are on the DTO but unset by that stub, so * they carry their C# defaults (false / null). */ diff --git a/apps/api/src/routes/moderation.ts b/apps/api/src/routes/moderation.ts index 31ab5f2..8bb6ecb 100644 --- a/apps/api/src/routes/moderation.ts +++ b/apps/api/src/routes/moderation.ts @@ -16,22 +16,27 @@ import type { App } from '../context' export const moderationRoutes = new Hono({ strict: false }) // Whether the caller is currently blocked (banned / timed out / host-kicked). No ban // storage yet, so this is always the "not blocked" answer — the reference server's - // stub `ReturnModerationBlockDetails()` verbatim. `ReportCategory` is `Unknown` (-1), - // not 0, which is a real category, and `Message` is the empty string that stub sends. + // stub `ReturnModerationBlockDetails()`. `ReportCategory` is `Unknown` (-1), not 0, + // which is a real category. `Message` is null rather than the empty string that stub + // sends: the client distinguishes "no message" from a blank one. // `IsVoiceModAutoban`/`TimeoutStartedAt` are on the DTO but left unset there, so they // go out with their C# defaults. - // POST with no body — the client's actual call, despite this being a pure read. - .post( + // POST with no body is the client's actual call, despite this being a pure read; it + // answers GET too, so the path is reachable either way. + .on( + ['GET', 'POST'], '/api/PlayerReporting/v1/moderationBlockDetails', describeRoute({ tags: ['Moderation'], summary: 'Whether the caller is blocked', description: 'Ban / timeout / host-kick state for the caller. There is no ban storage yet, so ' + - 'this is always the “not blocked” answer, matching the reference server’s stub: ' + - '`ReportCategory` is `Unknown` (-1) rather than 0, which is a real category, and ' + - '`Message` is an empty string. `IsVoiceModAutoban` and `TimeoutStartedAt` are on ' + - 'the DTO but unset by that stub, so they carry their defaults.', + 'this is always the “not blocked” answer, following the reference server’s stub: ' + + '`ReportCategory` is `Unknown` (-1) rather than 0, which is a real category. ' + + '`Message` is null rather than the empty string that stub sends — the client ' + + 'distinguishes “no message” from a blank one. `IsVoiceModAutoban` and ' + + '`TimeoutStartedAt` are on the DTO but unset by that stub, so they carry their ' + + 'defaults.', responses: { 200: json(ModerationBlockDetails, 'Always “not blocked”') }, }), (c) => @@ -42,7 +47,7 @@ export const moderationRoutes = new Hono({ strict: false }) IsBan: false, IsHostKick: false, IsVoiceModAutoban: false, - Message: '', + Message: null, PlayerIdReporter: null, TimeoutStartedAt: null, }) diff --git a/apps/api/src/test/integration/api.test.ts b/apps/api/src/test/integration/api.test.ts index fede1bc..e635adb 100644 --- a/apps/api/src/test/integration/api.test.ts +++ b/apps/api/src/test/integration/api.test.ts @@ -233,27 +233,31 @@ describe('public endpoints', () => { expect(await res.json()).toEqual([]) }) - test('POST /api/PlayerReporting/v1/moderationBlockDetails reports "not blocked"', async () => { - // The client POSTs this with no body, despite it being a pure read. - const res = await exports.default.fetch( - `${ORIGIN}/api/PlayerReporting/v1/moderationBlockDetails`, - { method: 'POST' } - ) - expect(res.status).toBe(200) - // The reference server's stub verbatim: ReportCategory -1 = Unknown (0 is a real - // category) and an empty-string Message. - expect(await res.json()).toEqual({ - ReportCategory: -1, - Duration: 0, - GameSessionId: 0, - IsBan: false, - IsHostKick: false, - IsVoiceModAutoban: false, - Message: '', - PlayerIdReporter: null, - TimeoutStartedAt: null, - }) - }) + // The client POSTs this with no body, despite it being a pure read; the route answers + // GET as well, and both methods serve the same body. + test.each(['GET', 'POST'])( + '%s /api/PlayerReporting/v1/moderationBlockDetails reports "not blocked"', + async (method) => { + const res = await exports.default.fetch( + `${ORIGIN}/api/PlayerReporting/v1/moderationBlockDetails`, + { method } + ) + expect(res.status).toBe(200) + // ReportCategory -1 = Unknown (0 is a real category). Message is null, not the + // reference stub's empty string — the client tells "no message" from a blank one. + expect(await res.json()).toEqual({ + ReportCategory: -1, + Duration: 0, + GameSessionId: 0, + IsBan: false, + IsHostKick: false, + IsVoiceModAutoban: false, + Message: null, + PlayerIdReporter: null, + TimeoutStartedAt: null, + }) + } + ) // Unauthenticated by design — the client posts this before it has an account, so // there's no bearer token to check and nothing to attribute the id to. @@ -1899,6 +1903,7 @@ describe('openapi', () => { ) expect([...documented].sort()).toEqual([ 'DELETE /api/images/v1/deletesaved', + 'GET /api/PlayerReporting/v1/moderationBlockDetails', 'GET /api/PlayerReporting/v1/voteToKickReasons', 'GET /api/activities/charades/v1/words/{activity}', 'GET /api/announcement/v1/get',