@@ -736,3 +736,168 @@ def test_second_call_after_refetch_uses_cache(self, import_main):
736736
737737 assert group_ids == {"group-y" }
738738 assert email == "twice@test.com"
739+
740+
741+ class TestShowButtonsForApproverGroups :
742+ """Approve/Deny buttons must render whenever a decision has approvers OR approver_groups.
743+
744+ Regression: previously `show_buttons = bool(decision.approvers)` ignored approver_groups,
745+ leaving requests that only mentioned groups un-actionable in Slack.
746+ """
747+
748+ def _build_request (self ):
749+ import slack_helpers
750+
751+ return slack_helpers .RequestForAccess (
752+ permission_set_name = "TestPermissionSet" ,
753+ account_id = "111111111111" ,
754+ reason = "Testing" ,
755+ requester_slack_id = "U_REQUESTER" ,
756+ permission_duration = timedelta (hours = 1 ),
757+ )
758+
759+ def _decision (self , reason , approvers = frozenset (), approver_groups = frozenset ()):
760+ import access_control
761+
762+ return access_control .AccessRequestDecision (
763+ grant = False ,
764+ reason = reason ,
765+ based_on_statements = frozenset (),
766+ approvers = approvers ,
767+ approver_groups = approver_groups ,
768+ )
769+
770+ def _setup_patches (self , main_module , decision ):
771+ import access_control
772+
773+ patches = [
774+ patch .object (main_module .access_control , "make_decision_on_access_request" , return_value = decision ),
775+ patch .object (
776+ main_module .access_control ,
777+ "execute_decision" ,
778+ return_value = access_control .ExecuteDecisionResult (granted = False ),
779+ ),
780+ patch .object (
781+ main_module .sso ,
782+ "get_permission_set" ,
783+ return_value = entities .aws .PermissionSet (name = "TestPermissionSet" , arn = "arn:sso:ps/test" , description = None ),
784+ ),
785+ patch .object (main_module .sso , "get_identity_store_id" , return_value = "d-123456" ),
786+ patch .object (main_module .sso , "get_user_principal_id_by_email" , return_value = ("principal-id" , None )),
787+ patch .object (
788+ main_module .organizations ,
789+ "describe_account" ,
790+ side_effect = lambda _c , account_id : entities .aws .Account (id = account_id , name = f"Account-{ account_id } " ),
791+ ),
792+ patch .object (main_module .slack_helpers , "build_approval_request_message_blocks" , return_value = []),
793+ patch .object (main_module .slack_helpers , "check_if_user_is_in_channel" , return_value = True ),
794+ patch .object (
795+ main_module .slack_helpers ,
796+ "get_user" ,
797+ return_value = entities .slack .User (id = "U_REQUESTER" , email = "requester@test.com" , real_name = "Requester" ),
798+ ),
799+ patch .object (main_module .slack_helpers , "find_approvers_in_slack" , return_value = ([], [])),
800+ patch .object (main_module .slack_helpers , "build_approver_group_mentions" , return_value = "<!subteam^S_GROUP>" ),
801+ patch .object (main_module .analytics , "capture" ),
802+ patch .object (main_module .schedule , "schedule_discard_buttons_event" ),
803+ patch .object (main_module .schedule , "schedule_approver_notification_event" ),
804+ ]
805+ return patches
806+
807+ def _run (self , main_module ):
808+ mock_client = MagicMock ()
809+ mock_client .chat_postMessage .return_value = {"ts" : "123.456" , "message" : {"blocks" : []}}
810+ main_module ._process_single_access_request (
811+ request = self ._build_request (),
812+ requester = entities .slack .User (id = "U_REQUESTER" , email = "requester@test.com" , real_name = "Requester" ),
813+ user_group_ids = set (),
814+ client = mock_client ,
815+ is_user_in_channel = True ,
816+ )
817+ return mock_client
818+
819+ def _show_buttons_kwarg (self , main_module ):
820+ build = main_module .slack_helpers .build_approval_request_message_blocks
821+ assert build .called , "build_approval_request_message_blocks was not called"
822+ return build .call_args .kwargs ["show_buttons" ]
823+
824+ def test_show_buttons_true_when_only_approver_groups (self , import_main ):
825+ """Regression: approver_groups alone must still render buttons."""
826+ import access_control
827+
828+ main = import_main
829+ decision = self ._decision (
830+ reason = access_control .DecisionReason .RequiresApproval ,
831+ approvers = frozenset (),
832+ approver_groups = frozenset (["S_GROUP" ]),
833+ )
834+ patches = self ._setup_patches (main , decision )
835+ for p in patches :
836+ p .start ()
837+ try :
838+ self ._run (main )
839+ assert self ._show_buttons_kwarg (main ) is True
840+ main .schedule .schedule_discard_buttons_event .assert_called_once ()
841+ main .schedule .schedule_approver_notification_event .assert_called_once ()
842+ finally :
843+ for p in patches :
844+ p .stop ()
845+
846+ def test_show_buttons_true_when_only_individual_approvers (self , import_main ):
847+ import access_control
848+
849+ main = import_main
850+ decision = self ._decision (
851+ reason = access_control .DecisionReason .RequiresApproval ,
852+ approvers = frozenset (["approver@test.com" ]),
853+ approver_groups = frozenset (),
854+ )
855+ patches = self ._setup_patches (main , decision )
856+ for p in patches :
857+ p .start ()
858+ try :
859+ self ._run (main )
860+ assert self ._show_buttons_kwarg (main ) is True
861+ finally :
862+ for p in patches :
863+ p .stop ()
864+
865+ def test_show_buttons_true_when_both_set (self , import_main ):
866+ import access_control
867+
868+ main = import_main
869+ decision = self ._decision (
870+ reason = access_control .DecisionReason .RequiresApproval ,
871+ approvers = frozenset (["approver@test.com" ]),
872+ approver_groups = frozenset (["S_GROUP" ]),
873+ )
874+ patches = self ._setup_patches (main , decision )
875+ for p in patches :
876+ p .start ()
877+ try :
878+ self ._run (main )
879+ assert self ._show_buttons_kwarg (main ) is True
880+ finally :
881+ for p in patches :
882+ p .stop ()
883+
884+ def test_show_buttons_false_when_neither_set (self , import_main ):
885+ import access_control
886+
887+ main = import_main
888+ decision = self ._decision (
889+ reason = access_control .DecisionReason .NoApprovers ,
890+ approvers = frozenset (),
891+ approver_groups = frozenset (),
892+ )
893+ patches = self ._setup_patches (main , decision )
894+ for p in patches :
895+ p .start ()
896+ try :
897+ self ._run (main )
898+ assert self ._show_buttons_kwarg (main ) is False
899+ main .schedule .schedule_discard_buttons_event .assert_not_called ()
900+ main .schedule .schedule_approver_notification_event .assert_not_called ()
901+ finally :
902+ for p in patches :
903+ p .stop ()
0 commit comments