Add response schema validation (only when in TESTING mode) and add one schema. More will be added in a followup CL

This commit is contained in:
Joseph Schorr 2014-08-27 20:57:46 -04:00
parent 4fd249589d
commit 6f1a4030b6
3 changed files with 78 additions and 4 deletions

View file

@ -1,6 +1,7 @@
import logging import logging
import json import json
from app import app
from flask import Blueprint, request, make_response, jsonify from flask import Blueprint, request, make_response, jsonify
from flask.ext.restful import Resource, abort, Api, reqparse from flask.ext.restful import Resource, abort, Api, reqparse
from flask.ext.restful.utils.cors import crossdomain from flask.ext.restful.utils.cors import crossdomain
@ -52,6 +53,11 @@ class InvalidRequest(ApiException):
ApiException.__init__(self, 'invalid_request', 400, error_description, payload) ApiException.__init__(self, 'invalid_request', 400, error_description, payload)
class InvalidResponse(ApiException):
def __init__(self, error_description, payload=None):
ApiException.__init__(self, 'invalid_response', 500, error_description, payload)
class InvalidToken(ApiException): class InvalidToken(ApiException):
def __init__(self, error_description, payload=None): def __init__(self, error_description, payload=None):
ApiException.__init__(self, 'invalid_token', 401, error_description, payload) ApiException.__init__(self, 'invalid_token', 401, error_description, payload)
@ -286,6 +292,25 @@ def validate_json_request(schema_name):
return wrapper return wrapper
def define_json_response(schema_name):
def wrapper(func):
@add_method_metadata('response_schema', schema_name)
@wraps(func)
def wrapped(self, *args, **kwargs):
schema = self.schemas[schema_name]
try:
resp = func(self, *args, **kwargs)
if app.config['TESTING']:
validate(resp, schema)
return resp
except ValidationError as ex:
raise InvalidResponse(ex.message)
return wrapped
return wrapper
def request_error(exception=None, **kwargs): def request_error(exception=None, **kwargs):
data = kwargs.copy() data = kwargs.copy()
message = 'Request error.' message = 'Request error.'

View file

@ -94,12 +94,18 @@ def swagger_route_data(include_internal=False, compact=False):
new_operation = { new_operation = {
'method': method_name, 'method': method_name,
'nickname': method_metadata(method, 'nickname') 'nickname': method_metadata(method, 'nickname') or '(unnamed)'
} }
if not compact: if not compact:
response_type = 'void'
res_schema_name = method_metadata(method, 'response_schema')
if res_schema_name:
models[res_schema_name] = view_class.schemas[res_schema_name]
response_type = res_schema_name
new_operation.update({ new_operation.update({
'type': 'void', 'type': response_type,
'summary': method.__doc__.strip() if method.__doc__ else '', 'summary': method.__doc__.strip() if method.__doc__ else '',
'parameters': parameters, 'parameters': parameters,
}) })

View file

@ -8,7 +8,8 @@ from flask.ext.principal import identity_changed, AnonymousIdentity
from app import app, billing as stripe, authentication from app import app, billing as stripe, authentication
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error, from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
log_action, internal_only, NotFound, require_user_admin, path_param, log_action, internal_only, NotFound, require_user_admin, path_param,
InvalidToken, require_scope, format_date, hide_if, show_if, license_error) InvalidToken, require_scope, format_date, hide_if, show_if, license_error,
define_json_response)
from endpoints.api.subscribe import subscribe from endpoints.api.subscribe import subscribe
from endpoints.common import common_login from endpoints.common import common_login
from data import model from data import model
@ -50,13 +51,13 @@ def user_view(user):
'verified': user.verified, 'verified': user.verified,
'anonymous': False, 'anonymous': False,
'username': user.username, 'username': user.username,
'email': user.email,
'gravatar': compute_hash(user.email), 'gravatar': compute_hash(user.email),
} }
user_admin = UserAdminPermission(user.username) user_admin = UserAdminPermission(user.username)
if user_admin.can(): if user_admin.can():
user_response.update({ user_response.update({
'email': user.email,
'organizations': [org_view(o) for o in organizations], 'organizations': [org_view(o) for o in organizations],
'logins': [login_view(login) for login in logins], 'logins': [login_view(login) for login in logins],
'can_create_repo': True, 'can_create_repo': True,
@ -130,10 +131,51 @@ class User(ApiResource):
}, },
}, },
}, },
'UserView': {
'id': 'UserView',
'type': 'object',
'description': 'Describes a user',
'required': ['verified', 'anonymous', 'gravatar'],
'properties': {
'verified': {
'type': 'boolean',
'description': 'Whether the user\'s email address has been verified'
},
'anonymous': {
'type': 'boolean',
'description': 'true if this user data represents a guest user'
},
'email': {
'type': 'string',
'description': 'The user\'s email address',
},
'gravatar': {
'type': 'string',
'description': 'Gravatar hash representing the user\'s icon'
},
'organizations': {
'type': 'array',
'description': 'Information about the organizations in which the user is a member'
},
'logins': {
'type': 'array',
'description': 'The list of external login providers against which the user has authenticated'
},
'can_create_repo': {
'type': 'boolean',
'description': 'Whether the user has permission to create repositories'
},
'preferred_namespace': {
'type': 'boolean',
'description': 'If true, the user\'s namespace is the preferred namespace to display'
}
}
},
} }
@require_scope(scopes.READ_USER) @require_scope(scopes.READ_USER)
@nickname('getLoggedInUser') @nickname('getLoggedInUser')
@define_json_response('UserView')
def get(self): def get(self):
""" Get user information for the authenticated user. """ """ Get user information for the authenticated user. """
user = get_authenticated_user() user = get_authenticated_user()
@ -146,6 +188,7 @@ class User(ApiResource):
@nickname('changeUserDetails') @nickname('changeUserDetails')
@internal_only @internal_only
@validate_json_request('UpdateUser') @validate_json_request('UpdateUser')
@define_json_response('UserView')
def put(self): def put(self):
""" Update a users details such as password or email. """ """ Update a users details such as password or email. """
user = get_authenticated_user() user = get_authenticated_user()