@@ -2,12 +2,13 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
22
33import { buildPackagistUserAgent , fetchPackagistP2 , fetchPackagistStats } from '../fetchPackage'
44
5- // Minimal Response stand-in for the global fetch mock.
5+ // Minimal Response stand-in for the global fetch mock. `body.cancel` is a spy so
6+ // error-path tests can assert the response body is drained before returning.
67function fakeResponse (
78 status : number ,
89 body ?: unknown ,
910 opts : { jsonThrows ?: boolean ; lastModified ?: string } = { } ,
10- ) : Response {
11+ ) : Response & { body : { cancel : ReturnType < typeof vi . fn > } } {
1112 return {
1213 status,
1314 ok : status >= 200 && status < 300 ,
@@ -19,7 +20,8 @@ function fakeResponse(
1920 if ( opts . jsonThrows ) throw new Error ( 'bad json' )
2021 return body
2122 } ,
22- } as unknown as Response
23+ body : { cancel : vi . fn ( ) } ,
24+ } as unknown as Response & { body : { cancel : ReturnType < typeof vi . fn > } }
2325}
2426
2527const validStats = { package : { name : 'monolog/monolog' , downloads : { monthly : 5 } } }
@@ -65,28 +67,35 @@ describe('fetchPackagistStats', () => {
6567 expect ( headers [ 'User-Agent' ] ) . toMatch ( / m a i l t o = / )
6668 } )
6769
68- it ( 'maps 404 → NOT_FOUND' , async ( ) => {
69- vi . stubGlobal ( 'fetch' , vi . fn ( ) . mockResolvedValue ( fakeResponse ( 404 ) ) )
70+ it ( 'maps 404 → NOT_FOUND and drains the response body' , async ( ) => {
71+ const res = fakeResponse ( 404 )
72+ vi . stubGlobal ( 'fetch' , vi . fn ( ) . mockResolvedValue ( res ) )
7073 expect ( await fetchPackagistStats ( 'gone/gone' ) ) . toMatchObject ( {
7174 kind : 'NOT_FOUND' ,
7275 statusCode : 404 ,
7376 } )
77+ // an undrained body can pin the socket instead of returning it to the shared pool
78+ expect ( res . body . cancel ) . toHaveBeenCalled ( )
7479 } )
7580
76- it ( 'maps 429 → RATE_LIMIT' , async ( ) => {
77- vi . stubGlobal ( 'fetch' , vi . fn ( ) . mockResolvedValue ( fakeResponse ( 429 ) ) )
81+ it ( 'maps 429 → RATE_LIMIT and drains the response body' , async ( ) => {
82+ const res = fakeResponse ( 429 )
83+ vi . stubGlobal ( 'fetch' , vi . fn ( ) . mockResolvedValue ( res ) )
7884 expect ( await fetchPackagistStats ( 'busy/busy' ) ) . toMatchObject ( {
7985 kind : 'RATE_LIMIT' ,
8086 statusCode : 429 ,
8187 } )
88+ expect ( res . body . cancel ) . toHaveBeenCalled ( )
8289 } )
8390
84- it ( 'maps other non-ok statuses (5xx) → TRANSIENT with the status code' , async ( ) => {
85- vi . stubGlobal ( 'fetch' , vi . fn ( ) . mockResolvedValue ( fakeResponse ( 503 ) ) )
91+ it ( 'maps other non-ok statuses (5xx) → TRANSIENT with the status code and drains the body' , async ( ) => {
92+ const res = fakeResponse ( 503 )
93+ vi . stubGlobal ( 'fetch' , vi . fn ( ) . mockResolvedValue ( res ) )
8694 expect ( await fetchPackagistStats ( 'flaky/flaky' ) ) . toMatchObject ( {
8795 kind : 'TRANSIENT' ,
8896 statusCode : 503 ,
8997 } )
98+ expect ( res . body . cancel ) . toHaveBeenCalled ( )
9099 } )
91100
92101 it ( 'maps a network rejection → TRANSIENT without a status code' , async ( ) => {
@@ -170,12 +179,16 @@ describe('fetchPackagistP2', () => {
170179 } )
171180 } )
172181
173- it ( 'maps 404 → NOT_FOUND and 429 → RATE_LIMIT' , async ( ) => {
174- vi . stubGlobal ( 'fetch' , vi . fn ( ) . mockResolvedValue ( fakeResponse ( 404 ) ) )
182+ it ( 'maps 404 → NOT_FOUND and 429 → RATE_LIMIT, draining the body each time' , async ( ) => {
183+ const notFound = fakeResponse ( 404 )
184+ vi . stubGlobal ( 'fetch' , vi . fn ( ) . mockResolvedValue ( notFound ) )
175185 expect ( await fetchPackagistP2 ( 'gone/gone' , null ) ) . toMatchObject ( { kind : 'NOT_FOUND' } )
186+ expect ( notFound . body . cancel ) . toHaveBeenCalled ( )
176187
177- vi . stubGlobal ( 'fetch' , vi . fn ( ) . mockResolvedValue ( fakeResponse ( 429 ) ) )
188+ const rateLimited = fakeResponse ( 429 )
189+ vi . stubGlobal ( 'fetch' , vi . fn ( ) . mockResolvedValue ( rateLimited ) )
178190 expect ( await fetchPackagistP2 ( 'busy/busy' , null ) ) . toMatchObject ( { kind : 'RATE_LIMIT' } )
191+ expect ( rateLimited . body . cancel ) . toHaveBeenCalled ( )
179192 } )
180193
181194 it ( 'maps a payload missing the package key → MALFORMED' , async ( ) => {
0 commit comments