From d85837a63aa302432924b051965dca473df39da1 Mon Sep 17 00:00:00 2001 From: Valera V Harseko Date: Wed, 10 Jun 2026 14:27:10 +0300 Subject: [PATCH] test(openidm-script): reproduce custom endpoint field projection collision (#183) Add CustomEndpointFieldProjectionTest reproducing the field-projection bug from discussion #183: a custom scripted endpoint returns the full object without setting response fields (like ScriptedRequestHandler.evaluate()), so the generic CREST projection Resources.filterResource() is applied to the raw result using request.getFields(). When two requested fields share the same leaf name on different nesting levels (userName and manager/userName), the nested one overwrote the top-level one, so top-level userName ended up holding the manager's userName. The test goes through the real path (Resources.newInternalConnection -> connection.read) and asserts the correct behaviour: - top-level userName keeps the user's own userName - manager/userName is nested under manager Fixed in commons: OpenIdentityPlatform/commons#183 Refs: OpenIdentityPlatform/OpenIDM#183 (discussion) --- .../CustomEndpointFieldProjectionTest.java | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 openidm-script/src/test/java/org/forgerock/openidm/script/CustomEndpointFieldProjectionTest.java diff --git a/openidm-script/src/test/java/org/forgerock/openidm/script/CustomEndpointFieldProjectionTest.java b/openidm-script/src/test/java/org/forgerock/openidm/script/CustomEndpointFieldProjectionTest.java new file mode 100644 index 0000000000..1b86803e92 --- /dev/null +++ b/openidm-script/src/test/java/org/forgerock/openidm/script/CustomEndpointFieldProjectionTest.java @@ -0,0 +1,111 @@ +/* + * The contents of this file are subject to the terms of the Common Development and + * Distribution License (the License). You may not use this file except in compliance with the + * License. + * + * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the + * specific language governing permission and limitations under the License. + * + * When distributing Covered Software, include this CDDL Header Notice in each file and include + * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL + * Header, with the fields enclosed by brackets [] replaced by your own identifying + * information: "Portions copyright [year] [name of copyright owner]". + * + * Copyright 2026 3A Systems LLC. + */ + +package org.forgerock.openidm.script; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.forgerock.json.JsonValue.field; +import static org.forgerock.json.JsonValue.json; +import static org.forgerock.json.JsonValue.object; +import static org.forgerock.json.resource.Responses.newResourceResponse; + +import org.forgerock.json.JsonValue; +import org.forgerock.json.resource.AbstractRequestHandler; +import org.forgerock.json.resource.Connection; +import org.forgerock.json.resource.ReadRequest; +import org.forgerock.json.resource.Requests; +import org.forgerock.json.resource.ResourceException; +import org.forgerock.json.resource.ResourceResponse; +import org.forgerock.json.resource.Resources; +import org.forgerock.services.context.Context; +import org.forgerock.services.context.RootContext; +import org.forgerock.util.promise.Promise; +import org.testng.annotations.Test; + +/** + * Reproduces the custom-endpoint field-projection bug discussed in + * discussion #183. + * + *

A custom (scripted) endpoint returns the full object without explicitly setting the + * response fields (see {@code ScriptedRequestHandler.evaluate()} which calls + * {@code newResourceResponse(id, null, resultJson)} without {@code addField(...)}). As a + * consequence the generic CREST field projection in + * {@code org.forgerock.json.resource.Resources.filterResource(JsonValue, Collection)} is applied + * to the raw result using {@code request.getFields()}. + * + *

That generic projection flattens every requested {@link org.forgerock.json.JsonPointer} + * down to its {@code leaf()} name when building the filtered output. When two requested fields + * share the same leaf name on different nesting levels (e.g. {@code userName} and + * {@code manager/userName}), the nested one overwrites the top-level one, so the top-level + * {@code userName} ends up containing the manager's {@code userName}. + * + *

This test asserts the correct behaviour. It therefore fails against the buggy + * commons {@code json-resource} (3.1.1-SNAPSHOT) and is expected to pass once the generic + * projection is fixed to preserve the pointer structure instead of collapsing to the leaf name. + */ +public class CustomEndpointFieldProjectionTest { + + private static final String USER_NAME = "bjensen"; + private static final String MANAGER_USER_NAME = "jdoe"; + + /** + * A request handler that mimics a custom scripted endpoint: it returns the full object on + * read and does not set the response fields (no {@code addField(...)}), exactly like + * {@code ScriptedRequestHandler.evaluate()}. + */ + private static final class FullObjectEndpoint extends AbstractRequestHandler { + @Override + public Promise handleRead(final Context context, + final ReadRequest request) { + final JsonValue content = json(object( + field("_id", "user1"), + field("userName", USER_NAME), + field("givenName", "Barbara"), + field("sn", "Jensen"), + field("description", "Example user"), + field("manager", object( + field("_id", "user2"), + field("userName", MANAGER_USER_NAME), + field("givenName", "John"), + field("sn", "Doe"))))); + return newResourceResponse("user1", null, content).asPromise(); + } + } + + @Test + public void topLevelFieldIsNotOverwrittenByNestedFieldWithSameLeafName() throws Exception { + final Connection connection = + Resources.newInternalConnection(new FullObjectEndpoint()); + + // Request the same leaf name on two nesting levels: userName and manager/userName. + final ReadRequest request = Requests.newReadRequest("managed/user/user1") + .addField("description", "userName", "givenName", "sn", "manager", "manager/userName"); + + final ResourceResponse response = connection.read(new RootContext(), request); + final JsonValue content = response.getContent(); + + // The top-level userName must remain the user's own userName... + assertThat(content.get("userName").asString()) + .as("top-level userName must not be overwritten by manager/userName") + .isEqualTo(USER_NAME); + + // ...and the manager's userName must be nested under manager. + assertThat(content.get(new org.forgerock.json.JsonPointer("manager/userName")).asString()) + .as("manager/userName must be projected into the nested manager object") + .isEqualTo(MANAGER_USER_NAME); + } +} +