Merge pull request #5 from denismakogon/master

Implements integration testing framework
This commit is contained in:
Denis Makogon 2016-11-23 23:33:57 +02:00 committed by GitHub
commit 7eedd99ee1
27 changed files with 617 additions and 432 deletions

View File

@ -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,24 @@ Cons:
Testing: Integration
--------------------
TBD
Integration tests are dependent on following env variables:
* TEST_DB_URI - similar to functional tests, database endpoint
* FUNCTIONS_API_URL - IronFunctions API URL (default value - `http://localhost:8080/v1`)
* OS_AUTH_URL - OpenStack Identity endpoint
* 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://<your-user>:<your-user-password>@<mysql-host>:<mysql-port>/<functions-db>
export FUNCTIONS_API_URL=<functions-api-protocol>://<functions-host>:<functions-port>/<functions-api-version>
export OS_AUTH_URL=<identity-api-protocol>://<identity-host>:<identity-port>/<identity-api-version>
export OS_PROJECT_NAME=<project-name>
export OS_USERNAME=<project-name>
export OS_PASSWORD=<project-name>
tox -epy35-integration
Testing: Coverage regression
----------------------------
@ -213,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
-----

View File

@ -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

View File

@ -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):
"""
---
@ -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)

View File

@ -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):
"""

View File

@ -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):
"""
---

View File

@ -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):
"""

View File

@ -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,

View File

@ -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

View File

@ -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)

View File

@ -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 - '

View File

@ -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__":

84
laos/tests/common/apps.py Normal file
View File

@ -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)

42
laos/tests/common/base.py Normal file
View File

@ -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

View File

@ -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)

118
laos/tests/common/routes.py Normal file
View File

@ -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"])

View File

@ -12,13 +12,12 @@
# 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 aioservice.http import service
from laos.api.controllers import apps
from laos.api.controllers import routes
@ -26,58 +25,42 @@ 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.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()
self.testloop, logger = self.get_loop_and_logger("functional")
logger = log.UnifiedLogger(
log_to_console=False,
filename=("/tmp/laos-integration-tests-run-{}.log"
.format(datetime.datetime.now())),
level="DEBUG").setup_logger(__package__)
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)

View File

@ -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()

View File

@ -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()

View File

View File

@ -0,0 +1,119 @@
# 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 aiohttp
import json
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
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_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(
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=os_auth_url,
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)
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
})
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()

View File

@ -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()

View File

@ -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()

View File

@ -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

View File

@ -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}

View File

@ -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",

13
tox.ini
View File

@ -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,11 @@ skipsdist = True
passenv =
PYTHONASYNCIODEBUG
TEST_DB_URI
FUNCTIONS_API_URL
OS_AUTH_URL
OS_PASSWORD
OS_USERNAME
OS_PROJECT_NAME
setenv = VIRTUAL_ENV={envdir}
usedevelop = True
install_command = pip install -U {opts} {packages}
@ -26,13 +31,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 =