From 37a56f75afac2805e1ba958eebd496e77b7079e7 Mon Sep 17 00:00:00 2001 From: Simon Pichugin Date: Mon, 28 Jul 2025 15:35:50 -0700 Subject: [PATCH] Issue 6594 - Add test for numSubordinates replication consistency with tombstones (#6862) Description: Add a comprehensive test to verify that numSubordinates and tombstoneNumSubordinates attributes are correctly replicated between instances when tombstone entries are present. Fixes: https://github.com/389ds/389-ds-base/issues/6594 Reviewed by: @progier389 (Thanks!) --- .../numsubordinates_replication_test.py | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 dirsrvtests/tests/suites/replication/numsubordinates_replication_test.py diff --git a/dirsrvtests/tests/suites/replication/numsubordinates_replication_test.py b/dirsrvtests/tests/suites/replication/numsubordinates_replication_test.py new file mode 100644 index 000000000..9ba10657d --- /dev/null +++ b/dirsrvtests/tests/suites/replication/numsubordinates_replication_test.py @@ -0,0 +1,144 @@ +# --- BEGIN COPYRIGHT BLOCK --- +# Copyright (C) 2025 Red Hat, Inc. +# All rights reserved. +# +# License: GPL (version 3 or any later version). +# See LICENSE for details. +# --- END COPYRIGHT BLOCK --- + +import os +import logging +import pytest +from lib389._constants import DEFAULT_SUFFIX +from lib389.replica import ReplicationManager +from lib389.idm.organizationalunit import OrganizationalUnits +from lib389.idm.user import UserAccounts +from lib389.topologies import topology_i2 as topo_i2 + + +pytestmark = pytest.mark.tier1 + +DEBUGGING = os.getenv("DEBUGGING", default=False) +if DEBUGGING: + logging.getLogger(__name__).setLevel(logging.DEBUG) +else: + logging.getLogger(__name__).setLevel(logging.INFO) +log = logging.getLogger(__name__) + + +def test_numsubordinates_tombstone_replication_mismatch(topo_i2): + """Test that numSubordinates values match between replicas after tombstone creation + + :id: c43ecc7a-d706-42e8-9179-1ff7d0e7163a + :setup: Two standalone instances + :steps: + 1. Create a container (organizational unit) on the first instance + 2. Create a user object in that container + 3. Delete the user object (this creates a tombstone) + 4. Set up replication between the two instances + 5. Wait for replication to complete + 6. Check numSubordinates on both instances + 7. Check tombstoneNumSubordinates on both instances + 8. Verify that numSubordinates values match on both instances + :expectedresults: + 1. Container should be created successfully + 2. User object should be created successfully + 3. User object should be deleted successfully + 4. Replication should be set up successfully + 5. Replication should complete successfully + 6. numSubordinates should be accessible on both instances + 7. tombstoneNumSubordinates should be accessible on both instances + 8. numSubordinates values should match on both instances + """ + + instance1 = topo_i2.ins["standalone1"] + instance2 = topo_i2.ins["standalone2"] + + log.info("Create a container (organizational unit) on the first instance") + ous1 = OrganizationalUnits(instance1, DEFAULT_SUFFIX) + container = ous1.create(properties={ + 'ou': 'test_container', + 'description': 'Test container for numSubordinates replication test' + }) + container_rdn = container.rdn + log.info(f"Created container: {container_rdn}") + + log.info("Create a user object in that container") + users1 = UserAccounts(instance1, DEFAULT_SUFFIX, rdn=f"ou={container_rdn}") + test_user = users1.create_test_user(uid=1001) + log.info(f"Created user: {test_user.dn}") + + log.info("Checking initial numSubordinates on container") + container_obj1 = OrganizationalUnits(instance1, DEFAULT_SUFFIX).get(container_rdn) + initial_numsubordinates = container_obj1.get_attr_val_int('numSubordinates') + log.info(f"Initial numSubordinates: {initial_numsubordinates}") + assert initial_numsubordinates == 1 + + log.info("Delete the user object (this creates a tombstone)") + test_user.delete() + + log.info("Checking numSubordinates after deletion") + after_delete_numsubordinates = container_obj1.get_attr_val_int('numSubordinates') + log.info(f"numSubordinates after deletion: {after_delete_numsubordinates}") + + log.info("Checking tombstoneNumSubordinates after deletion") + try: + tombstone_numsubordinates = container_obj1.get_attr_val_int('tombstoneNumSubordinates') + log.info(f"tombstoneNumSubordinates: {tombstone_numsubordinates}") + except Exception as e: + log.info(f"tombstoneNumSubordinates not found or error: {e}") + tombstone_numsubordinates = 0 + + log.info("Set up replication between the two instances") + repl = ReplicationManager(DEFAULT_SUFFIX) + repl.create_first_supplier(instance1) + repl.join_supplier(instance1, instance2) + + log.info("Wait for replication to complete") + repl.wait_for_replication(instance1, instance2) + + log.info("Check numSubordinates on both instances") + container_obj1 = OrganizationalUnits(instance1, DEFAULT_SUFFIX).get(container_rdn) + numsubordinates_instance1 = container_obj1.get_attr_val_int('numSubordinates') + log.info(f"numSubordinates on instance1: {numsubordinates_instance1}") + + container_obj2 = OrganizationalUnits(instance2, DEFAULT_SUFFIX).get(container_rdn) + numsubordinates_instance2 = container_obj2.get_attr_val_int('numSubordinates') + log.info(f"numSubordinates on instance2: {numsubordinates_instance2}") + + log.info("Check tombstoneNumSubordinates on both instances") + try: + tombstone_numsubordinates_instance1 = container_obj1.get_attr_val_int('tombstoneNumSubordinates') + log.info(f"tombstoneNumSubordinates on instance1: {tombstone_numsubordinates_instance1}") + except Exception as e: + log.info(f"tombstoneNumSubordinates not found on instance1: {e}") + tombstone_numsubordinates_instance1 = 0 + + try: + tombstone_numsubordinates_instance2 = container_obj2.get_attr_val_int('tombstoneNumSubordinates') + log.info(f"tombstoneNumSubordinates on instance2: {tombstone_numsubordinates_instance2}") + except Exception as e: + log.info(f"tombstoneNumSubordinates not found on instance2: {e}") + tombstone_numsubordinates_instance2 = 0 + + log.info("Verify that numSubordinates values match on both instances") + log.info(f"Comparison: instance1 numSubordinates={numsubordinates_instance1}, " + f"instance2 numSubordinates={numsubordinates_instance2}") + log.info(f"Comparison: instance1 tombstoneNumSubordinates={tombstone_numsubordinates_instance1}, " + f"instance2 tombstoneNumSubordinates={tombstone_numsubordinates_instance2}") + + assert numsubordinates_instance1 == numsubordinates_instance2, ( + f"numSubordinates mismatch: instance1 has {numsubordinates_instance1}, " + f"instance2 has {numsubordinates_instance2}. " + ) + assert tombstone_numsubordinates_instance1 == tombstone_numsubordinates_instance2, ( + f"tombstoneNumSubordinates mismatch: instance1 has {tombstone_numsubordinates_instance1}, " + f"instance2 has {tombstone_numsubordinates_instance2}. " + ) + + +if __name__ == '__main__': + # Run isolated + # -s for DEBUG mode + CURRENT_FILE = os.path.realpath(__file__) + pytest.main("-s %s" % CURRENT_FILE) \ No newline at end of file -- 2.49.0