2026-02-25 21:56:53 -05:00
import { createHash } from "node:crypto" ;
import { once } from "node:events" ;
2026-03-03 00:14:52 +00:00
import { request , type IncomingMessage } from "node:http" ;
2026-02-25 21:56:53 -05:00
import { setTimeout as sleep } from "node:timers/promises" ;
2025-12-07 22:52:57 +01:00
import { describe , expect , it , vi } from "vitest" ;
import { startTelegramWebhook } from "./webhook.js" ;
2026-02-25 21:56:53 -05:00
const handlerSpy = vi . hoisted ( ( ) = > vi . fn ( ( . . . _args : unknown [ ] ) : unknown = > undefined ) ) ;
2026-02-16 22:37:34 -05:00
const setWebhookSpy = vi . hoisted ( ( ) = > vi . fn ( ) ) ;
2026-02-25 21:56:53 -05:00
const deleteWebhookSpy = vi . hoisted ( ( ) = > vi . fn ( async ( ) = > true ) ) ;
const initSpy = vi . hoisted ( ( ) = > vi . fn ( async ( ) = > undefined ) ) ;
2026-02-16 22:37:34 -05:00
const stopSpy = vi . hoisted ( ( ) = > vi . fn ( ) ) ;
const webhookCallbackSpy = vi . hoisted ( ( ) = > vi . fn ( ( ) = > handlerSpy ) ) ;
const createTelegramBotSpy = vi . hoisted ( ( ) = >
vi . fn ( ( ) = > ( {
2026-02-25 21:56:53 -05:00
init : initSpy ,
api : { setWebhook : setWebhookSpy , deleteWebhook : deleteWebhookSpy } ,
2026-02-16 22:37:34 -05:00
stop : stopSpy ,
} ) ) ,
2025-12-08 01:48:53 +01:00
) ;
2026-01-12 21:59:28 -05:00
2026-02-25 21:56:53 -05:00
const WEBHOOK_POST_TIMEOUT_MS = process . platform === "win32" ? 20_000 : 8_000 ;
2026-03-02 06:41:31 +00:00
const TELEGRAM_TOKEN = "tok" ;
const TELEGRAM_SECRET = "secret" ;
const TELEGRAM_WEBHOOK_PATH = "/hook" ;
2026-02-25 21:56:53 -05:00
2026-03-03 00:14:52 +00:00
function collectResponseBody (
res : IncomingMessage ,
onDone : ( payload : { statusCode : number ; body : string } ) = > void ,
) : void {
const chunks : Buffer [ ] = [ ] ;
res . on ( "data" , ( chunk : Buffer | string ) = > {
chunks . push ( Buffer . isBuffer ( chunk ) ? chunk : Buffer.from ( chunk ) ) ;
} ) ;
res . on ( "end" , ( ) = > {
onDone ( {
statusCode : res.statusCode ? ? 0 ,
body : Buffer.concat ( chunks ) . toString ( "utf-8" ) ,
} ) ;
} ) ;
}
2026-01-15 17:20:17 +00:00
vi . mock ( "grammy" , async ( importOriginal ) = > {
const actual = await importOriginal < typeof import ( "grammy" ) > ( ) ;
2026-02-14 18:57:05 -08:00
return {
. . . actual ,
2026-02-17 11:59:41 +09:00
webhookCallback : webhookCallbackSpy ,
2026-02-14 18:57:05 -08:00
} ;
2026-01-15 17:20:17 +00:00
} ) ;
2025-12-07 22:52:57 +01:00
vi . mock ( "./bot.js" , ( ) = > ( {
2026-02-17 11:59:41 +09:00
createTelegramBot : createTelegramBotSpy ,
2025-12-07 22:52:57 +01:00
} ) ) ;
2026-02-25 21:56:53 -05:00
async function fetchWithTimeout (
input : string ,
init : Omit < RequestInit , "signal" > ,
timeoutMs : number ,
) : Promise < Response > {
const abort = new AbortController ( ) ;
const timer = setTimeout ( ( ) = > {
abort . abort ( ) ;
} , timeoutMs ) ;
try {
return await fetch ( input , { . . . init , signal : abort.signal } ) ;
} finally {
clearTimeout ( timer ) ;
}
}
async function postWebhookJson ( params : {
url : string ;
payload : string ;
secret? : string ;
timeoutMs? : number ;
} ) : Promise < Response > {
return await fetchWithTimeout (
params . url ,
{
method : "POST" ,
headers : {
"content-type" : "application/json" ,
. . . ( params . secret ? { "x-telegram-bot-api-secret-token" : params . secret } : { } ) ,
} ,
body : params.payload ,
} ,
params . timeoutMs ? ? 5 _000 ,
) ;
}
2026-03-13 23:12:17 +01:00
async function postWebhookHeadersOnly ( params : {
port : number ;
path : string ;
declaredLength : number ;
secret? : string ;
timeoutMs? : number ;
} ) : Promise < { statusCode : number ; body : string } > {
return await new Promise ( ( resolve , reject ) = > {
let settled = false ;
const finishResolve = ( value : { statusCode : number ; body : string } ) = > {
if ( settled ) {
return ;
}
settled = true ;
clearTimeout ( timeout ) ;
resolve ( value ) ;
} ;
const finishReject = ( error : unknown ) = > {
if ( settled ) {
return ;
}
settled = true ;
clearTimeout ( timeout ) ;
reject ( error ) ;
} ;
const req = request (
{
hostname : "127.0.0.1" ,
port : params.port ,
path : params.path ,
method : "POST" ,
headers : {
"content-type" : "application/json" ,
"content-length" : String ( params . declaredLength ) ,
. . . ( params . secret ? { "x-telegram-bot-api-secret-token" : params . secret } : { } ) ,
} ,
} ,
( res ) = > {
collectResponseBody ( res , ( payload ) = > {
finishResolve ( payload ) ;
req . destroy ( ) ;
} ) ;
} ,
) ;
const timeout = setTimeout ( ( ) = > {
req . destroy (
new Error ( ` webhook header-only post timed out after ${ params . timeoutMs ? ? 5 _000 } ms ` ) ,
) ;
finishReject ( new Error ( "timed out waiting for webhook response" ) ) ;
} , params . timeoutMs ? ? 5 _000 ) ;
req . on ( "error" , ( error ) = > {
if ( settled && ( error as NodeJS . ErrnoException ) . code === "ECONNRESET" ) {
return ;
}
finishReject ( error ) ;
} ) ;
req . flushHeaders ( ) ;
} ) ;
}
2026-02-25 21:56:53 -05:00
function createDeterministicRng ( seed : number ) : ( ) = > number {
let state = seed >>> 0 ;
return ( ) = > {
state = ( state * 1 _664_525 + 1 _013_904_223 ) >>> 0 ;
return state / 4 _294_967_296 ;
} ;
}
async function postWebhookPayloadWithChunkPlan ( params : {
port : number ;
path : string ;
payload : string ;
secret : string ;
mode : "single" | "random-chunked" ;
timeoutMs? : number ;
} ) : Promise < { statusCode : number ; body : string } > {
const payloadBuffer = Buffer . from ( params . payload , "utf-8" ) ;
return await new Promise ( ( resolve , reject ) = > {
let bytesQueued = 0 ;
let chunksQueued = 0 ;
let phase : "writing" | "awaiting-response" = "writing" ;
let settled = false ;
const finishResolve = ( value : { statusCode : number ; body : string } ) = > {
if ( settled ) {
return ;
}
settled = true ;
clearTimeout ( timeout ) ;
resolve ( value ) ;
} ;
const finishReject = ( error : unknown ) = > {
if ( settled ) {
return ;
}
settled = true ;
clearTimeout ( timeout ) ;
reject ( error ) ;
} ;
const req = request (
{
hostname : "127.0.0.1" ,
port : params.port ,
path : params.path ,
method : "POST" ,
headers : {
"content-type" : "application/json" ,
"content-length" : String ( payloadBuffer . length ) ,
"x-telegram-bot-api-secret-token" : params . secret ,
} ,
} ,
( res ) = > {
2026-03-03 00:14:52 +00:00
collectResponseBody ( res , finishResolve ) ;
2026-02-25 21:56:53 -05:00
} ,
) ;
const timeout = setTimeout ( ( ) = > {
finishReject (
new Error (
` webhook post timed out after ${ params . timeoutMs ? ? 15 _000 } ms (phase= ${ phase } , bytesQueued= ${ bytesQueued } , chunksQueued= ${ chunksQueued } , totalBytes= ${ payloadBuffer . length } ) ` ,
) ,
) ;
req . destroy ( ) ;
} , params . timeoutMs ? ? 15 _000 ) ;
req . on ( "error" , ( error ) = > {
finishReject ( error ) ;
} ) ;
const writeAll = async ( ) = > {
if ( params . mode === "single" ) {
req . end ( payloadBuffer ) ;
return ;
}
const rng = createDeterministicRng ( 26156 ) ;
let offset = 0 ;
while ( offset < payloadBuffer . length ) {
const remaining = payloadBuffer . length - offset ;
const nextSize = Math . max ( 1 , Math . min ( remaining , 1 + Math . floor ( rng ( ) * 8 _192 ) ) ) ;
const chunk = payloadBuffer . subarray ( offset , offset + nextSize ) ;
const canContinue = req . write ( chunk ) ;
offset += nextSize ;
bytesQueued = offset ;
chunksQueued += 1 ;
if ( chunksQueued % 10 === 0 ) {
await sleep ( 1 + Math . floor ( rng ( ) * 3 ) ) ;
}
if ( ! canContinue ) {
// Windows CI occasionally stalls on waiting for drain indefinitely.
// Bound the wait, then continue queuing this small (~1MB) payload.
await Promise . race ( [ once ( req , "drain" ) , sleep ( 25 ) ] ) ;
}
}
phase = "awaiting-response" ;
req . end ( ) ;
} ;
void writeAll ( ) . catch ( ( error ) = > {
finishReject ( error ) ;
} ) ;
} ) ;
}
function createNearLimitTelegramPayload ( ) : { payload : string ; sizeBytes : number } {
const maxBytes = 1 _024 * 1 _024 ;
const targetBytes = maxBytes - 4 _096 ;
const shell = { update_id : 77_777 , message : { text : "" } } ;
const shellSize = Buffer . byteLength ( JSON . stringify ( shell ) , "utf-8" ) ;
const textLength = Math . max ( 1 , targetBytes - shellSize ) ;
const pattern = "the quick brown fox jumps over the lazy dog " ;
const repeats = Math . ceil ( textLength / pattern . length ) ;
const text = pattern . repeat ( repeats ) . slice ( 0 , textLength ) ;
const payload = JSON . stringify ( {
update_id : 77_777 ,
message : { text } ,
} ) ;
return { payload , sizeBytes : Buffer.byteLength ( payload , "utf-8" ) } ;
}
function sha256 ( text : string ) : string {
return createHash ( "sha256" ) . update ( text ) . digest ( "hex" ) ;
}
2026-03-02 06:41:31 +00:00
type StartWebhookOptions = Omit <
Parameters < typeof startTelegramWebhook > [ 0 ] ,
"token" | "port" | "abortSignal"
> ;
type StartedWebhook = Awaited < ReturnType < typeof startTelegramWebhook > > ;
function getServerPort ( server : StartedWebhook [ "server" ] ) : number {
const address = server . address ( ) ;
if ( ! address || typeof address === "string" ) {
throw new Error ( "no addr" ) ;
}
return address . port ;
}
function webhookUrl ( port : number , webhookPath : string ) : string {
return ` http://127.0.0.1: ${ port } ${ webhookPath } ` ;
}
async function withStartedWebhook < T > (
options : StartWebhookOptions ,
run : ( ctx : { server : StartedWebhook [ "server" ] ; port : number } ) = > Promise < T > ,
) : Promise < T > {
const abort = new AbortController ( ) ;
const started = await startTelegramWebhook ( {
token : TELEGRAM_TOKEN ,
port : 0 ,
abortSignal : abort.signal ,
. . . options ,
} ) ;
try {
return await run ( { server : started.server , port : getServerPort ( started . server ) } ) ;
} finally {
abort . abort ( ) ;
}
}
function expectSingleNearLimitUpdate ( params : {
seenUpdates : Array < { update_id : number ; message : { text : string } } > ;
expected : { update_id : number ; message : { text : string } } ;
} ) {
expect ( params . seenUpdates ) . toHaveLength ( 1 ) ;
expect ( params . seenUpdates [ 0 ] ? . update_id ) . toBe ( params . expected . update_id ) ;
expect ( params . seenUpdates [ 0 ] ? . message . text . length ) . toBe ( params . expected . message . text . length ) ;
expect ( sha256 ( params . seenUpdates [ 0 ] ? . message . text ? ? "" ) ) . toBe (
sha256 ( params . expected . message . text ) ,
) ;
}
async function runNearLimitPayloadTest ( mode : "single" | "random-chunked" ) : Promise < void > {
const seenUpdates : Array < { update_id : number ; message : { text : string } } > = [ ] ;
webhookCallbackSpy . mockImplementationOnce (
( ) = >
vi . fn (
(
update : unknown ,
reply : ( json : string ) = > Promise < void > ,
_secretHeader : string | undefined ,
_unauthorized : ( ) = > Promise < void > ,
) = > {
seenUpdates . push ( update as { update_id : number ; message : { text : string } } ) ;
void reply ( "ok" ) ;
} ,
) as unknown as typeof handlerSpy ,
) ;
const { payload , sizeBytes } = createNearLimitTelegramPayload ( ) ;
expect ( sizeBytes ) . toBeLessThan ( 1 _024 * 1 _024 ) ;
expect ( sizeBytes ) . toBeGreaterThan ( 256 * 1 _024 ) ;
const expected = JSON . parse ( payload ) as { update_id : number ; message : { text : string } } ;
await withStartedWebhook (
{
secret : TELEGRAM_SECRET ,
path : TELEGRAM_WEBHOOK_PATH ,
} ,
async ( { port } ) = > {
const response = await postWebhookPayloadWithChunkPlan ( {
port ,
path : TELEGRAM_WEBHOOK_PATH ,
payload ,
secret : TELEGRAM_SECRET ,
mode ,
timeoutMs : WEBHOOK_POST_TIMEOUT_MS ,
} ) ;
expect ( response . statusCode ) . toBe ( 200 ) ;
expectSingleNearLimitUpdate ( { seenUpdates , expected } ) ;
} ,
) ;
}
2025-12-07 22:52:57 +01:00
describe ( "startTelegramWebhook" , ( ) = > {
it ( "starts server, registers webhook, and serves health" , async ( ) = > {
2026-02-25 21:56:53 -05:00
initSpy . mockClear ( ) ;
2026-01-12 21:59:28 -05:00
createTelegramBotSpy . mockClear ( ) ;
2026-02-14 18:57:05 -08:00
webhookCallbackSpy . mockClear ( ) ;
2026-02-25 22:06:08 -05:00
const runtimeLog = vi . fn ( ) ;
2026-01-12 22:16:46 -05:00
const cfg = { bindings : [ ] } ;
2026-03-02 06:41:31 +00:00
await withStartedWebhook (
2026-02-14 18:57:05 -08:00
{
2026-03-02 06:41:31 +00:00
secret : TELEGRAM_SECRET ,
accountId : "opie" ,
config : cfg ,
runtime : { log : runtimeLog , error : vi.fn ( ) , exit : vi.fn ( ) } ,
} ,
async ( { port } ) = > {
expect ( createTelegramBotSpy ) . toHaveBeenCalledWith (
expect . objectContaining ( {
accountId : "opie" ,
config : expect.objectContaining ( { bindings : [ ] } ) ,
} ) ,
) ;
const health = await fetch ( ` http://127.0.0.1: ${ port } /healthz ` ) ;
expect ( health . status ) . toBe ( 200 ) ;
expect ( initSpy ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( setWebhookSpy ) . toHaveBeenCalled ( ) ;
expect ( webhookCallbackSpy ) . toHaveBeenCalledWith (
expect . objectContaining ( {
api : expect.objectContaining ( {
setWebhook : expect.any ( Function ) ,
} ) ,
} ) ,
"callback" ,
{
secretToken : TELEGRAM_SECRET ,
onTimeout : "return" ,
timeoutMilliseconds : 10_000 ,
} ,
) ;
expect ( runtimeLog ) . toHaveBeenCalledWith (
expect . stringContaining ( "webhook local listener on http://127.0.0.1:" ) ,
) ;
expect ( runtimeLog ) . toHaveBeenCalledWith ( expect . stringContaining ( "/telegram-webhook" ) ) ;
expect ( runtimeLog ) . toHaveBeenCalledWith (
expect . stringContaining ( "webhook advertised to telegram on http://" ) ,
) ;
2026-02-14 18:57:05 -08:00
} ,
) ;
2026-03-08 02:27:05 +00:00
} ) ;
it ( "registers webhook with certificate when webhookCertPath is provided" , async ( ) = > {
setWebhookSpy . mockClear ( ) ;
await withStartedWebhook (
{
secret : TELEGRAM_SECRET ,
path : TELEGRAM_WEBHOOK_PATH ,
webhookCertPath : "/path/to/cert.pem" ,
} ,
async ( ) = > {
expect ( setWebhookSpy ) . toHaveBeenCalledWith (
expect . any ( String ) ,
expect . objectContaining ( {
certificate : expect.objectContaining ( {
fileData : "/path/to/cert.pem" ,
} ) ,
} ) ,
) ;
} ,
) ;
2025-12-07 22:52:57 +01:00
} ) ;
it ( "invokes webhook handler on matching path" , async ( ) = > {
handlerSpy . mockClear ( ) ;
2026-01-12 21:59:28 -05:00
createTelegramBotSpy . mockClear ( ) ;
2026-01-12 22:16:46 -05:00
const cfg = { bindings : [ ] } ;
2026-03-02 06:41:31 +00:00
await withStartedWebhook (
{
secret : TELEGRAM_SECRET ,
2026-01-12 22:16:46 -05:00
accountId : "opie" ,
2026-03-02 06:41:31 +00:00
config : cfg ,
path : TELEGRAM_WEBHOOK_PATH ,
} ,
async ( { port } ) = > {
expect ( createTelegramBotSpy ) . toHaveBeenCalledWith (
expect . objectContaining ( {
accountId : "opie" ,
config : expect.objectContaining ( { bindings : [ ] } ) ,
} ) ,
) ;
const payload = JSON . stringify ( { update_id : 1 , message : { text : "hello" } } ) ;
const response = await postWebhookJson ( {
url : webhookUrl ( port , TELEGRAM_WEBHOOK_PATH ) ,
payload ,
secret : TELEGRAM_SECRET ,
} ) ;
expect ( response . status ) . toBe ( 200 ) ;
2026-03-13 23:12:17 +01:00
expect ( handlerSpy ) . toHaveBeenCalledWith (
JSON . parse ( payload ) ,
expect . any ( Function ) ,
TELEGRAM_SECRET ,
expect . any ( Function ) ,
) ;
} ,
) ;
} ) ;
it ( "rejects unauthenticated requests before reading the request body" , async ( ) = > {
handlerSpy . mockClear ( ) ;
await withStartedWebhook (
{
secret : TELEGRAM_SECRET ,
path : TELEGRAM_WEBHOOK_PATH ,
} ,
async ( { port } ) = > {
const response = await postWebhookHeadersOnly ( {
port ,
path : TELEGRAM_WEBHOOK_PATH ,
declaredLength : 1_024 * 1 _024 ,
secret : "wrong-secret" ,
} ) ;
expect ( response . statusCode ) . toBe ( 401 ) ;
expect ( response . body ) . toBe ( "unauthorized" ) ;
expect ( handlerSpy ) . not . toHaveBeenCalled ( ) ;
2026-03-02 06:41:31 +00:00
} ,
2026-01-12 22:16:46 -05:00
) ;
2025-12-07 22:52:57 +01:00
} ) ;
2026-02-09 20:20:35 -08:00
it ( "rejects startup when webhook secret is missing" , async ( ) = > {
await expect (
startTelegramWebhook ( {
token : "tok" ,
} ) ,
) . rejects . toThrow ( /requires a non-empty secret token/i ) ;
} ) ;
2026-02-25 21:56:53 -05:00
it ( "registers webhook using the bound listening port when port is 0" , async ( ) = > {
setWebhookSpy . mockClear ( ) ;
2026-02-26 08:29:06 -05:00
const runtimeLog = vi . fn ( ) ;
2026-03-02 06:41:31 +00:00
await withStartedWebhook (
{
secret : TELEGRAM_SECRET ,
path : TELEGRAM_WEBHOOK_PATH ,
runtime : { log : runtimeLog , error : vi.fn ( ) , exit : vi.fn ( ) } ,
} ,
async ( { port } ) = > {
expect ( port ) . toBeGreaterThan ( 0 ) ;
expect ( setWebhookSpy ) . toHaveBeenCalledTimes ( 1 ) ;
expect ( setWebhookSpy ) . toHaveBeenCalledWith (
webhookUrl ( port , TELEGRAM_WEBHOOK_PATH ) ,
expect . objectContaining ( {
secret_token : TELEGRAM_SECRET ,
} ) ,
) ;
expect ( runtimeLog ) . toHaveBeenCalledWith (
` webhook local listener on ${ webhookUrl ( port , TELEGRAM_WEBHOOK_PATH ) } ` ,
) ;
} ,
) ;
2026-02-25 21:56:53 -05:00
} ) ;
it ( "keeps webhook payload readable when callback delays body read" , async ( ) = > {
handlerSpy . mockImplementationOnce ( async ( . . . args : unknown [ ] ) = > {
const [ update , reply ] = args as [ unknown , ( json : string ) = > Promise < void > ] ;
2026-03-02 17:48:36 +00:00
await sleep ( 10 ) ;
2026-02-25 21:56:53 -05:00
await reply ( JSON . stringify ( update ) ) ;
} ) ;
2026-03-02 06:41:31 +00:00
await withStartedWebhook (
{
secret : TELEGRAM_SECRET ,
path : TELEGRAM_WEBHOOK_PATH ,
} ,
async ( { port } ) = > {
const payload = JSON . stringify ( { update_id : 1 , message : { text : "hello" } } ) ;
const res = await postWebhookJson ( {
url : webhookUrl ( port , TELEGRAM_WEBHOOK_PATH ) ,
payload ,
secret : TELEGRAM_SECRET ,
} ) ;
expect ( res . status ) . toBe ( 200 ) ;
const responseBody = await res . text ( ) ;
expect ( JSON . parse ( responseBody ) ) . toEqual ( JSON . parse ( payload ) ) ;
} ,
) ;
2026-02-25 21:56:53 -05:00
} ) ;
it ( "keeps webhook payload readable across multiple delayed reads" , async ( ) = > {
const seenPayloads : string [ ] = [ ] ;
const delayedHandler = async ( . . . args : unknown [ ] ) = > {
const [ update , reply ] = args as [ unknown , ( json : string ) = > Promise < void > ] ;
2026-03-02 17:48:36 +00:00
await sleep ( 10 ) ;
2026-02-25 21:56:53 -05:00
seenPayloads . push ( JSON . stringify ( update ) ) ;
await reply ( "ok" ) ;
} ;
handlerSpy . mockImplementationOnce ( delayedHandler ) . mockImplementationOnce ( delayedHandler ) ;
2026-03-02 06:41:31 +00:00
await withStartedWebhook (
{
secret : TELEGRAM_SECRET ,
path : TELEGRAM_WEBHOOK_PATH ,
} ,
async ( { port } ) = > {
const payloads = [
JSON . stringify ( { update_id : 1 , message : { text : "first" } } ) ,
JSON . stringify ( { update_id : 2 , message : { text : "second" } } ) ,
] ;
for ( const payload of payloads ) {
const res = await postWebhookJson ( {
url : webhookUrl ( port , TELEGRAM_WEBHOOK_PATH ) ,
payload ,
secret : TELEGRAM_SECRET ,
} ) ;
expect ( res . status ) . toBe ( 200 ) ;
}
2026-02-25 21:56:53 -05:00
2026-03-02 06:41:31 +00:00
expect ( seenPayloads . map ( ( x ) = > JSON . parse ( x ) ) ) . toEqual ( payloads . map ( ( x ) = > JSON . parse ( x ) ) ) ;
} ,
) ;
2026-02-25 21:56:53 -05:00
} ) ;
it ( "processes a second request after first-request delayed-init data loss" , async ( ) = > {
const seenUpdates : unknown [ ] = [ ] ;
webhookCallbackSpy . mockImplementationOnce (
( ) = >
vi . fn (
(
update : unknown ,
reply : ( json : string ) = > Promise < void > ,
_secretHeader : string | undefined ,
_unauthorized : ( ) = > Promise < void > ,
) = > {
seenUpdates . push ( update ) ;
void ( async ( ) = > {
2026-03-02 17:48:36 +00:00
await sleep ( 10 ) ;
2026-02-25 21:56:53 -05:00
await reply ( "ok" ) ;
} ) ( ) ;
} ,
) as unknown as typeof handlerSpy ,
) ;
2026-03-02 06:41:31 +00:00
await withStartedWebhook (
{
secret : TELEGRAM_SECRET ,
path : TELEGRAM_WEBHOOK_PATH ,
} ,
async ( { port } ) = > {
const firstPayload = JSON . stringify ( { update_id : 100 , message : { text : "first" } } ) ;
const secondPayload = JSON . stringify ( { update_id : 101 , message : { text : "second" } } ) ;
const firstResponse = await postWebhookPayloadWithChunkPlan ( {
port ,
path : TELEGRAM_WEBHOOK_PATH ,
payload : firstPayload ,
secret : TELEGRAM_SECRET ,
mode : "single" ,
timeoutMs : WEBHOOK_POST_TIMEOUT_MS ,
} ) ;
const secondResponse = await postWebhookPayloadWithChunkPlan ( {
port ,
path : TELEGRAM_WEBHOOK_PATH ,
payload : secondPayload ,
secret : TELEGRAM_SECRET ,
mode : "single" ,
timeoutMs : WEBHOOK_POST_TIMEOUT_MS ,
} ) ;
2026-02-25 21:56:53 -05:00
2026-03-02 06:41:31 +00:00
expect ( firstResponse . statusCode ) . toBe ( 200 ) ;
expect ( secondResponse . statusCode ) . toBe ( 200 ) ;
expect ( seenUpdates ) . toEqual ( [ JSON . parse ( firstPayload ) , JSON . parse ( secondPayload ) ] ) ;
} ,
) ;
2026-02-25 21:56:53 -05:00
} ) ;
it ( "handles near-limit payload with random chunk writes and event-loop yields" , async ( ) = > {
2026-03-02 06:41:31 +00:00
await runNearLimitPayloadTest ( "random-chunked" ) ;
2026-02-25 21:56:53 -05:00
} ) ;
it ( "handles near-limit payload written in a single request write" , async ( ) = > {
2026-03-02 06:41:31 +00:00
await runNearLimitPayloadTest ( "single" ) ;
2026-02-25 21:56:53 -05:00
} ) ;
it ( "rejects payloads larger than 1MB before invoking webhook handler" , async ( ) = > {
handlerSpy . mockClear ( ) ;
2026-03-02 06:41:31 +00:00
await withStartedWebhook (
{
secret : TELEGRAM_SECRET ,
path : TELEGRAM_WEBHOOK_PATH ,
} ,
async ( { port } ) = > {
const responseOrError = await new Promise <
| { kind : "response" ; statusCode : number ; body : string }
| { kind : "error" ; code : string | undefined }
> ( ( resolve ) = > {
const req = request (
{
hostname : "127.0.0.1" ,
port ,
path : TELEGRAM_WEBHOOK_PATH ,
method : "POST" ,
headers : {
"content-type" : "application/json" ,
"content-length" : String ( 1 _024 * 1 _024 + 2 _048 ) ,
"x-telegram-bot-api-secret-token" : TELEGRAM_SECRET ,
} ,
2026-02-25 21:56:53 -05:00
} ,
2026-03-02 06:41:31 +00:00
( res ) = > {
2026-03-03 00:14:52 +00:00
collectResponseBody ( res , ( payload ) = > {
resolve ( { kind : "response" , . . . payload } ) ;
2026-03-02 06:41:31 +00:00
} ) ;
} ,
) ;
req . on ( "error" , ( error : NodeJS.ErrnoException ) = > {
resolve ( { kind : "error" , code : error.code } ) ;
} ) ;
req . end ( "{}" ) ;
2026-02-25 21:56:53 -05:00
} ) ;
2026-03-02 06:41:31 +00:00
if ( responseOrError . kind === "response" ) {
expect ( responseOrError . statusCode ) . toBe ( 413 ) ;
expect ( responseOrError . body ) . toBe ( "Payload too large" ) ;
} else {
expect ( responseOrError . code ) . toBeOneOf ( [ "ECONNRESET" , "EPIPE" ] ) ;
}
expect ( handlerSpy ) . not . toHaveBeenCalled ( ) ;
} ,
) ;
2026-02-25 21:56:53 -05:00
} ) ;
it ( "de-registers webhook when shutting down" , async ( ) = > {
deleteWebhookSpy . mockClear ( ) ;
const abort = new AbortController ( ) ;
await startTelegramWebhook ( {
2026-03-02 06:41:31 +00:00
token : TELEGRAM_TOKEN ,
secret : TELEGRAM_SECRET ,
2026-02-25 21:56:53 -05:00
port : 0 ,
abortSignal : abort.signal ,
2026-03-02 06:41:31 +00:00
path : TELEGRAM_WEBHOOK_PATH ,
2026-02-25 21:56:53 -05:00
} ) ;
abort . abort ( ) ;
2026-03-02 17:48:36 +00:00
await vi . waitFor ( ( ) = > expect ( deleteWebhookSpy ) . toHaveBeenCalledTimes ( 1 ) ) ;
2026-02-25 21:56:53 -05:00
expect ( deleteWebhookSpy ) . toHaveBeenCalledWith ( { drop_pending_updates : false } ) ;
} ) ;
2025-12-07 22:52:57 +01:00
} ) ;