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()); + } + +}