From a2d67f674f7e80dcd9ca901ea63df41ef47f4214 Mon Sep 17 00:00:00 2001
From: vharseko <vharseko@openam.org.ru>
Date: Wed, 22 Nov 2017 05:08:57 +0000
Subject: [PATCH] Merge pull request #3 from GuyPaddock/wren/feature/subtree-flattening

---
 opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java |  725 +++++++++++++++++++++++++++++++++++++++++++++++--------
 1 files changed, 616 insertions(+), 109 deletions(-)

diff --git a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java
index 343ed60..cb1bab0 100644
--- a/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java
+++ b/opendj-rest2ldap/src/test/java/org/forgerock/opendj/rest2ldap/BasicRequestsTest.java
@@ -12,6 +12,7 @@
  * information: "Portions copyright [year] [name of copyright owner]".
  *
  * Copyright 2013-2016 ForgeRock AS.
+ * Portions Copyright 2017 Rosie Applications, Inc.
  */
 package org.forgerock.opendj.rest2ldap;
 
@@ -92,72 +93,331 @@
     private static final QueryFilter<JsonPointer> NO_FILTER = QueryFilter.alwaysTrue();
 
     @Test
-    public void testQueryAll() throws Exception {
+    public void testQueryAllWithNoSubtreeFlatteningAndNoSearchFilter() throws Exception {
         final Connection connection = newConnection();
         final List<ResourceResponse> resources = new LinkedList<>();
-        final QueryResponse result = connection.query(
-                newAuthConnectionContext(), newQueryRequest("").setQueryFilter(NO_FILTER), resources);
-        assertThat(resources).hasSize(5);
+        final QueryResponse result =
+            connection.query(
+                newAuthConnectionContext(),
+                newQueryRequest("").setQueryFilter(NO_FILTER),
+                resources);
+
+        assertThat(resources).hasSize(7);
         assertThat(result.getPagedResultsCookie()).isNull();
         assertThat(result.getTotalPagedResults()).isEqualTo(-1);
+
+        checkThatOrgUnitsExist(resources, "level1");
+
+        checkThatUsersExist(resources, 1,
+          "test1",
+          "test2",
+          "test3",
+          "test4",
+          "test5",
+          "test6"
+        );
     }
 
     @Test
-    public void testQueryNone() throws Exception {
+    public void testQueryAllWithSearchFilterAndNoSubtreeFlattening() throws Exception {
         final Connection connection = newConnection();
         final List<ResourceResponse> resources = new LinkedList<>();
-        final QueryResponse result = connection.query(newAuthConnectionContext(),
-                newQueryRequest("").setQueryFilter(QueryFilter.<JsonPointer> alwaysFalse()), resources);
+        final QueryResponse result =
+            connection.query(
+                newAuthConnectionContext(),
+                newQueryRequest("top-level-users").setQueryFilter(NO_FILTER),
+                resources);
+
+        assertThat(resources).hasSize(6);
+        assertThat(result.getPagedResultsCookie()).isNull();
+        assertThat(result.getTotalPagedResults()).isEqualTo(-1);
+
+        checkThatUsersExist(resources,
+          "test1",
+          "test2",
+          "test3",
+          "test4",
+          "test5",
+          "test6"
+        );
+    }
+
+    @Test
+    public void testQueryAllWithSubtreeFlatteningAndNoSearchFilter() throws Exception {
+        final Connection connection = newConnection();
+        final List<ResourceResponse> resources = new LinkedList<>();
+        final QueryResponse result =
+            connection.query(
+                newAuthConnectionContext(),
+                newQueryRequest("all-entries").setQueryFilter(NO_FILTER),
+                resources);
+
+        assertThat(resources).hasSize(10);
+        assertThat(result.getPagedResultsCookie()).isNull();
+        assertThat(result.getTotalPagedResults()).isEqualTo(-1);
+
+        checkThatOrgUnitsExist(resources,
+          "level1",
+          "level2"
+        );
+
+        checkThatUsersExist(resources, 2,
+          "sub2",
+          "sub1",
+          "test1",
+          "test2",
+          "test3",
+          "test4",
+          "test5",
+          "test6"
+        );
+    }
+
+    @Test
+    public void testQueryAllWithSubtreeFlatteningAndSearchFilter() throws Exception {
+        final Connection connection = newConnection();
+        final List<ResourceResponse> resources = new LinkedList<>();
+        final QueryResponse result =
+            connection.query(
+                newAuthConnectionContext(),
+                newQueryRequest("all-users").setQueryFilter(NO_FILTER),
+                resources);
+
+        assertThat(resources).hasSize(8);
+        assertThat(result.getPagedResultsCookie()).isNull();
+        assertThat(result.getTotalPagedResults()).isEqualTo(-1);
+
+        checkThatUsersExist(resources,
+          "sub2",
+          "sub1",
+          "test1",
+          "test2",
+          "test3",
+          "test4",
+          "test5",
+          "test6"
+        );
+    }
+
+    @Test
+    public void testQueryNoneWithNoSubtreeFlatteningAndNoSearchFilter() throws Exception {
+        final Connection connection = newConnection();
+        final List<ResourceResponse> resources = new LinkedList<>();
+        final QueryResponse result =
+            connection.query(
+                newAuthConnectionContext(),
+                newQueryRequest("").setQueryFilter(QueryFilter.<JsonPointer> alwaysFalse()),
+                resources);
+
         assertThat(resources).hasSize(0);
         assertThat(result.getPagedResultsCookie()).isNull();
         assertThat(result.getTotalPagedResults()).isEqualTo(-1);
     }
 
     @Test
-    public void testQueryPageResultsCookie() throws Exception {
+    public void testQueryNoneWithSearchFilterAndNoSubtreeFlattening() throws Exception {
+        final Connection connection = newConnection();
+        final List<ResourceResponse> resources = new LinkedList<>();
+        final QueryResponse result =
+            connection.query(
+                newAuthConnectionContext(),
+                newQueryRequest("top-level-users")
+                    .setQueryFilter(QueryFilter.<JsonPointer> alwaysFalse()),
+                resources);
+
+        assertThat(resources).hasSize(0);
+        assertThat(result.getPagedResultsCookie()).isNull();
+        assertThat(result.getTotalPagedResults()).isEqualTo(-1);
+    }
+
+    @Test
+    public void testQueryNoneWithSubtreeFlatteningAndNoSearchFilter() throws Exception {
+        final Connection connection = newConnection();
+        final List<ResourceResponse> resources = new LinkedList<>();
+        final QueryResponse result =
+            connection.query(
+                newAuthConnectionContext(),
+                newQueryRequest("all-entries")
+                    .setQueryFilter(QueryFilter.<JsonPointer> alwaysFalse()),
+                resources);
+
+        assertThat(resources).hasSize(0);
+        assertThat(result.getPagedResultsCookie()).isNull();
+        assertThat(result.getTotalPagedResults()).isEqualTo(-1);
+    }
+
+    @Test
+    public void testQueryNoneWithSubtreeFlatteningAndSearchFilter() throws Exception {
+        final Connection connection = newConnection();
+        final List<ResourceResponse> resources = new LinkedList<>();
+        final QueryResponse result = connection.query(newAuthConnectionContext(),
+          newQueryRequest("all-users").setQueryFilter(QueryFilter.<JsonPointer> alwaysFalse()), resources);
+
+        assertThat(resources).hasSize(0);
+        assertThat(result.getPagedResultsCookie()).isNull();
+        assertThat(result.getTotalPagedResults()).isEqualTo(-1);
+    }
+
+    @Test
+    public void testQueryPageResultsCookieWithNoSubtreeFlatteningAndNoSearchFilter()
+    throws Exception {
         final Connection connection = newConnection();
         final List<ResourceResponse> resources = new ArrayList<>();
 
         // Read first page.
-        QueryResponse result = connection.query(
-                newAuthConnectionContext(), newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2), resources);
+        QueryResponse result =
+            connection.query(
+                newAuthConnectionContext(),
+                newQueryRequest("")
+                    .setQueryFilter(NO_FILTER)
+                    .setPageSize(3),
+                resources);
+
         assertThat(result.getPagedResultsCookie()).isNotNull();
-        assertThat(resources).hasSize(2);
-        assertThat(resources.get(0).getId()).isEqualTo("test1");
-        assertThat(resources.get(1).getId()).isEqualTo("test2");
+        assertThat(resources).hasSize(3);
+
+        checkThatOrgUnitsExist(resources, "level1");
+
+        checkThatUsersExist(resources, 1,
+          "test1",
+          "test2");
 
         String cookie = result.getPagedResultsCookie();
+
         resources.clear();
 
         // Read second page.
-        result = connection.query(newAuthConnectionContext(),
-                newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2).setPagedResultsCookie(cookie), resources);
+        result =
+            connection.query(
+                newAuthConnectionContext(),
+                newQueryRequest("")
+                    .setQueryFilter(NO_FILTER)
+                    .setPageSize(3)
+                    .setPagedResultsCookie(cookie),
+                resources);
+
         assertThat(result.getPagedResultsCookie()).isNotNull();
-        assertThat(resources).hasSize(2);
-        assertThat(resources.get(0).getId()).isEqualTo("test3");
-        assertThat(resources.get(1).getId()).isEqualTo("test4");
+        assertThat(resources).hasSize(3);
+
+        checkThatUsersExist(resources,
+            "test3",
+            "test4",
+            "test5");
 
         cookie = result.getPagedResultsCookie();
+
         resources.clear();
 
         // Read third page.
-        result = connection.query(newAuthConnectionContext(),
-                newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2).setPagedResultsCookie(cookie), resources);
+        result =
+            connection.query(
+                newAuthConnectionContext(),
+                newQueryRequest("")
+                    .setQueryFilter(NO_FILTER)
+                    .setPageSize(3)
+                    .setPagedResultsCookie(cookie),
+                resources);
+
         assertThat(result.getPagedResultsCookie()).isNull();
         assertThat(resources).hasSize(1);
-        assertThat(resources.get(0).getId()).isEqualTo("test5");
+
+        checkThatUsersExist(resources,
+            "test6");
     }
 
     @Test
