Merge pull request #1361 from ecordell/application-problem-json

Return application/problem+json format errors
This commit is contained in:
Evan Cordell 2016-04-12 17:25:14 -04:00
commit 29eb0304e5
30 changed files with 309 additions and 141 deletions

View file

@ -13,6 +13,7 @@ import scopes
from data import model
from app import app, authentication
from endpoints.exception import InvalidToken, ExpiredToken
from permissions import QuayDeferredPermissionUser
from auth_context import (set_authenticated_user, set_validated_token, set_grant_context,
set_validated_oauth_token)
@ -50,20 +51,10 @@ def _validate_and_apply_oauth_token(token):
validated = model.oauth.validate_access_token(token)
if not validated:
logger.warning('OAuth access token could not be validated: %s', token)
authenticate_header = {
'WWW-Authenticate': ('Bearer error="invalid_token", '
'error_description="The access token is invalid"'),
}
abort(401, message='OAuth access token could not be validated: %(token)s',
issue='invalid-oauth-token', token=token, headers=authenticate_header)
raise InvalidToken('OAuth access token could not be validated: {token}'.format(token=token))
elif validated.expires_at <= datetime.utcnow():
logger.info('OAuth access with an expired token: %s', token)
authenticate_header = {
'WWW-Authenticate': ('Bearer error="invalid_token", '
'error_description="The access token expired"'),
}
abort(401, message='OAuth access token has expired: %(token)s',
issue='invalid-oauth-token', token=token, headers=authenticate_header)
raise ExpiredToken('OAuth access token has expired: {token}'.format(token=token))
# Don't allow disabled users to login.
if not validated.authorized_user.enabled:

View file

@ -16,7 +16,7 @@ from buildtrigger.basehandler import BuildTriggerHandler
from util.security.ssh import generate_ssh_keypair
from util.dict_wrappers import JSONPathDict, SafeDictSetter
from endpoints.api import ExternalServiceTimeout
from endpoints.exception import ExternalServiceTimeout
import gitlab
import requests

View file

@ -1,9 +1,10 @@
import logging
import datetime
import json
from enum import Enum
from app import app, metric_queue
from flask import Blueprint, request, make_response, jsonify, session
from flask import Blueprint, Response, request, make_response, jsonify, session, url_for
from flask.ext.restful import Resource, abort, Api, reqparse
from flask.ext.restful.utils.cors import crossdomain
from calendar import timegm
@ -20,6 +21,7 @@ from auth import scopes
from auth.auth_context import get_authenticated_user, get_validated_oauth_token
from auth.auth import process_oauth
from endpoints.csrf import csrf_protect
from endpoints.exception import ApiException, Unauthorized, InvalidRequest, InvalidResponse
from endpoints.decorators import check_anon_protection
from util.saas.metricqueue import time_decorator
from util.pagination import encrypt_page_token, decrypt_page_token
@ -33,84 +35,15 @@ api.decorators = [csrf_protect,
process_oauth, time_decorator(api_bp.name, metric_queue)]
class ApiException(Exception):
def __init__(self, error_type, status_code, error_description, payload=None):
Exception.__init__(self)
self.error_description = error_description
self.status_code = status_code
self.payload = payload
self.error_type = error_type
def to_dict(self):
rv = dict(self.payload or ())
if self.error_description is not None:
rv['error_description'] = self.error_description
rv['error_type'] = self.error_type
return rv
class ExternalServiceTimeout(ApiException):
def __init__(self, error_description, payload=None):
ApiException.__init__(self, 'external_service_timeout', 520, error_description, payload)
class InvalidRequest(ApiException):
def __init__(self, error_description, payload=None):
ApiException.__init__(self, 'invalid_request', 400, error_description, payload)
class InvalidResponse(ApiException):
def __init__(self, error_description, payload=None):
ApiException.__init__(self, 'invalid_response', 400, error_description, payload)
class InvalidToken(ApiException):
def __init__(self, error_description, payload=None):
ApiException.__init__(self, 'invalid_token', 401, error_description, payload)
class Unauthorized(ApiException):
def __init__(self, payload=None):
user = get_authenticated_user()
if user is None or user.organization:
ApiException.__init__(self, 'invalid_token', 401, "Requires authentication", payload)
else:
ApiException.__init__(self, 'insufficient_scope', 403, 'Unauthorized', payload)
class FreshLoginRequired(ApiException):
def __init__(self, payload=None):
ApiException.__init__(self, 'fresh_login_required', 401, "Requires fresh login", payload)
class ExceedsLicenseException(ApiException):
def __init__(self, payload=None):
ApiException.__init__(self, None, 402, 'Payment Required', payload)
class NotFound(ApiException):
def __init__(self, payload=None):
ApiException.__init__(self, None, 404, 'Not Found', payload)
class DownstreamIssue(ApiException):
def __init__(self, payload=None):
ApiException.__init__(self, None, 520, 'Downstream Issue', payload)
@api_bp.app_errorhandler(ApiException)
@crossdomain(origin='*', headers=['Authorization', 'Content-Type'])
def handle_api_error(error):
response = jsonify(error.to_dict())
response.status_code = error.status_code
if error.error_type is not None:
response = Response(json.dumps(error.to_dict()), error.status_code, mimetype='application/problem+json')
if error.status_code is 401:
response.headers['WWW-Authenticate'] = ('Bearer error="%s" error_description="%s"' %
(error.error_type, error.error_description))
return response
def resource(*urls, **kwargs):
def wrapper(api_resource):
if not api_resource:
@ -383,6 +316,7 @@ def request_error(exception=None, **kwargs):
message = 'Request error.'
if exception:
message = exception.message
message = data.pop('message', message)
raise InvalidRequest(message, data)
@ -427,6 +361,7 @@ def define_json_response(schema_name):
import endpoints.api.billing
import endpoints.api.build
import endpoints.api.discovery
import endpoints.api.error
import endpoints.api.image
import endpoints.api.logs
import endpoints.api.organization

