Skip to content

Commit 4ca2b4f

Browse files
committed
build: unit tests for pac4j, vertx, pebble, hibernate-validator and problem handler core
1 parent b69969a commit 4ca2b4f

27 files changed

Lines changed: 2736 additions & 82 deletions

File tree

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.problem;
7+
8+
import static org.junit.jupiter.api.Assertions.*;
9+
import static org.mockito.ArgumentMatchers.any;
10+
import static org.mockito.Mockito.*;
11+
12+
import java.net.URI;
13+
import java.util.List;
14+
import java.util.Map;
15+
16+
import org.junit.jupiter.api.BeforeEach;
17+
import org.junit.jupiter.api.DisplayName;
18+
import org.junit.jupiter.api.Test;
19+
import org.mockito.ArgumentCaptor;
20+
import org.slf4j.Logger;
21+
22+
import com.typesafe.config.Config;
23+
import io.jooby.*;
24+
import io.jooby.exception.NotAcceptableException;
25+
26+
class ProblemDetailsHandlerTest {
27+
28+
private ProblemDetailsHandler handler;
29+
private Context ctx;
30+
private Router router;
31+
private Logger log;
32+
33+
@BeforeEach
34+
void setUp() {
35+
handler = new ProblemDetailsHandler();
36+
ctx = mock(Context.class);
37+
router = mock(Router.class);
38+
log = mock(Logger.class);
39+
40+
when(ctx.getRouter()).thenReturn(router);
41+
when(router.getLog()).thenReturn(log);
42+
43+
// Mock these to prevent DefaultErrorHandler from crashing
44+
when(ctx.getMethod()).thenReturn("GET");
45+
when(ctx.getRequestPath()).thenReturn("/test");
46+
when(ctx.getRequestType(any())).thenReturn(MediaType.json);
47+
48+
// Default accept behavior
49+
when(ctx.accept(anyList())).thenReturn(MediaType.html);
50+
when(ctx.setResponseType(any(MediaType.class))).thenReturn(ctx);
51+
when(ctx.setResponseCode(anyInt())).thenReturn(ctx);
52+
when(ctx.setResponseCode(any(StatusCode.class))).thenReturn(ctx);
53+
}
54+
55+
@Test
56+
@DisplayName("Verify static from(Config) parses full configuration")
57+
void testFromConfig() {
58+
Config conf = mock(Config.class);
59+
Config problemConfig = mock(Config.class);
60+
61+
when(conf.hasPath(ProblemDetailsHandler.ROOT_CONFIG_PATH)).thenReturn(true);
62+
when(conf.getConfig(ProblemDetailsHandler.ROOT_CONFIG_PATH)).thenReturn(problemConfig);
63+
64+
when(problemConfig.hasPath("log4xxErrors")).thenReturn(true);
65+
when(problemConfig.getBoolean("log4xxErrors")).thenReturn(true);
66+
67+
when(problemConfig.hasPath("muteCodes")).thenReturn(true);
68+
when(problemConfig.getIntList("muteCodes")).thenReturn(List.of(404));
69+
70+
when(problemConfig.hasPath("muteTypes")).thenReturn(true);
71+
when(problemConfig.getStringList("muteTypes"))
72+
.thenReturn(List.of("java.lang.IllegalArgumentException"));
73+
74+
ProblemDetailsHandler result = ProblemDetailsHandler.from(conf);
75+
assertNotNull(result);
76+
}
77+
78+
@Test
79+
@DisplayName("Verify NotAcceptableException triggers immediate HTML response")
80+
void testApplyNotAcceptable() {
81+
NotAcceptableException ex = new NotAcceptableException("No match");
82+
handler.apply(ctx, ex, StatusCode.NOT_ACCEPTABLE);
83+
84+
verify(ctx).setResponseType(MediaType.html);
85+
verify(ctx).send(contains("<h1>Not Acceptable</h1>"));
86+
}
87+
88+
@Test
89+
@DisplayName("Verify JSON content negotiation and problem response mapping")
90+
@SuppressWarnings("unchecked")
91+
void testApplyJsonProblem() {
92+
when(ctx.accept(anyList())).thenReturn(MediaType.json);
93+
IllegalArgumentException ex = new IllegalArgumentException("Invalid ID");
94+
95+
handler.apply(ctx, ex, StatusCode.BAD_REQUEST);
96+
97+
verify(ctx).setResponseType(MediaType.PROBLEM_JSON);
98+
ArgumentCaptor<Object> resultCaptor = ArgumentCaptor.forClass(Object.class);
99+
verify(ctx).render(resultCaptor.capture());
100+
101+
Map<String, Object> map = (Map<String, Object>) resultCaptor.getValue();
102+
assertEquals("Bad Request", map.get("title"));
103+
assertEquals("Invalid ID", map.get("detail"));
104+
}
105+
106+
@Test
107+
@DisplayName("Verify XML content negotiation")
108+
void testApplyXmlProblem() {
109+
when(ctx.accept(anyList())).thenReturn(MediaType.xml);
110+
handler.apply(ctx, new Exception("XML Error"), StatusCode.BAD_REQUEST);
111+
112+
verify(ctx).setResponseType(MediaType.PROBLEM_XML);
113+
verify(ctx).render(any());
114+
}
115+
116+
@Test
117+
@DisplayName("Verify Plain Text content negotiation")
118+
void testApplyTextProblem() {
119+
when(ctx.accept(anyList())).thenReturn(MediaType.text);
120+
handler.apply(ctx, new Exception("Text Error"), StatusCode.BAD_REQUEST);
121+
122+
verify(ctx).setResponseType(MediaType.text);
123+
verify(ctx).send(contains("title='Bad Request'"));
124+
verify(ctx).send(contains("Text Error"));
125+
}
126+
127+
@Test
128+
@DisplayName("Verify internal error fallback when render fails")
129+
void testApplyRenderFailureFallback() {
130+
when(ctx.accept(anyList())).thenReturn(MediaType.json);
131+
doThrow(new NotAcceptableException("foo")).when(ctx).render(any());
132+
133+
handler.apply(ctx, new Exception("fail"), StatusCode.BAD_REQUEST);
134+
135+
verify(ctx, atLeastOnce()).setResponseType(MediaType.html);
136+
}
137+
138+
@Test
139+
@DisplayName("Evaluate problem: HttpProblem instance")
140+
void testEvaluateHttpProblem() {
141+
HttpProblem problem = HttpProblem.valueOf(StatusCode.FORBIDDEN, "Forbidden", "Access Denied");
142+
handler.apply(ctx, problem, StatusCode.FORBIDDEN);
143+
144+
// apply() calls setResponseCode, and fallback sendHtml() also calls setResponseCode
145+
verify(ctx, atLeastOnce()).setResponseCode(403);
146+
verify(ctx).send(contains("Access Denied"));
147+
}
148+
149+
@Test
150+
@DisplayName("Evaluate problem: 500 error mapping")
151+
void testEvaluateInternalError() {
152+
handler.apply(ctx, new RuntimeException("boom"), StatusCode.SERVER_ERROR);
153+
verify(ctx).send(contains("Server Error"));
154+
}
155+
156+
@Test
157+
@DisplayName("Evaluate problem: message multi-line splitting")
158+
void testEvaluateMessageSplitting() {
159+
handler.apply(ctx, new Exception("First Line\nSecond Line"), StatusCode.BAD_REQUEST);
160+
verify(ctx).send(contains("First Line"));
161+
}
162+
163+
@Test
164+
@DisplayName("Logging: Server Error (Error level)")
165+
void testLogServerError() {
166+
handler.apply(ctx, new Exception("critical"), StatusCode.SERVER_ERROR);
167+
verify(log, atLeastOnce()).error(anyString(), any(Throwable.class));
168+
}
169+
170+
@Test
171+
@DisplayName("Logging: 4xx Errors (Info/Debug level)")
172+
void testLog4xxError() {
173+
handler.log4xxErrors();
174+
when(log.isDebugEnabled()).thenReturn(false);
175+
176+
handler.apply(ctx, new Exception("client error"), StatusCode.BAD_REQUEST);
177+
verify(log).info(anyString());
178+
179+
reset(log);
180+
when(log.isDebugEnabled()).thenReturn(true);
181+
handler.apply(ctx, new Exception("client error debug"), StatusCode.BAD_REQUEST);
182+
verify(log).debug(anyString(), any(Throwable.class));
183+
}
184+
185+
// Dummy exception class that correctly extends Throwable to satisfy the type constraints
186+
private static class MappableException extends RuntimeException implements HttpProblemMappable {
187+
private final HttpProblem problem;
188+
189+
public MappableException(HttpProblem problem) {
190+
this.problem = problem;
191+
}
192+
193+
@Override
194+
public HttpProblem toHttpProblem() {
195+
return problem;
196+
}
197+
}
198+
199+
@Test
200+
@DisplayName("HTML Generation: verify extra details (instance, params, errors)")
201+
void testHtmlExtraDetails() {
202+
HttpProblem problem =
203+
HttpProblem.builder()
204+
.status(StatusCode.BAD_REQUEST)
205+
.title("Bad Request")
206+
.instance(URI.create("/path"))
207+
.detail("Some details")
208+
.param("p1", "v1")
209+
.errors(List.of(new HttpProblem.Error("err1", "err1 detail")))
210+
.build();
211+
212+
// Use our custom dummy exception to pass the Type constraints
213+
MappableException mappableException = new MappableException(problem);
214+
215+
handler.apply(ctx, mappableException, StatusCode.BAD_REQUEST);
216+
217+
verify(ctx)
218+
.send(
219+
argThat(
220+
(String s) ->
221+
s.contains("instance: /path")
222+
&& s.contains("detail: Some details")
223+
&& s.contains("parameters: {p1=v1}")
224+
// Match the class name since toString() prints the object reference
225+
&& s.contains("errors: [io.jooby.problem.HttpProblem$Error@")));
226+
}
227+
228+
@Test
229+
@DisplayName("Verify apply handles unexpected internal exceptions silently")
230+
void testApplyUnexpectedException() {
231+
when(ctx.accept(anyList())).thenThrow(new RuntimeException("Negotiation Crash"));
232+
233+
assertDoesNotThrow(() -> handler.apply(ctx, new Exception("original"), StatusCode.BAD_REQUEST));
234+
verify(log).error(contains("Unexpected error"), any(Throwable.class));
235+
}
236+
}

