diff --git a/pom.xml b/pom.xml index 1768ccf9af1..075df7d5eb6 100644 --- a/pom.xml +++ b/pom.xml @@ -269,7 +269,6 @@ true - ${basedir}/src/test/resources @@ -277,7 +276,6 @@ **/*.* - ${basedir}/rest-api-spec rest-api-spec @@ -287,7 +285,6 @@ - org.apache.maven.plugins diff --git a/src/main/java/org/elasticsearch/watcher/actions/index/IndexAction.java b/src/main/java/org/elasticsearch/watcher/actions/index/IndexAction.java index 8e46cb8bbab..5770c92090f 100644 --- a/src/main/java/org/elasticsearch/watcher/actions/index/IndexAction.java +++ b/src/main/java/org/elasticsearch/watcher/actions/index/IndexAction.java @@ -38,8 +38,8 @@ public class IndexAction extends Action { private final ClientProxy client; - private final String index; - private final String type; + final String index; + final String type; public IndexAction(ESLogger logger, @Nullable Transform transform, ClientProxy client, String index, String type) { super(logger, transform); @@ -235,8 +235,8 @@ public class IndexAction extends Action { public static class Result extends Action.Result { - private final Payload response; - private final String reason; + final Payload response; + final String reason; public Result(Payload response, String reason, boolean isCreated) { super(TYPE, isCreated); diff --git a/src/main/java/org/elasticsearch/watcher/actions/webhook/WebhookAction.java b/src/main/java/org/elasticsearch/watcher/actions/webhook/WebhookAction.java index 7d0e32df67c..bfb0a0bde86 100644 --- a/src/main/java/org/elasticsearch/watcher/actions/webhook/WebhookAction.java +++ b/src/main/java/org/elasticsearch/watcher/actions/webhook/WebhookAction.java @@ -7,6 +7,7 @@ package org.elasticsearch.watcher.actions.webhook; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.base.Charsets; import org.elasticsearch.common.component.AbstractComponent; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.logging.ESLogger; @@ -23,15 +24,15 @@ import org.elasticsearch.watcher.support.http.HttpClient; import org.elasticsearch.watcher.support.http.HttpMethod; import org.elasticsearch.watcher.support.http.HttpResponse; import org.elasticsearch.watcher.support.template.Template; -import org.elasticsearch.watcher.support.template.XContentTemplate; import org.elasticsearch.watcher.transform.Transform; import org.elasticsearch.watcher.transform.TransformRegistry; import org.elasticsearch.watcher.watch.Payload; import org.elasticsearch.watcher.watch.WatchExecutionContext; import java.io.IOException; -import java.util.Locale; -import java.util.Map; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.*; /** */ @@ -41,9 +42,9 @@ public class WebhookAction extends Action { private final HttpClient httpClient; - private final HttpMethod method; - private final Template url; - private final @Nullable Template body; + final HttpMethod method; + final Template url; + final @Nullable Template body; public WebhookAction(ESLogger logger, @Nullable Transform transform, HttpClient httpClient, HttpMethod method, Template url, Template body) { super(logger, transform); @@ -61,8 +62,14 @@ public class WebhookAction extends Action { @Override protected Result execute(WatchExecutionContext ctx, Payload payload) throws IOException { Map model = Variables.createCtxModel(ctx, payload); - String urlText = url.render(model); - String bodyText = body != null ? body.render(model) : XContentTemplate.YAML.render(model); + Map urlSafeModel = new HashMap<>(model.size()); + + for (Map.Entry entry : model.entrySet()) { + urlSafeModel.put(entry.getKey(), makeURLSafe(entry.getValue())); + } + + String urlText = url.render(urlSafeModel); + String bodyText = body != null ? body.render(model) : ""; //If body is null send an empty body try { try (HttpResponse response = httpClient.execute(method, urlText, bodyText)) { int status = response.status(); @@ -158,7 +165,7 @@ public class WebhookAction extends Action { @Override protected XContentBuilder xContentBody(XContentBuilder builder, Params params) throws IOException { - return builder.field("success", success()) + return builder.field(SUCCESS_FIELD.getPreferredName(), success()) .field(WebhookAction.Parser.HTTP_STATUS_FIELD.getPreferredName(), httpStatus) .field(WebhookAction.Parser.URL_FIELD.getPreferredName(), url) .field(WebhookAction.Parser.BODY_FIELD.getPreferredName(), body); @@ -174,6 +181,10 @@ public class WebhookAction extends Action { this.reason = reason; } + public String reason() { + return reason; + } + @Override protected XContentBuilder xContentBody(XContentBuilder builder, Params params) throws IOException { return builder.field(WebhookAction.Parser.REASON_FIELD.getPreferredName(), reason); @@ -299,7 +310,8 @@ public class WebhookAction extends Action { throw new ActionException("could not parse webhook result. expected boolean field [success]"); } - Result result = success ? new Result.Executed(httpStatus, url, body) : new Result.Failure(reason); + + Result result = (reason == null) ? new Result.Executed(httpStatus, url, body) : new Result.Failure(reason); if (transformResult != null) { result.transformResult(transformResult); } @@ -307,6 +319,28 @@ public class WebhookAction extends Action { } } + private Object makeURLSafe(Object toSafe) throws UnsupportedEncodingException { + if (toSafe instanceof List) { + List returnObject = new ArrayList<>(((List) toSafe).size()); + for (Object o : (List)toSafe) { + returnObject.add(makeURLSafe(o)); + } + return returnObject; + } else if (toSafe instanceof Map) { + Map returnObject = new HashMap<>(((Map) toSafe).size()); + for (Object key : ((Map) toSafe).keySet()) { + returnObject.put(key, makeURLSafe(((Map) toSafe).get(key))); + } + return returnObject; + } else if (toSafe instanceof String) { + return URLEncoder.encode(toSafe.toString(), Charsets.UTF_8.name()); + } else { + //Don't know how to convert anything else + return toSafe; + } + } + + public static class SourceBuilder implements Action.SourceBuilder { private final Script url; diff --git a/src/main/java/org/elasticsearch/watcher/support/http/HttpClient.java b/src/main/java/org/elasticsearch/watcher/support/http/HttpClient.java index ea6b84c21b7..8257f745b3e 100644 --- a/src/main/java/org/elasticsearch/watcher/support/http/HttpClient.java +++ b/src/main/java/org/elasticsearch/watcher/support/http/HttpClient.java @@ -82,8 +82,8 @@ public class HttpClient extends AbstractComponent { urlConnection.getOutputStream().close(); } - HttpResponse response = new HttpResponse(); - response.status(urlConnection.getResponseCode()); + HttpResponse response = new HttpResponse(urlConnection.getResponseCode()); + response.inputStream(urlConnection.getInputStream()); logger.debug("http status code: {}", response.status()); response.inputStream(urlConnection.getInputStream()); return response; diff --git a/src/main/java/org/elasticsearch/watcher/support/http/HttpResponse.java b/src/main/java/org/elasticsearch/watcher/support/http/HttpResponse.java index baa7d326556..a60ba717246 100644 --- a/src/main/java/org/elasticsearch/watcher/support/http/HttpResponse.java +++ b/src/main/java/org/elasticsearch/watcher/support/http/HttpResponse.java @@ -18,12 +18,12 @@ public class HttpResponse implements Closeable { private InputStream inputStream; private byte[] body; - public int status() { - return status; + public HttpResponse(int status) { + this.status = status; } - public void status(int status) { - this.status = status; + public int status() { + return status; } public byte[] body() { @@ -44,6 +44,8 @@ public class HttpResponse implements Closeable { @Override public void close() throws IOException { - inputStream.close(); + if (inputStream != null) { + inputStream.close(); + } } } \ No newline at end of file diff --git a/src/test/java/org/elasticsearch/watcher/actions/TransformMocks.java b/src/test/java/org/elasticsearch/watcher/actions/TransformMocks.java new file mode 100644 index 00000000000..e8e6c778ae6 --- /dev/null +++ b/src/test/java/org/elasticsearch/watcher/actions/TransformMocks.java @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.watcher.actions; + +import org.elasticsearch.common.collect.ImmutableMap; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.watcher.transform.Transform; +import org.elasticsearch.watcher.transform.TransformRegistry; +import org.elasticsearch.watcher.watch.Payload; +import org.elasticsearch.watcher.watch.WatchExecutionContext; + +import java.io.IOException; +import java.util.Map; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; + +/** + */ +public class TransformMocks { + + public static class TransformMock extends Transform { + + @Override + public String type() { + return "_transform"; + } + + @Override + public Result apply(WatchExecutionContext ctx, Payload payload) throws IOException { + return new Result("_transform", new Payload.Simple("_key", "_value")); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.startObject().endObject(); + } + + public static class Result extends Transform.Result { + + public Result(String type, Payload payload) { + super(type, payload); + } + + @Override + protected XContentBuilder xContentBody(XContentBuilder builder, Params params) throws IOException { + return builder; + } + } + } + + public static class TransformRegistryMock extends TransformRegistry { + + public TransformRegistryMock(final Transform transform) { + super(ImmutableMap.of("_transform", new Transform.Parser() { + @Override + public String type() { + return transform.type(); + } + + @Override + public Transform parse(XContentParser parser) throws IOException { + parser.nextToken(); + assertThat(parser.currentToken(), is(XContentParser.Token.END_OBJECT)); + return transform; + } + + @Override + public Transform.Result parseResult(XContentParser parser) throws IOException { + return null; // should not be called when this ctor is used + } + })); + } + + public TransformRegistryMock(final Transform.Result result) { + super(ImmutableMap.of("_transform_type", new Transform.Parser() { + @Override + public String type() { + return result.type(); + } + + @Override + public Transform parse(XContentParser parser) throws IOException { + return null; // should not be called when this ctor is used. + } + + @Override + public Transform.Result parseResult(XContentParser parser) throws IOException { + assertThat(parser.currentToken(), is(XContentParser.Token.START_OBJECT)); + parser.nextToken(); + assertThat(parser.currentToken(), is(XContentParser.Token.FIELD_NAME)); + assertThat(parser.currentName(), is("payload")); + parser.nextToken(); + assertThat(parser.currentToken(), is(XContentParser.Token.START_OBJECT)); + Map data = parser.map(); + assertThat(data, equalTo(result.payload().data())); + parser.nextToken(); + assertThat(parser.currentToken(), is(XContentParser.Token.END_OBJECT)); + return result; + } + })); + } + } + +} diff --git a/src/test/java/org/elasticsearch/watcher/actions/email/EmailActionTests.java b/src/test/java/org/elasticsearch/watcher/actions/email/EmailActionTests.java index ef35e906e61..bae2d745b5c 100644 --- a/src/test/java/org/elasticsearch/watcher/actions/email/EmailActionTests.java +++ b/src/test/java/org/elasticsearch/watcher/actions/email/EmailActionTests.java @@ -15,6 +15,7 @@ import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.test.ElasticsearchTestCase; import org.elasticsearch.watcher.actions.ActionSettingsException; +import org.elasticsearch.watcher.actions.TransformMocks; import org.elasticsearch.watcher.actions.email.service.*; import org.elasticsearch.watcher.support.init.proxy.ScriptServiceProxy; import org.elasticsearch.watcher.support.template.ScriptTemplate; @@ -148,8 +149,8 @@ public class EmailActionTests extends ElasticsearchTestCase { ScriptTemplate subject = randomBoolean() ? new ScriptTemplate(scriptService, "_subject") : null; ScriptTemplate textBody = randomBoolean() ? new ScriptTemplate(scriptService, "_text_body") : null; ScriptTemplate htmlBody = randomBoolean() ? new ScriptTemplate(scriptService, "_text_html") : null; - final Transform transform = randomBoolean() ? null : new TransformMock(); - TransformRegistry transformRegistry = transform == null ? mock(TransformRegistry.class) : new TransformRegistryMock(transform); + final Transform transform = randomBoolean() ? null : new TransformMocks.TransformMock(); + TransformRegistry transformRegistry = transform == null ? mock(TransformRegistry.class) : new TransformMocks.TransformRegistryMock(transform); boolean attachPayload = randomBoolean(); XContentBuilder builder = jsonBuilder().startObject() .field("account", "_account") @@ -283,8 +284,8 @@ public class EmailActionTests extends ElasticsearchTestCase { Template textBody = new TemplateMock("_text_body"); Template htmlBody = randomBoolean() ? null : new TemplateMock("_html_body"); boolean attachPayload = randomBoolean(); - Transform transform = randomBoolean() ? null : new TransformMock(); - TransformRegistry transformRegistry = transform == null ? mock(TransformRegistry.class) : new TransformRegistryMock(transform); + Transform transform = randomBoolean() ? null : new TransformMocks.TransformMock(); + TransformRegistry transformRegistry = transform == null ? mock(TransformRegistry.class) : new TransformMocks.TransformRegistryMock(transform); EmailAction action = new EmailAction(logger, transform, service, email, auth, profile, account, subject, textBody, htmlBody, attachPayload); @@ -327,7 +328,7 @@ public class EmailActionTests extends ElasticsearchTestCase { when(transformResult.type()).thenReturn("_transform_type"); when(transformResult.payload()).thenReturn(new Payload.Simple("_key", "_value")); } - TransformRegistry transformRegistry = transformResult != null ? new TransformRegistryMock(transformResult) : mock(TransformRegistry.class); + TransformRegistry transformRegistry = transformResult != null ? new TransformMocks.TransformRegistryMock(transformResult) : mock(TransformRegistry.class); XContentBuilder builder = jsonBuilder().startObject() .field("success", success); @@ -423,87 +424,4 @@ public class EmailActionTests extends ElasticsearchTestCase { } } } - - static class TransformMock extends Transform { - - @Override - public String type() { - return "_transform"; - } - - @Override - public Result apply(WatchExecutionContext ctx, Payload payload) throws IOException { - return new Result("_transform", new Payload.Simple("_key", "_value")); - } - - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - return builder.startObject().endObject(); - } - - public static class Result extends Transform.Result { - - public Result(String type, Payload payload) { - super(type, payload); - } - - @Override - protected XContentBuilder xContentBody(XContentBuilder builder, Params params) throws IOException { - return builder; - } - } - } - - static class TransformRegistryMock extends TransformRegistry { - - public TransformRegistryMock(final Transform transform) { - super(ImmutableMap.of("_transform", new Transform.Parser() { - @Override - public String type() { - return transform.type(); - } - - @Override - public Transform parse(XContentParser parser) throws IOException { - parser.nextToken(); - assertThat(parser.currentToken(), is(XContentParser.Token.END_OBJECT)); - return transform; - } - - @Override - public Transform.Result parseResult(XContentParser parser) throws IOException { - return null; // should not be called when this ctor is used - } - })); - } - - public TransformRegistryMock(final Transform.Result result) { - super(ImmutableMap.of("_transform_type", new Transform.Parser() { - @Override - public String type() { - return result.type(); - } - - @Override - public Transform parse(XContentParser parser) throws IOException { - return null; // should not be called when this ctor is used. - } - - @Override - public Transform.Result parseResult(XContentParser parser) throws IOException { - assertThat(parser.currentToken(), is(XContentParser.Token.START_OBJECT)); - parser.nextToken(); - assertThat(parser.currentToken(), is(XContentParser.Token.FIELD_NAME)); - assertThat(parser.currentName(), is("payload")); - parser.nextToken(); - assertThat(parser.currentToken(), is(XContentParser.Token.START_OBJECT)); - Map data = parser.map(); - assertThat(data, equalTo(result.payload().data())); - parser.nextToken(); - assertThat(parser.currentToken(), is(XContentParser.Token.END_OBJECT)); - return result; - } - })); - } - } } diff --git a/src/test/java/org/elasticsearch/watcher/actions/index/IndexActionTests.java b/src/test/java/org/elasticsearch/watcher/actions/index/IndexActionTests.java new file mode 100644 index 00000000000..1fdc995753a --- /dev/null +++ b/src/test/java/org/elasticsearch/watcher/actions/index/IndexActionTests.java @@ -0,0 +1,202 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.watcher.actions.index; + +import com.carrotsearch.randomizedtesting.annotations.Repeat; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.common.joda.time.DateTime; +import org.elasticsearch.common.settings.ImmutableSettings; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.script.ScriptService; +import org.elasticsearch.test.ElasticsearchIntegrationTest; +import org.elasticsearch.watcher.actions.ActionException; +import org.elasticsearch.watcher.actions.TransformMocks; +import org.elasticsearch.watcher.actions.email.service.Authentication; +import org.elasticsearch.watcher.actions.email.service.Email; +import org.elasticsearch.watcher.actions.email.service.EmailService; +import org.elasticsearch.watcher.actions.email.service.Profile; +import org.elasticsearch.watcher.support.http.HttpClient; +import org.elasticsearch.watcher.support.init.proxy.ClientProxy; +import org.elasticsearch.watcher.support.init.proxy.ScriptServiceProxy; +import org.elasticsearch.watcher.test.WatcherTestUtils; +import org.elasticsearch.watcher.transform.Transform; +import org.elasticsearch.watcher.transform.TransformRegistry; +import org.elasticsearch.watcher.trigger.schedule.ScheduleTriggerEvent; +import org.elasticsearch.watcher.watch.Payload; +import org.elasticsearch.watcher.watch.Watch; +import org.elasticsearch.watcher.watch.WatchExecutionContext; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery; +import static org.elasticsearch.search.builder.SearchSourceBuilder.searchSource; +import static org.hamcrest.Matchers.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + */ +public class IndexActionTests extends ElasticsearchIntegrationTest { + + @Test + public void testIndexActionExecute() throws Exception { + + IndexAction action = new IndexAction(logger, null, ClientProxy.of(client()), "test-index", "test-type"); + final String account = "account1"; + Watch alert = WatcherTestUtils.createTestWatch("testAlert", + ClientProxy.of(client()), + ScriptServiceProxy.of(internalCluster().getInstance(ScriptService.class)), + new HttpClient(ImmutableSettings.EMPTY), + new EmailService() { + @Override + public EmailService.EmailSent send(Email email, Authentication auth, Profile profile) { + return new EmailSent(account, email); + } + + @Override + public EmailSent send(Email email, Authentication auth, Profile profile, String accountName) { + return new EmailSent(account, email); + } + }, + logger); + WatchExecutionContext ctx = new WatchExecutionContext("testid", alert, new DateTime(), new ScheduleTriggerEvent(new DateTime(), new DateTime())); + + Map payloadMap = new HashMap<>(); + payloadMap.put("test", "foo"); + IndexAction.Result result = action.execute(ctx, new Payload.Simple(payloadMap)); + + assertThat(result.success(), equalTo(true)); + Map responseData = result.response().data(); + assertThat(responseData.get("created"), equalTo((Object)Boolean.TRUE)); + assertThat(responseData.get("version"), equalTo((Object) 1L)); + assertThat(responseData.get("type").toString(), equalTo("test-type")); + assertThat(responseData.get("index").toString(), equalTo("test-index")); + + refresh(); //Manually refresh to make sure data is available + + SearchResponse sr = client().prepareSearch("test-index") + .setTypes("test-type") + .setSource(searchSource().query(matchAllQuery()).buildAsBytes()).get(); + + assertThat(sr.getHits().totalHits(), equalTo(1L)); + } + + @Test @Repeat(iterations = 10) + public void testParser() throws Exception { + final Transform transform = randomBoolean() ? null : new TransformMocks.TransformMock(); + TransformRegistry transformRegistry = transform == null ? mock(TransformRegistry.class) : new TransformMocks.TransformRegistryMock(transform); + XContentBuilder builder = jsonBuilder(); + builder.startObject(); + { + builder.field(IndexAction.Parser.INDEX_FIELD.getPreferredName(), "test-index"); + builder.field(IndexAction.Parser.TYPE_FIELD.getPreferredName(), "test-type"); + if (transform != null){ + builder.startObject(Transform.Parser.TRANSFORM_FIELD.getPreferredName()).field(transform.type(), transform); + } + } + builder.endObject(); + + IndexAction.Parser actionParser = new IndexAction.Parser(ImmutableSettings.EMPTY, ClientProxy.of(client()), transformRegistry); + XContentParser parser = JsonXContent.jsonXContent.createParser(builder.bytes()); + parser.nextToken(); + + IndexAction action = actionParser.parse(parser); + + assertThat(action.type, equalTo("test-type")); + assertThat(action.index, equalTo("test-index")); + + if (transform != null) { + assertThat(action.transform(), notNullValue()); + assertThat(action.transform(), equalTo(transform)); + } else { + assertThat(action.transform(), nullValue()); + } + } + + @Test @Repeat(iterations = 10) + public void testParser_Failure() throws Exception { + XContentBuilder builder = jsonBuilder(); + boolean useIndex = randomBoolean(); + boolean useType = randomBoolean(); + builder.startObject(); + { + if (useIndex) { + builder.field(IndexAction.Parser.INDEX_FIELD.getPreferredName(), "test-index"); + } + if (useType) { + builder.field(IndexAction.Parser.TYPE_FIELD.getPreferredName(), "test-type"); + } + } + builder.endObject(); + IndexAction.Parser actionParser = new IndexAction.Parser(ImmutableSettings.EMPTY, ClientProxy.of(client()), null); + XContentParser parser = JsonXContent.jsonXContent.createParser(builder.bytes()); + parser.nextToken(); + try { + actionParser.parse(parser); + if (!(useIndex && useType)) { + fail(); + } + } catch (ActionException ae) { + assertThat(useIndex && useType, equalTo(false)); + } + } + + @Test @Repeat(iterations =30) + public void testParser_Result() throws Exception { + boolean success = randomBoolean(); + + Transform.Result transformResult = randomBoolean() ? null : mock(Transform.Result.class); + if (transformResult != null) { + when(transformResult.type()).thenReturn("_transform_type"); + when(transformResult.payload()).thenReturn(new Payload.Simple("_key", "_value")); + } + TransformRegistry transformRegistry = transformResult != null ? new TransformMocks.TransformRegistryMock(transformResult) : mock(TransformRegistry.class); + + XContentBuilder builder = jsonBuilder().startObject() + .field("success", success); + if (success) { + Map data = new HashMap<>(); + data.put("created", true); + data.put("id", "0"); + data.put("version", 1); + data.put("type", "test-type"); + data.put("index", "test-index"); + + builder.field(IndexAction.Parser.RESPONSE_FIELD.getPreferredName(), data); + if (transformResult != null) { + builder.startObject("transform_result") + .startObject("_transform_type") + .field("payload", new Payload.Simple("_key", "_value").data()) + .endObject() + .endObject(); + } + } else { + builder.field("reason", "_reason"); + } + + XContentParser parser = JsonXContent.jsonXContent.createParser(builder.bytes()); + parser.nextToken(); + IndexAction.Result result = new IndexAction.Parser(ImmutableSettings.EMPTY, ClientProxy.of(client()), transformRegistry) + .parseResult(parser); + + assertThat(result.success(), is(success)); + if (success) { + Map responseData = result.response().data(); + assertThat(responseData.get("created"), equalTo((Object)Boolean.TRUE)); + assertThat(responseData.get("version"), equalTo((Object) 1)); + assertThat(responseData.get("type").toString(), equalTo("test-type")); + assertThat(responseData.get("index").toString(), equalTo("test-index")); + } else { + assertThat(result.reason, is("_reason")); + } + } + +} diff --git a/src/test/java/org/elasticsearch/watcher/actions/webhook/WebhookActionTests.java b/src/test/java/org/elasticsearch/watcher/actions/webhook/WebhookActionTests.java new file mode 100644 index 00000000000..66294c40d69 --- /dev/null +++ b/src/test/java/org/elasticsearch/watcher/actions/webhook/WebhookActionTests.java @@ -0,0 +1,466 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.watcher.actions.webhook; + +import com.carrotsearch.randomizedtesting.annotations.Repeat; +import org.elasticsearch.common.joda.time.DateTime; +import org.elasticsearch.common.settings.ImmutableSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.env.Environment; +import org.elasticsearch.node.settings.NodeSettingsService; +import org.elasticsearch.script.ScriptEngineService; +import org.elasticsearch.script.ScriptService; +import org.elasticsearch.script.mustache.MustacheScriptEngineService; +import org.elasticsearch.test.ElasticsearchTestCase; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.watcher.ResourceWatcherService; +import org.elasticsearch.watcher.actions.Action; +import org.elasticsearch.watcher.actions.ActionException; +import org.elasticsearch.watcher.actions.TransformMocks; +import org.elasticsearch.watcher.actions.email.service.Authentication; +import org.elasticsearch.watcher.actions.email.service.Email; +import org.elasticsearch.watcher.actions.email.service.EmailService; +import org.elasticsearch.watcher.actions.email.service.Profile; +import org.elasticsearch.watcher.support.Script; +import org.elasticsearch.watcher.support.http.HttpClient; +import org.elasticsearch.watcher.support.http.HttpMethod; +import org.elasticsearch.watcher.support.http.HttpResponse; +import org.elasticsearch.watcher.support.init.proxy.ClientProxy; +import org.elasticsearch.watcher.support.init.proxy.ScriptServiceProxy; +import org.elasticsearch.watcher.support.template.ScriptTemplate; +import org.elasticsearch.watcher.support.template.Template; +import org.elasticsearch.watcher.test.WatcherTestUtils; +import org.elasticsearch.watcher.transform.Transform; +import org.elasticsearch.watcher.transform.TransformRegistry; +import org.elasticsearch.watcher.trigger.schedule.ScheduleTriggerEvent; +import org.elasticsearch.watcher.watch.Payload; +import org.elasticsearch.watcher.watch.Watch; +import org.elasticsearch.watcher.watch.WatchExecutionContext; +import org.hamcrest.Matchers; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import javax.mail.internet.AddressException; +import java.io.IOException; +import java.net.URI; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.lessThanOrEqualTo; +import static org.hamcrest.core.Is.is; +import static org.mockito.AdditionalMatchers.not; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + */ +public class WebhookActionTests extends ElasticsearchTestCase { + + static final String TEST_BODY = "ERROR HAPPENED"; + static final String TEST_URL = "http://test.com/testurl"; + + private ThreadPool tp = null; + private ScriptServiceProxy scriptService; + + @Before + public void init() throws IOException { + tp = new ThreadPool(ThreadPool.Names.SAME); + Settings settings = ImmutableSettings.settingsBuilder().build(); + MustacheScriptEngineService mustacheScriptEngineService = new MustacheScriptEngineService(settings); + Set engineServiceSet = new HashSet<>(); + engineServiceSet.add(mustacheScriptEngineService); + scriptService = ScriptServiceProxy.of(new ScriptService(settings, new Environment(), engineServiceSet, new ResourceWatcherService(settings, tp), new NodeSettingsService(settings))); + } + + @After + public void cleanup() { + tp.shutdownNow(); + } + + @Test @Repeat(iterations = 30) + public void testExecute() throws Exception { + ClientProxy client = mock(ClientProxy.class); + ExecuteScenario scenario = randomFrom(ExecuteScenario.values()); + + HttpClient httpClient = scenario.client(); + HttpMethod method = randomFrom(HttpMethod.GET, HttpMethod.POST, HttpMethod.PUT); + + final Transform transform = randomBoolean() ? null : new TransformMocks.TransformMock(); + final String account = "account1"; + + WebhookAction webhookAction = + new WebhookAction(logger, + transform, + httpClient, + method, + new ScriptTemplate(scriptService, TEST_URL), + new ScriptTemplate(scriptService, TEST_BODY)); + + Watch watch = createWatch("test_watch", client, account); + WatchExecutionContext ctx = new WatchExecutionContext("testid", watch, new DateTime(), new ScheduleTriggerEvent(new DateTime(), new DateTime())); + + WebhookAction.Result actionResult = webhookAction.execute(ctx, new Payload.Simple()); + scenario.assertResult(actionResult); + } + + + @Test @Repeat(iterations = 10) + public void testParser() throws Exception { + final Transform transform = randomBoolean() ? null : new TransformMocks.TransformMock(); + TransformRegistry transformRegistry = transform == null ? mock(TransformRegistry.class) : new TransformMocks.TransformRegistryMock(transform); + Template body = randomBoolean() ? new ScriptTemplate(scriptService, "_subject") : null; + Template url = new ScriptTemplate(scriptService, "_url"); + HttpMethod method = randomFrom(HttpMethod.GET, HttpMethod.POST, HttpMethod.PUT, null); + + + XContentBuilder builder = jsonBuilder(); + builder.startObject(); + { + builder.field(WebhookAction.Parser.URL_FIELD.getPreferredName(), url); + + if (method != null) { + builder.field(WebhookAction.Parser.METHOD_FIELD.getPreferredName(), method.method()); + } + if (body != null) { + builder.field(WebhookAction.Parser.BODY_FIELD.getPreferredName(), body); + } + + if (transform != null) { + builder.startObject(Transform.Parser.TRANSFORM_FIELD.getPreferredName()) + .field(transform.type(), transform); + } + } + builder.endObject(); + + WebhookAction.Parser actionParser = new WebhookAction.Parser(ImmutableSettings.EMPTY, + new ScriptTemplate.Parser(ImmutableSettings.EMPTY, scriptService), + ExecuteScenario.Success.client(), transformRegistry); + + XContentParser parser = JsonXContent.jsonXContent.createParser(builder.bytes()); + parser.nextToken(); + + WebhookAction action = actionParser.parse(parser); + + if (method != null) { + assertThat(action.method, equalTo(method)); + } else { + assertThat(action.method, equalTo(HttpMethod.POST)); + } + + if (body != null) { + assertThat(action.body, equalTo(body)); + } + + assertThat(action.url, equalTo(url)); + + if (transform != null) { + assertThat(action.transform(), equalTo(transform)); + } + } + + @Test @Repeat(iterations = 10) + public void testParser_SelfGenerated() throws Exception { + final Transform transform = randomBoolean() ? null : new TransformMocks.TransformMock(); + TransformRegistry transformRegistry = transform == null ? mock(TransformRegistry.class) : new TransformMocks.TransformRegistryMock(transform); + Template body = randomBoolean() ? new ScriptTemplate(scriptService, "_body") : null; + Template url = new ScriptTemplate(scriptService, "_url"); + HttpMethod method = randomFrom(HttpMethod.GET, HttpMethod.POST, HttpMethod.PUT); + + WebhookAction webhookAction = new WebhookAction(logger, transform, ExecuteScenario.Success.client(), method, url, body); + + XContentBuilder builder = jsonBuilder(); + webhookAction.toXContent(builder, ToXContent.EMPTY_PARAMS); + + WebhookAction.Parser actionParser = new WebhookAction.Parser(ImmutableSettings.EMPTY, + new ScriptTemplate.Parser(ImmutableSettings.EMPTY, scriptService), + ExecuteScenario.Success.client(), transformRegistry); + + XContentParser parser = JsonXContent.jsonXContent.createParser(builder.bytes()); + parser.nextToken(); + + WebhookAction parsedAction = actionParser.parse(parser); + + assertThat(webhookAction.body, equalTo(parsedAction.body)); + assertThat(webhookAction.method, equalTo(parsedAction.method)); + assertThat(webhookAction.url, equalTo(parsedAction.url)); + assertThat(webhookAction.transform(), equalTo(parsedAction.transform())); + } + + @Test @Repeat(iterations = 10) + public void testParser_SourceBuilder() throws Exception { + Script body = randomBoolean() ? new Script("_body", ScriptService.ScriptType.INLINE, "mustache") : null; + Script url = new Script("_url", ScriptService.ScriptType.INLINE, "mustache"); + HttpMethod method = randomFrom(HttpMethod.GET, HttpMethod.POST, HttpMethod.PUT); + WebhookAction.SourceBuilder sourceBuilder = new WebhookAction.SourceBuilder(url); + sourceBuilder.method(method); + sourceBuilder.body(body); + + XContentBuilder builder = jsonBuilder(); + sourceBuilder.toXContent(builder, ToXContent.EMPTY_PARAMS); + + WebhookAction.Parser actionParser = new WebhookAction.Parser(ImmutableSettings.EMPTY, + new ScriptTemplate.Parser(ImmutableSettings.EMPTY, scriptService), + ExecuteScenario.Success.client(), null); + + XContentParser parser = JsonXContent.jsonXContent.createParser(builder.bytes()); + parser.nextToken(); + + Map emptyModel = new HashMap<>(); + + WebhookAction parsedAction = actionParser.parse(parser); + if (body != null) { + assertThat(body.script(), equalTo(parsedAction.body.render(emptyModel))); + } else { + assertThat(parsedAction.body, equalTo(null)); + } + assertThat(url.script(), equalTo(parsedAction.url.render(emptyModel))); + assertThat(method, equalTo(parsedAction.method)); + } + + @Test(expected = ActionException.class) + public void testParser_Failure() throws Exception { + final Transform transform = randomBoolean() ? null : new TransformMocks.TransformMock(); + TransformRegistry transformRegistry = transform == null ? mock(TransformRegistry.class) : new TransformMocks.TransformRegistryMock(transform); + Template body = randomBoolean() ? new ScriptTemplate(scriptService, "_subject") : null; + HttpMethod method = randomFrom(HttpMethod.GET, HttpMethod.POST, HttpMethod.PUT, null); + + + XContentBuilder builder = jsonBuilder(); + builder.startObject(); + { + if (method != null) { + builder.field(WebhookAction.Parser.METHOD_FIELD.getPreferredName(), method.method()); + } + if (body != null) { + builder.field(WebhookAction.Parser.BODY_FIELD.getPreferredName(), body); + } + + if (transform != null) { + builder.startObject(Transform.Parser.TRANSFORM_FIELD.getPreferredName()) + .field(transform.type(), transform); + } + } + builder.endObject(); + + WebhookAction.Parser actionParser = new WebhookAction.Parser(ImmutableSettings.EMPTY, + new ScriptTemplate.Parser(ImmutableSettings.EMPTY, scriptService), + ExecuteScenario.Success.client(), transformRegistry); + + XContentParser parser = JsonXContent.jsonXContent.createParser(builder.bytes()); + parser.nextToken(); + //This should fail since we are not supplying a url + actionParser.parse(parser); + } + + @Test @Repeat(iterations = 30) + public void testParser_Result() throws Exception { + Transform.Result transformResult = randomBoolean() ? null : mock(Transform.Result.class); + if (transformResult != null) { + when(transformResult.type()).thenReturn("_transform_type"); + when(transformResult.payload()).thenReturn(new Payload.Simple("_key", "_value")); + } + TransformRegistry transformRegistry = transformResult != null ? new TransformMocks.TransformRegistryMock(transformResult) : mock(TransformRegistry.class); + + String body = "_body"; + String url = "_url"; + String reason = "_reason"; + int responseCode = randomIntBetween(200, 599); + + boolean error = randomBoolean(); + + boolean success = !error && responseCode < 400; + + WebhookAction.Parser actionParser = new WebhookAction.Parser(ImmutableSettings.EMPTY, + new ScriptTemplate.Parser(ImmutableSettings.EMPTY, scriptService), + ExecuteScenario.Success.client(), transformRegistry); + + XContentBuilder builder = jsonBuilder(); + builder.startObject(); + { + builder.field(Action.Result.SUCCESS_FIELD.getPreferredName(), success); + if (!error) { + builder.field(WebhookAction.Parser.HTTP_STATUS_FIELD.getPreferredName(), responseCode); + builder.field(WebhookAction.Parser.BODY_FIELD.getPreferredName(), body); + builder.field(WebhookAction.Parser.URL_FIELD.getPreferredName(), url); + if (transformResult != null) { + builder.startObject("transform_result") + .startObject("_transform_type") + .field("payload", new Payload.Simple("_key", "_value").data()) + .endObject() + .endObject(); + } + } else { + builder.field(WebhookAction.Parser.REASON_FIELD.getPreferredName(), reason); + } + } + builder.endObject(); + + XContentParser parser = JsonXContent.jsonXContent.createParser(builder.bytes()); + parser.nextToken(); + + WebhookAction.Result result = actionParser.parseResult(parser); + + assertThat(result.success(), equalTo(success)); + if (!error) { + assertThat(result, instanceOf(WebhookAction.Result.Executed.class)); + WebhookAction.Result.Executed executedResult = (WebhookAction.Result.Executed) result; + assertThat(executedResult.body(), equalTo(body)); + assertThat(executedResult.httpStatus(), equalTo(responseCode)); + assertThat(executedResult.url(), equalTo(url)); + if (transformResult != null) { + assertThat(transformResult, equalTo(executedResult.transformResult())); + } + } else { + assertThat(result, Matchers.instanceOf(WebhookAction.Result.Failure.class)); + WebhookAction.Result.Failure failedResult = (WebhookAction.Result.Failure) result; + assertThat(failedResult.reason(), equalTo(reason)); + } + } + + @Test @Repeat(iterations = 100) + public void testValidUrls() throws Exception { + + HttpClient httpClient = ExecuteScenario.Success.client(); + HttpMethod method = HttpMethod.GET; + + WebhookAction webhookAction = + new WebhookAction(logger, + null, + httpClient, + method, + new ScriptTemplate(scriptService, "http://test.com/test_{{ctx.watch_name}}"), + new ScriptTemplate(scriptService, TEST_BODY)); + + String watchName = "test_url_encode" + randomAsciiOfLength(10); + Watch watch = createWatch(watchName, mock(ClientProxy.class), "account1"); + WatchExecutionContext ctx = new WatchExecutionContext("testid", watch, new DateTime(), new ScheduleTriggerEvent(new DateTime(), new DateTime())); + WebhookAction.Result result = webhookAction.execute(ctx, new Payload.Simple()); + assertThat(result, Matchers.instanceOf(WebhookAction.Result.Executed.class)); + WebhookAction.Result.Executed executed = (WebhookAction.Result.Executed)result; + URI.create(executed.url()); //This will throw an IllegalArgumentException if the url is invalid + } + + private Watch createWatch(String watchName, ClientProxy client, final String account) throws AddressException, IOException { + return WatcherTestUtils.createTestWatch(watchName, + client, + scriptService, + ExecuteScenario.Success.client(), + new EmailService() { + @Override + public EmailSent send(Email email, Authentication auth, Profile profile) { + return new EmailSent(account, email); + } + + @Override + public EmailSent send(Email email, Authentication auth, Profile profile, String accountName) { + return new EmailSent(account, email); + } + }, + logger); + } + + + private static enum ExecuteScenario { + ErrorCode() { + @Override + public HttpClient client() throws IOException { + HttpClient client = mock(HttpClient.class); + when(client.execute(any(HttpMethod.class), any(String.class), any(String.class))) + .thenReturn(new HttpResponse(randomIntBetween(400,599))); + return client; + } + + @Override + public void assertResult(WebhookAction.Result actionResult) { + assertThat(actionResult.success(), is(false)); + assertThat(actionResult, instanceOf(WebhookAction.Result.Executed.class)); + WebhookAction.Result.Executed executedActionResult = (WebhookAction.Result.Executed) actionResult; + assertThat(executedActionResult.httpStatus(), greaterThanOrEqualTo(400)); + assertThat(executedActionResult.httpStatus(), lessThanOrEqualTo(599)); + assertThat(executedActionResult.body(), equalTo(TEST_BODY)); + assertThat(executedActionResult.url(), equalTo(TEST_URL)); + } + }, + + Error() { + @Override + public HttpClient client() throws IOException { + HttpClient client = mock(HttpClient.class); + when(client.execute(any(HttpMethod.class), any(String.class), any(String.class))) + .thenThrow(new IOException("Unable to connect")); + return client; + } + + @Override + public void assertResult(WebhookAction.Result actionResult) { + assertThat(actionResult, instanceOf(WebhookAction.Result.Failure.class)); + WebhookAction.Result.Failure failResult = (WebhookAction.Result.Failure) actionResult; + assertThat(failResult.success(), is(false)); + } + }, + + Success() { + @Override + public HttpClient client() throws IOException{ + HttpClient client = mock(HttpClient.class); + when(client.execute(any(HttpMethod.class), any(String.class), any(String.class))) + .thenReturn(new HttpResponse(randomIntBetween(200,399))); + return client; + } + + @Override + public void assertResult(WebhookAction.Result actionResult) { + assertThat(actionResult, instanceOf(WebhookAction.Result.Executed.class)); + assertThat(actionResult, instanceOf(WebhookAction.Result.Executed.class)); + WebhookAction.Result.Executed executedActionResult = (WebhookAction.Result.Executed) actionResult; + assertThat(executedActionResult.httpStatus(), greaterThanOrEqualTo(200)); + assertThat(executedActionResult.httpStatus(), lessThanOrEqualTo(399)); + assertThat(executedActionResult.body(), equalTo(TEST_BODY)); + assertThat(executedActionResult.url(), equalTo(TEST_URL)); + } + }, + + SuccessVerify() { + @Override + public HttpClient client() throws IOException{ + HttpClient client = mock(HttpClient.class); + when(client.execute(any(HttpMethod.class), eq(TEST_URL), eq(TEST_BODY))) + .thenReturn(new HttpResponse(200)); + when(client.execute(any(HttpMethod.class), eq(not(TEST_URL)), eq(not(TEST_BODY)))).thenThrow(new IOException("bad url or body")); + return client; + } + + @Override + public void assertResult(WebhookAction.Result actionResult) { + assertThat(actionResult, instanceOf(WebhookAction.Result.Executed.class)); + assertThat(actionResult, instanceOf(WebhookAction.Result.Executed.class)); + WebhookAction.Result.Executed executedActionResult = (WebhookAction.Result.Executed) actionResult; + assertThat(executedActionResult.httpStatus(), equalTo(200)); + assertThat(executedActionResult.body(), equalTo(TEST_BODY)); + assertThat(executedActionResult.url(), equalTo(TEST_URL)); + } + }; + + public abstract HttpClient client() throws IOException; + + public abstract void assertResult(WebhookAction.Result result); + + } + +} diff --git a/src/test/java/org/elasticsearch/watcher/input/http/HttpInputTests.java b/src/test/java/org/elasticsearch/watcher/input/http/HttpInputTests.java index b7e42bdd55b..32bec09a24a 100644 --- a/src/test/java/org/elasticsearch/watcher/input/http/HttpInputTests.java +++ b/src/test/java/org/elasticsearch/watcher/input/http/HttpInputTests.java @@ -81,8 +81,7 @@ public class HttpInputTests extends ElasticsearchTestCase { request.body(mockBody); HttpInput input = new HttpInput(logger, httpClient, request, null); - HttpResponse response = new HttpResponse(); - response.status(123); + HttpResponse response = new HttpResponse(123); response.inputStream(new ByteArrayInputStream("{\"key\" : \"value\"}".getBytes(UTF8))); when(httpClient.execute(any(HttpRequest.class))).thenReturn(response); diff --git a/src/test/java/org/elasticsearch/watcher/test/WatcherTestUtils.java b/src/test/java/org/elasticsearch/watcher/test/WatcherTestUtils.java index 98b34c62e29..12531b0732f 100644 --- a/src/test/java/org/elasticsearch/watcher/test/WatcherTestUtils.java +++ b/src/test/java/org/elasticsearch/watcher/test/WatcherTestUtils.java @@ -25,6 +25,7 @@ import org.elasticsearch.watcher.actions.email.service.Profile; import org.elasticsearch.watcher.actions.webhook.WebhookAction; import org.elasticsearch.watcher.condition.script.ScriptCondition; import org.elasticsearch.watcher.input.search.SearchInput; +import org.elasticsearch.watcher.input.simple.SimpleInput; import org.elasticsearch.watcher.support.Script; import org.elasticsearch.watcher.support.WatcherUtils; import org.elasticsearch.watcher.support.clock.SystemClock; @@ -108,7 +109,14 @@ public final class WatcherTestUtils { return ctx; } + public static Watch createTestWatch(String watchName, ScriptServiceProxy scriptService, HttpClient httpClient, EmailService emailService, ESLogger logger) throws AddressException { + return createTestWatch(watchName, ClientProxy.of(ElasticsearchIntegrationTest.client()), scriptService, httpClient, emailService, logger); + } + + + public static Watch createTestWatch(String watchName, ClientProxy client, ScriptServiceProxy scriptService, HttpClient httpClient, EmailService emailService, ESLogger logger) throws AddressException { + SearchRequest conditionRequest = newInputSearchRequest("my-condition-index").source(searchSource().query(matchAllQuery())); SearchRequest transformRequest = newInputSearchRequest("my-payload-index").source(searchSource().query(matchAllQuery())); transformRequest.searchType(SearchTransform.DEFAULT_SEARCH_TYPE); @@ -140,13 +148,16 @@ public final class WatcherTestUtils { Map metadata = new LinkedHashMap<>(); metadata.put("foo", "bar"); + Map inputData = new LinkedHashMap<>(); + inputData.put("bar", "foo"); + return new Watch( watchName, SystemClock.INSTANCE, new ScheduleTrigger(new CronSchedule("0/5 * * * * ? *")), - new SearchInput(logger, scriptService, ClientProxy.of(ElasticsearchIntegrationTest.client()), conditionRequest, null), + new SimpleInput(logger, new Payload.Simple(inputData)), new ScriptCondition(logger, scriptService, new Script("return true")), - new SearchTransform(logger, scriptService, ClientProxy.of(ElasticsearchIntegrationTest.client()), transformRequest), + new SearchTransform(logger, scriptService, client, transformRequest), new Actions(actions), metadata, new TimeValue(0),