# Copyright 2017 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from __future__ import unicode_literals from unittest import mock import pytest from google.api import auth_pb2 from google.api_core import path_template @pytest.mark.parametrize( "tmpl, args, kwargs, expected_result", [ # Basic positional params ["/v1/*", ["a"], {}, "/v1/a"], ["/v1/**", ["a/b"], {}, "/v1/a/b"], ["/v1/*/*", ["a", "b"], {}, "/v1/a/b"], ["/v1/*/*/**", ["a", "b", "c/d"], {}, "/v1/a/b/c/d"], # Basic named params ["/v1/{name}", [], {"name": "parent"}, "/v1/parent"], ["/v1/{name=**}", [], {"name": "parent/child"}, "/v1/parent/child"], # Named params with a sub-template ["/v1/{name=parent/*}", [], {"name": "parent/child"}, "/v1/parent/child"], [ "/v1/{name=parent/**}", [], {"name": "parent/child/object"}, "/v1/parent/child/object", ], # Combining positional and named params ["/v1/*/{name}", ["a"], {"name": "parent"}, "/v1/a/parent"], ["/v1/{name}/*", ["a"], {"name": "parent"}, "/v1/parent/a"], [ "/v1/{parent}/*/{child}/*", ["a", "b"], {"parent": "thor", "child": "thorson"}, "/v1/thor/a/thorson/b", ], ["/v1/{name}/**", ["a/b"], {"name": "parent"}, "/v1/parent/a/b"], # Combining positional and named params with sub-templates. [ "/v1/{name=parent/*}/*", ["a"], {"name": "parent/child"}, "/v1/parent/child/a", ], [ "/v1/*/{name=parent/**}", ["a"], {"name": "parent/child/object"}, "/v1/a/parent/child/object", ], ], ) def test_expand_success(tmpl, args, kwargs, expected_result): result = path_template.expand(tmpl, *args, **kwargs) assert result == expected_result assert path_template.validate(tmpl, result) @pytest.mark.parametrize( "tmpl, args, kwargs, exc_match", [ # Missing positional arg. ["v1/*", [], {}, "Positional"], # Missing named arg. ["v1/{name}", [], {}, "Named"], ], ) def test_expanded_failure(tmpl, args, kwargs, exc_match): with pytest.raises(ValueError, match=exc_match): path_template.expand(tmpl, *args, **kwargs) @pytest.mark.parametrize( "request_obj, field, expected_result", [ [{"field": "stringValue"}, "field", "stringValue"], [{"field": "stringValue"}, "nosuchfield", None], [{"field": "stringValue"}, "field.subfield", None], [{"field": {"subfield": "stringValue"}}, "field", None], [{"field": {"subfield": "stringValue"}}, "field.subfield", "stringValue"], [{"field": {"subfield": [1, 2, 3]}}, "field.subfield", [1, 2, 3]], [{"field": {"subfield": "stringValue"}}, "field", None], [{"field": {"subfield": "stringValue"}}, "field.nosuchfield", None], [ {"field": {"subfield": {"subsubfield": "stringValue"}}}, "field.subfield.subsubfield", "stringValue", ], ["string", "field", None], ], ) def test_get_field(request_obj, field, expected_result): result = path_template.get_field(request_obj, field) assert result == expected_result @pytest.mark.parametrize( "request_obj, field, expected_result", [ [{"field": "stringValue"}, "field", {}], [{"field": "stringValue"}, "nosuchfield", {"field": "stringValue"}], [{"field": "stringValue"}, "field.subfield", {"field": "stringValue"}], [{"field": {"subfield": "stringValue"}}, "field.subfield", {"field": {}}], [ {"field": {"subfield": "stringValue", "q": "w"}, "e": "f"}, "field.subfield", {"field": {"q": "w"}, "e": "f"}, ], [ {"field": {"subfield": "stringValue"}}, "field.nosuchfield", {"field": {"subfield": "stringValue"}}, ], [ {"field": {"subfield": {"subsubfield": "stringValue", "q": "w"}}}, "field.subfield.subsubfield", {"field": {"subfield": {"q": "w"}}}, ], ["string", "field", "string"], ["string", "field.subfield", "string"], ], ) def test_delete_field(request_obj, field, expected_result): path_template.delete_field(request_obj, field) assert request_obj == expected_result @pytest.mark.parametrize( "tmpl, path", [ # Single segment template, but multi segment value ["v1/*", "v1/a/b"], ["v1/*/*", "v1/a/b/c"], # Single segement named template, but multi segment value ["v1/{name}", "v1/a/b"], ["v1/{name}/{value}", "v1/a/b/c"], # Named value with a sub-template but invalid value ["v1/{name=parent/*}", "v1/grandparent/child"], ], ) def test_validate_failure(tmpl, path): assert not path_template.validate(tmpl, path) def test__expand_variable_match_unexpected(): match = mock.Mock(spec=["group"]) match.group.return_value = None with pytest.raises(ValueError, match="Unknown"): path_template._expand_variable_match([], {}, match) def test__replace_variable_with_pattern(): match = mock.Mock(spec=["group"]) match.group.return_value = None with pytest.raises(ValueError, match="Unknown"): path_template._replace_variable_with_pattern(match) @pytest.mark.parametrize( "http_options, message, request_kwargs, expected_result", [ [ [["get", "/v1/no/template", ""]], None, {"foo": "bar"}, ["get", "/v1/no/template", {}, {"foo": "bar"}], ], [ [["get", "/v1/no/template", ""]], auth_pb2.AuthenticationRule(selector="bar"), {}, [ "get", "/v1/no/template", None, auth_pb2.AuthenticationRule(selector="bar"), ], ], # Single templates [ [["get", "/v1/{field}", ""]], None, {"field": "parent"}, ["get", "/v1/parent", {}, {}], ], [ [["get", "/v1/{selector}", ""]], auth_pb2.AuthenticationRule(selector="parent"), {}, ["get", "/v1/parent", None, auth_pb2.AuthenticationRule()], ], [ [["get", "/v1/{field.sub}", ""]], None, {"field": {"sub": "parent"}, "foo": "bar"}, ["get", "/v1/parent", {}, {"field": {}, "foo": "bar"}], ], [ [["get", "/v1/{oauth.canonical_scopes}", ""]], auth_pb2.AuthenticationRule( selector="bar", oauth=auth_pb2.OAuthRequirements(canonical_scopes="parent"), ), {}, [ "get", "/v1/parent", None, auth_pb2.AuthenticationRule( selector="bar", oauth=auth_pb2.OAuthRequirements() ), ], ], ], ) def test_transcode_base_case(http_options, message, request_kwargs, expected_result): http_options, expected_result = helper_test_transcode(http_options, expected_result) result = path_template.transcode(http_options, message, **request_kwargs) assert result == expected_result @pytest.mark.parametrize( "http_options, message, request_kwargs, expected_result", [ [ [["get", "/v1/{field.subfield}", ""]], None, {"field": {"subfield": "parent"}, "foo": "bar"}, ["get", "/v1/parent", {}, {"field": {}, "foo": "bar"}], ], [ [["get", "/v1/{oauth.canonical_scopes}", ""]], auth_pb2.AuthenticationRule( selector="bar", oauth=auth_pb2.OAuthRequirements(canonical_scopes="parent"), ), {}, [ "get", "/v1/parent", None, auth_pb2.AuthenticationRule( selector="bar", oauth=auth_pb2.OAuthRequirements() ), ], ], [ [["get", "/v1/{field.subfield.subsubfield}", ""]], None, {"field": {"subfield": {"subsubfield": "parent"}}, "foo": "bar"}, ["get", "/v1/parent", {}, {"field": {"subfield": {}}, "foo": "bar"}], ], [ [["get", "/v1/{field.subfield1}/{field.subfield2}", ""]], None, {"field": {"subfield1": "parent", "subfield2": "child"}, "foo": "bar"}, ["get", "/v1/parent/child", {}, {"field": {}, "foo": "bar"}], ], [ [["get", "/v1/{selector}/{oauth.canonical_scopes}", ""]], auth_pb2.AuthenticationRule( selector="parent", oauth=auth_pb2.OAuthRequirements(canonical_scopes="child"), ), {"field": {"subfield1": "parent", "subfield2": "child"}, "foo": "bar"}, [ "get", "/v1/parent/child", None, auth_pb2.AuthenticationRule(oauth=auth_pb2.OAuthRequirements()), ], ], ], ) def test_transcode_subfields(http_options, message, request_kwargs, expected_result): http_options, expected_result = helper_test_transcode(http_options, expected_result) result = path_template.transcode(http_options, message, **request_kwargs) assert result == expected_result @pytest.mark.parametrize( "http_options, message, request_kwargs, expected_result", [ # Single segment wildcard [ [["get", "/v1/{field=*}", ""]], None, {"field": "parent"}, ["get", "/v1/parent", {}, {}], ], [ [["get", "/v1/{selector=*}", ""]], auth_pb2.AuthenticationRule(selector="parent"), {}, ["get", "/v1/parent", None, auth_pb2.AuthenticationRule()], ], [ [["get", "/v1/{field=a/*/b/*}", ""]], None, {"field": "a/parent/b/child", "foo": "bar"}, ["get", "/v1/a/parent/b/child", {}, {"foo": "bar"}], ], [ [["get", "/v1/{selector=a/*/b/*}", ""]], auth_pb2.AuthenticationRule( selector="a/parent/b/child", allow_without_credential=True ), {}, [ "get", "/v1/a/parent/b/child", None, auth_pb2.AuthenticationRule(allow_without_credential=True), ], ], # Double segment wildcard [ [["get", "/v1/{field=**}", ""]], None, {"field": "parent/p1"}, ["get", "/v1/parent/p1", {}, {}], ], [ [["get", "/v1/{selector=**}", ""]], auth_pb2.AuthenticationRule(selector="parent/p1"), {}, ["get", "/v1/parent/p1", None, auth_pb2.AuthenticationRule()], ], [ [["get", "/v1/{field=a/**/b/**}", ""]], None, {"field": "a/parent/p1/b/child/c1", "foo": "bar"}, ["get", "/v1/a/parent/p1/b/child/c1", {}, {"foo": "bar"}], ], [ [["get", "/v1/{selector=a/**/b/**}", ""]], auth_pb2.AuthenticationRule( selector="a/parent/p1/b/child/c1", allow_without_credential=True ), {}, [ "get", "/v1/a/parent/p1/b/child/c1", None, auth_pb2.AuthenticationRule(allow_without_credential=True), ], ], # Combined single and double segment wildcard [ [["get", "/v1/{field=a/*/b/**}", ""]], None, {"field": "a/parent/b/child/c1"}, ["get", "/v1/a/parent/b/child/c1", {}, {}], ], [ [["get", "/v1/{selector=a/*/b/**}", ""]], auth_pb2.AuthenticationRule(selector="a/parent/b/child/c1"), {}, ["get", "/v1/a/parent/b/child/c1", None, auth_pb2.AuthenticationRule()], ], [ [["get", "/v1/{field=a/**/b/*}/v2/{name}", ""]], None, {"field": "a/parent/p1/b/child", "name": "first", "foo": "bar"}, ["get", "/v1/a/parent/p1/b/child/v2/first", {}, {"foo": "bar"}], ], [ [["get", "/v1/{selector=a/**/b/*}/v2/{oauth.canonical_scopes}", ""]], auth_pb2.AuthenticationRule( selector="a/parent/p1/b/child", oauth=auth_pb2.OAuthRequirements(canonical_scopes="first"), ), {"field": "a/parent/p1/b/child", "name": "first", "foo": "bar"}, [ "get", "/v1/a/parent/p1/b/child/v2/first", None, auth_pb2.AuthenticationRule(oauth=auth_pb2.OAuthRequirements()), ], ], ], ) def test_transcode_with_wildcard( http_options, message, request_kwargs, expected_result ): http_options, expected_result = helper_test_transcode(http_options, expected_result) result = path_template.transcode(http_options, message, **request_kwargs) assert result == expected_result @pytest.mark.parametrize( "http_options, message, request_kwargs, expected_result", [ # Single field body [ [["post", "/v1/no/template", "data"]], None, {"data": {"id": 1, "info": "some info"}, "foo": "bar"}, ["post", "/v1/no/template", {"id": 1, "info": "some info"}, {"foo": "bar"}], ], [ [["post", "/v1/no/template", "oauth"]], auth_pb2.AuthenticationRule( selector="bar", oauth=auth_pb2.OAuthRequirements(canonical_scopes="child"), ), {}, [ "post", "/v1/no/template", auth_pb2.OAuthRequirements(canonical_scopes="child"), auth_pb2.AuthenticationRule(selector="bar"), ], ], [ [["post", "/v1/{field=a/*}/b/{name=**}", "data"]], None, { "field": "a/parent", "name": "first/last", "data": {"id": 1, "info": "some info"}, "foo": "bar", }, [ "post", "/v1/a/parent/b/first/last", {"id": 1, "info": "some info"}, {"foo": "bar"}, ], ], [ [["post", "/v1/{selector=a/*}/b/{oauth.canonical_scopes=**}", "oauth"]], auth_pb2.AuthenticationRule( selector="a/parent", allow_without_credential=True, requirements=[auth_pb2.AuthRequirement(provider_id="p")], oauth=auth_pb2.OAuthRequirements(canonical_scopes="first/last"), ), {}, [ "post", "/v1/a/parent/b/first/last", auth_pb2.OAuthRequirements(), auth_pb2.AuthenticationRule( requirements=[auth_pb2.AuthRequirement(provider_id="p")], allow_without_credential=True, ), ], ], # Wildcard body [ [["post", "/v1/{field=a/*}/b/{name=**}", "*"]], None, { "field": "a/parent", "name": "first/last", "data": {"id": 1, "info": "some info"}, "foo": "bar", }, [ "post", "/v1/a/parent/b/first/last", {"data": {"id": 1, "info": "some info"}, "foo": "bar"}, {}, ], ], [ [["post", "/v1/{selector=a/*}/b/{oauth.canonical_scopes=**}", "*"]], auth_pb2.AuthenticationRule( selector="a/parent", allow_without_credential=True, oauth=auth_pb2.OAuthRequirements(canonical_scopes="first/last"), ), { "field": "a/parent", "name": "first/last", "data": {"id": 1, "info": "some info"}, "foo": "bar", }, [ "post", "/v1/a/parent/b/first/last", auth_pb2.AuthenticationRule( allow_without_credential=True, oauth=auth_pb2.OAuthRequirements() ), auth_pb2.AuthenticationRule(), ], ], ], ) def test_transcode_with_body(http_options, message, request_kwargs, expected_result): http_options, expected_result = helper_test_transcode(http_options, expected_result) result = path_template.transcode(http_options, message, **request_kwargs) assert result == expected_result @pytest.mark.parametrize( "http_options, message, request_kwargs, expected_result", [ # Additional bindings [ [ ["post", "/v1/{field=a/*}/b/{name=**}", "extra_data"], ["post", "/v1/{field=a/*}/b/{name=**}", "*"], ], None, { "field": "a/parent", "name": "first/last", "data": {"id": 1, "info": "some info"}, "foo": "bar", }, [ "post", "/v1/a/parent/b/first/last", {"data": {"id": 1, "info": "some info"}, "foo": "bar"}, {}, ], ], [ [ [ "post", "/v1/{selector=a/*}/b/{oauth.canonical_scopes=**}", "extra_data", ], ["post", "/v1/{selector=a/*}/b/{oauth.canonical_scopes=**}", "*"], ], auth_pb2.AuthenticationRule( selector="a/parent", allow_without_credential=True, oauth=auth_pb2.OAuthRequirements(canonical_scopes="first/last"), ), {}, [ "post", "/v1/a/parent/b/first/last", auth_pb2.AuthenticationRule( allow_without_credential=True, oauth=auth_pb2.OAuthRequirements() ), auth_pb2.AuthenticationRule(), ], ], [ [ ["get", "/v1/{field=a/*}/b/{name=**}", ""], ["get", "/v1/{field=a/*}/b/first/last", ""], ], None, {"field": "a/parent", "foo": "bar"}, ["get", "/v1/a/parent/b/first/last", {}, {"foo": "bar"}], ], [ [ ["get", "/v1/{selector=a/*}/b/{oauth.allow_without_credential=**}", ""], ["get", "/v1/{selector=a/*}/b/first/last", ""], ], auth_pb2.AuthenticationRule( selector="a/parent", allow_without_credential=True, oauth=auth_pb2.OAuthRequirements(), ), {}, [ "get", "/v1/a/parent/b/first/last", None, auth_pb2.AuthenticationRule( allow_without_credential=True, oauth=auth_pb2.OAuthRequirements() ), ], ], ], ) def test_transcode_with_additional_bindings( http_options, message, request_kwargs, expected_result ): http_options, expected_result = helper_test_transcode(http_options, expected_result) result = path_template.transcode(http_options, message, **request_kwargs) assert result == expected_result @pytest.mark.parametrize( "http_options, message, request_kwargs", [ [[["get", "/v1/{name}", ""]], None, {"foo": "bar"}], [[["get", "/v1/{selector}", ""]], auth_pb2.AuthenticationRule(), {}], [[["get", "/v1/{name}", ""]], auth_pb2.AuthenticationRule(), {}], [[["get", "/v1/{name}", ""]], None, {"name": "first/last"}], [ [["get", "/v1/{selector}", ""]], auth_pb2.AuthenticationRule(selector="first/last"), {}, ], [[["get", "/v1/{name=mr/*/*}", ""]], None, {"name": "first/last"}], [ [["get", "/v1/{selector=mr/*/*}", ""]], auth_pb2.AuthenticationRule(selector="first/last"), {}, ], [[["post", "/v1/{name}", "data"]], None, {"name": "first/last"}], [ [["post", "/v1/{selector}", "data"]], auth_pb2.AuthenticationRule(selector="first"), {}, ], [[["post", "/v1/{first_name}", "data"]], None, {"last_name": "last"}], [ [["post", "/v1/{first_name}", ""]], auth_pb2.AuthenticationRule(selector="first"), {}, ], ], ) def test_transcode_fails(http_options, message, request_kwargs): http_options, _ = helper_test_transcode(http_options, range(4)) with pytest.raises(ValueError) as exc_info: path_template.transcode(http_options, message, **request_kwargs) assert str(exc_info.value).count("URI") == len(http_options) def helper_test_transcode(http_options_list, expected_result_list): http_options = [] for opt_list in http_options_list: http_option = {"method": opt_list[0], "uri": opt_list[1]} if opt_list[2]: http_option["body"] = opt_list[2] http_options.append(http_option) expected_result = { "method": expected_result_list[0], "uri": expected_result_list[1], "query_params": expected_result_list[3], } if expected_result_list[2]: expected_result["body"] = expected_result_list[2] return (http_options, expected_result)