5type TestFn = () => Promise<{passed: boolean, msg: string}> 6type TestAdder = (name: string, fn: TestFn) => void 8const gameActionMutation = `mutation($gameRoom: String!, $actionJSON: JSON!) { gameAction(gameRoom: $gameRoom, actionJSON: $actionJSON) }` 10// Add common hardening tests to a test collector 11export const addCommonHardeningTests = (test: TestAdder, baseUrl: string, opts?: {hasQuota?: boolean}) => { 12 test('queryDepthLimitEnforced', async () => { 13 const deepQuery = `query { __schema { types { fields { type { ofType { ofType { ofType { name } } } } } } } }` 14 const resp = await gqlFetch({baseUrl, query: deepQuery}) 16 const hasIntrospectionDisabled = hasGqlErrMatch(resp, 'introspection') || (resp as any)?.errors?.some((e: any) => e.message === 'Internal Server Error') 17 const passed = hasDepthError || hasIntrospectionDisabled 20 msg: passed ? '' : `expected depth/introspection error, got: ${JSON.stringify((resp as any)?.errors || 'no error')}`, 24 test('normalDepthQueryAllowed', async () => { 25 const resp = await gqlFetch({baseUrl, query: `query { gqGame(gameRoom: "test") { id gqGameState } }`}) 27 return {passed: !hasDepthError, msg: hasDepthError ? `unexpected depth error: ${JSON.stringify((resp as any)?.errors)}` : ''} 30 test('batchedRequestsRejected', async () => { 31 const resp = await fetch(`${baseUrl}/api/graphql`, { 33 body: JSON.stringify([{query: '{ gqGame(gameRoom: "a") { id } }'}, {query: '{ gqGame(gameRoom: "b") { id } }'}]), 35 const data = await resp.json() 37 const isNotArrayResponse = !Array.isArray(data) 39 passed: isBatchError || isNotArrayResponse, 40 msg: (isBatchError || isNotArrayResponse) ? '' : `unexpected: got array of ${(data as any[])?.length} responses`, 44 test('oversizedPlayerAliasRejected', async () => { 45 const oversizedAlias = genTestStr(inputLimits.playerAliasMaxChars + 10) 46 const resp = await gqlFetch({baseUrl, query: gameActionMutation, variables: { 47 gameRoom: 'test-hardening', actionJSON: {playerAction: 'setPlayerAlias', playerAlias: oversizedAlias}, 50 return {passed, msg: passed ? `${oversizedAlias.length} chars` : `expected 'too long' error, got: ${JSON.stringify((resp as any)?.errors?.[0]?.message || resp)}`} 53 test('oversizedDescriptionRejected', async () => { 54 const oversizedDesc = genTestStr(inputLimits.descriptionTextMaxChars + 50) 55 const resp = await gqlFetch({baseUrl, query: gameActionMutation, variables: { 56 gameRoom: 'test-hardening', actionJSON: {playerAction: 'submitDescription', descriptionText: oversizedDesc}, 59 return {passed, msg: passed ? `${oversizedDesc.length} chars` : `expected 'too long' error, got: ${JSON.stringify((resp as any)?.errors?.[0]?.message || resp)}`} 62 test('oversizedPayloadRejected', async () => { 63 const largePayload = genTestStr(inputLimits.actionJsonMaxBytes + 1000) 64 const resp = await gqlFetch({baseUrl, query: gameActionMutation, variables: { 65 gameRoom: 'test-hardening', actionJSON: {playerAction: 'test', largeData: largePayload}, 68 return {passed, msg: passed ? `~${Math.round(largePayload.length / 1024)}KB` : `expected 'too large' error, got: ${JSON.stringify((resp as any)?.errors?.[0]?.message || resp)}`} 71 test('normalSizedInputsAllowed', async () => { 72 const normalAlias = genTestStr(inputLimits.playerAliasMaxChars - 5) 73 const resp = await gqlFetch({baseUrl, query: gameActionMutation, variables: { 74 gameRoom: 'test-hardening', actionJSON: {playerAction: 'setPlayerAlias', playerAlias: normalAlias}, 77 return {passed: !hasSizeError, msg: hasSizeError ? `unexpected size error: ${JSON.stringify((resp as any)?.errors)}` : ''} 80 if (!opts?.hasQuota) return 81 test('gqlQuotaLimitEnforced [~2s]', async () => { 83 if (!testIpHeader) return {passed: false, msg: 'appSecret not available, cannot test quota'} 84 const randomOctet = () => Math.floor(Math.random() * 255) 85 const fakeIp = `10.88.${randomOctet()}.${randomOctet()}` 86 const query = `query { gqDeployInfo }` 87 const send = () => gqlFetch({baseUrl, query, extraHeaders: {[testIpHeader]: fakeIp}}) 88 for (let i = 0; i < 30; i++) await send() 89 const resp = await send() 91 return {passed, msg: passed ? '31st request rate limited' : `expected RATE_LIMITED after 30 req/min, got: ${JSON.stringify((resp as any)?.errors?.[0]?.message || 'no error')}`} 95// Export gameActionMutation for app-specific tests that need it 96export { gameActionMutation, inputLimits }