Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(606)

Unified Diff: third_party/google-endpoints/endpoints/test/api_config_test.py

Issue 2666783008: Add google-endpoints to third_party/. (Closed)
Patch Set: Created 3 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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()

Powered by Google App Engine
This is Rietveld 408576698