# test_fetching.py - unit tests for fetching resources
#
# Copyright 2011 Lincoln de Sousa <lincoln@comum.org>.
# Copyright 2012, 2013, 2014, 2015, 2016 Jeffrey Finkelstein
#           <jeffrey.finkelstein@gmail.com> and contributors.
#
# This file is part of Flask-Restless.
#
# Flask-Restless is distributed under both the GNU Affero General Public
# License version 3 and under the 3-clause BSD license. For more
# information, see LICENSE.AGPL and LICENSE.BSD.
"""Unit tests for fetching resources from endpoints generated by
Flask-Restless.

This module includes tests for additional functionality that is not
already tested by :mod:`test_jsonapi`, the package that guarantees
Flask-Restless meets the minimum requirements of the JSON API
specification.

"""
from sqlalchemy import Column
from sqlalchemy import ForeignKey
from sqlalchemy import Integer
from sqlalchemy import Unicode
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.orm import backref
from sqlalchemy.orm import relationship

from flask_restless import APIManager
from flask_restless import ProcessingException

from .helpers import FlaskSQLAlchemyTestBase
from .helpers import ManagerTestBase
from .helpers import check_sole_error
from .helpers import dumps


class TestFetchRelation(ManagerTestBase):

    def setUp(self):
        super(TestFetchRelation, self).setUp()

        class Article(self.Base):
            __tablename__ = 'article'
            id = Column(Integer, primary_key=True)
            title = Column(Unicode)
            author_id = Column(Integer, ForeignKey('person.id'))
            author = relationship('Person', backref=backref('articles'))

        class Person(self.Base):
            __tablename__ = 'person'
            id = Column(Integer, primary_key=True)

        self.Article = Article
        self.Person = Person
        self.Base.metadata.create_all(bind=self.engine)
        self.manager.create_api(Article)
        self.manager.create_api(Person)

    def test_nonexistent_resource(self):
        """Tests that a request for a relation on a nonexistent resource yields
        an error.

        """
        response = self.app.get('/api/person/bogus/articles')
        assert response.status_code == 404
        # TODO Check error message here.

    def test_nonexistent_relation(self):
        """Tests that a request for a nonexistent relation yields an error."""
        person = self.Person(id=1)
        self.session.add(person)
        self.session.commit()
        response = self.app.get('/api/person/1/bogus')
        assert response.status_code == 404
        # TODO Check error message here.

    def test_to_many_pagination(self):
        """Tests that fetching a to-many relation obeys pagination.

        For more information, see the `Pagination`_ section of the JSON
        API specification.

        .. _Pagination: https://jsonapi.org/format/#fetching-pagination

        """
        person = self.Person(id=1)
        articles = [self.Article(id=i) for i in range(10)]
        person.articles = articles
        self.session.add(person)
        self.session.add_all(articles)
        self.session.commit()
        params = {'page[number]': 3, 'page[size]': 2}
        base_url = '/api/person/1/articles'
        response = self.app.get(base_url, query_string=params)
        document = response.json
        articles = document['data']
        assert all(article['type'] == 'article' for article in articles)
        assert ['4', '5'] == sorted(article['id'] for article in articles)
        pagination = document['links']
        base_url = '{0}?'.format(base_url)
        assert base_url in pagination['first']
        assert 'page[number]=1' in pagination['first']
        assert base_url in pagination['last']
        assert 'page[number]=5' in pagination['last']
        assert base_url in pagination['prev']
        assert 'page[number]=2' in pagination['prev']
        assert base_url in pagination['next']
        assert 'page[number]=4' in pagination['next']

    def test_to_many_sorting(self):
        """Tests for sorting a to-many relation."""
        person = self.Person(id=1)
        article1 = self.Article(id=1, title=u'b')
        article2 = self.Article(id=2, title=u'c')
        article3 = self.Article(id=3, title=u'a')
        articles = [article1, article2, article3]
        person.articles = articles
        self.session.add(person)
        self.session.add_all(articles)
        self.session.commit()
        params = {'sort': '-title'}
        response = self.app.get('/api/person/1/articles', query_string=params)
        document = response.json
        articles = document['data']
        assert ['c', 'b', 'a'] == [article['attributes']['title']
                                   for article in articles]
        assert ['2', '1', '3'] == [article['id'] for article in articles]


