/* * 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 2006-2009 Sun Microsystems, Inc. * Portions Copyright 2011-2016 ForgeRock AS. * Portions Copyright 2026 3A Systems, LLC */ package org.opends.server.tasks; import java.io.File; import java.io.IOException; import java.io.Writer; import java.lang.reflect.Field; import java.util.UUID; import org.forgerock.opendj.ldap.ResultCode; import org.opends.server.TestCaseUtils; import org.opends.server.api.LocalBackend; import org.opends.server.api.TestTaskListener; import org.opends.server.backends.task.Task; import org.opends.server.backends.task.TaskState; import org.opends.server.core.AddOperation; import org.opends.server.core.BackendConfigManager; import org.opends.server.core.DirectoryServer; import org.opends.server.types.Entry; import org.opends.server.types.LDIFImportConfig; import org.forgerock.opendj.ldap.schema.ObjectClass; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import static org.opends.server.protocols.internal.InternalClientConnection.*; import static org.testng.Assert.*; /** * Tests invocation of the import and export tasks, but does not aim to * thoroughly test the underlying backend implementations. */ public class TestImportAndExport extends TasksTestCase { /** * A makeldif template used to create some test entries. */ private static String[] template = new String[] { "define suffix=dc=example,dc=com", "define maildomain=example.com", "define numusers=101", "", "branch: [suffix]", "", "branch: ou=People,[suffix]", "subordinateTemplate: person:[numusers]", "", "template: person", "rdnAttr: uid", "objectClass: top", "objectClass: person", "objectClass: organizationalPerson", "objectClass: inetOrgPerson", "givenName: ", "sn: ", "cn: {givenName} {sn}", "initials: {givenName:1}{sn:1}", "employeeNumber: ", "uid: user.{employeeNumber}", "mail: {uid}@[maildomain]", "userPassword: password", "telephoneNumber: ", "homePhone: ", "pager: ", "mobile: ", "street: Street", "l: ", "st: ", "postalCode: ", "postalAddress: {cn}${street}${l}, {st} {postalCode}", "description: This is the description for {cn}.", ""}; /** * A temporary LDIF file containing some test entries. */ private File ldifFile; /** * A temporary file to contain rejected entries. */ private File rejectFile; @BeforeClass public void setUp() throws Exception { // The server must be running for these tests. TestCaseUtils.startServer(); // Create a temporary test LDIF file. ldifFile = File.createTempFile("import-test", ".ldif"); String resourcePath = DirectoryServer.getInstanceRoot() + File.separator + "config" + File.separator + "MakeLDIF"; LdifFileWriter.makeLdif(ldifFile.getPath(), resourcePath, template); // Create a temporary rejects file. rejectFile = File.createTempFile("import-test-rejects", ".ldif"); TestTaskListener.registerListeners(); } @AfterClass public void tearDown() { ldifFile.delete(); rejectFile.delete(); TestTaskListener.deregisterListeners(); } /** * Import and export tasks test data provider. * * @return The array of tasks test data. The first column is a task entry * and the second column is the expected completed task state. */ @DataProvider(name = "importexport") public Object[][] createData() throws Exception { return new Object[][] { // A fairly simple, valid import task using backend ID. { TestCaseUtils.makeEntry( "dn: ds-task-id=" + UUID.randomUUID() + ",cn=Scheduled Tasks,cn=Tasks", "objectclass: top", "objectclass: ds-task", "objectclass: ds-task-import", "ds-task-class-name: org.opends.server.tasks.ImportTask", "ds-task-import-backend-id: userRoot", "ds-task-import-ldif-file: " + ldifFile.getPath(), "ds-task-import-reject-file: " + rejectFile.getPath(), "ds-task-import-overwrite-rejects: TRUE", "ds-task-import-exclude-attribute: description", "ds-task-import-exclude-filter: (st=CA)", "ds-task-import-exclude-branch: o=exclude,dc=example,dc=com" ), TaskState.COMPLETED_SUCCESSFULLY }, // A fairly simple, valid import task using include base DN. { TestCaseUtils.makeEntry( "dn: ds-task-id=" + UUID.randomUUID() + ",cn=Scheduled Tasks,cn=Tasks", "objectclass: top", "objectclass: ds-task", "objectclass: ds-task-import", "ds-task-class-name: org.opends.server.tasks.ImportTask", "ds-task-import-include-branch: dc=example,dc=com", "ds-task-import-ldif-file: " + ldifFile.getPath(), "ds-task-import-reject-file: " + rejectFile.getPath(), "ds-task-import-overwrite-rejects: TRUE", "ds-task-import-exclude-attribute: description", "ds-task-import-exclude-filter: (st=CA)", "ds-task-import-exclude-branch: o=exclude,dc=example,dc=com" ), TaskState.COMPLETED_SUCCESSFULLY }, // A complex, valid import task. { TestCaseUtils.makeEntry( "dn: ds-task-id=" + UUID.randomUUID() + ",cn=Scheduled Tasks,cn=Tasks", "objectclass: top", "objectclass: ds-task", "objectclass: ds-task-import", "ds-task-class-name: org.opends.server.tasks.ImportTask", "ds-task-import-backend-id: userRoot", "ds-task-import-ldif-file: " + ldifFile.getPath(), "ds-task-import-is-compressed: FALSE", "ds-task-import-is-encrypted: FALSE", "ds-task-import-reject-file: " + rejectFile.getPath(), "ds-task-import-overwrite-rejects: FALSE", "ds-task-import-skip-schema-validation: TRUE", "ds-task-import-include-branch: dc=example,dc=com", "ds-task-import-exclude-branch: o=exclude,dc=example,dc=com", "ds-task-import-include-attribute: cn", "ds-task-import-include-attribute: sn", "ds-task-import-include-attribute: uid", "ds-task-import-include-filter: (objectclass=*)" ), TaskState.COMPLETED_SUCCESSFULLY }, // A partial, valid import task. { TestCaseUtils.makeEntry( "dn: ds-task-id=" + UUID.randomUUID() + ",cn=Scheduled Tasks,cn=Tasks", "objectclass: top", "objectclass: ds-task", "objectclass: ds-task-import", "ds-task-class-name: org.opends.server.tasks.ImportTask", "ds-task-import-include-branch: ou=people,dc=example,dc=com", "ds-task-import-ldif-file: " + ldifFile.getPath(), "ds-task-import-reject-file: " + rejectFile.getPath(), "ds-task-import-overwrite-rejects: TRUE", "ds-task-import-exclude-attribute: description", "ds-task-import-exclude-filter: (st=CA)", "ds-task-import-exclude-branch: o=exclude,dc=example,dc=com" ), TaskState.COMPLETED_SUCCESSFULLY }, // Rejects file is a directory. { TestCaseUtils.makeEntry( "dn: ds-task-id=" + UUID.randomUUID() + ",cn=Scheduled Tasks,cn=Tasks", "objectclass: top", "objectclass: ds-task", "objectclass: ds-task-import", "ds-task-class-name: org.opends.server.tasks.ImportTask", "ds-task-import-backend-id: userRoot", "ds-task-import-ldif-file: " + ldifFile.getPath(), "ds-task-import-reject-file: " + ldifFile.getParent(), "ds-task-import-overwrite-rejects: TRUE" ), TaskState.STOPPED_BY_ERROR }, }; } /** * Import and export tasks bad test data provider. * * @return The array of tasks test data. The first column is a task entry * and the second column is the expected completed task state. */ @DataProvider(name = "badimportexport") public Object[][] createBadData() throws Exception { return new Object[][] { // Invalid exclude filter. { TestCaseUtils.makeEntry( "dn: ds-task-id=" + UUID.randomUUID() + ",cn=Scheduled Tasks,cn=Tasks", "objectclass: top", "objectclass: ds-task", "objectclass: ds-task-import", "ds-task-class-name: org.opends.server.tasks.ImportTask", "ds-task-import-ldif-file: " + ldifFile.getPath(), "ds-task-import-backend-id: userRoot", "ds-task-import-exclude-filter: ()" ), ResultCode.UNWILLING_TO_PERFORM }, // Invalid include filter. { TestCaseUtils.makeEntry( "dn: ds-task-id=" + UUID.randomUUID() + ",cn=Scheduled Tasks,cn=Tasks", "objectclass: top", "objectclass: ds-task", "objectclass: ds-task-import", "ds-task-class-name: org.opends.server.tasks.ImportTask", "ds-task-import-ldif-file: " + ldifFile.getPath(), "ds-task-import-backend-id: userRoot", "ds-task-import-include-filter: ()" ), ResultCode.UNWILLING_TO_PERFORM }, // Backend id does not exist. { TestCaseUtils.makeEntry( "dn: ds-task-id=" + UUID.randomUUID() + ",cn=Scheduled Tasks,cn=Tasks", "objectclass: top", "objectclass: ds-task", "objectclass: ds-task-import", "ds-task-class-name: org.opends.server.tasks.ImportTask", "ds-task-import-ldif-file: " + ldifFile.getPath(), "ds-task-import-backend-id: doesnotexist" ), ResultCode.UNWILLING_TO_PERFORM }, // Backend does not support import. { TestCaseUtils.makeEntry( "dn: ds-task-id=" + UUID.randomUUID() + ",cn=Scheduled Tasks,cn=Tasks", "objectclass: top", "objectclass: ds-task", "objectclass: ds-task-import", "ds-task-class-name: org.opends.server.tasks.ImportTask", "ds-task-import-ldif-file: " + ldifFile.getPath(), "ds-task-import-backend-id: monitor" ), ResultCode.UNWILLING_TO_PERFORM }, // Backend does not handle include branch. { TestCaseUtils.makeEntry( "dn: ds-task-id=" + UUID.randomUUID() + ",cn=Scheduled Tasks,cn=Tasks", "objectclass: top", "objectclass: ds-task", "objectclass: ds-task-import", "ds-task-class-name: org.opends.server.tasks.ImportTask", "ds-task-import-ldif-file: " + ldifFile.getPath(), "ds-task-import-backend-id: userRoot", "ds-task-import-include-branch: dc=opends,dc=org" ), ResultCode.UNWILLING_TO_PERFORM }, // Not specifying a destination. { TestCaseUtils.makeEntry( "dn: ds-task-id=" + UUID.randomUUID() + ",cn=Scheduled Tasks,cn=Tasks", "objectclass: top", "objectclass: ds-task", "objectclass: ds-task-import", "ds-task-class-name: org.opends.server.tasks.ImportTask", "ds-task-import-ldif-file: " + ldifFile.getPath() ), ResultCode.UNWILLING_TO_PERFORM }, // ldif file does not exist. { TestCaseUtils.makeEntry( "dn: ds-task-id=" + UUID.randomUUID() + ",cn=Scheduled Tasks,cn=Tasks", "objectclass: top", "objectclass: ds-task", "objectclass: ds-task-import", "ds-task-class-name: org.opends.server.tasks.ImportTask", "ds-task-import-ldif-file: doesnotexist", "ds-task-import-backend-id: userRoot" ), ResultCode.UNWILLING_TO_PERFORM } }; } /** * Test that various import and export task definitions complete with the * expected state. * @param taskEntry The task entry. * @param expectedState The expected completion state of the task. */ @Test(dataProvider = "importexport") public void testImportExport(Entry taskEntry, TaskState expectedState) throws Exception { int exportBeginCount = TestTaskListener.exportBeginCount.get(); int exportEndCount = TestTaskListener.exportEndCount.get(); int importBeginCount = TestTaskListener.importBeginCount.get(); int importEndCount = TestTaskListener.importEndCount.get(); ObjectClass exportClass = TestCaseUtils.getServerContext().getSchema().getObjectClass("ds-task-export"); // Use a big timeout since this test is sensitive to host environment (e.g. // low memory, etc). See issue OPENDJ-256. testTask(taskEntry, expectedState, 600); if (expectedState == TaskState.COMPLETED_SUCCESSFULLY || expectedState == TaskState.COMPLETED_WITH_ERRORS) { if (taskEntry.hasObjectClass(exportClass)) { assertEquals(TestTaskListener.exportBeginCount.get(), exportBeginCount+1); assertEquals(TestTaskListener.exportEndCount.get(), exportEndCount+1); assertEquals(TestTaskListener.exportBeginCount.get(), TestTaskListener.exportEndCount.get()); } else { assertEquals(TestTaskListener.importBeginCount.get(), importBeginCount+1); assertEquals(TestTaskListener.importEndCount.get(), importEndCount+1); assertEquals(TestTaskListener.importBeginCount.get(), TestTaskListener.importEndCount.get()); } } } /** * An import which cannot disable its backend must still tell the import task listeners * that the import is over: a listener which took something offline when the import began * - a replication domain disables itself - has no other chance to put it back. The reject * and skip files it opened before touching the backend must be closed on that road too: * it is the one which ends furthest from the backend, so a close which rides on the * backend having been disabled, or on the import having been launched, misses it. */ @Test public void testImportEndsWhenTheBackendCannotBeDisabled() throws Exception { /* * A backend registered at runtime has no entry in cn=config, and disabling a backend * is a modification of that entry, so TaskUtils.disableBackend() cannot do it. */ final String backendID = "importTaskUnconfiguredBackend"; TestCaseUtils.initializeMemoryBackend(backendID, "dc=unconfigured,dc=com", true); File skipFile = File.createTempFile("import-test-skipped", ".ldif"); try { int importBeginCount = TestTaskListener.importBeginCount.get(); int importEndCount = TestTaskListener.importEndCount.get(); Entry taskEntry = TestCaseUtils.makeEntry( "dn: ds-task-id=" + UUID.randomUUID() + ",cn=Scheduled Tasks,cn=Tasks", "objectclass: top", "objectclass: ds-task", "objectclass: ds-task-import", "ds-task-class-name: org.opends.server.tasks.ImportTask", "ds-task-import-backend-id: " + backendID, "ds-task-import-ldif-file: " + ldifFile.getPath(), "ds-task-import-reject-file: " + rejectFile.getPath(), "ds-task-import-skip-file: " + skipFile.getPath(), "ds-task-import-overwrite-rejects: TRUE"); TestTaskListener.lastImportEndConfig.set(null); testTask(taskEntry, TaskState.STOPPED_BY_ERROR, 60); assertEquals(TestTaskListener.importBeginCount.get(), importBeginCount + 1); assertEquals(TestTaskListener.importEndCount.get(), importEndCount + 1); LDIFImportConfig importConfig = TestTaskListener.lastImportEndConfig.get(); assertNotNull(importConfig, "The import end was not notified"); assertClosed(importConfig.getRejectWriter(), "reject"); assertClosed(importConfig.getSkipWriter(), "skip"); } finally { skipFile.delete(); removeMemoryBackend(backendID); } } /** * A failed import must notify the listeners exactly once, as its beginning was notified * once: a replication domain enabled a second time reloads and rewinds its replication * state for nothing. */ @Test public void testFailedImportEndsOnlyOnce() throws Exception { int importBeginCount = TestTaskListener.importBeginCount.get(); int importEndCount = TestTaskListener.importEndCount.get(); // A directory can be read, so the task accepts it, but it cannot be read as LDIF. Entry taskEntry = TestCaseUtils.makeEntry( "dn: ds-task-id=" + UUID.randomUUID() + ",cn=Scheduled Tasks,cn=Tasks", "objectclass: top", "objectclass: ds-task", "objectclass: ds-task-import", "ds-task-class-name: org.opends.server.tasks.ImportTask", "ds-task-import-backend-id: userRoot", "ds-task-import-ldif-file: " + ldifFile.getParent()); testTask(taskEntry, TaskState.STOPPED_BY_ERROR, 60); assertEquals(TestTaskListener.importBeginCount.get(), importBeginCount + 1); assertEquals(TestTaskListener.importEndCount.get(), importEndCount + 1); } /** * A failed import must close the reject and skip files it opened. A backend closes the * import config together with its LDIF reader, so an import which fails before that reader * exists leaves the closing to the task, whose error returns must not skip it. */ @Test public void testFailedImportClosesItsRejectAndSkipFiles() throws Exception { File skipFile = File.createTempFile("import-test-skipped", ".ldif"); try { // A directory can be read, so the task accepts it, but it cannot be opened as LDIF: // the import fails before the backend creates the reader which would close the config. Entry taskEntry = TestCaseUtils.makeEntry( "dn: ds-task-id=" + UUID.randomUUID() + ",cn=Scheduled Tasks,cn=Tasks", "objectclass: top", "objectclass: ds-task", "objectclass: ds-task-import", "ds-task-class-name: org.opends.server.tasks.ImportTask", "ds-task-import-backend-id: userRoot", "ds-task-import-ldif-file: " + ldifFile.getParent(), "ds-task-import-reject-file: " + rejectFile.getPath(), "ds-task-import-skip-file: " + skipFile.getPath(), "ds-task-import-overwrite-rejects: TRUE"); TestTaskListener.lastImportEndConfig.set(null); testTask(taskEntry, TaskState.STOPPED_BY_ERROR, 60); LDIFImportConfig importConfig = TestTaskListener.lastImportEndConfig.get(); assertNotNull(importConfig, "The import end was not notified"); assertClosed(importConfig.getRejectWriter(), "reject"); assertClosed(importConfig.getSkipWriter(), "skip"); } finally { skipFile.delete(); } } /** * An import which cannot open its skip file must close the reject file it already opened. * That failure returns before the import is announced, so the config cannot be read through * the listener the other cases use, and is read from the task the scheduler kept instead. */ @Test public void testImportWhichCannotOpenItsSkipFileClosesItsRejectFile() throws Exception { Entry taskEntry = TestCaseUtils.makeEntry( "dn: ds-task-id=" + UUID.randomUUID() + ",cn=Scheduled Tasks,cn=Tasks", "objectclass: top", "objectclass: ds-task", "objectclass: ds-task-import", "ds-task-class-name: org.opends.server.tasks.ImportTask", "ds-task-import-backend-id: userRoot", "ds-task-import-ldif-file: " + ldifFile.getPath(), "ds-task-import-reject-file: " + rejectFile.getPath(), // The task stores the path as it is given, and a directory cannot be opened for writing. "ds-task-import-skip-file: " + ldifFile.getParent(), "ds-task-import-overwrite-rejects: TRUE"); testTask(taskEntry, TaskState.STOPPED_BY_ERROR, 60); LDIFImportConfig importConfig = importConfigOf(getDoneTask(taskEntry.getName())); assertNotNull(importConfig, "The task never built an import config"); assertClosed(importConfig.getRejectWriter(), "reject"); assertNull(importConfig.getSkipWriter(), "The skip writer was opened after all"); } /** The config an import task worked with, which the task keeps to itself. */ private static LDIFImportConfig importConfigOf(Task task) throws Exception { Field importConfig = ImportTask.class.getDeclaredField("importConfig"); importConfig.setAccessible(true); return (LDIFImportConfig) importConfig.get(task); } private static void assertClosed(Writer writer, String name) { assertNotNull(writer, "The " + name + " writer was never opened"); try { writer.write("still open"); fail("The " + name + " writer is still open after the import ended"); } catch (IOException expected) { // A closed BufferedWriter refuses the write: that is the closed state being checked. } } private void removeMemoryBackend(String backendID) throws Exception { BackendConfigManager backendConfigManager = TestCaseUtils.getServerContext().getBackendConfigManager(); LocalBackend backend = backendConfigManager.getLocalBackendById(backendID); if (backend != null) { backend.finalizeBackend(); backendConfigManager.deregisterLocalBackend(backend); } } /** * Add a task definition and check that it completes with the expected state. * @param taskEntry The task entry. * @param resultCode The expected result code of the task add. * @throws Exception If the test fails. */ @Test(dataProvider = "badimportexport") public void testBadTask(Entry taskEntry, ResultCode resultCode) throws Exception { AddOperation addOperation = getRootConnection().processAdd(taskEntry); assertEquals(addOperation.getResultCode(), resultCode); } }