| | |
| | | * Header, with the fields enclosed by brackets [] replaced by your own identifying |
| | | * information: "Portions Copyright [year] [name of copyright owner]". |
| | | * |
| | | * Copyright 2024-2025 3A Systems, LLC. |
| | | * Copyright 2024-2026 3A Systems, LLC. |
| | | */ |
| | | package org.opends.server.backends.jdbc; |
| | | |
| | | import org.forgerock.opendj.ldap.ByteString; |
| | | import org.forgerock.opendj.server.config.server.JDBCBackendCfg; |
| | | import org.opends.server.backends.pluggable.PluggableBackendImplTestCase; |
| | | import org.opends.server.backends.pluggable.spi.AccessMode; |
| | | import org.opends.server.backends.pluggable.spi.Cursor; |
| | | import org.opends.server.backends.pluggable.spi.ReadOperation; |
| | | import org.opends.server.backends.pluggable.spi.ReadableTransaction; |
| | | import org.opends.server.backends.pluggable.spi.TreeName; |
| | | import org.opends.server.backends.pluggable.spi.WriteOperation; |
| | | import org.opends.server.backends.pluggable.spi.WriteableTransaction; |
| | | import org.testcontainers.DockerClientFactory; |
| | | import org.testcontainers.containers.JdbcDatabaseContainer; |
| | | import org.testng.SkipException; |
| | | import org.testng.annotations.AfterClass; |
| | | import org.testng.annotations.BeforeClass; |
| | | import org.testng.annotations.Test; |
| | | |
| | | import java.sql.Connection; |
| | | import java.sql.DriverManager; |
| | | import java.sql.ResultSet; |
| | | import java.sql.SQLException; |
| | | import java.sql.Statement; |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | import java.util.NoSuchElementException; |
| | | |
| | | import static org.forgerock.opendj.config.ConfigurationMock.mockCfg; |
| | | import static org.mockito.Mockito.when; |
| | | import static org.testng.Assert.assertEquals; |
| | | import static org.testng.Assert.assertFalse; |
| | | import static org.testng.Assert.assertNull; |
| | | import static org.testng.Assert.assertTrue; |
| | | import static org.testng.Assert.fail; |
| | | |
| | | |
| | | |
| | |
| | | throw new SkipException(getContainerDockerCommand()); |
| | | } |
| | | } |
| | | try(Connection ignored = DriverManager.getConnection(createBackendCfg().getDBDirectory())){ |
| | | |
| | | try(Connection con = DriverManager.getConnection(createBackendCfg().getDBDirectory())){ |
| | | dropStaleTrees(con); |
| | | } catch (Exception e) { |
| | | throw new SkipException(getContainerDockerCommand()); |
| | | } |
| | | super.setUp(); |
| | | } |
| | | |
| | | /** |
| | | * Backend test classes sharing one database map the same tree names to the same tables, |
| | | * so a previous run may leave trees behind — including entries encrypted with a lost cipher key. |
| | | */ |
| | | static void dropStaleTrees(Connection con) throws SQLException { |
| | | final List<String> stale = new ArrayList<>(); |
| | | try (final ResultSet rs = con.getMetaData().getTables(null, null, null, new String[]{"TABLE"})) { |
| | | while (rs.next()) { |
| | | final String name = rs.getString("TABLE_NAME"); |
| | | if (name.toLowerCase().startsWith("opendj_")) { |
| | | stale.add(name); |
| | | } |
| | | } |
| | | } |
| | | try (final Statement st = con.createStatement()) { |
| | | for (final String name : stale) { |
| | | st.execute("drop table " + name); |
| | | } |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | protected Backend createBackend() { |
| | | return new Backend(); |
| | |
| | | protected abstract String getBackendId(); |
| | | |
| | | protected abstract String getJdbcUrl(); |
| | | |
| | | private static ByteString key(int i) { |
| | | return ByteString.valueOfUtf8(String.format("key%02d", i)); |
| | | } |
| | | |
| | | private static ByteString value(int i) { |
| | | return ByteString.valueOfUtf8("value" + i); |
| | | } |
| | | |
| | | /** Cursor operations must keep working when the tree spans several "fetchsize" batches. */ |
| | | @Test |
| | | public void testCursorCrossesFetchSizeBatches() throws Exception { |
| | | System.setProperty("org.openidentityplatform.opendj.jdbc.fetchsize", "2"); |
| | | final Storage storage = new Storage(createBackendCfg(), null); |
| | | final TreeName tree = new TreeName("testCursorBatch", "tree"); |
| | | try { |
| | | storage.open(AccessMode.READ_WRITE); |
| | | storage.write(new WriteOperation() { |
| | | @Override |
| | | public void run(WriteableTransaction txn) throws Exception { |
| | | txn.openTree(tree, true); |
| | | for (int i = 0; i < 7; i++) { |
| | | txn.put(tree, key(i), value(i)); |
| | | } |
| | | } |
| | | }); |
| | | storage.read(new ReadOperation<Void>() { |
| | | @Override |
| | | public Void run(ReadableTransaction txn) throws Exception { |
| | | try (final Cursor<ByteString, ByteString> cursor = txn.openCursor(tree)) { |
| | | for (int i = 0; i < 7; i++) { |
| | | assertTrue(cursor.next(), "next() at " + i); |
| | | assertEquals(cursor.getKey(), key(i)); |
| | | assertEquals(cursor.getValue(), value(i)); |
| | | } |
| | | assertFalse(cursor.next()); |
| | | assertFalse(cursor.isDefined()); |
| | | try { |
| | | cursor.getKey(); |
| | | fail("getKey() on undefined cursor must fail"); |
| | | } catch (NoSuchElementException expected) {} |
| | | |
| | | assertTrue(cursor.positionToKeyOrNext(key(3))); |
| | | assertEquals(cursor.getKey(), key(3)); |
| | | assertTrue(cursor.positionToKeyOrNext(ByteString.valueOfUtf8("key031"))); |
| | | assertEquals(cursor.getKey(), key(4)); |
| | | assertTrue(cursor.next()); |
| | | assertEquals(cursor.getKey(), key(5)); |
| | | assertTrue(cursor.next()); |
| | | assertEquals(cursor.getKey(), key(6)); |
| | | assertFalse(cursor.next()); |
| | | assertFalse(cursor.positionToKeyOrNext(ByteString.valueOfUtf8("z"))); |
| | | assertFalse(cursor.isDefined()); |
| | | |
| | | assertTrue(cursor.positionToKey(key(5))); |
| | | assertEquals(cursor.getValue(), value(5)); |
| | | assertTrue(cursor.next()); |
| | | assertEquals(cursor.getKey(), key(6)); |
| | | assertFalse(cursor.positionToKey(ByteString.valueOfUtf8("key99"))); |
| | | assertFalse(cursor.isDefined()); |
| | | |
| | | assertTrue(cursor.positionToIndex(0)); |
| | | assertEquals(cursor.getKey(), key(0)); |
| | | assertTrue(cursor.positionToIndex(5)); |
| | | assertEquals(cursor.getKey(), key(5)); |
| | | assertTrue(cursor.next()); |
| | | assertEquals(cursor.getKey(), key(6)); |
| | | assertFalse(cursor.positionToIndex(7)); |
| | | assertFalse(cursor.positionToIndex(-1)); |
| | | |
| | | assertTrue(cursor.positionToLastKey()); |
| | | assertEquals(cursor.getKey(), key(6)); |
| | | assertFalse(cursor.next()); |
| | | |
| | | assertTrue(cursor.positionToKey(key(0))); |
| | | try { |
| | | cursor.delete(); |
| | | fail("delete() on read-only cursor must fail"); |
| | | } catch (UnsupportedOperationException expected) {} |
| | | } |
| | | return null; |
| | | } |
| | | }); |
| | | storage.write(new WriteOperation() { |
| | | @Override |
| | | public void run(WriteableTransaction txn) throws Exception { |
| | | try (final Cursor<ByteString, ByteString> cursor = txn.openCursor(tree)) { |
| | | assertTrue(cursor.positionToKey(key(3))); |
| | | cursor.delete(); |
| | | assertTrue(cursor.next()); |
| | | assertEquals(cursor.getKey(), key(4)); |
| | | } |
| | | assertNull(txn.read(tree, key(3))); |
| | | assertEquals(txn.getRecordCount(tree), 6); |
| | | } |
| | | }); |
| | | } finally { |
| | | System.clearProperty("org.openidentityplatform.opendj.jdbc.fetchsize"); |
| | | try { |
| | | storage.write(new WriteOperation() { |
| | | @Override |
| | | public void run(WriteableTransaction txn) throws Exception { |
| | | txn.deleteTree(tree); |
| | | } |
| | | }); |
| | | } catch (Exception ignored) {} |
| | | storage.close(); |
| | | } |
| | | } |
| | | } |