@@ -759,3 +759,161 @@ def test_json_deserialization(self) -> None:
759759 config = BashActionConfig (** json_data )
760760 assert config .id == make_action_id ("deploy" )
761761 assert config .name == "Deploy"
762+
763+
764+ class TestCollectionLimits :
765+ """Test collection size limits for DoS protection."""
766+
767+ def test_rejects_too_many_actions_in_branch (self ) -> None :
768+ """Should reject branch with too many actions (>100)."""
769+ with pytest .raises (ValidationError , match = "Too many actions" ):
770+ BranchConfig (
771+ id = make_branch_id ("test" ),
772+ title = "Test" ,
773+ actions = [
774+ BashActionConfig (
775+ id = make_action_id (f"action{ i } " ),
776+ name = f"Action { i } " ,
777+ command = "echo test" ,
778+ )
779+ for i in range (101 ) # Over limit
780+ ],
781+ )
782+
783+ def test_accepts_max_actions_in_branch (self ) -> None :
784+ """Should accept branch with exactly 100 actions."""
785+ config = BranchConfig (
786+ id = make_branch_id ("test" ),
787+ title = "Test" ,
788+ actions = [
789+ BashActionConfig (
790+ id = make_action_id (f"action{ i } " ),
791+ name = f"Action { i } " ,
792+ command = "echo test" ,
793+ )
794+ for i in range (100 ) # At limit
795+ ],
796+ )
797+ assert len (config .actions ) == 100
798+
799+ def test_rejects_too_many_options_in_branch (self ) -> None :
800+ """Should reject branch with too many options (>50)."""
801+ with pytest .raises (ValidationError , match = "Too many options" ):
802+ BranchConfig (
803+ id = make_branch_id ("test" ),
804+ title = "Test" ,
805+ options = [
806+ StringOptionConfig (
807+ id = make_option_key (f"option{ i } " ),
808+ name = f"Option { i } " ,
809+ description = "Test option" ,
810+ )
811+ for i in range (51 ) # Over limit
812+ ],
813+ )
814+
815+ def test_accepts_max_options_in_branch (self ) -> None :
816+ """Should accept branch with exactly 50 options."""
817+ config = BranchConfig (
818+ id = make_branch_id ("test" ),
819+ title = "Test" ,
820+ options = [
821+ StringOptionConfig (
822+ id = make_option_key (f"option{ i } " ),
823+ name = f"Option { i } " ,
824+ description = "Test option" ,
825+ )
826+ for i in range (50 ) # At limit
827+ ],
828+ )
829+ assert len (config .options ) == 50
830+
831+ def test_rejects_too_many_menus_in_branch (self ) -> None :
832+ """Should reject branch with too many menus (>20)."""
833+ with pytest .raises (ValidationError , match = "Too many menus" ):
834+ BranchConfig (
835+ id = make_branch_id ("test" ),
836+ title = "Test" ,
837+ menus = [
838+ MenuConfig (
839+ id = make_menu_id (f"menu{ i } " ),
840+ label = f"Menu { i } " ,
841+ target = make_branch_id ("target" ),
842+ )
843+ for i in range (21 ) # Over limit
844+ ],
845+ )
846+
847+ def test_accepts_max_menus_in_branch (self ) -> None :
848+ """Should accept branch with exactly 20 menus."""
849+ config = BranchConfig (
850+ id = make_branch_id ("test" ),
851+ title = "Test" ,
852+ menus = [
853+ MenuConfig (
854+ id = make_menu_id (f"menu{ i } " ),
855+ label = f"Menu { i } " ,
856+ target = make_branch_id ("target" ),
857+ )
858+ for i in range (20 ) # At limit
859+ ],
860+ )
861+ assert len (config .menus ) == 20
862+
863+ def test_rejects_too_many_branches_in_wizard (self ) -> None :
864+ """Should reject wizard with too many branches (>100)."""
865+ with pytest .raises (ValidationError , match = "Too many branches" ):
866+ WizardConfig (
867+ name = "test" ,
868+ version = "1.0.0" ,
869+ entry_branch = make_branch_id ("branch0" ),
870+ branches = [
871+ BranchConfig (
872+ id = make_branch_id (f"branch{ i } " ),
873+ title = f"Branch { i } " ,
874+ )
875+ for i in range (101 ) # Over limit
876+ ],
877+ )
878+
879+ def test_accepts_max_branches_in_wizard (self ) -> None :
880+ """Should accept wizard with exactly 100 branches."""
881+ config = WizardConfig (
882+ name = "test" ,
883+ version = "1.0.0" ,
884+ entry_branch = make_branch_id ("branch0" ),
885+ branches = [
886+ BranchConfig (
887+ id = make_branch_id (f"branch{ i } " ),
888+ title = f"Branch { i } " ,
889+ )
890+ for i in range (100 ) # At limit
891+ ],
892+ )
893+ assert len (config .branches ) == 100
894+
895+ def test_rejects_too_many_option_values_in_session (self ) -> None :
896+ """Should reject session with too many option values (>1000)."""
897+ with pytest .raises (ValidationError , match = "Too many options" ):
898+ SessionState (
899+ option_values = {
900+ make_option_key (f"option{ i } " ): "value" for i in range (1001 )
901+ }
902+ )
903+
904+ def test_accepts_max_option_values_in_session (self ) -> None :
905+ """Should accept session with exactly 1000 option values."""
906+ state = SessionState (
907+ option_values = {make_option_key (f"option{ i } " ): "value" for i in range (1000 )}
908+ )
909+ assert len (state .option_values ) == 1000
910+
911+ def test_rejects_too_many_variables_in_session (self ) -> None :
912+ """Should reject session with too many variables (>1000)."""
913+ with pytest .raises (ValidationError , match = "Too many variables" ):
914+ SessionState (variables = {f"var{ i } " : "value" for i in range (1001 )})
915+
916+ def test_accepts_max_variables_in_session (self ) -> None :
917+ """Should accept session with exactly 1000 variables."""
918+ state = SessionState (variables = {f"var{ i } " : "value" for i in range (1000 )})
919+ assert len (state .variables ) == 1000
0 commit comments