View file

@ -5,8 +5,9 @@ import stripe
from flask import request
from app import billing
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, log_action,
related_user_resource, internal_only, Unauthorized, NotFound,
require_user_admin, show_if, path_param, require_scope, abort)
related_user_resource, internal_only, require_user_admin, show_if,
path_param, require_scope, abort)
from endpoints.exception import Unauthorized, NotFound
from endpoints.api.subscribe import subscribe, subscription_view
from auth.permissions import AdministerOrganizationPermission
from auth.auth_context import get_authenticated_user

View file

@ -12,8 +12,9 @@ from app import app, userfiles as user_files, build_logs, log_archive, dockerfil
from buildtrigger.basehandler import BuildTriggerHandler
from endpoints.api import (RepositoryParamResource, parse_args, query_param, nickname, resource,
require_repo_read, require_repo_write, validate_json_request,
ApiResource, internal_only, format_date, api, Unauthorized, NotFound,
path_param, InvalidRequest, require_repo_admin)
ApiResource, internal_only, format_date, api, path_param,
require_repo_admin)
from endpoints.exception import Unauthorized, NotFound, InvalidRequest
from endpoints.building import start_build, PreparedBuild
from data import database
from data import model

View file

@ -3,7 +3,6 @@
import re
import logging
import sys
import copy
from flask.ext.restful import reqparse
@ -180,24 +179,54 @@ def swagger_route_data(include_internal=False, compact=False):
if response_schema_name:
models[response_schema_name] = view_class.schemas[response_schema_name]
models['ApiError'] = {
'type': 'object',
'properties': {
'status': {
'type': 'integer',
'description': 'Status code of the response.'
},
'type': {
'type': 'string',
'description': 'Reference to the type of the error.'
},
'detail': {
'type': 'string',
'description': 'Details about the specific instance of the error.'
},
'title': {
'type': 'string',
'description': 'Unique error code to identify the type of error.'
}
},
'required': [
'status',
'type',
'title',
]
}
responses = {
'400': {
'description': 'Bad Request'
'description': 'Bad Request',
},
'401': {
'description': 'Session required'
'description': 'Session required',
},
'403': {
'description': 'Unauthorized access'
'description': 'Unauthorized access',
},
'404': {
'description': 'Not found'
'description': 'Not found',
},
}
for status, body in responses.items():
body['schema'] = {'$ref': '#/definitions/ApiError'}
if method_name == 'DELETE':
responses['204'] = {
'description': 'Deleted'

61
endpoints/api/error.py Normal file
View file

@ -0,0 +1,61 @@
""" Error details API """
from flask import url_for
from endpoints.api import (resource, nickname, ApiResource, path_param,
define_json_response)
from endpoints.exception import NotFound, ApiErrorType, ERROR_DESCRIPTION
def error_view(error_type):
return {
'type': url_for('error', error_type=error_type, _external=True),
'title': error_type,
'description': ERROR_DESCRIPTION[error_type]
}
@resource('/v1/error/<error_type>')
@path_param('error_type', 'The error code identifying the type of error.')
class Error(ApiResource):
""" Resource for Error Descriptions"""
schemas = {
'ApiErrorDescription': {
'type': 'object',
'description': 'Description of an error',
'required': [
'type',
'description',
'title',
],
'properties': {
'type': {
'type': 'string',
'description': 'A reference to the error type resource'
},
'title': {
'type': 'string',
'description': (
'The title of the error. Can be used to uniquely identify the kind'
' of error.'
),
'enum': list(ApiErrorType.__members__)
},
'description': {
'type': 'string',
'description': (
'A more detailed description of the error that may include help for'
' fixing the issue.'
)
}
},
},
}
@define_json_response('ApiErrorDescription')
@nickname('getErrorDescription')
def get(self, error_type):
""" Get a detailed description of the error """
if error_type in ERROR_DESCRIPTION.keys():
return error_view(error_type)
raise NotFound()

View file

@ -4,7 +4,8 @@ import json
from collections import defaultdict
from endpoints.api import (resource, nickname, require_repo_read, RepositoryParamResource,
format_date, NotFound, path_param)
format_date, path_param)
from endpoints.exception import NotFound
from data import model
from util.cache import cache_control_flask_restful

