Skip to content

Commit 5eb7223

Browse files
committed
test(customer-service): add unit tests for introspector & schema customizer
chore(codecov): update config and add module flags
1 parent 1d3f387 commit 5eb7223

File tree

3 files changed

+243
-1
lines changed

3 files changed

+243
-1
lines changed

codecov.yml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,16 @@ comment:
1313
behavior: default
1414

1515
ignore:
16+
- "customer-service-client/target/**"
17+
- "customer-service/target/**"
18+
- "**/target/**"
19+
- "**/generated-sources/**"
1620
- "**/generated/**"
17-
- "**/openapi/**"
21+
22+
flags:
23+
customer-service:
24+
paths:
25+
- "customer-service/"
26+
customer-service-client:
27+
paths:
28+
- "customer-service-client/"
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package io.github.bsayli.customerservice.common.openapi.autoreg;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
import static org.mockito.Mockito.*;
5+
6+
import io.github.bsayli.customerservice.common.openapi.OpenApiSchemas;
7+
import io.github.bsayli.customerservice.common.openapi.introspector.ResponseTypeIntrospector;
8+
import io.swagger.v3.oas.models.Components;
9+
import io.swagger.v3.oas.models.OpenAPI;
10+
import java.lang.reflect.Method;
11+
import java.util.LinkedHashMap;
12+
import java.util.Map;
13+
import java.util.Optional;
14+
import org.junit.jupiter.api.DisplayName;
15+
import org.junit.jupiter.api.Tag;
16+
import org.junit.jupiter.api.Test;
17+
import org.springdoc.core.customizers.OpenApiCustomizer;
18+
import org.springframework.beans.factory.ListableBeanFactory;
19+
import org.springframework.web.method.HandlerMethod;
20+
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
21+
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
22+
23+
@Tag("unit")
24+
@DisplayName("Unit Test: AutoWrapperSchemaCustomizer")
25+
class AutoWrapperSchemaCustomizerTest {
26+
27+
@Test
28+
@DisplayName("Registers composed wrapper schemas for all discovered data refs")
29+
void registersSchemas_forDiscoveredRefs() throws Exception {
30+
var beanFactory = mock(ListableBeanFactory.class);
31+
var handlerMapping = mock(RequestMappingHandlerMapping.class);
32+
var introspector = mock(ResponseTypeIntrospector.class);
33+
34+
var controller = new SampleController();
35+
Method foo = SampleController.class.getMethod("foo");
36+
Method bar = SampleController.class.getMethod("bar");
37+
38+
var handlerMap = new LinkedHashMap<RequestMappingInfo, HandlerMethod>();
39+
handlerMap.put(mock(RequestMappingInfo.class), new HandlerMethod(controller, foo));
40+
handlerMap.put(mock(RequestMappingInfo.class), new HandlerMethod(controller, bar));
41+
42+
when(handlerMapping.getHandlerMethods()).thenReturn(handlerMap);
43+
when(beanFactory.getBeansOfType(RequestMappingHandlerMapping.class))
44+
.thenReturn(Map.of("rmh", handlerMapping));
45+
46+
when(introspector.extractDataRefName(any(Method.class)))
47+
.then(
48+
inv -> {
49+
Method m = inv.getArgument(0);
50+
return switch (m.getName()) {
51+
case "foo" -> Optional.of("FooRef");
52+
case "bar" -> Optional.of("BarRef");
53+
default -> Optional.empty();
54+
};
55+
});
56+
57+
var customizerCfg = new AutoWrapperSchemaCustomizer(beanFactory, introspector);
58+
OpenApiCustomizer customizer = customizerCfg.autoResponseWrappers();
59+
60+
var openAPI = new OpenAPI().components(new Components());
61+
62+
customizer.customise(openAPI);
63+
64+
var schemas = openAPI.getComponents().getSchemas();
65+
assertNotNull(schemas, "schemas should be initialized");
66+
assertTrue(
67+
schemas.containsKey(OpenApiSchemas.SCHEMA_SERVICE_RESPONSE + "FooRef"),
68+
"Should contain composed schema for FooRef");
69+
assertTrue(
70+
schemas.containsKey(OpenApiSchemas.SCHEMA_SERVICE_RESPONSE + "BarRef"),
71+
"Should contain composed schema for BarRef");
72+
}
73+
74+
@Test
75+
@DisplayName("Does nothing when no data refs are discovered")
76+
void noRefs_noSchemasAdded() {
77+
var beanFactory = mock(ListableBeanFactory.class);
78+
var introspector = mock(ResponseTypeIntrospector.class);
79+
80+
when(beanFactory.getBeansOfType(RequestMappingHandlerMapping.class)).thenReturn(Map.of());
81+
82+
var customizerCfg = new AutoWrapperSchemaCustomizer(beanFactory, introspector);
83+
OpenApiCustomizer customizer = customizerCfg.autoResponseWrappers();
84+
85+
var openAPI = new OpenAPI().components(new Components());
86+
87+
customizer.customise(openAPI);
88+
89+
assertTrue(
90+
openAPI.getComponents().getSchemas() == null
91+
|| openAPI.getComponents().getSchemas().isEmpty(),
92+
"No schemas should be added when no refs exist");
93+
}
94+
95+
static class SampleController {
96+
public String foo() {
97+
return "ok";
98+
}
99+
100+
public Integer bar() {
101+
return 42;
102+
}
103+
}
104+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package io.github.bsayli.customerservice.common.openapi.introspector;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
5+
import io.github.bsayli.customerservice.common.api.response.ServiceResponse;
6+
import java.lang.reflect.Method;
7+
import java.util.Optional;
8+
import java.util.concurrent.CompletableFuture;
9+
import java.util.concurrent.CompletionStage;
10+
import org.junit.jupiter.api.DisplayName;
11+
import org.junit.jupiter.api.Tag;
12+
import org.junit.jupiter.api.Test;
13+
import org.springframework.http.ResponseEntity;
14+
import org.springframework.web.context.request.async.DeferredResult;
15+
import org.springframework.web.context.request.async.WebAsyncTask;
16+
17+
@Tag("unit")
18+
@DisplayName("Unit Test: ResponseTypeIntrospector")
19+
class ResponseTypeIntrospectorTest {
20+
21+
private final ResponseTypeIntrospector introspector = new ResponseTypeIntrospector();
22+
23+
private Method method(String name) throws Exception {
24+
for (Method m : Samples.class.getDeclaredMethods()) {
25+
if (m.getName().equals(name)) return m;
26+
}
27+
throw new NoSuchMethodException(name);
28+
}
29+
30+
@Test
31+
@DisplayName("Returns T simple name for plain ServiceResponse<T>")
32+
void plain_serviceResponse() throws Exception {
33+
Optional<String> ref = introspector.extractDataRefName(method("plain"));
34+
assertEquals(Optional.of("Foo"), ref);
35+
}
36+
37+
@Test
38+
@DisplayName("Unwraps ResponseEntity<ServiceResponse<T>> to T")
39+
void responseEntity_wrapper() throws Exception {
40+
assertEquals(Optional.of("Foo"), introspector.extractDataRefName(method("responseEntity")));
41+
}
42+
43+
@Test
44+
@DisplayName("Unwraps CompletionStage<ServiceResponse<T>> to T")
45+
void completionStage_wrapper() throws Exception {
46+
assertEquals(Optional.of("Foo"), introspector.extractDataRefName(method("completionStage")));
47+
}
48+
49+
@Test
50+
@DisplayName("Unwraps CompletableFuture<ServiceResponse<T>> to T")
51+
void future_wrapper() throws Exception {
52+
assertEquals(Optional.of("Foo"), introspector.extractDataRefName(method("future")));
53+
}
54+
55+
@Test
56+
@DisplayName("Unwraps DeferredResult<ServiceResponse<T>> to T")
57+
void deferredResult_wrapper() throws Exception {
58+
assertEquals(Optional.of("Foo"), introspector.extractDataRefName(method("deferredResult")));
59+
}
60+
61+
@Test
62+
@DisplayName("Unwraps WebAsyncTask<ServiceResponse<T>> to T")
63+
void webAsyncTask_wrapper() throws Exception {
64+
assertEquals(Optional.of("Foo"), introspector.extractDataRefName(method("webAsyncTask")));
65+
}
66+
67+
@Test
68+
@DisplayName("Unwraps deeply nested wrappers down to ServiceResponse<T>")
69+
void deepNesting_unwraps() throws Exception {
70+
assertEquals(Optional.of("Bar"), introspector.extractDataRefName(method("deepNesting")));
71+
}
72+
73+
@Test
74+
@DisplayName("Returns empty for raw ServiceResponse (no generic parameter)")
75+
void rawServiceResponse_empty() throws Exception {
76+
assertTrue(introspector.extractDataRefName(method("rawServiceResponse")).isEmpty());
77+
}
78+
79+
@Test
80+
@DisplayName("Returns empty when return type is not a wrapper")
81+
void notAWrapper_empty() throws Exception {
82+
assertTrue(introspector.extractDataRefName(method("notAWrapper")).isEmpty());
83+
}
84+
85+
static class Samples {
86+
public ServiceResponse<Foo> plain() {
87+
return null;
88+
}
89+
90+
public ResponseEntity<ServiceResponse<Foo>> responseEntity() {
91+
return null;
92+
}
93+
94+
public CompletionStage<ServiceResponse<Foo>> completionStage() {
95+
return null;
96+
}
97+
98+
public CompletableFuture<ServiceResponse<Foo>> future() {
99+
return null;
100+
}
101+
102+
public DeferredResult<ServiceResponse<Foo>> deferredResult() {
103+
return null;
104+
}
105+
106+
public WebAsyncTask<ServiceResponse<Foo>> webAsyncTask() {
107+
return null;
108+
}
109+
110+
public ResponseEntity<CompletionStage<DeferredResult<ServiceResponse<Bar>>>> deepNesting() {
111+
return null;
112+
}
113+
114+
@SuppressWarnings("rawtypes")
115+
public ServiceResponse rawServiceResponse() {
116+
return null;
117+
}
118+
119+
public Foo notAWrapper() {
120+
return null;
121+
}
122+
123+
static class Foo {}
124+
125+
static class Bar {}
126+
}
127+
}

0 commit comments

Comments
 (0)