@@ -20,6 +20,7 @@ import { OSSIndexCompatibilityApi } from '@sonatype/sonatype-guide-api-client';
2020import type { InitOverrideFunction } from '@sonatype/sonatype-guide-api-client' ;
2121import { Coordinates } from '../Types/Coordinates' ;
2222import { rmSync , existsSync } from 'node:fs' ;
23+ import { ProxyAgent } from 'undici' ;
2324
2425// node-persist is mocked globally so tests never touch the filesystem cache.
2526vi . mock ( 'node-persist' , ( ) => ( {
@@ -36,12 +37,19 @@ const CACHE_PATH = '/tmp/.sonatype-guide-test';
3637const SERVER = 'https://api.guide.sonatype.com' ;
3738
3839describe ( 'GuideRequestService' , ( ) => {
40+ const mockFetch = vi . fn ( ) ;
41+
3942 beforeEach ( ( ) => {
4043 if ( existsSync ( CACHE_PATH ) ) rmSync ( CACHE_PATH , { recursive : true , force : true } ) ;
44+ vi . stubGlobal ( 'fetch' , mockFetch ) ;
4145 } ) ;
4246
4347 afterEach ( ( ) => {
4448 vi . restoreAllMocks ( ) ;
49+ vi . unstubAllGlobals ( ) ;
50+ delete process . env . http_proxy ;
51+ delete process . env . https_proxy ;
52+ mockFetch . mockClear ( ) ;
4553 } ) ;
4654
4755 it ( 'sends Authorization: Bearer when accessToken is set (PAT token mode)' , async ( ) => {
@@ -119,4 +127,49 @@ describe('GuideRequestService', () => {
119127 expect ( result ) . toEqual ( [ ] ) ;
120128 } ) ;
121129 } ) ;
130+
131+ describe ( 'proxy support' , ( ) => {
132+ const mockSuccessResponse = {
133+ ok : true ,
134+ status : 200 ,
135+ statusText : 'OK' ,
136+ json : vi . fn ( ) . mockResolvedValue ( [
137+ {
138+ coordinates : 'pkg:npm/test@1.0.0' ,
139+ reference : 'https://guide.sonatype.com/blah' ,
140+ vulnerabilities : [ ] ,
141+ } ,
142+ ] ) ,
143+ } ;
144+
145+ it ( 'should pass ProxyAgent as dispatcher when http_proxy is set' , async ( ) => {
146+ process . env . http_proxy = 'http://proxy.example.com:8080' ;
147+ mockFetch . mockResolvedValueOnce ( mockSuccessResponse ) ;
148+
149+ const svc = new GuideRequestService ( 'user' , 'token' , CACHE_PATH , SERVER ) ;
150+ const coords = [ new Coordinates ( 'test' , '1.0.0' ) ] ;
151+ await svc . callGuideOrGetFromCache ( coords , 'npm' ) ;
152+
153+ // Verify fetch was called with a dispatcher (ProxyAgent)
154+ expect ( mockFetch ) . toHaveBeenCalled ( ) ;
155+ const fetchOptions = mockFetch . mock . calls [ 0 ] [ 1 ] as RequestInit & { dispatcher ?: ProxyAgent } ;
156+ expect ( fetchOptions . dispatcher ) . toBeDefined ( ) ;
157+ expect ( fetchOptions . dispatcher ) . toBeInstanceOf ( ProxyAgent ) ;
158+ } ) ;
159+
160+ it ( 'should not include dispatcher when no proxy is configured' , async ( ) => {
161+ // Ensure no proxy env vars are set before creating service
162+ delete process . env . http_proxy ;
163+ delete process . env . https_proxy ;
164+ mockFetch . mockResolvedValueOnce ( mockSuccessResponse ) ;
165+
166+ const svc = new GuideRequestService ( 'user' , 'token' , CACHE_PATH , SERVER ) ;
167+ const coords = [ new Coordinates ( 'test' , '1.0.0' ) ] ;
168+ await svc . callGuideOrGetFromCache ( coords , 'npm' ) ;
169+
170+ expect ( mockFetch ) . toHaveBeenCalled ( ) ;
171+ const fetchOptions = mockFetch . mock . calls [ 0 ] [ 1 ] as RequestInit & { dispatcher ?: unknown } ;
172+ expect ( fetchOptions . dispatcher ) . toBeUndefined ( ) ;
173+ } ) ;
174+ } ) ;
122175} ) ;
0 commit comments