/* * 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 2013 ForgeRock AS. */ package org.forgerock.opendj.rest2ldap; import static java.util.Arrays.asList; import static org.fest.assertions.Assertions.assertThat; import static org.fest.assertions.Fail.fail; import static org.forgerock.json.fluent.JsonValue.field; import static org.forgerock.json.fluent.JsonValue.json; import static org.forgerock.json.fluent.JsonValue.object; import static org.forgerock.json.resource.PatchOperation.add; import static org.forgerock.json.resource.PatchOperation.remove; import static org.forgerock.json.resource.PatchOperation.replace; import static org.forgerock.json.resource.Requests.newDeleteRequest; import static org.forgerock.json.resource.Requests.newPatchRequest; import static org.forgerock.json.resource.Requests.newReadRequest; import static org.forgerock.json.resource.Requests.newUpdateRequest; import static org.forgerock.json.resource.Resources.newCollection; import static org.forgerock.json.resource.Resources.newInternalConnection; import static org.forgerock.opendj.ldap.Connections.newInternalConnectionFactory; import static org.forgerock.opendj.rest2ldap.Rest2LDAP.constant; import static org.forgerock.opendj.rest2ldap.Rest2LDAP.object; import static org.forgerock.opendj.rest2ldap.Rest2LDAP.simple; import static org.forgerock.opendj.rest2ldap.TestUtils.asResource; import static org.forgerock.opendj.rest2ldap.TestUtils.content; import static org.forgerock.opendj.rest2ldap.TestUtils.ctx; import java.io.IOException; import org.forgerock.json.fluent.JsonValue; import org.forgerock.json.resource.BadRequestException; import org.forgerock.json.resource.Connection; import org.forgerock.json.resource.NotFoundException; import org.forgerock.json.resource.PreconditionFailedException; import org.forgerock.json.resource.RequestHandler; import org.forgerock.json.resource.Resource; import org.forgerock.opendj.ldap.ConnectionFactory; import org.forgerock.opendj.ldap.MemoryBackend; import org.forgerock.opendj.ldif.LDIFEntryReader; import org.forgerock.opendj.rest2ldap.Rest2LDAP.Builder; import org.forgerock.testng.ForgeRockTestCase; import org.testng.annotations.Test; /** * Tests that CREST requests are correctly mapped to LDAP. */ @SuppressWarnings({ "javadoc" }) @Test public final class BasicRequestsTest extends ForgeRockTestCase { // FIXME: we need to test the request handler, not internal connections, // so that we can check that the request handler is returning everything. // FIXME: factor out test for re-use as common test suite (e.g. for InMemoryBackend). @Test public void testDelete() throws Exception { final RequestHandler handler = newCollection(builder().build()); final Connection connection = newInternalConnection(handler); final Resource resource = connection.delete(ctx(), newDeleteRequest("/test1")); checkResourcesAreEqual(resource, getTestUser1(12345)); try { connection.read(ctx(), newReadRequest("/test1")); fail("Read succeeded unexpectedly"); } catch (final NotFoundException e) { // Expected. } } @Test public void testDeleteMVCCMatch() throws Exception { final RequestHandler handler = newCollection(builder().build()); final Connection connection = newInternalConnection(handler); final Resource resource = connection.delete(ctx(), newDeleteRequest("/test1").setRevision("12345")); checkResourcesAreEqual(resource, getTestUser1(12345)); try { connection.read(ctx(), newReadRequest("/test1")); fail("Read succeeded unexpectedly"); } catch (final NotFoundException e) { // Expected. } } @Test(expectedExceptions = PreconditionFailedException.class) public void testDeleteMVCCNoMatch() throws Exception { final RequestHandler handler = newCollection(builder().build()); final Connection connection = newInternalConnection(handler); connection.delete(ctx(), newDeleteRequest("/test1").setRevision("12346")); } @Test(expectedExceptions = NotFoundException.class) public void testDeleteNotFound() throws Exception { final RequestHandler handler = newCollection(builder().build()); final Connection connection = newInternalConnection(handler); connection.delete(ctx(), newDeleteRequest("/missing")); } @Test public void testPatch() throws Exception { final RequestHandler handler = newCollection(builder().build()); final Connection connection = newInternalConnection(handler); final Resource resource1 = connection.patch(ctx(), newPatchRequest("/test1", add("/name/displayName", "changed"))); checkResourcesAreEqual(resource1, getTestUser1Updated(12345)); final Resource resource2 = connection.read(ctx(), newReadRequest("/test1")); checkResourcesAreEqual(resource2, getTestUser1Updated(12345)); } @Test public void testPatchReplaceWholeObject() throws Exception { final RequestHandler handler = newCollection(builder().build()); final Connection connection = newInternalConnection(handler); 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 Resource resource1 = connection.patch(ctx(), newPatchRequest("/test1", replace("/name", JsonValue .object(field("displayName", "Humpty"), field("surname", "Dumpty"))))); checkResourcesAreEqual(resource1, expected); final Resource resource2 = connection.read(ctx(), newReadRequest("/test1")); checkResourcesAreEqual(resource2, expected); } @Test public void testPatchAddOptionalAttribute() throws Exception { final RequestHandler handler = newCollection(builder().build()); final Connection connection = newInternalConnection(handler); final JsonValue newContent = getTestUser1(12345); newContent.put("description", asList("one", "two")); final Resource resource1 = connection.patch(ctx(), newPatchRequest("/test1", add("/description", asList("one", "two")))); checkResourcesAreEqual(resource1, newContent); final Resource resource2 = connection.read(ctx(), newReadRequest("/test1")); checkResourcesAreEqual(resource2, newContent); } @Test(expectedExceptions = BadRequestException.class) public void testPatchConstantAttribute() throws Exception { final RequestHandler handler = newCollection(builder().build()); final Connection connection = newInternalConnection(handler); connection.patch(ctx(), newPatchRequest("/test1", add("/schemas", asList("junk")))); } @Test public void testPatchDeleteOptionalAttribute() throws Exception { final RequestHandler handler = newCollection(builder().build()); final Connection connection = newInternalConnection(handler); connection.patch(ctx(), newPatchRequest("/test1", add("/description", asList("one", "two")))); final Resource resource1 = connection.patch(ctx(), newPatchRequest("/test1", remove("/description"))); checkResourcesAreEqual(resource1, getTestUser1(12345)); final Resource resource2 = connection.read(ctx(), newReadRequest("/test1")); checkResourcesAreEqual(resource2, getTestUser1(12345)); } @Test(expectedExceptions = BadRequestException.class) public void testPatchMissingRequiredAttribute() throws Exception { final RequestHandler handler = newCollection(builder().build()); final Connection connection = newInternalConnection(handler); connection.patch(ctx(), newPatchRequest("/test1", remove("/name/surname"))); } @Test public void testPatchModifyOptionalAttribute() throws Exception { final RequestHandler handler = newCollection(builder().build()); final Connection connection = newInternalConnection(handler); connection.patch(ctx(), newPatchRequest("/test1", add("/description", asList("one", "two")))); final Resource resource1 = connection.patch(ctx(), newPatchRequest("/test1", add("/description", asList("three")))); final JsonValue newContent = getTestUser1(12345); newContent.put("description", asList("one", "two", "three")); checkResourcesAreEqual(resource1, newContent); final Resource resource2 = connection.read(ctx(), newReadRequest("/test1")); checkResourcesAreEqual(resource2, newContent); } @Test public void testPatchMVCCMatch() throws Exception { final RequestHandler handler = newCollection(builder().build()); final Connection connection = newInternalConnection(handler); final Resource resource1 = connection.patch(ctx(), newPatchRequest("/test1", add("/name/displayName", "changed")).setRevision("12345")); checkResourcesAreEqual(resource1, getTestUser1Updated(12345)); final Resource resource2 = connection.read(ctx(), newReadRequest("/test1")); checkResourcesAreEqual(resource2, getTestUser1Updated(12345)); } @Test(expectedExceptions = PreconditionFailedException.class) public void testPatchMVCCNoMatch() throws Exception { final RequestHandler handler = newCollection(builder().build()); final Connection connection = newInternalConnection(handler); connection.patch(ctx(), newPatchRequest("/test1", add("/name/displayName", "changed")) .setRevision("12346")); } @Test(expectedExceptions = NotFoundException.class) public void testPatchNotFound() throws Exception { final RequestHandler handler = newCollection(builder().build()); final Connection connection = newInternalConnection(handler); connection.patch(ctx(), newPatchRequest("/missing", add("/name/displayName", "changed"))); } @Test(expectedExceptions = BadRequestException.class) public void testPatchReadOnlyAttribute() throws Exception { final RequestHandler handler = newCollection(builder().build()); final Connection connection = newInternalConnection(handler); // Etag is read-only. connection.patch(ctx(), newPatchRequest("/test1", add("_rev", "99999"))); } @Test(expectedExceptions = BadRequestException.class) public void testPatchSingleValuedAttributeWithMultipleValues() throws Exception { final RequestHandler handler = newCollection(builder().build()); final Connection connection = newInternalConnection(handler); connection.patch(ctx(), newPatchRequest("/test1", add("/name/surname", asList("black", "white")))); } @Test(expectedExceptions = BadRequestException.class) public void testPatchUnknownAttribute() throws Exception { final RequestHandler handler = newCollection(builder().build()); final Connection connection = newInternalConnection(handler); final JsonValue newContent = getTestUser1Updated(12345); newContent.add("dummy", "junk"); connection.patch(ctx(), newPatchRequest("/test1", add("/dummy", "junk"))); } @Test public void testRead() throws Exception { final RequestHandler handler = newCollection(builder().build()); final Resource resource = newInternalConnection(handler).read(ctx(), newReadRequest("/test1")); checkResourcesAreEqual(resource, getTestUser1(12345)); } @Test(expectedExceptions = NotFoundException.class) public void testReadNotFound() throws Exception { final RequestHandler handler = newCollection(builder().build()); newInternalConnection(handler).read(ctx(), newReadRequest("/missing")); } @Test public void testReadSelectAllFields() throws Exception { final RequestHandler handler = newCollection(builder().build()); final Resource resource = newInternalConnection(handler).read(ctx(), newReadRequest("/test1").addField("/")); checkResourcesAreEqual(resource, getTestUser1(12345)); } @Test public void testReadSelectPartial() throws Exception { final RequestHandler handler = newCollection(builder().build()); final Resource resource = newInternalConnection(handler).read(ctx(), newReadRequest("/test1").addField("/name/surname")); assertThat(resource.getId()).isEqualTo("test1"); assertThat(resource.getRevision()).isEqualTo("12345"); assertThat(resource.getContent().get("_id").asString()).isNull(); assertThat(resource.getContent().get("name").asMap()).isNull(); assertThat(resource.getContent().get("surname").asString()).isEqualTo("user 1"); assertThat(resource.getContent().get("_rev").asString()).isNull(); } // Disabled - see CREST-86 (Should JSON resource fields be case insensitive?) @Test(enabled = false) public void testReadSelectPartialInsensitive() throws Exception { final RequestHandler handler = newCollection(builder().build()); final Resource resource = newInternalConnection(handler).read(ctx(), newReadRequest("/test1").addField("/name/SURNAME")); assertThat(resource.getId()).isEqualTo("test1"); assertThat(resource.getRevision()).isEqualTo("12345"); assertThat(resource.getContent().get("_id").asString()).isNull(); assertThat(resource.getContent().get("/name/displayName").asString()).isNull(); assertThat(resource.getContent().get("/name/surname").asString()).isEqualTo("user 1"); assertThat(resource.getContent().get("_rev").asString()).isNull(); } @Test public void testUpdate() throws Exception { final RequestHandler handler = newCollection(builder().build()); final Connection connection = newInternalConnection(handler); final Resource resource1 = connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345))); checkResourcesAreEqual(resource1, getTestUser1Updated(12345)); final Resource resource2 = connection.read(ctx(), newReadRequest("/test1")); checkResourcesAreEqual(resource2, getTestUser1Updated(12345)); } @Test public void testUpdateAddOptionalAttribute() throws Exception { final RequestHandler handler = newCollection(builder().build()); final Connection connection = newInternalConnection(handler); final JsonValue newContent = getTestUser1Updated(12345); newContent.put("description", asList("one", "two")); final Resource resource1 = connection.update(ctx(), newUpdateRequest("/test1", newContent)); checkResourcesAreEqual(resource1, newContent); final Resource resource2 = connection.read(ctx(), newReadRequest("/test1")); checkResourcesAreEqual(resource2, newContent); } @Test(expectedExceptions = BadRequestException.class) public void testUpdateConstantAttribute() throws Exception { final RequestHandler handler = newCollection(builder().build()); final Connection connection = newInternalConnection(handler); final JsonValue newContent = getTestUser1Updated(12345); newContent.put("schemas", asList("junk")); connection.update(ctx(), newUpdateRequest("/test1", newContent)); } @Test public void testUpdateDeleteOptionalAttribute() throws Exception { final RequestHandler handler = newCollection(builder().build()); final Connection connection = newInternalConnection(handler); final JsonValue newContent = getTestUser1Updated(12345); newContent.put("description", asList("one", "two")); connection.update(ctx(), newUpdateRequest("/test1", newContent)); newContent.remove("description"); final Resource resource1 = connection.update(ctx(), newUpdateRequest("/test1", newContent)); checkResourcesAreEqual(resource1, newContent); final Resource resource2 = connection.read(ctx(), newReadRequest("/test1")); checkResourcesAreEqual(resource2, newContent); } @Test(expectedExceptions = BadRequestException.class) public void testUpdateMissingRequiredAttribute() throws Exception { final RequestHandler handler = newCollection(builder().build()); final Connection connection = newInternalConnection(handler); final JsonValue newContent = getTestUser1Updated(12345); newContent.get("name").remove("surname"); connection.update(ctx(), newUpdateRequest("/test1", newContent)); } @Test public void testUpdateModifyOptionalAttribute() throws Exception { final RequestHandler handler = newCollection(builder().build()); final Connection connection = newInternalConnection(handler); final JsonValue newContent = getTestUser1Updated(12345); newContent.put("description", asList("one", "two")); connection.update(ctx(), newUpdateRequest("/test1", newContent)); newContent.put("description", asList("three")); final Resource resource1 = connection.update(ctx(), newUpdateRequest("/test1", newContent)); checkResourcesAreEqual(resource1, newContent); final Resource resource2 = connection.read(ctx(), newReadRequest("/test1")); checkResourcesAreEqual(resource2, newContent); } @Test public void testUpdateMVCCMatch() throws Exception { final RequestHandler handler = newCollection(builder().build()); final Connection connection = newInternalConnection(handler); final Resource resource1 = connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345)) .setRevision("12345")); checkResourcesAreEqual(resource1, getTestUser1Updated(12345)); final Resource resource2 = connection.read(ctx(), newReadRequest("/test1")); checkResourcesAreEqual(resource2, getTestUser1Updated(12345)); } @Test(expectedExceptions = PreconditionFailedException.class) public void testUpdateMVCCNoMatch() throws Exception { final RequestHandler handler = newCollection(builder().build()); final Connection connection = newInternalConnection(handler); connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(12345)) .setRevision("12346")); } @Test(expectedExceptions = NotFoundException.class) public void testUpdateNotFound() throws Exception { final RequestHandler handler = newCollection(builder().build()); final Connection connection = newInternalConnection(handler); connection.update(ctx(), newUpdateRequest("/missing", getTestUser1Updated(12345))); } @Test(expectedExceptions = BadRequestException.class) public void testUpdateReadOnlyAttribute() throws Exception { final RequestHandler handler = newCollection(builder().build()); final Connection connection = newInternalConnection(handler); // Etag is read-only. connection.update(ctx(), newUpdateRequest("/test1", getTestUser1Updated(99999))); } @Test(expectedExceptions = BadRequestException.class) public void testUpdateSingleValuedAttributeWithMultipleValues() throws Exception { final RequestHandler handler = newCollection(builder().build()); final Connection connection = newInternalConnection(handler); final JsonValue newContent = getTestUser1Updated(12345); newContent.put("surname", asList("black", "white")); connection.update(ctx(), newUpdateRequest("/test1", newContent)); } @Test(expectedExceptions = BadRequestException.class) public void testUpdateUnknownAttribute() throws Exception { final RequestHandler handler = newCollection(builder().build()); final Connection connection = newInternalConnection(handler); final JsonValue newContent = getTestUser1Updated(12345); newContent.add("dummy", "junk"); connection.update(ctx(), newUpdateRequest("/test1", newContent)); } private Builder builder() throws IOException { return Rest2LDAP.builder().ldapConnectionFactory(getConnectionFactory()).baseDN("dc=test") .useEtagAttribute().useClientDNNaming("uid").readOnUpdatePolicy( ReadOnUpdatePolicy.CONTROLS).authorizationPolicy(AuthorizationPolicy.NONE) .additionalLDAPAttribute("objectClass", "top", "person") .mapper(object() .attribute("schemas", constant(asList("urn:scim:schemas:core:1.0"))) .attribute( "_id", simple("uid").isSingleValued().isRequired().writability( WritabilityPolicy.CREATE_ONLY)).attribute( "name", object().attribute("displayName", simple("cn").isSingleValued().isRequired()).attribute( "surname", simple("sn").isSingleValued().isRequired())) .attribute( "_rev", simple("etag").isSingleValued().isRequired().writability( WritabilityPolicy.READ_ONLY)).attribute("description", simple("description"))); } private void checkResourcesAreEqual(final Resource actual, final JsonValue expected) { final Resource expectedResource = asResource(expected); assertThat(actual.getId()).isEqualTo(expectedResource.getId()); assertThat(actual.getRevision()).isEqualTo(expectedResource.getRevision()); assertThat(actual.getContent().getObject()).isEqualTo( expectedResource.getContent().getObject()); } private ConnectionFactory getConnectionFactory() throws IOException { // @formatter:off final MemoryBackend backend = new MemoryBackend(new LDIFEntryReader( "dn: dc=test", "objectClass: domain", "objectClass: top", "dc: com", "", "dn: uid=test1,dc=test", "objectClass: top", "objectClass: person", "uid: test1", "userpassword: password", "cn: test user 1", "sn: user 1", "etag: 12345", "", "dn: uid=test2,dc=test", "objectClass: top", "objectClass: person", "uid: test2", "userpassword: password", "cn: test user 2", "sn: user 2", "etag: 67890" )); // @formatter:on return newInternalConnectionFactory(backend); } private JsonValue getTestUser1(final int rev) { return content(object(field("schemas", asList("urn:scim:schemas:core:1.0")), field("_id", "test1"), field("_rev", String.valueOf(rev)), field("name", object(field( "displayName", "test user 1"), field("surname", "user 1"))))); } private JsonValue getTestUser1Updated(final int rev) { return content(object(field("schemas", asList("urn:scim:schemas:core:1.0")), field("_id", "test1"), field("_rev", String.valueOf(rev)), field("name", object(field( "displayName", "changed"), field("surname", "user 1"))))); } }