diff --git a/requirements.txt b/requirements.txt
index 3e267f9a..0cb66af9 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,9 +1,6 @@
netifaces
-tornado==3.2.2
-pyzmq
jsonschema
pycurl
python-dateutil
apache-libcloud
requests
-
diff --git a/scripts/documentation.sh b/scripts/documentation.sh
new file mode 100755
index 00000000..67f10e6d
--- /dev/null
+++ b/scripts/documentation.sh
@@ -0,0 +1,27 @@
+#!/bin/sh
+#
+# Copyright (C) 2015 GNS3 Technologies Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+#
+# Build the documentation
+#
+
+set -e
+
+py.test
+python ../gns3server/web/documentation.py
+cd ../docs
+make html
diff --git a/tests/api/__init__.py b/tests/api/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/api/base.py b/tests/api/base.py
new file mode 100644
index 00000000..5650b217
--- /dev/null
+++ b/tests/api/base.py
@@ -0,0 +1,159 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2015 GNS3 Technologies Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""Base code use for all API tests"""
+
+import json
+import re
+import asyncio
+import socket
+import pytest
+from aiohttp import web
+import aiohttp
+
+from gns3server.web.route import Route
+#TODO: get rid of *
+from gns3server.handlers import *
+from gns3server.modules import MODULES
+
+
+class Query:
+ def __init__(self, loop, host='localhost', port=8001):
+ self._loop = loop
+ self._port = port
+ self._host = host
+
+ def post(self, path, body, **kwargs):
+ return self._fetch("POST", path, body, **kwargs)
+
+ def get(self, path, **kwargs):
+ return self._fetch("GET", path, **kwargs)
+
+ def _get_url(self, path):
+ return "http://{}:{}{}".format(self._host, self._port, path)
+
+ def _fetch(self, method, path, body=None, **kwargs):
+ """Fetch an url, parse the JSON and return response
+
+ Options:
+ - example if True the session is included inside documentation
+ - raw do not JSON encode the query
+ """
+ if body is not None and not kwargs.get("raw", False):
+ body = json.dumps(body)
+
+ @asyncio.coroutine
+ def go(future):
+ response = yield from aiohttp.request(method, self._get_url(path), data=body)
+ future.set_result(response)
+ future = asyncio.Future()
+ asyncio.async(go(future))
+ self._loop.run_until_complete(future)
+ response = future.result()
+
+ @asyncio.coroutine
+ def go(future, response):
+ response = yield from response.read()
+ future.set_result(response)
+ future = asyncio.Future()
+ asyncio.async(go(future, response))
+ self._loop.run_until_complete(future)
+ response.body = future.result()
+ response.route = response.headers.get('X-Route', None)
+
+ if response.body is not None:
+ try:
+ response.json = json.loads(response.body.decode("utf-8"))
+ except ValueError:
+ response.json = None
+ else:
+ response.json = {}
+ if kwargs.get('example'):
+ self._dump_example(method, response.route, body, response)
+ return response
+
+ def _dump_example(self, method, path, body, response):
+ """Dump the request for the documentation"""
+ if path is None:
+ return
+ with open(self._example_file_path(method, path), 'w+') as f:
+ f.write("curl -i -x{} 'http://localhost:8000{}'".format(method, path))
+ if body:
+ f.write(" -d '{}'".format(re.sub(r"\n", "", json.dumps(json.loads(body), sort_keys=True))))
+ f.write("\n\n")
+
+ f.write("{} {} HTTP/1.1\n".format(method, path))
+ if body:
+ f.write(json.dumps(json.loads(body), sort_keys=True, indent=4))
+ f.write("\n\n\n")
+ f.write("HTTP/1.1 {}\n".format(response.status))
+ for header, value in sorted(response.headers.items()):
+ if header == 'DATE':
+ # We fix the date otherwise the example is always different
+ value = "Thu, 08 Jan 2015 16:09:15 GMT"
+ f.write("{}: {}\n".format(header, value))
+ f.write("\n")
+ f.write(json.dumps(json.loads(response.body.decode('utf-8')), sort_keys=True, indent=4))
+ f.write("\n")
+
+ def _example_file_path(self, method, path):
+ path = re.sub('[^a-z0-9]', '', path)
+ return "docs/api/examples/{}_{}.txt".format(method.lower(), path)
+
+
+def _get_unused_port():
+ """ Return an unused port on localhost. In rare occasion it can return
+ an already used port (race condition)"""
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ s.bind(('localhost', 0))
+ addr, port = s.getsockname()
+ s.close()
+ return port
+
+
+@pytest.fixture(scope="module")
+def loop(request):
+ """Return an event loop and destroy it at the end of test"""
+ loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(loop) # Replace main loop to avoid conflict between tests
+
+ def tear_down():
+ loop.close()
+ asyncio.set_event_loop(None)
+ request.addfinalizer(tear_down)
+ return loop
+
+
+@pytest.fixture(scope="module")
+def server(request, loop):
+ port = _get_unused_port()
+ host = "localhost"
+ app = web.Application()
+ for method, route, handler in Route.get_routes():
+ app.router.add_route(method, route, handler)
+ for module in MODULES:
+ instance = module.instance()
+ srv = loop.create_server(app.make_handler(), host, port)
+ srv = loop.run_until_complete(srv)
+
+ def tear_down():
+ for module in MODULES:
+ loop.run_until_complete(module.destroy())
+ srv.close()
+ srv.wait_closed()
+ request.addfinalizer(tear_down)
+ return Query(loop, host=host, port=port)
diff --git a/tests/api/test_version.py b/tests/api/test_version.py
new file mode 100644
index 00000000..178c0918
--- /dev/null
+++ b/tests/api/test_version.py
@@ -0,0 +1,64 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2015 GNS3 Technologies Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+"""
+This test suite check /version endpoint
+It's also used for unittest the HTTP implementation.
+"""
+
+from tests.utils import asyncio_patch
+from tests.api.base import server, loop
+from gns3server.version import __version__
+
+
+def test_version_output(server):
+ response = server.get('/version', example=True)
+ assert response.status == 200
+ assert response.json == {'version': __version__}
+
+
+def test_version_input(server):
+ query = {'version': __version__}
+ response = server.post('/version', query, example=True)
+ assert response.status == 200
+ assert response.json == {'version': __version__}
+
+
+def test_version_invalid_input(server):
+ query = {'version': "0.4.2"}
+ response = server.post('/version', query)
+ assert response.status == 409
+ assert response.json == {'message': '409: Invalid version', 'status': 409}
+
+
+def test_version_invalid_input_schema(server):
+ query = {'version': "0.4.2", "bla": "blu"}
+ response = server.post('/version', query)
+ assert response.status == 400
+
+
+@asyncio_patch("demoserver.handlers.version_handler.VersionHandler", return_value={})
+def test_version_invalid_output_schema():
+ query = {'version': "0.4.2"}
+ response = server.post('/version', query)
+ assert response.status == 400
+
+
+def test_version_invalid_json(server):
+ query = "BOUM"
+ response = server.post('/version', query, raw=True)
+ assert response.status == 400
diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py
new file mode 100644
index 00000000..fb2b3c9a
--- /dev/null
+++ b/tests/api/test_vpcs.py
@@ -0,0 +1,53 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2015 GNS3 Technologies Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+from tests.api.base import server, loop
+from tests.utils import asyncio_patch
+from gns3server import modules
+
+
+def test_vpcs_create(server):
+ response = server.post('/vpcs', {'name': 'PC TEST 1'}, example=True)
+ assert response.status == 200
+ assert response.route == '/vpcs'
+ assert response.json['name'] == 'PC TEST 1'
+ assert response.json['vpcs_id'] == 42
+
+
+@asyncio_patch('demoserver.modules.VPCS.create', return_value=84)
+def test_vpcs_mock(server, mock):
+ response = server.post('/vpcs', {'name': 'PC TEST 1'}, example=False)
+ assert response.status == 200
+ assert response.route == '/vpcs'
+ assert response.json['name'] == 'PC TEST 1'
+ assert response.json['vpcs_id'] == 84
+
+
+def test_vpcs_nio_create(server):
+ response = server.post('/vpcs/42/nio', {
+ 'id': 42,
+ 'nio': {
+ 'type': 'nio_unix',
+ 'local_file': '/tmp/test',
+ 'remote_file': '/tmp/remote'
+ },
+ 'port': 0,
+ 'port_id': 0},
+ example=True)
+ assert response.status == 200
+ assert response.route == '/vpcs/{vpcs_id}/nio'
+ assert response.json['name'] == 'PC 2'
diff --git a/tests/utils.py b/tests/utils.py
new file mode 100644
index 00000000..bb529541
--- /dev/null
+++ b/tests/utils.py
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2015 GNS3 Technologies Inc.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+
+import asyncio
+from unittest.mock import patch
+
+
+def asyncio_patch(function, *args, **kwargs):
+ @asyncio.coroutine
+ def fake_anwser(*a, **kw):
+ return kwargs["return_value"]
+
+ def register(func):
+ @patch(function, return_value=fake_anwser)
+ @asyncio.coroutine
+ def inner(*a, **kw):
+ return func(*a, **kw)
+ return inner
+ return register