Custom-Webclient Spock test throws unwanted NullPointerException in GET call - java

Using the custom WebClient below:
#Slf4j
#RequiredArgsConstructor
#Component
public class TransitApiClient {
private final TransitApiClientProperties transitApiClientProperties;
private final WebClient transitApiWebClient;
private final OAuth2CustomClient oAuth2CustomClient;
public ResponseEntity<Void> isOfficeOfTransitValidAndNational(String officeId){
try {
final String url = UriComponentsBuilder.fromUriString(transitApiClientProperties.getFindOfficeOfTransit())
.queryParam("codelistKey", "CL173")
.queryParam("itemCode", officeId)
.build()
.toUriString();
return transitApiWebClient.get()
.uri(url)
.header(AUTHORIZATION, getAccessTokenHeaderValue(oAuth2CustomClient.getJwtToken()))
.retrieve()
.onStatus(status -> status.value() == HttpStatus.NO_CONTENT.value(),
clientResponse -> Mono.error( new InvalidOfficeException(null,
"Invalid Office exception occurred while invoking :" + transitApiClientProperties.getFindOfficeOfTransit() + officeId)))
.toBodilessEntity()
.block();
} catch (WebClientResponseException webClientResponseException) {
log.error("Technical exception occurred while invoking :" + transitApiClientProperties.getFindOfficeOfTransit(), webClientResponseException);
throw new TechnicalErrorException(null, "Technical exception occurred while trying to find " + transitApiClientProperties.getFindOfficeOfTransit(), webClientResponseException);
}
}
with its intended usage to hit an endpoint, and check if it returns a 200 code with a body or 204 NoContent code, and react accordingly with some custom exceptions.
I've implemented the groovy-spock test below :
class TransitApiClientSpec extends Specification {
private WebClient transitApiWebClient
private TransitApiClient transitApiClient
private OAuth2CustomClient oAuth2CustomClient
private TransitApiClientProperties transitApiClientProperties
private RequestBodyUriSpec requestBodyUriSpec
private RequestHeadersSpec requestHeadersSpec
private RequestBodySpec requestBodySpec
private ResponseSpec responseSpec
private RequestHeadersUriSpec requestHeadersUriSpec
def setup() {
transitApiClientProperties = new TransitApiClientProperties()
transitApiClientProperties.setServiceUrl("https://test-url")
transitApiClientProperties.setFindOfficeOfTransit("/transit?")
transitApiClientProperties.setUsername("username")
transitApiClientProperties.setPassword("password")
transitApiClientProperties.setAuthorizationGrantType("grantType")
transitApiClientProperties.setClientId("clientId")
transitApiClientProperties.setClientSecret("clientSecret")
oAuth2CustomClient = Stub(OAuth2CustomClient)
oAuth2CustomClient.getJwtToken() >> "token"
transitApiWebClient = Mock(WebClient)
requestHeadersSpec = Mock(RequestHeadersSpec)
responseSpec = Mock(ResponseSpec)
requestHeadersUriSpec = Mock(RequestHeadersUriSpec)
transitApiClient = new TransitApiClient(transitApiClientProperties, transitApiWebClient, oAuth2CustomClient)
}
def "request validation of OoTra and throw InvalidOfficeException"(){
given :
def officeId = "testId"
def uri = UriComponentsBuilder
.fromUriString(transitApiClientProperties.getFindOfficeOfTransit())
.queryParam("codelistKey", "CL173")
.queryParam("itemCode", officeId)
.build()
.toUriString()
1 * transitApiWebClient.get() >> requestHeadersUriSpec
1 * requestHeadersUriSpec.uri(uri) >> requestHeadersSpec
1 * requestHeadersSpec.header(HttpHeaders.AUTHORIZATION, "Bearer token") >> requestHeadersSpec
1 * requestHeadersSpec.retrieve() >> responseSpec
1 * responseSpec.onStatus() >> Mono.error( new InvalidOfficeException(null,null) )
when :
def response = transitApiClient.isOfficeOfTransitValidAndNational(officeId)
then :
thrown(InvalidOfficeException)
}
But instead of an InvalidOfficeException being thrown, a java.lang.NullPointerException is thrown.
It seems to be triggered when during the test run, the program calls the following :
return transitApiWebClient.get()
.uri(url)
.header(AUTHORIZATION, getAccessTokenHeaderValue(oAuth2CustomClient.getJwtToken()))
.retrieve()
.onStatus(status -> status.value() == HttpStatus.NO_CONTENT.value(),
clientResponse -> Mono.error( new InvalidOfficeException(null,
"Invalid Office exception occurred while invoking :" + transitApiClientProperties.getFindOfficeOfTransit() + officeId)))
.toBodilessEntity() <---------------------- **HERE**
.block();
I understand that I haven't mocked its behavior but seems to me that some other mock hasn't been done correctly.

I can only recommend not to mock WebClient calls, as the necessary steps are a pain to mock, as you have seen yourself, requiring a lot of intermediary mocks without actually adding much value. This basically repeats the implementation, thus locking it in, which is not a good thing.
What I usually do is to extract all code that interacts with WebClient into a client class, and only mock this class interactions in my code. From the looks of it this is what you are already doing with TransitApiClient. For these client classes, I would recommend testing them with MockServer, WireMock, or any of the other frameworks. This way you actually make sure that the correct request/responses are sent/received, and you don't have to awkwardly deal with the WebClient interface.

Related

CSRF in Tests stopped working with Spring Boot 3 and Spring Security 6

We got the problem described here and used the solution linked there and provided by Spring.
The solution fixes our end-to-end using WebClient and a filter function, but unfortunately introduces Problems with our REST-controller unit tests, that stop working.
The relevant cecurity configuration using the documented answer:
// Enable CSRF security
http.csrf { csrfConfigurer ->
// see https://docs.spring.io/spring-security/reference/5.8/migration/servlet/exploits.html#_i_am_using_angularjs_or_another_javascript_framework
val tokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse()
val delegate = XorCsrfTokenRequestAttributeHandler()
// set the name of the attribute the CsrfToken will be populated on
delegate.setCsrfRequestAttributeName("_csrf")
// Use only the handle() method of XorCsrfTokenRequestAttributeHandler and the
// default implementation of resolveCsrfTokenValue() from CsrfTokenRequestHandler
val requestHandler = CsrfTokenRequestHandler(delegate::handle)
csrfConfigurer.csrfTokenRepository(tokenRepository)
csrfConfigurer.csrfTokenRequestHandler(requestHandler)
}
This configuration fixes our end-to-end-tests using a WebClient with the FilterFunction:
override fun filter(request: ClientRequest, next: ExchangeFunction): Mono<ClientResponse> =
next.exchange(request)
.flatMap { response: ClientResponse ->
if (response.statusCode().is4xxClientError) {
val csrfCookie = response.cookies().getFirst("XSRF-TOKEN")
if (csrfCookie != null) {
val retryRequest: ClientRequest = ClientRequest.from(request)
.headers { httpHeaders -> httpHeaders.set("X-XSRF-TOKEN", csrfCookie.value) }
.cookies { cookies -> cookies.add("XSRF-TOKEN", csrfCookie.value) }
.build()
return#flatMap next.exchange(retryRequest)
}
}
Mono.just(response)
}
The failing tests look like this:
#Test
fun `create tender with copyFrom null should succeed and return 201 and the uuid`() {
mockMvc
.perform(
post("/api/my/endpoint")
.param("title", "Angebot 1")
.param("copyFrom", null)
.with(user(tendererTestUsers[0]))
.with(csrf())
)
.andExpectAll(
status().isCreated,
content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON),
jsonPath("$", `is`(notNullValue()))
)
Debugging leads to a mismatching CSRF-Token provided as shown in this screenshot. Most likely we need to update the test's csrf configuration; anyone any clue on how to do that?
Using the pre-Spring-Boot-3 CSRF security configuration makes the mock-Tests work again, but then the end-to-end-tests fail...

Why is spring returning me an empty llist?

I dont seem to know why Spring is returning me an empty list enough I have passed in a JSON.stringify() string from reactJS
This is my code for reactJS
postData(item){
console.log(item)
fetch("http://localhost:8080/addSuspect", {
"method": "POST",
"headers": {
"content-type": "application/json"
},
"body": item
})
.then(response => {
console.log(response);
})
.catch(err => {
console.log(err);
});
}
uploadFile(event) {
let file
let file2
//Check if the movements andsuspected case profiles are uploaded
if(event.target.files.length !== 2){
this.setState({error:true, errorMsg:"You need to upload at least 2 files!"})
return
}
//Check if the file is the correct file
console.log("Files:")
for (var i=0, l=event.target.files.length; i<l; i++) {
console.log(event.target.files[i].name);
if (event.target.files[i].name.includes("_suspected")){
file = event.target.files[i]
}
else if (event.target.files[i].name.includes("_movements")){
file2 = event.target.files[i]
}
else{
this.setState({error:true, errorMsg:"You have uploaded invalid files! Please rename the files to <filename>_suspected (For suspected cases) or <filename>_movement (For suspected case movement)"})
return
}
}
//Reads the first file (Suspected profile)
if (file) {
const reader = new FileReader();
reader.onload = () => {
// Use reader.result
const lols = Papa.parse(reader.result, {header: true, skipEmptyLines: true}, )
console.log(lols.data)
// Posting csv data into db
// this.postData('"' + JSON.stringify(lols.data) + '"')
this.postData(JSON.stringify(lols.data))
// Adds names into dropdown
this.setState({dataList: ["None", ...lols.data.map(names => names.firstName + " " + names.lastName)]})
const data = lols.data
this.setState({suspectCases: data})
}
reader.readAsText(file)
}
}
Here is what I get from console.log():
[{"id":"5","firstName":"Bernadene","lastName":"Earey","email":"bearey4#huffingtonpost.com","gender":"Female","homeLongtitude":"","homeLatitude":"","homeShortaddress":"","homePostalcode":"552209","maritalStatus":"M","phoneNumber":"92568768","company":"Yadel","companyLongtitude":"","companyLatitude":""},{"id":"14","firstName":"Mada","lastName":"Lafaye","email":"mlafayed#gravatar.com","gender":"Female","homeLongtitude":"","homeLatitude":"","homeShortaddress":"","homePostalcode":"447136","maritalStatus":"M","phoneNumber":"85769345","company":"Eare","companyLongtitude":"","companyLatitude":""}]
Below shows the Code in my Spring Controller
#RestController
public class HomeController {
private final profileMapper profileMapper;
private final suspectedMapper suspectedMapper;
public HomeController(#Autowired profileMapper profileMapper, #Autowired suspectedMapper suspectedMapper) {
this.profileMapper = profileMapper;
this.suspectedMapper = suspectedMapper;
}
#GetMapping("/listAllPeopleProfiles")
//Removes the CORS error
#CrossOrigin(origins = "http://localhost:3000")
private Iterable<Peopleprofile> getAllPeopleProfiles (){
return profileMapper.findAllPeopleProfile();
}
#GetMapping("/listAllSuspectedCases")
#CrossOrigin(origins = "http://localhost:3000")
private Iterable<Suspected> getAllSuspected(){
return suspectedMapper.findallSuspected();
}
#PostMapping("/addSuspect")
#CrossOrigin(origins = "http://localhost:3000")
private void newSuspectedcases(ArrayList<Suspected> unformattedcases){
// try {
// final JSONObject obj = new JSONObject(unformattedcases);
//
// System.out.println(obj);
//// ObjectMapper mapper = new ObjectMapper();
//// List<Suspected> value = mapper.writeValue(obj, Suspected.class);
// } catch (JSONException e) {
// e.printStackTrace();
// }
//
// Gson gson = new Gson();
// List<Suspected> suspectedCases = gson.fromJson(unformattedcases, new TypeToken<List<Suspected>>(){}.getType());
System.out.println(unformattedcases);
// for (Suspected suspected : suspectedCases){
// suspectedMapper.addSuspectedCase(suspected);
// }
}
}
I am not sure I understand your issue. This is my best guess about what you meant and what you want to happen :
You want your controller to receive ArrayList < Suspected > as the POST request body
You want your controller to return ArrayList < Suspected > as the POST response body
If that's the case, try this :
[...]
#PostMapping("/addSuspect")
#CrossOrigin(origins = "http://localhost:3000")
#ResponseBody
private ArrayList<Suspected> newSuspectedcases(#RequestBody ArrayList<Suspected> unformattedcases){
[...]
System.out.println(unformattedcases);
[...]
return unformattedcases;
}
If it's not what you meant, please provide more information.
Firstly, your controller method is returning void and not, if I undestand correctly, the payload that you're trying to send. You have to make your controller method return List<Suspected> to receive a body in the response.
Another issue is that you're missing a #RequestBody annotation on the param, which tells Spring to get the body from the request and try to deserialize it to a ArrayList of Suspects.
Another thing to note, it is a good practice to use interfaces instead of implementation classes as parameters and return value in your methods. Consider using List<Suspected> instead of ArrayList<Suspected>
So the final method should look like this:
#PostMapping("/addSuspect")
#CrossOrigin(origins = "http://localhost:3000")
private List<Suspected> newSuspectedcases(#RequestBody List<Suspected> unformattedcases){
[...]
System.out.println(unformattedcases);
[...]
return unformattedcases;
}
PS For CORS issues you may want to using a local proxy setup as described in React docs: https://create-react-app.dev/docs/proxying-api-requests-in-development/ And configure CORS for remote environments, without adding localhost:3000.

Sending output of one websocket client as input to another

Quick search on SO failed to find me a similar question so here we go
I basically want RSocket's requestChannel syntax with Webflux so I am able to process the received Flux outside of WebSocketClient.execute() method and write something like this (with session being opened only when the returned flux is subscribed to, proper error propagation, automatic completion and closing of the WS session when both inbound and outbound fluxes are complete -
either completed by the server side or cancelled by the consumer)
service /f wraps its received string messages in 'f(...)': 'str' -> 'f(str)'
service /g does the same with 'g(...)' and the following test passes:
private final DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
private WebSocketMessage serializeString(final String text) {
return new WebSocketMessage(Type.TEXT, dataBufferFactory.wrap(text.getBytes(StandardCharsets.UTF_8)));
}
#Test
void test() {
var requests = 5;
var input = Flux.range(0, requests).map(String::valueOf);
var wsClient = new ReactorNettyWebSocketClient(
HttpClient.from(TcpClient.create(ConnectionProvider.newConnection())));
var f = requestChannel(wsClient, fUri, input.map(this::serializeString))
.map(WebSocketMessage::getPayloadAsText);
var g = requestChannel(wsClient, gUri, f.map(this::serializeString))
.map(WebSocketMessage::getPayloadAsText);
var responses = g.take(requests);
var expectedResponses = Stream.range(0, requests)
.map(i -> "g(f(" + i + "))")
.toJavaArray(String[]::new);
StepVerifier.create(responses)
.expectSubscription()
.expectNext(expectedResponses)
.verifyComplete();
}
And this seems to work for me... so far
public static Flux<WebSocketMessage> requestChannel(
WebSocketClient wsClient, URI uri, Flux<WebSocketMessage> outbound) {
CompletableFuture<Flux<WebSocketMessage>> recvFuture = new CompletableFuture<>();
CompletableFuture<Integer> consumerDoneCallback = new CompletableFuture<>();
var executeMono = wsClient.execute(uri,
wss -> {
recvFuture.complete(wss.receive().log("requestChannel.receive " + uri, Level.FINE));
return wss.send(outbound)
.and(Mono.fromFuture(consumerDoneCallback));
}).log("requestChannel.execute " + uri, Level.FINE);
return Mono.fromFuture(recvFuture)
.flatMapMany(recv -> recv.doOnComplete(() -> consumerDoneCallback.complete(1)))
.mergeWith(executeMono.cast(WebSocketMessage.class));
}
Rather interested if there're any flaws with this solution I haven't stumbled upon yet

How get results of test in a Dynamic Test in Junit5?

my function is similar to:
#TestFactory
public Stream<DynamicTest> dynamicTest() throws Exception {
String geocodingAnasJsonTest = properties.getProperty("smart-road.simulator.json.geocoding-it.anas.testSuite.test");
String endpoint = properties.getProperty("smart-road.simulator.endpoint.anasGeocoding");
RequestSpecification request = RestAssured.given().header("Authorization", auth);
request.accept(ContentType.JSON);
request.contentType(ContentType.JSON);
JsonNode jsonObjectArray = JsonMappingUtil.getJsonFileFromPath(geocodingAnasJsonTest);
Stream<JsonNode> elementStream = StreamSupport.stream(Spliterators
.spliteratorUnknownSize(jsonObjectArray.elements(),
Spliterator.ORDERED), false);
return elementStream.map(jsonNode -> DynamicTest.dynamicTest(String.format("Test ID: %s", jsonNode.get("test_name")),
() -> {request.body(jsonNode.get("request").toString());
Response response = request.post(endpoint);
int statusCode = response.getStatusCode();
boolean res = false;
if (statusCode >= 200 && statusCode < 300) {
res = true;
}
try {
assertEquals(true, res, properties.getProperty("smart-road.response.smart-road.message.status.ok"));
logger.info(properties.getProperty("smart-road.response.smart-road.message.status.ok"));
String responseOK=jsonNode.get("response").toString();
assertEquals(responseOK, response.asString(), properties.getProperty("smart-road.response.smart-road.message.status.right-end"));
logger.info(properties.getProperty("smart-road.response.smart-road.message.status.right-end"));
} catch (AssertionFailedError er) {
logger.error(properties.getProperty("smart-road.response.smart-road.message.status.assertion-failed"));
fail("Test Fallito");
Assertions.assertTrue(true);
}
}
)//fine dynamicTest
);//fine map
}//fine metodo
I have 20 children test.
I run test in main:
SummaryGeneratingListener listener = new SummaryGeneratingListener();
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
.selectors(selectMethod(Test.class,"dynamicTest"))
.build();
Launcher launcher = LauncherFactory.create();
launcher.registerTestExecutionListeners(listener);
launcher.execute(request);
Now with summary= listener.getSummary() i dont read all tests result but only count Failed or Successfull test.
How i read all result fail/success for all tests?
I will want a map like this:
TEST_ID - RESULTS
test0001 Success
test0002 Fail
test0003 Success
test0004 Success
test0005 Fail
How i get this? Is possible?
Thanks
Regards
One approach is to create your own implementation of org.junit.platform.launcher.TestExecutionListener and register it with the launcher. You may look at the source code of SummaryGeneratingListener as a first start. You could change executionFinished(..) to build up the map of test results. Here's a sketch:
class MySummaryListener implements TestExecutionListener {
private Map<String, TestExecutionResult.Status> summary = new HashMap<>();
#Override
public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) {
summary.put(testIdentifier.getDisplayName(), testExecutionResult.getStatus());
}
}
There's probably more you want to do in the listener but it should give you an idea where to start.

