@@ -675,98 +675,117 @@ def helper_test_transcode(http_options_list, expected_result_list):
675675 return (http_options , expected_result )
676676
677677
678- def test_path_traversal_dots_validation_star ():
679- # 1. Dots in values matched to *
680- # Single-segment positional variable * with exactly '.' or '..'
681- with pytest .raises (
682- ValueError , match = "Invalid value \\ . for positional variable\\ ."
683- ):
684- path_template .expand ("/v1/*" , "." )
685- with pytest .raises (
686- ValueError , match = "Invalid value \\ .\\ . for positional variable\\ ."
687- ):
688- path_template .expand ("/v1/*" , ".." )
689-
690- # Named variable matching * with exactly '.' or '..'
691- with pytest .raises (ValueError , match = "Invalid value \\ .\\ . for region\\ ." ):
692- path_template .expand (
678+ @pytest .mark .parametrize (
679+ "tmpl, args, kwargs, expected_err_match" ,
680+ [
681+ # Positional * with . or ..
682+ [
683+ "/v1/*" ,
684+ [
685+ "." ,
686+ ],
687+ {},
688+ "Invalid value \\ . for positional variable\\ ." ,
689+ ],
690+ [
691+ "/v1/*" ,
692+ [
693+ ".." ,
694+ ],
695+ {},
696+ "Invalid value \\ .\\ . for positional variable\\ ." ,
697+ ],
698+ # Named matching * with . or ..
699+ [
693700 "/compute/v1/projects/{project}/regions/{region}/addresses" ,
694- project = "my-project" ,
695- region = ".." ,
696- )
697-
698- # Sub-template named variable where a segment matching * is exactly '.' or '..'
699- with pytest .raises (
700- ValueError ,
701- match = "Invalid value projects/my-project/locations/\\ .\\ . for parent\\ ." ,
702- ):
703- path_template .expand (
701+ [],
702+ {"project" : "my-project" , "region" : ".." },
703+ "Invalid value \\ .\\ . for region\\ ." ,
704+ ],
705+ [
706+ "/compute/v1/projects/{project}/regions/{region}/addresses" ,
707+ [],
708+ {"project" : "my-project" , "region" : "." },
709+ "Invalid value \\ . for region\\ ." ,
710+ ],
711+ # Sub-template named matching * with . or ..
712+ [
704713 "/v2/{parent=projects/*/locations/*}/content:inspect" ,
705- parent = "projects/my-project/locations/.." ,
706- )
707- with pytest .raises (
708- ValueError ,
709- match = "Invalid value projects/my-project/locations/\\ . for parent\\ ." ,
710- ):
711- path_template .expand (
714+ [],
715+ {"parent" : "projects/my-project/locations/.." },
716+ "Invalid value projects/my-project/locations/\\ .\\ . for parent\\ ." ,
717+ ],
718+ [
712719 "/v2/{parent=projects/*/locations/*}/content:inspect" ,
713- parent = "projects/my-project/locations/." ,
714- )
715-
720+ [],
721+ {"parent" : "projects/my-project/locations/." },
722+ "Invalid value projects/my-project/locations/\\ . for parent\\ ." ,
723+ ],
724+ ],
725+ )
726+ def test_path_traversal_dots_validation_star (tmpl , args , kwargs , expected_err_match ):
727+ with pytest .raises (ValueError , match = expected_err_match ):
728+ path_template .expand (tmpl , * args , ** kwargs )
716729
717- def test_path_traversal_dots_validation_double_star ():
718- # 2. Dots in values matched to **
719- # Valid cases: leftover_segments > 0
720- # leftover_segments == 1
721- assert (
722- path_template .expand (
723- "/v3/{name=projects/*/monitoredResourceDescriptors/**}" ,
724- name = "projects/my-project/monitoredResourceDescriptors/instance/my-instance/.." ,
725- )
726- == "/v3/projects/my-project/monitoredResourceDescriptors/instance/my-instance/.."
727- )
728730
731+ @pytest .mark .parametrize (
732+ "name_val, expected_path" ,
733+ [
734+ (
735+ "projects/my-project/monitoredResourceDescriptors/instance/my-instance/.." ,
736+ "/v3/projects/my-project/monitoredResourceDescriptors/instance/my-instance/.." ,
737+ ),
738+ (
739+ "projects/my-project/monitoredResourceDescriptors/instance/my-instance/." ,
740+ "/v3/projects/my-project/monitoredResourceDescriptors/instance/my-instance/." ,
741+ ),
742+ (
743+ "projects/my-project/monitoredResourceDescriptors/a/b/c/d/e/../../../.." ,
744+ "/v3/projects/my-project/monitoredResourceDescriptors/a/b/c/d/e/../../../.." ,
745+ ),
746+ ],
747+ )
748+ def test_path_traversal_dots_validation_double_star_valid (name_val , expected_path ):
729749 assert (
730750 path_template .expand (
731751 "/v3/{name=projects/*/monitoredResourceDescriptors/**}" ,
732- name = "projects/my-project/monitoredResourceDescriptors/instance/my-instance/." ,
752+ name = name_val ,
733753 )
734- == "/v3/projects/my-project/monitoredResourceDescriptors/instance/my-instance/."
754+ == expected_path
735755 )
736756
737- assert (
738- path_template .expand (
739- "/v3/{name=projects/*/monitoredResourceDescriptors/**}" ,
740- name = "projects/my-project/monitoredResourceDescriptors/a/b/c/d/e/../../../.." ,
741- )
742- == "/v3/projects/my-project/monitoredResourceDescriptors/a/b/c/d/e/../../../.."
743- )
744757
745- # Invalid cases: resulting path traversal consumes whole value or overflows left
746- invalid_cases = [
758+ @pytest .mark .parametrize (
759+ "name_val" ,
760+ [
747761 "projects/my-project/monitoredResourceDescriptors/." ,
748762 "projects/my-project/monitoredResourceDescriptors/instance/my-instance/../.." ,
749763 "projects/my-project/monitoredResourceDescriptors/instance/../my-instance/.." ,
750764 "projects/my-project/monitoredResourceDescriptors/.." ,
751765 "projects/my-project/monitoredResourceDescriptors/instance/../.." ,
752766 "projects/my-project/monitoredResourceDescriptors/a/b/../../../c/d/e/.." ,
753- ]
754- for case in invalid_cases :
755- with pytest .raises (ValueError , match = "Invalid value .* for name\\ ." ):
756- path_template .expand (
757- "/v3/{name=projects/*/monitoredResourceDescriptors/**}" , name = case
758- )
759-
760-
761- def test_percent_encoding_unreserved_characters ():
762- # 3. Values for all variable parts should be percent-encoded except for [-_.~/0-9a-zA-Z] characters.
763- # We should keep [-_.~/0-9a-zA-Z] safe for both single-star and double-star
764- result = path_template .expand ("/v1/{name}" , name = "abc-._~" )
765- assert result == "/v1/abc-._~"
766-
767- result = path_template .expand ("/v1/{name=**}" , name = "abc-._~/" )
768- assert result == "/v1/abc-._~/"
769-
770- # Other characters like '$', '?', '=', '#', ' ' should be encoded
771- result = path_template .expand ("/v1/{name}" , name = "a$b?c=d#e f" )
772- assert result == "/v1/a%24b%3Fc%3Dd%23e%20f"
767+ ],
768+ )
769+ def test_path_traversal_dots_validation_double_star_invalid (name_val ):
770+ with pytest .raises (ValueError , match = "Invalid value .* for name\\ ." ):
771+ path_template .expand (
772+ "/v3/{name=projects/*/monitoredResourceDescriptors/**}" ,
773+ name = name_val ,
774+ )
775+
776+
777+ @pytest .mark .parametrize (
778+ "tmpl, kwargs, expected_result" ,
779+ [
780+ ["/v1/{name}" , {"name" : "abc-._~" }, "/v1/abc-._~" ],
781+ ["/v1/{name=**}" , {"name" : "abc-._~/" }, "/v1/abc-._~/" ],
782+ ["/v1/{name}" , {"name" : "a/b" }, "/v1/a/b" ],
783+ ["/v1/{name}" , {"name" : "a$b?c=d#e f" }, "/v1/a%24b%3Fc%3Dd%23e%20f" ],
784+ ],
785+ )
786+ def test_percent_encoding_unreserved_characters (tmpl , kwargs , expected_result ):
787+ result = path_template .expand (tmpl , ** kwargs )
788+ assert result == expected_result
789+ # For single-segment with '/', validate should fail because '/' is preserved
790+ if "/" in kwargs .get ("name" , "" ) and tmpl == "/v1/{name}" :
791+ assert not path_template .validate (tmpl , result )
0 commit comments