@@ -372,6 +372,60 @@ describe('credential-bound OCI client', () => {
372372 } )
373373 } )
374374
375+ it . each ( [
376+ 'reports%2Fdaily.csv' ,
377+ 'reports%2fdaily%5cfile.csv' ,
378+ '%2Freports%2F%2Fdaily.csv' ,
379+ '%2F' ,
380+ '%5Creports%5Cdaily.csv' ,
381+ 'reports%2F..%2Fdaily.csv' ,
382+ 'reports%2F%E2%98%83%20caf%C3%A9.csv' ,
383+ 'literal%252F%255C%2500.csv' ,
384+ 'what%3Fpart%231.txt' ,
385+ ] ) ( 'preserves and signs an encoded object name exactly: %s' , async ( encodedName ) => {
386+ const { client } = await createPreparedClient ( )
387+ const endpoint = await client . prepareStaticEndpoint (
388+ createOciStaticEndpointPolicy ( {
389+ serviceId : OCI_SERVICE_ID ,
390+ serviceName : 'objectstorage' ,
391+ hostnameTemplate : 'regional' ,
392+ } )
393+ )
394+ const encodedPath = `/n/synthetic_namespace/b/synthetic_bucket/o/${ encodedName } `
395+ const target = `${ encodedPath } ?versionId=v%2F1`
396+ await client . request ( {
397+ endpoint,
398+ method : 'GET' ,
399+ encodedPath,
400+ queryPairs : [ [ 'versionId' , 'v/1' ] ] ,
401+ timeoutMs : 10_000 ,
402+ maxResponseBytes : 1024 ,
403+ } )
404+ expect ( mocks . secureFetch ) . toHaveBeenCalledOnce ( )
405+ const [ url , , options ] = mocks . secureFetch . mock . calls [ 0 ] as [
406+ string ,
407+ string ,
408+ { headers : Record < string , string > } ,
409+ ]
410+ const hostname = 'objectstorage.us-ashburn-1.oraclecloud.com'
411+ expect ( url ) . toBe ( `https://${ hostname } ${ target } ` )
412+ const signature = options . headers . authorization . match ( / s i g n a t u r e = " ( [ ^ " ] + ) " / ) ?. [ 1 ]
413+ expect ( signature ) . toBeDefined ( )
414+ const signingString = [
415+ `x-date: ${ options . headers [ 'x-date' ] } ` ,
416+ `(request-target): get ${ target } ` ,
417+ `host: ${ hostname } ` ,
418+ ] . join ( '\n' )
419+ expect (
420+ verify (
421+ 'RSA-SHA256' ,
422+ Buffer . from ( signingString , 'utf8' ) ,
423+ createPublicKey ( PRIVATE_KEY ) ,
424+ Buffer . from ( signature ?? '' , 'base64' )
425+ )
426+ ) . toBe ( true )
427+ } )
428+
375429 it . each ( [ 'GET' , 'HEAD' , 'DELETE' ] as const ) ( 'rejects bodies for %s' , async ( method ) => {
376430 const { client, endpoint } = await createPreparedClient ( )
377431 await expect (
@@ -437,13 +491,23 @@ describe('credential-bound OCI client', () => {
437491 '//host/path' ,
438492 '/double//slash' ,
439493 '/query?x=1' ,
494+ '/fragment#value' ,
440495 '/back\\slash' ,
441- '/encoded%2Fslash' ,
442- '/encoded%5Cbackslash' ,
443496 '/encoded%00control' ,
444497 '/encoded%1fcontrol' ,
445498 '/encoded%7Fcontrol' ,
446499 '/bad%2' ,
500+ '/bad%GG' ,
501+ '/raw\0control' ,
502+ '/raw\u007fcontrol' ,
503+ '/raw\ncontrol' ,
504+ '/raw\tcontrol' ,
505+ '/raw space' ,
506+ '/raw\ud800surrogate' ,
507+ '/a/../b' ,
508+ '/a/./b' ,
509+ '/a/%2e%2e/b' ,
510+ '/a/%2E/b' ,
447511 ] ) ( 'rejects ambiguous encoded paths: %s' , async ( encodedPath ) => {
448512 const { client, endpoint } = await createPreparedClient ( )
449513 await expect (
@@ -455,6 +519,8 @@ describe('credential-bound OCI client', () => {
455519 maxResponseBytes : 1024 ,
456520 } )
457521 ) . rejects . toMatchObject ( { code : 'invalid_request' } )
522+ expect ( mocks . validateUrl ) . not . toHaveBeenCalled ( )
523+ expect ( mocks . secureFetch ) . not . toHaveBeenCalled ( )
458524 } )
459525
460526 it ( 'rejects signing-controlled headers' , async ( ) => {
0 commit comments