diff --git a/news/app.py b/news/app.py index 107551d..85d973b 100644 --- a/news/app.py +++ b/news/app.py @@ -1,19 +1,76 @@ -from apistar import Include, Route +from apistar import Include, Route, http +from apistar.interfaces import Router from apistar.frameworks.wsgi import WSGIApp as App from apistar.backends import sqlalchemy_backend from apistar.backends.sqlalchemy_backend import Session from apistar.handlers import docs_urls, static_urls -from .models import Base +from .models import Base, NewsArticle, NewsSource from .renders import JSONRenderer +from .schema import NewsSourceSchema +news_source_schema = NewsSourceSchema() -def welcome(): - return {"message": "welcome to news"} + +def get_sources(session: Session): + sources = session.query(NewsSource).all() + return news_source_schema.dump(sources, many=True).data + + +def add_source(session: Session, request_data: http.RequestData, router: Router): + news_source_data, errors = news_source_schema.load(request_data) + if errors: + msg = {"message": "400 Bad Request", "error": errors} + return http.Response(msg, status=400) + + news_source = NewsSource(**news_source_data) + session.add(news_source) + session.flush() + + headers = {"Location": router.reverse_url('get_source', {"id": news_source.id})} + + return http.Response(news_source_schema.dump(news_source).data, status=201, headers=headers) + + +def delete_source(session: Session, id: int): + """Delete a single News Sources from the collection by id""" + news_source = session.query(NewsSource).filter_by(id=id).one_or_none() + if news_source is None: + msg = {"message": "404 Not Found"} + return http.Response(msg, status=404) + + msg = {"message": "200 OK"} + return http.Response(msg, status=200) + + +def get_source(session: Session, id: int): + news_source = session.query(NewsSource).filter_by(id=id).one_or_none() + if news_source is None: + msg = {"message": "404 Not Found"} + return http.Response(msg, status=404) + + return http.Response(news_source_schema.dump(news_source).data, status=200) + + +def get_articles(session: Session): + articles = session.query(NewsArticle).all() + return [article.to_dict() for article in articles] + + +def add_article(session: Session): + pass + + +def delete_article(session: Session): + pass routes = [ - Route('/', 'GET', welcome), + Route('/sources', 'GET', get_sources), + Route('/sources', 'POST', add_source), + Route('/sources/{id}', 'GET', get_source), + Route('/sources/{id}', 'DELETE', delete_source), + Route('/articles', 'GET', get_articles), Include('/docs', docs_urls), Include('/static', static_urls) ] @@ -26,7 +83,6 @@ _settings = { 'RENDERERS': [JSONRenderer()] } - routes = routes commands = sqlalchemy_backend.commands diff --git a/news/renders.py b/news/renders.py index 353cca4..4ed0318 100644 --- a/news/renders.py +++ b/news/renders.py @@ -9,7 +9,7 @@ from apistar.renderers import Renderer def extended_encoder(obj): """JSON encoder function with support for ISO 8601 datetime serialization and Decimal to float casting""" if isinstance(obj, dt.datetime): - return obj.isoformat() + return obj.timestamp() elif isinstance(obj, decimal.Decimal): return float(obj) diff --git a/news/schema.py b/news/schema.py new file mode 100644 index 0000000..6d46cb9 --- /dev/null +++ b/news/schema.py @@ -0,0 +1,13 @@ +from marshmallow import Schema, fields + + +class NewsSourceSchema(Schema): + id = fields.Int(dump_only=True) + created_date = fields.DateTime(dump_only=True) + modified_date = fields.DateTime() + url = fields.URL() + source_name = fields.Str(required=True, error_messages={'required': 'NewsSource name is a required field'}) + source_type = fields.Str(required=True, error_messages={'required': 'NewsSource tyoe is a required field'}) + + class Meta: + ordered = True \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 4bad142..ecf0bb4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,8 +9,7 @@ from news.app import commands, routes, components settings = { 'DATABASE': { - 'URL': 'postgresql://apistar:local@localhost/news', - #'URL': 'sqlite:///', + 'URL': 'postgresql://apistar:local@localhost/testnews', 'METADATA': Base.metadata }, 'RENDERERS': [JSONRenderer()], diff --git a/tests/test_app_routes.py b/tests/test_app_routes.py index b890fe4..9720da2 100644 --- a/tests/test_app_routes.py +++ b/tests/test_app_routes.py @@ -1,5 +1,143 @@ -from news.app import welcome +import datetime as dt +from unittest import mock -def test_welcome_route(): - message = {"message": "welcome to news"} - assert message == welcome() \ No newline at end of file +import pytest + +from news.app import get_articles, get_sources, add_source, get_source, delete_source +from news.models import NewsArticle, NewsSource, Category, Tag + + +# Sources + +def test_get_empty__news_sources(rb_session): + assert [] == get_sources(rb_session) + + +def test_get_news_sources(rb_session): + test_source = NewsSource(url='http://money.test.com', + source_name='TEST', + source_type='website', + categories=[Category(category_name='finance')] + ) + rb_session.add(test_source) + rb_session.flush() + + assert 1 == len(get_sources(rb_session)) + + +def test_add_news_source(rb_session): + test_source = { + 'source_name': 'TEST', + 'source_type': 'website', + 'url': 'http://money.test.com' + } + + mock_router = mock.Mock() + mock_router.configure_mock( + **{"reverse_url.return_value": "/mylocation"} + ) + + result = add_source(rb_session, test_source, mock_router) + assert 201 == result.status + assert 'location' in result.headers.keys() + + +def test_add_news_source_error(rb_session): + """Testing with missing required field""" + test_source = { + 'source_type': 'website', + 'url': 'http://money.test.com' + } + + mock_router = mock.Mock() + mock_router.configure_mock( + **{"reverse_url.return_value": "/mylocation"} + ) + + result = add_source(rb_session, test_source, mock_router) + print(result.headers) + assert 400 == result.status + + +def test_get_news_source(rb_session): + test_source = NewsSource(url='http://money.test.com', + source_name='TEST', + source_type='website', + categories=[Category(category_name='finance')] + ) + rb_session.add(test_source) + rb_session.flush() + + result = get_source(rb_session, 1) + + assert 200 == result.status + + +def test_get_news_source_404_error(rb_session): + test_source = NewsSource(url='http://money.test.com', + source_name='TEST', + source_type='website', + categories=[Category(category_name='finance')] + ) + rb_session.add(test_source) + rb_session.flush() + + result = get_source(rb_session, 2) + + assert 404 == result.status + + +def test_delete_news_source(rb_session): + test_source = NewsSource(url='http://money.test.com', + source_name='TEST', + source_type='website', + categories=[Category(category_name='finance')] + ) + rb_session.add(test_source) + rb_session.flush() + + result = delete_source(rb_session, 1) + + assert 200 == result.status + + +def test_delete_news_source_404(rb_session): + test_source = NewsSource(url='http://money.test.com', + source_name='TEST', + source_type='website', + categories=[Category(category_name='finance')] + ) + rb_session.add(test_source) + rb_session.flush() + + result = delete_source(rb_session, 2) + + assert 404 == result.status + + +# Articles + +def test_get_empty_articles(rb_session): + assert get_articles(rb_session) == [] + + +def test_get_articles(rb_session): + source = NewsSource(url='http://money.test.com', + source_name='TEST', + source_type='website', + categories=[Category(category_name='finance')] + ) + + article = NewsArticle(url='http://money.test.com/article', + title='article', + authors=['drew', 'jesse'], + publish_date=dt.datetime.utcnow(), + news_blob='article content', + news_source=source, + tags=[Tag(tag_name='article')] + ) + + rb_session.add(article) + rb_session.flush() + + assert 1 == len(get_articles(rb_session)) diff --git a/tests/test_renders.py b/tests/test_renders.py index 7de3a9a..01ba1f9 100644 --- a/tests/test_renders.py +++ b/tests/test_renders.py @@ -7,7 +7,7 @@ from news.renders import extended_encoder, JSONRenderer def test_extended_encoder_date_parsing(): test_date = dt.datetime(2017, 5, 10) - assert test_date.isoformat() == extended_encoder(test_date) + assert test_date.timestamp() == extended_encoder(test_date) def test_extended_encoder_decimal_casting(): @@ -18,6 +18,6 @@ def test_extended_encoder_decimal_casting(): def test_render_with_extended_encoder(): test_date = dt.datetime(2017, 5, 10) test_decimal = Decimal('0.1') - expected = dict(my_date="2017-05-10T00:00:00", my_float=0.1) + expected = dict(my_date=1494388800.0, my_float=0.1) test_response = dict(my_date=test_date, my_float=test_decimal) assert json.dumps(expected).encode('utf-8') == JSONRenderer().render(test_response) \ No newline at end of file