@@ -13,10 +13,6 @@ const ORIGIN = 'https://vision.fa.us2.oraclecloud.com'
1313const COLLECTION = '/hcmRestApi/resources/11.13.18.05/workers'
1414const COLLECTION_ADDRESS = { family : 'hcm' , relativePath : 'workers' } as const
1515
16- function resource ( href : unknown , links : unknown [ ] = [ ] ) : Record < string , unknown > {
17- return { links : [ { rel : 'self' , href } , ...links ] }
18- }
19-
2016describe ( 'parseOracleFusionCollection' , ( ) => {
2117 it ( 'projects a valid page and calculates the next offset' , ( ) => {
2218 expect (
@@ -132,7 +128,17 @@ describe('parseOracleFusionCollection', () => {
132128 } )
133129} )
134130
135- describe ( 'Oracle self links' , ( ) => {
131+ describe . each ( [ 'legacy' , 'context' , 'both' ] as const ) ( 'Oracle %s self links' , ( representation ) => {
132+ function resource ( href : unknown , otherLinks : unknown [ ] = [ ] ) : Record < string , unknown > {
133+ const links = [ { rel : 'self' , href } , ...otherLinks ]
134+ return {
135+ ...( representation !== 'context' ? { links } : { } ) ,
136+ ...( representation !== 'legacy'
137+ ? { '@context' : { key : 'not-the-resource-key' , links } }
138+ : { } ) ,
139+ }
140+ }
141+
136142 it ( 'accepts exactly one same-origin self link for the expected path' , ( ) => {
137143 expect ( ( ) =>
138144 validateOracleFusionSelfLink ( resource ( `${ ORIGIN } ${ COLLECTION } /abc` ) , ORIGIN , {
@@ -142,6 +148,42 @@ describe('Oracle self links', () => {
142148 ) . not . toThrow ( )
143149 } )
144150
151+ it ( 'extracts item keys from a collection without trusting context keys or collection links' , ( ) => {
152+ const address = { family : 'fscm' , relativePath : 'invoices' } as const
153+ const collectionPath = '/fscmRestApi/resources/11.13.18.05/invoices'
154+ const key = 'invoice:123,installment=2'
155+ const encodedKey = encodeOracleFusionPathSegment ( key )
156+ const page = parseOracleFusionCollection (
157+ {
158+ items : [ resource ( `${ ORIGIN } ${ collectionPath } /${ encodedKey } ` ) ] ,
159+ count : 1 ,
160+ limit : 25 ,
161+ offset : 0 ,
162+ hasMore : false ,
163+ links : [ { rel : 'self' , href : `${ ORIGIN } ${ collectionPath } ` } ] ,
164+ } ,
165+ ( item ) => {
166+ validateOracleFusionSelfLink ( item , ORIGIN , {
167+ ...address ,
168+ relativePath : `invoices/${ encodedKey } ` ,
169+ } )
170+ return extractOracleFusionOpaqueKey ( item , ORIGIN , address )
171+ }
172+ )
173+
174+ expect ( page . items ) . toEqual ( [ key ] )
175+ } )
176+
177+ it ( 'continues ignoring unrelated link relations and non-link entries' , ( ) => {
178+ const value = resource ( `${ ORIGIN } ${ COLLECTION } /abc` , [
179+ null ,
180+ 'not a link' ,
181+ [ ] ,
182+ { rel : 'canonical' , href : 'unrelated' } ,
183+ ] )
184+ expect ( extractOracleFusionOpaqueKey ( value , ORIGIN , COLLECTION_ADDRESS ) ) . toBe ( 'abc' )
185+ } )
186+
145187 it . each ( [
146188 [ { } , 'exactly one' ] ,
147189 [ { links : [ ] } , 'exactly one' ] ,
@@ -153,6 +195,11 @@ describe('Oracle self links', () => {
153195 [ resource ( 'not a URL' ) , 'malformed' ] ,
154196 [ resource ( `https://evil.example${ COLLECTION } /abc` ) , 'credential-bound origin' ] ,
155197 [ resource ( `${ ORIGIN } ${ COLLECTION } /abc?secret=value` ) , 'credential-bound origin' ] ,
198+ [ resource ( `${ ORIGIN } ${ COLLECTION } /abc#fragment` ) , 'credential-bound origin' ] ,
199+ [
200+ resource ( `${ ORIGIN . replace ( 'https://' , 'https://user:password@' ) } ${ COLLECTION } /abc` ) ,
201+ 'credential-bound origin' ,
202+ ] ,
156203 [ resource ( `${ ORIGIN } ${ COLLECTION } /other` ) , 'requested resource path' ] ,
157204 ] ) ( 'rejects missing, duplicate, malformed, or unbound self links %#' , ( value , message ) => {
158205 expect ( ( ) =>
@@ -258,3 +305,89 @@ describe('Oracle self links', () => {
258305 )
259306 } )
260307} )
308+
309+ describe ( 'Oracle self-link representation compatibility' , ( ) => {
310+ const href = `${ ORIGIN } ${ COLLECTION } /abc`
311+ const links = [ { rel : 'self' , href } ]
312+ const detailAddress = { family : 'hcm' , relativePath : 'workers/abc' } as const
313+
314+ it ( 'accepts legacy links alongside context metadata without a links property' , ( ) => {
315+ const value = { links, '@context' : { key : 'not-the-resource-key' , headers : { ETag : 'etag' } } }
316+ expect ( extractOracleFusionOpaqueKey ( value , ORIGIN , COLLECTION_ADDRESS ) ) . toBe ( 'abc' )
317+ expect ( ( ) => validateOracleFusionSelfLink ( value , ORIGIN , detailAddress ) ) . not . toThrow ( )
318+ } )
319+
320+ it . each ( [ undefined , null , [ ] , 'context' , 1 ] . map ( ( context ) => ( { context } ) ) ) (
321+ 'rejects malformed context %# despite valid legacy links' ,
322+ ( { context } ) => {
323+ const value = { links, '@context' : context }
324+ expect ( ( ) => extractOracleFusionOpaqueKey ( value , ORIGIN , COLLECTION_ADDRESS ) ) . toThrow (
325+ 'Oracle resource context must be an object'
326+ )
327+ expect ( ( ) => validateOracleFusionSelfLink ( value , ORIGIN , detailAddress ) ) . toThrow (
328+ 'Oracle resource context must be an object'
329+ )
330+ }
331+ )
332+
333+ it . each ( [ { } , { '@context' : { } } , { '@context' : { key : 'abc' } } , { '@context' : { links : [ ] } } ] ) (
334+ 'rejects missing self links without deriving a key from context %#' ,
335+ ( value ) => {
336+ expect ( ( ) => extractOracleFusionOpaqueKey ( value , ORIGIN , COLLECTION_ADDRESS ) ) . toThrow (
337+ 'exactly one'
338+ )
339+ expect ( ( ) => validateOracleFusionSelfLink ( value , ORIGIN , detailAddress ) ) . toThrow (
340+ 'exactly one'
341+ )
342+ }
343+ )
344+
345+ describe . each ( [ 'legacy' , 'context' ] as const ) ( 'invalid %s links' , ( representation ) => {
346+ it . each ( [
347+ { links : undefined , error : 'exactly one' } ,
348+ { links : null , error : 'exactly one' } ,
349+ { links : { } , error : 'exactly one' } ,
350+ { links : [ ] , error : 'exactly one' } ,
351+ { links : [ ...links , ...links ] , error : 'exactly one' } ,
352+ { links : [ { rel : 'self' } ] , error : 'malformed' } ,
353+ { links : [ { rel : 'self' , href : 'not a URL' } ] , error : 'malformed' } ,
354+ {
355+ links : [ { rel : 'self' , href : `${ ORIGIN } ${ COLLECTION } /parent/../abc` } ] ,
356+ error : 'malformed' ,
357+ } ,
358+ ] ) (
359+ 'does not fall back to the other valid representation %#' ,
360+ ( { links : invalidLinks , error } ) => {
361+ const value = {
362+ links : representation === 'legacy' ? invalidLinks : links ,
363+ '@context' : { links : representation === 'context' ? invalidLinks : links } ,
364+ }
365+ expect ( ( ) => extractOracleFusionOpaqueKey ( value , ORIGIN , COLLECTION_ADDRESS ) ) . toThrow ( error )
366+ expect ( ( ) => validateOracleFusionSelfLink ( value , ORIGIN , detailAddress ) ) . toThrow ( error )
367+ }
368+ )
369+ } )
370+
371+ it . each ( [
372+ `${ ORIGIN } ${ COLLECTION } /other` ,
373+ `https://other.fa.us2.oraclecloud.com${ COLLECTION } /abc` ,
374+ `${ ORIGIN . toUpperCase ( ) } ${ COLLECTION } /abc` ,
375+ `${ ORIGIN } ${ COLLECTION } /%61bc` ,
376+ ] ) ( 'rejects conflicting href strings without normalizing or reflecting them %#' , ( otherHref ) => {
377+ for ( const [ legacyHref , contextHref ] of [
378+ [ href , otherHref ] ,
379+ [ otherHref , href ] ,
380+ ] ) {
381+ const value = {
382+ links : [ { rel : 'self' , href : legacyHref } ] ,
383+ '@context' : { links : [ { rel : 'self' , href : contextHref } ] } ,
384+ }
385+ expect ( ( ) => extractOracleFusionOpaqueKey ( value , ORIGIN , COLLECTION_ADDRESS ) ) . toThrow (
386+ new Error ( 'Oracle response self-link representations conflict' )
387+ )
388+ expect ( ( ) => validateOracleFusionSelfLink ( value , ORIGIN , detailAddress ) ) . toThrow (
389+ new Error ( 'Oracle response self-link representations conflict' )
390+ )
391+ }
392+ } )
393+ } )
0 commit comments