class TestFetchRelatedResource(ManagerTestBase):

    def setUp(self):
        super(TestFetchRelatedResource, self).setUp()

        class Article(self.Base):
            __tablename__ = 'article'
            id = Column(Integer, primary_key=True)
            author_id = Column(Integer, ForeignKey('person.id'))
            author = relationship('Person', backref=backref('articles'))

        class Person(self.Base):
            __tablename__ = 'person'
            id = Column(Integer, primary_key=True)

        self.Article = Article
        self.Person = Person
        self.Base.metadata.create_all(bind=self.engine)
        self.manager.create_api(Article)
        self.manager.create_api(Person)

    def test_nonexistent_related_resource(self):
        """Tests that a request for a nonexistent related resource yields an
        error.

        """
        person = self.Person(id=1)
        self.session.add(person)
        self.session.commit()
        response = self.app.get('/api/person/1/articles/1')
        assert response.status_code == 404
        # TODO Check error message here.

    def test_nonexistent_related_model(self):
        """Tests that a request for a nonexistent related model yields
        an error.

        """
        person = self.Person(id=1)
        self.session.add(person)
        self.session.commit()
        response = self.app.get('/api/person/1/bogus/1')
        check_sole_error(response, 404, ['No such relation', 'bogus'])

    def test_to_one_with_id(self):
        """Tests that a request to fetch a resource by its ID from a to-one
        relation yields an error.

        """
        article = self.Article(id=1)
        person = self.Person(id=1)
        article.author = person
        self.session.add_all([article, person])
        self.session.commit()
        response = self.app.get('/api/article/1/author/1')
        check_sole_error(response, 404, ['Cannot access', 'to-one',
                                         'related resource'])

    def test_related_resource(self):
        """Tests for fetching a single resource from a to-many relation.

        This is not required by the JSON API specification.

        """
        article = self.Article(id=1)
        person = self.Person(id=1)
        article.author = person
        self.session.add_all([article, person])
        self.session.commit()
        response = self.app.get('/api/person/1/articles/1')
        assert response.status_code == 200
        document = response.json
        article = document['data']
        assert article['id'] == '1'
        assert article['type'] == 'article'
        author = article['relationships']['author']['data']
        assert author['id'] == '1'
        assert author['type'] == 'person'

    def test_nonexistent_resource(self):
        """Tests that a request for a relation on a nonexistent resource yields
        an error.

        """
        article = self.Article(id=1)
        self.session.add(article)
        self.session.commit()
        response = self.app.get('/api/person/foo/articles/1')
        assert response.status_code == 404
        # TODO Check error message here.

    def test_nonexistent_relation(self):
        """Tests that a request for a nonexistent relation on a resource yields
        an error.

        """
        person = self.Person(id=1)
        self.session.add(person)
        self.session.commit()
        response = self.app.get('/api/person/1/bogus/1')
        assert response.status_code == 404
        # TODO Check error message here.


class TestFetchRelationship(ManagerTestBase):
    """Tests for fetching from a relationship URL."""

    def setUp(self):
        super(TestFetchRelationship, self).setUp()

        class Article(self.Base):
            __tablename__ = 'article'
            id = Column(Integer, primary_key=True)
            author_id = Column(Integer, ForeignKey('person.id'))
            author = relationship('Person', backref=backref('articles'))

        class Person(self.Base):
            __tablename__ = 'person'
            id = Column(Integer, primary_key=True)

        self.Article = Article
        self.Person = Person
        self.Base.metadata.create_all(bind=self.engine)
        self.manager.create_api(Article)
        self.manager.create_api(Person)

    def test_relationship_url_nonexistent_instance(self):
        """Tests that an attempt to fetch from a relationship URL for a
        resource that doesn't exist yields an error.

        """
        response = self.app.get('/api/person/bogus/relationships/articles')
        assert response.status_code == 404
        # TODO check error message here

    def test_relationship_url_nonexistent_relation(self):
        """Tests that an attempt to fetch from a relationship URL without
        specifying a relationship yields an error.

        """
        person = self.Person(id=1)
        self.session.add(person)
        self.session.commit()
        response = self.app.get('/api/person/1/relationships')
        assert response.status_code == 404
        # TODO check error message here


