Spock testing for SpringBoot RestController and mocking of exceptions - java

I am new to groovy way of testing RestController for spring boot application. I have a Controller class :
#RestController
#RequestMapping(value = "/onboarding/v1")
public class OnboardingController {
private static final Logger LOG = LoggerFactory.getLogger(OnboardingServiceImpl.class);
#Autowired
private OnboardingService onboardingService;
#RequestMapping(
value = "/service-models",
method = RequestMethod.POST,
consumes = { "multipart/form-data" },
produces = { "application/json" }
)
public ResponseEntity createServiceModel(
#RequestParam("name") final String name,
#RequestParam("file") final MultipartFile file
){
try {
final ServiceModelRequestData serviceModelRequestData =
new ServiceModelRequestData(name, file);
final ServiceModelDetail createdServiceModel =
onboardingService.createServiceModel(serviceModelRequestData);
return new ResponseEntity<>(createdServiceModel, HttpStatus.OK);
}
catch (MalformedContentException ex) {
LOG.error("Malformed Content:", ex);
return new ResponseEntity<>(errorMessage(ex), HttpStatus.BAD_REQUEST);
}
catch (ServiceModelNameAlreadyExistsException ex) {
LOG.error("Service Model Name Already Exists:", ex);
return new ResponseEntity<>(errorMessage(ex), HttpStatus.CONFLICT);
}
catch (ServiceUnavailableException ex) {
LOG.error("Service Unavailable currently:" + ex);
return new ResponseEntity<>(errorMessage(ex), HttpStatus.SERVICE_UNAVAILABLE);
}
}
//...
}
I am unable to find how to spock test the above class along with writing the spock test cases for exceptions and checking the desired response?I want to create a test case which throws an error when the method is called and returns the responseentity which I want to check contains the given Jason data along with the desired Http Status. Sample spock test for the code snippet would be highly appreciated.
EDITED:
The test Class is as below(along with comments of what I was trying to do):
package com.service.onboarding.web.controller
import spock.lang.Specification
import com.service.onboarding.business.OnboardingServiceImpl
import com.service.onboarding.business.api.OnboardingService
import com.service.onboarding.domain.exception.MalformedContentException
import com.service.onboarding.domain.exception.ServiceModelNameAlreadyExistsException
import com.service.onboarding.domain.exception.ServiceUnavailableException
import com.service.onboarding.domain.resource.Greeting
import groovy.json.internal.Exceptions
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest
import org.springframework.test.web.servlet.MockMvc
import org.springframework.web.multipart.MultipartFile
import org.springframework.boot.test.mock.mockito.MockBean
import org.springframework.http.ResponseEntity
import spock.lang.Unroll
import static org.mockito.BDDMockito.given
/*#WebMvcTest
public class OnboardingControllerSpec extends Specification {
#MockBean
private OnboardingService onboardingService;
#Autowired
private MockMvc mockMvc;
def "controller should return expected JSON content and OK response"() {
given: 'hello world service responds with greeting'
def name = "Emily"
given(onboardingService.getPersonalGreeting("${name}")).willReturn(new Greeting(1, "Hi, ${name}"));
when: 'hello world service is called with name provided'
def response = mockMvc.perform(get("/onboarding/v1?name=${name}"))
then: 'expected JSON returned and response code is OK'
response
.andExpect(status().isOk())
.andExpect(content().json("{'id': 1, 'content': 'Hi, ${name}'}"))
}
}*/
public class OnboardingControllerSpec extends Specification{
OnboardingServiceImpl service =new OnboardingServiceImpl()
OnboardingController controller
ResponseEntity response
#Unroll
def "HTTP response #statusCode when creating service model"() {
given:
if (exception) {
service = Stub() {
createServiceModel(_) >> { throw exception }
}
}
controller= new OnboardingController(onboardingService: service)
when:
response=controller.createServiceModel("test", Mock(MultipartFile))
then:
response.statusCode == statusCode
where:
exception | statusCode
null | OK
new MalformedContentException() | BAD_REQUEST
new ServiceModelNameAlreadyExistsException() | CONFLICT
new ServiceUnavailableException() | SERVICE_UNAVAILABLE
}
}
My service class is as follows:
package com.service.onboarding.business;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicLong;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.service.onboarding.business.api.OnboardingService;
import com.service.onboarding.business.servicemanagement.ServiceModelRepository;
import com.service.onboarding.domain.exception.MalformedContentException;
import com.service.onboarding.domain.exception.ServiceModelDoesNotExistException;
import com.service.onboarding.domain.exception.ServiceModelInUseException;
import com.service.onboarding.domain.exception.ServiceModelNameAlreadyExistsException;
import com.onboarding.domain.exception.ServiceUnavailableException;
import com.service.onboarding.domain.requestdata.ServiceModelPaginationFilter;
import com.service.onboarding.domain.requestdata.ServiceModelRequestData;
import com.service.onboarding.domain.resource.Greeting;
import com.service.onboarding.domain.resource.ServiceModel;
import com.service.onboarding.domain.resource.ServiceModelDetail;
import com.service.onboarding.domain.responsedata.PaginatedServiceResponseData;
/*
* Sample service to demonstrate what the API would use to get things done
*/
#Service
public class OnboardingServiceImpl implements OnboardingService {
private final AtomicLong counter = new AtomicLong();
private static final String TEMPLATE = "Hello, %s!";
private static final Logger LOG = LoggerFactory.getLogger(OnboardingServiceImpl.class);
#Autowired
private ServiceModelRepository serviceModelRepository;
public OnboardingServiceImpl() {
}
#Override
public Greeting getPersonalGreeting(final String name) {
return new Greeting(counter.incrementAndGet(),
String.format(TEMPLATE, name));
}
#Override
public ServiceModelDetail createServiceModel(final ServiceModelRequestData serviceModelRequestData) throws MalformedContentException, ServiceModelNameAlreadyExistsException, ServiceUnavailableException {
return serviceModelRepository.create(serviceModelRequestData);
}

Your test should look like this:
package de.scrum_master.stackoverflow
import org.springframework.http.ResponseEntity
import org.springframework.web.multipart.MultipartFile
import spock.lang.Specification
import spock.lang.Unroll
import static org.springframework.http.HttpStatus.*
class OnboardingControllerTest extends Specification {
OnboardingService service = new OnboardingService()
OnboardingController controller
ResponseEntity response
#Unroll
def "HTTP response #statusCode when creating service model"() {
given:
if (exception) {
service = Stub() {
createServiceModel(_) >> { throw exception }
}
}
controller = new OnboardingController(onboardingService: service)
when:
response = controller.createServiceModel("test", Mock(MultipartFile))
then:
response.statusCode == statusCode
where:
exception | statusCode
null | OK
new MalformedContentException() | BAD_REQUEST
new ServiceModelNameAlreadyExistsException() | CONFLICT
new ServiceUnavailableException() | SERVICE_UNAVAILABLE
}
}
The console log (I switched to Java logging when replicating your use case) would be:
Mär 10, 2018 12:51:50 PM de.scrum_master.stackoverflow.OnboardingController createServiceModel
INFORMATION: Malformed Content: de.scrum_master.stackoverflow.MalformedContentException
Mär 10, 2018 12:51:50 PM de.scrum_master.stackoverflow.OnboardingController createServiceModel
INFORMATION: Service Model Name Already Exists: de.scrum_master.stackoverflow.ServiceModelNameAlreadyExistsException
Mär 10, 2018 12:51:50 PM de.scrum_master.stackoverflow.OnboardingController createServiceModel
INFORMATION: Service Unavailable currently: de.scrum_master.stackoverflow.ServiceUnavailableException
In my IDE (IntelliJ IDEA) the test result looks like this:
P.S.:
I know you are new to SO. So this was your free shot. Next time please provide an MCVE, i.e. a minimal, compilable and executable example, not just a code snippet without package name, imports and test class. You want help with Spock, so please write a Spock specification (test class), show it to us and explain your problem with it. That way we do not need to recreate your situation from scratch but can just concentrate on fixing your test and solving your problem.
You were lucky I just felt a bit bored, waiting for a guest to arrive, otherwise I would not have done that. Saying "I have nothing, please do everything for me" only shows that you are lazy. If you want help, make it easy for others to help and show some respect towards their time budget. They are doing it for free!

Related

Wiremock verify doesn't work if run together with other integration tests

I'm not an expert with Wiremock and probably there's something wrong in my settings/configuration but I'm facing a weird scenario when it comes to verify a specific test.
if I run this specific test among the whole IT suite, the test fails on WireMockServer.verify(int count, RequestPatternBuilder requestPatternBuilder) but if I run the test alone, it works fine and the assertion is correct.
so what I did so far to understand this weird behavior is :
I set a break point the check if the actualCount inside the method was correct and indeed it is (only if run alone).
more over, I actually put some log messages to verify on the application side that this method has been invoked 4 times (one is the first request which throws exception and the other 3 are the so called retries)
I've debugged this specific use case by sending some requests with postman and the behaviour is what I expected
so at the moment I got no clue about what the heck is going on with this specific test when is running among the whole IT suite.
any input about what could it be the issue or why Wiremock behaves in this specific way?
Thank you in advance for the help & inputs :)
I have the same issue: tests finish successfully if I run them separately and fail when run with a class.
Wiremock version "com.github.tomakehurst:wiremock-jre8:2.33.1"
package com.kn.tic.web.client.jira;
import com.atlassian.jira.rest.client.api.domain.input.IssueInput;
import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder;
import com.atlassian.jira.rest.client.api.domain.util.UriUtil;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.kn.tic.config.ApplicationProperties;
import org.apache.http.HttpStatus;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.data.util.Pair;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.UUID;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
class CustomAsynchronousIssueRestClientTest {
private static final String USER = "user";
private static final String PASS = "pass";
private static final String HOST = "localhost";
private static final String PROTOCOL = "http";
private static final String JIRA_TRACKING_ID = "jiraTrackingId";
#TempDir
Path tempDir;
private final WireMockServer wireMockServer = new WireMockServer();
private JiraClient jiraClient;
#BeforeEach
public void setUp() {
this.wireMockServer.start();
ApplicationProperties applicationProperties = new ApplicationProperties();
applicationProperties.getJira().setUsername(USER);
applicationProperties.getJira().setPassword(PASS);
applicationProperties.getJira().setHost(HOST);
applicationProperties.getJira().setProtocol(PROTOCOL);
applicationProperties.getJira().setPort(wireMockServer.port());
this.jiraClient = new JiraClient(applicationProperties.getJira());
}
#AfterEach
void afterAll() {
this.wireMockServer.stop();
}
#Test
void testThatCustomCreateIssueImplementationAddsJiraTrackingIdToQuery() {
String jiraTrackingId = UUID.randomUUID().toString();
String uriPath = "/rest/api/latest/issue";
wireMockServer.stubFor(post(urlPathEqualTo(uriPath))
.withQueryParam(JIRA_TRACKING_ID, equalTo(jiraTrackingId))
.withHeader(HttpHeaders.CONTENT_TYPE, containing(MediaType.APPLICATION_JSON_VALUE))
.withBasicAuth(USER, PASS)
.willReturn(
aResponse()
.withStatus(HttpStatus.SC_OK)
.withHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.withBody("{\"id\":\"2197656\",\"key\":\"TTT-822\",\"self\":\"https://test-jira.com/rest/api/latest/issue/2197656\"}")
));
IssueInput issueInput = new IssueInputBuilder()
.setProjectKey("TCF")
.setIssueTypeId(5L)
.setSummary("summary")
.setReporterName("reporter")
.setDescription("description")
.build();
jiraClient.createIssue(issueInput, jiraTrackingId);
verify(postRequestedFor(urlPathEqualTo(uriPath))
.withQueryParam(JIRA_TRACKING_ID, equalTo(jiraTrackingId))
.withHeader(HttpHeaders.CONTENT_TYPE, containing(MediaType.APPLICATION_JSON_VALUE)));
}
#Test
void testThatCustomAddAttachmentImplementationAddsJiraTrackingIdToQuery() throws URISyntaxException, IOException {
String jiraTrackingId = UUID.randomUUID().toString();
URI issueUri = new URI(String.format("%s://%s:%s/rest/api/latest/issue/2197654",PROTOCOL, HOST, wireMockServer.port()));
URI attachmentUri = UriUtil.path(issueUri, "attachments");
wireMockServer.stubFor(post(urlPathEqualTo(attachmentUri.getPath()))
.withQueryParam(JIRA_TRACKING_ID, equalTo(jiraTrackingId))
.withHeader(HttpHeaders.CONTENT_TYPE, containing(MediaType.MULTIPART_FORM_DATA_VALUE))
.withBasicAuth(USER, PASS)
.willReturn(
aResponse()
.withStatus(HttpStatus.SC_OK)
));
Path tempFile = Files.createFile(tempDir.resolve("attachment.pdf"));
Files.writeString(tempFile, "test");
List<Pair<File, String>> attachments = List.of(Pair.of(tempFile.toFile(), "attachment.pdf"));
jiraClient.addAttachmentsToIssue(issueUri, attachments, jiraTrackingId);
verify(postRequestedFor(urlPathEqualTo(attachmentUri.getPath()))
.withQueryParam(JIRA_TRACKING_ID, equalTo(jiraTrackingId))
.withHeader(HttpHeaders.CONTENT_TYPE, containing(MediaType.MULTIPART_FORM_DATA_VALUE)));
}
}

