@@ -10,6 +10,7 @@ const mocks = vi.hoisted(() => ({
1010 predicates : undefined as unknown ,
1111 rows : [ ] as { encryptedServiceAccountKey : string | null } [ ] ,
1212 secureFetch : vi . fn ( ) ,
13+ validateUrl : vi . fn ( ) ,
1314} ) )
1415
1516vi . mock ( '@sim/db' , ( ) => ( {
@@ -44,7 +45,8 @@ vi.mock('@/lib/core/security/encryption', () => ({ decryptSecret: mocks.decryptS
4445
4546vi . mock ( '@/lib/core/security/input-validation.server' , ( ) => ( {
4647 DEFAULT_MAX_RESPONSE_BYTES : 100 * 1024 * 1024 ,
47- secureFetchWithValidation : mocks . secureFetch ,
48+ secureFetchWithPinnedIP : mocks . secureFetch ,
49+ validateUrlWithDNS : mocks . validateUrl ,
4850} ) )
4951
5052vi . mock ( '@sim/utils/retry' , ( ) => ( {
@@ -176,7 +178,7 @@ async function createPreparedClient(params: { region?: string } = {}): Promise<{
176178}
177179
178180function authorizationFromLastRequest ( ) : string {
179- const options = mocks . secureFetch . mock . calls . at ( - 1 ) ?. [ 1 ] as { headers : Record < string , string > }
181+ const options = mocks . secureFetch . mock . calls . at ( - 1 ) ?. [ 2 ] as { headers : Record < string , string > }
180182 return options . headers . authorization
181183}
182184
@@ -187,6 +189,11 @@ describe('credential-bound OCI client', () => {
187189 mocks . decryptSecret . mockReset ( ) . mockResolvedValue ( { decrypted : SECRET } )
188190 mocks . backoff . mockReset ( ) . mockReturnValue ( 0 )
189191 mocks . secureFetch . mockReset ( ) . mockResolvedValue ( secureResponse ( { } ) )
192+ mocks . validateUrl . mockReset ( ) . mockResolvedValue ( {
193+ isValid : true ,
194+ resolvedIP : '203.0.113.10' ,
195+ originalHostname : 'identity.us-ashburn-1.oci.oraclecloud.com' ,
196+ } )
190197 } )
191198
192199 afterEach ( ( ) => {
@@ -312,7 +319,7 @@ describe('credential-bound OCI client', () => {
312319 expect ( authorizationFromLastRequest ( ) ) . toBe (
313320 'Signature version="1",keyId="ocid1.tenancy.oc1..aaaaaaaafixedvector/ocid1.user.oc1..aaaaaaaafixedvector/25:53:22:62:aa:db:ff:ef:f5:77:08:d1:a2:ed:8b:e6",algorithm="rsa-sha256",headers="x-date (request-target) host content-type content-length x-content-sha256",signature="W2/OGoa2XuOin6+CQt32/+/lAXG5PWoamkAHr/k84oCYGUuub2mEYw1z9p4gc6/GPgeZ30wVp4DNVLzOjup3nJir1WsEsYzAk27XAIRVjxiQ7oBzCccnSnB88KLeNz1NDz7r4QPQGxZ50MBQEe0C+DEH2P+utpfFN73o7GCUhIN9hb27COg4l7ffdSLgjBWPN/B4AiZXpjz3I/GRHo29otGAhZ3MiX10gJTjy+qeAchAfmXmTx/nJqNhF0Aj255+B2lepCrHdkpcBpiTs5E+ppE6VvML0ByQ9ZLzBISB4MBljuFyey6tnTkueT73fqjQyM/OT+aO9HrAlemc3HSAXA=="'
314321 )
315- expect ( mocks . secureFetch . mock . calls [ 0 ] [ 1 ] . headers ) . toMatchObject ( {
322+ expect ( mocks . secureFetch . mock . calls [ 0 ] [ 2 ] . headers ) . toMatchObject ( {
316323 'content-length' : '0' ,
317324 'content-type' : 'application/json' ,
318325 'x-content-sha256' : '47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=' ,
@@ -338,13 +345,15 @@ describe('credential-bound OCI client', () => {
338345 maxResponseBytes : 1024 ,
339346 } )
340347
341- const [ url , options ] = mocks . secureFetch . mock . calls [ 0 ] as [
348+ const [ url , resolvedIP , options ] = mocks . secureFetch . mock . calls [ 0 ] as [
349+ string ,
342350 string ,
343351 { body : Uint8Array ; headers : Record < string , string > } ,
344352 ]
345353 expect ( url ) . toBe (
346354 'https://identity.us-ashburn-1.oci.oraclecloud.com/v1/%E2%98%83?z=last&a=&a=%20%21%27%28%29%2A'
347355 )
356+ expect ( resolvedIP ) . toBe ( '203.0.113.10' )
348357 expect ( [ ...options . body ] ) . toEqual ( [ ...body ] )
349358 expect ( options . body ) . not . toBe ( body )
350359 expect ( options . headers ) . toMatchObject ( {
@@ -380,7 +389,7 @@ describe('credential-bound OCI client', () => {
380389 timeoutMs : 10_000 ,
381390 maxResponseBytes : 1024 ,
382391 } )
383- const options = mocks . secureFetch . mock . calls . at ( - 1 ) ?. [ 1 ]
392+ const options = mocks . secureFetch . mock . calls . at ( - 1 ) ?. [ 2 ]
384393 expect ( options . method ) . toBe ( method )
385394 expect ( options ) . not . toHaveProperty ( 'body' )
386395 expect ( options . headers ) . not . toHaveProperty ( 'content-length' )
@@ -410,7 +419,7 @@ describe('credential-bound OCI client', () => {
410419 timeoutMs : 10_000 ,
411420 maxResponseBytes : 1024 ,
412421 } )
413- expect ( mocks . secureFetch . mock . calls . at ( - 1 ) ?. [ 1 ] . headers [ 'content-length' ] ) . toBe ( '0' )
422+ expect ( mocks . secureFetch . mock . calls . at ( - 1 ) ?. [ 2 ] . headers [ 'content-length' ] ) . toBe ( '0' )
414423 }
415424 )
416425
@@ -515,8 +524,8 @@ describe('credential-bound OCI client', () => {
515524 maxResponseBytes : 1024 ,
516525 } )
517526
518- const first = mocks . secureFetch . mock . calls [ 0 ] [ 1 ]
519- const second = mocks . secureFetch . mock . calls [ 1 ] [ 1 ]
527+ const first = mocks . secureFetch . mock . calls [ 0 ] [ 2 ]
528+ const second = mocks . secureFetch . mock . calls [ 1 ] [ 2 ]
520529 expect ( [ ...first . body ] ) . toEqual ( [ ...second . body ] )
521530 expect ( first . headers [ 'opc-retry-token' ] ) . toBe ( 'operation-token' )
522531 expect ( second . headers [ 'opc-retry-token' ] ) . toBe ( 'operation-token' )
@@ -809,13 +818,45 @@ describe('credential-bound OCI client', () => {
809818 await assertion
810819 } )
811820
821+ it ( 'does not start a pinned request when destination validation outlives the deadline' , async ( ) => {
822+ vi . useFakeTimers ( )
823+ let finishValidation :
824+ | ( ( value : { isValid : true ; resolvedIP : string ; originalHostname : string } ) => void )
825+ | undefined
826+ mocks . validateUrl . mockImplementationOnce (
827+ ( ) =>
828+ new Promise ( ( resolve ) => {
829+ finishValidation = resolve
830+ } )
831+ )
832+ const { client, endpoint } = await createPreparedClient ( )
833+ const pending = client . request ( {
834+ endpoint,
835+ method : 'GET' ,
836+ encodedPath : '/v1/test' ,
837+ timeoutMs : 100 ,
838+ maxResponseBytes : 1024 ,
839+ } )
840+ const assertion = expect ( pending ) . rejects . toMatchObject ( { code : 'deadline_exceeded' } )
841+ await vi . advanceTimersByTimeAsync ( 101 )
842+ await assertion
843+ finishValidation ?.( {
844+ isValid : true ,
845+ resolvedIP : '203.0.113.10' ,
846+ originalHostname : 'identity.us-ashburn-1.oci.oraclecloud.com' ,
847+ } )
848+ await Promise . resolve ( )
849+ expect ( mocks . secureFetch ) . not . toHaveBeenCalled ( )
850+ } )
851+
812852 it ( 'propagates caller abort while setup destination validation is pending' , async ( ) => {
813853 const controller = new AbortController ( )
814- mocks . secureFetch . mockImplementationOnce ( ( ) => new Promise ( ( ) => { } ) )
854+ mocks . validateUrl . mockImplementationOnce ( ( ) => new Promise ( ( ) => { } ) )
815855 const pending = verifyOciApiKeyCredentialForSetup ( SECRET , controller . signal )
816- await vi . waitFor ( ( ) => expect ( mocks . secureFetch ) . toHaveBeenCalledOnce ( ) )
856+ await vi . waitFor ( ( ) => expect ( mocks . validateUrl ) . toHaveBeenCalledOnce ( ) )
817857 controller . abort ( )
818858 await expect ( pending ) . rejects . toMatchObject ( { code : 'aborted' } )
859+ expect ( mocks . secureFetch ) . not . toHaveBeenCalled ( )
819860 } )
820861
821862 it ( 'does not start setup destination validation after an earlier caller abort' , async ( ) => {
@@ -824,16 +865,32 @@ describe('credential-bound OCI client', () => {
824865 await expect (
825866 verifyOciApiKeyCredentialForSetup ( SECRET , controller . signal )
826867 ) . rejects . toMatchObject ( { code : 'aborted' } )
868+ expect ( mocks . validateUrl ) . not . toHaveBeenCalled ( )
827869 expect ( mocks . secureFetch ) . not . toHaveBeenCalled ( )
828870 } )
829871
830872 it ( 'applies the setup deadline while destination validation is pending' , async ( ) => {
831873 vi . useFakeTimers ( )
832- mocks . secureFetch . mockImplementationOnce ( ( ) => new Promise ( ( ) => { } ) )
874+ let finishValidation :
875+ | ( ( value : { isValid : true ; resolvedIP : string ; originalHostname : string } ) => void )
876+ | undefined
877+ mocks . validateUrl . mockImplementationOnce (
878+ ( ) =>
879+ new Promise ( ( resolve ) => {
880+ finishValidation = resolve
881+ } )
882+ )
833883 const pending = verifyOciApiKeyCredentialForSetup ( SECRET )
834884 const assertion = expect ( pending ) . rejects . toMatchObject ( { code : 'deadline_exceeded' } )
835885 await vi . advanceTimersByTimeAsync ( 10_001 )
836886 await assertion
887+ finishValidation ?.( {
888+ isValid : true ,
889+ resolvedIP : '203.0.113.10' ,
890+ originalHostname : 'objectstorage.us-ashburn-1.oraclecloud.com' ,
891+ } )
892+ await Promise . resolve ( )
893+ expect ( mocks . secureFetch ) . not . toHaveBeenCalled ( )
837894 } )
838895
839896 it ( 'applies the same deadline while reading the response body' , async ( ) => {
0 commit comments