blob: 577d070b8f9bf3f268c0a6cb159ac68a2ad37338 [file] [log] [blame]
#
# Copyright 2016 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import json
import os
from unittest import TestCase
from mock import Mock
from chameleon.protoc_plugins.descriptor_parser import DescriptorParser
from chameleon.protoc_plugins.swagger_template \
import native_descriptors_to_swagger, DuplicateMethodAndPathError, \
ProtobufCompilationFailedError, InvalidPathArgumentError
from tests.utests.chameleon.protoc_plugins.test_utils import unindent, \
json_rt, generate_plugin_request, load_file
class SwaggerTemplateTests(TestCase):
maxDiff = 10000
def gen_swagger(self, proto):
request = generate_plugin_request(proto)
parser = DescriptorParser()
native_data = parser.parse_file_descriptors(request.proto_file,
type_tag_name='_type',
fold_comments=True)
swagger = native_descriptors_to_swagger(native_data)
return swagger
def test_swagger_url(self):
grpc = {
'_channel': {
'_Rendezvous': {}
}
}
from chameleon.web_server.web_server import WebServer
server = yield WebServer(9101, '/', '/swagger', grpc)
server.app = 'app'
server.add_swagger_routes = Mock()
self.assertEqual(server.add_swagger_routes.call_count, 1)
server.add_swagger_routes.assert_called_once_with('app', '/swagger')
def test_empty_def(self):
proto = unindent("""
syntax = "proto3";
package test;
""")
expected_swagger = {
u'swagger': u'2.0',
u'info': {
u'title': u'test.proto',
u'version': u'version not set'
},
u'schemes': [u"http", u"https"],
u'consumes': [u"application/json"],
u'produces': [u"application/json"],
u'paths': {},
u'definitions': {}
}
swagger = self.gen_swagger(proto)
self.assertEqual(json_rt(swagger), expected_swagger)
def test_empty_message_with_service(self):
proto = unindent("""
syntax = "proto3";
package test;
import "google/api/annotations.proto";
message Null {}
service TestService {
rpc Get(Null) returns(Null) {
option (google.api.http) = {
get: "/test"
};
}
}
""")
expected_swagger = {
u'swagger': u'2.0',
u'info': {
u'title': u'test.proto',
u'version': u"version not set"
},
u'schemes': [u"http", u"https"],
u'consumes': [u"application/json"],
u'produces': [u"application/json"],
u'paths': {
u'/test': {
u'get': {
u'operationId': u'Get',
u'responses': {
u'200': {
u'description': u'',
u'schema': {
u'$ref': u'#/definitions/test.Null'
}
}
},
u'tags': [u'TestService']
}
}
},
u'definitions': {
u'test.Null': {
u'type': u'object'
}
}
}
swagger = self.gen_swagger(proto)
self.assertEqual(json_rt(swagger), expected_swagger)
def test_simple_annotated_message_with_simple_annotated_service(self):
proto = unindent("""
syntax = "proto3";
package test;
import "google/api/annotations.proto";
// Simple Message
message Simple {
string str = 1; // a string attribute
int32 int = 2; // an int32 attribute
}
// Service to get things done
service TestService {
/* Get simple answer
*
* Returns the true answer to all of life's persistent questions.
*/
rpc Get(Simple) returns(Simple) {
option (google.api.http) = {
get: "/test"
};
}
}
""")
expected_swagger = {
u'swagger': u'2.0',
u'info': {
u'title': u'test.proto',
u'version': u"version not set"
},
u'schemes': [u"http", u"https"],
u'consumes': [u"application/json"],
u'produces': [u"application/json"],
u'paths': {
u'/test': {
u'get': {
u'summary': u'Get simple answer',
u'description':
u' Returns the true answer to all of life\'s '
u'persistent questions.',
u'operationId': u'Get',
u'responses': {
u'200': {
u'description': u'',
u'schema': {
u'$ref': u'#/definitions/test.Simple'
}
}
},
u'tags': [u'TestService']
}
}
},
u'definitions': {
u'test.Simple': {
u'description': u'Simple Message',
u'type': u'object',
u'properties': {
u'int': {
u'description': u'an int32 attribute',
u'type': u'integer',
u'format': u'int32'
},
u'str': {
u'description': u'a string attribute',
u'type': u'string',
u'format': u'string'
}
}
}
}
}
swagger = self.gen_swagger(proto)
self.assertEqual(json_rt(swagger), expected_swagger)
def test_method_input_params_in_body(self):
proto = unindent("""
syntax = "proto3";
package test;
import "google/api/annotations.proto";
// Simple Message
message Simple {
string str = 1; // a string attribute
int32 int = 2; // an int32 attribute
}
// Service to get things done
service TestService {
/* Get simple answer
*
* Returns the true answer to all of life's persistent questions.
*/
rpc Get(Simple) returns(Simple) {
option (google.api.http) = {
get: "/test"
};
}
/*
* Make up an answer (notice the leading blank line)
*
* Define the ultimate answer
*/
rpc MakeUp(Simple) returns(Simple) {
option (google.api.http) = {
post: "/test"
body: "*"
};
}
}
""")
expected_swagger = {
u'swagger': u'2.0',
u'info': {
u'title': u'test.proto',
u'version': u"version not set"
},
u'schemes': [u"http", u"https"],
u'consumes': [u"application/json"],
u'produces': [u"application/json"],
u'paths': {
u'/test': {
u'get': {
u'summary': u'Get simple answer',
u'description':
u' Returns the true answer to all of life\'s '
u'persistent questions.',
u'operationId': u'Get',
u'responses': {
u'200': {
u'description': u'',
u'schema': {
u'$ref': u'#/definitions/test.Simple'
}
}
},
u'tags': [u'TestService']
},
u'post': {
u'summary': u'Make up an answer (notice the leading '
u'blank line)',
u'description': u' Define the ultimate answer',
u'operationId': u'MakeUp',
u'parameters': [{
u'name': u'body',
u'in': u'body',
u'required': True,
u'schema': {u'$ref': u'#/definitions/test.Simple'}
}],
u'responses': {
u'200': {
u'description': u'',
u'schema': {
u'$ref': u'#/definitions/test.Simple'
}
}
},
u'tags': [u'TestService']
}
}
},
u'definitions': {
u'test.Simple': {
u'description': u'Simple Message',
u'type': u'object',
u'properties': {
u'int': {
u'description': u'an int32 attribute',
u'type': u'integer',
u'format': u'int32'
},
u'str': {
u'description': u'a string attribute',
u'type': u'string',
u'format': u'string'
}
}
}
}
}
swagger = self.gen_swagger(proto)
self.assertEqual(json_rt(swagger), expected_swagger)
def test_catch_repeating_verbs_for_same_path(self):
proto = unindent("""
syntax = "proto3";
package test;
import "google/api/annotations.proto";
message Null {}
service TestService {
rpc Get(Null) returns(Null) {
option (google.api.http) = {
get: "/test"
};
}
rpc MakeUp(Null) returns(Null) {
option (google.api.http) = {
get: "/test"
body: "*"
};
}
}
""")
with self.assertRaises(DuplicateMethodAndPathError):
self.gen_swagger(proto)
def test_catch_unresolved_message_type_reference(self):
proto = unindent("""
syntax = "proto3";
package test;
import "google/api/annotations.proto";
message Null {}
service TestService {
rpc Get(Simple) returns(Simple) {
option (google.api.http) = {
get: "/test"
};
}
rpc MakeUp(Simple) returns(Simple) {
option (google.api.http) = {
get: "/test"
body: "*"
};
}
}
""")
with self.assertRaises(ProtobufCompilationFailedError):
self.gen_swagger(proto)
def test_path_parameter_handling(self):
proto = unindent("""
syntax = "proto3";
package test;
import "google/api/annotations.proto";
message Simple {
string str = 1;
int32 int = 2;
}
service TestService {
rpc Get(Simple) returns(Simple) {
option (google.api.http) = {
get: "/test/{str}/{int}"
};
}
}
""")
expected_swagger_path = {
u'/test/{str}/{int}': {
u'get': {
u'operationId': u'Get',
u'parameters': [{
u'name': u'str',
u'in': u'path',
u'type': u'string',
u'format': u'string',
u'required': True
}, {
u'name': u'int',
u'in': u'path',
u'type': u'integer',
u'format': u'int32',
u'required': True
}],
u'responses': {
u'200': {
u'description': u'',
u'schema': {
u'$ref': u'#/definitions/test.Simple'
}
}
},
u'tags': [u'TestService']
}
}
}
swagger = self.gen_swagger(proto)
self.assertEqual(json_rt(swagger['paths']), expected_swagger_path)
def test_path_parameter_error_handling(self):
proto = unindent("""
syntax = "proto3";
package test;
import "google/api/annotations.proto";
message Simple {
string str = 1;
int32 int = 2;
}
service TestService {
rpc Get(Simple) returns(Simple) {
option (google.api.http) = {
get: "/test/{str}/{xxxxx}/{int}"
};
}
}
""")
with self.assertRaises(InvalidPathArgumentError):
self.gen_swagger(proto)
def test_alternative_bindings(self):
proto = unindent("""
syntax = "proto3";
package test;
import "google/api/annotations.proto";
message Simple {
string str = 1;
int32 int = 2;
}
service TestService {
rpc Get(Simple) returns(Simple) {
option (google.api.http) = {
get: "/v1/test/{str}/{int}"
additional_bindings {
post: "/v2/test"
body: "*"
}
additional_bindings {
get: "/v2/test/{int}/{str}"
}
};
}
}
""")
expected_swagger_path = {
u'/v1/test/{str}/{int}': {
u'get': {
u'operationId': u'Get',
u'parameters': [{
u'name': u'str',
u'in': u'path',
u'type': u'string',
u'format': u'string',
u'required': True
}, {
u'name': u'int',
u'in': u'path',
u'type': u'integer',
u'format': u'int32',
u'required': True
}],
u'responses': {
u'200': {
u'description': u'',
u'schema': {
u'$ref': u'#/definitions/test.Simple'
}
}
},
u'tags': [u'TestService']
}
},
u'/v2/test': {
u'post': {
u'operationId': u'Get',
u'parameters': [{
u'in': u'body',
u'name': u'body',
u'required': True,
u'schema': {u'$ref': u'#/definitions/test.Simple'}
}],
u'responses': {
u'200': {
u'description': u'',
u'schema': {u'$ref': u'#/definitions/test.Simple'}
}
},
u'tags': [u'TestService']
}
},
u'/v2/test/{int}/{str}': {
u'get': {
u'operationId': u'Get',
u'parameters': [{
u'format': u'int32',
u'in': u'path',
u'name': u'int',
u'required': True,
u'type': u'integer'
}, {
u'format': u'string',
u'in': u'path',
u'name': u'str',
u'required': True,
u'type': u'string'
}],
u'responses': {
u'200': {
u'description': u'',
u'schema': {u'$ref': u'#/definitions/test.Simple'}
}
},
u'tags': [u'TestService']
}
}
}
swagger = self.gen_swagger(proto)
self.assertEqual(json_rt(swagger['paths']), expected_swagger_path)
def test_google_null(self):
proto = unindent("""
syntax = "proto3";
package test;
import "google/api/annotations.proto";
import "google/protobuf/empty.proto";
service TestService {
rpc Get(google.protobuf.Empty) returns(google.protobuf.Empty) {
option (google.api.http) = {
get: "/echo"
};
}
}
""")
expected_swagger = {
u'swagger': u'2.0',
u'info': {
u'title': u'test.proto',
u'version': u"version not set"
},
u'schemes': [u"http", u"https"],
u'consumes': [u"application/json"],
u'produces': [u"application/json"],
u'paths': {
u'/echo': {
u'get': {
u'operationId': u'Get',
u'responses': {
u'200': {
u'description': u'',
u'schema': {
u'$ref':
u'#/definitions/google.protobuf.Empty'
}
}
},
u'tags': [u'TestService']
}
}
},
u'definitions': {
u'google.protobuf.Empty': {
u'description': u'A generic empty message that you can '
u're-use to avoid defining duplicated\n '
u'empty messages in your APIs. A typical '
u'example is to use it as the request\n '
u'or the response type of an API method. '
u'For instance:\n\n service Foo {\n '
u' rpc Bar(google.protobuf.Empty) '
u'returns (google.protobuf.Empty);\n '
u'}\n\n The JSON representation for '
u'`Empty` is empty JSON object `{}`.',
u'type': u'object'
}
}
}
swagger = self.gen_swagger(proto)
self.assertEqual(json_rt(swagger), expected_swagger)
def test_nested_type_definitions(self):
proto = unindent("""
syntax = "proto3";
package test;
import "google/api/annotations.proto";
import "google/protobuf/empty.proto";
message Null {}
message Outer {
message Inner {
message Innermost {
bool healthy = 1;
string illness = 2;
}
Innermost innermost = 1;
string other = 2;
}
string name = 1;
Inner inner = 2;
}
service TestService {
rpc Get(Null) returns(Outer) {
option (google.api.http) = {
get: "/test"
};
}
}
""")
expected_swagger_definitions = {
u'test.Null': {u'type': u'object'},
u'test.Outer': {
u'type': u'object',
u'properties': {
u'inner': {
u'$ref': u'#/definitions/test.Outer.Inner'
},
u'name': {
u'type': u'string',
u'format': u'string'
}
}
},
u'test.Outer.Inner': {
u'type': u'object',
u'properties': {
u'innermost': {
u'$ref': u'#/definitions/test.Outer.Inner.Innermost'
},
u'other': {
u'type': u'string',
u'format': u'string'
}
}
},
u'test.Outer.Inner.Innermost': {
u'type': u'object',
u'properties': {
u'healthy': {
u'type': u'boolean',
u'format': u'boolean'
},
u'illness': {
u'type': u'string',
u'format': u'string'
}
}
}
}
swagger = self.gen_swagger(proto)
self.assertEqual(json_rt(swagger['definitions']),
expected_swagger_definitions)
def test_enum(self):
proto = unindent("""
syntax = "proto3";
package test;
import "google/api/annotations.proto";
message Null {};
// Detailed weather state
enum WeatherState {
GOOD = 0; // Weather is good
BAD = 1; // Weather is bad
}
message Forecast {
WeatherState forecast = 1;
}
service ForecastService {
rpc GetForecast(Null) returns(Forecast) {
option (google.api.http) = {
get: "/forecast"
};
}
}
""")
expected_swagger_definitions = {
u'test.Null': {u'type': u'object'},
u'test.WeatherState': {
u'default': u'GOOD',
u'description': u'Detailed weather state\n'
u'Valid values:\n'
u' - GOOD: Weather is good\n'
u' - BAD: Weather is bad',
u'type': u'string',
u'enum': [u'GOOD', u'BAD']
},
u'test.Forecast': {
u'type': u'object',
u'properties': {
u'forecast': {u'$ref': u'#/definitions/test.WeatherState'}
}
}
}
swagger = self.gen_swagger(proto)
self.assertEqual(json_rt(swagger['definitions']),
expected_swagger_definitions)
def test_nested_enum(self):
proto = unindent("""
syntax = "proto3";
package test;
import "google/api/annotations.proto";
message Null {};
message Forecast {
// Detailed weather state
enum WeatherState {
GOOD = 0; // Weather is good
BAD = 1; // Weather is bad
}
WeatherState forecast = 1;
}
service ForecastService {
rpc GetForecast(Null) returns(Forecast) {
option (google.api.http) = {
get: "/forecast"
};
}
}
""")
expected_swagger_definitions = {
u'test.Null': {u'type': u'object'},
u'test.Forecast.WeatherState': {
u'default': u'GOOD',
u'description': u'Detailed weather state\n'
u'Valid values:\n'
u' - GOOD: Weather is good\n'
u' - BAD: Weather is bad',
u'type': u'string',
u'enum': [u'GOOD', u'BAD']
},
u'test.Forecast': {
u'type': u'object',
u'properties': {
u'forecast': {
u'$ref': u'#/definitions/test.Forecast.WeatherState'}
}
}
}
swagger = self.gen_swagger(proto)
self.assertEqual(json_rt(swagger['definitions']),
expected_swagger_definitions)
def test_array_of_simple_types(self):
proto = unindent("""
syntax = "proto3";
package test;
import "google/api/annotations.proto";
message Null {};
message Invitations {
string event = 1;
repeated string names = 2;
repeated int32 ages = 3;
}
service RsvpService {
rpc Get(Null) returns(Invitations) {
option (google.api.http) = {
get: "/invitations"
};
}
}
""")
expected_swagger_definitions = {
u'test.Null': {u'type': u'object'},
u'test.Invitations': {
u'type': u'object',
u'properties': {
u'event': {
u'type': u'string',
u'format': u'string'
},
u'names': {
u'type': u'array',
u'items': {
u'type': u'string',
u'format': u'string'
}
},
u'ages': {
u'type': u'array',
u'items': {
u'type': u'integer',
u'format': u'int32'
}
}
}
}
}
swagger = self.gen_swagger(proto)
self.assertEqual(json_rt(swagger['definitions']),
expected_swagger_definitions)
def test_array_of_object_type(self):
proto = unindent("""
syntax = "proto3";
package test;
import "google/api/annotations.proto";
message Null {};
message Invitations {
message Address {
string street = 1;
string city = 2;
}
string event = 1;
repeated Null nulles = 2;
repeated Address addresses = 3;
}
service RsvpService {
rpc Get(Null) returns(Invitations) {
option (google.api.http) = {
get: "/invitations"
};
}
}
""")
expected_swagger_definitions = {
u'test.Null': {u'type': u'object'},
u'test.Invitations.Address': {
u'type': u'object',
u'properties': {
u'street': {
u'type': u'string',
u'format': u'string'
},
u'city': {
u'type': u'string',
u'format': u'string'
}
}
},
u'test.Invitations': {
u'type': u'object',
u'properties': {
u'event': {
u'type': u'string',
u'format': u'string'
},
u'nulles': {
u'type': u'array',
u'items': {
u'$ref': u'#/definitions/test.Null'
}
},
u'addresses': {
u'type': u'array',
u'items': {
u'$ref': u'#/definitions/test.Invitations.Address'
}
}
}
}
}
swagger = self.gen_swagger(proto)
self.assertEqual(json_rt(swagger['definitions']),
expected_swagger_definitions)
def test_recursively_nested_values(self):
proto = unindent("""
syntax = "proto3";
package test;
import "google/api/annotations.proto";
message Null {};
message TreeNode {
string name = 1;
repeated TreeNode children = 2;
}
service RsvpService {
rpc Get(Null) returns(TreeNode) {
option (google.api.http) = {
get: "/invitations"
};
}
}
""")
expected_swagger_definitions = {
u'test.Null': {u'type': u'object'},
u'test.TreeNode': {
u'type': u'object',
u'properties': {
u'name': {
u'type': u'string',
u'format': u'string'
},
u'children': {
u'type': u'array',
u'items': {
u'$ref': u'#/definitions/test.TreeNode'
}
}
}
}
}
swagger = self.gen_swagger(proto)
self.assertEqual(json_rt(swagger['definitions']),
expected_swagger_definitions)
def test_map_fields(self):
proto = unindent("""
syntax = "proto3";
package test;
import "google/api/annotations.proto";
message Null {};
message Maps {
map<string, string> string_map = 1;
map<string, int32> int32_map = 2;
map<string, Null> object_map = 3;
}
service RsvpService {
rpc Get(Null) returns(Maps) {
option (google.api.http) = {
get: "/maps"
};
}
}
""")
expected_swagger_definitions = {
u'test.Null': {u'type': u'object'},
u'test.Maps': {
u'type': u'object',
u'properties': {
u'string_map': {
u'type': u'object',
u'additionalProperties': {
u'type': u'string',
u'format': u'string'
}
},
u'int32_map': {
u'type': u'object',
u'additionalProperties': {
u'type': u'integer',
u'format': u'int32'
}
},
u'object_map': {
u'type': u'object',
u'additionalProperties': {
u'$ref': u'#/definitions/test.Null',
}
}
}
}
}
swagger = self.gen_swagger(proto)
self.assertEqual(json_rt(swagger['definitions']),
expected_swagger_definitions)
def test_array_and_map_of_enum(self):
proto = unindent("""
syntax = "proto3";
package test;
import "google/api/annotations.proto";
message Null {};
enum State {
GOOD = 0;
BAD = 1;
}
message Map {
map<string, State> enum_map = 1;
repeated State states = 2;
}
service RsvpService {
rpc Get(Null) returns(Map) {
option (google.api.http) = {
get: "/maps"
};
}
}
""")
expected_swagger_definitions = {
u'test.Null': {u'type': u'object'},
u'test.State': {
u'default': u'GOOD',
u'description': u'State\n'
u'Valid values:\n'
u' - GOOD\n'
u' - BAD',
u'type': u'string',
u'enum': [u'GOOD', u'BAD']
},
u'test.Map': {
u'type': u'object',
u'properties': {
u'enum_map': {
u'type': u'object',
u'additionalProperties': {
u'$ref': u'#/definitions/test.State',
}
},
u'states': {
u'type': u'array',
u'items': {
u'$ref': u'#/definitions/test.State'
}
}
}
}
}
swagger = self.gen_swagger(proto)
self.assertEqual(json_rt(swagger['definitions']),
expected_swagger_definitions)
def test_kitchen_sink(self):
proto = load_file(
os.path.dirname(__file__) + '/a_bit_of_everything.proto')
swagger = self.gen_swagger(proto)
expected_swagger = json.loads(load_file(
os.path.dirname(__file__) + '/a_bit_of_everything.swagger.json')
)
self.maxDiff = 100000
self.assertEqual(json_rt(swagger), expected_swagger)