-    public void testQueryPageResultsIndexed() throws Exception {
+    public void testQueryPageResultsCookieWithSubtreeFlatteningAndSearchFilter() throws Exception {
         final Connection connection = newConnection();
         final List<ResourceResponse> resources = new ArrayList<>();
-        QueryResponse result = connection.query(newAuthConnectionContext(),
-                newQueryRequest("").setQueryFilter(NO_FILTER).setPageSize(2).setPagedResultsOffset(1), resources);
+
+        // Read first page.
+        QueryResponse result =
+            connection.query(
+                newAuthConnectionContext(),
+                newQueryRequest("all-users")
+                    .setQueryFilter(NO_FILTER)
+                    .setPageSize(5),
+                resources);
+
+        assertThat(result.getPagedResultsCookie()).isNotNull();
+        assertThat(resources).hasSize(5);
+
+        checkThatUsersExist(resources,
+            "sub2",
+            "sub1",
+            "test1",
+            "test2",
+            "test3");
+
+        String cookie = result.getPagedResultsCookie();
+
+        resources.clear();
+
+        // Read second page.
+        result =
+            connection.query(
+                newAuthConnectionContext(),
+                newQueryRequest("all-users")
+                    .setQueryFilter(NO_FILTER)
+                    .setPageSize(5)
+                    .setPagedResultsCookie(cookie),
+                resources);
+
+        assertThat(result.getPagedResultsCookie()).isNull();
+        assertThat(resources).hasSize(3);
+
+        checkThatUsersExist(resources,
+            "test4",
+            "test5",
+            "test6");
+
+        resources.clear();
+    }
+
+    @Test
+    public void testQueryPageResultsIndexedWithNoSubtreeFlatteningAndNoSearchFilter()
+    throws Exception {
+        final Connection connection = newConnection();
+        final List<ResourceResponse> resources = new ArrayList<>();
+
+        QueryResponse result =
+            connection.query(
+                newAuthConnectionContext(),
+                newQueryRequest("")
+                    .setQueryFilter(NO_FILTER)
+                    .setPageSize(2)
+                    .setPagedResultsOffset(1),
+                resources);
+
         assertThat(result.getPagedResultsCookie()).isNotNull();
         assertThat(resources).hasSize(2);
-        assertThat(resources.get(0).getId()).isEqualTo("test3");
-        assertThat(resources.get(1).getId()).isEqualTo("test4");
+
+        checkThatUsersExist(resources,
+            "test2",
+            "test3");
+    }
+
+    @Test
+    public void testQueryPageResultsIndexedWithSubtreeFlatteningAndSearchFilter() throws Exception {
+        final Connection connection = newConnection();
+        final List<ResourceResponse> resources = new ArrayList<>();
+
+        QueryResponse result =
+            connection.query(
+                newAuthConnectionContext(),
+                newQueryRequest("all-users")
+                    .setQueryFilter(NO_FILTER)
+                    .setPageSize(3)
+                    .setPagedResultsOffset(1),
+                resources);
+
+        assertThat(result.getPagedResultsCookie()).isNotNull();
+        assertThat(resources).hasSize(3);
+
+        checkThatUsersExist(resources,
+            "test2",
+            "test3",
+            "test4");
     }
 
     @Test(expectedExceptions = NotFoundException.class)
@@ -165,6 +425,7 @@
         final Context context = newAuthConnectionContext();
         final Connection connection = newConnection();
         final ResourceResponse resource = connection.delete(context, newDeleteRequest("/test1"));
+
         checkResourcesAreEqual(resource, getTestUser1(12345));
         connection.read(context, newReadRequest("/test1"));
     }
