@@ -202,6 +202,346 @@ describe('Cloud Code', () => {
202202 }
203203 } ) ;
204204
205+ describe ( 'beforeFind without DB operations' , ( ) => {
206+ let findSpy ;
207+
208+ beforeEach ( ( ) => {
209+ const config = Config . get ( 'test' ) ;
210+ const databaseAdapter = config . database . adapter ;
211+ findSpy = spyOn ( databaseAdapter , 'find' ) . and . callThrough ( ) ;
212+ } ) ;
213+
214+ it ( 'beforeFind can return object without DB operation' , async ( ) => {
215+ Parse . Cloud . beforeFind ( 'TestObject' , ( ) => {
216+ return new Parse . Object ( 'TestObject' , { foo : 'bar' } ) ;
217+ } ) ;
218+ Parse . Cloud . afterFind ( 'TestObject' , req => {
219+ expect ( req . objects ) . toBeDefined ( ) ;
220+ expect ( req . objects [ 0 ] . get ( 'foo' ) ) . toBe ( 'bar' ) ;
221+ } ) ;
222+
223+ const newObj = await new Parse . Query ( 'TestObject' ) . first ( ) ;
224+ expect ( newObj . className ) . toBe ( 'TestObject' ) ;
225+ expect ( newObj . toJSON ( ) ) . toEqual ( { foo : 'bar' } ) ;
226+ expect ( findSpy ) . not . toHaveBeenCalled ( ) ;
227+ await newObj . save ( ) ;
228+ } ) ;
229+
230+ it ( 'beforeFind can return array of objects without DB operation' , async ( ) => {
231+ Parse . Cloud . beforeFind ( 'TestObject' , ( ) => {
232+ return [ new Parse . Object ( 'TestObject' , { foo : 'bar' } ) ] ;
233+ } ) ;
234+ Parse . Cloud . afterFind ( 'TestObject' , req => {
235+ expect ( req . objects ) . toBeDefined ( ) ;
236+ expect ( req . objects [ 0 ] . get ( 'foo' ) ) . toBe ( 'bar' ) ;
237+ } ) ;
238+
239+ const newObj = await new Parse . Query ( 'TestObject' ) . first ( ) ;
240+ expect ( newObj . className ) . toBe ( 'TestObject' ) ;
241+ expect ( newObj . toJSON ( ) ) . toEqual ( { foo : 'bar' } ) ;
242+ expect ( findSpy ) . not . toHaveBeenCalled ( ) ;
243+ await newObj . save ( ) ;
244+ } ) ;
245+
246+ it ( 'beforeFind can return object for get query without DB operation' , async ( ) => {
247+ Parse . Cloud . beforeFind ( 'TestObject' , ( ) => {
248+ return [ new Parse . Object ( 'TestObject' , { foo : 'bar' } ) ] ;
249+ } ) ;
250+ Parse . Cloud . afterFind ( 'TestObject' , req => {
251+ expect ( req . objects ) . toBeDefined ( ) ;
252+ expect ( req . objects [ 0 ] . get ( 'foo' ) ) . toBe ( 'bar' ) ;
253+ } ) ;
254+
255+ const testObj = new Parse . Object ( 'TestObject' ) ;
256+ await testObj . save ( ) ;
257+ findSpy . calls . reset ( ) ;
258+
259+ const newObj = await new Parse . Query ( 'TestObject' ) . get ( testObj . id ) ;
260+ expect ( newObj . className ) . toBe ( 'TestObject' ) ;
261+ expect ( newObj . toJSON ( ) ) . toEqual ( { foo : 'bar' } ) ;
262+ expect ( findSpy ) . not . toHaveBeenCalled ( ) ;
263+ await newObj . save ( ) ;
264+ } ) ;
265+
266+ it ( 'beforeFind can return empty array without DB operation' , async ( ) => {
267+ Parse . Cloud . beforeFind ( 'TestObject' , ( ) => {
268+ return [ ] ;
269+ } ) ;
270+ Parse . Cloud . afterFind ( 'TestObject' , req => {
271+ expect ( req . objects . length ) . toBe ( 0 ) ;
272+ } ) ;
273+
274+ const obj = new Parse . Object ( 'TestObject' ) ;
275+ await obj . save ( ) ;
276+ findSpy . calls . reset ( ) ;
277+
278+ const newObj = await new Parse . Query ( 'TestObject' ) . first ( ) ;
279+ expect ( newObj ) . toBeUndefined ( ) ;
280+ expect ( findSpy ) . not . toHaveBeenCalled ( ) ;
281+ } ) ;
282+ } ) ;
283+
284+ describe ( 'beforeFind security with returned objects' , ( ) => {
285+ let userA ;
286+ let userB ;
287+ let secret ;
288+
289+ beforeEach ( async ( ) => {
290+ userA = new Parse . User ( ) ;
291+ userA . setUsername ( 'userA_' + Date . now ( ) ) ;
292+ userA . setPassword ( 'passA' ) ;
293+ await userA . signUp ( ) ;
294+
295+ userB = new Parse . User ( ) ;
296+ userB . setUsername ( 'userB_' + Date . now ( ) ) ;
297+ userB . setPassword ( 'passB' ) ;
298+ await userB . signUp ( ) ;
299+
300+ // Create an object readable only by userB
301+ const acl = new Parse . ACL ( ) ;
302+ acl . setPublicReadAccess ( false ) ;
303+ acl . setPublicWriteAccess ( false ) ;
304+ acl . setReadAccess ( userB . id , true ) ;
305+ acl . setWriteAccess ( userB . id , true ) ;
306+
307+ secret = new Parse . Object ( 'SecretDoc' ) ;
308+ secret . set ( 'title' , 'top' ) ;
309+ secret . set ( 'content' , 'classified' ) ;
310+ secret . setACL ( acl ) ;
311+ await secret . save ( null , { sessionToken : userB . getSessionToken ( ) } ) ;
312+
313+ Parse . Cloud . beforeFind ( 'SecretDoc' , ( ) => {
314+ return [ secret ] ;
315+ } ) ;
316+ } ) ;
317+
318+ it ( 'should not expose objects not readable by current user' , async ( ) => {
319+ const q = new Parse . Query ( 'SecretDoc' ) ;
320+ const results = await q . find ( { sessionToken : userA . getSessionToken ( ) } ) ;
321+ expect ( results . length ) . toBe ( 0 ) ;
322+ } ) ;
323+
324+ it ( 'should allow authorized user to see their objects' , async ( ) => {
325+ const q = new Parse . Query ( 'SecretDoc' ) ;
326+ const results = await q . find ( { sessionToken : userB . getSessionToken ( ) } ) ;
327+ expect ( results . length ) . toBe ( 1 ) ;
328+ expect ( results [ 0 ] . id ) . toBe ( secret . id ) ;
329+ expect ( results [ 0 ] . get ( 'title' ) ) . toBe ( 'top' ) ;
330+ expect ( results [ 0 ] . get ( 'content' ) ) . toBe ( 'classified' ) ;
331+ } ) ;
332+
333+ it ( 'should return OBJECT_NOT_FOUND on get() for unauthorized user' , async ( ) => {
334+ const q = new Parse . Query ( 'SecretDoc' ) ;
335+ await expectAsync (
336+ q . get ( secret . id , { sessionToken : userA . getSessionToken ( ) } )
337+ ) . toBeRejectedWith ( jasmine . objectContaining ( { code : Parse . Error . OBJECT_NOT_FOUND } ) ) ;
338+ } ) ;
339+
340+ it ( 'should allow master key to bypass ACL filtering when returning objects' , async ( ) => {
341+ const q = new Parse . Query ( 'SecretDoc' ) ;
342+ const results = await q . find ( { useMasterKey : true } ) ;
343+ expect ( results . length ) . toBe ( 1 ) ;
344+ expect ( results [ 0 ] . id ) . toBe ( secret . id ) ;
345+ } ) ;
346+
347+ it ( 'should apply protectedFields masking after re-filtering' , async ( ) => {
348+ // Configure protectedFields for SecretMask: mask `secretField` for everyone
349+ const protectedFields = { SecretMask : { '*' : [ 'secretField' ] } } ;
350+ await reconfigureServer ( { protectedFields } ) ;
351+
352+ const user = new Parse . User ( ) ;
353+ user . setUsername ( 'pfUser' ) ;
354+ user . setPassword ( 'pfPass' ) ;
355+ await user . signUp ( ) ;
356+
357+ // Object is publicly readable but has a protected field
358+ const doc = new Parse . Object ( 'SecretMask' ) ;
359+ doc . set ( 'name' , 'visible' ) ;
360+ doc . set ( 'secretField' , 'hiddenValue' ) ;
361+ await doc . save ( null , { useMasterKey : true } ) ;
362+
363+ Parse . Cloud . beforeFind ( 'SecretMask' , ( ) => {
364+ return [ doc ] ;
365+ } ) ;
366+
367+ // Query as normal user; after re-filtering, secretField should be removed
368+ const res = await new Parse . Query ( 'SecretMask' ) . first ( { sessionToken : user . getSessionToken ( ) } ) ;
369+ expect ( res ) . toBeDefined ( ) ;
370+ expect ( res . get ( 'name' ) ) . toBe ( 'visible' ) ;
371+ expect ( res . get ( 'secretField' ) ) . toBeUndefined ( ) ;
372+ const json = res . toJSON ( ) ;
373+ expect ( Object . prototype . hasOwnProperty . call ( json , 'secretField' ) ) . toBeFalse ( ) ;
374+ } ) ;
375+ } ) ;
376+ const { maybeRunAfterFindTrigger } = require ( '../lib/triggers' ) ;
377+
378+ describe ( 'maybeRunAfterFindTrigger - direct function tests' , ( ) => {
379+ const testConfig = {
380+ applicationId : 'test' ,
381+ logLevels : { triggerBeforeSuccess : 'info' , triggerAfter : 'info' } ,
382+ } ;
383+
384+ it ( 'should convert Parse.Object instances to JSON when no trigger defined' , async ( ) => {
385+ const className = 'TestParseObjectDirect_' + Date . now ( ) ;
386+
387+ const parseObj1 = new Parse . Object ( className ) ;
388+ parseObj1 . set ( 'name' , 'test1' ) ;
389+ parseObj1 . id = 'obj1' ;
390+
391+ const parseObj2 = new Parse . Object ( className ) ;
392+ parseObj2 . set ( 'name' , 'test2' ) ;
393+ parseObj2 . id = 'obj2' ;
394+
395+ const result = await maybeRunAfterFindTrigger (
396+ 'afterFind' ,
397+ null ,
398+ className ,
399+ [ parseObj1 , parseObj2 ] ,
400+ testConfig ,
401+ null ,
402+ { }
403+ ) ;
404+
405+ expect ( result ) . toBeDefined ( ) ;
406+ expect ( Array . isArray ( result ) ) . toBe ( true ) ;
407+ expect ( result . length ) . toBe ( 2 ) ;
408+ expect ( result [ 0 ] . name ) . toBe ( 'test1' ) ;
409+ expect ( result [ 1 ] . name ) . toBe ( 'test2' ) ;
410+ } ) ;
411+
412+ it ( 'should handle null/undefined objectsInput when no trigger' , async ( ) => {
413+ const className = 'TestNullDirect_' + Date . now ( ) ;
414+
415+ const resultNull = await maybeRunAfterFindTrigger (
416+ 'afterFind' ,
417+ null ,
418+ className ,
419+ null ,
420+ testConfig ,
421+ null ,
422+ { }
423+ ) ;
424+ expect ( resultNull ) . toEqual ( [ ] ) ;
425+
426+ const resultUndefined = await maybeRunAfterFindTrigger (
427+ 'afterFind' ,
428+ null ,
429+ className ,
430+ undefined ,
431+ testConfig ,
432+ null ,
433+ { }
434+ ) ;
435+ expect ( resultUndefined ) . toEqual ( [ ] ) ;
436+
437+ const resultEmpty = await maybeRunAfterFindTrigger (
438+ 'afterFind' ,
439+ null ,
440+ className ,
441+ [ ] ,
442+ testConfig ,
443+ null ,
444+ { }
445+ ) ;
446+ expect ( resultEmpty ) . toEqual ( [ ] ) ;
447+ } ) ;
448+
449+ it ( 'should handle plain object query with where clause' , async ( ) => {
450+ const className = 'TestQueryWhereDirect_' + Date . now ( ) ;
451+ let receivedQuery = null ;
452+
453+ Parse . Cloud . afterFind ( className , req => {
454+ receivedQuery = req . query ;
455+ return req . objects ;
456+ } ) ;
457+
458+ const mockObject = { id : 'test123' , className : className , name : 'test' } ;
459+
460+ const result = await maybeRunAfterFindTrigger (
461+ 'afterFind' ,
462+ null ,
463+ className ,
464+ [ mockObject ] ,
465+ testConfig ,
466+ { where : { name : 'test' } , limit : 10 } ,
467+ { }
468+ ) ;
469+
470+ expect ( receivedQuery ) . toBeInstanceOf ( Parse . Query ) ;
471+ expect ( result ) . toBeDefined ( ) ;
472+ } ) ;
473+
474+ it ( 'should handle plain object query without where clause' , async ( ) => {
475+ const className = 'TestQueryNoWhereDirect_' + Date . now ( ) ;
476+ let receivedQuery = null ;
477+
478+ Parse . Cloud . afterFind ( className , req => {
479+ receivedQuery = req . query ;
480+ return req . objects ;
481+ } ) ;
482+
483+ const mockObject = { id : 'test456' , className : className , name : 'test' } ;
484+ const pq = new Parse . Query ( className ) . withJSON ( { limit : 5 , skip : 1 } ) ;
485+
486+ const result = await maybeRunAfterFindTrigger (
487+ 'afterFind' ,
488+ null ,
489+ className ,
490+ [ mockObject ] ,
491+ testConfig ,
492+ pq ,
493+ { }
494+ ) ;
495+
496+ expect ( receivedQuery ) . toBeInstanceOf ( Parse . Query ) ;
497+ const qJSON = receivedQuery . toJSON ( ) ;
498+ expect ( qJSON . limit ) . toBe ( 5 ) ;
499+ expect ( qJSON . skip ) . toBe ( 1 ) ;
500+ expect ( qJSON . where ) . toEqual ( { } ) ;
501+ expect ( result ) . toBeDefined ( ) ;
502+ } ) ;
503+
504+ it ( 'should create default query for invalid query parameter' , async ( ) => {
505+ const className = 'TestInvalidQueryDirect_' + Date . now ( ) ;
506+ let receivedQuery = null ;
507+
508+ Parse . Cloud . afterFind ( className , req => {
509+ receivedQuery = req . query ;
510+ return req . objects ;
511+ } ) ;
512+
513+ const mockObject = { id : 'test789' , className : className , name : 'test' } ;
514+
515+ await maybeRunAfterFindTrigger (
516+ 'afterFind' ,
517+ null ,
518+ className ,
519+ [ mockObject ] ,
520+ testConfig ,
521+ 'invalid_query_string' ,
522+ { }
523+ ) ;
524+
525+ expect ( receivedQuery ) . toBeInstanceOf ( Parse . Query ) ;
526+ expect ( receivedQuery . className ) . toBe ( className ) ;
527+
528+ receivedQuery = null ;
529+
530+ await maybeRunAfterFindTrigger (
531+ 'afterFind' ,
532+ null ,
533+ className ,
534+ [ mockObject ] ,
535+ testConfig ,
536+ null ,
537+ { }
538+ ) ;
539+
540+ expect ( receivedQuery ) . toBeInstanceOf ( Parse . Query ) ;
541+ expect ( receivedQuery . className ) . toBe ( className ) ;
542+ } ) ;
543+ } ) ;
544+
205545 it ( 'beforeSave rejection with custom error code' , function ( done ) {
206546 Parse . Cloud . beforeSave ( 'BeforeSaveFailWithErrorCode' , function ( ) {
207547 throw new Parse . Error ( 999 , 'Nope' ) ;
0 commit comments