View file

@ -7,8 +7,8 @@ from dateutil.relativedelta import relativedelta
from endpoints.api import (resource, nickname, ApiResource, query_param, parse_args,
RepositoryParamResource, require_repo_admin, related_user_resource,
format_date, Unauthorized, NotFound, require_user_admin,
path_param, require_scope, page_support)
format_date, require_user_admin, path_param, require_scope, page_support)
from endpoints.exception import Unauthorized, NotFound
from auth.permissions import AdministerOrganizationPermission, AdministerOrganizationPermission
from auth.auth_context import get_authenticated_user
from data import model, database

View file

@ -8,9 +8,9 @@ import features
from app import billing as stripe, avatar
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
related_user_resource, internal_only, Unauthorized, NotFound,
require_user_admin, log_action, show_if, path_param,
require_scope)
related_user_resource, internal_only, require_user_admin, log_action,
show_if, path_param, require_scope)
from endpoints.exception import Unauthorized, NotFound
from endpoints.api.team import team_view
from endpoints.api.user import User, PrivateRepositories
from auth.permissions import (AdministerOrganizationPermission, OrganizationMemberPermission,

View file

@ -6,8 +6,8 @@ from flask import request
from app import avatar
from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource,
log_action, request_error, validate_json_request, path_param,
NotFound)
log_action, request_error, validate_json_request, path_param)
from endpoints.exception import NotFound
from data import model

View file

@ -3,7 +3,8 @@
from flask import request
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
log_action, Unauthorized, NotFound, path_param, require_scope)
log_action, path_param, require_scope)
from endpoints.exception import Unauthorized, NotFound
from auth.permissions import AdministerOrganizationPermission
from auth.auth_context import get_authenticated_user
from auth import scopes

View file

@ -5,9 +5,9 @@ import logging
from flask import request, abort
from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource,
log_action, validate_json_request, NotFound, internal_only,
log_action, validate_json_request, internal_only,
path_param, show_if)
from endpoints.exception import NotFound
from app import tf
from data import model
from data.database import db

View file

@ -13,8 +13,8 @@ from data.database import Repository as RepositoryTable
from endpoints.api import (truthy_bool, format_date, nickname, log_action, validate_json_request,
require_repo_read, require_repo_write, require_repo_admin,
RepositoryParamResource, resource, query_param, parse_args, ApiResource,
request_error, require_scope, Unauthorized, NotFound, InvalidRequest,
path_param, ExceedsLicenseException, page_support)
request_error, require_scope, path_param, page_support)
from endpoints.exception import Unauthorized, NotFound, InvalidRequest, ExceedsLicenseException
from endpoints.api.billing import lookup_allowed_private_repos, get_namespace_plan
from endpoints.api.subscribe import check_repository_usage

