This is an automated email from the ASF dual-hosted git repository.
rombert pushed a commit to branch master
in repository
https://gitbox.apache.org/repos/asf/sling-org-apache-sling-graphql-core.git
The following commit(s) were added to refs/heads/master by this push:
new d4f779c SLING-13175 - [GraphQL] Duplicate field aliases lose
sub-fields present only in the second alias (#46)
d4f779c is described below
commit d4f779c44068fdc1069b4a3e1db8987e0259087f
Author: Martin Schulze <[email protected]>
AuthorDate: Thu Apr 23 09:49:05 2026 +0200
SLING-13175 - [GraphQL] Duplicate field aliases lose sub-fields present
only in the second alias (#46)
---------
Co-authored-by: Martin Schulze <[email protected]>
---
.../graphql/core/engine/SelectedFieldWrapper.java | 29 ++-
.../core/engine/SelectedFieldWrapperTest.java | 241 ++++++++++++++-------
.../core/engine/SelectionSetWrapperTest.java | 157 ++++++++++++++
3 files changed, 334 insertions(+), 93 deletions(-)
diff --git
a/src/main/java/org/apache/sling/graphql/core/engine/SelectedFieldWrapper.java
b/src/main/java/org/apache/sling/graphql/core/engine/SelectedFieldWrapper.java
index 9f399fb..0deaf39 100644
---
a/src/main/java/org/apache/sling/graphql/core/engine/SelectedFieldWrapper.java
+++
b/src/main/java/org/apache/sling/graphql/core/engine/SelectedFieldWrapper.java
@@ -71,15 +71,16 @@ public class SelectedFieldWrapper implements SelectedField {
if (selectionSet != null) {
selectionSet.getImmediateFields().forEach(sf -> {
SelectedFieldWrapper selectedChildField = new
SelectedFieldWrapper(sf);
+ // Always add to the by-name multimap so
hasDuplicateFieldByName() works
+ subFieldMap.put(sf.getName(), selectedChildField);
String fqn = sf.getFullyQualifiedName();
SelectedField existing = subFQNFieldMap.get(fqn);
if (existing instanceof SelectedFieldWrapper) {
- // Merge sub-fields from duplicate entries (e.g. aliased
selections
+ // Merge sub-fields from duplicate FQN entries (e.g.
aliased selections
// of the same field with different inline fragments)
((SelectedFieldWrapper)
existing).mergeSubFields(selectedChildField);
} else {
subFQNFieldMap.put(fqn, selectedChildField);
- subFieldMap.put(sf.getName(), selectedChildField);
}
});
}
@@ -91,17 +92,27 @@ public class SelectedFieldWrapper implements SelectedField {
* Merge sub-fields from another SelectedFieldWrapper into this one.
* This handles the case where the same field is selected multiple times
* (e.g. via aliases with different inline fragments), and the
sub-selections
- * need to be combined.
+ * need to be combined. The merge is recursive: if both wrappers contain a
+ * child with the same FQN, their sub-fields are merged in turn rather than
+ * silently dropping one.
*/
+ // S3824: computeIfAbsent cannot be used here because the existing-value
branch
+ // must trigger a recursive merge rather than a replacement.
+ @SuppressWarnings("java:S3824")
void mergeSubFields(SelectedFieldWrapper other) {
for (SelectedField otherSub : other.getSubSelectedFields()) {
String fqn = otherSub.getFullyQualifiedName();
- if (fqn != null) {
- subFQNFieldMap.computeIfAbsent(fqn, k -> {
- subFieldMap.put(otherSub.getName(), otherSub);
- subFields.add(otherSub);
- return otherSub;
- });
+ if (fqn == null) {
+ continue;
+ }
+ SelectedField existingSub = subFQNFieldMap.get(fqn);
+ if (existingSub instanceof SelectedFieldWrapper && otherSub
instanceof SelectedFieldWrapper) {
+ // Recursively merge deeper levels
+ ((SelectedFieldWrapper)
existingSub).mergeSubFields((SelectedFieldWrapper) otherSub);
+ } else if (existingSub == null) {
+ subFQNFieldMap.put(fqn, otherSub);
+ subFieldMap.put(otherSub.getName(), otherSub);
+ subFields.add(otherSub);
}
}
}
diff --git
a/src/test/java/org/apache/sling/graphql/core/engine/SelectedFieldWrapperTest.java
b/src/test/java/org/apache/sling/graphql/core/engine/SelectedFieldWrapperTest.java
index 66f1e53..902b9cb 100644
---
a/src/test/java/org/apache/sling/graphql/core/engine/SelectedFieldWrapperTest.java
+++
b/src/test/java/org/apache/sling/graphql/core/engine/SelectedFieldWrapperTest.java
@@ -181,22 +181,18 @@ public class SelectedFieldWrapperTest {
}
/**
- * Tests the mergeSubFields behavior (SITES-42449): when graphql-java
returns
- * two immediate children with the same FQN (e.g. two "items" entries from
aliased
- * inline fragments), their sub-fields must be merged rather than the
first being
- * overwritten by the second.
- *
- * Simulates: { parentList { items { ... on ModelA { fieldA } ... on
ModelB { fieldB } } } }
- * where graphql-java produces two "items" entries with FQN
"Parent.items", each
- * carrying different sub-fields (ModelA.fieldA vs ModelB.fieldB).
+ * Tests merge when two immediate children share the same FQN but carry
+ * different sub-fields (e.g. two aliased selections of the same field with
+ * different inline fragments). The merged wrapper must contain sub-fields
+ * from both entries, and hasDuplicateFieldByName must still report the
+ * duplicate.
*/
@Test
public void testMergeSubFieldsForDuplicateFQN() {
- // FQN shared by both "items" entries
String itemsFqn = "Parent.items";
String itemsName = "items";
- // Sub-fields from the first inline fragment (ModelA)
+ // First "items" entry carries sub-field fieldA
String subFqnA = "ModelA.fieldA";
String subNameA = "fieldA";
graphql.schema.SelectedField sourceSubA =
mock(graphql.schema.SelectedField.class);
@@ -213,7 +209,7 @@ public class SelectedFieldWrapperTest {
doReturn(itemsFqn).when(sourceItems1).getFullyQualifiedName();
doReturn(selSetItems1).when(sourceItems1).getSelectionSet();
- // Sub-fields from the second inline fragment (ModelB)
+ // Second "items" entry carries sub-field fieldB
String subFqnB = "ModelB.fieldB";
String subNameB = "fieldB";
graphql.schema.SelectedField sourceSubB =
mock(graphql.schema.SelectedField.class);
@@ -230,7 +226,7 @@ public class SelectedFieldWrapperTest {
doReturn(itemsFqn).when(sourceItems2).getFullyQualifiedName();
doReturn(selSetItems2).when(sourceItems2).getSelectionSet();
- // Parent field with both "items" entries as immediate children
+ // Parent with both items entries
graphql.schema.SelectedField sourceParent =
mock(graphql.schema.SelectedField.class);
doReturn("parent").when(sourceParent).getName();
doReturn("parent").when(sourceParent).getQualifiedName();
@@ -242,98 +238,175 @@ public class SelectedFieldWrapperTest {
SelectedFieldWrapper parent = new SelectedFieldWrapper(sourceParent);
- // The parent should have exactly one "items" child (merged, not
duplicated)
+ // hasDuplicateFieldByName must still detect both entries
+ assertTrue("items should be reported as duplicate by name",
+ parent.hasDuplicateFieldByName(itemsName));
+
+ // Merged items must contain sub-fields from BOTH entries
SelectedField itemsField = parent.getSubSelectedFieldByFQN(itemsFqn);
assertNotNull("Items field not found by FQN", itemsField);
-
- // The merged "items" field should contain sub-fields from BOTH inline
fragments
- assertTrue("Sub-field from first inline fragment (ModelA.fieldA)
missing",
- itemsField.hasSubSelectedFieldsByFQN(subFqnA));
- assertTrue("Sub-field from second inline fragment (ModelB.fieldB)
missing",
- itemsField.hasSubSelectedFieldsByFQN(subFqnB));
-
- // Verify by direct lookup
- SelectedField foundA = itemsField.getSubSelectedFieldByFQN(subFqnA);
- assertNotNull("fieldA not found by FQN", foundA);
- assertEquals("Wrong name for fieldA", subNameA, foundA.getName());
-
- SelectedField foundB = itemsField.getSubSelectedFieldByFQN(subFqnB);
- assertNotNull("fieldB not found by FQN", foundB);
- assertEquals("Wrong name for fieldB", subNameB, foundB.getName());
-
- // Total sub-fields count should be 2 (one from each inline fragment)
+ assertTrue("fieldA missing",
itemsField.hasSubSelectedFieldsByFQN(subFqnA));
+ assertTrue("fieldB missing",
itemsField.hasSubSelectedFieldsByFQN(subFqnB));
assertEquals("Expected 2 merged sub-fields", 2,
itemsField.getSubSelectedFields().size());
}
/**
- * Verifies that when two immediate children share the same FQN (and
simple name),
- * the merged-away duplicate does NOT remain in subFieldMap.
+ * Tests recursive merge: two aliased selections of the same field where
+ * both contain a child with the same FQN (e.g. policyCategories) but with
+ * different deeper sub-fields. The merge must recurse so that the deeper
+ * sub-fields from both branches are visible.
*
- * Before the fix, subFieldMap.put() ran unconditionally before the FQN
dedup merge,
- * leaving a stale wrapper in the map. This caused
hasDuplicateFieldByName() to report
- * a false positive and getSubSelectedFieldByName() to return both the
merged wrapper
- * and the stale one.
+ * Simulates: policyTabList: policy { policyCategories { id, name } }
+ * policyTabDetailList: policy { policyCategories { id, name,
policies { ... } } }
+ */
+ @Test
+ public void testRecursiveMergeForSharedChildFQN() {
+ String policyFqn = "PoliciesModel.policy";
+ String policyName = "policy";
+ String catFqn = "PolicyModel.policyCategories";
+ String catName = "policyCategories";
+
+ // --- Shared leaf fields under policyCategories ---
+ graphql.schema.SelectedField idField =
mock(graphql.schema.SelectedField.class);
+ doReturn("policyCategoryId").when(idField).getName();
+ doReturn("policyCategoryId").when(idField).getQualifiedName();
+
doReturn("CategoryModel.policyCategoryId").when(idField).getFullyQualifiedName();
+
+ graphql.schema.SelectedField nameField =
mock(graphql.schema.SelectedField.class);
+ doReturn("policyCategoryName").when(nameField).getName();
+ doReturn("policyCategoryName").when(nameField).getQualifiedName();
+
doReturn("CategoryModel.policyCategoryName").when(nameField).getFullyQualifiedName();
+
+ // --- Extra deep field only in second alias ---
+ graphql.schema.SelectedField policiesField =
mock(graphql.schema.SelectedField.class);
+ doReturn("policies").when(policiesField).getName();
+ doReturn("policies").when(policiesField).getQualifiedName();
+
doReturn("CategoryModel.policies").when(policiesField).getFullyQualifiedName();
+
+ // First policyCategories (shallow: id + name)
+ DataFetchingFieldSelectionSet catSelSet1 =
mock(DataFetchingFieldSelectionSet.class);
+ doReturn(Arrays.asList(idField,
nameField)).when(catSelSet1).getImmediateFields();
+ graphql.schema.SelectedField cat1 =
mock(graphql.schema.SelectedField.class);
+ doReturn(catName).when(cat1).getName();
+ doReturn(catName).when(cat1).getQualifiedName();
+ doReturn(catFqn).when(cat1).getFullyQualifiedName();
+ doReturn(catSelSet1).when(cat1).getSelectionSet();
+
+ // Second policyCategories (deep: id + name + policies)
+ DataFetchingFieldSelectionSet catSelSet2 =
mock(DataFetchingFieldSelectionSet.class);
+ doReturn(Arrays.asList(idField, nameField,
policiesField)).when(catSelSet2).getImmediateFields();
+ graphql.schema.SelectedField cat2 =
mock(graphql.schema.SelectedField.class);
+ doReturn(catName).when(cat2).getName();
+ doReturn(catName).when(cat2).getQualifiedName();
+ doReturn(catFqn).when(cat2).getFullyQualifiedName();
+ doReturn(catSelSet2).when(cat2).getSelectionSet();
+
+ // First policy entry (alias policyTabList)
+ DataFetchingFieldSelectionSet policySelSet1 =
mock(DataFetchingFieldSelectionSet.class);
+ doReturn(Arrays.asList(cat1)).when(policySelSet1).getImmediateFields();
+ graphql.schema.SelectedField policy1 =
mock(graphql.schema.SelectedField.class);
+ doReturn(policyName).when(policy1).getName();
+ doReturn(policyName).when(policy1).getQualifiedName();
+ doReturn(policyFqn).when(policy1).getFullyQualifiedName();
+ doReturn(policySelSet1).when(policy1).getSelectionSet();
+
+ // Second policy entry (alias policyTabDetailList)
+ DataFetchingFieldSelectionSet policySelSet2 =
mock(DataFetchingFieldSelectionSet.class);
+ doReturn(Arrays.asList(cat2)).when(policySelSet2).getImmediateFields();
+ graphql.schema.SelectedField policy2 =
mock(graphql.schema.SelectedField.class);
+ doReturn(policyName).when(policy2).getName();
+ doReturn(policyName).when(policy2).getQualifiedName();
+ doReturn(policyFqn).when(policy2).getFullyQualifiedName();
+ doReturn(policySelSet2).when(policy2).getSelectionSet();
+
+ // Parent item containing both policy selections
+ graphql.schema.SelectedField parentField =
mock(graphql.schema.SelectedField.class);
+ doReturn("item").when(parentField).getName();
+ doReturn("item").when(parentField).getQualifiedName();
+ doReturn("Query.item").when(parentField).getFullyQualifiedName();
+ DataFetchingFieldSelectionSet parentSelSet =
mock(DataFetchingFieldSelectionSet.class);
+ doReturn(Arrays.asList(policy1,
policy2)).when(parentSelSet).getImmediateFields();
+ doReturn(parentSelSet).when(parentField).getSelectionSet();
+
+ SelectedFieldWrapper parent = new SelectedFieldWrapper(parentField);
+
+ // policy should be flagged as duplicate
+ assertTrue("policy should be duplicate by name",
+ parent.hasDuplicateFieldByName(policyName));
+
+ // Merged policy → policyCategories must contain all 3 sub-fields
+ SelectedField policyField = parent.getSubSelectedFieldByFQN(policyFqn);
+ assertNotNull("policy not found by FQN", policyField);
+
+ SelectedField catField = policyField.getSubSelectedFieldByFQN(catFqn);
+ assertNotNull("policyCategories not found", catField);
+
+ assertTrue("policyCategoryId missing",
+
catField.hasSubSelectedFieldsByFQN("CategoryModel.policyCategoryId"));
+ assertTrue("policyCategoryName missing",
+
catField.hasSubSelectedFieldsByFQN("CategoryModel.policyCategoryName"));
+ assertTrue("policies missing (recursive merge failed)",
+ catField.hasSubSelectedFieldsByFQN("CategoryModel.policies"));
+ assertEquals("Expected 3 sub-fields after recursive merge",
+ 3, catField.getSubSelectedFields().size());
+ }
+
+ /**
+ * Tests that mergeSubFields silently skips sub-fields whose FQN is null.
+ * Such entries must not be added to the merged wrapper.
*/
@Test
- public void testNoDuplicateInSubFieldMapAfterFQNMerge() {
+ public void testMergeSubFieldsSkipsNullFqn() {
String itemsFqn = "Parent.items";
String itemsName = "items";
- // First "items" child with one sub-field
- graphql.schema.SelectedField sourceSubA =
mock(graphql.schema.SelectedField.class);
- doReturn("fieldA").when(sourceSubA).getName();
- doReturn("fieldA").when(sourceSubA).getQualifiedName();
- doReturn("ModelA.fieldA").when(sourceSubA).getFullyQualifiedName();
-
- DataFetchingFieldSelectionSet selSetItems1 =
mock(DataFetchingFieldSelectionSet.class);
-
doReturn(Arrays.asList(sourceSubA)).when(selSetItems1).getImmediateFields();
-
- graphql.schema.SelectedField sourceItems1 =
mock(graphql.schema.SelectedField.class);
- doReturn(itemsName).when(sourceItems1).getName();
- doReturn(itemsName).when(sourceItems1).getQualifiedName();
- doReturn(itemsFqn).when(sourceItems1).getFullyQualifiedName();
- doReturn(selSetItems1).when(sourceItems1).getSelectionSet();
-
- // Second "items" child (same name + same FQN) with a different
sub-field
- graphql.schema.SelectedField sourceSubB =
mock(graphql.schema.SelectedField.class);
- doReturn("fieldB").when(sourceSubB).getName();
- doReturn("fieldB").when(sourceSubB).getQualifiedName();
- doReturn("ModelB.fieldB").when(sourceSubB).getFullyQualifiedName();
-
- DataFetchingFieldSelectionSet selSetItems2 =
mock(DataFetchingFieldSelectionSet.class);
-
doReturn(Arrays.asList(sourceSubB)).when(selSetItems2).getImmediateFields();
-
- graphql.schema.SelectedField sourceItems2 =
mock(graphql.schema.SelectedField.class);
- doReturn(itemsName).when(sourceItems2).getName();
- doReturn(itemsName).when(sourceItems2).getQualifiedName();
- doReturn(itemsFqn).when(sourceItems2).getFullyQualifiedName();
- doReturn(selSetItems2).when(sourceItems2).getSelectionSet();
-
- // Parent with both "items" entries
+ // First "items" entry with a real sub-field
+ graphql.schema.SelectedField realSub =
mock(graphql.schema.SelectedField.class);
+ doReturn("realField").when(realSub).getName();
+ doReturn("realField").when(realSub).getQualifiedName();
+ doReturn("Model.realField").when(realSub).getFullyQualifiedName();
+
+ DataFetchingFieldSelectionSet selSet1 =
mock(DataFetchingFieldSelectionSet.class);
+ doReturn(Arrays.asList(realSub)).when(selSet1).getImmediateFields();
+
+ graphql.schema.SelectedField items1 =
mock(graphql.schema.SelectedField.class);
+ doReturn(itemsName).when(items1).getName();
+ doReturn(itemsName).when(items1).getQualifiedName();
+ doReturn(itemsFqn).when(items1).getFullyQualifiedName();
+ doReturn(selSet1).when(items1).getSelectionSet();
+
+ // Second "items" entry with a sub-field whose FQN is null
+ graphql.schema.SelectedField nullFqnSub =
mock(graphql.schema.SelectedField.class);
+ doReturn("ghostField").when(nullFqnSub).getName();
+ doReturn("ghostField").when(nullFqnSub).getQualifiedName();
+ doReturn(null).when(nullFqnSub).getFullyQualifiedName();
+
+ DataFetchingFieldSelectionSet selSet2 =
mock(DataFetchingFieldSelectionSet.class);
+ doReturn(Arrays.asList(nullFqnSub)).when(selSet2).getImmediateFields();
+
+ graphql.schema.SelectedField items2 =
mock(graphql.schema.SelectedField.class);
+ doReturn(itemsName).when(items2).getName();
+ doReturn(itemsName).when(items2).getQualifiedName();
+ doReturn(itemsFqn).when(items2).getFullyQualifiedName();
+ doReturn(selSet2).when(items2).getSelectionSet();
+
+ // Parent with both items entries
graphql.schema.SelectedField sourceParent =
mock(graphql.schema.SelectedField.class);
doReturn("parent").when(sourceParent).getName();
doReturn("parent").when(sourceParent).getQualifiedName();
doReturn("Query.parent").when(sourceParent).getFullyQualifiedName();
-
DataFetchingFieldSelectionSet parentSelSet =
mock(DataFetchingFieldSelectionSet.class);
- doReturn(Arrays.asList(sourceItems1,
sourceItems2)).when(parentSelSet).getImmediateFields();
+ doReturn(Arrays.asList(items1,
items2)).when(parentSelSet).getImmediateFields();
doReturn(parentSelSet).when(sourceParent).getSelectionSet();
SelectedFieldWrapper parent = new SelectedFieldWrapper(sourceParent);
- // subFieldMap must contain exactly one entry for "items" (the merged
wrapper),
- // NOT two (merged + stale)
- assertFalse("Same-FQN children must not be reported as duplicates by
simple name",
- parent.hasDuplicateFieldByName(itemsName));
- Collection<SelectedField> byName =
parent.getSubSelectedFieldByName(itemsName);
- assertEquals("Expected exactly 1 entry in subFieldMap for same-FQN
children", 1, byName.size());
-
- // The single entry must be the merged wrapper containing sub-fields
from both fragments
- SelectedField merged = byName.iterator().next();
- assertEquals("Expected 2 sub-fields in merged wrapper", 2,
merged.getSubSelectedFields().size());
- assertTrue("fieldA missing from merged wrapper",
- merged.hasSubSelectedFieldsByFQN("ModelA.fieldA"));
- assertTrue("fieldB missing from merged wrapper",
- merged.hasSubSelectedFieldsByFQN("ModelB.fieldB"));
+ SelectedField itemsField = parent.getSubSelectedFieldByFQN(itemsFqn);
+ assertNotNull("items not found by FQN", itemsField);
+
+ // realField must be present
+ assertTrue("realField missing after merge",
itemsField.hasSubSelectedFieldsByFQN("Model.realField"));
+ // null-FQN ghost must be silently skipped — only 1 sub-field in
merged wrapper
+ assertEquals("Null-FQN sub-field must be skipped during merge", 1,
itemsField.getSubSelectedFields().size());
}
}
diff --git
a/src/test/java/org/apache/sling/graphql/core/engine/SelectionSetWrapperTest.java
b/src/test/java/org/apache/sling/graphql/core/engine/SelectionSetWrapperTest.java
new file mode 100644
index 0000000..957f723
--- /dev/null
+++
b/src/test/java/org/apache/sling/graphql/core/engine/SelectionSetWrapperTest.java
@@ -0,0 +1,157 @@
+/*
+ * 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.graphql.core.engine;
+
+import graphql.schema.DataFetchingFieldSelectionSet;
+import org.apache.sling.graphql.api.SelectedField;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+public class SelectionSetWrapperTest {
+
+ /** Null selectionSet must yield an empty wrapper without NPE. */
+ @Test
+ public void testNullSelectionSet() {
+ SelectionSetWrapper wrapper = new SelectionSetWrapper(null);
+ assertTrue("fields must be empty for null input",
wrapper.getFields().isEmpty());
+ }
+
+ /** Two fields with different names are both kept independently. */
+ @Test
+ public void testDistinctFields() {
+ graphql.schema.SelectedField sfA = mockField("fieldA", "Model.fieldA");
+ graphql.schema.SelectedField sfB = mockField("fieldB", "Model.fieldB");
+
+ DataFetchingFieldSelectionSet selSet =
mock(DataFetchingFieldSelectionSet.class);
+ doReturn(Arrays.asList(sfA, sfB)).when(selSet).getImmediateFields();
+
+ SelectionSetWrapper wrapper = new SelectionSetWrapper(selSet);
+
+ List<SelectedField> fields = wrapper.getFields();
+ assertEquals("Expected 2 top-level fields", 2, fields.size());
+ assertNotNull("fieldA must be accessible by name",
wrapper.get("fieldA"));
+ assertNotNull("fieldB must be accessible by name",
wrapper.get("fieldB"));
+ assertTrue("contains(fieldA)", wrapper.contains("fieldA"));
+ assertTrue("contains(fieldB)", wrapper.contains("fieldB"));
+ }
+
+ /**
+ * Two top-level entries with the same simple name (different aliases
pointing at the same field).
+ * The second entry must be merged into the first, not added as a second
top-level field.
+ * Sub-fields present in only one of the entries must be visible after the
merge.
+ */
+ @Test
+ public void testDuplicateFieldNameMergesSubFields() {
+ // Sub-field present only in the first alias
+ graphql.schema.SelectedField subA = mockField("subA",
"ItemModel.subA");
+ // Sub-field present only in the second alias
+ graphql.schema.SelectedField subB = mockField("subB",
"ItemModel.subB");
+
+ // First alias selects only subA
+ DataFetchingFieldSelectionSet selSet1 =
mock(DataFetchingFieldSelectionSet.class);
+ doReturn(Arrays.asList(subA)).when(selSet1).getImmediateFields();
+ graphql.schema.SelectedField item1 = mockFieldWithSelectionSet("item",
"HolderModel.item", selSet1);
+
+ // Second alias selects only subB
+ DataFetchingFieldSelectionSet selSet2 =
mock(DataFetchingFieldSelectionSet.class);
+ doReturn(Arrays.asList(subB)).when(selSet2).getImmediateFields();
+ graphql.schema.SelectedField item2 = mockFieldWithSelectionSet("item",
"HolderModel.item", selSet2);
+
+ DataFetchingFieldSelectionSet root =
mock(DataFetchingFieldSelectionSet.class);
+ doReturn(Arrays.asList(item1, item2)).when(root).getImmediateFields();
+
+ SelectionSetWrapper wrapper = new SelectionSetWrapper(root);
+
+ // Only one top-level field named "item"
+ assertEquals("Duplicate-named entries must be merged to a single
top-level field",
+ 1, wrapper.getFields().size());
+
+ // The merged field must expose sub-fields from BOTH aliases
+ SelectedField merged = wrapper.get("item");
+ assertNotNull("merged item field not found", merged);
+ assertTrue("subA (from first alias) must be present after merge",
+ merged.hasSubSelectedFieldsByFQN("ItemModel.subA"));
+ assertTrue("subB (from second alias) must be present after merge",
+ merged.hasSubSelectedFieldsByFQN("ItemModel.subB"));
+ assertEquals("merged item must have 2 sub-fields", 2,
merged.getSubSelectedFields().size());
+ }
+
+ /**
+ * A field whose name contains "/" is treated as a flat-map path entry
only and must
+ * not appear in the top-level fields list.
+ */
+ @Test
+ public void testSlashFieldExcludedFromTopLevel() {
+ graphql.schema.SelectedField slashField = mockField("parent/child",
"Model.parentChild");
+
+ DataFetchingFieldSelectionSet selSet =
mock(DataFetchingFieldSelectionSet.class);
+ doReturn(Arrays.asList(slashField)).when(selSet).getImmediateFields();
+
+ SelectionSetWrapper wrapper = new SelectionSetWrapper(selSet);
+
+ assertTrue("Slash-named field must not appear in top-level fields
list",
+ wrapper.getFields().isEmpty());
+ }
+
+ /** Nested sub-fields must be accessible via slash-qualified flat-map
paths. */
+ @Test
+ public void testNestedFieldAccessibleByQualifiedPath() {
+ graphql.schema.SelectedField leaf = mockField("leaf",
"ChildModel.leaf");
+ DataFetchingFieldSelectionSet childSelSet =
mock(DataFetchingFieldSelectionSet.class);
+ doReturn(Arrays.asList(leaf)).when(childSelSet).getImmediateFields();
+
+ graphql.schema.SelectedField parent =
mockFieldWithSelectionSet("parent", "Model.parent", childSelSet);
+
+ DataFetchingFieldSelectionSet root =
mock(DataFetchingFieldSelectionSet.class);
+ doReturn(Arrays.asList(parent)).when(root).getImmediateFields();
+
+ SelectionSetWrapper wrapper = new SelectionSetWrapper(root);
+
+ assertNotNull("parent must be found by simple name",
wrapper.get("parent"));
+ assertNotNull("leaf must be found by flat qualified path parent/leaf",
+ wrapper.get("parent/leaf"));
+ assertNull("unknown path must return null",
wrapper.get("parent/unknown"));
+ }
+
+ // --- helpers ---
+
+ private static graphql.schema.SelectedField mockField(String name, String
fqn) {
+ graphql.schema.SelectedField sf =
mock(graphql.schema.SelectedField.class);
+ doReturn(name).when(sf).getName();
+ doReturn(name).when(sf).getQualifiedName();
+ doReturn(fqn).when(sf).getFullyQualifiedName();
+ return sf;
+ }
+
+ private static graphql.schema.SelectedField mockFieldWithSelectionSet(
+ String name, String fqn, DataFetchingFieldSelectionSet selSet) {
+ graphql.schema.SelectedField sf = mockField(name, fqn);
+ doReturn(selSet).when(sf).getSelectionSet();
+ return sf;
+ }
+}
\ No newline at end of file