@@ -173,7 +434,9 @@
     public void testDeleteMVCCMatch() throws Exception {
         final Context context = newAuthConnectionContext();
         final Connection connection = newConnection();
-        final ResourceResponse resource = connection.delete(context, newDeleteRequest("/test1").setRevision("12345"));
+        final ResourceResponse resource =
+            connection.delete(context, newDeleteRequest("/test1").setRevision("12345"));
+
         checkResourcesAreEqual(resource, getTestUser1(12345));
         connection.read(context, newReadRequest("/test1"));
     }
@@ -182,6 +445,7 @@
     public void testDeleteMVCCNoMatch() throws Exception {
         final Context context = newAuthConnectionContext();
         final Connection connection = newConnection();
+
         connection.delete(context, newDeleteRequest("/test1").setRevision("12346"));
     }
 
@@ -189,6 +453,7 @@
     public void testDeleteNotFound() throws Exception {
         final Context context = newAuthConnectionContext();
         final Connection connection = newConnection();
+
         connection.delete(context, newDeleteRequest("/missing"));
     }
 
@@ -199,6 +464,7 @@
         final ResourceResponse resource1 = connection.patch(context,
                 newPatchRequest("/test1", add("/name/displayName", "changed")));
         checkResourcesAreEqual(resource1, getTestUser1Updated(12345));
+
         final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
         checkResourcesAreEqual(resource2, getTestUser1Updated(12345));
     }
@@ -209,6 +475,7 @@
         final Context context = newAuthConnectionContext(requests);
         final Connection connection = newConnection();
         final ResourceResponse resource1 = connection.patch(context, newPatchRequest("/test1"));
