1818import datadog .trace .api .internal .TraceSegment ;
1919import java .io .ByteArrayOutputStream ;
2020import java .io .IOException ;
21+ import java .util .Arrays ;
22+ import java .util .Collections ;
23+ import java .util .List ;
24+ import java .util .function .BiFunction ;
2125import javax .servlet .ServletOutputStream ;
2226import javax .servlet .WriteListener ;
2327import javax .servlet .http .HttpServletRequest ;
2428import javax .servlet .http .HttpServletResponse ;
29+ import javax .servlet .http .Part ;
2530import org .junit .jupiter .api .Test ;
2631
2732class GlassFishBlockingHelperTest {
@@ -170,6 +175,146 @@ void tryBlock_effectivelyBlockedThrows_stillReturnsTrue() throws Exception {
170175 assertTrue (GlassFishBlockingHelper .tryBlock (reqCtx , null , null , rba (403 )));
171176 }
172177
178+ // ------- processPartsAndBlock() -------
179+
180+ @ Test
181+ void processPartsAndBlock_formField_skipped () throws Exception {
182+ Part formField = mockPart (null , "text/plain" , new byte [0 ]);
183+ RequestContext reqCtx = mockReqCtx (null , mock (TraceSegment .class ));
184+ BiFunction <RequestContext , List <String >, Flow <Void >> filenamesCb = mockPassThroughCb ();
185+
186+ assertFalse (
187+ GlassFishBlockingHelper .processPartsAndBlock (
188+ Collections .singletonList (formField ), reqCtx , null , null , filenamesCb , null ));
189+ verify (formField ).getSubmittedFileName ();
190+ verify (formField , never ()).getInputStream ();
191+ }
192+
193+ @ Test
194+ void processPartsAndBlock_emptyFilename_notAddedToFilenames_butContentRead () throws Exception {
195+ byte [] content = "data" .getBytes ();
196+ Part filePart = mockPart ("" , "application/octet-stream" , content );
197+ RequestContext reqCtx = mockReqCtx (null , mock (TraceSegment .class ));
198+ BiFunction <RequestContext , List <String >, Flow <Void >> filenamesCb = mockPassThroughCb ();
199+ BiFunction <RequestContext , List <String >, Flow <Void >> contentCb = mockPassThroughCb ();
200+
201+ assertFalse (
202+ GlassFishBlockingHelper .processPartsAndBlock (
203+ Collections .singletonList (filePart ), reqCtx , null , null , filenamesCb , contentCb ));
204+
205+ verify (filePart ).getInputStream ();
206+ verify (filenamesCb , never ()).apply (any (), any ());
207+ verify (contentCb ).apply (eq (reqCtx ), any ());
208+ }
209+
210+ @ Test
211+ void processPartsAndBlock_normalFilename_reportedViaFilenamesCb () throws Exception {
212+ Part filePart = mockPart ("file.txt" , "text/plain" , "hello" .getBytes ());
213+ RequestContext reqCtx = mockReqCtx (null , mock (TraceSegment .class ));
214+ BiFunction <RequestContext , List <String >, Flow <Void >> filenamesCb = mockPassThroughCb ();
215+
216+ assertFalse (
217+ GlassFishBlockingHelper .processPartsAndBlock (
218+ Collections .singletonList (filePart ), reqCtx , null , null , filenamesCb , null ));
219+
220+ verify (filenamesCb ).apply (eq (reqCtx ), eq (Collections .singletonList ("file.txt" )));
221+ }
222+
223+ @ Test
224+ void processPartsAndBlock_contentRead_reportedViaContentCb () throws Exception {
225+ Part filePart = mockPart ("file.bin" , "application/octet-stream" , new byte [] {1 , 2 , 3 });
226+ RequestContext reqCtx = mockReqCtx (null , mock (TraceSegment .class ));
227+ BiFunction <RequestContext , List <String >, Flow <Void >> contentCb = mockPassThroughCb ();
228+
229+ assertFalse (
230+ GlassFishBlockingHelper .processPartsAndBlock (
231+ Collections .singletonList (filePart ), reqCtx , null , null , null , contentCb ));
232+
233+ verify (contentCb ).apply (eq (reqCtx ), any ());
234+ }
235+
236+ @ Test
237+ void processPartsAndBlock_maxFilesLimit_enforced () throws Exception {
238+ int limit = GlassFishBlockingHelper .MAX_FILE_CONTENT_COUNT ;
239+ Part [] tooMany = new Part [limit + 1 ];
240+ for (int i = 0 ; i <= limit ; i ++) {
241+ tooMany [i ] = mockPart ("f" + i + ".bin" , "application/octet-stream" , new byte [0 ]);
242+ }
243+ RequestContext reqCtx = mockReqCtx (null , mock (TraceSegment .class ));
244+ BiFunction <RequestContext , List <String >, Flow <Void >> contentCb = mockPassThroughCb ();
245+
246+ assertFalse (
247+ GlassFishBlockingHelper .processPartsAndBlock (
248+ Arrays .asList (tooMany ), reqCtx , null , null , null , contentCb ));
249+
250+ verify (contentCb ).apply (eq (reqCtx ), any (List .class ));
251+ verify (tooMany [limit ], never ()).getInputStream ();
252+ }
253+
254+ @ Test
255+ @ SuppressWarnings ("unchecked" )
256+ void processPartsAndBlock_getInputStreamThrows_emptyStringFallback () throws Exception {
257+ Part filePart = mock (Part .class );
258+ when (filePart .getSubmittedFileName ()).thenReturn ("bad.bin" );
259+ when (filePart .getInputStream ()).thenThrow (new IOException ("disk error" ));
260+ RequestContext reqCtx = mockReqCtx (null , mock (TraceSegment .class ));
261+ BiFunction <RequestContext , List <String >, Flow <Void >> contentCb = mockPassThroughCb ();
262+
263+ assertFalse (
264+ GlassFishBlockingHelper .processPartsAndBlock (
265+ Collections .singletonList (filePart ), reqCtx , null , null , null , contentCb ));
266+
267+ verify (contentCb ).apply (eq (reqCtx ), eq (Collections .singletonList ("" )));
268+ }
269+
270+ @ Test
271+ void processPartsAndBlock_filenamesCbBlocks_contentCbNotFired () throws Exception {
272+ Part filePart = mockPart ("evil.exe" , "application/octet-stream" , "content" .getBytes ());
273+ TraceSegment segment = mock (TraceSegment .class );
274+ RequestContext reqCtx = mockReqCtx (null , segment );
275+ TestServletOutputStream out = new TestServletOutputStream ();
276+ HttpServletResponse resp = mock (HttpServletResponse .class );
277+ when (resp .isCommitted ()).thenReturn (false );
278+ when (resp .getOutputStream ()).thenReturn (out );
279+ BiFunction <RequestContext , List <String >, Flow <Void >> filenamesCb = mockBlockingCb (403 );
280+ BiFunction <RequestContext , List <String >, Flow <Void >> contentCb = mockPassThroughCb ();
281+
282+ assertTrue (
283+ GlassFishBlockingHelper .processPartsAndBlock (
284+ Collections .singletonList (filePart ), reqCtx , null , resp , filenamesCb , contentCb ));
285+
286+ verify (contentCb , never ()).apply (any (), any ());
287+ }
288+
289+ @ Test
290+ void processPartsAndBlock_contentCbBlocks_returnsTrue () throws Exception {
291+ Part filePart = mockPart ("upload.bin" , "application/octet-stream" , "payload" .getBytes ());
292+ TraceSegment segment = mock (TraceSegment .class );
293+ RequestContext reqCtx = mockReqCtx (null , segment );
294+ TestServletOutputStream out = new TestServletOutputStream ();
295+ HttpServletResponse resp = mock (HttpServletResponse .class );
296+ when (resp .isCommitted ()).thenReturn (false );
297+ when (resp .getOutputStream ()).thenReturn (out );
298+ BiFunction <RequestContext , List <String >, Flow <Void >> filenamesCb = mockPassThroughCb ();
299+ BiFunction <RequestContext , List <String >, Flow <Void >> contentCb = mockBlockingCb (403 );
300+
301+ assertTrue (
302+ GlassFishBlockingHelper .processPartsAndBlock (
303+ Collections .singletonList (filePart ), reqCtx , null , resp , filenamesCb , contentCb ));
304+ }
305+
306+ @ Test
307+ void processPartsAndBlock_nonPartObject_skipped () {
308+ RequestContext reqCtx = mockReqCtx (null , mock (TraceSegment .class ));
309+ BiFunction <RequestContext , List <String >, Flow <Void >> filenamesCb = mockPassThroughCb ();
310+
311+ assertFalse (
312+ GlassFishBlockingHelper .processPartsAndBlock (
313+ Collections .singletonList ("not-a-part" ), reqCtx , null , null , filenamesCb , null ));
314+
315+ verify (filenamesCb , never ()).apply (any (), any ());
316+ }
317+
173318 // ------- Helpers -------
174319
175320 private static Flow .Action .RequestBlockingAction rba (int statusCode ) {
@@ -183,6 +328,35 @@ private static RequestContext mockReqCtx(BlockResponseFunction brf, TraceSegment
183328 return reqCtx ;
184329 }
185330
331+ private static Part mockPart (String submittedFilename , String contentType , byte [] content )
332+ throws Exception {
333+ Part part = mock (Part .class );
334+ when (part .getSubmittedFileName ()).thenReturn (submittedFilename );
335+ when (part .getContentType ()).thenReturn (contentType );
336+ when (part .getInputStream ()).thenAnswer (ignored -> new java .io .ByteArrayInputStream (content ));
337+ return part ;
338+ }
339+
340+ @ SuppressWarnings ("unchecked" )
341+ private static BiFunction <RequestContext , List <String >, Flow <Void >> mockPassThroughCb () {
342+ BiFunction <RequestContext , List <String >, Flow <Void >> cb = mock (BiFunction .class );
343+ Flow <Void > flow = mock (Flow .class );
344+ when (flow .getAction ()).thenReturn (Flow .Action .Noop .INSTANCE );
345+ when (cb .apply (any (), any ())).thenReturn (flow );
346+ return cb ;
347+ }
348+
349+ @ SuppressWarnings ("unchecked" )
350+ private static BiFunction <RequestContext , List <String >, Flow <Void >> mockBlockingCb (
351+ int statusCode ) {
352+ BiFunction <RequestContext , List <String >, Flow <Void >> cb = mock (BiFunction .class );
353+ Flow <Void > flow = mock (Flow .class );
354+ when (flow .getAction ())
355+ .thenReturn (new Flow .Action .RequestBlockingAction (statusCode , BlockingContentType .AUTO ));
356+ when (cb .apply (any (), any ())).thenReturn (flow );
357+ return cb ;
358+ }
359+
186360 private static final class TestServletOutputStream extends ServletOutputStream {
187361 private final ByteArrayOutputStream buffer = new ByteArrayOutputStream ();
188362
0 commit comments