Create endpoint for listing of all uploads id without auth
Add new endpoint in refstack's API for listing uploads id, their creation data and cpid. Add pagination for results. Makes it possible to specify number of page in url base_url/v1/results?page=<page number> and other filtering parameters. If page not specified, then by default page equals 1. Add unit and functional tests. Change-Id: Ie57df6b4c2607679b5d2746821b6753fc672d7c3 Implement: https://storyboard.openstack.org/#!/story/2000200 Implement: https://github.com/stackforge/refstack/blob/master/specs/proposed/test-record-api.rst
This commit is contained in:
parent
5d1d1cfb3e
commit
4ab98f7d8e
|
@ -89,5 +89,9 @@ gunicorn:
|
|||
|
||||
- `refstack-api --env REFSTACK_OSLO_CONFIG=/path/to/refstack.conf`
|
||||
|
||||
Now available http://localhost:8000/ with JSON response {'Root': 'OK'}
|
||||
and http://localhost:8000/v1/results/ with JSON response {'Results': 'OK'}.
|
||||
Now available:
|
||||
|
||||
- http://localhost:8000/ with JSON response {'Root': 'OK'};
|
||||
- http://localhost:8000/v1/results with response JSON including records consisted of <upload id>, <upload date> and <cloud cpid> of the test runs. The default response is limited to one page of the most recent uploaded test run records. The number of records per page is configurable via the Refstack configuration file. Filtering parameters such as page, start_date, end_date ... can also be used to specify the desired records. For example: get http://localhost:8000/v1/results?page=n will return page n of the data.
|
||||
|
||||
- http://localhost:8000/v1/results/<upload id> with response JSON including the detail test results of the specified <upload id>
|
||||
|
|
|
@ -70,7 +70,7 @@
|
|||
#logging_exception_prefix = %(asctime)s.%(msecs)03d %(process)d TRACE %(name)s %(instance)s
|
||||
|
||||
# List of logger=LEVEL pairs. (list value)
|
||||
#default_log_levels = amqp=WARN,amqplib=WARN,boto=WARN,qpid=WARN,sqlalchemy=WARN,suds=INFO,oslo.messaging=INFO,iso8601=WARN,requests.packages.urllib3.connectionpool=WARN,urllib3.connectionpool=WARN,websocket=WARN,keystonemiddleware=WARN,routes.middleware=WARN,stevedore=WARN
|
||||
#default_log_levels = amqp=WARN,amqplib=WARN,boto=WARN,qpid=WARN,sqlalchemy=WARN,suds=INFO,oslo.messaging=INFO,iso8601=WARN,requests.packages.urllib3.connectionpool=WARN,urllib3.connectionpool=WARN,websocket=WARN,requests.packages.urllib3.util.retry=WARN,urllib3.util.retry=WARN,keystonemiddleware=WARN,routes.middleware=WARN,stevedore=WARN
|
||||
|
||||
# Enables or disables publication of error events. (boolean value)
|
||||
#publish_errors = false
|
||||
|
@ -122,6 +122,12 @@
|
|||
# (boolean value)
|
||||
#app_dev_mode = false
|
||||
|
||||
# Number of results for one page (integer value)
|
||||
#results_per_page = 20
|
||||
|
||||
# The format for start_date and end_date parameters (string value)
|
||||
#input_date_format = %Y-%m-%d %H:%M:%S
|
||||
|
||||
|
||||
[database]
|
||||
|
||||
|
@ -212,17 +218,18 @@
|
|||
# lost. (boolean value)
|
||||
#use_db_reconnect = false
|
||||
|
||||
# Seconds between database connection retries. (integer value)
|
||||
# Seconds between retries of a database transaction. (integer value)
|
||||
#db_retry_interval = 1
|
||||
|
||||
# If True, increases the interval between database connection retries
|
||||
# up to db_max_retry_interval. (boolean value)
|
||||
# If True, increases the interval between retries of a database
|
||||
# operation up to db_max_retry_interval. (boolean value)
|
||||
#db_inc_retry_interval = true
|
||||
|
||||
# If db_inc_retry_interval is set, the maximum seconds between
|
||||
# database connection retries. (integer value)
|
||||
# If db_inc_retry_interval is set, the maximum seconds between retries
|
||||
# of a database operation. (integer value)
|
||||
#db_max_retry_interval = 10
|
||||
|
||||
# Maximum database connection retries before error is raised. Set to
|
||||
# -1 to specify an infinite retry count. (integer value)
|
||||
# Maximum retries in case of connection error or deadlock error before
|
||||
# error is raised. Set to -1 to specify an infinite retry count.
|
||||
# (integer value)
|
||||
#db_max_retries = 20
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
# Copyright (c) 2015 Mirantis, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
"""
|
||||
Constants for Refstack API
|
||||
"""
|
||||
|
||||
# Names of input parameters for request
|
||||
START_DATE = 'start_date'
|
||||
END_DATE = 'end_date'
|
||||
CPID = 'cpid'
|
||||
PAGE = 'page'
|
|
@ -14,17 +14,36 @@
|
|||
# under the License.
|
||||
|
||||
"""Version 1 of the API."""
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
import pecan
|
||||
from pecan import rest
|
||||
|
||||
from refstack import db
|
||||
from refstack.api import constants as const
|
||||
from refstack.api import utils as api_utils
|
||||
from refstack.common import validators
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
|
||||
CTRLS_OPTS = [
|
||||
cfg.IntOpt('results_per_page',
|
||||
default=20,
|
||||
help='Number of results for one page'),
|
||||
cfg.StrOpt('input_date_format',
|
||||
default='%Y-%m-%d %H:%M:%S',
|
||||
help='The format for %(start)s and %(end)s parameters' % {
|
||||
'start': const.START_DATE,
|
||||
'end': const.END_DATE
|
||||
})
|
||||
]
|
||||
|
||||
class RestControllerWithValidation(rest.RestController):
|
||||
CONF = cfg.CONF
|
||||
|
||||
CONF.register_opts(CTRLS_OPTS, group='api')
|
||||
|
||||
|
||||
class BaseRestControllerWithValidation(rest.RestController):
|
||||
|
||||
"""
|
||||
Controller provides validation for POSTed data
|
||||
|
@ -68,7 +87,7 @@ class RestControllerWithValidation(rest.RestController):
|
|||
return item_id
|
||||
|
||||
|
||||
class ResultsController(RestControllerWithValidation):
|
||||
class ResultsController(BaseRestControllerWithValidation):
|
||||
|
||||
"""/v1/results handler."""
|
||||
|
||||
|
@ -89,6 +108,61 @@ class ResultsController(RestControllerWithValidation):
|
|||
test_id = db.store_results(item_in_json)
|
||||
return {'test_id': test_id}
|
||||
|
||||
@pecan.expose('json')
|
||||
def get(self):
|
||||
"""
|
||||
Get information of all uploaded test results in descending
|
||||
chronological order.
|
||||
Make it possible to specify some input parameters
|
||||
for filtering.
|
||||
For example:
|
||||
/v1/results?page=<page number>&cpid=1234.
|
||||
By default, page is set to page number 1,
|
||||
if the page parameter is not specified.
|
||||
"""
|
||||
|
||||
expected_input_params = [
|
||||
const.START_DATE,
|
||||
const.END_DATE,
|
||||
const.CPID,
|
||||
]
|
||||
|
||||
try:
|
||||
filters = api_utils.parse_input_params(expected_input_params)
|
||||
records_count = db.get_test_records_count(filters)
|
||||
page_number, total_pages_number = \
|
||||
api_utils.get_page_number(records_count)
|
||||
except api_utils.ParseInputsError as ex:
|
||||
pecan.abort(400, 'Reason: %s' % ex)
|
||||
except Exception as ex:
|
||||
LOG.debug('An error occurred: %s' % ex)
|
||||
pecan.abort(500)
|
||||
|
||||
try:
|
||||
per_page = CONF.api.results_per_page
|
||||
records = db.get_test_records(page_number, per_page, filters)
|
||||
|
||||
results = []
|
||||
for r in records:
|
||||
results.append({
|
||||
'test_id': r.id,
|
||||
'created_at': r.created_at,
|
||||
'cpid': r.cpid
|
||||
})
|
||||
|
||||
page = {}
|
||||
page['results'] = results
|
||||
page['pagination'] = {
|
||||
'current_page': page_number,
|
||||
'total_pages': total_pages_number
|
||||
}
|
||||
except Exception as ex:
|
||||
LOG.debug('An error occurred during '
|
||||
'operation with database: %s' % ex)
|
||||
pecan.abort(400)
|
||||
|
||||
return page
|
||||
|
||||
|
||||
class V1Controller(object):
|
||||
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
# Copyright (c) 2015 Mirantis, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Refstack API's utils."""
|
||||
import copy
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log
|
||||
from oslo_utils import timeutils
|
||||
import pecan
|
||||
|
||||
from refstack.api import constants as const
|
||||
|
||||
LOG = log.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
class ParseInputsError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def _get_input_params_from_request(expected_params):
|
||||
"""Get input parameters from request.
|
||||
|
||||
:param expecred_params: (array) Expected input
|
||||
params specified in constants.
|
||||
"""
|
||||
filters = {}
|
||||
for param in expected_params:
|
||||
value = pecan.request.GET.get(param)
|
||||
if value is not None:
|
||||
filters[param] = value
|
||||
LOG.debug('Parameter %(param)s has been received '
|
||||
'with value %(value)s' % {
|
||||
'param': param,
|
||||
'value': value
|
||||
})
|
||||
return filters
|
||||
|
||||
|
||||
def parse_input_params(expected_input_params):
|
||||
"""Parse input parameters from request.
|
||||
|
||||
:param expecred_params: (array) Expected input
|
||||
params specified in constants.
|
||||
|
||||
"""
|
||||
raw_filters = _get_input_params_from_request(expected_input_params)
|
||||
filters = copy.deepcopy(raw_filters)
|
||||
date_fmt = CONF.api.input_date_format
|
||||
|
||||
for key, value in filters.items():
|
||||
if key == const.START_DATE or key == const.END_DATE:
|
||||
try:
|
||||
filters[key] = timeutils.parse_strtime(value, date_fmt)
|
||||
except (ValueError, TypeError) as exc:
|
||||
raise ParseInputsError('Invalid date format: %(exc)s'
|
||||
% {'exc': exc})
|
||||
|
||||
start_date = filters.get(const.START_DATE)
|
||||
end_date = filters.get(const.END_DATE)
|
||||
if start_date and end_date:
|
||||
if start_date > end_date:
|
||||
raise ParseInputsError('Invalid dates: %(start)s '
|
||||
'more than %(end)s' % {
|
||||
'start': const.START_DATE,
|
||||
'end': const.END_DATE
|
||||
})
|
||||
return filters
|
||||
|
||||
|
||||
def _calculate_pages_number(per_page, records_count):
|
||||
"""Return pages number.
|
||||
:param per_page: (int) results number fot one page.
|
||||
:param records_count: (int) total records count.
|
||||
"""
|
||||
quotient, remainder = divmod(records_count, per_page)
|
||||
if remainder > 0:
|
||||
return quotient + 1
|
||||
return quotient
|
||||
|
||||
|
||||
def get_page_number(records_count):
|
||||
"""Get page number from request
|
||||
:param records_count: (int) total records count.
|
||||
"""
|
||||
page_number = pecan.request.GET.get(const.PAGE)
|
||||
per_page = CONF.api.results_per_page
|
||||
|
||||
total_pages = _calculate_pages_number(per_page, records_count)
|
||||
# The first page exists in any case
|
||||
if page_number is None:
|
||||
return (1, total_pages)
|
||||
try:
|
||||
page_number = int(page_number)
|
||||
except (ValueError, TypeError):
|
||||
raise ParseInputsError('Invalid page number: The page number can not '
|
||||
'be converted to an integer')
|
||||
|
||||
if page_number == 1:
|
||||
return (page_number, total_pages)
|
||||
|
||||
if page_number <= 0:
|
||||
raise ParseInputsError('Invalid page number: '
|
||||
'The page number less or equal zero.')
|
||||
|
||||
if page_number > total_pages:
|
||||
raise ParseInputsError('Invalid page number: The page number '
|
||||
'is greater than the total number of pages.')
|
||||
|
||||
return (page_number, total_pages)
|
|
@ -61,3 +61,21 @@ def get_test_results(test_id):
|
|||
:param test_id: The ID of the test.
|
||||
"""
|
||||
return IMPL.get_test_results(test_id)
|
||||
|
||||
|
||||
def get_test_records(page_number, per_page, filters):
|
||||
"""Get page with applied filters for uploaded test records.
|
||||
|
||||
:param page_number: The number of page.
|
||||
:param per_page: The number of results for one page.
|
||||
:param filters: (Dict) Filters that will be applied for records.
|
||||
"""
|
||||
return IMPL.get_test_records(page_number, per_page, filters)
|
||||
|
||||
|
||||
def get_test_records_count(filters):
|
||||
"""Get total pages number with applied filters for uploaded test records.
|
||||
|
||||
:param filters: (Dict) Filters that will be applied for records.
|
||||
"""
|
||||
return IMPL.get_test_records_count(filters)
|
||||
|
|
|
@ -22,6 +22,7 @@ from oslo_config import cfg
|
|||
from oslo_db import options as db_options
|
||||
from oslo_db.sqlalchemy import session as db_session
|
||||
|
||||
from refstack.api import constants as api_const
|
||||
from refstack.db.sqlalchemy import models
|
||||
|
||||
|
||||
|
@ -92,3 +93,40 @@ def get_test_results(test_id):
|
|||
filter_by(test_id=test_id).\
|
||||
all()
|
||||
return results
|
||||
|
||||
|
||||
def _apply_filters_for_query(query, filters):
|
||||
start_date = filters.get(api_const.START_DATE)
|
||||
if start_date:
|
||||
query = query.filter(models.Test.created_at >= start_date)
|
||||
|
||||
end_date = filters.get(api_const.END_DATE)
|
||||
if end_date:
|
||||
query = query.filter(models.Test.created_at <= end_date)
|
||||
|
||||
cpid = filters.get(api_const.CPID)
|
||||
if cpid:
|
||||
query = query.filter(models.Test.cpid == cpid)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
def get_test_records(page, per_page, filters):
|
||||
session = get_session()
|
||||
query = session.query(models.Test.id,
|
||||
models.Test.created_at,
|
||||
models.Test.cpid)
|
||||
|
||||
query = _apply_filters_for_query(query, filters)
|
||||
results = query.order_by(models.Test.created_at.desc()).\
|
||||
offset(per_page * (page - 1)).\
|
||||
limit(per_page)
|
||||
return results
|
||||
|
||||
|
||||
def get_test_records_count(filters):
|
||||
session = get_session()
|
||||
query = session.query(models.Test.id)
|
||||
records_count = _apply_filters_for_query(query, filters).count()
|
||||
|
||||
return records_count
|
||||
|
|
|
@ -17,11 +17,8 @@
|
|||
SQLAlchemy models for Refstack data.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_db.sqlalchemy import models
|
||||
from oslo_utils import timeutils
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import orm
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
|
@ -30,24 +27,15 @@ CONF = cfg.CONF
|
|||
BASE = declarative_base()
|
||||
|
||||
|
||||
class RefStackBase(models.ModelBase, models.TimestampMixin):
|
||||
class RefStackBase(models.ModelBase,
|
||||
models.TimestampMixin,
|
||||
models.SoftDeleteMixin):
|
||||
|
||||
"""Base class for RefStack Models."""
|
||||
|
||||
__table_args__ = {'mysql_engine': 'InnoDB'}
|
||||
created_at = sa.Column(sa.DateTime(), default=datetime.datetime.utcnow,
|
||||
nullable=False)
|
||||
updated_at = sa.Column(sa.DateTime())
|
||||
deleted_at = sa.Column(sa.DateTime)
|
||||
deleted = sa.Column(sa.Integer, default=0)
|
||||
metadata = None
|
||||
|
||||
def delete(self, session=None):
|
||||
"""Delete this object."""
|
||||
self.deleted = self.id
|
||||
self.deleted_at = timeutils.utcnow()
|
||||
self.save(session=session)
|
||||
|
||||
|
||||
class Test(BASE, RefStackBase):
|
||||
|
||||
|
|
|
@ -31,7 +31,10 @@
|
|||
]
|
||||
...
|
||||
"""
|
||||
import itertools
|
||||
|
||||
import refstack.api.app
|
||||
import refstack.api.controllers.v1
|
||||
import refstack.db.api
|
||||
|
||||
|
||||
|
@ -39,5 +42,6 @@ def list_opts():
|
|||
return [
|
||||
# Keep a list in alphabetical order
|
||||
('DEFAULT', refstack.db.api.db_opts),
|
||||
('api', refstack.api.app.API_OPTS),
|
||||
('api', itertools.chain(refstack.api.app.API_OPTS,
|
||||
refstack.api.controllers.v1.CTRLS_OPTS)),
|
||||
]
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
import json
|
||||
import uuid
|
||||
|
||||
from oslo_config import fixture as config_fixture
|
||||
import six
|
||||
import webtest.app
|
||||
|
||||
|
@ -52,6 +53,11 @@ class TestResultsController(api.FunctionalTest):
|
|||
|
||||
URL = '/v1/results/'
|
||||
|
||||
def setUp(self):
|
||||
super(TestResultsController, self).setUp()
|
||||
self.config_fixture = config_fixture.Config()
|
||||
self.CONF = self.useFixture(self.config_fixture).conf
|
||||
|
||||
def test_post(self):
|
||||
"""Test results endpoint with post request."""
|
||||
results = json.dumps(FAKE_TESTS_RESULT)
|
||||
|
@ -103,3 +109,114 @@ class TestResultsController(api.FunctionalTest):
|
|||
self.assertRaises(webtest.app.AppError,
|
||||
self.get_json,
|
||||
self.URL + 'fake_url')
|
||||
|
||||
def test_get_pagination(self):
|
||||
self.CONF.set_override('results_per_page',
|
||||
2,
|
||||
'api')
|
||||
|
||||
responses = []
|
||||
for i in range(3):
|
||||
fake_results = {
|
||||
'cpid': six.text_type(i),
|
||||
'duration_seconds': i,
|
||||
'results': [
|
||||
{'name': 'tempest.foo.bar'},
|
||||
{'name': 'tempest.buzz'}
|
||||
]
|
||||
}
|
||||
actual_response = self.post_json(self.URL,
|
||||
params=json.dumps(fake_results))
|
||||
responses.append(actual_response)
|
||||
|
||||
page_one = self.get_json(self.URL)
|
||||
page_two = self.get_json('/v1/results?page=2')
|
||||
|
||||
self.assertEqual(len(page_one['results']), 2)
|
||||
self.assertEqual(len(page_two['results']), 1)
|
||||
self.assertNotIn(page_two['results'][0], page_one)
|
||||
|
||||
self.assertEqual(page_one['pagination']['current_page'], 1)
|
||||
self.assertEqual(page_one['pagination']['total_pages'], 2)
|
||||
|
||||
self.assertEqual(page_two['pagination']['current_page'], 2)
|
||||
self.assertEqual(page_two['pagination']['total_pages'], 2)
|
||||
|
||||
def test_get_with_not_existing_page(self):
|
||||
self.assertRaises(webtest.app.AppError,
|
||||
self.get_json,
|
||||
'/v1/results?page=2')
|
||||
|
||||
def test_get_with_empty_database(self):
|
||||
results = self.get_json(self.URL)
|
||||
self.assertEqual(results, [])
|
||||
|
||||
def test_get_with_cpid_filter(self):
|
||||
self.CONF.set_override('results_per_page',
|
||||
2,
|
||||
'api')
|
||||
|
||||
responses = []
|
||||
for i in range(2):
|
||||
fake_results = {
|
||||
'cpid': '12345',
|
||||
'duration_seconds': i,
|
||||
'results': [
|
||||
{'name': 'tempest.foo'},
|
||||
{'name': 'tempest.bar'}
|
||||
]
|
||||
}
|
||||
json_result = json.dumps(fake_results)
|
||||
actual_response = self.post_json(self.URL,
|
||||
params=json_result)
|
||||
responses.append(actual_response)
|
||||
|
||||
for i in range(3):
|
||||
fake_results = {
|
||||
'cpid': '54321',
|
||||
'duration_seconds': i,
|
||||
'results': [
|
||||
{'name': 'tempest.foo'},
|
||||
{'name': 'tempest.bar'}
|
||||
]
|
||||
}
|
||||
|
||||
results = self.get_json('/v1/results?page=1&cpid=12345')
|
||||
self.asserEqual(len(results), 2)
|
||||
|
||||
for r in results:
|
||||
self.assertIn(r['test_id'], responses)
|
||||
|
||||
def test_get_with_date_filters(self):
|
||||
self.CONF.set_override('results_per_page',
|
||||
10,
|
||||
'api')
|
||||
|
||||
responses = []
|
||||
for i in range(5):
|
||||
fake_results = {
|
||||
'cpid': '12345',
|
||||
'duration_seconds': i,
|
||||
'results': [
|
||||
{'name': 'tempest.foo'},
|
||||
{'name': 'tempest.bar'}
|
||||
]
|
||||
}
|
||||
json_result = json.dumps(fake_results)
|
||||
actual_response = self.post_json(self.URL,
|
||||
params=json_result)
|
||||
responses.append(actual_response)
|
||||
|
||||
all_results = self.get_json(self.URL)
|
||||
|
||||
slice_results = all_results[1:3]
|
||||
|
||||
url = 'v1/results?start_date=%(start)s&end_date=%(end)s' % {
|
||||
'start': slice_results[2]['created_at'],
|
||||
'end': slice_results[0]['created_at']
|
||||
}
|
||||
|
||||
filtering_results = self.get_json(url)
|
||||
self.assertEqual(len(filtering_results), 3)
|
||||
for r in slice_results:
|
||||
self.assertEqual(r, filtering_results)
|
||||
|
|
|
@ -16,8 +16,11 @@
|
|||
"""Tests for API's controllers"""
|
||||
|
||||
import mock
|
||||
from oslo_config import fixture as config_fixture
|
||||
from oslotest import base
|
||||
|
||||
from refstack.api import constants as const
|
||||
from refstack.api import utils as api_utils
|
||||
from refstack.api.controllers import root
|
||||
from refstack.api.controllers import v1
|
||||
|
||||
|
@ -36,6 +39,8 @@ class ResultsControllerTestCase(base.BaseTestCase):
|
|||
super(ResultsControllerTestCase, self).setUp()
|
||||
self.validator = mock.Mock()
|
||||
self.controller = v1.ResultsController(self.validator)
|
||||
self.config_fixture = config_fixture.Config()
|
||||
self.CONF = self.useFixture(self.config_fixture).conf
|
||||
|
||||
@mock.patch('refstack.db.get_test')
|
||||
@mock.patch('refstack.db.get_test_results')
|
||||
|
@ -115,13 +120,123 @@ class ResultsControllerTestCase(base.BaseTestCase):
|
|||
self.assertEqual(result, {'test_id': 'fake_result'})
|
||||
mock_store_item.assert_called_once_with('fake_item')
|
||||
|
||||
@mock.patch('pecan.abort')
|
||||
@mock.patch('refstack.api.utils.parse_input_params')
|
||||
def test_get_failed_in_parse_input_params(self,
|
||||
parse_inputs,
|
||||
pecan_abort):
|
||||
|
||||
class RestControllerWithValidationTestCase(base.BaseTestCase):
|
||||
parse_inputs.side_effect = api_utils.ParseInputsError()
|
||||
pecan_abort.side_effect = Exception()
|
||||
self.assertRaises(Exception,
|
||||
self.controller.get)
|
||||
|
||||
@mock.patch('refstack.db.get_test_records_count')
|
||||
@mock.patch('pecan.abort')
|
||||
@mock.patch('refstack.api.utils.parse_input_params')
|
||||
def test_get_failed_in_get_test_records_number(self,
|
||||
parse_inputs,
|
||||
pecan_abort,
|
||||
db_get_count):
|
||||
db_get_count.side_effect = Exception()
|
||||
pecan_abort.side_effect = Exception()
|
||||
self.assertRaises(Exception,
|
||||
self.controller.get)
|
||||
|
||||
@mock.patch('refstack.db.get_test_records_count')
|
||||
@mock.patch('refstack.api.utils.parse_input_params')
|
||||
@mock.patch('refstack.api.utils.get_page_number')
|
||||
@mock.patch('pecan.abort')
|
||||
def test_get_failed_in_get_page_number(self,
|
||||
pecan_abort,
|
||||
get_page,
|
||||
parse_input,
|
||||
db_get_count):
|
||||
|
||||
get_page.side_effect = api_utils.ParseInputsError()
|
||||
pecan_abort.side_effect = Exception()
|
||||
self.assertRaises(Exception,
|
||||
self.controller.get)
|
||||
|
||||
@mock.patch('refstack.db.get_test_records')
|
||||
@mock.patch('refstack.db.get_test_records_count')
|
||||
@mock.patch('refstack.api.utils.parse_input_params')
|
||||
@mock.patch('refstack.api.utils.get_page_number')
|
||||
@mock.patch('pecan.abort')
|
||||
def test_get_failed_in_get_test_records(self,
|
||||
pecan_abort,
|
||||
get_page,
|
||||
parce_input,
|
||||
db_get_count,
|
||||
db_get_test):
|
||||
|
||||
get_page.return_value = (mock.Mock(), mock.Mock())
|
||||
db_get_test.side_effect = Exception()
|
||||
pecan_abort.side_effect = Exception()
|
||||
self.assertRaises(Exception,
|
||||
self.controller.get)
|
||||
|
||||
@mock.patch('refstack.db.get_test_records')
|
||||
@mock.patch('refstack.db.get_test_records_count')
|
||||
@mock.patch('refstack.api.utils.get_page_number')
|
||||
@mock.patch('refstack.api.utils.parse_input_params')
|
||||
def test_get_success(self,
|
||||
parse_input,
|
||||
get_page,
|
||||
get_test_count,
|
||||
db_get_test):
|
||||
|
||||
expected_input_params = [
|
||||
const.START_DATE,
|
||||
const.END_DATE,
|
||||
const.CPID,
|
||||
]
|
||||
page_number = 1
|
||||
total_pages_number = 10
|
||||
per_page = 5
|
||||
records_count = 50
|
||||
get_test_count.return_value = records_count
|
||||
get_page.return_value = (page_number, total_pages_number)
|
||||
self.CONF.set_override('results_per_page',
|
||||
per_page,
|
||||
'api')
|
||||
|
||||
record = mock.Mock()
|
||||
record.id = 111
|
||||
record.created_at = '12345'
|
||||
record.cpid = '54321'
|
||||
|
||||
db_get_test.return_value = [record]
|
||||
expected_result = {
|
||||
'results': [{
|
||||
'test_id': record.id,
|
||||
'created_at': record.created_at,
|
||||
'cpid': record.cpid
|
||||
}],
|
||||
'pagination': {
|
||||
'current_page': page_number,
|
||||
'total_pages': total_pages_number
|
||||
}
|
||||
}
|
||||
|
||||
actual_result = self.controller.get()
|
||||
self.assertEqual(actual_result, expected_result)
|
||||
|
||||
parse_input.assert_called_once_with(expected_input_params)
|
||||
|
||||
filters = parse_input.return_value
|
||||
get_test_count.assert_called_once_with(filters)
|
||||
get_page.assert_called_once_with(records_count)
|
||||
|
||||
db_get_test.assert_called_once_with(page_number, per_page, filters)
|
||||
|
||||
|
||||
class BaseRestControllerWithValidationTestCase(base.BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(RestControllerWithValidationTestCase, self).setUp()
|
||||
super(BaseRestControllerWithValidationTestCase, self).setUp()
|
||||
self.validator = mock.Mock()
|
||||
self.controller = v1.RestControllerWithValidation(self.validator)
|
||||
self.controller = v1.BaseRestControllerWithValidation(self.validator)
|
||||
|
||||
@mock.patch('pecan.response')
|
||||
@mock.patch('refstack.common.validators.safe_load_json_body')
|
||||
|
|
|
@ -0,0 +1,257 @@
|
|||
# Copyright (c) 2015 Mirantis, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Tests for API's utils"""
|
||||
|
||||
import mock
|
||||
from oslo_config import fixture as config_fixture
|
||||
from oslo_utils import timeutils
|
||||
from oslotest import base
|
||||
|
||||
from refstack.api import constants as const
|
||||
from refstack.api import utils as api_utils
|
||||
|
||||
|
||||
class APIUtilsTestCase(base.BaseTestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(APIUtilsTestCase, self).setUp()
|
||||
self.config_fixture = config_fixture.Config()
|
||||
self.CONF = self.useFixture(self.config_fixture).conf
|
||||
|
||||
@mock.patch('pecan.request')
|
||||
def test_get_input_params_from_request_all_results(self, mock_request):
|
||||
received_params = {
|
||||
const.START_DATE: '2015-03-26 15:04:40',
|
||||
const.END_DATE: '2015-03-26 15:04:45',
|
||||
const.CPID: '12345',
|
||||
}
|
||||
|
||||
expected_params = [
|
||||
const.START_DATE,
|
||||
const.END_DATE,
|
||||
const.CPID
|
||||
]
|
||||
|
||||
mock_request.GET = received_params
|
||||
|
||||
result = api_utils._get_input_params_from_request(expected_params)
|
||||
|
||||
self.assertEqual(result, received_params)
|
||||
|
||||
@mock.patch('pecan.request')
|
||||
def test_get_input_params_from_request_partial_results(self,
|
||||
mock_request):
|
||||
received_params = {
|
||||
const.START_DATE: '2015-03-26 15:04:40',
|
||||
const.END_DATE: '2015-03-26 15:04:45',
|
||||
const.CPID: '12345',
|
||||
}
|
||||
|
||||
expected_params = [
|
||||
const.START_DATE,
|
||||
const.END_DATE,
|
||||
]
|
||||
|
||||
expected_results = {
|
||||
const.START_DATE: '2015-03-26 15:04:40',
|
||||
const.END_DATE: '2015-03-26 15:04:45',
|
||||
}
|
||||
|
||||
mock_request.GET = received_params
|
||||
|
||||
result = api_utils._get_input_params_from_request(expected_params)
|
||||
|
||||
self.assertEqual(result, expected_results)
|
||||
|
||||
@mock.patch('oslo_utils.timeutils.parse_strtime')
|
||||
@mock.patch.object(api_utils, '_get_input_params_from_request')
|
||||
def test_parse_input_params_failed_in_parse_time(self, mock_get_input,
|
||||
mock_strtime):
|
||||
fmt = '%Y-%m-%d %H:%M:%S'
|
||||
self.CONF.set_override('input_date_format',
|
||||
fmt,
|
||||
'api')
|
||||
raw_filters = {
|
||||
const.START_DATE: '2015-03-26 15:04:40',
|
||||
const.END_DATE: '2015-03-26 15:04:45',
|
||||
const.CPID: '12345',
|
||||
}
|
||||
|
||||
expected_params = mock.Mock()
|
||||
mock_get_input.return_value = raw_filters
|
||||
mock_strtime.side_effect = ValueError()
|
||||
self.assertRaises(api_utils.ParseInputsError,
|
||||
api_utils.parse_input_params,
|
||||
expected_params)
|
||||
|
||||
@mock.patch.object(api_utils, '_get_input_params_from_request')
|
||||
def test_parse_input_params_failed_in_compare_date(self, mock_get_input):
|
||||
fmt = '%Y-%m-%d %H:%M:%S'
|
||||
self.CONF.set_override('input_date_format',
|
||||
fmt,
|
||||
'api')
|
||||
raw_filters = {
|
||||
const.START_DATE: '2015-03-26 15:04:50',
|
||||
const.END_DATE: '2015-03-26 15:04:40',
|
||||
const.CPID: '12345',
|
||||
}
|
||||
|
||||
expected_params = mock.Mock()
|
||||
mock_get_input.return_value = raw_filters
|
||||
self.assertRaises(api_utils.ParseInputsError,
|
||||
api_utils.parse_input_params,
|
||||
expected_params)
|
||||
|
||||
@mock.patch.object(api_utils, '_get_input_params_from_request')
|
||||
def test_parse_input_params_success(self, mock_get_input):
|
||||
fmt = '%Y-%m-%d %H:%M:%S'
|
||||
self.CONF.set_override('input_date_format',
|
||||
fmt,
|
||||
'api')
|
||||
raw_filters = {
|
||||
const.START_DATE: '2015-03-26 15:04:40',
|
||||
const.END_DATE: '2015-03-26 15:04:50',
|
||||
const.CPID: '12345',
|
||||
}
|
||||
|
||||
expected_params = mock.Mock()
|
||||
mock_get_input.return_value = raw_filters
|
||||
|
||||
parsed_start_date = timeutils.parse_strtime(
|
||||
raw_filters[const.START_DATE],
|
||||
fmt
|
||||
)
|
||||
|
||||
parsed_end_date = timeutils.parse_strtime(
|
||||
raw_filters[const.END_DATE],
|
||||
fmt
|
||||
)
|
||||
|
||||
expected_result = {
|
||||
const.START_DATE: parsed_start_date,
|
||||
const.END_DATE: parsed_end_date,
|
||||
const.CPID: '12345'
|
||||
}
|
||||
|
||||
result = api_utils.parse_input_params(expected_params)
|
||||
self.assertEqual(result, expected_result)
|
||||
|
||||
mock_get_input.assert_called_once_with(expected_params)
|
||||
|
||||
def test_calculate_pages_number_full_pages(self):
|
||||
# expected pages number: 20/10 = 2
|
||||
page_number = api_utils._calculate_pages_number(10, 20)
|
||||
self.assertEqual(page_number, 2)
|
||||
|
||||
def test_calculate_pages_number_half_page(self):
|
||||
# expected pages number: 25/10
|
||||
# => quotient == 2 and remainder == 5
|
||||
# => total number of pages == 3
|
||||
page_number = api_utils._calculate_pages_number(10, 25)
|
||||
self.assertEqual(page_number, 3)
|
||||
|
||||
@mock.patch('pecan.request')
|
||||
def test_get_page_number_page_number_is_none(self, mock_request):
|
||||
per_page = 20
|
||||
total_records = 100
|
||||
self.CONF.set_override('results_per_page',
|
||||
per_page,
|
||||
'api')
|
||||
mock_request.GET = {
|
||||
const.PAGE: None
|
||||
}
|
||||
|
||||
page_number, total_pages = api_utils.get_page_number(total_records)
|
||||
|
||||
self.assertEqual(page_number, 1)
|
||||
self.assertEqual(total_pages, total_records / per_page)
|
||||
|
||||
@mock.patch('pecan.request')
|
||||
def test_get_page_number_page_number_not_int(self, mock_request):
|
||||
per_page = 20
|
||||
total_records = 100
|
||||
self.CONF.set_override('results_per_page',
|
||||
per_page,
|
||||
'api')
|
||||
mock_request.GET = {
|
||||
const.PAGE: 'abc'
|
||||
}
|
||||
|
||||
self.assertRaises(api_utils.ParseInputsError,
|
||||
api_utils.get_page_number,
|
||||
total_records)
|
||||
|
||||
@mock.patch('pecan.request')
|
||||
def test_get_page_number_page_number_is_one(self, mock_request):
|
||||
per_page = 20
|
||||
total_records = 100
|
||||
self.CONF.set_override('results_per_page',
|
||||
per_page,
|
||||
'api')
|
||||
mock_request.GET = {
|
||||
const.PAGE: '1'
|
||||
}
|
||||
|
||||
page_number, total_pages = api_utils.get_page_number(total_records)
|
||||
|
||||
self.assertEqual(page_number, 1)
|
||||
self.assertEqual(total_pages, total_records / per_page)
|
||||
|
||||
@mock.patch('pecan.request')
|
||||
def test_get_page_number_page_number_less_zero(self, mock_request):
|
||||
per_page = 20
|
||||
total_records = 100
|
||||
self.CONF.set_override('results_per_page',
|
||||
per_page,
|
||||
'api')
|
||||
mock_request.GET = {
|
||||
const.PAGE: '-1'
|
||||
}
|
||||
|
||||
self.assertRaises(api_utils.ParseInputsError,
|
||||
api_utils.get_page_number,
|
||||
total_records)
|
||||
|
||||
@mock.patch('pecan.request')
|
||||
def test_get_page_number_page_number_more_than_total(self, mock_request):
|
||||
per_page = 20
|
||||
total_records = 100
|
||||
self.CONF.set_override('results_per_page',
|
||||
per_page,
|
||||
'api')
|
||||
mock_request.GET = {
|
||||
const.PAGE: '100'
|
||||
}
|
||||
|
||||
self.assertRaises(api_utils.ParseInputsError,
|
||||
api_utils.get_page_number,
|
||||
total_records)
|
||||
|
||||
@mock.patch('pecan.request')
|
||||
def test_get_page_number_success(self, mock_request):
|
||||
per_page = 20
|
||||
total_records = 100
|
||||
self.CONF.set_override('results_per_page',
|
||||
per_page,
|
||||
'api')
|
||||
mock_request.GET = {
|
||||
const.PAGE: '2'
|
||||
}
|
||||
|
||||
page_number, total_pages = api_utils.get_page_number(total_records)
|
||||
|
||||
self.assertEqual(page_number, 2)
|
||||
self.assertEqual(total_pages, total_records / per_page)
|
|
@ -17,30 +17,12 @@
|
|||
|
||||
import six
|
||||
import mock
|
||||
from oslo_config import fixture as config_fixture
|
||||
from oslotest import base
|
||||
|
||||
from refstack import db
|
||||
from refstack.api import constants as api_const
|
||||
from refstack.db.sqlalchemy import api
|
||||
from refstack.db.sqlalchemy import models
|
||||
|
||||
|
||||
class RefStackBaseTestCase(base.BaseTestCase):
|
||||
"""Test case for RefStackBase model."""
|
||||
|
||||
@mock.patch('oslo_utils.timeutils.utcnow')
|
||||
def test_delete(self, utcnow):
|
||||
utcnow.return_value = '123'
|
||||
|
||||
base_model = models.RefStackBase()
|
||||
base_model.id = 'fake_id'
|
||||
base_model.save = mock.Mock()
|
||||
session = mock.MagicMock()
|
||||
|
||||
base_model.delete(session)
|
||||
|
||||
self.assertEqual(base_model.deleted, 'fake_id')
|
||||
self.assertEqual(base_model.deleted_at, '123')
|
||||
base_model.save.assert_called_once_with(session=session)
|
||||
|
||||
|
||||
class DBAPITestCase(base.BaseTestCase):
|
||||
|
@ -61,6 +43,18 @@ class DBAPITestCase(base.BaseTestCase):
|
|||
db.get_test_results(12345)
|
||||
mock_get_test_results.assert_called_once_with(12345)
|
||||
|
||||
@mock.patch.object(api, 'get_test_records')
|
||||
def test_get_test_records(self, mock_db):
|
||||
filters = mock.Mock()
|
||||
db.get_test_records(1, 2, filters)
|
||||
mock_db.assert_called_once_with(1, 2, filters)
|
||||
|
||||
@mock.patch.object(api, 'get_test_records_count')
|
||||
def test_get_test_records_count(self, mock_db):
|
||||
filters = mock.Mock()
|
||||
db.get_test_records_count(filters)
|
||||
mock_db.assert_called_once_with(filters)
|
||||
|
||||
|
||||
class DBHelpersTestCase(base.BaseTestCase):
|
||||
"""Test case for database backend helpers."""
|
||||
|
@ -97,6 +91,11 @@ class DBHelpersTestCase(base.BaseTestCase):
|
|||
class DBBackendTestCase(base.BaseTestCase):
|
||||
"""Test case for database backend."""
|
||||
|
||||
def setUp(self):
|
||||
super(DBBackendTestCase, self).setUp()
|
||||
self.config_fixture = config_fixture.Config()
|
||||
self.CONF = self.useFixture(self.config_fixture).conf
|
||||
|
||||
@mock.patch.object(api, 'get_session')
|
||||
@mock.patch('refstack.db.sqlalchemy.models.TestResults')
|
||||
@mock.patch('refstack.db.sqlalchemy.models.Test')
|
||||
|
@ -181,3 +180,85 @@ class DBBackendTestCase(base.BaseTestCase):
|
|||
query.filter_by.assert_called_once_with(test_id=test_id)
|
||||
filter_by.all.assert_called_once_with()
|
||||
self.assertEqual(expected_result, actual_result)
|
||||
|
||||
@mock.patch('refstack.db.sqlalchemy.models.Test')
|
||||
def test_apply_filters_for_query(self, mock_model):
|
||||
query = mock.Mock()
|
||||
mock_model.created_at = six.text_type()
|
||||
|
||||
filters = {
|
||||
api_const.START_DATE: 'fake1',
|
||||
api_const.END_DATE: 'fake2',
|
||||
api_const.CPID: 'fake3'
|
||||
}
|
||||
|
||||
result = api._apply_filters_for_query(query, filters)
|
||||
|
||||
query.filter.assert_called_once_with(mock_model.created_at >=
|
||||
filters[api_const.START_DATE])
|
||||
|
||||
query = query.filter.return_value
|
||||
query.filter.assert_called_once_with(mock_model.created_at <=
|
||||
filters[api_const.END_DATE])
|
||||
|
||||
query = query.filter.return_value
|
||||
query.filter.assert_called_once_with(mock_model.cpid ==
|
||||
filters[api_const.CPID])
|
||||
|
||||
query = query.filter.return_value
|
||||
self.assertEqual(result, query)
|
||||
|
||||
@mock.patch.object(api, '_apply_filters_for_query')
|
||||
@mock.patch.object(api, 'get_session')
|
||||
@mock.patch('refstack.db.sqlalchemy.models.Test')
|
||||
def test_get_test_records(self, mock_model,
|
||||
mock_get_session,
|
||||
mock_apply):
|
||||
|
||||
per_page = 9000
|
||||
filters = {
|
||||
api_const.START_DATE: 'fake1',
|
||||
api_const.END_DATE: 'fake2',
|
||||
api_const.CPID: 'fake3'
|
||||
}
|
||||
|
||||
session = mock_get_session.return_value
|
||||
first_query = session.query.return_value
|
||||
second_query = mock_apply.return_value
|
||||
ordered_query = second_query.order_by.return_value
|
||||
query_with_offset = ordered_query.offset.return_value
|
||||
query_with_offset.limit.return_value = 'fake_uploads'
|
||||
|
||||
result = api.get_test_records(2, per_page, filters)
|
||||
|
||||
mock_get_session.assert_called_once_with()
|
||||
session.query.assert_called_once_with(mock_model.id,
|
||||
mock_model.created_at,
|
||||
mock_model.cpid)
|
||||
mock_apply.assert_called_once_with(first_query, filters)
|
||||
second_query.order_by.\
|
||||
assert_called_once_with(mock_model.created_at.desc())
|
||||
|
||||
self.assertEqual(result, 'fake_uploads')
|
||||
ordered_query.offset.assert_called_once_with(per_page)
|
||||
query_with_offset.limit.assert_called_once_with(per_page)
|
||||
|
||||
@mock.patch.object(api, '_apply_filters_for_query')
|
||||
@mock.patch.object(api, 'get_session')
|
||||
@mock.patch('refstack.db.sqlalchemy.models.Test')
|
||||
def test_get_test_records_count(self, mock_model,
|
||||
mock_get_session,
|
||||
mock_apply):
|
||||
|
||||
filters = mock.Mock()
|
||||
session = mock_get_session.return_value
|
||||
query = session.query.return_value
|
||||
apply_result = mock_apply.return_value
|
||||
apply_result.count.return_value = 999
|
||||
|
||||
result = api.get_test_records_count(filters)
|
||||
self.assertEqual(result, 999)
|
||||
|
||||
session.query.assert_called_once_with(mock_model.id)
|
||||
mock_apply.assert_called_once_with(query, filters)
|
||||
apply_result.count.assert_called_once_with()
|
||||
|
|
Loading…
Reference in New Issue