@@ -460,6 +460,96 @@ describe('Oracle EPM guarded client', () => {
460460 }
461461 )
462462
463+ describe . each ( [ 'endpoint' , 'route' ] as const ) (
464+ '%s-bound typed returned-link queries' ,
465+ ( binding ) => {
466+ const declaration = {
467+ method : 'GET' ,
468+ version : 'v3' ,
469+ path : [ oracleEpmLiteral ( 'jobs' ) ] ,
470+ query : {
471+ offset : oracleEpmQuery . integer ( { required : true , minimum : - 10 , maximum : 10 } ) ,
472+ include : oracleEpmQuery . boolean ( { required : true } ) ,
473+ token : oracleEpmQuery . string ( { required : true , maxBytes : 32 } ) ,
474+ } ,
475+ body : 'none' ,
476+ response : 'json' ,
477+ timeoutMs : 5_000 ,
478+ maxResponseBytes : 4_096 ,
479+ } as const
480+ const endpoint = routes . defineEndpoint ( declaration )
481+ const policy = routes . defineReturnedLinkPolicy ( {
482+ ...( binding === 'endpoint' ? { endpoint, method : 'GET' as const } : declaration ) ,
483+ relation : 'next' ,
484+ preserveGatewayBasePath : true ,
485+ } )
486+ const client = createOracleEpmClient ( {
487+ instanceUrl : 'https://epm.example.com/gateway' ,
488+ accessToken : Buffer . from ( 'u:p' ) . toString ( 'base64' ) ,
489+ } )
490+ const prefix = 'https://epm.example.com/gateway/SyntheticAlpha/rest/v3/jobs'
491+
492+ it . each ( [ 'offset=-10&include=false' , 'offset=0&include=false' , 'offset=10&include=true' ] ) (
493+ 'accepts canonical typed query %s without rewriting the returned URL' ,
494+ async ( query ) => {
495+ const href = `${ prefix } ?${ query } &token=signed%2Bquery%2Fsecret`
496+ const handle = client . validateReturnedLink ( policy , { rel : 'next' , href } )
497+ expect ( JSON . stringify ( handle ) ) . toBe ( '{}' )
498+ await client . requestValidatedLink ( handle )
499+ expect ( mockSecureFetch . mock . calls [ 0 ] [ 0 ] ) . toBe ( href )
500+ }
501+ )
502+
503+ it . each ( [
504+ 'offset=-11&include=true' ,
505+ 'offset=11&include=true' ,
506+ 'offset=9007199254740992&include=true' ,
507+ 'offset=1.5&include=true' ,
508+ 'offset=1e0&include=true' ,
509+ 'offset=0x1&include=true' ,
510+ 'offset=01&include=true' ,
511+ 'offset=-0&include=true' ,
512+ 'offset=%201&include=true' ,
513+ 'offset=%2B1&include=true' ,
514+ 'offset=&include=true' ,
515+ 'offset=NaN&include=true' ,
516+ 'offset=Infinity&include=true' ,
517+ 'offset=1&include=True' ,
518+ 'offset=1&include=FALSE' ,
519+ 'offset=1&include=1' ,
520+ 'offset=1&include=0' ,
521+ 'offset=1&include=' ,
522+ 'offset=1' ,
523+ 'include=true' ,
524+ 'offset=1&offset=2&include=true' ,
525+ 'offset=1&include=true&include=false' ,
526+ 'offset=1&include=true&unknown=value' ,
527+ ] ) ( 'rejects invalid typed query %s before DNS or fetch' , ( query ) => {
528+ expect ( ( ) =>
529+ client . validateReturnedLink ( policy , {
530+ rel : 'next' ,
531+ href : `${ prefix } ?${ query } &token=signed-query-secret` ,
532+ } )
533+ ) . toThrowError (
534+ expect . objectContaining ( { name : 'OracleEpmError' , category : 'invalid_input' } )
535+ )
536+ expect ( mockValidateUrl ) . not . toHaveBeenCalled ( )
537+ expect ( mockSecureFetch ) . not . toHaveBeenCalled ( )
538+ } )
539+
540+ it . each ( [
541+ { offset : '1' , include : true } ,
542+ { offset : 1 , include : 'true' } ,
543+ ] ) ( 'keeps direct request query types strict for %j' , async ( query ) => {
544+ await expect (
545+ client . request ( endpoint , { query : { ...query , token : 'signed-query-secret' } } )
546+ ) . rejects . toMatchObject ( { category : 'invalid_input' } )
547+ expect ( mockValidateUrl ) . not . toHaveBeenCalled ( )
548+ expect ( mockSecureFetch ) . not . toHaveBeenCalled ( )
549+ } )
550+ }
551+ )
552+
463553 it . each ( [ 'download' , 'Job Status' ] ) (
464554 'keeps %s links opaque and client-owned' ,
465555 async ( relation ) => {
0 commit comments