From 4d223282b0e9ddb47e307dfb2ee85aa2578df086 Mon Sep 17 00:00:00 2001 From: spbolton Date: Thu, 26 Sep 2024 15:56:12 +0100 Subject: [PATCH 1/3] fix(backend): Unnecessary additional Host caching in resolveHostName causing null return (#30108) (#30132) There is confusion in the code whether we should be caching the mapping between requested host and the backend host, or whether the mapping should cache the default entry returned. The issue is occuring as a request for "localhost" that does not appear in the db hosts used to be mapped to the default host in com.dotmarketing.portlets.contentlet.business.HostAPIImpl#resolveHostName so null was not returned from this method. With the change to this method in the original PR when it calls com.dotmarketing.portlets.contentlet.business.HostAPIImpl#resolveHostNameWithoutDefault it ends up with a 404 entry in the host cache for localhost. A short time later it should replace this at the end of the method by adding a mapping to the resolved default host, subsequent calls to localhost should then end up using this cached entry to the specific default host that was cached. There is a short window of time though where the 404 entry was added, but the cache has not been updated to the default host where the current logic on finding the 404 entry is to just return null. This null was causing the ThreadNameFilter to throw a null pointer exception as it is using the result without null checking. Caching the resolved default host may also cause issues if the default host is changed as it may not take this change into account. There should actually be no reason to handle the caching logic in resolveHostName and only have it within resolveHostNameWithoutDefault that it calls. This way even if there is a 404 entry for an unknown like "localhost" it will always return the current default value (that should itself be cached) and not a null. We can therefore revert the original changes to just this one method but still make use of the caching logic provided when it calls resolveHostNameWithoutDefault ensuring null is not returned by resolveHostName method. This PR resolves #30108 (Flakey CLI Test PushServiceIT.Test_Push_New_Site). --- .../contentlet/business/HostAPIImpl.java | 32 ++++++------------- .../api/client/files/PushServiceIT.java | 3 +- 2 files changed, 10 insertions(+), 25 deletions(-) diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/HostAPIImpl.java b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/HostAPIImpl.java index 504083e15fbe..b8b8c685ce7f 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/HostAPIImpl.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/contentlet/business/HostAPIImpl.java @@ -121,31 +121,17 @@ public Host findDefaultHost(User user, boolean respectFrontendRoles) throws DotS @Override @CloseDBIfOpened public Host resolveHostName(String serverName, User user, boolean respectFrontendRoles) throws DotDataException, DotSecurityException { + User systemUser = APILocator.systemUser(); Host host; - final Host cachedHostByAlias = hostCache.getHostByAlias(serverName); - if (UtilMethods.isSet(() -> cachedHostByAlias.getIdentifier())) { - if (HostCache.CACHE_404_HOST.equals(cachedHostByAlias.getIdentifier())) { - return null; - } - host = cachedHostByAlias; - } else { - final User systemUser = APILocator.systemUser(); - try { - final Optional optional = resolveHostNameWithoutDefault(serverName, systemUser, respectFrontendRoles); - host = optional.isPresent() ? optional.get() : findDefaultHost(systemUser, respectFrontendRoles); - } catch (Exception e) { - host = findDefaultHost(systemUser, respectFrontendRoles); - } - - if (host != null) { - hostCache.addHostAlias(serverName, host); - } else { - hostCache.addHostAlias(serverName, HostCache.cache404Contentlet); - } - } - if (host != null) { - checkSitePermission(user, respectFrontendRoles, host); + try { + final Optional optional = + resolveHostNameWithoutDefault(serverName, systemUser, respectFrontendRoles); + host = optional.isPresent() ? optional.get() : findDefaultHost(systemUser, respectFrontendRoles); + } catch (Exception e) { + Logger.debug(this, "Exception resolving host using default", e); + host = findDefaultHost(systemUser, respectFrontendRoles); } + checkSitePermission(user, respectFrontendRoles, host); return host; } diff --git a/tools/dotcms-cli/cli/src/test/java/com/dotcms/api/client/files/PushServiceIT.java b/tools/dotcms-cli/cli/src/test/java/com/dotcms/api/client/files/PushServiceIT.java index ba77c0e4a3f6..29325fb3995b 100644 --- a/tools/dotcms-cli/cli/src/test/java/com/dotcms/api/client/files/PushServiceIT.java +++ b/tools/dotcms-cli/cli/src/test/java/com/dotcms/api/client/files/PushServiceIT.java @@ -200,8 +200,7 @@ void Test_Nothing_To_Push() throws IOException { * * @throws IOException if an I/O error occurs */ - // Temporarily ignore this test until the root cause of the problem is found - //@Test + @Test void Test_Push_New_Site() throws IOException { // Create a temporal folder for the pull From eec3e87f4c92ba2989ba115cf195cce801ed302d Mon Sep 17 00:00:00 2001 From: Jonathan Date: Thu, 26 Sep 2024 10:27:30 -0600 Subject: [PATCH 2/3] Issue 29866 analytics viewtool (#30118) Adding first draft for the analytics view tool --------- Co-authored-by: freddyDOTCMS Co-authored-by: Jose Castro Co-authored-by: freddyDOTCMS <147462678+freddyDOTCMS@users.noreply.github.com> --- .../content/ContentAnalyticsAPI.java | 15 ++ .../content/ContentAnalyticsAPIImpl.java | 8 + .../content/ContentAnalyticsFactory.java | 10 + .../content/ContentAnalyticsFactoryImpl.java | 14 + .../analytics/content/ReportResponse.java | 7 + .../dotcms/analytics/model/ResultSetItem.java | 6 + .../analytics/viewtool/AnalyticsTool.java | 169 ++++++++++++ .../java/com/dotcms/cube/CubeJSQuery.java | 18 +- .../content/ContentAnalyticsResource.java | 27 +- dotCMS/src/main/webapp/WEB-INF/toolbox.xml | 5 + .../analytics/viewtool/AnalyticsToolTest.java | 246 ++++++++++++++++++ 11 files changed, 503 insertions(+), 22 deletions(-) create mode 100644 dotCMS/src/main/java/com/dotcms/analytics/viewtool/AnalyticsTool.java create mode 100644 dotCMS/src/test/java/com/dotcms/analytics/viewtool/AnalyticsToolTest.java diff --git a/dotCMS/src/main/java/com/dotcms/analytics/content/ContentAnalyticsAPI.java b/dotCMS/src/main/java/com/dotcms/analytics/content/ContentAnalyticsAPI.java index 76694ea7a331..26531579feae 100644 --- a/dotCMS/src/main/java/com/dotcms/analytics/content/ContentAnalyticsAPI.java +++ b/dotCMS/src/main/java/com/dotcms/analytics/content/ContentAnalyticsAPI.java @@ -1,6 +1,7 @@ package com.dotcms.analytics.content; import com.dotcms.analytics.query.AnalyticsQuery; +import com.dotcms.cube.CubeJSQuery; import com.liferay.portal.model.User; /** @@ -14,6 +15,20 @@ */ public interface ContentAnalyticsAPI { + /** + * Run a report based on an analytics query + * @param query + * @param user + * @return ReportResponse + */ ReportResponse runReport(final AnalyticsQuery query, final User user); + /** + * Runs a raw report based on a cubeJS query + * @param cubeJSQuery + * @param user + * @return ReportResponse + */ + ReportResponse runRawReport(CubeJSQuery cubeJSQuery, User user); + } diff --git a/dotCMS/src/main/java/com/dotcms/analytics/content/ContentAnalyticsAPIImpl.java b/dotCMS/src/main/java/com/dotcms/analytics/content/ContentAnalyticsAPIImpl.java index 9fff54bf4257..2417314de079 100644 --- a/dotCMS/src/main/java/com/dotcms/analytics/content/ContentAnalyticsAPIImpl.java +++ b/dotCMS/src/main/java/com/dotcms/analytics/content/ContentAnalyticsAPIImpl.java @@ -1,6 +1,7 @@ package com.dotcms.analytics.content; import com.dotcms.analytics.query.AnalyticsQuery; +import com.dotcms.cube.CubeJSQuery; import com.dotmarketing.util.Logger; import com.liferay.portal.model.User; @@ -31,4 +32,11 @@ public ReportResponse runReport(final AnalyticsQuery query, final User user) { return this.contentAnalyticsFactory.getReport(query, user); } + @Override + public ReportResponse runRawReport(CubeJSQuery cubeJSQuery, User user) { + Logger.debug(this, ()-> "Running the report for the raw query: " + cubeJSQuery); + // note: should check any permissions for an user. + return this.contentAnalyticsFactory.getRawReport(cubeJSQuery, user); + } + } diff --git a/dotCMS/src/main/java/com/dotcms/analytics/content/ContentAnalyticsFactory.java b/dotCMS/src/main/java/com/dotcms/analytics/content/ContentAnalyticsFactory.java index 9f2f771a48ae..2821e6de5009 100644 --- a/dotCMS/src/main/java/com/dotcms/analytics/content/ContentAnalyticsFactory.java +++ b/dotCMS/src/main/java/com/dotcms/analytics/content/ContentAnalyticsFactory.java @@ -1,6 +1,7 @@ package com.dotcms.analytics.content; import com.dotcms.analytics.query.AnalyticsQuery; +import com.dotcms.cube.CubeJSQuery; import com.liferay.portal.model.User; /** @@ -20,4 +21,13 @@ public interface ContentAnalyticsFactory { */ ReportResponse getReport(final AnalyticsQuery query, final User user); + /** + * Runs the raw report based on the cube js query and user. + * + * @param query the query to run the report. + * @param user the user to run the report. + * @return the report response. + */ + ReportResponse getRawReport(final CubeJSQuery query, final User user); + } diff --git a/dotCMS/src/main/java/com/dotcms/analytics/content/ContentAnalyticsFactoryImpl.java b/dotCMS/src/main/java/com/dotcms/analytics/content/ContentAnalyticsFactoryImpl.java index b7338db2329d..be1e7f999d5e 100644 --- a/dotCMS/src/main/java/com/dotcms/analytics/content/ContentAnalyticsFactoryImpl.java +++ b/dotCMS/src/main/java/com/dotcms/analytics/content/ContentAnalyticsFactoryImpl.java @@ -41,6 +41,20 @@ public ReportResponse getReport(final AnalyticsQuery query, final User user) { Logger.debug(this, ()-> "Getting the report for the query: " + query); try { final CubeJSQuery cubeJSQuery = this.queryParser.parseQueryToCubeQuery(query); + return getRawReport(cubeJSQuery, user); + } catch (Exception e) { + + Logger.error(this, e.getMessage(), e); + throw new DotRuntimeException(e); + } + } + + @Override + public ReportResponse getRawReport(final CubeJSQuery cubeJSQuery, final User user) { + + try { + + Logger.debug(this, ()-> "Getting the report for the raw query: " + cubeJSQuery); final CubeJSClient cubeClient = cubeJSClientFactory.create(user); return toReportResponse(cubeClient.send(cubeJSQuery)); } catch (DotDataException| DotSecurityException e) { diff --git a/dotCMS/src/main/java/com/dotcms/analytics/content/ReportResponse.java b/dotCMS/src/main/java/com/dotcms/analytics/content/ReportResponse.java index dc1ca92650a8..10ca5a528641 100644 --- a/dotCMS/src/main/java/com/dotcms/analytics/content/ReportResponse.java +++ b/dotCMS/src/main/java/com/dotcms/analytics/content/ReportResponse.java @@ -21,4 +21,11 @@ public ReportResponse(final List results){ public List getResults() { return results; } + + @Override + public String toString() { + return "ReportResponse{" + + "results=" + results + + '}'; + } } diff --git a/dotCMS/src/main/java/com/dotcms/analytics/model/ResultSetItem.java b/dotCMS/src/main/java/com/dotcms/analytics/model/ResultSetItem.java index e249fd3b2bd7..6dd93abc9747 100644 --- a/dotCMS/src/main/java/com/dotcms/analytics/model/ResultSetItem.java +++ b/dotCMS/src/main/java/com/dotcms/analytics/model/ResultSetItem.java @@ -31,4 +31,10 @@ public Map getAll(){ return new HashMap<>(item); } + @Override + public String toString() { + return "ResultSetItem{" + + "item=" + item + + '}'; + } } diff --git a/dotCMS/src/main/java/com/dotcms/analytics/viewtool/AnalyticsTool.java b/dotCMS/src/main/java/com/dotcms/analytics/viewtool/AnalyticsTool.java new file mode 100644 index 000000000000..1890d00d802a --- /dev/null +++ b/dotCMS/src/main/java/com/dotcms/analytics/viewtool/AnalyticsTool.java @@ -0,0 +1,169 @@ +package com.dotcms.analytics.viewtool; + +import com.dotcms.analytics.content.ContentAnalyticsAPI; +import com.dotcms.analytics.content.ReportResponse; +import com.dotcms.analytics.query.AnalyticsQuery; +import com.dotcms.analytics.query.AnalyticsQueryParser; +import com.dotcms.cdi.CDIUtils; +import com.dotcms.cube.CubeJSQuery; +import com.dotcms.rest.api.v1.DotObjectMapperProvider; +import com.dotmarketing.business.web.UserWebAPI; +import com.dotmarketing.business.web.WebAPILocator; +import com.dotmarketing.exception.DotRuntimeException; +import com.dotmarketing.util.Logger; +import com.liferay.portal.model.User; +import org.apache.velocity.tools.view.context.ViewContext; +import org.apache.velocity.tools.view.tools.ViewTool; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; + +/** + * This class is a ViewTool that can be used to access the analytics data. + * @author jsanca + */ +public class AnalyticsTool implements ViewTool { + + private final ContentAnalyticsAPI contentAnalyticsAPI; + private final AnalyticsQueryParser analyticsQueryParser; + private final UserWebAPI userWebAPI; + + private User user = null; + + public AnalyticsTool() { + this(getContentAnalyticsAPI(), + getAnalyticsQueryParser(), + WebAPILocator.getUserWebAPI()); + } + + private static ContentAnalyticsAPI getContentAnalyticsAPI() { + final Optional contentAnalyticsAPI = CDIUtils.getBean(ContentAnalyticsAPI.class); + if (!contentAnalyticsAPI.isPresent()) { + throw new DotRuntimeException("Could not instance ContentAnalyticsAPI"); + } + return contentAnalyticsAPI.get(); + } + + private static AnalyticsQueryParser getAnalyticsQueryParser() { + final Optional queryParserOptional = CDIUtils.getBean(AnalyticsQueryParser.class); + if (!queryParserOptional.isPresent()) { + throw new DotRuntimeException("Could not instance AnalyticsQueryParser"); + } + return queryParserOptional.get(); + } + + public AnalyticsTool(final ContentAnalyticsAPI contentAnalyticsAPI, + final AnalyticsQueryParser analyticsQueryParser, + final UserWebAPI userWebAPI) { + + this.contentAnalyticsAPI = contentAnalyticsAPI; + this.analyticsQueryParser = analyticsQueryParser; + this.userWebAPI = userWebAPI; + } + + @Override + public void init(final Object initData) { + + if (initData instanceof ViewContext) { + + final HttpServletRequest request = ((ViewContext) initData).getRequest(); + final HttpSession session = request.getSession(false); + + if (session != null) { + try { + user = userWebAPI.getLoggedInUser(request); + } catch (DotRuntimeException e) { + Logger.error(this.getClass(), e.getMessage()); + } + } + } + } + + /** + * Runs an analytics report based on the string json query. + * example: + * + * #set($query = "{ + * "dimensions": ["Events.referer", "Events.experiment", "Events.variant", "Events.utcTime", "Events.url", "Events.lookBackWindow", "Events.eventType"], + * "measures": ["Events.count", "Events.uniqueCount"], + * "filters": "Events.variant = ['B'] or Events.experiments = ['B']", + * "limit":100, + * "offset":1, + * "timeDimensions":"Events.day day", + * "orders":"Events.day ASC" + * }") + * + * $analytics.runReportFromJson($query) + * + * @param query + * @return + */ + public ReportResponse runReportFromJson(final String query) { + + Logger.debug(this, () -> "Running report from json: " + query); + return contentAnalyticsAPI.runReport(this.analyticsQueryParser.parseJsonToQuery(query), user); + } + + /** + * Runs an analytics report based on Map query. + * example: + * + * #set ($myQuery = {}) + * $myMap.put('dimensions', ["Events.referer", "Events.experiment", "Events.variant", "Events.utcTime", "Events.url", "Events.lookBackWindow", "Events.eventType"]) + * $myMap.put('measures', ["Events.count", "Events.uniqueCount"]) + * $myMap.put('filters', "Events.variant = ['B'] or Events.experiments = ['B']") + * $myMap.put('limit', 100) + * $myMap.put('offset', 1) + * $myMap.put('timeDimensions', "Events.day day") + * $myMap.put('orders', "Events.day ASC") + * + * $analytics.runReportFromMap($myQuery) + * + * @param query + * @return + */ + public ReportResponse runReportFromMap(final Map query) { + + if (Objects.isNull(query)) { + throw new IllegalArgumentException("Query can not be null"); + } + + Logger.debug(this, () -> "Running report from map: " + query); + final AnalyticsQuery analyticsQuery = DotObjectMapperProvider.getInstance() + .getDefaultObjectMapper().convertValue(query, AnalyticsQuery.class); + return contentAnalyticsAPI.runReport(analyticsQuery, user); + } + + /** + * Creates a CubeJSQuery.Builder instance + * @return + */ + public CubeJSQuery.Builder createCubeJSQueryBuilder() { + return new CubeJSQuery.Builder(); + } + + /** + * Runs an analytics report based cube js raw json string query + * + * example: + * + * #set($query = $analytics.createCubeJSQueryBuilder()) + * $query.dimensions("Events.experiment", "Events.variant") + * $analytics.runRawReport($query.build()) + * + * @param query + * @return + */ + public ReportResponse runRawReport(final CubeJSQuery query) { + + if (Objects.isNull(query)) { + throw new IllegalArgumentException("Query can not be null"); + } + + Logger.debug(this, () -> "Running report from raw query: " + query); + return contentAnalyticsAPI.runRawReport(query, user); + } +} diff --git a/dotCMS/src/main/java/com/dotcms/cube/CubeJSQuery.java b/dotCMS/src/main/java/com/dotcms/cube/CubeJSQuery.java index 882eba39695a..d63209147fa7 100644 --- a/dotCMS/src/main/java/com/dotcms/cube/CubeJSQuery.java +++ b/dotCMS/src/main/java/com/dotcms/cube/CubeJSQuery.java @@ -1,19 +1,17 @@ package com.dotcms.cube; - -import com.dotcms.api.system.event.SystemEventsFactory; +import com.dotcms.cube.filters.Filter; import com.dotcms.cube.filters.Filter.Order; import com.dotcms.cube.filters.LogicalFilter; import com.dotcms.cube.filters.SimpleFilter; import com.dotcms.cube.filters.SimpleFilter.Operator; -import com.dotcms.cube.filters.Filter; -import com.dotcms.experiments.business.result.ExperimentResults; -import com.dotcms.experiments.business.result.ExperimentResults.Builder; import com.dotcms.util.DotPreconditions; import com.dotcms.util.JsonUtil; import com.dotmarketing.util.UtilMethods; import com.google.common.collect.Iterables; +import org.jetbrains.annotations.NotNull; + import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -24,16 +22,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.Set; -import java.util.TreeSet; -import java.util.stream.Collectors; -import org.jetbrains.annotations.NotNull; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; import java.util.stream.Collectors; diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/analytics/content/ContentAnalyticsResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/analytics/content/ContentAnalyticsResource.java index de0b8a9a4e82..49761e1249bc 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/analytics/content/ContentAnalyticsResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/analytics/content/ContentAnalyticsResource.java @@ -28,6 +28,7 @@ import javax.ws.rs.core.MediaType; /** + * Resource class that exposes endpoints to query content analytics data. * This REST Endpoint exposes different operations to query Content Analytics data. Content * Analytics will enable customers to track the health and engagement of their content at the level * of individual content items. @@ -48,6 +49,7 @@ public ContentAnalyticsResource() { this(CDIUtils.getBean(ContentAnalyticsAPI.class).orElseGet(APILocator::getContentAnalyticsAPI)); } + //@Inject @VisibleForTesting public ContentAnalyticsResource(final ContentAnalyticsAPI contentAnalyticsAPI) { this(new WebResource(), contentAnalyticsAPI); @@ -60,12 +62,14 @@ public ContentAnalyticsResource(final WebResource webResource, this.contentAnalyticsAPI = contentAnalyticsAPI; } - @POST - @Path("/_query") - @JSONP - @NoCache - @Consumes(MediaType.APPLICATION_JSON) - @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) + /** + * Query Content Analytics data. + * + * @param request the HTTP request. + * @param response the HTTP response. + * @param queryForm the query form. + * @return the report response entity view. + */ @Operation( operationId = "postContentAnalyticsQuery", summary = "Retrieve Content Analytics data", @@ -101,18 +105,27 @@ public ContentAnalyticsResource(final WebResource webResource, @ApiResponse(responseCode = "500", description = "Internal Server Error") } ) + @POST + @Path("/_query") + @JSONP + @NoCache + @Consumes(MediaType.APPLICATION_JSON) + @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) public ReportResponseEntityView query(@Context final HttpServletRequest request, @Context final HttpServletResponse response, final QueryForm queryForm) { + final InitDataObject initDataObject = new WebResource.InitBuilder(this.webResource) .requestAndResponse(request, response) .requiredBackendUser(true) .rejectWhenNoUser(true) .init(); + + final User user = initDataObject.getUser(); DotPreconditions.checkNotNull(queryForm, IllegalArgumentException.class, "The 'query' JSON data cannot be null"); Logger.debug(this, () -> "Querying content analytics data with the form: " + queryForm); final ReportResponse reportResponse = - this.contentAnalyticsAPI.runReport(queryForm.getQuery(), initDataObject.getUser()); + this.contentAnalyticsAPI.runReport(queryForm.getQuery(), user); return new ReportResponseEntityView(reportResponse); } diff --git a/dotCMS/src/main/webapp/WEB-INF/toolbox.xml b/dotCMS/src/main/webapp/WEB-INF/toolbox.xml index dbc7172ebbaa..2371ca535fdb 100644 --- a/dotCMS/src/main/webapp/WEB-INF/toolbox.xml +++ b/dotCMS/src/main/webapp/WEB-INF/toolbox.xml @@ -322,4 +322,9 @@ request com.dotcms.ai.viewtool.AIViewTool + + analytics + request + com.dotcms.analytics.viewtool.AnalyticsTool + diff --git a/dotCMS/src/test/java/com/dotcms/analytics/viewtool/AnalyticsToolTest.java b/dotCMS/src/test/java/com/dotcms/analytics/viewtool/AnalyticsToolTest.java new file mode 100644 index 000000000000..2c587385a8c7 --- /dev/null +++ b/dotCMS/src/test/java/com/dotcms/analytics/viewtool/AnalyticsToolTest.java @@ -0,0 +1,246 @@ +package com.dotcms.analytics.viewtool; + +import com.dotcms.analytics.content.ContentAnalyticsAPI; +import com.dotcms.analytics.content.ReportResponse; +import com.dotcms.analytics.query.AnalyticsQueryParser; +import com.dotcms.cube.CubeJSQuery; +import com.dotmarketing.business.web.UserWebAPI; +import com.dotmarketing.exception.DotRuntimeException; +import com.liferay.portal.model.User; +import org.apache.velocity.tools.view.context.ViewContext; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Test class for AnalyticsTool + * @author jsanca + */ +public class AnalyticsToolTest { + + /** + * Method to test: {@link AnalyticsTool#runReportFromJson(String)} + * Given Scenario: Sending a null as json + * ExpectedResult: Should throw IllegalArgumentException + */ + @Test(expected = IllegalArgumentException.class) + public void test_run_report_from_json_npe() { + + final HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + final HttpSession session = Mockito.mock(HttpSession.class); + final ContentAnalyticsAPI contentAnalyticsAPI = Mockito.mock(ContentAnalyticsAPI.class); + final AnalyticsQueryParser analyticsQueryParser = new AnalyticsQueryParser(); + final UserWebAPI userWebAPI = Mockito.mock(UserWebAPI.class); + final ViewContext viewContext = Mockito.mock(ViewContext.class); + final AnalyticsTool analyticsTool = new AnalyticsTool(contentAnalyticsAPI, + analyticsQueryParser, userWebAPI); + final User user = new User(); + + Mockito.when(viewContext.getRequest()).thenReturn(request); + Mockito.when(request.getSession(false)).thenReturn(session); + Mockito.when(userWebAPI.getLoggedInUser(request)).thenReturn(user); + + analyticsTool.init(viewContext); + analyticsTool.runReportFromJson(null); + } + + /** + * Method to test: {@link AnalyticsTool#runReportFromJson(String)} + * Given Scenario: Sending wrong as json + * ExpectedResult: Should throw IllegalArgumentException wrapped on DotRuntimeException + */ + @Test(expected = DotRuntimeException.class) + public void test_run_report_from_json_bad_json() { + + final HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + final HttpSession session = Mockito.mock(HttpSession.class); + final ContentAnalyticsAPI contentAnalyticsAPI = Mockito.mock(ContentAnalyticsAPI.class); + final AnalyticsQueryParser analyticsQueryParser = new AnalyticsQueryParser(); + final UserWebAPI userWebAPI = Mockito.mock(UserWebAPI.class); + final ViewContext viewContext = Mockito.mock(ViewContext.class); + final AnalyticsTool analyticsTool = new AnalyticsTool(contentAnalyticsAPI, + analyticsQueryParser, userWebAPI); + final User user = new User(); + + Mockito.when(viewContext.getRequest()).thenReturn(request); + Mockito.when(request.getSession(false)).thenReturn(session); + Mockito.when(userWebAPI.getLoggedInUser(request)).thenReturn(user); + + analyticsTool.init(viewContext); + analyticsTool.runReportFromJson("{\n" + + "\t\"dimensions\": [\"Events.referer\", \"Events.experiment\", \"Events.variant\", \"Events.utcTime\", \"Events.url\", \"Events.lookBackWindow\", \"Events.eventType\"],\n" + + "\t\"measures\": [\"Events.count\", \"Events.uniqueCount\"],\n" + + "\t\"filters\": \"Events.variant = ['B'] or Events.experiments = ['B']\",\n" + + "\t\"limit\":100,\n" + + "\t\"offset\":1,\n" + + "\t\"timeDimensions\":Events.day day\",\n" + // here is a sintax error + "\t\"orders\":\"Events.day ASC\"\n" + + "}"); + } + + /** + * Method to test: {@link AnalyticsTool#runReportFromJson(String)} + * Given Scenario: Sending a good as json + * ExpectedResult: Should a non null ReportResponse + */ + @Test() + public void test_run_report_from_json_good_json() { + + final HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + final HttpSession session = Mockito.mock(HttpSession.class); + final ContentAnalyticsAPI contentAnalyticsAPI = Mockito.mock(ContentAnalyticsAPI.class); + final AnalyticsQueryParser analyticsQueryParser = new AnalyticsQueryParser(); + final UserWebAPI userWebAPI = Mockito.mock(UserWebAPI.class); + final ViewContext viewContext = Mockito.mock(ViewContext.class); + final AnalyticsTool analyticsTool = new AnalyticsTool(contentAnalyticsAPI, + analyticsQueryParser, userWebAPI); + final User user = new User(); + + Mockito.when(viewContext.getRequest()).thenReturn(request); + Mockito.when(request.getSession(false)).thenReturn(session); + Mockito.when(userWebAPI.getLoggedInUser(request)).thenReturn(user); + Mockito.when(contentAnalyticsAPI.runReport(Mockito.any(), Mockito.eq(user))).thenReturn(new ReportResponse(List.of())); + + analyticsTool.init(viewContext); + final ReportResponse reportResponse = analyticsTool.runReportFromJson("{\n" + + "\t\"dimensions\": [\"Events.referer\", \"Events.experiment\", \"Events.variant\", \"Events.utcTime\", \"Events.url\", \"Events.lookBackWindow\", \"Events.eventType\"],\n" + + "\t\"measures\": [\"Events.count\", \"Events.uniqueCount\"],\n" + + "\t\"filters\": \"Events.variant = ['B'] or Events.experiments = ['B']\",\n" + + "\t\"limit\":100,\n" + + "\t\"offset\":1,\n" + + "\t\"timeDimensions\":\"Events.day day\",\n" + + "\t\"orders\":\"Events.day ASC\"\n" + + "}"); + + Assert.assertNotNull(reportResponse); + } + + /** + * Method to test: {@link AnalyticsTool#runReportFromMap(Map)} + * Given Scenario: Sending a null map + * ExpectedResult: Should throw {@link IllegalArgumentException} + */ + @Test(expected = IllegalArgumentException.class) + public void test_run_report_from_map_with_null_map() { + + final HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + final HttpSession session = Mockito.mock(HttpSession.class); + final ContentAnalyticsAPI contentAnalyticsAPI = Mockito.mock(ContentAnalyticsAPI.class); + final AnalyticsQueryParser analyticsQueryParser = new AnalyticsQueryParser(); + final UserWebAPI userWebAPI = Mockito.mock(UserWebAPI.class); + final ViewContext viewContext = Mockito.mock(ViewContext.class); + final AnalyticsTool analyticsTool = new AnalyticsTool(contentAnalyticsAPI, + analyticsQueryParser, userWebAPI); + final User user = new User(); + + Mockito.when(viewContext.getRequest()).thenReturn(request); + Mockito.when(request.getSession(false)).thenReturn(session); + Mockito.when(userWebAPI.getLoggedInUser(request)).thenReturn(user); + Mockito.when(contentAnalyticsAPI.runReport(Mockito.any(), Mockito.eq(user))).thenReturn(new ReportResponse(List.of())); + + analyticsTool.init(viewContext); + final ReportResponse reportResponse = analyticsTool.runReportFromMap(null); + + Assert.assertNotNull(reportResponse); + } + + /** + * Method to test: {@link AnalyticsTool#runReportFromMap(Map)} + * Given Scenario: Sending a good as map + * ExpectedResult: Should a non null ReportResponse + */ + @Test() + public void test_run_report_from_map_good_map() { + + final HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + final HttpSession session = Mockito.mock(HttpSession.class); + final ContentAnalyticsAPI contentAnalyticsAPI = Mockito.mock(ContentAnalyticsAPI.class); + final AnalyticsQueryParser analyticsQueryParser = new AnalyticsQueryParser(); + final UserWebAPI userWebAPI = Mockito.mock(UserWebAPI.class); + final ViewContext viewContext = Mockito.mock(ViewContext.class); + final AnalyticsTool analyticsTool = new AnalyticsTool(contentAnalyticsAPI, + analyticsQueryParser, userWebAPI); + final User user = new User(); + + Mockito.when(viewContext.getRequest()).thenReturn(request); + Mockito.when(request.getSession(false)).thenReturn(session); + Mockito.when(userWebAPI.getLoggedInUser(request)).thenReturn(user); + Mockito.when(contentAnalyticsAPI.runReport(Mockito.any(), Mockito.eq(user))).thenReturn(new ReportResponse(List.of())); + + analyticsTool.init(viewContext); + final Map queryMap = new HashMap<>(); + queryMap.put("dimensions", List.of("Events.referer", "Events.experiment", "Events.variant", "Events.utcTime", "Events.url", "Events.lookBackWindow", "Events.eventType")); + queryMap.put("measures", List.of("Events.count", "Events.uniqueCount")); + queryMap.put("filters", "Events.variant = ['B'] or Events.experiments = ['B']"); + queryMap.put("limit", 100); + queryMap.put("offset", 1); + queryMap.put("timeDimensions", "Events.day day"); + queryMap.put("orders", "Events.day ASC"); + final ReportResponse reportResponse = analyticsTool.runReportFromMap(queryMap); + + Assert.assertNotNull(reportResponse); + } + + /** + * Method to test: {@link AnalyticsTool#runRawReportFromJson(String)} + * Given Scenario: Sending a null json + * ExpectedResult: Should throw {@link IllegalArgumentException} + */ + @Test(expected = IllegalArgumentException.class) + public void test_run_raw_report_from_json_npe() { + + final HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + final HttpSession session = Mockito.mock(HttpSession.class); + final ContentAnalyticsAPI contentAnalyticsAPI = Mockito.mock(ContentAnalyticsAPI.class); + final AnalyticsQueryParser analyticsQueryParser = new AnalyticsQueryParser(); + final UserWebAPI userWebAPI = Mockito.mock(UserWebAPI.class); + final ViewContext viewContext = Mockito.mock(ViewContext.class); + final AnalyticsTool analyticsTool = new AnalyticsTool(contentAnalyticsAPI, + analyticsQueryParser, userWebAPI); + final User user = new User(); + + Mockito.when(viewContext.getRequest()).thenReturn(request); + Mockito.when(request.getSession(false)).thenReturn(session); + Mockito.when(userWebAPI.getLoggedInUser(request)).thenReturn(user); + Mockito.when(contentAnalyticsAPI.runReport(Mockito.any(), Mockito.eq(user))).thenReturn(new ReportResponse(List.of())); + + analyticsTool.init(viewContext); + analyticsTool.runRawReport(null); + } + + /** + * Method to test: {@link AnalyticsTool#runRawReportFromJson(String)} + * Given Scenario: Sending a good json + * ExpectedResult: Should return not null ReportResponse + */ + @Test() + public void test_run_raw_report_from_json_good_job() { + + final HttpServletRequest request = Mockito.mock(HttpServletRequest.class); + final HttpSession session = Mockito.mock(HttpSession.class); + final ContentAnalyticsAPI contentAnalyticsAPI = Mockito.mock(ContentAnalyticsAPI.class); + final AnalyticsQueryParser analyticsQueryParser = new AnalyticsQueryParser(); + final UserWebAPI userWebAPI = Mockito.mock(UserWebAPI.class); + final ViewContext viewContext = Mockito.mock(ViewContext.class); + final AnalyticsTool analyticsTool = new AnalyticsTool(contentAnalyticsAPI, + analyticsQueryParser, userWebAPI); + final User user = new User(); + + Mockito.when(viewContext.getRequest()).thenReturn(request); + Mockito.when(request.getSession(false)).thenReturn(session); + Mockito.when(userWebAPI.getLoggedInUser(request)).thenReturn(user); + Mockito.when(contentAnalyticsAPI.runRawReport(Mockito.any(), Mockito.eq(user))).thenReturn(new ReportResponse(List.of())); + + analyticsTool.init(viewContext); + final CubeJSQuery.Builder builder = analyticsTool.createCubeJSQueryBuilder(); + builder.dimensions("Events.experiment", "Events.variant"); + final ReportResponse reportResponse = analyticsTool.runRawReport(builder.build()); + Assert.assertNotNull(reportResponse); + } +} From 34b682f5798a721e612f95a369c8a99b506052dc Mon Sep 17 00:00:00 2001 From: Victor Alfaro Date: Thu, 26 Sep 2024 10:46:08 -0600 Subject: [PATCH 3/3] Fixing broken trunk after E2E tests were included in the CICD pipeline (#30150) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Daniel Enrique Colina Rodríguez --- .github/workflows/cicd_1-pr.yml | 3 +-- .github/workflows/cicd_2-merge-queue.yml | 2 ++ .github/workflows/cicd_3-trunk.yml | 1 - 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cicd_1-pr.yml b/.github/workflows/cicd_1-pr.yml index 83e29908aaf1..505c3800547c 100644 --- a/.github/workflows/cicd_1-pr.yml +++ b/.github/workflows/cicd_1-pr.yml @@ -69,8 +69,7 @@ jobs: postman: ${{ needs.initialize.outputs.backend == 'true' }} frontend: ${{ needs.initialize.outputs.frontend == 'true' }} cli: ${{ needs.initialize.outputs.cli == 'true' }} -# TODO: remove this param after testing E2E tests invocation - e2e: true + e2e: ${{ needs.initialize.outputs.build }} secrets: DOTCMS_LICENSE: ${{ secrets.DOTCMS_LICENSE }} diff --git a/.github/workflows/cicd_2-merge-queue.yml b/.github/workflows/cicd_2-merge-queue.yml index 1e9479bea03c..84191b7c483f 100644 --- a/.github/workflows/cicd_2-merge-queue.yml +++ b/.github/workflows/cicd_2-merge-queue.yml @@ -26,6 +26,8 @@ jobs: postman: ${{ needs.initialize.outputs.backend == 'true' }} frontend: ${{ needs.initialize.outputs.frontend == 'true' }} cli: ${{ needs.initialize.outputs.cli == 'true' }} +# This will probably need to be re-evaluated, that is if it makes sense to tun E2E tests at the merge queue +# e2e: ${{ needs.initialize.outputs.build }} secrets: DOTCMS_LICENSE: ${{ secrets.DOTCMS_LICENSE }} finalize: diff --git a/.github/workflows/cicd_3-trunk.yml b/.github/workflows/cicd_3-trunk.yml index d219b6fa4a8a..ab3e724ca8a1 100644 --- a/.github/workflows/cicd_3-trunk.yml +++ b/.github/workflows/cicd_3-trunk.yml @@ -66,7 +66,6 @@ jobs: with: run-all-tests: ${{ inputs.run-all-tests || false }} artifact-run-id: ${{ needs.initialize.outputs.artifact-run-id }} - e2e: true secrets: DOTCMS_LICENSE: ${{ secrets.DOTCMS_LICENSE }} permissions: