| #!/usr/bin/env python |
| # |
| # Copyright 2007 Google Inc. |
| # |
| # 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. |
| # |
| """Unit tests for the api_config_manager module.""" |
| |
| |
| |
| import json |
| import re |
| import unittest |
| |
| from google.appengine.tools.devappserver2.endpoints import api_config_manager |
| |
| |
| class ApiConfigManagerTest(unittest.TestCase): |
| |
| def setUp(self): |
| """Make ApiConfigManager with a few helpful fakes.""" |
| self.config_manager = api_config_manager.ApiConfigManager() |
| |
| def test_parse_api_config_empty_response(self): |
| self.config_manager.parse_api_config_response('') |
| actual_method = self.config_manager.lookup_rpc_method('guestbook_api.get', |
| 'v1') |
| self.assertEqual(None, actual_method) |
| |
| def test_parse_api_config_invalid_response(self): |
| self.config_manager.parse_api_config_response('{"name": "foo"}') |
| actual_method = self.config_manager.lookup_rpc_method('guestbook_api.get', |
| 'v1') |
| self.assertEqual(None, actual_method) |
| |
| def test_parse_api_config(self): |
| fake_method = {'httpMethod': 'GET', |
| 'path': 'greetings/{gid}', |
| 'rosyMethod': 'baz.bim'} |
| config = json.dumps({'name': 'guestbook_api', |
| 'version': 'X', |
| 'methods': {'guestbook_api.foo.bar': fake_method}}) |
| self.config_manager.parse_api_config_response( |
| json.dumps({'items': [config]})) |
| actual_method = self.config_manager.lookup_rpc_method( |
| 'guestbook_api.foo.bar', 'X') |
| self.assertEqual(fake_method, actual_method) |
| |
| def test_parse_api_config_order_length(self): |
| test_method_info = ( |
| ('guestbook_api.foo.bar', 'greetings/{gid}', 'baz.bim'), |
| ('guestbook_api.list', 'greetings', 'greetings.list'), |
| ('guestbook_api.f3', 'greetings/{gid}/sender/property/blah', |
| 'greetings.f3'), |
| ('guestbook_api.shortgreet', 'greet', 'greetings.short_greeting')) |
| methods = {} |
| for method_name, path, rosy_method in test_method_info: |
| method = {'httpMethod': 'GET', |
| 'path': path, |
| 'rosyMethod': rosy_method} |
| methods[method_name] = method |
| config = json.dumps({'name': 'guestbook_api', |
| 'version': 'X', |
| 'methods': methods}) |
| self.config_manager.parse_api_config_response( |
| json.dumps({'items': [config]})) |
| # Make sure all methods appear in the result. |
| for method_name, _, _ in test_method_info: |
| self.assertIsNotNone( |
| self.config_manager.lookup_rpc_method(method_name, 'X')) |
| # Make sure paths and partial paths return the right methods. |
| self.assertEqual( |
| self.config_manager.lookup_rest_method( |
| 'guestbook_api/X/greetings', 'GET')[0], |
| 'guestbook_api.list') |
| self.assertEqual( |
| self.config_manager.lookup_rest_method( |
| 'guestbook_api/X/greetings/1', 'GET')[0], |
| 'guestbook_api.foo.bar') |
| self.assertEqual( |
| self.config_manager.lookup_rest_method( |
| 'guestbook_api/X/greetings/2/sender/property/blah', 'GET')[0], |
| 'guestbook_api.f3') |
| self.assertEqual( |
| self.config_manager.lookup_rest_method( |
| 'guestbook_api/X/greet', 'GET')[0], |
| 'guestbook_api.shortgreet') |
| |
| def test_get_sorted_methods1(self): |
| test_method_info = ( |
| ('name1', 'greetings', 'POST'), |
| ('name2', 'greetings', 'GET'), |
| ('name3', 'short/but/many/constants', 'GET'), |
| ('name4', 'greetings', ''), |
| ('name5', 'greetings/{gid}', 'GET'), |
| ('name6', 'greetings/{gid}', 'PUT'), |
| ('name7', 'a/b/{var}/{var2}', 'GET')) |
| methods = {} |
| for method_name, path, http_method in test_method_info: |
| method = {'httpMethod': http_method, |
| 'path': path} |
| methods[method_name] = method |
| sorted_methods = self.config_manager._get_sorted_methods(methods) |
| |
| expected_data = [ |
| ('name3', 'short/but/many/constants', 'GET'), |
| ('name7', 'a/b/{var}/{var2}', 'GET'), |
| ('name4', 'greetings', ''), |
| ('name2', 'greetings', 'GET'), |
| ('name1', 'greetings', 'POST'), |
| ('name5', 'greetings/{gid}', 'GET'), |
| ('name6', 'greetings/{gid}', 'PUT')] |
| expected_methods = [(name, {'httpMethod': http_method, 'path': path}) |
| for name, path, http_method in expected_data] |
| self.assertEqual(expected_methods, sorted_methods) |
| |
| def test_get_sorted_methods2(self): |
| test_method_info = ( |
| ('name1', 'abcdefghi', 'GET'), |
| ('name2', 'foo', 'GET'), |
| ('name3', 'greetings', 'GET'), |
| ('name4', 'bar', 'POST'), |
| ('name5', 'baz', 'GET'), |
| ('name6', 'baz', 'PUT'), |
| ('name7', 'baz', 'DELETE')) |
| methods = {} |
| for method_name, path, http_method in test_method_info: |
| method = {'httpMethod': http_method, |
| 'path': path} |
| methods[method_name] = method |
| sorted_methods = self.config_manager._get_sorted_methods(methods) |
| |
| # Single-part paths should be sorted by path name, http_method. |
| expected_data = [ |
| ('name1', 'abcdefghi', 'GET'), |
| ('name4', 'bar', 'POST'), |
| ('name7', 'baz', 'DELETE'), |
| ('name5', 'baz', 'GET'), |
| ('name6', 'baz', 'PUT'), |
| ('name2', 'foo', 'GET'), |
| ('name3', 'greetings', 'GET')] |
| expected_methods = [(name, {'httpMethod': http_method, 'path': path}) |
| for name, path, http_method in expected_data] |
| self.assertEqual(expected_methods, sorted_methods) |
| |
| def test_parse_api_config_invalid_api_config(self): |
| fake_method = {'httpMethod': 'GET', |
| 'path': 'greetings/{gid}', |
| 'rosyMethod': 'baz.bim'} |
| config = json.dumps({'name': 'guestbook_api', |
| 'version': 'X', |
| 'methods': {'guestbook_api.foo.bar': fake_method}}) |
| # Invalid Json |
| config2 = '{' |
| self.config_manager.parse_api_config_response( |
| json.dumps({'items': [config, config2]})) |
| actual_method = self.config_manager.lookup_rpc_method( |
| 'guestbook_api.foo.bar', 'X') |
| self.assertEqual(fake_method, actual_method) |
| |
| def test_parse_api_config_convert_https(self): |
| """Test that the parsed API config has switched HTTPS to HTTP.""" |
| config = json.dumps({'name': 'guestbook_api', |
| 'version': 'X', |
| 'adapter': {'bns': 'https://localhost/_ah/spi', |
| 'type': 'lily'}, |
| 'root': 'https://localhost/_ah/api', |
| 'methods': {}}) |
| self.config_manager.parse_api_config_response( |
| json.dumps({'items': [config]})) |
| |
| self.assertEqual( |
| 'http://localhost/_ah/spi', |
| self.config_manager.configs[('guestbook_api', 'X')]['adapter']['bns']) |
| self.assertEqual( |
| 'http://localhost/_ah/api', |
| self.config_manager.configs[('guestbook_api', 'X')]['root']) |
| |
| def test_convert_https_to_http(self): |
| """Test that the _convert_https_to_http function works.""" |
| config = {'name': 'guestbook_api', |
| 'version': 'X', |
| 'adapter': {'bns': 'https://tictactoe.appspot.com/_ah/spi', |
| 'type': 'lily'}, |
| 'root': 'https://tictactoe.appspot.com/_ah/api', |
| 'methods': {}} |
| self.config_manager._convert_https_to_http(config) |
| |
| self.assertEqual('http://tictactoe.appspot.com/_ah/spi', |
| config['adapter']['bns']) |
| self.assertEqual('http://tictactoe.appspot.com/_ah/api', config['root']) |
| |
| def test_dont_convert_non_https_to_http(self): |
| """Verify that we don't change non-HTTPS URLs.""" |
| config = {'name': 'guestbook_api', |
| 'version': 'X', |
| 'adapter': {'bns': 'http://https.appspot.com/_ah/spi', |
| 'type': 'lily'}, |
| 'root': 'ios://https.appspot.com/_ah/api', |
| 'methods': {}} |
| self.config_manager._convert_https_to_http(config) |
| |
| self.assertEqual('http://https.appspot.com/_ah/spi', |
| config['adapter']['bns']) |
| self.assertEqual('ios://https.appspot.com/_ah/api', config['root']) |
| |
| def test_save_lookup_rpc_method(self): |
| # First attempt, guestbook.get does not exist |
| actual_method = self.config_manager.lookup_rpc_method('guestbook_api.get', |
| 'v1') |
| self.assertEqual(None, actual_method) |
| |
| # Now we manually save it, and should find it |
| fake_method = {'some': 'object'} |
| self.config_manager._save_rpc_method('guestbook_api.get', 'v1', fake_method) |
| actual_method = self.config_manager.lookup_rpc_method('guestbook_api.get', |
| 'v1') |
| self.assertEqual(fake_method, actual_method) |
| |
| def test_save_lookup_rest_method(self): |
| # First attempt, guestbook.get does not exist |
| method_spec = self.config_manager.lookup_rest_method( |
| 'guestbook_api/v1/greetings/i', 'GET') |
| self.assertEqual((None, None, None), method_spec) |
| |
| # Now we manually save it, and should find it |
| fake_method = {'httpMethod': 'GET', |
| 'path': 'greetings/{id}'} |
| self.config_manager._save_rest_method('guestbook_api.get', 'guestbook_api', |
| 'v1', fake_method) |
| method_name, method_spec, params = self.config_manager.lookup_rest_method( |
| 'guestbook_api/v1/greetings/i', 'GET') |
| self.assertEqual('guestbook_api.get', method_name) |
| self.assertEqual(fake_method, method_spec) |
| self.assertEqual({'id': 'i'}, params) |
| |
| def test_trailing_slash_optional(self): |
| # Create a typical get resource URL. |
| fake_method = {'httpMethod': 'GET', 'path': 'trailingslash'} |
| self.config_manager._save_rest_method('guestbook_api.trailingslash', |
| 'guestbook_api', 'v1', fake_method) |
| |
| # Make sure we get this method when we query without a slash. |
| method_name, method_spec, params = self.config_manager.lookup_rest_method( |
| 'guestbook_api/v1/trailingslash', 'GET') |
| self.assertEqual('guestbook_api.trailingslash', method_name) |
| self.assertEqual(fake_method, method_spec) |
| self.assertEqual({}, params) |
| |
| # Make sure we get this method when we query with a slash. |
| method_name, method_spec, params = self.config_manager.lookup_rest_method( |
| 'guestbook_api/v1/trailingslash/', 'GET') |
| self.assertEqual('guestbook_api.trailingslash', method_name) |
| self.assertEqual(fake_method, method_spec) |
| self.assertEqual({}, params) |
| |
| |
| class ParameterizedPathTest(unittest.TestCase): |
| |
| |
| def test_invalid_variable_name_leading_digit(self): |
| self.assertEqual( |
| None, re.match(api_config_manager._PATH_VARIABLE_PATTERN, '1abc')) |
| |
| # Ensure users can not add variables starting with ! |
| # This is used for reserved variables (e.g. !name and !version) |
| def test_invalid_var_name_leading_exclamation(self): |
| self.assertEqual( |
| None, re.match(api_config_manager._PATH_VARIABLE_PATTERN, '!abc')) |
| |
| def test_valid_variable_name(self): |
| self.assertEqual( |
| 'AbC1', re.match(api_config_manager._PATH_VARIABLE_PATTERN, |
| 'AbC1').group(0)) |
| |
| def assert_no_match(self, path, param_path): |
| """Assert that the given path does not match param_path pattern. |
| |
| For example, /xyz/123 does not match /abc/{x}. |
| |
| Args: |
| path: A string, the inbound request path. |
| param_path: A string, the parameterized path pattern to match against |
| this path. |
| """ |
| config_manager = api_config_manager.ApiConfigManager |
| params = config_manager._compile_path_pattern(param_path).match(path) |
| self.assertEqual(None, params) |
| |
| def test_prefix_no_match(self): |
| self.assert_no_match('/xyz/123', '/abc/{x}') |
| |
| def test_suffix_no_match(self): |
| self.assert_no_match('/abc/123', '/abc/{x}/456') |
| |
| def test_suffix_no_match_with_more_variables(self): |
| self.assert_no_match('/abc/456/123/789', '/abc/{x}/123/{y}/xyz') |
| |
| def test_no_match_collection_with_item(self): |
| self.assert_no_match('/api/v1/resources/123', '/{name}/{version}/resources') |
| |
| def assert_match(self, path, param_path, param_count): |
| """Assert that the given path does match param_path pattern. |
| |
| For example, /abc/123 does not match /abc/{x}. |
| |
| Args: |
| path: A string, the inbound request path. |
| param_path: A string, the parameterized path pattern to match against |
| this path. |
| param_count: An int, the expected number of parameters to match in |
| pattern. |
| |
| Returns: |
| Dict mapping path variable name to path variable value. |
| """ |
| config_manager = api_config_manager.ApiConfigManager |
| match = config_manager._compile_path_pattern(param_path).match(path) |
| self.assertTrue(match is not None) # Will be None if path was not matched |
| params = config_manager._get_path_params(match) |
| self.assertEquals(param_count, len(params)) |
| return params |
| |
| def test_one_variable_match(self): |
| params = self.assert_match('/abc/123', '/abc/{x}', 1) |
| self.assertEquals('123', params.get('x')) |
| |
| def test_two_variable_match(self): |
| params = self.assert_match('/abc/456/123/789', '/abc/{x}/123/{y}', 2) |
| self.assertEquals('456', params.get('x')) |
| self.assertEquals('789', params.get('y')) |
| |
| def test_message_variable_match(self): |
| params = self.assert_match('/abc/123', '/abc/{x.y}', 1) |
| self.assertEquals('123', params.get('x.y')) |
| |
| def test_message_and_simple_variable_match(self): |
| params = self.assert_match('/abc/123/456', '/abc/{x.y.z}/{t}', 2) |
| self.assertEquals('123', params.get('x.y.z')) |
| self.assertEquals('456', params.get('t')) |
| |
| def assert_invalid_value(self, value): |
| """Assert that the path parameter value is not valid. |
| |
| For example, /abc/3!:2 is invalid for /abc/{x}. |
| |
| Args: |
| value: A string containing a variable value to check for validity. |
| """ |
| param_path = '/abc/{x}' |
| path = '/abc/%s' % value |
| config_manager = api_config_manager.ApiConfigManager |
| params = config_manager._compile_path_pattern(param_path).match(path) |
| self.assertEqual(None, params) |
| |
| def test_invalid_values(self): |
| for reserved in [':', '?', '#', '[', ']', '{', '}']: |
| self.assert_invalid_value('123%s' % reserved) |
| |
| if __name__ == '__main__': |
| unittest.main() |