From 50f1de9f6b728320ee3050cec1e715bb0e76e2f1 Mon Sep 17 00:00:00 2001 From: Alwin Joseph Date: Wed, 25 Feb 2026 14:58:13 +0530 Subject: [PATCH 1/9] SITES-41041: Resolves versionhistory resources to source content for GraphQL/config lookup --- .../client/MagentoGraphqlClientImpl.java | 3 + ...ComponentsConfigurationAdapterFactory.java | 3 + .../utils/VersionHistoryResourceResolver.java | 120 ++++++++++++++++++ .../client/MagentoGraphqlClientImplTest.java | 21 +++ ...onentsConfigurationAdapterFactoryTest.java | 12 ++ 5 files changed, 159 insertions(+) create mode 100644 bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryResourceResolver.java diff --git a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/client/MagentoGraphqlClientImpl.java b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/client/MagentoGraphqlClientImpl.java index 480e0a72dc..b00d4abf07 100644 --- a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/client/MagentoGraphqlClientImpl.java +++ b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/client/MagentoGraphqlClientImpl.java @@ -47,6 +47,7 @@ import com.adobe.cq.commerce.core.components.client.DeniedHttpHeaders; import com.adobe.cq.commerce.core.components.client.MagentoGraphqlClient; +import com.adobe.cq.commerce.core.components.internal.utils.VersionHistoryResourceResolver; import com.adobe.cq.commerce.core.components.services.ComponentsConfiguration; import com.adobe.cq.commerce.graphql.client.CachingStrategy; import com.adobe.cq.commerce.graphql.client.CachingStrategy.DataFetchingPolicy; @@ -148,6 +149,8 @@ private void initModel(Resource resource, Page page, SlingHttpServletRequest req configurationResource = resource; } + configurationResource = VersionHistoryResourceResolver.resolveSourceResource(configurationResource); + LOGGER.debug("Try to get a graphql client from the resource at {}", configurationResource.getPath()); ComponentsConfiguration configuration = configurationResource.adaptTo(ComponentsConfiguration.class); diff --git a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/services/ComponentsConfigurationAdapterFactory.java b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/services/ComponentsConfigurationAdapterFactory.java index 1f4fe89ace..af4e2ee14f 100644 --- a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/services/ComponentsConfigurationAdapterFactory.java +++ b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/services/ComponentsConfigurationAdapterFactory.java @@ -30,6 +30,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.adobe.cq.commerce.core.components.internal.utils.VersionHistoryResourceResolver; import com.adobe.cq.commerce.core.components.services.ComponentsConfiguration; import com.adobe.cq.wcm.launches.utils.LaunchUtils; @@ -71,6 +72,8 @@ public AdapterType getAdapter(Object adaptable, Class return null; } + resource = VersionHistoryResourceResolver.resolveSourceResource(resource); + if (LaunchUtils.isLaunchBasedPath(resource.getPath())) { // In Launches we have to resolve the ComponentConfigurations from the production resource as there is still an issue // with CA Configs not working properly in Launches in 6.5.x. Additionally, if the resource was created in the Launch diff --git a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryResourceResolver.java b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryResourceResolver.java new file mode 100644 index 0000000000..037e20b00f --- /dev/null +++ b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryResourceResolver.java @@ -0,0 +1,120 @@ +/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ~ Copyright 2026 Adobe + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ +package com.adobe.cq.commerce.core.components.internal.utils; + +import org.apache.commons.lang3.StringUtils; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; +import org.apache.sling.api.resource.ValueMap; + +/** + * Utility methods to resolve source content resources from AEM timeline/version preview resources. + */ +public final class VersionHistoryResourceResolver { + + private static final String VERSION_HISTORY_ROOT = "/tmp/versionhistory/"; + + private static final String[] SOURCE_PATH_PROPERTIES = { + "cq:sourcePath", + "sourcePath", + "jcr:sourcePath" + }; + + private VersionHistoryResourceResolver() {} + + public static Resource resolveSourceResource(Resource resource) { + if (resource == null) { + return null; + } + + Resource propertyBasedResource = resolveFromSourcePathProperty(resource); + if (propertyBasedResource != null) { + return propertyBasedResource; + } + + if (!StringUtils.startsWith(resource.getPath(), VERSION_HISTORY_ROOT)) { + return resource; + } + + String relativePath = getRelativePath(resource.getPath()); + if (StringUtils.isBlank(relativePath)) { + return resource; + } + + ResourceResolver resolver = resource.getResourceResolver(); + Resource candidate = resolveCandidatePath(resolver, "/" + relativePath); + if (candidate != null) { + return candidate; + } + + if (!StringUtils.startsWith(relativePath, "content/")) { + candidate = resolveCandidatePath(resolver, "/content/" + relativePath); + if (candidate != null) { + return candidate; + } + } + + return resource; + } + + private static Resource resolveFromSourcePathProperty(Resource resource) { + ValueMap properties = resource.getValueMap(); + for (String propertyName : SOURCE_PATH_PROPERTIES) { + String sourcePath = properties.get(propertyName, String.class); + if (StringUtils.isNotBlank(sourcePath)) { + Resource sourceResource = resource.getResourceResolver().getResource(sourcePath); + if (sourceResource != null) { + return sourceResource; + } + } + } + return null; + } + + private static Resource resolveCandidatePath(ResourceResolver resolver, String candidatePath) { + String path = StringUtils.removeEnd(candidatePath, "/"); + while (StringUtils.isNotBlank(path)) { + Resource candidate = resolver.getResource(path); + if (candidate != null) { + return candidate; + } + int lastSlash = path.lastIndexOf('/'); + if (lastSlash <= 0) { + break; + } + path = path.substring(0, lastSlash); + } + return null; + } + + private static String getRelativePath(String path) { + String suffix = StringUtils.substringAfter(path, VERSION_HISTORY_ROOT); + if (StringUtils.isBlank(suffix)) { + return null; + } + + int firstSlash = suffix.indexOf('/'); + if (firstSlash < 0) { + return null; + } + int secondSlash = suffix.indexOf('/', firstSlash + 1); + if (secondSlash < 0 || secondSlash + 1 >= suffix.length()) { + return null; + } + + return suffix.substring(secondSlash + 1); + } +} diff --git a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/client/MagentoGraphqlClientImplTest.java b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/client/MagentoGraphqlClientImplTest.java index 97fad1d611..8428273dd3 100644 --- a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/client/MagentoGraphqlClientImplTest.java +++ b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/client/MagentoGraphqlClientImplTest.java @@ -318,6 +318,27 @@ public void testPreviewVersionHeaderOnLaunchPage() { verify(graphqlClient).execute(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.argThat(matcher)); } + @Test + public void testVersionHistoryPathResolvesSourceConfiguration() { + context.registerAdapter(Resource.class, ComponentsConfiguration.class, (Function) resource -> { + if (resource.getPath().startsWith("/content/pageA")) { + return MOCK_CONFIGURATION_OBJECT; + } + return ComponentsConfiguration.EMPTY; + }); + + context.create().page("/tmp/versionhistory/hash/version/content/pageA"); + Resource versionResource = context.create().resource( + "/tmp/versionhistory/hash/version/content/pageA/jcr:content/root/responsivegrid/product"); + + MagentoGraphqlClient client = new MagentoGraphqlClientImpl(versionResource, null, null); + assertNotNull("GraphQL client created successfully", client); + + client.execute("{dummy}"); + RequestOptionsMatcher matcher = new RequestOptionsMatcher(Collections.singletonList(new BasicHeader("Store", "my-store")), null); + verify(graphqlClient).execute(Mockito.any(), Mockito.any(), Mockito.any(), Mockito.argThat(matcher)); + } + @Test public void testPreviewVersionHeaderWithTimewarpRequestParameter() { Calendar time = Calendar.getInstance(); diff --git a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/services/ComponentsConfigurationAdapterFactoryTest.java b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/services/ComponentsConfigurationAdapterFactoryTest.java index 9a1c792e45..32e38ae50e 100644 --- a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/services/ComponentsConfigurationAdapterFactoryTest.java +++ b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/services/ComponentsConfigurationAdapterFactoryTest.java @@ -116,6 +116,18 @@ public void testAdaptFromResourceWithinLaunch() { } } + @Test + public void testAdaptFromResourceWithinVersionHistory() { + Resource versionHistoryResource = context.create().resource("/tmp/versionhistory/hash/version/pageH"); + ComponentsConfiguration configuration = versionHistoryResource.adaptTo(ComponentsConfiguration.class); + + Assert.assertNotNull("Configuration is not null", configuration); + Assert.assertTrue("The configuration has some data in it", configuration.size() > 0); + + String unrelatedProperty = configuration.get("aTotallyUnrelatedProperty", String.class); + Assert.assertEquals("The configuration is correct", unrelatedProperty, "true"); + } + @Test public void testAdaptNullResource() { ComponentsConfiguration configuration = context.resourceResolver().adaptTo(ComponentsConfiguration.class); From 46467b9c2abbc093509a306abdd8cdb53695c4c6 Mon Sep 17 00:00:00 2001 From: Alwin Joseph Date: Wed, 25 Feb 2026 17:49:21 +0530 Subject: [PATCH 2/9] SITES-41041: Adds the integration test --- .../it/http/VersionHistoryPreviewIT.java | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 it/http/src/test/java/com/adobe/cq/commerce/it/http/VersionHistoryPreviewIT.java diff --git a/it/http/src/test/java/com/adobe/cq/commerce/it/http/VersionHistoryPreviewIT.java b/it/http/src/test/java/com/adobe/cq/commerce/it/http/VersionHistoryPreviewIT.java new file mode 100644 index 0000000000..cf4bdce619 --- /dev/null +++ b/it/http/src/test/java/com/adobe/cq/commerce/it/http/VersionHistoryPreviewIT.java @@ -0,0 +1,57 @@ +/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ~ Copyright 2026 Adobe + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ +package com.adobe.cq.commerce.it.http; + +import org.apache.sling.testing.clients.ClientException; +import org.apache.sling.testing.clients.SlingHttpResponse; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.select.Elements; +import org.junit.After; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +public class VersionHistoryPreviewIT extends CommerceTestBase { + + private static final String SOURCE_PRODUCT_TEASER_PAGE = COMMERCE_LIBRARY_PATH + "/productteaser"; + private static final String VERSION_HISTORY_ROOT = "/tmp/versionhistory/cif-it-hash/cif-it-version"; + private static final String VERSION_HISTORY_PARENT = VERSION_HISTORY_ROOT + "/content/core-components-examples/library/commerce"; + private static final String VERSION_HISTORY_PAGE_NODE = VERSION_HISTORY_PARENT + "/productteaser"; + private static final String VERSION_HISTORY_PAGE_PATH = VERSION_HISTORY_PARENT + "/productteaser.html"; + private static final String PRODUCT_TEASER_SELECTOR = CMP_EXAMPLES_DEMO_SELECTOR + " .productteaser .item__name > span"; + + @After + public void cleanup() throws ClientException { + if (adminAuthor.exists(VERSION_HISTORY_ROOT)) { + adminAuthor.deletePath(VERSION_HISTORY_ROOT); + } + } + + @Test + public void testVersionHistoryPathRendersProductTeaser() throws ClientException { + assertTrue("Source page missing: " + SOURCE_PRODUCT_TEASER_PAGE, adminAuthor.exists(SOURCE_PRODUCT_TEASER_PAGE)); + adminAuthor.createNodeRecursive(VERSION_HISTORY_PARENT, "sling:Folder"); + adminAuthor.copyPage(new String[] { SOURCE_PRODUCT_TEASER_PAGE }, "productteaser", null, VERSION_HISTORY_PARENT, null, false); + assertTrue("Version history preview page was not created", adminAuthor.exists(VERSION_HISTORY_PAGE_NODE)); + + SlingHttpResponse response = adminAuthor.doGet(VERSION_HISTORY_PAGE_PATH, 200); + Document doc = Jsoup.parse(response.getContent()); + Elements elements = doc.select(PRODUCT_TEASER_SELECTOR); + assertEquals("Summit Watch", elements.first().html()); + } +} From 7f6d19bf6f5b2b7a8a7676e94cc1fe28171e9dc7 Mon Sep 17 00:00:00 2001 From: Alwin Joseph Date: Thu, 26 Feb 2026 07:45:55 +0530 Subject: [PATCH 3/9] SITES-41041: Fix the integration issue --- .../it/http/VersionHistoryPreviewIT.java | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/it/http/src/test/java/com/adobe/cq/commerce/it/http/VersionHistoryPreviewIT.java b/it/http/src/test/java/com/adobe/cq/commerce/it/http/VersionHistoryPreviewIT.java index cf4bdce619..3276d1f13f 100644 --- a/it/http/src/test/java/com/adobe/cq/commerce/it/http/VersionHistoryPreviewIT.java +++ b/it/http/src/test/java/com/adobe/cq/commerce/it/http/VersionHistoryPreviewIT.java @@ -15,12 +15,15 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ package com.adobe.cq.commerce.it.http; +import org.apache.http.HttpEntity; import org.apache.sling.testing.clients.ClientException; import org.apache.sling.testing.clients.SlingHttpResponse; +import org.apache.sling.testing.clients.util.FormEntityBuilder; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.select.Elements; import org.junit.After; +import org.junit.Before; import org.junit.Test; import static org.junit.Assert.assertEquals; @@ -29,27 +32,39 @@ public class VersionHistoryPreviewIT extends CommerceTestBase { private static final String SOURCE_PRODUCT_TEASER_PAGE = COMMERCE_LIBRARY_PATH + "/productteaser"; - private static final String VERSION_HISTORY_ROOT = "/tmp/versionhistory/cif-it-hash/cif-it-version"; - private static final String VERSION_HISTORY_PARENT = VERSION_HISTORY_ROOT + "/content/core-components-examples/library/commerce"; - private static final String VERSION_HISTORY_PAGE_NODE = VERSION_HISTORY_PARENT + "/productteaser"; - private static final String VERSION_HISTORY_PAGE_PATH = VERSION_HISTORY_PARENT + "/productteaser.html"; + private static final String VERSION_HISTORY_ROOT_BASE = "/tmp/versionhistory/cif-it-hash"; + private static final String VERSION_HISTORY_PAGE_SUFFIX = "/content/core-components-examples/library/commerce/productteaser"; private static final String PRODUCT_TEASER_SELECTOR = CMP_EXAMPLES_DEMO_SELECTOR + " .productteaser .item__name > span"; + private String versionHistoryRoot; + private String versionHistoryPagePath; + + @Before + public void setup() throws ClientException { + assertTrue("Source page missing: " + SOURCE_PRODUCT_TEASER_PAGE, adminAuthor.exists(SOURCE_PRODUCT_TEASER_PAGE)); + versionHistoryRoot = VERSION_HISTORY_ROOT_BASE + "/cif-it-version-" + System.currentTimeMillis(); + String versionHistoryParent = versionHistoryRoot + "/content/core-components-examples/library/commerce"; + String versionHistoryPageNode = versionHistoryRoot + VERSION_HISTORY_PAGE_SUFFIX; + versionHistoryPagePath = versionHistoryPageNode + ".html"; + + adminAuthor.createNodeRecursive(versionHistoryParent, "sling:Folder"); + HttpEntity copyEntity = FormEntityBuilder.create() + .addParameter(":operation", "copy") + .addParameter(":dest", versionHistoryPageNode) + .build(); + adminAuthor.doPost(SOURCE_PRODUCT_TEASER_PAGE, copyEntity, 200, 201); + assertTrue("Version history preview page was not created", adminAuthor.exists(versionHistoryPageNode)); + } @After public void cleanup() throws ClientException { - if (adminAuthor.exists(VERSION_HISTORY_ROOT)) { - adminAuthor.deletePath(VERSION_HISTORY_ROOT); + if (versionHistoryRoot != null && adminAuthor.exists(versionHistoryRoot)) { + adminAuthor.deletePath(versionHistoryRoot); } } @Test public void testVersionHistoryPathRendersProductTeaser() throws ClientException { - assertTrue("Source page missing: " + SOURCE_PRODUCT_TEASER_PAGE, adminAuthor.exists(SOURCE_PRODUCT_TEASER_PAGE)); - adminAuthor.createNodeRecursive(VERSION_HISTORY_PARENT, "sling:Folder"); - adminAuthor.copyPage(new String[] { SOURCE_PRODUCT_TEASER_PAGE }, "productteaser", null, VERSION_HISTORY_PARENT, null, false); - assertTrue("Version history preview page was not created", adminAuthor.exists(VERSION_HISTORY_PAGE_NODE)); - - SlingHttpResponse response = adminAuthor.doGet(VERSION_HISTORY_PAGE_PATH, 200); + SlingHttpResponse response = adminAuthor.doGet(versionHistoryPagePath, 200); Document doc = Jsoup.parse(response.getContent()); Elements elements = doc.select(PRODUCT_TEASER_SELECTOR); assertEquals("Summit Watch", elements.first().html()); From 4f0d0cc342877793148d16537d1fe450f861ea1f Mon Sep 17 00:00:00 2001 From: Alwin Joseph Date: Thu, 26 Feb 2026 12:12:23 +0530 Subject: [PATCH 4/9] SITES-41041: Adds more test coverage --- .../VersionHistoryResourceResolverTest.java | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryResourceResolverTest.java diff --git a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryResourceResolverTest.java b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryResourceResolverTest.java new file mode 100644 index 0000000000..0f0eb309ba --- /dev/null +++ b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryResourceResolverTest.java @@ -0,0 +1,133 @@ +/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ~ Copyright 2026 Adobe + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ +package com.adobe.cq.commerce.core.components.internal.utils; + +import java.util.Collections; + +import org.apache.sling.api.resource.Resource; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; + +import com.adobe.cq.commerce.core.testing.TestContext; +import com.google.common.collect.ImmutableMap; +import io.wcm.testing.mock.aem.junit.AemContext; + +public class VersionHistoryResourceResolverTest { + + @Rule + public final AemContext context = TestContext.newAemContext(); + + @Test + public void testResolveNullResource() { + Assert.assertNull(VersionHistoryResourceResolver.resolveSourceResource(null)); + } + + @Test + public void testResolveNonVersionHistoryResourceReturnsSameResource() { + Resource resource = context.create().resource("/content/site/page"); + + Resource resolved = VersionHistoryResourceResolver.resolveSourceResource(resource); + + Assert.assertEquals("/content/site/page", resolved.getPath()); + } + + @Test + public void testResolveFromCqSourcePathProperty() { + context.create().resource("/content/site/source-page"); + Resource resource = context.create().resource("/tmp/versionhistory/hash/version/site/page", + Collections.singletonMap("cq:sourcePath", "/content/site/source-page")); + + Resource resolved = VersionHistoryResourceResolver.resolveSourceResource(resource); + + Assert.assertEquals("/content/site/source-page", resolved.getPath()); + } + + @Test + public void testResolveFromSourcePathPropertyFallback() { + context.create().resource("/content/site/source-page"); + Resource resource = context.create().resource("/tmp/versionhistory/hash/version/site/page", + ImmutableMap.of( + "cq:sourcePath", "", + "sourcePath", "/content/site/source-page")); + + Resource resolved = VersionHistoryResourceResolver.resolveSourceResource(resource); + + Assert.assertEquals("/content/site/source-page", resolved.getPath()); + } + + @Test + public void testResolveFromJcrSourcePathPropertyFallback() { + context.create().resource("/content/site/source-page"); + Resource resource = context.create().resource("/tmp/versionhistory/hash/version/site/page", + ImmutableMap.of( + "cq:sourcePath", " ", + "sourcePath", "", + "jcr:sourcePath", "/content/site/source-page")); + + Resource resolved = VersionHistoryResourceResolver.resolveSourceResource(resource); + + Assert.assertEquals("/content/site/source-page", resolved.getPath()); + } + + @Test + public void testResolveUsingRelativePathCandidate() { + context.create().resource("/content/site/page"); + Resource resource = context.create().resource("/tmp/versionhistory/hash/version/content/site/page"); + + Resource resolved = VersionHistoryResourceResolver.resolveSourceResource(resource); + + Assert.assertEquals("/content/site/page", resolved.getPath()); + } + + @Test + public void testResolveUsingRelativePathParentFallback() { + context.create().resource("/content/site/page"); + Resource resource = context.create().resource("/tmp/versionhistory/hash/version/content/site/page/child"); + + Resource resolved = VersionHistoryResourceResolver.resolveSourceResource(resource); + + Assert.assertEquals("/content/site/page", resolved.getPath()); + } + + @Test + public void testResolveUsingContentPrefixedFallback() { + context.create().resource("/content/site/page"); + Resource resource = context.create().resource("/tmp/versionhistory/hash/version/site/page"); + + Resource resolved = VersionHistoryResourceResolver.resolveSourceResource(resource); + + Assert.assertEquals("/content/site/page", resolved.getPath()); + } + + @Test + public void testResolveInvalidVersionHistoryPathReturnsSameResource() { + Resource resource = context.create().resource("/tmp/versionhistory/hash/version"); + + Resource resolved = VersionHistoryResourceResolver.resolveSourceResource(resource); + + Assert.assertEquals("/tmp/versionhistory/hash/version", resolved.getPath()); + } + + @Test + public void testResolveWithoutAnyCandidateReturnsSameResource() { + Resource resource = context.create().resource("/tmp/versionhistory/hash/version/site/page"); + + Resource resolved = VersionHistoryResourceResolver.resolveSourceResource(resource); + + Assert.assertEquals("/tmp/versionhistory/hash/version/site/page", resolved.getPath()); + } +} From a26b31719954ad8ca4649c1782d6ffa889a81410 Mon Sep 17 00:00:00 2001 From: Alwin Joseph Date: Thu, 26 Feb 2026 14:24:23 +0530 Subject: [PATCH 5/9] SITES-41041: Adds more test coverage --- .../VersionHistoryResourceResolverTest.java | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryResourceResolverTest.java b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryResourceResolverTest.java index 0f0eb309ba..1116c0ea40 100644 --- a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryResourceResolverTest.java +++ b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryResourceResolverTest.java @@ -39,9 +39,7 @@ public void testResolveNullResource() { @Test public void testResolveNonVersionHistoryResourceReturnsSameResource() { Resource resource = context.create().resource("/content/site/page"); - Resource resolved = VersionHistoryResourceResolver.resolveSourceResource(resource); - Assert.assertEquals("/content/site/page", resolved.getPath()); } @@ -52,7 +50,6 @@ public void testResolveFromCqSourcePathProperty() { Collections.singletonMap("cq:sourcePath", "/content/site/source-page")); Resource resolved = VersionHistoryResourceResolver.resolveSourceResource(resource); - Assert.assertEquals("/content/site/source-page", resolved.getPath()); } @@ -65,7 +62,6 @@ public void testResolveFromSourcePathPropertyFallback() { "sourcePath", "/content/site/source-page")); Resource resolved = VersionHistoryResourceResolver.resolveSourceResource(resource); - Assert.assertEquals("/content/site/source-page", resolved.getPath()); } @@ -79,7 +75,6 @@ public void testResolveFromJcrSourcePathPropertyFallback() { "jcr:sourcePath", "/content/site/source-page")); Resource resolved = VersionHistoryResourceResolver.resolveSourceResource(resource); - Assert.assertEquals("/content/site/source-page", resolved.getPath()); } @@ -87,9 +82,7 @@ public void testResolveFromJcrSourcePathPropertyFallback() { public void testResolveUsingRelativePathCandidate() { context.create().resource("/content/site/page"); Resource resource = context.create().resource("/tmp/versionhistory/hash/version/content/site/page"); - Resource resolved = VersionHistoryResourceResolver.resolveSourceResource(resource); - Assert.assertEquals("/content/site/page", resolved.getPath()); } @@ -97,9 +90,7 @@ public void testResolveUsingRelativePathCandidate() { public void testResolveUsingRelativePathParentFallback() { context.create().resource("/content/site/page"); Resource resource = context.create().resource("/tmp/versionhistory/hash/version/content/site/page/child"); - Resource resolved = VersionHistoryResourceResolver.resolveSourceResource(resource); - Assert.assertEquals("/content/site/page", resolved.getPath()); } @@ -107,27 +98,51 @@ public void testResolveUsingRelativePathParentFallback() { public void testResolveUsingContentPrefixedFallback() { context.create().resource("/content/site/page"); Resource resource = context.create().resource("/tmp/versionhistory/hash/version/site/page"); - Resource resolved = VersionHistoryResourceResolver.resolveSourceResource(resource); - Assert.assertEquals("/content/site/page", resolved.getPath()); } @Test public void testResolveInvalidVersionHistoryPathReturnsSameResource() { Resource resource = context.create().resource("/tmp/versionhistory/hash/version"); + Resource resourceWithTrailingSlash = context.create().resource("/tmp/versionhistory/hash/version/"); Resource resolved = VersionHistoryResourceResolver.resolveSourceResource(resource); + Resource resolvedWithTrailingSlash = VersionHistoryResourceResolver.resolveSourceResource(resourceWithTrailingSlash); Assert.assertEquals("/tmp/versionhistory/hash/version", resolved.getPath()); + Assert.assertEquals("/tmp/versionhistory/hash/version", resolvedWithTrailingSlash.getPath()); } @Test public void testResolveWithoutAnyCandidateReturnsSameResource() { Resource resource = context.create().resource("/tmp/versionhistory/hash/version/site/page"); + Resource resolved = VersionHistoryResourceResolver.resolveSourceResource(resource); + Assert.assertEquals("/tmp/versionhistory/hash/version/site/page", resolved.getPath()); + } + @Test + public void testResolveSourcePathFallsBackWhenTargetMissing() { + context.create().resource("/content/site/source-page"); + Resource resource = context.create().resource("/tmp/versionhistory/hash/version/site/page", + ImmutableMap.of( + "cq:sourcePath", "/content/site/missing-page", + "sourcePath", "/content/site/source-page")); + Resource resolved = VersionHistoryResourceResolver.resolveSourceResource(resource); + Assert.assertEquals("/content/site/source-page", resolved.getPath()); + } + + @Test + public void testResolveInvalidVersionHistoryPathWithoutVersionIdReturnsSameResource() { + Resource resource = context.create().resource("/tmp/versionhistory/hashonly"); Resource resolved = VersionHistoryResourceResolver.resolveSourceResource(resource); + Assert.assertEquals("/tmp/versionhistory/hashonly", resolved.getPath()); + } - Assert.assertEquals("/tmp/versionhistory/hash/version/site/page", resolved.getPath()); + @Test + public void testResolveContentRelativePathWithoutCandidateReturnsSameResource() { + Resource resource = context.create().resource("/tmp/versionhistory/hash/version/content/unknown"); + Resource resolved = VersionHistoryResourceResolver.resolveSourceResource(resource); + Assert.assertEquals("/tmp/versionhistory/hash/version/content/unknown", resolved.getPath()); } } From fda1e4807835856137cf5a0c183204fcb19ac0e4 Mon Sep 17 00:00:00 2001 From: Alwin Joseph Date: Tue, 3 Mar 2026 12:39:36 +0530 Subject: [PATCH 6/9] SITES-41041: Fixs CIF rendering for version-history preview --- .../client/MagentoGraphqlClientImpl.java | 9 +- .../StoreConfigExporterImpl.java | 26 +++ ...ComponentsConfigurationAdapterFactory.java | 7 +- .../utils/VersionHistoryResourceResolver.java | 120 -------------- .../internal/utils/VersionHistoryUtils.java | 74 +++++++++ .../client/MagentoGraphqlClientImplTest.java | 25 ++- .../StoreConfigExporterImplTest.java | 44 ++++++ ...onentsConfigurationAdapterFactoryTest.java | 22 ++- .../VersionHistoryResourceResolverTest.java | 148 ------------------ .../utils/VersionHistoryUtilsTest.java | 113 +++++++++++++ .../it/http/VersionHistoryPreviewIT.java | 95 ++++++++--- 11 files changed, 384 insertions(+), 299 deletions(-) delete mode 100644 bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryResourceResolver.java create mode 100644 bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryUtils.java delete mode 100644 bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryResourceResolverTest.java create mode 100644 bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryUtilsTest.java diff --git a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/client/MagentoGraphqlClientImpl.java b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/client/MagentoGraphqlClientImpl.java index b00d4abf07..ddf711e5e7 100644 --- a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/client/MagentoGraphqlClientImpl.java +++ b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/client/MagentoGraphqlClientImpl.java @@ -47,7 +47,7 @@ import com.adobe.cq.commerce.core.components.client.DeniedHttpHeaders; import com.adobe.cq.commerce.core.components.client.MagentoGraphqlClient; -import com.adobe.cq.commerce.core.components.internal.utils.VersionHistoryResourceResolver; +import com.adobe.cq.commerce.core.components.internal.utils.VersionHistoryUtils; import com.adobe.cq.commerce.core.components.services.ComponentsConfiguration; import com.adobe.cq.commerce.graphql.client.CachingStrategy; import com.adobe.cq.commerce.graphql.client.CachingStrategy.DataFetchingPolicy; @@ -140,6 +140,11 @@ private void initModel(Resource resource, Page page, SlingHttpServletRequest req if (page != null) { configurationResource = Objects.requireNonNull(page.adaptTo(Resource.class), "page is not a Resource"); + // If the page is rendered from AEM version history preview, resolve back to the source resource. + if (VersionHistoryUtils.isVersionHistoryResource(configurationResource)) { + configurationResource = VersionHistoryUtils.resolveSourceResource(configurationResource); + } + // If the page is an AEM Launch, we get the configuration from the production page if (LaunchUtils.isLaunchBasedPath(page.getPath())) { Resource launchResource = LaunchUtils.getLaunchResource(configurationResource); @@ -149,8 +154,6 @@ private void initModel(Resource resource, Page page, SlingHttpServletRequest req configurationResource = resource; } - configurationResource = VersionHistoryResourceResolver.resolveSourceResource(configurationResource); - LOGGER.debug("Try to get a graphql client from the resource at {}", configurationResource.getPath()); ComponentsConfiguration configuration = configurationResource.adaptTo(ComponentsConfiguration.class); diff --git a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/models/v1/storeconfigexporter/StoreConfigExporterImpl.java b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/models/v1/storeconfigexporter/StoreConfigExporterImpl.java index dd8dcf21c0..a41186c0d0 100644 --- a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/models/v1/storeconfigexporter/StoreConfigExporterImpl.java +++ b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/models/v1/storeconfigexporter/StoreConfigExporterImpl.java @@ -33,6 +33,7 @@ import org.slf4j.LoggerFactory; import com.adobe.cq.commerce.core.components.client.MagentoGraphqlClient; +import com.adobe.cq.commerce.core.components.internal.utils.VersionHistoryUtils; import com.adobe.cq.commerce.core.components.models.common.SiteStructure; import com.adobe.cq.commerce.core.components.models.storeconfigexporter.StoreConfigExporter; import com.adobe.cq.commerce.core.components.services.ComponentsConfiguration; @@ -124,6 +125,11 @@ public String getMethod() { public String getStoreRootUrl() { if (storeRootPage == null) { storeRootPage = siteStructure.getLandingPage(); + if (storeRootPage == null) { + // Timeline preview pages live under /tmp/versionhistory and may not have a resolvable landing page. + // In that case, resolve the source /content page and reuse its site structure. + storeRootPage = getStoreRootPageFromVersionHistorySource(); + } } if (storeRootPage == null) { @@ -156,4 +162,24 @@ public boolean isClientSidePriceLoadingEnabled() { public String getLanguage() { return language; } + + /** + * Resolves the landing page from the source /content page when the current page is rendered from + * AEM version history preview under /tmp/versionhistory. + */ + private Page getStoreRootPageFromVersionHistorySource() { + Resource currentPageResource = currentPage != null ? currentPage.adaptTo(Resource.class) : null; + if (!VersionHistoryUtils.isVersionHistoryResource(currentPageResource)) { + return null; + } + + Resource sourcePageResource = VersionHistoryUtils.resolveSourceResource(currentPageResource); + Page sourcePage = sourcePageResource != null ? sourcePageResource.adaptTo(Page.class) : null; + if (sourcePage == null) { + return null; + } + + SiteStructure sourceSiteStructure = sourcePage.adaptTo(SiteStructure.class); + return sourceSiteStructure != null ? sourceSiteStructure.getLandingPage() : null; + } } diff --git a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/services/ComponentsConfigurationAdapterFactory.java b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/services/ComponentsConfigurationAdapterFactory.java index af4e2ee14f..9ee4201d41 100644 --- a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/services/ComponentsConfigurationAdapterFactory.java +++ b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/services/ComponentsConfigurationAdapterFactory.java @@ -30,7 +30,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.adobe.cq.commerce.core.components.internal.utils.VersionHistoryResourceResolver; +import com.adobe.cq.commerce.core.components.internal.utils.VersionHistoryUtils; import com.adobe.cq.commerce.core.components.services.ComponentsConfiguration; import com.adobe.cq.wcm.launches.utils.LaunchUtils; @@ -72,7 +72,10 @@ public AdapterType getAdapter(Object adaptable, Class return null; } - resource = VersionHistoryResourceResolver.resolveSourceResource(resource); + // If the adapted resource comes from version history preview, resolve it to the source content path. + if (VersionHistoryUtils.isVersionHistoryResource(resource)) { + resource = VersionHistoryUtils.resolveSourceResource(resource); + } if (LaunchUtils.isLaunchBasedPath(resource.getPath())) { // In Launches we have to resolve the ComponentConfigurations from the production resource as there is still an issue diff --git a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryResourceResolver.java b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryResourceResolver.java deleted file mode 100644 index 037e20b00f..0000000000 --- a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryResourceResolver.java +++ /dev/null @@ -1,120 +0,0 @@ -/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - ~ Copyright 2026 Adobe - ~ - ~ Licensed under the Apache License, Version 2.0 (the "License"); - ~ you may not use this file except in compliance with the License. - ~ You may obtain a copy of the License at - ~ - ~ http://www.apache.org/licenses/LICENSE-2.0 - ~ - ~ Unless required by applicable law or agreed to in writing, software - ~ distributed under the License is distributed on an "AS IS" BASIS, - ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - ~ See the License for the specific language governing permissions and - ~ limitations under the License. - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ -package com.adobe.cq.commerce.core.components.internal.utils; - -import org.apache.commons.lang3.StringUtils; -import org.apache.sling.api.resource.Resource; -import org.apache.sling.api.resource.ResourceResolver; -import org.apache.sling.api.resource.ValueMap; - -/** - * Utility methods to resolve source content resources from AEM timeline/version preview resources. - */ -public final class VersionHistoryResourceResolver { - - private static final String VERSION_HISTORY_ROOT = "/tmp/versionhistory/"; - - private static final String[] SOURCE_PATH_PROPERTIES = { - "cq:sourcePath", - "sourcePath", - "jcr:sourcePath" - }; - - private VersionHistoryResourceResolver() {} - - public static Resource resolveSourceResource(Resource resource) { - if (resource == null) { - return null; - } - - Resource propertyBasedResource = resolveFromSourcePathProperty(resource); - if (propertyBasedResource != null) { - return propertyBasedResource; - } - - if (!StringUtils.startsWith(resource.getPath(), VERSION_HISTORY_ROOT)) { - return resource; - } - - String relativePath = getRelativePath(resource.getPath()); - if (StringUtils.isBlank(relativePath)) { - return resource; - } - - ResourceResolver resolver = resource.getResourceResolver(); - Resource candidate = resolveCandidatePath(resolver, "/" + relativePath); - if (candidate != null) { - return candidate; - } - - if (!StringUtils.startsWith(relativePath, "content/")) { - candidate = resolveCandidatePath(resolver, "/content/" + relativePath); - if (candidate != null) { - return candidate; - } - } - - return resource; - } - - private static Resource resolveFromSourcePathProperty(Resource resource) { - ValueMap properties = resource.getValueMap(); - for (String propertyName : SOURCE_PATH_PROPERTIES) { - String sourcePath = properties.get(propertyName, String.class); - if (StringUtils.isNotBlank(sourcePath)) { - Resource sourceResource = resource.getResourceResolver().getResource(sourcePath); - if (sourceResource != null) { - return sourceResource; - } - } - } - return null; - } - - private static Resource resolveCandidatePath(ResourceResolver resolver, String candidatePath) { - String path = StringUtils.removeEnd(candidatePath, "/"); - while (StringUtils.isNotBlank(path)) { - Resource candidate = resolver.getResource(path); - if (candidate != null) { - return candidate; - } - int lastSlash = path.lastIndexOf('/'); - if (lastSlash <= 0) { - break; - } - path = path.substring(0, lastSlash); - } - return null; - } - - private static String getRelativePath(String path) { - String suffix = StringUtils.substringAfter(path, VERSION_HISTORY_ROOT); - if (StringUtils.isBlank(suffix)) { - return null; - } - - int firstSlash = suffix.indexOf('/'); - if (firstSlash < 0) { - return null; - } - int secondSlash = suffix.indexOf('/', firstSlash + 1); - if (secondSlash < 0 || secondSlash + 1 >= suffix.length()) { - return null; - } - - return suffix.substring(secondSlash + 1); - } -} diff --git a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryUtils.java b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryUtils.java new file mode 100644 index 0000000000..b222c8ef62 --- /dev/null +++ b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryUtils.java @@ -0,0 +1,74 @@ +/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ~ Copyright 2026 Adobe + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ +package com.adobe.cq.commerce.core.components.internal.utils; + +import org.apache.commons.lang3.StringUtils; +import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.ResourceResolver; + +/** + * Utility methods to resolve source content resources from AEM timeline/version preview resources. + */ +public final class VersionHistoryUtils { + + private static final String VERSION_HISTORY_ROOT = "/tmp/versionhistory/"; + + private VersionHistoryUtils() {} + + /** + * Returns {@code true} when the resource is a synthetic version preview resource under /tmp/versionhistory. + */ + public static boolean isVersionHistoryResource(Resource resource) { + return resource != null && StringUtils.startsWith(resource.getPath(), VERSION_HISTORY_ROOT); + } + + /** + * Resolves a version preview resource back to its source page/resource so configuration lookups can work. + */ + public static Resource resolveSourceResource(Resource resource) { + if (!isVersionHistoryResource(resource)) { + return resource; + } + + String sourcePagePath = getSourcePagePath(resource.getPath()); + if (StringUtils.isBlank(sourcePagePath)) { + return resource; + } + + ResourceResolver resolver = resource.getResourceResolver(); + Resource sourcePageResource = resolver.getResource(sourcePagePath); + if (sourcePageResource != null) { + return sourcePageResource; + } + + return resource; + } + + private static String getSourcePagePath(String path) { + String suffix = StringUtils.substringAfter(path, VERSION_HISTORY_ROOT); + if (StringUtils.isBlank(suffix)) { + return null; + } + + int secondSlash = StringUtils.ordinalIndexOf(suffix, "/", 2); + if (secondSlash < 0 || secondSlash == suffix.length() - 1) { + return null; + } + + String relativePath = suffix.substring(secondSlash + 1); + return "/content/" + StringUtils.removeEnd(relativePath, "/"); + } +} diff --git a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/client/MagentoGraphqlClientImplTest.java b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/client/MagentoGraphqlClientImplTest.java index 8428273dd3..b1f789dfd8 100644 --- a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/client/MagentoGraphqlClientImplTest.java +++ b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/client/MagentoGraphqlClientImplTest.java @@ -15,6 +15,7 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ package com.adobe.cq.commerce.core.components.internal.client; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -91,6 +92,10 @@ public class MagentoGraphqlClientImplTest { private static final String LAUNCH_BASE_PATH = "/content/launches/2020/09/14/mylaunch"; private static final String LAUNCH_PAGE_A = LAUNCH_BASE_PATH + PAGE_A; private static final String PRODUCT_COMPONENT_PATH = "/content/pageA/jcr:content/root/responsivegrid/product"; + private static final String VERSION_HISTORY_ROOT = "/tmp/versionhistory"; + private static final String VERSION_HISTORY_PAGE_A = VERSION_HISTORY_ROOT + "/hash/version/pageA"; + private static final String VERSION_HISTORY_PRODUCT_TEASER_RESOURCE_PATH = VERSION_HISTORY_PAGE_A + + "/jcr:content/root/responsivegrid/productteaser-simple"; private GraphqlClient graphqlClient; @@ -285,6 +290,18 @@ public void testError() { new MagentoGraphqlClientImpl(resource, null, null); } + @Test + public void testGetPageFromResourceResolvesContainingPage() throws Exception { + Method method = MagentoGraphqlClientImpl.class.getDeclaredMethod("getPageFromResource", Resource.class); + method.setAccessible(true); + + Resource contentResource = context.resourceResolver().getResource("/content/pageB/pageC/jcr:content"); + Page page = (Page) method.invoke(null, contentResource); + + assertNotNull(page); + assertEquals("/content/pageB/pageC", page.getPath()); + } + @Test public void testPreviewVersionHeaderOnLaunchPage() { context.registerAdapter(Resource.class, Launch.class, (Function) resource -> new MockLaunch(resource)); @@ -327,11 +344,11 @@ public void testVersionHistoryPathResolvesSourceConfiguration() { return ComponentsConfiguration.EMPTY; }); - context.create().page("/tmp/versionhistory/hash/version/content/pageA"); - Resource versionResource = context.create().resource( - "/tmp/versionhistory/hash/version/content/pageA/jcr:content/root/responsivegrid/product"); + context.create().page(VERSION_HISTORY_PAGE_A); + Resource versionResource = context.create().resource(VERSION_HISTORY_PRODUCT_TEASER_RESOURCE_PATH); + Page versionPage = context.pageManager().getPage(VERSION_HISTORY_PAGE_A); - MagentoGraphqlClient client = new MagentoGraphqlClientImpl(versionResource, null, null); + MagentoGraphqlClient client = new MagentoGraphqlClientImpl(versionResource, versionPage, null); assertNotNull("GraphQL client created successfully", client); client.execute("{dummy}"); diff --git a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/models/v1/storeconfigexporter/StoreConfigExporterImplTest.java b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/models/v1/storeconfigexporter/StoreConfigExporterImplTest.java index ec0f911113..c35f21218e 100644 --- a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/models/v1/storeconfigexporter/StoreConfigExporterImplTest.java +++ b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/models/v1/storeconfigexporter/StoreConfigExporterImplTest.java @@ -15,6 +15,7 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ package com.adobe.cq.commerce.core.components.internal.models.v1.storeconfigexporter; +import java.lang.reflect.Method; import java.util.Collections; import java.util.Map; @@ -60,6 +61,10 @@ public class StoreConfigExporterImplTest { .put("httpHeaders", new String[] { "customHeader-1=value1", "customHeader-2=value2" }) .put("jcr:language", "de_de") .put("enableClientSidePriceLoading", true).build()); + private static final String VERSION_HISTORY_ROOT = "/tmp/versionhistory/hash/version"; + private static final String VERSION_HISTORY_PAGE_C = VERSION_HISTORY_ROOT + "/pageB/pageC"; + private static final String VERSION_HISTORY_UNKNOWN_PAGE = VERSION_HISTORY_ROOT + "/unknown/page"; + private static final String VERSION_HISTORY_PAGE_WITH_NON_PAGE_SOURCE = VERSION_HISTORY_ROOT + "/nonpage"; @Rule public final AemContext context = TestContext.newAemContext(); @@ -181,6 +186,45 @@ public void testGetStoreRootUrlWithMappingInDifferentDomain() { assertEquals("/content/pageB.html", storeConfigExporter.getStoreRootUrl()); } + @Test + public void testGetStoreRootUrlForVersionHistoryPage() { + context.create().page(VERSION_HISTORY_PAGE_C); + setupWithPage(VERSION_HISTORY_PAGE_C, HttpMethod.POST); + StoreConfigExporterImpl storeConfigExporter = context.request().adaptTo(StoreConfigExporterImpl.class); + assertNotNull(storeConfigExporter); + assertEquals("/content/pageB.html", storeConfigExporter.getStoreRootUrl()); + } + + @Test + public void testGetStoreRootUrlForVersionHistoryPageWithoutSourceLandingPage() { + context.create().page(VERSION_HISTORY_UNKNOWN_PAGE); + setupWithPage(VERSION_HISTORY_UNKNOWN_PAGE, HttpMethod.POST); + StoreConfigExporterImpl storeConfigExporter = context.request().adaptTo(StoreConfigExporterImpl.class); + assertNotNull(storeConfigExporter); + Assert.assertNull(storeConfigExporter.getStoreRootUrl()); + } + + @Test + public void testVersionHistorySourceResolverReturnsNullForNonVersionPage() throws Exception { + setupWithPage("/content/pageD", HttpMethod.POST); + StoreConfigExporterImpl storeConfigExporter = context.request().adaptTo(StoreConfigExporterImpl.class); + assertNotNull(storeConfigExporter); + + Method method = StoreConfigExporterImpl.class.getDeclaredMethod("getStoreRootPageFromVersionHistorySource"); + method.setAccessible(true); + Assert.assertNull(method.invoke(storeConfigExporter)); + } + + @Test + public void testGetStoreRootUrlForVersionHistoryPageWithNonPageSource() { + context.create().page(VERSION_HISTORY_PAGE_WITH_NON_PAGE_SOURCE); + context.create().resource("/content/nonpage", "jcr:primaryType", "nt:unstructured"); + setupWithPage(VERSION_HISTORY_PAGE_WITH_NON_PAGE_SOURCE, HttpMethod.POST); + StoreConfigExporterImpl storeConfigExporter = context.request().adaptTo(StoreConfigExporterImpl.class); + assertNotNull(storeConfigExporter); + Assert.assertNull(storeConfigExporter.getStoreRootUrl()); + } + @Test public void testCustomHttpHeaders() { mockConfiguration = new ComponentsConfiguration(MOCK_CONFIGURATION); diff --git a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/services/ComponentsConfigurationAdapterFactoryTest.java b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/services/ComponentsConfigurationAdapterFactoryTest.java index 32e38ae50e..a2ca6a97d4 100644 --- a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/services/ComponentsConfigurationAdapterFactoryTest.java +++ b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/services/ComponentsConfigurationAdapterFactoryTest.java @@ -19,6 +19,7 @@ import java.util.Hashtable; import org.apache.sling.api.resource.Resource; +import org.apache.sling.api.resource.SyntheticResource; import org.apache.sling.serviceusermapping.ServiceUserMapped; import org.apache.sling.testing.mock.caconfig.ContextPlugins; import org.apache.sling.testing.mock.sling.ResourceResolverType; @@ -40,6 +41,10 @@ public class ComponentsConfigurationAdapterFactoryTest { + private static final String VERSION_HISTORY_ROOT = "/tmp/versionhistory"; + private static final String VERSION_HISTORY_PAGE_H = VERSION_HISTORY_ROOT + "/hash/version/pageH"; + private ComponentsConfigurationAdapterFactory factory; + @Rule public final AemContext context = new AemContextBuilder(ResourceResolverType.JCR_MOCK).plugin(ContextPlugins.CACONFIG) .beforeSetUp(context -> { @@ -72,7 +77,7 @@ public void setup() { context.registerService(ServiceUserMapped.class, serviceUserMapped, ImmutableMap.of(ServiceUserMapped.SUBSERVICENAME, "cif-components-configuration")); - ComponentsConfigurationAdapterFactory factory = new ComponentsConfigurationAdapterFactory(); + factory = new ComponentsConfigurationAdapterFactory(); context.registerInjectActivateService(factory); } @@ -118,7 +123,7 @@ public void testAdaptFromResourceWithinLaunch() { @Test public void testAdaptFromResourceWithinVersionHistory() { - Resource versionHistoryResource = context.create().resource("/tmp/versionhistory/hash/version/pageH"); + Resource versionHistoryResource = context.create().resource(VERSION_HISTORY_PAGE_H); ComponentsConfiguration configuration = versionHistoryResource.adaptTo(ComponentsConfiguration.class); Assert.assertNotNull("Configuration is not null", configuration); @@ -133,4 +138,17 @@ public void testAdaptNullResource() { ComponentsConfiguration configuration = context.resourceResolver().adaptTo(ComponentsConfiguration.class); Assert.assertNull(configuration); } + + @Test + public void testAdaptFromNonResourceReturnsNull() { + ComponentsConfiguration configuration = factory.getAdapter("not-a-resource", ComponentsConfiguration.class); + Assert.assertNull(configuration); + } + + @Test + public void testAdaptFromMissingResourceReturnsNull() { + Resource missingResource = new SyntheticResource(context.resourceResolver(), "/content/does-not-exist", "nt:unstructured"); + ComponentsConfiguration configuration = factory.getAdapter(missingResource, ComponentsConfiguration.class); + Assert.assertNull(configuration); + } } diff --git a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryResourceResolverTest.java b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryResourceResolverTest.java deleted file mode 100644 index 1116c0ea40..0000000000 --- a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryResourceResolverTest.java +++ /dev/null @@ -1,148 +0,0 @@ -/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - ~ Copyright 2026 Adobe - ~ - ~ Licensed under the Apache License, Version 2.0 (the "License"); - ~ you may not use this file except in compliance with the License. - ~ You may obtain a copy of the License at - ~ - ~ http://www.apache.org/licenses/LICENSE-2.0 - ~ - ~ Unless required by applicable law or agreed to in writing, software - ~ distributed under the License is distributed on an "AS IS" BASIS, - ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - ~ See the License for the specific language governing permissions and - ~ limitations under the License. - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ -package com.adobe.cq.commerce.core.components.internal.utils; - -import java.util.Collections; - -import org.apache.sling.api.resource.Resource; -import org.junit.Assert; -import org.junit.Rule; -import org.junit.Test; - -import com.adobe.cq.commerce.core.testing.TestContext; -import com.google.common.collect.ImmutableMap; -import io.wcm.testing.mock.aem.junit.AemContext; - -public class VersionHistoryResourceResolverTest { - - @Rule - public final AemContext context = TestContext.newAemContext(); - - @Test - public void testResolveNullResource() { - Assert.assertNull(VersionHistoryResourceResolver.resolveSourceResource(null)); - } - - @Test - public void testResolveNonVersionHistoryResourceReturnsSameResource() { - Resource resource = context.create().resource("/content/site/page"); - Resource resolved = VersionHistoryResourceResolver.resolveSourceResource(resource); - Assert.assertEquals("/content/site/page", resolved.getPath()); - } - - @Test - public void testResolveFromCqSourcePathProperty() { - context.create().resource("/content/site/source-page"); - Resource resource = context.create().resource("/tmp/versionhistory/hash/version/site/page", - Collections.singletonMap("cq:sourcePath", "/content/site/source-page")); - - Resource resolved = VersionHistoryResourceResolver.resolveSourceResource(resource); - Assert.assertEquals("/content/site/source-page", resolved.getPath()); - } - - @Test - public void testResolveFromSourcePathPropertyFallback() { - context.create().resource("/content/site/source-page"); - Resource resource = context.create().resource("/tmp/versionhistory/hash/version/site/page", - ImmutableMap.of( - "cq:sourcePath", "", - "sourcePath", "/content/site/source-page")); - - Resource resolved = VersionHistoryResourceResolver.resolveSourceResource(resource); - Assert.assertEquals("/content/site/source-page", resolved.getPath()); - } - - @Test - public void testResolveFromJcrSourcePathPropertyFallback() { - context.create().resource("/content/site/source-page"); - Resource resource = context.create().resource("/tmp/versionhistory/hash/version/site/page", - ImmutableMap.of( - "cq:sourcePath", " ", - "sourcePath", "", - "jcr:sourcePath", "/content/site/source-page")); - - Resource resolved = VersionHistoryResourceResolver.resolveSourceResource(resource); - Assert.assertEquals("/content/site/source-page", resolved.getPath()); - } - - @Test - public void testResolveUsingRelativePathCandidate() { - context.create().resource("/content/site/page"); - Resource resource = context.create().resource("/tmp/versionhistory/hash/version/content/site/page"); - Resource resolved = VersionHistoryResourceResolver.resolveSourceResource(resource); - Assert.assertEquals("/content/site/page", resolved.getPath()); - } - - @Test - public void testResolveUsingRelativePathParentFallback() { - context.create().resource("/content/site/page"); - Resource resource = context.create().resource("/tmp/versionhistory/hash/version/content/site/page/child"); - Resource resolved = VersionHistoryResourceResolver.resolveSourceResource(resource); - Assert.assertEquals("/content/site/page", resolved.getPath()); - } - - @Test - public void testResolveUsingContentPrefixedFallback() { - context.create().resource("/content/site/page"); - Resource resource = context.create().resource("/tmp/versionhistory/hash/version/site/page"); - Resource resolved = VersionHistoryResourceResolver.resolveSourceResource(resource); - Assert.assertEquals("/content/site/page", resolved.getPath()); - } - - @Test - public void testResolveInvalidVersionHistoryPathReturnsSameResource() { - Resource resource = context.create().resource("/tmp/versionhistory/hash/version"); - Resource resourceWithTrailingSlash = context.create().resource("/tmp/versionhistory/hash/version/"); - - Resource resolved = VersionHistoryResourceResolver.resolveSourceResource(resource); - Resource resolvedWithTrailingSlash = VersionHistoryResourceResolver.resolveSourceResource(resourceWithTrailingSlash); - - Assert.assertEquals("/tmp/versionhistory/hash/version", resolved.getPath()); - Assert.assertEquals("/tmp/versionhistory/hash/version", resolvedWithTrailingSlash.getPath()); - } - - @Test - public void testResolveWithoutAnyCandidateReturnsSameResource() { - Resource resource = context.create().resource("/tmp/versionhistory/hash/version/site/page"); - Resource resolved = VersionHistoryResourceResolver.resolveSourceResource(resource); - Assert.assertEquals("/tmp/versionhistory/hash/version/site/page", resolved.getPath()); - } - - @Test - public void testResolveSourcePathFallsBackWhenTargetMissing() { - context.create().resource("/content/site/source-page"); - Resource resource = context.create().resource("/tmp/versionhistory/hash/version/site/page", - ImmutableMap.of( - "cq:sourcePath", "/content/site/missing-page", - "sourcePath", "/content/site/source-page")); - Resource resolved = VersionHistoryResourceResolver.resolveSourceResource(resource); - Assert.assertEquals("/content/site/source-page", resolved.getPath()); - } - - @Test - public void testResolveInvalidVersionHistoryPathWithoutVersionIdReturnsSameResource() { - Resource resource = context.create().resource("/tmp/versionhistory/hashonly"); - Resource resolved = VersionHistoryResourceResolver.resolveSourceResource(resource); - Assert.assertEquals("/tmp/versionhistory/hashonly", resolved.getPath()); - } - - @Test - public void testResolveContentRelativePathWithoutCandidateReturnsSameResource() { - Resource resource = context.create().resource("/tmp/versionhistory/hash/version/content/unknown"); - Resource resolved = VersionHistoryResourceResolver.resolveSourceResource(resource); - Assert.assertEquals("/tmp/versionhistory/hash/version/content/unknown", resolved.getPath()); - } -} diff --git a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryUtilsTest.java b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryUtilsTest.java new file mode 100644 index 0000000000..c007e86e5f --- /dev/null +++ b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryUtilsTest.java @@ -0,0 +1,113 @@ +/*~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + ~ Copyright 2026 Adobe + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ +package com.adobe.cq.commerce.core.components.internal.utils; + +import java.lang.reflect.Method; + +import org.apache.sling.api.resource.Resource; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; + +import com.adobe.cq.commerce.core.testing.TestContext; +import io.wcm.testing.mock.aem.junit.AemContext; + +public class VersionHistoryUtilsTest { + + private static final String VERSION_HISTORY_ROOT = "/tmp/versionhistory"; + + @Rule + public final AemContext context = TestContext.newAemContext(); + + @Test + public void testResolveNullResource() { + Assert.assertNull(VersionHistoryUtils.resolveSourceResource(null)); + } + + @Test + public void testResolveNonVersionHistoryResourceReturnsSameResource() { + Resource resource = context.create().resource("/content/site/page"); + Resource resolved = VersionHistoryUtils.resolveSourceResource(resource); + Assert.assertEquals("/content/site/page", resolved.getPath()); + } + + @Test + public void testResolveUsingRelativePathCandidate() { + context.create().page("/content/site/page"); + Resource resource = context.create().resource(VERSION_HISTORY_ROOT + "/hash/version/site/page"); + Resource resolved = VersionHistoryUtils.resolveSourceResource(resource); + Assert.assertEquals("/content/site/page", resolved.getPath()); + } + + @Test + public void testResolveUsingRelativeChildPathReturnsSameResource() { + context.create().page("/content/site/page"); + String childPath = VERSION_HISTORY_ROOT + "/hash/version/site/page/child"; + Resource resource = context.create().resource(childPath); + Resource resolved = VersionHistoryUtils.resolveSourceResource(resource); + Assert.assertEquals(childPath, resolved.getPath()); + } + + @Test + public void testResolveInvalidVersionHistoryPathReturnsSameResource() { + Resource resource = context.create().resource(VERSION_HISTORY_ROOT + "/hash/version"); + Resource resourceWithTrailingSlash = context.create().resource(VERSION_HISTORY_ROOT + "/hash/version/"); + + Resource resolved = VersionHistoryUtils.resolveSourceResource(resource); + Resource resolvedWithTrailingSlash = VersionHistoryUtils.resolveSourceResource(resourceWithTrailingSlash); + + Assert.assertEquals(VERSION_HISTORY_ROOT + "/hash/version", resolved.getPath()); + Assert.assertEquals(VERSION_HISTORY_ROOT + "/hash/version", resolvedWithTrailingSlash.getPath()); + } + + @Test + public void testResolveWithoutAnyCandidateReturnsSameResource() { + Resource resource = context.create().resource(VERSION_HISTORY_ROOT + "/hash/version/site/page"); + Resource resolved = VersionHistoryUtils.resolveSourceResource(resource); + Assert.assertEquals(VERSION_HISTORY_ROOT + "/hash/version/site/page", resolved.getPath()); + } + + @Test + public void testResolveInvalidVersionHistoryPathWithoutVersionIdReturnsSameResource() { + Resource resource = context.create().resource(VERSION_HISTORY_ROOT + "/hashonly"); + Resource resolved = VersionHistoryUtils.resolveSourceResource(resource); + Assert.assertEquals(VERSION_HISTORY_ROOT + "/hashonly", resolved.getPath()); + } + + @Test + public void testResolveUnknownPathWithoutCandidateReturnsSameResource() { + Resource resource = context.create().resource(VERSION_HISTORY_ROOT + "/hash/version/unknown"); + Resource resolved = VersionHistoryUtils.resolveSourceResource(resource); + Assert.assertEquals(VERSION_HISTORY_ROOT + "/hash/version/unknown", resolved.getPath()); + } + + @Test + public void testIsVersionHistoryResource() { + Resource versionResource = context.create().resource(VERSION_HISTORY_ROOT + "/hash/version/site/page"); + Resource contentResource = context.create().resource("/content/site/page"); + + Assert.assertTrue(VersionHistoryUtils.isVersionHistoryResource(versionResource)); + Assert.assertFalse(VersionHistoryUtils.isVersionHistoryResource(contentResource)); + Assert.assertFalse(VersionHistoryUtils.isVersionHistoryResource(null)); + } + + @Test + public void testGetSourcePagePathReturnsNullForVersionHistoryRoot() throws Exception { + Method method = VersionHistoryUtils.class.getDeclaredMethod("getSourcePagePath", String.class); + method.setAccessible(true); + Assert.assertNull(method.invoke(null, VERSION_HISTORY_ROOT + "/")); + } +} diff --git a/it/http/src/test/java/com/adobe/cq/commerce/it/http/VersionHistoryPreviewIT.java b/it/http/src/test/java/com/adobe/cq/commerce/it/http/VersionHistoryPreviewIT.java index 3276d1f13f..46eaf7b2c6 100644 --- a/it/http/src/test/java/com/adobe/cq/commerce/it/http/VersionHistoryPreviewIT.java +++ b/it/http/src/test/java/com/adobe/cq/commerce/it/http/VersionHistoryPreviewIT.java @@ -15,10 +15,12 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ package com.adobe.cq.commerce.it.http; -import org.apache.http.HttpEntity; +import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.sling.testing.clients.ClientException; import org.apache.sling.testing.clients.SlingHttpResponse; import org.apache.sling.testing.clients.util.FormEntityBuilder; +import org.apache.sling.testing.clients.util.JsonUtils; +import org.codehaus.jackson.JsonNode; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.select.Elements; @@ -31,34 +33,31 @@ public class VersionHistoryPreviewIT extends CommerceTestBase { - private static final String SOURCE_PRODUCT_TEASER_PAGE = COMMERCE_LIBRARY_PATH + "/productteaser"; - private static final String VERSION_HISTORY_ROOT_BASE = "/tmp/versionhistory/cif-it-hash"; + private static final String VERSION_HISTORY_ROOT = "/tmp/versionhistory/"; private static final String VERSION_HISTORY_PAGE_SUFFIX = "/content/core-components-examples/library/commerce/productteaser"; + private static final String SOURCE_PRODUCT_TEASER_PAGE = VERSION_HISTORY_PAGE_SUFFIX; + private static final String VERSION_HISTORY_SERVLET = "/mnt/overlay/wcm/core/content/sites/versionhistory/_jcr_content.txt"; private static final String PRODUCT_TEASER_SELECTOR = CMP_EXAMPLES_DEMO_SELECTOR + " .productteaser .item__name > span"; - private String versionHistoryRoot; + private static final int VERSION_POLL_ATTEMPTS = 30; + private static final long VERSION_POLL_DELAY_MS = 1000L; private String versionHistoryPagePath; + private String versionHistoryVersionRoot; @Before - public void setup() throws ClientException { - assertTrue("Source page missing: " + SOURCE_PRODUCT_TEASER_PAGE, adminAuthor.exists(SOURCE_PRODUCT_TEASER_PAGE)); - versionHistoryRoot = VERSION_HISTORY_ROOT_BASE + "/cif-it-version-" + System.currentTimeMillis(); - String versionHistoryParent = versionHistoryRoot + "/content/core-components-examples/library/commerce"; - String versionHistoryPageNode = versionHistoryRoot + VERSION_HISTORY_PAGE_SUFFIX; - versionHistoryPagePath = versionHistoryPageNode + ".html"; - - adminAuthor.createNodeRecursive(versionHistoryParent, "sling:Folder"); - HttpEntity copyEntity = FormEntityBuilder.create() - .addParameter(":operation", "copy") - .addParameter(":dest", versionHistoryPageNode) - .build(); - adminAuthor.doPost(SOURCE_PRODUCT_TEASER_PAGE, copyEntity, 200, 201); - assertTrue("Version history preview page was not created", adminAuthor.exists(versionHistoryPageNode)); + public void setup() throws Exception { + String label = "it-version-" + System.currentTimeMillis(); + adminAuthor.createVersion(SOURCE_PRODUCT_TEASER_PAGE, "IT version", label); + String versionId = waitForVersionId(SOURCE_PRODUCT_TEASER_PAGE, label); + versionHistoryPagePath = determinePreviewUrl(versionId); + versionHistoryVersionRoot = getVersionHistoryVersionRoot(versionHistoryPagePath); + assertTrue("Version preview URL should be a version history path", versionHistoryPagePath.contains(VERSION_HISTORY_ROOT)); + assertTrue("Version preview URL should end with .html", versionHistoryPagePath.endsWith(".html")); } @After public void cleanup() throws ClientException { - if (versionHistoryRoot != null && adminAuthor.exists(versionHistoryRoot)) { - adminAuthor.deletePath(versionHistoryRoot); + if (versionHistoryVersionRoot != null && adminAuthor.exists(versionHistoryVersionRoot)) { + adminAuthor.deletePath(versionHistoryVersionRoot); } } @@ -69,4 +68,60 @@ public void testVersionHistoryPathRendersProductTeaser() throws ClientException Elements elements = doc.select(PRODUCT_TEASER_SELECTOR); assertEquals("Summit Watch", elements.first().html()); } + + private String determinePreviewUrl(String versionId) throws ClientException { + UrlEncodedFormEntity formEntity = FormEntityBuilder.create() + .addParameter("wcmmode", "disabled") + .addParameter("versionId", versionId) + .build(); + SlingHttpResponse response = adminAuthor.doPost(VERSION_HISTORY_SERVLET, formEntity, 200); + return response.getContent().trim() + ".html"; + } + + private String waitForVersionId(String pagePath, String label) throws Exception { + String versionId = null; + for (int attempt = 0; attempt < VERSION_POLL_ATTEMPTS; attempt++) { + versionId = getVersionIdByLabel(pagePath, label); + if (versionId != null) { + return versionId; + } + Thread.sleep(VERSION_POLL_DELAY_MS); + } + throw new AssertionError("Version with label '" + label + "' was not created for " + pagePath); + } + + private String getVersionIdByLabel(String pagePath, String label) throws ClientException { + SlingHttpResponse response = adminAuthor.doGet("/bin/wcm/versions.json?path=" + pagePath + "&showChildren=false", 200); + JsonNode versions = JsonUtils.getJsonNodeFromString(response.getContent()).path("versions"); + if (versions == null || versions.size() == 0) { + return null; + } + for (int i = 0; i < versions.size(); i++) { + JsonNode version = versions.get(i); + if (label.equals(version.path("label").getTextValue())) { + return version.path("id").getTextValue(); + } + } + return null; + } + + private String getVersionHistoryVersionRoot(String previewPagePath) { + int start = previewPagePath.indexOf(VERSION_HISTORY_ROOT); + if (start < 0) { + return null; + } + + int hashStart = start + VERSION_HISTORY_ROOT.length(); + int hashEnd = previewPagePath.indexOf('/', hashStart); + if (hashEnd < 0) { + return null; + } + + int versionEnd = previewPagePath.indexOf('/', hashEnd + 1); + if (versionEnd < 0) { + return null; + } + + return previewPagePath.substring(0, versionEnd); + } } From 94468cc614b96ca8b4d2176d884bc21af35b3e10 Mon Sep 17 00:00:00 2001 From: Alwin Joseph Date: Tue, 3 Mar 2026 18:19:16 +0530 Subject: [PATCH 7/9] SITES-41041: Removes unwanted test & improves the logic --- .../StoreConfigExporterImpl.java | 16 ++++++++-------- .../client/MagentoGraphqlClientImplTest.java | 13 ------------- .../StoreConfigExporterImplTest.java | 12 ------------ 3 files changed, 8 insertions(+), 33 deletions(-) diff --git a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/models/v1/storeconfigexporter/StoreConfigExporterImpl.java b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/models/v1/storeconfigexporter/StoreConfigExporterImpl.java index a41186c0d0..6c31cd8ae2 100644 --- a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/models/v1/storeconfigexporter/StoreConfigExporterImpl.java +++ b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/models/v1/storeconfigexporter/StoreConfigExporterImpl.java @@ -126,9 +126,12 @@ public String getStoreRootUrl() { if (storeRootPage == null) { storeRootPage = siteStructure.getLandingPage(); if (storeRootPage == null) { + Resource currentPageResource = currentPage != null ? currentPage.adaptTo(Resource.class) : null; // Timeline preview pages live under /tmp/versionhistory and may not have a resolvable landing page. // In that case, resolve the source /content page and reuse its site structure. - storeRootPage = getStoreRootPageFromVersionHistorySource(); + if (VersionHistoryUtils.isVersionHistoryResource(currentPageResource)) { + storeRootPage = getStoreRootPageFromVersionHistorySource(currentPageResource); + } } } @@ -166,14 +169,11 @@ public String getLanguage() { /** * Resolves the landing page from the source /content page when the current page is rendered from * AEM version history preview under /tmp/versionhistory. + * + * @param versionHistoryPageResource the version history page resource under /tmp/versionhistory */ - private Page getStoreRootPageFromVersionHistorySource() { - Resource currentPageResource = currentPage != null ? currentPage.adaptTo(Resource.class) : null; - if (!VersionHistoryUtils.isVersionHistoryResource(currentPageResource)) { - return null; - } - - Resource sourcePageResource = VersionHistoryUtils.resolveSourceResource(currentPageResource); + private Page getStoreRootPageFromVersionHistorySource(Resource versionHistoryPageResource) { + Resource sourcePageResource = VersionHistoryUtils.resolveSourceResource(versionHistoryPageResource); Page sourcePage = sourcePageResource != null ? sourcePageResource.adaptTo(Page.class) : null; if (sourcePage == null) { return null; diff --git a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/client/MagentoGraphqlClientImplTest.java b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/client/MagentoGraphqlClientImplTest.java index b1f789dfd8..e635f4ae19 100644 --- a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/client/MagentoGraphqlClientImplTest.java +++ b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/client/MagentoGraphqlClientImplTest.java @@ -15,7 +15,6 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ package com.adobe.cq.commerce.core.components.internal.client; -import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -290,18 +289,6 @@ public void testError() { new MagentoGraphqlClientImpl(resource, null, null); } - @Test - public void testGetPageFromResourceResolvesContainingPage() throws Exception { - Method method = MagentoGraphqlClientImpl.class.getDeclaredMethod("getPageFromResource", Resource.class); - method.setAccessible(true); - - Resource contentResource = context.resourceResolver().getResource("/content/pageB/pageC/jcr:content"); - Page page = (Page) method.invoke(null, contentResource); - - assertNotNull(page); - assertEquals("/content/pageB/pageC", page.getPath()); - } - @Test public void testPreviewVersionHeaderOnLaunchPage() { context.registerAdapter(Resource.class, Launch.class, (Function) resource -> new MockLaunch(resource)); diff --git a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/models/v1/storeconfigexporter/StoreConfigExporterImplTest.java b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/models/v1/storeconfigexporter/StoreConfigExporterImplTest.java index c35f21218e..122cc50108 100644 --- a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/models/v1/storeconfigexporter/StoreConfigExporterImplTest.java +++ b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/models/v1/storeconfigexporter/StoreConfigExporterImplTest.java @@ -15,7 +15,6 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~*/ package com.adobe.cq.commerce.core.components.internal.models.v1.storeconfigexporter; -import java.lang.reflect.Method; import java.util.Collections; import java.util.Map; @@ -204,17 +203,6 @@ public void testGetStoreRootUrlForVersionHistoryPageWithoutSourceLandingPage() { Assert.assertNull(storeConfigExporter.getStoreRootUrl()); } - @Test - public void testVersionHistorySourceResolverReturnsNullForNonVersionPage() throws Exception { - setupWithPage("/content/pageD", HttpMethod.POST); - StoreConfigExporterImpl storeConfigExporter = context.request().adaptTo(StoreConfigExporterImpl.class); - assertNotNull(storeConfigExporter); - - Method method = StoreConfigExporterImpl.class.getDeclaredMethod("getStoreRootPageFromVersionHistorySource"); - method.setAccessible(true); - Assert.assertNull(method.invoke(storeConfigExporter)); - } - @Test public void testGetStoreRootUrlForVersionHistoryPageWithNonPageSource() { context.create().page(VERSION_HISTORY_PAGE_WITH_NON_PAGE_SOURCE); From 9fa268189baf1c2854f38960e2a172ad75b3e791 Mon Sep 17 00:00:00 2001 From: Alwin Joseph Date: Thu, 5 Mar 2026 12:09:40 +0530 Subject: [PATCH 8/9] SITES-41041: Rename version-history helper to isVersionPreviewResource and update usages --- .../internal/client/MagentoGraphqlClientImpl.java | 2 +- .../v1/storeconfigexporter/StoreConfigExporterImpl.java | 2 +- .../services/ComponentsConfigurationAdapterFactory.java | 2 +- .../core/components/internal/utils/VersionHistoryUtils.java | 4 ++-- .../components/internal/utils/VersionHistoryUtilsTest.java | 6 +++--- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/client/MagentoGraphqlClientImpl.java b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/client/MagentoGraphqlClientImpl.java index ddf711e5e7..8597494216 100644 --- a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/client/MagentoGraphqlClientImpl.java +++ b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/client/MagentoGraphqlClientImpl.java @@ -141,7 +141,7 @@ private void initModel(Resource resource, Page page, SlingHttpServletRequest req configurationResource = Objects.requireNonNull(page.adaptTo(Resource.class), "page is not a Resource"); // If the page is rendered from AEM version history preview, resolve back to the source resource. - if (VersionHistoryUtils.isVersionHistoryResource(configurationResource)) { + if (VersionHistoryUtils.isVersionPreviewResource(configurationResource)) { configurationResource = VersionHistoryUtils.resolveSourceResource(configurationResource); } diff --git a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/models/v1/storeconfigexporter/StoreConfigExporterImpl.java b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/models/v1/storeconfigexporter/StoreConfigExporterImpl.java index 6c31cd8ae2..496d0175c1 100644 --- a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/models/v1/storeconfigexporter/StoreConfigExporterImpl.java +++ b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/models/v1/storeconfigexporter/StoreConfigExporterImpl.java @@ -129,7 +129,7 @@ public String getStoreRootUrl() { Resource currentPageResource = currentPage != null ? currentPage.adaptTo(Resource.class) : null; // Timeline preview pages live under /tmp/versionhistory and may not have a resolvable landing page. // In that case, resolve the source /content page and reuse its site structure. - if (VersionHistoryUtils.isVersionHistoryResource(currentPageResource)) { + if (VersionHistoryUtils.isVersionPreviewResource(currentPageResource)) { storeRootPage = getStoreRootPageFromVersionHistorySource(currentPageResource); } } diff --git a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/services/ComponentsConfigurationAdapterFactory.java b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/services/ComponentsConfigurationAdapterFactory.java index 9ee4201d41..0304808d1e 100644 --- a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/services/ComponentsConfigurationAdapterFactory.java +++ b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/services/ComponentsConfigurationAdapterFactory.java @@ -73,7 +73,7 @@ public AdapterType getAdapter(Object adaptable, Class } // If the adapted resource comes from version history preview, resolve it to the source content path. - if (VersionHistoryUtils.isVersionHistoryResource(resource)) { + if (VersionHistoryUtils.isVersionPreviewResource(resource)) { resource = VersionHistoryUtils.resolveSourceResource(resource); } diff --git a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryUtils.java b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryUtils.java index b222c8ef62..afb6921aac 100644 --- a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryUtils.java +++ b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryUtils.java @@ -31,7 +31,7 @@ private VersionHistoryUtils() {} /** * Returns {@code true} when the resource is a synthetic version preview resource under /tmp/versionhistory. */ - public static boolean isVersionHistoryResource(Resource resource) { + public static boolean isVersionPreviewResource(Resource resource) { return resource != null && StringUtils.startsWith(resource.getPath(), VERSION_HISTORY_ROOT); } @@ -39,7 +39,7 @@ public static boolean isVersionHistoryResource(Resource resource) { * Resolves a version preview resource back to its source page/resource so configuration lookups can work. */ public static Resource resolveSourceResource(Resource resource) { - if (!isVersionHistoryResource(resource)) { + if (!isVersionPreviewResource(resource)) { return resource; } diff --git a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryUtilsTest.java b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryUtilsTest.java index c007e86e5f..890387ad0b 100644 --- a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryUtilsTest.java +++ b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryUtilsTest.java @@ -99,9 +99,9 @@ public void testIsVersionHistoryResource() { Resource versionResource = context.create().resource(VERSION_HISTORY_ROOT + "/hash/version/site/page"); Resource contentResource = context.create().resource("/content/site/page"); - Assert.assertTrue(VersionHistoryUtils.isVersionHistoryResource(versionResource)); - Assert.assertFalse(VersionHistoryUtils.isVersionHistoryResource(contentResource)); - Assert.assertFalse(VersionHistoryUtils.isVersionHistoryResource(null)); + Assert.assertTrue(VersionHistoryUtils.isVersionPreviewResource(versionResource)); + Assert.assertFalse(VersionHistoryUtils.isVersionPreviewResource(contentResource)); + Assert.assertFalse(VersionHistoryUtils.isVersionPreviewResource(null)); } @Test From 4c457332a7e38707122d0854361c10b400cdddb4 Mon Sep 17 00:00:00 2001 From: Alwin Joseph Date: Thu, 12 Mar 2026 18:09:49 +0530 Subject: [PATCH 9/9] SITES-41041: Resolve the breadcrumb issue in preview version --- .../models/v1/breadcrumb/BreadcrumbImpl.java | 150 +++++++++++++++--- .../v1/breadcrumb/NavigationItemImpl.java | 12 ++ .../internal/utils/VersionHistoryUtils.java | 34 +++- .../v1/breadcrumb/BreadcrumbImplTest.java | 82 ++++++++++ .../utils/VersionHistoryUtilsTest.java | 7 + 5 files changed, 262 insertions(+), 23 deletions(-) diff --git a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/models/v1/breadcrumb/BreadcrumbImpl.java b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/models/v1/breadcrumb/BreadcrumbImpl.java index 57c60c8636..2f2c407a7b 100644 --- a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/models/v1/breadcrumb/BreadcrumbImpl.java +++ b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/models/v1/breadcrumb/BreadcrumbImpl.java @@ -42,6 +42,7 @@ import com.adobe.cq.commerce.core.components.internal.datalayer.DataLayerComponent; import com.adobe.cq.commerce.core.components.internal.services.site.SiteStructureImpl; import com.adobe.cq.commerce.core.components.internal.services.urlformats.UrlFormatBase; +import com.adobe.cq.commerce.core.components.internal.utils.VersionHistoryUtils; import com.adobe.cq.commerce.core.components.models.breadcrumb.Breadcrumb; import com.adobe.cq.commerce.core.components.models.common.SiteStructure; import com.adobe.cq.commerce.core.components.models.navigation.Navigation; @@ -106,17 +107,21 @@ void initModel() { @Override public Collection getItems() { - // Useful for the template editor - if (!currentPage.getPath().startsWith("/content")) { + BreadcrumbContext context = getBreadcrumbContext(); + if (context == null) { return Collections.emptyList(); } if (items == null) { items = new ArrayList<>(); if (magentoGraphqlClient != null) { - Collection pageItems = breadcrumb.getItems(); - for (NavigationItem item : pageItems) { - if (!populateItems(item)) { + for (NavigationItem item : getPageItems(context)) { + Page page = context.versionPreview ? item.getPage() : resolveSourcePage(item.getPage()); + if (page == null) { + continue; + } + + if (!populateItems(item, page, context)) { break; } } @@ -125,6 +130,56 @@ public Collection getItems() { return Collections.unmodifiableList(items); } + private BreadcrumbContext getBreadcrumbContext() { + Resource currentPageResource = currentPage != null ? currentPage.adaptTo(Resource.class) : null; + boolean versionPreview = VersionHistoryUtils.isVersionPreviewResource(currentPageResource); + Page breadcrumbPage = versionPreview ? resolveSourcePage(currentPage) : currentPage; + + // Useful for the template editor + if (breadcrumbPage == null || !breadcrumbPage.getPath().startsWith("/content")) { + return null; + } + + SiteStructure effectiveSiteStructure = versionPreview ? breadcrumbPage.adaptTo(SiteStructure.class) : siteStructure; + if (effectiveSiteStructure == null) { + effectiveSiteStructure = siteStructure; + } + + return new BreadcrumbContext(currentPageResource, breadcrumbPage, effectiveSiteStructure, versionPreview); + } + + private Collection getPageItems(BreadcrumbContext context) { + return context.versionPreview ? getVersionPreviewItems(context) : breadcrumb.getItems(); + } + + private Collection getVersionPreviewItems(BreadcrumbContext context) { + Page sourceCurrentPage = context.breadcrumbPage; + int startLevel = properties.get(com.adobe.cq.wcm.core.components.models.Breadcrumb.PN_START_LEVEL, + currentStyle.get(com.adobe.cq.wcm.core.components.models.Breadcrumb.PN_START_LEVEL, 2)); + Page firstIncludedSourcePage = sourceCurrentPage.getAbsoluteParent(startLevel); + if (firstIncludedSourcePage == null) { + return Collections.emptyList(); + } + + List pages = new ArrayList<>(); + for (Page page = sourceCurrentPage; page != null; page = page.getParent()) { + pages.add(page); + if (StringUtils.equals(page.getPath(), firstIncludedSourcePage.getPath())) { + break; + } + } + + Collections.reverse(pages); + return pages.stream() + .map(page -> newNavigationItem( + page, + getPageTitle(page), + toContextualUrl(context, page.getPath() + ".html"), + StringUtils.equals(page.getPath(), sourceCurrentPage.getPath()), + currentPage.getContentResource())) + .collect(Collectors.toList()); + } + /** * Populates the breadcrumb items with the given item. If the item * a) is a content page it is kept as is @@ -134,21 +189,13 @@ public Collection getItems() { * @param item * @return true if more original items should be considered for the breadcrumb, otherwise false */ - private boolean populateItems(NavigationItem item) { - Page page = item.getPage(); + private boolean populateItems(NavigationItem item, Page page, BreadcrumbContext context) { Resource contentResource; - // We build the breadcrumb based on the production version of the page structure - if (page != null && LaunchUtils.isLaunchBasedPath(page.getPath())) { - PageManager pageManager = page.getPageManager(); - contentResource = LaunchUtils.getTargetResource(page.getContentResource(), null); - page = pageManager.getContainingPage(contentResource); - } - contentResource = page != null ? page.getContentResource() : null; // If we encounter the catalog page and it's configured to show the main categories, we skip that page - if (siteStructure.isCatalogPage(page)) { + if (context.siteStructure.isCatalogPage(page)) { if (contentResource.getValueMap().get(Navigation.PN_SHOW_MAIN_CATEGORIES, Boolean.TRUE)) { return true; } @@ -160,7 +207,7 @@ private boolean populateItems(NavigationItem item) { List categoriesBreadcrumbs = null; ProductInterface product = null; - if (siteStructure.isProductPage(page)) { + if (context.siteStructure.isProductPage(page)) { categoriesBreadcrumbs = fetchProductBreadcrumbs(); product = retriever.fetchProduct(); isProductPage = true; @@ -168,12 +215,16 @@ private boolean populateItems(NavigationItem item) { if (product == null) { return false; } - } else if (siteStructure.isCategoryPage(page)) { + } else if (context.siteStructure.isCategoryPage(page)) { categoriesBreadcrumbs = fetchCategoryBreadcrumbs(); isCategoryPage = true; } else { // we reached a content page - items.add(item); + String url = toContextualUrl(context, item.getURL()); + String title = context.versionPreview ? getPageTitle(page) : item.getTitle(); + items.add(url.equals(item.getURL()) && StringUtils.equals(title, item.getTitle()) ? item + : newNavigationItem(title, url, + item.isActive())); return true; } @@ -181,7 +232,7 @@ private boolean populateItems(NavigationItem item) { return false; } - SiteStructure.Entry siteStructureEntry = siteStructure.getEntry(page); + SiteStructure.Entry siteStructureEntry = context.siteStructure.getEntry(page); // A product can be in multiple categories so we select the "primary" category categoriesBreadcrumbs.sort(Comparator.comparing(CategoryInterface::getUrlPath).reversed()); @@ -210,7 +261,7 @@ && shouldIncludeInBreadcrumb(categoryBreadcrumb.getUrlPath(), siteStructureEntry // We finally add the product if it's a product page if (isProductPage) { ProductUrlFormat.Params params = new ProductUrlFormat.Params(product); - String url = urlProvider.toProductUrl(request, currentPage, params); + String url = toContextualUrl(context, urlProvider.toProductUrl(request, currentPage, params)); NavigationItemImpl productItem = newNavigationItem(product.getName(), url, true); items.add(productItem); } @@ -218,6 +269,33 @@ && shouldIncludeInBreadcrumb(categoryBreadcrumb.getUrlPath(), siteStructureEntry return false; } + private Page resolveSourcePage(Page page) { + if (page == null) { + return null; + } + + if (LaunchUtils.isLaunchBasedPath(page.getPath())) { + PageManager pageManager = page.getPageManager(); + Resource launchSourceResource = LaunchUtils.getTargetResource(page.getContentResource(), null); + Page launchSourcePage = pageManager != null ? pageManager.getContainingPage(launchSourceResource) : null; + if (launchSourcePage != null) { + return launchSourcePage; + } + } + + Resource pageResource = page.adaptTo(Resource.class); + if (VersionHistoryUtils.isVersionPreviewResource(pageResource)) { + PageManager pageManager = page.getPageManager(); + Resource sourceResource = VersionHistoryUtils.resolveSourceResource(pageResource); + Page sourcePage = pageManager != null ? pageManager.getContainingPage(sourceResource) : null; + if (sourcePage != null) { + return sourcePage; + } + } + + return page; + } + private boolean shouldIncludeInBreadcrumb(String breadcrumbUrlPath, Page catalogPage) { ValueMap properties = catalogPage != null ? catalogPage.getProperties() : ValueMap.EMPTY; boolean showMainCategories = properties.get(Navigation.PN_SHOW_MAIN_CATEGORIES, Boolean.TRUE); @@ -255,7 +333,9 @@ private void addCategoryItem(ID uid, String urlKey, String urlPath, String name, params.setUid(uid.toString()); params.setUrlKey(urlKey); params.setUrlPath(urlPath); - String url = urlProvider.toCategoryUrl(request, currentPage, params); + String url = VersionHistoryUtils.toVersionPreviewUrl( + currentPage != null ? currentPage.adaptTo(Resource.class) : null, + urlProvider.toCategoryUrl(request, currentPage, params)); // if there is no category page, the url will contain the placeholder {{page}} if (!url.contains(PAGE_PLACEHOLDER)) { NavigationItemImpl categoryItem = newNavigationItem(name, url, isActive); @@ -279,6 +359,34 @@ private NavigationItemImpl newNavigationItem(String name, String url, boolean is return new NavigationItemImpl(name, url, isActive, this.getId(), currentPage.getContentResource()); } + private NavigationItemImpl newNavigationItem(Page page, String name, String url, boolean isActive, Resource resource) { + return new NavigationItemImpl(page, name, url, isActive, this.getId(), resource); + } + + private String toContextualUrl(BreadcrumbContext context, String url) { + return VersionHistoryUtils.toVersionPreviewUrl(context.currentPageResource, url); + } + + private String getPageTitle(Page page) { + return StringUtils.defaultIfBlank( + page.getNavigationTitle(), + StringUtils.defaultIfBlank(page.getPageTitle(), StringUtils.defaultIfBlank(page.getTitle(), page.getName()))); + } + + private static final class BreadcrumbContext { + private final Resource currentPageResource; + private final Page breadcrumbPage; + private final SiteStructure siteStructure; + private final boolean versionPreview; + + private BreadcrumbContext(Resource currentPageResource, Page breadcrumbPage, SiteStructure siteStructure, boolean versionPreview) { + this.currentPageResource = currentPageResource; + this.breadcrumbPage = breadcrumbPage; + this.siteStructure = siteStructure; + this.versionPreview = versionPreview; + } + } + @Override public Comparator getCategoryInterfaceComparator() { return Comparator diff --git a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/models/v1/breadcrumb/NavigationItemImpl.java b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/models/v1/breadcrumb/NavigationItemImpl.java index 6ffbe781ce..17c70b46a2 100644 --- a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/models/v1/breadcrumb/NavigationItemImpl.java +++ b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/models/v1/breadcrumb/NavigationItemImpl.java @@ -19,15 +19,22 @@ import com.adobe.cq.commerce.core.components.internal.datalayer.DataLayerListItem; import com.adobe.cq.wcm.core.components.models.NavigationItem; +import com.day.cq.wcm.api.Page; public class NavigationItemImpl extends DataLayerListItem implements NavigationItem { protected String title; protected String url; protected boolean isActive; + protected Page page; public NavigationItemImpl(String title, String url, boolean isActive, String parentId, Resource resource) { + this(null, title, url, isActive, parentId, resource); + } + + public NavigationItemImpl(Page page, String title, String url, boolean isActive, String parentId, Resource resource) { super(parentId, resource); + this.page = page; this.title = title; this.url = url; this.isActive = isActive; @@ -48,6 +55,11 @@ public boolean isActive() { return isActive; } + @Override + public Page getPage() { + return page; + } + // DataLayer methods @Override diff --git a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryUtils.java b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryUtils.java index afb6921aac..ed948bfb11 100644 --- a/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryUtils.java +++ b/bundles/core/src/main/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryUtils.java @@ -57,7 +57,38 @@ public static Resource resolveSourceResource(Resource resource) { return resource; } + /** + * Keeps generated URLs inside the current /tmp/versionhistory preview tree. + */ + public static String toVersionPreviewUrl(Resource versionPreviewResource, String url) { + if (!isVersionPreviewResource(versionPreviewResource) || StringUtils.isBlank(url) || !StringUtils.startsWith(url, "/content/")) { + return url; + } + + String sourceRelativePath = getSourceRelativePath(versionPreviewResource.getPath()); + if (StringUtils.isBlank(sourceRelativePath)) { + return url; + } + + String previewPath = versionPreviewResource.getPath(); + if (!StringUtils.endsWith(previewPath, sourceRelativePath)) { + return url; + } + + String previewRoot = StringUtils.substringBeforeLast(previewPath, sourceRelativePath); + return previewRoot + StringUtils.removeStart(url, "/content/"); + } + private static String getSourcePagePath(String path) { + String relativePath = getSourceRelativePath(path); + if (StringUtils.isBlank(relativePath)) { + return null; + } + + return "/content/" + StringUtils.removeEnd(relativePath, "/"); + } + + private static String getSourceRelativePath(String path) { String suffix = StringUtils.substringAfter(path, VERSION_HISTORY_ROOT); if (StringUtils.isBlank(suffix)) { return null; @@ -68,7 +99,6 @@ private static String getSourcePagePath(String path) { return null; } - String relativePath = suffix.substring(secondSlash + 1); - return "/content/" + StringUtils.removeEnd(relativePath, "/"); + return StringUtils.removeEnd(suffix.substring(secondSlash + 1), "/"); } } diff --git a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/models/v1/breadcrumb/BreadcrumbImplTest.java b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/models/v1/breadcrumb/BreadcrumbImplTest.java index 0989baecba..38279daa99 100644 --- a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/models/v1/breadcrumb/BreadcrumbImplTest.java +++ b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/models/v1/breadcrumb/BreadcrumbImplTest.java @@ -76,6 +76,7 @@ public class BreadcrumbImplTest { "my-store", "enableUIDSupport", "true")); private static final ComponentsConfiguration MOCK_CONFIGURATION_OBJECT = new ComponentsConfiguration(MOCK_CONFIGURATION); + private static final String VERSION_PREVIEW_ROOT = "/tmp/versionhistory/hash/version"; @Rule public final AemContext context = buildAemContext("/context/jcr-content-breadcrumb.json") @@ -97,6 +98,7 @@ public class BreadcrumbImplTest { @Before public void setUp() throws Exception { + context.load().json("/context/jcr-content-breadcrumb.json", VERSION_PREVIEW_ROOT); httpClient = mock(CloseableHttpClient.class); context.registerService(HttpClientBuilderFactory.class, new MockHttpClientBuilderFactory(httpClient)); @@ -311,6 +313,71 @@ public void testCategoryPage() throws Exception { assertThat(topsCategory.isActive()).isTrue(); } + @Test + public void testCategoryPageOnVersionPreview() throws Exception { + Utils.setupHttpResponse("graphql/magento-graphql-category-breadcrumb-result.json", httpClient, HttpStatus.SC_OK, + "{categoryList(filters:{url_path"); + + String versionPreviewPage = VERSION_PREVIEW_ROOT + "/venia/us/en/products/category-page"; + prepareModel(versionPreviewPage); + + MockRequestPathInfo requestPathInfo = (MockRequestPathInfo) context.request().getRequestPathInfo(); + requestPathInfo.setSuffix("/men.html"); + + breadcrumbModel = context.request().adaptTo(BreadcrumbImpl.class); + List items = (List) breadcrumbModel.getItems(); + assertThat(items.stream().map(NavigationItem::getTitle)).containsExactly("en", "Men", "Tops"); + + NavigationItem homeItem = items.get(0); + assertThat(homeItem.getURL()).startsWith(VERSION_PREVIEW_ROOT + "/venia"); + + NavigationItem menCategory = items.get(1); + assertThat(menCategory.getURL()).isEqualTo(versionPreviewPage + ".html/men.html"); + assertThat(menCategory.isActive()).isFalse(); + + NavigationItem topsCategory = items.get(2); + assertThat(topsCategory.getURL()).isEqualTo(versionPreviewPage + ".html/men/tops-men.html"); + assertThat(topsCategory.isActive()).isTrue(); + } + + @Test + public void testVersionPreviewSkipsSyntheticAncestorItems() throws Exception { + String versionPreviewPage = VERSION_PREVIEW_ROOT + "/venia/us/en/another-page"; + String syntheticPreviewPage = VERSION_PREVIEW_ROOT + "/venia/us/en/20e4b245-cbeb-4769-b6f1-5018354508b2"; + prepareModel(versionPreviewPage); + context.resourceResolver().getResource("/content/venia/us/en/jcr:content") + .adaptTo(ModifiableValueMap.class) + .put(JcrConstants.JCR_TITLE, "Venia Demo Store - Home"); + + breadcrumbModel = context.request().adaptTo(BreadcrumbImpl.class); + + com.adobe.cq.wcm.core.components.models.Breadcrumb wrappedBreadcrumb = mock( + com.adobe.cq.wcm.core.components.models.Breadcrumb.class); + List wrappedItems = Arrays.asList( + mockNavigationItem( + "20e4b245-cbeb-4769-b6f1-5018354508b2", + syntheticPreviewPage, + syntheticPreviewPage + ".html", + false), + mockNavigationItem( + "en", + VERSION_PREVIEW_ROOT + "/venia/us/en", + "/content/venia/us/en.html", + false), + mockNavigationItem( + "another-page", + versionPreviewPage, + "/content/venia/us/en/another-page.html", + true)); + when(wrappedBreadcrumb.getItems()).thenReturn(wrappedItems); + Whitebox.setInternalState(breadcrumbModel, "breadcrumb", wrappedBreadcrumb); + + List items = (List) breadcrumbModel.getItems(); + assertThat(items.stream().map(NavigationItem::getTitle)).containsExactly("Venia Demo Store - Home", "another-page"); + assertThat(items.get(0).getURL()).isEqualTo(VERSION_PREVIEW_ROOT + "/venia/us/en.html"); + assertThat(items.get(1).getURL()).isEqualTo(versionPreviewPage + ".html"); + } + @Test public void testCategorySpecificPage() throws Exception { Utils.setupHttpResponse("graphql/magento-graphql-category-breadcrumb-result.json", httpClient, HttpStatus.SC_OK, @@ -529,4 +596,19 @@ public void testBreadcrumbContainsOnlyDescendantCategoriesOfSpecificCatalogPage( assertThat(product.getURL()).isEqualTo("/content/venia/us/en/products/product-page.html/tiberius-gym-tank.html"); assertThat(product.isActive()).isTrue(); } + + private NavigationItem mockNavigationItem(String title, String pagePath, String url, boolean active) { + NavigationItem item = mock(NavigationItem.class); + Page page = context.pageManager().getPage(pagePath); + if (page == null) { + context.create().page(pagePath); + page = context.pageManager().getPage(pagePath); + } + + when(item.getPage()).thenReturn(page); + when(item.getTitle()).thenReturn(title); + when(item.getURL()).thenReturn(url); + when(item.isActive()).thenReturn(active); + return item; + } } diff --git a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryUtilsTest.java b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryUtilsTest.java index 890387ad0b..7d037d38a1 100644 --- a/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryUtilsTest.java +++ b/bundles/core/src/test/java/com/adobe/cq/commerce/core/components/internal/utils/VersionHistoryUtilsTest.java @@ -94,6 +94,13 @@ public void testResolveUnknownPathWithoutCandidateReturnsSameResource() { Assert.assertEquals(VERSION_HISTORY_ROOT + "/hash/version/unknown", resolved.getPath()); } + @Test + public void testToVersionPreviewUrlMapsContentUrlToPreviewUrl() { + Resource resource = context.create().resource(VERSION_HISTORY_ROOT + "/hash/version/site/page"); + String previewUrl = VersionHistoryUtils.toVersionPreviewUrl(resource, "/content/site/other-page.html"); + Assert.assertEquals(VERSION_HISTORY_ROOT + "/hash/version/site/other-page.html", previewUrl); + } + @Test public void testIsVersionHistoryResource() { Resource versionResource = context.create().resource(VERSION_HISTORY_ROOT + "/hash/version/site/page");