From 9fcaf1e930ae2344c43e59b6ad9fea84adc6f340 Mon Sep 17 00:00:00 2001
From: Serghei Mihai <smihai@entrouvert.com>
Date: Tue, 14 May 2019 09:47:44 +0200
Subject: [PATCH] lille_kimoce: initial connector (#33099)

---
 passerelle/contrib/lille_kimoce/__init__.py   |   0
 .../lille_kimoce/migrations/0001_initial.py   |  33 +
 .../lille_kimoce/migrations/__init__.py       |   0
 passerelle/contrib/lille_kimoce/models.py     | 232 +++++++
 passerelle/utils/http_authenticators.py       |  15 +
 tests/settings.py                             |   1 +
 tests/test_lille_kimoce.py                    | 591 ++++++++++++++++++
 7 files changed, 872 insertions(+)
 create mode 100644 passerelle/contrib/lille_kimoce/__init__.py
 create mode 100644 passerelle/contrib/lille_kimoce/migrations/0001_initial.py
 create mode 100644 passerelle/contrib/lille_kimoce/migrations/__init__.py
 create mode 100644 passerelle/contrib/lille_kimoce/models.py
 create mode 100644 tests/test_lille_kimoce.py

diff --git a/passerelle/contrib/lille_kimoce/__init__.py b/passerelle/contrib/lille_kimoce/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/passerelle/contrib/lille_kimoce/migrations/0001_initial.py b/passerelle/contrib/lille_kimoce/migrations/0001_initial.py
new file mode 100644
index 00000000..3c8194ed
--- /dev/null
+++ b/passerelle/contrib/lille_kimoce/migrations/0001_initial.py
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.20 on 2019-05-13 08:21
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    initial = True
+
+    dependencies = [
+        ('base', '0012_job'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='Kimoce',
+            fields=[
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('title', models.CharField(max_length=50, verbose_name='Title')),
+                ('description', models.TextField(verbose_name='Description')),
+                ('slug', models.SlugField(unique=True, verbose_name='Identifier')),
+                ('base_url', models.URLField(help_text='API base URL', max_length=256, verbose_name='Base URL')),
+                ('username', models.CharField(max_length=128, verbose_name='Username')),
+                ('password', models.CharField(max_length=128, verbose_name='Password')),
+                ('users', models.ManyToManyField(blank=True, to='base.ApiUser')),
+            ],
+            options={
+                'verbose_name': 'Lille Kimoce',
+            },
+        ),
+    ]
diff --git a/passerelle/contrib/lille_kimoce/migrations/__init__.py b/passerelle/contrib/lille_kimoce/migrations/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/passerelle/contrib/lille_kimoce/models.py b/passerelle/contrib/lille_kimoce/models.py
new file mode 100644
index 00000000..4b62b645
--- /dev/null
+++ b/passerelle/contrib/lille_kimoce/models.py
@@ -0,0 +1,232 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2019  Entr'ouvert
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Affero 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 Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+from six.moves.urllib_parse import urljoin
+
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+from django.core.cache import cache
+
+from passerelle.base.models import BaseResource
+from passerelle.utils.api import endpoint
+from passerelle.utils.http_authenticators import HttpBearerAuth
+from passerelle.utils.jsonresponse import APIError
+
+
+DEMAND_SCHEMA = {
+    '$schema': 'http://json-schema.org/draft-03/schema#',
+    'title': 'KIMOCE',
+    'description': '',
+    'type': 'object',
+    'properties': {
+        'category': {
+            'description': 'demand category',
+            'type': 'string',
+            'required': True
+        },
+        'type': {
+            'description': 'demand type',
+            'type': 'string',
+            'required': True
+        },
+        'subtype': {
+            'description': 'demand sub type',
+            'type': 'string',
+            'required': True
+        },
+        'form_url': {
+            'description': 'form url',
+            'type': 'string',
+            'required': True
+        },
+        'first_name': {
+            'description': 'user first name',
+            'type': 'string',
+            'required': True
+        },
+        'last_name': {
+            'description': 'user last name',
+            'type': 'string',
+            'required': True
+        },
+        'email': {
+            'description': 'user email',
+            'type': 'string',
+            'required': True
+        },
+        'priorityId': {
+            'description': 'demand priority',
+            'type': 'integer',
+            'required': False
+        },
+        'city': {
+            'description': 'demand city',
+            'type': 'string',
+            'required': False
+        },
+        'zipcode': {
+            'description': 'demand zipcode',
+            'type': 'string',
+            'required': False
+        },
+        'street_number': {
+            'description': 'demand street number',
+            'type': 'string',
+            'required': False
+        },
+        'street_name': {
+            'description': 'demand street name',
+            'type': 'string',
+            'required': False
+        },
+        'lat': {
+            'description': 'demand latitude',
+            'type': 'string',
+            'required': False
+        },
+        'lon': {
+            'description': 'demand longitude',
+            'type': 'string',
+            'required': False
+        },
+        'picture1': {
+            'description': 'first picture base64 content',
+            'type': 'any',
+            'required': False
+        },
+        'picture2': {
+            'description': 'second picture base64 content',
+            'type': 'any',
+            'required': False
+        }
+    }
+}
+
+
+class Kimoce(BaseResource):
+    base_url = models.URLField(max_length=256, blank=False,
+                               verbose_name=_('Base URL'),
+                               help_text=_('API base URL'))
+    username = models.CharField(max_length=128, verbose_name=_('Username'))
+    password = models.CharField(max_length=128, verbose_name=_('Password'))
+
+    category = _('Business Process Connectors')
+
+    class Meta:
+        verbose_name = 'Lille Kimoce'
+
+    @classmethod
+    def get_verbose_name(cls):
+        return cls._meta.verbose_name
+
+    def get_token(self, renew=False):
+        token_key = 'lille-kimoce-%s-token' % self.id
+        if not renew and cache.get(token_key):
+            return cache.get(token_key)
+        url = urljoin(self.base_url, 'login_check')
+        resp = self.requests.post(url, json={'username': self.username, 'password': self.password})
+        if not resp.ok:
+            raise APIError(resp.content)
+        token = resp.json()['token']
+        cache.set(token_key, token, 3600)
+        return token
+
+    def request(self, endpoint, params=None):
+        url = urljoin(self.base_url, endpoint)
+        data = []
+        response = self.requests.get(url, params=params,
+                            auth=HttpBearerAuth(self.get_token()))
+        if response.status_code == 401:
+            response = self.requests.get(url, params=params,
+                                auth=HttpBearerAuth(self.get_token(renew=True)))
+        if response.ok:
+            for member in response.json()['hydra:member']:
+                data.append({'id': member['@id'],
+                             'text': member['label'],
+                             'number': member['id']})
+        return {'data': data}
+
+    def check_status(self):
+        response = self.requests.get(urljoin(self.base_url, 'categories'))
+        response.raise_for_status()
+
+    @endpoint(perm='can_access', description=_('List categories'))
+    def categories(self, request, *args, **kwargs):
+        return self.request('categories')
+
+    @endpoint(perm='can_access', description=_('List types'))
+    def types(self, request, category_id):
+        return self.request('types', {'category.id': category_id})
+
+    @endpoint(perm='can_access', description=_('List subtypes'))
+    def subtypes(self, request, type_id):
+        return self.request('sub_types', {'types.id': type_id})
+
+    @endpoint(perm='can_access', description=_('List streets'))
+    def streets(self, request, q):
+        url = urljoin(self.base_url, 'company_locations')
+        data = []
+        # parentId is a flag to filter street names only
+        response = self.requests.get(url, params={'streetAddress': q, 'parentId': 0},
+                                     auth=HttpBearerAuth(self.get_token()))
+        if response.status_code == 401:
+            response = self.requests.get(url, params={'streetAddress': q, 'parentId': 0},
+                                     auth=HttpBearerAuth(self.get_token(renew=True)))
+        if response.ok:
+            for member in response.json()['hydra:member']:
+                data.append({'id': member['@id'],
+                             'text': member['streetAddress']})
+        return {'data': data}
+
+    @endpoint(perm='can_access', description=_('Create demand'), post={
+        'description': _('Create demand into KIMOCE'),
+        'request_body': {
+            'schema': {
+                'application/json': DEMAND_SCHEMA
+            }
+        }
+    })
+    def create_demand(self, request, post_data):
+        payload = {'category': post_data['category'],
+                   'type': post_data['type'],
+                   'subType': post_data['subtype'],
+                   'priorityId': post_data.get('priorityId', 3),
+                   'companyLocation': {
+                       'number': post_data.get('street_number', ''),
+                       'road': post_data.get('street_name', ''),
+                       'city': post_data.get('city', ''),
+                       'zipCode': post_data.get('zipcode', ''),
+                   },
+                   'sourceContact': {'firstname': post_data['first_name'],
+                                     'lastname': post_data['last_name'],
+                                     'mail': post_data['email']},
+                   'pictures': [],
+                   'GRUResponseLink': post_data['form_url']
+        }
+        if post_data.get('lat') and post_data.get('lon'):
+            payload['coordinate'] = {'latitude': post_data['lat'],
+                                     'longitude': post_data['lon']}
+        for param_name in ('picture1', 'picture2'):
+            if post_data.get(param_name):
+                payload['pictures'].append({'content': post_data[param_name]['content']})
+        url = urljoin(self.base_url, 'demands')
+        result = self.requests.post(url, json=payload, auth=HttpBearerAuth(self.get_token()))
+        if result.status_code  == 401:
+            result = self.requests.post(url, json=payload, auth=HttpBearerAuth(self.get_token(renew=True)))
+        if result.ok:
+            return {'demand_id': result.json()['@id']}
+        raise APIError(result.content)
diff --git a/passerelle/utils/http_authenticators.py b/passerelle/utils/http_authenticators.py
index a448572a..2249e80a 100644
--- a/passerelle/utils/http_authenticators.py
+++ b/passerelle/utils/http_authenticators.py
@@ -68,3 +68,18 @@ class HawkAuth(AuthBase):
     def __call__(self, r):
         r.headers['Authorization'] = self.get_authorization_header(r)
         return r
+
+
+class HttpBearerAuth(AuthBase):
+    def __init__(self, token):
+        self.token = token
+
+    def __eq__(self, other):
+        return self.token == getattr(other, 'token', None)
+
+    def __ne__(self, other):
+        return not self == other
+
+    def __call__(self, r):
+        r.headers['Authorization'] = 'Bearer ' + self.token
+        return r
diff --git a/tests/settings.py b/tests/settings.py
index 375daabd..80cd3f26 100644
--- a/tests/settings.py
+++ b/tests/settings.py
@@ -38,6 +38,7 @@ INSTALLED_APPS += (
     'passerelle.contrib.teamnet_axel',
     'passerelle.contrib.tlmcom',
     'passerelle.contrib.tcl',
+    'passerelle.contrib.lille_kimoce',
 )
 
 # enable applications that are otherwise disabled
diff --git a/tests/test_lille_kimoce.py b/tests/test_lille_kimoce.py
new file mode 100644
index 00000000..a024a67b
--- /dev/null
+++ b/tests/test_lille_kimoce.py
@@ -0,0 +1,591 @@
+# -*- coding: utf-8 -*-
+# Passerelle - uniform access to data and services
+# Copyright (C) 2019  Entr'ouvert
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Affero 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; exclude even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a.deepcopy of the GNU Affero General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+
+import os
+import mock
+import pytest
+import six
+
+import utils
+
+from django.core.urlresolvers import reverse
+
+from passerelle.contrib.lille_kimoce.models import Kimoce
+from passerelle.utils.jsonresponse import APIError
+
+@pytest.fixture
+def setup(db):
+    return utils.setup_access_rights(
+        Kimoce.objects.create(slug='test',
+                              base_url='https://kimoce.mairie-lille.fr/api/',
+                              username='test', password='secret'))
+
+
+CATEGORIES_RESPONSE = """{
+  "@context": "/api/contexts/Category",
+  "@id": "/api/categories",
+  "@type": "hydra:Collection",
+  "hydra:member": [
+    {"@id": "/api/categories/2",
+     "id": 2,
+     "label": "Accessoire répertorié",
+     "reference": "ARE"
+    },
+    {"@id": "/api/categories/3",
+     "id": 3,
+     "label": "Appareil photo numérique",
+     "reference": "PHT"
+    },
+    {"@id": "/api/categories/4",
+     "id": 4,
+     "label": "Ecran",
+    "reference": "ECR"
+    },
+    {"@id": "/api/categories/5",
+     "id": 5,
+     "label": "Imprimante",
+     "reference": "IMP"
+    },
+    {"@id": "/api/categories/6",
+     "id": 6,
+     "label": "Progiciel/Logiciel",
+    "reference": "PRG"
+    },
+    {"@id": "/api/categories/11",
+     "id": 11,
+     "label": "Périphérique",
+     "reference": "PHM"
+    },
+    {"@id": "/api/categories/12",
+     "id": 12,
+     "label": "Périphérique réseau",
+     "reference": "PHR"
+    },
+    {"@id": "/api/categories/13",
+     "id": 13,
+     "label": "Ordinateur Portable",
+     "reference": "POR"
+    },
+    {"@id": "/api/categories/14",
+     "id": 14,
+     "label": "Scanner",
+     "reference": "SCA"
+    },
+    {"@id": "/api/categories/15",
+     "id": 15,
+     "label": "Serveur",
+     "reference": "SER"
+    },
+    {"@id": "/api/categories/16",
+     "id": 16,
+     "label": "Station graphique",
+     "reference": "STG"
+    },
+    {"@id": "/api/categories/17",
+     "id": 17,
+     "label": "Tablet PC",
+     "reference": "TPC"
+    },
+    {"@id": "/api/categories/18",
+     "id": 18,
+     "label": "Unité centrale",
+     "reference": "UNC"
+    },
+    {"@id": "/api/categories/19",
+     "id": 19,
+     "label": "Vidéo projecteur",
+     "reference": "VPJ"
+    },
+    {"@id": "/api/categories/20",
+     "id": 20,
+     "label": "Achats",
+     "reference": "PRE"
+    },
+    {"@id": "/api/categories/21",
+     "id": 21,
+     "label": "Catalogue de Service",
+     "reference": "GEN"
+    },
+    {"@id": "/api/categories/24",
+     "id": 24,
+     "label": "GSM",
+     "reference": "GSM"
+    },
+    {"@id": "/api/categories/26",
+     "id": 26,
+     "label": "Carte SIM",
+     "reference": "SIM"
+    },
+    {"@id": "/api/categories/31",
+     "id": 31,
+     "label": "Intervention / Information",
+     "reference": "P01"
+    },
+    {"@id": "/api/categories/32",
+     "id": 32,
+     "label": "Fourniture Sel",
+     "reference": "P02"
+    },
+    {"@id": "/api/categories/33",
+     "id": 33,
+     "label": "Fourniture récipients",
+     "reference": "P03"
+    },
+    {"@id": "/api/categories/34",
+     "id": 34,
+     "label": "Fourniture support Sac",
+     "reference": "P04"
+    },
+    {"@id": "/api/categories/35",
+     "id": 35,
+     "label": "Nettoiement et Enlèvement dépôt",
+     "reference": "P05"
+    },
+    {"@id": "/api/categories/36",
+     "id": 36,
+     "label": "Brigade",
+     "reference": "P06"
+    },
+    {"@id": "/api/categories/37",
+     "id": 37,
+     "label": "Consommable Bureautique",
+     "reference": "CONSO1"
+    },
+    {"@id": "/api/categories/39",
+     "id": 39,
+     "label": "Photocopieurs",
+     "reference": "PHO"
+    },
+    {"@id": "/api/categories/40",
+     "id": 40,
+     "label": "Téléphone Fixe",
+     "reference": "TLF"
+    },
+    {"@id": "/api/categories/42",
+     "id": 42,
+     "label": "Fax",
+     "reference": "FAX"
+    },
+    {"@id": "/api/categories/43",
+     "id": 43,
+     "label": "Radio-PMR",
+     "reference": "RAD"
+    },
+    {"@id": "/api/categories/67",
+     "id": 67,
+     "label": "Consomable Réseau",
+     "reference": "CONSO2"
+    },
+    {"@id": "/api/categories/68",
+     "id": 68,
+     "label": "Monétique",
+     "reference": "TPE"
+    },
+    {"@id": "/api/categories/69",
+     "id": 69,
+     "label": "Borne tactile",
+     "reference": "BMQ"
+    },
+    {"@id": "/api/categories/70",
+     "id": 70,
+     "label": "Table surface",
+     "reference": "TBS"
+    },
+    {"@id": "/api/categories/73",
+     "id": 73,
+     "label": "Tableau Blanc Interactif",
+     "reference": "TBI"
+    },
+    {"@id": "/api/categories/76",
+     "id": 76,
+     "label": "Arbres-Espaces verts",
+     "reference": "W01"
+    },
+    {"@id": "/api/categories/77",
+     "id": 77,
+     "label": "Mobilier urbain - Jeux pour enfants",
+     "reference": "W02"
+    },
+    {"@id": "/api/categories/78",
+     "id": 78,
+     "label": "Panneau-Signalisation-Stationnement",
+     "reference": "W03"
+    },
+    {"@id": "/api/categories/79",
+     "id": 79,
+     "label": "Eclairage public",
+     "reference": "W04"
+    },
+    {"@id": "/api/categories/80",
+     "id": 80,
+     "label": "Propreté-Déchets-Tags",
+     "reference": "W05"
+    },
+    {"@id": "/api/categories/81",
+     "id": 81,
+     "label": "Etat de la voirie-Assainissement",
+     "reference": "W06"
+    },
+    {"@id": "/api/categories/82",
+     "id": 82,
+     "label": "Travaux-Chantiers-Terrasse",
+     "reference": "W07"
+    },
+    {"@id": "/api/categories/83",
+     "id": 83,
+     "label": "Mur Ecran",
+     "reference": "MUR"
+    },
+    {"@id": "/api/categories/84",
+     "id": 84,
+     "label": "Gestion de conflits en temps réel",
+     "reference": "MD1"
+    },
+    {"@id": "/api/categories/85",
+     "id": 85,
+     "label": "Gestion de conflits en temps différ",
+     "reference": "MD2"
+    }
+  ],
+  "hydra:totalItems": 44
+}"""
+
+TYPES_RESPONSE = """{
+  "@context": "/api/contexts/Type",
+  "@id": "/api/types",
+  "@type": "hydra:Collection",
+  "hydra:member": [
+    {"@id": "/api/types/1825",
+     "@type": "Type",
+     "id": 1825,
+     "label": "Tags"
+    },
+    {"@id": "/api/types/1826",
+     "@type": "Type",
+     "id": 1826,
+     "label": "Dépôt sauvage"
+    },
+    {"@id": "/api/types/1827",
+     "@type": "Type",
+     "id": 1827,
+     "label": "Poubelle, Corbeille publique de rue"
+    },
+    {"@id": "/api/types/1828",
+     "@type": "Type",
+     "id": 1828,
+     "label": "Affichage sauvage"
+    },
+    {"@id": "/api/types/1829",
+     "@type": "Type",
+     "id": 1829,
+     "label": "Rue,Espace sales,Déchets éparpillés"
+    },
+    {"@id": "/api/types/1830",
+     "@type": "Type",
+     "id": 1830,
+     "label": "Poubelle de particuliers"
+    },
+    {"@id": "/api/types/1831",
+     "@type": "Type",
+     "id": 1831,
+     "label": "Mauvaises herbes, Désherbage"
+    },
+    {"@id": "/api/types/1832",
+     "@type": "Type",
+     "id": 1832,
+     "label": "Demande exceptionelle de nettoyage"
+    },
+    {"@id": "/api/types/1875",
+     "@type": "Type",
+     "id": 1875,
+     "label": "Pigeons, Animaux"
+    },
+    {"@id": "/api/types/2013",
+     "@type": "Type",
+     "id": 2013,
+     "label": "Déjections canines"
+    },
+    {"@id": "/api/types/2014",
+     "@type": "Type",
+     "id": 2014,
+     "label": "Sol glissant"
+    },
+    {"@id": "/api/types/2015",
+     "@type": "Type",
+     "id": 2015,
+     "label": "Mauvaise odeur"
+    },
+    {"@id": "/api/types/2088",
+     "@type": "Type",
+     "id": 2088,
+     "label": "Rats"
+    }
+  ],
+  "hydra:totalItems": 13
+}"""
+
+SUB_TYPES_RESPONSE = """{
+  "@context": "/api/contexts/SubType",
+  "@id": "/api/sub_types",
+  "@type": "hydra:Collection",
+  "hydra:member": [
+    {"@id": "/api/sub_types/163",
+     "@type": "SubType",
+     "id": 163,
+     "label": "Tag classique"
+    },
+    {"@id": "/api/sub_types/164",
+     "@type": "SubType",
+     "id": 164,
+     "label": "Tag injurieux, tendancieux"
+    }
+  ],
+  "hydra:totalItems": 2
+}"""
+
+STREETS_RESPONSE = """{
+  "@context": "/api/contexts/CompanyLocation",
+  "@id": "/api/company_locations",
+  "@type": "hydra:Collection",
+  "hydra:member": [
+    {"@id": "/api/company_locations/4368",
+     "@type": "CompanyLocation",
+     "streetAddress": "PLACE JOSEPH HENTGES, H"
+    },
+    {"@id": "/api/company_locations/7550",
+     "@type": "CompanyLocation",
+     "streetAddress": "9 PLACE JOSEPH HENTGES"
+    },
+    {"@id": "/api/company_locations/7551",
+     "@type": "CompanyLocation",
+     "streetAddress": "5 PLACE JOSEPH HENTGES"
+    },
+    {"@id": "/api/company_locations/7552",
+     "@type": "CompanyLocation",
+     "streetAddress": "3 PLACE JOSEPH HENTGES"
+    },
+    {"@id": "/api/company_locations/7553",
+     "@type": "CompanyLocation",
+     "streetAddress": "1 PLACE JOSEPH HENTGES"
+    },
+    {"@id": "/api/company_locations/7554",
+     "@type": "CompanyLocation",
+     "streetAddress": "7 PLACE JOSEPH HENTGES"
+    },
+    {"@id": "/api/company_locations/7555",
+     "@type": "CompanyLocation",
+     "streetAddress": "181 PLACE JOSEPH HENTGES"
+    },
+    {"@id": "/api/company_locations/46455",
+     "@type": "CompanyLocation",
+     "streetAddress": "2 PLACE JOSEPH HENTGES"
+    },
+    {"@id": "/api/company_locations/46456",
+     "@type": "CompanyLocation",
+     "streetAddress": "25 PLACE JOSEPH HENTGES"
+    }
+  ],
+  "hydra:totalItems": 9
+}"""
+
+DEMAND_CREATION_RESPONSE = """{
+    "@context": "/api/contexts/Demand",
+    "@id": "/api/demands/166961",
+    "@type": "Demand",
+    "category": "/api/categories/33",
+    "coordinate": {
+        "latitude": 3.50895,
+        "longitude": 50.340892
+    },
+    "pictures": [],
+    "priorityId": 3,
+    "subType": "/api/sub_types/17",
+    "type": "/api/types/916",
+    "sourceContact": {
+        "firstname": "Adrien",
+        "lastname": "MILLOT",
+        "mail": "adrien.millot@smile.fr"
+    },
+    "companyLocation": {
+        "@id": "/api/company_locations/3656",
+        "@type": "CompanyLocation",
+        "city": "lille",
+        "number": "55 bis",
+        "road": "squares du portugal",
+        "zipCode": "59000"
+    }
+}"""
+
+TOKEN_RESPONSE = """{"token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9"}"""
+
+TOKEN_ERROR_RESPONSE = """{"message": "Bad credentials"}"""
+
+@mock.patch('passerelle.utils.Request.post')
+def test_get_token(mocked_post, app, setup):
+    with pytest.raises(APIError):
+        mocked_post.return_value = utils.FakedResponse(content=TOKEN_ERROR_RESPONSE, ok=False)
+        setup.get_token()
+    mocked_post.return_value = utils.FakedResponse(content=TOKEN_RESPONSE, ok=True)
+    setup.get_token()
+    assert mocked_post.call_count == 2
+    print mocked_post.call_args
+    assert "api/login_check" in mocked_post.call_args[0][0]
+    assert mocked_post.call_args[1]['json']['username'] == 'test'
+    assert mocked_post.call_args[1]['json']['password'] == 'secret'
+    # make sure the token from cache is used
+    setup.get_token()
+    assert mocked_post.call_count == 2
+    setup.get_token(True)
+    assert mocked_post.call_count == 3
+
+@mock.patch('passerelle.utils.Request.post')
+@mock.patch('passerelle.utils.Request.get')
+def test_get_categories(mocked_get, mocked_post, app, setup):
+    mocked_post.return_value = utils.FakedResponse(content=TOKEN_RESPONSE, ok=True)
+    mocked_get.return_value = utils.FakedResponse(ok=True, content=CATEGORIES_RESPONSE)
+    endpoint = reverse(
+        'generic-endpoint',
+        kwargs={
+            'connector': 'lille-kimoce',
+            'slug': setup.slug,
+            'endpoint': 'categories'
+        }
+    )
+    response = app.get(endpoint)
+    assert 'data' in response.json
+    for item in response.json['data']:
+        assert 'id' in item
+        assert 'text' in item
+        assert 'number' in item
+
+
+@mock.patch('passerelle.utils.Request.post')
+@mock.patch('passerelle.utils.Request.get')
+def test_get_types(mocked_get, mocked_post, app, setup):
+    mocked_post.return_value = utils.FakedResponse(content=TOKEN_RESPONSE, ok=True)
+    mocked_get.return_value = utils.FakedResponse(ok=True, content=TYPES_RESPONSE)
+    endpoint = reverse('generic-endpoint',
+        kwargs={'connector': 'lille-kimoce',
+                'slug': setup.slug,
+                'endpoint': 'types'
+        }
+    )
+    response = app.get(endpoint, status=400)
+    assert response.json['err']
+    assert response.json['err_desc'] == 'missing parameters: \'category_id\'.'
+    assert response.json['err_class'] == 'passerelle.views.WrongParameter'
+    response = app.get(endpoint, params={'category_id': 80})
+    assert 'data' in response.json
+    for item in response.json['data']:
+        assert 'id' in item
+        assert 'text' in item
+        assert 'number' in item
+
+
+@mock.patch('passerelle.utils.Request.post')
+@mock.patch('passerelle.utils.Request.get')
+def test_get_sub_types(mocked_get, mocked_post, app, setup):
+    mocked_post.return_value = utils.FakedResponse(content=TOKEN_RESPONSE, ok=True)
+    mocked_get.return_value = utils.FakedResponse(ok=True, content=SUB_TYPES_RESPONSE)
+    endpoint = reverse('generic-endpoint',
+        kwargs={'connector': 'lille-kimoce',
+                'slug': setup.slug,
+                'endpoint': 'subtypes'
+        }
+    )
+    response = app.get(endpoint, status=400)
+    assert response.json['err']
+    assert response.json['err_desc'] == 'missing parameters: \'type_id\'.'
+    assert response.json['err_class'] == 'passerelle.views.WrongParameter'
+    response = app.get(endpoint, params={'type_id': 1825})
+    assert 'data' in response.json
+    for item in response.json['data']:
+        assert 'id' in item
+        assert 'text' in item
+        assert 'number' in item
+
+
+@mock.patch('passerelle.utils.Request.post')
+@mock.patch('passerelle.utils.Request.get')
+def test_get_streets(mocked_get, mocked_post, app, setup):
+    mocked_post.return_value = utils.FakedResponse(content=TOKEN_RESPONSE, ok=True)
+    mocked_get.return_value = utils.FakedResponse(ok=True, content=STREETS_RESPONSE)
+    endpoint = reverse('generic-endpoint',
+        kwargs={'connector': 'lille-kimoce',
+                'slug': setup.slug,
+                'endpoint': 'streets'
+        }
+    )
+    response = app.get(endpoint, status=400)
+    assert response.json['err']
+    assert response.json['err_desc'] == 'missing parameters: \'q\'.'
+    assert response.json['err_class'] == 'passerelle.views.WrongParameter'
+    response = app.get(endpoint, params={'q': 'PLACE JosEPH'})
+    assert 'data' in response.json
+    for item in response.json['data']:
+        assert 'id' in item
+        assert 'text' in item
+
+
+@mock.patch('passerelle.utils.Request.post')
+def test_create_demand(mocked_post, app, setup):
+    mocked_post.side_effect = [
+        utils.FakedResponse(content=TOKEN_RESPONSE, ok=True),
+        utils.FakedResponse(ok=True, content=DEMAND_CREATION_RESPONSE)]
+    data = {
+        'category': '/api/categories/80',
+        'type': '/api/types/1825',
+        'subtype': '/api/sub_types/164',
+        'form_url': 'http://example.com/form/1/',
+        'first_name': 'Foo',
+        'last_name': 'Bar',
+        'email': 'foo@example.net',
+    }
+    endpoint = reverse('generic-endpoint',
+        kwargs={'connector': 'lille-kimoce',
+                'slug': setup.slug,
+                'endpoint': 'create_demand'
+        }
+    )
+    response = app.post_json(endpoint, params=data)
+    assert mocked_post.call_args[0][0] == 'https://kimoce.mairie-lille.fr/api/demands'
+    assert mocked_post.call_args[1]['json'] == {'GRUResponseLink': 'http://example.com/form/1/',
+                    'priorityId': 3, 'type': '/api/types/1825',
+                                               'category': '/api/categories/80',
+                    'companyLocation': {'number': '',
+                                'road': '',
+                                'zipCode': '',
+                                'city': ''},
+                    'pictures': [],
+                    'subType': '/api/sub_types/164',
+                    'sourceContact': {'firstname': 'Foo',
+                                      'mail': 'foo@example.net',
+                                      'lastname': u'Bar'}
+    }
+    assert response.json['demand_id'] == '/api/demands/166961'
+    assert mocked_post.call_count == 2
+    data['picture1'] = {'content': 'base64encoded_picture'}
+    data['lat'] = '48.85438994604021'
+    data['lon'] = '2.3497223854064946'
+    mocked_post.return_value = utils.FakedResponse(ok=True, content=DEMAND_CREATION_RESPONSE)
+    mocked_post.side_effect = None
+    response = app.post_json(endpoint, params=data)
+    assert mocked_post.call_count == 3
+    assert mocked_post.call_args[1]['json']['pictures'][0]['content'] == 'base64encoded_picture'
+    assert mocked_post.call_args[1]['json']['coordinate']['latitude'] == '48.85438994604021'
+    assert mocked_post.call_args[1]['json']['coordinate']['longitude'] == '2.3497223854064946'
-- 
2.20.1