View file

@ -6,8 +6,9 @@ from flask import request
from app import notification_queue
from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin,
log_action, validate_json_request, NotFound, request_error,
log_action, validate_json_request, request_error,
path_param)
from endpoints.exception import NotFound
from endpoints.notificationevent import NotificationEvent
from endpoints.notificationmethod import (NotificationMethod,
CannotValidateNotificationMethodException)

View file

@ -5,7 +5,8 @@ import logging
from flask import request
from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource,
log_action, validate_json_request, NotFound, path_param)
log_action, validate_json_request, path_param)
from endpoints.exception import NotFound
from data import model

View file

@ -1,8 +1,9 @@
""" Manage user and organization robot accounts. """
from endpoints.api import (resource, nickname, ApiResource, log_action, related_user_resource,
Unauthorized, require_user_admin, require_scope, path_param, parse_args,
require_user_admin, require_scope, path_param, parse_args,
truthy_bool, query_param)
from endpoints.exception import Unauthorized
from auth.permissions import AdministerOrganizationPermission, OrganizationMemberPermission
from auth.auth_context import get_authenticated_user
from auth import scopes

View file

@ -5,9 +5,10 @@ import features
from app import secscan_api
from data import model
from endpoints.api import (require_repo_read, NotFound, DownstreamIssue, path_param,
from endpoints.api import (require_repo_read, path_param,
RepositoryParamResource, resource, nickname, show_if, parse_args,
query_param, truthy_bool)
from endpoints.exception import NotFound, DownstreamIssue
from util.secscan.api import APIRequestFailure

View file

@ -4,7 +4,8 @@ import logging
import stripe
from app import billing
from endpoints.api import request_error, log_action, NotFound
from endpoints.api import request_error, log_action
from endpoints.exception import NotFound
from data import model
from data.billing import PLANS

View file

@ -3,8 +3,9 @@
from flask import request, abort
from endpoints.api import (resource, nickname, require_repo_read, require_repo_write,
RepositoryParamResource, log_action, NotFound, validate_json_request,
RepositoryParamResource, log_action, validate_json_request,
path_param, parse_args, query_param, truthy_bool)
from endpoints.exception import NotFound
from endpoints.api.image import image_view
from data import model
from auth.auth_context import get_authenticated_user

View file

@ -5,9 +5,9 @@ from flask import request
import features
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
log_action, Unauthorized, NotFound, internal_only, require_scope,
path_param, query_param, truthy_bool, parse_args, require_user_admin,
show_if)
log_action, internal_only, require_scope, path_param, query_param,
truthy_bool, parse_args, require_user_admin, show_if)
from endpoints.exception import Unauthorized, NotFound
from auth.permissions import AdministerOrganizationPermission, ViewTeamPermission
from auth.auth_context import get_authenticated_user
from auth import scopes

View file

@ -15,8 +15,8 @@ from buildtrigger.triggerutil import (TriggerDeactivationException,
RepositoryReadException, TriggerStartException)
from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin,
log_action, request_error, query_param, parse_args, internal_only,
validate_json_request, api, Unauthorized, NotFound, InvalidRequest,
path_param)
validate_json_request, api, path_param)
from endpoints.exception import NotFound, Unauthorized, InvalidRequest
from endpoints.api.build import build_status_view, trigger_view, RepositoryBuildStatus
from endpoints.building import start_build
from data import model

View file

@ -13,10 +13,11 @@ import features
from app import app, billing as stripe, authentication, avatar
from data.database import Repository as RepositoryTable
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
log_action, internal_only, NotFound, require_user_admin, parse_args,
query_param, InvalidToken, require_scope, format_date, show_if,
log_action, internal_only, require_user_admin, parse_args,
query_param, require_scope, format_date, show_if,
license_error, require_fresh_login, path_param, define_json_response,
RepositoryParamResource, page_support)
from endpoints.exception import NotFound, InvalidToken
from endpoints.api.subscribe import subscribe
from endpoints.common import common_login
from endpoints.decorators import anon_allowed