+
         checkResourcesAreEqual(resource1, getTestUser1(12345));
 
         /*
@@ -227,11 +494,16 @@
         final Context context = newAuthConnectionContext();
         final Connection connection = newConnection();
         final JsonValue newContent = getTestUser1(12345);
+
         newContent.put("description", asList("one", "two"));
+
         final ResourceResponse resource1 =
                 connection.patch(context,
-                                 newPatchRequest("/test1", add("/description", asList("one", "two"))));
+                                 newPatchRequest(
+                                    "/test1",
+                                    add("/description", asList("one", "two"))));
         checkResourcesAreEqual(resource1, newContent);
+
         final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
         checkResourcesAreEqual(resource2, newContent);
     }
@@ -241,26 +513,37 @@
         final Context context = newAuthConnectionContext();
         final Connection connection = newConnection();
         final JsonValue newContent = getTestUser1(12345);
+
         newContent.put("description", asList("one", "two"));
-        final ResourceResponse resource1 = connection.patch(
-            context, newPatchRequest("/test1", add("/description/-", "one"), add("/description/-", "two")));
+
+        final ResourceResponse resource1 =
+            connection.patch(
+                context,
+                newPatchRequest("/test1", add("/description/-", "one"),
+                                          add("/description/-", "two")));
         checkResourcesAreEqual(resource1, newContent);
+
         final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
         checkResourcesAreEqual(resource2, newContent);
     }
 
     @Test(expectedExceptions = BadRequestException.class)
     public void testPatchConstantAttribute() throws Exception {
-        newConnection().patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/schemas", asList("junk"))));
+        newConnection().patch(
+            newAuthConnectionContext(),
+            newPatchRequest("/test1", add("/schemas", asList("junk"))));
     }
 
     @Test
     public void testPatchDeleteOptionalAttribute() throws Exception {
         final Context context = newAuthConnectionContext();
         final Connection connection = newConnection();
+
         connection.patch(context, newPatchRequest("/test1", add("/description", asList("one", "two"))));
+
         final ResourceResponse resource1 = connection.patch(context, newPatchRequest("/test1", remove("/description")));
         checkResourcesAreEqual(resource1, getTestUser1(12345));
+
         final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
         checkResourcesAreEqual(resource2, getTestUser1(12345));
     }
@@ -270,34 +553,52 @@
         final Context context = newAuthConnectionContext();
         final Connection connection = newConnection();
         final JsonValue newContent = getTestUser1(12345);
+
         newContent.put("singleNumber", 100);
         newContent.put("multiNumber", asList(200, 300));
 
-        final ResourceResponse resource1 = connection.patch(context, newPatchRequest("/test1",
-            add("/singleNumber", 0),
-            add("/multiNumber", asList(100, 200)),
-            increment("/singleNumber", 100),
-            increment("/multiNumber", 100)));
+        final ResourceResponse resource1 =
+            connection.patch(
+                context,
+                newPatchRequest(
+                    "/test1",
+                    add("/singleNumber", 0),
+                    add("/multiNumber", asList(100, 200)),
+                    increment("/singleNumber", 100),
+                    increment("/multiNumber", 100)));
+
         checkResourcesAreEqual(resource1, newContent);
+
         final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
         checkResourcesAreEqual(resource2, newContent);
     }
 
     @Test(expectedExceptions = BadRequestException.class)
     public void testPatchMissingRequiredAttribute() throws Exception {
-        newConnection().patch(newAuthConnectionContext(), newPatchRequest("/test1", remove("/name/surname")));
+        newConnection().patch(
+            newAuthConnectionContext(),
+            newPatchRequest("/test1", remove("/name/surname")));
     }
 
     @Test
     public void testPatchModifyOptionalAttribute() throws Exception {
         final Connection connection = newConnection();
         final Context context = newAuthConnectionContext();
-        connection.patch(context, newPatchRequest("/test1", add("/description", asList("one", "two"))));
+
+        connection.patch(
+            context,
+            newPatchRequest("/test1", add("/description", asList("one", "two"))));
+
         final ResourceResponse resource1 =
-                connection.patch(context, newPatchRequest("/test1", add("/description", asList("three"))));
+            connection.patch(
+                context,
+                newPatchRequest("/test1", add("/description", asList("three"))));
+
         final JsonValue newContent = getTestUser1(12345);
+
         newContent.put("description", asList("one", "two", "three"));
         checkResourcesAreEqual(resource1, newContent);
+
         final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
         checkResourcesAreEqual(resource2, newContent);
     }
@@ -305,29 +606,42 @@
     @Test(expectedExceptions = NotSupportedException.class)
     public void testPatchMultiValuedAttributeIndexAppend() throws Exception {
         final Connection connection = newConnection();
-        connection.patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/description/0", "junk")));
+
+        connection.patch(
+            newAuthConnectionContext(),
+            newPatchRequest("/test1", add("/description/0", "junk")));
     }
 
     @Test(expectedExceptions = BadRequestException.class)
     public void testPatchMultiValuedAttributeIndexAppendWithList() throws Exception {
         final Connection connection = newConnection();
-        connection.patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/description/-",
-                asList("one", "two"))));
+
+        connection.patch(
+            newAuthConnectionContext(),
+            newPatchRequest("/test1", add("/description/-", asList("one", "two"))));
     }
 
     @Test(expectedExceptions = BadRequestException.class)
     public void testPatchMultiValuedAttributeWithSingleValue() throws Exception {
         final Connection connection = newConnection();
-        connection.patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/description", "one")));
+
+        connection.patch(
+            newAuthConnectionContext(),
+            newPatchRequest("/test1", add("/description", "one")));
     }
 
     @Test
     public void testPatchMVCCMatch() throws Exception {
         final Connection connection = newConnection();
         final Context context = newAuthConnectionContext();
-        final ResourceResponse resource1 = connection.patch(
-                context, newPatchRequest("/test1", add("/name/displayName", "changed")).setRevision("12345"));
+        final ResourceResponse resource1 =
+            connection.patch(
+                context,
+                newPatchRequest(
+                    "/test1", add("/name/displayName", "changed")).setRevision("12345"));
+
         checkResourcesAreEqual(resource1, getTestUser1Updated(12345));
+
         final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
         checkResourcesAreEqual(resource2, getTestUser1Updated(12345));
     }
@@ -335,22 +649,27 @@
     @Test(expectedExceptions = PreconditionFailedException.class)
     public void testPatchMVCCNoMatch() throws Exception {
         final Connection connection = newConnection();
+
         connection.patch(
-                newAuthConnectionContext(),
-                newPatchRequest("/test1", add("/name/displayName", "changed")).setRevision("12346"));
+            newAuthConnectionContext(),
+            newPatchRequest(
+                "/test1",
+                add("/name/displayName", "changed")).setRevision("12346"));
     }
 
     @Test(expectedExceptions = NotFoundException.class)
     public void testPatchNotFound() throws Exception {
         newConnection().patch(
-                newAuthConnectionContext(),
-                newPatchRequest("/missing", add("/name/displayName", "changed")));
+            newAuthConnectionContext(),
+            newPatchRequest("/missing", add("/name/displayName", "changed")));
     }
 
     @Test(expectedExceptions = BadRequestException.class)
     public void testPatchReadOnlyAttribute() throws Exception {
         // Etag is read-only.
-        newConnection().patch(newAuthConnectionContext(), newPatchRequest("/test1", add("_rev", "99999")));
+        newConnection().patch(
+            newAuthConnectionContext(),
+            newPatchRequest("/test1", add("_rev", "99999")));
     }
 
     @Test
@@ -363,9 +682,15 @@
             field("_rev", "12345"),
             field("name", object(field("displayName", "Humpty"),
                                  field("surname", "Dumpty")))));
-        final ResourceResponse resource1 = connection.patch(context, newPatchRequest("/test1",
-            replace("/name", object(field("displayName", "Humpty"), field("surname", "Dumpty")))));
+
+        final ResourceResponse resource1 =
+            connection.patch(
+                context,
+                newPatchRequest("/test1",
+                replace("/name", object(field("displayName", "Humpty"),
+                                        field("surname",     "Dumpty")))));
         checkResourcesAreEqual(resource1, expected);
+
         final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
         checkResourcesAreEqual(resource2, expected);
     }
@@ -374,18 +699,23 @@
     public void testPatchReplaceWholeObject() throws Exception {
         final Connection connection = newConnection();
         final Context context = newAuthConnectionContext();
-        final JsonValue newContent = json(object(
-            field("name", object(field("displayName", "Humpty"),
-                                 field("surname", "Dumpty")))));
-        final JsonValue expected = json(object(
-            field("schemas", asList("urn:scim:schemas:core:1.0")),
-            field("_id", "test1"),
-            field("_rev", "12345"),
-            field("name", object(field("displayName", "Humpty"),
-                                 field("surname", "Dumpty")))));
+        final JsonValue newContent =
+            json(object(
+                field("name", object(field("displayName", "Humpty"),
+                                     field("surname",     "Dumpty")))));
+
+        final JsonValue expected =
+            json(object(
+                field("schemas", asList("urn:scim:schemas:core:1.0")),
+                field("_id", "test1"),
+                field("_rev", "12345"),
+                field("name", object(field("displayName", "Humpty"),
+                                     field("surname",     "Dumpty")))));
+
         final ResourceResponse resource1 =
-                connection.patch(context, newPatchRequest("/test1", replace("/", newContent)));
+            connection.patch(context, newPatchRequest("/test1", replace("/", newContent)));
         checkResourcesAreEqual(resource1, expected);
+
         final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
         checkResourcesAreEqual(resource2, expected);
     }
@@ -393,32 +723,48 @@
     @Test(expectedExceptions = BadRequestException.class)
     public void testPatchSingleValuedAttributeIndexAppend() throws Exception {
         final Connection connection = newConnection();
-        connection.patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/name/surname/-", "junk")));
+
+        connection.patch(
+            newAuthConnectionContext(),
+            newPatchRequest("/test1", add("/name/surname/-", "junk")));
     }
 
     @Test(expectedExceptions = NotSupportedException.class)
     public void testPatchSingleValuedAttributeIndexNumber() throws Exception {
         final Connection connection = newConnection();
-        connection.patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/name/surname/0", "junk")));
+
+        connection.patch(
+            newAuthConnectionContext(),
+            newPatchRequest("/test1", add("/name/surname/0", "junk")));
     }
 
     @Test(expectedExceptions = BadRequestException.class)
     public void testPatchSingleValuedAttributeWithMultipleValues() throws Exception {
         final Connection connection = newConnection();
-        connection.patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/name/surname", asList("black",
-                "white"))));
+
+        connection.patch(
+            newAuthConnectionContext(),
+            newPatchRequest(
+                "/test1",
+                add("/name/surname", asList("black", "white"))));
     }
 
     @Test(expectedExceptions = BadRequestException.class)
     public void testPatchUnknownAttribute() throws Exception {
         final Connection connection = newConnection();
-        connection.patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/dummy", "junk")));
+
+        connection.patch(
+            newAuthConnectionContext(),
+            newPatchRequest("/test1", add("/dummy", "junk")));
     }
 
     @Test(expectedExceptions = BadRequestException.class)
     public void testPatchUnknownSubAttribute() throws Exception {
         final Connection connection = newConnection();
-        connection.patch(newAuthConnectionContext(), newPatchRequest("/test1", add("/description/dummy", "junk")));
+
+        connection.patch(
+            newAuthConnectionContext(),
+            newPatchRequest("/test1", add("/description/dummy", "junk")));
     }
 
     @Test(expectedExceptions = BadRequestException.class)
@@ -430,7 +776,9 @@
 
     @Test
     public void testRead() throws Exception {
-        final ResourceResponse resource = newConnection().read(newAuthConnectionContext(), newReadRequest("/test1"));
+        final ResourceResponse resource =
+            newConnection().read(newAuthConnectionContext(), newReadRequest("/test1"));
+
         checkResourcesAreEqual(resource, getTestUser1(12345));
     }
 
@@ -441,15 +789,19 @@
 
     @Test
     public void testReadSelectAllFields() throws Exception {
-        final ResourceResponse resource = newConnection().read(newAuthConnectionContext(),
-                                                               newReadRequest("/test1").addField("/"));
+        final ResourceResponse resource =
+            newConnection().read(
+                newAuthConnectionContext(), newReadRequest("/test1").addField("/"));
+
         checkResourcesAreEqual(resource, getTestUser1(12345));
     }
 
     @Test
     public void testReadSelectPartial() throws Exception {
-        final ResourceResponse resource = newConnection().read(
-            newAuthConnectionContext(), newReadRequest("/test1").addField("/name/surname"));
+        final ResourceResponse resource =
+            newConnection().read(
+                newAuthConnectionContext(), newReadRequest("/test1").addField("/name/surname"));
+
         assertThat(resource.getId()).isEqualTo("test1");
         assertThat(resource.getRevision()).isEqualTo("12345");
         assertThat(resource.getContent().get("_id").asString()).isNull();
@@ -461,8 +813,10 @@
     /** Disabled - see CREST-86 (Should JSON resource fields be case insensitive?) */
     @Test(enabled = false)
     public void testReadSelectPartialInsensitive() throws Exception {
-        final ResourceResponse resource = newConnection().read(
-            newAuthConnectionContext(), newReadRequest("/test1").addField("/name/SURNAME"));
+        final ResourceResponse resource =
+            newConnection().read(
+                newAuthConnectionContext(), newReadRequest("/test1").addField("/name/SURNAME"));
+
         assertThat(resource.getId()).isEqualTo("test1");
         assertThat(resource.getRevision()).isEqualTo("12345");
         assertThat(resource.getContent().get("_id").asString()).isNull();
@@ -476,8 +830,9 @@
         final Connection connection = newConnection();
         final Context context = newAuthConnectionContext();
         final ResourceResponse resource1 = connection.update(
-                context, newUpdateRequest("/test1", getTestUser1Updated(12345)));
+            context, newUpdateRequest("/test1", getTestUser1Updated(12345)));
         checkResourcesAreEqual(resource1, getTestUser1Updated(12345));
