Skip to content
74 changes: 74 additions & 0 deletions spec/ParseServerRESTController.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,80 @@ describe('ParseServerRESTController', () => {
);
});

it('should deep copy context so mutations in beforeSave do not leak across requests', async () => {
const sharedContext = { counter: 0, nested: { value: 'original' } };

Parse.Cloud.beforeSave('ContextTestObject', req => {
// Mutate the context in beforeSave
req.context.counter = (req.context.counter || 0) + 1;
req.context.nested.value = 'mutated';
req.context.addedByHook = true;
});

// First save — this should not affect the original sharedContext
await RESTController.request(
'POST',
'/classes/ContextTestObject',
{ key: 'value1' },
{ context: sharedContext }
);

// The original context object must remain unchanged
expect(sharedContext.counter).toEqual(0);
expect(sharedContext.nested.value).toEqual('original');
expect(sharedContext.addedByHook).toBeUndefined();

// Second save with the same context — should also start with the original values
await RESTController.request(
'POST',
'/classes/ContextTestObject',
{ key: 'value2' },
{ context: sharedContext }
);

// The original context object must still remain unchanged
expect(sharedContext.counter).toEqual(0);
expect(sharedContext.nested.value).toEqual('original');
expect(sharedContext.addedByHook).toBeUndefined();
});

it('should isolate context between concurrent requests', async () => {
const contexts = [];

Parse.Cloud.beforeSave('ConcurrentContextObject', req => {
// Each request should see its own context, not a shared one
req.context.requestId = req.object.get('requestId');
contexts.push({ ...req.context });
});

const sharedContext = { shared: true };

await Promise.all([
RESTController.request(
'POST',
'/classes/ConcurrentContextObject',
{ requestId: 'req1' },
{ context: sharedContext }
),
RESTController.request(
'POST',
'/classes/ConcurrentContextObject',
{ requestId: 'req2' },
{ context: sharedContext }
),
]);

// Each hook should have seen its own requestId, not the other's
const req1Context = contexts.find(c => c.requestId === 'req1');
const req2Context = contexts.find(c => c.requestId === 'req2');
expect(req1Context).toBeDefined();
expect(req2Context).toBeDefined();
expect(req1Context.requestId).toEqual('req1');
expect(req2Context.requestId).toEqual('req2');
// Original context must remain unchanged
expect(sharedContext.requestId).toBeUndefined();
});

it('ensures sessionTokens are properly handled', async () => {
const user = await Parse.User.signUp('user', 'pass');
const sessionToken = user.getSessionToken();
Expand Down
2 changes: 1 addition & 1 deletion src/ParseServerRESTController.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ function ParseServerRESTController(applicationId, router) {
applicationId: applicationId,
sessionToken: options.sessionToken,
installationId: options.installationId,
context: options.context || {},
context: structuredClone(options.context || {}),
},
query,
};
Expand Down
Loading