127
endpoints/exception.py Normal file
View file

@ -0,0 +1,127 @@
from enum import Enum
from flask import url_for
from auth.auth_context import get_authenticated_user
class ApiErrorType(Enum):
external_service_timeout = 'external_service_timeout'
invalid_request = 'invalid_request'
invalid_response = 'invalid_response'
invalid_token = 'invalid_token'
expired_token = 'expired_token'
insufficient_scope = 'insufficient_scope'
fresh_login_required = 'fresh_login_required'
exceeds_license = 'exceeds_license'
not_found = 'not_found'
downstream_issue = 'downstream_issue'
ERROR_DESCRIPTION = {
ApiErrorType.external_service_timeout.value: "An external service timed out. Retrying the request may resolve the issue.",
ApiErrorType.invalid_request.value: "The request was invalid. It may have contained invalid values or was improperly formatted.",
ApiErrorType.invalid_response.value: "The response was invalid.",
ApiErrorType.invalid_token.value: "The access token provided was invalid.",
ApiErrorType.expired_token.value: "The access token provided has expired.",
ApiErrorType.insufficient_scope.value: "The access token did not have sufficient scope to access the requested resource.",
ApiErrorType.fresh_login_required.value: "The action requires a fresh login to succeed.",
ApiErrorType.exceeds_license.value: "The action was refused because the current license does not allow it.",
ApiErrorType.not_found.value: "The resource was not found.",
ApiErrorType.downstream_issue.value: "An error occurred in a downstream service.",
}
class ApiException(Exception):
"""
Represents an error in the application/problem+json format.
See: https://tools.ietf.org/html/rfc7807
- "type" (string) - A URI reference that identifies the
problem type.
- "title" (string) - A short, human-readable summary of the problem
type. It SHOULD NOT change from occurrence to occurrence of the
problem, except for purposes of localization
- "status" (number) - The HTTP status code
- "detail" (string) - A human-readable explanation specific to this
occurrence of the problem.
- "instance" (string) - A URI reference that identifies the specific
occurrence of the problem. It may or may not yield further
information if dereferenced.
"""
def __init__(self, error_type, status_code, error_description, payload=None):
Exception.__init__(self)
self.error_description = error_description
self.status_code = status_code
self.payload = payload
self.error_type = error_type
def to_dict(self):
rv = dict(self.payload or ())
if self.error_description is not None:
rv['detail'] = self.error_description
rv['title'] = self.error_type.value
rv['type'] = url_for('error', error_type=self.error_type.value, _external=True)
rv['status'] = self.status_code
return rv
class ExternalServiceTimeout(ApiException):
def __init__(self, error_description, payload=None):
ApiException.__init__(self, ApiErrorType.external_service_timeout, 520, error_description, payload)
class InvalidRequest(ApiException):
def __init__(self, error_description, payload=None):
ApiException.__init__(self, ApiErrorType.invalid_request, 400, error_description, payload)
class InvalidResponse(ApiException):
def __init__(self, error_description, payload=None):
ApiException.__init__(self, ApiErrorType.invalid_response, 400, error_description, payload)
class InvalidToken(ApiException):
def __init__(self, error_description, payload=None):
ApiException.__init__(self, ApiErrorType.invalid_token, 401, error_description, payload)
class ExpiredToken(ApiException):
def __init__(self, error_description, payload=None):
ApiException.__init__(self, ApiErrorType.expired_token, 401, error_description, payload)
class Unauthorized(ApiException):
def __init__(self, payload=None):
user = get_authenticated_user()
if user is None or user.organization:
ApiException.__init__(self, ApiErrorType.invalid_token, 401, "Requires authentication", payload)
else:
ApiException.__init__(self, ApiErrorType.insufficient_scope, 403, 'Unauthorized', payload)
class FreshLoginRequired(ApiException):
def __init__(self, payload=None):
ApiException.__init__(self, ApiErrorType.fresh_login_required, 401, "Requires fresh login", payload)
class ExceedsLicenseException(ApiException):
def __init__(self, payload=None):
ApiException.__init__(self, ApiErrorType.exceeds_license, 402, 'Payment Required', payload)
class NotFound(ApiException):
def __init__(self, payload=None):
ApiException.__init__(self, ApiErrorType.not_found, 404, 'Not Found', payload)
class DownstreamIssue(ApiException):
def __init__(self, payload=None):
ApiException.__init__(self, ApiErrorType.downstream_issue, 520, 'Downstream Issue', payload)