+
         final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
         checkResourcesAreEqual(resource2, getTestUser1Updated(12345));
     }
@@ -487,14 +842,15 @@
         final List<Request> requests = new LinkedList<>();
         final Connection connection = newConnection();
         final Context context = newAuthConnectionContext(requests);
-        final ResourceResponse resource1 = connection.update(context, newUpdateRequest("/test1", getTestUser1(12345)));
+        final ResourceResponse resource1 =
+            connection.update(context, newUpdateRequest("/test1", getTestUser1(12345)));
 
         // Check that no modify operation was sent
         // (only a single search should be sent in order to get the current resource).
         assertThat(requests).hasSize(1);
         assertThat(requests.get(0)).isInstanceOf(SearchRequest.class);
-
         checkResourcesAreEqual(resource1, getTestUser1(12345));
+
         final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
         checkResourcesAreEqual(resource2, getTestUser1(12345));
     }
@@ -504,9 +860,13 @@
         final Connection connection = newConnection();
         final Context context = newAuthConnectionContext();
         final JsonValue newContent = getTestUser1Updated(12345);
+
         newContent.put("description", asList("one", "two"));
-        final ResourceResponse resource1 = connection.update(context, newUpdateRequest("/test1", newContent));
+
+        final ResourceResponse resource1 =
+            connection.update(context, newUpdateRequest("/test1", newContent));
         checkResourcesAreEqual(resource1, newContent);