class TestServerSparseFieldsets(ManagerTestBase):
    """Tests for specifying default sparse fieldsets on the server."""

    def setUp(self):
        super(TestServerSparseFieldsets, self).setUp()

        class Person(self.Base):
            __tablename__ = 'person'
            id = Column(Integer, primary_key=True)
            name = Column(Unicode)
            age = Column(Integer)

        class Article(self.Base):
            __tablename__ = 'article'
            id = Column(Integer, primary_key=True)
            title = Column(Unicode)
            author_id = Column(Integer, ForeignKey('person.id'))
            author = relationship(Person, backref=backref('articles'))
            comments = relationship('Comment', backref='article')

        class Comment(self.Base):
            __tablename__ = 'comment'
            id = Column(Integer, primary_key=True)
            article_id = Column(Integer, ForeignKey('article.id'))

        class Photo(self.Base):
            __tablename__ = 'photo'
            id = Column(Integer, primary_key=True)
            title = Column(Unicode)

            def website(self):
                return 'example.com'

            @property
            def year(self):
                return 2015

        self.Article = Article
        self.Comment = Comment
        self.Person = Person
        self.Photo = Photo
        self.Base.metadata.create_all(bind=self.engine)

    def test_only_column(self):
        """Tests for specifying that responses should only include certain
        column fields.

        """
        person = self.Person(id=1, name=u'foo')
        self.session.add(person)
        self.session.commit()
        self.manager.create_api(self.Person, only=['name'])
        response = self.app.get('/api/person/1')
        document = response.json
        person = document['data']
        assert ['attributes', 'id', 'type'] == sorted(person)
        assert ['name'] == sorted(person['attributes'])

    def test_only_relationship(self):
        """Tests for specifying that response should only include certain
        relationships.

        """
        person = self.Person(id=1)
        self.session.add(person)
        self.session.commit()
        self.manager.create_api(self.Person, only=['articles'])
        response = self.app.get('/api/person/1')
        document = response.json
        person = document['data']
        assert ['id', 'relationships', 'type'] == sorted(person)
        assert ['articles'] == sorted(person['relationships'])

    def test_only_as_objects(self):
        """Test for specifying included columns as SQLAlchemy column objects
        instead of strings.

        """
        person = self.Person(id=1, name=u'foo')
        self.session.add(person)
        self.session.commit()
        self.manager.create_api(self.Person, only=[self.Person.name])
        response = self.app.get('/api/person/1')
        document = response.json
        person = document['data']
        assert ['attributes', 'id', 'type'] == sorted(person)
        assert ['name'] == sorted(person['attributes'])

    def test_only_none(self):
        """Tests that providing an empty list as the list of fields to include
        in responses causes responses to have only the ``id`` and ``type``
        elements.

        """
        person = self.Person(id=1)
        self.session.add(person)
        self.session.commit()
        self.manager.create_api(self.Person, only=[])
        response = self.app.get('/api/person/1')
        document = response.json
        person = document['data']
        assert ['id', 'type'] == sorted(person)

    def test_additional_attributes(self):
        """Tests that additional attributes other than SQLAlchemy columns can
        be included in responses by default.

        """
        self.Person.foo = 'bar'
        person = self.Person(id=1)
        self.session.add(person)
        self.session.commit()
        self.manager.create_api(self.Person, additional_attributes=['foo'])
        response = self.app.get('/api/person/1')
        document = response.json
        person = document['data']
        assert person['attributes']['foo'] == 'bar'

    def test_additional_attributes_not_related(self):
        """Tests that we do not try to include additional attributes when
        requesting a related resource.

        For more information, see pull request #257.

        """
        self.Article.foo = 'bar'
        person = self.Person(id=1)
        article = self.Article(id=1)
        article.author = person
        self.session.add_all([person, article])
        self.session.commit()
        self.manager.create_api(self.Article, additional_attributes=['foo'])
        self.manager.create_api(self.Person)
        response = self.app.get('/api/article/1/author')
        document = response.json
        person = document['data']
        assert 'foo' not in person['attributes']

    def test_additional_attributes_callable(self):
        """Tests that callable attributes can be included using the
        ``additional_attributes`` keyword argument.

        """
        photo = self.Photo(id=1)
        self.session.add(photo)
        self.session.commit()
        self.manager.create_api(self.Photo, additional_attributes=['website'])
        response = self.app.get('/api/photo/1')
        document = response.json
        photo = document['data']
        assert photo['attributes']['website'] == 'example.com'

    def test_additional_attributes_property(self):
        """Tests that class properties can be included using the
        ``additional_attributes`` keyword argument.

        """
        photo = self.Photo(id=1)
        self.session.add(photo)
        self.session.commit()
        self.manager.create_api(self.Photo, additional_attributes=['year'])
        response = self.app.get('/api/photo/1')
        document = response.json
        photo = document['data']
        assert photo['attributes']['year'] == 2015

    def test_exclude(self):
        """Test for excluding columns from a resource's representation."""
        person = self.Person(id=1)
        self.session.add(person)
        self.session.commit()
        self.manager.create_api(self.Person, exclude=['name'])
        response = self.app.get('/api/person/1')
        document = response.json
        person = document['data']
        assert 'name' not in person['attributes']

    def test_exclude_as_objects(self):
        """Test for specifying excluded columns as SQLAlchemy column
        attributes.

        """
        person = self.Person(id=1)
        self.session.add(person)
        self.session.commit()
        self.manager.create_api(self.Person, exclude=[self.Person.name])
        response = self.app.get('/api/person/1')
        document = response.json
        person = document['data']
        assert 'name' not in person['attributes']

    def test_exclude_relations(self):
        """Tests for excluding relationships of a resource."""
        article = self.Article(id=1)
        comment = self.Comment()
        person = self.Person()
        article.author = person
        comment.article = article
        self.session.add_all([article, comment, person])
        self.session.commit()
        self.manager.create_api(self.Article, exclude=['comments'])
        # Create the APIs for the other models, just so they have URLs.
        self.manager.create_api(self.Person)
        self.manager.create_api(self.Comment)
        response = self.app.get('/api/article/1')
        document = response.json
        article = document['data']
        assert 'comments' not in article['relationships']
        author = article['relationships']['author']['data']
        assert '1' == author['id']
        assert 'person' == author['type']