modules/jooby-hibernate-validator/pom.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,15 @@
4949
<version>${jooby.version}</version>
5050
<scope>test</scope>
5151
</dependency>
52+
<dependency>
53+
<groupId>org.mockito</groupId>
54+
<artifactId>mockito-core</artifactId>
55+
<scope>test</scope>
56+
</dependency>
57+
<dependency>
58+
<groupId>org.mockito</groupId>
59+
<artifactId>mockito-junit-jupiter</artifactId>
60+
<scope>test</scope>
61+
</dependency>
5262
</dependencies>
5363
</project>

modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/HibernateValidatorModule.java

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010
import java.util.ArrayList;
1111
import java.util.List;
12-
import java.util.function.Consumer;
1312

1413
import org.hibernate.validator.HibernateValidator;
1514
import org.hibernate.validator.HibernateValidatorConfiguration;
@@ -26,15 +25,15 @@
2625
*
2726
* <pre>{@code
2827
* {
29-
* install(new HibernateValidatorModule());
28+
* install(new HibernateValidatorModule());
3029
*
3130
* }
3231
*
3332
* public class Controller {
3433
*
35-
* @POST("/create")
36-
* public void create(@Valid Bean bean) {
37-
* }
34+
* @POST("/create")
35+
* public void create(@Valid Bean bean) {
36+
* }
3837
*
3938
* }
4039
* }</pre>
@@ -53,8 +52,6 @@
5352
*/
5453
public class HibernateValidatorModule implements Extension {
5554
private static final String CONFIG_ROOT_PATH = "hibernate.validator";
56-
// TODO: remove it on next major
57-
private Consumer<HibernateValidatorConfiguration> configurer;
5855
private StatusCode statusCode = StatusCode.UNPROCESSABLE_ENTITY;
5956
private String title = "Validation failed";
6057
private boolean disableDefaultViolationHandler = false;
@@ -153,24 +150,23 @@ public void install(Jooby app) throws Exception {
153150
this.factories.clear();
154151
}
155152
configuration.constraintValidatorFactory(delegateFactory);
156-
if (configurer != null) {
157-
configurer.accept(configuration);
158-
}
159153
var services = app.getServices();
160-
try (var factory = configuration.buildValidatorFactory()) {
161-
var validator = factory.getValidator();
162-
services.put(Validator.class, validator);
163-
services.put(BeanValidator.class, new BeanValidatorImpl(validator));
164-
// Allow to access validator factory so hibernate can access later
165-
var constraintValidatorFactory = factory.getConstraintValidatorFactory();
166-
services.put(ConstraintValidatorFactory.class, constraintValidatorFactory);
167-
168-
if (!disableDefaultViolationHandler) {
169-
app.error(
170-
ConstraintViolationException.class,
171-
new ConstraintViolationHandler(
172-
statusCode, title, logException, app.problemDetailsIsEnabled()));
173-
}
154+
155+
var factory = configuration.buildValidatorFactory();
156+
app.onStop(factory);
157+
158+
var validator = factory.getValidator();
159+
services.put(Validator.class, validator);
160+
services.put(BeanValidator.class, new BeanValidatorImpl(validator));
161+
// Allow to access validator factory so hibernate can access later
162+
var constraintValidatorFactory = factory.getConstraintValidatorFactory();
163+
services.put(ConstraintValidatorFactory.class, constraintValidatorFactory);
164+
165+
if (!disableDefaultViolationHandler) {
166+
app.error(
167+
ConstraintViolationException.class,
168+
new ConstraintViolationHandler(
169+
statusCode, title, logException, app.problemDetailsIsEnabled()));
174170
}
175171
}
176172

0 commit comments

Comments
 (0)