+
         final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
         checkResourcesAreEqual(resource2, newContent);
     }
@@ -515,6 +875,7 @@
     public void testUpdateConstantAttribute() throws Exception {
         final Connection connection = newConnection();
         final JsonValue newContent = getTestUser1Updated(12345);
+
         newContent.put("schemas", asList("junk"));
         connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", newContent));
     }
@@ -524,11 +885,15 @@
         final Connection connection = newConnection();
         final Context context = newAuthConnectionContext();
         final JsonValue newContent = getTestUser1Updated(12345);
+
         newContent.put("description", asList("one", "two"));
         connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", newContent));
         newContent.remove("description");
-        final ResourceResponse resource1 = connection.update(context, newUpdateRequest("/test1", newContent));
+
+        final ResourceResponse resource1 =
+          connection.update(context, newUpdateRequest("/test1", newContent));
         checkResourcesAreEqual(resource1, newContent);
+
         final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
         checkResourcesAreEqual(resource2, newContent);
     }
@@ -537,6 +902,7 @@
     public void testUpdateMissingRequiredAttribute() throws Exception {
         final Connection connection = newConnection();
         final JsonValue newContent = getTestUser1Updated(12345);
+
         newContent.get("name").remove("surname");
         connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", newContent));
     }
@@ -546,11 +912,15 @@
         final Connection connection = newConnection();
         final Context context = newAuthConnectionContext();
         final JsonValue newContent = getTestUser1Updated(12345);
+
         newContent.put("description", asList("one", "two"));
         connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", newContent));
         newContent.put("description", asList("three"));
-        final ResourceResponse resource1 = connection.update(context, newUpdateRequest("/test1", newContent));
+
+        final ResourceResponse resource1 =
+            connection.update(context, newUpdateRequest("/test1", newContent));
         checkResourcesAreEqual(resource1, newContent);