How to parse REST API STREAM

I am sending a get request to the server and server returns the following two responses. These responses are received as the event occurs on servers in streams (like id1,id2,id3,id4.....and so on) not in one shot.
Now, I need to take these response one by one and parse it and then save it the objects for further use.
How do i achieve this java 8 and spring MVC?
id: 1
data: {"event_type":"ABC","business_call_type":"XYZ","agent_number":"nnn","call_recording":null,"number":"0000","uuid":"a","call_direction":"Outbound","caller":"+100000000000","customer_number":"+100000000000","version":"1.0","k_number":"+917303454203","type":"AGENT_CALL","unique_id":"0","call_solution":"xx","FreeSWITCH_IPv4":"11111","Event_Date_Local":"2020-03-28 11:46:47"}
id: 2
data: {"event_type":"AGENT_ANSWER","business_call_type":"Outbound","agent_number":"+1111111111","call_recording":null,"number":"+22222222","uuid":"bbbbbbbbbbbbbb","call_direction":"Outbound","caller":"+100000000000","customer_number":"+100000000000","version":"1.0","k_number":"+1111111111","type":"AGENT_ANSWER","unique_id":"bbbbbbbbbb","call_solution":"xx","FreeSWITCH_IPv4":"0.0.0.0","Event_Date_Local":"2020-03-28 11:47:00"}
below is the code used foe above json parsing.
import java.util.HashMap;
import java.util.logging.Logger;
import javax.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Service;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureCallback;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.client.AsyncRestTemplate;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.context.request.async.DeferredResult;
import com.psg.async_tasks.controller;
import com.psg.dao.CtiIntegrationdao;
// #Controller
#Service
public class ListningService {
private static final Logger logger = Logger.getLogger(ListningService.class.getName());
#Autowired
CtiIntegrationdao daoCtiInt;
//#RequestMapping({"list"})
#PostConstruct
public void ListningReponse() {
HashMap<String,String> results=daoCtiInt.getKnolarity_Config();
String endpoint;
endpoint=results.get("30");
endpoint=endpoint.replace("<<AUTH>>",results.get("26"));
logger.info(endpoint);
logger.info("============================================================================================#postconstruct=========");
AsyncRestTemplate asyncrestTemplate = new AsyncRestTemplate();
try {
final DeferredResult<String> result = new DeferredResult<>();
ListenableFuture<ResponseEntity<String>> futureEntity = asyncrestTemplate.getForEntity(endpoint, String.class);
logger.info("IN TRY");
logger.info(futureEntity.toString());
futureEntity.addCallback(new ListenableFutureCallback<ResponseEntity<String>>() {
#Override
public void onSuccess(ResponseEntity<String> result) {
String[] idno = result.getBody().split("\\R", 3);
System.out.println("==================="+idno[0]);
String responseBody =result.getBody().replaceAll("id: (\\d+)","").replace("data: ","");;
logger.info("-----responsebody-----"+responseBody);
logger.info("-----responsebody-----"+result.getBody());
// logger.info("-----responsebody-----"+result.getBody().getAgent_number());
// logger.info("-----responsebody-----"+result.getBody().getBusiness_call_type());
// logger.info("-----responsebody-----"+result.getBody().getCall_duration());
// logger.info("-----responsebody-----"+result.getBody().getCall_recording());
// logger.info("-----responsebody-----"+result.getBody().getCall_solution());
// logger.info("-----responsebody-----"+result.getBody().getCall_Type());
// logger.info("-----responsebody-----"+result.getBody().getDestination());
}
#Override
public void onFailure(Throwable ex) {
result.setErrorResult(ex.getMessage());
logger.info("------------Failure Block"+result.toString());
}
});
}catch(HttpClientErrorException ex) {
logger.info(ex.getMessage());
}catch(Exception ex) {
ex.printStackTrace();
}
}
}

