@@ -308,75 +308,106 @@ describe('Oracle EPM guarded client', () => {
308308 expect ( mockSecureFetch ) . not . toHaveBeenCalled ( )
309309 } )
310310
311- it ( 'returns an opaque same-client link capability and keeps query secrets out of serialization' , async ( ) => {
312- const download = routes . defineEndpoint ( {
311+ it . each ( [ 'download' , 'Job Status' ] ) (
312+ 'keeps %s links opaque and client-owned' ,
313+ async ( relation ) => {
314+ const download = routes . defineEndpoint ( {
315+ method : 'GET' ,
316+ version : 'v3' ,
317+ path : [ oracleEpmLiteral ( 'files' ) , oracleEpmPathParameter ( 'fileId' , { maxBytes : 32 } ) ] ,
318+ query : { token : oracleEpmQuery . string ( { required : true , maxBytes : 128 } ) } ,
319+ body : 'none' ,
320+ response : 'stream' ,
321+ timeoutMs : 5_000 ,
322+ maxResponseBytes : 4_096 ,
323+ } )
324+ const policy = routes . defineReturnedLinkPolicy ( {
325+ relation,
326+ method : 'GET' ,
327+ endpoint : download ,
328+ preserveGatewayBasePath : true ,
329+ } )
330+ const client = createOracleEpmClient ( {
331+ instanceUrl : 'https://epm.example.com/gateway' ,
332+ accessToken : Buffer . from ( 'user:password' ) . toString ( 'base64' ) ,
333+ } )
334+ const secret = 'signed-query-secret'
335+ const link = client . validateReturnedLink ( policy , {
336+ rel : relation ,
337+ href : `https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=${ secret } ` ,
338+ } )
339+
340+ expect ( Object . isFrozen ( link ) ) . toBe ( true )
341+ expect ( Object . keys ( link ) ) . toEqual ( [ ] )
342+ expect ( JSON . stringify ( link ) ) . toBe ( '{}' )
343+ expect ( String ( link ) ) . not . toContain ( secret )
344+
345+ mockSecureFetch . mockResolvedValue ( secureResponse ( { body : new ReadableStream ( ) } ) )
346+ await client . requestValidatedLink ( link )
347+ expect ( mockSecureFetch ) . toHaveBeenCalledWith (
348+ `https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=${ secret } ` ,
349+ '203.0.113.10' ,
350+ expect . objectContaining ( { method : 'GET' } )
351+ )
352+
353+ const otherClient = createOracleEpmClient ( {
354+ instanceUrl : 'https://epm.example.com/gateway' ,
355+ accessToken : Buffer . from ( 'other:password' ) . toString ( 'base64' ) ,
356+ } )
357+ await expect ( otherClient . requestValidatedLink ( link ) ) . rejects . toBeInstanceOf ( OracleEpmError )
358+ expect ( mockValidateUrl ) . toHaveBeenCalledTimes ( 1 )
359+ expect ( mockSecureFetch ) . toHaveBeenCalledTimes ( 1 )
360+ }
361+ )
362+
363+ it . each (
364+ [
365+ 'https://evil.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=x' ,
366+ 'https://user@epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=x' ,
367+ 'https://epm.example.com/SyntheticAlpha/rest/v3/files/abc?token=x' ,
368+ 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=x&token=y' ,
369+ 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?unknown=x' ,
370+ 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=x#fragment' ,
371+ 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=x#' ,
372+ 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/ab\nc?token=x' ,
373+ 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/\uD800?token=x' ,
374+ 'https://epm.example.com/gateway/SyntheticAlpha/rest//v3/files/abc?token=x' ,
375+ 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc/?token=x' ,
376+ 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/./abc?token=x' ,
377+ 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/%2e%2e?token=x' ,
378+ 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/%2e.?token=x' ,
379+ 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/%252e%252e?token=x' ,
380+ 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files%2Fabc?token=x' ,
381+ 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files%5Cabc?token=x' ,
382+ 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files\\abc?token=x' ,
383+ ] . flatMap ( ( href ) => [ 'download' , 'Job Status' ] . map ( ( relation ) => ( { relation, href } ) ) )
384+ ) ( 'rejects unsafe $relation link $href' , ( { relation, href } ) => {
385+ const policy = routes . defineReturnedLinkPolicy ( {
386+ relation,
313387 method : 'GET' ,
314388 version : 'v3' ,
315389 path : [ oracleEpmLiteral ( 'files' ) , oracleEpmPathParameter ( 'fileId' , { maxBytes : 32 } ) ] ,
316390 query : { token : oracleEpmQuery . string ( { required : true , maxBytes : 128 } ) } ,
317- body : 'none' ,
318391 response : 'stream' ,
319392 timeoutMs : 5_000 ,
320393 maxResponseBytes : 4_096 ,
321- } )
322- const policy = routes . defineReturnedLinkPolicy ( {
323- relation : 'download' ,
324- method : 'GET' ,
325- endpoint : download ,
326394 preserveGatewayBasePath : true ,
327395 } )
328396 const client = createOracleEpmClient ( {
329397 instanceUrl : 'https://epm.example.com/gateway' ,
330- accessToken : Buffer . from ( 'user:password' ) . toString ( 'base64' ) ,
331- } )
332- const secret = 'signed-query-secret'
333- const link = client . validateReturnedLink ( policy , {
334- rel : 'download' ,
335- href : `https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=${ secret } ` ,
336- } )
337-
338- expect ( Object . isFrozen ( link ) ) . toBe ( true )
339- expect ( Object . keys ( link ) ) . toEqual ( [ ] )
340- expect ( JSON . stringify ( link ) ) . toBe ( '{}' )
341- expect ( String ( link ) ) . not . toContain ( secret )
342-
343- mockSecureFetch . mockResolvedValue ( secureResponse ( { body : new ReadableStream ( ) } ) )
344- await client . requestValidatedLink ( link )
345- expect ( mockSecureFetch ) . toHaveBeenCalledTimes ( 1 )
346-
347- const otherClient = createOracleEpmClient ( {
348- instanceUrl : 'https://epm.example.com/gateway' ,
349- accessToken : Buffer . from ( 'other:password' ) . toString ( 'base64' ) ,
398+ accessToken : Buffer . from ( 'u:p' ) . toString ( 'base64' ) ,
350399 } )
351- await expect ( otherClient . requestValidatedLink ( link ) ) . rejects . toBeInstanceOf ( OracleEpmError )
400+ expect ( ( ) => client . validateReturnedLink ( policy , { rel : relation , href } ) ) . toThrow ( )
401+ expect ( mockValidateUrl ) . not . toHaveBeenCalled ( )
402+ expect ( mockSecureFetch ) . not . toHaveBeenCalled ( )
352403 } )
353404
354- it . each ( [
355- 'https://evil.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=x' ,
356- 'https://user@epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=x' ,
357- 'https://epm.example.com/SyntheticAlpha/rest/v3/files/abc?token=x' ,
358- 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=x&token=y' ,
359- 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?unknown=x' ,
360- 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=x#fragment' ,
361- 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc?token=x#' ,
362- 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/ab\nc?token=x' ,
363- 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/\uD800?token=x' ,
364- 'https://epm.example.com/gateway/SyntheticAlpha/rest//v3/files/abc?token=x' ,
365- 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc/?token=x' ,
366- 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/./abc?token=x' ,
367- 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/%2e%2e?token=x' ,
368- 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/%2e.?token=x' ,
369- 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/%252e%252e?token=x' ,
370- 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files%2Fabc?token=x' ,
371- 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files%5Cabc?token=x' ,
372- 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files\\abc?token=x' ,
373- ] ) ( 'rejects unsafe returned link %j' , ( href ) => {
405+ it . each ( [ 'download' , 'Job Status' ] ) ( 'rejects an incorrect %s link method' , ( relation ) => {
374406 const policy = routes . defineReturnedLinkPolicy ( {
375- relation : 'download' ,
407+ relation,
376408 method : 'GET' ,
377409 version : 'v3' ,
378410 path : [ oracleEpmLiteral ( 'files' ) , oracleEpmPathParameter ( 'fileId' , { maxBytes : 32 } ) ] ,
379- query : { token : oracleEpmQuery . string ( { required : true , maxBytes : 128 } ) } ,
380411 response : 'stream' ,
381412 timeoutMs : 5_000 ,
382413 maxResponseBytes : 4_096 ,
@@ -386,31 +417,47 @@ describe('Oracle EPM guarded client', () => {
386417 instanceUrl : 'https://epm.example.com/gateway' ,
387418 accessToken : Buffer . from ( 'u:p' ) . toString ( 'base64' ) ,
388419 } )
389- expect ( ( ) => client . validateReturnedLink ( policy , { rel : 'download' , href } ) ) . toThrow ( )
420+ expect ( ( ) =>
421+ client . validateReturnedLink ( policy , {
422+ rel : relation ,
423+ method : 'POST' ,
424+ href : 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc' ,
425+ } )
426+ ) . toThrow ( )
427+ expect ( mockValidateUrl ) . not . toHaveBeenCalled ( )
428+ expect ( mockSecureFetch ) . not . toHaveBeenCalled ( )
390429 } )
391430
392- it ( 'rejects an incorrect returned-link method' , ( ) => {
431+ it . each ( [
432+ 'job status' ,
433+ 'Job status' ,
434+ ' Job Status' ,
435+ 'Job Status ' ,
436+ 'Job Status' ,
437+ 'Job\tStatus' ,
438+ 'Job Status\n' ,
439+ 'download' ,
440+ ] ) ( 'rejects nonmatching relation %j before DNS or network access' , ( rel ) => {
393441 const policy = routes . defineReturnedLinkPolicy ( {
394- relation : 'download ' ,
442+ relation : 'Job Status ' ,
395443 method : 'GET' ,
396- version : 'v3' ,
397- path : [ oracleEpmLiteral ( 'files' ) , oracleEpmPathParameter ( 'fileId' , { maxBytes : 32 } ) ] ,
398- response : 'stream' ,
399- timeoutMs : 5_000 ,
400- maxResponseBytes : 4_096 ,
444+ endpoint : getJob ,
401445 preserveGatewayBasePath : true ,
402446 } )
403447 const client = createOracleEpmClient ( {
404448 instanceUrl : 'https://epm.example.com/gateway' ,
405449 accessToken : Buffer . from ( 'u:p' ) . toString ( 'base64' ) ,
406450 } )
451+
407452 expect ( ( ) =>
408453 client . validateReturnedLink ( policy , {
409- rel : 'download' ,
410- method : 'POST ' ,
411- href : 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/files/abc ' ,
454+ rel,
455+ method : 'GET ' ,
456+ href : 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/jobs/42 ' ,
412457 } )
413- ) . toThrow ( )
458+ ) . toThrow ( OracleEpmError )
459+ expect ( mockValidateUrl ) . not . toHaveBeenCalled ( )
460+ expect ( mockSecureFetch ) . not . toHaveBeenCalled ( )
414461 } )
415462
416463 it ( 'rejects forged validated-link handles' , async ( ) => {
0 commit comments