+
         final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
         checkResourcesAreEqual(resource2, newContent);
     }
@@ -560,8 +930,11 @@
         final Connection connection = newConnection();
         final Context context = newAuthConnectionContext();
         final ResourceResponse resource1 =
-                connection.update(context, newUpdateRequest("/test1", getTestUser1Updated(12345)).setRevision("12345"));
+            connection.update(
+                context,
+                newUpdateRequest("/test1", getTestUser1Updated(12345)).setRevision("12345"));
         checkResourcesAreEqual(resource1, getTestUser1Updated(12345));
+
         final ResourceResponse resource2 = connection.read(context, newReadRequest("/test1"));
         checkResourcesAreEqual(resource2, getTestUser1Updated(12345));
     }
@@ -569,27 +942,36 @@
     @Test(expectedExceptions = PreconditionFailedException.class)
     public void testUpdateMVCCNoMatch() throws Exception {
         final Connection connection = newConnection();
-        connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", getTestUser1Updated(12345))
-                .setRevision("12346"));
+
+        connection.update(
+            newAuthConnectionContext(),
+            newUpdateRequest("/test1", getTestUser1Updated(12345)).setRevision("12346"));
     }
 
     @Test(expectedExceptions = NotFoundException.class)
     public void testUpdateNotFound() throws Exception {
         final Connection connection = newConnection();
-        connection.update(newAuthConnectionContext(), newUpdateRequest("/missing", getTestUser1Updated(12345)));
+
+        connection.update(
+            newAuthConnectionContext(),
+            newUpdateRequest("/missing", getTestUser1Updated(12345)));
     }
 
     @Test(expectedExceptions = BadRequestException.class)
     public void testUpdateReadOnlyAttribute() throws Exception {
         final Connection connection = newConnection();
+
         // Etag is read-only.
-        connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", getTestUser1Updated(99999)));
+        connection.update(
+            newAuthConnectionContext(),
+            newUpdateRequest("/test1", getTestUser1Updated(99999)));
     }
 
     @Test(expectedExceptions = BadRequestException.class)
     public void testUpdateSingleValuedAttributeWithMultipleValues() throws Exception {
         final Connection connection = newConnection();
         final JsonValue newContent = getTestUser1Updated(12345);
+
         newContent.put("surname", asList("black", "white"));
         connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", newContent));
     }
@@ -598,6 +980,7 @@
     public void testUpdateUnknownAttribute() throws Exception {
         final Connection connection = newConnection();
         final JsonValue newContent = getTestUser1Updated(12345);
+
         newContent.add("dummy", "junk");
         connection.update(newAuthConnectionContext(), newUpdateRequest("/test1", newContent));
     }
@@ -607,40 +990,120 @@
     }
 
     private Rest2Ldap usersApi() throws IOException {
-        return rest2Ldap(defaultOptions(),
-                         resource("api").subResource(collectionOf("user").dnTemplate("dc=test")
-                                                                         .useClientDnNaming("uid")),
-                         resource("user").objectClasses("top", "person")
-                                         .property("schemas", constant(asList("urn:scim:schemas:core:1.0")))
-                                         .property("_id", simple("uid").isRequired(true).writability(CREATE_ONLY))
-                                         .property("name", object().property("displayName",
-                                                                             simple("cn").isRequired(true))
-                                                                   .property("surname", simple("sn").isRequired(true)))
-                                         .property("_rev", simple("etag").isRequired(true).writability(READ_ONLY))
-                                         .property("description", simple("description").isMultiValued(true))
-                                         .property("singleNumber",
-                                                   simple("singleNumber").decoder(byteStringToInteger()))
-                                         .property("multiNumber",
-                                                   simple("multiNumber").isMultiValued(true)
-                                                                        .decoder(byteStringToInteger())));
+        return rest2Ldap(
+            defaultOptions(),
+            resource("api")
+                .subResource(
+                    collectionOf("user")
+                        .dnTemplate("dc=test")
+                        .useClientDnNaming("uid"))
+                .subResource(
+                    collectionOf("user")
+                        .urlTemplate("top-level-users")
+                        .dnTemplate("dc=test")
+                        .useClientDnNaming("uid")
+                        .baseSearchFilter("(objectClass=person)"))
+                .subResource(
+                    collectionOf("user")
+                        .urlTemplate("all-entries")
+                        .dnTemplate("dc=test")
+                        .useClientDnNaming("uid")
+                        .isReadOnly(true)
+                        .flattenSubtree(true))
+                .subResource(
+                    collectionOf("user")
+                        .urlTemplate("all-users")
+                        .dnTemplate("dc=test")
+                        .useClientDnNaming("uid")
+                        .isReadOnly(true)
+                        .flattenSubtree(true)
+                        .baseSearchFilter("(objectClass=person)")),
+            resource("user")
+                .objectClasses("top", "person")
+                .property(
+                    "schemas",
+                    constant(asList("urn:scim:schemas:core:1.0")))
+                .property(
+                    "_id",
+                    simple("uid").isRequired(true).writability(CREATE_ONLY))
+                .property(
+                    "_ou",
+                    simple("ou").isRequired(false).writability(CREATE_ONLY))
+                .property(
+                    "name",
+                    object()
+                      .property("displayName", simple("cn").isRequired(true))
+                      .property("surname",     simple("sn").isRequired(true)))
+                .property(
+                    "_rev",
+                    simple("etag").isRequired(true).writability(READ_ONLY))
+                .property(
+                    "description",
+                    simple("description").isMultiValued(true))
+                .property(
+                    "singleNumber",
+                    simple("singleNumber").decoder(byteStringToInteger()))
+                .property(
+                    "multiNumber",
+                    simple("multiNumber").isMultiValued(true).decoder(byteStringToInteger()))
+        );
     }
 
     private void checkResourcesAreEqual(final ResourceResponse actual, final JsonValue expected) {
         final ResourceResponse expectedResource = asResource(expected);
+
         assertThat(actual.getId()).isEqualTo(expectedResource.getId());
         assertThat(actual.getRevision()).isEqualTo(expectedResource.getRevision());
-        assertThat(actual.getContent().getObject()).isEqualTo(expectedResource.getContent().getObject());
+
+        assertThat(actual.getContent().getObject())
+            .isEqualTo(expectedResource.getContent().getObject());
+    }
+
+    private void checkThatOrgUnitsExist(final List<ResourceResponse> resources,
+                                        final String... orgUnitIds) {
+        checkThatOrgUnitsExist(resources, 0, orgUnitIds);
+    }
+
+    private void checkThatOrgUnitsExist(final List<ResourceResponse> resources,
+                                        final int startingIndex,
+                                        final String... expectedOrgUnitIds) {
+        for (int orgUnitIndex = 0; orgUnitIndex < expectedOrgUnitIds.length; ++orgUnitIndex) {
+            final ResourceResponse resource = resources.get(startingIndex + orgUnitIndex);
+            final JsonValue orgUnitId = resource.getContent().get("_ou");
+
+            assertThat(orgUnitId).isNotNull();
+            assertThat(orgUnitId.asString()).isEqualTo(expectedOrgUnitIds[orgUnitIndex]);
+        }
+    }
+
+    private void checkThatUsersExist(final List<ResourceResponse> resources,
+                                     final String... expectedUserIds) {
+        checkThatUsersExist(resources, 0, expectedUserIds);
+    }
+
+    private void checkThatUsersExist(final List<ResourceResponse> resources,
+                                     final int startingIndex, final String... expectedUserIds) {
+        for (int userIndex = 0; userIndex < expectedUserIds.length; ++userIndex) {
+            final ResourceResponse resource = resources.get(startingIndex + userIndex);
+
+            assertThat(resource.getContent().get("_ou").isNull());
+            assertThat(resource.getId()).isEqualTo(expectedUserIds[userIndex]);
+        }
     }
 
     private AuthenticatedConnectionContext newAuthConnectionContext() throws IOException {
         return newAuthConnectionContext(new ArrayList<Request>());
     }
 