class TestProcessors(ManagerTestBase):
    """Tests for pre- and postprocessors."""

    def setUp(self):
        """Creates the database, the :class:`~flask.Flask` object, the
        :class:`~flask_restless.manager.APIManager` for that application, and
        creates the ReSTful API endpoints for the :class:`TestSupport.Person`
        and :class:`TestSupport.Article` models.

        """
        super(TestProcessors, self).setUp()

        class Person(self.Base):
            __tablename__ = 'person'
            id = Column(Integer, primary_key=True)
            name = Column(Unicode)
            articles = relationship('Article', backref='author')

        class Article(self.Base):
            __tablename__ = 'article'
            id = Column(Integer, primary_key=True)
            author_id = Column(Integer, ForeignKey('person.id'))

        self.Article = Article
        self.Person = Person
        self.Base.metadata.create_all(bind=self.engine)

    def test_single_resource_processing_exception(self):
        """Tests for a preprocessor that raises a :exc:`ProcessingException`
        when fetching a single resource.

        """
        person = self.Person(id=1)
        self.session.add(person)
        self.session.commit()

        def forbidden(**kw):
            raise ProcessingException(status=403, detail='forbidden')

        preprocessors = dict(GET_RESOURCE=[forbidden])
        self.manager.create_api(self.Person, preprocessors=preprocessors)
        response = self.app.get('/api/person/1')
        assert response.status_code == 403
        document = response.json
        errors = document['errors']
        assert len(errors) == 1
        error = errors[0]
        assert 'forbidden' == error['detail']

    def test_collection_processing_exception(self):
        """Tests for a preprocessor that raises a :exc:`ProcessingException`
        when fetching a collection of resources.

        """

        def forbidden(**kw):
            raise ProcessingException(status=403, detail='forbidden')

        preprocessors = dict(GET_COLLECTION=[forbidden])
        self.manager.create_api(self.Person, preprocessors=preprocessors)
        response = self.app.get('/api/person')
        assert response.status_code == 403
        errors = response.json['errors']
        assert len(errors) == 1
        assert 'forbidden' == errors[0]['detail']

    def test_resource(self):
        """Tests for running a preprocessor on a request to fetch a
        single resource.

        """
        person = self.Person(id=1)
        self.session.add(person)
        self.session.commit()

        data = {'triggered': False}

        def update_data(*args, **kw):
            data['triggered'] = True

        preprocessors = {'GET_RESOURCE': [update_data]}
        self.manager.create_api(self.Person, preprocessors=preprocessors)
        self.app.get('/api/person/1')
        assert data['triggered']

    def test_change_id(self):
        """Tests that a return value from a preprocessor overrides the ID of
        the resource to fetch as given in the request URL.

        """
        person = self.Person(id=1, name=u'foo')
        self.session.add(person)
        self.session.commit()

        def increment_id(resource_id=None, **kw):
            if resource_id is None:
                raise ProcessingException
            return int(resource_id) + 1

        preprocessors = dict(GET_RESOURCE=[increment_id])
        self.manager.create_api(self.Person, preprocessors=preprocessors)
        response = self.app.get('/api/person/0')
        assert response.status_code == 200
        document = response.json
        person = document['data']
        assert person['id'] == '1'
        assert person['attributes']['name'] == 'foo'

    def test_change_relation(self):
        """Tests for changing the resource ID when fetching a relation.

        """
        person = self.Person(id=1)
        article = self.Article(id=1)
        article.author = person
        self.session.add_all([article, person])
        self.session.commit()

        def change_id(*args, **kw):
            # We will change the primary resource ID.
            return 1

        preprocessors = {'GET_RELATION': [change_id]}
        self.manager.create_api(self.Person, preprocessors=preprocessors)
        # Need to create an API for Article resources so that each
        # Article has a URL.
        self.manager.create_api(self.Article)
        response = self.app.get('/api/person/bogus/articles')
        document = response.json
        articles = document['data']
        assert len(articles) == 1
        article = articles[0]
        assert 'article' == article['type']
        assert '1' == article['id']

    def test_relationship(self):
        """Tests for running a preprocessor on a request to fetch a
        relationship.

        """
        person = self.Person(id=1)
        article = self.Article(id=1)
        article.author = person
        self.session.add_all([article, person])
        self.session.commit()

        data = {'triggered': False}

        def update_data(*args, **kw):
            data['triggered'] = True

        preprocessors = {'GET_RELATIONSHIP': [update_data]}
        self.manager.create_api(self.Person, preprocessors=preprocessors)
        # Need to create an API for Article resources so that each
        # Article has a URL.
        self.manager.create_api(self.Article)
        self.app.get('/api/person/1/relationships/articles')
        assert data['triggered']

    def test_change_id_relationship(self):
        """Tests for changing the resource ID when fetching a
        relationship.

        """
        person = self.Person(id=1)
        article = self.Article(id=1)
        article.author = person
        self.session.add_all([article, person])
        self.session.commit()

        def change_id(*args, **kw):
            # We will change the primary resource ID.
            return 1

        preprocessors = {'GET_RELATIONSHIP': [change_id]}
        self.manager.create_api(self.Person, preprocessors=preprocessors)
        # Need to create an API for Article resources so that each
        # Article has a URL.
        self.manager.create_api(self.Article)
        response = self.app.get('/api/person/bogus/relationships/articles')
        document = response.json
        articles = document['data']
        assert len(articles) == 1
        article = articles[0]
        assert 'article' == article['type']
        assert '1' == article['id']

    def test_relation(self):
        """Tests that a preprocessor is executed when fetching a
        relation.

        """
        person = self.Person(id=1)
        article = self.Article(id=1)
        article.author = person
        self.session.add_all([article, person])
        self.session.commit()

        data = {'triggered': False}

        def update_data(*args, **kw):
            data['triggered'] = True

        preprocessors = {'GET_RELATION': [update_data]}
        self.manager.create_api(self.Person, preprocessors=preprocessors)
        # Need to create an API for Article resources so that each
        # Article has a URL.
        self.manager.create_api(self.Article)
        self.app.get('/api/person/1/articles')
        assert data['triggered']

    def test_change_relation_2(self):
        """Tests for changing the primary resource ID and the relation
        name in a preprocessor for fetching a relation.

        """
        person = self.Person(id=1)
        article = self.Article(id=1)
        article.author = person
        self.session.add_all([article, person])
        self.session.commit()

        def change_two(*args, **kw):
            # We will change the primary resource ID and the relation name.
            return 1, 'articles'

        preprocessors = {'GET_RELATION': [change_two]}
        self.manager.create_api(self.Person, preprocessors=preprocessors)
        # Need to create an API for Article resources so that each
        # Article has a URL.
        self.manager.create_api(self.Article)
        response = self.app.get('/api/person/foo/bar')
        document = response.json
        articles = document['data']
        assert len(articles) == 1
        article = articles[0]
        assert 'article' == article['type']
        assert '1' == article['id']

    def test_related_resource(self):
        """Tests that a preprocessor is executed when fetching a
        related resource from a to-many relation.

        """
        person = self.Person(id=1)
        article = self.Article(id=1)
        article.author = person
        self.session.add_all([article, person])
        self.session.commit()

        data = {'triggered': False}

        def update_data(*args, **kw):
            data['triggered'] = True

        preprocessors = {'GET_RELATED_RESOURCE': [update_data]}
        self.manager.create_api(self.Person, preprocessors=preprocessors)
        # Need to create an API for Article resources so that each
        # Article has a URL.
        self.manager.create_api(self.Article)
        self.app.get('/api/person/1/articles/1')
        assert data['triggered']

    def test_change_related_resource_1(self):
        """Tests for changing the primary resource ID in a preprocessor
        for fetching a related resource.

        """
        person = self.Person(id=1)
        article = self.Article(id=1)
        article.author = person
        self.session.add_all([article, person])
        self.session.commit()

        def change_one(*args, **kw):
            # We will change the primary resource ID only.
            return 1

        preprocessors = {'GET_RELATED_RESOURCE': [change_one]}
        self.manager.create_api(self.Person, preprocessors=preprocessors)
        # Need to create an API for Article resources so that each
        # Article has a URL.
        self.manager.create_api(self.Article)
        response = self.app.get('/api/person/foo/articles/1')
        document = response.json
        resource = document['data']
        assert 'article' == resource['type']
        assert '1' == resource['id']

    def test_change_related_resource_2(self):
        """Tests for changing the primary resource ID and the relation
        name in a preprocessor for fetching a related resource.

        """
        person = self.Person(id=1)
        article = self.Article(id=1)
        article.author = person
        self.session.add_all([article, person])
        self.session.commit()

        def change_two(*args, **kw):
            # We will change the primary resource ID and the relation name.
            return 1, 'articles'

        preprocessors = {'GET_RELATED_RESOURCE': [change_two]}
        self.manager.create_api(self.Person, preprocessors=preprocessors)
        # Need to create an API for Article resources so that each
        # Article has a URL.
        self.manager.create_api(self.Article)
        response = self.app.get('/api/person/foo/bar/1')
        document = response.json
        resource = document['data']
        assert 'article' == resource['type']
        assert '1' == resource['id']

    def test_change_related_resource_3(self):
        """Tests for changing the primary resource ID, the relation
        name, and the related resource ID in a preprocessor for fetching
        a related resource.

        """
        person = self.Person(id=1)
        article = self.Article(id=1)
        article.author = person
        self.session.add_all([article, person])
        self.session.commit()

        def change_three(*args, **kw):
            # We will change the primary resource ID, the relation name,
            # and the related resource ID.
            return 1, 'articles', 1

        preprocessors = {'GET_RELATED_RESOURCE': [change_three]}
        self.manager.create_api(self.Person, preprocessors=preprocessors)
        # Need to create an API for Article resources so that each
        # Article has a URL.
        self.manager.create_api(self.Article)
        response = self.app.get('/api/person/foo/bar/baz')
        document = response.json
        resource = document['data']
        assert 'article' == resource['type']
        assert '1' == resource['id']

    def test_last_preprocessor_changes_id(self):
        """Tests that a return value from the last preprocessor in the list
        overrides the ID of the resource to fetch as given in the request URL.

        """
        person = self.Person(id=2, name=u'foo')
        self.session.add(person)
        self.session.commit()

        def increment_id(resource_id=None, **kw):
            if resource_id is None:
                raise ProcessingException
            return int(resource_id) + 1

        preprocessors = dict(GET_RESOURCE=[increment_id, increment_id])
        self.manager.create_api(self.Person, preprocessors=preprocessors)
        response = self.app.get('/api/person/0')
        assert response.status_code == 200
        document = response.json
        person = document['data']
        assert person['id'] == '2'
        assert person['attributes']['name'] == 'foo'

    def test_no_client_filters(self):
        """Tests that a preprocessor can modify the filter objects in a
        request, even if the client did not specify any ``filter[objects]``
        query parameter.

        """
        person1 = self.Person(id=1)
        person2 = self.Person(id=2)
        self.session.add_all([person1, person2])
        self.session.commit()

        def restrict_ids(filters=None, **kw):
            """Adds an additional filter to any existing filters that restricts
            which resources appear in the response.

            """
            if filters is None:
                raise ProcessingException
            filt = dict(name='id', op='lt', val=2)
            filters.append(filt)

        preprocessors = dict(GET_COLLECTION=[restrict_ids])
        self.manager.create_api(self.Person, preprocessors=preprocessors)
        response = self.app.get('/api/person')
        assert response.status_code == 200
        document = response.json
        people = document['data']
        assert ['1'] == sorted(person['id'] for person in people)

    def test_add_filters(self):
        """Tests that a preprocessor can modify the filter objects provided by
        the client in the ``filter[objects]`` query parameter.

        """
        person1 = self.Person(id=1)
        person2 = self.Person(id=2)
        person3 = self.Person(id=3)
        self.session.add_all([person1, person2, person3])
        self.session.commit()

        def restrict_ids(filters=None, **kw):
            """Adds an additional filter to any existing filters that restricts
            which resources appear in the response.

            """
            if filters is None:
                raise ProcessingException
            filt = dict(name='id', op='lt', val=2)
            filters.append(filt)

        preprocessors = dict(GET_COLLECTION=[restrict_ids])
        self.manager.create_api(self.Person, preprocessors=preprocessors)
        filters = [dict(name='id', op='in', val=[1, 3])]
        query = {'filter[objects]': dumps(filters)}
        response = self.app.get('/api/person', query_string=query)
        assert response.status_code == 200
        document = response.json
        people = document['data']
        assert ['1'] == sorted(person['id'] for person in people)

    def test_collection_postprocessor(self):
        """Tests that a postprocessor for a collection endpoint has access to
        the filters specified by the client.

        """
        client_filters = [dict(name='id', op='eq', val=1)]

        def check_filters(filters=None, **kw):
            """Assert that the filters that Flask-Restless understood from the
            request are the same filter objects provided by the client.

            """
            assert filters == client_filters

        postprocessors = dict(GET_COLLECTION=[check_filters])
        self.manager.create_api(self.Person, postprocessors=postprocessors)
        query_string = {'filter[objects]': dumps(client_filters)}
        response = self.app.get('/api/person', query_string=query_string)
        assert response.status_code == 200

    def test_resource_postprocessor(self):
        """Tests for a postprocessor for a single resource."""
        person = self.Person(id=1)
        self.session.add(person)
        self.session.commit()

        def modify_result(result=None, **kw):
            result['foo'] = 'bar'

        postprocessors = dict(GET_RESOURCE=[modify_result])
        self.manager.create_api(self.Person, postprocessors=postprocessors)
        response = self.app.get('/api/person/1')
        assert response.status_code == 200
        document = response.json
        assert document['foo'] == 'bar'


