diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..45a0dfbf5d45bf0f346cf4f9c1031ebfdc5c07d1
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,69 @@
+variables:
+  PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache"
+  VENV2: "python_client_venv_p2/"
+  VENV3: "python_client_venv_p3/"
+
+cache:
+  paths:
+    - $VENV2
+    - $VENV3
+
+# ----------------------------
+stages:
+  - test
+  - doc
+  - deploy
+
+# ----------------------------
+image: python:3.7
+
+before_script:
+  - python -V
+  - pip install virtualenv
+  - virtualenv $VENV3
+  - source $VENV3/bin/activate
+  - pip install -r requirements.txt
+  - pwd
+
+# ----------------------------
+tests:2.7:
+  stage: test
+  image: python:2.7
+  before_script:
+    - python -V
+    - pip install virtualenv
+    - virtualenv $VENV2
+    - source $VENV2/bin/activate
+    - pip install -r requirements.txt mock
+    - pwd
+  script:
+    - python -V
+    - python -m pytest --pep8 --cov=allgo tests/
+
+# ----------------------------
+tests:3.7:
+  stage: test
+  script:
+    - python -V
+    - python -m pytest --pep8 --cov=allgo tests/
+
+# ----------------------------
+sphinx:
+  stage: doc
+  script:
+    - apt update && apt install -y pandoc
+    - cd doc ; make html
+
+# ----------------------------
+# gitlab pages : documentation generation & deploy only for master branch.
+pages:
+  stage: deploy
+  script:
+    - apt update && apt install -y pandoc
+    - cd doc ; make html
+    - mv _build/html/ ../public/
+  artifacts:
+    paths:
+      - public
+  only:
+    - master
diff --git a/README.md b/README.md
index f1c2ee723802df0ccf5122a77b9d558bd2e301e2..78bfdaef04bfa226a219995e2f6e9c433bdc0932 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,10 @@
 # A||GO API-Client python library
 We provide here a simple API client for [A||GO](https://allgo18.inria.fr) written in Python.
 
+[![pipeline status](https://gitlab.inria.fr/allgo/api-clients/python_client/badges/client_api/pipeline.svg)](https://gitlab.inria.fr/allgo/api-clients/python_client/commits/client_api)
+python 2.7: [![coverage report 2.7](https://gitlab.inria.fr/allgo/api-clients/python_client/badges/client_api/coverage.svg?job=tests:2.7)](https://gitlab.inria.fr/allgo/api-clients/python_client/commits/client_api?job=tests:2.7)
+python 3.7: [![coverage report 3.7](https://gitlab.inria.fr/allgo/api-clients/python_client/badges/client_api/coverage.svg?job=tests:3.7)](https://gitlab.inria.fr/allgo/api-clients/python_client/commits/client_api?job=tests:3.7)
+
 ## Description
 AllGo is a SaaS (Software as a Service) platform provided by Inria. It may be seen as a virtual showroom of technologies developed by research teams.
 First follow https://allgo18.inria.fr/accounts/signup to create an account on AllGo (anyone may create such an account). Once your account creation is confirmed, please connect to https://allgo18.inria.fr to obtain your private token, which will allow yo to use the AllGo REST API. You will need this token later (cf. §3 below).
diff --git a/allgo/__init__.py b/allgo/__init__.py
index 78cd843ebc1a2b1f9c8e3e71ce891266d5adc242..c3fa80ac7905068d392694926160726bf2e588f6 100644
--- a/allgo/__init__.py
+++ b/allgo/__init__.py
@@ -223,7 +223,7 @@ class Client:
             msg += "KeyError: {}".format(str(err))
             raise AllgoError(msg)
 
-    def create_job(self, app, version=None, params='', files=None):
+    def create_job(self, app, version=None, params='', files=None, queue=None):
         """Create a job for app, with params.
 
         Launch the API request to create a job for the given app,
@@ -240,6 +240,8 @@ class Client:
             all the app command line params as one string.
         files  : list of filepaths, optional
             filepaths to upload.
+        queue  : string
+            name of the queue to use.
 
         Returns
         -------
@@ -253,10 +255,11 @@ class Client:
             with the Response status code if it's not 200.
         """
         data = {
-            "job[webapp_name]": app,
+            # ~ "job[webapp_name]": app,
             "job[webapp_id]"  : app,
             "job[param]"      : params,
-            "job[version]"    : version
+            "job[version]"    : version,
+            "job[queue]"      : queue,
         }
 
         file_dict = {}
diff --git a/doc/conf.py b/doc/conf.py
index d986506ac65252aab54cbaa59047f8474293d6af..ce1a08291683c8966e3c97ba2ab43ce06e9c42d8 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -91,7 +91,7 @@ html_theme = 'sphinx_rtd_theme'
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
 # so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
+# html_static_path = ['_static']
 
 # Custom sidebar templates, must be a dictionary that maps document names
 # to template names.
diff --git a/pytest.ini b/pytest.ini
index db328198fc04802f3fb5ed9e0fbce2cfbded84b1..1775774e320a6ddf29bf1fa2aa733592c9b2bb4b 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -5,3 +5,11 @@ markers =
     error: test when an error occured
     success: test output on successfull request
     dbg: for debug.
+    pep8: workaround for https://bitbucket.org/pytest-dev/pytest-pep8/issues/23/
+
+# spaces rules are disabled
+pep8ignore = E201 E202 E203 E221 E231 E272
+
+# developper are recommanded to use a max length = 80
+# but as doctrings are quite longer, we allow up to 100.
+pep8maxlinelength = 100
diff --git a/requirements.txt b/requirements.txt
index 6566012f04aac252ab361294aef221b7b8ee07a9..a5d15da2f144f83542dbac0ddfc54621cf49bcd0 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,9 @@
 requests
 pytest
+pytest-cov
+pytest-pep8
 sphinx
+sphinx_rtd_theme
+ipython
 nbsphinx
 pandoc
diff --git a/tests/test_allgo.py b/tests/test_allgo.py
index b69ffcf259ff355a30a78a11f77359a0f9c47db9..74c1f45e51c53f79bff5e71755835c43d4f664a2 100644
--- a/tests/test_allgo.py
+++ b/tests/test_allgo.py
@@ -9,14 +9,32 @@ import tempfile
 # not standard
 import pytest
 
+# local import
+import allgo  # I need to modify constant
+from allgo import *
+
 if 2 == sys.version_info.major:
     from mock import Mock, patch
 else:
     from unittest.mock import Mock, patch
 
-# local import
-import allgo  # I need to modify constant
-from allgo import *
+
+# ================================================
+class TestStatusError:
+
+    REF_MSG = "whatever, an error message."
+
+    @pytest.fixture
+    def response(self):
+        m = Mock()
+        m.json.return_value = \
+            { 'error': self.REF_MSG }
+        return m
+
+    @pytest.mark.dbg
+    def test_msg(self, response):
+        err = StatusError(42, response)
+        assert self.REF_MSG == err.msg
 
 
 # ================================================
@@ -149,7 +167,7 @@ def test_create_job__app_not_exist(mock_post, client):
 
     err = err_info.value
     assert 404 == err.status_code
-    assert 'Application not found' == err.msg
+    # ~ assert 'Application not found' == err.msg
 
 
 @pytest.mark.error
@@ -166,7 +184,7 @@ def test_create_job__app_not_published(mock_post, client):
 
     err = err_info.value
     assert 404 == err.status_code
-    assert 'not yet published' in err.msg
+    # ~ assert 'not yet published' in err.msg
 
 
 @pytest.mark.error
@@ -179,11 +197,11 @@ def test_create_job__queue_not_exist(mock_post, client):
 
     # -- tests
     with pytest.raises(StatusError) as err_info:
-        client.create_job('fake')
+        client.create_job('fake', queue='no_queue')
 
     err = err_info.value
     assert 400 == err.status_code
-    assert 'Unknown queue' == err.msg
+    # ~ assert 'Unknown queue' == err.msg
 
 
 @pytest.mark.error
@@ -200,7 +218,7 @@ def test_create_job__param_error(mock_post, client):
 
     err = err_info.value
     assert 400 == err.status_code
-    assert err.msg.startswith('Invalid parameters')
+    # ~ assert err.msg.startswith('Invalid parameters')
 
 
 @pytest.mark.success
@@ -256,7 +274,7 @@ def test_job_status__error(mock_get, client):
 
     err = err_info.value
     assert 404 == err.status_code
-    assert 'Job not found' == err.msg
+    # ~ assert 'Job not found' == err.msg
 
 
 @pytest.mark.success
@@ -388,7 +406,7 @@ def test_download_file__default(mock_get, client):
     file_url = "https://whatever.fr/" + filename
     filepath = client.download_file(file_url)
 
-    assert "./"+filename == filepath
+    assert "./" + filename == filepath
 
     content = ""
     with open(filepath, "r") as file_out:
@@ -530,7 +548,7 @@ def test_get_log__big(mock_get, mock_status, client):
     for c in alphabet:
         chunk = ''
         for i in range(8):
-            chunk += c*10 + "\n"
+            chunk += c * 10 + "\n"
         file_content.append(chunk)
     # file_content is 5 chunks of 8 lines each
     #   and each line is the same letter repeated 10 times.
diff --git a/tests/test_integration.py b/tests/test_integration.py
new file mode 100644
index 0000000000000000000000000000000000000000..af1ac73fcededd9ec7e03eb7911f7fd3dfce76b9
--- /dev/null
+++ b/tests/test_integration.py
@@ -0,0 +1,270 @@
+#! /usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+# standard lib
+import os
+
+# not standard
+import pytest
+
+# local import
+from allgo import *
+
+
+# ==========================================================
+class ApiClientIntegration:
+    CLIENT = None
+    TEST_APP = 'allgo api test'
+
+    @classmethod
+    def setup_class(cls):
+        """Create a client object, and set it as attribute."""
+        raise NotImplementedError("This class has to be derived to be used.")
+
+    @classmethod
+    def _build_client(cls):
+        raise NotImplementedError
+
+    @classmethod
+    def _get_job_id(cls):
+        raise NotImplementedError
+
+    @pytest.mark.error
+    def test_create_job__user_not_exist(self):
+        """create a job with a token that refers to no one.
+            We test that we get an error 401."""
+        # The token should refer to unexisting user
+        client = self._build_client(token='0')
+
+        with pytest.raises(StatusError) as err_info:
+            client.create_job('fake')
+
+        err = err_info.value
+        assert 401 == err.status_code
+        # ~ assert '' == err.msg
+
+    @pytest.mark.error
+    def test_create_job__app_not_exist(self):
+        """I try to create a job with an app that cannot exist.
+            Its name is illegal, an error 404 should be raised."""
+        with pytest.raises(StatusError) as err_info:
+            self.CLIENT.create_job('ç')
+
+        err = err_info.value
+        assert 404 == err.status_code
+        # ~ assert 'Application not found' == err.msg
+
+    @pytest.mark.skip(reason="I don't want to create another fake app. \
+                              Wait for version to be considered.")
+    @pytest.mark.error
+    def test_create_job__app_not_published(self):
+        with pytest.raises(StatusError) as err_info:
+            self.CLIENT.create_job('not_published')
+
+        err = err_info.value
+        assert 404 == err.status_code
+        # ~ assert 'not yet published' in err.msg
+
+    @pytest.mark.error
+    def test_create_job__queue_not_exist(self):
+        """create a job to run on a queue that does not exist.
+            Should raise an error 400."""
+        with pytest.raises(StatusError) as err_info:
+            self.CLIENT.create_job(self.TEST_APP, queue='no_queue')
+
+        err = err_info.value
+        assert 400 == err.status_code
+        # ~ assert 'Unknown queue' == err.msg
+
+    @pytest.mark.error
+    def test_create_job__param_error(self):
+        """create a job with an invalid param.
+            should raise an error 400."""
+        with pytest.raises(StatusError) as err_info:
+            self.CLIENT.create_job(self.TEST_APP, params='$')
+
+        err = err_info.value
+        assert 400 == err.status_code
+        # ~ assert err.msg.startswith('Invalid parameters')
+
+    @pytest.mark.success
+    def test_create_job(self):
+        """create a job that will succeed.
+            response should be a dict with at least keys 'id' and 'url'."""
+        response = self.CLIENT.create_job(self.TEST_APP)
+
+        assert isinstance(response, dict)
+        assert 'id'  in response
+        assert 'url' in response
+
+    # I think this test is useless as the failure is not related to the submission.
+    @pytest.mark.success
+    def test_create_job__that_will_fail(self):
+        """create a job that will fail (the job, not its submission).
+            response should be a dict with at least keys 'id' and 'url'."""
+        response = self.CLIENT.create_job(self.TEST_APP, 1)
+
+        assert isinstance(response, dict)
+        assert 'id'  in response
+        assert 'url' in response
+
+    @pytest.mark.error
+    def test_job_status__do_not_exist(self):
+        """try to get status of a job that does not exist.
+            should raise an error 404."""
+        with pytest.raises(StatusError) as err_info:
+            self.CLIENT.job_status('fake_id')  # not found, as job id should be integer
+
+        err = err_info.value
+        assert 404 == err.status_code
+        # ~ assert 'Job not found' == err.msg
+
+    @pytest.mark.success
+    def test_job_status(self):
+        """create a job and get its status,
+            the method should return the request response as a dictionnary.
+            it should contain the 'status' key and the job id as key."""
+        response = self.CLIENT.create_job(self.TEST_APP)
+        job_id   = response['id']
+        response = self.CLIENT.job_status(job_id)
+
+        assert isinstance(response, dict)
+        assert str(job_id) in response
+        assert 'status'    in response
+
+    @pytest.mark.dbg
+    @pytest.mark.success
+    def test_download_file(self, tmp_path):
+        """Get the job status of an existing job, and try to download the log file.
+            The file should have been written in a given directory.
+            The content file is also checked.
+
+            The job queried exists and belongs to the testing user.
+        """
+        JOB_ID = self._get_job_id()
+        response = self.CLIENT.job_status(JOB_ID)
+
+        file_url = response[str(JOB_ID)]['allgo.log']
+        filepath = self.CLIENT.download_file(file_url, outdir=tmp_path)
+
+        assert filepath.startswith(str(tmp_path))
+
+        content = ""
+        with open(filepath, "r") as file_out:
+            content = file_out.read()
+        assert content.endswith("====  ALLGO JOB SUCCESS  ====\n")
+
+        # cleaning
+        os.remove(filepath)
+
+
+# ==========================================================
+class TestAllgo18(ApiClientIntegration):
+
+    @classmethod
+    def _build_client(cls, token):
+        return Client(token)
+
+    @classmethod
+    def _get_job_id(cls):
+        """return a job id that exist, with a log file named allgo.log"""
+        return 874
+
+    def setup_class(cls):
+        """define the client used in tests.
+            It uses a token defined in as env variable."""
+        TOKEN = os.environ.get('ALLGO_18_TOKEN')
+        cls.CLIENT = cls._build_client(TOKEN)
+
+
+@pytest.mark.skipif(not os.environ.get('DEV'), reason="No dev test in CI.")
+class TestAllgoDev(ApiClientIntegration):
+    TEST_APP = 'Allgo Api Test'
+
+    @classmethod
+    def _build_client(cls, token):
+        ALLGO_URL = 'https://localhost'
+
+        return Client(token, allgo_url=ALLGO_URL, verify_tls=False)
+
+    @classmethod
+    def _get_job_id(cls):
+        """return a job id that exist, with a log file named allgo.log"""
+        return 37
+
+    @classmethod
+    def setup_class(cls):
+        """define the client used in local instance.
+            It uses a token defined in as env variable."""
+        TOKEN = os.environ.get('ALLGO_DEV_TOKEN')
+        cls.CLIENT = cls._build_client(TOKEN)
+
+    # should be removed when a global solution will be found.
+    @pytest.mark.xfail(reason="published state is not yet implemented.")
+    @pytest.mark.error
+    def test_create_job__app_not_published(self):
+        with pytest.raises(StatusError) as err_info:
+            self.CLIENT.create_job('not_published')
+
+        err = err_info.value
+        assert 404 == err.status_code
+        # ~ assert 'not yet published' in err.msg
+
+
+class TestAllgoOld(ApiClientIntegration):
+    TEST_APP  = 206
+
+    @classmethod
+    def _build_client(cls, token):
+        ALLGO_URL = 'https://allgo.inria.fr'
+
+        return Client(token, allgo_url=ALLGO_URL)
+
+    @classmethod
+    def _get_job_id(cls):
+        """return a job id that exist, with a log file named allgo.log"""
+        return 71771
+
+    def setup_class(cls):
+        """define the client used in local instance.
+            It uses a token defined in as env variable."""
+        TOKEN = os.environ.get('ALLGO_OLD_TOKEN')
+        cls.CLIENT = cls._build_client(TOKEN)
+
+    # behaviour changed between old and new.
+    @pytest.mark.error
+    def test_create_job__queue_not_exist(self):
+        """create a job to run on a queue that does not exist.
+            Should raise an error 404 (code error changed between old and new version)."""
+        with pytest.raises(StatusError) as err_info:
+            self.CLIENT.create_job(self.TEST_APP, queue='no_queue')
+
+        err = err_info.value
+        assert 404 == err.status_code
+        # ~ assert 'Unknown queue' == err.msg
+
+    # behaviour changed between old and new.
+    # I didn't find the source that emit the error in rails code.
+    @pytest.mark.error
+    def test_create_job__param_error(self):
+        """create a job with an invalid param.
+            should raise an error 422 (code error changed between old and new version)."""
+        with pytest.raises(StatusError) as err_info:
+            self.CLIENT.create_job(self.TEST_APP, params='$')
+
+        err = err_info.value
+        assert 422 == err.status_code
+        # ~ assert err.msg.startswith('Invalid parameters')
+
+    # behaviour changed between old and new.
+    @pytest.mark.success
+    def test_job_status(self):
+        """create a job and get its status,
+            the method should return the request response as a dictionnary.
+            it should contain only the 'status' key."""
+        response = self.CLIENT.create_job(self.TEST_APP)
+        job_id   = response['id']
+        response = self.CLIENT.job_status(job_id)
+
+        assert isinstance(response, dict)
+        assert 'status'    in response