View file

@ -211,8 +211,9 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi
// Handle session expiration.
Restangular.setErrorInterceptor(function(response) {
if (response.status == 401 && response.data['error_type'] == 'invalid_token' &&
response.data['session_required'] !== false) {
//TODO: remove check for error_type (old style errors)
var invalid_token = response.data['title'] == 'invalid_token' || response.data['error_type'] == 'invalid_token';
if (response.status == 401 && invalid_token && response.data['session_required'] !== false) {
$('#sessionexpiredModal').modal({});
return false;
}

View file

@ -51,4 +51,4 @@ angular.module('quay').directive('signupForm', function () {
}
};
return directiveDefinitionObject;
});
});

View file

@ -129,7 +129,7 @@
$scope.showInterface = true;
}, function(resp) {
$scope.users = [];
$scope.usersError = resp['data']['message'] || resp['data']['error_description'];
$scope.usersError = ApiService.getErrorMessage(resp);
});
};
@ -365,4 +365,4 @@
// Load the initial status.
$scope.checkStatus();
}
}());
}());

View file

@ -124,7 +124,9 @@ angular.module('quay').factory('ApiService', ['Restangular', '$q', 'UtilService'
var deferred = $q.defer();
// If the error is a fresh login required, show the dialog.
if (resp.status == 401 && resp.data['error_type'] == 'fresh_login_required') {
// TODO: remove error_type (old style error)
var fresh_login_required = resp.data['title'] == 'fresh_login_required' || resp.data['error_type'] == 'fresh_login_required';
if (resp.status == 401 && fresh_login_required) {
var retryOperation = function() {
apiService[opName].apply(apiService, opArgs).then(function(resp) {
deferred.resolve(resp);
@ -293,7 +295,8 @@ angular.module('quay').factory('ApiService', ['Restangular', '$q', 'UtilService'
apiService.getErrorMessage = function(resp, defaultMessage) {
var message = defaultMessage;
if (resp['data']) {
message = resp['data']['error_message'] || resp['data']['message'] || resp['data']['error_description'] || message;
//TODO: remove error_message and error_description (old style error)
message = resp['data']['detail'] || resp['data']['error_message'] || resp['data']['message'] || resp['data']['error_description'] || message;
}
return message;

View file

@ -1,7 +1,7 @@
/**
* Service which provides helper methods for performing some simple UI operations.
*/
angular.module('quay').factory('UIService', ['$timeout', '$rootScope', '$location', function($timeout, $rootScope, $location) {
angular.module('quay').factory('UIService', ['$timeout', '$rootScope', '$location', 'ApiService', function($timeout, $rootScope, $location, ApiService) {
var CheckStateController = function(items, itemKey) {
this.items = items;
this.checked = [];
@ -12,6 +12,7 @@ angular.module('quay').factory('UIService', ['$timeout', '$rootScope', '$locatio
this.itemKey_ = itemKey;
this.listeners_ = [];
this.page_ = null;
this.ApiService = ApiService
};
CheckStateController.prototype.listen = function(callback) {
@ -138,7 +139,7 @@ angular.module('quay').factory('UIService', ['$timeout', '$rootScope', '$locatio
};
uiService.showFormError = function(elem, result, opt_placement) {
var message = result.data['message'] || result.data['error_description'] || '';
var message = ApiService.getErrorMessage(result, 'error');
if (message) {
uiService.showPopover(elem, message, opt_placement || 'bottom');
} else {

View file

@ -44,6 +44,7 @@ from endpoints.api.logs import UserLogs, OrgLogs, OrgAggregateLogs, UserAggregat
from endpoints.api.billing import (UserCard, UserPlan, ListPlans, OrganizationCard,
OrganizationPlan)
from endpoints.api.discovery import DiscoveryResource
from endpoints.api.error import Error
from endpoints.api.organization import (OrganizationList, OrganizationMember,
OrgPrivateRepositories, OrganizationMemberList,
Organization, ApplicationInformation,
@ -236,6 +237,14 @@ class TestDiscovery(ApiTestCase):
assert 'paths' in json
class TestErrorDescription(ApiTestCase):
def test_get_error(self):
json = self.getJsonResponse(Error, params=dict(error_type='not_found'))
assert json['title'] == 'not_found'
assert 'type' in json
assert 'description' in json
class TestPlans(ApiTestCase):
def test_plans(self):
json = self.getJsonResponse(ListPlans)
@ -372,7 +381,7 @@ class TestConvertToOrganization(ApiTestCase):
'plan': 'free'},
expected_code=400)
self.assertEqual('The admin user is not valid', json['message'])
self.assertEqual('The admin user is not valid', json['detail'])
def test_sameadminuser_by_email(self):
self.login(READ_ACCESS_USER)
@ -382,7 +391,7 @@ class TestConvertToOrganization(ApiTestCase):
'plan': 'free'},
expected_code=400)
self.assertEqual('The admin user is not valid', json['message'])
self.assertEqual('The admin user is not valid', json['detail'])
def test_invalidadminuser(self):
self.login(READ_ACCESS_USER)
@ -393,7 +402,7 @@ class TestConvertToOrganization(ApiTestCase):
expected_code=400)
self.assertEqual('The admin user credentials are not valid',
json['message'])
json['detail'])
def test_invalidadminpassword(self):
self.login(READ_ACCESS_USER)
@ -404,7 +413,7 @@ class TestConvertToOrganization(ApiTestCase):
expected_code=400)
self.assertEqual('The admin user credentials are not valid',
json['message'])
json['detail'])
def test_convert(self):
self.login(READ_ACCESS_USER)
@ -489,7 +498,7 @@ class TestCreateNewUser(ApiTestCase):
email='test@example.com'),
expected_code=400)
self.assertEquals('The username already exists', json['message'])
self.assertEquals('The username already exists', json['detail'])
def test_trycreatetooshort(self):
json = self.postJsonResponse(User,
@ -499,7 +508,7 @@ class TestCreateNewUser(ApiTestCase):
expected_code=400)
self.assertEquals('Invalid username a: Username must be between 4 and 30 characters in length',
json['error_description'])
json['detail'])
def test_trycreateregexmismatch(self):
json = self.postJsonResponse(User,
@ -509,7 +518,7 @@ class TestCreateNewUser(ApiTestCase):
expected_code=400)
self.assertEquals('Invalid username auserName: Username must match expression [a-z0-9_]+',
json['error_description'])
json['detail'])
def test_createuser(self):
data = self.postJsonResponse(User, data=NEW_USER_DETAILS, expected_code=200)
@ -740,7 +749,7 @@ class TestCreateOrganization(ApiTestCase):
expected_code=400)
self.assertEquals('A user or organization with this name already exists',
json['message'])
json['detail'])
def test_existingorg(self):
self.login(ADMIN_ACCESS_USER)
@ -750,7 +759,7 @@ class TestCreateOrganization(ApiTestCase):
expected_code=400)
self.assertEquals('A user or organization with this name already exists',
json['message'])
json['detail'])
def test_createorg(self):
self.login(ADMIN_ACCESS_USER)
@ -845,7 +854,7 @@ class TestCreateOrganizationPrototypes(ApiTestCase):
delegate={'kind': 'team', 'name': 'owners'}),
expected_code=400)
self.assertEquals('Unknown activating user', json['message'])
self.assertEquals('Unknown activating user', json['detail'])
def test_missingdelegate(self):
@ -1330,7 +1339,7 @@ class TestCreateRepo(ApiTestCase):
description=''),
expected_code=400)
self.assertEquals('Invalid repository name', json['error_description'])
self.assertEquals('Invalid repository name', json['detail'])
def test_duplicaterepo(self):
self.login(ADMIN_ACCESS_USER)
@ -1341,7 +1350,7 @@ class TestCreateRepo(ApiTestCase):
description=''),
expected_code=400)
self.assertEquals('Repository already exists', json['message'])
self.assertEquals('Repository already exists', json['detail'])
def test_createrepo(self):