From fd43c0f1574bf169c36ad1759bd090e2320f5301 Mon Sep 17 00:00:00 2001 From: Denis Makogon Date: Sat, 19 Nov 2016 04:06:24 +0200 Subject: [PATCH 1/3] Implements integration testing framework - reusing functional tests for integration purposes - common parts of both test frameworks were extracted to tests/common - tox.ini updated - README updated - test_regression.sh updated to run regression checks on both integration and functional --- README.md | 68 ++++++++++- laos/api/controllers/apps.py | 3 +- laos/tests/common/__init__.py | 0 laos/tests/common/apps.py | 84 ++++++++++++++ laos/tests/common/base.py | 42 +++++++ laos/tests/{functional => common}/client.py | 5 +- laos/tests/common/routes.py | 118 ++++++++++++++++++++ laos/tests/functional/base.py | 22 +--- laos/tests/functional/test_apps.py | 61 ++-------- laos/tests/functional/test_routes.py | 105 +++-------------- laos/tests/integration/__init__.py | 0 laos/tests/integration/base.py | 72 ++++++++++++ laos/tests/integration/test_apps.py | 38 +++++++ laos/tests/integration/test_routes.py | 50 +++++++++ scripts/test_regression.sh | 12 +- tox.ini | 15 ++- 16 files changed, 517 insertions(+), 178 deletions(-) create mode 100644 laos/tests/common/__init__.py create mode 100644 laos/tests/common/apps.py create mode 100644 laos/tests/common/base.py rename laos/tests/{functional => common}/client.py (95%) create mode 100644 laos/tests/common/routes.py create mode 100644 laos/tests/integration/__init__.py create mode 100644 laos/tests/integration/base.py create mode 100644 laos/tests/integration/test_apps.py create mode 100644 laos/tests/integration/test_routes.py diff --git a/README.md b/README.md index 3d85bb1..ae88768 100644 --- a/README.md +++ b/README.md @@ -61,10 +61,6 @@ Install dependencies: $ pip install -r requirements.txt -r test-requirements.txt -Install `functions_python` lib: - - $ pip install -e git+ssh://git@github.com/iron-io/functions_python.git#egg=functions-python - Install LaOS itself: $ pip install -e . @@ -191,8 +187,70 @@ Cons: Testing: Integration -------------------- -TBD +Integration tests are dependent on following env variables: +* TEST_DB_URI - similar to functional tests, database endpoint +* FUNCTIONS_HOST - IronFunctions API host +* FUNCTIONS_PORT - IronFunctions API port +* FUNCTIONS_API_PROTO - IronFunctions API protocol +* FUNCTIONS_API_VERSION - IronFunctions API version +* OS_AUTH_URL - OpenStack Identity endpoint +* OS_PROJECT_ID - OpenStack user-specific project ID +* OS_TOKEN - OpenStack user project-bound auth token + +How to get token? - Set env variables: + +* OS_USERNAME +* OS_PASSWORD +* OS_PROJECT_NAME + +and run following command: + + echo -e "{ + \"auth\": { + \"identity\": { + \"methods\": [\"password\"], + \"password\": { + \"user\": { + \"name\": \"${OS_USERNAME:-admin}\", + \"domain\": { \"id\": \"${OS_DOMAIN:-default}\" }, + \"password\": \"${OS_PASSWORD:-root}\" + } + } + }, + \"scope\": { + \"project\": { + \"name\": \"${OS_PROJECT_NAME:-admin}\", + \"domain\": {\"id\": \"${OS_DOMAIN:-default}\" } + } + } + } + }" >> token_request.json + +After that: + + export OS_TOKEN=`curl -si -d @token_request.json -H "Content-type: application/json" ${OS_AUTH_URL}/auth/tokens | awk '/X-Subject-Token/ {print $2}'` + +If variable remains empty, please check your authentication parameters specified in `token_request.json`. + +How to get project ID? - Use following command + + export RAW_PrID=`curl -si -d @token_request.json -H "Content-type: application/json" ${OS_AUTH_URL}/auth/tokens | grep Default | awk '{print $20}'` + export OS_PROJECT_ID=${RAW_PrID:1:-2} + +If variable remains empty, please check your authentication parameters specified in `token_request.json`. + +To run tests use following command: + + export TEST_DB_URI=mysql://:@:/ + export FUNCTIONS_HOST= + export FUNCTIONS_PORT= + export FUNCTIONS_API_PROTO= + export FUNCTIONS_API_VERSION= + export OS_AUTH_URL= + export OS_PROJECT_ID= + export OS_TOKEN= + tox -epy35-integration Testing: Coverage regression ---------------------------- diff --git a/laos/api/controllers/apps.py b/laos/api/controllers/apps.py index 81495aa..90447a1 100644 --- a/laos/api/controllers/apps.py +++ b/laos/api/controllers/apps.py @@ -210,7 +210,8 @@ class AppV1Controller(controllers.ServiceControllerBase): if fn_app_routes: return web.json_response(data={ "error": { - "message": ("Unable to delete app {} with routes".format(app)) + "message": ("Unable to delete app {} " + "with routes".format(app)) } }, status=403) diff --git a/laos/tests/common/__init__.py b/laos/tests/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/laos/tests/common/apps.py b/laos/tests/common/apps.py new file mode 100644 index 0000000..3f5f048 --- /dev/null +++ b/laos/tests/common/apps.py @@ -0,0 +1,84 @@ +# 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. + + +class AppsTestSuite(object): + + def list_apps(self): + json, http_status = self.testloop.run_until_complete( + self.test_client.apps.list()) + self.assertIn("message", json) + self.assertIn("apps", json) + self.assertEqual(200, http_status) + + def get_unknown(self): + json, http_status = self.testloop.run_until_complete( + self.test_client.apps.show("unknown")) + self.assertEqual(404, http_status) + self.assertIn("error", json) + + def create_and_delete(self): + create_json, create_status = self.testloop.run_until_complete( + self.test_client.apps.create("testapp")) + delete_json, delete_status = self.testloop.run_until_complete( + self.test_client.apps.delete(create_json["app"]["name"])) + + self.assertIn("message", create_json) + self.assertIn("app", create_json) + self.assertEqual(200, create_status) + + self.assertIn("message", delete_json) + self.assertEqual(200, delete_status) + + def attempt_to_double_create(self): + app = "testapp" + create_json, _ = self.testloop.run_until_complete( + self.test_client.apps.create(app)) + err, status = self.testloop.run_until_complete( + self.test_client.apps.create(app)) + self.testloop.run_until_complete( + self.test_client.apps.delete(create_json["app"]["name"])) + self.assertEqual(409, status) + self.assertIn("error", err) + self.assertIn("message", err["error"]) + + def attempt_delete_unknonw(self): + json, http_status = self.testloop.run_until_complete( + self.test_client.apps.delete("unknown")) + self.assertEqual(404, http_status) + self.assertIn("error", json) + + def delete_with_routes(self): + app_name = "testapp" + app, _ = self.testloop.run_until_complete( + self.test_client.apps.create(app_name)) + self.testloop.run_until_complete( + self.test_client.routes.create( + app["app"]["name"], **self.route_data) + ) + attempt, status = self.testloop.run_until_complete( + self.test_client.apps.delete(app["app"]["name"]) + ) + self.testloop.run_until_complete( + self.test_client.routes.delete( + app["app"]["name"], self.route_data["path"]) + ) + _, status_2 = self.testloop.run_until_complete( + self.test_client.apps.delete(app["app"]["name"]) + ) + self.assertEqual(403, status) + self.assertIn("error", attempt) + self.assertIn("message", attempt["error"]) + self.assertIn("with routes", attempt["error"]["message"]) + self.assertEqual(200, status_2) diff --git a/laos/tests/common/base.py b/laos/tests/common/base.py new file mode 100644 index 0000000..924adab --- /dev/null +++ b/laos/tests/common/base.py @@ -0,0 +1,42 @@ +# 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. + +import asyncio +import datetime +import uvloop + +from laos.common import logger as log + + +class LaosTestsBase(object): + + def get_loop_and_logger(self, test_type): + self.route_data = { + "type": "sync", + "path": "/hello-sync-private", + "image": "iron/hello", + "is_public": "false" + } + try: + testloop = asyncio.get_event_loop() + except Exception: + asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) + testloop = asyncio.get_event_loop() + + logger = log.UnifiedLogger( + log_to_console=False, + filename=("/tmp/laos-{}-tests-run-{}.log" + .format(test_type, datetime.datetime.now())), + level="DEBUG").setup_logger(__package__) + return testloop, logger diff --git a/laos/tests/functional/client.py b/laos/tests/common/client.py similarity index 95% rename from laos/tests/functional/client.py rename to laos/tests/common/client.py index 5639dfa..a9009b4 100644 --- a/laos/tests/functional/client.py +++ b/laos/tests/common/client.py @@ -74,12 +74,15 @@ class RoutesV1(object): class ProjectBoundLaosTestClient(test_utils.TestClient): - def __init__(self, app_or_server, project_id): + def __init__(self, app_or_server, project_id, **kwargs): super(ProjectBoundLaosTestClient, self).__init__(app_or_server) self.project_id = project_id self.headers = { "Content-Type": "application/json" } + if kwargs.get("headers"): + self.headers.update(kwargs.get("headers")) + self.apps = AppsV1(self) self.routes = RoutesV1(self) diff --git a/laos/tests/common/routes.py b/laos/tests/common/routes.py new file mode 100644 index 0000000..34197da --- /dev/null +++ b/laos/tests/common/routes.py @@ -0,0 +1,118 @@ +# 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. + +import json as jsonlib + + +class AppRoutesTestSuite(object): + + def list_routes_from_unknown_app(self): + json, status = self.testloop.run_until_complete( + self.test_client.routes.list("uknown_app") + ) + self.assertEqual(status, 404) + self.assertIn("error", json) + self.assertIn("not found", json["error"]["message"]) + + def list_routes_from_existing_app(self): + create_json, _ = self.testloop.run_until_complete( + self.test_client.apps.create("testapp")) + json, status = self.testloop.run_until_complete( + self.test_client.routes.list(create_json["app"]["name"]) + ) + self.testloop.run_until_complete( + self.test_client.apps.delete(create_json["app"]["name"])) + self.assertEqual(status, 200) + self.assertIn("routes", json) + self.assertIn("message", json) + + def show_unknown_route_from_existing_app(self): + path = "/unknown_path" + create_json, _ = self.testloop.run_until_complete( + self.test_client.apps.create("testapp")) + json, status = self.testloop.run_until_complete( + self.test_client.routes.show( + create_json["app"]["name"], path) + ) + self.testloop.run_until_complete( + self.test_client.apps.delete(create_json["app"]["name"])) + + self.assertEqual(404, status) + self.assertIn("error", json) + self.assertIn("not found", json["error"]["message"]) + + def delete_unknown_route_from_existing_app(self): + create_json, _ = self.testloop.run_until_complete( + self.test_client.apps.create("testapp")) + json, status = self.testloop.run_until_complete( + self.test_client.routes.delete( + create_json["app"]["name"], "/unknown_path") + ) + self.testloop.run_until_complete( + self.test_client.apps.delete(create_json["app"]["name"])) + + self.assertEqual(404, status) + self.assertIn("error", json) + self.assertIn("not found", json["error"]["message"]) + + def create_and_delete_route(self): + app, _ = self.testloop.run_until_complete( + self.test_client.apps.create("testapp")) + route, create_status = self.testloop.run_until_complete( + self.test_client.routes.create( + app["app"]["name"], **self.route_data) + ) + route_deleted, delete_status = self.testloop.run_until_complete( + self.test_client.routes.delete( + app["app"]["name"], self.route_data["path"]) + ) + self.testloop.run_until_complete( + self.test_client.apps.delete(app["app"]["name"])) + after_post = route["route"] + for k in self.route_data: + if k == "path": + self.assertIn(self.route_data["path"], after_post[k]) + continue + if k == "is_public": + is_public = jsonlib.loads(self.route_data[k]) + self.assertEqual(is_public, after_post[k]) + continue + self.assertEqual(self.route_data[k], after_post[k]) + self.assertEqual(200, delete_status) + self.assertEqual(200, create_status) + self.assertIn("message", route_deleted) + + def double_create_route(self): + app, _ = self.testloop.run_until_complete( + self.test_client.apps.create("testapp")) + self.testloop.run_until_complete( + self.test_client.routes.create( + app["app"]["name"], **self.route_data) + ) + + json, double_create_status = self.testloop.run_until_complete( + self.test_client.routes.create( + app["app"]["name"], **self.route_data) + ) + self.testloop.run_until_complete( + self.test_client.routes.delete( + app["app"]["name"], self.route_data["path"]) + ) + self.testloop.run_until_complete( + self.test_client.apps.delete(app["app"]["name"])) + + self.assertEqual(409, double_create_status) + self.assertIn("error", json) + self.assertIn("message", json["error"]) + self.assertIn("already exist", json["error"]["message"]) diff --git a/laos/tests/functional/base.py b/laos/tests/functional/base.py index 58e71c2..7237049 100644 --- a/laos/tests/functional/base.py +++ b/laos/tests/functional/base.py @@ -12,13 +12,10 @@ # License for the specific language governing permissions and limitations # under the License. -import asyncio import collections -import datetime import os import testtools import uuid -import uvloop from laos.api.controllers import apps from laos.api.controllers import routes @@ -28,27 +25,16 @@ from laos.api.middleware import content_type from laos.common.base import service from laos.common import config -from laos.common import logger as log - +from laos.tests.common import base +from laos.tests.common import client from laos.tests.fakes import functions_api -from laos.tests.functional import client -class LaosFunctionalTestsBase(testtools.TestCase): +class LaosFunctionalTestsBase(base.LaosTestsBase, testtools.TestCase): def setUp(self): - try: - self.testloop = asyncio.get_event_loop() - except Exception: - asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) - self.testloop = asyncio.get_event_loop() - - logger = log.UnifiedLogger( - log_to_console=False, - filename=("/tmp/laos-integration-tests-run-{}.log" - .format(datetime.datetime.now())), - level="DEBUG").setup_logger(__package__) + self.testloop, logger = self.get_loop_and_logger("functional") self.testapp = service.AbstractWebServer( host="localhost", diff --git a/laos/tests/functional/test_apps.py b/laos/tests/functional/test_apps.py index 02c5802..adc5038 100644 --- a/laos/tests/functional/test_apps.py +++ b/laos/tests/functional/test_apps.py @@ -12,71 +12,26 @@ # License for the specific language governing permissions and limitations # under the License. +from laos.tests.common import apps as apps_suite from laos.tests.functional import base -class TestApps(base.LaosFunctionalTestsBase): +class TestApps(base.LaosFunctionalTestsBase, apps_suite.AppsTestSuite): def test_list_apps(self): - json, http_status = self.testloop.run_until_complete( - self.test_client.apps.list()) - self.assertIn("message", json) - self.assertIn("apps", json) - self.assertEqual(200, http_status) + super(TestApps, self).list_apps() def test_get_unknown(self): - json, http_status = self.testloop.run_until_complete( - self.test_client.apps.show("unknown")) - self.assertEqual(404, http_status) - self.assertIn("error", json) + super(TestApps, self).get_unknown() def test_create_and_delete(self): - create_json, create_status = self.testloop.run_until_complete( - self.test_client.apps.create("testapp")) - delete_json, delete_status = self.testloop.run_until_complete( - self.test_client.apps.delete(create_json["app"]["name"])) - - self.assertIn("message", create_json) - self.assertIn("app", create_json) - self.assertEqual(200, create_status) - - self.assertIn("message", delete_json) - self.assertEqual(200, delete_status) + super(TestApps, self).create_and_delete() def test_attempt_to_double_create(self): - app = "testapp" - create_json, _ = self.testloop.run_until_complete( - self.test_client.apps.create(app)) - err, status = self.testloop.run_until_complete( - self.test_client.apps.create(app)) - self.testloop.run_until_complete( - self.test_client.apps.delete(create_json["app"]["name"])) - self.assertEqual(409, status) - self.assertIn("error", err) - self.assertIn("message", err["error"]) + super(TestApps, self).attempt_to_double_create() def test_attempt_delete_unknonw(self): - json, http_status = self.testloop.run_until_complete( - self.test_client.apps.delete("unknown")) - self.assertEqual(404, http_status) - self.assertIn("error", json) + super(TestApps, self).attempt_delete_unknonw() def test_delete_with_routes(self): - app_name = "testapp" - app, _ = self.testloop.run_until_complete( - self.test_client.apps.create(app_name)) - self.testloop.run_until_complete( - self.test_client.routes.create( - app["app"]["name"], **self.route_data) - ) - attempt, status = self.testloop.run_until_complete( - self.test_client.apps.delete(app["app"]["name"]) - ) - self.testloop.run_until_complete( - self.test_client.routes.delete( - app["app"]["name"], self.route_data["path"]) - ) - self.assertEqual(403, status) - self.assertIn("error", attempt) - self.assertIn("message", attempt["error"]) - self.assertIn("has routes", attempt["error"]["message"]) + super(TestApps, self).delete_with_routes() diff --git a/laos/tests/functional/test_routes.py b/laos/tests/functional/test_routes.py index 9ecd6b7..2544d93 100644 --- a/laos/tests/functional/test_routes.py +++ b/laos/tests/functional/test_routes.py @@ -12,110 +12,31 @@ # License for the specific language governing permissions and limitations # under the License. -import json as jsonlib - +from laos.tests.common import routes as routes_suite from laos.tests.functional import base -class TestAppRoutes(base.LaosFunctionalTestsBase): +class TestAppRoutes(base.LaosFunctionalTestsBase, + routes_suite.AppRoutesTestSuite): def test_list_routes_from_unknown_app(self): - json, status = self.testloop.run_until_complete( - self.test_client.routes.list("uknown_app") - ) - self.assertEqual(status, 404) - self.assertIn("error", json) - self.assertIn("not found", json["error"]["message"]) + super(TestAppRoutes, self).list_routes_from_unknown_app() def test_list_routes_from_existing_app(self): - create_json, _ = self.testloop.run_until_complete( - self.test_client.apps.create("testapp")) - json, status = self.testloop.run_until_complete( - self.test_client.routes.list(create_json["app"]["name"]) - ) - self.testloop.run_until_complete( - self.test_client.apps.delete(create_json["app"]["name"])) - self.assertEqual(status, 200) - self.assertIn("routes", json) - self.assertIn("message", json) + super(TestAppRoutes, self).list_routes_from_existing_app() def test_show_unknown_route_from_existing_app(self): - path = "/unknown_path" - create_json, _ = self.testloop.run_until_complete( - self.test_client.apps.create("testapp")) - json, status = self.testloop.run_until_complete( - self.test_client.routes.show( - create_json["app"]["name"], path) - ) - self.testloop.run_until_complete( - self.test_client.apps.delete(create_json["app"]["name"])) - - self.assertEqual(404, status) - self.assertIn("error", json) - self.assertIn("not found", json["error"]["message"]) - self.assertIn(path[1:], json["error"]["message"]) + super( + TestAppRoutes, self + ).show_unknown_route_from_existing_app() def test_delete_unknown_route_from_existing_app(self): - create_json, _ = self.testloop.run_until_complete( - self.test_client.apps.create("testapp")) - json, status = self.testloop.run_until_complete( - self.test_client.routes.delete( - create_json["app"]["name"], "/unknown_path") - ) - self.testloop.run_until_complete( - self.test_client.apps.delete(create_json["app"]["name"])) - - self.assertEqual(404, status) - self.assertIn("error", json) - self.assertIn("not found", json["error"]["message"]) + super( + TestAppRoutes, self + ).delete_unknown_route_from_existing_app() def test_create_and_delete_route(self): - app, _ = self.testloop.run_until_complete( - self.test_client.apps.create("testapp")) - route, create_status = self.testloop.run_until_complete( - self.test_client.routes.create( - app["app"]["name"], **self.route_data) - ) - route_deleted, delete_status = self.testloop.run_until_complete( - self.test_client.routes.delete( - app["app"]["name"], self.route_data["path"]) - ) - self.testloop.run_until_complete( - self.test_client.apps.delete(app["app"]["name"])) - after_post = route["route"] - for k in self.route_data: - if k == "path": - self.assertIn(self.route_data["path"], after_post[k]) - continue - if k == "is_public": - is_public = jsonlib.loads(self.route_data[k]) - self.assertEqual(is_public, after_post[k]) - continue - self.assertEqual(self.route_data[k], after_post[k]) - self.assertEqual(200, delete_status) - self.assertEqual(200, create_status) - self.assertIn("message", route_deleted) + super(TestAppRoutes, self).create_and_delete_route() def test_double_create_route(self): - app, _ = self.testloop.run_until_complete( - self.test_client.apps.create("testapp")) - self.testloop.run_until_complete( - self.test_client.routes.create( - app["app"]["name"], **self.route_data) - ) - - json, double_create_status = self.testloop.run_until_complete( - self.test_client.routes.create( - app["app"]["name"], **self.route_data) - ) - self.testloop.run_until_complete( - self.test_client.routes.delete( - app["app"]["name"], self.route_data["path"]) - ) - self.testloop.run_until_complete( - self.test_client.apps.delete(app["app"]["name"])) - - self.assertEqual(409, double_create_status) - self.assertIn("error", json) - self.assertIn("message", json["error"]) - self.assertIn("already exist", json["error"]["message"]) + super(TestAppRoutes, self).double_create_route() diff --git a/laos/tests/integration/__init__.py b/laos/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/laos/tests/integration/base.py b/laos/tests/integration/base.py new file mode 100644 index 0000000..145daad --- /dev/null +++ b/laos/tests/integration/base.py @@ -0,0 +1,72 @@ +# 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. + +import os +import testtools + +from laos.common import config +from laos.service import laos_api + +from laos.tests.common import base +from laos.tests.common import client + + +class LaosIntegrationTestsBase(base.LaosTestsBase, testtools.TestCase): + + def setUp(self): + + self.testloop, logger = self.get_loop_and_logger("integration") + + (functions_host, functions_port, + functions_api_protocol, + functions_api_version, db_uri, + keystone_endpoint, project_id, os_token) = ( + os.getenv("FUNCTIONS_HOST"), os.getenv("FUNCTIONS_PORT", 8080), + os.getenv("FUNCTIONS_API_PROTO", "http"), + os.getenv("FUNCTIONS_API_VERSION", "v1"), os.getenv("TEST_DB_URI"), + os.getenv("OS_AUTH_URL"), os.getenv("OS_PROJECT_ID"), + os.getenv("OS_TOKEN"), + ) + + fnclient = config.FunctionsClient( + functions_host, + api_port=functions_port, + api_protocol=functions_api_protocol, + api_version=functions_api_version, + ) + self.testloop.run_until_complete(fnclient.ping(loop=self.testloop)) + connection_pool = config.Connection(db_uri, loop=self.testloop) + + config.Config( + auth_url=keystone_endpoint, + functions_client=fnclient, + logger=logger, + connection=connection_pool, + event_loop=self.testloop, + ) + + self.test_app = laos_api.API( + loop=self.testloop, logger=logger, debug=True) + + self.test_client = client.ProjectBoundLaosTestClient( + self.test_app.root_service, project_id, headers={ + "X-Auth-Token": os_token + }) + self.testloop.run_until_complete( + self.test_client.start_server()) + super(LaosIntegrationTestsBase, self).setUp() + + def tearDown(self): + self.testloop.run_until_complete(self.test_client.close()) + super(LaosIntegrationTestsBase, self).tearDown() diff --git a/laos/tests/integration/test_apps.py b/laos/tests/integration/test_apps.py new file mode 100644 index 0000000..09ee9ac --- /dev/null +++ b/laos/tests/integration/test_apps.py @@ -0,0 +1,38 @@ +# 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. + +from laos.tests.common import apps as apps_suite +from laos.tests.integration import base + + +class TestIntegrationApps(base.LaosIntegrationTestsBase, + apps_suite.AppsTestSuite): + + def test_list_apps(self): + super(TestIntegrationApps, self).list_apps() + + def test_get_unknown(self): + super(TestIntegrationApps, self).get_unknown() + + def test_create_and_delete(self): + super(TestIntegrationApps, self).create_and_delete() + + def test_attempt_to_double_create(self): + super(TestIntegrationApps, self).attempt_to_double_create() + + def test_attempt_delete_unknonw(self): + super(TestIntegrationApps, self).attempt_delete_unknonw() + + def test_delete_with_routes(self): + super(TestIntegrationApps, self).delete_with_routes() diff --git a/laos/tests/integration/test_routes.py b/laos/tests/integration/test_routes.py new file mode 100644 index 0000000..d14e2ed --- /dev/null +++ b/laos/tests/integration/test_routes.py @@ -0,0 +1,50 @@ +# 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. + +from laos.tests.common import routes as routes_suite +from laos.tests.functional import base + + +class TestIntegrationAppRoutes(base.LaosFunctionalTestsBase, + routes_suite.AppRoutesTestSuite): + + def test_list_routes_from_unknown_app(self): + super( + TestIntegrationAppRoutes, self + ).list_routes_from_unknown_app() + + def test_list_routes_from_existing_app(self): + super( + TestIntegrationAppRoutes, self + ).list_routes_from_existing_app() + + def test_show_unknown_route_from_existing_app(self): + super( + TestIntegrationAppRoutes, self + ).show_unknown_route_from_existing_app() + + def test_delete_unknown_route_from_existing_app(self): + super( + TestIntegrationAppRoutes, self + ).delete_unknown_route_from_existing_app() + + def test_create_and_delete_route(self): + super( + TestIntegrationAppRoutes, self + ).create_and_delete_route() + + def test_double_create_route(self): + super( + TestIntegrationAppRoutes, self + ).double_create_route() diff --git a/scripts/test_regression.sh b/scripts/test_regression.sh index 2daca8e..b743f6d 100755 --- a/scripts/test_regression.sh +++ b/scripts/test_regression.sh @@ -4,17 +4,17 @@ set +x set +e function get_current_coverage { - local prev_stat_raw=`TEST_DB_URI=${TEST_DB_URI} pytest --tb=long --capture=sys --cov=laos --capture=fd laos/tests/functional | grep TOTAL | awk '{print $4}'` + local prev_stat_raw=`pytest --tb=long --capture=sys --cov=laos --capture=fd laos/tests/${1:-functional} | grep TOTAL | awk '{print $4}'` echo ${prev_stat_raw:0:2} } function get_coverage_delta { - local current_coverage=$(get_current_coverage) + local current_coverage=$(get_current_coverage $1) local current_branch=`git branch | awk '{print $2}'` - echo -e "Falling back to ${1} commits." - local commits=`seq -f "^" -s '' ${1:-1}` + echo -e "Falling back to ${2} commits." + local commits=`seq -f "^" -s '' ${2}` git checkout `git rev-parse --verify HEAD${commits}` &> /dev/null - local prev_coverage=$(get_current_coverage) + local prev_coverage=$(get_current_coverage $1) echo -e "Current coverage: ${current_coverage}%" echo -e "Previous coverage: ${prev_coverage}%" if [ "${prev_coverage}" -gt "${current_coverage}" ]; then @@ -27,4 +27,4 @@ function get_coverage_delta { } -get_coverage_delta ${1:-1} +get_coverage_delta ${1:-functional} ${2:-1} diff --git a/tox.ini b/tox.ini index 76881f2..98d30d4 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ # Project LaOS [tox] -envlist = py35-functional,pep8 +envlist = py35-functional,py35-functional-regression,py35-integration,py35-integration-regression,pep8 minversion = 1.6 skipsdist = True @@ -9,6 +9,13 @@ skipsdist = True passenv = PYTHONASYNCIODEBUG TEST_DB_URI + FUNCTIONS_HOST + FUNCTIONS_PORT + FUNCTIONS_API_PROTO + FUNCTIONS_API_VERSION + OS_AUTH_URL + OS_PROJECT_ID + OS_TOKEN setenv = VIRTUAL_ENV={envdir} usedevelop = True install_command = pip install -U {opts} {packages} @@ -26,13 +33,17 @@ commands = flake8 [testenv:venv] commands = {posargs} +[testenv:py35-integration] +commands = pytest --tb=long --capture=sys --cov=laos --capture=fd {toxinidir}/laos/tests/integration [testenv:py35-functional] commands = pytest --tb=long --capture=sys --cov=laos --capture=fd {toxinidir}/laos/tests/functional [testenv:py35-functional-regression] -commands = {toxinidir}/scripts/test_regression.sh {posargs} +commands = {toxinidir}/scripts/test_regression.sh functional {posargs} +[testenv:py35-integration-regression] +commands = {toxinidir}/scripts/test_regression.sh integration {posargs} [testenv:docs] commands = From 15710f6cd45c8626d03f6aa02b9c36521a0b76d8 Mon Sep 17 00:00:00 2001 From: Denis Makogon Date: Tue, 22 Nov 2016 17:16:43 +0200 Subject: [PATCH 2/3] Use aioservice v0.0.1 --- examples/hello-lambda.sh | 6 +-- laos/api/controllers/apps.py | 18 +++---- laos/api/controllers/routes.py | 16 +++--- laos/api/controllers/runnable.py | 16 +++--- laos/api/controllers/tasks.py | 10 ++-- laos/api/views/app.py | 4 +- laos/common/base/__init__.py | 0 laos/common/base/controllers.py | 77 ---------------------------- laos/common/base/service.py | 86 -------------------------------- laos/common/logger.py | 2 +- laos/service/laos_api.py | 61 ++++++++++++---------- laos/tests/functional/base.py | 47 ++++++++--------- laos/tests/integration/base.py | 2 +- requirements.txt | 6 ++- setup.py | 2 +- 15 files changed, 102 insertions(+), 251 deletions(-) delete mode 100644 laos/common/base/__init__.py delete mode 100644 laos/common/base/controllers.py delete mode 100644 laos/common/base/service.py diff --git a/examples/hello-lambda.sh b/examples/hello-lambda.sh index f21831e..88ee696 100755 --- a/examples/hello-lambda.sh +++ b/examples/hello-lambda.sh @@ -70,16 +70,16 @@ echo -e "Show app public route\n" curl ${LAOS_API_URL}/v1/${OS_PROJECT_ID}/apps/${app_name}/routes/hello-sync-public -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool echo -e "Running app sync private route\n" -curl -X POST -d '{"name": "Johnny"}' ${LAOS_API_URL}/private/${OS_PROJECT_ID}/${app_name}/hello-sync-private -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool +curl -X POST -d '{"name": "Johnny"}' ${LAOS_API_URL}/v1/r/${OS_PROJECT_ID}/${app_name}/hello-sync-private -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool echo -e "Running app sync public route\n" -curl -X POST -d '{"name": "Johnny"}' ${LAOS_API_URL}/public/${app_name}/hello-sync-public -H "Content-Type: application/json" | python3 -mjson.tool +curl -X POST -d '{"name": "Johnny"}' ${LAOS_API_URL}/r/${app_name}/hello-sync-public -H "Content-Type: application/json" | python3 -mjson.tool echo -e "Creating app async route\n" curl -X POST -d '{"route":{"type": "async", "path": "/hello-async-private", "image": "iron/hello", "is_public": "false"}}' ${LAOS_API_URL}/v1/${OS_PROJECT_ID}/apps/${app_name}/routes -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool echo -e "Running app async route\n" -curl -X POST -d '{"name": "Johnny"}' ${LAOS_API_URL}/private/${OS_PROJECT_ID}/${app_name}/hello-async-private -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool +curl -X POST -d '{"name": "Johnny"}' ${LAOS_API_URL}/v1/r/${OS_PROJECT_ID}/${app_name}/hello-async-private -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool echo -e "Deleting app route\n" curl -X DELETE ${LAOS_API_URL}/v1/${OS_PROJECT_ID}/apps/${app_name}/routes/hello-sync-public -H "X-Auth-Token:${OS_TOKEN}" -H "Content-Type: application/json" | python3 -mjson.tool diff --git a/laos/api/controllers/apps.py b/laos/api/controllers/apps.py index 90447a1..bde58cc 100644 --- a/laos/api/controllers/apps.py +++ b/laos/api/controllers/apps.py @@ -14,20 +14,20 @@ from aiohttp import web +from aioservice.http import controller +from aioservice.http import requests + from laos.api.views import app as app_view - -from laos.common.base import controllers from laos.common import config - from laos.models import app as app_model -class AppV1Controller(controllers.ServiceControllerBase): +class AppV1Controller(controller.ServiceController): controller_name = "apps" version = "v1" - @controllers.api_action(method='GET', route='{project_id}/apps') + @requests.api_action(method='GET', route='{project_id}/apps') async def list(self, request, **kwargs): """ --- @@ -59,7 +59,7 @@ class AppV1Controller(controllers.ServiceControllerBase): status=200 ) - @controllers.api_action(method='POST', route='{project_id}/apps') + @requests.api_action(method='POST', route='{project_id}/apps') async def create(self, request, **kwargs): """ --- @@ -118,7 +118,7 @@ class AppV1Controller(controllers.ServiceControllerBase): }, status=200 ) - @controllers.api_action(method='GET', route='{project_id}/apps/{app}') + @requests.api_action(method='GET', route='{project_id}/apps/{app}') async def get(self, request, **kwargs): """ --- @@ -161,7 +161,7 @@ class AppV1Controller(controllers.ServiceControllerBase): ) # TODO(denismakogon): disabled until iron-io/functions/pull/259 # - # @controllers.api_action(method='PUT', route='{project_id}/apps/{app}') + # @requests.api_action(method='PUT', route='{project_id}/apps/{app}') # async def update(self, request, **kwargs): # log = config.Config.config_instance().logger # project_id = request.match_info.get('project_id') @@ -176,7 +176,7 @@ class AppV1Controller(controllers.ServiceControllerBase): # status=200 # ) - @controllers.api_action(method='DELETE', route='{project_id}/apps/{app}') + @requests.api_action(method='DELETE', route='{project_id}/apps/{app}') async def delete(self, request, **kwargs): """ --- diff --git a/laos/api/controllers/routes.py b/laos/api/controllers/routes.py index e3ffa73..886f55a 100644 --- a/laos/api/controllers/routes.py +++ b/laos/api/controllers/routes.py @@ -16,20 +16,20 @@ import json from aiohttp import web +from aioservice.http import controller +from aioservice.http import requests + from laos.api.views import app as app_view - -from laos.common.base import controllers from laos.common import config - from laos.models import app as app_model -class AppRouteV1Controller(controllers.ServiceControllerBase): +class AppRouteV1Controller(controller.ServiceController): controller_name = "routes" version = "v1" - @controllers.api_action( + @requests.api_action( method='GET', route='{project_id}/apps/{app}/routes') async def list(self, request, **kwargs): """ @@ -82,7 +82,7 @@ class AppRouteV1Controller(controllers.ServiceControllerBase): "message": "Successfully loaded app routes", }, status=200) - @controllers.api_action( + @requests.api_action( method='POST', route='{project_id}/apps/{app}/routes') async def create(self, request, **kwargs): """ @@ -185,7 +185,7 @@ class AppRouteV1Controller(controllers.ServiceControllerBase): "message": "App route successfully created" }, status=200) - @controllers.api_action( + @requests.api_action( method='GET', route='{project_id}/apps/{app}/routes/{route}') async def get(self, request, **kwargs): """ @@ -248,7 +248,7 @@ class AppRouteV1Controller(controllers.ServiceControllerBase): "message": "App route successfully loaded" }, status=200) - @controllers.api_action( + @requests.api_action( method='DELETE', route='{project_id}/apps/{app}/routes/{route}') async def delete(self, request, **kwargs): """ diff --git a/laos/api/controllers/runnable.py b/laos/api/controllers/runnable.py index a442441..7a83a20 100644 --- a/laos/api/controllers/runnable.py +++ b/laos/api/controllers/runnable.py @@ -14,7 +14,9 @@ from aiohttp import web -from laos.common.base import controllers +from aioservice.http import controller +from aioservice.http import requests + from laos.common import config @@ -64,14 +66,14 @@ class RunnableMixin(object): return web.json_response(status=200, data=process_result(result)) -class PublicRunnableV1Controller(controllers.ServiceControllerBase, +class PublicRunnableV1Controller(controller.ServiceController, RunnableMixin): controller_name = "public_runnable" # IronFunction uses `r` as runnable instead API version version = "r" - @controllers.api_action( + @requests.api_action( method='POST', route='{app}/{route}') async def run(self, request, **kwargs): """ @@ -93,15 +95,15 @@ class PublicRunnableV1Controller(controllers.ServiceControllerBase, self).run(request, **kwargs) -class RunnableV1Controller(controllers.ServiceControllerBase, +class RunnableV1Controller(controller.ServiceController, RunnableMixin): controller_name = "runnable" # IronFunction uses `r` as runnable instead API version - version = "r" + version = "v1" - @controllers.api_action( - method='POST', route='{project_id}/{app}/{route}') + @requests.api_action( + method='POST', route='r/{project_id}/{app}/{route}') async def run(self, request, **kwargs): """ --- diff --git a/laos/api/controllers/tasks.py b/laos/api/controllers/tasks.py index 27dce9b..dc6adda 100644 --- a/laos/api/controllers/tasks.py +++ b/laos/api/controllers/tasks.py @@ -16,13 +16,15 @@ from aiohttp import web # from laos.models import app as app_model -from laos.common.base import controllers +from aioservice.http import controller +from aioservice.http import requests + # from laos.common import config # TODO(denismakogon): disabled until # https://github.com/iron-io/functions/issues/275 -class TasksV1Controller(controllers.ServiceControllerBase): +class TasksV1Controller(controller.ServiceController): controller_name = "tasks" version = "v1" @@ -33,7 +35,7 @@ class TasksV1Controller(controllers.ServiceControllerBase): # - on each request check if route is public our private # * reject with 401 if route is private # * accept with 200 if route is public - @controllers.api_action( + @requests.api_action( method='GET', route='{project_id}/tasks') async def list(self, request, **kwargs): """ @@ -63,7 +65,7 @@ class TasksV1Controller(controllers.ServiceControllerBase): } }, status=405) - @controllers.api_action( + @requests.api_action( method='GET', route='{project_id}/tasks/{task}') async def show(self, request, **kwargs): """ diff --git a/laos/api/views/app.py b/laos/api/views/app.py index 3641aea..8a61e98 100644 --- a/laos/api/views/app.py +++ b/laos/api/views/app.py @@ -45,11 +45,11 @@ class AppRouteView(object): view = [] for route in self.routes: if not route.is_public: - path = ("{}/private/{}/{}{}".format( + path = ("{}/v1/r/{}/{}{}".format( self.api_url, self.project_id, route.appname, route.path)) else: - path = ("{}/public/{}{}".format( + path = ("{}/r/{}{}".format( self.api_url, route.appname, route.path)) view.append({ "path": path, diff --git a/laos/common/base/__init__.py b/laos/common/base/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/laos/common/base/controllers.py b/laos/common/base/controllers.py deleted file mode 100644 index 4ec4ed6..0000000 --- a/laos/common/base/controllers.py +++ /dev/null @@ -1,77 +0,0 @@ -# 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. - -import functools -import inspect - -from aiohttp import web - - -class ServiceControllerBase(object): - - controller_name = 'abstract' - version = "" - - def __get_handlers(self): - # when this method gets executed by child classes - # method list includes a method of parent class, - # so this code ignores it because it doesn't belong to controllers - methods = [getattr(self, _m) - for _m in dir(self) if inspect.ismethod( - getattr(self, _m)) and "__" not in _m] - - return [[method, - method.arg_method, - method.arg_route] for method in methods] - - def __init__(self, sub_service: web.Application): - for fn, http_method, route in self.__get_handlers(): - proxy_fn = '_'.join([fn.__name__, self.controller_name]) - setattr(self, proxy_fn, fn) - sub_service.router.add_route( - http_method, "/{}".format(route), - getattr(self, proxy_fn), name=proxy_fn) - - -def api_action(**outter_kwargs): - """ - Wrapps API controller action actions handler - :param outter_kwargs: API instance action key-value args - :return: _api_handler - - Example: - - class Controller(ControllerBase): - - @api_action(method='GET') - async def index(self, request, **kwargs): - return web.Response( - text=str(request), - reason="dumb API", - status=201) - - """ - - def _api_handler(func): - - @functools.wraps(func) - async def wrapper(self, *args, **kwargs): - return await func(self, *args, *kwargs) - - for key, value in outter_kwargs.items(): - setattr(wrapper, 'arg_{}'.format(key), value) - setattr(wrapper, 'is_module_function', True) - return wrapper - - return _api_handler diff --git a/laos/common/base/service.py b/laos/common/base/service.py deleted file mode 100644 index 0fa96ce..0000000 --- a/laos/common/base/service.py +++ /dev/null @@ -1,86 +0,0 @@ -# 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. - -import asyncio - -import aiohttp_swagger - -from aiohttp import web - -from laos.common import logger as log - - -class AbstractWebServer(object): - - def __init__(self, host: str='127.0.0.1', - port: int= '10001', - private_controllers: dict=None, - private_middlewares: list=None, - public_middlewares: list=None, - public_controllers: dict=None, - event_loop: asyncio.AbstractEventLoop=None, - logger=log.UnifiedLogger( - log_to_console=True, - level="INFO").setup_logger(__name__), - debug=False): - """ - HTTP server abstraction class - :param host: Bind host - :param port: Bind port - :param private_controllers: private API controllers mapping - :param private_middlewares: list of private API middleware - :param public_middlewares: - list of public API middleware - :param public_controllers: - public API controllers mapping - :param event_loop: asyncio eventloop - :param logger: logging.Logger - """ - self.host = host - self.port = port - self.event_loop = event_loop - self.logger = logger - - self.root_service = web.Application( - logger=self.logger, - loop=self.event_loop, - debug=debug - ) - - self.register_subapps(private_controllers, private_middlewares) - self.register_subapps(public_controllers, public_middlewares) - - def _apply_routers(self, service, controllers): - for controller in controllers: - controller(service) - return service - - def register_subapps(self, controllers_mapping: dict, middlewares: list): - if controllers_mapping: - for sub_route, controllers in controllers_mapping.items(): - service = self._apply_routers( - web.Application( - logger=self.logger, - loop=self.event_loop, - middlewares=middlewares - if middlewares else []), - controllers) - self.root_service.router.add_subapp( - "/{}/".format(sub_route), service) - - def initialize(self): - aiohttp_swagger.setup_swagger( - self.root_service, swagger_url="/api") - web.run_app(self.root_service, host=self.host, port=self.port, - shutdown_timeout=10, access_log=self.logger) diff --git a/laos/common/logger.py b/laos/common/logger.py index ec997b4..1a399cb 100644 --- a/laos/common/logger.py +++ b/laos/common/logger.py @@ -21,7 +21,7 @@ from laos.common import utils def common_logger_setup( level=logging.DEBUG, - filename='/tmp/aiorchestra.log', + filename='/tmp/laos-api.log', log_formatter='[%(asctime)s] - ' '%(name)s - ' '%(levelname)s - ' diff --git a/laos/service/laos_api.py b/laos/service/laos_api.py index effa89b..c17ddd5 100644 --- a/laos/service/laos_api.py +++ b/laos/service/laos_api.py @@ -17,6 +17,8 @@ import asyncio import click import uvloop +from aioservice.http import service + from laos.api.controllers import apps from laos.api.controllers import routes from laos.api.controllers import runnable @@ -25,46 +27,46 @@ from laos.api.controllers import tasks from laos.api.middleware import content_type from laos.api.middleware import keystone -from laos.common.base import service from laos.common import config from laos.common import logger as log -class API(service.AbstractWebServer): +class API(service.HTTPService): def __init__(self, host: str='0.0.0.0', port: int=10001, loop: asyncio.AbstractEventLoop=asyncio.get_event_loop(), logger=None, debug=False): + + v1_service = service.VersionedService( + [ + apps.AppV1Controller, + routes.AppRouteV1Controller, + runnable.RunnableV1Controller, + tasks.TasksV1Controller + ], middleware=[ + keystone.auth_through_token, + content_type.content_type_validator + ]) + + public_runnable_service = service.VersionedService( + [ + runnable.PublicRunnableV1Controller + ], middleware=[ + content_type.content_type_validator, + ] + ) + super(API, self).__init__( host=host, port=port, - private_controllers={ - "v1": [ - apps.AppV1Controller, - routes.AppRouteV1Controller, - tasks.TasksV1Controller, - ], - "private": [ - runnable.RunnableV1Controller, - ] - }, - public_controllers={ - "public": [ - runnable.PublicRunnableV1Controller, - ], - }, - private_middlewares=[ - keystone.auth_through_token, - content_type.content_type_validator, - ], - public_middlewares=[ - content_type.content_type_validator, - ], event_loop=loop, logger=logger, debug=debug, + subservice_definitions=[ + v1_service, public_runnable_service + ] ) @@ -126,8 +128,15 @@ def server(host, port, db_uri, event_loop=loop, ) - API(host=host, port=port, loop=loop, - logger=logger, debug=debug).initialize() + API( + host=host, port=port, loop=loop, + logger=logger, debug=debug + ).apply_swagger( + swagger_url="/api", + description="Laos API service docs", + api_version="v1.0.0", + title="Laos API", + ).initialize() if __name__ == "__main__": diff --git a/laos/tests/functional/base.py b/laos/tests/functional/base.py index 7237049..9bb28aa 100644 --- a/laos/tests/functional/base.py +++ b/laos/tests/functional/base.py @@ -17,13 +17,14 @@ import os import testtools import uuid +from aioservice.http import service + from laos.api.controllers import apps from laos.api.controllers import routes from laos.api.controllers import runnable from laos.api.controllers import tasks from laos.api.middleware import content_type -from laos.common.base import service from laos.common import config from laos.tests.common import base @@ -36,34 +37,30 @@ class LaosFunctionalTestsBase(base.LaosTestsBase, testtools.TestCase): def setUp(self): self.testloop, logger = self.get_loop_and_logger("functional") - self.testapp = service.AbstractWebServer( - host="localhost", + v1_service = service.VersionedService( + [ + apps.AppV1Controller, + routes.AppRouteV1Controller, + tasks.TasksV1Controller, + runnable.RunnableV1Controller, + ], middleware=[ + content_type.content_type_validator + ] + ) + public_runnable = service.VersionedService( + [ + runnable.PublicRunnableV1Controller, + ], middleware=[ + content_type.content_type_validator, + ] + ) + self.testapp = service.HTTPService( + [v1_service, public_runnable], port=10001, - private_controllers={ - "v1": [ - apps.AppV1Controller, - routes.AppRouteV1Controller, - tasks.TasksV1Controller, - ], - "private": [ - runnable.RunnableV1Controller, - ] - }, - public_controllers={ - "public": [ - runnable.PublicRunnableV1Controller, - ], - }, - private_middlewares=[ - content_type.content_type_validator, - ], - public_middlewares=[ - content_type.content_type_validator, - ], event_loop=self.testloop, logger=logger, debug=True, - ).root_service + ).root connection_pool = config.Connection( os.getenv("TEST_DB_URI"), loop=self.testloop) diff --git a/laos/tests/integration/base.py b/laos/tests/integration/base.py index 145daad..1efb2e8 100644 --- a/laos/tests/integration/base.py +++ b/laos/tests/integration/base.py @@ -60,7 +60,7 @@ class LaosIntegrationTestsBase(base.LaosTestsBase, testtools.TestCase): loop=self.testloop, logger=logger, debug=True) self.test_client = client.ProjectBoundLaosTestClient( - self.test_app.root_service, project_id, headers={ + self.test_app.root, project_id, headers={ "X-Auth-Token": os_token }) self.testloop.run_until_complete( diff --git a/requirements.txt b/requirements.txt index a6ecafa..865e4aa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,11 +3,15 @@ # process, which may cause wedges in the gate later. uvloop==0.6.0 # Apache-2.0 -aiohttp==1.1.5 # Apache-2.0 +aioservice==0.0.1 # Apache-2.0 aiomysql==0.0.9 # Apache-2.0 alembic==0.8.8 # MIT click==6.6 # Apache-2.0 + +# IronFunctions python-functionsclient==0.0.1 + +# OpenStack keystoneauth1==2.15.0 # Apache-2.0 python-keystoneclient==3.6.0 # Apache-2.0 diff --git a/setup.py b/setup.py index 68aa679..6a27e37 100644 --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ setuptools.setup( packages=setuptools.find_packages(), install_requires=[ "uvloop==0.6.0", - "aiohttp==1.1.5", + "aioservice==0.0.1", "aiomysql==0.0.9", "alembic==0.8.8", "click==6.6", From 207bc86f3e6f38a544a054908fa4e25316f829d1 Mon Sep 17 00:00:00 2001 From: Denis Makogon Date: Wed, 23 Nov 2016 00:31:07 +0200 Subject: [PATCH 3/3] Simplify integration tests configuration - Make it more OpenStack-aligned --- README.md | 68 ++++-------------------------- laos/tests/integration/base.py | 75 +++++++++++++++++++++++++++------- tox.ini | 10 ++--- 3 files changed, 74 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index ae88768..05582f2 100644 --- a/README.md +++ b/README.md @@ -190,66 +190,20 @@ Testing: Integration Integration tests are dependent on following env variables: * TEST_DB_URI - similar to functional tests, database endpoint -* FUNCTIONS_HOST - IronFunctions API host -* FUNCTIONS_PORT - IronFunctions API port -* FUNCTIONS_API_PROTO - IronFunctions API protocol -* FUNCTIONS_API_VERSION - IronFunctions API version +* FUNCTIONS_API_URL - IronFunctions API URL (default value - `http://localhost:8080/v1`) * OS_AUTH_URL - OpenStack Identity endpoint -* OS_PROJECT_ID - OpenStack user-specific project ID -* OS_TOKEN - OpenStack user project-bound auth token - -How to get token? - Set env variables: - -* OS_USERNAME -* OS_PASSWORD -* OS_PROJECT_NAME - -and run following command: - - echo -e "{ - \"auth\": { - \"identity\": { - \"methods\": [\"password\"], - \"password\": { - \"user\": { - \"name\": \"${OS_USERNAME:-admin}\", - \"domain\": { \"id\": \"${OS_DOMAIN:-default}\" }, - \"password\": \"${OS_PASSWORD:-root}\" - } - } - }, - \"scope\": { - \"project\": { - \"name\": \"${OS_PROJECT_NAME:-admin}\", - \"domain\": {\"id\": \"${OS_DOMAIN:-default}\" } - } - } - } - }" >> token_request.json - -After that: - - export OS_TOKEN=`curl -si -d @token_request.json -H "Content-type: application/json" ${OS_AUTH_URL}/auth/tokens | awk '/X-Subject-Token/ {print $2}'` - -If variable remains empty, please check your authentication parameters specified in `token_request.json`. - -How to get project ID? - Use following command - - export RAW_PrID=`curl -si -d @token_request.json -H "Content-type: application/json" ${OS_AUTH_URL}/auth/tokens | grep Default | awk '{print $20}'` - export OS_PROJECT_ID=${RAW_PrID:1:-2} - -If variable remains empty, please check your authentication parameters specified in `token_request.json`. +* OS_PROJECT_NAME - OpenStack user-specific project name +* OS_USERNAME - OpenStack user name +* OS_PASSWORD - OpenStack user user password To run tests use following command: export TEST_DB_URI=mysql://:@:/ - export FUNCTIONS_HOST= - export FUNCTIONS_PORT= - export FUNCTIONS_API_PROTO= - export FUNCTIONS_API_VERSION= - export OS_AUTH_URL= - export OS_PROJECT_ID= - export OS_TOKEN= + export FUNCTIONS_API_URL=://:/ + export OS_AUTH_URL=://:/ + export OS_PROJECT_NAME= + export OS_USERNAME= + export OS_PASSWORD= tox -epy35-integration Testing: Coverage regression @@ -271,10 +225,6 @@ IronFunctions: * https://github.com/iron-io/functions/issues/275 * https://github.com/iron-io/functions/issues/274 -aiohttp_swagger: - -* https://github.com/cr0hn/aiohttp-swagger/issues/12 ([fix proposed](https://github.com/cr0hn/aiohttp-swagger/pull/13)) - TODOs ----- diff --git a/laos/tests/integration/base.py b/laos/tests/integration/base.py index 1efb2e8..9c2cf66 100644 --- a/laos/tests/integration/base.py +++ b/laos/tests/integration/base.py @@ -12,6 +12,8 @@ # License for the specific language governing permissions and limitations # under the License. +import aiohttp +import json import os import testtools @@ -21,35 +23,75 @@ from laos.service import laos_api from laos.tests.common import base from laos.tests.common import client +from urllib import parse + class LaosIntegrationTestsBase(base.LaosTestsBase, testtools.TestCase): + async def authenticate(self, os_user, os_pass, os_project, url): + auth_request_data = { + "auth": { + "identity": { + "methods": ["password"], + "password": { + "user": { + "name": os_user, + "domain": {"id": "default"}, + "password": os_pass + } + } + }, + "scope": { + "project": { + "name": os_project, + "domain": {"id": "default"} + } + } + } + } + with aiohttp.ClientSession(loop=self.testloop) as session: + response = await session.post( + "{}/auth/tokens".format(url), + data=json.dumps(auth_request_data), + headers={ + "Content-Type": "application/json" + }, + timeout=20) + response.raise_for_status() + result = await response.json() + return (response.headers["X-Subject-Token"], + result["token"]["project"]["id"]) + def setUp(self): self.testloop, logger = self.get_loop_and_logger("integration") - (functions_host, functions_port, - functions_api_protocol, - functions_api_version, db_uri, - keystone_endpoint, project_id, os_token) = ( - os.getenv("FUNCTIONS_HOST"), os.getenv("FUNCTIONS_PORT", 8080), - os.getenv("FUNCTIONS_API_PROTO", "http"), - os.getenv("FUNCTIONS_API_VERSION", "v1"), os.getenv("TEST_DB_URI"), - os.getenv("OS_AUTH_URL"), os.getenv("OS_PROJECT_ID"), - os.getenv("OS_TOKEN"), + (functions_url, db_uri, + os_auth_url, os_user, os_pass, os_project) = ( + os.getenv("FUNCTIONS_API_URL", "http://localhost:8080/v1"), + os.getenv("TEST_DB_URI"), + os.getenv("OS_AUTH_URL"), + os.getenv("OS_USERNAME"), + os.getenv("OS_PASSWORD"), + os.getenv("OS_PROJECT_NAME"), ) + if not all([db_uri, os_auth_url, os_user, os_pass, os_project]): + raise self.skipTest("Not all test env variables were set.") + + parts = parse.urlparse(functions_url) + fnclient = config.FunctionsClient( - functions_host, - api_port=functions_port, - api_protocol=functions_api_protocol, - api_version=functions_api_version, + parts.hostname, + api_port=parts.port, + api_protocol=parts.scheme, + api_version=parts.path[1:] ) self.testloop.run_until_complete(fnclient.ping(loop=self.testloop)) connection_pool = config.Connection(db_uri, loop=self.testloop) config.Config( - auth_url=keystone_endpoint, + auth_url=os_auth_url, functions_client=fnclient, logger=logger, connection=connection_pool, @@ -59,6 +101,11 @@ class LaosIntegrationTestsBase(base.LaosTestsBase, testtools.TestCase): self.test_app = laos_api.API( loop=self.testloop, logger=logger, debug=True) + os_token, project_id = self.testloop.run_until_complete( + self.authenticate( + os_user, os_pass, + os_project, os_auth_url)) + self.test_client = client.ProjectBoundLaosTestClient( self.test_app.root, project_id, headers={ "X-Auth-Token": os_token diff --git a/tox.ini b/tox.ini index 98d30d4..ef3ef62 100644 --- a/tox.ini +++ b/tox.ini @@ -9,13 +9,11 @@ skipsdist = True passenv = PYTHONASYNCIODEBUG TEST_DB_URI - FUNCTIONS_HOST - FUNCTIONS_PORT - FUNCTIONS_API_PROTO - FUNCTIONS_API_VERSION + FUNCTIONS_API_URL OS_AUTH_URL - OS_PROJECT_ID - OS_TOKEN + OS_PASSWORD + OS_USERNAME + OS_PROJECT_NAME setenv = VIRTUAL_ENV={envdir} usedevelop = True install_command = pip install -U {opts} {packages}