Skip to content

Commit 5ac192f

Browse files
committed
Improve test coverage for Catalina valves
Add tests for RemoteCIDRValve and LoadBalancerDrainingValve. Extend tests for ErrorReportValve, RemoteIpValve, SSLValve, and StuckThreadDetectionValve to cover edge cases and properties.
1 parent 7557c61 commit 5ac192f

6 files changed

Lines changed: 595 additions & 5 deletions

File tree

test/org/apache/catalina/valves/TestErrorReportValve.java

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import org.junit.Test;
3333

3434
import org.apache.catalina.Context;
35+
import org.apache.catalina.Valve;
3536
import org.apache.catalina.Wrapper;
3637
import org.apache.catalina.startup.Tomcat;
3738
import org.apache.catalina.startup.TomcatBaseTest;
@@ -263,4 +264,146 @@ public void testErrorPageServlet() throws Exception {
263264
}
264265

265266

267+
@Test
268+
public void testShowReportFalse() throws Exception {
269+
Tomcat tomcat = getTomcatInstance();
270+
271+
Context ctx = getProgrammaticRootContext();
272+
273+
// Must use sendError() — setStatus() alone commits the response before
274+
// ErrorReportValve can write a body, so showReport would have nothing to suppress.
275+
Tomcat.addServlet(ctx, "error404", new HttpServlet() {
276+
private static final long serialVersionUID = 1L;
277+
@Override
278+
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
279+
throws ServletException, IOException {
280+
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
281+
}
282+
});
283+
ctx.addServletMappingDecoded("/", "error404");
284+
285+
tomcat.start();
286+
287+
ErrorReportValve erv = findErrorReportValve(tomcat);
288+
Assert.assertNotNull("ErrorReportValve should exist", erv);
289+
erv.setShowReport(false);
290+
291+
ByteChunk res = new ByteChunk();
292+
res.setCharset(StandardCharsets.UTF_8);
293+
int rc = getUrl("http://localhost:" + getPort(), res, null);
294+
295+
Assert.assertEquals(HttpServletResponse.SC_NOT_FOUND, rc);
296+
String body = res.toString();
297+
Assert.assertNotNull(body);
298+
Assert.assertFalse("Report section should be suppressed when showReport=false",
299+
body.contains(sm.getString("errorReportValve.description")));
300+
Assert.assertFalse(erv.isShowReport());
301+
}
302+
303+
304+
@Test
305+
public void testShowServerInfoFalse() throws Exception {
306+
Tomcat tomcat = getTomcatInstance();
307+
308+
Context ctx = getProgrammaticRootContext();
309+
310+
Tomcat.addServlet(ctx, "error500", new ErrorServlet());
311+
ctx.addServletMappingDecoded("/", "error500");
312+
313+
tomcat.start();
314+
315+
ErrorReportValve erv = findErrorReportValve(tomcat);
316+
Assert.assertNotNull("ErrorReportValve should exist", erv);
317+
erv.setShowServerInfo(false);
318+
319+
ByteChunk res = new ByteChunk();
320+
res.setCharset(StandardCharsets.UTF_8);
321+
getUrl("http://localhost:" + getPort(), res, null);
322+
323+
String body = res.toString();
324+
Assert.assertNotNull(body);
325+
Assert.assertFalse("Server info should be hidden", body.contains("Apache Tomcat"));
326+
Assert.assertFalse(erv.isShowServerInfo());
327+
}
328+
329+
330+
@Test
331+
public void testSetPropertyErrorCode() {
332+
ErrorReportValve valve = new ErrorReportValve();
333+
334+
Assert.assertTrue(valve.setProperty("errorCode.404", "/error404.html"));
335+
Assert.assertEquals("/error404.html", valve.getProperty("errorCode.404"));
336+
}
337+
338+
339+
@Test
340+
public void testSetPropertyExceptionType() {
341+
ErrorReportValve valve = new ErrorReportValve();
342+
343+
Assert.assertTrue(valve.setProperty(
344+
"exceptionType.java.lang.NullPointerException", "/npe.html"));
345+
Assert.assertEquals("/npe.html", valve.getProperty(
346+
"exceptionType.java.lang.NullPointerException"));
347+
}
348+
349+
350+
@Test
351+
public void testGetPropertyNotFound() {
352+
ErrorReportValve valve = new ErrorReportValve();
353+
354+
Assert.assertNull(valve.getProperty("errorCode.999"));
355+
Assert.assertNull(valve.getProperty("exceptionType.com.example.Nope"));
356+
}
357+
358+
359+
@Test
360+
public void testGetPropertyUnknownPrefix() {
361+
ErrorReportValve valve = new ErrorReportValve();
362+
363+
Assert.assertFalse(valve.setProperty("unknownPrefix.something", "/x.html"));
364+
Assert.assertNull(valve.getProperty("unknownPrefix.something"));
365+
}
366+
367+
368+
@Test
369+
public void testExceptionWithRootCause() throws Exception {
370+
Tomcat tomcat = getTomcatInstance();
371+
372+
Context ctx = getProgrammaticRootContext();
373+
374+
Tomcat.addServlet(ctx, "nestedError", new HttpServlet() {
375+
private static final long serialVersionUID = 1L;
376+
@Override
377+
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
378+
throws ServletException, IOException {
379+
Throwable root = new IllegalStateException("root cause");
380+
Throwable wrapper = new RuntimeException("wrapper", root);
381+
req.setAttribute(RequestDispatcher.ERROR_EXCEPTION, wrapper);
382+
resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
383+
}
384+
});
385+
ctx.addServletMappingDecoded("/", "nestedError");
386+
387+
tomcat.start();
388+
389+
ByteChunk res = new ByteChunk();
390+
res.setCharset(StandardCharsets.UTF_8);
391+
int rc = getUrl("http://localhost:" + getPort(), res, null);
392+
393+
Assert.assertEquals(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, rc);
394+
String body = res.toString();
395+
Assert.assertNotNull(body);
396+
// Should contain root cause section
397+
Assert.assertTrue(body.contains(sm.getString("errorReportValve.rootCause")));
398+
}
399+
400+
401+
private static ErrorReportValve findErrorReportValve(Tomcat tomcat) {
402+
for (Valve v : tomcat.getHost().getPipeline().getValves()) {
403+
if (v instanceof ErrorReportValve) {
404+
return (ErrorReportValve) v;
405+
}
406+
}
407+
return null;
408+
}
266409
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.catalina.valves;
18+
19+
import java.lang.reflect.Method;
20+
21+
import jakarta.servlet.ServletContext;
22+
23+
import org.junit.Assert;
24+
import org.junit.Test;
25+
26+
import org.apache.catalina.Context;
27+
import org.apache.catalina.Valve;
28+
import org.apache.catalina.connector.Request;
29+
import org.apache.catalina.connector.Response;
30+
import org.apache.catalina.core.StandardPipeline;
31+
import org.easymock.EasyMock;
32+
import org.easymock.IMocksControl;
33+
34+
/**
35+
* Unit tests for {@link LoadBalancerDrainingValve} covering URI manipulation behaviours
36+
* not covered by the parameterised integration test.
37+
*/
38+
public class TestLoadBalancerDrainingValveUnit {
39+
40+
/**
41+
* collapseLeadingSlashes() is the private static helper that normalises URIs
42+
* before issuing the redirect. Test it directly via reflection.
43+
*/
44+
@Test
45+
public void testCollapseMultipleSlashesInURI() throws Exception {
46+
Method collapse = LoadBalancerDrainingValve.class
47+
.getDeclaredMethod("collapseLeadingSlashes", String.class);
48+
collapse.setAccessible(true);
49+
50+
LoadBalancerDrainingValve valve = new LoadBalancerDrainingValve();
51+
52+
// Multiple leading slashes are collapsed to one
53+
Assert.assertEquals("/test", collapse.invoke(valve, "///test"));
54+
Assert.assertEquals("/test/path", collapse.invoke(valve, "//test/path"));
55+
56+
// A URI consisting entirely of slashes becomes a single slash
57+
Assert.assertEquals("/", collapse.invoke(valve, "///"));
58+
59+
// URIs that need no change are returned as-is
60+
Assert.assertEquals("/test", collapse.invoke(valve, "/test"));
61+
Assert.assertEquals("test", collapse.invoke(valve, "test"));
62+
}
63+
64+
65+
/**
66+
* When JK_LB_ACTIVATION=DIS and the session is invalid, the valve must
67+
* redirect the client after stripping the ;jsessionid parameter from the URI
68+
* so that the load-balancer can assign the client to a healthy node.
69+
*/
70+
@Test
71+
public void testSessionURIParamStripped() throws Exception {
72+
IMocksControl control = EasyMock.createControl();
73+
ServletContext servletContext = control.createMock(ServletContext.class);
74+
Context ctx = control.createMock(Context.class);
75+
Request request = control.createMock(Request.class);
76+
Response response = control.createMock(Response.class);
77+
78+
// Minimal context stubs required for valve initialisation
79+
EasyMock.expect(ctx.getMBeanKeyProperties()).andStubReturn("");
80+
EasyMock.expect(ctx.getName()).andStubReturn("");
81+
EasyMock.expect(ctx.getPipeline()).andStubReturn(new StandardPipeline());
82+
EasyMock.expect(ctx.getDomain()).andStubReturn("foo");
83+
EasyMock.expect(ctx.getLogger())
84+
.andStubReturn(org.apache.juli.logging.LogFactory.getLog(LoadBalancerDrainingValve.class));
85+
EasyMock.expect(ctx.getServletContext()).andStubReturn(servletContext);
86+
87+
// Simulate a disabled node with an invalid session
88+
EasyMock.expect(request.getAttribute(LoadBalancerDrainingValve.ATTRIBUTE_KEY_JK_LB_ACTIVATION))
89+
.andStubReturn("DIS");
90+
EasyMock.expect(Boolean.valueOf(request.isRequestedSessionIdValid()))
91+
.andStubReturn(Boolean.FALSE);
92+
93+
// No cookies present — session cookie deletion path is not exercised here
94+
EasyMock.expect(request.getCookies()).andStubReturn(null);
95+
EasyMock.expect(request.getContext()).andStubReturn(ctx);
96+
// SessionConfig.getSessionCookieName / getSessionUriParamName both key off
97+
// getSessionCookieName(); returning "jsessionid" short-circuits both lookups.
98+
EasyMock.expect(ctx.getSessionCookieName()).andStubReturn("jsessionid");
99+
100+
// URI carries a jsessionid path parameter that the valve must strip
101+
EasyMock.expect(request.getRequestURI()).andStubReturn("/test;jsessionid=abc123");
102+
EasyMock.expect(request.getQueryString()).andStubReturn(null);
103+
104+
// The valve must redirect to the clean URI — jsessionid removed
105+
response.sendRedirect("/test", 307);
106+
EasyMock.expectLastCall();
107+
108+
Valve next = control.createMock(Valve.class);
109+
// next.invoke must NOT be called; the valve redirects instead
110+
111+
control.replay();
112+
113+
LoadBalancerDrainingValve valve = new LoadBalancerDrainingValve();
114+
valve.setContainer(ctx);
115+
valve.init();
116+
valve.setNext(next);
117+
valve.invoke(request, response);
118+
119+
control.verify();
120+
}
121+
}

0 commit comments

Comments
 (0)