This is an automated email from the ASF dual-hosted git repository.

joerghoh pushed a commit to branch SLING-13133
in repository 
https://gitbox.apache.org/repos/asf/sling-org-apache-sling-servlets-resolver.git

commit 486fff03c552cbb23ecc8615d932fda615796117
Author: Joerg Hoh <[email protected]>
AuthorDate: Fri Mar 6 15:43:03 2026 +0100

    add test cases
---
 .../MergingServletResourceProviderTest.java        | 243 +++++++++++++++++++++
 1 file changed, 243 insertions(+)

diff --git 
a/src/test/java/org/apache/sling/servlets/resolver/internal/resource/MergingServletResourceProviderTest.java
 
b/src/test/java/org/apache/sling/servlets/resolver/internal/resource/MergingServletResourceProviderTest.java
new file mode 100644
index 0000000..d117208
--- /dev/null
+++ 
b/src/test/java/org/apache/sling/servlets/resolver/internal/resource/MergingServletResourceProviderTest.java
@@ -0,0 +1,243 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.sling.servlets.resolver.internal.resource;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+import jakarta.servlet.GenericServlet;
+import jakarta.servlet.Servlet;
+import jakarta.servlet.ServletRequest;
+import jakarta.servlet.ServletResponse;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.SyntheticResource;
+import org.apache.sling.spi.resource.provider.ResolveContext;
+import org.apache.sling.spi.resource.provider.ResourceProvider;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.osgi.framework.ServiceReference;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+public class MergingServletResourceProviderTest {
+
+    private static final Servlet TEST_SERVLET = new GenericServlet() {
+        private static final long serialVersionUID = 1L;
+
+        @Override
+        public void service(ServletRequest req, ServletResponse res) {}
+    };
+
+    private interface Marker {}
+
+    /**
+     * Validates behavior when there is no parent provider and only indexed 
servlet paths are available, ensuring that
+     * synthetic intermediate resources are still exposed as children so 
traversal to deeper servlet paths remains
+     * possible.
+     */
+    @Test
+    public void 
testListChildrenWithNoParentProviderCreatesSyntheticIntermediate() {
+        final ResourceResolver resolver = Mockito.mock(ResourceResolver.class);
+        final ResolveContext<Object> ctx = mockContext(resolver, null, null);
+        final MergingServletResourceProvider mergingProvider = new 
MergingServletResourceProvider();
+        final Resource parent = new SyntheticResource(resolver, "/apps", 
"type");
+
+        // Register a deep path so /apps/sling becomes a synthetic child.
+        addProvider(mergingProvider, "/apps/sling/sample/GET.servlet");
+
+        final List<Resource> children = 
toList(mergingProvider.listChildren(ctx, parent));
+
+        assertEquals(1, children.size());
+        assertEquals("/apps/sling", children.get(0).getPath());
+        assertTrue(children.get(0) instanceof SyntheticResource);
+    }
+
+    /**
+     * Verifies merge semantics when parent children and servlet-backed 
children overlap, asserting that existing parent
+     * entries are overridden in place, wrapped-resource adaptation still 
works, and additional indexed branches are
+     * represented as synthetic children.
+     */
+    @Test
+    public void testListChildrenMergesParentAndOverlaysProviderResource() {
+        final ResourceResolver resolver = Mockito.mock(ResourceResolver.class);
+
+        final Marker marker = new Marker() {};
+        final Resource parentA = mockParentChild("/apps/parent/a", marker);
+        final Resource parentB = mockParentChild("/apps/parent/b", marker);
+        final Resource parent = new SyntheticResource(resolver, 
"/apps/parent", "type");
+
+        @SuppressWarnings("unchecked")
+        final ResourceProvider<Object> parentProvider = 
Mockito.mock(ResourceProvider.class);
+        Mockito.when(parentProvider.listChildren(Mockito.any(), 
Mockito.eq(parent)))
+                .thenReturn(List.of(parentA, parentB).iterator());
+
+        final ResolveContext<Object> parentCtx = 
Mockito.mock(ResolveContext.class);
+        final ResolveContext<Object> ctx = mockContext(resolver, 
parentProvider, parentCtx);
+
+        final MergingServletResourceProvider mergingProvider = new 
MergingServletResourceProvider();
+        addProvider(mergingProvider, "/apps/parent/b", "/apps/parent/c/deep");
+
+        final List<Resource> children = 
toList(mergingProvider.listChildren(ctx, parent));
+
+        assertEquals(3, children.size());
+        assertEquals("/apps/parent/a", children.get(0).getPath());
+        assertEquals("/apps/parent/b", children.get(1).getPath());
+        assertEquals("/apps/parent/c", children.get(2).getPath());
+
+        assertTrue(children.get(1) instanceof ServletResource);
+        assertSame(marker, children.get(1).adaptTo(Marker.class));
+        assertTrue(children.get(2) instanceof SyntheticResource);
+    }
+
+    /**
+     * Covers defensive handling for providers returning a null child 
iterator, confirming that overlay children are
+     * still returned and that listChildren continues to produce a valid 
result without throwing or dropping indexed
+     * servlet resources.
+     */
+    @Test
+    public void testListChildrenHandlesNullParentIterator() {
+        final ResourceResolver resolver = Mockito.mock(ResourceResolver.class);
+        final Resource parent = new SyntheticResource(resolver, 
"/apps/parent", "type");
+
+        @SuppressWarnings("unchecked")
+        final ResourceProvider<Object> parentProvider = 
Mockito.mock(ResourceProvider.class);
+        Mockito.when(parentProvider.listChildren(Mockito.any(), 
Mockito.eq(parent)))
+                .thenReturn(null);
+
+        final ResolveContext<Object> parentCtx = 
Mockito.mock(ResolveContext.class);
+        final ResolveContext<Object> ctx = mockContext(resolver, 
parentProvider, parentCtx);
+
+        final MergingServletResourceProvider mergingProvider = new 
MergingServletResourceProvider();
+        addProvider(mergingProvider, "/apps/parent/child");
+
+        final List<Resource> children = 
toList(mergingProvider.listChildren(ctx, parent));
+
+        assertEquals(1, children.size());
+        assertEquals("/apps/parent/child", children.get(0).getPath());
+        assertTrue(children.get(0) instanceof ServletResource);
+    }
+
+    /**
+     * Exercises a large parent-child set to lock in expected merge outcomes 
under high cardinality, including stable
+     * presence of overridden entries and appended overlay-only entries, which 
provides a baseline for later
+     * performance-focused refactoring.
+     */
+    @Test
+    public void testListChildrenWithLargeParentChildrenSet() {
+        final ResourceResolver resolver = Mockito.mock(ResourceResolver.class);
+        final Resource parent = new SyntheticResource(resolver, 
"/apps/parent", "type");
+
+        final int count = 5000;
+        final List<Resource> parentChildren = new ArrayList<>(count);
+        for (int i = 0; i < count; i++) {
+            parentChildren.add(mockParentChild("/apps/parent/child-" + i, 
null));
+        }
+
+        @SuppressWarnings("unchecked")
+        final ResourceProvider<Object> parentProvider = 
Mockito.mock(ResourceProvider.class);
+        Mockito.when(parentProvider.listChildren(Mockito.any(), 
Mockito.eq(parent)))
+                .thenReturn(parentChildren.iterator());
+
+        final ResolveContext<Object> parentCtx = 
Mockito.mock(ResolveContext.class);
+        final ResolveContext<Object> ctx = mockContext(resolver, 
parentProvider, parentCtx);
+
+        final MergingServletResourceProvider mergingProvider = new 
MergingServletResourceProvider();
+        addProvider(mergingProvider, "/apps/parent/child-1200", 
"/apps/parent/child-2200", "/apps/parent/overlay-only");
+
+        final List<Resource> children = 
toList(mergingProvider.listChildren(ctx, parent));
+
+        assertEquals(count + 1, children.size());
+        assertEquals("/apps/parent/child-0", children.get(0).getPath());
+        assertEquals(
+                "/apps/parent/overlay-only", children.get(children.size() - 
1).getPath());
+        assertTrue(pathExists(children, "/apps/parent/child-1200"));
+        assertTrue(pathExists(children, "/apps/parent/child-2200"));
+        assertTrue(pathExists(children, "/apps/parent/overlay-only"));
+
+        final Resource overridden = childByPath(children, 
"/apps/parent/child-1200");
+        assertNotNull(overridden);
+        assertTrue(overridden instanceof ServletResource);
+    }
+
+    private static boolean pathExists(List<Resource> resources, String path) {
+        return childByPath(resources, path) != null;
+    }
+
+    private static Resource childByPath(List<Resource> resources, String path) 
{
+        for (Resource child : resources) {
+            if (path.equals(child.getPath())) {
+                return child;
+            }
+        }
+        return null;
+    }
+
+    private static Resource mockParentChild(String path, Marker marker) {
+        final Resource resource = Mockito.mock(Resource.class);
+        Mockito.when(resource.getPath()).thenReturn(path);
+        Mockito.when(resource.getResourceType()).thenReturn("parent/type");
+        if (marker != null) {
+            Mockito.when(resource.adaptTo(Marker.class)).thenReturn(marker);
+        } else {
+            Mockito.when(resource.adaptTo(Marker.class)).thenReturn(null);
+        }
+        return resource;
+    }
+
+    private static ResolveContext<Object> mockContext(
+            ResourceResolver resolver, ResourceProvider<?> parentProvider, 
ResolveContext<?> parentCtx) {
+        @SuppressWarnings("unchecked")
+        final ResolveContext<Object> ctx = Mockito.mock(ResolveContext.class);
+        Mockito.when(ctx.getResourceResolver()).thenReturn(resolver);
+        Mockito.doReturn(parentProvider).when(ctx).getParentResourceProvider();
+        Mockito.doReturn(parentCtx).when(ctx).getParentResolveContext();
+        return ctx;
+    }
+
+    @SafeVarargs
+    private static void addProvider(MergingServletResourceProvider 
mergingProvider, String... paths) {
+        final Set<String> servletPaths = new LinkedHashSet<>();
+        Collections.addAll(servletPaths, paths);
+
+        final ServletResourceProvider servletProvider =
+                new ServletResourceProvider(TEST_SERVLET, servletPaths, 
Collections.emptySet(), null);
+        @SuppressWarnings("unchecked")
+        final ServiceReference<Servlet> reference = 
Mockito.mock(ServiceReference.class);
+        mergingProvider.add(servletProvider, reference);
+    }
+
+    private static List<Resource> toList(Iterator<Resource> it) {
+        if (it == null) {
+            return Collections.emptyList();
+        }
+        final List<Resource> resources = new ArrayList<>();
+        while (it.hasNext()) {
+            resources.add(it.next());
+        }
+        return resources;
+    }
+}

Reply via email to