Creating Text Stream Using Spring WebFlux

I've been using Spring WebFlux to create a text stream, here is the code.
#SpringBootApplication
#RestController
public class ReactiveServer {
private static final String FILE_PATH = "c:/test/";
#GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE, value = "/events")
Flux<String> events() {
Flux<String> eventFlux = Flux.fromStream(Stream.generate(() -> FileReader.readFile()));
Flux<Long> durationFlux = Flux.interval(Duration.ofMillis(500));
return Flux.zip(eventFlux, durationFlux).map(Tuple2::getT1);
}
public static void main(String[] args) {
SpringApplication.run(ReactiveServer.class, args);
}
}
When I access the /events url on the browser I get this, that's almost what I want to get:
data:{"type":"HystrixCommand","name":"GetConsumerCommand","group":"ConsumerRemoteGroup","currentTime":1542379993662,"isCircuitBreakerOpen":false,"errorPercentage":0,"errorCount":0,"requestCount":0,"rollingCountBadRequests":0}
data:{"type":"HystrixCommand","name":"GetConsumerCommand","group":"ConsumerRemoteGroup","currentTime":1542379994203,"isCircuitBreakerOpen":false,"errorPercentage":0,"errorCount":0,"requestCount":2,"rollingCountBadRequests":0}
data:{"type":"HystrixCommand","name":"GetConsumerCommand","group":"ConsumerRemoteGroup","currentTime":1542379994706,"isCircuitBreakerOpen":false,"errorPercentage":0,"errorCount":0,"requestCount":2,"rollingCountBadRequests":0}
data:{"type":"HystrixCommand","name":"GetConsumerCommand","group":"ConsumerRemoteGroup","currentTime":1542379995213,"isCircuitBreakerOpen":false,"errorPercentage":0,"errorCount":0,"requestCount":3,"rollingCountBadRequests":0}
What I need to do is to insert a "ping:" in between iterations to get:
ping:
data:{"type":"HystrixCommand","name":"GetConsumerCommand","group":"ConsumerRemoteGroup","currentTime":1542379993662,"isCircuitBreakerOpen":false,"errorPercentage":0,"errorCount":0,"requestCount":0,"rollingCountBadRequests":0}
data:{"type":"HystrixCommand","name":"GetConsumerCommand","group":"ConsumerRemoteGroup","currentTime":1542379994203,"isCircuitBreakerOpen":false,"errorPercentage":0,"errorCount":0,"requestCount":2,"rollingCountBadRequests":0}
ping:
data:{"type":"HystrixCommand","name":"GetConsumerCommand","group":"ConsumerRemoteGroup","currentTime":1542379994706,"isCircuitBreakerOpen":false,"errorPercentage":0,"errorCount":0,"requestCount":2,"rollingCountBadRequests":0}
data:{"type":"HystrixCommand","name":"GetConsumerCommand","group":"ConsumerRemoteGroup","currentTime":1542379995213,"isCircuitBreakerOpen":false,"errorPercentage":0,"errorCount":0,"requestCount":3,"rollingCountBadRequests":0}
But, the best I could get was:
data: ping:
data:{"type":"HystrixCommand","name":"GetConsumerCommand","group":"ConsumerRemoteGroup","currentTime":1542379993662,"isCircuitBreakerOpen":false,"errorPercentage":0,"errorCount":0,"requestCount":0,"rollingCountBadRequests":0}
data:{"type":"HystrixCommand","name":"GetConsumerCommand","group":"ConsumerRemoteGroup","currentTime":1542379994203,"isCircuitBreakerOpen":false,"errorPercentage":0,"errorCount":0,"requestCount":2,"rollingCountBadRequests":0}
data: ping:
data:{"type":"HystrixCommand","name":"GetConsumerCommand","group":"ConsumerRemoteGroup","currentTime":1542379994706,"isCircuitBreakerOpen":false,"errorPercentage":0,"errorCount":0,"requestCount":2,"rollingCountBadRequests":0}
data:{"type":"HystrixCommand","name":"GetConsumerCommand","group":"ConsumerRemoteGroup","currentTime":1542379995213,"isCircuitBreakerOpen":false,"errorPercentage":0,"errorCount":0,"requestCount":3,"rollingCountBadRequests":0}
Does anyone know of a way to what I need?
You could try returning a Flux<ServerSentEvent> and specify the type of event you're trying to send. Like this:
#RestController
public class TestController {
#GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE, path = "/events")
Flux<ServerSentEvent> events() {
Flux<String> events = Flux.interval(Duration.ofMillis(200)).map(String::valueOf);
Flux<ServerSentEvent<String>> sseData = events.map(event -> ServerSentEvent.builder(event).build());
Flux<ServerSentEvent<String>> ping = Flux.interval(Duration.ofMillis(500))
.map(l -> ServerSentEvent.builder("").event("ping").build());
return Flux.merge(sseData, ping);
}
}
With that code snippet, I'm getting the following output:
$ http --stream :8080/events
HTTP/1.1 200 OK
Content-Type: text/event-stream;charset=UTF-8
transfer-encoding: chunked
data:0
data:1
event:ping
data:
data:2
data:3
data:4
event:ping
data:
Which is consistent with Server Sent Events. Is the ping: prefix something specific to Hystrix? If it is, I don't think this is consistent with the SSE spec and that it's something supported in Spring Framework.

Categories