How to write a proper unit test for Elasticsearch in Java

Overview:
I'm totally new to Elastic search testing and I'm gonna add proper unit tests. The project compatibilities are as follow:
Java 8
Elasticsearch 6.2.4
Project uses low level rest client for fetching data from ES
More info about ES configurations is as follow:
import static java.net.InetAddress.getByName;
import static java.util.Arrays.stream;
import java.net.UnknownHostException;
import java.util.Map;
import java.util.Objects;
import javax.inject.Inject;
import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.transport.TransportClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.TransportAddress;
import org.elasticsearch.transport.client.PreBuiltTransportClient;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import au.com.api.util.RestClientUtil;
import lombok.extern.slf4j.Slf4j;
#Slf4j
#Configuration
public class ElasticConfiguration implements InitializingBean{
#Value(value = "${elasticsearch.hosts}")
private String[] hosts;
#Value(value = "${elasticsearch.httpPort}")
private int httpPort;
#Value(value = "${elasticsearch.tcpPort}")
private int tcpPort;
#Value(value = "${elasticsearch.clusterName}")
private String clusterName;
#Inject
private RestClientUtil client;
#Bean
public RestHighLevelClient restHighClient() {
return new RestHighLevelClient(RestClient.builder(httpHosts()));
}
#Bean
#Deprecated
public RestClient restClient() {
return RestClient.builder(httpHosts()).build();
}
/**
* #return TransportClient
* #throws UnknownHostException
*/
#SuppressWarnings("resource")
#Bean
public TransportClient transportClient() throws UnknownHostException{
Settings settings = Settings.builder()
.put("cluster.name", clusterName).build();
return new PreBuiltTransportClient(settings).addTransportAddresses(transportAddresses());
}
#Override
public void afterPropertiesSet() throws Exception {
log.debug("loading search templates...");
try {
for (Map.Entry<String, String> entry : Constants.SEARCH_TEMPLATE_MAP.entrySet()) {
client.putInlineSearchTemplateToElasticsearch(entry.getKey(), entry.getValue());
}
} catch (Exception e) {
log.error("Exception has occurred in putting search templates into ES.", e);
}
}
private HttpHost[] httpHosts() {
return stream(hosts).map(h -> new HttpHost(h, httpPort, "http")).toArray(HttpHost[]::new);
}
private TransportAddress[] transportAddresses() throws UnknownHostException {
TransportAddress[] transportAddresses = stream(hosts).map(h -> {
try {
return new TransportAddress(getByName(h), tcpPort);
} catch (UnknownHostException e) {
log.error("Exception has occurred in creating ES TransportAddress. host: '{}', tcpPort: '{}'", h, tcpPort, e);
}
return null;
}).filter(Objects::nonNull).toArray(TransportAddress[]::new);
if (transportAddresses.length == 0) {
throw new UnknownHostException();
}
return transportAddresses;
}
}
Issue:
I don't know how to Mock ES or how to test ES without running an standalone ES on my machine. Please use the following class as an example and let me know how could I write a testcase (unit test not integration) for getSearchResponse method:
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.transport.NoNodeAvailableException;
import org.elasticsearch.client.transport.TransportClient;
import org.elasticsearch.script.ScriptType;
import org.elasticsearch.script.mustache.SearchTemplateRequestBuilder;
import org.elasticsearch.search.Scroll;
import org.elasticsearch.search.aggregations.Aggregation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Repository;
#Slf4j
#Repository
#NoArgsConstructor
public abstract class NewBaseElasticsearchRepository {
#Autowired
protected NewIndexLocator newIndexLocator;
#Value(value = "${elasticsearch.client.timeout}")
private Long timeout;
#Autowired
protected TransportClient transportClient;
#Autowired
protected ThresholdService thresholdService;
#Autowired
protected MessageSource messageSource;
/**
* #param script the name of the script to be executed
* #param templateParams a map of the parameters to be sent to the script
* #param indexName the index to target (an empty indexName will search all indexes)
*
* #return a Search Response object containing details of the request results from Elasticsearch
*
* #throws NoNodeAvailableException thrown when the transport client cannot connect to any ES Nodes (or Coordinators)
* #throws Exception thrown for all other request errors such as parsing and non-connectivity related issues
*/
protected SearchResponse getSearchResponse(String script, Map<String, Object> templateParams, String... indexName) {
log.debug("transport client >> index name --> {}", Arrays.toString(indexName));
SearchResponse searchResponse;
try {
searchResponse = new SearchTemplateRequestBuilder(transportClient)
.setScript(script)
.setScriptType(ScriptType.STORED)
.setScriptParams(templateParams)
.setRequest(new SearchRequest(indexName))
.execute()
.actionGet(timeout)
.getResponse();
} catch (NoNodeAvailableException e) {
log.error(ELASTIC_SEARCH_EXCEPTION_NOT_FOUND, e.getMessage());
throw new ElasticSearchException(ELASTIC_SEARCH_EXCEPTION_NOT_FOUND);
} catch (Exception e) {
log.error(ELASTIC_SEARCH_EXCEPTION, e.getMessage());
throw new ElasticSearchException(ELASTIC_SEARCH_EXCEPTION);
}
log.debug("searchResponse ==> {}", searchResponse);
return searchResponse;
}
So, I would be grateful if you could have a look on the example class and share your genuine solutions with me here about how could I mock TransportClient and get a proper response from SearchResponse object.
Note:
I tried to use ESTestCase from org.elasticsearch.test:framework:6.2.4 but faced jar hell issue and could't resolve it. In the meantime, I could't find any proper docs related to that or Java ES unit testing, in general.

Set the time out on Rest end points

I am trying to impose time limit on http end points.
In the example below, I am aiming that this method shall be executed before 5 seconds. If it is taking more time, I would like to throw exception and return error to client.
Spring : 4.1.7
Jersey 1.1.9
Code
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Response;
#Path("/pets")
#Component
public class PetsController {
#GET
#Produces({MediaTypeApi.JSON, MediaTypeApi.XML})
//Timeout of 5 secs
public List<Pet> getPets() {
//Return
}
}
Any idea to handle this in better way considering optimum utilization of threads.
EDIT
When writing this answer I didn't notice the version of Jersey OP is using. The async API was added in Jersey 2 therefore this answer is not an answer given OP's constraints.
EDIT 2
Apart from upgrading your Jersey libs you might consider migrating your api to Spring MVC and using their async API (available from Spring 3.2). Handling timeouts the Spring way (using the DeferredResult object):
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
#RestController
#RequestMapping("/api")
public class AsyncController {
private static final TIMEOUT = 5000L;
private final AService aService;
#Inject
public AsyncController(final AService aService) {
this.aService = aService;
}
#RequestMapping(value = "/async-endpoint", method = RequestMethod.GET)
public DeferredResult<ResponseEntity<ADto>> asyncEndpoint() {
DeferredResult<ResponseEntity<ADto>> deferredResult = new DeferredResult<>(TIMEOUT);
CompletableFuture
.supplyAsync(() -> aService.aVeryExpensiveOperation())
.thenAccept(result -> {
deferredResult.setResult(new ResponseEntity<>(result, HttpStatus.OK));
})
.exceptionally(throwable -> {
deferredResult.setErrorResult(
throwable instanceof CompletionException ? throwable.getCause() : throwable);
return null;
});
return deferredResult;
}
}
ORIGINAL ANSWER:
There is an example in Jersey Asynchronous Server API Documentation doing exactly what you want:
import javax.ws.rs.container.Suspended;
import javax.ws.rs.container.TimeoutHandler;
import javax.ws.rs.core.Response;
import java.util.concurrent.TimeUnit;
#Path("/resource")
public class AsyncResource {
#GET
#Path("/timeoutAsync")
public void asyncGetWithTimeout(#Suspended final AsyncResponse asyncResponse) {
asyncResponse.setTimeoutHandler(new TimeoutHandler() {
#Override
public void handleTimeout(AsyncResponse asyncResponse) {
asyncResponse.resume(Response.status(Response.Status.SERVICE_UNAVAILABLE).entity("Operation time out.").build());
}
});
asyncResponse.setTimeout(5, TimeUnit.SECONDS);
new Thread(new Runnable() {
#Override
public void run() {
String result = veryExpensiveOperation();
asyncResponse.resume(result);
}
private String veryExpensiveOperation() {
return "Very Expensive Operation with Timeout";
}
}).start();
}
}
Please note that in a real life scenario you'd probably use a threadpool thread instead of creating it yourself like in this Jersey example

dropwizard hibernate use a resource from within the server

I'm new to dropwizard and i'm trying to create an Authenticator which gets credentials from the user, then it uses the rest api getUser method which i implemented in my UserResouce class to get the user with the username that is in the credentials from the db users table. However in my autheticator class i having troubles in figuring out how to use the user resource functions to get the user.
I was trying to do something like that:
public List<com.amitbaz.tss.db.User> getUsersFromDB(String username){
SessionFactory sessionFactory = TradingSystemServerApplication.hibernateBundle.getSessionFactory();
UserDAO userDAO = new UserDAO(sessionFactory);
List<com.amitbaz.tss.db.User> user = userDAO.getUser(username);
logger.debug(user.toString());
return user;
}
inside the autheticator and call it from the authenticte function but it says there is no session bound...
EDIT:
Ok so after much thinking i got to this:
I'm dropwizard authenticator and authorizer implementions with BasicCredentials and.
Autheticator (don't mind the VALID_USER thing..):
package com.amitbaz.tss.auth;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import javax.persistence.NamedQuery;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.amitbaz.tss.TradingSystemServerApplication;
import com.amitbaz.tss.db.UserDAO;
import com.amitbaz.tss.db.UserResource;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import io.dropwizard.auth.AuthenticationException;
import io.dropwizard.auth.Authenticator;
import io.dropwizard.auth.basic.BasicCredentials;
public class TradingSystemServerAuthenticator implements Authenticator<BasicCredentials, User> {
private Logger logger = LoggerFactory.getLogger(TradingSystemServerAuthenticator.class);
private static final Map<String, Set<String>> VALID_USERS = ImmutableMap.of(
"guest", ImmutableSet.of(),
"amit", ImmutableSet.of("admin"),
"stav", ImmutableSet.of("broker")
);
private UserDAO userDAO;
public TradingSystemServerAuthenticator(UserDAO userDAO) {
// TODO Auto-generated constructor stub
this.userDAO = userDAO;
}
#Override
public Optional<User> authenticate(BasicCredentials credentials) throws AuthenticationException {
// TODO Auto-generated method stub
List<com.amitbaz.tss.db.User> user = userDAO.getUser(credentials.getUsername());
logger.debug(user.toString());
if("amit".equals(credentials.getPassword())){
return Optional.of(new User(credentials.getUsername(), VALID_USERS.get(credentials.getUsername())));
}
return Optional.empty();
}
}
Authorizer:
package com.amitbaz.tss.auth;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.amitbaz.tss.db.UserDAO;
import io.dropwizard.auth.Authorizer;
public class TradingSystemServerAuthorizer implements Authorizer<User>{
private Logger logger = LoggerFactory.getLogger(TradingSystemServerAuthorizer.class);
private UserDAO userDAO;
public TradingSystemServerAuthorizer(UserDAO userDAO) {
super();
this.userDAO = userDAO;
}
#Override
public boolean authorize(User user, String role) {
// TODO Auto-generated method stub
logger.debug(userDAO.getUser(user.getName()).toString());
return user.getName().equals("amit") && user.getRole().contains(new String("admin"));
}
}
Now, In my Application class I do this:
package com.amitbaz.tss;
import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.amitbaz.tss.auth.TradingSystemServerAuthenticator;
import com.amitbaz.tss.auth.TradingSystemServerAuthorizer;
import com.amitbaz.tss.auth.User;
import com.amitbaz.tss.db.Broker;
import com.amitbaz.tss.db.BrokerDAO;
import com.amitbaz.tss.db.BrokerResource;
import com.amitbaz.tss.db.Contact;
import com.amitbaz.tss.db.ContactDAO;
import com.amitbaz.tss.db.ContactResource;
import com.amitbaz.tss.db.Product;
import com.amitbaz.tss.db.ProductDAO;
import com.amitbaz.tss.db.ProductResource;
import com.amitbaz.tss.db.Test;
import com.amitbaz.tss.db.TestDAO;
import com.amitbaz.tss.db.TestResource;
import com.amitbaz.tss.db.Transaction;
import com.amitbaz.tss.db.TransactionDAO;
import com.amitbaz.tss.db.TransactionResource;
import com.amitbaz.tss.db.UserDAO;
import com.amitbaz.tss.db.UserResource;
import com.amitbaz.tss.db.UserRole;
import com.amitbaz.tss.db.UserRoleDAO;
import com.amitbaz.tss.db.UserRoleResource;
import com.amitbaz.tss.db.Website;
import com.amitbaz.tss.db.WebsiteDAO;
import com.amitbaz.tss.db.WebsiteResource;
import io.dropwizard.Application;
import io.dropwizard.auth.AuthDynamicFeature;
import io.dropwizard.auth.AuthValueFactoryProvider;
import io.dropwizard.auth.basic.BasicCredentialAuthFilter;
import io.dropwizard.db.DataSourceFactory;
import io.dropwizard.hibernate.HibernateBundle;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
import javassist.tools.web.Webserver;
public class TradingSystemServerApplication extends Application<TradingSystemServerConfiguration>{
public static void main(String[] args) throws Exception{
new TradingSystemServerApplication().run(args);
}
public final static HibernateBundle<TradingSystemServerConfiguration> hibernateBundle
= new HibernateBundle<TradingSystemServerConfiguration>(
Test.class,Broker.class, com.amitbaz.tss.db.User.class, UserRole.class
,Product.class, Transaction.class, Website.class, Contact.class
) {
#Override
public DataSourceFactory getDataSourceFactory(
TradingSystemServerConfiguration configuration
) {
return configuration.getDataSourceFactory();
}
};
final Logger logger = LoggerFactory.getLogger(TradingSystemServerApplication.class);
#Override
public void initialize(
final Bootstrap<TradingSystemServerConfiguration> bootstrap) {
bootstrap.addBundle(hibernateBundle);
}
#Override
public void run(TradingSystemServerConfiguration config, Environment env) throws Exception {
final UserDAO userDAO = new UserDAO(hibernateBundle.getSessionFactory());
final UserRoleDAO userRoleDAO = new
env.jersey().register(new UserResource(userDAO));
/...
BasicCredentialAuthFilter.Builder<User>()
.setAuthenticator(new TradingSystemServerAuthenticator(userDAO))
.setAuthorizer(new TradingSystemServerAuthorizer(userDAO))
.setRealm("Authetication Required")
.buildAuthFilter()));
env.jersey().register(RolesAllowedDynamicFeature.class);
env.jersey().register(new AuthValueFactoryProvider.Binder<>(User.class));
}
}
And I have the annotation #RolesAllowed("role_name") on one of the rest api methods which with im trying to test the auth.
Now when i try to test this and i make a request to that rest api method, I
get the error No session currently bound to execution context where i do userDAO.getUser(...) in the authanticator and in the authorizer
EDIT 2:
UserDAO implementation:
package com.amitbaz.tss.db;
import java.util.List;
import org.hibernate.SessionFactory;
import io.dropwizard.hibernate.AbstractDAO;
public class UserDAO extends AbstractDAO<User>{
public UserDAO(SessionFactory sessionFactory) {
super(sessionFactory);
// TODO Auto-generated constructor stub
}
public List<User> getUser(String username){
return list(namedQuery("com.amitbaz.tss.db.user.getUser")
.setParameter("username", username));
}
}
EDIT 3:
Added #UnitOfWork to authenticate and authorize methods.
registered them as follow ( Notice the changes in hibernateBundle and run method):
package com.amitbaz.tss;
import javax.servlet.ServletRegistration;
import org.atmosphere.cpr.ApplicationConfig;
import org.atmosphere.cpr.AtmosphereServlet;
import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.amitbaz.tss.auth.TradingSystemServerAuthenticator;
import com.amitbaz.tss.auth.TradingSystemServerAuthorizer;
import com.amitbaz.tss.auth.User;
import com.amitbaz.tss.db.Broker;
import com.amitbaz.tss.db.BrokerDAO;
import com.amitbaz.tss.db.BrokerResource;
import com.amitbaz.tss.db.Contact;
import com.amitbaz.tss.db.ContactDAO;
import com.amitbaz.tss.db.ContactResource;
import com.amitbaz.tss.db.Product;
import com.amitbaz.tss.db.ProductDAO;
import com.amitbaz.tss.db.ProductResource;
import com.amitbaz.tss.db.Test;
import com.amitbaz.tss.db.TestDAO;
import com.amitbaz.tss.db.TestResource;
import com.amitbaz.tss.db.Transaction;
import com.amitbaz.tss.db.TransactionDAO;
import com.amitbaz.tss.db.TransactionResource;
import com.amitbaz.tss.db.UserDAO;
import com.amitbaz.tss.db.UserResource;
import com.amitbaz.tss.db.UserRole;
import com.amitbaz.tss.db.UserRoleDAO;
import com.amitbaz.tss.db.UserRoleResource;
import com.amitbaz.tss.db.Website;
import com.amitbaz.tss.db.WebsiteDAO;
import com.amitbaz.tss.db.WebsiteResource;
import io.dropwizard.Application;
import io.dropwizard.auth.AuthDynamicFeature;
import io.dropwizard.auth.AuthValueFactoryProvider;
import io.dropwizard.auth.basic.BasicCredentialAuthFilter;
import io.dropwizard.db.DataSourceFactory;
import io.dropwizard.hibernate.HibernateBundle;
import io.dropwizard.setup.Bootstrap;
import io.dropwizard.setup.Environment;
import javassist.tools.web.Webserver;
public class TradingSystemServerApplication extends Application<TradingSystemServerConfiguration>{
public static void main(String[] args) throws Exception{
new TradingSystemServerApplication().run(args);
}
public final static HibernateBundle<TradingSystemServerConfiguration> hibernateBundle
= new HibernateBundle<TradingSystemServerConfiguration>(
Test.class,Broker.class, com.amitbaz.tss.db.User.class, UserRole.class
,Product.class, Transaction.class, Website.class, Contact.class
,TradingSystemServerAuthenticator.class, TradingSystemServerAuthorizer.class
) {
#Override
public DataSourceFactory getDataSourceFactory(
TradingSystemServerConfiguration configuration
) {
return configuration.getDataSourceFactory();
}
};
final Logger logger = LoggerFactory.getLogger(TradingSystemServerApplication.class);
#Override
public void initialize(
final Bootstrap<TradingSystemServerConfiguration> bootstrap) {
bootstrap.addBundle(hibernateBundle);
}
#Override
public void run(TradingSystemServerConfiguration config, Environment env) throws Exception {
final UserDAO userDAO = new UserDAO(hibernateBundle.getSessionFactory());
final UserRoleDAO userRoleDAO = new UserRoleDAO(hibernateBundle.getSessionFactory());
final TradingSystemServerAuthorizer authorizer = new TradingSystemServerAuthorizer(userDAO);
final TradingSystemServerAuthenticator authenticator = new TradingSystemServerAuthenticator(userDAO);
env.jersey().register(new UserResource(userDAO));
env.jersey().register(new UserRoleResource(userRoleDAO));
env.jersey().register(authorizer);
env.jersey().register(authenticator);
env.jersey().register(new AuthDynamicFeature(new BasicCredentialAuthFilter.Builder<User>()
.setAuthenticator(authenticator)
.setAuthorizer(authorizer)
.setRealm("Authetication Required")
.buildAuthFilter()));
env.jersey().register(RolesAllowedDynamicFeature.class);
env.jersey().register(new AuthValueFactoryProvider.Binder<>(User.class));
}
Your approach looks like a design issue. The issue I see is that you are trying to integrate via rest with a service that is already accessible for you within your application. That adds a lot of overhead and complicates things.
Fortunately, DW already has a fully integrated Authorization and Authentication system just waiting for you to plug in. You can read more about it here: http://www.dropwizard.io/1.0.2/docs/manual/auth.html
The essential thing to note here is that you should split the service used by your resource from your resource. In your case for example the UserDao, or you could split it into a UserService and UserResource, where the UserService provides access to your database layer. Up to you really.
Here is how you would implement this with DW integrated auth and how you would register this as well.
In my example I am skipping the Hibernate aspect of this as it isn't too relevant. you can read about it here: http://www.dropwizard.io/1.0.2/docs/manual/hibernate.html
Here's my code:
public class AuthenticatorTest extends io.dropwizard.Application<Configuration> {
#Override
public void run(Configuration configuration, Environment environment) throws Exception {
// register resource
environment.jersey().register(MyHelloResource.class);
// create the dao + dependencies
UserDao dao = new UserDao(null);
// register new authenticator
environment.jersey().register(new AuthDynamicFeature(new BasicCredentialAuthFilter.Builder<Principal>()
.setAuthenticator(new UserAuth(dao)).setRealm("SUPER SECRET STUFF").buildAuthFilter()));
// enables authentication via filter
environment.jersey().register(RolesAllowedDynamicFeature.class);
}
public static void main(String[] args) throws Exception {
new AuthenticatorTest().run("server", "/home/artur/dev/repo/sandbox/src/main/resources/config/test.yaml");
}
#Path("test")
#Produces(MediaType.APPLICATION_JSON)
public static class MyHelloResource {
#GET
#Path("asd")
#PermitAll
public String test(String x) {
return "Hello";
}
}
public static class UserAuth implements Authenticator<BasicCredentials, Principal> {
private UserDao dao;
public UserAuth(UserDao dao) {
this.dao = dao;
}
#Override
public Optional<Principal> authenticate(BasicCredentials credentials) throws AuthenticationException {
String user = dao.getUser();
return Optional.of(new Principal() {
#Override
public String getName() {
return user;
}
});
}
}
public static class UserDao {
private SessionFactory s;
public UserDao(final SessionFactory s) {
this.s = s;
}
public String getUser() {
return "pandaadb";
}
}
}
And this is the breakdown of what we are doing.
First, as per docs, you would register your HibernateBundle within the bootstrap method as shown (in docs). This gives you access to the SessionFactory you require for your authentication.
Your resource method will be annotated with a java security annotation. i am using PermitAll because I am disregarding roles.
In the run method, you then create your DAO, register your resource and use the DW builder to add the required Filter and the Authenticator. This one specifically is for BasicCredentials, however there is nothing stopping you from doing any kind of filter for this. DW already supports things like Ldap (in a different dependency), Basic Auth and so on.
Now, since you create your beans in the run method, and you added your Hibernate bundle in the bootstrap method, you have access to the SessionFactory and can instantiate the DAO accordingly. No need to have to pass it around.
You also don't have to do any rest-request to access your user (though there is nothing stopping you adding that resource anyway, in case you need external access to it.)
So, to sum up, the important parts are:
Add A security annotation to your resource (e.g. PermitAll to allow all roles)
Add an authenticator implementation (in my case UserAuth) that uses your DAO
Instantiate it in the run method provided by dropwizard and register it with the jersey environment.
Note, this requires your user to implement the javax.security.Principal interface. this is not a bad idea in general as a lot of security Frameworks make use of this.
This, also, gives you more options with regards to DW.
You can add an Authorization implementation and a filter, and you'll be able to inject the User object into any resource method by adding an #Auth annotated object (see docs).
Finally, the test of the standalone app from above:
artur#pandaadb:~$ curl "localhost:9085/api/test/asd" -v
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 9085 (#0)
> GET /api/test/asd HTTP/1.1
> Host: localhost:9085
> User-Agent: curl/7.47.0
> Accept: */*
>
< HTTP/1.1 401 Unauthorized
< Date: Mon, 17 Oct 2016 10:45:51 GMT
< WWW-Authenticate: Basic realm="SUPER SECRET STUFF"
< Content-Type: text/plain
< Content-Length: 49
<
* Connection #0 to host localhost left intact
Credentials are required to access this resource.
artur#pandaadb:~$ curl "localhost:9085/api/test/asd" -H "Authorization: Basic dXNlcjpwYXNz" -v
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 9085 (#0)
> GET /api/test/asd HTTP/1.1
> Host: localhost:9085
> User-Agent: curl/7.47.0
> Accept: */*
> Authorization: Basic dXNlcjpwYXNz
>
< HTTP/1.1 200 OK
< Date: Mon, 17 Oct 2016 10:46:11 GMT
< Content-Type: application/json
< Vary: Accept-Encoding
< Content-Length: 5
<
* Connection #0 to host localhost left intact
Helloartur
I hope this helps you with your issue.
Of course you don't need to use the DW method for Authentication. However I would recommend going down that road as you will have more support and a lot of things out of the box.
However, the one thing you should rethink (if you don't use DW) is to not do a curl request to your Filter. Instead, instantiate it in the run method, and pass that instance to your Filter.
Note also, if you register your DAO with DW (as seen in hibernate docs), you will be able to use #Inject to inject your DAO into the Filter class that needs to use it.
Right, I think that's all the info you need :)
Let me know if you have any problems,
Artur
Edit:
I am doing an edit because I wrote a lot above and don't want to go over it.
I set up Hibernate to test this. The reason you are seeing issues is because the UnitOfWork is bound to the request scope. However, the resource annotations is matched AFTER the filter is invoked (since you need to do auth before executing the method). This is why you don't have a session.
This is the solution.
In your run method, register a proxy for your auth implementation:
UnitOfWorkAwareProxyFactory fac = new UnitOfWorkAwareProxyFactory(hibernate);
UserAuth proxy = fac.create(UserAuth.class, UserDao.class, dao);
environment.jersey().register(new AuthDynamicFeature(new BasicCredentialAuthFilter.Builder<Principal>()
.setAuthenticator(proxy).setRealm("SUPER SECRET STUFF").buildAuthFilter()));
This creates a proxy around the UserAuth class so that it is aware of the UnitOfWork annotation.
In your UserAuth class (or mine rather) you do:
public static class UserAuth implements Authenticator<BasicCredentials, Principal> {
private UserDao dao;
public UserAuth(UserDao dao) {
this.dao = dao;
}
#Override
#UnitOfWork
public Optional<Principal> authenticate(BasicCredentials credentials) throws AuthenticationException {
String user = dao.getUser();
return Optional.of(new Principal() {
#Override
public String getName() {
return user;
}
});
}
}
Note the UnitOfWork annotation on the authenticate. This now opens a new session for you. Please make sure to read up on UnitOfWork as it may have tricky side effects (or not) depending on how you use it.
Finally, this allowed my dao to talk to the database on an existing session.
Regards,
Artur
I finally, after much debugginf, find the proper way to open a session and execute a query.
I added this lines in Authenticator's authenticate mehod:
Session session = userDAO.getSessionFactory().openSession();
Transaction transaction = session.getTransaction();
Query userquery = session.createQuery("select u from User u where u.username = :username").setParameter("username", credentials.getUsername());
List<com.amitbaz.tss.db.User> u = userquery.list();
session.close();
And the same in Authorizater and it now works :)

Categories

Resources