OLD | NEW |
(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() |
OLD | NEW |