-    private AuthenticatedConnectionContext newAuthConnectionContext(List<Request> requests) throws IOException {
-        return new AuthenticatedConnectionContext(ctx(), getConnectionFactory(requests).getConnection());
+    private AuthenticatedConnectionContext newAuthConnectionContext(List<Request> requests)
+    throws IOException {
+        return new AuthenticatedConnectionContext(
+            ctx(),
+            getConnectionFactory(requests).getConnection());
     }
 
-    private ConnectionFactory getConnectionFactory(final List<Request> requests) throws IOException {
+    private ConnectionFactory getConnectionFactory(final List<Request> requests)
+    throws IOException {
         // @formatter:off
         final MemoryBackend backend =
                 new MemoryBackend(new LDIFEntryReader(
@@ -692,7 +1155,46 @@
                         "userpassword: password",
                         "cn: test user 5",
                         "sn: user 5",
-                        "etag: 55555"
+                        "etag: 55555",
+                        "",
+                        "dn: uid=test6,dc=test",
+                        "objectClass: top",
+                        "objectClass: person",
+                        "uid: test6",
+                        "userpassword: password",
+                        "cn: test user 6",
+                        "sn: user 6",
+                        "etag: 66666",
+                        "",
+                        "dn: ou=level1,dc=test",
+                        "objectClass: top",
+                        "objectClass: organizationalUnit",
+                        "ou: level1",
+                        "etag: 77777",
+                        "",
+                        "dn: uid=sub1,ou=level1,dc=test",
+                        "objectClass: top",
+                        "objectClass: person",
+                        "uid: sub1",
+                        "userpassword: password",
+                        "cn: test user level 1",
+                        "sn: user 7",
+                        "etag: 88888",
+                        "",
+                        "dn: ou=level2,ou=level1,dc=test",
+                        "objectClass: top",
+                        "objectClass: organizationalUnit",
+                        "ou: level2",
+                        "etag: 99999",
+                        "",
+                        "dn: uid=sub2,ou=level2,ou=level1,dc=test",
+                        "objectClass: top",
+                        "objectClass: person",
+                        "uid: sub2",
+                        "userpassword: password",
+                        "cn: test user level 2",
+                        "sn: user 8",
+                        "etag: 86753"
                 ));
         // @formatter:on
 
@@ -768,10 +1270,15 @@
 
             @Override
             public void handleSearch(RequestContext requestContext, SearchRequest request,
-                IntermediateResponseHandler intermediateResponseHandler, SearchResultHandler entryHandler,
+                IntermediateResponseHandler intermediateResponseHandler,
+                SearchResultHandler entryHandler,
                 LdapResultHandler<Result> resultHandler) {
                 requests.add(request);
-                handler.handleSearch(requestContext, request, intermediateResponseHandler, entryHandler,
+                handler.handleSearch(
+                    requestContext,
+                    request,
+                    intermediateResponseHandler,
+                    entryHandler,
                     resultHandler);
             }
 

--
Gitblit v1.10.0