@@ -519,6 +519,97 @@ describe('ParseServerRESTController', () => {
519519 ) ;
520520 } ) ;
521521
522+ it ( 'should deep copy context so mutations in beforeSave do not leak across requests' , async ( ) => {
523+ const sharedContext = { counter : 0 , nested : { value : 'original' } } ;
524+
525+ Parse . Cloud . beforeSave ( 'ContextTestObject' , req => {
526+ // Mutate the context in beforeSave
527+ req . context . counter = ( req . context . counter || 0 ) + 1 ;
528+ req . context . nested . value = 'mutated' ;
529+ req . context . addedByHook = true ;
530+ } ) ;
531+
532+ // First save — this should not affect the original sharedContext
533+ await RESTController . request (
534+ 'POST' ,
535+ '/classes/ContextTestObject' ,
536+ { key : 'value1' } ,
537+ { context : sharedContext }
538+ ) ;
539+
540+ // The original context object must remain unchanged
541+ expect ( sharedContext . counter ) . toEqual ( 0 ) ;
542+ expect ( sharedContext . nested . value ) . toEqual ( 'original' ) ;
543+ expect ( sharedContext . addedByHook ) . toBeUndefined ( ) ;
544+
545+ // Second save with the same context — should also start with the original values
546+ await RESTController . request (
547+ 'POST' ,
548+ '/classes/ContextTestObject' ,
549+ { key : 'value2' } ,
550+ { context : sharedContext }
551+ ) ;
552+
553+ // The original context object must still remain unchanged
554+ expect ( sharedContext . counter ) . toEqual ( 0 ) ;
555+ expect ( sharedContext . nested . value ) . toEqual ( 'original' ) ;
556+ expect ( sharedContext . addedByHook ) . toBeUndefined ( ) ;
557+ } ) ;
558+
559+ it ( 'should isolate context between concurrent requests' , async ( ) => {
560+ const contexts = [ ] ;
561+
562+ Parse . Cloud . beforeSave ( 'ConcurrentContextObject' , req => {
563+ // Each request should see its own context, not a shared one
564+ req . context . requestId = req . object . get ( 'requestId' ) ;
565+ contexts . push ( { ...req . context } ) ;
566+ } ) ;
567+
568+ const sharedContext = { shared : true } ;
569+
570+ await Promise . all ( [
571+ RESTController . request (
572+ 'POST' ,
573+ '/classes/ConcurrentContextObject' ,
574+ { requestId : 'req1' } ,
575+ { context : sharedContext }
576+ ) ,
577+ RESTController . request (
578+ 'POST' ,
579+ '/classes/ConcurrentContextObject' ,
580+ { requestId : 'req2' } ,
581+ { context : sharedContext }
582+ ) ,
583+ ] ) ;
584+
585+ // Each hook should have seen its own requestId, not the other's
586+ const req1Context = contexts . find ( c => c . requestId === 'req1' ) ;
587+ const req2Context = contexts . find ( c => c . requestId === 'req2' ) ;
588+ expect ( req1Context ) . toBeDefined ( ) ;
589+ expect ( req2Context ) . toBeDefined ( ) ;
590+ expect ( req1Context . requestId ) . toEqual ( 'req1' ) ;
591+ expect ( req2Context . requestId ) . toEqual ( 'req2' ) ;
592+ // Original context must remain unchanged
593+ expect ( sharedContext . requestId ) . toBeUndefined ( ) ;
594+ } ) ;
595+
596+ it ( 'should reject with an error when context contains non-cloneable values' , async ( ) => {
597+ const nonCloneableContext = { fn : ( ) => { } } ;
598+ try {
599+ await RESTController . request (
600+ 'POST' ,
601+ '/classes/MyObject' ,
602+ { key : 'value' } ,
603+ { context : nonCloneableContext }
604+ ) ;
605+ fail ( 'should have rejected for non-cloneable context' ) ;
606+ } catch ( error ) {
607+ expect ( error ) . toBeDefined ( ) ;
608+ expect ( error . code ) . toEqual ( Parse . Error . INVALID_VALUE ) ;
609+ expect ( error . message ) . toContain ( 'Context contains non-cloneable values' ) ;
610+ }
611+ } ) ;
612+
522613 it ( 'ensures sessionTokens are properly handled' , async ( ) => {
523614 const user = await Parse . User . signUp ( 'user' , 'pass' ) ;
524615 const sessionToken = user . getSessionToken ( ) ;
0 commit comments