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:
Vladislav Kuzmin 2015-03-20 17:22:13 +03:00
parent 5d1d1cfb3e
commit 4ab98f7d8e
13 changed files with 900 additions and 51 deletions

View File

@ -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>

View File

@ -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

23
refstack/api/constants.py Normal file
View File

@ -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'

View File

@ -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):

123
refstack/api/utils.py Normal file
View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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):

View File

@ -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)),
]

View File

@ -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)

View File

@ -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')

View File

@ -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)

View File

@ -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()