1# Copyright 2017 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7#     http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15from __future__ import unicode_literals
16
17import mock
18import pytest
19
20from google.api_core import path_template
21
22
23@pytest.mark.parametrize(
24    "tmpl, args, kwargs, expected_result",
25    [
26        # Basic positional params
27        ["/v1/*", ["a"], {}, "/v1/a"],
28        ["/v1/**", ["a/b"], {}, "/v1/a/b"],
29        ["/v1/*/*", ["a", "b"], {}, "/v1/a/b"],
30        ["/v1/*/*/**", ["a", "b", "c/d"], {}, "/v1/a/b/c/d"],
31        # Basic named params
32        ["/v1/{name}", [], {"name": "parent"}, "/v1/parent"],
33        ["/v1/{name=**}", [], {"name": "parent/child"}, "/v1/parent/child"],
34        # Named params with a sub-template
35        ["/v1/{name=parent/*}", [], {"name": "parent/child"}, "/v1/parent/child"],
36        [
37            "/v1/{name=parent/**}",
38            [],
39            {"name": "parent/child/object"},
40            "/v1/parent/child/object",
41        ],
42        # Combining positional and named params
43        ["/v1/*/{name}", ["a"], {"name": "parent"}, "/v1/a/parent"],
44        ["/v1/{name}/*", ["a"], {"name": "parent"}, "/v1/parent/a"],
45        [
46            "/v1/{parent}/*/{child}/*",
47            ["a", "b"],
48            {"parent": "thor", "child": "thorson"},
49            "/v1/thor/a/thorson/b",
50        ],
51        ["/v1/{name}/**", ["a/b"], {"name": "parent"}, "/v1/parent/a/b"],
52        # Combining positional and named params with sub-templates.
53        [
54            "/v1/{name=parent/*}/*",
55            ["a"],
56            {"name": "parent/child"},
57            "/v1/parent/child/a",
58        ],
59        [
60            "/v1/*/{name=parent/**}",
61            ["a"],
62            {"name": "parent/child/object"},
63            "/v1/a/parent/child/object",
64        ],
65    ],
66)
67def test_expand_success(tmpl, args, kwargs, expected_result):
68    result = path_template.expand(tmpl, *args, **kwargs)
69    assert result == expected_result
70    assert path_template.validate(tmpl, result)
71
72
73@pytest.mark.parametrize(
74    "tmpl, args, kwargs, exc_match",
75    [
76        # Missing positional arg.
77        ["v1/*", [], {}, "Positional"],
78        # Missing named arg.
79        ["v1/{name}", [], {}, "Named"],
80    ],
81)
82def test_expanded_failure(tmpl, args, kwargs, exc_match):
83    with pytest.raises(ValueError, match=exc_match):
84        path_template.expand(tmpl, *args, **kwargs)
85
86
87@pytest.mark.parametrize(
88    "request_obj, field, expected_result",
89    [
90        [{"field": "stringValue"}, "field", "stringValue"],
91        [{"field": "stringValue"}, "nosuchfield", None],
92        [{"field": "stringValue"}, "field.subfield", None],
93        [{"field": {"subfield": "stringValue"}}, "field", None],
94        [{"field": {"subfield": "stringValue"}}, "field.subfield", "stringValue"],
95        [{"field": {"subfield": [1, 2, 3]}}, "field.subfield", [1, 2, 3]],
96        [{"field": {"subfield": "stringValue"}}, "field", None],
97        [{"field": {"subfield": "stringValue"}}, "field.nosuchfield", None],
98        [
99            {"field": {"subfield": {"subsubfield": "stringValue"}}},
100            "field.subfield.subsubfield",
101            "stringValue",
102        ],
103        ["string", "field", None],
104    ],
105)
106def test_get_field(request_obj, field, expected_result):
107    result = path_template.get_field(request_obj, field)
108    assert result == expected_result
109
110
111@pytest.mark.parametrize(
112    "request_obj, field, expected_result",
113    [
114        [{"field": "stringValue"}, "field", {}],
115        [{"field": "stringValue"}, "nosuchfield", {"field": "stringValue"}],
116        [{"field": "stringValue"}, "field.subfield", {"field": "stringValue"}],
117        [{"field": {"subfield": "stringValue"}}, "field.subfield", {"field": {}}],
118        [
119            {"field": {"subfield": "stringValue", "q": "w"}, "e": "f"},
120            "field.subfield",
121            {"field": {"q": "w"}, "e": "f"},
122        ],
123        [
124            {"field": {"subfield": "stringValue"}},
125            "field.nosuchfield",
126            {"field": {"subfield": "stringValue"}},
127        ],
128        [
129            {"field": {"subfield": {"subsubfield": "stringValue", "q": "w"}}},
130            "field.subfield.subsubfield",
131            {"field": {"subfield": {"q": "w"}}},
132        ],
133        ["string", "field", "string"],
134        ["string", "field.subfield", "string"],
135    ],
136)
137def test_delete_field(request_obj, field, expected_result):
138    path_template.delete_field(request_obj, field)
139    assert request_obj == expected_result
140
141
142@pytest.mark.parametrize(
143    "tmpl, path",
144    [
145        # Single segment template, but multi segment value
146        ["v1/*", "v1/a/b"],
147        ["v1/*/*", "v1/a/b/c"],
148        # Single segement named template, but multi segment value
149        ["v1/{name}", "v1/a/b"],
150        ["v1/{name}/{value}", "v1/a/b/c"],
151        # Named value with a sub-template but invalid value
152        ["v1/{name=parent/*}", "v1/grandparent/child"],
153    ],
154)
155def test_validate_failure(tmpl, path):
156    assert not path_template.validate(tmpl, path)
157
158
159def test__expand_variable_match_unexpected():
160    match = mock.Mock(spec=["group"])
161    match.group.return_value = None
162    with pytest.raises(ValueError, match="Unknown"):
163        path_template._expand_variable_match([], {}, match)
164
165
166def test__replace_variable_with_pattern():
167    match = mock.Mock(spec=["group"])
168    match.group.return_value = None
169    with pytest.raises(ValueError, match="Unknown"):
170        path_template._replace_variable_with_pattern(match)
171
172
173@pytest.mark.parametrize(
174    "http_options, request_kwargs, expected_result",
175    [
176        [
177            [["get", "/v1/no/template", ""]],
178            {"foo": "bar"},
179            ["get", "/v1/no/template", {}, {"foo": "bar"}],
180        ],
181        # Single templates
182        [
183            [["get", "/v1/{field}", ""]],
184            {"field": "parent"},
185            ["get", "/v1/parent", {}, {}],
186        ],
187        [
188            [["get", "/v1/{field.sub}", ""]],
189            {"field": {"sub": "parent"}, "foo": "bar"},
190            ["get", "/v1/parent", {}, {"field": {}, "foo": "bar"}],
191        ],
192    ],
193)
194def test_transcode_base_case(http_options, request_kwargs, expected_result):
195    http_options, expected_result = helper_test_transcode(http_options, expected_result)
196    result = path_template.transcode(http_options, **request_kwargs)
197    assert result == expected_result
198
199
200@pytest.mark.parametrize(
201    "http_options, request_kwargs, expected_result",
202    [
203        [
204            [["get", "/v1/{field.subfield}", ""]],
205            {"field": {"subfield": "parent"}, "foo": "bar"},
206            ["get", "/v1/parent", {}, {"field": {}, "foo": "bar"}],
207        ],
208        [
209            [["get", "/v1/{field.subfield.subsubfield}", ""]],
210            {"field": {"subfield": {"subsubfield": "parent"}}, "foo": "bar"},
211            ["get", "/v1/parent", {}, {"field": {"subfield": {}}, "foo": "bar"}],
212        ],
213        [
214            [["get", "/v1/{field.subfield1}/{field.subfield2}", ""]],
215            {"field": {"subfield1": "parent", "subfield2": "child"}, "foo": "bar"},
216            ["get", "/v1/parent/child", {}, {"field": {}, "foo": "bar"}],
217        ],
218    ],
219)
220def test_transcode_subfields(http_options, request_kwargs, expected_result):
221    http_options, expected_result = helper_test_transcode(http_options, expected_result)
222    result = path_template.transcode(http_options, **request_kwargs)
223    assert result == expected_result
224
225
226@pytest.mark.parametrize(
227    "http_options, request_kwargs, expected_result",
228    [
229        # Single segment wildcard
230        [
231            [["get", "/v1/{field=*}", ""]],
232            {"field": "parent"},
233            ["get", "/v1/parent", {}, {}],
234        ],
235        [
236            [["get", "/v1/{field=a/*/b/*}", ""]],
237            {"field": "a/parent/b/child", "foo": "bar"},
238            ["get", "/v1/a/parent/b/child", {}, {"foo": "bar"}],
239        ],
240        # Double segment wildcard
241        [
242            [["get", "/v1/{field=**}", ""]],
243            {"field": "parent/p1"},
244            ["get", "/v1/parent/p1", {}, {}],
245        ],
246        [
247            [["get", "/v1/{field=a/**/b/**}", ""]],
248            {"field": "a/parent/p1/b/child/c1", "foo": "bar"},
249            ["get", "/v1/a/parent/p1/b/child/c1", {}, {"foo": "bar"}],
250        ],
251        # Combined single and double segment wildcard
252        [
253            [["get", "/v1/{field=a/*/b/**}", ""]],
254            {"field": "a/parent/b/child/c1"},
255            ["get", "/v1/a/parent/b/child/c1", {}, {}],
256        ],
257        [
258            [["get", "/v1/{field=a/**/b/*}/v2/{name}", ""]],
259            {"field": "a/parent/p1/b/child", "name": "first", "foo": "bar"},
260            ["get", "/v1/a/parent/p1/b/child/v2/first", {}, {"foo": "bar"}],
261        ],
262    ],
263)
264def test_transcode_with_wildcard(http_options, request_kwargs, expected_result):
265    http_options, expected_result = helper_test_transcode(http_options, expected_result)
266    result = path_template.transcode(http_options, **request_kwargs)
267    assert result == expected_result
268
269
270@pytest.mark.parametrize(
271    "http_options, request_kwargs, expected_result",
272    [
273        # Single field body
274        [
275            [["post", "/v1/no/template", "data"]],
276            {"data": {"id": 1, "info": "some info"}, "foo": "bar"},
277            ["post", "/v1/no/template", {"id": 1, "info": "some info"}, {"foo": "bar"}],
278        ],
279        [
280            [["post", "/v1/{field=a/*}/b/{name=**}", "data"]],
281            {
282                "field": "a/parent",
283                "name": "first/last",
284                "data": {"id": 1, "info": "some info"},
285                "foo": "bar",
286            },
287            [
288                "post",
289                "/v1/a/parent/b/first/last",
290                {"id": 1, "info": "some info"},
291                {"foo": "bar"},
292            ],
293        ],
294        # Wildcard body
295        [
296            [["post", "/v1/{field=a/*}/b/{name=**}", "*"]],
297            {
298                "field": "a/parent",
299                "name": "first/last",
300                "data": {"id": 1, "info": "some info"},
301                "foo": "bar",
302            },
303            [
304                "post",
305                "/v1/a/parent/b/first/last",
306                {"data": {"id": 1, "info": "some info"}, "foo": "bar"},
307                {},
308            ],
309        ],
310    ],
311)
312def test_transcode_with_body(http_options, request_kwargs, expected_result):
313    http_options, expected_result = helper_test_transcode(http_options, expected_result)
314    result = path_template.transcode(http_options, **request_kwargs)
315    assert result == expected_result
316
317
318@pytest.mark.parametrize(
319    "http_options, request_kwargs, expected_result",
320    [
321        # Additional bindings
322        [
323            [
324                ["post", "/v1/{field=a/*}/b/{name=**}", "extra_data"],
325                ["post", "/v1/{field=a/*}/b/{name=**}", "*"],
326            ],
327            {
328                "field": "a/parent",
329                "name": "first/last",
330                "data": {"id": 1, "info": "some info"},
331                "foo": "bar",
332            },
333            [
334                "post",
335                "/v1/a/parent/b/first/last",
336                {"data": {"id": 1, "info": "some info"}, "foo": "bar"},
337                {},
338            ],
339        ],
340        [
341            [
342                ["get", "/v1/{field=a/*}/b/{name=**}", ""],
343                ["get", "/v1/{field=a/*}/b/first/last", ""],
344            ],
345            {"field": "a/parent", "foo": "bar"},
346            ["get", "/v1/a/parent/b/first/last", {}, {"foo": "bar"}],
347        ],
348    ],
349)
350def test_transcode_with_additional_bindings(
351    http_options, request_kwargs, expected_result
352):
353    http_options, expected_result = helper_test_transcode(http_options, expected_result)
354    result = path_template.transcode(http_options, **request_kwargs)
355    assert result == expected_result
356
357
358@pytest.mark.parametrize(
359    "http_options, request_kwargs",
360    [
361        [[["get", "/v1/{name}", ""]], {"foo": "bar"}],
362        [[["get", "/v1/{name}", ""]], {"name": "first/last"}],
363        [[["get", "/v1/{name=mr/*/*}", ""]], {"name": "first/last"}],
364        [[["post", "/v1/{name}", "data"]], {"name": "first/last"}],
365    ],
366)
367def test_transcode_fails(http_options, request_kwargs):
368    http_options, _ = helper_test_transcode(http_options, range(4))
369    with pytest.raises(ValueError):
370        path_template.transcode(http_options, **request_kwargs)
371
372
373def helper_test_transcode(http_options_list, expected_result_list):
374    http_options = []
375    for opt_list in http_options_list:
376        http_option = {"method": opt_list[0], "uri": opt_list[1]}
377        if opt_list[2]:
378            http_option["body"] = opt_list[2]
379        http_options.append(http_option)
380
381    expected_result = {
382        "method": expected_result_list[0],
383        "uri": expected_result_list[1],
384        "query_params": expected_result_list[3],
385    }
386    if expected_result_list[2]:
387        expected_result["body"] = expected_result_list[2]
388
389    return (http_options, expected_result)
390