From ce3787ad13781d930e0b56fd58739b25092a3501 Mon Sep 17 00:00:00 2001 From: Dan Klco Date: Fri, 19 Aug 2022 14:44:25 -0400 Subject: [PATCH] Improving the process for configuring the marketo integration, fixes #2874 (#2875) * Improving the process for configuring the marketo integration, fixes #2874 --- CHANGELOG.md | 2 + .../marketo/client/MarketoApiException.java | 113 ++++++ .../commons/marketo/client/MarketoClient.java | 61 +-- .../client/impl/MarketoClientImpl.java | 379 +++++++++++------- .../impl/MarketoClientConfigurationImpl.java | 160 +++++--- .../marketo/impl/MarketoFieldDataSource.java | 1 + .../impl/TestMarketoConnectionServlet.java | 179 +++++++++ .../MarketoClientConfigurationImplTest.java | 91 +++++ .../impl/StaticResponseMarketoClient.java | 70 +++- .../client/impl/TestMarketoClient.java | 4 +- .../marketo/impl/MarketoAPIExceptionTest.java | 73 ++++ .../impl/TestMarketoClientConfiguration.java | 2 +- .../TestMarketoConnectionServletTest.java | 194 +++++++++ .../utilities/cloudconfig/clientlib/delete.js | 21 + .../cloudconfiglist/cloudconfiglist.html | 1 + .../marketo/_cq_dialog/.content.xml | 2 +- .../utilities/marketocloudconfig/.content.xml | 1 + 17 files changed, 1094 insertions(+), 260 deletions(-) create mode 100644 bundle/src/main/java/com/adobe/acs/commons/marketo/client/MarketoApiException.java create mode 100644 bundle/src/main/java/com/adobe/acs/commons/marketo/impl/TestMarketoConnectionServlet.java create mode 100644 bundle/src/test/java/com/adobe/acs/commons/marketo/MarketoClientConfigurationImplTest.java create mode 100644 bundle/src/test/java/com/adobe/acs/commons/marketo/impl/MarketoAPIExceptionTest.java create mode 100644 bundle/src/test/java/com/adobe/acs/commons/marketo/impl/TestMarketoConnectionServletTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 4442e86a83..9351d31c6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com) [unreleased changes details]: https://github.com/Adobe-Consulting-Services/acs-aem-commons/compare/acs-aem-commons-5.0.14...HEAD ### Changed + +- #2874 - Make Marketo Forms Easy to configure - #2931 - Cloud Manager SonarQube report - 2022.08.10 @ v5.3.2 #2931 - #2877 - Support for selector-based redirects diff --git a/bundle/src/main/java/com/adobe/acs/commons/marketo/client/MarketoApiException.java b/bundle/src/main/java/com/adobe/acs/commons/marketo/client/MarketoApiException.java new file mode 100644 index 0000000000..8c258c7aa4 --- /dev/null +++ b/bundle/src/main/java/com/adobe/acs/commons/marketo/client/MarketoApiException.java @@ -0,0 +1,113 @@ +/* + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2022 Adobe + * %% + * 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. + * #L% + */ +package com.adobe.acs.commons.marketo.client; + +import java.io.IOException; +import java.util.Optional; + +import org.apache.commons.lang.StringEscapeUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpResponse; +import org.apache.http.ParseException; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class MarketoApiException extends IOException { + + private static final Logger log = LoggerFactory.getLogger(MarketoApiException.class); + + private final String requestLine; + private final int statusCode; + private final String reasonString; + private final String responseBody; + + private static final String getResponseBody(HttpResponse response) { + if (response != null) { + try { + return StringEscapeUtils + .escapeHtml(StringUtils.abbreviate(EntityUtils.toString(response.getEntity()), 100)); + } catch (ParseException | IOException e) { + log.warn("Failed to read response from: {}", response, e); + } + } + return null; + } + + public MarketoApiException(String message, HttpRequestBase request, HttpResponse response) { + this(message, request, response, getResponseBody(response), null); + } + + public MarketoApiException(String message, HttpRequestBase request, HttpResponse response, String responseBody, + Exception cause) { + super(message, cause); + this.requestLine = Optional.ofNullable(request).map(r -> r.getRequestLine().toString()).orElse(null); + if (response != null) { + this.statusCode = response.getStatusLine().getStatusCode(); + this.reasonString = response.getStatusLine().getReasonPhrase(); + this.responseBody = responseBody; + } else { + this.statusCode = -1; + this.reasonString = null; + this.responseBody = responseBody; + } + } + + public MarketoApiException(String message, HttpRequestBase request, HttpResponse response, String responseBody) { + this(message, request, response, responseBody, null); + } + + @Override + public String getMessage() { + return String.format( + "%s\tREQUEST{%s}\tRESPONSE{Status Code: %d, Reason Phrase: %s, Response Body: %s}", + super.getMessage(), getRequestLine(), getStatusCode(), getReasonString(), getResponseBody()); + } + + /** + * @return the requestLine + */ + public String getRequestLine() { + return requestLine; + } + + /** + * @return the statusCode + */ + public int getStatusCode() { + return statusCode; + } + + /** + * @return the reasonString + */ + public String getReasonString() { + return reasonString; + } + + /** + * @return the responseBody + */ + public String getResponseBody() { + return responseBody; + } + +} diff --git a/bundle/src/main/java/com/adobe/acs/commons/marketo/client/MarketoClient.java b/bundle/src/main/java/com/adobe/acs/commons/marketo/client/MarketoClient.java index 42d8eec4c5..75088c1801 100644 --- a/bundle/src/main/java/com/adobe/acs/commons/marketo/client/MarketoClient.java +++ b/bundle/src/main/java/com/adobe/acs/commons/marketo/client/MarketoClient.java @@ -24,6 +24,8 @@ import javax.annotation.Nonnull; +import org.apache.http.impl.client.CloseableHttpClient; + import com.adobe.acs.commons.marketo.MarketoClientConfiguration; /** @@ -31,31 +33,38 @@ */ public interface MarketoClient { - /** - * Retrieve an API token used for interacting with the Marketo API. - * - * @param config the configuration to use to retrieve the token - * @return a valid Marketo API Token - * @throws IOException an error occurs retrieving the token - */ - public @Nonnull String getApiToken(@Nonnull MarketoClientConfiguration config) throws IOException; - - /** - * Retrieve all of the available forms from the current organization in Marketo. - * - * @param config the configuration for this request - * @return the full list of available forms - * @throws IOException an exception occurs interacting with the API - */ - public @Nonnull List getForms(@Nonnull MarketoClientConfiguration config) throws IOException; - - /** - * Retrieve all of the available forms from the current organization in Marketo. - * - * @param config the configuration for this request - * @return the full list of available forms - * @throws IOException an exception occurs interacting with the API - */ - public @Nonnull List getFields(@Nonnull MarketoClientConfiguration config) throws IOException; + /** + * Retrieve an API token used for interacting with the Marketo API. + * + * @param config the configuration to use to retrieve the token + * @return a valid Marketo API Token + * @throws IOException an error occurs retrieving the token + */ + public @Nonnull String getApiToken(@Nonnull MarketoClientConfiguration config) throws MarketoApiException; + + /** + * Retrieve a HttpClient for interacting with the Marketo API + * + * @return the httpclient + */ + public @Nonnull CloseableHttpClient getHttpClient(); + + /** + * Retrieve all of the available forms from the current organization in Marketo. + * + * @param config the configuration for this request + * @return the full list of available forms + * @throws IOException an exception occurs interacting with the API + */ + public @Nonnull List getForms(@Nonnull MarketoClientConfiguration config) throws MarketoApiException; + + /** + * Retrieve all of the available forms from the current organization in Marketo. + * + * @param config the configuration for this request + * @return the full list of available forms + * @throws IOException an exception occurs interacting with the API + */ + public @Nonnull List getFields(@Nonnull MarketoClientConfiguration config) throws MarketoApiException; } diff --git a/bundle/src/main/java/com/adobe/acs/commons/marketo/client/impl/MarketoClientImpl.java b/bundle/src/main/java/com/adobe/acs/commons/marketo/client/impl/MarketoClientImpl.java index d849a521e1..0526bc6b1f 100644 --- a/bundle/src/main/java/com/adobe/acs/commons/marketo/client/impl/MarketoClientImpl.java +++ b/bundle/src/main/java/com/adobe/acs/commons/marketo/client/impl/MarketoClientImpl.java @@ -20,29 +20,20 @@ package com.adobe.acs.commons.marketo.client.impl; import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Dictionary; import java.util.List; import java.util.Map; +import java.util.function.BiFunction; import java.util.stream.Collectors; import javax.annotation.Nonnull; import javax.annotation.Nullable; -import com.adobe.acs.commons.marketo.MarketoClientConfiguration; -import com.adobe.acs.commons.marketo.client.MarketoClient; -import com.adobe.acs.commons.marketo.client.MarketoError; -import com.adobe.acs.commons.marketo.client.MarketoField; -import com.adobe.acs.commons.marketo.client.MarketoForm; -import com.adobe.acs.commons.marketo.client.MarketoResponse; -import com.fasterxml.jackson.databind.ObjectMapper; - -import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpHost; +import org.apache.http.HttpResponse; import org.apache.http.auth.AUTH; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; @@ -55,6 +46,8 @@ import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.DefaultProxyRoutePlanner; +import org.apache.http.util.EntityUtils; +import org.apache.poi.util.IOUtils; import org.osgi.framework.InvalidSyntaxException; import org.osgi.service.cm.Configuration; import org.osgi.service.cm.ConfigurationAdmin; @@ -64,166 +57,258 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.adobe.acs.commons.marketo.MarketoClientConfiguration; +import com.adobe.acs.commons.marketo.client.MarketoApiException; +import com.adobe.acs.commons.marketo.client.MarketoClient; +import com.adobe.acs.commons.marketo.client.MarketoError; +import com.adobe.acs.commons.marketo.client.MarketoField; +import com.adobe.acs.commons.marketo.client.MarketoForm; +import com.adobe.acs.commons.marketo.client.MarketoResponse; +import com.fasterxml.jackson.databind.ObjectMapper; + /** * Implementation of the MarketoClient using the REST API. */ @Component(service = MarketoClient.class) public class MarketoClientImpl implements MarketoClient { - private static final Logger log = LoggerFactory.getLogger(MarketoClientImpl.class); - - private static final int SOCKET_TIMEOUT_MS = 5000; - private static final int CONNECT_TIMEOUT_MS = 5000; - private static final int PAGE_SIZE = 200; - - private ObjectMapper mapper = new ObjectMapper(); - - private final HttpClientBuilder clientBuilder = HttpClients.custom() - .setDefaultRequestConfig(RequestConfig.copy(RequestConfig.DEFAULT) - .setSocketTimeout(SOCKET_TIMEOUT_MS) - .setConnectTimeout(CONNECT_TIMEOUT_MS) - .build()); - - @Reference - protected ConfigurationAdmin configAdmin; - - @Activate - public void activate() { - Configuration[] configs; - try { - configs = configAdmin.listConfigurations("(service.factoryPid=org.apache.http.proxyconfigurator)"); - - if (configs != null) { - for (Configuration config : configs) { - log.info("Evaluating proxy configuration: {}", config.getPid()); - - Dictionary properties = config.getProperties(); - if (isEnabled(properties)) { - String host = String.class.cast(properties.get("proxy.host")); - int port = Integer.class.cast(properties.get("proxy.port")); - - log.debug("Using proxy host: {}", host); - HttpHost proxyhost = new HttpHost(host, port); - HttpRoutePlanner routePlanner = new DefaultProxyRoutePlanner(proxyhost); - clientBuilder.setRoutePlanner(routePlanner); - - String user = String.class.cast(properties.get("proxy.user")); - String password = String.class.cast(properties.get("proxy.password")); - if (StringUtils.isNotBlank(user)) { - log.debug("Using proxy authentication with user: {}", user); - BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); - credentialsProvider.setCredentials(new AuthScope(host, port), - new UsernamePasswordCredentials(user, password)); - clientBuilder.setDefaultCredentialsProvider(credentialsProvider); + private static final Logger log = LoggerFactory.getLogger(MarketoClientImpl.class); + + private static final int SOCKET_TIMEOUT_MS = 5000; + private static final int CONNECT_TIMEOUT_MS = 5000; + private static final int PAGE_SIZE = 200; + + private ObjectMapper mapper = new ObjectMapper(); + + private final HttpClientBuilder clientBuilder = HttpClients.custom() + .setDefaultRequestConfig(RequestConfig.copy(RequestConfig.DEFAULT) + .setSocketTimeout(SOCKET_TIMEOUT_MS) + .setConnectTimeout(CONNECT_TIMEOUT_MS) + .build()); + + @Reference + protected ConfigurationAdmin configAdmin; + + @Activate + public void activate() { + Configuration[] configs; + try { + configs = configAdmin.listConfigurations("(service.factoryPid=org.apache.http.proxyconfigurator)"); + + if (configs != null) { + for (Configuration config : configs) { + log.info("Evaluating proxy configuration: {}", config.getPid()); + + Dictionary properties = config.getProperties(); + if (isEnabled(properties)) { + String host = String.class.cast(properties.get("proxy.host")); + int port = Integer.class.cast(properties.get("proxy.port")); + + log.debug("Using proxy host: {}", host); + HttpHost proxyhost = new HttpHost(host, port); + HttpRoutePlanner routePlanner = new DefaultProxyRoutePlanner(proxyhost); + clientBuilder.setRoutePlanner(routePlanner); + + String user = String.class.cast(properties.get("proxy.user")); + String password = String.class.cast(properties.get("proxy.password")); + if (StringUtils.isNotBlank(user)) { + log.debug("Using proxy authentication with user: {}", user); + BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials(new AuthScope(host, port), + new UsernamePasswordCredentials(user, password)); + clientBuilder.setDefaultCredentialsProvider(credentialsProvider); + } + + } else { + log.debug("Proxy configuration not enabled"); + } + } } - - } else { - log.debug("Proxy configuration not enabled"); - } + } catch (IOException | InvalidSyntaxException e) { + log.error("Failed to load proxy configuration", e); } - } - } catch (IOException | InvalidSyntaxException e) { - log.error("Failed to load proxy configuration", e); + } - } + private boolean isEnabled(Dictionary properties) { + if (properties == null) { + return false; + } + + Object value = properties.get("proxy.enabled"); + if (!(value instanceof Boolean) || !(Boolean.class.cast(value))) { + return false; + } - private boolean isEnabled(Dictionary properties) { - if (properties == null) { - return false; + String host = String.class.cast(properties.get("proxy.host")); + if (StringUtils.isBlank(host)) { + return false; + } + return true; } - Object value = properties.get("proxy.enabled"); - if (!(value instanceof Boolean) || !(Boolean.class.cast(value))) { - return false; + private boolean isSuccessStatus(HttpResponse response) { + return response.getStatusLine().getStatusCode() < 300 && response.getStatusLine().getStatusCode() >= 200; } - String host = String.class.cast(properties.get("proxy.host")); - if (StringUtils.isBlank(host)) { - return false; + @Override + public CloseableHttpClient getHttpClient() { + return clientBuilder.build(); } - return true; - } - - protected @Nonnull String getApiResponse(@Nonnull String url, String bearerToken) throws IOException { - - try (CloseableHttpClient httpClient = clientBuilder.build()) { - log.debug("Sending request to: {}", url); - HttpGet httpGet = new HttpGet(url); - if (StringUtils.isNotBlank(bearerToken)) { - httpGet.setHeader(AUTH.WWW_AUTH_RESP, "Bearer " + bearerToken); - } - try (CloseableHttpResponse httpResponse = httpClient.execute(httpGet)) { - try (InputStream content = httpResponse.getEntity().getContent()) { - return IOUtils.toString(content, StandardCharsets.UTF_8); + + protected @Nonnull T getApiResponse(@Nonnull String url, String bearerToken, + BiFunction> callback) + throws MarketoApiException { + CloseableHttpClient client = null; + HttpGet httpGet = null; + CloseableHttpResponse response = null; + try { + client = getHttpClient(); + log.debug("Sending request to: {}", url); + httpGet = new HttpGet(url); + if (StringUtils.isNotBlank(bearerToken)) { + httpGet.setHeader(AUTH.WWW_AUTH_RESP, "Bearer " + bearerToken); + } + response = client.execute(httpGet); + if (!isSuccessStatus(response)) { + throw new MarketoApiException("Unexpected API response", httpGet, response); + } + ParsedResponse parsed = callback.apply(httpGet, response); + if (parsed.isSuccess()) { + return parsed.getResult(); + } else { + throw parsed.getException(); + } + } catch (MarketoApiException mae) { + throw mae; + } catch (IOException ioe) { + throw new MarketoApiException("Unexpected I/O Exception calling Marketo API", httpGet, response); + } finally { + IOUtils.closeQuietly(client); + IOUtils.closeQuietly(response); } - } } - } - - public @Nonnull String getApiToken(@Nonnull MarketoClientConfiguration config) throws IOException { - log.trace("getApiToken"); - String url = String.format( - "https://%s/identity/oauth/token?grant_type=client_credentials&client_id=%s&client_secret=%s", - config.getEndpointHost(), config.getClientId(), config.getClientSecret()); - String response = getApiResponse(url, null); - Map responseData = mapper.readValue(response, Map.class); - return (String) responseData.get("access_token"); - - } - - @Override - public List getFields(MarketoClientConfiguration config) throws IOException { - String apiToken = getApiToken(config); - List fields = new ArrayList<>(); - - String base = String.format("https://%s/rest/asset/v1/form/fields.json?", config.getEndpointHost()); - - for (int i = 0; true; i++) { - MarketoField[] page = getApiPage(base, apiToken, i, MarketoFieldResponse.class); - if (page == null || page.length == 0) { - break; - } else { - Arrays.stream(page).forEach(fields::add); - } + + public @Nonnull String getApiToken(@Nonnull MarketoClientConfiguration config) throws MarketoApiException { + log.trace("getApiToken"); + String url = String.format( + "https://%s/identity/oauth/token?grant_type=client_credentials&client_id=%s&client_secret=%s", + config.getEndpointHost(), config.getClientId(), config.getClientSecret()); + return getApiResponse(url, null, (req, res) -> { + String body = null; + try { + body = EntityUtils.toString(res.getEntity()); + Map responseData = mapper.readValue(body, Map.class); + String token = (String) responseData.get("access_token"); + return new ParsedResponse(token); + } catch (IOException e) { + return new ParsedResponse(new MarketoApiException("Failed to get API Token", req, res, body)); + } + }); } - return fields; - } - private @Nullable > T[] getApiPage(@Nonnull String urlBase, @Nonnull String token, - int page, Class responseType) throws IOException { - log.trace("getApiPage({})", page); - int offset = PAGE_SIZE * page; + @Override + public List getFields(MarketoClientConfiguration config) throws MarketoApiException { + String apiToken = getApiToken(config); + List fields = new ArrayList<>(); + + String base = String.format("https://%s/rest/asset/v1/form/fields.json?", config.getEndpointHost()); + + for (int i = 0; true; i++) { + MarketoField[] page = getApiPage(base, apiToken, i, MarketoFieldResponse.class); + if (page == null || page.length == 0) { + break; + } else { + Arrays.stream(page).forEach(fields::add); + } + } + return fields; + } - String url = String.format("%smaxReturn=%s&offset=%s", urlBase, PAGE_SIZE, offset); + private @Nullable > T[] getApiPage(@Nonnull String urlBase, @Nonnull String token, + int page, Class responseType) throws MarketoApiException { + log.trace("getApiPage({})", page); + int offset = PAGE_SIZE * page; + + String url = String.format("%smaxReturn=%s&offset=%s", urlBase, PAGE_SIZE, offset); + + return getApiResponse(url, token, (req, res) -> { + String body = null; + try { + body = EntityUtils.toString(res.getEntity()); + MarketoResponse response = mapper.readValue(body, responseType); + if (response.getErrors() != null && response.getErrors().length > 0) { + throw new IOException("Retrieved errors in response: " + + Arrays.stream(response.getErrors()).map(MarketoError::getMessage) + .collect(Collectors.joining(", "))); + } + if (!response.isSuccess()) { + throw new IOException("Retrieved non-success response"); + } + return new ParsedResponse(response.getResult()); + } catch (IOException ioe) { + return new ParsedResponse( + new MarketoApiException("Failed to retrieve Marketo API Page", req, res, body)); + } + }); - String responseText = getApiResponse(url, token); - MarketoResponse response = mapper.readValue(responseText, responseType); - if (response.getErrors() != null && response.getErrors().length > 0) { - throw new IOException("Retrieved errors in response: " - + Arrays.stream(response.getErrors()).map(MarketoError::getMessage).collect(Collectors.joining(", "))); } - if (!response.isSuccess()) { - throw new IOException("Retrieved non-success response"); + + @Override + public List getForms(@Nonnull MarketoClientConfiguration config) throws MarketoApiException { + String apiToken = getApiToken(config); + List forms = new ArrayList<>(); + String base = String.format("https://%s/rest/asset/v1/forms.json?status=approved&", config.getEndpointHost()); + for (int i = 0; true; i++) { + + MarketoForm[] page = getApiPage(base, apiToken, i, MarketoFormResponse.class); + if (page == null || page.length == 0) { + break; + } else { + Arrays.stream(page).forEach(forms::add); + } + } + return forms; } - return response.getResult(); - } - - @Override - public List getForms(@Nonnull MarketoClientConfiguration config) throws IOException { - String apiToken = getApiToken(config); - List forms = new ArrayList<>(); - String base = String.format("https://%s/rest/asset/v1/forms.json?status=approved&", config.getEndpointHost()); - for (int i = 0; true; i++) { - - MarketoForm[] page = getApiPage(base, apiToken, i, MarketoFormResponse.class); - if (page == null || page.length == 0) { - break; - } else { - Arrays.stream(page).forEach(forms::add); - } + + class ParsedResponse { + private final boolean success; + private final MarketoApiException exception; + private final T result; + + public ParsedResponse(T result) { + this.success = true; + this.result = result; + this.exception = null; + } + + public ParsedResponse(MarketoApiException exception) { + this.success = false; + this.result = null; + this.exception = exception; + } + + /** + * @return the success + */ + public boolean isSuccess() { + return success; + } + + /** + * @return the exception + */ + public MarketoApiException getException() { + return exception; + } + + /** + * @return the result + */ + public T getResult() { + return result; + } + } - return forms; - } } diff --git a/bundle/src/main/java/com/adobe/acs/commons/marketo/impl/MarketoClientConfigurationImpl.java b/bundle/src/main/java/com/adobe/acs/commons/marketo/impl/MarketoClientConfigurationImpl.java index a9487ee645..6a9685ae88 100644 --- a/bundle/src/main/java/com/adobe/acs/commons/marketo/impl/MarketoClientConfigurationImpl.java +++ b/bundle/src/main/java/com/adobe/acs/commons/marketo/impl/MarketoClientConfigurationImpl.java @@ -19,6 +19,9 @@ */ package com.adobe.acs.commons.marketo.impl; +import javax.inject.Inject; +import javax.inject.Named; + import org.apache.sling.api.resource.Resource; import org.apache.sling.models.annotations.Model; import org.apache.sling.models.annotations.injectorspecific.ValueMapValue; @@ -26,82 +29,111 @@ import com.adobe.acs.commons.marketo.MarketoClientConfiguration; /** - * A Model retrieving the configuration for interacting with the Marketo REST API + * A Model retrieving the configuration for interacting with the Marketo REST + * API */ -@Model(adaptables = Resource.class, adapters=MarketoClientConfiguration.class) +@Model(adaptables = Resource.class, adapters = MarketoClientConfiguration.class) public class MarketoClientConfigurationImpl implements MarketoClientConfiguration { - @ValueMapValue - private String clientId; + private final String clientId; - @ValueMapValue - private String clientSecret; + private final String clientSecret; - @ValueMapValue - private String endpointHost; + private final String endpointHost; - @ValueMapValue - private String munchkinId; + private final String munchkinId; - @ValueMapValue - private String serverInstance; + private final String serverInstance; - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; + @Inject + public MarketoClientConfigurationImpl(@ValueMapValue @Named("clientId") String clientId, + @ValueMapValue @Named("clientSecret") String clientSecret, + @ValueMapValue @Named("endpointHost") String endpointHost, + @ValueMapValue @Named("munchkinId") String munchkinId, + @ValueMapValue @Named("serverInstance") String serverInstance) { + this.clientId = clientId; + this.clientSecret = clientSecret; + this.endpointHost = endpointHost; + this.munchkinId = munchkinId; + this.serverInstance = serverInstance; } - if (obj == null) { - return false; + + private String getHost(String url) { + int start = url.indexOf("://"); + if (url.startsWith("//")) { // handle the special case of starting with // + start = 2; + } else if (start < 0) { // no protocol + start = 0; + } else { // has protocol + start += 3; + } + int end = url.indexOf('/', start); + if (end < 0) { + end = url.length(); + } + String domainName = url.substring(start, end); + + // handle port + int port = domainName.indexOf(':'); + if (port >= 0) { + domainName = domainName.substring(0, port); + } + return domainName; } - if (getClass() != obj.getClass()) { - return false; + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + MarketoClientConfigurationImpl other = (MarketoClientConfigurationImpl) obj; + if (clientId == null) { + if (other.clientId != null) { + return false; + } + } else if (!clientId.equals(other.clientId)) { + return false; + } + return true; } - MarketoClientConfigurationImpl other = (MarketoClientConfigurationImpl) obj; - if (clientId == null) { - if (other.clientId != null) { - return false; - } - } else if (!clientId.equals(other.clientId)) { - return false; + + @Override + public String getClientId() { + return clientId; } - return true; - } - - @Override - public String getClientId() { - return clientId; - } - - @Override - public String getClientSecret() { - return clientSecret; - } - - @Override - public String getEndpointHost() { - if(endpointHost.startsWith("https://")){ - return endpointHost.substring("https://".length()); + + @Override + public String getClientSecret() { + return clientSecret; + } + + @Override + public String getEndpointHost() { + return getHost(endpointHost); + } + + @Override + public String getMunchkinId() { + return munchkinId; + } + + @Override + public String getServerInstance() { + return getHost(serverInstance); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((clientId == null) ? 0 : clientId.hashCode()); + return result; } - return endpointHost; - } - - @Override - public String getMunchkinId() { - return munchkinId; - } - - @Override - public String getServerInstance() { - return serverInstance; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((clientId == null) ? 0 : clientId.hashCode()); - return result; - } } diff --git a/bundle/src/main/java/com/adobe/acs/commons/marketo/impl/MarketoFieldDataSource.java b/bundle/src/main/java/com/adobe/acs/commons/marketo/impl/MarketoFieldDataSource.java index 9d3f61fb2b..5afd2c8165 100644 --- a/bundle/src/main/java/com/adobe/acs/commons/marketo/impl/MarketoFieldDataSource.java +++ b/bundle/src/main/java/com/adobe/acs/commons/marketo/impl/MarketoFieldDataSource.java @@ -100,6 +100,7 @@ public void doGet(@Nonnull SlingHttpServletRequest request, @Nonnull SlingHttpSe throw new RepositoryException(msg); } + options = formCache.get(config).stream() .sorted((MarketoField f1, MarketoField f2) -> f1.getId().compareTo(f2.getId())).map(f -> { Map data = new HashMap<>(); diff --git a/bundle/src/main/java/com/adobe/acs/commons/marketo/impl/TestMarketoConnectionServlet.java b/bundle/src/main/java/com/adobe/acs/commons/marketo/impl/TestMarketoConnectionServlet.java new file mode 100644 index 0000000000..58dde14427 --- /dev/null +++ b/bundle/src/main/java/com/adobe/acs/commons/marketo/impl/TestMarketoConnectionServlet.java @@ -0,0 +1,179 @@ +/* + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2022 Adobe + * %% + * 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. + * #L% + */ +package com.adobe.acs.commons.marketo.impl; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.annotation.Nonnull; +import javax.servlet.Servlet; +import javax.servlet.ServletException; + +import org.apache.http.HttpHeaders; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.jackrabbit.JcrConstants; +import org.apache.sling.api.SlingHttpServletRequest; +import org.apache.sling.api.SlingHttpServletResponse; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.servlets.SlingSafeMethodsServlet; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.adobe.acs.commons.marketo.MarketoClientConfiguration; +import com.adobe.acs.commons.marketo.client.MarketoApiException; +import com.adobe.acs.commons.marketo.client.MarketoClient; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; + +@Component(service = Servlet.class, property = "sling.servlet.paths=/bin/acs-commons/mkto-cfg-test") +public class TestMarketoConnectionServlet extends SlingSafeMethodsServlet { + + private static final Logger log = LoggerFactory.getLogger(TestMarketoConnectionServlet.class); + + private static final ObjectWriter objectWriter = new ObjectMapper().writerWithDefaultPrettyPrinter(); + + @Reference + private MarketoClient client; + + protected void doGet(@Nonnull SlingHttpServletRequest request, + @Nonnull SlingHttpServletResponse response) throws ServletException, + IOException { + + List logs = new ArrayList<>(); + String configPath = request.getParameter("path"); + if (configPath == null) { + sendProblem(response, 400, "Missing Config Path", "Please specify the parameter 'path'", + Collections.emptyList()); + return; + } + if (!configPath.contains(JcrConstants.JCR_CONTENT)) { + configPath += "/" + JcrConstants.JCR_CONTENT; + } + logs.add("Using config path: " + configPath); + + Resource configResource = request.getResourceResolver().getResource(configPath); + if (configResource == null) { + log.warn("Failed to validate Marketo configuration, configuration not found. Logs: {}", log); + sendProblem(response, 404, "Configuration Not Found", "No configuration found at " + configPath, logs); + return; + } + logs.add("Resolved resource: " + configResource); + + MarketoClientConfiguration config = configResource.adaptTo(MarketoClientConfiguration.class); + if (config == null) { + log.warn( + "Failed to validate Marketo configuration, usually this indicates that fields are missing. Logs: {}", + log); + sendProblem(response, 400, "Invalid Configuration", + "Unable to retrieve configuration from resource" + configResource, logs); + return; + } + logs.add("Resolved configuration: " + config); + + try { + client.getApiToken(config); + logs.add("Retrieved token successfully"); + } catch (MarketoApiException e) { + log.warn("Failed to validate Marketo configuration, cannot retrieve token. Logs: {}", log, e); + sendProblem(response, 400, "Unable to Retrieve API Token", + "Failed to retrieve the API token from Marketo. Usually, this indicates that the REST Endpoint Host, Client Id or Client Secret are incorrect. Exception: " + + e.toString(), + logs); + return; + } + + try { + client.getForms(config); + logs.add("Retrieved forms successfully"); + } catch (MarketoApiException e) { + log.warn("Failed to validate Marketo configuration, cannot retrieve forms. Logs: {}", log, e); + sendProblem(response, 400, "Unable to Retrieve Forms", + "Failed to retrieve the forms from Marketo. Usually, this indicates that the account does not have sufficient access in Marketo. Exception: " + + e.toString(), + logs); + return; + } + + try { + try (CloseableHttpClient httpClient = client.getHttpClient()) { + HttpGet getRequest = new HttpGet("https://" + config.getServerInstance() + "/js/forms2/js/forms2.js"); + try (CloseableHttpResponse httpResponse = httpClient.execute(getRequest)) { + if (!isValidJavaScript(httpResponse)) { + throw new MarketoApiException("Failed to get expected response for Marketo forms script", + getRequest, + httpResponse); + } else { + logs.add("Validated script successfully"); + } + } + } + } catch (IOException e) { + log.warn("Failed to validate Marketo configuration, did not get valid response for forms script. Logs: {}", + log, e); + sendProblem(response, 400, "Invalid Script Response", + "Unexpected response for forms script. Usually, this indicates that the Marketo Server Instance is not set correctly. Exception: " + + e.toString(), + logs); + return; + } + + log.info("Successfully validated Marketo configuration: {}", configPath); + Map body = new HashMap<>(); + body.put("status", 200); + body.put("title", "Configuration Validated Successfully"); + body.put("logs", logs); + sendJsonResponse(response, 200, "application/json", body); + } + + private boolean isValidJavaScript(HttpResponse response) { + return response.getStatusLine().getStatusCode() == 200 + && Arrays.stream(response.getHeaders(HttpHeaders.CONTENT_TYPE)) + .anyMatch(h -> h.getValue().contains("javascript")); + } + + private void sendProblem(SlingHttpServletResponse response, int status, String title, String detail, + List logs) throws IOException { + Map body = new HashMap<>(); + body.put("status", status); + body.put("title", title); + body.put("detail", detail); + body.put("logs", logs); + sendJsonResponse(response, status, "application/problem+json", body); + } + + private void sendJsonResponse(SlingHttpServletResponse response, int status, String contentType, + Map data) throws IOException { + response.setStatus(status); + response.setContentType(contentType); + response.getWriter().write(objectWriter.writeValueAsString(data)); + response.getWriter().flush(); + } + +} diff --git a/bundle/src/test/java/com/adobe/acs/commons/marketo/MarketoClientConfigurationImplTest.java b/bundle/src/test/java/com/adobe/acs/commons/marketo/MarketoClientConfigurationImplTest.java new file mode 100644 index 0000000000..9c3d372eb3 --- /dev/null +++ b/bundle/src/test/java/com/adobe/acs/commons/marketo/MarketoClientConfigurationImplTest.java @@ -0,0 +1,91 @@ +/* + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2022 Adobe + * %% + * 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. + * #L% + */ +package com.adobe.acs.commons.marketo; + +import static org.junit.Assert.assertEquals; + +import java.util.Arrays; +import java.util.Collection; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import com.adobe.acs.commons.marketo.impl.MarketoClientConfigurationImpl; + +@RunWith(Parameterized.class) +public class MarketoClientConfigurationImplTest { + + private static final String CLIENT_ID = "client123"; + private static final String CLIENT_SECRET = "secret456"; + private static final String MUNCHKIN_ID = "123-456-789"; + + private static Object[] testData(String endpointHost, String serverInstance, String expectedEndpointHost, + String expectedServerInstance) { + return new Object[] { + new MarketoClientConfigurationImpl(CLIENT_ID, CLIENT_SECRET, endpointHost, MUNCHKIN_ID, serverInstance), + expectedEndpointHost, expectedServerInstance }; + } + + @Parameters + public static Collection data() { + return Arrays.asList(new Object[][] { + testData("123-456-789.mktorest.com", "//app-abc123.marketo.com", + "123-456-789.mktorest.com", "app-abc123.marketo.com"), + testData("https://123-456-789.mktorest.com", "https://app-abc123.marketo.com", + "123-456-789.mktorest.com", "app-abc123.marketo.com"), + testData("http://123-456-789.mktorest.com", "http://app-abc123.marketo.com", + "123-456-789.mktorest.com", "app-abc123.marketo.com"), + testData("http://123-456-789.mktorest.com:80/some/thing.jpg", + "http://app-abc123.marketo.com:440//some/thing.jpg", + "123-456-789.mktorest.com", "app-abc123.marketo.com") + }); + } + + private final MarketoClientConfiguration config; + private final String expectedEndpointHost; + private final String expectedServerInstance; + + public MarketoClientConfigurationImplTest(MarketoClientConfiguration config, String expectedEndpointHost, + String expectedServerInstance) { + this.config = config; + this.expectedEndpointHost = expectedEndpointHost; + this.expectedServerInstance = expectedServerInstance; + } + + @Test + public void testSimpleProperties() { + assertEquals(CLIENT_ID, config.getClientId()); + assertEquals(CLIENT_SECRET, config.getClientSecret()); + assertEquals(MUNCHKIN_ID, config.getMunchkinId()); + } + + @Test + public void testEndpointHost() { + assertEquals(expectedEndpointHost, config.getEndpointHost()); + } + + @Test + public void testServerInstance() { + assertEquals(expectedServerInstance, config.getServerInstance()); + } + +} diff --git a/bundle/src/test/java/com/adobe/acs/commons/marketo/client/impl/StaticResponseMarketoClient.java b/bundle/src/test/java/com/adobe/acs/commons/marketo/client/impl/StaticResponseMarketoClient.java index 9fbcac9afb..82d4c19bd8 100644 --- a/bundle/src/test/java/com/adobe/acs/commons/marketo/client/impl/StaticResponseMarketoClient.java +++ b/bundle/src/test/java/com/adobe/acs/commons/marketo/client/impl/StaticResponseMarketoClient.java @@ -19,36 +19,66 @@ */ package com.adobe.acs.commons.marketo.client.impl; -import java.io.IOException; -import java.nio.charset.StandardCharsets; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.InputStream; import java.util.Arrays; import java.util.Iterator; +import java.util.function.BiFunction; + +import javax.annotation.Nonnull; -import org.apache.commons.io.IOUtils; +import org.apache.http.HttpResponse; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.entity.InputStreamEntity; +import org.apache.http.message.BasicRequestLine; -import com.drew.lang.annotations.NotNull; +import org.apache.http.ProtocolVersion; + +import com.adobe.acs.commons.marketo.client.MarketoApiException; public class StaticResponseMarketoClient extends MarketoClientImpl { - private String resourcePath; - private Iterator resourcePaths; + private String resourcePath; + private Iterator resourcePaths; - public StaticResponseMarketoClient(String resourcePath) { - this.resourcePath = resourcePath; - } + public StaticResponseMarketoClient(String resourcePath) { + this.resourcePath = resourcePath; + } - public StaticResponseMarketoClient(String[] resourcePaths) { - this.resourcePaths = Arrays.asList(resourcePaths).iterator(); - if (this.resourcePaths.hasNext()) { - resourcePath = this.resourcePaths.next(); + public StaticResponseMarketoClient(String[] resourcePaths) { + this.resourcePaths = Arrays.asList(resourcePaths).iterator(); + if (this.resourcePaths.hasNext()) { + resourcePath = this.resourcePaths.next(); + } } - } - protected @NotNull String getApiResponse(@NotNull String url, String bearerToken) throws IOException { - String resp = IOUtils.toString(StaticResponseMarketoClient.class.getResourceAsStream(resourcePath), StandardCharsets.UTF_8); - if (resourcePaths != null && resourcePaths.hasNext()) { - resourcePath = resourcePaths.next(); + @Override + protected @Nonnull T getApiResponse(@Nonnull String url, String bearerToken, + BiFunction> callback) + throws MarketoApiException { + InputStream is = StaticResponseMarketoClient.class.getResourceAsStream(resourcePath); + if (resourcePaths != null && resourcePaths.hasNext()) { + resourcePath = resourcePaths.next(); + } + HttpGet req = mock(HttpGet.class); + when(req.getRequestLine()).thenReturn(new BasicRequestLine("GET", url, new ProtocolVersion("http", 0, 0))); + HttpResponse res = mock(HttpResponse.class); + + StatusLine status = mock(StatusLine.class); + when(status.getStatusCode()).thenReturn(200); + + when(res.getStatusLine()).thenReturn(status); + when(res.getEntity()) + .thenReturn(new InputStreamEntity(is)); + + ParsedResponse resp = callback.apply(req, res); + if (resp.isSuccess()) { + return resp.getResult(); + } else { + throw resp.getException(); + } } - return resp; - } } diff --git a/bundle/src/test/java/com/adobe/acs/commons/marketo/client/impl/TestMarketoClient.java b/bundle/src/test/java/com/adobe/acs/commons/marketo/client/impl/TestMarketoClient.java index 8f57d3a551..33a936642a 100644 --- a/bundle/src/test/java/com/adobe/acs/commons/marketo/client/impl/TestMarketoClient.java +++ b/bundle/src/test/java/com/adobe/acs/commons/marketo/client/impl/TestMarketoClient.java @@ -22,6 +22,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.IOException; @@ -32,6 +33,7 @@ import org.junit.Test; import com.adobe.acs.commons.marketo.MarketoClientConfiguration; +import com.adobe.acs.commons.marketo.client.MarketoApiException; import com.adobe.acs.commons.marketo.client.MarketoClient; import com.adobe.acs.commons.marketo.client.MarketoField; import com.adobe.acs.commons.marketo.client.MarketoForm; @@ -76,7 +78,7 @@ public void testError() throws IOException { client.getForms(config); fail(); } catch (IOException e) { - assertEquals("Retrieved errors in response: Access token invalid", e.getMessage()); + assertTrue(e instanceof MarketoApiException); } } diff --git a/bundle/src/test/java/com/adobe/acs/commons/marketo/impl/MarketoAPIExceptionTest.java b/bundle/src/test/java/com/adobe/acs/commons/marketo/impl/MarketoAPIExceptionTest.java new file mode 100644 index 0000000000..2a2f862b6e --- /dev/null +++ b/bundle/src/test/java/com/adobe/acs/commons/marketo/impl/MarketoAPIExceptionTest.java @@ -0,0 +1,73 @@ +/* + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2022 Adobe + * %% + * 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. + * #L% + */ +package com.adobe.acs.commons.marketo.impl; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.UnsupportedEncodingException; + +import org.apache.http.HttpResponse; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.entity.StringEntity; +import org.junit.Test; + +import com.adobe.acs.commons.marketo.client.MarketoApiException; + +public class MarketoAPIExceptionTest { + + @Test + public void canHandleNullRequestResponse() { + MarketoApiException ex = new MarketoApiException("Bad", null, null); + assertEquals("Bad REQUEST{null} RESPONSE{Status Code: -1, Reason Phrase: null, Response Body: null}", + ex.getMessage()); + + assertNull(ex.getReasonString()); + assertNull(ex.getRequestLine()); + assertNull(ex.getResponseBody()); + assertEquals(-1, ex.getStatusCode()); + } + + @Test + public void canParseValues() throws UnsupportedEncodingException { + HttpRequestBase request = new HttpGet("http://www.marketo.com"); + + HttpResponse response = mock(HttpResponse.class); + StatusLine statusLine = mock(StatusLine.class); + when(statusLine.getStatusCode()).thenReturn(418); + when(statusLine.getReasonPhrase()).thenReturn("I'm a Teapot"); + when(response.getStatusLine()).thenReturn(statusLine); + when(response.getEntity()).thenReturn(new StringEntity("Short and Stout")); + + MarketoApiException ex = new MarketoApiException("Bad", request, response); + assertEquals("Bad REQUEST{GET http://www.marketo.com HTTP/1.1} RESPONSE{Status Code: 418, Reason Phrase: I'm a Teapot, Response Body: Short and Stout}", + ex.getMessage()); + + assertEquals("I'm a Teapot", ex.getReasonString()); + assertEquals("GET http://www.marketo.com HTTP/1.1", ex.getRequestLine()); + assertEquals("Short and Stout", ex.getResponseBody()); + assertEquals(418, ex.getStatusCode()); + } + +} diff --git a/bundle/src/test/java/com/adobe/acs/commons/marketo/impl/TestMarketoClientConfiguration.java b/bundle/src/test/java/com/adobe/acs/commons/marketo/impl/TestMarketoClientConfiguration.java index eedc85ab90..367608c3b0 100644 --- a/bundle/src/test/java/com/adobe/acs/commons/marketo/impl/TestMarketoClientConfiguration.java +++ b/bundle/src/test/java/com/adobe/acs/commons/marketo/impl/TestMarketoClientConfiguration.java @@ -83,7 +83,7 @@ public void testConfig() { assertEquals("456", mcc.getClientSecret()); assertEquals("test.mktorest.com", mcc.getEndpointHost()); assertEquals("123-456-789", mcc.getMunchkinId()); - assertEquals("//test.marketo.com", mcc.getServerInstance()); + assertEquals("test.marketo.com", mcc.getServerInstance()); assertEquals(48721, mcc.hashCode()); MarketoClientConfiguration mcc2 = Optional diff --git a/bundle/src/test/java/com/adobe/acs/commons/marketo/impl/TestMarketoConnectionServletTest.java b/bundle/src/test/java/com/adobe/acs/commons/marketo/impl/TestMarketoConnectionServletTest.java new file mode 100644 index 0000000000..57c5527e4a --- /dev/null +++ b/bundle/src/test/java/com/adobe/acs/commons/marketo/impl/TestMarketoConnectionServletTest.java @@ -0,0 +1,194 @@ +/* + * #%L + * ACS AEM Commons Bundle + * %% + * Copyright (C) 2019 Adobe + * %% + * 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. + * #L% + */ +package com.adobe.acs.commons.marketo.impl; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletResponse; + +import org.apache.http.Header; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.jackrabbit.JcrConstants; +import org.apache.sling.testing.mock.sling.junit.SlingContext; +import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletResponse; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import com.adobe.acs.commons.marketo.client.MarketoApiException; +import com.adobe.acs.commons.marketo.client.MarketoClient; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +public class TestMarketoConnectionServletTest { + + @Rule + public final SlingContext context = new SlingContext(); + + private MarketoClient client; + private TestMarketoConnectionServlet servlet; + private CloseableHttpClient httpClient; + + @Before + public void before() { + client = mock(MarketoClient.class); + httpClient = mock(CloseableHttpClient.class); + when(client.getHttpClient()).thenReturn(httpClient); + context.registerService(MarketoClient.class, client); + servlet = context.registerInjectActivateService(new TestMarketoConnectionServlet()); + + context.addModelsForClasses(MarketoClientConfigurationImpl.class); + } + + private String getResponseTitle(MockSlingHttpServletResponse response) throws IOException { + Map body = new ObjectMapper().readValue(context.response().getOutputAsString(), + new TypeReference>() { + }); + return (String) body.get("title"); + } + + private void createConfig() { + + Map properties = new HashMap<>(); + properties.put(JcrConstants.JCR_PRIMARYTYPE, JcrConstants.NT_UNSTRUCTURED); + properties.put("clientId", "123"); + properties.put("clientSecret", "456"); + properties.put("endpointHost", "123.mktorest.com"); + properties.put("munchkinId", "123"); + properties.put("serverInstance", "123.marketo.com"); + + context.create().resource("/conf/marketo/jcr:content", properties); + } + + @Test + public void requiresPath() throws ServletException, IOException { + servlet.doGet(context.request(), context.response()); + assertEquals(HttpServletResponse.SC_BAD_REQUEST, context.response().getStatus()); + + assertEquals("Missing Config Path", getResponseTitle(context.response())); + } + + @Test + public void pathMustExist() throws ServletException, IOException { + context.request().addRequestParameter("path", "/conf/marketo"); + servlet.doGet(context.request(), context.response()); + assertEquals(HttpServletResponse.SC_NOT_FOUND, context.response().getStatus()); + + assertEquals("Configuration Not Found", getResponseTitle(context.response())); + } + + @Test + public void pathMustContainConfiguration() throws ServletException, IOException { + context.create().resource("/conf/marketo/jcr:content"); + context.request().addRequestParameter("path", "/conf/marketo"); + servlet.doGet(context.request(), context.response()); + assertEquals(HttpServletResponse.SC_BAD_REQUEST, context.response().getStatus()); + + assertEquals("Invalid Configuration", getResponseTitle(context.response())); + } + + @Test + public void mustGetAccessToken() throws ServletException, IOException { + createConfig(); + + when(client.getApiToken(any())).thenThrow(mock(MarketoApiException.class)); + + context.request().addRequestParameter("path", "/conf/marketo"); + servlet.doGet(context.request(), context.response()); + assertEquals(HttpServletResponse.SC_BAD_REQUEST, context.response().getStatus()); + + assertEquals("Unable to Retrieve API Token", getResponseTitle(context.response())); + } + + @Test + public void mustGetForms() throws ServletException, IOException { + createConfig(); + + when(client.getApiToken(any())).thenReturn("TOKEN"); + when(client.getForms(any())).thenThrow(mock(MarketoApiException.class)); + + context.request().addRequestParameter("path", "/conf/marketo"); + servlet.doGet(context.request(), context.response()); + assertEquals(HttpServletResponse.SC_BAD_REQUEST, context.response().getStatus()); + + assertEquals("Unable to Retrieve Forms", getResponseTitle(context.response())); + } + + @Test + public void mustGetJavaScriptResponse() throws ServletException, IOException { + createConfig(); + + when(client.getApiToken(any())).thenReturn("TOKEN"); + when(client.getForms(any())).thenReturn(Collections.emptyList()); + + CloseableHttpResponse response = mock(CloseableHttpResponse.class); + StatusLine sl = mock(StatusLine.class); + when(sl.getStatusCode()).thenReturn(404); + when(response.getStatusLine()).thenReturn(sl); + when(response.getEntity()).thenReturn(new StringEntity("")); + when(httpClient.execute(any(HttpGet.class))).thenReturn(response); + + context.request().addRequestParameter("path", "/conf/marketo"); + servlet.doGet(context.request(), context.response()); + assertEquals(HttpServletResponse.SC_BAD_REQUEST, context.response().getStatus()); + + assertEquals("Invalid Script Response", getResponseTitle(context.response())); + } + + @Test + public void canRunSuccessfully() throws ServletException, IOException { + createConfig(); + + when(client.getApiToken(any())).thenReturn("TOKEN"); + when(client.getForms(any())).thenReturn(Collections.emptyList()); + + CloseableHttpResponse response = mock(CloseableHttpResponse.class); + StatusLine sl = mock(StatusLine.class); + when(sl.getStatusCode()).thenReturn(200); + + Header ct = mock(Header.class); + when(ct.getValue()).thenReturn("application/json"); + when(response.getHeaders(anyString())).thenReturn(new Header[] { ct }); + when(response.getStatusLine()).thenReturn(sl); + when(response.getEntity()).thenReturn(new StringEntity("")); + when(httpClient.execute(any(HttpGet.class))).thenReturn(response); + + context.request().addRequestParameter("path", "/conf/marketo"); + servlet.doGet(context.request(), context.response()); + assertEquals(HttpServletResponse.SC_BAD_REQUEST, context.response().getStatus()); + + assertEquals("Invalid Script Response", getResponseTitle(context.response())); + } + +} diff --git a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/cloudconfig/clientlib/delete.js b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/cloudconfig/clientlib/delete.js index eebd84e6fe..e98db665ad 100644 --- a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/cloudconfig/clientlib/delete.js +++ b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/cloudconfig/clientlib/delete.js @@ -55,4 +55,25 @@ ui.clearWait(); }); } + + $(window).adaptTo("foundation-registry").register("foundation.collection.action.action", { + name: "acscommons.clientlib.check", + handler: function(_name, _el, config) { + function renderLogs(logs){ + return 'Logs
  • ' + logs.join('
  • ') + '
