Impelemts functional testing framework
Includes: - base functional test case class - apps tests - route tests - coverage included README updated
This commit is contained in:
parent
967bd9fa3f
commit
b78d1a7dc3
29
.coveragerc
29
.coveragerc
@ -1,29 +0,0 @@
|
||||
# .coveragerc to control coverage.py
|
||||
[run]
|
||||
branch = True
|
||||
|
||||
source=laos
|
||||
omit=laos/tests*,
|
||||
|
||||
[report]
|
||||
# Regexes for lines to exclude from consideration
|
||||
exclude_lines =
|
||||
# Have to re-enable the standard pragma
|
||||
pragma: no cover
|
||||
|
||||
# Don't complain about missing debug-only code:
|
||||
def __repr__
|
||||
if self\.debug
|
||||
|
||||
# Don't complain if tests don't hit defensive assertion code:
|
||||
raise AssertionError
|
||||
raise NotImplementedError
|
||||
|
||||
# Don't complain if non-runnable code isn't run:
|
||||
if 0:
|
||||
if __name__ == .__main__.:
|
||||
|
||||
ignore_errors = False
|
||||
|
||||
[html]
|
||||
directory=cover
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -14,3 +14,4 @@ python-troveclient.iml
|
||||
releasenotes/build
|
||||
.coverage.*
|
||||
*.json
|
||||
.cache
|
||||
|
@ -1,6 +0,0 @@
|
||||
[DEFAULT]
|
||||
test_command=PYTHONASYNCIODEBUG=${PYTHONASYNCIODEBUG:-1} \
|
||||
${PYTHON:-python} -m subunit.run discover -t ./ ./ $LISTOPT $IDOPTION
|
||||
|
||||
test_id_option=--load-list $IDFILE
|
||||
test_list_option=--list
|
48
README.md
48
README.md
@ -146,7 +146,53 @@ Once server is launched you can navigate to:
|
||||
|
||||
http://<laos-host>:<laos-port>/api
|
||||
|
||||
Or if you need to create
|
||||
to see recent API docs
|
||||
|
||||
Testing (general information)
|
||||
-----------------------------
|
||||
|
||||
In order to run tests you need to install `Tox`:
|
||||
|
||||
$ pip install tox
|
||||
|
||||
Also you'd need live MySQL instance with applied migrations.
|
||||
Tests are depending on pre-created MySQL database for persistence.
|
||||
Please set env var
|
||||
|
||||
$ export TEST_DB_URI=mysql://<your-user>:<your-user-password>@<mysql-host>:<mysql-port>/<functions-db>
|
||||
|
||||
Testing: PEP8
|
||||
-------------
|
||||
|
||||
In order to run `PEP8` style checks run next command:
|
||||
|
||||
$ tox -e pep8
|
||||
|
||||
|
||||
Testing: Functional
|
||||
-------------------
|
||||
|
||||
In order to run `functional` tests run next command:
|
||||
|
||||
$ tox -e py35-functional
|
||||
|
||||
Pros:
|
||||
|
||||
* lightweight (controllers and DB models testing)
|
||||
* no OpenStack required
|
||||
* no IronFunctions required
|
||||
|
||||
Cons:
|
||||
|
||||
* MySQL server required
|
||||
* OpenStack authentication is not tested
|
||||
* IronFunctions API stabbed with fake implementation
|
||||
|
||||
Testing: Integration
|
||||
--------------------
|
||||
|
||||
TBD
|
||||
|
||||
|
||||
3rd party bugs to resolve
|
||||
-------------------------
|
||||
|
@ -195,7 +195,8 @@ class AppV1Controller(controllers.ServiceControllerBase):
|
||||
"""
|
||||
project_id = request.match_info.get('project_id')
|
||||
app = request.match_info.get('app')
|
||||
|
||||
c = config.Config.config_instance()
|
||||
fnclient = c.functions_client
|
||||
if not (await app_model.Apps.exists(app, project_id)):
|
||||
return web.json_response(data={
|
||||
"error": {
|
||||
@ -203,6 +204,17 @@ class AppV1Controller(controllers.ServiceControllerBase):
|
||||
}
|
||||
}, status=404)
|
||||
|
||||
fn_app = await fnclient.apps.show(app, loop=c.event_loop)
|
||||
fn_app_routes = await fn_app.routes.list(loop=c.event_loop)
|
||||
|
||||
if fn_app_routes:
|
||||
return web.json_response(data={
|
||||
"error": {
|
||||
"message": ("App {} has routes, "
|
||||
"delete them in first place")
|
||||
}
|
||||
}, status=403)
|
||||
|
||||
await app_model.Apps.delete(
|
||||
project_id=project_id, name=app)
|
||||
# TODO(denismakogon): enable DELETE to IronFunctions when once
|
||||
|
@ -178,7 +178,7 @@ class AppRouteV1Controller(controllers.ServiceControllerBase):
|
||||
|
||||
setattr(new_fn_route, "is_public", stored_route.public)
|
||||
view = app_view.AppRouteView(
|
||||
api_url, project_id, [new_fn_route]).view()
|
||||
api_url, project_id, [new_fn_route]).view_one()
|
||||
|
||||
return web.json_response(data={
|
||||
"route": view,
|
||||
@ -244,7 +244,7 @@ class AppRouteV1Controller(controllers.ServiceControllerBase):
|
||||
return web.json_response(data={
|
||||
"route": app_view.AppRouteView(api_url,
|
||||
project_id,
|
||||
[route]).view().pop(),
|
||||
[route]).view_one(),
|
||||
"message": "App route successfully loaded"
|
||||
}, status=200)
|
||||
|
||||
|
@ -38,6 +38,9 @@ class AppRouteView(object):
|
||||
self.api_url = api_url
|
||||
self.project_id = project_id
|
||||
|
||||
def view_one(self):
|
||||
return self.view().pop()
|
||||
|
||||
def view(self):
|
||||
view = []
|
||||
for route in self.routes:
|
||||
|
@ -37,18 +37,19 @@ class Connection(object, metaclass=utils.Singleton):
|
||||
|
||||
def __init__(self, db_uri, loop=None):
|
||||
self.uri = db_uri
|
||||
self.engine = loop.run_until_complete(self.get_engine(loop=loop))
|
||||
self.loop = loop
|
||||
self.pool = loop.run_until_complete(self.get_pool())
|
||||
self.loop = loop
|
||||
|
||||
def get_engine(self, loop=None):
|
||||
def get_pool(self):
|
||||
username, password, host, port, db_name = utils.split_db_uri(self.uri)
|
||||
return aiomysql.create_pool(host=host, port=port if port else 3306,
|
||||
user=username, password=password,
|
||||
db=db_name, loop=loop)
|
||||
db=db_name, loop=self.loop)
|
||||
|
||||
@classmethod
|
||||
def from_class(cls):
|
||||
return cls._instance.engine
|
||||
return cls._instance.pool
|
||||
|
||||
|
||||
class FunctionsClient(client.FunctionsAPIV1, metaclass=utils.Singleton):
|
||||
|
@ -38,14 +38,15 @@ class BaseDatabaseModel(object):
|
||||
setattr(self, k, v)
|
||||
|
||||
async def save(self):
|
||||
logger = config.Config.config_instance().logger
|
||||
c = config.Config.config_instance()
|
||||
logger, pool = c.logger, c.connection.pool
|
||||
insert = self.INSERT.format(
|
||||
self.table_name,
|
||||
str(tuple([getattr(self, clmn) for clmn in self.columns]))
|
||||
)
|
||||
logger.info("Attempting to save object '{}' "
|
||||
"using SQL query: {}".format(self.table_name, insert))
|
||||
async with config.Connection.from_class().acquire() as conn:
|
||||
async with pool.acquire() as conn:
|
||||
async with conn.cursor() as cur:
|
||||
await cur.execute(insert)
|
||||
await conn.commit()
|
||||
@ -54,12 +55,13 @@ class BaseDatabaseModel(object):
|
||||
|
||||
@classmethod
|
||||
async def delete(cls, **kwargs):
|
||||
logger = config.Config.config_instance().logger
|
||||
c = config.Config.config_instance()
|
||||
logger, pool = c.logger, c.connection.pool
|
||||
delete = cls.DELETE.format(
|
||||
cls.table_name, cls.__define_where(**kwargs))
|
||||
logger.info("Attempting to delete object '{}' "
|
||||
"using SQL query: {}".format(cls.table_name, delete))
|
||||
async with config.Connection.from_class().acquire() as conn:
|
||||
async with pool.acquire() as conn:
|
||||
async with conn.cursor() as cur:
|
||||
await cur.execute(delete)
|
||||
await conn.commit()
|
||||
@ -92,18 +94,19 @@ class BaseDatabaseModel(object):
|
||||
|
||||
@classmethod
|
||||
async def find_by(cls, **kwargs):
|
||||
logger = config.Config.config_instance().logger
|
||||
c = config.Config.config_instance()
|
||||
logger, pool = c.logger, c.connection.pool
|
||||
where = cls.__define_where(**kwargs)
|
||||
select = cls.SELECT.format(
|
||||
cls.table_name, where)
|
||||
logger.info("Attempting to find object(s) '{}' "
|
||||
"using SQL : {}".format(cls.table_name, select))
|
||||
async with config.Connection.from_class().acquire() as conn:
|
||||
async with pool.acquire() as conn:
|
||||
async with conn.cursor() as cur:
|
||||
await cur.execute(select)
|
||||
results = await cur.fetchall()
|
||||
return [cls.from_tuple(instance)
|
||||
for instance in results] if results else []
|
||||
return [cls.from_tuple(instance)
|
||||
for instance in results] if results else []
|
||||
|
||||
@classmethod
|
||||
def from_tuple(cls, tpl):
|
||||
|
@ -116,13 +116,13 @@ def server(host, port, db_uri,
|
||||
api_version=functions_api_version,
|
||||
)
|
||||
loop.run_until_complete(fnclient.ping(loop=loop))
|
||||
conn = config.Connection(db_uri, loop=loop)
|
||||
connection_pool = config.Connection(db_uri, loop=loop)
|
||||
|
||||
config.Config(
|
||||
auth_url=keystone_endpoint,
|
||||
functions_client=fnclient,
|
||||
logger=logger,
|
||||
connection=conn,
|
||||
connection=connection_pool,
|
||||
event_loop=loop,
|
||||
)
|
||||
|
||||
|
0
laos/tests/fakes/__init__.py
Normal file
0
laos/tests/fakes/__init__.py
Normal file
122
laos/tests/fakes/functions_api.py
Normal file
122
laos/tests/fakes/functions_api.py
Normal file
@ -0,0 +1,122 @@
|
||||
# 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 collections
|
||||
import uuid
|
||||
|
||||
from functionsclient import client
|
||||
from functionsclient.v1 import apps
|
||||
from functionsclient.v1 import routes
|
||||
|
||||
|
||||
APPS = dict()
|
||||
APP_ROUTES = collections.defaultdict(list)
|
||||
|
||||
|
||||
class FakeRoutes(object):
|
||||
|
||||
def __init__(self, app_name):
|
||||
self.app_name = app_name
|
||||
|
||||
async def list(self, loop=None):
|
||||
return APP_ROUTES[self.app_name]
|
||||
|
||||
async def create(self, loop=None, **data):
|
||||
app_routes = APP_ROUTES[self.app_name]
|
||||
if data.get("path") in [route.path for route in app_routes]:
|
||||
raise client.FunctionsAPIException(
|
||||
"App {} route {} already exist.".format(
|
||||
self.app_name, data.get("path")), 409)
|
||||
else:
|
||||
data.update(
|
||||
appname=self.app_name,
|
||||
memory=256
|
||||
)
|
||||
_route = routes.AppRouteResource(**data)
|
||||
APP_ROUTES[self.app_name].append(_route)
|
||||
return _route
|
||||
|
||||
async def show(self, path, loop=None):
|
||||
app_routes = APP_ROUTES[self.app_name]
|
||||
for route in app_routes:
|
||||
if path == route.path:
|
||||
return route
|
||||
raise client.FunctionsAPIException(
|
||||
"App {} route {} not found.".format(
|
||||
self.app_name, path), 404)
|
||||
|
||||
async def delete(self, path, loop=None):
|
||||
app_routes = APP_ROUTES[self.app_name]
|
||||
if path not in [route.path for route in app_routes]:
|
||||
raise client.FunctionsAPIException(
|
||||
"App {} route {} not found.".format(
|
||||
self.app_name, path), 404)
|
||||
else:
|
||||
app_routes.pop(
|
||||
app_routes.index(
|
||||
await self.show(path, loop=loop)
|
||||
)
|
||||
)
|
||||
|
||||
async def execute(self, path, loop=None):
|
||||
app_routes = APP_ROUTES[self.app_name]
|
||||
if path not in [route.path for route in app_routes]:
|
||||
raise client.FunctionsAPIException(
|
||||
"App {} route {} not found.".format(
|
||||
self.app_name, path), 404)
|
||||
else:
|
||||
route = await self.show(path)
|
||||
return "Hello world!" if route.type == "sync" else {
|
||||
"call_id": uuid.uuid4().hex
|
||||
}
|
||||
|
||||
|
||||
class FakeApps(object):
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
async def list(self, loop=None):
|
||||
return list(APPS.values())
|
||||
|
||||
async def create(self, app_name, loop=None):
|
||||
if app_name not in APPS:
|
||||
_app = apps.AppResource(
|
||||
FakeRoutes(app_name),
|
||||
**{"name": app_name, "config": None})
|
||||
APPS.update({app_name: _app})
|
||||
return _app
|
||||
else:
|
||||
raise client.FunctionsAPIException(
|
||||
"App {} already exist".format(app_name), 409)
|
||||
|
||||
async def show(self, app_name, loop=None):
|
||||
if app_name not in APPS:
|
||||
raise client.FunctionsAPIException(
|
||||
"App {} already exist.".format(app_name), 404)
|
||||
else:
|
||||
return APPS.get(app_name)
|
||||
|
||||
async def delete(self, app_name, loop=None):
|
||||
if app_name not in APPS:
|
||||
raise client.FunctionsAPIException(
|
||||
"App {} already exist.".format(app_name), 404)
|
||||
else:
|
||||
del APPS[app_name]
|
||||
|
||||
|
||||
class FunctionsAPIV1(object):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.apps = FakeApps()
|
0
laos/tests/functional/__init__.py
Normal file
0
laos/tests/functional/__init__.py
Normal file
114
laos/tests/functional/base.py
Normal file
114
laos/tests/functional/base.py
Normal file
@ -0,0 +1,114 @@
|
||||
# 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 collections
|
||||
import datetime
|
||||
import os
|
||||
import testtools
|
||||
import uuid
|
||||
import uvloop
|
||||
|
||||
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.common import logger as log
|
||||
|
||||
|
||||
from laos.tests.fakes import functions_api
|
||||
from laos.tests.functional import client
|
||||
|
||||
|
||||
class LaosFunctionalTestsBase(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.testapp = service.AbstractWebServer(
|
||||
host="localhost",
|
||||
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
|
||||
|
||||
connection_pool = config.Connection(
|
||||
os.getenv("TEST_DB_URI"), loop=self.testloop)
|
||||
|
||||
fnclient = functions_api.FunctionsAPIV1()
|
||||
|
||||
self.test_config = config.Config(
|
||||
logger=logger,
|
||||
connection=connection_pool,
|
||||
event_loop=self.testloop,
|
||||
functions_client=fnclient,
|
||||
)
|
||||
|
||||
self.project_id = str(uuid.uuid4()).replace("-", "")
|
||||
self.test_client = client.ProjectBoundLaosTestClient(
|
||||
self.testapp, self.project_id)
|
||||
|
||||
self.route_data = {
|
||||
"type": "sync",
|
||||
"path": "/hello-sync-private",
|
||||
"image": "iron/hello",
|
||||
"is_public": "false"
|
||||
}
|
||||
|
||||
self.testloop.run_until_complete(self.test_client.start_server())
|
||||
super(LaosFunctionalTestsBase, self).setUp()
|
||||
|
||||
def tearDown(self):
|
||||
functions_api.APPS = {}
|
||||
functions_api.ROUTES = collections.defaultdict(list)
|
||||
# ^ temporary solution,
|
||||
# until https://github.com/iron-io/functions/issues/274 fixed
|
||||
self.testloop.run_until_complete(self.test_client.close())
|
||||
super(LaosFunctionalTestsBase, self).tearDown()
|
112
laos/tests/functional/client.py
Normal file
112
laos/tests/functional/client.py
Normal file
@ -0,0 +1,112 @@
|
||||
# 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
|
||||
|
||||
from aiohttp import test_utils
|
||||
|
||||
|
||||
class AppsV1(object):
|
||||
|
||||
# /v1/{project_id}/apps
|
||||
apps_path = "/v1/{}/apps"
|
||||
# /v1/{project_id}/apps/{app}
|
||||
app_path = apps_path + "/{}"
|
||||
|
||||
def __init__(self, test_client):
|
||||
self.client = test_client
|
||||
|
||||
async def list(self):
|
||||
return await self.client.list(self.apps_path)
|
||||
|
||||
async def show(self, app_name):
|
||||
return await self.client.show(self.app_path, app_name)
|
||||
|
||||
async def create(self, app_name):
|
||||
data = {
|
||||
"app": {
|
||||
"name": app_name
|
||||
}
|
||||
}
|
||||
return await self.client.create(self.apps_path, data)
|
||||
|
||||
async def delete(self, app_name):
|
||||
return await self.client.remove(self.app_path, app_name)
|
||||
|
||||
|
||||
class RoutesV1(object):
|
||||
|
||||
# /v1/{project_id}/apps/{app}/routes
|
||||
routes_path = "/v1/{}/apps/{}/routes"
|
||||
# /v1/{project_id}/apps/{app}/routes{}
|
||||
route_path = routes_path + "{}"
|
||||
|
||||
def __init__(self, test_client):
|
||||
self.client = test_client
|
||||
|
||||
async def list(self, app_name):
|
||||
return await self.client.list(
|
||||
self.routes_path, app_name)
|
||||
|
||||
async def show(self, app_name, path):
|
||||
return await self.client.show(
|
||||
self.route_path, app_name, path)
|
||||
|
||||
async def create(self, app_name, **data):
|
||||
return await self.client.create(
|
||||
self.routes_path, {"route": data}, app_name)
|
||||
|
||||
async def delete(self, app_name, path):
|
||||
return await self.client.remove(
|
||||
self.route_path, app_name, path)
|
||||
|
||||
|
||||
class ProjectBoundLaosTestClient(test_utils.TestClient):
|
||||
|
||||
def __init__(self, app_or_server, project_id):
|
||||
super(ProjectBoundLaosTestClient, self).__init__(app_or_server)
|
||||
self.project_id = project_id
|
||||
self.headers = {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
self.apps = AppsV1(self)
|
||||
self.routes = RoutesV1(self)
|
||||
|
||||
async def create(self, route, data, *parts):
|
||||
resp = await self.post(
|
||||
route.format(self.project_id, *parts),
|
||||
data=jsonlib.dumps(data), headers=self.headers)
|
||||
json = await resp.json()
|
||||
return json, resp.status
|
||||
|
||||
async def list(self, route, *parts):
|
||||
resp = await self.get(
|
||||
route.format(self.project_id, *parts),
|
||||
headers=self.headers)
|
||||
json = await resp.json()
|
||||
return json, resp.status
|
||||
|
||||
async def show(self, route, resource_name, *parts):
|
||||
resp = await self.get(
|
||||
route.format(self.project_id, resource_name, *parts),
|
||||
headers=self.headers)
|
||||
json = await resp.json()
|
||||
return json, resp.status
|
||||
|
||||
async def remove(self, route, resource_name, *parts):
|
||||
resp = await self.delete(
|
||||
route.format(self.project_id, resource_name, *parts),
|
||||
headers=self.headers)
|
||||
json = await resp.json()
|
||||
return json, resp.status
|
62
laos/tests/functional/test_apps.py
Normal file
62
laos/tests/functional/test_apps.py
Normal file
@ -0,0 +1,62 @@
|
||||
# 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.functional import base
|
||||
|
||||
|
||||
class TestApps(base.LaosFunctionalTestsBase):
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
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"])
|
||||
|
||||
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)
|
121
laos/tests/functional/test_routes.py
Normal file
121
laos/tests/functional/test_routes.py
Normal file
@ -0,0 +1,121 @@
|
||||
# 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
|
||||
|
||||
from laos.tests.functional import base
|
||||
|
||||
|
||||
class TestAppRoutes(base.LaosFunctionalTestsBase):
|
||||
|
||||
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"])
|
||||
|
||||
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)
|
||||
|
||||
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"])
|
||||
|
||||
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"])
|
||||
|
||||
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)
|
||||
|
||||
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"])
|
@ -2,13 +2,13 @@
|
||||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
||||
|
||||
uvloop
|
||||
aiohttp # Apache-2.0
|
||||
aiomysql
|
||||
alembic>=0.8.4 # MIT
|
||||
click
|
||||
keystoneauth1>=2.14.0 # Apache-2.0
|
||||
python-keystoneclient==3.6.0
|
||||
uvloop==0.6.0 # Apache-2.0
|
||||
aiohttp==1.1.5 # Apache-2.0
|
||||
aiomysql==0.0.9 # Apache-2.0
|
||||
alembic==0.8.8 # MIT
|
||||
click==6.6 # Apache-2.0
|
||||
keystoneauth1==2.15.0 # Apache-2.0
|
||||
python-keystoneclient==3.6.0 # Apache-2.0
|
||||
|
||||
#swagger-api
|
||||
aiohttp-swagger
|
||||
aiohttp-swagger==1.0.2 # Apache-2.0
|
||||
|
18
setup.py
18
setup.py
@ -29,14 +29,14 @@ setuptools.setup(
|
||||
author_email='denis@iron.io',
|
||||
packages=setuptools.find_packages(),
|
||||
install_requires=[
|
||||
"uvloop",
|
||||
"aiohttp",
|
||||
"aiomysql",
|
||||
"alembic>=0.8.4",
|
||||
"click",
|
||||
"keystoneauth1>=2.14.0",
|
||||
"uvloop==0.6.0",
|
||||
"aiohttp==1.1.5",
|
||||
"aiomysql==0.0.9",
|
||||
"alembic==0.8.8",
|
||||
"click==6.6",
|
||||
"keystoneauth1==2.15.0",
|
||||
"python-keystoneclient==3.6.0",
|
||||
"aiohttp-swagger",
|
||||
"aiohttp-swagger==1.0.2",
|
||||
],
|
||||
license='License :: OSI Approved :: Apache Software License',
|
||||
classifiers=[
|
||||
@ -61,10 +61,10 @@ setuptools.setup(
|
||||
tests_require=[
|
||||
'flake8==2.5.0',
|
||||
'hacking<0.11,>=0.10.0',
|
||||
'coverage>=4.0',
|
||||
'sphinx!=1.3b1,<1.4,>=1.2.1',
|
||||
'testrepository>=0.0.18',
|
||||
'testtools>=1.4.0',
|
||||
"pytest-aiohttp",
|
||||
"pytest-cov",
|
||||
],
|
||||
zip_safe=True,
|
||||
entry_points={
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
flake8==2.5.0 # fix for https://gitlab.com/pycqa/flake8/issues/94
|
||||
hacking<0.11,>=0.10.0
|
||||
coverage>=4.0 # Apache-2.0
|
||||
sphinx!=1.3b1,<1.4,>=1.2.1 # BSD
|
||||
testrepository>=0.0.18 # Apache-2.0/BSD
|
||||
testtools>=1.4.0 # MIT
|
||||
pytest-aiohttp==0.1.3
|
||||
pytest-cov==2.4.0
|
||||
|
17
tox.ini
17
tox.ini
@ -1,19 +1,20 @@
|
||||
# Function Python client
|
||||
# Project LaOS
|
||||
|
||||
[tox]
|
||||
envlist = py35,pep8
|
||||
envlist = py35-functional,pep8
|
||||
minversion = 1.6
|
||||
skipsdist = True
|
||||
|
||||
[testenv]
|
||||
passenv =
|
||||
PYTHONASYNCIODEBUG
|
||||
|
||||
TEST_DB_URI
|
||||
setenv = VIRTUAL_ENV={envdir}
|
||||
usedevelop = True
|
||||
install_command = pip install -U {opts} {packages}
|
||||
deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
git+ssh://git@github.com/iron-io/functions_python.git#egg=functions-python
|
||||
commands = find . -type f -name "*.pyc" -delete
|
||||
rm -f .testrepository/times.dbm
|
||||
python setup.py testr --testr-args='{posargs}'
|
||||
@ -26,12 +27,10 @@ commands = flake8
|
||||
[testenv:venv]
|
||||
commands = {posargs}
|
||||
|
||||
[testenv:cover]
|
||||
commands =
|
||||
coverage erase
|
||||
python setup.py testr --coverage --testr-args='{posargs}'
|
||||
coverage html
|
||||
coverage report
|
||||
|
||||
[testenv:py35-functional]
|
||||
commands = pytest --tb=long --capture=sys --cov=laos --capture=fd {toxinidir}/laos/tests/functional
|
||||
|
||||
|
||||
[testenv:docs]
|
||||
commands =
|
||||
|
Loading…
x
Reference in New Issue
Block a user