diff --git a/packages/webhook/webhook-github/tests/handler.spec.ts b/packages/webhook/webhook-github/tests/handler.spec.ts index 424216161e..25e6dda2f5 100644 --- a/packages/webhook/webhook-github/tests/handler.spec.ts +++ b/packages/webhook/webhook-github/tests/handler.spec.ts @@ -1,5 +1,5 @@ import { createHmac } from 'node:crypto' -import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http' +import { createServer, request as httpRequest, type IncomingMessage, type Server, type ServerResponse } from 'node:http' import type { AddressInfo } from 'node:net' import type { Context } from '@deepseek-ai/cordis' import { afterEach, describe, expect, it, vi } from 'vitest' @@ -81,6 +81,37 @@ async function post( }) } +/** Send body chunks without Content-Length through a real Node client socket. */ +async function postChunked( + base: string, + chunks: readonly string[], + endDelayMs = 0, +): Promise<{ body: string; status: number }> { + return await new Promise((resolve, reject) => { + const request = httpRequest(base, { + method: 'POST', + headers: { + connection: 'close', + 'content-type': 'application/json', + 'transfer-encoding': 'chunked', + 'x-hub-signature-256': 'sha256=unused', + 'x-github-event': 'pull_request', + 'x-github-delivery': 'chunked-delivery', + }, + }, (response) => { + let body = '' + response.setEncoding('utf8') + response.on('data', (chunk: string) => { body += chunk }) + response.on('end', () => { resolve({ body, status: response.statusCode ?? 0 }) }) + }) + request.once('error', reject) + request.once('socket', (socket) => { socket.setNoDelay(true) }) + for (const chunk of chunks) request.write(chunk) + if (endDelayMs === 0) request.end() + else setTimeout(() => { request.end() }, endDelayMs) + }) +} + describe('GitHub webhook HTTP handler', () => { it('verifies, projects, dispatches, and answers 202', async () => { const fake = fakeContext() @@ -180,7 +211,7 @@ describe('GitHub webhook HTTP handler', () => { expect(fake.dispatch).not.toHaveBeenCalled() }) - it('rejects declared and streamed bodies over the configured cap', async () => { + it('rejects a declared body over the configured cap', async () => { const fake = fakeContext() const base = await serve(fake.ctx, 2) const response = await post(base, '{} ') @@ -188,6 +219,17 @@ describe('GitHub webhook HTTP handler', () => { expect(fake.dispatch).not.toHaveBeenCalled() }) + it('answers 413 for a chunked body over the cap without resetting the connection', async () => { + const fake = fakeContext() + const base = await serve(fake.ctx, 2) + + await expect(postChunked(base, ['abc'], 50)).resolves.toEqual({ + body: 'request body is too large', + status: 413, + }) + expect(fake.dispatch).not.toHaveBeenCalled() + }) + it('answers 503 when the credential or runtime is unavailable', async () => { const missing = fakeContext() missing.setSecret(undefined)