'; + } + ui.wait(); + $.ajax({ + url: config.data.test + "?path=" + config.data.path, + dataType: "json", + }).done(function(data) { + ui.alert(data.title,renderLogs(data.logs), "success"); + }).fail(function(jqXHR) { + var data = JSON.parse(jqXHR.responseText); + ui.alert(data.title, data.detail + '

' +renderLogs(data.logs), "error"); + }).always(function() { + ui.clearWait(); + }); + } + }); })(window, document, Granite.$, Granite); diff --git a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/cloudconfig/cloudconfiglist/cloudconfiglist.html b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/cloudconfig/cloudconfiglist/cloudconfiglist.html index 55b5d8f4a8..ccadc2fae7 100644 --- a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/cloudconfig/cloudconfiglist/cloudconfiglist.html +++ b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/cloudconfig/cloudconfiglist/cloudconfiglist.html @@ -36,6 +36,7 @@

${config.title}

${'Edit Cloud Configuration' @ i18n} + ${'Check Configuration' @ i18n} ${'Delete' @ i18n} diff --git a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/cloudconfig/marketo/_cq_dialog/.content.xml b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/cloudconfig/marketo/_cq_dialog/.content.xml index c63ca2a4ec..b022985dcf 100644 --- a/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/cloudconfig/marketo/_cq_dialog/.content.xml +++ b/ui.apps/src/main/content/jcr_root/apps/acs-commons/components/utilities/cloudconfig/marketo/_cq_dialog/.content.xml @@ -41,7 +41,7 @@ jcr:primaryType="nt:unstructured" sling:resourceType="granite/ui/components/coral/foundation/form/textfield" fieldLabel="Marketo Server Instance" - fieldDescription="URL Used to access marketo, e.g. //app-ab3123.marketo.com" + fieldDescription="Host for the Marketo Forms script. Can be retrieved from the embed code on a Marketo form, e.g. //app-ab3123.marketo.com" name="./serverInstance" required="{Boolean}true" />