From 2edf0ba8b0a0f026a96bd122c96a0c0e8c59507a Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 28 Jun 2016 18:59:20 +0200 Subject: [PATCH] the application was rewritten a bit to be more testable --- api/__init__.py | 49 +-------------------------------- api/core.py | 30 ++++++++++++++++++++ api/extensions/__init__.py | 0 api/extensions/esales.py | 12 ++++++++ api/helpers.py | 12 ++++++++ api/tasks/esales.py | 6 ++-- api/views/server.py | 11 ++++++++ esales/__init__.py | 1 + esales/cluster.py | 2 +- manage.py | 5 ++-- tests/api/__init__.py | 0 tests/{ => api}/test_helpers.py | 0 tests/api/test_web.py | 46 +++++++++++++++++++++++++++++++ wsgi.py | 2 ++ 14 files changed, 122 insertions(+), 54 deletions(-) create mode 100644 api/core.py create mode 100644 api/extensions/__init__.py create mode 100644 api/extensions/esales.py create mode 100644 api/views/server.py create mode 100644 tests/api/__init__.py rename tests/{ => api}/test_helpers.py (100%) create mode 100644 tests/api/test_web.py create mode 100644 wsgi.py diff --git a/api/__init__.py b/api/__init__.py index d8e5390..a8ce586 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -1,48 +1 @@ -from flask import Flask, jsonify, Response, request -from flask_sqlalchemy import SQLAlchemy -from esales.cluster import Cluster - -app = Flask(__name__) -app.config.from_object('config') - - -def on_404(e): - return jsonify(dict(error='Not found')), 404 - -app.errorhandler(404)(on_404) - -# SQLAlchemy -db = SQLAlchemy(app) - -# eSales -esales = Cluster(app.config.get("ESALES_URL")) - -from api.views import designers, categories -app.register_blueprint(designers.mod) -app.register_blueprint(categories.mod) - - -def check_authentication(): - auth = request.authorization - if not auth: - return False - return auth.username in app.config.get("API_KEYS") - - -@app.before_request -def authenticate(): - if not check_authentication(): - return Response( - 'No valid API key provided', 401, - {'WWW-Authenticate': 'Basic realm="Login Required"'} - ) - - -@app.route("/") -def index(): - data = { - "info": { - "api_version": "0.0.1" - } - } - return jsonify(data) +from .core import * \ No newline at end of file diff --git a/api/core.py b/api/core.py new file mode 100644 index 0000000..1e18545 --- /dev/null +++ b/api/core.py @@ -0,0 +1,30 @@ +from flask import Flask, jsonify, Response, request +from flask_sqlalchemy import SQLAlchemy +from api.extensions.esales import Esales +from api.helpers import check_api_key + +db = SQLAlchemy() +esales = Esales() + +def create_app(config_name='config'): + app = Flask(__name__) + app.config.from_object(config_name) + + db.init_app(app) + esales.init_app(app) + + # register errors handlers + @app.errorhandler(404) + def on_404(e): + return jsonify(dict(error='Not found')), 404 + + # register blueprints + from api.views import server, designers, categories + app.register_blueprint(server.mod) + app.register_blueprint(designers.mod) + app.register_blueprint(categories.mod) + + # authentication + app.before_request(check_api_key) + + return app \ No newline at end of file diff --git a/api/extensions/__init__.py b/api/extensions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/extensions/esales.py b/api/extensions/esales.py new file mode 100644 index 0000000..d64e4bd --- /dev/null +++ b/api/extensions/esales.py @@ -0,0 +1,12 @@ +from esales.cluster import Cluster + +class Esales(object): + + def __init__(self, app=None): + self.cluster = Cluster() + + if app: + self.init_app(app) + + def init_app(self, app): + self.cluster.url = app.config.get('ESALES_URL') \ No newline at end of file diff --git a/api/helpers.py b/api/helpers.py index bd3bed1..6965c55 100644 --- a/api/helpers.py +++ b/api/helpers.py @@ -1,7 +1,19 @@ # coding=UTF-8 +from flask import request, current_app, Response import unidecode import re +def check_api_key(): + api_keys = current_app.config.get("API_KEYS") + if not api_keys: + return + auth = request.authorization + if not auth or not auth.username in api_keys: + return Response( + 'No valid API key provided', 401, + {'WWW-Authenticate': 'Basic realm="Login Required"'} + ) + def slugify(text): """Converts a string to a human-readable url""" diff --git a/api/tasks/esales.py b/api/tasks/esales.py index ec979b1..a2092ef 100644 --- a/api/tasks/esales.py +++ b/api/tasks/esales.py @@ -1,5 +1,5 @@ # coding=utf-8 -from api import app +from flask import current_app from api.models.locale import Locale from esales.models import Category, CategoryTree from esales.utils import ImportFile @@ -25,7 +25,7 @@ def update_designer(designer): tree.add_category(category) importfile.add_operation("update", tree) - directory = app.config.get("ESALES_IMPORT_DIR") + directory = current_app.config.get("ESALES_IMPORT_DIR") importfile.write(directory) @@ -46,5 +46,5 @@ def update_category(category): )) importfile.add_operation("update", tree) - directory = app.config.get("ESALES_IMPORT_DIR") + directory = current_app.config.get("ESALES_IMPORT_DIR") importfile.write(directory) diff --git a/api/views/server.py b/api/views/server.py new file mode 100644 index 0000000..18b8a3e --- /dev/null +++ b/api/views/server.py @@ -0,0 +1,11 @@ +from flask import Blueprint, jsonify + +mod = Blueprint('server', __name__, url_prefix='/') + +@mod.route("/", methods=["GET"]) +def status(): + data = { + 'version': '0.0.1', + 'status': 'ok' + } + return jsonify(data) diff --git a/esales/__init__.py b/esales/__init__.py index e69de29..c7630a1 100644 --- a/esales/__init__.py +++ b/esales/__init__.py @@ -0,0 +1 @@ +from .cluster import Cluster \ No newline at end of file diff --git a/esales/cluster.py b/esales/cluster.py index 338b0da..cc586a5 100644 --- a/esales/cluster.py +++ b/esales/cluster.py @@ -14,7 +14,7 @@ class CommandException(Exception): class Cluster(object): - def __init__(self, url): + def __init__(self, url=None): self.url = url def run_command(self, command): diff --git a/manage.py b/manage.py index a8a54a1..12940a0 100644 --- a/manage.py +++ b/manage.py @@ -1,7 +1,8 @@ from flask.ext.script import Manager, prompt_bool -from api import app, db, esales +from api import create_app, db, esales from api.models import Designer +app = create_app() manager = Manager(app) @@ -30,7 +31,7 @@ def seed_db(): def run_esales_import(): print("Importing products and categories into eSales") pattern = "{}/*.xml".format(app.config.get("ESALES_IMPORT_DIR")) - esales.import_products_and_categories(pattern) + esales.cluster.import_products_and_categories(pattern) if __name__ == '__main__': manager.run() diff --git a/tests/api/__init__.py b/tests/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_helpers.py b/tests/api/test_helpers.py similarity index 100% rename from tests/test_helpers.py rename to tests/api/test_helpers.py diff --git a/tests/api/test_web.py b/tests/api/test_web.py new file mode 100644 index 0000000..786d8a7 --- /dev/null +++ b/tests/api/test_web.py @@ -0,0 +1,46 @@ +# coding=UTF-8 + +import unittest2 +import base64 +from api import create_app + +try: + import httplib +except ImportError: + # support for python 3 + import http.client as httplib + + +class ApiTest(unittest2.TestCase): + + def setUp(self): + self.app = create_app('tests.config') + self.client = self.app.test_client() + + +class TestHttpBasicAuth(ApiTest): + + api_key = 'secret' + + def _enable_basic_auth(self): + self.app.config['API_KEYS'] = [self.api_key] + + def _create_auth_headers(self, username, password=''): + data = base64.b64encode(bytes(':'.join([username, password]), 'ascii')).decode('ascii') + headers = [('Authorization', 'Basic %s' % data)] + return headers + + def test_missing_credentials(self): + self._enable_basic_auth() + resp = self.client.get('/') + self.assertEqual(resp.status_code, httplib.UNAUTHORIZED) + + def test_correct_credentials(self): + self._enable_basic_auth() + headers = self._create_auth_headers(self.api_key) + resp = self.client.get('/', headers=headers) + self.assertEqual(resp.status_code, httplib.OK) + + def test_basic_auth_disabled(self): + resp = self.client.get('/') + self.assertEqual(resp.status_code, httplib.OK) \ No newline at end of file diff --git a/wsgi.py b/wsgi.py new file mode 100644 index 0000000..dde7077 --- /dev/null +++ b/wsgi.py @@ -0,0 +1,2 @@ +from api import create_app +app = create_app() \ No newline at end of file