From 211f159043842f422fa4eed17fe08a85952ed6b8 Mon Sep 17 00:00:00 2001 From: Jonathan Marcantonio Date: Thu, 17 Oct 2024 11:39:28 -0400 Subject: [PATCH 1/8] Quick mock to test replicate new system role gets called Signed-off-by: Jonathan Marcantonio --- tests/management/role/test_definer.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/management/role/test_definer.py b/tests/management/role/test_definer.py index a97a9424f..255d26e07 100644 --- a/tests/management/role/test_definer.py +++ b/tests/management/role/test_definer.py @@ -33,7 +33,7 @@ def setUp(self): self.public_tenant = Tenant.objects.get(tenant_name="public") @patch("core.kafka.RBACProducer.send_kafka_message") - def test_role_create(self, send_kafka_message): + def test_role_create(self, send_kafka_message): # can we modify this func? kafka_mock = copy_call_args(send_kafka_message) """Test that we can run a role seeding update.""" with self.settings(NOTIFICATIONS_RH_ENABLED=True, NOTIFICATIONS_ENABLED=True): @@ -240,3 +240,19 @@ def test_try_seed_permissions_update_description(self): self.assertEqual(permission.first().description, "Approval local test templates read.") # Previous string verb still works self.assertEqual(Permission.objects.filter(permission="inventory:*:*").count(), 1) + + + @patch("management.role.relation_api_dual_write_handler.SeedingRelationApiDualWriteHandler.replicate_new_system_role") + @patch("management.role.relation_api_dual_write_handler.SeedingRelationApiDualWriteHandler.replicate_deleted_system_role") + @patch("management.role.definer.destructive_ok") + def test_seed_roles(self, replicate_new_system_role, replicate_deleted_system_role, destructive_ok): + destructive_ok.return_value = True + # roles_to_delete.return_value + + seed_roles() + + replicate_new_system_role.assert_called() + destructive_ok.assert_called() + # replicate_deleted_system_role.assert_called() + + From 3409dd4620fa233a56e07dd2e1a97114a47a9ea2 Mon Sep 17 00:00:00 2001 From: Jonathan Marcantonio Date: Thu, 17 Oct 2024 12:10:12 -0400 Subject: [PATCH 2/8] Mock tests for seed_roles to create & delete Signed-off-by: Jonathan Marcantonio --- tests/management/role/test_definer.py | 58 ++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 11 deletions(-) diff --git a/tests/management/role/test_definer.py b/tests/management/role/test_definer.py index 255d26e07..1e437d677 100644 --- a/tests/management/role/test_definer.py +++ b/tests/management/role/test_definer.py @@ -16,7 +16,7 @@ # """Test the role definer.""" from django.conf import settings -from unittest.mock import ANY, call, patch +from unittest.mock import ANY, call, patch, mock_open from management.role.definer import seed_roles, seed_permissions from api.models import Tenant from tests.core.test_kafka import copy_call_args @@ -33,7 +33,7 @@ def setUp(self): self.public_tenant = Tenant.objects.get(tenant_name="public") @patch("core.kafka.RBACProducer.send_kafka_message") - def test_role_create(self, send_kafka_message): # can we modify this func? + def test_role_create(self, send_kafka_message): kafka_mock = copy_call_args(send_kafka_message) """Test that we can run a role seeding update.""" with self.settings(NOTIFICATIONS_RH_ENABLED=True, NOTIFICATIONS_ENABLED=True): @@ -241,18 +241,54 @@ def test_try_seed_permissions_update_description(self): # Previous string verb still works self.assertEqual(Permission.objects.filter(permission="inventory:*:*").count(), 1) + @patch( + "management.role.relation_api_dual_write_handler.SeedingRelationApiDualWriteHandler.replicate_deleted_system_role" + ) + @patch( + "management.role.relation_api_dual_write_handler.SeedingRelationApiDualWriteHandler.replicate_new_system_role" + ) + def test_seed_roles_new_role(self, mock_replicate_new_system_role, mock_replicate_deleted_system_role): + seed_roles() + + # ensure replication for new role was called + mock_replicate_new_system_role.assert_called() - @patch("management.role.relation_api_dual_write_handler.SeedingRelationApiDualWriteHandler.replicate_new_system_role") - @patch("management.role.relation_api_dual_write_handler.SeedingRelationApiDualWriteHandler.replicate_deleted_system_role") + # no roles to remove + mock_replicate_deleted_system_role.assert_not_called() + + @patch( + "management.role.relation_api_dual_write_handler.SeedingRelationApiDualWriteHandler.replicate_new_system_role" + ) + @patch( + "management.role.relation_api_dual_write_handler.SeedingRelationApiDualWriteHandler.replicate_deleted_system_role" + ) @patch("management.role.definer.destructive_ok") - def test_seed_roles(self, replicate_new_system_role, replicate_deleted_system_role, destructive_ok): - destructive_ok.return_value = True - # roles_to_delete.return_value + @patch("builtins.open", new_callable=mock_open, read_data='{"roles": []}') + @patch("os.listdir") + @patch("os.path.isfile") + def test_seed_roles_delete_role( + self, + mock_isfile, + mock_listdir, + mock_open, + mock_destructive_ok, + mock_replicate_deleted_system_role, + mock_replicate_new_system_role, + ): + mock_destructive_ok.return_value = True + + # mock files + mock_listdir.return_value = ["role.json"] + mock_isfile.return_value = True + + # create a role in the database that's not in config + role_to_delete = Role.objects.create(name="dummy_role_delete", system=True, tenant=self.public_tenant) seed_roles() - replicate_new_system_role.assert_called() - destructive_ok.assert_called() - # replicate_deleted_system_role.assert_called() - + mock_replicate_new_system_role.assert_not_called() + mock_destructive_ok.assert_called_with("seeding") + mock_replicate_deleted_system_role.assert_called_with(role_to_delete) + # verify role was deleted from the database + self.assertFalse(Role.objects.filter(id=role_to_delete.id).exists()) From 59b16de4341d844994741f2b01abb3637868886f Mon Sep 17 00:00:00 2001 From: Jonathan Marcantonio Date: Thu, 17 Oct 2024 14:18:46 -0400 Subject: [PATCH 3/8] Mock an update role call Signed-off-by: Jonathan Marcantonio --- tests/management/role/test_definer.py | 44 +++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/management/role/test_definer.py b/tests/management/role/test_definer.py index 1e437d677..b9a6253a6 100644 --- a/tests/management/role/test_definer.py +++ b/tests/management/role/test_definer.py @@ -292,3 +292,47 @@ def test_seed_roles_delete_role( # verify role was deleted from the database self.assertFalse(Role.objects.filter(id=role_to_delete.id).exists()) + + @patch("management.role.relation_api_dual_write_handler.SeedingRelationApiDualWriteHandler.prepare_for_update") + @patch( + "management.role.relation_api_dual_write_handler.SeedingRelationApiDualWriteHandler.replicate_new_system_role" + ) + @patch( + "management.role.relation_api_dual_write_handler.SeedingRelationApiDualWriteHandler.replicate_update_system_role" + ) + @patch( + "management.role.relation_api_dual_write_handler.SeedingRelationApiDualWriteHandler.replicate_deleted_system_role" + ) + @patch( + "builtins.open", + new_callable=mock_open, + read_data='{"roles": [{"name": "dummy_role_update", "system": true, "version": 3}]}', + ) + @patch("os.listdir") + @patch("os.path.isfile") + def test_seed_roles_update_role( + self, + mock_isfile, + mock_listdir, + mock_open, + mock_replicate_deleted_system_role, + mock_replicate_update_system_role, + mock_replicate_new_system_role, + mock_prepare_for_update, + ): + # mock files + mock_listdir.return_value = ["role.json"] + mock_isfile.return_value = True + + # create a role in the database that exists in config + Role.objects.create(name="dummy_role_update", system=True, tenant=self.public_tenant) + + seed_roles() + + mock_prepare_for_update.assert_called() + mock_replicate_update_system_role.assert_called() + + # no new rule to create + mock_replicate_new_system_role.assert_not_called() + # no roles to remove + mock_replicate_deleted_system_role.assert_not_called() From c1e3406690445c659da5364aa30d9ea2997065a3 Mon Sep 17 00:00:00 2001 From: Jonathan Marcantonio Date: Thu, 17 Oct 2024 16:25:03 -0400 Subject: [PATCH 4/8] Updates to tests Signed-off-by: Jonathan Marcantonio --- tests/management/role/test_definer.py | 73 ++++++++++----------------- 1 file changed, 28 insertions(+), 45 deletions(-) diff --git a/tests/management/role/test_definer.py b/tests/management/role/test_definer.py index b9a6253a6..2489f8719 100644 --- a/tests/management/role/test_definer.py +++ b/tests/management/role/test_definer.py @@ -22,7 +22,7 @@ from tests.core.test_kafka import copy_call_args from tests.identity_request import IdentityRequest from management.models import Access, ExtRoleRelation, Permission, ResourceDefinition, Role - +from management.relation_replicator.relation_replicator import ReplicationEvent, ReplicationEventType class RoleDefinerTests(IdentityRequest): """Test the role definer functions.""" @@ -241,27 +241,22 @@ def test_try_seed_permissions_update_description(self): # Previous string verb still works self.assertEqual(Permission.objects.filter(permission="inventory:*:*").count(), 1) - @patch( - "management.role.relation_api_dual_write_handler.SeedingRelationApiDualWriteHandler.replicate_deleted_system_role" - ) - @patch( - "management.role.relation_api_dual_write_handler.SeedingRelationApiDualWriteHandler.replicate_new_system_role" - ) - def test_seed_roles_new_role(self, mock_replicate_new_system_role, mock_replicate_deleted_system_role): - seed_roles() + def matches_pattern(self, arg): + """Custom predicate to match specific call patterns.""" + return arg[0].event_type == ReplicationEventType.CREATE_SYSTEM_ROLE - # ensure replication for new role was called - mock_replicate_new_system_role.assert_called() + def is_create_event(self, relation: str, evt: ReplicationEvent) -> bool: + return evt.event_type == ReplicationEventType.CREATE_SYSTEM_ROLE and any(t.relation == relation for t in evt.add) - # no roles to remove - mock_replicate_deleted_system_role.assert_not_called() + def is_remove_event(self, relation: str, evt: ReplicationEvent) -> bool: + return evt.event_type == ReplicationEventType.DELETE_SYSTEM_ROLE and any(t.relation == relation for t in evt.remove) - @patch( - "management.role.relation_api_dual_write_handler.SeedingRelationApiDualWriteHandler.replicate_new_system_role" - ) - @patch( - "management.role.relation_api_dual_write_handler.SeedingRelationApiDualWriteHandler.replicate_deleted_system_role" - ) + @patch("management.relation_replicator.outbox_replicator.OutboxReplicator.replicate") + def test_seed_roles_new_role(self, mock_replicate): + seed_roles() + self.assertTrue(any(self.is_create_event("inventory_hosts_read", args[0]) for args, _ in mock_replicate.call_args_list)) + + @patch("management.relation_replicator.outbox_replicator.OutboxReplicator.replicate") @patch("management.role.definer.destructive_ok") @patch("builtins.open", new_callable=mock_open, read_data='{"roles": []}') @patch("os.listdir") @@ -272,37 +267,30 @@ def test_seed_roles_delete_role( mock_listdir, mock_open, mock_destructive_ok, - mock_replicate_deleted_system_role, - mock_replicate_new_system_role, + mock_replicate, ): mock_destructive_ok.return_value = True - # mock files mock_listdir.return_value = ["role.json"] mock_isfile.return_value = True # create a role in the database that's not in config role_to_delete = Role.objects.create(name="dummy_role_delete", system=True, tenant=self.public_tenant) + # role_to_delete.access.add(["inventory:groups:read"]) + public_tenant = Tenant.objects.get(tenant_name="public") + permission, created = Permission.objects.get_or_create("inventory:groups:read", tenant=public_tenant) + access_obj = Access.objects.create(permission=permission, role=role_to_delete, tenant=public_tenant) + + role_to_delete.save() seed_roles() - mock_replicate_new_system_role.assert_not_called() - mock_destructive_ok.assert_called_with("seeding") - mock_replicate_deleted_system_role.assert_called_with(role_to_delete) + self.assertTrue(any(self.is_remove_event("inventory_groups_read", args[0]) for args, _ in mock_replicate.call_args_list)) # verify role was deleted from the database self.assertFalse(Role.objects.filter(id=role_to_delete.id).exists()) - @patch("management.role.relation_api_dual_write_handler.SeedingRelationApiDualWriteHandler.prepare_for_update") - @patch( - "management.role.relation_api_dual_write_handler.SeedingRelationApiDualWriteHandler.replicate_new_system_role" - ) - @patch( - "management.role.relation_api_dual_write_handler.SeedingRelationApiDualWriteHandler.replicate_update_system_role" - ) - @patch( - "management.role.relation_api_dual_write_handler.SeedingRelationApiDualWriteHandler.replicate_deleted_system_role" - ) + @patch("management.relation_replicator.outbox_replicator.OutboxReplicator.replicate") @patch( "builtins.open", new_callable=mock_open, @@ -310,15 +298,13 @@ def test_seed_roles_delete_role( ) @patch("os.listdir") @patch("os.path.isfile") + # @patch("management.relation_replicator.outbox_replicator.OutboxReplicator.replicate") def test_seed_roles_update_role( self, mock_isfile, mock_listdir, mock_open, - mock_replicate_deleted_system_role, - mock_replicate_update_system_role, - mock_replicate_new_system_role, - mock_prepare_for_update, + mock_replicate, ): # mock files mock_listdir.return_value = ["role.json"] @@ -329,10 +315,7 @@ def test_seed_roles_update_role( seed_roles() - mock_prepare_for_update.assert_called() - mock_replicate_update_system_role.assert_called() + # return arg[0].event_type == ReplicationEventType.CREATE_SYSTEM_ROLE + + self.assertTrue(any(arg[0].add for arg, _ in mock_replicate.call_args_list)) - # no new rule to create - mock_replicate_new_system_role.assert_not_called() - # no roles to remove - mock_replicate_deleted_system_role.assert_not_called() From 70539415fa477e2ebe425d7959681610b1b6fdf6 Mon Sep 17 00:00:00 2001 From: wscalf Date: Thu, 17 Oct 2024 17:04:44 -0400 Subject: [PATCH 5/8] Get delete test passing Co-authored-by: Jonathan Marcantonio Co-authored-by: Jay Z --- tests/management/role/test_definer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/management/role/test_definer.py b/tests/management/role/test_definer.py index 2489f8719..8edbaf5f0 100644 --- a/tests/management/role/test_definer.py +++ b/tests/management/role/test_definer.py @@ -278,14 +278,14 @@ def test_seed_roles_delete_role( role_to_delete = Role.objects.create(name="dummy_role_delete", system=True, tenant=self.public_tenant) # role_to_delete.access.add(["inventory:groups:read"]) public_tenant = Tenant.objects.get(tenant_name="public") - permission, created = Permission.objects.get_or_create("inventory:groups:read", tenant=public_tenant) + permission, created = Permission.objects.get_or_create(permission="inventory:hosts:read", tenant=public_tenant) access_obj = Access.objects.create(permission=permission, role=role_to_delete, tenant=public_tenant) role_to_delete.save() seed_roles() - self.assertTrue(any(self.is_remove_event("inventory_groups_read", args[0]) for args, _ in mock_replicate.call_args_list)) + self.assertTrue(any(self.is_remove_event("inventory_hosts_read", args[0]) for args, _ in mock_replicate.call_args_list)) # verify role was deleted from the database self.assertFalse(Role.objects.filter(id=role_to_delete.id).exists()) From f65b2d65e9dec20a74f8655aeec7864274a9e2a1 Mon Sep 17 00:00:00 2001 From: Jonathan Marcantonio Date: Fri, 18 Oct 2024 09:00:13 -0400 Subject: [PATCH 6/8] Move replication to after permissions Signed-off-by: Jonathan Marcantonio --- rbac/management/role/definer.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/rbac/management/role/definer.py b/rbac/management/role/definer.py index 6d6ac20c4..005fd79ec 100644 --- a/rbac/management/role/definer.py +++ b/rbac/management/role/definer.py @@ -83,14 +83,12 @@ def _make_role(data, dual_write_handler): if role.display_name != display_name: role.display_name = display_name role.save() - dual_write_handler.replicate_new_system_role(role) logger.info("Created system role %s.", name) role_obj_change_notification_handler(role, "created") else: if role.version != defaults["version"]: dual_write_handler.prepare_for_update(role) Role.objects.filter(name=name).update(**defaults, display_name=display_name, modified=timezone.now()) - dual_write_handler.replicate_update_system_role(role) logger.info("Updated system role %s.", name) role.access.all().delete() role_obj_change_notification_handler(role, "updated") @@ -108,6 +106,13 @@ def _make_role(data, dual_write_handler): ResourceDefinition.objects.create(**resource_def_item, access=access_obj, tenant=public_tenant) _add_ext_relation_if_it_exists(data.get("external"), role) + + if created: + dual_write_handler.replicate_new_system_role(role) + else: + if role.version != defaults["version"]: + dual_write_handler.replicate_update_system_role(role) + return role From 359f18f93a68d36ee7b7db3168be6e7cea22fd8a Mon Sep 17 00:00:00 2001 From: Jonathan Marcantonio Date: Fri, 18 Oct 2024 09:44:36 -0400 Subject: [PATCH 7/8] Fix bug for update_role test Signed-off-by: Jonathan Marcantonio --- rbac/management/role/definer.py | 2 +- tests/management/role/test_definer.py | 44 +++++++++++++++------------ 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/rbac/management/role/definer.py b/rbac/management/role/definer.py index 005fd79ec..7435b35e3 100644 --- a/rbac/management/role/definer.py +++ b/rbac/management/role/definer.py @@ -99,7 +99,7 @@ def _make_role(data, dual_write_handler): if access_list: # Allow external roles to have none access object for access_item in access_list: resource_def_list = access_item.pop("resourceDefinitions", []) - permission, created = Permission.objects.get_or_create(**access_item, tenant=public_tenant) + permission, _ = Permission.objects.get_or_create(**access_item, tenant=public_tenant) access_obj = Access.objects.create(permission=permission, role=role, tenant=public_tenant) for resource_def_item in resource_def_list: diff --git a/tests/management/role/test_definer.py b/tests/management/role/test_definer.py index 8edbaf5f0..ab1822baf 100644 --- a/tests/management/role/test_definer.py +++ b/tests/management/role/test_definer.py @@ -24,6 +24,7 @@ from management.models import Access, ExtRoleRelation, Permission, ResourceDefinition, Role from management.relation_replicator.relation_replicator import ReplicationEvent, ReplicationEventType + class RoleDefinerTests(IdentityRequest): """Test the role definer functions.""" @@ -241,20 +242,27 @@ def test_try_seed_permissions_update_description(self): # Previous string verb still works self.assertEqual(Permission.objects.filter(permission="inventory:*:*").count(), 1) - def matches_pattern(self, arg): - """Custom predicate to match specific call patterns.""" - return arg[0].event_type == ReplicationEventType.CREATE_SYSTEM_ROLE - def is_create_event(self, relation: str, evt: ReplicationEvent) -> bool: - return evt.event_type == ReplicationEventType.CREATE_SYSTEM_ROLE and any(t.relation == relation for t in evt.add) + return evt.event_type == ReplicationEventType.CREATE_SYSTEM_ROLE and any( + t.relation == relation for t in evt.add + ) def is_remove_event(self, relation: str, evt: ReplicationEvent) -> bool: - return evt.event_type == ReplicationEventType.DELETE_SYSTEM_ROLE and any(t.relation == relation for t in evt.remove) + return evt.event_type == ReplicationEventType.DELETE_SYSTEM_ROLE and any( + t.relation == relation for t in evt.remove + ) + + def is_update_event(self, relation: str, evt: ReplicationEvent) -> bool: + return evt.event_type == ReplicationEventType.UPDATE_SYSTEM_ROLE and any( + t.relation == relation for t in evt.add + ) @patch("management.relation_replicator.outbox_replicator.OutboxReplicator.replicate") def test_seed_roles_new_role(self, mock_replicate): seed_roles() - self.assertTrue(any(self.is_create_event("inventory_hosts_read", args[0]) for args, _ in mock_replicate.call_args_list)) + self.assertTrue( + any(self.is_create_event("inventory_hosts_read", args[0]) for args, _ in mock_replicate.call_args_list) + ) @patch("management.relation_replicator.outbox_replicator.OutboxReplicator.replicate") @patch("management.role.definer.destructive_ok") @@ -276,16 +284,16 @@ def test_seed_roles_delete_role( # create a role in the database that's not in config role_to_delete = Role.objects.create(name="dummy_role_delete", system=True, tenant=self.public_tenant) - # role_to_delete.access.add(["inventory:groups:read"]) - public_tenant = Tenant.objects.get(tenant_name="public") - permission, created = Permission.objects.get_or_create(permission="inventory:hosts:read", tenant=public_tenant) - access_obj = Access.objects.create(permission=permission, role=role_to_delete, tenant=public_tenant) + permission, _ = Permission.objects.get_or_create(permission="inventory:hosts:read", tenant=self.public_tenant) + _ = Access.objects.create(permission=permission, role=role_to_delete, tenant=self.public_tenant) role_to_delete.save() seed_roles() - self.assertTrue(any(self.is_remove_event("inventory_hosts_read", args[0]) for args, _ in mock_replicate.call_args_list)) + self.assertTrue( + any(self.is_remove_event("inventory_hosts_read", args[0]) for args, _ in mock_replicate.call_args_list) + ) # verify role was deleted from the database self.assertFalse(Role.objects.filter(id=role_to_delete.id).exists()) @@ -294,11 +302,10 @@ def test_seed_roles_delete_role( @patch( "builtins.open", new_callable=mock_open, - read_data='{"roles": [{"name": "dummy_role_update", "system": true, "version": 3}]}', + read_data='{"roles": [{"name": "dummy_role_update", "system": true, "version": 3, "access": [{"permission": "dummy:hosts:read"}]}]}', ) @patch("os.listdir") @patch("os.path.isfile") - # @patch("management.relation_replicator.outbox_replicator.OutboxReplicator.replicate") def test_seed_roles_update_role( self, mock_isfile, @@ -311,11 +318,10 @@ def test_seed_roles_update_role( mock_isfile.return_value = True # create a role in the database that exists in config - Role.objects.create(name="dummy_role_update", system=True, tenant=self.public_tenant) + Role.objects.create(name="dummy_role_update", system=True, version=1, tenant=self.public_tenant) seed_roles() - # return arg[0].event_type == ReplicationEventType.CREATE_SYSTEM_ROLE - - self.assertTrue(any(arg[0].add for arg, _ in mock_replicate.call_args_list)) - + self.assertTrue( + any(self.is_update_event("dummy_hosts_read", args[0]) for args, _ in mock_replicate.call_args_list) + ) From abcf8f493df9be9c6834f59591e27da9572dfd00 Mon Sep 17 00:00:00 2001 From: Jonathan Marcantonio Date: Fri, 18 Oct 2024 10:40:22 -0400 Subject: [PATCH 8/8] Add test to seed to create then seed to delete Signed-off-by: Jonathan Marcantonio --- tests/management/role/test_definer.py | 29 +++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/management/role/test_definer.py b/tests/management/role/test_definer.py index ab1822baf..0debed045 100644 --- a/tests/management/role/test_definer.py +++ b/tests/management/role/test_definer.py @@ -325,3 +325,32 @@ def test_seed_roles_update_role( self.assertTrue( any(self.is_update_event("dummy_hosts_read", args[0]) for args, _ in mock_replicate.call_args_list) ) + + @patch("management.relation_replicator.outbox_replicator.OutboxReplicator.replicate") + def test_seed_roles_create_and_delete_role( + self, + mock_replicate, + ): + # seed to create role + seed_roles() + self.assertTrue( + any(self.is_create_event("inventory_hosts_read", args[0]) for args, _ in mock_replicate.call_args_list) + ) + + # seed to remove role + with ( + patch("os.path.isfile") as mock_isfile, + patch("os.listdir") as mock_listdir, + patch("builtins.open", mock_open(read_data='{"roles": []}')) as mock_file, + patch("management.role.definer.destructive_ok") as mock_destructive_ok, + ): + # mock files + mock_destructive_ok.return_value = True + mock_listdir.return_value = ["role.json"] + mock_isfile.return_value = True + + seed_roles() + + self.assertTrue( + any(self.is_remove_event("inventory_hosts_read", args[0]) for args, _ in mock_replicate.call_args_list) + )