diff --git a/extras/jackson-support/build.gradle b/extras/jackson-support/build.gradle new file mode 100644 index 0000000..3415c10 --- /dev/null +++ b/extras/jackson-support/build.gradle @@ -0,0 +1,26 @@ +/* + * Copyright 2017 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +apply from: "${rootDir}/gradle/java.gradle" +apply from: "${rootDir}/gradle/junit.gradle" + +dependencies { + compile "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" + compile "com.fasterxml.jackson.module:jackson-module-afterburner:${jacksonVersion}" + compile "com.fasterxml.jackson.datatype:jackson-datatype-jdk8:${jacksonVersion}" + compile "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${jacksonVersion}" + compile "com.fasterxml.jackson.datatype:jackson-datatype-guava:${jacksonVersion}" +} diff --git a/roboslack-api/src/test/java/com/palantir/roboslack/api/testing/ObjectMappers.java b/extras/jackson-support/src/main/java/com/palantir/roboslack/jackson/ObjectMappers.java similarity index 93% rename from roboslack-api/src/test/java/com/palantir/roboslack/api/testing/ObjectMappers.java rename to extras/jackson-support/src/main/java/com/palantir/roboslack/jackson/ObjectMappers.java index 532fe5b..09a2bb5 100644 --- a/roboslack-api/src/test/java/com/palantir/roboslack/api/testing/ObjectMappers.java +++ b/extras/jackson-support/src/main/java/com/palantir/roboslack/jackson/ObjectMappers.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.palantir.roboslack.api.testing; +package com.palantir.roboslack.jackson; import com.fasterxml.jackson.databind.DeserializationFeature; @@ -25,11 +25,11 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.module.afterburner.AfterburnerModule; -final class ObjectMappers { +public final class ObjectMappers { private ObjectMappers() {} - static ObjectMapper newObjectMapper() { + public static ObjectMapper newObjectMapper() { return new ObjectMapper().registerModule(new GuavaModule()) .registerModule(new Jdk8Module().configureAbsentsAsNulls(true)) .registerModule(new AfterburnerModule()) diff --git a/extras/slack-clients/build.gradle b/extras/slack-clients/build.gradle new file mode 100644 index 0000000..e150476 --- /dev/null +++ b/extras/slack-clients/build.gradle @@ -0,0 +1,25 @@ +/* + * Copyright 2017 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +apply from: "${rootDir}/gradle/java.gradle" +apply from: "${rootDir}/gradle/junit.gradle" + +dependencies { + compile project(":extras:jackson-support") + compile "com.google.guava:guava:${guavaVersion}" + compile "com.squareup.retrofit2:retrofit:${retrofitVersion}" + compile "com.squareup.retrofit2:converter-jackson:${retrofitVersion}" +} diff --git a/extras/slack-clients/src/main/java/com/palantir/roboslack/clients/SlackClients.java b/extras/slack-clients/src/main/java/com/palantir/roboslack/clients/SlackClients.java new file mode 100644 index 0000000..0d1cdcc --- /dev/null +++ b/extras/slack-clients/src/main/java/com/palantir/roboslack/clients/SlackClients.java @@ -0,0 +1,64 @@ +/* + * Copyright 2017 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.palantir.roboslack.clients; + +import com.palantir.roboslack.jackson.ObjectMappers; +import java.util.concurrent.TimeUnit; +import java.util.stream.Stream; +import okhttp3.ConnectionPool; +import okhttp3.OkHttpClient; +import retrofit2.Converter; +import retrofit2.Retrofit; +import retrofit2.converter.jackson.JacksonConverterFactory; + +/** + * Utility class for generating service clients for RPC calls to Slack (intended for internal use only). + * + * @since 1.0.0 + */ +public final class SlackClients { + + private static final String DEFAULT_USER_AGENT = "RoboSlack/1.0.0"; + + private SlackClients() {} + + private static String addTrailingSlash(String uri) { + return uri.charAt(uri.length() - 1) == '/' ? uri : uri + "/"; + } + + private static OkHttpClient createOkHttpClient(String userAgent) { + return new OkHttpClient.Builder() + .addInterceptor(UserAgentInterceptor.of(userAgent)) + .connectionPool(new ConnectionPool(100, 10, TimeUnit.MINUTES)) + .build(); + } + + public static T create(Class clazz, String userAgent, String uri, + Converter.Factory... specialPurposeConverters) { + Retrofit.Builder retrofit = new Retrofit.Builder() + .baseUrl(addTrailingSlash(uri)) + .client(createOkHttpClient(userAgent)); + Stream.of(specialPurposeConverters).forEach(retrofit::addConverterFactory); + retrofit.addConverterFactory(JacksonConverterFactory.create(ObjectMappers.newObjectMapper())); + return retrofit.build().create(clazz); + } + + public static T create(Class clazz, String uri, Converter.Factory... specialPurposeConverters) { + return create(clazz, DEFAULT_USER_AGENT, uri, specialPurposeConverters); + } + +} diff --git a/extras/slack-clients/src/main/java/com/palantir/roboslack/clients/UserAgentInterceptor.java b/extras/slack-clients/src/main/java/com/palantir/roboslack/clients/UserAgentInterceptor.java new file mode 100644 index 0000000..96b3935 --- /dev/null +++ b/extras/slack-clients/src/main/java/com/palantir/roboslack/clients/UserAgentInterceptor.java @@ -0,0 +1,53 @@ +/* + * Copyright 2017 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.palantir.roboslack.clients; + +import static com.google.common.base.Preconditions.checkArgument; + +import java.io.IOException; +import java.util.regex.Pattern; +import okhttp3.Interceptor; +import okhttp3.Response; + +/** + * Intercepts requests, then injects and validates the user agent string. + * + * @since 1.0.0 + */ +public final class UserAgentInterceptor implements Interceptor { + + private static final Pattern VALID_USER_AGENT = Pattern.compile("[A-Za-z0-9()\\-#;/.,_\\s]+"); + private final String userAgent; + + private UserAgentInterceptor(String userAgent) { + checkArgument(VALID_USER_AGENT.matcher(userAgent).matches(), + "User Agent must match pattern '%s': %s", VALID_USER_AGENT, userAgent); + this.userAgent = userAgent; + } + + public static UserAgentInterceptor of(String userAgent) { + return new UserAgentInterceptor(userAgent); + } + + @Override + public Response intercept(Chain chain) throws IOException { + return chain.proceed(chain.request().newBuilder() + .header("User-Agent", userAgent) + .build()); + } + +} diff --git a/gradle.properties b/gradle.properties index db65c12..b226570 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,11 +1,10 @@ # Project Dependencies guavaVersion = 22.0 -httpRemotingVersion = 2.5.1 +retrofitVersion = 2.3.0 immutablesVersion = 2.5.3 jacksonVersion = 2.8.8 jaxRsVersion = 2.0.1 mockitoVersion = 1.10.19 -retrofitVersion = 2.3.0 # Build System baselineVersion = 0.14.0 @@ -16,6 +15,7 @@ nebulaPublishingPluginVersion = 5.1.0 bintrayPluginVersion = 1.7.3 # Testing +awaitilityVersion = 3.0.0 hamcrestVersion = 1.3 -junitPlatformVersion = 1.0.0-M4 -junitJupiterVersion = 5.0.0-M4 +junitPlatformVersion = 1.0.0-M5 +junitJupiterVersion = 5.0.0-M5 diff --git a/roboslack-api/build.gradle b/roboslack-api/build.gradle index 591ef6c..5816dd3 100644 --- a/roboslack-api/build.gradle +++ b/roboslack-api/build.gradle @@ -20,12 +20,8 @@ apply from: "${rootDir}/gradle/immutables.gradle" apply from: "${rootDir}/gradle/publish.gradle" dependencies { + compile project(":extras:jackson-support") compile "com.google.guava:guava:${guavaVersion}" - compile "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" - compile "com.fasterxml.jackson.module:jackson-module-afterburner:${jacksonVersion}" - compile "com.fasterxml.jackson.datatype:jackson-datatype-jdk8:${jacksonVersion}" - compile "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:${jacksonVersion}" - compile "com.fasterxml.jackson.datatype:jackson-datatype-guava:${jacksonVersion}" testCompile "org.hamcrest:hamcrest-all:${hamcrestVersion}" testCompile "org.mockito:mockito-core:${mockitoVersion}" diff --git a/roboslack-api/src/main/java/com/palantir/roboslack/api/MessageRequest.java b/roboslack-api/src/main/java/com/palantir/roboslack/api/MessageRequest.java index 05dfe2c..c3eddf8 100644 --- a/roboslack-api/src/main/java/com/palantir/roboslack/api/MessageRequest.java +++ b/roboslack-api/src/main/java/com/palantir/roboslack/api/MessageRequest.java @@ -192,6 +192,7 @@ public interface Builder { Builder iconUrl(URL iconUrl); Builder username(String username); Builder channel(String channel); + Builder from(MessageRequest messageRequest); MessageRequest build(); } diff --git a/roboslack-api/src/main/java/com/palantir/roboslack/api/attachments/Attachment.java b/roboslack-api/src/main/java/com/palantir/roboslack/api/attachments/Attachment.java index 57d1bca..0112737 100644 --- a/roboslack-api/src/main/java/com/palantir/roboslack/api/attachments/Attachment.java +++ b/roboslack-api/src/main/java/com/palantir/roboslack/api/attachments/Attachment.java @@ -24,15 +24,19 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.palantir.roboslack.api.attachments.components.Author; import com.palantir.roboslack.api.attachments.components.Color; import com.palantir.roboslack.api.attachments.components.Field; import com.palantir.roboslack.api.attachments.components.Footer; import com.palantir.roboslack.api.attachments.components.Title; +import com.palantir.roboslack.api.markdown.MarkdownInput; import com.palantir.roboslack.utils.MorePreconditions; import java.net.URL; +import java.util.Collection; import java.util.List; import java.util.Optional; +import java.util.Set; import javax.annotation.Nullable; import org.immutables.value.Value; @@ -54,6 +58,7 @@ public abstract class Attachment { private static final String PRETEXT_FIELD = "pretext"; private static final String IMAGE_URL_FIELD = "image_url"; private static final String THUMB_URL_FIELD = "thumb_url"; + private static final String MARKDOWN_INPUTS_FIELD = "mrkdwn_in"; /** * Generate a new {@link Attachment.Builder}. @@ -67,13 +72,11 @@ public static Builder builder() { @Value.Check protected final void check() { checkArgument(!Strings.isNullOrEmpty(fallback()), "Attachment fallback message cannot be null or empty"); - MorePreconditions.checkDoesNotContainMarkdown(FALLBACK_FIELD, fallback()); - pretext().ifPresent(s -> MorePreconditions.checkDoesNotContainMarkdown(PRETEXT_FIELD, s)); } /** * The {@link List} get {@link Field}s for this {@link Attachment}. Fields are displayed in a tabular fashion near - * the bottom get the {@link Attachment}. + * the bottom of the {@link Attachment}. * * @return the {@link List} get {@link Field}s */ @@ -82,25 +85,10 @@ public List fields() { return ImmutableList.of(); } - public interface Builder { - Builder fallback(String fallback); - Builder color(Color color); - Builder pretext(String pretext); - Builder author(Author author); - Builder title(Title title); - Builder text(String text); - Builder addFields(Field field); - Builder addFields(Field... fields); - Builder fields(Iterable elements); - Builder imageUrl(URL imageUrl); - Builder thumbUrl(URL thumbUrl); - Builder footer(Footer footer); - Attachment build(); - } - /** - * The plaintext summary of this {@link Attachment}. This text is used in clients that don't show formatted text - * (eg. IRC, mobile notifications) and should not contain any markup. + * The plaintext summary of this {@link Attachment} used in clients that don't display formatted text.
+ * Note: If this text contains any {@link com.palantir.roboslack.api.markdown.SlackMarkdown} special + * characters, they will be treated as literal plaintext characters when rendered in any Slack client. * * @return the {@code fallback} text */ @@ -201,4 +189,60 @@ public Footer footer() { return null; } + /** + * A special list of flags that tells Slack where to expect Markdown in an Attachment. + * Valid values are ["pretext", "text", "fields"]. + * + * @return the {@link Collection} of {@code markdownInputs} + */ + @Value.Default + @JsonProperty(MARKDOWN_INPUTS_FIELD) + public Set markdownInputs() { + // inspect the values of the Attachment object and create the mrkdwnIn list. + ImmutableSet.Builder markdownInputs = ImmutableSet.builder(); + // check if the pretext contains Markdown. + if (pretext().isPresent() && MorePreconditions.containsMarkdown(pretext().get())) { + markdownInputs.add(MarkdownInput.PRETEXT); + } + // check if the text contains Markdown. + if (text().isPresent() && MorePreconditions.containsMarkdown(text().get())) { + markdownInputs.add(MarkdownInput.TEXT); + } + // check if any of the Fields' values contain Markdown. + fields().stream() + .map(Field::value) + .filter(MorePreconditions::containsMarkdown) + .findFirst() + .ifPresent(ignored -> markdownInputs.add(MarkdownInput.FIELDS)); + return markdownInputs.build(); + } + + public interface Builder { + Builder fallback(String fallback); + + Builder color(Color color); + + Builder pretext(String pretext); + + Builder author(Author author); + + Builder title(Title title); + + Builder text(String text); + + Builder addFields(Field field); + + Builder addFields(Field... fields); + + Builder fields(Iterable elements); + + Builder imageUrl(URL imageUrl); + + Builder thumbUrl(URL thumbUrl); + + Builder footer(Footer footer); + + Attachment build(); + } + } diff --git a/roboslack-api/src/main/java/com/palantir/roboslack/api/attachments/components/Author.java b/roboslack-api/src/main/java/com/palantir/roboslack/api/attachments/components/Author.java index 904ee27..ddc1131 100644 --- a/roboslack-api/src/main/java/com/palantir/roboslack/api/attachments/components/Author.java +++ b/roboslack-api/src/main/java/com/palantir/roboslack/api/attachments/components/Author.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.palantir.roboslack.utils.MorePreconditions; import java.net.URL; import java.util.Optional; import org.immutables.value.Value; @@ -50,18 +49,13 @@ public static Author of(String name) { @Value.Check protected final void check() { - MorePreconditions.checkDoesNotContainMarkdown(NAME_FIELD, name()); - } - public interface Builder { - Builder name(String name); - Builder link(URL link); - Builder icon(URL icon); - Author build(); } /** - * Small text used to display this {@link Author}'s {@code name}. + * Small text used to display this {@link Author}'s {@code name}.
+ * Note: If this text contains any {@link com.palantir.roboslack.api.markdown.SlackMarkdown} special + * characters, they will be treated as literal plaintext characters when rendered in any Slack client. * * @return the author's name */ @@ -71,7 +65,7 @@ public interface Builder { /** * A valid {@link URL} that will be applied to the {@link Author#name()}. * - * @return an {@link Optional} containing the link applied to the {@code name} get the {@link Author} + * @return an {@link Optional} containing the link applied to the {@code name} for the {@link Author} */ @JsonProperty(LINK_FIELD) public abstract Optional link(); @@ -80,10 +74,20 @@ public interface Builder { * A valid {@link URL} that referencing a small 16x16px image that is displayed the left of the {@link * Author#name()}. * - * @return an {@link Optional} containing the link to the {@code icon} get the {@link Author} + * @return an {@link Optional} containing the link to the {@code icon} for the {@link Author} */ @JsonProperty(ICON_FIELD) public abstract Optional icon(); + public interface Builder { + Builder name(String name); + + Builder link(URL link); + + Builder icon(URL icon); + + Author build(); + } + } diff --git a/roboslack-api/src/main/java/com/palantir/roboslack/api/attachments/components/Color.java b/roboslack-api/src/main/java/com/palantir/roboslack/api/attachments/components/Color.java index 9571ac6..35dd519 100644 --- a/roboslack-api/src/main/java/com/palantir/roboslack/api/attachments/components/Color.java +++ b/roboslack-api/src/main/java/com/palantir/roboslack/api/attachments/components/Color.java @@ -18,11 +18,14 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreType; -import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonValue; -import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.google.common.base.Joiner; import com.palantir.roboslack.utils.MorePreconditions; +import java.io.IOException; import java.util.Arrays; import java.util.Optional; import javax.annotation.CheckForNull; @@ -36,12 +39,11 @@ * @since 0.1.0 */ @Value.Immutable -@JsonSerialize(as = ImmutableColor.class) +@Value.Style(jacksonIntegration = false) +@JsonDeserialize(using = Color.Deserializer.class) public abstract class Color { - private static final String VALUE_FIELD = "color"; - - protected static Builder builder() { + private static Builder builder() { return ImmutableColor.builder(); } @@ -50,7 +52,7 @@ public static Color of(Preset preset) { } @JsonCreator - public static Color of(@JsonProperty(VALUE_FIELD) String value) { + public static Color of(String value) { return builder().value(value).build(); } @@ -108,6 +110,11 @@ public Preset asPreset() { return Preset.of(value()).get(); } + @Override + public final String toString() { + return value(); + } + @JsonIgnoreType public enum Preset { /** @@ -150,6 +157,15 @@ protected interface Builder { Color build(); } + static class Deserializer extends JsonDeserializer { + + @Override + public Color deserialize(JsonParser parser, DeserializationContext ctxt) + throws IOException { + return Color.of(parser.getValueAsString()); + } + } + /** * Represents either a hex color value in the form of {@code #XXXXXX} or any defined {@link Preset#toString()}. * @@ -158,9 +174,4 @@ protected interface Builder { @JsonValue public abstract String value(); - @Override - public final String toString() { - return value(); - } - } diff --git a/roboslack-api/src/main/java/com/palantir/roboslack/api/attachments/components/Field.java b/roboslack-api/src/main/java/com/palantir/roboslack/api/attachments/components/Field.java index 27d8b60..b2727e4 100644 --- a/roboslack-api/src/main/java/com/palantir/roboslack/api/attachments/components/Field.java +++ b/roboslack-api/src/main/java/com/palantir/roboslack/api/attachments/components/Field.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import com.palantir.roboslack.utils.MorePreconditions; import org.immutables.value.Value; /** @@ -59,19 +58,14 @@ public boolean isShort() { @Value.Check protected final void check() { - MorePreconditions.checkDoesNotContainMarkdown(TITLE_FIELD, title()); - MorePreconditions.checkDoesNotContainMarkdown(VALUE_FIELD, value()); - } - public interface Builder { - Builder title(String title); - Builder value(String value); - Builder isShort(boolean isShort); - Field build(); } /** - * The bold heading above the {@link Field#value()} text. + * The bold heading above the {@link Field#value()} text.
+ * Note: If this text contains any {@link com.palantir.roboslack.api.markdown.SlackMarkdown} special + * characters, they will be treated as literal plaintext characters when rendered in any Slack client. + * Note that Slack does allow this field to contain emoji, but no other Markdown. * * @return the title */ @@ -86,4 +80,14 @@ public interface Builder { @JsonProperty(VALUE_FIELD) public abstract String value(); + public interface Builder { + Builder title(String title); + + Builder value(String value); + + Builder isShort(boolean isShort); + + Field build(); + } + } diff --git a/roboslack-api/src/main/java/com/palantir/roboslack/api/attachments/components/Footer.java b/roboslack-api/src/main/java/com/palantir/roboslack/api/attachments/components/Footer.java index 6f11d57..bf096e9 100644 --- a/roboslack-api/src/main/java/com/palantir/roboslack/api/attachments/components/Footer.java +++ b/roboslack-api/src/main/java/com/palantir/roboslack/api/attachments/components/Footer.java @@ -56,7 +56,6 @@ public static Footer of(String text) { @Value.Check protected final void check() { - MorePreconditions.checkDoesNotContainMarkdown(TEXT_FIELD, text()); MorePreconditions.checkCharacterLength(TEXT_FIELD, text(), MAX_FOOTER_CHARACTER_LENGTH); } @@ -68,7 +67,9 @@ public interface Builder { } /** - * Text that describes and contextualizes its attachment. + * Text that describes and contextualizes its attachment.
+ * Note: If this text contains any {@link com.palantir.roboslack.api.markdown.SlackMarkdown} special + * characters, they will be treated as literal plaintext characters when rendered in any Slack client. * * @return the text */ diff --git a/roboslack-api/src/main/java/com/palantir/roboslack/api/attachments/components/Title.java b/roboslack-api/src/main/java/com/palantir/roboslack/api/attachments/components/Title.java index 9580ab4..6963219 100644 --- a/roboslack-api/src/main/java/com/palantir/roboslack/api/attachments/components/Title.java +++ b/roboslack-api/src/main/java/com/palantir/roboslack/api/attachments/components/Title.java @@ -22,7 +22,6 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import com.google.common.base.Strings; -import com.palantir.roboslack.utils.MorePreconditions; import java.net.URL; import java.util.Optional; import org.immutables.value.Value; @@ -51,18 +50,13 @@ public static Title of(String text) { @Value.Check protected final void check() { - MorePreconditions.checkDoesNotContainMarkdown(TEXT_FIELD, text()); checkArgument(!Strings.isNullOrEmpty(text()), "The title text field cannot be null or empty"); } - public interface Builder { - Builder text(String text); - Builder link(URL link); - Title build(); - } - /** - * The title text displayed at the top get the message attachment. + * The title text displayed at the top of the message attachment.
+ * Note: If this text contains any {@link com.palantir.roboslack.api.markdown.SlackMarkdown} special + * characters, they will be treated as literal plaintext characters when rendered in any Slack client. * * @return the title text */ @@ -77,4 +71,12 @@ public interface Builder { @JsonProperty(LINK_FIELD) public abstract Optional link(); + public interface Builder { + Builder text(String text); + + Builder link(URL link); + + Title build(); + } + } diff --git a/roboslack-api/src/main/java/com/palantir/roboslack/api/markdown/MarkdownInput.java b/roboslack-api/src/main/java/com/palantir/roboslack/api/markdown/MarkdownInput.java new file mode 100644 index 0000000..2b90961 --- /dev/null +++ b/roboslack-api/src/main/java/com/palantir/roboslack/api/markdown/MarkdownInput.java @@ -0,0 +1,63 @@ +/* + * Copyright 2017 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.palantir.roboslack.api.markdown; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import java.util.Arrays; +import javax.annotation.CheckForNull; + +/** + * Enumerated types for handling the mrkdwn_in flag in Slack message formatting. + * + * @since v0.2.2 + */ +@JsonSerialize(using = ToStringSerializer.class) +public enum MarkdownInput { + /** + * Pretext. + */ + PRETEXT, + /** + * Text. + */ + TEXT, + /** + * Fields. + */ + FIELDS; + + private static final String NOT_FOUND_ERR = "No Markdown Input value matching: %s"; + + @JsonCreator + public static MarkdownInput of(@CheckForNull String value) { + return Arrays.stream(MarkdownInput.values()) + .filter(preset -> preset.toString().equalsIgnoreCase(value)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(String.format(NOT_FOUND_ERR, value))); + } + + public String value() { + return this.toString(); + } + + @Override + public String toString() { + return name().toLowerCase(); + } +} diff --git a/roboslack-api/src/main/java/com/palantir/roboslack/api/markdown/SlackMarkdown.java b/roboslack-api/src/main/java/com/palantir/roboslack/api/markdown/SlackMarkdown.java index ccbd67d..d126658 100644 --- a/roboslack-api/src/main/java/com/palantir/roboslack/api/markdown/SlackMarkdown.java +++ b/roboslack-api/src/main/java/com/palantir/roboslack/api/markdown/SlackMarkdown.java @@ -42,7 +42,7 @@ public final class SlackMarkdown { */ public static final String BOLD_DECORATION = "*"; public static final String ITALIC_DECORATION = "_"; - public static final String STRIKE_DECORATION = "-"; + public static final String STRIKE_DECORATION = "~"; public static final String EMOJI_DECORATION = ":"; public static final String PREFORMAT_DECORATION = "`"; public static final String PREFORMAT_MULTILINE_DECORATION = "```"; @@ -66,7 +66,7 @@ public final class SlackMarkdown { public static final ValueDecorator EMOJI = StringDecorator.of(EMOJI_DECORATION); public static final ValueDecorator PREFORMAT = StringDecorator.of(PREFORMAT_DECORATION); public static final ValueDecorator PREFORMAT_MULTILINE = StringDecorator.of( - PREFORMAT_MULTILINE_DECORATION + NEWLINE_SEPARATOR, PREFORMAT_MULTILINE_DECORATION); + PREFORMAT_MULTILINE_DECORATION + NEWLINE_SEPARATOR, NEWLINE_SEPARATOR + PREFORMAT_MULTILINE_DECORATION); public static final ValueDecorator MENTION_USER = StringDecorator.ofPrefix(MENTION_USER_PREFIX); public static final ValueDecorator MENTION_CHANNEL = StringDecorator.ofPrefix(MENTION_CHANNEL_PREFIX); public static final ValueDecorator QUOTE = StringDecorator.of(NEWLINE_SEPARATOR + QUOTE_PREFIX, diff --git a/roboslack-api/src/main/java/com/palantir/roboslack/utils/MorePreconditions.java b/roboslack-api/src/main/java/com/palantir/roboslack/utils/MorePreconditions.java index 41be7f2..421e5c9 100644 --- a/roboslack-api/src/main/java/com/palantir/roboslack/utils/MorePreconditions.java +++ b/roboslack-api/src/main/java/com/palantir/roboslack/utils/MorePreconditions.java @@ -50,10 +50,6 @@ public static void checkCharacterLength(String fieldName, String content, int ch content.length()); } - public static void checkDoesNotContainMarkdown(String fieldName, String content) { - checkArgument(!containsMarkdown(content), MARKDOWN_ERROR_FORMAT, fieldName); - } - public static void checkHexColor(String value) { checkArgument(HEX_COLOR_PATTERN.matcher(value).find(), HEX_COLOR_ERROR_FORMAT, value); } @@ -75,12 +71,12 @@ public static void checkAtLeastOnePresentAndValid(Collection fieldNames, /** * Returns true if the parameter {@link String} contains any symbols that Slack would process as markdown. * Bold, italic, strikethrough, and emojis are tested for in pairs - e.g. one asterisk will not return true, but - * two will. + * a pair will. * * @param text {@link String} for Slack markdown * @return {@link boolean} telling us if Slack markdown was found */ - private static boolean containsMarkdown(@CheckForNull String text) { + public static boolean containsMarkdown(@CheckForNull String text) { return text != null && SlackMarkdown.PATTERN.matcher(text).find(); } diff --git a/roboslack-api/src/test/java/com/palantir/roboslack/api/MessageRequestTests.java b/roboslack-api/src/test/java/com/palantir/roboslack/api/MessageRequestTests.java index 9caac1a..3b6d87d 100644 --- a/roboslack-api/src/test/java/com/palantir/roboslack/api/MessageRequestTests.java +++ b/roboslack-api/src/test/java/com/palantir/roboslack/api/MessageRequestTests.java @@ -20,16 +20,31 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import com.fasterxml.jackson.databind.JsonNode; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.palantir.roboslack.api.attachments.Attachment; +import com.palantir.roboslack.api.attachments.AttachmentTests; import com.palantir.roboslack.api.markdown.SlackMarkdown; +import com.palantir.roboslack.api.testing.MoreAssertions; +import com.palantir.roboslack.api.testing.ResourcesReader; import java.util.List; +import java.util.stream.Stream; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; class MessageRequestTests { + private static final String RESOURCES_DIRECTORY = "parameters"; + private static final String INPUT_EMOJI = "smile"; private static MessageRequest defaultWithIconEmoji(String iconEmoji) { @@ -55,6 +70,18 @@ private static List attachments(int count) { return attachments.build(); } + private static void assertValid(MessageRequest message) { + message.iconEmoji().ifPresent(iconEmoji -> + assertTrue(SlackMarkdown.EMOJI.decorate(iconEmoji).equalsIgnoreCase(iconEmoji))); + message.channel().ifPresent(channel -> + assertTrue(channel.startsWith(SlackMarkdown.MENTION_CHANNEL_PREFIX) + || channel.startsWith(SlackMarkdown.MENTION_USER_PREFIX))); + message.iconUrl().ifPresent(iconUrl -> assertFalse(Strings.isNullOrEmpty(iconUrl.toString()))); + message.attachments().forEach(AttachmentTests::assertValid); + assertFalse(Strings.isNullOrEmpty(message.username())); + assertFalse(Strings.isNullOrEmpty(message.text())); + } + @Test void testNormalizationEmoji() { MessageRequest message = defaultWithIconEmoji(INPUT_EMOJI); @@ -74,4 +101,21 @@ void testTooManyAttachments() { MessageRequest.MAX_ATTACHMENTS_COUNT))); } + @ParameterizedTest + @ArgumentsSource(SerializedMessageRequestsProvider.class) + void testDeserialization(JsonNode json) { + MoreAssertions.assertSerializable(json, + MessageRequest.class, + MessageRequestTests::assertValid); + } + + static class SerializedMessageRequestsProvider implements ArgumentsProvider { + + @Override + public Stream provideArguments(ExtensionContext context) throws Exception { + return ResourcesReader.readJson(RESOURCES_DIRECTORY).map(Arguments::of); + } + + } + } diff --git a/roboslack-api/src/test/java/com/palantir/roboslack/api/attachments/AttachmentTests.java b/roboslack-api/src/test/java/com/palantir/roboslack/api/attachments/AttachmentTests.java index 5d371e4..ac2b66d 100644 --- a/roboslack-api/src/test/java/com/palantir/roboslack/api/attachments/AttachmentTests.java +++ b/roboslack-api/src/test/java/com/palantir/roboslack/api/attachments/AttachmentTests.java @@ -23,28 +23,31 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; +import com.fasterxml.jackson.databind.JsonNode; import com.google.common.base.Strings; import com.palantir.roboslack.api.attachments.components.AuthorTests; import com.palantir.roboslack.api.attachments.components.ColorTests; import com.palantir.roboslack.api.attachments.components.FieldTests; import com.palantir.roboslack.api.attachments.components.FooterTests; import com.palantir.roboslack.api.attachments.components.TitleTests; -import com.palantir.roboslack.api.testing.ResourcesDeserializer; +import com.palantir.roboslack.api.testing.MoreAssertions; +import com.palantir.roboslack.api.testing.ResourcesReader; import java.util.Optional; import java.util.stream.Stream; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ContainerExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.function.Executable; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.ArgumentsProvider; import org.junit.jupiter.params.provider.ArgumentsSource; import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.ObjectArrayArguments; -class AttachmentTests { +public final class AttachmentTests { - private static void assertValid(Attachment attachment) { + private static final String RESOURCES_DIRECTORY = "parameters/attachments"; + + public static void assertValid(Attachment attachment) { attachment.fields().forEach(FieldTests::assertValid); assertFalse(Strings.isNullOrEmpty(attachment.fallback())); Optional.ofNullable(attachment.color()).ifPresent(ColorTests::assertValid); @@ -60,9 +63,7 @@ private static void assertValid(Attachment attachment) { @SuppressWarnings("unchecked") // Called from reflection static Stream invalidConstructors() { return Stream.of( - () -> Attachment.builder().fallback("").build(), - () -> Attachment.builder().fallback("text").pretext("*mark* -down-").build(), - () -> Attachment.builder().fallback("_markdown_").build() + () -> Attachment.builder().fallback("").build() ); } @@ -77,7 +78,7 @@ void testConstructNoState() { } @ParameterizedTest - @MethodSource(names = "invalidConstructors") + @MethodSource(value = "invalidConstructors") void testConstructionConstraints(Executable executable) { Throwable thrown = assertThrows(IllegalArgumentException.class, executable); assertThat(thrown.getMessage(), either(containsString("cannot be null or empty")) @@ -86,20 +87,19 @@ void testConstructionConstraints(Executable executable) { @ParameterizedTest @ArgumentsSource(SerializedAttachmentsProvider.class) - void testDeserialization(Attachment attachment) { - assertValid(attachment); + void testDeserialization(JsonNode json) { + MoreAssertions.assertSerializable(json, + Attachment.class, + AttachmentTests::assertValid); } static class SerializedAttachmentsProvider implements ArgumentsProvider { - private static final String RESOURCES_DIRECTORY = "parameters/attachments"; - @Override - public Stream arguments(ContainerExtensionContext context) throws Exception { - return ResourcesDeserializer.deserialize(Attachment.class, RESOURCES_DIRECTORY) - .map(ObjectArrayArguments::create); + public Stream provideArguments(ExtensionContext context) throws Exception { + return ResourcesReader.readJson(RESOURCES_DIRECTORY).map(Arguments::of); } - } + } } diff --git a/roboslack-api/src/test/java/com/palantir/roboslack/api/attachments/components/AuthorTests.java b/roboslack-api/src/test/java/com/palantir/roboslack/api/attachments/components/AuthorTests.java index 15122ba..832de26 100644 --- a/roboslack-api/src/test/java/com/palantir/roboslack/api/attachments/components/AuthorTests.java +++ b/roboslack-api/src/test/java/com/palantir/roboslack/api/attachments/components/AuthorTests.java @@ -16,61 +16,43 @@ package com.palantir.roboslack.api.attachments.components; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsString; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; +import com.fasterxml.jackson.databind.JsonNode; import com.google.common.base.Strings; -import com.palantir.roboslack.api.testing.ResourcesDeserializer; +import com.palantir.roboslack.api.testing.MoreAssertions; +import com.palantir.roboslack.api.testing.ResourcesReader; import java.util.stream.Stream; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.extension.ContainerExtensionContext; -import org.junit.jupiter.api.function.Executable; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.ArgumentsProvider; import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.ObjectArrayArguments; public final class AuthorTests { + private static final String RESOURCES_DIRECTORY = "parameters/attachments/components/authors"; + public static void assertValid(Author author) { assertFalse(Strings.isNullOrEmpty(author.name())); author.link().ifPresent(Assertions::assertNotNull); author.icon().ifPresent(Assertions::assertNotNull); } - @SuppressWarnings("unused") // Called from reflection - static Stream invalidMarkdownConstructors() { - return Stream.of( - () -> Author.builder().name("*name*").build(), - () -> Author.of("-strike-") - ); - } - - @ParameterizedTest - @MethodSource(names = "invalidMarkdownConstructors") - void testDoesNotContainMarkdown(Executable executable) { - Throwable thrown = assertThrows(IllegalArgumentException.class, executable); - assertThat(thrown.getMessage(), containsString("cannot contain markdown")); - } - @ParameterizedTest @ArgumentsSource(SerializedAuthorsProvider.class) - void testDeserialization(Author author) { - assertValid(author); + void testSerialization(JsonNode json) { + MoreAssertions.assertSerializable(json, + Author.class, + AuthorTests::assertValid); } static class SerializedAuthorsProvider implements ArgumentsProvider { - private static final String RESOURCES_DIRECTORY = "parameters/attachments/components/authors"; - @Override - public Stream arguments(ContainerExtensionContext context) throws Exception { - return ResourcesDeserializer.deserialize(Author.class, RESOURCES_DIRECTORY) - .map(ObjectArrayArguments::create); + public Stream provideArguments(ExtensionContext context) throws Exception { + return ResourcesReader.readJson(RESOURCES_DIRECTORY).map(Arguments::of); } } diff --git a/roboslack-api/src/test/java/com/palantir/roboslack/api/attachments/components/ColorTests.java b/roboslack-api/src/test/java/com/palantir/roboslack/api/attachments/components/ColorTests.java index c19753c..91fb2ed 100644 --- a/roboslack-api/src/test/java/com/palantir/roboslack/api/attachments/components/ColorTests.java +++ b/roboslack-api/src/test/java/com/palantir/roboslack/api/attachments/components/ColorTests.java @@ -24,24 +24,27 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import com.fasterxml.jackson.databind.JsonNode; import com.google.common.base.Strings; +import com.palantir.roboslack.api.testing.MoreAssertions; import com.palantir.roboslack.api.testing.MoreReflection; -import com.palantir.roboslack.api.testing.ResourcesDeserializer; +import com.palantir.roboslack.api.testing.ResourcesReader; import java.util.stream.Stream; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.TestFactory; -import org.junit.jupiter.api.extension.ContainerExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.ArgumentsProvider; import org.junit.jupiter.params.provider.ArgumentsSource; import org.junit.jupiter.params.provider.EnumSource; -import org.junit.jupiter.params.provider.ObjectArrayArguments; import org.junit.jupiter.params.provider.ValueSource; public final class ColorTests { + private static final String RESOURCES_DIRECTORY = "parameters/attachments/components/colors"; + public static void assertValid(Color color) { Assertions.assertFalse(Strings.isNullOrEmpty(color.toString())); Assertions.assertFalse(Strings.isNullOrEmpty(color.value())); @@ -98,19 +101,19 @@ Stream testNoArgStaticFactories() { @ParameterizedTest @ArgumentsSource(SerializedColorsProvider.class) - void testDeserialization(Color color) { - assertValid(color); + void testSerialization(JsonNode json) { + MoreAssertions.assertSerializable(json, + Color.class, + ColorTests::assertValid); } static class SerializedColorsProvider implements ArgumentsProvider { - private static final String RESOURCES_DIRECTORY = "parameters/attachments/components/colors"; - @Override - public Stream arguments(ContainerExtensionContext context) throws Exception { - return ResourcesDeserializer.deserialize(Color.class, RESOURCES_DIRECTORY) - .map(ObjectArrayArguments::create); + public Stream provideArguments(ExtensionContext context) throws Exception { + return ResourcesReader.readJson(RESOURCES_DIRECTORY).map(Arguments::of); } + } } diff --git a/roboslack-api/src/test/java/com/palantir/roboslack/api/attachments/components/FieldTests.java b/roboslack-api/src/test/java/com/palantir/roboslack/api/attachments/components/FieldTests.java index 87651b7..91bbe78 100644 --- a/roboslack-api/src/test/java/com/palantir/roboslack/api/attachments/components/FieldTests.java +++ b/roboslack-api/src/test/java/com/palantir/roboslack/api/attachments/components/FieldTests.java @@ -17,62 +17,43 @@ package com.palantir.roboslack.api.attachments.components; -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.containsString; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertThrows; +import com.fasterxml.jackson.databind.JsonNode; import com.google.common.base.Strings; -import com.palantir.roboslack.api.testing.ResourcesDeserializer; +import com.palantir.roboslack.api.testing.MoreAssertions; +import com.palantir.roboslack.api.testing.ResourcesReader; import java.util.stream.Stream; -import org.junit.jupiter.api.extension.ContainerExtensionContext; -import org.junit.jupiter.api.function.Executable; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.ArgumentsProvider; import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.ObjectArrayArguments; public final class FieldTests { + private static final String RESOURCES_DIRECTORY = "parameters/attachments/components/fields"; + public static void assertValid(Field field) { assertFalse(Strings.isNullOrEmpty(field.title())); assertFalse(Strings.isNullOrEmpty(field.value())); } - @SuppressWarnings("unused") // Called from reflection - static Stream invalidMarkdownConstructors() { - return Stream.of( - () -> Field.of("*title with bold*", "Valid"), - () -> Field.of("Valid", "-strike through text-"), - () -> Field.builder().title("_Sad Times_") - .value("Hello *failing* test! :smile:").build() - ); - } - @ParameterizedTest @ArgumentsSource(SerializedFieldsProvider.class) - void testDeserialization(Field field) { - assertValid(field); - } - - @ParameterizedTest - @MethodSource(names = "invalidMarkdownConstructors") - void testTitleCannotContainMarkdown(Executable executable) { - Throwable thrown = assertThrows(IllegalArgumentException.class, executable); - assertThat(thrown.getMessage(), containsString("cannot contain markdown")); + void testSerialization(JsonNode json) { + MoreAssertions.assertSerializable(json, + Field.class, + FieldTests::assertValid); } static class SerializedFieldsProvider implements ArgumentsProvider { - private static final String RESOURCES_DIRECTORY = "parameters/attachments/components/fields"; - @Override - public Stream arguments(ContainerExtensionContext context) throws Exception { - return ResourcesDeserializer.deserialize(Field.class, RESOURCES_DIRECTORY) - .map(ObjectArrayArguments::create); + public Stream provideArguments(ExtensionContext context) throws Exception { + return ResourcesReader.readJson(RESOURCES_DIRECTORY).map(Arguments::of); } + } } diff --git a/roboslack-api/src/test/java/com/palantir/roboslack/api/attachments/components/FooterTests.java b/roboslack-api/src/test/java/com/palantir/roboslack/api/attachments/components/FooterTests.java index ab77395..4774d7e 100644 --- a/roboslack-api/src/test/java/com/palantir/roboslack/api/attachments/components/FooterTests.java +++ b/roboslack-api/src/test/java/com/palantir/roboslack/api/attachments/components/FooterTests.java @@ -21,29 +21,22 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; +import com.fasterxml.jackson.databind.JsonNode; import com.google.common.base.Strings; -import com.palantir.roboslack.api.testing.ResourcesDeserializer; +import com.palantir.roboslack.api.testing.MoreAssertions; +import com.palantir.roboslack.api.testing.ResourcesReader; import java.util.Random; import java.util.stream.Stream; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ContainerExtensionContext; -import org.junit.jupiter.api.function.Executable; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.ArgumentsProvider; import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.ObjectArrayArguments; public final class FooterTests { - @SuppressWarnings("unused") // Called from reflection - static Stream invalidMarkdownConstructors() { - return Stream.of( - () -> Footer.of("*footer*"), - () -> Footer.builder().text("-footer-").build() - ); - } + private static final String RESOURCES_DIRECTORY = "parameters/attachments/components/footers"; private static String generateRandomStringOfSize(int size) { return new Random().ints('a', 'z') @@ -60,13 +53,6 @@ public static void assertValid(Footer footer) { assertFalse(Strings.isNullOrEmpty(timestamp.toString()))); } - @ParameterizedTest - @MethodSource(names = "invalidMarkdownConstructors") - void testDoesNotContainMarkdown(Executable executable) { - Throwable thrown = assertThrows(IllegalArgumentException.class, executable); - assertThat(thrown.getMessage(), containsString("cannot contain markdown")); - } - @Test void testDoesNotExceedMaxCharacterLength() { String string = generateRandomStringOfSize(Footer.MAX_FOOTER_CHARACTER_LENGTH + 1); @@ -77,18 +63,17 @@ void testDoesNotExceedMaxCharacterLength() { @ParameterizedTest @ArgumentsSource(SerializedFootersProvider.class) - void testDeserialization(Footer footer) { - assertValid(footer); + void testDeserialization(JsonNode json) { + MoreAssertions.assertSerializable(json, + Footer.class, + FooterTests::assertValid); } static class SerializedFootersProvider implements ArgumentsProvider { - private static final String RESOURCES_DIRECTORY = "parameters/attachments/components/footers"; - @Override - public Stream arguments(ContainerExtensionContext context) throws Exception { - return ResourcesDeserializer.deserialize(Footer.class, RESOURCES_DIRECTORY) - .map(ObjectArrayArguments::create); + public Stream provideArguments(ExtensionContext context) throws Exception { + return ResourcesReader.readJson(RESOURCES_DIRECTORY).map(Arguments::of); } } diff --git a/roboslack-api/src/test/java/com/palantir/roboslack/api/attachments/components/TitleTests.java b/roboslack-api/src/test/java/com/palantir/roboslack/api/attachments/components/TitleTests.java index 96fe92d..efdd40c 100644 --- a/roboslack-api/src/test/java/com/palantir/roboslack/api/attachments/components/TitleTests.java +++ b/roboslack-api/src/test/java/com/palantir/roboslack/api/attachments/components/TitleTests.java @@ -22,20 +22,23 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; +import com.fasterxml.jackson.databind.JsonNode; import com.google.common.base.Strings; -import com.palantir.roboslack.api.testing.ResourcesDeserializer; +import com.palantir.roboslack.api.testing.MoreAssertions; +import com.palantir.roboslack.api.testing.ResourcesReader; import java.util.stream.Stream; -import org.junit.jupiter.api.extension.ContainerExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.function.Executable; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.ArgumentsProvider; import org.junit.jupiter.params.provider.ArgumentsSource; import org.junit.jupiter.params.provider.MethodSource; -import org.junit.jupiter.params.provider.ObjectArrayArguments; public final class TitleTests { + private static final String RESOURCES_DIRECTORY = "parameters/attachments/components/titles"; + public static void assertValid(Title title) { assertFalse(Strings.isNullOrEmpty(title.text())); title.link().ifPresent(link -> assertFalse(Strings.isNullOrEmpty(link.toString()))); @@ -44,14 +47,12 @@ public static void assertValid(Title title) { @SuppressWarnings("unused") // Called from reflection static Stream invalidConstructors() { return Stream.of( - () -> Title.of(""), - () -> Title.of("-strike-"), - () -> Title.builder().text("*bold*").build() + () -> Title.of("") ); } @ParameterizedTest - @MethodSource(names = "invalidConstructors") + @MethodSource(value = "invalidConstructors") void testConstructionConstraints(Executable executable) { Throwable thrown = assertThrows(IllegalArgumentException.class, executable); assertThat(thrown.getMessage(), @@ -61,18 +62,19 @@ void testConstructionConstraints(Executable executable) { @ParameterizedTest @ArgumentsSource(SerializedTitlesProvider.class) - void testDeserialization(Title title) { - assertValid(title); + void testDeserialization(JsonNode json) { + MoreAssertions.assertSerializable(json, + Title.class, + TitleTests::assertValid); } static class SerializedTitlesProvider implements ArgumentsProvider { - private static final String RESOURCES_DIRECTORY = "parameters/attachments/components/titles"; @Override - public Stream arguments(ContainerExtensionContext context) throws Exception { - return ResourcesDeserializer.deserialize(Title.class, RESOURCES_DIRECTORY) - .map(ObjectArrayArguments::create); + public Stream provideArguments(ExtensionContext context) throws Exception { + return ResourcesReader.readJson(RESOURCES_DIRECTORY).map(Arguments::of); } + } } diff --git a/roboslack-api/src/test/java/com/palantir/roboslack/api/markdown/LinkDecoratorTests.java b/roboslack-api/src/test/java/com/palantir/roboslack/api/markdown/LinkDecoratorTests.java index 0641996..086b1b3 100644 --- a/roboslack-api/src/test/java/com/palantir/roboslack/api/markdown/LinkDecoratorTests.java +++ b/roboslack-api/src/test/java/com/palantir/roboslack/api/markdown/LinkDecoratorTests.java @@ -45,13 +45,13 @@ static Stream illegalArgumentConstructors() { } @ParameterizedTest - @MethodSource(names = "illegalStateConstructors") + @MethodSource(value = "illegalStateConstructors") void testIllegalStateConstructors(Executable executable) { assertThrows(IllegalStateException.class, executable); } @ParameterizedTest - @MethodSource(names = "illegalArgumentConstructors") + @MethodSource(value = "illegalArgumentConstructors") void testIllegalArgumentConstruction(Executable executable) { Throwable thrown = assertThrows(IllegalArgumentException.class, executable); assertThat(thrown.getMessage(), containsString("present and valid")); diff --git a/roboslack-api/src/test/java/com/palantir/roboslack/api/markdown/StringDecoratorTests.java b/roboslack-api/src/test/java/com/palantir/roboslack/api/markdown/StringDecoratorTests.java index abee8af..382a74c 100644 --- a/roboslack-api/src/test/java/com/palantir/roboslack/api/markdown/StringDecoratorTests.java +++ b/roboslack-api/src/test/java/com/palantir/roboslack/api/markdown/StringDecoratorTests.java @@ -66,20 +66,20 @@ static Stream validDecorators() { } @ParameterizedTest - @MethodSource(names = "nullPointerExceptionConstructors") + @MethodSource(value = "nullPointerExceptionConstructors") void testNullConstruction(Executable executable) { assertThrows(NullPointerException.class, executable); } @ParameterizedTest - @MethodSource(names = "illegalArgumentConstructors") + @MethodSource(value = "illegalArgumentConstructors") void testInvalidConstruction(Executable executable) { Throwable thrown = assertThrows(IllegalArgumentException.class, executable); assertThat(thrown.getMessage(), containsString("present and valid")); } @ParameterizedTest - @MethodSource(names = "validDecorators") + @MethodSource(value = "validDecorators") void testDecorate(StringDecorator decorator) { String decorated = decorator.decorate(EXAMPLE_INPUT_STRING); assertThat(decorated, is(not(equalTo(EXAMPLE_INPUT_STRING)))); diff --git a/roboslack-api/src/test/java/com/palantir/roboslack/api/testing/MoreAssertions.java b/roboslack-api/src/test/java/com/palantir/roboslack/api/testing/MoreAssertions.java new file mode 100644 index 0000000..e11b45d --- /dev/null +++ b/roboslack-api/src/test/java/com/palantir/roboslack/api/testing/MoreAssertions.java @@ -0,0 +1,52 @@ +/* + * Copyright 2017 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.palantir.roboslack.api.testing; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.palantir.roboslack.jackson.ObjectMappers; +import java.io.IOException; +import java.util.function.Consumer; + +public final class MoreAssertions { + + private static final ObjectMapper OBJECT_MAPPER = ObjectMappers.newObjectMapper(); + + private MoreAssertions() {} + + public static void assertSerializable(JsonNode serialized, Class clazz, Consumer assertion) { + try { + // First try deserializing + T instance = OBJECT_MAPPER.readValue(OBJECT_MAPPER.writeValueAsString(serialized), clazz); + assertion.accept(instance); + // Then reserializing and comparing + String reserialized = OBJECT_MAPPER.writeValueAsString(instance); + assertEquals(serialized.toString(), reserialized, + String.format("Serialized input %s does not match reserialized string: %s", + Joiner.on(serialized.toString()).join(ImmutableList.of(System.lineSeparator())), + Joiner.on(reserialized).join(ImmutableList.of(System.lineSeparator())))); + } catch (IOException e) { + fail(e); + } + } + +} diff --git a/roboslack-api/src/test/java/com/palantir/roboslack/api/testing/ResourcesDeserializer.java b/roboslack-api/src/test/java/com/palantir/roboslack/api/testing/ResourcesReader.java similarity index 57% rename from roboslack-api/src/test/java/com/palantir/roboslack/api/testing/ResourcesDeserializer.java rename to roboslack-api/src/test/java/com/palantir/roboslack/api/testing/ResourcesReader.java index e7a4600..b06ea4a 100644 --- a/roboslack-api/src/test/java/com/palantir/roboslack/api/testing/ResourcesDeserializer.java +++ b/roboslack-api/src/test/java/com/palantir/roboslack/api/testing/ResourcesReader.java @@ -19,35 +19,50 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.Joiner; +import com.google.common.io.Files; import com.google.common.io.Resources; +import com.palantir.roboslack.jackson.ObjectMappers; import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.stream.Stream; -public final class ResourcesDeserializer { +public final class ResourcesReader { - private static final ObjectMapper MAPPER = ObjectMappers.newObjectMapper(); + private static final ObjectMapper OBJECT_MAPPER = ObjectMappers.newObjectMapper(); - private ResourcesDeserializer() {} + private ResourcesReader() {} - private static T readValueOrThrow(Class clazz, File file) { + private static Stream listFiles(String resourcesDirectory) { + File directory = new File(Resources.getResource(resourcesDirectory).getFile()); + checkArgument(directory.isDirectory(), "%s is not a directory", resourcesDirectory); + File[] files = directory.listFiles(); + checkNotNull(files, "No files found in '%s', or I/O exception occurred", resourcesDirectory); + return Stream.of(files).filter(File::isFile); + } + + private static String readString(File file) { try { - return MAPPER.readValue(file, clazz); + return Joiner.on(System.lineSeparator()) + .join(Files.readLines(file, StandardCharsets.UTF_8)); } catch (IOException e) { throw new IllegalArgumentException(e.getMessage(), e); } } - public static Stream deserialize(Class clazz, - String resourcesDirectory) throws Exception { - File directory = new File(Resources.getResource(resourcesDirectory).getFile()); - checkArgument(directory.isDirectory(), "%s is not a directory", resourcesDirectory); - File[] files = directory.listFiles(); - checkNotNull(files, "No files found in '%s', or I/O exception occurred", resourcesDirectory); - return Stream.of(files) - .filter(File::isFile) - .map(file -> readValueOrThrow(clazz, file)); + private static JsonNode readJson(File file) { + try { + return OBJECT_MAPPER.readTree(readString(file)); + } catch (IOException e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + } + + public static Stream readJson(String resourcesDirectory) throws Exception { + return listFiles(resourcesDirectory).map(ResourcesReader::readJson); } } diff --git a/roboslack-api/src/test/resources/parameters/attachments/attachment1.json b/roboslack-api/src/test/resources/parameters/attachments/attachment1.json index f1a96fa..00754f7 100644 --- a/roboslack-api/src/test/resources/parameters/attachments/attachment1.json +++ b/roboslack-api/src/test/resources/parameters/attachments/attachment1.json @@ -12,7 +12,7 @@ } ], "fallback": "Fallback...", - "color": "#C3C6C4", + "color": "#c3c6c4", "pretext": "pretext", "author_name": "Author", "author_link": "https://platform.slack-edge.com/img/default_application_icon.png", @@ -24,5 +24,6 @@ "thumb_url": "https://www.palantir.com/build/images/about/phil-eng-hero.jpg", "footer": "Footer", "footer_icon": "https://platform.slack-edge.com/img/default_application_icon.png", - "ts": 1497012014 + "ts": 1497012014, + "mrkdwn_in": [] } diff --git a/roboslack-api/src/test/resources/parameters/attachments/components/colors/color1.json b/roboslack-api/src/test/resources/parameters/attachments/components/colors/color1.json index 764cb18..9d3ba41 100644 --- a/roboslack-api/src/test/resources/parameters/attachments/components/colors/color1.json +++ b/roboslack-api/src/test/resources/parameters/attachments/components/colors/color1.json @@ -1,3 +1 @@ -{ - "color": "good" -} +"good" diff --git a/roboslack-api/src/test/resources/parameters/attachments/components/colors/color2.json b/roboslack-api/src/test/resources/parameters/attachments/components/colors/color2.json index bf30973..8cdfd41 100644 --- a/roboslack-api/src/test/resources/parameters/attachments/components/colors/color2.json +++ b/roboslack-api/src/test/resources/parameters/attachments/components/colors/color2.json @@ -1,3 +1 @@ -{ - "color": "#8be9fd" -} +"#8be9fd" diff --git a/roboslack-api/src/test/resources/parameters/attachments/components/fields/field2.json b/roboslack-api/src/test/resources/parameters/attachments/components/fields/field2.json new file mode 100644 index 0000000..9d58763 --- /dev/null +++ b/roboslack-api/src/test/resources/parameters/attachments/components/fields/field2.json @@ -0,0 +1,5 @@ +{ + "title": "A Title", + "value": "Lorem Ipsum", + "short": false +} diff --git a/roboslack-api/src/test/resources/parameters/attachments/components/footers/footer2.json b/roboslack-api/src/test/resources/parameters/attachments/components/footers/footer2.json new file mode 100644 index 0000000..6f4bfd7 --- /dev/null +++ b/roboslack-api/src/test/resources/parameters/attachments/components/footers/footer2.json @@ -0,0 +1,5 @@ +{ + "footer": "Only footer text", + "footer_icon": null, + "ts": null +} diff --git a/roboslack-api/src/test/resources/parameters/message_request1.json b/roboslack-api/src/test/resources/parameters/message_request1.json new file mode 100644 index 0000000..57e045e --- /dev/null +++ b/roboslack-api/src/test/resources/parameters/message_request1.json @@ -0,0 +1,13 @@ +{ + "link_names": true, + "unfurl_media": true, + "unfurl_links": true, + "mrkdwn": true, + "parse": "full", + "attachments": [], + "text": "The simplest message", + "icon_emoji": ":smile:", + "icon_url": null, + "username": "robo-slack", + "channel": null +} diff --git a/roboslack-webhook-api/src/main/java/com/palantir/roboslack/webhook/api/SlackWebHook.java b/roboslack-webhook-api/src/main/java/com/palantir/roboslack/webhook/api/SlackWebHook.java index eed17a7..d478832 100644 --- a/roboslack-webhook-api/src/main/java/com/palantir/roboslack/webhook/api/SlackWebHook.java +++ b/roboslack-webhook-api/src/main/java/com/palantir/roboslack/webhook/api/SlackWebHook.java @@ -17,10 +17,10 @@ package com.palantir.roboslack.webhook.api; import com.palantir.roboslack.api.MessageRequest; +import com.palantir.roboslack.webhook.api.model.response.ResponseCode; import javax.ws.rs.Consumes; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; -import okhttp3.ResponseBody; import retrofit2.Call; import retrofit2.http.Body; import retrofit2.http.POST; @@ -37,7 +37,7 @@ public interface SlackWebHook { @POST("{token_t_part}/{token_b_part}/{token_x_part}") - Call sendMessage( + Call sendMessage( @Path("token_t_part") String tokenTPart, @Path("token_b_part") String tokenBPart, @Path("token_x_part") String tokenXPart, diff --git a/roboslack-webhook/build.gradle b/roboslack-webhook/build.gradle index 37d5355..c69e4c8 100644 --- a/roboslack-webhook/build.gradle +++ b/roboslack-webhook/build.gradle @@ -20,13 +20,13 @@ apply from: "${rootDir}/gradle/immutables.gradle" apply from: "${rootDir}/gradle/publish.gradle" dependencies { + compile project(':extras:slack-clients') + compile project(':roboslack-webhook-api') compile "com.google.guava:guava:${guavaVersion}" - // Retrofit for REST client specs - compile "com.palantir.remoting2:retrofit2-clients:${httpRemotingVersion}" - + testCompile "org.awaitility:awaitility:${awaitilityVersion}" testCompile "org.hamcrest:hamcrest-all:${hamcrestVersion}" testCompile "org.mockito:mockito-core:${mockitoVersion}" } diff --git a/roboslack-webhook/src/main/java/com/palantir/roboslack/webhook/ResponseCodeConverter.java b/roboslack-webhook/src/main/java/com/palantir/roboslack/webhook/ResponseCodeConverter.java new file mode 100644 index 0000000..facefd8 --- /dev/null +++ b/roboslack-webhook/src/main/java/com/palantir/roboslack/webhook/ResponseCodeConverter.java @@ -0,0 +1,44 @@ +/* + * Copyright 2017 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.palantir.roboslack.webhook; + +import com.palantir.roboslack.webhook.api.model.response.ResponseCode; +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import okhttp3.ResponseBody; +import retrofit2.Converter; +import retrofit2.Retrofit; + +public final class ResponseCodeConverter implements Converter { + + @Override + public ResponseCode convert(ResponseBody value) throws IOException { + return ResponseCode.of(value.string()); + } + + public static Converter.Factory factory() { + return new Converter.Factory() { + @Override + public Converter responseBodyConverter(Type type, Annotation[] annotations, + Retrofit retrofit) { + return new ResponseCodeConverter(); + } + }; + } + +} diff --git a/roboslack-webhook/src/main/java/com/palantir/roboslack/webhook/SlackWebHookService.java b/roboslack-webhook/src/main/java/com/palantir/roboslack/webhook/SlackWebHookService.java index 6b8996f..93c1fec 100644 --- a/roboslack-webhook/src/main/java/com/palantir/roboslack/webhook/SlackWebHookService.java +++ b/roboslack-webhook/src/main/java/com/palantir/roboslack/webhook/SlackWebHookService.java @@ -18,33 +18,40 @@ import static com.google.common.base.Preconditions.checkNotNull; -import com.palantir.remoting2.retrofit2.Retrofit2Client; import com.palantir.roboslack.api.MessageRequest; +import com.palantir.roboslack.clients.SlackClients; import com.palantir.roboslack.webhook.api.SlackWebHook; import com.palantir.roboslack.webhook.api.model.WebHookToken; import com.palantir.roboslack.webhook.api.model.response.ResponseCode; import java.io.IOException; -import okhttp3.ResponseBody; import retrofit2.Call; +import retrofit2.Callback; /** * Main entry point class to interact with a {@link SlackWebHook}. Instantiate it with a {@link WebHookToken} and a * {@code userAgent} {@link String}, then send your composed {@link MessageRequest}s via the {@link - * SlackWebHookService#sendMessage(MessageRequest)} method. Ensure that you check the returned {@link ResponseCode} for - * Slack status feedback. + * SlackWebHookService#sendMessageAsync(MessageRequest)} method. Ensure that you check the returned {@link ResponseCode} + * for Slack status feedback. */ public final class SlackWebHookService { - private static final String DEFAULT_USER_AGENT = "RoboSlack/1.0"; + private static final String TOKEN_ERR = "WebHookToken must be valid and non-null."; + private static final String DEFAULT_WEB_HOOK_URL = "https://hooks.slack.com/services/"; private final WebHookToken token; private final SlackWebHook webHook; private SlackWebHookService(WebHookToken token, String userAgent) { - this.token = checkNotNull(token, "WebHookToken must be valid and non-null."); - this.webHook = Retrofit2Client.builder() - .build(SlackWebHook.class, userAgent, DEFAULT_WEB_HOOK_URL); + this.token = checkNotNull(token, TOKEN_ERR); + this.webHook = SlackClients.create(SlackWebHook.class, userAgent, DEFAULT_WEB_HOOK_URL, + ResponseCodeConverter.factory()); + } + + private SlackWebHookService(WebHookToken token) { + this.token = checkNotNull(token, TOKEN_ERR); + this.webHook = SlackClients.create(SlackWebHook.class, DEFAULT_WEB_HOOK_URL, + ResponseCodeConverter.factory()); } /** @@ -54,7 +61,7 @@ private SlackWebHookService(WebHookToken token, String userAgent) { * @return the new {@link SlackWebHookService} interaction object */ public static SlackWebHookService with(WebHookToken token) { - return new SlackWebHookService(token, DEFAULT_USER_AGENT); + return new SlackWebHookService(token); } /** @@ -69,31 +76,33 @@ public static SlackWebHookService with(WebHookToken token, String userAgent) { return new SlackWebHookService(token, userAgent); } + private Call sendCall(MessageRequest messageRequest) { + return webHook.sendMessage(token.partT(), token.partB(), token.partX(), messageRequest); + } + /** - * We can't serialize the response correctly since Slack only sends back a 'text/html' string, - * so we manually pull it from the {@link ResponseBody} instead. + * Sends a message to a connected {@link SlackWebHookService} asynchronously using provided {@link Callback}. + * + * @param messageRequest the {@link MessageRequest} to execute sending + * @param callback the {@link Callback} to trigger on response */ - private static String executeCallAndGetResponseBody(Call call) { - try { - ResponseBody body = call.execute().body(); - if (body != null) { - return body.string(); - } - return ""; - } catch (IOException e) { - throw new RuntimeException("Unable to execute call", e); - } + public void sendMessageAsync(MessageRequest messageRequest, Callback callback) { + sendCall(messageRequest).enqueue(callback); } /** - * Sends a message to connected {@link SlackWebHookService}. + * Sends a message to connected {@link SlackWebHookService} synchronously. * * @param messageRequest the {@link MessageRequest} to execute sending * @return the resulting {@link ResponseCode} from the operation + * @throws IllegalStateException if unable to connect to Slack */ - public ResponseCode sendMessage(MessageRequest messageRequest) { - return ResponseCode.of(executeCallAndGetResponseBody(webHook - .sendMessage(token.partT(), token.partB(), token.partX(), messageRequest))); + public ResponseCode sendMessageAsync(MessageRequest messageRequest) { + try { + return sendCall(messageRequest).execute().body(); + } catch (IOException e) { + throw new IllegalStateException(String.format("Could not connect to %s.", DEFAULT_WEB_HOOK_URL), e); + } } } diff --git a/roboslack-webhook/src/test/java/com/palantir/roboslack/webhook/EnrichTestMessageRequest.java b/roboslack-webhook/src/test/java/com/palantir/roboslack/webhook/EnrichTestMessageRequest.java new file mode 100644 index 0000000..c10e51f --- /dev/null +++ b/roboslack-webhook/src/test/java/com/palantir/roboslack/webhook/EnrichTestMessageRequest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2017 Palantir Technologies, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.palantir.roboslack.webhook; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.palantir.roboslack.api.MessageRequest; +import com.palantir.roboslack.api.attachments.Attachment; +import java.util.function.BiFunction; +import org.junit.jupiter.api.TestInfo; + +final class EnrichTestMessageRequest implements BiFunction { + + private EnrichTestMessageRequest() {} + + public static EnrichTestMessageRequest get() { + return new EnrichTestMessageRequest(); + } + + @Override + public MessageRequest apply(MessageRequest messageRequest, TestInfo testInfo) { + checkArgument(testInfo.getTestClass().isPresent()); + checkArgument(testInfo.getTestMethod().isPresent()); + String methodName = testInfo.getTestMethod().get().getName(); + String className = testInfo.getTestClass().get().getSimpleName(); + String basicSummary = String.format("Called from %s within %s", + methodName, + className); + return MessageRequest.builder() + .from(messageRequest) + .addAttachments(Attachment.builder() + .fallback(basicSummary) + .text(basicSummary) + .build()) + .build(); + } + +} diff --git a/roboslack-webhook/src/test/java/com/palantir/roboslack/webhook/SlackWebHookServiceTests.java b/roboslack-webhook/src/test/java/com/palantir/roboslack/webhook/SlackWebHookServiceTests.java index b5ec662..f50ad96 100644 --- a/roboslack-webhook/src/test/java/com/palantir/roboslack/webhook/SlackWebHookServiceTests.java +++ b/roboslack-webhook/src/test/java/com/palantir/roboslack/webhook/SlackWebHookServiceTests.java @@ -20,6 +20,8 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assumptions.assumeTrue; import com.google.common.collect.ImmutableList; @@ -37,13 +39,20 @@ import java.net.URL; import java.time.LocalDateTime; import java.time.ZoneId; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Stream; -import org.junit.jupiter.api.extension.ContainerExtensionContext; +import javax.annotation.ParametersAreNonnullByDefault; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.TestInfo; +import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.ArgumentsProvider; import org.junit.jupiter.params.provider.ArgumentsSource; -import org.junit.jupiter.params.provider.ObjectArrayArguments; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; class SlackWebHookServiceTests { @@ -58,9 +67,36 @@ private static WebHookToken assumingEnvironmentWebHookToken() { @ParameterizedTest @ArgumentsSource(MessageRequestProvider.class) - void testSendMessage(MessageRequest messageRequest) { + void testSendMessage(MessageRequest messageRequest, TestInfo testInfo) { assertThat(SlackWebHookService.with(assumingEnvironmentWebHookToken()) - .sendMessage(messageRequest), is(equalTo(ResponseCode.OK))); + .sendMessageAsync(EnrichTestMessageRequest.get().apply(messageRequest, testInfo)), + is(equalTo(ResponseCode.OK))); + } + + @ParameterizedTest + @ArgumentsSource(MessageRequestProvider.class) + void testSendMessageAsync(MessageRequest messageRequest, TestInfo testInfo) { + AtomicBoolean submitted = new AtomicBoolean(false); + SlackWebHookService.with(assumingEnvironmentWebHookToken()) + .sendMessageAsync(EnrichTestMessageRequest.get().apply(messageRequest, testInfo), + new Callback() { + @Override + @ParametersAreNonnullByDefault + public void onResponse(Call call, Response response) { + submitted.set(true); + assertTrue(call.isExecuted()); + assertThat(response.body(), is(equalTo(ResponseCode.OK))); + } + + @Override + @ParametersAreNonnullByDefault + public void onFailure(Call call, Throwable throwable) { + submitted.set(true); + assertTrue(call.isExecuted()); + fail(throwable); + } + }); + Awaitility.await().atMost(5, TimeUnit.SECONDS).untilTrue(submitted); } static class MessageRequestProvider implements ArgumentsProvider { @@ -71,6 +107,78 @@ static class MessageRequestProvider implements ArgumentsProvider { .text("The simplest message") .build(); + private static final MessageRequest MESSAGE_MARKDOWN_IN_ATTACHMENT_PRETEXT = MessageRequest.builder() + .username("robo-slack") + .iconEmoji(SlackMarkdown.EMOJI.decorate("smile")) + .text("Message with Markdown pretext in Attachment") + .addAttachments(Attachment.builder() + .fallback("attachment fallback text") + .pretext(SlackMarkdown.BOLD.decorate("bold markdown")) + .text("some attachment text") + .build()) + .build(); + + private static final MessageRequest MESSAGE_MARKDOWN_IN_PLAINTEXT_ATTACHMENT_FIELDS = MessageRequest.builder() + .username("robo-slack") + .iconEmoji(SlackMarkdown.EMOJI.decorate("smile")) + .text("Message with Markdown in plaintext Attachment fields") + .addAttachments(Attachment.builder() + .fallback(SlackMarkdown.STRIKE.decorate("attachment fallback text")) + .text("some attachment text") + .title( + Title.builder() + .text(SlackMarkdown.PREFORMAT.decorate("preformat markdown")) + .build() + ) + .author( + Author.builder() + .name(SlackMarkdown.QUOTE.decorate("quote markdown")) + .build() + ) + .addFields( + Field.builder() + .title(SlackMarkdown.STRIKE.decorate("strikethrough markdown")) + .value("value") + .build() + ) + .footer( + Footer.builder() + .text(SlackMarkdown.ITALIC.decorate("italic markdown")) + .timestamp(LocalDateTime.now() + .atZone(ZoneId.systemDefault()).toEpochSecond()) + .build() + ) + .build()) + .build(); + + private static final MessageRequest MESSAGE_MARKDOWN_IN_ATTACHMENT_TEXT = MessageRequest.builder() + .username("robo-slack") + .iconEmoji(SlackMarkdown.EMOJI.decorate("smile")) + .text("Message with Markdown text in Attachment") + .addAttachments(Attachment.builder() + .fallback("attachment fallback text") + .text(SlackMarkdown.EMOJI.decorate("boom")) + .build()) + .build(); + + private static final MessageRequest MESSAGE_MARKDOWN_IN_ATTACHMENT_FIELDS = MessageRequest.builder() + .username("robo-slack") + .iconEmoji(SlackMarkdown.EMOJI.decorate("smile")) + .text("Message with Markdown in Attachment Field values") + .addAttachments(Attachment.builder() + .fallback("attachment fallback text") + .text("some attachment text") + .addFields(Field.builder() + .title("strike field") + .value(SlackMarkdown.STRIKE.decorate("strike text")) + .build(), + Field.builder() + .title("markdown field") + .value(SlackMarkdown.PREFORMAT.decorate("some code")) + .build()) + .build()) + .build(); + private static final MessageRequest MESSAGE_WITH_ATTACHMENT_FOOTER = MessageRequest.builder() .username("robo-slack") .iconEmoji("smile") @@ -152,15 +260,18 @@ private static URL url(String url) { } @Override - public Stream arguments(ContainerExtensionContext context) throws Exception { + public Stream provideArguments(ExtensionContext context) throws Exception { return Stream.of( MESSAGE_SIMPLE, + MESSAGE_MARKDOWN_IN_ATTACHMENT_PRETEXT, + MESSAGE_MARKDOWN_IN_PLAINTEXT_ATTACHMENT_FIELDS, + MESSAGE_MARKDOWN_IN_ATTACHMENT_TEXT, + MESSAGE_MARKDOWN_IN_ATTACHMENT_FIELDS, MESSAGE_WITH_ATTACHMENT_FOOTER, MESSAGE_WITH_ATTACHMENTS, MESSAGE_COMPLEX - ).map(ObjectArrayArguments::create); + ).map(Arguments::of); } - } } diff --git a/settings.gradle b/settings.gradle index c98ce26..ea5cf58 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,5 +1,8 @@ rootProject.name = 'roboslack' +include 'extras:jackson-support' +include 'extras:slack-clients' + include 'roboslack-api' include 'roboslack-webhook-api' include 'roboslack-webhook'