From 82ddbb0e83853c263bcd775718f07ab6e4af1d40 Mon Sep 17 00:00:00 2001 From: Fabrizzio Araya <37148755+fabrizzio-dotCMS@users.noreply.github.com> Date: Mon, 16 Dec 2024 16:03:55 -0600 Subject: [PATCH] feat(GraphQL) publishDate param to GraphQL Refs:#30780 (#30885) ### Proposed Changes * I'm opening the possibility of passing a publishDate through the GQL API * Additionally, I saw an opportunity to improve our passing the request into a method that uses it. Previously, it was being grabbed by the current thread. We should avoid using the Current Thread to pass around the request, as it is the source of memory leaks --- .../business/ESContentletAPIImpl.java | 4 + .../PageAPIGraphQLFieldsProvider.java | 4 + .../page/ContainersDataFetcher.java | 3 - .../datafetcher/page/PageDataFetcher.java | 24 ++++ .../velocity/services/PageRenderUtil.java | 12 +- .../render/HTMLPageAssetRenderedAPIImpl.java | 20 +-- test-karate/src/test/java/KarateCITests.java | 5 +- .../src/test/java/graphql/ftm/helpers.feature | 132 ++++++++++++++++++ .../java/graphql/ftm/newContainer.feature | 24 ++++ .../test/java/graphql/ftm/newContent.feature | 21 +++ .../java/graphql/ftm/newContentType.feature | 118 ++++++++++++++++ .../graphql/ftm/newContentVersion.feature | 18 +++ .../src/test/java/graphql/ftm/newPage.feature | 24 ++++ .../test/java/graphql/ftm/newTemplate.feature | 60 ++++++++ .../test/java/graphql/ftm/publishPage.feature | 25 ++++ .../java/graphql/ftm/publishTemplate.feature | 12 ++ .../src/test/java/graphql/ftm/setup.feature | 52 +++++++ test-karate/src/test/java/karate-config.js | 21 ++- .../graphql/ftm/CheckingTimeMachine.feature | 63 +++++++++ .../ftm/CheckingTimeMachineRunner.java | 12 ++ 20 files changed, 629 insertions(+), 25 deletions(-) create mode 100644 test-karate/src/test/java/graphql/ftm/helpers.feature create mode 100644 test-karate/src/test/java/graphql/ftm/newContainer.feature create mode 100644 test-karate/src/test/java/graphql/ftm/newContent.feature create mode 100644 test-karate/src/test/java/graphql/ftm/newContentType.feature create mode 100644 test-karate/src/test/java/graphql/ftm/newContentVersion.feature create mode 100644 test-karate/src/test/java/graphql/ftm/newPage.feature create mode 100644 test-karate/src/test/java/graphql/ftm/newTemplate.feature create mode 100644 test-karate/src/test/java/graphql/ftm/publishPage.feature create mode 100644 test-karate/src/test/java/graphql/ftm/publishTemplate.feature create mode 100644 test-karate/src/test/java/graphql/ftm/setup.feature create mode 100644 test-karate/src/test/java/tests/graphql/ftm/CheckingTimeMachine.feature create mode 100644 test-karate/src/test/java/tests/graphql/ftm/CheckingTimeMachineRunner.java diff --git a/dotCMS/src/main/java/com/dotcms/content/elasticsearch/business/ESContentletAPIImpl.java b/dotCMS/src/main/java/com/dotcms/content/elasticsearch/business/ESContentletAPIImpl.java index afeb560188e0..d6ceb97dd977 100644 --- a/dotCMS/src/main/java/com/dotcms/content/elasticsearch/business/ESContentletAPIImpl.java +++ b/dotCMS/src/main/java/com/dotcms/content/elasticsearch/business/ESContentletAPIImpl.java @@ -686,6 +686,10 @@ public Contentlet findContentletByIdentifier(final String identifier, final long final Date timeMachineDate, final User user, final boolean respectFrontendRoles) throws DotDataException, DotSecurityException, DotContentletStateException{ final Contentlet contentlet = contentFactory.findContentletByIdentifier(identifier, languageId, variantId, timeMachineDate); + if (contentlet == null) { + Logger.debug(this, "Contentlet not found for identifier: " + identifier + " lang:" + languageId + " variant:" + variantId + " date:" + timeMachineDate); + return null; + } if (permissionAPI.doesUserHavePermission(contentlet, PermissionAPI.PERMISSION_READ, user, respectFrontendRoles)) { return contentlet; } else { diff --git a/dotCMS/src/main/java/com/dotcms/graphql/business/PageAPIGraphQLFieldsProvider.java b/dotCMS/src/main/java/com/dotcms/graphql/business/PageAPIGraphQLFieldsProvider.java index 8d17d5ff43a5..36fa9bff8e3b 100644 --- a/dotCMS/src/main/java/com/dotcms/graphql/business/PageAPIGraphQLFieldsProvider.java +++ b/dotCMS/src/main/java/com/dotcms/graphql/business/PageAPIGraphQLFieldsProvider.java @@ -50,6 +50,10 @@ public Collection getFields() throws DotDataException { .name("site") .type(GraphQLString) .build()) + .argument(GraphQLArgument.newArgument() //This is time machine + .name("publishDate") + .type(GraphQLString) + .build()) .type(PageAPIGraphQLTypesProvider.INSTANCE.getTypesMap().get(DOT_PAGE)) .dataFetcher(new PageDataFetcher()).build()); } diff --git a/dotCMS/src/main/java/com/dotcms/graphql/datafetcher/page/ContainersDataFetcher.java b/dotCMS/src/main/java/com/dotcms/graphql/datafetcher/page/ContainersDataFetcher.java index 1ea5b74a62a7..84e6d17e4818 100644 --- a/dotCMS/src/main/java/com/dotcms/graphql/datafetcher/page/ContainersDataFetcher.java +++ b/dotCMS/src/main/java/com/dotcms/graphql/datafetcher/page/ContainersDataFetcher.java @@ -14,7 +14,6 @@ import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; import java.util.List; -import javax.servlet.http.HttpServletRequest; /** * This DataFetcher returns the {@link TemplateLayout} associated to the requested {@link HTMLPageAsset}. @@ -31,8 +30,6 @@ public List get(final DataFetchingEnvironment environment) throws final String languageId = (String) context.getParam("languageId"); final PageMode mode = PageMode.get(pageModeAsString); - final HttpServletRequest request = context.getHttpServletRequest(); - final HTMLPageAsset pageAsset = APILocator.getHTMLPageAssetAPI() .fromContentlet(page); diff --git a/dotCMS/src/main/java/com/dotcms/graphql/datafetcher/page/PageDataFetcher.java b/dotCMS/src/main/java/com/dotcms/graphql/datafetcher/page/PageDataFetcher.java index 719e99bc1c44..a885e60fb2d5 100644 --- a/dotCMS/src/main/java/com/dotcms/graphql/datafetcher/page/PageDataFetcher.java +++ b/dotCMS/src/main/java/com/dotcms/graphql/datafetcher/page/PageDataFetcher.java @@ -2,6 +2,8 @@ import com.dotcms.graphql.DotGraphQLContext; import com.dotcms.graphql.exception.PermissionDeniedGraphQLException; +import com.dotcms.rest.api.v1.page.PageResource; +import com.dotcms.variant.VariantAPI; import com.dotmarketing.beans.Host; import com.dotmarketing.business.APILocator; import com.dotmarketing.exception.DotSecurityException; @@ -15,6 +17,7 @@ import com.dotmarketing.portlets.htmlpageasset.model.HTMLPageAsset; import com.dotmarketing.portlets.rules.business.RulesEngine; import com.dotmarketing.portlets.rules.model.Rule.FireOn; +import com.dotmarketing.util.DateUtil; import com.dotmarketing.util.Logger; import com.dotmarketing.util.PageMode; import com.dotmarketing.util.UtilMethods; @@ -22,6 +25,9 @@ import com.liferay.portal.model.User; import graphql.schema.DataFetcher; import graphql.schema.DataFetchingEnvironment; +import io.vavr.control.Try; +import java.time.Instant; +import java.util.Date; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -53,6 +59,7 @@ public Contentlet get(final DataFetchingEnvironment environment) throws Exceptio final boolean fireRules = environment.getArgument("fireRules"); final String persona = environment.getArgument("persona"); final String site = environment.getArgument("site"); + final String publishDate = environment.getArgument("publishDate"); context.addParam("url", url); context.addParam("languageId", languageId); @@ -60,6 +67,7 @@ public Contentlet get(final DataFetchingEnvironment environment) throws Exceptio context.addParam("fireRules", fireRules); context.addParam("persona", persona); context.addParam("site", site); + context.addParam("publishDate", publishDate); final PageMode mode = PageMode.get(pageModeAsString); PageMode.setPageMode(request, mode); @@ -77,6 +85,22 @@ public Contentlet get(final DataFetchingEnvironment environment) throws Exceptio request.setAttribute(Host.HOST_VELOCITY_VAR_NAME, site); } + Date publishDateObj = null; + + if(UtilMethods.isSet(publishDate)) { + publishDateObj = Try.of(()-> DateUtil.convertDate(publishDate)).getOrElse(() -> { + Logger.error(this, "Invalid publish date: " + publishDate); + return null; + }); + if(null != publishDateObj) { + //We get a valid time machine date + final Instant instant = publishDateObj.toInstant(); + final long epochMilli = instant.toEpochMilli(); + context.addParam(PageResource.TM_DATE, epochMilli); + request.setAttribute(PageResource.TM_DATE, epochMilli); + } + } + Logger.debug(this, ()-> "Fetching page for URL: " + url); final PageContext pageContext = PageContextBuilder.builder() diff --git a/dotCMS/src/main/java/com/dotcms/rendering/velocity/services/PageRenderUtil.java b/dotCMS/src/main/java/com/dotcms/rendering/velocity/services/PageRenderUtil.java index 14c2eb885f77..d1daac3664c2 100644 --- a/dotCMS/src/main/java/com/dotcms/rendering/velocity/services/PageRenderUtil.java +++ b/dotCMS/src/main/java/com/dotcms/rendering/velocity/services/PageRenderUtil.java @@ -356,27 +356,27 @@ private Optional timeMachineDate(final HttpServletRequest request) { return Optional.empty(); } - Optional millis = Optional.empty(); + Optional millis = Optional.empty(); final HttpSession session = request.getSession(false); if (session != null) { - millis = Optional.ofNullable ((String)session.getAttribute(PageResource.TM_DATE)); + millis = Optional.ofNullable (session.getAttribute(PageResource.TM_DATE)); } if (millis.isEmpty()) { - millis = Optional.ofNullable((String)request.getAttribute(PageResource.TM_DATE)); + millis = Optional.ofNullable(request.getAttribute(PageResource.TM_DATE)); } if (millis.isEmpty()) { return Optional.empty(); } - + final Object object = millis.get(); try { - final long milliseconds = Long.parseLong(millis.get()); + final long milliseconds = object instanceof Number ? (Long) object : Long.parseLong(object.toString()); return milliseconds > 0 ? Optional.of(Date.from(Instant.ofEpochMilli(milliseconds))) : Optional.empty(); } catch (NumberFormatException e) { - Logger.error(this, "Invalid timestamp format: " + millis.get(), e); + Logger.error(this, "Invalid timestamp format: " + object, e); return Optional.empty(); } diff --git a/dotCMS/src/main/java/com/dotmarketing/portlets/htmlpageasset/business/render/HTMLPageAssetRenderedAPIImpl.java b/dotCMS/src/main/java/com/dotmarketing/portlets/htmlpageasset/business/render/HTMLPageAssetRenderedAPIImpl.java index 5301f3ab334c..c5e606d445ef 100644 --- a/dotCMS/src/main/java/com/dotmarketing/portlets/htmlpageasset/business/render/HTMLPageAssetRenderedAPIImpl.java +++ b/dotCMS/src/main/java/com/dotmarketing/portlets/htmlpageasset/business/render/HTMLPageAssetRenderedAPIImpl.java @@ -367,7 +367,7 @@ private HTMLPageUrl getHtmlPageAsset(final PageContext context, final Host host, throws DotDataException, DotSecurityException { Logger.debug(this, "--HTMLPageAssetRenderedAPIImpl_getHtmlPageAsset--"); - Optional htmlPageUrlOptional = findPageByContext(host, context); + Optional htmlPageUrlOptional = findPageByContext(host, context, request); if (htmlPageUrlOptional.isEmpty()) { Logger.debug(this, "HTMLPageAssetRenderedAPIImpl_getHtmlPageAsset htmlPageUrlOptional is Empty trying to find by URL Map"); @@ -428,17 +428,18 @@ private void checkPagePermission(final PageContext context, final IHTMLPage html * @throws DotSecurityException The User accessing the APIs does not have the required permissions to perform * this action. */ - private Optional findPageByContext(final Host host, final PageContext context) + private Optional findPageByContext(final Host host, final PageContext context, final HttpServletRequest request) throws DotDataException, DotSecurityException { final User user = context.getUser(); - final String uri = context.getPageUri(); final PageMode mode = context.getPageMode(); - final String pageUri = (UUIDUtil.isUUID(uri) ||( uri.length()>0 && '/' == uri.charAt(0))) ? uri : ("/" + uri); - Logger.debug(this, "HTMLPageAssetRenderedAPIImpl_findPageByContext user: " + user + " uri: " + uri + " mode: " + mode + " host: " + host + " pageUri: " + pageUri); - final HTMLPageAsset htmlPageAsset = (HTMLPageAsset) (UUIDUtil.isUUID(pageUri) ? - this.htmlPageAssetAPI.findPage(pageUri, user, mode.respectAnonPerms) : - getPageByUri(mode, host, pageUri)); + String uri = context.getPageUri(); + uri = uri == null ? StringPool.BLANK : uri; + final String pageUriOrInode = (UUIDUtil.isUUID(uri) ||(!uri.isEmpty() && '/' == uri.charAt(0))) ? uri : ("/" + uri); + Logger.debug(this, "HTMLPageAssetRenderedAPIImpl_findPageByContext user: " + user + " uri: " + uri + " mode: " + mode + " host: " + host + " pageUriOrInode: " + pageUriOrInode); + final HTMLPageAsset htmlPageAsset = (HTMLPageAsset) (UUIDUtil.isUUID(pageUriOrInode) ? + this.htmlPageAssetAPI.findPage(pageUriOrInode, user, mode.respectAnonPerms) : + getPageByUri(mode, host, pageUriOrInode, request)); Logger.debug(this, "HTMLPageAssetRenderedAPIImpl_findPageByContext htmlPageAsset: " + (htmlPageAsset == null ? "Not Found" : htmlPageAsset.toString())); return Optional.ofNullable(htmlPageAsset == null ? null : new HTMLPageUrl(htmlPageAsset)); @@ -494,10 +495,9 @@ private Optional findByURLMap( } } - private IHTMLPage getPageByUri(final PageMode mode, final Host host, final String pageUri) + private IHTMLPage getPageByUri(final PageMode mode, final Host host, final String pageUri, final HttpServletRequest request) throws DotDataException, DotSecurityException { - final HttpServletRequest request = HttpServletRequestThreadLocal.INSTANCE.getRequest(); final Language defaultLanguage = this.languageAPI.getDefaultLanguage(); final Language language = this.getCurrentLanguage(request); Logger.debug(this, "HTMLPageAssetRenderedAPIImpl_getPageByUri pageUri: " + pageUri + " host: " + host + " language: " + language + " mode: " + mode); diff --git a/test-karate/src/test/java/KarateCITests.java b/test-karate/src/test/java/KarateCITests.java index a2a990e0fe84..07152e558a97 100644 --- a/test-karate/src/test/java/KarateCITests.java +++ b/test-karate/src/test/java/KarateCITests.java @@ -9,7 +9,10 @@ public class KarateCITests { @Test void defaults() { - Results results = Runner.path("classpath:tests/defaults").tags("~@ignore") + Results results = Runner.path( + "classpath:tests/defaults", + "classpath:tests/graphql/ftm" + ).tags("~@ignore") .outputHtmlReport(true) .outputJunitXml(true) .outputCucumberJson(true) diff --git a/test-karate/src/test/java/graphql/ftm/helpers.feature b/test-karate/src/test/java/graphql/ftm/helpers.feature new file mode 100644 index 000000000000..c33701d4ed6f --- /dev/null +++ b/test-karate/src/test/java/graphql/ftm/helpers.feature @@ -0,0 +1,132 @@ +Feature: Reusable Functions and Helpers + + Scenario: Define reusable functions + + ## General error free validation + * def validateNoErrors = + """ + function (response) { + const errors = response.errors; + if (errors) { + return errors; + } + return []; + } + """ + + ## Builds a payload for creating a new content version + * def buildContentRequestPayload = + """ + function(contentType, title, publishDate, expiresOn, identifier) { + let payload = { + "contentlets": [ + { + "contentType": contentType, + "title": title, + "host":"8a7d5e23-da1e-420a-b4f0-471e7da8ea2d" + } + ] + }; + if (publishDate) payload.contentlets[0].publishDate = publishDate; + if (expiresOn) payload.contentlets[0].expiresOn = expiresOn; + if (identifier) payload.contentlets[0].identifier = identifier; + return payload; + } + """ + ## Extracts all errors from a response + * def extractErrors = + """ + function(response) { + let errors = []; + let results = response.entity.results; + if (results && results.length > 0) { + for (let i = 0; i < results.length; i++) { + let result = results[i]; + // Handle both nested error messages and direct error messages + for (let key in result) { + if (result[key] && result[key].errorMessage) { + errors.push(result[key].errorMessage); + } + } + } + } + return errors; + } + """ + + ## Extracts all contentlets from a response + * def extractContentlets = + """ + function(response) { + let containers = response.entity.containers; + let allContentlets = []; + for (let key in containers) { + if (containers[key].contentlets) { + for (let contentletKey in containers[key].contentlets) { + allContentlets = allContentlets.concat(containers[key].contentlets[contentletKey]); + } + } + } + return allContentlets; + } + """ + + ## Generates a random suffix for test data + * def testSuffix = + """ + function() { + if (!karate.get('testSuffix')) { + let prefix = '__' + Math.floor(Math.random() * 100000); + karate.set('testSuffix', prefix); + } + return karate.get('testSuffix'); + } + """ + + ## Extracts a specific object from a JSON array by UUID + * def getContentletByUUID = + """ + function(jsonArray, uuid) { + for (let i = 0; i < jsonArray.length; i++) { + let keys = Object.keys(jsonArray[i]); + if (keys.includes(uuid)) { + return jsonArray[i][uuid]; + } + } + return null; // Return null if not found + } + """ + + ## Builds a payload for creating a new GraphQL request + * def buildGraphQLRequestPayload = + """ + function(pageUri, publishDate) { + if (!pageUri.startsWith('/')) { + pageUri = '/' + pageUri; + } + var query = 'query Page { page(url: "' + pageUri + '"'; + if (publishDate) { + query += ' publishDate: "' + publishDate + '"'; + } + query += ') { containers { containerContentlets { contentlets { title } } } } }'; + return { query: query }; + } + """ + + ## Extracts all contentlet titles from a GraphQL response + * def contentletsFromGraphQlResponse = + """ + function(response) { + let containers = response.data.page.containers; + let allTitles = []; + containers.forEach(container => { + container.containerContentlets.forEach(cc => { + cc.contentlets.forEach(contentlet => { + allTitles.push(contentlet.title); + }); + }); + }); + return allTitles; + } + """ + ## \ No newline at end of file diff --git a/test-karate/src/test/java/graphql/ftm/newContainer.feature b/test-karate/src/test/java/graphql/ftm/newContainer.feature new file mode 100644 index 000000000000..b52636cb9147 --- /dev/null +++ b/test-karate/src/test/java/graphql/ftm/newContainer.feature @@ -0,0 +1,24 @@ +Feature: Create a Container + Background: + * def containerNameVariable = 'MyContainer' + Math.floor(Math.random() * 100000) + + Scenario: Create a content type and expect 200 OK + Given url baseUrl + '/api/v1/containers' + And headers commonHeaders + And request + """ + { + "title":"#(containerNameVariable)", + "friendlyName":"My test container.", + "maxContentlets":10, + "notes":"Notes", + "containerStructures":[ + { + "structureId":"#(contentTypeId)", + "code":"$!{dotContentMap.title}" + } + ] + } + """ + When method POST + Then status 200 diff --git a/test-karate/src/test/java/graphql/ftm/newContent.feature b/test-karate/src/test/java/graphql/ftm/newContent.feature new file mode 100644 index 000000000000..64affd0890be --- /dev/null +++ b/test-karate/src/test/java/graphql/ftm/newContent.feature @@ -0,0 +1,21 @@ +Feature: Create an instance of a new Content Type and expect 200 OK +Background: + + Scenario: Create an instance of a new Content Type and expect 200 OK + + # Params are expected as arguments to the feature file + * def contentTypeId = __arg.contentTypeId + * def title = __arg.title + * def publishDate = __arg.publishDate + * def expiresOn = __arg.expiresOn + + Given url baseUrl + '/api/v1/workflow/actions/default/fire/PUBLISH?indexPolicy=WAIT_FOR' + And headers commonHeaders + + * def requestPayload = buildContentRequestPayload (contentTypeId, title, publishDate, expiresOn) + And request requestPayload + + When method POST + Then status 200 + * def errors = call extractErrors response + * match errors == [] \ No newline at end of file diff --git a/test-karate/src/test/java/graphql/ftm/newContentType.feature b/test-karate/src/test/java/graphql/ftm/newContentType.feature new file mode 100644 index 000000000000..cba1f9b210ff --- /dev/null +++ b/test-karate/src/test/java/graphql/ftm/newContentType.feature @@ -0,0 +1,118 @@ +Feature: Create a Content Type + Background: + * def contentTypeVariable = 'MyContentType' + Math.floor(Math.random() * 100000) + + Scenario: Create a content type and expect 200 OK + Given url baseUrl + '/api/v1/contenttype' + And headers commonHeaders + And request + """ + { + "baseType":"CONTENT", + "clazz":"com.dotcms.contenttype.model.type.ImmutableSimpleContentType", + "defaultType":false, + "fields":[ + { + "clazz":"com.dotcms.contenttype.model.field.ImmutableTextField", + "dataType":"TEXT", + "fieldType":"Text", + "fieldTypeLabel":"Text", + "fieldVariables":[ + + ], + "fixed":false, + "forceIncludeInApi":false, + "indexed":true, + "listed":false, + "name":"title", + "readOnly":false, + "required":true, + "searchable":true, + "sortOrder":2, + "unique":false, + "variable":"title" + }, + { + "clazz":"com.dotcms.contenttype.model.field.ImmutableDateTimeField", + "dataType":"DATE", + "fieldType":"Date-and-Time", + "fieldTypeLabel":"Date and Time", + "fieldVariables":[ + + ], + "fixed":false, + "forceIncludeInApi":false, + "indexed":true, + "listed":false, + "name":"publishDate", + "readOnly":false, + "required":false, + "searchable":true, + "sortOrder":3, + "unique":false, + "variable":"publishDate" + }, + { + "clazz":"com.dotcms.contenttype.model.field.ImmutableDateTimeField", + "dataType":"DATE", + "fieldType":"Date-and-Time", + "fieldTypeLabel":"Date and Time", + "fieldVariables":[ + + ], + "fixed":false, + "forceIncludeInApi":false, + "indexed":true, + "listed":false, + "name":"expiresOn", + "readOnly":false, + "required":false, + "searchable":true, + "sortOrder":4, + "unique":false, + "variable":"expiresOn" + }, + { + "clazz":"com.dotcms.contenttype.model.field.ImmutableTagField", + "dataType":"SYSTEM", + "fieldType":"Tag", + "fieldTypeLabel":"Tag", + "fieldVariables":[ + + ], + "fixed":false, + "forceIncludeInApi":false, + "indexed":true, + "listed":false, + "name":"tags", + "readOnly":false, + "required":false, + "searchable":false, + "sortOrder":5, + "unique":false, + "variable":"tags" + } + ], + "fixed":false, + "folder":"SYSTEM_FOLDER", + "folderPath":"/", + "host":"8a7d5e23-da1e-420a-b4f0-471e7da8ea2d", + "icon":"adjust", + "multilingualable":false, + "name":"#(contentTypeVariable)", + "publishDateVar":"publishDate", + "expireDateVar":"expiresOn", + "sortOrder":0, + "system":false, + "variable":"#(contentTypeVariable)", + "versionable":true, + "workflows" : [ { + "id" : "d61a59e1-a49c-46f2-a929-db2b4bfa88b2", + "variableName" : "SystemWorkflow" + } ] + } + """ + When method POST + Then status 200 + And match response.entity[0].id != null + And match response.entity[0].variable == contentTypeVariable \ No newline at end of file diff --git a/test-karate/src/test/java/graphql/ftm/newContentVersion.feature b/test-karate/src/test/java/graphql/ftm/newContentVersion.feature new file mode 100644 index 000000000000..e6594519efc1 --- /dev/null +++ b/test-karate/src/test/java/graphql/ftm/newContentVersion.feature @@ -0,0 +1,18 @@ +Feature: Create a new version of a piece of content + Scenario: Create a new version of a piece of content + + # Params are expected as arguments to the feature file + * def identifier = __arg.identifier + * def contentTypeId = __arg.contentTypeId + * def title = __arg.title + * def publishDate = __arg.publishDate + * def expiresOn = __arg.expiresOn + + Given url baseUrl + '/api/v1/workflow/actions/default/fire/PUBLISH?identifier='+identifier+'&indexPolicy=WAIT_FOR' + And headers commonHeaders + * def requestPayload = buildContentRequestPayload (contentTypeId, title, publishDate, expiresOn, identifier) + And request requestPayload + When method POST + Then status 200 + * def errors = call extractErrors response + * match errors == [] diff --git a/test-karate/src/test/java/graphql/ftm/newPage.feature b/test-karate/src/test/java/graphql/ftm/newPage.feature new file mode 100644 index 000000000000..5d5f7b52ac94 --- /dev/null +++ b/test-karate/src/test/java/graphql/ftm/newPage.feature @@ -0,0 +1,24 @@ + Feature: Create a Page + Scenario: Create a new version of a piece of content + Given url baseUrl + '/api/v1/workflow/actions/default/fire/PUBLISH?indexPolicy=WAIT_FOR' + And headers commonHeaders + And request + """ + { + "contentlet" : { + "title" : "#(title)", + "url": "#(pageUrl)", + "languageId" : 1, + "stInode": "c541abb1-69b3-4bc5-8430-5e09e5239cc8", + "template": "#(templateId)", + "friendlyName": "#(title)", + "hostFolder": "8a7d5e23-da1e-420a-b4f0-471e7da8ea2d", + "cachettl": 0, + "sortOrder": 0 + } + } + """ + When method POST + Then status 200 + * def errors = call extractErrors response + * match errors == [] \ No newline at end of file diff --git a/test-karate/src/test/java/graphql/ftm/newTemplate.feature b/test-karate/src/test/java/graphql/ftm/newTemplate.feature new file mode 100644 index 000000000000..652ee787ddd6 --- /dev/null +++ b/test-karate/src/test/java/graphql/ftm/newTemplate.feature @@ -0,0 +1,60 @@ +Feature: Create a new Template for later use during Time Machine testing + Background: + * def templateName = 'MyTemplate' + Math.floor(Math.random() * 1000) + Scenario: Create a new Template + Given url baseUrl + '/api/v1/templates' + And headers commonHeaders + And request + """ + { + "title":"#(templateName)", + "theme":"13f88067-1e25-4e30-bc64-7e8f42ad542f", + "friendlyName":"Test Template.", + "layout":{ + "body":{ + "rows":[ + { + "styleClass":"", + "columns":[ + { + "styleClass":"", + "leftOffset":1, + "width":100, + "containers":[ + { + "identifier":"#(containerId)", + } + ] + } + ] + },{ + "styleClass":"", + "columns":[ + { + "styleClass":"", + "leftOffset":1, + "width":100, + "containers":[ + { + "identifier":"#(containerId)", + } + ] + } + ] + } + ] + }, + "header":true, + "footer":true, + "sidebar":{ + "location":"", + "containers":[ + + ], + "width":"small" + } + } + } + """ + When method post + Then status 200 \ No newline at end of file diff --git a/test-karate/src/test/java/graphql/ftm/publishPage.feature b/test-karate/src/test/java/graphql/ftm/publishPage.feature new file mode 100644 index 000000000000..68b9b9923732 --- /dev/null +++ b/test-karate/src/test/java/graphql/ftm/publishPage.feature @@ -0,0 +1,25 @@ +Feature: Add pieces of content then Publish the Page + Background: + + * def page_id = __arg.page_id + * def content1_id = __arg.content1_id + * def content2_id = __arg.content2_id + * def container_id = __arg.container_id + + Scenario: Create a new version of a piece of content + Given url baseUrl + '/api/v1/page/'+page_id+'/content' + And headers commonHeaders + And request + """ + [ + { + "contentletsId": ["#(content1_id)", "#(content2_id)"], + "identifier": "#(container_id)", + "uuid": "1" + } + ] + """ + When method POST + Then status 200 + * def errors = call validateNoErrors response + * match errors == [] \ No newline at end of file diff --git a/test-karate/src/test/java/graphql/ftm/publishTemplate.feature b/test-karate/src/test/java/graphql/ftm/publishTemplate.feature new file mode 100644 index 000000000000..d2f314c5dc87 --- /dev/null +++ b/test-karate/src/test/java/graphql/ftm/publishTemplate.feature @@ -0,0 +1,12 @@ +Feature: Publish a Template + Background: + + Scenario: Create a new Template + Given url baseUrl + '/api/v1/templates/_publish' + And headers commonHeaders + And request + """ + ["#(templateId)"] + """ + When method PUT + Then status 200 diff --git a/test-karate/src/test/java/graphql/ftm/setup.feature b/test-karate/src/test/java/graphql/ftm/setup.feature new file mode 100644 index 000000000000..1d8b6a2f5628 --- /dev/null +++ b/test-karate/src/test/java/graphql/ftm/setup.feature @@ -0,0 +1,52 @@ +Feature: Setting up the Future Time Machine Test + + Background: + * callonce read('classpath:graphql/ftm/helpers.feature') + # Make the prefix available to the scenario + # Setup required data + # Lets start by creating a new content type, container, template and publish the template + # First the Content Type + * def contentTypeResult = callonce read('classpath:graphql/ftm/newContentType.feature') + * def contentTypeId = contentTypeResult.response.entity[0].id + * def contentTypeVariable = contentTypeResult.response.entity[0].variable + # Now the container, template and publish the template + * def containerResult = callonce read('classpath:graphql/ftm/newContainer.feature') { contentTypeId: '#(contentTypeId)' } + * def containerId = containerResult.response.entity.identifier + * def templateResult = callonce read('classpath:graphql/ftm/newTemplate.feature') { containerId: '#(containerId)' } + * def templateId = templateResult.response.entity.identifier + * callonce read('classpath:graphql/ftm/publishTemplate.feature') { templateId: '#(templateId)' } + + # Create a couple of new pieces of content + * def createContentPieceOneResult = callonce read('classpath:graphql/ftm/newContent.feature') { contentTypeId: '#(contentTypeId)', title: 'test 1' } + * def contentPieceOne = createContentPieceOneResult.response.entity.results + * def contentPieceOneId = contentPieceOne.map(result => Object.keys(result)[0]) + * def contentPieceOneId = contentPieceOneId[0] + + * def createContentPieceTwoResult = callonce read('classpath:graphql/ftm/newContent.feature') { contentTypeId: '#(contentTypeId)', title: 'test 2' } + * def contentPieceTwo = createContentPieceTwoResult.response.entity.results + * def contentPieceTwoId = contentPieceTwo.map(result => Object.keys(result)[0]) + * def contentPieceTwoId = contentPieceTwoId[0] + + # Now lets create a new version for each piece of content + * def formatter = java.time.format.DateTimeFormatter.ofPattern('yyyy-MM-dd') + * def now = java.time.LocalDateTime.now() + * def futureDateTime = now.plusDays(10) + * def formattedFutureDateTime = futureDateTime.format(formatter) + + * def newContentPiceOneVersion2 = callonce read('classpath:graphql/ftm/newContentVersion.feature') { contentTypeId: '#(contentTypeId)', identifier: '#(contentPieceOneId)', title: 'test 1 v2 (This ver will be publshed in the future)', publishDate: '#(formattedFutureDateTime)' } + * def newContentPiceTwoVersion2 = callonce read('classpath:graphql/ftm/newContentVersion.feature') { contentTypeId: '#(contentTypeId)', identifier: '#(contentPieceTwoId)', title: 'test 2 v2' } + + * def pageUrl = 'ftm-test-page' + Math.floor(Math.random() * 10000) + + # Finally lets create a new page + * def createPageResult = callonce read('classpath:graphql/ftm/newPage.feature') { pageUrl:'#(pageUrl)' ,title: 'Future Time Machine Test page', templateId:'#(templateId)' } + + * def pages = createPageResult.response.entity.results + * def pageId = pages.map(result => Object.keys(result)[0]) + * def pageId = pageId[0] + + * def publishPageResult = callonce read('classpath:graphql/ftm/publishPage.feature') { page_id: '#(pageId)', content1_id: '#(contentPieceOneId)', content2_id: '#(contentPieceTwoId)', container_id: '#(containerId)' } + + * karate.log('Page created and Published ::', pageUrl) + + Scenario: \ No newline at end of file diff --git a/test-karate/src/test/java/karate-config.js b/test-karate/src/test/java/karate-config.js index d28f0028725c..d33a045d38f0 100644 --- a/test-karate/src/test/java/karate-config.js +++ b/test-karate/src/test/java/karate-config.js @@ -1,19 +1,30 @@ function fn() { - var env = karate.env; // get system property 'karate.env' + let env = karate.env; // get system property 'karate.env' karate.log('karate.env system property was:', env); if (!env) { env = 'dev'; } - var config = { + let baseUrl = karate.properties['karate.base.url'] || 'http://localhost:8080'; + let authString = 'admin@dotcms.com:admin'; + let encodedAuth = function(s) { + return java.util.Base64.getEncoder().encodeToString(s.getBytes('UTF-8')); + }; + let authHeader = 'Basic ' + encodedAuth(authString); + let config = { env: env, - baseUrl: karate.properties['karate.base.url'] || 'http://localhost:8080' + baseUrl: baseUrl, + commonHeaders : { + 'Content-Type': 'application/json', + 'Authorization': authHeader + } } - if (env == 'dev') { + if (env === 'dev') { // customize // e.g. config.foo = 'bar'; - } else if (env == 'e2e') { + } else if (env === 'e2e') { // customize } karate.log('Base URL set to:', config.baseUrl); + return config; } \ No newline at end of file diff --git a/test-karate/src/test/java/tests/graphql/ftm/CheckingTimeMachine.feature b/test-karate/src/test/java/tests/graphql/ftm/CheckingTimeMachine.feature new file mode 100644 index 000000000000..4bd6dd382506 --- /dev/null +++ b/test-karate/src/test/java/tests/graphql/ftm/CheckingTimeMachine.feature @@ -0,0 +1,63 @@ +Feature: Test Time Machine functionality + + Background: + * callonce read('classpath:graphql/ftm/setup.feature') + + @smoke @positive + Scenario: Test Time Machine functionality when no publish date is provided + Given url baseUrl + '/api/v1/page/render/'+pageUrl+'?language_id=1&mode=LIVE' + And headers commonHeaders + When method GET + Then status 200 + * def pageContents = extractContentlets (response) + + * def contentPieceOne = getContentletByUUID(contentPieceOne, contentPieceOneId) + * def contentPieceTwo = getContentletByUUID(contentPieceTwo, contentPieceTwoId) + + * def titles = pageContents.map(x => x.title) + # This is the first version of the content, test 1 v2 as the title says it will be published in the future + * match titles contains 'test 1' + # This is the second version of the content, Thisone is already published therefore it should be displayed + * match titles contains 'test 2 v2' + + @positive + Scenario: Test Time Machine functionality when a publish date is provided expect the future content to be displayed + + Given url baseUrl + '/api/v1/page/render/'+pageUrl+'?language_id=1&mode=LIVE&publishDate='+formattedFutureDateTime + And headers commonHeaders + When method GET + Then status 200 + * def pageContents = extractContentlets (response) + + * def contentPieceOne = getContentletByUUID(contentPieceOne, contentPieceOneId) + * def contentPieceTwo = getContentletByUUID(contentPieceTwo, contentPieceTwoId) + + * def titles = pageContents.map(x => x.title) + * match titles contains 'test 1 v2 (This ver will be publshed in the future)' + + @smoke @positive + Scenario: Send GraphQL query to fetch page details no publish date is sent + * def graphQLRequestPayLoad = buildGraphQLRequestPayload (pageUrl) + Given url baseUrl + '/api/v1/graphql' + And headers commonHeaders + And request graphQLRequestPayLoad + + When method post + Then status 200 + * def contentlets = contentletsFromGraphQlResponse(response) + * karate.log('contentlets:', contentlets) + * match contentlets contains 'test 1' + * match contentlets contains 'test 2 v2' + + @smoke @positive + Scenario: Send GraphQL query to fetch page details, publish date is sent expect the future content to be displayed + * def graphQLRequestPayLoad = buildGraphQLRequestPayload (pageUrl, formattedFutureDateTime) + Given url baseUrl + '/api/v1/graphql' + And headers commonHeaders + And request graphQLRequestPayLoad + + When method post + Then status 200 + * def contentlets = contentletsFromGraphQlResponse(response) + * karate.log('contentlets:', contentlets) + * match contentlets contains 'test 1 v2 (This ver will be publshed in the future)' diff --git a/test-karate/src/test/java/tests/graphql/ftm/CheckingTimeMachineRunner.java b/test-karate/src/test/java/tests/graphql/ftm/CheckingTimeMachineRunner.java new file mode 100644 index 000000000000..d5a7c06824c2 --- /dev/null +++ b/test-karate/src/test/java/tests/graphql/ftm/CheckingTimeMachineRunner.java @@ -0,0 +1,12 @@ +package tests.graphql.ftm; + +import com.intuit.karate.junit5.Karate; + +public class CheckingTimeMachineRunner { + + @Karate.Test + Karate testCheckingTimeMachine() { + return Karate.run("CheckingTimeMachine").relativeTo(getClass()); + } + +}