Skip to content

Commit 53170da

Browse files
authored
test(openidm-script): reproduce custom endpoint field projection collision (#183) (#190)
1 parent 7b117d4 commit 53170da

1 file changed

Lines changed: 111 additions & 0 deletions

File tree

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* The contents of this file are subject to the terms of the Common Development and
3+
* Distribution License (the License). You may not use this file except in compliance with the
4+
* License.
5+
*
6+
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
7+
* specific language governing permission and limitations under the License.
8+
*
9+
* When distributing Covered Software, include this CDDL Header Notice in each file and include
10+
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
11+
* Header, with the fields enclosed by brackets [] replaced by your own identifying
12+
* information: "Portions copyright [year] [name of copyright owner]".
13+
*
14+
* Copyright 2026 3A Systems LLC.
15+
*/
16+
17+
package org.forgerock.openidm.script;
18+
19+
import static org.assertj.core.api.Assertions.assertThat;
20+
import static org.forgerock.json.JsonValue.field;
21+
import static org.forgerock.json.JsonValue.json;
22+
import static org.forgerock.json.JsonValue.object;
23+
import static org.forgerock.json.resource.Responses.newResourceResponse;
24+
25+
import org.forgerock.json.JsonValue;
26+
import org.forgerock.json.resource.AbstractRequestHandler;
27+
import org.forgerock.json.resource.Connection;
28+
import org.forgerock.json.resource.ReadRequest;
29+
import org.forgerock.json.resource.Requests;
30+
import org.forgerock.json.resource.ResourceException;
31+
import org.forgerock.json.resource.ResourceResponse;
32+
import org.forgerock.json.resource.Resources;
33+
import org.forgerock.services.context.Context;
34+
import org.forgerock.services.context.RootContext;
35+
import org.forgerock.util.promise.Promise;
36+
import org.testng.annotations.Test;
37+
38+
/**
39+
* Reproduces the custom-endpoint field-projection bug discussed in
40+
* <a href="https://github.com/OpenIdentityPlatform/OpenIDM/discussions/183">discussion #183</a>.
41+
*
42+
* <p>A custom (scripted) endpoint returns the full object without explicitly setting the
43+
* response fields (see {@code ScriptedRequestHandler.evaluate()} which calls
44+
* {@code newResourceResponse(id, null, resultJson)} without {@code addField(...)}). As a
45+
* consequence the generic CREST field projection in
46+
* {@code org.forgerock.json.resource.Resources.filterResource(JsonValue, Collection)} is applied
47+
* to the raw result using {@code request.getFields()}.
48+
*
49+
* <p>That generic projection flattens every requested {@link org.forgerock.json.JsonPointer}
50+
* down to its {@code leaf()} name when building the filtered output. When two requested fields
51+
* share the same leaf name on different nesting levels (e.g. {@code userName} and
52+
* {@code manager/userName}), the nested one overwrites the top-level one, so the top-level
53+
* {@code userName} ends up containing the manager's {@code userName}.
54+
*
55+
* <p>This test asserts the <b>correct</b> behaviour. It therefore <b>fails against the buggy
56+
* commons {@code json-resource} (3.1.1-SNAPSHOT)</b> and is expected to pass once the generic
57+
* projection is fixed to preserve the pointer structure instead of collapsing to the leaf name.
58+
*/
59+
public class CustomEndpointFieldProjectionTest {
60+
61+
private static final String USER_NAME = "bjensen";
62+
private static final String MANAGER_USER_NAME = "jdoe";
63+
64+
/**
65+
* A request handler that mimics a custom scripted endpoint: it returns the full object on
66+
* read and does <b>not</b> set the response fields (no {@code addField(...)}), exactly like
67+
* {@code ScriptedRequestHandler.evaluate()}.
68+
*/
69+
private static final class FullObjectEndpoint extends AbstractRequestHandler {
70+
@Override
71+
public Promise<ResourceResponse, ResourceException> handleRead(final Context context,
72+
final ReadRequest request) {
73+
final JsonValue content = json(object(
74+
field("_id", "user1"),
75+
field("userName", USER_NAME),
76+
field("givenName", "Barbara"),
77+
field("sn", "Jensen"),
78+
field("description", "Example user"),
79+
field("manager", object(
80+
field("_id", "user2"),
81+
field("userName", MANAGER_USER_NAME),
82+
field("givenName", "John"),
83+
field("sn", "Doe")))));
84+
return newResourceResponse("user1", null, content).asPromise();
85+
}
86+
}
87+
88+
@Test
89+
public void topLevelFieldIsNotOverwrittenByNestedFieldWithSameLeafName() throws Exception {
90+
final Connection connection =
91+
Resources.newInternalConnection(new FullObjectEndpoint());
92+
93+
// Request the same leaf name on two nesting levels: userName and manager/userName.
94+
final ReadRequest request = Requests.newReadRequest("managed/user/user1")
95+
.addField("description", "userName", "givenName", "sn", "manager", "manager/userName");
96+
97+
final ResourceResponse response = connection.read(new RootContext(), request);
98+
final JsonValue content = response.getContent();
99+
100+
// The top-level userName must remain the user's own userName...
101+
assertThat(content.get("userName").asString())
102+
.as("top-level userName must not be overwritten by manager/userName")
103+
.isEqualTo(USER_NAME);
104+
105+
// ...and the manager's userName must be nested under manager.
106+
assertThat(content.get(new org.forgerock.json.JsonPointer("manager/userName")).asString())
107+
.as("manager/userName must be projected into the nested manager object")
108+
.isEqualTo(MANAGER_USER_NAME);
109+
}
110+
}
111+

0 commit comments

Comments
 (0)