From 8f3c5e959404d8e0439a3f337cd9bebac6661016 Mon Sep 17 00:00:00 2001 From: Joshua Carp Date: Sun, 2 Aug 2015 17:19:13 -0400 Subject: [PATCH 1/8] Allow custom base model class. --- sandman/model/utils.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/sandman/model/utils.py b/sandman/model/utils.py index 1f79a5c..1987d44 100644 --- a/sandman/model/utils.py +++ b/sandman/model/utils.py @@ -22,7 +22,7 @@ def _get_session(): return session -def generate_endpoint_classes(db, generate_pks=False): +def generate_endpoint_classes(db, generate_pks=False, base=None): """Return a list of model classes generated for each reflected database table.""" seen_classes = set() @@ -38,7 +38,7 @@ def generate_endpoint_classes(db, generate_pks=False): else: cls = type( str(name), - (sandman_model, db.Model), + (base or sandman_model, db.Model), {'__tablename__': name}) register(cls) @@ -160,7 +160,7 @@ def register_classes_for_admin(db_session, show_pks=True, name='admin'): admin_view.add_view(admin_view_class(cls, db_session)) -def activate(admin=True, browser=True, name='admin', reflect_all=False): +def activate(admin=True, browser=True, name='admin', reflect_all=False, base=None): """Activate each pre-registered model or generate the model classes and (possibly) register them for the admin. @@ -170,13 +170,14 @@ def activate(admin=True, browser=True, name='admin', reflect_all=False): this to avoid naming conflicts with other blueprints (if trying to use sandman to connect to multiple databases simultaneously) + :param base: Optional base model class; defaults to `model.Model` """ with app.app_context(): generate_pks = app.config.get('SANDMAN_GENERATE_PKS', None) or False if getattr(app, 'class_references', None) is None or reflect_all: app.class_references = collections.OrderedDict() - generate_endpoint_classes(db, generate_pks) + generate_endpoint_classes(db, generate_pks, base=base) else: Model.prepare(db.engine) prepare_relationships(db, current_app.class_references) From 20bf0582f4ed81cef98394de17195334e662cc56 Mon Sep 17 00:00:00 2001 From: Joshua Carp Date: Sun, 2 Aug 2015 17:24:29 -0400 Subject: [PATCH 2/8] Throw 405 on unsupported method. --- sandman/sandman.py | 2 +- tests/test_sandman.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sandman/sandman.py b/sandman/sandman.py index b86c186..0130eb2 100644 --- a/sandman/sandman.py +++ b/sandman/sandman.py @@ -188,7 +188,7 @@ def _validate(cls, method, resource=None): """ if method not in cls.__methods__: - raise InvalidAPIUsage(403, FORBIDDEN_EXCEPTION_MESSAGE.format( + raise InvalidAPIUsage(405, FORBIDDEN_EXCEPTION_MESSAGE.format( method, cls.endpoint(), cls.__methods__)) diff --git a/tests/test_sandman.py b/tests/test_sandman.py index 7986b3a..99f9b29 100644 --- a/tests/test_sandman.py +++ b/tests/test_sandman.py @@ -301,25 +301,25 @@ class TestSandmanValidation(TestSandmanBase): def test_delete_not_supported(self): """Test DELETEing a resource for an endpoint that doesn't support it.""" response = self.app.delete('/playlists/1') - assert response.status_code == 403 + assert response.status_code == 405 def test_unsupported_patch_resource(self): """Test PATCHing a resource for an endpoint that doesn't support it.""" response = self.app.patch('/styles/26', content_type='application/json', data=json.dumps({u'Name': u'Hip-Hop'})) - assert response.status_code == 403 + assert response.status_code == 405 def test_unsupported_get_resource(self): """Test GETing a resource for an endpoint that doesn't support it.""" - self.get_response('/playlists', 403, False) + self.get_response('/playlists', 405, False) def test_unsupported_collection_method(self): """Test POSTing a collection for an endpoint that doesn't support it.""" response = self.app.post('/styles', content_type='application/json', data=json.dumps({u'Name': u'Jeff Knupp'})) - assert response.status_code == 403 + assert response.status_code == 405 def test_pagination(self): """Can we get paginated results?""" From 7a27cb28fea52665535708714a9f8345748d4b39 Mon Sep 17 00:00:00 2001 From: Joshua Carp Date: Sat, 1 Aug 2015 00:21:06 -0400 Subject: [PATCH 3/8] Fix tests for current package versions. There are currently two sets of dependency versions for sandman: one set of pinned versions in `requirements.txt` and another set of unpinned versions in `setup.py`. This means that installing via `pip` installs the latest versions of the dependencies, but installing from `requirements.txt` installs older, pinned versions. Several tests fail with the latest versions of Flask-Admin and Flask-WTForms. This patch fixes the breaking tests and installs tests on Travis using `setup.py` for consistency. --- .travis.yml | 12 ++++++------ tests/test_sandman.py | 12 +++++++++--- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index a97547e..8e3baec 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,11 @@ language: python python: - "2.7" -install: - - "pip install -r requirements.txt --use-mirrors" - - "pip install coverage" - - "pip install coveralls" -script: - - "coverage run --source=sandman setup.py test" +install: + - python setup.py develop + - pip install coverage + - pip install coveralls +script: + - coverage run --source=sandman setup.py test after_success: coveralls diff --git a/tests/test_sandman.py b/tests/test_sandman.py index 99f9b29..2a18684 100644 --- a/tests/test_sandman.py +++ b/tests/test_sandman.py @@ -5,6 +5,8 @@ import json import datetime +from flask import url_for + from sandman import app class TestSandmanBase(object): @@ -27,12 +29,15 @@ def setup_method(self, _): app.config['SANDMAN_GENERATE_PKS'] = False app.config['TESTING'] = True self.app = app.test_client() + self.context = app.test_request_context() + self.context.push() #pylint: disable=unused-variable from . import models def teardown_method(self, _): """Remove the database file copied during setup.""" os.unlink(self.DB_LOCATION) + self.context.pop() #pylint: disable=attribute-defined-outside-init self.app = None @@ -427,6 +432,7 @@ def test_post_no_json_data(self): def test_post_no_html_form_data(self): """Test POSTing a resource with no form data.""" response = self.app.post('/artists', + content_type='application/json', data=dict()) assert response.status_code == 400 @@ -469,7 +475,7 @@ def test_admin_collection_view(self): """Ensure user-defined ``__str__`` implementations are being picked up by the admin.""" - response = self.get_response('/admin/trackview/', 200) + response = self.get_response(url_for('track.index_view'), 200) # If related tables are being loaded correctly, Tracks will have a # Mediatype column, at least one of which has the value 'MPEG audio # file'. @@ -478,7 +484,7 @@ def test_admin_collection_view(self): def test_admin_default_str_repr(self): """Ensure default ``__str__`` implementations works in the admin.""" - response = self.get_response('/admin/trackview/?page=3/', 200) + response = self.get_response(url_for('track.index_view', page=3), 200) # If related tables are being loaded correctly, Tracks will have a # Genre column, but should display the GenreId and not the name ('Jazz' # is the genre for many results on the third page @@ -490,7 +496,7 @@ def test_admin_default_str_repr_different_table_class_name(self): classname differs from the table name show up with the classname (not the table name).""" - response = self.get_response('/admin/styleview/', 200) + response = self.get_response(url_for('style.index_view'), 200) assert 'Genre' not in str(response.get_data(as_text=True)) From ea98dbcc04ca61db82e08cb1579293abcdad2246 Mon Sep 17 00:00:00 2001 From: Joshua Carp Date: Fri, 31 Jul 2015 23:34:01 -0400 Subject: [PATCH 4/8] Paginate results using the database. Instead of fetching all results and paginating in memory, use the pagination helpers built into Flask-SQLAlchemy. --- sandman/model/utils.py | 6 +++++- sandman/sandman.py | 44 ++++++++++++++++-------------------------- tests/models.py | 3 ++- 3 files changed, 24 insertions(+), 29 deletions(-) diff --git a/sandman/model/utils.py b/sandman/model/utils.py index 1987d44..82caeb9 100644 --- a/sandman/model/utils.py +++ b/sandman/model/utils.py @@ -5,6 +5,7 @@ from flask import current_app, g from flask.ext.admin import Admin from flask.ext.admin.contrib.sqla import ModelView +from flask.ext.sqlalchemy import SQLAlchemy from sqlalchemy.engine import reflection from sqlalchemy.ext.declarative import declarative_base, DeferredReflection from sqlalchemy.orm import relationship @@ -200,4 +201,7 @@ def activate(admin=True, browser=True, name='admin', reflect_all=False, base=Non # actually the same thing. sandman_model = Model -Model = declarative_base(cls=(Model, DeferredReflection)) +# Model = declarative_base(cls=(Model, DeferredReflection)) + +class Model(Model, DeferredReflection, db.Model): + __abstract__ = True diff --git a/sandman/sandman.py b/sandman/sandman.py index 0130eb2..a1fa258 100644 --- a/sandman/sandman.py +++ b/sandman/sandman.py @@ -135,7 +135,7 @@ def _single_resource_html_response(resource): tablename=tablename)) -def _collection_json_response(cls, resources, start, stop, depth=0): +def _collection_json_response(cls, resources, depth=0): """Return the JSON representation of the collection *resources*. :param list resources: list of :class:`sandman.model.Model`s to render @@ -153,16 +153,11 @@ def _collection_json_response(cls, resources, start, stop, depth=0): for resource in resources: result_list.append(resource.as_dict(depth)) - payload = {} - if start is not None: - payload[top_level_json_name] = result_list[start:stop] - else: - payload[top_level_json_name] = result_list - + payload = {top_level_json_name: result_list} return jsonify(payload) -def _collection_html_response(resources, start=0, stop=20): +def _collection_html_response(resources): """Return the HTML representation of the collection *resources*. :param list resources: list of :class:`sandman.model.Model`s to render @@ -171,7 +166,7 @@ def _collection_html_response(resources, start=0, stop=20): """ return make_response(render_template( 'collection.html', - resources=resources[start:stop])) + resources=resources)) def _validate(cls, method, resource=None): @@ -244,27 +239,22 @@ def retrieve_collection(collection, query_arguments=None): :rtype: class:`sandman.model.Model` """ - session = _get_session() cls = endpoint_class(collection) if query_arguments: filters = [] order = [] - limit = None for key, value in query_arguments.items(): - if key == 'page': + if key in ['page', 'limit']: continue if value.startswith('%'): filters.append(getattr(cls, key).like(str(value), escape='/')) elif key == 'sort': order.append(getattr(cls, value)) - elif key == 'limit': - limit = value elif key: filters.append(getattr(cls, key) == value) - resources = session.query(cls).filter(*filters).order_by( - *order).limit(limit) + resources = cls.query.filter(*filters).order_by(*order) else: - resources = session.query(cls).all() + resources = cls.query return resources @@ -303,7 +293,7 @@ def resource_created_response(resource): return response -def collection_response(cls, resources, start=None, stop=None): +def collection_response(cls, resources): """Return a response for the *resources* of the appropriate content type. :param resources: resources to be returned in request @@ -312,9 +302,9 @@ def collection_response(cls, resources, start=None, stop=None): """ if _get_acceptable_response_type() == JSON: - return _collection_json_response(cls, resources, start, stop) + return _collection_json_response(cls, resources) else: - return _collection_html_response(resources, start, stop) + return _collection_html_response(resources) def resource_response(resource, depth=0): @@ -530,13 +520,13 @@ def get_collection(collection): _validate(cls, request.method, resources) - start = stop = None - - if request.args and 'page' in request.args: - page = int(request.args['page']) - results_per_page = app.config.get('RESULTS_PER_PAGE', 20) - start, stop = page * results_per_page, (page + 1) * results_per_page - return collection_response(cls, resources, start, stop) + try: + page = int(request.args.get('page', 1)) + except (TypeError, ValueError): + raise InvalidAPIUsage(422) + per_page = app.config.get('RESULTS_PER_PAGE', 20) + resources = resources.paginate(page, per_page) + return collection_response(cls, resources.items) @app.route('/', methods=['GET']) diff --git a/tests/models.py b/tests/models.py index 4914087..1b75ba2 100644 --- a/tests/models.py +++ b/tests/models.py @@ -1,5 +1,6 @@ """Models for unit testing sandman""" +from flask.ext.sqlalchemy import BaseQuery from flask.ext.admin.contrib.sqla import ModelView from sandman.model import register, Model, activate from sandman.model.models import db @@ -90,7 +91,7 @@ def validate_GET(resource=None): """ - if isinstance(resource, list): + if isinstance(resource, BaseQuery): return True elif resource and resource.GenreId == 1: return False From e85115bc5c91c16322cce6d1fc5366ebf8781914 Mon Sep 17 00:00:00 2001 From: Joshua Carp Date: Wed, 5 Aug 2015 10:55:23 -0400 Subject: [PATCH 5/8] Add pagination metadata to JSON response. --- sandman/sandman.py | 27 +++++++++++++-------------- tests/models.py | 2 +- tests/test_sandman.py | 32 ++++++++++++++++++++------------ 3 files changed, 34 insertions(+), 27 deletions(-) diff --git a/sandman/sandman.py b/sandman/sandman.py index a1fa258..1cd544b 100644 --- a/sandman/sandman.py +++ b/sandman/sandman.py @@ -143,17 +143,16 @@ def _collection_json_response(cls, resources, depth=0): """ - top_level_json_name = None - if cls.__top_level_json_name__ is not None: - top_level_json_name = cls.__top_level_json_name__ - else: - top_level_json_name = 'resources' - - result_list = [] - for resource in resources: - result_list.append(resource.as_dict(depth)) - - payload = {top_level_json_name: result_list} + resource_key = cls.__top_level_json_name__ or 'resources' + + payload = { + resource_key: [each.as_dict(depth) for each in resources.items], + 'pagination': { + 'page': resources.page, + 'per_page': resources.per_page, + 'count': resources.total, + } + } return jsonify(payload) @@ -166,7 +165,7 @@ def _collection_html_response(resources): """ return make_response(render_template( 'collection.html', - resources=resources)) + resources=resources.items)) def _validate(cls, method, resource=None): @@ -525,8 +524,8 @@ def get_collection(collection): except (TypeError, ValueError): raise InvalidAPIUsage(422) per_page = app.config.get('RESULTS_PER_PAGE', 20) - resources = resources.paginate(page, per_page) - return collection_response(cls, resources.items) + resources = resources.paginate(page, per_page, error_out=False) + return collection_response(cls, resources) @app.route('/', methods=['GET']) diff --git a/tests/models.py b/tests/models.py index 1b75ba2..48ab103 100644 --- a/tests/models.py +++ b/tests/models.py @@ -98,4 +98,4 @@ def validate_GET(resource=None): return True register((Artist, Album, Playlist, Track, MediaType, Style, SomeModel)) -activate(browser=True) +activate(browser=False) diff --git a/tests/test_sandman.py b/tests/test_sandman.py index 2a18684..2442552 100644 --- a/tests/test_sandman.py +++ b/tests/test_sandman.py @@ -75,27 +75,29 @@ class TestSandmanBasicVerbs(TestSandmanBase): def test_get(self): """Test simple HTTP GET""" response = self.get_response('/artists', 200) - assert len(json.loads(response.get_data(as_text=True))[u'resources']) == 275 - - def test_get_with_limit(self): - """Test simple HTTP GET""" - response = self.get_response('/artists', 200, params={'limit': 10}) - assert len(json.loads(response.get_data(as_text=True))[u'resources']) == 10 + parsed = json.loads(response.get_data(as_text=True)) + assert parsed['pagination']['count'] == 275 + assert len(parsed['resources']) == 20 def test_get_with_filter(self): """Test simple HTTP GET""" response = self.get_response('/artists', 200, params={'Name': 'AC/DC'}) - assert len(json.loads(response.get_data(as_text=True))[u'resources']) == 1 + parsed = json.loads(response.get_data(as_text=True)) + assert parsed['pagination']['count'] == 1 + assert len(parsed['resources']) == 1 def test_get_with_like_filter(self): """Test simple HTTP GET""" response = self.get_response('/artists', 200, params={'Name': '%AC%DC%'}) - assert len(json.loads(response.get_data(as_text=True))[u'resources']) == 1 + parsed = json.loads(response.get_data(as_text=True)) + assert parsed['pagination']['count'] == 1 + assert len(parsed['resources']) == 1 def test_get_with_sort(self): """Test simple HTTP GET""" response = self.get_response('/artists', 200, params={'sort': 'Name'}) - assert json.loads(response.get_data(as_text=True))[u'resources'][0]['Name'] == 'A Cor Do Som' + parsed = json.loads(response.get_data(as_text=True)) + assert parsed['resources'][0]['Name'] == 'A Cor Do Som' def test_get_attribute(self): """Test simple HTTP GET""" @@ -268,7 +270,9 @@ class TestSandmanUserDefinitions(TestSandmanBase): def test_user_defined_endpoint(self): """Make sure user-defined endpoint exists.""" response = self.get_response('/styles', 200) - assert len(json.loads(response.get_data(as_text=True))[u'resources']) == 25 + parsed = json.loads(response.get_data(as_text=True)) + assert parsed['pagination']['count'] == 25 + assert len(parsed['resources']) == 20 def test_user_validation_reject(self): """Test user-defined validation which on request which should be @@ -298,7 +302,9 @@ def test_responds_with_top_level_json_name_if_present(self): """Test top level json element is the one defined on the Model rather than the string 'resources'""" response = self.get_response('/albums', 200) - assert len(json.loads(response.get_data(as_text=True))[u'Albums']) == 347 + parsed = json.loads(response.get_data(as_text=True)) + assert parsed['pagination']['count'] == 347 + assert len(parsed['Albums']) == 20 class TestSandmanValidation(TestSandmanBase): """Sandman tests related to request validation""" @@ -374,7 +380,9 @@ def test_get_json(self): response = self.get_response('/artists', 200, headers={'Accept': 'application/json'}) - assert len(json.loads(response.get_data(as_text=True))[u'resources']) == 275 + parsed = json.loads(response.get_data(as_text=True)) + assert parsed['pagination']['count'] == 275 + assert len(parsed['resources']) == 20 def test_get_unknown_url(self): """Test sending a GET request to a URL that would match the From 86b1938f2dc9ac828e0b49af7872eda3602ef5dd Mon Sep 17 00:00:00 2001 From: Joshua Carp Date: Fri, 31 Jul 2015 21:26:43 -0400 Subject: [PATCH 6/8] Include templates and static files on install. --- MANIFEST.in | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 MANIFEST.in diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..4c5cb1a --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +recursive-include sandman/static * +recursive-include sandman/templates * From 42fb80f8c710ae60086297b7b0b16b78796b3c66 Mon Sep 17 00:00:00 2001 From: Joshua Carp Date: Thu, 6 Aug 2015 15:28:28 -0400 Subject: [PATCH 7/8] Allow case-insensitive queries on text columns. --- sandman/sandman.py | 32 ++++++++++++++++++++++++++------ tests/test_sandman.py | 15 +++++++++++++++ 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/sandman/sandman.py b/sandman/sandman.py index 1cd544b..fde601e 100644 --- a/sandman/sandman.py +++ b/sandman/sandman.py @@ -7,7 +7,7 @@ Response, render_template, make_response) -from sqlalchemy.exc import IntegrityError +import sqlalchemy as sa from . import app from .decorators import etag, no_cache from .exception import InvalidAPIUsage @@ -54,6 +54,20 @@ def _get_acceptable_response_type(): raise InvalidAPIUsage(406) +def _get_column(model, key): + try: + return getattr(model, key) + except AttributeError: + raise InvalidAPIUsage(422) + + +def _column_type(attribute): + columns = attribute.property.columns + if len(columns) == 1: + return columns[0].type.python_type + return None + + @app.errorhandler(InvalidAPIUsage) def handle_exception(error): """Return a response with the appropriate status code, message, and content @@ -246,11 +260,17 @@ def retrieve_collection(collection, query_arguments=None): if key in ['page', 'limit']: continue if value.startswith('%'): - filters.append(getattr(cls, key).like(str(value), escape='/')) + filters.append(_get_column(cls, key).like(str(value), escape='/')) elif key == 'sort': - order.append(getattr(cls, value)) + order.append(_get_column(cls, value)) + elif key == 'limit': + limit = value elif key: - filters.append(getattr(cls, key) == value) + column = _get_column(cls, key) + if app.config.get('CASE_INSENSITIVE') and issubclass(_column_type(column), str): + filters.append(sa.func.upper(column) == value.upper()) + else: + filters.append(column == value) resources = cls.query.filter(*filters).order_by(*order) else: resources = cls.query @@ -417,7 +437,7 @@ def put_resource(collection, key): resource.replace(get_resource_data(request)) try: _perform_database_action('add', resource) - except IntegrityError as exception: + except sa.exc.IntegrityError as exception: raise InvalidAPIUsage(422, FORWARDED_EXCEPTION_MESSAGE.format( exception)) return no_content_response() @@ -460,7 +480,7 @@ def delete_resource(collection, key): try: _perform_database_action('delete', resource) - except IntegrityError as exception: + except sa.exc.IntegrityError as exception: raise InvalidAPIUsage(422, FORWARDED_EXCEPTION_MESSAGE.format( exception)) return no_content_response() diff --git a/tests/test_sandman.py b/tests/test_sandman.py index 2442552..9dc00b2 100644 --- a/tests/test_sandman.py +++ b/tests/test_sandman.py @@ -86,6 +86,21 @@ def test_get_with_filter(self): assert parsed['pagination']['count'] == 1 assert len(parsed['resources']) == 1 + def test_get_with_filter_case_insensitive(self): + """Test simple HTTP GET""" + self.app.application.config['CASE_INSENSITIVE'] = True + try: + response = self.get_response('/artists', 200, params={'Name': 'ac/dc'}) + parsed = json.loads(response.get_data(as_text=True)) + assert parsed['pagination']['count'] == 1 + assert len(parsed['resources']) == 1 + finally: + del self.app.application.config['CASE_INSENSITIVE'] + + def test_get_with_filter_missing_column(self): + """Test simple HTTP GET""" + response = self.get_response('/artists', 422, params={'Missing': 'AC/DC'}) + def test_get_with_like_filter(self): """Test simple HTTP GET""" response = self.get_response('/artists', 200, params={'Name': '%AC%DC%'}) From 65f236e96a4af73083a299458c1070230c4c7c1b Mon Sep 17 00:00:00 2001 From: Joshua Carp Date: Thu, 6 Aug 2015 15:43:38 -0400 Subject: [PATCH 8/8] Update unit tests. --- requirements.txt | 1 + sandman/sandman.py | 3 ++- setup.py | 1 + tests/test_sandman.py | 13 ++++++++++++- 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index ee15d09..0cd05e5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,3 +11,4 @@ itsdangerous==0.24 wsgiref==0.1.2 click==0.7 sphinx-rtd-theme==0.1.6 +six==1.9.0 diff --git a/sandman/sandman.py b/sandman/sandman.py index fde601e..cd7bac7 100644 --- a/sandman/sandman.py +++ b/sandman/sandman.py @@ -1,5 +1,6 @@ """Sandman REST API creator for Flask and SQLAlchemy""" +import six from flask import ( jsonify, request, @@ -267,7 +268,7 @@ def retrieve_collection(collection, query_arguments=None): limit = value elif key: column = _get_column(cls, key) - if app.config.get('CASE_INSENSITIVE') and issubclass(_column_type(column), str): + if app.config.get('CASE_INSENSITIVE') and issubclass(_column_type(column), six.string_types): filters.append(sa.func.upper(column) == value.upper()) else: filters.append(column == value) diff --git a/setup.py b/setup.py index f13e56c..04d8eb2 100644 --- a/setup.py +++ b/setup.py @@ -47,6 +47,7 @@ def run_tests(self): 'Flask-HTTPAuth>=2.2.1', 'docopt>=0.6.1', 'click', + 'six', #'sphinx-rtd-theme', ], cmdclass={'test': PyTest}, diff --git a/tests/test_sandman.py b/tests/test_sandman.py index 9dc00b2..58f06c0 100644 --- a/tests/test_sandman.py +++ b/tests/test_sandman.py @@ -97,6 +97,17 @@ def test_get_with_filter_case_insensitive(self): finally: del self.app.application.config['CASE_INSENSITIVE'] + def test_get_with_filter_non_string_case_insensitive(self): + """Test simple HTTP GET""" + self.app.application.config['CASE_INSENSITIVE'] = True + try: + response = self.get_response('/artists', 200, params={'ArtistId': 1}) + parsed = json.loads(response.get_data(as_text=True)) + assert parsed['pagination']['count'] == 1 + assert len(parsed['resources']) == 1 + finally: + del self.app.application.config['CASE_INSENSITIVE'] + def test_get_with_filter_missing_column(self): """Test simple HTTP GET""" response = self.get_response('/artists', 422, params={'Missing': 'AC/DC'}) @@ -511,7 +522,7 @@ def test_admin_default_str_repr(self): # If related tables are being loaded correctly, Tracks will have a # Genre column, but should display the GenreId and not the name ('Jazz' # is the genre for many results on the third page - assert 'Jazz' not in str(response.get_data(as_text=True)) + assert 'Jazz' not in response.get_data(as_text=True) #pylint: disable=invalid-name def test_admin_default_str_repr_different_table_class_name(self):