package de.unioninvestment.eai.portal.portlet.crud.scripting.domain.container.rest;
import static java.util.Arrays.asList;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.sameInstance;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.isA;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import groovy.lang.Closure;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.util.List;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.ProtocolVersion;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicStatusLine;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import de.unioninvestment.eai.portal.portlet.crud.config.GroovyScript;
import de.unioninvestment.eai.portal.portlet.crud.config.ReSTChangeConfig;
import de.unioninvestment.eai.portal.portlet.crud.config.ReSTChangeMethodConfig;
import de.unioninvestment.eai.portal.portlet.crud.config.ReSTContainerConfig;
import de.unioninvestment.eai.portal.portlet.crud.config.ReSTDeleteConfig;
import de.unioninvestment.eai.portal.portlet.crud.domain.exception.BusinessException;
import de.unioninvestment.eai.portal.portlet.crud.domain.exception.InvalidConfigurationException;
import de.unioninvestment.eai.portal.portlet.crud.domain.model.ContainerRow;
import de.unioninvestment.eai.portal.portlet.crud.domain.model.ReSTContainer;
import de.unioninvestment.eai.portal.portlet.crud.domain.model.authentication.Realm;
import de.unioninvestment.eai.portal.portlet.crud.domain.support.AuditLogger;
import de.unioninvestment.eai.portal.portlet.crud.scripting.model.ScriptRow;
import de.unioninvestment.eai.portal.support.scripting.ScriptBuilder;
import de.unioninvestment.eai.portal.support.vaadin.container.Column;
import de.unioninvestment.eai.portal.support.vaadin.container.GenericItem;
import de.unioninvestment.eai.portal.support.vaadin.container.GenericItemId;
import de.unioninvestment.eai.portal.support.vaadin.container.MetaData;
import de.unioninvestment.eai.portal.support.vaadin.container.UpdateContext;
public class ReSTDelegateImplTest {
private static final String JSON_STRING = "{ { a: 1}, {a: 2}, {a: 3} }";
@Mock
private HttpClient httpMock;
@Mock
private PayloadParser parserMock;
@Captor
private ArgumentCaptor<HttpUriRequest> requestCaptor;
@Captor
private ArgumentCaptor<HttpPost> postRequestCaptor;
@Captor
private ArgumentCaptor<HttpPut> putRequestCaptor;
@Captor
private ArgumentCaptor<HttpDelete> deleteRequestCaptor;
@Mock
private HttpResponse responseMock;
@Mock
private GenericItem itemMock;
@Mock
private GenericItemId itemIdMock;
@Mock
private HttpPost postRequestMock;
@Mock
private PayloadCreator creatorMock;
@Mock
private ScriptBuilder scriptBuilderMock;
@Mock
private Closure<Object> urlClosureMock;
@Mock
private ReSTContainer containerMock;
@Mock
private ContainerRow containerRowMock;
private byte[] contents;
private ReSTDelegateImpl delegate;
private ReSTContainerConfig config;
@Mock
private AuditLogger auditLoggerMock;
@Mock
private Realm realmMock;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
when(scriptBuilderMock.buildClosure(isA(GroovyScript.class)))
.thenAnswer(new Answer<Closure<Object>>() {
@Override
public Closure<Object> answer(InvocationOnMock invocation)
throws Throwable {
GroovyScript script = (GroovyScript) invocation
.getArguments()[0];
return (Closure<Object>) script.getClazz()
.newInstance().run();
}
});
}
@Test
public void shouldReturnAttributeAsMetadata() {
ReSTDelegateImpl delegate = newDelegate(RestTestConfig.readonlyConfig());
MetaData metaData = delegate.getMetaData();
assertThat(metaData.getColumns().get(0), is(new Column("id",
String.class, false, false, true, null)));
}
@Test
public void shouldBeReadonlyByDefault() {
ReSTDelegateImpl delegate = newDelegate(RestTestConfig.readonlyConfig());
MetaData metaData = delegate.getMetaData();
assertThat(metaData.isInsertSupported(), is(false));
assertThat(metaData.isUpdateSupported(), is(false));
assertThat(metaData.isRemoveSupported(), is(false));
}
@Test
public void shouldAllowInsertIfConfigured() {
ReSTContainerConfig config = RestTestConfig.readonlyConfig();
config.setInsert(new ReSTChangeConfig());
ReSTDelegateImpl delegate = newDelegate(config);
MetaData metaData = delegate.getMetaData();
assertThat(metaData.isInsertSupported(), is(true));
}
@Test
public void shouldAllowUpdateIfConfigured() {
ReSTContainerConfig config = RestTestConfig.readonlyConfig();
config.setUpdate(new ReSTChangeConfig());
ReSTDelegateImpl delegate = newDelegate(config);
MetaData metaData = delegate.getMetaData();
assertThat(metaData.isUpdateSupported(), is(true));
}
@Test
public void shouldAllowDeleteIfConfigured() {
ReSTContainerConfig config = RestTestConfig.readonlyConfig();
config.setDelete(new ReSTDeleteConfig());
ReSTDelegateImpl delegate = newDelegate(config);
MetaData metaData = delegate.getMetaData();
assertThat(metaData.isRemoveSupported(), is(true));
}
@Test
public void shouldTellThatIsNotTransactional() {
ReSTDelegateImpl delegate = newDelegate(RestTestConfig.readonlyConfig());
MetaData metaData = delegate.getMetaData();
assertThat(metaData.isTransactional(), is(false));
}
@Test
public void shouldTellThatBackendFilteringIsNotPossible() {
ReSTDelegateImpl delegate = newDelegate(RestTestConfig.readonlyConfig());
MetaData metaData = delegate.getMetaData();
assertThat(metaData.isFilterSupported(), is(false));
}
@Test
public void shouldReturmEmptyListIfQueryUrlIsNullOrBlank()
throws ClientProtocolException, IOException {
ReSTContainerConfig config = RestTestConfig.readonlyConfig();
config.setBaseUrl(RestTestConfig
.createGStringScript(("http://test.de/path")));
config.getQuery().setUrl(RestTestConfig.createGStringScript(""));
ReSTDelegateImpl delegate = newDelegate(config);
List<Object[]> result = delegate.getRows();
assertThat(result.size(), is(0));
}
@Test
public void shouldUseQueryUrlIfBaseUrlIsMissing()
throws ClientProtocolException, IOException {
ReSTContainerConfig config = RestTestConfig.readonlyConfig();
config.setBaseUrl(null);
ReSTDelegateImpl delegate = newDelegate(config);
assumeValidResponse();
delegate.getRows();
verify(httpMock).execute(requestCaptor.capture());
assertThat(requestCaptor.getValue().getURI().toString(),
is("http://test.de/path"));
}
@Test
public void shouldSendConfiguredMimetypeInAcceptHeader()
throws ClientProtocolException, IOException {
ReSTContainerConfig config = RestTestConfig.readonlyConfig();
config.setMimetype("text/xml");
ReSTDelegateImpl delegate = newDelegate(config);
assumeValidResponse();
when(creatorMock.getMimeType()).thenReturn("application/xml");
delegate.getRows();
verify(httpMock).execute(requestCaptor.capture());
HttpUriRequest request = requestCaptor.getValue();
assertThat(request.getFirstHeader("Accept").getValue(), is("text/xml"));
}
@Test
public void shouldSendDefaultMimetypeInAcceptHeader()
throws ClientProtocolException, IOException {
ReSTContainerConfig config = RestTestConfig.readonlyConfig();
config.setMimetype(null);
ReSTDelegateImpl delegate = newDelegate(config);
assumeValidResponse();
when(creatorMock.getMimeType()).thenReturn("application/xml");
delegate.getRows();
verify(httpMock).execute(requestCaptor.capture());
HttpUriRequest request = requestCaptor.getValue();
assertThat(request.getFirstHeader("Accept").getValue(),
is("application/xml"));
}
@Test
public void shouldConcatBaseUrlAndQueryUrl()
throws ClientProtocolException, IOException {
ReSTContainerConfig config = RestTestConfig.readonlyConfig();
config.setBaseUrl(RestTestConfig.createGStringScript("http://test.de/"));
config.getQuery().setUrl(RestTestConfig.createGStringScript("path"));
ReSTDelegateImpl delegate = newDelegate(config);
assumeValidResponse();
delegate.getRows();
verify(httpMock).execute(requestCaptor.capture());
assertThat(requestCaptor.getValue().getURI().toString(),
is("http://test.de/path"));
}
@Test
public void shouldAllowChangingTheBaseUrlAtRuntime()
throws ClientProtocolException, IOException {
ReSTContainerConfig config = RestTestConfig.readonlyConfig();
config.setBaseUrl(RestTestConfig.createGStringScript("http://test.de/"));
config.getQuery().setUrl(RestTestConfig.createGStringScript("path"));
ReSTDelegateImpl delegate = newDelegate(config);
delegate.setBaseUrl("http://test.nl/");
assumeValidResponse();
delegate.getRows();
verify(httpMock).execute(requestCaptor.capture());
assertThat(requestCaptor.getValue().getURI().toString(),
is("http://test.nl/path"));
}
@Test
public void shouldAllowChangingTheQueryUrlAtRuntime()
throws ClientProtocolException, IOException {
ReSTContainerConfig config = RestTestConfig.readonlyConfig();
config.setBaseUrl(RestTestConfig.createGStringScript("http://test.de/"));
config.getQuery().setUrl(RestTestConfig.createGStringScript("path"));
ReSTDelegateImpl delegate = newDelegate(config);
delegate.setQueryUrl("otherpath");
assumeValidResponse();
delegate.getRows();
verify(httpMock).execute(requestCaptor.capture());
assertThat(requestCaptor.getValue().getURI().toString(),
is("http://test.de/otherpath"));
}
@Test(expected = BusinessException.class)
public void shouldThrowReadableExceptionOnWrongResultCode()
throws ClientProtocolException, IOException {
ReSTDelegateImpl delegate = newDelegate(RestTestConfig.readonlyConfig());
assumeInvalidResponse(HttpStatus.SC_NOT_FOUND, "NOT FOUND");
delegate.getRows();
}
@Test(expected = InvalidConfigurationException.class)
public void shouldThrowConfigurationExceptionIfUrlIsInvalid() {
ReSTContainerConfig config = RestTestConfig.readonlyConfig();
config.getQuery().setUrl(RestTestConfig.createGStringScript("\\\\"));
ReSTDelegateImpl delegate = newDelegate(config);
delegate.getRows();
}
@Test(expected = BusinessException.class)
public void shouldThrowReadableExceptionOnIOErrors() throws IOException {
ReSTDelegateImpl delegate = newDelegate(RestTestConfig.readonlyConfig());
assumeValidResponse();
when(parserMock.getRows(responseMock)).thenThrow(
new IOException("Connection problem"));
delegate.getRows();
}
@Test(expected = BusinessException.class)
public void shouldThrowReadableExceptionOnProtocolErrors()
throws IOException {
ReSTDelegateImpl delegate = newDelegate(RestTestConfig.readonlyConfig());
when(httpMock.execute(any(HttpUriRequest.class))).thenThrow(
new ClientProtocolException("bla"));
delegate.getRows();
}
@Test
public void shouldReturnResultFromParser() throws ClientProtocolException,
IOException {
ReSTDelegateImpl delegate = newDelegate(RestTestConfig.readonlyConfig());
assumeValidResponse();
List<Object[]> expectedResult = asList(new Object[2], new Object[2]);
when(parserMock.getRows(responseMock)).thenReturn(expectedResult);
List<Object[]> result = delegate.getRows();
assertThat(result, sameInstance(expectedResult));
}
@Test
public void shouldSendInsertPostRequestToServer()
throws ClientProtocolException, IOException {
// given
config = RestTestConfig.readwriteConfig();
delegate = newDelegate(config);
stubValidInsertOperation();
stubSuccessfulPostResponse();
// when
delegate.update(asList(itemMock), new UpdateContext());
// then
HttpPost postRequest = capturePostRequest();
HttpEntity entity = postRequest.getEntity();
assertThat(postRequest.getURI().toString(),
is("http://test.de/insertpath/4711"));
assertArrayEquals(contents, IOUtils.toByteArray(entity.getContent()));
assertThat(ContentType.getOrDefault(entity).getCharset(),
is(Charset.forName("UTF-8")));
assertThat(ContentType.getOrDefault(entity).getMimeType(),
is("text/json"));
}
@Test
public void shouldAuditPostRequest() throws ClientProtocolException,
IOException {
// given
config = RestTestConfig.readwriteConfig();
delegate = newDelegate(config);
stubValidInsertOperation();
stubSuccessfulPostResponse();
// when
delegate.update(asList(itemMock), new UpdateContext());
// then
verify(auditLoggerMock).auditReSTRequest("POST",
"http://test.de/insertpath/4711", JSON_STRING,
"HTTP/1.1 201 CREATED");
}
@Test
public void shouldSendInsertPutRequestIfConfigured()
throws ClientProtocolException, IOException {
// given
config = RestTestConfig.readwriteConfig();
config.getInsert().setMethod(ReSTChangeMethodConfig.PUT);
delegate = newDelegate(config);
stubValidInsertOperation();
stubSuccessfulPutResponse();
// when
delegate.update(asList(itemMock), new UpdateContext());
// then
verify(httpMock).execute(isA(HttpPut.class));
}
@Test
public void shouldAuditPutRequest() throws ClientProtocolException,
IOException {
// given
config = RestTestConfig.readwriteConfig();
config.getInsert().setMethod(ReSTChangeMethodConfig.PUT);
delegate = newDelegate(config);
stubValidInsertOperation();
stubSuccessfulPutResponse();
// when
delegate.update(asList(itemMock), new UpdateContext());
// then
verify(auditLoggerMock).auditReSTRequest("PUT",
"http://test.de/insertpath/4711", JSON_STRING,
"HTTP/1.1 200 CREATED");
}
@Test
public void shouldSendUpdatePutRequestToServer()
throws ClientProtocolException, IOException {
// given
config = RestTestConfig.readwriteConfig();
delegate = newDelegate(config);
stubValidUpdateOperation();
stubSuccessfulPutResponse();
// when
delegate.update(asList(itemMock), new UpdateContext());
// then
HttpPut request = capturePutRequest();
HttpEntity entity = request.getEntity();
assertThat(request.getURI().toString(),
is("http://test.de/insertpath/4711"));
assertArrayEquals(contents, IOUtils.toByteArray(entity.getContent()));
assertThat(ContentType.getOrDefault(entity).getCharset(),
is(Charset.forName("UTF-8")));
assertThat(ContentType.getOrDefault(entity).getMimeType(),
is("text/json"));
}
@Test
public void shouldSendUpdatePostRequestIfConfigured()
throws ClientProtocolException, IOException {
// given
config = RestTestConfig.readwriteConfig();
config.getUpdate().setMethod(ReSTChangeMethodConfig.POST);
delegate = newDelegate(config);
stubValidUpdateOperation();
stubSuccessfulPostResponse();
// when
delegate.update(asList(itemMock), new UpdateContext());
// then
verify(httpMock).execute(isA(HttpPost.class));
}
@Test
public void shouldSendConfiguredMimetype() throws ClientProtocolException,
IOException {
// given
config = RestTestConfig.readwriteConfig();
config.setMimetype("text/xml");
delegate = newDelegate(config);
stubValidUpdateOperation();
stubSuccessfulPutResponse();
// when
delegate.update(asList(itemMock), new UpdateContext());
// then
HttpPut request = capturePutRequest();
HttpEntity entity = request.getEntity();
assertThat(ContentType.getOrDefault(entity).getMimeType(),
is("text/xml"));
}
@Test
public void shouldSendDeleteRequestToServer()
throws ClientProtocolException, IOException {
// given
config = RestTestConfig.readwriteConfig();
delegate = newDelegate(config);
prepareValidDeleteRequest();
// when
delegate.update(asList(itemMock), new UpdateContext());
// then
HttpDelete request = captureDeleteRequest();
assertThat(request.getURI().toString(),
is("http://test.de/deletepath/4711"));
}
@Test
public void shouldAuditDeleteRequest() throws ClientProtocolException,
IOException {
// given
config = RestTestConfig.readwriteConfig();
delegate = newDelegate(config);
prepareValidDeleteRequest();
// when
delegate.update(asList(itemMock), new UpdateContext());
// then
verify(auditLoggerMock).auditReSTRequest("DELETE",
"http://test.de/deletepath/4711", "HTTP/1.1 200 DELETED");
}
@Test
public void shouldTriggerContainerRefreshOnUpdate()
throws ClientProtocolException, IOException {
// given
config = RestTestConfig.readwriteConfig();
delegate = newDelegate(config);
stubValidInsertOperation();
stubSuccessfulPostResponse();
UpdateContext context = new UpdateContext();
// when
delegate.update(asList(itemMock), context);
assertThat(context.isRefreshRequired(), is(true));
}
@Test(expected = BusinessException.class)
public void shouldThrowExceptionOnWrongResponseCode()
throws ClientProtocolException, IOException {
// given
config = RestTestConfig.readwriteConfig();
delegate = newDelegate(config);
stubValidInsertOperation();
stubSuccessfulPostResponse();
when(responseMock.getStatusLine()).thenReturn(
new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1),
HttpStatus.SC_FORBIDDEN, "FORBIDDEN"));
// when
delegate.update(asList(itemMock), new UpdateContext());
}
@Test
public void shouldApplyBasicAuthIfRealmIsGiven() {
config = RestTestConfig.readwriteConfig();
config.setRealm("testserver");
delegate = new ReSTDelegateImpl(config, containerMock, realmMock,
scriptBuilderMock, auditLoggerMock);
verify(realmMock).applyBasicAuthentication(
(DefaultHttpClient) delegate.httpClient);
}
private void stubSuccessfulPostResponse() throws ClientProtocolException,
IOException {
when(httpMock.execute(any(HttpUriRequest.class))).thenReturn(
responseMock);
when(responseMock.getStatusLine()).thenReturn(
new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 201,
"CREATED"));
}
private void stubValidInsertOperation() throws ClientProtocolException,
IOException {
// Input for URL
when(scriptBuilderMock.buildClosure(config.getInsert().getUrl()))
.thenReturn(urlClosureMock);
when(urlClosureMock.call(any(ScriptRow.class))).thenReturn(
"http://test.de/insertpath/4711");
when(itemMock.isNewItem()).thenReturn(true);
// fake for Script
when(itemMock.getId()).thenReturn(itemIdMock);
when(containerMock.getRowByInternalRowId(itemIdMock, false, true))
.thenReturn(containerRowMock);
contents = JSON_STRING.getBytes("UTF-8");
when(creatorMock.getMimeType()).thenReturn("text/json");
when(
creatorMock.create(itemMock, config.getInsert().getValue(),
"UTF-8")).thenReturn(contents);
}
private void stubValidUpdateOperation() throws UnsupportedEncodingException {
// Input for URL
when(scriptBuilderMock.buildClosure(config.getUpdate().getUrl()))
.thenReturn(urlClosureMock);
when(urlClosureMock.call(any(ScriptRow.class))).thenReturn(
"http://test.de/insertpath/4711");
when(itemMock.isModified()).thenReturn(true);
// fake for Script
when(itemMock.getId()).thenReturn(itemIdMock);
when(containerMock.getRowByInternalRowId(itemIdMock, false, true))
.thenReturn(containerRowMock);
contents = JSON_STRING.getBytes("UTF-8");
when(creatorMock.getMimeType()).thenReturn("text/json");
when(
creatorMock.create(itemMock, config.getUpdate().getValue(),
"UTF-8")).thenReturn(contents);
}
private void stubSuccessfulPutResponse() throws IOException,
ClientProtocolException {
when(httpMock.execute(any(HttpUriRequest.class))).thenReturn(
responseMock);
when(responseMock.getStatusLine()).thenReturn(
new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200,
"CREATED"));
}
private void prepareValidDeleteRequest() throws ClientProtocolException,
IOException {
// Input for URL
when(scriptBuilderMock.buildClosure(config.getDelete().getUrl()))
.thenReturn(urlClosureMock);
when(urlClosureMock.call(any(ScriptRow.class))).thenReturn(
"http://test.de/deletepath/4711");
when(itemMock.isDeleted()).thenReturn(true);
// fake for Script
when(itemMock.getId()).thenReturn(itemIdMock);
when(containerMock.getRowByInternalRowId(itemIdMock, false, true))
.thenReturn(containerRowMock);
when(httpMock.execute(any(HttpUriRequest.class))).thenReturn(
responseMock);
when(responseMock.getStatusLine()).thenReturn(
new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1),
HttpStatus.SC_OK, "DELETED"));
}
private HttpPost capturePostRequest() throws IOException,
ClientProtocolException {
verify(httpMock).execute(postRequestCaptor.capture());
HttpPost postRequest = postRequestCaptor.getValue();
return postRequest;
}
private HttpPut capturePutRequest() throws IOException,
ClientProtocolException {
verify(httpMock).execute(putRequestCaptor.capture());
HttpPut putRequest = putRequestCaptor.getValue();
return putRequest;
}
private HttpDelete captureDeleteRequest() throws IOException,
ClientProtocolException {
verify(httpMock).execute(deleteRequestCaptor.capture());
HttpDelete request = deleteRequestCaptor.getValue();
return request;
}
private void assumeValidResponse() throws ClientProtocolException,
IOException {
assumeValidQueryResponse(JSON_STRING, "text/json", "utf-8");
}
private void assumeInvalidResponse(int status, String message)
throws ClientProtocolException, IOException {
when(httpMock.execute(any(HttpUriRequest.class))).thenReturn(
responseMock);
when(responseMock.getStatusLine()).thenReturn(
new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), status, message));
}
private void assumeValidQueryResponse(String content, String mimeType,
String charset) throws IOException, ClientProtocolException {
when(httpMock.execute(any(HttpUriRequest.class))).thenReturn(
responseMock);
when(responseMock.getStatusLine()).thenReturn(
new BasicStatusLine(new ProtocolVersion("HTTP", 1, 1), 200,
"OK"));
when(responseMock.getEntity())
.thenReturn(
new StringEntity(content, ContentType.create(mimeType,
charset)));
}
private ReSTDelegateImpl newDelegate(ReSTContainerConfig config) {
ReSTDelegateImpl delegate = new ReSTDelegateImpl(config, containerMock,
realmMock, scriptBuilderMock, auditLoggerMock);
delegate.httpClient = httpMock;
delegate.parser = parserMock;
delegate.creator = creatorMock;
return delegate;
}
}