@@ -248,4 +248,86 @@ class AppSecSpanPostProcessorTest extends DDSpecification {
248248 1 * sampler. releaseOne()
249249 0 * _
250250 }
251+
252+ void ' permit is released even if extractSchemas throws exception' () {
253+ given :
254+ def sampler = Mock (ApiSecuritySamplerImpl )
255+ def producer = Mock (EventProducerService )
256+ def span = Mock (AgentSpan )
257+ def reqCtx = Mock (RequestContext )
258+ def traceSegment = Mock (TraceSegment )
259+ def ctx = Mock (AppSecRequestContext )
260+ def processor = new AppSecSpanPostProcessor (sampler, producer)
261+
262+ when :
263+ processor. process(span, { false })
264+
265+ then :
266+ def ex = thrown(RuntimeException )
267+ ex. message == " Unexpected error"
268+ 1 * span. getRequestContext() >> reqCtx
269+ 1 * reqCtx. getData(_) >> ctx
270+ 1 * ctx. isKeepOpenForApiSecurityPostProcessing() >> true
271+ 1 * sampler. sampleRequest(_) >> true
272+ 1 * reqCtx. getTraceSegment() >> { throw new RuntimeException (" Unexpected error" ) }
273+ 1 * ctx. setKeepOpenForApiSecurityPostProcessing(false )
274+ 1 * ctx. closeWafContext()
275+ 1 * ctx. close()
276+ 1 * sampler. releaseOne() // Critical: permit is still released despite exception
277+ 0 * _
278+ }
279+
280+ void ' multiple requests do not exhaust semaphore permits' () {
281+ given :
282+ // Use real ApiSecuritySamplerImpl which has a semaphore with 4 permits
283+ def realSampler = new ApiSecuritySamplerImpl ()
284+ def producer = Mock (EventProducerService )
285+ def processor = new AppSecSpanPostProcessor (realSampler, producer)
286+
287+ when : ' Process 5 consecutive requests that acquire permits'
288+ 5. times { i ->
289+ def span = Mock (AgentSpan )
290+ def reqCtx = Mock (RequestContext )
291+ def ctx = Mock (AppSecRequestContext )
292+
293+ // Mock the interactions
294+ span. getRequestContext() >> reqCtx
295+ reqCtx. getData(_) >> ctx
296+ ctx. isKeepOpenForApiSecurityPostProcessing() >> true
297+ ctx. setKeepOpenForApiSecurityPostProcessing(false )
298+ ctx. closeWafContext()
299+ ctx. close()
300+
301+ // Process should complete without issues, releasing permit each time
302+ processor. process(span, { false })
303+ }
304+
305+ then : ' All requests complete successfully without permit exhaustion'
306+ noExceptionThrown()
307+ }
308+
309+ void ' permit is released when ctx cleanup operations fail' () {
310+ given :
311+ def sampler = Mock (ApiSecuritySamplerImpl )
312+ def producer = Mock (EventProducerService )
313+ def span = Mock (AgentSpan )
314+ def reqCtx = Mock (RequestContext )
315+ def ctx = Mock (AppSecRequestContext )
316+ def processor = new AppSecSpanPostProcessor (sampler, producer)
317+
318+ when :
319+ processor. process(span, { false })
320+
321+ then :
322+ noExceptionThrown()
323+ 1 * span. getRequestContext() >> reqCtx
324+ 1 * reqCtx. getData(_) >> ctx
325+ 1 * ctx. isKeepOpenForApiSecurityPostProcessing() >> true
326+ 1 * sampler. sampleRequest(_) >> false
327+ 1 * ctx. setKeepOpenForApiSecurityPostProcessing(false )
328+ 1 * ctx. closeWafContext() >> { throw new RuntimeException (" WAF context close failed" ) }
329+ 1 * ctx. close() >> { throw new RuntimeException (" Context close failed" ) }
330+ 1 * sampler. releaseOne() // Critical: permit is still released despite cleanup failures
331+ 0 * _
332+ }
251333}
0 commit comments