diff --git a/system/cache/util/EventURLFacade.cfc b/system/cache/util/EventURLFacade.cfc index 1cc807d96..c41ee2093 100644 --- a/system/cache/util/EventURLFacade.cfc +++ b/system/cache/util/EventURLFacade.cfc @@ -35,36 +35,66 @@ component accessors="true" { /** * Build a unique hash from an incoming request context + * Note: the 'event' key is always ignored from the request collection * * @event A request context object + * @targetContext The targeted request context object + * @eventDictionary The event metadata containing cache annotations */ - string function getUniqueHash( required event ){ - var rcTarget = arguments.event - .getCollection() - .filter( function( key, value ){ - // Remove event, not needed for hashing purposes - return ( key != "event" ); - } ); - - // systemOutput( "=====> uniquehash-rcTarget: #variables.jTreeMap.init( rcTarget ).toString()#", true ); - // systemOutput( "=====> uniquehash-rcTargetHash: #hash( variables.jTreeMap.init( rcTarget ).toString() )#", true ); - - var targetMixer = { - // Get the original incoming context hash - "incomingHash" : hash( variables.jTreeMap.init( rcTarget ).toString() ), - // Multi-Host support - "cgihost" : buildAppLink() - }; - - // Incorporate Routed Structs - structAppend( - targetMixer, - arguments.event.getRoutedStruct(), - true - ); - - // Return unique identifier - return hash( targetmixer.toString() ); + string function getUniqueHash( + required event, + required struct eventDictionary + ){ + + // Assign the RC struct and filter out the "event" key, which is not needed for cache keys + var rcTarget = arguments.event.getCollection().filter( ( key, value ) => { + return key != "event"; + } ); + + // Apply cache key filtering based on annotations + // We apply them in the following order: + // 1. Custom filter closure (if provided) + // 2. Include specific keys (if `cacheInclude` is not "*") + // 3. Exclude specific keys (if `cacheExclude` is provided and not empty) + + // If cacheFilter isn't a simple value, we assume it's a closure and call it + if ( !isSimpleValue( arguments.eventDictionary.cacheFilter ) ) { + rcTarget = arguments.eventDictionary.cacheFilter( rcTarget ); + } + + // Cache Includes + // only process if cacheInclude isn't set to "*" + if ( arguments.eventDictionary.cacheInclude != "*" ) { + // Whitelist specific keys + var includeKeys = arguments.eventDictionary.cacheInclude.listToArray(); + rcTarget = rcTarget.filter( ( key, value ) => { + return includeKeys.findNoCase( key ) > 0; + }); + } + + // Cache Excludes + if ( len( arguments.eventDictionary.cacheExclude ) ) { + // Blacklist specific keys + var excludeKeys = arguments.eventDictionary.cacheExclude.listToArray(); + rcTarget = rcTarget.filter( ( key, value ) => { + return excludeKeys.findNoCase( key ) == 0; + }); + } + + var targetMixer = { + // Get the original incoming context hash + "incomingHash" : hash( variables.jTreeMap.init( rcTarget ).toString() ), + // Multi-Host support + "cgihost" : buildAppLink() + }; + + // Incorporate Routed Structs + targetMixer.append( arguments.event.getRoutedStruct(), true ); + + + + // Return unique identifier + return hash( targetmixer.toString() ); } /** @@ -101,16 +131,19 @@ component accessors="true" { /** * Build an event key according to passed in params * - * @keySuffix The key suffix used in the cache key * @targetEvent The targeted ColdBox event executed * @targetContext The targeted request context object + * @eventDictionary The event metadata containing cache annotations */ - string function buildEventKey( - required keySuffix, - required targetEvent, - required targetContext - ){ - return buildBasicCacheKey( argumentCollection = arguments ) & getUniqueHash( arguments.targetContext ); + string function buildEventKey( + required targetEvent, + required targetContext, + required struct eventDictionary + ){ + return buildBasicCacheKey( + keySuffix = arguments.eventDictionary.suffix, + targetEvent = arguments.targetEvent + ) & getUniqueHash( arguments.targetContext, arguments.eventDictionary ); } /** diff --git a/system/web/services/HandlerService.cfc b/system/web/services/HandlerService.cfc index aa434b7a3..1445132e0 100644 --- a/system/web/services/HandlerService.cfc +++ b/system/web/services/HandlerService.cfc @@ -208,11 +208,11 @@ component extends="coldbox.system.web.services.BaseService" accessors="true" { structAppend( eventCachingData, eventDictionaryEntry, true ); // Create the Cache Key to save - eventCachingData.cacheKey = oEventURLFacade.buildEventKey( - keySuffix = eventDictionaryEntry.suffix, - targetEvent = arguments.ehBean.getFullEvent(), - targetContext = oRequestContext - ); + eventCachingData.cacheKey = oEventURLFacade.buildEventKey( + targetEvent = arguments.ehBean.getFullEvent(), + targetContext = oRequestContext, + eventDictionary = eventDictionaryEntry + ); // Event is cacheable and we need to flag it so the Renderer caches it oRequestContext.setEventCacheableEntry( eventCachingData ); @@ -654,7 +654,10 @@ component extends="coldbox.system.web.services.BaseService" accessors="true" { "lastAccessTimeout" : "", "cacheKey" : "", "suffix" : "", - "provider" : "template" + "provider" : "template", + "cacheInclude" : "*", + "cacheExclude" : "", + "cacheFilter" : "" }; } @@ -689,17 +692,47 @@ component extends="coldbox.system.web.services.BaseService" accessors="true" { "" ); mdEntry.provider = arguments.ehBean.getActionMetadata( "cacheProvider", "template" ); + mdEntry.cacheInclude = arguments.ehBean.getActionMetadata( "cacheInclude", "*" ); + mdEntry.cacheExclude = arguments.ehBean.getActionMetadata( "cacheExclude", "" ); + mdEntry.cacheFilter = arguments.ehBean.getActionMetadata( "cacheFilter", "" ); // Handler Event Cache Key Suffix, this is global to the event if ( - isClosure( arguments.oEventHandler.EVENT_CACHE_SUFFIX ) || isCustomFunction( - arguments.oEventHandler.EVENT_CACHE_SUFFIX - ) + isClosure( arguments.oEventHandler.EVENT_CACHE_SUFFIX ) || + isCustomFunction( arguments.oEventHandler.EVENT_CACHE_SUFFIX ) ) { mdEntry.suffix = oEventHandler.EVENT_CACHE_SUFFIX( arguments.ehBean ); } else { mdEntry.suffix = arguments.oEventHandler.EVENT_CACHE_SUFFIX; } + + // if the cacheFilter has a length and is a method, then we need to verify and store the resulting closure + if ( len( mdEntry.cacheFilter ) ) { + + // if the method doesn't exist, then throw an exception + if ( !arguments.oEventHandler._actionExists( mdEntry.cacheFilter ) ) { + throw( + message = "CacheFilter expected a private method '#mdEntry.cacheFilter#'", + type = "HandlerInvalidCacheFilterException", + detail = "CacheFilter method '#mdEntry.cacheFilter#' does not exist in handler '#getMetaData( oEventHandler ).name#'. Please verify your cacheFilter annotation." + ); + } + + mdEntry.cacheFilter = arguments.oEventHandler._privateInvoker( mdEntry.cacheFilter, {} ); + + // if the cacheFilter isn't a closure, throw an exception + // We check for isClosure and isCustomFunction for ACF/Lucee compatibility + if ( + !isClosure( mdEntry.cacheFilter ) && + !isCustomFunction( mdEntry.cacheFilter ) + ) { + throw( + message = "CacheFilter expected a closure.", + type = "HandlerInvalidCacheFilterException", + detail = "Please verify your cacheFilter annotation in handler '#getMetaData( oEventHandler ).name# to ensure it returns a closure." + ); + } + } } // end cache metadata is true diff --git a/system/web/services/RequestService.cfc b/system/web/services/RequestService.cfc index 53825d5be..099310332 100644 --- a/system/web/services/RequestService.cfc +++ b/system/web/services/RequestService.cfc @@ -153,12 +153,12 @@ component extends="coldbox.system.web.services.BaseService" { } // Incorporate metadata about event - eventCache.append( eventDictionary, true ); + eventCache.append( eventDictionary, true ); // Build the event cache key according to incoming request eventCache[ "cacheKey" ] = oEventURLFacade.buildEventKey( - keySuffix = eventDictionary.suffix, targetEvent = currentEvent, - targetContext = arguments.context + targetContext = arguments.context, + eventDictionary = eventDictionary ); // Check for Event Cache Purge diff --git a/test-harness/handlers/eventcaching.cfc b/test-harness/handlers/eventcaching.cfc index 1f5552f0c..9d9bc3fba 100644 --- a/test-harness/handlers/eventcaching.cfc +++ b/test-harness/handlers/eventcaching.cfc @@ -22,7 +22,8 @@ // Default Action function index( event, rc, prc ) cache="true" cacheTimeout="10"{ - prc.data = [ + + prc.data = [ { id : createUUID(), name : "luis" }, { id : createUUID(), name : "lucas" }, { id : createUUID(), name : "fernando" } @@ -46,6 +47,218 @@ return prc.data; } + // CacheInclude + + // Default (all keys used for cache) + function withIncludeAllRcKeys( event, rc, prc ) + cache ="true" + cacheTimeout ="10" + { + + prc.data = [ + { id : createUUID(), name : "luis" }, + { id : createUUID(), name : "lucas" }, + { id : createUUID(), name : "fernando" } + ]; + + return prc.data; + } + + // cacheInclude (no keys) + function withIncludeNoRcKeys( event, rc, prc ) + cache ="true" + cacheTimeout ="10" + cacheInclude="" + { + + prc.data = [ + { id : createUUID(), name : "luis" }, + { id : createUUID(), name : "lucas" }, + { id : createUUID(), name : "fernando" } + ]; + + return prc.data; + } + + // cacheInclude (1 RC key) + function withIncludeOneRcKey( event, rc, prc ) + cache ="true" + cacheTimeout ="10" + cacheInclude="slug" + { + + param rc.slug = ""; + + prc.data = [ + { id : createUUID(), name : "luis" }, + { id : createUUID(), name : "lucas" }, + { id : createUUID(), name : "fernando" } + ]; + + return prc.data; + } + + // cacheInclude (RC 2 keys) + function withIncludeRcKeyList( event, rc, prc ) + cache ="true" + cacheTimeout ="10" + cacheInclude="slug,id" + { + + param rc.slug = ""; + param rc.id = ""; + + prc.data = [ + { id : createUUID(), name : "luis" }, + { id : createUUID(), name : "lucas" }, + { id : createUUID(), name : "fernando" } + ]; + + return prc.data; + } + + // CacheExclude + + // cacheExclude (nothing will be filtered) + function withExcludeNoRcKeys( event, rc, prc ) + cache ="true" + cacheTimeout ="10" + cacheExclude="" + { + + prc.data = [ + { id : createUUID(), name : "luis" }, + { id : createUUID(), name : "lucas" }, + { id : createUUID(), name : "fernando" } + ]; + + return prc.data; + } + + // cacheExclude (1 RC key) + function withExcludeOneRcKey( event, rc, prc ) + cache ="true" + cacheTimeout ="10" + cacheExclude="slug" + { + + param rc.slug = ""; + + prc.data = [ + { id : createUUID(), name : "luis" }, + { id : createUUID(), name : "lucas" }, + { id : createUUID(), name : "fernando" } + ]; + + return prc.data; + } + + // cacheExclude (RC 2 keys) + function withExcludeRcKeyList( event, rc, prc ) + cache ="true" + cacheTimeout ="10" + cacheExclude="slug,id" + { + + param rc.slug = ""; + param rc.id = ""; + + prc.data = [ + { id : createUUID(), name : "luis" }, + { id : createUUID(), name : "lucas" }, + { id : createUUID(), name : "fernando" } + ]; + + return prc.data; + } + + + // cacheFilter (closure) + function withFilterClosure( event, rc, prc ) + cache ="true" + cacheTimeout ="10" + cacheFilter = "filterUtmParams" + { + + param rc.slug = ""; + + prc.data = [ + { id : createUUID(), name : "luis" }, + { id : createUUID(), name : "lucas" }, + { id : createUUID(), name : "fernando" } + ]; + + return prc.data; + } + + // cacheFilter (returns a closure with rcTarget as argument) + private function filterUtmParams() { + var ignoreKeys = [ "utm_source", "utm_medium", "utm_campaign" ]; + return ( rcTarget ) => { + return rcTarget.filter( ( key, value ) => { + // Filter out UTM parameters + return !ignoreKeys.findNoCase( key ); + } ); + } + } + + // all filters + function withAllFilters( event, rc, prc ) + cache ="true" + cacheTimeout ="10" + cacheFilter ="filterMutateParams" // mutate params + cacheInclude ="slug,id" // include slug and id + cacheExclude ="id" // exclude id + { + + param rc.slug = ""; + + prc.data = [ + { id : createUUID(), name : "luis" }, + { id : createUUID(), name : "lucas" }, + { id : createUUID(), name : "fernando" } + ]; + + return prc.data; + } + + // cacheFilter: Returns a closure that mutates the rcTarget + private function filterMutateParams() { + return ( rcTarget ) => { + rcTarget[ "slug" ] = createUuid(); // randomize slug + rcTarget[ "id" ] = createUuid(); // randomize id + return rcTarget; + }; + } + + + // withBadCacheFilter (returns something other than a closure) + function withBadCacheFilter( event, rc, prc ) + cache ="true" + cacheTimeout ="10" + cacheFilter ="filterNoClosure" + { + + param rc.slug = ""; + + prc.data = [ + { id : createUUID(), name : "luis" }, + { id : createUUID(), name : "lucas" }, + { id : createUUID(), name : "fernando" } + ]; + + return prc.data; + } + + // filterNoClosure: returns something other than a closure + private function filterNoClosure() { + return { + "slug" : "foo", + "id" : 1 + }; + } + + function cacheKeys( event, rc, prc ){ var keys = { "template" : getCache( "template" ).getKeys(), diff --git a/tests/specs/cache/util/EventsURLFacadeTest.cfc b/tests/specs/cache/util/EventsURLFacadeTest.cfc index 3ded4159a..584c4b3f4 100755 --- a/tests/specs/cache/util/EventsURLFacadeTest.cfc +++ b/tests/specs/cache/util/EventsURLFacadeTest.cfc @@ -35,7 +35,21 @@ var context = createMock( "coldbox.system.web.context.RequestContext" ) .setRoutedStruct( routedStruct ) .setContext( { event : "main.index", id : "123" } ); - var testHash = facade.getUniqueHash( context ); + + // Since everything is mocked and you don't have a real HandlerService or ehBean here, + // you can simulate the eventDictionary as it would be returned from getEventCachingMetadata. + // This is a simplified version, assuming you have a method to get the event metadata. + var eventDictionary = { + "context" : context.getContext(), + "cacheFilter": "", + "cacheInclude" : "*", // Assuming you want to include all keys + "cacheExclude" : "", // Assuming no keys to exclude + "suffix" : "unittest" + }; + + // Optionally, if your facade expects more keys, add them here. + + var testHash = facade.getUniqueHash( context, eventDictionary ); expect( testhash ).notToBeEmpty(); } ); @@ -61,8 +75,20 @@ var context = createMock( "coldbox.system.web.context.RequestContext" ); context.setRoutedStruct( { "name" : "majano" } ).setContext( { event : "main.index", id : "123" } ); - var testCacheKey = facade.buildEventKey( "unittest", "main.index", context ); - var uniqueHash = facade.getUniqueHash( context ); + // Since everything is mocked and you don't have a real HandlerService or ehBean here, + // you can simulate the eventDictionary as it would be returned from getEventCachingMetadata. + // This is a simplified version, assuming you have a method to get the event metadata. + var eventDictionary = { + "context" : context.getContext(), + "cacheFilter": "", + "cacheInclude" : "*", // Assuming you want to include all keys + "cacheExclude" : "", // Assuming no keys to exclude + "suffix" : "unittest" + }; + + var testCacheKey = facade.buildEventKey( "main.index", context, eventDictionary ); + + var uniqueHash = facade.getUniqueHash( context, eventDictionary ); var targetKey = cm.getEventCacheKeyPrefix() & "main.index-unittest-" & uniqueHash; expect( testCacheKey ).toBe( targetKey ); diff --git a/tests/specs/integration/EventCachingSpec.cfc b/tests/specs/integration/EventCachingSpec.cfc index be8969030..f65726be8 100755 --- a/tests/specs/integration/EventCachingSpec.cfc +++ b/tests/specs/integration/EventCachingSpec.cfc @@ -24,13 +24,365 @@ it( "can do cached events with custom provider annotations", function(){ - var event = execute( event = "eventcaching.withProvider", renderResults = true ); + var event = execute( + event = "eventcaching.withProvider", + renderResults = true + ); var prc = event.getPrivateCollection(); expect( prc.cbox_eventCacheableEntry ).toBeStruct().toHaveKey( "provider" ); expect( prc.cbox_eventCacheableEntry.provider ).toBe( "default" ); } ); + it( "can handle different RC collections", function(){ + + // execute an event and specify a queryString variable + var event1 = execute( + event = "eventcaching", + renderResults = true, + queryString="id=1" + ); + var prc1 = event1.getPrivateCollection(); + + // reset to simulate another request with a different rc scope + setup(); + + var event2 = execute( + event = "eventcaching", + renderResults = true, + queryString="id=2" + ); + + var prc2 = event2.getPrivateCollection(); + + // because the default cache considers the rc scope, the cache keys should be different + expect( prc1.cbox_eventCacheableEntry.cacheKey ).notToBe( prc2.cbox_eventCacheableEntry.cacheKey ); + + } ); + + // Cache Includes + + it( "can handle the cacheInclude metadata", function(){ + + // execute an event and specify a queryString variable + var event1 = execute( + event = "eventcaching.withIncludeOneRcKey", + renderResults = true, + queryString="id=1" + ); + var prc1 = event1.getPrivateCollection(); + + expect( prc1.cbox_eventCacheableEntry ).toBeStruct().toHaveKey( "cacheInclude" ); + expect( prc1.cbox_eventCacheableEntry.cacheInclude ).toBe( "slug" ); + + } ); + + it( "can ignore the rc scope with an empty cacheInclude", function(){ + + // execute an event and specify a queryString variable + var event1 = execute( + event = "eventcaching.withIncludeNoRcKeys", + renderResults = true, + queryString="id=1" + ); + var prc1 = event1.getPrivateCollection(); + + expect( prc1.cbox_eventCacheableEntry ).toBeStruct().toHaveKey( "cacheInclude" ); + expect( prc1.cbox_eventCacheableEntry.cacheInclude ).toBe( "" ); + + // reset to simulate another request + setup(); + + var event2 = execute( + event = "eventcaching.withIncludeNoRcKeys", + renderResults = true, + queryString="id=2" + ); + + var prc2 = event2.getPrivateCollection(); + + expect( prc2.cbox_eventCacheableEntry ).toBeStruct().toHaveKey( "cacheInclude" ); + expect( prc2.cbox_eventCacheableEntry.cacheInclude ).toBe( "" ); + + // because we ignore the RC, the cache key should match + expect( prc1.cbox_eventCacheableEntry.cacheKey ).toBe( prc2.cbox_eventCacheableEntry.cacheKey ); + + } ); + + it( "can isolate specific RC scope keys and ignore the rest", function(){ + + // execute an event and specify a queryString variable + var event1 = execute( + event = "eventcaching.withIncludeOneRcKey", + renderResults = true, + queryString="id=1&slug=foo" + ); + var prc1 = event1.getPrivateCollection(); + + expect( prc1.cbox_eventCacheableEntry ).toBeStruct().toHaveKey( "cacheInclude" ); + expect( prc1.cbox_eventCacheableEntry.cacheInclude ).toBe( "slug" ); + + // reset to simulate another request + setup(); + + var event2 = execute( + event = "eventcaching.withIncludeOneRcKey", + renderResults = true, + queryString="id=2&slug=foo" + ); + + var prc2 = event2.getPrivateCollection(); + + expect( prc2.cbox_eventCacheableEntry ).toBeStruct().toHaveKey( "cacheInclude" ); + expect( prc2.cbox_eventCacheableEntry.cacheInclude ).toBe( "slug" ); + + // because we ignore the RC, the cache key should match + expect( prc1.cbox_eventCacheableEntry.cacheKey ).toBe( prc2.cbox_eventCacheableEntry.cacheKey ); + + } ); + + it( "can handle a list of specific RC scope keys and ignore the rest", function(){ + + // execute an event and specify a queryString variable + var event1 = execute( + event = "eventcaching.withIncludeRcKeyList", + renderResults = true, + queryString="id=1&slug=foo&source=google" + ); + var prc1 = event1.getPrivateCollection(); + + expect( prc1.cbox_eventCacheableEntry ).toBeStruct().toHaveKey( "cacheInclude" ); + expect( prc1.cbox_eventCacheableEntry.cacheInclude ).toBe( "slug,id" ); + + // reset to simulate another request + setup(); + + var event2 = execute( + event = "eventcaching.withIncludeRcKeyList", + renderResults = true, + queryString="id=1&slug=foo&source=bing" + ); + + var prc2 = event2.getPrivateCollection(); + + expect( prc2.cbox_eventCacheableEntry ).toBeStruct().toHaveKey( "cacheInclude" ); + expect( prc2.cbox_eventCacheableEntry.cacheInclude ).toBe( "slug,id" ); + + // because we ignore the RC, the cache key should match + expect( prc1.cbox_eventCacheableEntry.cacheKey ).toBe( prc2.cbox_eventCacheableEntry.cacheKey ); + + } ); + + // Cache Excludes + + it( "can handle the cacheExclude metadata", function(){ + + // execute an event and specify a queryString variable + var event1 = execute( + event = "eventcaching.withExcludeOneRcKey", + renderResults = true, + queryString="id=1" + ); + var prc1 = event1.getPrivateCollection(); + + expect( prc1.cbox_eventCacheableEntry ).toBeStruct().toHaveKey( "cacheExclude" ); + expect( prc1.cbox_eventCacheableEntry.cacheExclude ).toBe( "slug" ); + + } ); + + it( "will include the entire rc scope with an empty cacheExclude", function(){ + + // execute an event and specify a queryString variable + var event1 = execute( + event = "eventcaching.withExcludeNoRcKeys", + renderResults = true, + queryString="id=1" + ); + var prc1 = event1.getPrivateCollection(); + + expect( prc1.cbox_eventCacheableEntry ).toBeStruct().toHaveKey( "cacheExclude" ); + expect( prc1.cbox_eventCacheableEntry.cacheExclude ).toBe( "" ); + + // reset to simulate another request + setup(); + + var event2 = execute( + event = "eventcaching.withExcludeNoRcKeys", + renderResults = true, + queryString="id=2" + ); + + var prc2 = event2.getPrivateCollection(); + + expect( prc2.cbox_eventCacheableEntry ).toBeStruct().toHaveKey( "cacheExclude" ); + expect( prc2.cbox_eventCacheableEntry.cacheExclude ).toBe( "" ); + + // because we allowed the entire RC, the cache key should not match + expect( prc1.cbox_eventCacheableEntry.cacheKey ).notToBe( prc2.cbox_eventCacheableEntry.cacheKey ); + + } ); + + it( "can ignore a specific RC scope key and allow the rest", function(){ + + // execute an event and specify a queryString variable + var event1 = execute( + event = "eventcaching.withExcludeOneRcKey", + renderResults = true, + queryString="id=1&slug=foo" + ); + var prc1 = event1.getPrivateCollection(); + + expect( prc1.cbox_eventCacheableEntry ).toBeStruct().toHaveKey( "cacheExclude" ); + expect( prc1.cbox_eventCacheableEntry.cacheExclude ).toBe( "slug" ); + + // reset to simulate another request + setup(); + + var event2 = execute( + event = "eventcaching.withExcludeOneRcKey", + renderResults = true, + queryString="id=1&slug=bar" + ); + + var prc2 = event2.getPrivateCollection(); + + expect( prc2.cbox_eventCacheableEntry ).toBeStruct().toHaveKey( "cacheExclude" ); + expect( prc2.cbox_eventCacheableEntry.cacheExclude ).toBe( "slug" ); + + // because we ignored 'slug', the cache key should match + expect( prc1.cbox_eventCacheableEntry.cacheKey ).toBe( prc2.cbox_eventCacheableEntry.cacheKey ); + + } ); + + it( "can handle a list of specific RC scope keys to exclude", function(){ + + // execute an event and specify a queryString variable + var event1 = execute( + event = "eventcaching.withExcludeRcKeyList", + renderResults = true, + queryString="id=1&slug=foo&source=google" + ); + var prc1 = event1.getPrivateCollection(); + + expect( prc1.cbox_eventCacheableEntry ).toBeStruct().toHaveKey( "cacheExclude" ); + expect( prc1.cbox_eventCacheableEntry.cacheExclude ).toBe( "slug,id" ); + + // reset to simulate another request + setup(); + + var event2 = execute( + event = "eventcaching.withExcludeRcKeyList", + renderResults = true, + queryString="id=2&slug=bar&source=google" + ); + + var prc2 = event2.getPrivateCollection(); + + expect( prc2.cbox_eventCacheableEntry ).toBeStruct().toHaveKey( "cacheExclude" ); + expect( prc2.cbox_eventCacheableEntry.cacheExclude ).toBe( "slug,id" ); + + // because we ignored 'id and slug', the cache key should match + expect( prc1.cbox_eventCacheableEntry.cacheKey ).toBe( prc2.cbox_eventCacheableEntry.cacheKey ); + + } ); + + // includeFilter + + it( "can handle the cacheFilter metadata", function(){ + + // execute an event and specify a queryString variable + var event1 = execute( + event = "eventcaching.withFilterClosure", + renderResults = true, + queryString="id=1&utm_source=google&utm_medium=cpc" + ); + var prc1 = event1.getPrivateCollection(); + + expect( prc1.cbox_eventCacheableEntry ).toBeStruct().toHaveKey( "cacheFilter" ); + expect( isCustomFunction( prc1.cbox_eventCacheableEntry.cacheFilter ) ).toBeTrue(); + + } ); + + it( "requires cacheFilter to be a closure", function(){ + + expect( () => { + execute( + event = "eventcaching.withBadCacheFilter", + renderResults = true + ) + } ).toThrow( type="HandlerInvalidCacheFilterException" ); + + } ); + + + it( "can filter RC keys based on cacheFilter", function(){ + + // execute an event and specify a queryString variable + var event1 = execute( + event = "eventcaching.withFilterClosure", + renderResults = true, + queryString="id=1&utm_source=google&utm_medium=cpc" + ); + var prc1 = event1.getPrivateCollection(); + + expect( prc1.cbox_eventCacheableEntry ).toBeStruct().toHaveKey( "cacheFilter" ); + expect( isCustomFunction( prc1.cbox_eventCacheableEntry.cacheFilter ) ).toBeTrue(); + + setup(); + + var event2 = execute( + event = "eventcaching.withFilterClosure", + renderResults = true, + queryString="id=1&utm_source=bing&utm_medium=organic" + ); + + var prc2 = event2.getPrivateCollection(); + + expect( prc2.cbox_eventCacheableEntry ).toBeStruct().toHaveKey( "cacheFilter" ); + expect( isCustomFunction( prc2.cbox_eventCacheableEntry.cacheFilter ) ).toBeTrue(); + + // because we ignored 'all utm params in the method', the cache key should match + expect( prc1.cbox_eventCacheableEntry.cacheKey ).toBe( prc2.cbox_eventCacheableEntry.cacheKey ); + + } ); + + + it( "can filter RC keys based on cacheFilter, cacheInclude, and cacheExclude working together", function(){ + + // execute an event and specify a queryString variable + // in this test we know that the cacheFilter will randomize the slug and id keys + var event1 = execute( + event = "eventcaching.withAllFilters", + renderResults = true, + queryString="id=1&slug=foo&utm_source=google" + ); + var prc1 = event1.getPrivateCollection(); + + expect( prc1.cbox_eventCacheableEntry ).toBeStruct().toHaveKey( "cacheFilter,cacheInclude,cacheExclude" ); + expect( isCustomFunction( prc1.cbox_eventCacheableEntry.cacheFilter ) ).toBeTrue(); + expect( prc1.cbox_eventCacheableEntry.cacheInclude ).toBe( "slug,id" ); + expect( prc1.cbox_eventCacheableEntry.cacheExclude ).toBe( "id" ); + + setup(); + + var event2 = execute( + event = "eventcaching.withAllFilters", + renderResults = true, + queryString="id=1&slug=foo&utm_source=bing" + ); + + var prc2 = event2.getPrivateCollection(); + + expect( prc2.cbox_eventCacheableEntry ).toBeStruct().toHaveKey( "cacheExclude,cacheInclude,cacheExclude" ); + expect( isCustomFunction( prc2.cbox_eventCacheableEntry.cacheFilter ) ).toBeTrue(); + expect( prc2.cbox_eventCacheableEntry.cacheInclude ).toBe( "slug,id" ); + expect( prc2.cbox_eventCacheableEntry.cacheExclude ).toBe( "id" ); + + // because we forced the cacheFilter to mutate the slug and id keys, the cache key should never match + expect( prc1.cbox_eventCacheableEntry.cacheKey ).notToBe( prc2.cbox_eventCacheableEntry.cacheKey ); + + } ); + var formats = [ "json", "xml", "pdf" ]; for ( var thisFormat in formats ) { it(