/* * 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 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; public abstract class TestCase extends PluggableBackendImplTestCase { JdbcDatabaseContainer container; @BeforeClass @Override public void setUp() throws Exception { if(DockerClientFactory.instance().isDockerAvailable()) { try { container = getContainer(); container.start(); } catch (Exception e) { // The database container could not be started (e.g. a slow/flaky image // pull or DB initialization failure on CI). Skip the test instead of // failing the whole build - container startup is an infrastructure // concern, not a regression in the JDBC backend under test. throw new SkipException(getContainerDockerCommand()); } } 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 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(); } @Override protected JDBCBackendCfg createBackendCfg() { JDBCBackendCfg backendCfg = mockCfg(JDBCBackendCfg.class); when(backendCfg.getBackendId()).thenReturn(getBackendId()); when(backendCfg.getDBDirectory()).thenReturn(getJdbcUrl()); return backendCfg; } @AfterClass @Override public void cleanUp() throws Exception { super.cleanUp(); if(container != null) { container.close(); } } protected abstract JdbcDatabaseContainer getContainer(); protected abstract String getContainerDockerCommand(); 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() { @Override public Void run(ReadableTransaction txn) throws Exception { try (final Cursor 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 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(); } } }