class TestDynamicRelationships(ManagerTestBase):
    """Tests for fetching resources from dynamic to-many relationships."""

    def setUp(self):
        """Creates the database, the :class:`~flask.Flask` object, the
        :class:`~flask_restless.manager.APIManager` for that application, and
        creates the ReSTful API endpoints for the :class:`TestSupport.Person`
        and :class:`TestSupport.Article` models.

        """
        super(TestDynamicRelationships, self).setUp()

        class Article(self.Base):
            __tablename__ = 'article'
            id = Column(Integer, primary_key=True)
            author_id = Column(Integer, ForeignKey('person.id'))

        class Person(self.Base):
            __tablename__ = 'person'
            id = Column(Integer, primary_key=True)
            articles = relationship(Article, lazy='joined', backref='author')

        self.Article = Article
        self.Person = Person
        self.Base.metadata.create_all(bind=self.engine)
        self.manager.create_api(Article)
        self.manager.create_api(Person)

    def test_to_many(self):
        """Tests for fetching a resource with a dynamic link to a to-many
        relation.

        """
        person = self.Person(id=1)
        article1 = self.Article(id=1)
        article2 = self.Article(id=2)
        article1.author = person
        article2.author = person
        self.session.add_all([person, article1, article2])
        self.session.commit()
        response = self.app.get('/api/person/1')
        document = response.json
        person = document['data']
        articles = person['relationships']['articles']['data']
        assert ['1', '2'] == sorted(article['id'] for article in articles)

    def test_related_resource_url(self):
        """Tests for fetching a resource with a dynamic link to a to-many
        relation from the related resource URL.

        """
        person = self.Person(id=1)
        article1 = self.Article(id=1)
        article2 = self.Article(id=2)
        person.articles = [article1, article2]
        self.session.add_all([person, article1, article2])
        self.session.commit()
        response = self.app.get('/api/person/1/articles')
        assert response.status_code == 200
        document = response.json
        articles = document['data']
        assert ['1', '2'] == sorted(article['id'] for article in articles)
        assert all(article['type'] == 'article' for article in articles)

    def test_relationship_url(self):
        """Tests for fetching a resource with a dynamic link to a to-many
        relation from the relationship URL.

        """
        person = self.Person(id=1)
        article1 = self.Article(id=1)
        article2 = self.Article(id=2)
        person.articles = [article1, article2]
        self.session.add_all([person, article1, article2])
        self.session.commit()
        response = self.app.get('/api/person/1/relationships/articles')
        assert response.status_code == 200
        document = response.json
        articles = document['data']
        assert ['1', '2'] == sorted(article['id'] for article in articles)
        assert all(article['type'] == 'article' for article in articles)


