Index: third_party/google-endpoints/endpoints/test/api_config_test.py |
diff --git a/third_party/google-endpoints/endpoints/test/api_config_test.py b/third_party/google-endpoints/endpoints/test/api_config_test.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..c1a28d8df7d42ab817f3f8f9c54baf026f1d3e7f |
--- /dev/null |
+++ b/third_party/google-endpoints/endpoints/test/api_config_test.py |
@@ -0,0 +1,2345 @@ |
+# Copyright 2016 Google Inc. All Rights Reserved. |
+# |
+# 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. |
+ |
+"""Tests for endpoints.api_config.""" |
+ |
+import itertools |
+import json |
+import logging |
+import unittest |
+ |
+import endpoints.api_config as api_config |
+from endpoints.api_config import ApiConfigGenerator |
+from endpoints.api_config import AUTH_LEVEL |
+import endpoints.api_exceptions as api_exceptions |
+import mock |
+from protorpc import message_types |
+from protorpc import messages |
+from protorpc import remote |
+ |
+import endpoints.resource_container as resource_container |
+ |
+import test_util |
+ |
+package = 'api_config_test' |
+_DESCRIPTOR_PATH_PREFIX = '' |
+ |
+ |
+class ModuleInterfaceTest(test_util.ModuleInterfaceTest, |
+ unittest.TestCase): |
+ |
+ MODULE = api_config |
+ |
+ |
+class Nested(messages.Message): |
+ """Message class to be used in a message field.""" |
+ int_value = messages.IntegerField(1) |
+ string_value = messages.StringField(2) |
+ |
+ |
+class SimpleEnum(messages.Enum): |
+ """Simple enumeration type.""" |
+ VAL1 = 1 |
+ VAL2 = 2 |
+ |
+ |
+class AllFields(messages.Message): |
+ """Contains all field types.""" |
+ |
+ bool_value = messages.BooleanField(1, variant=messages.Variant.BOOL) |
+ bytes_value = messages.BytesField(2, variant=messages.Variant.BYTES) |
+ double_value = messages.FloatField(3, variant=messages.Variant.DOUBLE) |
+ enum_value = messages.EnumField(SimpleEnum, 4) |
+ float_value = messages.FloatField(5, variant=messages.Variant.FLOAT) |
+ int32_value = messages.IntegerField(6, variant=messages.Variant.INT32) |
+ int64_value = messages.IntegerField(7, variant=messages.Variant.INT64) |
+ string_value = messages.StringField(8, variant=messages.Variant.STRING) |
+ uint32_value = messages.IntegerField(9, variant=messages.Variant.UINT32) |
+ uint64_value = messages.IntegerField(10, variant=messages.Variant.UINT64) |
+ sint32_value = messages.IntegerField(11, variant=messages.Variant.SINT32) |
+ sint64_value = messages.IntegerField(12, variant=messages.Variant.SINT64) |
+ message_field_value = messages.MessageField(Nested, 13) |
+ datetime_value = message_types.DateTimeField(14) |
+ |
+ |
+# This is used test "all fields" as query parameters instead of the body |
+# in a request. |
+ALL_FIELDS_AS_PARAMETERS = resource_container.ResourceContainer( |
+ **{field.name: field for field in AllFields.all_fields()}) |
+ |
+ |
+class ApiConfigTest(unittest.TestCase): |
+ |
+ def setUp(self): |
+ self.generator = ApiConfigGenerator() |
+ self.maxDiff = None |
+ |
+ def testAllVariantsCovered(self): |
+ variants_covered = set([field.variant for field in AllFields.all_fields()]) |
+ |
+ for variant in variants_covered: |
+ self.assertTrue(isinstance(variant, messages.Variant)) |
+ |
+ variants_covered_dict = {} |
+ for variant in variants_covered: |
+ number = variant.number |
+ if variants_covered_dict.get(variant.name, number) != number: |
+ self.fail('Somehow have two variants with same name and ' |
+ 'different number') |
+ variants_covered_dict[variant.name] = number |
+ |
+ test_util.AssertDictEqual( |
+ messages.Variant.to_dict(), variants_covered_dict, self) |
+ |
+ def testAllFieldTypes(self): |
+ |
+ class PutRequest(messages.Message): |
+ """Message with just a body field.""" |
+ body = messages.MessageField(AllFields, 1) |
+ |
+ class ItemsPutRequest(messages.Message): |
+ """Message with path params and a body field.""" |
+ body = messages.MessageField(AllFields, 1) |
+ entryId = messages.StringField(2, required=True) |
+ |
+ class ItemsPutRequestForContainer(messages.Message): |
+ """Message with path params and a body field.""" |
+ body = messages.MessageField(AllFields, 1) |
+ items_put_request_container = resource_container.ResourceContainer( |
+ ItemsPutRequestForContainer, |
+ entryId=messages.StringField(2, required=True)) |
+ |
+ class EntryPublishRequest(messages.Message): |
+ """Message with two required params, one in path, one in body.""" |
+ title = messages.StringField(1, required=True) |
+ entryId = messages.StringField(2, required=True) |
+ |
+ class EntryPublishRequestForContainer(messages.Message): |
+ """Message with two required params, one in path, one in body.""" |
+ title = messages.StringField(1, required=True) |
+ entry_publish_request_container = resource_container.ResourceContainer( |
+ EntryPublishRequestForContainer, |
+ entryId=messages.StringField(2, required=True)) |
+ |
+ @api_config.api(name='root', hostname='example.appspot.com', version='v1') |
+ class MyService(remote.Service): |
+ """Describes MyService.""" |
+ |
+ @api_config.method(AllFields, message_types.VoidMessage, path='entries', |
+ http_method='GET', name='entries.get') |
+ def entries_get(self, unused_request): |
+ """All field types in the query parameters.""" |
+ return message_types.VoidMessage() |
+ |
+ @api_config.method(ALL_FIELDS_AS_PARAMETERS, message_types.VoidMessage, |
+ path='entries/container', http_method='GET', |
+ name='entries.getContainer') |
+ def entries_get_container(self, unused_request): |
+ """All field types in the query parameters.""" |
+ return message_types.VoidMessage() |
+ |
+ @api_config.method(PutRequest, message_types.VoidMessage, path='entries', |
+ name='entries.put') |
+ def entries_put(self, unused_request): |
+ """Request body is in the body field.""" |
+ return message_types.VoidMessage() |
+ |
+ @api_config.method(AllFields, message_types.VoidMessage, path='process', |
+ name='entries.process') |
+ def entries_process(self, unused_request): |
+ """Message is the request body.""" |
+ return message_types.VoidMessage() |
+ |
+ @api_config.method(message_types.VoidMessage, message_types.VoidMessage, |
+ name='entries.nested.collection.action', |
+ path='nested') |
+ def entries_nested_collection_action(self, unused_request): |
+ """A VoidMessage for a request body.""" |
+ return message_types.VoidMessage() |
+ |
+ @api_config.method(AllFields, AllFields, name='entries.roundtrip', |
+ path='roundtrip') |
+ def entries_roundtrip(self, unused_request): |
+ """All field types in the request and response.""" |
+ pass |
+ |
+ # Test a method with a required parameter in the request body. |
+ @api_config.method(EntryPublishRequest, message_types.VoidMessage, |
+ path='entries/{entryId}/publish', |
+ name='entries.publish') |
+ def entries_publish(self, unused_request): |
+ """Path has a parameter and request body has a required param.""" |
+ return message_types.VoidMessage() |
+ |
+ @api_config.method(entry_publish_request_container, |
+ message_types.VoidMessage, |
+ path='entries/container/{entryId}/publish', |
+ name='entries.publishContainer') |
+ def entries_publish_container(self, unused_request): |
+ """Path has a parameter and request body has a required param.""" |
+ return message_types.VoidMessage() |
+ |
+ # Test a method with a parameter in the path and a request body. |
+ @api_config.method(ItemsPutRequest, message_types.VoidMessage, |
+ path='entries/{entryId}/items', |
+ name='entries.items.put') |
+ def items_put(self, unused_request): |
+ """Path has a parameter and request body is in the body field.""" |
+ return message_types.VoidMessage() |
+ |
+ @api_config.method(items_put_request_container, message_types.VoidMessage, |
+ path='entries/container/{entryId}/items', |
+ name='entries.items.putContainer') |
+ def items_put_container(self, unused_request): |
+ """Path has a parameter and request body is in the body field.""" |
+ return message_types.VoidMessage() |
+ |
+ api = json.loads(self.generator.pretty_print_config_to_json(MyService)) |
+ |
+ expected = { |
+ 'root.entries.get': { |
+ 'description': 'All field types in the query parameters.', |
+ 'httpMethod': 'GET', |
+ 'path': 'entries', |
+ 'request': { |
+ 'body': 'empty', |
+ 'parameters': { |
+ 'bool_value': { |
+ 'type': 'boolean', |
+ }, |
+ 'bytes_value': { |
+ 'type': 'bytes', |
+ }, |
+ 'double_value': { |
+ 'type': 'double', |
+ }, |
+ 'enum_value': { |
+ 'type': 'string', |
+ 'enum': { |
+ 'VAL1': { |
+ 'backendValue': 'VAL1', |
+ }, |
+ 'VAL2': { |
+ 'backendValue': 'VAL2', |
+ }, |
+ }, |
+ }, |
+ 'float_value': { |
+ 'type': 'float', |
+ }, |
+ 'int32_value': { |
+ 'type': 'int32', |
+ }, |
+ 'int64_value': { |
+ 'type': 'int64', |
+ }, |
+ 'string_value': { |
+ 'type': 'string', |
+ }, |
+ 'uint32_value': { |
+ 'type': 'uint32', |
+ }, |
+ 'uint64_value': { |
+ 'type': 'uint64', |
+ }, |
+ 'sint32_value': { |
+ 'type': 'int32', |
+ }, |
+ 'sint64_value': { |
+ 'type': 'int64', |
+ }, |
+ 'message_field_value.int_value': { |
+ 'type': 'int64', |
+ }, |
+ 'message_field_value.string_value': { |
+ 'type': 'string', |
+ }, |
+ 'datetime_value.milliseconds': { |
+ 'type': 'int64', |
+ }, |
+ 'datetime_value.time_zone_offset': { |
+ 'type': 'int64', |
+ }, |
+ }, |
+ }, |
+ 'response': { |
+ 'body': 'empty', |
+ }, |
+ 'rosyMethod': 'MyService.entries_get', |
+ 'scopes': ['https://www.googleapis.com/auth/userinfo.email'], |
+ 'clientIds': [api_config.API_EXPLORER_CLIENT_ID], |
+ 'authLevel': 'NONE', |
+ }, |
+ 'root.entries.getContainer': { |
+ 'description': 'All field types in the query parameters.', |
+ 'httpMethod': 'GET', |
+ 'path': 'entries/container', |
+ 'request': { |
+ 'body': 'empty', |
+ 'parameters': { |
+ 'bool_value': { |
+ 'type': 'boolean' |
+ }, |
+ 'bytes_value': { |
+ 'type': 'bytes' |
+ }, |
+ 'datetime_value.milliseconds': { |
+ 'type': 'int64' |
+ }, |
+ 'datetime_value.time_zone_offset': { |
+ 'type': 'int64' |
+ }, |
+ 'double_value': { |
+ 'type': 'double' |
+ }, |
+ 'enum_value': { |
+ 'enum': { |
+ 'VAL1': {'backendValue': 'VAL1'}, |
+ 'VAL2': {'backendValue': 'VAL2'}, |
+ }, |
+ 'type': 'string', |
+ }, |
+ 'float_value': { |
+ 'type': 'float' |
+ }, |
+ 'int32_value': { |
+ 'type': 'int32' |
+ }, |
+ 'int64_value': { |
+ 'type': 'int64' |
+ }, |
+ 'message_field_value.int_value': { |
+ 'type': 'int64' |
+ }, |
+ 'message_field_value.string_value': { |
+ 'type': 'string' |
+ }, |
+ 'sint32_value': { |
+ 'type': 'int32' |
+ }, |
+ 'sint64_value': { |
+ 'type': 'int64' |
+ }, |
+ 'string_value': { |
+ 'type': 'string' |
+ }, |
+ 'uint32_value': { |
+ 'type': 'uint32' |
+ }, |
+ 'uint64_value': { |
+ 'type': 'uint64' |
+ } |
+ } |
+ }, |
+ 'response': { |
+ 'body': 'empty' |
+ }, |
+ 'rosyMethod': 'MyService.entries_get_container', |
+ 'scopes': ['https://www.googleapis.com/auth/userinfo.email'], |
+ 'clientIds': [api_config.API_EXPLORER_CLIENT_ID], |
+ 'authLevel': 'NONE', |
+ }, |
+ 'root.entries.publishContainer': { |
+ 'description': ('Path has a parameter and request body has a ' |
+ 'required param.'), |
+ 'httpMethod': 'POST', |
+ 'path': 'entries/container/{entryId}/publish', |
+ 'request': { |
+ 'body': 'autoTemplate(backendRequest)', |
+ 'bodyName': 'resource', |
+ 'parameterOrder': ['entryId'], |
+ 'parameters': { |
+ 'entryId': { |
+ 'required': True, |
+ 'type': 'string', |
+ } |
+ } |
+ }, |
+ 'response': { |
+ 'body': 'empty' |
+ }, |
+ 'rosyMethod': 'MyService.entries_publish_container', |
+ 'scopes': ['https://www.googleapis.com/auth/userinfo.email'], |
+ 'clientIds': [api_config.API_EXPLORER_CLIENT_ID], |
+ 'authLevel': 'NONE', |
+ }, |
+ 'root.entries.put': { |
+ 'description': 'Request body is in the body field.', |
+ 'httpMethod': 'POST', |
+ 'path': 'entries', |
+ 'request': { |
+ 'body': 'autoTemplate(backendRequest)', |
+ 'bodyName': 'resource' |
+ }, |
+ 'response': { |
+ 'body': 'empty' |
+ }, |
+ 'rosyMethod': 'MyService.entries_put', |
+ 'scopes': ['https://www.googleapis.com/auth/userinfo.email'], |
+ 'clientIds': [api_config.API_EXPLORER_CLIENT_ID], |
+ 'authLevel': 'NONE', |
+ }, |
+ 'root.entries.process': { |
+ 'description': 'Message is the request body.', |
+ 'httpMethod': 'POST', |
+ 'path': 'process', |
+ 'request': { |
+ 'body': 'autoTemplate(backendRequest)', |
+ 'bodyName': 'resource' |
+ }, |
+ 'response': { |
+ 'body': 'empty' |
+ }, |
+ 'rosyMethod': 'MyService.entries_process', |
+ 'scopes': ['https://www.googleapis.com/auth/userinfo.email'], |
+ 'clientIds': [api_config.API_EXPLORER_CLIENT_ID], |
+ 'authLevel': 'NONE', |
+ }, |
+ 'root.entries.nested.collection.action': { |
+ 'description': 'A VoidMessage for a request body.', |
+ 'httpMethod': 'POST', |
+ 'path': 'nested', |
+ 'request': { |
+ 'body': 'empty' |
+ }, |
+ 'response': { |
+ 'body': 'empty' |
+ }, |
+ 'rosyMethod': 'MyService.entries_nested_collection_action', |
+ 'scopes': ['https://www.googleapis.com/auth/userinfo.email'], |
+ 'clientIds': [api_config.API_EXPLORER_CLIENT_ID], |
+ 'authLevel': 'NONE', |
+ }, |
+ 'root.entries.roundtrip': { |
+ 'description': 'All field types in the request and response.', |
+ 'httpMethod': 'POST', |
+ 'path': 'roundtrip', |
+ 'request': { |
+ 'body': 'autoTemplate(backendRequest)', |
+ 'bodyName': 'resource' |
+ }, |
+ 'response': { |
+ 'body': 'autoTemplate(backendResponse)', |
+ 'bodyName': 'resource' |
+ }, |
+ 'rosyMethod': 'MyService.entries_roundtrip', |
+ 'scopes': ['https://www.googleapis.com/auth/userinfo.email'], |
+ 'clientIds': [api_config.API_EXPLORER_CLIENT_ID], |
+ 'authLevel': 'NONE', |
+ }, |
+ 'root.entries.publish': { |
+ 'description': |
+ 'Path has a parameter and request body has a required param.', |
+ 'httpMethod': 'POST', |
+ 'path': 'entries/{entryId}/publish', |
+ 'request': { |
+ 'body': 'autoTemplate(backendRequest)', |
+ 'bodyName': 'resource', |
+ 'parameterOrder': [ |
+ 'entryId' |
+ ], |
+ 'parameters': { |
+ 'entryId': { |
+ 'type': 'string', |
+ 'required': True, |
+ }, |
+ }, |
+ }, |
+ 'response': { |
+ 'body': 'empty' |
+ }, |
+ 'rosyMethod': 'MyService.entries_publish', |
+ 'scopes': ['https://www.googleapis.com/auth/userinfo.email'], |
+ 'clientIds': [api_config.API_EXPLORER_CLIENT_ID], |
+ 'authLevel': 'NONE', |
+ }, |
+ 'root.entries.items.put': { |
+ 'description': |
+ 'Path has a parameter and request body is in the body field.', |
+ 'httpMethod': 'POST', |
+ 'path': 'entries/{entryId}/items', |
+ 'request': { |
+ 'body': 'autoTemplate(backendRequest)', |
+ 'bodyName': 'resource', |
+ 'parameterOrder': [ |
+ 'entryId' |
+ ], |
+ 'parameters': { |
+ 'entryId': { |
+ 'type': 'string', |
+ 'required': True, |
+ }, |
+ }, |
+ }, |
+ 'response': { |
+ 'body': 'empty' |
+ }, |
+ 'rosyMethod': 'MyService.items_put', |
+ 'scopes': ['https://www.googleapis.com/auth/userinfo.email'], |
+ 'clientIds': [api_config.API_EXPLORER_CLIENT_ID], |
+ 'authLevel': 'NONE', |
+ }, |
+ 'root.entries.items.putContainer': { |
+ 'description': ('Path has a parameter and request body is in ' |
+ 'the body field.'), |
+ 'httpMethod': 'POST', |
+ 'path': 'entries/container/{entryId}/items', |
+ 'request': { |
+ 'body': 'autoTemplate(backendRequest)', |
+ 'bodyName': 'resource', |
+ 'parameterOrder': [ |
+ 'entryId' |
+ ], |
+ 'parameters': { |
+ 'entryId': { |
+ 'type': 'string', |
+ 'required': True, |
+ }, |
+ }, |
+ }, |
+ 'response': { |
+ 'body': 'empty' |
+ }, |
+ 'rosyMethod': 'MyService.items_put_container', |
+ 'scopes': ['https://www.googleapis.com/auth/userinfo.email'], |
+ 'clientIds': [api_config.API_EXPLORER_CLIENT_ID], |
+ 'authLevel': 'NONE', |
+ } |
+ } |
+ expected_descriptor = { |
+ 'methods': { |
+ 'MyService.entries_get': {}, |
+ 'MyService.entries_get_container': {}, |
+ 'MyService.entries_nested_collection_action': {}, |
+ 'MyService.entries_process': { |
+ 'request': { |
+ '$ref': (_DESCRIPTOR_PATH_PREFIX + |
+ 'ApiConfigTestAllFields') |
+ } |
+ }, |
+ 'MyService.entries_publish': { |
+ 'request': { |
+ '$ref': (_DESCRIPTOR_PATH_PREFIX + |
+ 'ApiConfigTestEntryPublishRequest') |
+ } |
+ }, |
+ 'MyService.entries_publish_container': { |
+ 'request': { |
+ '$ref': (_DESCRIPTOR_PATH_PREFIX + |
+ 'ApiConfigTestEntryPublishRequestForContainer') |
+ } |
+ }, |
+ 'MyService.entries_put': { |
+ 'request': { |
+ '$ref': (_DESCRIPTOR_PATH_PREFIX + |
+ 'ApiConfigTestPutRequest') |
+ } |
+ }, |
+ 'MyService.entries_roundtrip': { |
+ 'request': { |
+ '$ref': (_DESCRIPTOR_PATH_PREFIX + |
+ 'ApiConfigTestAllFields') |
+ }, |
+ 'response': { |
+ '$ref': (_DESCRIPTOR_PATH_PREFIX + |
+ 'ApiConfigTestAllFields') |
+ } |
+ }, |
+ 'MyService.items_put': { |
+ 'request': { |
+ '$ref': (_DESCRIPTOR_PATH_PREFIX + |
+ 'ApiConfigTestItemsPutRequest') |
+ } |
+ }, |
+ 'MyService.items_put_container': { |
+ 'request': { |
+ '$ref': (_DESCRIPTOR_PATH_PREFIX + |
+ 'ApiConfigTestItemsPutRequestForContainer') |
+ } |
+ } |
+ }, |
+ 'schemas': { |
+ _DESCRIPTOR_PATH_PREFIX + 'ApiConfigTestAllFields': { |
+ 'description': 'Contains all field types.', |
+ 'id': _DESCRIPTOR_PATH_PREFIX + 'ApiConfigTestAllFields', |
+ 'properties': { |
+ 'bool_value': { |
+ 'type': 'boolean' |
+ }, |
+ 'bytes_value': { |
+ 'type': 'string', |
+ 'format': 'byte' |
+ }, |
+ 'double_value': { |
+ 'format': 'double', |
+ 'type': 'number' |
+ }, |
+ 'enum_value': { |
+ 'type': 'string', |
+ 'enum': ['VAL1', 'VAL2'] |
+ }, |
+ 'float_value': { |
+ 'format': 'float', |
+ 'type': 'number' |
+ }, |
+ 'int32_value': { |
+ 'format': 'int32', |
+ 'type': 'integer' |
+ }, |
+ 'int64_value': { |
+ 'format': 'int64', |
+ 'type': 'string' |
+ }, |
+ 'string_value': { |
+ 'type': 'string' |
+ }, |
+ 'uint32_value': { |
+ 'format': 'uint32', |
+ 'type': 'integer' |
+ }, |
+ 'uint64_value': { |
+ 'format': 'uint64', |
+ 'type': 'string' |
+ }, |
+ 'sint32_value': { |
+ 'format': 'int32', |
+ 'type': 'integer' |
+ }, |
+ 'sint64_value': { |
+ 'format': 'int64', |
+ 'type': 'string' |
+ }, |
+ 'message_field_value': { |
+ '$ref': (_DESCRIPTOR_PATH_PREFIX + |
+ 'ApiConfigTestNested'), |
+ 'description': ('Message class to be used in a ' |
+ 'message field.'), |
+ }, |
+ 'datetime_value': { |
+ 'format': 'date-time', |
+ 'type': 'string' |
+ }, |
+ }, |
+ 'type': 'object' |
+ }, |
+ _DESCRIPTOR_PATH_PREFIX + 'ApiConfigTestEntryPublishRequest': { |
+ 'description': ('Message with two required params, ' |
+ 'one in path, one in body.'), |
+ 'id': (_DESCRIPTOR_PATH_PREFIX + |
+ 'ApiConfigTestEntryPublishRequest'), |
+ 'properties': { |
+ 'entryId': { |
+ 'required': True, |
+ 'type': 'string' |
+ }, |
+ 'title': { |
+ 'required': True, |
+ 'type': 'string' |
+ } |
+ }, |
+ 'type': 'object' |
+ }, |
+ (_DESCRIPTOR_PATH_PREFIX + |
+ 'ApiConfigTestEntryPublishRequestForContainer'): { |
+ 'description': ('Message with two required params, ' |
+ 'one in path, one in body.'), |
+ 'id': (_DESCRIPTOR_PATH_PREFIX + |
+ 'ApiConfigTestEntryPublishRequestForContainer'), |
+ 'properties': { |
+ 'title': { |
+ 'required': True, |
+ 'type': 'string' |
+ } |
+ }, |
+ 'type': 'object' |
+ }, |
+ _DESCRIPTOR_PATH_PREFIX + 'ApiConfigTestItemsPutRequest': { |
+ 'description': 'Message with path params and a body field.', |
+ 'id': (_DESCRIPTOR_PATH_PREFIX + |
+ 'ApiConfigTestItemsPutRequest'), |
+ 'properties': { |
+ 'body': { |
+ '$ref': (_DESCRIPTOR_PATH_PREFIX + |
+ 'ApiConfigTestAllFields'), |
+ 'description': 'Contains all field types.' |
+ }, |
+ 'entryId': { |
+ 'required': True, |
+ 'type': 'string' |
+ } |
+ }, |
+ 'type': 'object' |
+ }, |
+ (_DESCRIPTOR_PATH_PREFIX + |
+ 'ApiConfigTestItemsPutRequestForContainer'): { |
+ 'description': 'Message with path params and a body field.', |
+ 'id': (_DESCRIPTOR_PATH_PREFIX + |
+ 'ApiConfigTestItemsPutRequestForContainer'), |
+ 'properties': { |
+ 'body': { |
+ '$ref': (_DESCRIPTOR_PATH_PREFIX + |
+ 'ApiConfigTestAllFields'), |
+ 'description': 'Contains all field types.' |
+ }, |
+ }, |
+ 'type': 'object' |
+ }, |
+ _DESCRIPTOR_PATH_PREFIX + 'ApiConfigTestNested': { |
+ 'description': 'Message class to be used in a message field.', |
+ 'id': _DESCRIPTOR_PATH_PREFIX + 'ApiConfigTestNested', |
+ 'properties': { |
+ 'int_value': { |
+ 'format': 'int64', |
+ 'type': 'string' |
+ }, |
+ 'string_value': { |
+ 'type': 'string' |
+ } |
+ }, |
+ 'type': 'object' |
+ }, |
+ _DESCRIPTOR_PATH_PREFIX + 'ApiConfigTestPutRequest': { |
+ 'description': 'Message with just a body field.', |
+ 'id': _DESCRIPTOR_PATH_PREFIX + 'ApiConfigTestPutRequest', |
+ 'properties': { |
+ 'body': { |
+ '$ref': (_DESCRIPTOR_PATH_PREFIX + |
+ 'ApiConfigTestAllFields'), |
+ 'description': 'Contains all field types.' |
+ } |
+ }, |
+ 'type': 'object' |
+ }, |
+ 'ProtorpcMessageTypesVoidMessage': { |
+ 'description': 'Empty message.', |
+ 'id': 'ProtorpcMessageTypesVoidMessage', |
+ 'properties': {}, |
+ 'type': 'object' |
+ } |
+ } |
+ } |
+ expected_adapter = { |
+ 'bns': 'https://example.appspot.com/_ah/api', |
+ 'type': 'lily', |
+ 'deadline': 10.0} |
+ |
+ test_util.AssertDictEqual(expected, api['methods'], self) |
+ test_util.AssertDictEqual(expected_descriptor, api['descriptor'], self) |
+ test_util.AssertDictEqual(expected_adapter, api['adapter'], self) |
+ |
+ self.assertEqual('Describes MyService.', api['description']) |
+ |
+ methods = api['descriptor']['methods'] |
+ self.assertTrue('MyService.entries_get' in methods) |
+ self.assertTrue('MyService.entries_put' in methods) |
+ self.assertTrue('MyService.entries_process' in methods) |
+ self.assertTrue('MyService.entries_nested_collection_action' in methods) |
+ |
+ def testEmptyRequestNonEmptyResponse(self): |
+ class MyResponse(messages.Message): |
+ bool_value = messages.BooleanField(1) |
+ int32_value = messages.IntegerField(2) |
+ |
+ @api_config.api(name='root', version='v1', hostname='example.appspot.com') |
+ class MySimpleService(remote.Service): |
+ |
+ @api_config.method(message_types.VoidMessage, MyResponse, |
+ name='entries.get') |
+ def entries_get(self, request): |
+ pass |
+ |
+ api = json.loads(self.generator.pretty_print_config_to_json( |
+ MySimpleService)) |
+ |
+ expected_request = { |
+ 'body': 'empty' |
+ } |
+ expected_response = { |
+ 'body': 'autoTemplate(backendResponse)', |
+ 'bodyName': 'resource' |
+ } |
+ |
+ test_util.AssertDictEqual( |
+ expected_response, api['methods']['root.entries.get']['response'], self) |
+ |
+ test_util.AssertDictEqual( |
+ expected_request, api['methods']['root.entries.get']['request'], self) |
+ |
+ def testEmptyService(self): |
+ |
+ @api_config.api('root', 'v1', hostname='example.appspot.com') |
+ class EmptyService(remote.Service): |
+ pass |
+ |
+ api = json.loads(self.generator.pretty_print_config_to_json(EmptyService)) |
+ |
+ self.assertTrue('methods' not in api) |
+ |
+ def testOptionalProperties(self): |
+ """Verify that optional config properties show up if they're supposed to.""" |
+ optional_props = ( |
+ ('canonical_name', 'canonicalName', 'Test Canonical Name'), |
+ ('owner_domain', 'ownerDomain', 'google.com'), |
+ ('owner_name', 'ownerName', 'Google'), |
+ ('package_path', 'packagePath', 'cloud/platform'), |
+ ('title', 'title', 'My Root API'), |
+ ('documentation', 'documentation', 'http://link.to/docs')) |
+ |
+ # Try all combinations of the above properties. |
+ for length in range(1, len(optional_props) + 1): |
+ for combination in itertools.combinations(optional_props, length): |
+ kwargs = {} |
+ for property_name, _, value in combination: |
+ kwargs[property_name] = value |
+ |
+ @api_config.api('root', 'v1', **kwargs) |
+ class MyService(remote.Service): |
+ pass |
+ |
+ api = json.loads(self.generator.pretty_print_config_to_json(MyService)) |
+ |
+ for _, config_name, value in combination: |
+ self.assertEqual(api[config_name], value) |
+ |
+ # If the value is not set, verify that it's not there. |
+ for property_name, config_name, value in optional_props: |
+ |
+ @api_config.api('root2', 'v2') |
+ class EmptyService2(remote.Service): |
+ pass |
+ |
+ api = json.loads(self.generator.pretty_print_config_to_json( |
+ EmptyService2)) |
+ self.assertNotIn(config_name, api) |
+ |
+ def testAuth(self): |
+ """Verify that auth shows up in the config if it's supposed to.""" |
+ |
+ empty_auth = api_config.ApiAuth() |
+ used_auth = api_config.ApiAuth(allow_cookie_auth=False) |
+ cookie_auth = api_config.ApiAuth(allow_cookie_auth=True) |
+ empty_blocked_regions = api_config.ApiAuth(blocked_regions=[]) |
+ one_blocked = api_config.ApiAuth(blocked_regions=['us']) |
+ many_blocked = api_config.ApiAuth(blocked_regions=['CU', 'IR', 'KP', 'SD', |
+ 'SY', 'MM']) |
+ mixed = api_config.ApiAuth(allow_cookie_auth=True, |
+ blocked_regions=['US', 'IR']) |
+ |
+ for auth, expected_result in ((None, None), |
+ (empty_auth, None), |
+ (used_auth, {'allowCookieAuth': False}), |
+ (cookie_auth, {'allowCookieAuth': True}), |
+ (empty_blocked_regions, None), |
+ (one_blocked, {'blockedRegions': ['us']}), |
+ (many_blocked, {'blockedRegions': |
+ ['CU', 'IR', 'KP', 'SD', |
+ 'SY', 'MM']}), |
+ (mixed, {'allowCookieAuth': True, |
+ 'blockedRegions': ['US', 'IR']})): |
+ |
+ @api_config.api('root', 'v1', auth=auth) |
+ class EmptyService(remote.Service): |
+ pass |
+ |
+ api = json.loads(self.generator.pretty_print_config_to_json(EmptyService)) |
+ if expected_result is None: |
+ self.assertNotIn('auth', api) |
+ else: |
+ self.assertEqual(api['auth'], expected_result) |
+ |
+ def testFrontEndLimits(self): |
+ """Verify that frontendLimits info in the API is written to the config.""" |
+ rules = [ |
+ api_config.ApiFrontEndLimitRule(match='foo', qps=234, user_qps=567, |
+ daily=8910, analytics_id='asdf'), |
+ api_config.ApiFrontEndLimitRule(match='bar', qps=0, user_qps=0, |
+ analytics_id='sdf1'), |
+ api_config.ApiFrontEndLimitRule()] |
+ frontend_limits = api_config.ApiFrontEndLimits(unregistered_user_qps=123, |
+ unregistered_qps=456, |
+ unregistered_daily=789, |
+ rules=rules) |
+ |
+ @api_config.api('root', 'v1', frontend_limits=frontend_limits) |
+ class EmptyService(remote.Service): |
+ pass |
+ |
+ api = json.loads(self.generator.pretty_print_config_to_json(EmptyService)) |
+ self.assertIn('frontendLimits', api) |
+ self.assertEqual(123, api['frontendLimits'].get('unregisteredUserQps')) |
+ self.assertEqual(456, api['frontendLimits'].get('unregisteredQps')) |
+ self.assertEqual(789, api['frontendLimits'].get('unregisteredDaily')) |
+ self.assertEqual(2, len(api['frontendLimits'].get('rules'))) |
+ self.assertEqual('foo', api['frontendLimits']['rules'][0]['match']) |
+ self.assertEqual(234, api['frontendLimits']['rules'][0]['qps']) |
+ self.assertEqual(567, api['frontendLimits']['rules'][0]['userQps']) |
+ self.assertEqual(8910, api['frontendLimits']['rules'][0]['daily']) |
+ self.assertEqual('asdf', api['frontendLimits']['rules'][0]['analyticsId']) |
+ self.assertEqual('bar', api['frontendLimits']['rules'][1]['match']) |
+ self.assertEqual(0, api['frontendLimits']['rules'][1]['qps']) |
+ self.assertEqual(0, api['frontendLimits']['rules'][1]['userQps']) |
+ self.assertNotIn('daily', api['frontendLimits']['rules'][1]) |
+ self.assertEqual('sdf1', api['frontendLimits']['rules'][1]['analyticsId']) |
+ |
+ def testAllCombinationsRepeatedRequiredDefault(self): |
+ |
+ # TODO(kdeus): When the backwards compatibility for non-ResourceContainer |
+ # parameters requests is removed, this class and the |
+ # accompanying method should be removed. |
+ class AllCombinations(messages.Message): |
+ """Documentation for AllCombinations.""" |
+ string = messages.StringField(1) |
+ string_required = messages.StringField(2, required=True) |
+ string_default_required = messages.StringField(3, required=True, |
+ default='Foo') |
+ string_repeated = messages.StringField(4, repeated=True) |
+ enum_value = messages.EnumField(SimpleEnum, 5, default=SimpleEnum.VAL2) |
+ |
+ all_combinations_container = resource_container.ResourceContainer( |
+ **{field.name: field for field in AllCombinations.all_fields()}) |
+ |
+ @api_config.api('root', 'v1', hostname='example.appspot.com') |
+ class MySimpleService(remote.Service): |
+ |
+ @api_config.method(AllCombinations, message_types.VoidMessage, |
+ path='foo', http_method='GET') |
+ def get(self, unused_request): |
+ return message_types.VoidMessage() |
+ |
+ @api_config.method(all_combinations_container, message_types.VoidMessage, |
+ name='getContainer', |
+ path='bar', http_method='GET') |
+ def get_container(self, unused_request): |
+ return message_types.VoidMessage() |
+ |
+ api = json.loads(self.generator.pretty_print_config_to_json( |
+ MySimpleService)) |
+ |
+ get_config = { |
+ 'httpMethod': 'GET', |
+ 'path': 'foo', |
+ 'request': { |
+ 'body': 'empty', |
+ 'parameterOrder': [ |
+ 'string_required', |
+ 'string_default_required', |
+ ], |
+ 'parameters': { |
+ 'enum_value': { |
+ 'default': 'VAL2', |
+ 'type': 'string', |
+ 'enum': { |
+ 'VAL1': { |
+ 'backendValue': 'VAL1', |
+ }, |
+ 'VAL2': { |
+ 'backendValue': 'VAL2', |
+ }, |
+ }, |
+ }, |
+ 'string': { |
+ 'type': 'string', |
+ }, |
+ 'string_default_required': { |
+ 'default': 'Foo', |
+ 'required': True, |
+ 'type': 'string', |
+ }, |
+ 'string_repeated': { |
+ 'type': 'string', |
+ 'repeated': True, |
+ }, |
+ 'string_required': { |
+ 'required': True, |
+ 'type': 'string', |
+ }, |
+ }, |
+ }, |
+ 'response': { |
+ 'body': 'empty', |
+ }, |
+ 'rosyMethod': 'MySimpleService.get', |
+ 'scopes': ['https://www.googleapis.com/auth/userinfo.email'], |
+ 'clientIds': [api_config.API_EXPLORER_CLIENT_ID], |
+ 'authLevel': 'NONE', |
+ } |
+ |
+ get_container_config = get_config.copy() |
+ get_container_config['path'] = 'bar' |
+ get_container_config['rosyMethod'] = 'MySimpleService.get_container' |
+ expected = { |
+ 'root.get': get_config, |
+ 'root.getContainer': get_container_config |
+ } |
+ |
+ test_util.AssertDictEqual(expected, api['methods'], self) |
+ |
+ def testMultipleClassesSingleApi(self): |
+ """Test an API that's split into multiple classes.""" |
+ |
+ root_api = api_config.api('root', 'v1', hostname='example.appspot.com') |
+ |
+ # First class has a request that reads some arguments. |
+ class Response1(messages.Message): |
+ string_value = messages.StringField(1) |
+ |
+ @root_api.api_class(resource_name='request') |
+ class RequestService(remote.Service): |
+ |
+ @api_config.method(message_types.VoidMessage, Response1, |
+ path='request_path', http_method='GET') |
+ def my_request(self, unused_request): |
+ pass |
+ |
+ # Second class, no methods. |
+ @root_api.api_class(resource_name='empty') |
+ class EmptyService(remote.Service): |
+ pass |
+ |
+ # Third class (& data), one method that returns a response. |
+ class Response2(messages.Message): |
+ bool_value = messages.BooleanField(1) |
+ int32_value = messages.IntegerField(2) |
+ |
+ @root_api.api_class(resource_name='simple') |
+ class MySimpleService(remote.Service): |
+ |
+ @api_config.method(message_types.VoidMessage, Response2, |
+ name='entries.get', path='entries') |
+ def EntriesGet(self, request): |
+ pass |
+ |
+ # Make sure api info is the same for all classes and all the _ApiInfo |
+ # properties are accessible. |
+ for cls in (RequestService, EmptyService, MySimpleService): |
+ self.assertEqual(cls.api_info.name, 'root') |
+ self.assertEqual(cls.api_info.version, 'v1') |
+ self.assertEqual(cls.api_info.hostname, 'example.appspot.com') |
+ self.assertIsNone(cls.api_info.audiences) |
+ self.assertEqual(cls.api_info.allowed_client_ids, |
+ [api_config.API_EXPLORER_CLIENT_ID]) |
+ self.assertEqual(cls.api_info.scopes, [api_config.EMAIL_SCOPE]) |
+ |
+ # Get the config for the combination of all 3. |
+ api = json.loads(self.generator.pretty_print_config_to_json( |
+ [RequestService, EmptyService, MySimpleService])) |
+ expected = { |
+ 'root.request.my_request': { |
+ 'httpMethod': 'GET', |
+ 'path': 'request_path', |
+ 'request': {'body': 'empty'}, |
+ 'response': { |
+ 'body': 'autoTemplate(backendResponse)', |
+ 'bodyName': 'resource'}, |
+ 'rosyMethod': 'RequestService.my_request', |
+ 'clientIds': ['292824132082.apps.googleusercontent.com'], |
+ 'scopes': ['https://www.googleapis.com/auth/userinfo.email'], |
+ 'authLevel': 'NONE', |
+ }, |
+ 'root.simple.entries.get': { |
+ 'httpMethod': 'POST', |
+ 'path': 'entries', |
+ 'request': {'body': 'empty'}, |
+ 'response': { |
+ 'body': 'autoTemplate(backendResponse)', |
+ 'bodyName': 'resource'}, |
+ 'rosyMethod': 'MySimpleService.EntriesGet', |
+ 'clientIds': ['292824132082.apps.googleusercontent.com'], |
+ 'scopes': ['https://www.googleapis.com/auth/userinfo.email'], |
+ 'authLevel': 'NONE', |
+ }, |
+ } |
+ test_util.AssertDictEqual(expected, api['methods'], self) |
+ expected_descriptor = { |
+ 'methods': { |
+ 'MySimpleService.EntriesGet': { |
+ 'response': { |
+ '$ref': (_DESCRIPTOR_PATH_PREFIX + |
+ 'ApiConfigTestResponse2') |
+ } |
+ }, |
+ 'RequestService.my_request': { |
+ 'response': { |
+ '$ref': (_DESCRIPTOR_PATH_PREFIX + |
+ 'ApiConfigTestResponse1') |
+ } |
+ } |
+ }, |
+ 'schemas': { |
+ _DESCRIPTOR_PATH_PREFIX + 'ApiConfigTestResponse1': { |
+ 'id': _DESCRIPTOR_PATH_PREFIX + 'ApiConfigTestResponse1', |
+ 'properties': { |
+ 'string_value': { |
+ 'type': 'string' |
+ } |
+ }, |
+ 'type': 'object' |
+ }, |
+ _DESCRIPTOR_PATH_PREFIX + 'ApiConfigTestResponse2': { |
+ 'id': _DESCRIPTOR_PATH_PREFIX + 'ApiConfigTestResponse2', |
+ 'properties': { |
+ 'bool_value': { |
+ 'type': 'boolean' |
+ }, |
+ 'int32_value': { |
+ 'format': 'int64', |
+ 'type': 'string' |
+ } |
+ }, |
+ 'type': 'object' |
+ } |
+ } |
+ } |
+ |
+ test_util.AssertDictEqual(expected_descriptor, api['descriptor'], self) |
+ |
+ def testMultipleClassesDifferentDecoratorInstance(self): |
+ """Test that using different instances of @api fails.""" |
+ |
+ root_api1 = api_config.api('root', 'v1', hostname='example.appspot.com') |
+ root_api2 = api_config.api('root', 'v1', hostname='example.appspot.com') |
+ |
+ @root_api1.api_class() |
+ class EmptyService1(remote.Service): |
+ pass |
+ |
+ @root_api2.api_class() |
+ class EmptyService2(remote.Service): |
+ pass |
+ |
+ self.assertRaises(api_exceptions.ApiConfigurationError, |
+ self.generator.pretty_print_config_to_json, |
+ [EmptyService1, EmptyService2]) |
+ |
+ def testMultipleClassesUsingSingleApiDecorator(self): |
+ """Test an API that's split into multiple classes using @api.""" |
+ |
+ @api_config.api('api', 'v1') |
+ class EmptyService1(remote.Service): |
+ pass |
+ |
+ @api_config.api('api', 'v1') |
+ class EmptyService2(remote.Service): |
+ pass |
+ |
+ self.assertRaises(api_exceptions.ApiConfigurationError, |
+ self.generator.pretty_print_config_to_json, |
+ [EmptyService1, EmptyService2]) |
+ |
+ def testMultipleClassesRepeatedResourceName(self): |
+ """Test a multiclass API that reuses a resource_name.""" |
+ |
+ root_api = api_config.api('root', 'v1', hostname='example.appspot.com') |
+ |
+ @root_api.api_class(resource_name='repeated') |
+ class Service1(remote.Service): |
+ |
+ @api_config.method(message_types.VoidMessage, message_types.VoidMessage, |
+ name='get', http_method='GET', path='get') |
+ def get(self, request): |
+ pass |
+ |
+ @root_api.api_class(resource_name='repeated') |
+ class Service2(remote.Service): |
+ |
+ @api_config.method(message_types.VoidMessage, message_types.VoidMessage, |
+ name='list', http_method='GET', path='list') |
+ def list(self, request): |
+ pass |
+ |
+ api = json.loads(self.generator.pretty_print_config_to_json( |
+ [Service1, Service2])) |
+ expected = { |
+ 'root.repeated.get': { |
+ 'httpMethod': 'GET', |
+ 'path': 'get', |
+ 'request': {'body': 'empty'}, |
+ 'response': {'body': 'empty'}, |
+ 'rosyMethod': 'Service1.get', |
+ 'clientIds': [api_config.API_EXPLORER_CLIENT_ID], |
+ 'scopes': ['https://www.googleapis.com/auth/userinfo.email'], |
+ 'authLevel': 'NONE', |
+ }, |
+ 'root.repeated.list': { |
+ 'httpMethod': 'GET', |
+ 'path': 'list', |
+ 'request': {'body': 'empty'}, |
+ 'response': {'body': 'empty'}, |
+ 'rosyMethod': 'Service2.list', |
+ 'clientIds': [api_config.API_EXPLORER_CLIENT_ID], |
+ 'scopes': ['https://www.googleapis.com/auth/userinfo.email'], |
+ 'authLevel': 'NONE', |
+ }, |
+ } |
+ test_util.AssertDictEqual(expected, api['methods'], self) |
+ |
+ def testMultipleClassesRepeatedMethodName(self): |
+ """Test a multiclass API that reuses a method name.""" |
+ |
+ root_api = api_config.api('root', 'v1', hostname='example.appspot.com') |
+ |
+ @root_api.api_class(resource_name='repeated') |
+ class Service1(remote.Service): |
+ |
+ @api_config.method(message_types.VoidMessage, message_types.VoidMessage, |
+ name='get', http_method='GET') |
+ def get(self, request): |
+ pass |
+ |
+ @root_api.api_class(resource_name='repeated') |
+ class Service2(remote.Service): |
+ |
+ @api_config.method(message_types.VoidMessage, message_types.VoidMessage, |
+ name='get', http_method='POST') |
+ def get(self, request): |
+ pass |
+ |
+ self.assertRaises(api_exceptions.ApiConfigurationError, |
+ self.generator.pretty_print_config_to_json, |
+ [Service1, Service2]) |
+ |
+ def testRepeatedRestPathAndHttpMethod(self): |
+ """If the same HTTP method & path are reused, that should raise an error.""" |
+ |
+ @api_config.api(name='root', version='v1', hostname='example.appspot.com') |
+ class MySimpleService(remote.Service): |
+ |
+ @api_config.method(message_types.VoidMessage, message_types.VoidMessage, |
+ path='path', http_method='GET') |
+ def Path1(self, unused_request): |
+ return message_types.VoidMessage() |
+ |
+ @api_config.method(message_types.VoidMessage, message_types.VoidMessage, |
+ path='path', http_method='GET') |
+ def Path2(self, unused_request): |
+ return message_types.VoidMessage() |
+ |
+ self.assertRaises(api_exceptions.ApiConfigurationError, |
+ self.generator.pretty_print_config_to_json, |
+ MySimpleService) |
+ |
+ def testMulticlassRepeatedRestPathAndHttpMethod(self): |
+ """If the same HTTP method & path are reused, that should raise an error.""" |
+ |
+ root_api = api_config.api('root', 'v1', hostname='example.appspot.com') |
+ |
+ @root_api.api_class(resource_name='resource1') |
+ class Service1(remote.Service): |
+ |
+ @api_config.method(message_types.VoidMessage, message_types.VoidMessage, |
+ path='path', http_method='GET') |
+ def Path1(self, unused_request): |
+ return message_types.VoidMessage() |
+ |
+ @root_api.api_class(resource_name='resource2') |
+ class Service2(remote.Service): |
+ |
+ @api_config.method(message_types.VoidMessage, message_types.VoidMessage, |
+ path='path', http_method='GET') |
+ def Path2(self, unused_request): |
+ return message_types.VoidMessage() |
+ |
+ self.assertRaises(api_exceptions.ApiConfigurationError, |
+ self.generator.pretty_print_config_to_json, |
+ [Service1, Service2]) |
+ |
+ def testRepeatedRpcMethodName(self): |
+ """Test an API that reuses the same RPC name for two methods.""" |
+ |
+ @api_config.api('root', 'v1', hostname='example.appspot.com') |
+ class MyService(remote.Service): |
+ |
+ @api_config.method(message_types.VoidMessage, message_types.VoidMessage, |
+ name='get', http_method='GET', path='path1') |
+ def get(self, request): |
+ pass |
+ |
+ @api_config.method(message_types.VoidMessage, message_types.VoidMessage, |
+ name='get', http_method='GET', path='path2') |
+ def another_get(self, request): |
+ pass |
+ |
+ self.assertRaises(api_exceptions.ApiConfigurationError, |
+ self.generator.pretty_print_config_to_json, [MyService]) |
+ |
+ def testMultipleClassesRepeatedMethodNameUniqueResource(self): |
+ """Test a multiclass API reusing a method name but different resource.""" |
+ |
+ root_api = api_config.api('root', 'v1', hostname='example.appspot.com') |
+ |
+ @root_api.api_class(resource_name='resource1') |
+ class Service1(remote.Service): |
+ |
+ @api_config.method(message_types.VoidMessage, message_types.VoidMessage, |
+ name='get', http_method='GET', path='get1') |
+ def get(self, request): |
+ pass |
+ |
+ @root_api.api_class(resource_name='resource2') |
+ class Service2(remote.Service): |
+ |
+ @api_config.method(message_types.VoidMessage, message_types.VoidMessage, |
+ name='get', http_method='GET', path='get2') |
+ def get(self, request): |
+ pass |
+ |
+ api = json.loads(self.generator.pretty_print_config_to_json( |
+ [Service1, Service2])) |
+ expected = { |
+ 'root.resource1.get': { |
+ 'httpMethod': 'GET', |
+ 'path': 'get1', |
+ 'request': {'body': 'empty'}, |
+ 'response': {'body': 'empty'}, |
+ 'rosyMethod': 'Service1.get', |
+ 'clientIds': [api_config.API_EXPLORER_CLIENT_ID], |
+ 'scopes': ['https://www.googleapis.com/auth/userinfo.email'], |
+ 'authLevel': 'NONE', |
+ }, |
+ 'root.resource2.get': { |
+ 'httpMethod': 'GET', |
+ 'path': 'get2', |
+ 'request': {'body': 'empty'}, |
+ 'response': {'body': 'empty'}, |
+ 'rosyMethod': 'Service2.get', |
+ 'clientIds': [api_config.API_EXPLORER_CLIENT_ID], |
+ 'scopes': ['https://www.googleapis.com/auth/userinfo.email'], |
+ 'authLevel': 'NONE', |
+ }, |
+ } |
+ test_util.AssertDictEqual(expected, api['methods'], self) |
+ |
+ def testMultipleClassesRepeatedMethodNameUniqueResourceParams(self): |
+ """Test the same method name with different args in different resources.""" |
+ |
+ root_api = api_config.api('root', 'v1', hostname='example.appspot.com') |
+ |
+ class Request1(messages.Message): |
+ bool_value = messages.BooleanField(1) |
+ |
+ class Response1(messages.Message): |
+ bool_value = messages.BooleanField(1) |
+ |
+ class Request2(messages.Message): |
+ bool_value = messages.BooleanField(1) |
+ |
+ class Response2(messages.Message): |
+ bool_value = messages.BooleanField(1) |
+ |
+ @root_api.api_class(resource_name='resource1') |
+ class Service1(remote.Service): |
+ |
+ @api_config.method(Request1, Response1, |
+ name='get', http_method='GET', path='get1') |
+ def get(self, request): |
+ pass |
+ |
+ @root_api.api_class(resource_name='resource2') |
+ class Service2(remote.Service): |
+ |
+ @api_config.method(Request2, Response2, |
+ name='get', http_method='GET', path='get2') |
+ def get(self, request): |
+ pass |
+ |
+ api = json.loads(self.generator.pretty_print_config_to_json( |
+ [Service1, Service2])) |
+ expected = { |
+ 'root.resource1.get': { |
+ 'httpMethod': 'GET', |
+ 'path': 'get1', |
+ 'request': { |
+ 'body': 'empty', |
+ 'parameters': { |
+ 'bool_value': { |
+ 'type': 'boolean' |
+ } |
+ } |
+ }, |
+ 'response': {'body': 'autoTemplate(backendResponse)', |
+ 'bodyName': 'resource'}, |
+ 'rosyMethod': 'Service1.get', |
+ 'clientIds': [api_config.API_EXPLORER_CLIENT_ID], |
+ 'scopes': ['https://www.googleapis.com/auth/userinfo.email'], |
+ 'authLevel': 'NONE', |
+ }, |
+ 'root.resource2.get': { |
+ 'httpMethod': 'GET', |
+ 'path': 'get2', |
+ 'request': { |
+ 'body': 'empty', |
+ 'parameters': { |
+ 'bool_value': { |
+ 'type': 'boolean' |
+ } |
+ } |
+ }, |
+ 'response': {'body': 'autoTemplate(backendResponse)', |
+ 'bodyName': 'resource'}, |
+ 'rosyMethod': 'Service2.get', |
+ 'clientIds': [api_config.API_EXPLORER_CLIENT_ID], |
+ 'scopes': ['https://www.googleapis.com/auth/userinfo.email'], |
+ 'authLevel': 'NONE', |
+ }, |
+ } |
+ test_util.AssertDictEqual(expected, api['methods'], self) |
+ |
+ expected_descriptor = { |
+ 'methods': { |
+ 'Service1.get': { |
+ 'response': { |
+ '$ref': (_DESCRIPTOR_PATH_PREFIX + |
+ 'ApiConfigTestResponse1') |
+ } |
+ }, |
+ 'Service2.get': { |
+ 'response': { |
+ '$ref': (_DESCRIPTOR_PATH_PREFIX + |
+ 'ApiConfigTestResponse2') |
+ } |
+ } |
+ }, |
+ 'schemas': { |
+ _DESCRIPTOR_PATH_PREFIX + 'ApiConfigTestResponse1': { |
+ 'id': _DESCRIPTOR_PATH_PREFIX + 'ApiConfigTestResponse1', |
+ 'properties': { |
+ 'bool_value': { |
+ 'type': 'boolean' |
+ } |
+ }, |
+ 'type': 'object' |
+ }, |
+ _DESCRIPTOR_PATH_PREFIX + 'ApiConfigTestResponse2': { |
+ 'id': _DESCRIPTOR_PATH_PREFIX + 'ApiConfigTestResponse2', |
+ 'properties': { |
+ 'bool_value': { |
+ 'type': 'boolean' |
+ } |
+ }, |
+ 'type': 'object' |
+ } |
+ } |
+ } |
+ |
+ test_util.AssertDictEqual(expected_descriptor, api['descriptor'], self) |
+ |
+ def testMultipleClassesNoResourceName(self): |
+ """Test a multiclass API with a collection with no resource_name.""" |
+ |
+ root_api = api_config.api('root', 'v1', hostname='example.appspot.com') |
+ |
+ @root_api.api_class() |
+ class TestService(remote.Service): |
+ |
+ @api_config.method(http_method='GET') |
+ def donothing(self): |
+ pass |
+ |
+ @api_config.method(http_method='POST', name='alternate') |
+ def foo(self): |
+ pass |
+ |
+ api = json.loads(self.generator.pretty_print_config_to_json( |
+ [TestService])) |
+ expected = { |
+ 'root.donothing': { |
+ 'httpMethod': 'GET', |
+ 'path': 'donothing', |
+ 'request': {'body': 'empty'}, |
+ 'response': {'body': 'empty'}, |
+ 'rosyMethod': 'TestService.donothing', |
+ 'clientIds': ['292824132082.apps.googleusercontent.com'], |
+ 'scopes': ['https://www.googleapis.com/auth/userinfo.email'], |
+ 'authLevel': 'NONE', |
+ }, |
+ 'root.alternate': { |
+ 'httpMethod': 'POST', |
+ 'path': 'foo', |
+ 'request': {'body': 'empty'}, |
+ 'response': {'body': 'empty'}, |
+ 'rosyMethod': 'TestService.foo', |
+ 'clientIds': ['292824132082.apps.googleusercontent.com'], |
+ 'scopes': ['https://www.googleapis.com/auth/userinfo.email'], |
+ 'authLevel': 'NONE', |
+ }, |
+ } |
+ test_util.AssertDictEqual(expected, api['methods'], self) |
+ |
+ def testMultipleClassesBasePathInteraction(self): |
+ """Test path appending in a multiclass API.""" |
+ |
+ root_api = api_config.api('root', 'v1', hostname='example.appspot.com') |
+ |
+ @root_api.api_class(path='base_path') |
+ class TestService(remote.Service): |
+ |
+ @api_config.method(http_method='GET') |
+ def at_base(self): |
+ pass |
+ |
+ @api_config.method(http_method='GET', path='appended') |
+ def append_to_base(self): |
+ pass |
+ |
+ @api_config.method(http_method='GET', path='appended/more') |
+ def append_to_base2(self): |
+ pass |
+ |
+ @api_config.method(http_method='GET', path='/ignore_base') |
+ def absolute(self): |
+ pass |
+ |
+ api = json.loads(self.generator.pretty_print_config_to_json( |
+ [TestService])) |
+ expected = { |
+ 'root.at_base': { |
+ 'httpMethod': 'GET', |
+ 'path': 'base_path/at_base', |
+ 'request': {'body': 'empty'}, |
+ 'response': {'body': 'empty'}, |
+ 'rosyMethod': 'TestService.at_base', |
+ 'clientIds': ['292824132082.apps.googleusercontent.com'], |
+ 'scopes': ['https://www.googleapis.com/auth/userinfo.email'], |
+ 'authLevel': 'NONE', |
+ }, |
+ 'root.append_to_base': { |
+ 'httpMethod': 'GET', |
+ 'path': 'base_path/appended', |
+ 'request': {'body': 'empty'}, |
+ 'response': {'body': 'empty'}, |
+ 'rosyMethod': 'TestService.append_to_base', |
+ 'clientIds': ['292824132082.apps.googleusercontent.com'], |
+ 'scopes': ['https://www.googleapis.com/auth/userinfo.email'], |
+ 'authLevel': 'NONE', |
+ }, |
+ 'root.append_to_base2': { |
+ 'httpMethod': 'GET', |
+ 'path': 'base_path/appended/more', |
+ 'request': {'body': 'empty'}, |
+ 'response': {'body': 'empty'}, |
+ 'rosyMethod': 'TestService.append_to_base2', |
+ 'clientIds': ['292824132082.apps.googleusercontent.com'], |
+ 'scopes': ['https://www.googleapis.com/auth/userinfo.email'], |
+ 'authLevel': 'NONE', |
+ }, |
+ 'root.absolute': { |
+ 'httpMethod': 'GET', |
+ 'path': 'ignore_base', |
+ 'request': {'body': 'empty'}, |
+ 'response': {'body': 'empty'}, |
+ 'rosyMethod': 'TestService.absolute', |
+ 'clientIds': ['292824132082.apps.googleusercontent.com'], |
+ 'scopes': ['https://www.googleapis.com/auth/userinfo.email'], |
+ 'authLevel': 'NONE', |
+ }, |
+ } |
+ test_util.AssertDictEqual(expected, api['methods'], self) |
+ |
+ def testMultipleClassesDifferentCollectionDefaults(self): |
+ """Test a multi-class API with settings overridden per collection.""" |
+ |
+ BASE_SCOPES = ['base_scope'] |
+ BASE_CLIENT_IDS = ['base_client_id'] |
+ root_api = api_config.api('root', 'v1', hostname='example.appspot.com', |
+ audiences=['base_audience'], |
+ scopes=BASE_SCOPES, |
+ allowed_client_ids=BASE_CLIENT_IDS, |
+ auth_level=AUTH_LEVEL.REQUIRED) |
+ |
+ @root_api.api_class(resource_name='one', audiences=[]) |
+ class Service1(remote.Service): |
+ pass |
+ |
+ @root_api.api_class(resource_name='two', audiences=['audience2', 'foo'], |
+ scopes=['service2_scope'], |
+ allowed_client_ids=['s2_client_id'], |
+ auth_level=AUTH_LEVEL.OPTIONAL) |
+ class Service2(remote.Service): |
+ pass |
+ |
+ self.assertEqual(Service1.api_info.audiences, []) |
+ self.assertEqual(Service1.api_info.scopes, BASE_SCOPES) |
+ self.assertEqual(Service1.api_info.allowed_client_ids, BASE_CLIENT_IDS) |
+ self.assertEqual(Service1.api_info.auth_level, AUTH_LEVEL.REQUIRED) |
+ self.assertEqual(Service2.api_info.audiences, ['audience2', 'foo']) |
+ self.assertEqual(Service2.api_info.scopes, ['service2_scope']) |
+ self.assertEqual(Service2.api_info.allowed_client_ids, ['s2_client_id']) |
+ self.assertEqual(Service2.api_info.auth_level, AUTH_LEVEL.OPTIONAL) |
+ |
+ def testResourceContainerWarning(self): |
+ """Check the warning if a ResourceContainer isn't used when it should be.""" |
+ |
+ class TestGetRequest(messages.Message): |
+ item_id = messages.StringField(1) |
+ |
+ @api_config.api('myapi', 'v0', hostname='example.appspot.com') |
+ class MyApi(remote.Service): |
+ |
+ @api_config.method(TestGetRequest, message_types.VoidMessage, |
+ path='test/{item_id}') |
+ def Test(self, unused_request): |
+ return message_types.VoidMessage() |
+ |
+ # Verify that there's a warning and the name of the method is included |
+ # in the warning. |
+ logging.warning = mock.Mock() |
+ self.generator.pretty_print_config_to_json(MyApi) |
+ logging.warning.assert_called_with(mock.ANY, 'myapi.test') |
+ |
+ def testFieldInPathWithBodyIsRequired(self): |
+ |
+ # TODO(kdeus): When the backwards compatibility for non-ResourceContainer |
+ # parameters requests is removed, this class and the |
+ # accompanying method should be removed. |
+ class ItemsUpdateRequest(messages.Message): |
+ itemId = messages.StringField(1) |
+ |
+ items_update_request_container = resource_container.ResourceContainer( |
+ **{field.name: field for field in ItemsUpdateRequest.all_fields()}) |
+ |
+ @api_config.api(name='root', hostname='example.appspot.com', version='v1') |
+ class MyService(remote.Service): |
+ """Describes MyService.""" |
+ |
+ @api_config.method(ItemsUpdateRequest, message_types.VoidMessage, |
+ path='items/{itemId}', name='items.update', |
+ http_method='PUT') |
+ def items_update(self, unused_request): |
+ return message_types.VoidMessage() |
+ |
+ @api_config.method(items_update_request_container, |
+ path='items/container/{itemId}', |
+ name='items.updateContainer', |
+ http_method='PUT') |
+ def items_update_container(self, unused_request): |
+ return message_types.VoidMessage() |
+ |
+ api = json.loads(self.generator.pretty_print_config_to_json(MyService)) |
+ params = {'itemId': {'required': True, |
+ 'type': 'string'}} |
+ param_order = ['itemId'] |
+ items_update_config = { |
+ 'httpMethod': 'PUT', |
+ 'path': 'items/{itemId}', |
+ 'request': {'body': 'autoTemplate(backendRequest)', |
+ 'bodyName': 'resource', |
+ 'parameters': params, |
+ 'parameterOrder': param_order}, |
+ 'response': {'body': 'empty'}, |
+ 'rosyMethod': 'MyService.items_update', |
+ 'scopes': ['https://www.googleapis.com/auth/userinfo.email'], |
+ 'clientIds': [api_config.API_EXPLORER_CLIENT_ID], |
+ 'authLevel': 'NONE', |
+ } |
+ |
+ update_container_cfg = items_update_config.copy() |
+ update_container_cfg['path'] = 'items/container/{itemId}' |
+ update_container_cfg['rosyMethod'] = 'MyService.items_update_container' |
+ # Since we don't have a body in our container, the request will be empty. |
+ request = update_container_cfg['request'].copy() |
+ request.pop('bodyName') |
+ request['body'] = 'empty' |
+ update_container_cfg['request'] = request |
+ expected = { |
+ 'root.items.update': items_update_config, |
+ 'root.items.updateContainer': update_container_cfg, |
+ } |
+ test_util.AssertDictEqual(expected, api['methods'], self) |
+ |
+ def testFieldInPathNoBodyIsRequired(self): |
+ |
+ class ItemsGetRequest(messages.Message): |
+ itemId = messages.StringField(1) |
+ |
+ @api_config.api(name='root', hostname='example.appspot.com', version='v1') |
+ class MyService(remote.Service): |
+ """Describes MyService.""" |
+ |
+ @api_config.method(ItemsGetRequest, message_types.VoidMessage, |
+ path='items/{itemId}', name='items.get', |
+ http_method='GET') |
+ def items_get(self, unused_request): |
+ return message_types.VoidMessage() |
+ |
+ api = json.loads(self.generator.pretty_print_config_to_json(MyService)) |
+ params = {'itemId': {'required': True, |
+ 'type': 'string'}} |
+ param_order = ['itemId'] |
+ expected = { |
+ 'root.items.get': { |
+ 'httpMethod': 'GET', |
+ 'path': 'items/{itemId}', |
+ 'request': {'body': 'empty', |
+ 'parameters': params, |
+ 'parameterOrder': param_order}, |
+ 'response': {'body': 'empty'}, |
+ 'rosyMethod': 'MyService.items_get', |
+ 'scopes': ['https://www.googleapis.com/auth/userinfo.email'], |
+ 'clientIds': [api_config.API_EXPLORER_CLIENT_ID], |
+ 'authLevel': 'NONE', |
+ } |
+ } |
+ |
+ test_util.AssertDictEqual(expected, api['methods'], self) |
+ |
+ def testAuthLevelRequired(self): |
+ |
+ class ItemsGetRequest(messages.Message): |
+ itemId = messages.StringField(1) |
+ |
+ @api_config.api(name='root', hostname='example.appspot.com', version='v1') |
+ class MyService(remote.Service): |
+ """Describes MyService.""" |
+ |
+ @api_config.method(ItemsGetRequest, message_types.VoidMessage, |
+ path='items/{itemId}', name='items.get', |
+ http_method='GET', auth_level=AUTH_LEVEL.REQUIRED) |
+ def items_get(self, unused_request): |
+ return message_types.VoidMessage() |
+ |
+ api = json.loads(self.generator.pretty_print_config_to_json(MyService)) |
+ params = {'itemId': {'required': True, |
+ 'type': 'string'}} |
+ param_order = ['itemId'] |
+ expected = { |
+ 'root.items.get': { |
+ 'httpMethod': 'GET', |
+ 'path': 'items/{itemId}', |
+ 'request': {'body': 'empty', |
+ 'parameters': params, |
+ 'parameterOrder': param_order}, |
+ 'response': {'body': 'empty'}, |
+ 'rosyMethod': 'MyService.items_get', |
+ 'scopes': ['https://www.googleapis.com/auth/userinfo.email'], |
+ 'clientIds': [api_config.API_EXPLORER_CLIENT_ID], |
+ 'authLevel': 'REQUIRED', |
+ } |
+ } |
+ |
+ test_util.AssertDictEqual(expected, api['methods'], self) |
+ |
+ def testCustomUrl(self): |
+ |
+ test_request = resource_container.ResourceContainer( |
+ message_types.VoidMessage, |
+ id=messages.IntegerField(1, required=True)) |
+ |
+ @api_config.api(name='testapicustomurl', version='v3', |
+ hostname='example.appspot.com', |
+ description='A wonderful API.', base_path='/my/base/path/') |
+ class TestServiceCustomUrl(remote.Service): |
+ |
+ @api_config.method(test_request, |
+ message_types.VoidMessage, |
+ http_method='DELETE', path='items/{id}') |
+ # Silence lint warning about method naming conventions |
+ # pylint: disable=g-bad-name |
+ def delete(self, unused_request): |
+ return message_types.VoidMessage() |
+ |
+ api = json.loads( |
+ self.generator.pretty_print_config_to_json(TestServiceCustomUrl)) |
+ |
+ expected_adapter = { |
+ 'bns': 'https://example.appspot.com/my/base/path', |
+ 'type': 'lily', |
+ 'deadline': 10.0 |
+ } |
+ |
+ test_util.AssertDictEqual(expected_adapter, api['adapter'], self) |
+ |
+ |
+class ApiConfigParamsDescriptorTest(unittest.TestCase): |
+ |
+ def setUp(self): |
+ self.generator = ApiConfigGenerator() |
+ |
+ class OtherRefClass(messages.Message): |
+ three = messages.BooleanField(1, repeated=True) |
+ four = messages.FloatField(2, required=True) |
+ five = messages.IntegerField(3, default=42) |
+ self.other_ref_class = OtherRefClass |
+ |
+ class RefClass(messages.Message): |
+ one = messages.StringField(1) |
+ two = messages.MessageField(OtherRefClass, 2) |
+ not_two = messages.MessageField(OtherRefClass, 3, required=True) |
+ self.ref_class = RefClass |
+ |
+ class RefClassForContainer(messages.Message): |
+ not_two = messages.MessageField(OtherRefClass, 3, required=True) |
+ |
+ ref_class_container = resource_container.ResourceContainer( |
+ RefClassForContainer, |
+ one=messages.StringField(1), |
+ two=messages.MessageField(OtherRefClass, 2)) |
+ |
+ @api_config.api(name='root', hostname='example.appspot.com', version='v1') |
+ class MyService(remote.Service): |
+ |
+ @api_config.method(RefClass, RefClass, |
+ name='entries.get', |
+ path='/a/{two.three}/{two.four}', |
+ http_method='GET') |
+ def entries_get(self, request): |
+ return request |
+ |
+ @api_config.method(RefClass, RefClass, |
+ name='entries.put', |
+ path='/b/{two.three}/{one}', |
+ http_method='PUT') |
+ def entries_put(self, request): |
+ return request |
+ |
+ # Flatten the fields intended for the put request into only parameters. |
+ # This would not be a typical use, but is done to adhere to the behavior |
+ # in the non-ResourceContainer case. |
+ get_request_container = resource_container.ResourceContainer( |
+ **{field.name: field for field in |
+ ref_class_container.combined_message_class.all_fields()}) |
+ |
+ @api_config.method(get_request_container, RefClass, |
+ name='entries.getContainer', |
+ path='/a/container/{two.three}/{two.four}', |
+ http_method='GET') |
+ def entries_get_container(self, request): |
+ return request |
+ |
+ @api_config.method(ref_class_container, RefClass, |
+ name='entries.putContainer', |
+ path='/b/container/{two.three}/{one}', |
+ http_method='PUT') |
+ def entries_put_container(self, request): |
+ return request |
+ |
+ self.api_str = self.generator.pretty_print_config_to_json(MyService) |
+ self.api = json.loads(self.api_str) |
+ |
+ self.m_field = messages.MessageField(RefClass, 1) |
+ self.m_field.name = 'm_field' |
+ |
+ def GetPrivateMethod(self, attr_name): |
+ protected_attr_name = '_ApiConfigGenerator__' + attr_name |
+ return getattr(self.generator, protected_attr_name) |
+ |
+ def testFieldToSubfieldsSimpleField(self): |
+ m_field = messages.StringField(1) |
+ expected = [[m_field]] |
+ self.assertItemsEqual(expected, |
+ self.GetPrivateMethod('field_to_subfields')(m_field)) |
+ |
+ def testFieldToSubfieldsSingleMessageField(self): |
+ class RefClass(messages.Message): |
+ one = messages.StringField(1) |
+ two = messages.IntegerField(2) |
+ m_field = messages.MessageField(RefClass, 1) |
+ expected = [ |
+ [m_field, RefClass.one], |
+ [m_field, RefClass.two], |
+ ] |
+ self.assertItemsEqual(expected, |
+ self.GetPrivateMethod('field_to_subfields')(m_field)) |
+ |
+ def testFieldToSubfieldsDifferingDepth(self): |
+ expected = [ |
+ [self.m_field, self.ref_class.one], |
+ [self.m_field, self.ref_class.two, self.other_ref_class.three], |
+ [self.m_field, self.ref_class.two, self.other_ref_class.four], |
+ [self.m_field, self.ref_class.two, self.other_ref_class.five], |
+ [self.m_field, self.ref_class.not_two, self.other_ref_class.three], |
+ [self.m_field, self.ref_class.not_two, self.other_ref_class.four], |
+ [self.m_field, self.ref_class.not_two, self.other_ref_class.five], |
+ ] |
+ self.assertItemsEqual( |
+ expected, self.GetPrivateMethod('field_to_subfields')(self.m_field)) |
+ |
+ def testGetPathParameters(self): |
+ get_path_parameters = self.GetPrivateMethod('get_path_parameters') |
+ expected = { |
+ 'c': ['c'], |
+ 'd': ['d.e'], |
+ } |
+ test_util.AssertDictEqual( |
+ expected, get_path_parameters('/a/b/{c}/{d.e}/{}'), self) |
+ test_util.AssertDictEqual( |
+ {}, get_path_parameters('/stray{/brackets{in/the}middle'), self) |
+ |
+ def testValidatePathParameters(self): |
+ # This also tests __validate_simple_subfield indirectly |
+ validate_path_parameters = self.GetPrivateMethod('validate_path_parameters') |
+ self.assertRaises(TypeError, validate_path_parameters, |
+ self.m_field, ['x']) |
+ self.assertRaises(TypeError, validate_path_parameters, |
+ self.m_field, ['m_field']) |
+ self.assertRaises(TypeError, validate_path_parameters, |
+ self.m_field, ['m_field.one_typo']) |
+ # This should not fail |
+ validate_path_parameters(self.m_field, ['m_field.one']) |
+ |
+ def MethodDescriptorTest(self, method_name, path, param_order, parameters): |
+ method_descriptor = self.api['methods'][method_name] |
+ self.assertEqual(method_descriptor['path'], path) |
+ request_descriptor = method_descriptor['request'] |
+ self.assertEqual(param_order, request_descriptor['parameterOrder']) |
+ self.assertEqual(parameters, request_descriptor['parameters']) |
+ |
+ def testParametersDescriptorEntriesGet(self): |
+ parameters = { |
+ 'one': { |
+ 'type': 'string', |
+ }, |
+ 'two.three': { |
+ 'repeated': True, |
+ 'required': True, |
+ 'type': 'boolean', |
+ }, |
+ 'two.four': { |
+ 'required': True, |
+ 'type': 'double', |
+ }, |
+ 'two.five': { |
+ 'default': 42, |
+ 'type': 'int64' |
+ }, |
+ 'not_two.three': { |
+ 'repeated': True, |
+ 'type': 'boolean', |
+ }, |
+ 'not_two.four': { |
+ 'required': True, |
+ 'type': 'double', |
+ }, |
+ 'not_two.five': { |
+ 'default': 42, |
+ 'type': 'int64' |
+ }, |
+ } |
+ |
+ # Without container. |
+ self.MethodDescriptorTest('root.entries.get', 'a/{two.three}/{two.four}', |
+ ['two.three', 'two.four', 'not_two.four'], |
+ parameters) |
+ # With container. |
+ self.MethodDescriptorTest('root.entries.getContainer', |
+ 'a/container/{two.three}/{two.four}', |
+ # Not parameter order differs because of the way |
+ # combined_message_class combines classes. This |
+ # is not so big a deal. |
+ ['not_two.four', 'two.three', 'two.four'], |
+ parameters) |
+ |
+ def testParametersDescriptorEntriesPut(self): |
+ param_order = ['one', 'two.three'] |
+ parameters = { |
+ 'one': { |
+ 'required': True, |
+ 'type': 'string', |
+ }, |
+ 'two.three': { |
+ 'repeated': True, |
+ 'required': True, |
+ 'type': 'boolean', |
+ }, |
+ 'two.four': { |
+ 'type': 'double', |
+ }, |
+ 'two.five': { |
+ 'default': 42, |
+ 'type': 'int64' |
+ }, |
+ } |
+ |
+ # Without container. |
+ self.MethodDescriptorTest('root.entries.put', 'b/{two.three}/{one}', |
+ param_order, parameters) |
+ # With container. |
+ self.MethodDescriptorTest('root.entries.putContainer', |
+ 'b/container/{two.three}/{one}', |
+ param_order, parameters) |
+ |
+ |
+class ApiDecoratorTest(unittest.TestCase): |
+ |
+ def testApiInfoPopulated(self): |
+ |
+ @api_config.api(name='CoolService', version='vX', |
+ description='My Cool Service', hostname='myhost.com', |
+ canonical_name='Cool Service Name') |
+ class MyDecoratedService(remote.Service): |
+ """Describes MyDecoratedService.""" |
+ pass |
+ |
+ api_info = MyDecoratedService.api_info |
+ self.assertEqual('CoolService', api_info.name) |
+ self.assertEqual('vX', api_info.version) |
+ self.assertEqual('My Cool Service', api_info.description) |
+ self.assertEqual('myhost.com', api_info.hostname) |
+ self.assertEqual('Cool Service Name', api_info.canonical_name) |
+ self.assertIsNone(api_info.audiences) |
+ self.assertEqual([api_config.EMAIL_SCOPE], api_info.scopes) |
+ self.assertEqual([api_config.API_EXPLORER_CLIENT_ID], |
+ api_info.allowed_client_ids) |
+ self.assertEqual(AUTH_LEVEL.NONE, api_info.auth_level) |
+ self.assertEqual(None, api_info.resource_name) |
+ self.assertEqual(None, api_info.path) |
+ |
+ def testApiInfoDefaults(self): |
+ |
+ @api_config.api('CoolService2', 'v2') |
+ class MyDecoratedService(remote.Service): |
+ """Describes MyDecoratedService.""" |
+ pass |
+ |
+ api_info = MyDecoratedService.api_info |
+ self.assertEqual('CoolService2', api_info.name) |
+ self.assertEqual('v2', api_info.version) |
+ self.assertEqual(None, api_info.description) |
+ self.assertEqual(None, api_info.hostname) |
+ self.assertEqual(None, api_info.canonical_name) |
+ self.assertEqual(None, api_info.title) |
+ self.assertEqual(None, api_info.documentation) |
+ |
+ def testGetApiClassesSingle(self): |
+ """Test that get_api_classes works when one class has been decorated.""" |
+ my_api = api_config.api(name='My Service', version='v1') |
+ |
+ @my_api |
+ class MyDecoratedService(remote.Service): |
+ """Describes MyDecoratedService.""" |
+ |
+ self.assertEqual([MyDecoratedService], my_api.get_api_classes()) |
+ |
+ def testGetApiClassesSingleCollection(self): |
+ """Test that get_api_classes works with the collection() decorator.""" |
+ my_api = api_config.api(name='My Service', version='v1') |
+ |
+ @my_api.api_class(resource_name='foo') |
+ class MyDecoratedService(remote.Service): |
+ """Describes MyDecoratedService.""" |
+ |
+ self.assertEqual([MyDecoratedService], my_api.get_api_classes()) |
+ |
+ def testGetApiClassesMultiple(self): |
+ """Test that get_api_classes works with multiple classes.""" |
+ my_api = api_config.api(name='My Service', version='v1') |
+ |
+ @my_api.api_class(resource_name='foo') |
+ class MyDecoratedService1(remote.Service): |
+ """Describes MyDecoratedService.""" |
+ |
+ @my_api.api_class(resource_name='bar') |
+ class MyDecoratedService2(remote.Service): |
+ """Describes MyDecoratedService.""" |
+ |
+ @my_api.api_class(resource_name='baz') |
+ class MyDecoratedService3(remote.Service): |
+ """Describes MyDecoratedService.""" |
+ |
+ self.assertEqual([MyDecoratedService1, MyDecoratedService2, |
+ MyDecoratedService3], my_api.get_api_classes()) |
+ |
+ def testGetApiClassesMixedStyles(self): |
+ """Test that get_api_classes works when decorated differently.""" |
+ my_api = api_config.api(name='My Service', version='v1') |
+ |
+ # @my_api is equivalent to @my_api.api_class(). This is allowed, though |
+ # mixing styles like this shouldn't be encouraged. |
+ @my_api |
+ class MyDecoratedService1(remote.Service): |
+ """Describes MyDecoratedService.""" |
+ |
+ @my_api |
+ class MyDecoratedService2(remote.Service): |
+ """Describes MyDecoratedService.""" |
+ |
+ @my_api.api_class(resource_name='foo') |
+ class MyDecoratedService3(remote.Service): |
+ """Describes MyDecoratedService.""" |
+ |
+ self.assertEqual([MyDecoratedService1, MyDecoratedService2, |
+ MyDecoratedService3], my_api.get_api_classes()) |
+ |
+ |
+class MethodDecoratorTest(unittest.TestCase): |
+ |
+ def testMethodId(self): |
+ |
+ @api_config.api('foo', 'v2') |
+ class MyDecoratedService(remote.Service): |
+ """Describes MyDecoratedService.""" |
+ |
+ @api_config.method() |
+ def get(self): |
+ pass |
+ |
+ @api_config.method() |
+ def people(self): |
+ pass |
+ |
+ @api_config.method() |
+ def _get(self): |
+ pass |
+ |
+ @api_config.method() |
+ def get_(self): |
+ pass |
+ |
+ @api_config.method() |
+ def _(self): |
+ pass |
+ |
+ @api_config.method() |
+ def _____(self): |
+ pass |
+ |
+ @api_config.method() |
+ def people_update(self): |
+ pass |
+ |
+ @api_config.method() |
+ def people_search(self): |
+ pass |
+ |
+ # pylint: disable=g-bad-name |
+ @api_config.method() |
+ def _several_underscores__in_various___places__(self): |
+ pass |
+ |
+ test_cases = [ |
+ ('get', 'foo.get'), |
+ ('people', 'foo.people'), |
+ ('_get', 'foo.get'), |
+ ('get_', 'foo.get_'), |
+ ('_', 'foo.'), |
+ ('_____', 'foo.'), |
+ ('people_update', 'foo.people_update'), |
+ ('people_search', 'foo.people_search'), |
+ ('_several_underscores__in_various___places__', |
+ 'foo.several_underscores__in_various___places__') |
+ ] |
+ |
+ for protorpc_method_name, expected in test_cases: |
+ method_id = '' |
+ info = getattr(MyDecoratedService, protorpc_method_name, None) |
+ self.assertIsNotNone(info) |
+ |
+ method_id = info.method_info.method_id(MyDecoratedService.api_info) |
+ self.assertEqual(expected, method_id, |
+ 'unexpected result (%s) for: %s' % |
+ (method_id, protorpc_method_name)) |
+ |
+ def testMethodInfoPopulated(self): |
+ |
+ @api_config.api(name='CoolService', version='vX', |
+ description='My Cool Service', hostname='myhost.com') |
+ class MyDecoratedService(remote.Service): |
+ """Describes MyDecoratedService.""" |
+ |
+ @api_config.method(request_message=Nested, |
+ response_message=AllFields, |
+ name='items.operate', |
+ path='items', |
+ http_method='GET', |
+ scopes=['foo'], |
+ audiences=['bar'], |
+ allowed_client_ids=['baz', 'bim'], |
+ auth_level=AUTH_LEVEL.REQUIRED) |
+ def my_method(self): |
+ pass |
+ |
+ method_info = MyDecoratedService.my_method.method_info |
+ protorpc_info = MyDecoratedService.my_method.remote |
+ self.assertEqual(Nested, protorpc_info.request_type) |
+ self.assertEqual(AllFields, protorpc_info.response_type) |
+ self.assertEqual('items.operate', method_info.name) |
+ self.assertEqual('items', method_info.get_path(MyDecoratedService.api_info)) |
+ self.assertEqual('GET', method_info.http_method) |
+ self.assertEqual(['foo'], method_info.scopes) |
+ self.assertEqual(['bar'], method_info.audiences) |
+ self.assertEqual(['baz', 'bim'], method_info.allowed_client_ids) |
+ self.assertEqual(AUTH_LEVEL.REQUIRED, method_info.auth_level) |
+ |
+ def testMethodInfoDefaults(self): |
+ |
+ @api_config.api('CoolService2', 'v2') |
+ class MyDecoratedService(remote.Service): |
+ """Describes MyDecoratedService.""" |
+ |
+ @api_config.method() |
+ def my_method(self): |
+ pass |
+ |
+ method_info = MyDecoratedService.my_method.method_info |
+ protorpc_info = MyDecoratedService.my_method.remote |
+ self.assertEqual(message_types.VoidMessage, protorpc_info.request_type) |
+ self.assertEqual(message_types.VoidMessage, protorpc_info.response_type) |
+ self.assertEqual('my_method', method_info.name) |
+ self.assertEqual('my_method', |
+ method_info.get_path(MyDecoratedService.api_info)) |
+ self.assertEqual('POST', method_info.http_method) |
+ self.assertEqual(None, method_info.scopes) |
+ self.assertEqual(None, method_info.audiences) |
+ self.assertEqual(None, method_info.allowed_client_ids) |
+ self.assertEqual(None, method_info.auth_level) |
+ |
+ def testMethodInfoPath(self): |
+ |
+ class MyRequest(messages.Message): |
+ """Documentation for MyRequest.""" |
+ zebra = messages.StringField(1, required=True) |
+ kitten = messages.StringField(2, required=True) |
+ dog = messages.StringField(3) |
+ panda = messages.StringField(4, required=True) |
+ |
+ @api_config.api('CoolService3', 'v3') |
+ class MyDecoratedService(remote.Service): |
+ """Describes MyDecoratedService.""" |
+ |
+ @api_config.method(MyRequest, message_types.VoidMessage) |
+ def default_path_method(self): |
+ pass |
+ |
+ @api_config.method(MyRequest, message_types.VoidMessage, |
+ path='zebras/{zebra}/pandas/{panda}/kittens/{kitten}') |
+ def specified_path_method(self): |
+ pass |
+ |
+ specified_path_info = MyDecoratedService.specified_path_method.method_info |
+ specified_protorpc_info = MyDecoratedService.specified_path_method.remote |
+ self.assertEqual(MyRequest, specified_protorpc_info.request_type) |
+ self.assertEqual(message_types.VoidMessage, |
+ specified_protorpc_info.response_type) |
+ self.assertEqual('specified_path_method', specified_path_info.name) |
+ self.assertEqual('zebras/{zebra}/pandas/{panda}/kittens/{kitten}', |
+ specified_path_info.get_path(MyDecoratedService.api_info)) |
+ self.assertEqual('POST', specified_path_info.http_method) |
+ self.assertEqual(None, specified_path_info.scopes) |
+ self.assertEqual(None, specified_path_info.audiences) |
+ self.assertEqual(None, specified_path_info.allowed_client_ids) |
+ self.assertEqual(None, specified_path_info.auth_level) |
+ |
+ default_path_info = MyDecoratedService.default_path_method.method_info |
+ default_protorpc_info = MyDecoratedService.default_path_method.remote |
+ self.assertEqual(MyRequest, default_protorpc_info.request_type) |
+ self.assertEqual(message_types.VoidMessage, |
+ default_protorpc_info.response_type) |
+ self.assertEqual('default_path_method', default_path_info.name) |
+ self.assertEqual('default_path_method', |
+ default_path_info.get_path(MyDecoratedService.api_info)) |
+ self.assertEqual('POST', default_path_info.http_method) |
+ self.assertEqual(None, default_path_info.scopes) |
+ self.assertEqual(None, default_path_info.audiences) |
+ self.assertEqual(None, default_path_info.allowed_client_ids) |
+ self.assertEqual(None, specified_path_info.auth_level) |
+ |
+ def testInvalidPaths(self): |
+ for path in ('invalid/mixed{param}', |
+ 'invalid/{param}mixed', |
+ 'invalid/mixed{param}mixed', |
+ 'invalid/{extra}{vars}', |
+ 'invalid/{}/emptyvar'): |
+ |
+ @api_config.api('root', 'v1') |
+ class MyDecoratedService(remote.Service): |
+ """Describes MyDecoratedService.""" |
+ |
+ @api_config.method(message_types.VoidMessage, message_types.VoidMessage, |
+ path=path) |
+ def test(self): |
+ pass |
+ |
+ self.assertRaises(api_exceptions.ApiConfigurationError, |
+ MyDecoratedService.test.method_info.get_path, |
+ MyDecoratedService.api_info) |
+ |
+ def testMethodAttributeInheritance(self): |
+ """Test descriptor attributes that can be inherited from the main config.""" |
+ self.TryListAttributeVariations('audiences', 'audiences', None) |
+ self.TryListAttributeVariations( |
+ 'scopes', 'scopes', |
+ ['https://www.googleapis.com/auth/userinfo.email']) |
+ self.TryListAttributeVariations('allowed_client_ids', 'clientIds', |
+ [api_config.API_EXPLORER_CLIENT_ID]) |
+ |
+ def TryListAttributeVariations(self, attribute_name, config_name, |
+ default_expected): |
+ """Test setting an attribute in the API config and method configs. |
+ |
+ The audiences, scopes and allowed_client_ids settings can be set |
+ in either the main API config or on each of the methods. This helper |
+ function tests each variation of one of these (whichever is specified) |
+ and ensures that the api config has the right values. |
+ |
+ Args: |
+ attribute_name: Name of the keyword arg to pass to the api or method |
+ decorator. Also the name of the attribute used to access that |
+ variable on api_info or method_info. |
+ config_name: Name of the variable as it appears in the configuration |
+ output. |
+ default_expected: The default expected value if the attribute isn't |
+ specified on either the api or the method. |
+ """ |
+ |
+ # Try the various combinations of api-level and method-level settings. |
+ # Test cases are: (api-setting, method-setting, expected) |
+ test_cases = ((None, ['foo', 'bar'], ['foo', 'bar']), |
+ (None, [], None), |
+ (['foo', 'bar'], None, ['foo', 'bar']), |
+ (['foo', 'bar'], ['foo', 'bar'], ['foo', 'bar']), |
+ (['foo', 'bar'], ['foo', 'baz'], ['foo', 'baz']), |
+ (['foo', 'bar'], [], None), |
+ (['foo', 'bar'], ['abc'], ['abc']), |
+ (None, None, default_expected)) |
+ for api_value, method_value, expected_value in test_cases: |
+ api_kwargs = {attribute_name: api_value} |
+ method_kwargs = {attribute_name: method_value} |
+ |
+ @api_config.api('AuthService', 'v1', hostname='example.appspot.com', |
+ **api_kwargs) |
+ class AuthServiceImpl(remote.Service): |
+ """Describes AuthServiceImpl.""" |
+ |
+ @api_config.method(**method_kwargs) |
+ def baz(self): |
+ pass |
+ |
+ self.assertEqual(api_value if api_value is not None else default_expected, |
+ getattr(AuthServiceImpl.api_info, attribute_name)) |
+ self.assertEqual(method_value, |
+ getattr(AuthServiceImpl.baz.method_info, attribute_name)) |
+ |
+ generator = ApiConfigGenerator() |
+ api = json.loads(generator.pretty_print_config_to_json(AuthServiceImpl)) |
+ expected = { |
+ 'authService.baz': { |
+ 'httpMethod': 'POST', |
+ 'path': 'baz', |
+ 'request': {'body': 'empty'}, |
+ 'response': {'body': 'empty'}, |
+ 'rosyMethod': 'AuthServiceImpl.baz', |
+ 'scopes': ['https://www.googleapis.com/auth/userinfo.email'], |
+ 'clientIds': [api_config.API_EXPLORER_CLIENT_ID], |
+ 'authLevel': 'NONE' |
+ } |
+ } |
+ if expected_value: |
+ expected['authService.baz'][config_name] = expected_value |
+ elif config_name in expected['authService.baz']: |
+ del expected['authService.baz'][config_name] |
+ |
+ test_util.AssertDictEqual(expected, api['methods'], self) |
+ |
+ |
+if __name__ == '__main__': |
+ unittest.main() |