Hooks - Validation and Caching

This commit is contained in:
avikganguly01
2014-10-16 18:18:47 +05:30
parent 3847186f2b
commit 84c321cfe8
11 changed files with 212 additions and 17 deletions
@@ -0,0 +1,66 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.integrationtests;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import org.apache.http.conn.HttpHostConnectException;
import org.junit.Before;
import org.junit.Test;
import org.mifosplatform.integrationtests.common.HookHelper;
import org.mifosplatform.integrationtests.common.OfficeHelper;
import org.mifosplatform.integrationtests.common.Utils;
import com.jayway.restassured.RestAssured;
import com.jayway.restassured.builder.RequestSpecBuilder;
import com.jayway.restassured.builder.ResponseSpecBuilder;
import com.jayway.restassured.http.ContentType;
import com.jayway.restassured.path.json.JsonPath;
import com.jayway.restassured.specification.RequestSpecification;
import com.jayway.restassured.specification.ResponseSpecification;
public class HookIntegrationTest {
private RequestSpecification requestSpec;
private ResponseSpecification responseSpec;
private HookHelper hookHelper;
private OfficeHelper officeHelper;
@Before
public void setUp() throws Exception {
Utils.initializeRESTAssured();
this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
this.hookHelper = new HookHelper(this.requestSpec, this.responseSpec);
this.officeHelper = new OfficeHelper(this.requestSpec, this.responseSpec);
}
@Test
public void shouldSendOfficeCreationNotification() {
//Subject to https://echo-webhook.herokuapp.com being up
//See http://www.jamesward.com/2014/06/11/testing-webhooks-was-a-pain-so-i-fixed-the-glitch
final String payloadURL = "http://echo-webhook.herokuapp.com:80/Z7RXoCBdLSFMDrpn?";
this.hookHelper.createHook(payloadURL);
final Integer createdOfficeID = this.officeHelper.createOffice("01 January 2012");
try {
final String json = RestAssured.get(payloadURL.replace("?", "")).asString();
final Integer notificationOfficeId = JsonPath.with(json).get("officeId");
assertEquals("Equality check for created officeId and hook received payload officeId",
createdOfficeID, notificationOfficeId);
System.out.println("Notification Office Id - " + notificationOfficeId);
} catch (final Exception e) {
if (e instanceof HttpHostConnectException) {
fail("Failed to connect to https://echo-webhook.herokuapp.com platform");
}
throw new RuntimeException(e);
}
}
}
@@ -0,0 +1,52 @@
/**
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package org.mifosplatform.integrationtests.common;
import java.util.ArrayList;
import java.util.HashMap;
import com.google.gson.Gson;
import com.jayway.restassured.specification.RequestSpecification;
import com.jayway.restassured.specification.ResponseSpecification;
public class HookHelper {
private final RequestSpecification requestSpec;
private final ResponseSpecification responseSpec;
private static final String CREATE_HOOK_URL = "/mifosng-provider/api/v1/hooks?" + Utils.TENANT_IDENTIFIER;
public HookHelper(final RequestSpecification requestSpec, final ResponseSpecification responseSpec) {
this.requestSpec = requestSpec;
this.responseSpec = responseSpec;
}
public Integer createHook(final String payloadURL) {
System.out.println("---------------------------------CREATING A HOOK---------------------------------------------");
return Utils.performServerPost(requestSpec, responseSpec, CREATE_HOOK_URL, getTestHookAsJson(payloadURL),
"resourceId");
}
public String getTestHookAsJson(final String payloadURL) {
final HashMap<String, Object> map = new HashMap<>();
map.put("name", "Web");
map.put("displayName", Utils.randomNameGenerator("Hook_DisplayName_", 5));
map.put("isActive", "true");
final HashMap<String, String> config = new HashMap<>();
config.put("Content Type", "json");
config.put("Payload URL", payloadURL);
map.put("config", config);
final ArrayList<HashMap<String, String>> events = new ArrayList<>();
final HashMap<String, String> createOfficeEvent = new HashMap<>();
createOfficeEvent.put("actionName", "CREATE");
createOfficeEvent.put("entityName", "OFFICE");
events.add(createOfficeEvent);
map.put("events", events);
System.out.println("map : " + map);
return new Gson().toJson(map);
}
}
@@ -41,6 +41,7 @@ public class WebXmlConfiguration {
jerseyServletRegistration.addInitParameter("com.sun.jersey.api.json.POJOMappingFeature", "true");
jerseyServletRegistration.addInitParameter("com.sun.jersey.spi.container.ContainerResponseFilters",
ResponseCorsFilter.class.getName());
jerseyServletRegistration.addInitParameter("com.sun.jersey.config.feature.DisableWADL", "true");
// debugging for development:
// jerseyServletRegistration.addInitParameter("com.sun.jersey.spi.container.ContainerRequestFilters",
// LoggingFilter.class.getName());
@@ -65,8 +65,10 @@ public class Hook extends AbstractAuditableCustom<AppUser, Long> {
final Set<HookResource> events) {
final String displayName = command
.stringValueOfParameterNamed(displayNameParamName);
final Boolean isActive = command
Boolean isActive = command
.booleanObjectValueOfParameterNamed(isActiveParamName);
if(isActive == null)
isActive = false;
return new Hook(template, displayName, isActive, config, events);
}
@@ -10,11 +10,11 @@ import java.util.List;
import org.mifosplatform.infrastructure.core.domain.MifosPlatformTenant;
import org.mifosplatform.infrastructure.core.service.ThreadLocalContextUtil;
import org.mifosplatform.infrastructure.hooks.domain.Hook;
import org.mifosplatform.infrastructure.hooks.domain.HookRepository;
import org.mifosplatform.infrastructure.hooks.event.HookEvent;
import org.mifosplatform.infrastructure.hooks.event.HookEventSource;
import org.mifosplatform.infrastructure.hooks.processor.HookProcessor;
import org.mifosplatform.infrastructure.hooks.processor.HookProcessorProvider;
import org.mifosplatform.infrastructure.hooks.service.HookReadPlatformService;
import org.mifosplatform.infrastructure.security.service.TenantDetailsService;
import org.mifosplatform.useradministration.domain.AppUser;
import org.springframework.beans.factory.annotation.Autowired;
@@ -24,14 +24,14 @@ import org.springframework.stereotype.Service;
public class MifosHookListener implements HookListener {
private final HookProcessorProvider hookProcessorProvider;
private final HookRepository hookRepository;
private final HookReadPlatformService hookReadPlatformService;
private final TenantDetailsService tenantDetailsService;
@Autowired
public MifosHookListener(final HookProcessorProvider hookProcessorProvider,
final HookRepository hookRepository,
final HookReadPlatformService hookReadPlatformService,
final TenantDetailsService tenantDetailsService) {
this.hookRepository = hookRepository;
this.hookReadPlatformService = hookReadPlatformService;
this.hookProcessorProvider = hookProcessorProvider;
this.tenantDetailsService = tenantDetailsService;
}
@@ -51,10 +51,11 @@ public class MifosHookListener implements HookListener {
final String entityName = hookEventSource.getEntityName();
final String actionName = hookEventSource.getActionName();
final String payload = event.getPayload();
final List<Hook> hooks = this.hookReadPlatformService
.retrieveHooksByEvent(hookEventSource.getEntityName(),
hookEventSource.getActionName());
final List<Hook> hooks = this.hookRepository
.findAllHooksListeningToEvent(hookEventSource.getEntityName(),
hookEventSource.getActionName());
for (final Hook hook : hooks) {
final HookProcessor processor = this.hookProcessorProvider
.getProcessor(hook);
@@ -57,7 +57,7 @@ public class WebHookProcessor implements HookProcessor {
private void sendRequest(final String url, final String contentType,
final String payload, final String entityName,
final String actionName, final String tenantIdentifier,
final String authToken) {
@SuppressWarnings("unused") final String authToken) {
final String mifosEndpointUrl = System.getProperty("baseUrl");
final WebHookService service = ProcessorHelper
@@ -70,13 +70,11 @@ public class WebHookProcessor implements HookProcessor {
|| contentType.contains("json")) {
final JsonObject json = new JsonParser().parse(payload)
.getAsJsonObject();
json.addProperty("mifosToken", authToken);
service.sendJsonRequest(entityName, actionName, tenantIdentifier,
mifosEndpointUrl, json, callback);
} else {
Map<String, String> map = new HashMap<>();
map = new Gson().fromJson(payload, map.getClass());
map.put("mifosToken", authToken);
service.sendFormRequest(entityName, actionName, tenantIdentifier,
mifosEndpointUrl, map, callback);
}
@@ -14,6 +14,7 @@ import retrofit.client.Response;
import retrofit.http.Body;
import retrofit.http.FieldMap;
import retrofit.http.FormUrlEncoded;
import retrofit.http.GET;
import retrofit.http.Header;
import retrofit.http.POST;
@@ -27,6 +28,10 @@ public interface WebHookService {
final static String ENDPOINT_HEADER = "X-Mifos-Endpoint";
final static String API_KEY_HEADER = "X-Mifos-API-Key";
// Ping
@GET("/")
Response sendEmptyRequest();
// Template - Web
@POST("/")
void sendJsonRequest(@Header(ENTITY_HEADER) String entityHeader,
@@ -6,8 +6,10 @@
package org.mifosplatform.infrastructure.hooks.service;
import java.util.Collection;
import java.util.List;
import org.mifosplatform.infrastructure.hooks.data.HookData;
import org.mifosplatform.infrastructure.hooks.domain.Hook;
public interface HookReadPlatformService {
@@ -15,5 +17,7 @@ public interface HookReadPlatformService {
HookData retrieveHook(Long hookId);
HookData retrieveNewHookDetails(String templateName);
List<Hook> retrieveHooksByEvent(final String actionName, final String entityName);
HookData retrieveNewHookDetails(String templateName);
}
@@ -19,9 +19,12 @@ import org.mifosplatform.infrastructure.hooks.data.Field;
import org.mifosplatform.infrastructure.hooks.data.Grouping;
import org.mifosplatform.infrastructure.hooks.data.HookData;
import org.mifosplatform.infrastructure.hooks.data.HookTemplateData;
import org.mifosplatform.infrastructure.hooks.domain.Hook;
import org.mifosplatform.infrastructure.hooks.domain.HookRepository;
import org.mifosplatform.infrastructure.hooks.exception.HookNotFoundException;
import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
@@ -31,12 +34,15 @@ import org.springframework.stereotype.Service;
public class HookReadPlatformServiceImpl implements HookReadPlatformService {
private final JdbcTemplate jdbcTemplate;
private final HookRepository hookRepository;
private final PlatformSecurityContext context;
@Autowired
public HookReadPlatformServiceImpl(final PlatformSecurityContext context,
final HookRepository hookRepository,
final RoutingDataSource dataSource) {
this.context = context;
this.hookRepository = hookRepository;
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
@@ -64,6 +70,15 @@ public class HookReadPlatformServiceImpl implements HookReadPlatformService {
}
@Override
@Cacheable(value = "hooks", key = "T(org.mifosplatform.infrastructure.core.service.ThreadLocalContextUtil).getTenant().getTenantIdentifier().concat('HK')")
public List<Hook> retrieveHooksByEvent(final String actionName,
final String entityName) {
return this.hookRepository.findAllHooksListeningToEvent(actionName,
entityName);
}
@Override
public HookData retrieveNewHookDetails(final String templateName) {
@@ -7,9 +7,11 @@ package org.mifosplatform.infrastructure.hooks.service;
import static org.mifosplatform.infrastructure.hooks.api.HookApiConstants.actionNameParamName;
import static org.mifosplatform.infrastructure.hooks.api.HookApiConstants.configParamName;
import static org.mifosplatform.infrastructure.hooks.api.HookApiConstants.contentTypeName;
import static org.mifosplatform.infrastructure.hooks.api.HookApiConstants.entityNameParamName;
import static org.mifosplatform.infrastructure.hooks.api.HookApiConstants.eventsParamName;
import static org.mifosplatform.infrastructure.hooks.api.HookApiConstants.nameParamName;
import static org.mifosplatform.infrastructure.hooks.api.HookApiConstants.payloadURLName;
import static org.mifosplatform.infrastructure.hooks.api.HookApiConstants.webTemplateName;
import java.util.ArrayList;
@@ -36,13 +38,18 @@ import org.mifosplatform.infrastructure.hooks.domain.HookTemplateRepository;
import org.mifosplatform.infrastructure.hooks.domain.Schema;
import org.mifosplatform.infrastructure.hooks.exception.HookNotFoundException;
import org.mifosplatform.infrastructure.hooks.exception.HookTemplateNotFoundException;
import org.mifosplatform.infrastructure.hooks.processor.ProcessorHelper;
import org.mifosplatform.infrastructure.hooks.processor.WebHookService;
import org.mifosplatform.infrastructure.hooks.serialization.HookCommandFromApiJsonDeserializer;
import org.mifosplatform.infrastructure.security.service.PlatformSecurityContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import retrofit.RetrofitError;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
@@ -72,6 +79,7 @@ public class HookWritePlatformServiceJpaRepositoryImpl implements
@Transactional
@Override
@CacheEvict(value = "hooks", allEntries = true)
public CommandProcessingResult createHook(final JsonCommand command) {
try {
@@ -90,7 +98,7 @@ public class HookWritePlatformServiceJpaRepositoryImpl implements
final Hook hook = Hook.fromJson(command, template, config,
allEvents);
validateHookRules(template, config);
validateHookRules(template, config, allEvents);
this.hookRepository.save(hook);
@@ -105,6 +113,7 @@ public class HookWritePlatformServiceJpaRepositoryImpl implements
@Transactional
@Override
@CacheEvict(value = "hooks", allEntries = true)
public CommandProcessingResult updateHook(final Long hookId,
final JsonCommand command) {
@@ -156,6 +165,7 @@ public class HookWritePlatformServiceJpaRepositoryImpl implements
@Transactional
@Override
@CacheEvict(value = "hooks", allEntries = true)
public CommandProcessingResult deleteHook(final Long hookId) {
this.context.authenticatedUser();
@@ -200,7 +210,9 @@ public class HookWritePlatformServiceJpaRepositoryImpl implements
for (final Entry<String, String> configEntry : hookConfig.entrySet()) {
for (final Schema field : fields) {
if (field.getFieldName().equalsIgnoreCase(configEntry.getKey())) {
final String fieldName = field.getFieldName();
if (fieldName.equalsIgnoreCase(configEntry.getKey())) {
final HookConfiguration config = HookConfiguration
.createNewWithoutHook(field.getFieldType(),
configEntry.getKey(),
@@ -237,7 +249,7 @@ public class HookWritePlatformServiceJpaRepositoryImpl implements
}
private void validateHookRules(final HookTemplate template,
final Set<HookConfiguration> config) {
final Set<HookConfiguration> config, Set<HookResource> events) {
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(
@@ -250,6 +262,43 @@ public class HookWritePlatformServiceJpaRepositoryImpl implements
errorMessage);
}
for (final HookConfiguration conf : config) {
final String fieldValue = conf.getFieldValue();
if (conf.getFieldName().equals(contentTypeName)) {
if (!(fieldValue.equalsIgnoreCase("json") || fieldValue
.equalsIgnoreCase("form"))) {
final String errorMessage = "content.type.must.be.json.or.form";
baseDataValidator.reset()
.failWithCodeNoParameterAddedToErrorCode(
errorMessage);
}
}
if (conf.getFieldName().equals(payloadURLName)) {
try {
final WebHookService service = ProcessorHelper
.createWebHookService(fieldValue);
service.sendEmptyRequest();
} catch (RetrofitError re) {
// Swallow error if it's because of method not supported or
// if url throws 404 - required for integration test,
// url generated on 1st POST request
if (re.getResponse() == null) {
String errorMessage = "url.invalid";
baseDataValidator.reset()
.failWithCodeNoParameterAddedToErrorCode(
errorMessage);
}
}
}
}
if (events == null || events.isEmpty()) {
final String errorMessage = "registered.events.cannot.be.empty";
baseDataValidator.reset().failWithCodeNoParameterAddedToErrorCode(
errorMessage);
}
final Set<Schema> fields = template.getSchema();
for (final Schema field : fields) {
if (!field.isOptional()) {
@@ -261,8 +310,8 @@ public class HookWritePlatformServiceJpaRepositoryImpl implements
}
if (!found) {
final String errorMessage = "required.config.field."
+ field.getFieldName() + ".not.provided";
baseDataValidator.reset()
+ "not.provided";
baseDataValidator.reset().value(field.getFieldName())
.failWithCodeNoParameterAddedToErrorCode(
errorMessage);
}
@@ -34,4 +34,6 @@
overflowToDisk="false" />
<cache name="codes" maxEntriesLocalHeap="10000" eternal="true"
overflowToDisk="false" />
<cache name="hooks" maxEntriesLocalHeap="10000" eternal="true"
overflowToDisk="false" />
</ehcache>