class TestAssociationProxy(ManagerTestBase):
    """Tests for getting an object with a relationship using an association
    proxy.

    """

    def setUp(self):
        """Creates the database, the :class:`~flask.Flask` object, the
        :class:`~flask_restless.manager.APIManager` for that application,
        and creates the ReSTful API endpoints for the models used in the test
        methods.

        """
        super(TestAssociationProxy, self).setUp()

        class Article(self.Base):
            __tablename__ = 'article'
            id = Column(Integer, primary_key=True)
            tags = association_proxy('articletags', 'tag',
                                     creator=lambda tag: ArticleTag(tag=tag))

        class ArticleTag(self.Base):
            __tablename__ = 'articletag'
            article_id = Column(Integer, ForeignKey('article.id'),
                                primary_key=True)
            article = relationship(Article, backref=backref('articletags'))
            tag_id = Column(Integer, ForeignKey('tag.id'), primary_key=True)
            tag = relationship('Tag')
            # TODO this dummy column is required to create an API for this
            # object.
            id = Column(Integer)

        class Tag(self.Base):
            __tablename__ = 'tag'
            id = Column(Integer, primary_key=True)
            name = Column(Unicode)

        self.Article = Article
        self.Tag = Tag
        self.Base.metadata.create_all(bind=self.engine)
        self.manager.create_api(Article)
        # HACK Need to create APIs for these other models because otherwise
        # we're not able to create the link URLs to them.
        #
        # TODO Fix this by simply not creating links to related models for
        # which no API has been made.
        self.manager.create_api(Tag)
        self.manager.create_api(ArticleTag, primary_key='article_id')

    def test_fetch(self):
        """Test for fetching a resource that has a many-to-many relation that
        uses an association proxy.

        """
        article = self.Article(id=1)
        tag = self.Tag(id=1)
        article.tags.append(tag)
        with self.flaskapp.app_context():
            self.session.add_all([article, tag])
            self.session.commit()
        response = self.app.get('/api/article/1')
        document = response.json
        article = document['data']
        tags = article['relationships']['tags']['data']
        assert ['1'] == sorted(tag['id'] for tag in tags)


class TestFlaskSQLAlchemy(FlaskSQLAlchemyTestBase):
    """Tests for fetching resources defined as Flask-SQLAlchemy models
    instead of pure SQLAlchemy models.

    """

    def setUp(self):
        """Creates the Flask-SQLAlchemy database and models."""
        super(TestFlaskSQLAlchemy, self).setUp()

        class Person(self.db.Model):
            id = self.db.Column(self.db.Integer, primary_key=True)

        self.Person = Person
        self.db.create_all()
        self.manager = APIManager(self.flaskapp, session=self.db.session)
        self.manager.create_api(self.Person)

    def test_fetch_resource(self):
        """Test for fetching a resource."""
        person = self.Person(id=1)
        self.session.add(person)
        self.session.commit()
        response = self.app.get('/api/person/1')
        document = response.json
        person = document['data']
        assert person['id'] == '1'
        assert person['type'] == 'person'

    def test_fetch_collection(self):
        """Test for fetching a collection of resource."""
        person1 = self.Person(id=1)
        person2 = self.Person(id=2)
        self.session.add_all([person1, person2])
        self.session.commit()
        response = self.app.get('/api/person')
        document = response.json
        people = document['data']
        assert ['1', '2'] == sorted(person['id'] for person in people)
