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

Side by Side 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, 10 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 unified diff | Download patch
OLDNEW
(Empty)
1 # Copyright 2016 Google Inc. All Rights Reserved.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 """Tests for endpoints.api_config."""
16
17 import itertools
18 import json
19 import logging
20 import unittest
21
22 import endpoints.api_config as api_config
23 from endpoints.api_config import ApiConfigGenerator
24 from endpoints.api_config import AUTH_LEVEL
25 import endpoints.api_exceptions as api_exceptions
26 import mock
27 from protorpc import message_types
28 from protorpc import messages
29 from protorpc import remote
30
31 import endpoints.resource_container as resource_container
32
33 import test_util
34
35 package = 'api_config_test'
36 _DESCRIPTOR_PATH_PREFIX = ''
37
38
39 class ModuleInterfaceTest(test_util.ModuleInterfaceTest,
40 unittest.TestCase):
41
42 MODULE = api_config
43
44
45 class Nested(messages.Message):
46 """Message class to be used in a message field."""
47 int_value = messages.IntegerField(1)
48 string_value = messages.StringField(2)
49
50
51 class SimpleEnum(messages.Enum):
52 """Simple enumeration type."""
53 VAL1 = 1
54 VAL2 = 2
55
56
57 class AllFields(messages.Message):
58 """Contains all field types."""
59
60 bool_value = messages.BooleanField(1, variant=messages.Variant.BOOL)
61 bytes_value = messages.BytesField(2, variant=messages.Variant.BYTES)
62 double_value = messages.FloatField(3, variant=messages.Variant.DOUBLE)
63 enum_value = messages.EnumField(SimpleEnum, 4)
64 float_value = messages.FloatField(5, variant=messages.Variant.FLOAT)
65 int32_value = messages.IntegerField(6, variant=messages.Variant.INT32)
66 int64_value = messages.IntegerField(7, variant=messages.Variant.INT64)
67 string_value = messages.StringField(8, variant=messages.Variant.STRING)
68 uint32_value = messages.IntegerField(9, variant=messages.Variant.UINT32)
69 uint64_value = messages.IntegerField(10, variant=messages.Variant.UINT64)
70 sint32_value = messages.IntegerField(11, variant=messages.Variant.SINT32)
71 sint64_value = messages.IntegerField(12, variant=messages.Variant.SINT64)
72 message_field_value = messages.MessageField(Nested, 13)
73 datetime_value = message_types.DateTimeField(14)
74
75
76 # This is used test "all fields" as query parameters instead of the body
77 # in a request.
78 ALL_FIELDS_AS_PARAMETERS = resource_container.ResourceContainer(
79 **{field.name: field for field in AllFields.all_fields()})
80
81
82 class ApiConfigTest(unittest.TestCase):
83
84 def setUp(self):
85 self.generator = ApiConfigGenerator()
86 self.maxDiff = None
87
88 def testAllVariantsCovered(self):
89 variants_covered = set([field.variant for field in AllFields.all_fields()])
90
91 for variant in variants_covered:
92 self.assertTrue(isinstance(variant, messages.Variant))
93
94 variants_covered_dict = {}
95 for variant in variants_covered:
96 number = variant.number
97 if variants_covered_dict.get(variant.name, number) != number:
98 self.fail('Somehow have two variants with same name and '
99 'different number')
100 variants_covered_dict[variant.name] = number
101
102 test_util.AssertDictEqual(
103 messages.Variant.to_dict(), variants_covered_dict, self)
104
105 def testAllFieldTypes(self):
106
107 class PutRequest(messages.Message):
108 """Message with just a body field."""
109 body = messages.MessageField(AllFields, 1)
110
111 class ItemsPutRequest(messages.Message):
112 """Message with path params and a body field."""
113 body = messages.MessageField(AllFields, 1)
114 entryId = messages.StringField(2, required=True)
115
116 class ItemsPutRequestForContainer(messages.Message):
117 """Message with path params and a body field."""
118 body = messages.MessageField(AllFields, 1)
119 items_put_request_container = resource_container.ResourceContainer(
120 ItemsPutRequestForContainer,
121 entryId=messages.StringField(2, required=True))
122
123 class EntryPublishRequest(messages.Message):
124 """Message with two required params, one in path, one in body."""
125 title = messages.StringField(1, required=True)
126 entryId = messages.StringField(2, required=True)
127
128 class EntryPublishRequestForContainer(messages.Message):
129 """Message with two required params, one in path, one in body."""
130 title = messages.StringField(1, required=True)
131 entry_publish_request_container = resource_container.ResourceContainer(
132 EntryPublishRequestForContainer,
133 entryId=messages.StringField(2, required=True))
134
135 @api_config.api(name='root', hostname='example.appspot.com', version='v1')
136 class MyService(remote.Service):
137 """Describes MyService."""
138
139 @api_config.method(AllFields, message_types.VoidMessage, path='entries',
140 http_method='GET', name='entries.get')
141 def entries_get(self, unused_request):
142 """All field types in the query parameters."""
143 return message_types.VoidMessage()
144
145 @api_config.method(ALL_FIELDS_AS_PARAMETERS, message_types.VoidMessage,
146 path='entries/container', http_method='GET',
147 name='entries.getContainer')
148 def entries_get_container(self, unused_request):
149 """All field types in the query parameters."""
150 return message_types.VoidMessage()
151
152 @api_config.method(PutRequest, message_types.VoidMessage, path='entries',
153 name='entries.put')
154 def entries_put(self, unused_request):
155 """Request body is in the body field."""
156 return message_types.VoidMessage()
157
158 @api_config.method(AllFields, message_types.VoidMessage, path='process',
159 name='entries.process')
160 def entries_process(self, unused_request):
161 """Message is the request body."""
162 return message_types.VoidMessage()
163
164 @api_config.method(message_types.VoidMessage, message_types.VoidMessage,
165 name='entries.nested.collection.action',
166 path='nested')
167 def entries_nested_collection_action(self, unused_request):
168 """A VoidMessage for a request body."""
169 return message_types.VoidMessage()
170
171 @api_config.method(AllFields, AllFields, name='entries.roundtrip',
172 path='roundtrip')
173 def entries_roundtrip(self, unused_request):
174 """All field types in the request and response."""
175 pass
176
177 # Test a method with a required parameter in the request body.
178 @api_config.method(EntryPublishRequest, message_types.VoidMessage,
179 path='entries/{entryId}/publish',
180 name='entries.publish')
181 def entries_publish(self, unused_request):
182 """Path has a parameter and request body has a required param."""
183 return message_types.VoidMessage()
184
185 @api_config.method(entry_publish_request_container,
186 message_types.VoidMessage,
187 path='entries/container/{entryId}/publish',
188 name='entries.publishContainer')
189 def entries_publish_container(self, unused_request):
190 """Path has a parameter and request body has a required param."""
191 return message_types.VoidMessage()
192
193 # Test a method with a parameter in the path and a request body.
194 @api_config.method(ItemsPutRequest, message_types.VoidMessage,
195 path='entries/{entryId}/items',
196 name='entries.items.put')
197 def items_put(self, unused_request):
198 """Path has a parameter and request body is in the body field."""
199 return message_types.VoidMessage()
200
201 @api_config.method(items_put_request_container, message_types.VoidMessage,
202 path='entries/container/{entryId}/items',
203 name='entries.items.putContainer')
204 def items_put_container(self, unused_request):
205 """Path has a parameter and request body is in the body field."""
206 return message_types.VoidMessage()
207
208 api = json.loads(self.generator.pretty_print_config_to_json(MyService))
209
210 expected = {
211 'root.entries.get': {
212 'description': 'All field types in the query parameters.',
213 'httpMethod': 'GET',
214 'path': 'entries',
215 'request': {
216 'body': 'empty',
217 'parameters': {
218 'bool_value': {
219 'type': 'boolean',
220 },
221 'bytes_value': {
222 'type': 'bytes',
223 },
224 'double_value': {
225 'type': 'double',
226 },
227 'enum_value': {
228 'type': 'string',
229 'enum': {
230 'VAL1': {
231 'backendValue': 'VAL1',
232 },
233 'VAL2': {
234 'backendValue': 'VAL2',
235 },
236 },
237 },
238 'float_value': {
239 'type': 'float',
240 },
241 'int32_value': {
242 'type': 'int32',
243 },
244 'int64_value': {
245 'type': 'int64',
246 },
247 'string_value': {
248 'type': 'string',
249 },
250 'uint32_value': {
251 'type': 'uint32',
252 },
253 'uint64_value': {
254 'type': 'uint64',
255 },
256 'sint32_value': {
257 'type': 'int32',
258 },
259 'sint64_value': {
260 'type': 'int64',
261 },
262 'message_field_value.int_value': {
263 'type': 'int64',
264 },
265 'message_field_value.string_value': {
266 'type': 'string',
267 },
268 'datetime_value.milliseconds': {
269 'type': 'int64',
270 },
271 'datetime_value.time_zone_offset': {
272 'type': 'int64',
273 },
274 },
275 },
276 'response': {
277 'body': 'empty',
278 },
279 'rosyMethod': 'MyService.entries_get',
280 'scopes': ['https://www.googleapis.com/auth/userinfo.email'],
281 'clientIds': [api_config.API_EXPLORER_CLIENT_ID],
282 'authLevel': 'NONE',
283 },
284 'root.entries.getContainer': {
285 'description': 'All field types in the query parameters.',
286 'httpMethod': 'GET',
287 'path': 'entries/container',
288 'request': {
289 'body': 'empty',
290 'parameters': {
291 'bool_value': {
292 'type': 'boolean'
293 },
294 'bytes_value': {
295 'type': 'bytes'
296 },
297 'datetime_value.milliseconds': {
298 'type': 'int64'
299 },
300 'datetime_value.time_zone_offset': {
301 'type': 'int64'
302 },
303 'double_value': {
304 'type': 'double'
305 },
306 'enum_value': {
307 'enum': {
308 'VAL1': {'backendValue': 'VAL1'},
309 'VAL2': {'backendValue': 'VAL2'},
310 },
311 'type': 'string',
312 },
313 'float_value': {
314 'type': 'float'
315 },
316 'int32_value': {
317 'type': 'int32'
318 },
319 'int64_value': {
320 'type': 'int64'
321 },
322 'message_field_value.int_value': {
323 'type': 'int64'
324 },
325 'message_field_value.string_value': {
326 'type': 'string'
327 },
328 'sint32_value': {
329 'type': 'int32'
330 },
331 'sint64_value': {
332 'type': 'int64'
333 },
334 'string_value': {
335 'type': 'string'
336 },
337 'uint32_value': {
338 'type': 'uint32'
339 },
340 'uint64_value': {
341 'type': 'uint64'
342 }
343 }
344 },
345 'response': {
346 'body': 'empty'
347 },
348 'rosyMethod': 'MyService.entries_get_container',
349 'scopes': ['https://www.googleapis.com/auth/userinfo.email'],
350 'clientIds': [api_config.API_EXPLORER_CLIENT_ID],
351 'authLevel': 'NONE',
352 },
353 'root.entries.publishContainer': {
354 'description': ('Path has a parameter and request body has a '
355 'required param.'),
356 'httpMethod': 'POST',
357 'path': 'entries/container/{entryId}/publish',
358 'request': {
359 'body': 'autoTemplate(backendRequest)',
360 'bodyName': 'resource',
361 'parameterOrder': ['entryId'],
362 'parameters': {
363 'entryId': {
364 'required': True,
365 'type': 'string',
366 }
367 }
368 },
369 'response': {
370 'body': 'empty'
371 },
372 'rosyMethod': 'MyService.entries_publish_container',
373 'scopes': ['https://www.googleapis.com/auth/userinfo.email'],
374 'clientIds': [api_config.API_EXPLORER_CLIENT_ID],
375 'authLevel': 'NONE',
376 },
377 'root.entries.put': {
378 'description': 'Request body is in the body field.',
379 'httpMethod': 'POST',
380 'path': 'entries',
381 'request': {
382 'body': 'autoTemplate(backendRequest)',
383 'bodyName': 'resource'
384 },
385 'response': {
386 'body': 'empty'
387 },
388 'rosyMethod': 'MyService.entries_put',
389 'scopes': ['https://www.googleapis.com/auth/userinfo.email'],
390 'clientIds': [api_config.API_EXPLORER_CLIENT_ID],
391 'authLevel': 'NONE',
392 },
393 'root.entries.process': {
394 'description': 'Message is the request body.',
395 'httpMethod': 'POST',
396 'path': 'process',
397 'request': {
398 'body': 'autoTemplate(backendRequest)',
399 'bodyName': 'resource'
400 },
401 'response': {
402 'body': 'empty'
403 },
404 'rosyMethod': 'MyService.entries_process',
405 'scopes': ['https://www.googleapis.com/auth/userinfo.email'],
406 'clientIds': [api_config.API_EXPLORER_CLIENT_ID],
407 'authLevel': 'NONE',
408 },
409 'root.entries.nested.collection.action': {
410 'description': 'A VoidMessage for a request body.',
411 'httpMethod': 'POST',
412 'path': 'nested',
413 'request': {
414 'body': 'empty'
415 },
416 'response': {
417 'body': 'empty'
418 },
419 'rosyMethod': 'MyService.entries_nested_collection_action',
420 'scopes': ['https://www.googleapis.com/auth/userinfo.email'],
421 'clientIds': [api_config.API_EXPLORER_CLIENT_ID],
422 'authLevel': 'NONE',
423 },
424 'root.entries.roundtrip': {
425 'description': 'All field types in the request and response.',
426 'httpMethod': 'POST',
427 'path': 'roundtrip',
428 'request': {
429 'body': 'autoTemplate(backendRequest)',
430 'bodyName': 'resource'
431 },
432 'response': {
433 'body': 'autoTemplate(backendResponse)',
434 'bodyName': 'resource'
435 },
436 'rosyMethod': 'MyService.entries_roundtrip',
437 'scopes': ['https://www.googleapis.com/auth/userinfo.email'],
438 'clientIds': [api_config.API_EXPLORER_CLIENT_ID],
439 'authLevel': 'NONE',
440 },
441 'root.entries.publish': {
442 'description':
443 'Path has a parameter and request body has a required param.',
444 'httpMethod': 'POST',
445 'path': 'entries/{entryId}/publish',
446 'request': {
447 'body': 'autoTemplate(backendRequest)',
448 'bodyName': 'resource',
449 'parameterOrder': [
450 'entryId'
451 ],
452 'parameters': {
453 'entryId': {
454 'type': 'string',
455 'required': True,
456 },
457 },
458 },
459 'response': {
460 'body': 'empty'
461 },
462 'rosyMethod': 'MyService.entries_publish',
463 'scopes': ['https://www.googleapis.com/auth/userinfo.email'],
464 'clientIds': [api_config.API_EXPLORER_CLIENT_ID],
465 'authLevel': 'NONE',
466 },
467 'root.entries.items.put': {
468 'description':
469 'Path has a parameter and request body is in the body field.',
470 'httpMethod': 'POST',
471 'path': 'entries/{entryId}/items',
472 'request': {
473 'body': 'autoTemplate(backendRequest)',
474 'bodyName': 'resource',
475 'parameterOrder': [
476 'entryId'
477 ],
478 'parameters': {
479 'entryId': {
480 'type': 'string',
481 'required': True,
482 },
483 },
484 },
485 'response': {
486 'body': 'empty'
487 },
488 'rosyMethod': 'MyService.items_put',
489 'scopes': ['https://www.googleapis.com/auth/userinfo.email'],
490 'clientIds': [api_config.API_EXPLORER_CLIENT_ID],
491 'authLevel': 'NONE',
492 },
493 'root.entries.items.putContainer': {
494 'description': ('Path has a parameter and request body is in '
495 'the body field.'),
496 'httpMethod': 'POST',
497 'path': 'entries/container/{entryId}/items',
498 'request': {
499 'body': 'autoTemplate(backendRequest)',
500 'bodyName': 'resource',
501 'parameterOrder': [
502 'entryId'
503 ],
504 'parameters': {
505 'entryId': {
506 'type': 'string',
507 'required': True,
508 },
509 },
510 },
511 'response': {
512 'body': 'empty'
513 },
514 'rosyMethod': 'MyService.items_put_container',
515 'scopes': ['https://www.googleapis.com/auth/userinfo.email'],
516 'clientIds': [api_config.API_EXPLORER_CLIENT_ID],
517 'authLevel': 'NONE',
518 }
519 }
520 expected_descriptor = {
521 'methods': {
522 'MyService.entries_get': {},
523 'MyService.entries_get_container': {},
524 'MyService.entries_nested_collection_action': {},
525 'MyService.entries_process': {
526 'request': {
527 '$ref': (_DESCRIPTOR_PATH_PREFIX +
528 'ApiConfigTestAllFields')
529 }
530 },
531 'MyService.entries_publish': {
532 'request': {
533 '$ref': (_DESCRIPTOR_PATH_PREFIX +
534 'ApiConfigTestEntryPublishRequest')
535 }
536 },
537 'MyService.entries_publish_container': {
538 'request': {
539 '$ref': (_DESCRIPTOR_PATH_PREFIX +
540 'ApiConfigTestEntryPublishRequestForContainer')
541 }
542 },
543 'MyService.entries_put': {
544 'request': {
545 '$ref': (_DESCRIPTOR_PATH_PREFIX +
546 'ApiConfigTestPutRequest')
547 }
548 },
549 'MyService.entries_roundtrip': {
550 'request': {
551 '$ref': (_DESCRIPTOR_PATH_PREFIX +
552 'ApiConfigTestAllFields')
553 },
554 'response': {
555 '$ref': (_DESCRIPTOR_PATH_PREFIX +
556 'ApiConfigTestAllFields')
557 }
558 },
559 'MyService.items_put': {
560 'request': {
561 '$ref': (_DESCRIPTOR_PATH_PREFIX +
562 'ApiConfigTestItemsPutRequest')
563 }
564 },
565 'MyService.items_put_container': {
566 'request': {
567 '$ref': (_DESCRIPTOR_PATH_PREFIX +
568 'ApiConfigTestItemsPutRequestForContainer')
569 }
570 }
571 },
572 'schemas': {
573 _DESCRIPTOR_PATH_PREFIX + 'ApiConfigTestAllFields': {
574 'description': 'Contains all field types.',
575 'id': _DESCRIPTOR_PATH_PREFIX + 'ApiConfigTestAllFields',
576 'properties': {
577 'bool_value': {
578 'type': 'boolean'
579 },
580 'bytes_value': {
581 'type': 'string',
582 'format': 'byte'
583 },
584 'double_value': {
585 'format': 'double',
586 'type': 'number'
587 },
588 'enum_value': {
589 'type': 'string',
590 'enum': ['VAL1', 'VAL2']
591 },
592 'float_value': {
593 'format': 'float',
594 'type': 'number'
595 },
596 'int32_value': {
597 'format': 'int32',
598 'type': 'integer'
599 },
600 'int64_value': {
601 'format': 'int64',
602 'type': 'string'
603 },
604 'string_value': {
605 'type': 'string'
606 },
607 'uint32_value': {
608 'format': 'uint32',
609 'type': 'integer'
610 },
611 'uint64_value': {
612 'format': 'uint64',
613 'type': 'string'
614 },
615 'sint32_value': {
616 'format': 'int32',
617 'type': 'integer'
618 },
619 'sint64_value': {
620 'format': 'int64',
621 'type': 'string'
622 },
623 'message_field_value': {
624 '$ref': (_DESCRIPTOR_PATH_PREFIX +
625 'ApiConfigTestNested'),
626 'description': ('Message class to be used in a '
627 'message field.'),
628 },
629 'datetime_value': {
630 'format': 'date-time',
631 'type': 'string'
632 },
633 },
634 'type': 'object'
635 },
636 _DESCRIPTOR_PATH_PREFIX + 'ApiConfigTestEntryPublishRequest': {
637 'description': ('Message with two required params, '
638 'one in path, one in body.'),
639 'id': (_DESCRIPTOR_PATH_PREFIX +
640 'ApiConfigTestEntryPublishRequest'),
641 'properties': {
642 'entryId': {
643 'required': True,
644 'type': 'string'
645 },
646 'title': {
647 'required': True,
648 'type': 'string'
649 }
650 },
651 'type': 'object'
652 },
653 (_DESCRIPTOR_PATH_PREFIX +
654 'ApiConfigTestEntryPublishRequestForContainer'): {
655 'description': ('Message with two required params, '
656 'one in path, one in body.'),
657 'id': (_DESCRIPTOR_PATH_PREFIX +
658 'ApiConfigTestEntryPublishRequestForContainer'),
659 'properties': {
660 'title': {
661 'required': True,
662 'type': 'string'
663 }
664 },
665 'type': 'object'
666 },
667 _DESCRIPTOR_PATH_PREFIX + 'ApiConfigTestItemsPutRequest': {
668 'description': 'Message with path params and a body field.',
669 'id': (_DESCRIPTOR_PATH_PREFIX +
670 'ApiConfigTestItemsPutRequest'),
671 'properties': {
672 'body': {
673 '$ref': (_DESCRIPTOR_PATH_PREFIX +
674 'ApiConfigTestAllFields'),
675 'description': 'Contains all field types.'
676 },
677 'entryId': {
678 'required': True,
679 'type': 'string'
680 }
681 },
682 'type': 'object'
683 },
684 (_DESCRIPTOR_PATH_PREFIX +
685 'ApiConfigTestItemsPutRequestForContainer'): {
686 'description': 'Message with path params and a body field.',
687 'id': (_DESCRIPTOR_PATH_PREFIX +
688 'ApiConfigTestItemsPutRequestForContainer'),
689 'properties': {
690 'body': {
691 '$ref': (_DESCRIPTOR_PATH_PREFIX +
692 'ApiConfigTestAllFields'),
693 'description': 'Contains all field types.'
694 },
695 },
696 'type': 'object'
697 },
698 _DESCRIPTOR_PATH_PREFIX + 'ApiConfigTestNested': {
699 'description': 'Message class to be used in a message field.',
700 'id': _DESCRIPTOR_PATH_PREFIX + 'ApiConfigTestNested',
701 'properties': {
702 'int_value': {
703 'format': 'int64',
704 'type': 'string'
705 },
706 'string_value': {
707 'type': 'string'
708 }
709 },
710 'type': 'object'
711 },
712 _DESCRIPTOR_PATH_PREFIX + 'ApiConfigTestPutRequest': {
713 'description': 'Message with just a body field.',
714 'id': _DESCRIPTOR_PATH_PREFIX + 'ApiConfigTestPutRequest',
715 'properties': {
716 'body': {
717 '$ref': (_DESCRIPTOR_PATH_PREFIX +
718 'ApiConfigTestAllFields'),
719 'description': 'Contains all field types.'
720 }
721 },
722 'type': 'object'
723 },
724 'ProtorpcMessageTypesVoidMessage': {
725 'description': 'Empty message.',
726 'id': 'ProtorpcMessageTypesVoidMessage',
727 'properties': {},
728 'type': 'object'
729 }
730 }
731 }
732 expected_adapter = {
733 'bns': 'https://example.appspot.com/_ah/api',
734 'type': 'lily',
735 'deadline': 10.0}
736
737 test_util.AssertDictEqual(expected, api['methods'], self)
738 test_util.AssertDictEqual(expected_descriptor, api['descriptor'], self)
739 test_util.AssertDictEqual(expected_adapter, api['adapter'], self)
740
741 self.assertEqual('Describes MyService.', api['description'])
742
743 methods = api['descriptor']['methods']
744 self.assertTrue('MyService.entries_get' in methods)
745 self.assertTrue('MyService.entries_put' in methods)
746 self.assertTrue('MyService.entries_process' in methods)
747 self.assertTrue('MyService.entries_nested_collection_action' in methods)
748
749 def testEmptyRequestNonEmptyResponse(self):
750 class MyResponse(messages.Message):
751 bool_value = messages.BooleanField(1)
752 int32_value = messages.IntegerField(2)
753
754 @api_config.api(name='root', version='v1', hostname='example.appspot.com')
755 class MySimpleService(remote.Service):
756
757 @api_config.method(message_types.VoidMessage, MyResponse,
758 name='entries.get')
759 def entries_get(self, request):
760 pass
761
762 api = json.loads(self.generator.pretty_print_config_to_json(
763 MySimpleService))
764
765 expected_request = {
766 'body': 'empty'
767 }
768 expected_response = {
769 'body': 'autoTemplate(backendResponse)',
770 'bodyName': 'resource'
771 }
772
773 test_util.AssertDictEqual(
774 expected_response, api['methods']['root.entries.get']['response'], self)
775
776 test_util.AssertDictEqual(
777 expected_request, api['methods']['root.entries.get']['request'], self)
778
779 def testEmptyService(self):
780
781 @api_config.api('root', 'v1', hostname='example.appspot.com')
782 class EmptyService(remote.Service):
783 pass
784
785 api = json.loads(self.generator.pretty_print_config_to_json(EmptyService))
786
787 self.assertTrue('methods' not in api)
788
789 def testOptionalProperties(self):
790 """Verify that optional config properties show up if they're supposed to."""
791 optional_props = (
792 ('canonical_name', 'canonicalName', 'Test Canonical Name'),
793 ('owner_domain', 'ownerDomain', 'google.com'),
794 ('owner_name', 'ownerName', 'Google'),
795 ('package_path', 'packagePath', 'cloud/platform'),
796 ('title', 'title', 'My Root API'),
797 ('documentation', 'documentation', 'http://link.to/docs'))
798
799 # Try all combinations of the above properties.
800 for length in range(1, len(optional_props) + 1):
801 for combination in itertools.combinations(optional_props, length):
802 kwargs = {}
803 for property_name, _, value in combination:
804 kwargs[property_name] = value
805
806 @api_config.api('root', 'v1', **kwargs)
807 class MyService(remote.Service):
808 pass
809
810 api = json.loads(self.generator.pretty_print_config_to_json(MyService))
811
812 for _, config_name, value in combination:
813 self.assertEqual(api[config_name], value)
814
815 # If the value is not set, verify that it's not there.
816 for property_name, config_name, value in optional_props:
817
818 @api_config.api('root2', 'v2')
819 class EmptyService2(remote.Service):
820 pass
821
822 api = json.loads(self.generator.pretty_print_config_to_json(
823 EmptyService2))
824 self.assertNotIn(config_name, api)
825
826 def testAuth(self):
827 """Verify that auth shows up in the config if it's supposed to."""
828
829 empty_auth = api_config.ApiAuth()
830 used_auth = api_config.ApiAuth(allow_cookie_auth=False)
831 cookie_auth = api_config.ApiAuth(allow_cookie_auth=True)
832 empty_blocked_regions = api_config.ApiAuth(blocked_regions=[])
833 one_blocked = api_config.ApiAuth(blocked_regions=['us'])
834 many_blocked = api_config.ApiAuth(blocked_regions=['CU', 'IR', 'KP', 'SD',
835 'SY', 'MM'])
836 mixed = api_config.ApiAuth(allow_cookie_auth=True,
837 blocked_regions=['US', 'IR'])
838
839 for auth, expected_result in ((None, None),
840 (empty_auth, None),
841 (used_auth, {'allowCookieAuth': False}),
842 (cookie_auth, {'allowCookieAuth': True}),
843 (empty_blocked_regions, None),
844 (one_blocked, {'blockedRegions': ['us']}),
845 (many_blocked, {'blockedRegions':
846 ['CU', 'IR', 'KP', 'SD',
847 'SY', 'MM']}),
848 (mixed, {'allowCookieAuth': True,
849 'blockedRegions': ['US', 'IR']})):
850
851 @api_config.api('root', 'v1', auth=auth)
852 class EmptyService(remote.Service):
853 pass
854
855 api = json.loads(self.generator.pretty_print_config_to_json(EmptyService))
856 if expected_result is None:
857 self.assertNotIn('auth', api)
858 else:
859 self.assertEqual(api['auth'], expected_result)
860
861 def testFrontEndLimits(self):
862 """Verify that frontendLimits info in the API is written to the config."""
863 rules = [
864 api_config.ApiFrontEndLimitRule(match='foo', qps=234, user_qps=567,
865 daily=8910, analytics_id='asdf'),
866 api_config.ApiFrontEndLimitRule(match='bar', qps=0, user_qps=0,
867 analytics_id='sdf1'),
868 api_config.ApiFrontEndLimitRule()]
869 frontend_limits = api_config.ApiFrontEndLimits(unregistered_user_qps=123,
870 unregistered_qps=456,
871 unregistered_daily=789,
872 rules=rules)
873
874 @api_config.api('root', 'v1', frontend_limits=frontend_limits)
875 class EmptyService(remote.Service):
876 pass
877
878 api = json.loads(self.generator.pretty_print_config_to_json(EmptyService))
879 self.assertIn('frontendLimits', api)
880 self.assertEqual(123, api['frontendLimits'].get('unregisteredUserQps'))
881 self.assertEqual(456, api['frontendLimits'].get('unregisteredQps'))
882 self.assertEqual(789, api['frontendLimits'].get('unregisteredDaily'))
883 self.assertEqual(2, len(api['frontendLimits'].get('rules')))
884 self.assertEqual('foo', api['frontendLimits']['rules'][0]['match'])
885 self.assertEqual(234, api['frontendLimits']['rules'][0]['qps'])
886 self.assertEqual(567, api['frontendLimits']['rules'][0]['userQps'])
887 self.assertEqual(8910, api['frontendLimits']['rules'][0]['daily'])
888 self.assertEqual('asdf', api['frontendLimits']['rules'][0]['analyticsId'])
889 self.assertEqual('bar', api['frontendLimits']['rules'][1]['match'])
890 self.assertEqual(0, api['frontendLimits']['rules'][1]['qps'])
891 self.assertEqual(0, api['frontendLimits']['rules'][1]['userQps'])
892 self.assertNotIn('daily', api['frontendLimits']['rules'][1])
893 self.assertEqual('sdf1', api['frontendLimits']['rules'][1]['analyticsId'])
894
895 def testAllCombinationsRepeatedRequiredDefault(self):
896
897 # TODO(kdeus): When the backwards compatibility for non-ResourceContainer
898 # parameters requests is removed, this class and the
899 # accompanying method should be removed.
900 class AllCombinations(messages.Message):
901 """Documentation for AllCombinations."""
902 string = messages.StringField(1)
903 string_required = messages.StringField(2, required=True)
904 string_default_required = messages.StringField(3, required=True,
905 default='Foo')
906 string_repeated = messages.StringField(4, repeated=True)
907 enum_value = messages.EnumField(SimpleEnum, 5, default=SimpleEnum.VAL2)
908
909 all_combinations_container = resource_container.ResourceContainer(
910 **{field.name: field for field in AllCombinations.all_fields()})
911
912 @api_config.api('root', 'v1', hostname='example.appspot.com')
913 class MySimpleService(remote.Service):
914
915 @api_config.method(AllCombinations, message_types.VoidMessage,
916 path='foo', http_method='GET')
917 def get(self, unused_request):
918 return message_types.VoidMessage()
919
920 @api_config.method(all_combinations_container, message_types.VoidMessage,
921 name='getContainer',
922 path='bar', http_method='GET')
923 def get_container(self, unused_request):
924 return message_types.VoidMessage()
925
926 api = json.loads(self.generator.pretty_print_config_to_json(
927 MySimpleService))
928
929 get_config = {
930 'httpMethod': 'GET',
931 'path': 'foo',
932 'request': {
933 'body': 'empty',
934 'parameterOrder': [
935 'string_required',
936 'string_default_required',
937 ],
938 'parameters': {
939 'enum_value': {
940 'default': 'VAL2',
941 'type': 'string',
942 'enum': {
943 'VAL1': {
944 'backendValue': 'VAL1',
945 },
946 'VAL2': {
947 'backendValue': 'VAL2',
948 },
949 },
950 },
951 'string': {
952 'type': 'string',
953 },
954 'string_default_required': {
955 'default': 'Foo',
956 'required': True,
957 'type': 'string',
958 },
959 'string_repeated': {
960 'type': 'string',
961 'repeated': True,
962 },
963 'string_required': {
964 'required': True,
965 'type': 'string',
966 },
967 },
968 },
969 'response': {
970 'body': 'empty',
971 },
972 'rosyMethod': 'MySimpleService.get',
973 'scopes': ['https://www.googleapis.com/auth/userinfo.email'],
974 'clientIds': [api_config.API_EXPLORER_CLIENT_ID],
975 'authLevel': 'NONE',
976 }
977
978 get_container_config = get_config.copy()
979 get_container_config['path'] = 'bar'
980 get_container_config['rosyMethod'] = 'MySimpleService.get_container'
981 expected = {
982 'root.get': get_config,
983 'root.getContainer': get_container_config
984 }
985
986 test_util.AssertDictEqual(expected, api['methods'], self)
987
988 def testMultipleClassesSingleApi(self):
989 """Test an API that's split into multiple classes."""
990
991 root_api = api_config.api('root', 'v1', hostname='example.appspot.com')
992
993 # First class has a request that reads some arguments.
994 class Response1(messages.Message):
995 string_value = messages.StringField(1)
996
997 @root_api.api_class(resource_name='request')
998 class RequestService(remote.Service):
999
1000 @api_config.method(message_types.VoidMessage, Response1,
1001 path='request_path', http_method='GET')
1002 def my_request(self, unused_request):
1003 pass
1004
1005 # Second class, no methods.
1006 @root_api.api_class(resource_name='empty')
1007 class EmptyService(remote.Service):
1008 pass
1009
1010 # Third class (& data), one method that returns a response.
1011 class Response2(messages.Message):
1012 bool_value = messages.BooleanField(1)
1013 int32_value = messages.IntegerField(2)
1014
1015 @root_api.api_class(resource_name='simple')
1016 class MySimpleService(remote.Service):
1017
1018 @api_config.method(message_types.VoidMessage, Response2,
1019 name='entries.get', path='entries')
1020 def EntriesGet(self, request):
1021 pass
1022
1023 # Make sure api info is the same for all classes and all the _ApiInfo
1024 # properties are accessible.
1025 for cls in (RequestService, EmptyService, MySimpleService):
1026 self.assertEqual(cls.api_info.name, 'root')
1027 self.assertEqual(cls.api_info.version, 'v1')
1028 self.assertEqual(cls.api_info.hostname, 'example.appspot.com')
1029 self.assertIsNone(cls.api_info.audiences)
1030 self.assertEqual(cls.api_info.allowed_client_ids,
1031 [api_config.API_EXPLORER_CLIENT_ID])
1032 self.assertEqual(cls.api_info.scopes, [api_config.EMAIL_SCOPE])
1033
1034 # Get the config for the combination of all 3.
1035 api = json.loads(self.generator.pretty_print_config_to_json(
1036 [RequestService, EmptyService, MySimpleService]))
1037 expected = {
1038 'root.request.my_request': {
1039 'httpMethod': 'GET',
1040 'path': 'request_path',
1041 'request': {'body': 'empty'},
1042 'response': {
1043 'body': 'autoTemplate(backendResponse)',
1044 'bodyName': 'resource'},
1045 'rosyMethod': 'RequestService.my_request',
1046 'clientIds': ['292824132082.apps.googleusercontent.com'],
1047 'scopes': ['https://www.googleapis.com/auth/userinfo.email'],
1048 'authLevel': 'NONE',
1049 },
1050 'root.simple.entries.get': {
1051 'httpMethod': 'POST',
1052 'path': 'entries',
1053 'request': {'body': 'empty'},
1054 'response': {
1055 'body': 'autoTemplate(backendResponse)',
1056 'bodyName': 'resource'},
1057 'rosyMethod': 'MySimpleService.EntriesGet',
1058 'clientIds': ['292824132082.apps.googleusercontent.com'],
1059 'scopes': ['https://www.googleapis.com/auth/userinfo.email'],
1060 'authLevel': 'NONE',
1061 },
1062 }
1063 test_util.AssertDictEqual(expected, api['methods'], self)
1064 expected_descriptor = {
1065 'methods': {
1066 'MySimpleService.EntriesGet': {
1067 'response': {
1068 '$ref': (_DESCRIPTOR_PATH_PREFIX +
1069 'ApiConfigTestResponse2')
1070 }
1071 },
1072 'RequestService.my_request': {
1073 'response': {
1074 '$ref': (_DESCRIPTOR_PATH_PREFIX +
1075 'ApiConfigTestResponse1')
1076 }
1077 }
1078 },
1079 'schemas': {
1080 _DESCRIPTOR_PATH_PREFIX + 'ApiConfigTestResponse1': {
1081 'id': _DESCRIPTOR_PATH_PREFIX + 'ApiConfigTestResponse1',
1082 'properties': {
1083 'string_value': {
1084 'type': 'string'
1085 }
1086 },
1087 'type': 'object'
1088 },
1089 _DESCRIPTOR_PATH_PREFIX + 'ApiConfigTestResponse2': {
1090 'id': _DESCRIPTOR_PATH_PREFIX + 'ApiConfigTestResponse2',
1091 'properties': {
1092 'bool_value': {
1093 'type': 'boolean'
1094 },
1095 'int32_value': {
1096 'format': 'int64',
1097 'type': 'string'
1098 }
1099 },
1100 'type': 'object'
1101 }
1102 }
1103 }
1104
1105 test_util.AssertDictEqual(expected_descriptor, api['descriptor'], self)
1106
1107 def testMultipleClassesDifferentDecoratorInstance(self):
1108 """Test that using different instances of @api fails."""
1109
1110 root_api1 = api_config.api('root', 'v1', hostname='example.appspot.com')
1111 root_api2 = api_config.api('root', 'v1', hostname='example.appspot.com')
1112
1113 @root_api1.api_class()
1114 class EmptyService1(remote.Service):
1115 pass
1116
1117 @root_api2.api_class()
1118 class EmptyService2(remote.Service):
1119 pass
1120
1121 self.assertRaises(api_exceptions.ApiConfigurationError,
1122 self.generator.pretty_print_config_to_json,
1123 [EmptyService1, EmptyService2])
1124
1125 def testMultipleClassesUsingSingleApiDecorator(self):
1126 """Test an API that's split into multiple classes using @api."""
1127
1128 @api_config.api('api', 'v1')
1129 class EmptyService1(remote.Service):
1130 pass
1131
1132 @api_config.api('api', 'v1')
1133 class EmptyService2(remote.Service):
1134 pass
1135
1136 self.assertRaises(api_exceptions.ApiConfigurationError,
1137 self.generator.pretty_print_config_to_json,
1138 [EmptyService1, EmptyService2])
1139
1140 def testMultipleClassesRepeatedResourceName(self):
1141 """Test a multiclass API that reuses a resource_name."""
1142
1143 root_api = api_config.api('root', 'v1', hostname='example.appspot.com')
1144
1145 @root_api.api_class(resource_name='repeated')
1146 class Service1(remote.Service):
1147
1148 @api_config.method(message_types.VoidMessage, message_types.VoidMessage,
1149 name='get', http_method='GET', path='get')
1150 def get(self, request):
1151 pass
1152
1153 @root_api.api_class(resource_name='repeated')
1154 class Service2(remote.Service):
1155
1156 @api_config.method(message_types.VoidMessage, message_types.VoidMessage,
1157 name='list', http_method='GET', path='list')
1158 def list(self, request):
1159 pass
1160
1161 api = json.loads(self.generator.pretty_print_config_to_json(
1162 [Service1, Service2]))
1163 expected = {
1164 'root.repeated.get': {
1165 'httpMethod': 'GET',
1166 'path': 'get',
1167 'request': {'body': 'empty'},
1168 'response': {'body': 'empty'},
1169 'rosyMethod': 'Service1.get',
1170 'clientIds': [api_config.API_EXPLORER_CLIENT_ID],
1171 'scopes': ['https://www.googleapis.com/auth/userinfo.email'],
1172 'authLevel': 'NONE',
1173 },
1174 'root.repeated.list': {
1175 'httpMethod': 'GET',
1176 'path': 'list',
1177 'request': {'body': 'empty'},
1178 'response': {'body': 'empty'},
1179 'rosyMethod': 'Service2.list',
1180 'clientIds': [api_config.API_EXPLORER_CLIENT_ID],
1181 'scopes': ['https://www.googleapis.com/auth/userinfo.email'],
1182 'authLevel': 'NONE',
1183 },
1184 }
1185 test_util.AssertDictEqual(expected, api['methods'], self)
1186
1187 def testMultipleClassesRepeatedMethodName(self):
1188 """Test a multiclass API that reuses a method name."""
1189
1190 root_api = api_config.api('root', 'v1', hostname='example.appspot.com')
1191
1192 @root_api.api_class(resource_name='repeated')
1193 class Service1(remote.Service):
1194
1195 @api_config.method(message_types.VoidMessage, message_types.VoidMessage,
1196 name='get', http_method='GET')
1197 def get(self, request):
1198 pass
1199
1200 @root_api.api_class(resource_name='repeated')
1201 class Service2(remote.Service):
1202
1203 @api_config.method(message_types.VoidMessage, message_types.VoidMessage,
1204 name='get', http_method='POST')
1205 def get(self, request):
1206 pass
1207
1208 self.assertRaises(api_exceptions.ApiConfigurationError,
1209 self.generator.pretty_print_config_to_json,
1210 [Service1, Service2])
1211
1212 def testRepeatedRestPathAndHttpMethod(self):
1213 """If the same HTTP method & path are reused, that should raise an error."""
1214
1215 @api_config.api(name='root', version='v1', hostname='example.appspot.com')
1216 class MySimpleService(remote.Service):
1217
1218 @api_config.method(message_types.VoidMessage, message_types.VoidMessage,
1219 path='path', http_method='GET')
1220 def Path1(self, unused_request):
1221 return message_types.VoidMessage()
1222
1223 @api_config.method(message_types.VoidMessage, message_types.VoidMessage,
1224 path='path', http_method='GET')
1225 def Path2(self, unused_request):
1226 return message_types.VoidMessage()
1227
1228 self.assertRaises(api_exceptions.ApiConfigurationError,
1229 self.generator.pretty_print_config_to_json,
1230 MySimpleService)
1231
1232 def testMulticlassRepeatedRestPathAndHttpMethod(self):
1233 """If the same HTTP method & path are reused, that should raise an error."""
1234
1235 root_api = api_config.api('root', 'v1', hostname='example.appspot.com')
1236
1237 @root_api.api_class(resource_name='resource1')
1238 class Service1(remote.Service):
1239
1240 @api_config.method(message_types.VoidMessage, message_types.VoidMessage,
1241 path='path', http_method='GET')
1242 def Path1(self, unused_request):
1243 return message_types.VoidMessage()
1244
1245 @root_api.api_class(resource_name='resource2')
1246 class Service2(remote.Service):
1247
1248 @api_config.method(message_types.VoidMessage, message_types.VoidMessage,
1249 path='path', http_method='GET')
1250 def Path2(self, unused_request):
1251 return message_types.VoidMessage()
1252
1253 self.assertRaises(api_exceptions.ApiConfigurationError,
1254 self.generator.pretty_print_config_to_json,
1255 [Service1, Service2])
1256
1257 def testRepeatedRpcMethodName(self):
1258 """Test an API that reuses the same RPC name for two methods."""
1259
1260 @api_config.api('root', 'v1', hostname='example.appspot.com')
1261 class MyService(remote.Service):
1262
1263 @api_config.method(message_types.VoidMessage, message_types.VoidMessage,
1264 name='get', http_method='GET', path='path1')
1265 def get(self, request):
1266 pass
1267
1268 @api_config.method(message_types.VoidMessage, message_types.VoidMessage,
1269 name='get', http_method='GET', path='path2')
1270 def another_get(self, request):
1271 pass
1272
1273 self.assertRaises(api_exceptions.ApiConfigurationError,
1274 self.generator.pretty_print_config_to_json, [MyService])
1275
1276 def testMultipleClassesRepeatedMethodNameUniqueResource(self):
1277 """Test a multiclass API reusing a method name but different resource."""
1278
1279 root_api = api_config.api('root', 'v1', hostname='example.appspot.com')
1280
1281 @root_api.api_class(resource_name='resource1')
1282 class Service1(remote.Service):
1283
1284 @api_config.method(message_types.VoidMessage, message_types.VoidMessage,
1285 name='get', http_method='GET', path='get1')
1286 def get(self, request):
1287 pass
1288
1289 @root_api.api_class(resource_name='resource2')
1290 class Service2(remote.Service):
1291
1292 @api_config.method(message_types.VoidMessage, message_types.VoidMessage,
1293 name='get', http_method='GET', path='get2')
1294 def get(self, request):
1295 pass
1296
1297 api = json.loads(self.generator.pretty_print_config_to_json(
1298 [Service1, Service2]))
1299 expected = {
1300 'root.resource1.get': {
1301 'httpMethod': 'GET',
1302 'path': 'get1',
1303 'request': {'body': 'empty'},
1304 'response': {'body': 'empty'},
1305 'rosyMethod': 'Service1.get',
1306 'clientIds': [api_config.API_EXPLORER_CLIENT_ID],
1307 'scopes': ['https://www.googleapis.com/auth/userinfo.email'],
1308 'authLevel': 'NONE',
1309 },
1310 'root.resource2.get': {
1311 'httpMethod': 'GET',
1312 'path': 'get2',
1313 'request': {'body': 'empty'},
1314 'response': {'body': 'empty'},
1315 'rosyMethod': 'Service2.get',
1316 'clientIds': [api_config.API_EXPLORER_CLIENT_ID],
1317 'scopes': ['https://www.googleapis.com/auth/userinfo.email'],
1318 'authLevel': 'NONE',
1319 },
1320 }
1321 test_util.AssertDictEqual(expected, api['methods'], self)
1322
1323 def testMultipleClassesRepeatedMethodNameUniqueResourceParams(self):
1324 """Test the same method name with different args in different resources."""
1325
1326 root_api = api_config.api('root', 'v1', hostname='example.appspot.com')
1327
1328 class Request1(messages.Message):
1329 bool_value = messages.BooleanField(1)
1330
1331 class Response1(messages.Message):
1332 bool_value = messages.BooleanField(1)
1333
1334 class Request2(messages.Message):
1335 bool_value = messages.BooleanField(1)
1336
1337 class Response2(messages.Message):
1338 bool_value = messages.BooleanField(1)
1339
1340 @root_api.api_class(resource_name='resource1')
1341 class Service1(remote.Service):
1342
1343 @api_config.method(Request1, Response1,
1344 name='get', http_method='GET', path='get1')
1345 def get(self, request):
1346 pass
1347
1348 @root_api.api_class(resource_name='resource2')
1349 class Service2(remote.Service):
1350
1351 @api_config.method(Request2, Response2,
1352 name='get', http_method='GET', path='get2')
1353 def get(self, request):
1354 pass
1355
1356 api = json.loads(self.generator.pretty_print_config_to_json(
1357 [Service1, Service2]))
1358 expected = {
1359 'root.resource1.get': {
1360 'httpMethod': 'GET',
1361 'path': 'get1',
1362 'request': {
1363 'body': 'empty',
1364 'parameters': {
1365 'bool_value': {
1366 'type': 'boolean'
1367 }
1368 }
1369 },
1370 'response': {'body': 'autoTemplate(backendResponse)',
1371 'bodyName': 'resource'},
1372 'rosyMethod': 'Service1.get',
1373 'clientIds': [api_config.API_EXPLORER_CLIENT_ID],
1374 'scopes': ['https://www.googleapis.com/auth/userinfo.email'],
1375 'authLevel': 'NONE',
1376 },
1377 'root.resource2.get': {
1378 'httpMethod': 'GET',
1379 'path': 'get2',
1380 'request': {
1381 'body': 'empty',
1382 'parameters': {
1383 'bool_value': {
1384 'type': 'boolean'
1385 }
1386 }
1387 },
1388 'response': {'body': 'autoTemplate(backendResponse)',
1389 'bodyName': 'resource'},
1390 'rosyMethod': 'Service2.get',
1391 'clientIds': [api_config.API_EXPLORER_CLIENT_ID],
1392 'scopes': ['https://www.googleapis.com/auth/userinfo.email'],
1393 'authLevel': 'NONE',
1394 },
1395 }
1396 test_util.AssertDictEqual(expected, api['methods'], self)
1397
1398 expected_descriptor = {
1399 'methods': {
1400 'Service1.get': {
1401 'response': {
1402 '$ref': (_DESCRIPTOR_PATH_PREFIX +
1403 'ApiConfigTestResponse1')
1404 }
1405 },
1406 'Service2.get': {
1407 'response': {
1408 '$ref': (_DESCRIPTOR_PATH_PREFIX +
1409 'ApiConfigTestResponse2')
1410 }
1411 }
1412 },
1413 'schemas': {
1414 _DESCRIPTOR_PATH_PREFIX + 'ApiConfigTestResponse1': {
1415 'id': _DESCRIPTOR_PATH_PREFIX + 'ApiConfigTestResponse1',
1416 'properties': {
1417 'bool_value': {
1418 'type': 'boolean'
1419 }
1420 },
1421 'type': 'object'
1422 },
1423 _DESCRIPTOR_PATH_PREFIX + 'ApiConfigTestResponse2': {
1424 'id': _DESCRIPTOR_PATH_PREFIX + 'ApiConfigTestResponse2',
1425 'properties': {
1426 'bool_value': {
1427 'type': 'boolean'
1428 }
1429 },
1430 'type': 'object'
1431 }
1432 }
1433 }
1434
1435 test_util.AssertDictEqual(expected_descriptor, api['descriptor'], self)
1436
1437 def testMultipleClassesNoResourceName(self):
1438 """Test a multiclass API with a collection with no resource_name."""
1439
1440 root_api = api_config.api('root', 'v1', hostname='example.appspot.com')
1441
1442 @root_api.api_class()
1443 class TestService(remote.Service):
1444
1445 @api_config.method(http_method='GET')
1446 def donothing(self):
1447 pass
1448
1449 @api_config.method(http_method='POST', name='alternate')
1450 def foo(self):
1451 pass
1452
1453 api = json.loads(self.generator.pretty_print_config_to_json(
1454 [TestService]))
1455 expected = {
1456 'root.donothing': {
1457 'httpMethod': 'GET',
1458 'path': 'donothing',
1459 'request': {'body': 'empty'},
1460 'response': {'body': 'empty'},
1461 'rosyMethod': 'TestService.donothing',
1462 'clientIds': ['292824132082.apps.googleusercontent.com'],
1463 'scopes': ['https://www.googleapis.com/auth/userinfo.email'],
1464 'authLevel': 'NONE',
1465 },
1466 'root.alternate': {
1467 'httpMethod': 'POST',
1468 'path': 'foo',
1469 'request': {'body': 'empty'},
1470 'response': {'body': 'empty'},
1471 'rosyMethod': 'TestService.foo',
1472 'clientIds': ['292824132082.apps.googleusercontent.com'],
1473 'scopes': ['https://www.googleapis.com/auth/userinfo.email'],
1474 'authLevel': 'NONE',
1475 },
1476 }
1477 test_util.AssertDictEqual(expected, api['methods'], self)
1478
1479 def testMultipleClassesBasePathInteraction(self):
1480 """Test path appending in a multiclass API."""
1481
1482 root_api = api_config.api('root', 'v1', hostname='example.appspot.com')
1483
1484 @root_api.api_class(path='base_path')
1485 class TestService(remote.Service):
1486
1487 @api_config.method(http_method='GET')
1488 def at_base(self):
1489 pass
1490
1491 @api_config.method(http_method='GET', path='appended')
1492 def append_to_base(self):
1493 pass
1494
1495 @api_config.method(http_method='GET', path='appended/more')
1496 def append_to_base2(self):
1497 pass
1498
1499 @api_config.method(http_method='GET', path='/ignore_base')
1500 def absolute(self):
1501 pass
1502
1503 api = json.loads(self.generator.pretty_print_config_to_json(
1504 [TestService]))
1505 expected = {
1506 'root.at_base': {
1507 'httpMethod': 'GET',
1508 'path': 'base_path/at_base',
1509 'request': {'body': 'empty'},
1510 'response': {'body': 'empty'},
1511 'rosyMethod': 'TestService.at_base',
1512 'clientIds': ['292824132082.apps.googleusercontent.com'],
1513 'scopes': ['https://www.googleapis.com/auth/userinfo.email'],
1514 'authLevel': 'NONE',
1515 },
1516 'root.append_to_base': {
1517 'httpMethod': 'GET',
1518 'path': 'base_path/appended',
1519 'request': {'body': 'empty'},
1520 'response': {'body': 'empty'},
1521 'rosyMethod': 'TestService.append_to_base',
1522 'clientIds': ['292824132082.apps.googleusercontent.com'],
1523 'scopes': ['https://www.googleapis.com/auth/userinfo.email'],
1524 'authLevel': 'NONE',
1525 },
1526 'root.append_to_base2': {
1527 'httpMethod': 'GET',
1528 'path': 'base_path/appended/more',
1529 'request': {'body': 'empty'},
1530 'response': {'body': 'empty'},
1531 'rosyMethod': 'TestService.append_to_base2',
1532 'clientIds': ['292824132082.apps.googleusercontent.com'],
1533 'scopes': ['https://www.googleapis.com/auth/userinfo.email'],
1534 'authLevel': 'NONE',
1535 },
1536 'root.absolute': {
1537 'httpMethod': 'GET',
1538 'path': 'ignore_base',
1539 'request': {'body': 'empty'},
1540 'response': {'body': 'empty'},
1541 'rosyMethod': 'TestService.absolute',
1542 'clientIds': ['292824132082.apps.googleusercontent.com'],
1543 'scopes': ['https://www.googleapis.com/auth/userinfo.email'],
1544 'authLevel': 'NONE',
1545 },
1546 }
1547 test_util.AssertDictEqual(expected, api['methods'], self)
1548
1549 def testMultipleClassesDifferentCollectionDefaults(self):
1550 """Test a multi-class API with settings overridden per collection."""
1551
1552 BASE_SCOPES = ['base_scope']
1553 BASE_CLIENT_IDS = ['base_client_id']
1554 root_api = api_config.api('root', 'v1', hostname='example.appspot.com',
1555 audiences=['base_audience'],
1556 scopes=BASE_SCOPES,
1557 allowed_client_ids=BASE_CLIENT_IDS,
1558 auth_level=AUTH_LEVEL.REQUIRED)
1559
1560 @root_api.api_class(resource_name='one', audiences=[])
1561 class Service1(remote.Service):
1562 pass
1563
1564 @root_api.api_class(resource_name='two', audiences=['audience2', 'foo'],
1565 scopes=['service2_scope'],
1566 allowed_client_ids=['s2_client_id'],
1567 auth_level=AUTH_LEVEL.OPTIONAL)
1568 class Service2(remote.Service):
1569 pass
1570
1571 self.assertEqual(Service1.api_info.audiences, [])
1572 self.assertEqual(Service1.api_info.scopes, BASE_SCOPES)
1573 self.assertEqual(Service1.api_info.allowed_client_ids, BASE_CLIENT_IDS)
1574 self.assertEqual(Service1.api_info.auth_level, AUTH_LEVEL.REQUIRED)
1575 self.assertEqual(Service2.api_info.audiences, ['audience2', 'foo'])
1576 self.assertEqual(Service2.api_info.scopes, ['service2_scope'])
1577 self.assertEqual(Service2.api_info.allowed_client_ids, ['s2_client_id'])
1578 self.assertEqual(Service2.api_info.auth_level, AUTH_LEVEL.OPTIONAL)
1579
1580 def testResourceContainerWarning(self):
1581 """Check the warning if a ResourceContainer isn't used when it should be."""
1582
1583 class TestGetRequest(messages.Message):
1584 item_id = messages.StringField(1)
1585
1586 @api_config.api('myapi', 'v0', hostname='example.appspot.com')
1587 class MyApi(remote.Service):
1588
1589 @api_config.method(TestGetRequest, message_types.VoidMessage,
1590 path='test/{item_id}')
1591 def Test(self, unused_request):
1592 return message_types.VoidMessage()
1593
1594 # Verify that there's a warning and the name of the method is included
1595 # in the warning.
1596 logging.warning = mock.Mock()
1597 self.generator.pretty_print_config_to_json(MyApi)
1598 logging.warning.assert_called_with(mock.ANY, 'myapi.test')
1599
1600 def testFieldInPathWithBodyIsRequired(self):
1601
1602 # TODO(kdeus): When the backwards compatibility for non-ResourceContainer
1603 # parameters requests is removed, this class and the
1604 # accompanying method should be removed.
1605 class ItemsUpdateRequest(messages.Message):
1606 itemId = messages.StringField(1)
1607
1608 items_update_request_container = resource_container.ResourceContainer(
1609 **{field.name: field for field in ItemsUpdateRequest.all_fields()})
1610
1611 @api_config.api(name='root', hostname='example.appspot.com', version='v1')
1612 class MyService(remote.Service):
1613 """Describes MyService."""
1614
1615 @api_config.method(ItemsUpdateRequest, message_types.VoidMessage,
1616 path='items/{itemId}', name='items.update',
1617 http_method='PUT')
1618 def items_update(self, unused_request):
1619 return message_types.VoidMessage()
1620
1621 @api_config.method(items_update_request_container,
1622 path='items/container/{itemId}',
1623 name='items.updateContainer',
1624 http_method='PUT')
1625 def items_update_container(self, unused_request):
1626 return message_types.VoidMessage()
1627
1628 api = json.loads(self.generator.pretty_print_config_to_json(MyService))
1629 params = {'itemId': {'required': True,
1630 'type': 'string'}}
1631 param_order = ['itemId']
1632 items_update_config = {
1633 'httpMethod': 'PUT',
1634 'path': 'items/{itemId}',
1635 'request': {'body': 'autoTemplate(backendRequest)',
1636 'bodyName': 'resource',
1637 'parameters': params,
1638 'parameterOrder': param_order},
1639 'response': {'body': 'empty'},
1640 'rosyMethod': 'MyService.items_update',
1641 'scopes': ['https://www.googleapis.com/auth/userinfo.email'],
1642 'clientIds': [api_config.API_EXPLORER_CLIENT_ID],
1643 'authLevel': 'NONE',
1644 }
1645
1646 update_container_cfg = items_update_config.copy()
1647 update_container_cfg['path'] = 'items/container/{itemId}'
1648 update_container_cfg['rosyMethod'] = 'MyService.items_update_container'
1649 # Since we don't have a body in our container, the request will be empty.
1650 request = update_container_cfg['request'].copy()
1651 request.pop('bodyName')
1652 request['body'] = 'empty'
1653 update_container_cfg['request'] = request
1654 expected = {
1655 'root.items.update': items_update_config,
1656 'root.items.updateContainer': update_container_cfg,
1657 }
1658 test_util.AssertDictEqual(expected, api['methods'], self)
1659
1660 def testFieldInPathNoBodyIsRequired(self):
1661
1662 class ItemsGetRequest(messages.Message):
1663 itemId = messages.StringField(1)
1664
1665 @api_config.api(name='root', hostname='example.appspot.com', version='v1')
1666 class MyService(remote.Service):
1667 """Describes MyService."""
1668
1669 @api_config.method(ItemsGetRequest, message_types.VoidMessage,
1670 path='items/{itemId}', name='items.get',
1671 http_method='GET')
1672 def items_get(self, unused_request):
1673 return message_types.VoidMessage()
1674
1675 api = json.loads(self.generator.pretty_print_config_to_json(MyService))
1676 params = {'itemId': {'required': True,
1677 'type': 'string'}}
1678 param_order = ['itemId']
1679 expected = {
1680 'root.items.get': {
1681 'httpMethod': 'GET',
1682 'path': 'items/{itemId}',
1683 'request': {'body': 'empty',
1684 'parameters': params,
1685 'parameterOrder': param_order},
1686 'response': {'body': 'empty'},
1687 'rosyMethod': 'MyService.items_get',
1688 'scopes': ['https://www.googleapis.com/auth/userinfo.email'],
1689 'clientIds': [api_config.API_EXPLORER_CLIENT_ID],
1690 'authLevel': 'NONE',
1691 }
1692 }
1693
1694 test_util.AssertDictEqual(expected, api['methods'], self)
1695
1696 def testAuthLevelRequired(self):
1697
1698 class ItemsGetRequest(messages.Message):
1699 itemId = messages.StringField(1)
1700
1701 @api_config.api(name='root', hostname='example.appspot.com', version='v1')
1702 class MyService(remote.Service):
1703 """Describes MyService."""
1704
1705 @api_config.method(ItemsGetRequest, message_types.VoidMessage,
1706 path='items/{itemId}', name='items.get',
1707 http_method='GET', auth_level=AUTH_LEVEL.REQUIRED)
1708 def items_get(self, unused_request):
1709 return message_types.VoidMessage()
1710
1711 api = json.loads(self.generator.pretty_print_config_to_json(MyService))
1712 params = {'itemId': {'required': True,
1713 'type': 'string'}}
1714 param_order = ['itemId']
1715 expected = {
1716 'root.items.get': {
1717 'httpMethod': 'GET',
1718 'path': 'items/{itemId}',
1719 'request': {'body': 'empty',
1720 'parameters': params,
1721 'parameterOrder': param_order},
1722 'response': {'body': 'empty'},
1723 'rosyMethod': 'MyService.items_get',
1724 'scopes': ['https://www.googleapis.com/auth/userinfo.email'],
1725 'clientIds': [api_config.API_EXPLORER_CLIENT_ID],
1726 'authLevel': 'REQUIRED',
1727 }
1728 }
1729
1730 test_util.AssertDictEqual(expected, api['methods'], self)
1731
1732 def testCustomUrl(self):
1733
1734 test_request = resource_container.ResourceContainer(
1735 message_types.VoidMessage,
1736 id=messages.IntegerField(1, required=True))
1737
1738 @api_config.api(name='testapicustomurl', version='v3',
1739 hostname='example.appspot.com',
1740 description='A wonderful API.', base_path='/my/base/path/')
1741 class TestServiceCustomUrl(remote.Service):
1742
1743 @api_config.method(test_request,
1744 message_types.VoidMessage,
1745 http_method='DELETE', path='items/{id}')
1746 # Silence lint warning about method naming conventions
1747 # pylint: disable=g-bad-name
1748 def delete(self, unused_request):
1749 return message_types.VoidMessage()
1750
1751 api = json.loads(
1752 self.generator.pretty_print_config_to_json(TestServiceCustomUrl))
1753
1754 expected_adapter = {
1755 'bns': 'https://example.appspot.com/my/base/path',
1756 'type': 'lily',
1757 'deadline': 10.0
1758 }
1759
1760 test_util.AssertDictEqual(expected_adapter, api['adapter'], self)
1761
1762
1763 class ApiConfigParamsDescriptorTest(unittest.TestCase):
1764
1765 def setUp(self):
1766 self.generator = ApiConfigGenerator()
1767
1768 class OtherRefClass(messages.Message):
1769 three = messages.BooleanField(1, repeated=True)
1770 four = messages.FloatField(2, required=True)
1771 five = messages.IntegerField(3, default=42)
1772 self.other_ref_class = OtherRefClass
1773
1774 class RefClass(messages.Message):
1775 one = messages.StringField(1)
1776 two = messages.MessageField(OtherRefClass, 2)
1777 not_two = messages.MessageField(OtherRefClass, 3, required=True)
1778 self.ref_class = RefClass
1779
1780 class RefClassForContainer(messages.Message):
1781 not_two = messages.MessageField(OtherRefClass, 3, required=True)
1782
1783 ref_class_container = resource_container.ResourceContainer(
1784 RefClassForContainer,
1785 one=messages.StringField(1),
1786 two=messages.MessageField(OtherRefClass, 2))
1787
1788 @api_config.api(name='root', hostname='example.appspot.com', version='v1')
1789 class MyService(remote.Service):
1790
1791 @api_config.method(RefClass, RefClass,
1792 name='entries.get',
1793 path='/a/{two.three}/{two.four}',
1794 http_method='GET')
1795 def entries_get(self, request):
1796 return request
1797
1798 @api_config.method(RefClass, RefClass,
1799 name='entries.put',
1800 path='/b/{two.three}/{one}',
1801 http_method='PUT')
1802 def entries_put(self, request):
1803 return request
1804
1805 # Flatten the fields intended for the put request into only parameters.
1806 # This would not be a typical use, but is done to adhere to the behavior
1807 # in the non-ResourceContainer case.
1808 get_request_container = resource_container.ResourceContainer(
1809 **{field.name: field for field in
1810 ref_class_container.combined_message_class.all_fields()})
1811
1812 @api_config.method(get_request_container, RefClass,
1813 name='entries.getContainer',
1814 path='/a/container/{two.three}/{two.four}',
1815 http_method='GET')
1816 def entries_get_container(self, request):
1817 return request
1818
1819 @api_config.method(ref_class_container, RefClass,
1820 name='entries.putContainer',
1821 path='/b/container/{two.three}/{one}',
1822 http_method='PUT')
1823 def entries_put_container(self, request):
1824 return request
1825
1826 self.api_str = self.generator.pretty_print_config_to_json(MyService)
1827 self.api = json.loads(self.api_str)
1828
1829 self.m_field = messages.MessageField(RefClass, 1)
1830 self.m_field.name = 'm_field'
1831
1832 def GetPrivateMethod(self, attr_name):
1833 protected_attr_name = '_ApiConfigGenerator__' + attr_name
1834 return getattr(self.generator, protected_attr_name)
1835
1836 def testFieldToSubfieldsSimpleField(self):
1837 m_field = messages.StringField(1)
1838 expected = [[m_field]]
1839 self.assertItemsEqual(expected,
1840 self.GetPrivateMethod('field_to_subfields')(m_field))
1841
1842 def testFieldToSubfieldsSingleMessageField(self):
1843 class RefClass(messages.Message):
1844 one = messages.StringField(1)
1845 two = messages.IntegerField(2)
1846 m_field = messages.MessageField(RefClass, 1)
1847 expected = [
1848 [m_field, RefClass.one],
1849 [m_field, RefClass.two],
1850 ]
1851 self.assertItemsEqual(expected,
1852 self.GetPrivateMethod('field_to_subfields')(m_field))
1853
1854 def testFieldToSubfieldsDifferingDepth(self):
1855 expected = [
1856 [self.m_field, self.ref_class.one],
1857 [self.m_field, self.ref_class.two, self.other_ref_class.three],
1858 [self.m_field, self.ref_class.two, self.other_ref_class.four],
1859 [self.m_field, self.ref_class.two, self.other_ref_class.five],
1860 [self.m_field, self.ref_class.not_two, self.other_ref_class.three],
1861 [self.m_field, self.ref_class.not_two, self.other_ref_class.four],
1862 [self.m_field, self.ref_class.not_two, self.other_ref_class.five],
1863 ]
1864 self.assertItemsEqual(
1865 expected, self.GetPrivateMethod('field_to_subfields')(self.m_field))
1866
1867 def testGetPathParameters(self):
1868 get_path_parameters = self.GetPrivateMethod('get_path_parameters')
1869 expected = {
1870 'c': ['c'],
1871 'd': ['d.e'],
1872 }
1873 test_util.AssertDictEqual(
1874 expected, get_path_parameters('/a/b/{c}/{d.e}/{}'), self)
1875 test_util.AssertDictEqual(
1876 {}, get_path_parameters('/stray{/brackets{in/the}middle'), self)
1877
1878 def testValidatePathParameters(self):
1879 # This also tests __validate_simple_subfield indirectly
1880 validate_path_parameters = self.GetPrivateMethod('validate_path_parameters')
1881 self.assertRaises(TypeError, validate_path_parameters,
1882 self.m_field, ['x'])
1883 self.assertRaises(TypeError, validate_path_parameters,
1884 self.m_field, ['m_field'])
1885 self.assertRaises(TypeError, validate_path_parameters,
1886 self.m_field, ['m_field.one_typo'])
1887 # This should not fail
1888 validate_path_parameters(self.m_field, ['m_field.one'])
1889
1890 def MethodDescriptorTest(self, method_name, path, param_order, parameters):
1891 method_descriptor = self.api['methods'][method_name]
1892 self.assertEqual(method_descriptor['path'], path)
1893 request_descriptor = method_descriptor['request']
1894 self.assertEqual(param_order, request_descriptor['parameterOrder'])
1895 self.assertEqual(parameters, request_descriptor['parameters'])
1896
1897 def testParametersDescriptorEntriesGet(self):
1898 parameters = {
1899 'one': {
1900 'type': 'string',
1901 },
1902 'two.three': {
1903 'repeated': True,
1904 'required': True,
1905 'type': 'boolean',
1906 },
1907 'two.four': {
1908 'required': True,
1909 'type': 'double',
1910 },
1911 'two.five': {
1912 'default': 42,
1913 'type': 'int64'
1914 },
1915 'not_two.three': {
1916 'repeated': True,
1917 'type': 'boolean',
1918 },
1919 'not_two.four': {
1920 'required': True,
1921 'type': 'double',
1922 },
1923 'not_two.five': {
1924 'default': 42,
1925 'type': 'int64'
1926 },
1927 }
1928
1929 # Without container.
1930 self.MethodDescriptorTest('root.entries.get', 'a/{two.three}/{two.four}',
1931 ['two.three', 'two.four', 'not_two.four'],
1932 parameters)
1933 # With container.
1934 self.MethodDescriptorTest('root.entries.getContainer',
1935 'a/container/{two.three}/{two.four}',
1936 # Not parameter order differs because of the way
1937 # combined_message_class combines classes. This
1938 # is not so big a deal.
1939 ['not_two.four', 'two.three', 'two.four'],
1940 parameters)
1941
1942 def testParametersDescriptorEntriesPut(self):
1943 param_order = ['one', 'two.three']
1944 parameters = {
1945 'one': {
1946 'required': True,
1947 'type': 'string',
1948 },
1949 'two.three': {
1950 'repeated': True,
1951 'required': True,
1952 'type': 'boolean',
1953 },
1954 'two.four': {
1955 'type': 'double',
1956 },
1957 'two.five': {
1958 'default': 42,
1959 'type': 'int64'
1960 },
1961 }
1962
1963 # Without container.
1964 self.MethodDescriptorTest('root.entries.put', 'b/{two.three}/{one}',
1965 param_order, parameters)
1966 # With container.
1967 self.MethodDescriptorTest('root.entries.putContainer',
1968 'b/container/{two.three}/{one}',
1969 param_order, parameters)
1970
1971
1972 class ApiDecoratorTest(unittest.TestCase):
1973
1974 def testApiInfoPopulated(self):
1975
1976 @api_config.api(name='CoolService', version='vX',
1977 description='My Cool Service', hostname='myhost.com',
1978 canonical_name='Cool Service Name')
1979 class MyDecoratedService(remote.Service):
1980 """Describes MyDecoratedService."""
1981 pass
1982
1983 api_info = MyDecoratedService.api_info
1984 self.assertEqual('CoolService', api_info.name)
1985 self.assertEqual('vX', api_info.version)
1986 self.assertEqual('My Cool Service', api_info.description)
1987 self.assertEqual('myhost.com', api_info.hostname)
1988 self.assertEqual('Cool Service Name', api_info.canonical_name)
1989 self.assertIsNone(api_info.audiences)
1990 self.assertEqual([api_config.EMAIL_SCOPE], api_info.scopes)
1991 self.assertEqual([api_config.API_EXPLORER_CLIENT_ID],
1992 api_info.allowed_client_ids)
1993 self.assertEqual(AUTH_LEVEL.NONE, api_info.auth_level)
1994 self.assertEqual(None, api_info.resource_name)
1995 self.assertEqual(None, api_info.path)
1996
1997 def testApiInfoDefaults(self):
1998
1999 @api_config.api('CoolService2', 'v2')
2000 class MyDecoratedService(remote.Service):
2001 """Describes MyDecoratedService."""
2002 pass
2003
2004 api_info = MyDecoratedService.api_info
2005 self.assertEqual('CoolService2', api_info.name)
2006 self.assertEqual('v2', api_info.version)
2007 self.assertEqual(None, api_info.description)
2008 self.assertEqual(None, api_info.hostname)
2009 self.assertEqual(None, api_info.canonical_name)
2010 self.assertEqual(None, api_info.title)
2011 self.assertEqual(None, api_info.documentation)
2012
2013 def testGetApiClassesSingle(self):
2014 """Test that get_api_classes works when one class has been decorated."""
2015 my_api = api_config.api(name='My Service', version='v1')
2016
2017 @my_api
2018 class MyDecoratedService(remote.Service):
2019 """Describes MyDecoratedService."""
2020
2021 self.assertEqual([MyDecoratedService], my_api.get_api_classes())
2022
2023 def testGetApiClassesSingleCollection(self):
2024 """Test that get_api_classes works with the collection() decorator."""
2025 my_api = api_config.api(name='My Service', version='v1')
2026
2027 @my_api.api_class(resource_name='foo')
2028 class MyDecoratedService(remote.Service):
2029 """Describes MyDecoratedService."""
2030
2031 self.assertEqual([MyDecoratedService], my_api.get_api_classes())
2032
2033 def testGetApiClassesMultiple(self):
2034 """Test that get_api_classes works with multiple classes."""
2035 my_api = api_config.api(name='My Service', version='v1')
2036
2037 @my_api.api_class(resource_name='foo')
2038 class MyDecoratedService1(remote.Service):
2039 """Describes MyDecoratedService."""
2040
2041 @my_api.api_class(resource_name='bar')
2042 class MyDecoratedService2(remote.Service):
2043 """Describes MyDecoratedService."""
2044
2045 @my_api.api_class(resource_name='baz')
2046 class MyDecoratedService3(remote.Service):
2047 """Describes MyDecoratedService."""
2048
2049 self.assertEqual([MyDecoratedService1, MyDecoratedService2,
2050 MyDecoratedService3], my_api.get_api_classes())
2051
2052 def testGetApiClassesMixedStyles(self):
2053 """Test that get_api_classes works when decorated differently."""
2054 my_api = api_config.api(name='My Service', version='v1')
2055
2056 # @my_api is equivalent to @my_api.api_class(). This is allowed, though
2057 # mixing styles like this shouldn't be encouraged.
2058 @my_api
2059 class MyDecoratedService1(remote.Service):
2060 """Describes MyDecoratedService."""
2061
2062 @my_api
2063 class MyDecoratedService2(remote.Service):
2064 """Describes MyDecoratedService."""
2065
2066 @my_api.api_class(resource_name='foo')
2067 class MyDecoratedService3(remote.Service):
2068 """Describes MyDecoratedService."""
2069
2070 self.assertEqual([MyDecoratedService1, MyDecoratedService2,
2071 MyDecoratedService3], my_api.get_api_classes())
2072
2073
2074 class MethodDecoratorTest(unittest.TestCase):
2075
2076 def testMethodId(self):
2077
2078 @api_config.api('foo', 'v2')
2079 class MyDecoratedService(remote.Service):
2080 """Describes MyDecoratedService."""
2081
2082 @api_config.method()
2083 def get(self):
2084 pass
2085
2086 @api_config.method()
2087 def people(self):
2088 pass
2089
2090 @api_config.method()
2091 def _get(self):
2092 pass
2093
2094 @api_config.method()
2095 def get_(self):
2096 pass
2097
2098 @api_config.method()
2099 def _(self):
2100 pass
2101
2102 @api_config.method()
2103 def _____(self):
2104 pass
2105
2106 @api_config.method()
2107 def people_update(self):
2108 pass
2109
2110 @api_config.method()
2111 def people_search(self):
2112 pass
2113
2114 # pylint: disable=g-bad-name
2115 @api_config.method()
2116 def _several_underscores__in_various___places__(self):
2117 pass
2118
2119 test_cases = [
2120 ('get', 'foo.get'),
2121 ('people', 'foo.people'),
2122 ('_get', 'foo.get'),
2123 ('get_', 'foo.get_'),
2124 ('_', 'foo.'),
2125 ('_____', 'foo.'),
2126 ('people_update', 'foo.people_update'),
2127 ('people_search', 'foo.people_search'),
2128 ('_several_underscores__in_various___places__',
2129 'foo.several_underscores__in_various___places__')
2130 ]
2131
2132 for protorpc_method_name, expected in test_cases:
2133 method_id = ''
2134 info = getattr(MyDecoratedService, protorpc_method_name, None)
2135 self.assertIsNotNone(info)
2136
2137 method_id = info.method_info.method_id(MyDecoratedService.api_info)
2138 self.assertEqual(expected, method_id,
2139 'unexpected result (%s) for: %s' %
2140 (method_id, protorpc_method_name))
2141
2142 def testMethodInfoPopulated(self):
2143
2144 @api_config.api(name='CoolService', version='vX',
2145 description='My Cool Service', hostname='myhost.com')
2146 class MyDecoratedService(remote.Service):
2147 """Describes MyDecoratedService."""
2148
2149 @api_config.method(request_message=Nested,
2150 response_message=AllFields,
2151 name='items.operate',
2152 path='items',
2153 http_method='GET',
2154 scopes=['foo'],
2155 audiences=['bar'],
2156 allowed_client_ids=['baz', 'bim'],
2157 auth_level=AUTH_LEVEL.REQUIRED)
2158 def my_method(self):
2159 pass
2160
2161 method_info = MyDecoratedService.my_method.method_info
2162 protorpc_info = MyDecoratedService.my_method.remote
2163 self.assertEqual(Nested, protorpc_info.request_type)
2164 self.assertEqual(AllFields, protorpc_info.response_type)
2165 self.assertEqual('items.operate', method_info.name)
2166 self.assertEqual('items', method_info.get_path(MyDecoratedService.api_info))
2167 self.assertEqual('GET', method_info.http_method)
2168 self.assertEqual(['foo'], method_info.scopes)
2169 self.assertEqual(['bar'], method_info.audiences)
2170 self.assertEqual(['baz', 'bim'], method_info.allowed_client_ids)
2171 self.assertEqual(AUTH_LEVEL.REQUIRED, method_info.auth_level)
2172
2173 def testMethodInfoDefaults(self):
2174
2175 @api_config.api('CoolService2', 'v2')
2176 class MyDecoratedService(remote.Service):
2177 """Describes MyDecoratedService."""
2178
2179 @api_config.method()
2180 def my_method(self):
2181 pass
2182
2183 method_info = MyDecoratedService.my_method.method_info
2184 protorpc_info = MyDecoratedService.my_method.remote
2185 self.assertEqual(message_types.VoidMessage, protorpc_info.request_type)
2186 self.assertEqual(message_types.VoidMessage, protorpc_info.response_type)
2187 self.assertEqual('my_method', method_info.name)
2188 self.assertEqual('my_method',
2189 method_info.get_path(MyDecoratedService.api_info))
2190 self.assertEqual('POST', method_info.http_method)
2191 self.assertEqual(None, method_info.scopes)
2192 self.assertEqual(None, method_info.audiences)
2193 self.assertEqual(None, method_info.allowed_client_ids)
2194 self.assertEqual(None, method_info.auth_level)
2195
2196 def testMethodInfoPath(self):
2197
2198 class MyRequest(messages.Message):
2199 """Documentation for MyRequest."""
2200 zebra = messages.StringField(1, required=True)
2201 kitten = messages.StringField(2, required=True)
2202 dog = messages.StringField(3)
2203 panda = messages.StringField(4, required=True)
2204
2205 @api_config.api('CoolService3', 'v3')
2206 class MyDecoratedService(remote.Service):
2207 """Describes MyDecoratedService."""
2208
2209 @api_config.method(MyRequest, message_types.VoidMessage)
2210 def default_path_method(self):
2211 pass
2212
2213 @api_config.method(MyRequest, message_types.VoidMessage,
2214 path='zebras/{zebra}/pandas/{panda}/kittens/{kitten}')
2215 def specified_path_method(self):
2216 pass
2217
2218 specified_path_info = MyDecoratedService.specified_path_method.method_info
2219 specified_protorpc_info = MyDecoratedService.specified_path_method.remote
2220 self.assertEqual(MyRequest, specified_protorpc_info.request_type)
2221 self.assertEqual(message_types.VoidMessage,
2222 specified_protorpc_info.response_type)
2223 self.assertEqual('specified_path_method', specified_path_info.name)
2224 self.assertEqual('zebras/{zebra}/pandas/{panda}/kittens/{kitten}',
2225 specified_path_info.get_path(MyDecoratedService.api_info))
2226 self.assertEqual('POST', specified_path_info.http_method)
2227 self.assertEqual(None, specified_path_info.scopes)
2228 self.assertEqual(None, specified_path_info.audiences)
2229 self.assertEqual(None, specified_path_info.allowed_client_ids)
2230 self.assertEqual(None, specified_path_info.auth_level)
2231
2232 default_path_info = MyDecoratedService.default_path_method.method_info
2233 default_protorpc_info = MyDecoratedService.default_path_method.remote
2234 self.assertEqual(MyRequest, default_protorpc_info.request_type)
2235 self.assertEqual(message_types.VoidMessage,
2236 default_protorpc_info.response_type)
2237 self.assertEqual('default_path_method', default_path_info.name)
2238 self.assertEqual('default_path_method',
2239 default_path_info.get_path(MyDecoratedService.api_info))
2240 self.assertEqual('POST', default_path_info.http_method)
2241 self.assertEqual(None, default_path_info.scopes)
2242 self.assertEqual(None, default_path_info.audiences)
2243 self.assertEqual(None, default_path_info.allowed_client_ids)
2244 self.assertEqual(None, specified_path_info.auth_level)
2245
2246 def testInvalidPaths(self):
2247 for path in ('invalid/mixed{param}',
2248 'invalid/{param}mixed',
2249 'invalid/mixed{param}mixed',
2250 'invalid/{extra}{vars}',
2251 'invalid/{}/emptyvar'):
2252
2253 @api_config.api('root', 'v1')
2254 class MyDecoratedService(remote.Service):
2255 """Describes MyDecoratedService."""
2256
2257 @api_config.method(message_types.VoidMessage, message_types.VoidMessage,
2258 path=path)
2259 def test(self):
2260 pass
2261
2262 self.assertRaises(api_exceptions.ApiConfigurationError,
2263 MyDecoratedService.test.method_info.get_path,
2264 MyDecoratedService.api_info)
2265
2266 def testMethodAttributeInheritance(self):
2267 """Test descriptor attributes that can be inherited from the main config."""
2268 self.TryListAttributeVariations('audiences', 'audiences', None)
2269 self.TryListAttributeVariations(
2270 'scopes', 'scopes',
2271 ['https://www.googleapis.com/auth/userinfo.email'])
2272 self.TryListAttributeVariations('allowed_client_ids', 'clientIds',
2273 [api_config.API_EXPLORER_CLIENT_ID])
2274
2275 def TryListAttributeVariations(self, attribute_name, config_name,
2276 default_expected):
2277 """Test setting an attribute in the API config and method configs.
2278
2279 The audiences, scopes and allowed_client_ids settings can be set
2280 in either the main API config or on each of the methods. This helper
2281 function tests each variation of one of these (whichever is specified)
2282 and ensures that the api config has the right values.
2283
2284 Args:
2285 attribute_name: Name of the keyword arg to pass to the api or method
2286 decorator. Also the name of the attribute used to access that
2287 variable on api_info or method_info.
2288 config_name: Name of the variable as it appears in the configuration
2289 output.
2290 default_expected: The default expected value if the attribute isn't
2291 specified on either the api or the method.
2292 """
2293
2294 # Try the various combinations of api-level and method-level settings.
2295 # Test cases are: (api-setting, method-setting, expected)
2296 test_cases = ((None, ['foo', 'bar'], ['foo', 'bar']),
2297 (None, [], None),
2298 (['foo', 'bar'], None, ['foo', 'bar']),
2299 (['foo', 'bar'], ['foo', 'bar'], ['foo', 'bar']),
2300 (['foo', 'bar'], ['foo', 'baz'], ['foo', 'baz']),
2301 (['foo', 'bar'], [], None),
2302 (['foo', 'bar'], ['abc'], ['abc']),
2303 (None, None, default_expected))
2304 for api_value, method_value, expected_value in test_cases:
2305 api_kwargs = {attribute_name: api_value}
2306 method_kwargs = {attribute_name: method_value}
2307
2308 @api_config.api('AuthService', 'v1', hostname='example.appspot.com',
2309 **api_kwargs)
2310 class AuthServiceImpl(remote.Service):
2311 """Describes AuthServiceImpl."""
2312
2313 @api_config.method(**method_kwargs)
2314 def baz(self):
2315 pass
2316
2317 self.assertEqual(api_value if api_value is not None else default_expected,
2318 getattr(AuthServiceImpl.api_info, attribute_name))
2319 self.assertEqual(method_value,
2320 getattr(AuthServiceImpl.baz.method_info, attribute_name))
2321
2322 generator = ApiConfigGenerator()
2323 api = json.loads(generator.pretty_print_config_to_json(AuthServiceImpl))
2324 expected = {
2325 'authService.baz': {
2326 'httpMethod': 'POST',
2327 'path': 'baz',
2328 'request': {'body': 'empty'},
2329 'response': {'body': 'empty'},
2330 'rosyMethod': 'AuthServiceImpl.baz',
2331 'scopes': ['https://www.googleapis.com/auth/userinfo.email'],
2332 'clientIds': [api_config.API_EXPLORER_CLIENT_ID],
2333 'authLevel': 'NONE'
2334 }
2335 }
2336 if expected_value:
2337 expected['authService.baz'][config_name] = expected_value
2338 elif config_name in expected['authService.baz']:
2339 del expected['authService.baz'][config_name]
2340
2341 test_util.AssertDictEqual(expected, api['methods'], self)
2342
2343
2344 if __name__ == '__main__':
2345 unittest.main()
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698