From 771b1f3c9e5e6422c182cc992264fb621cc08e73 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Laur=C3=A9line=20Gu=C3=A9rin?= <zebuline@entrouvert.com>
Date: Fri, 11 Oct 2019 14:24:00 +0200
Subject: [PATCH 2/3] base resource: add automatic 'up' endpoint (#21978)

if method 'check_status' is defined, provide a 'up' endpoint
---
 passerelle/base/models.py      | 13 ++++++
 passerelle/views.py            |  3 ++
 tests/test_generic_endpoint.py | 74 ++++++++++++++++++++++++++++++++++
 3 files changed, 90 insertions(+)

diff --git a/passerelle/base/models.py b/passerelle/base/models.py
index f797de85..debeaf50 100644
--- a/passerelle/base/models.py
+++ b/passerelle/base/models.py
@@ -19,6 +19,7 @@ from django.db.models import Q
 from django.test import override_settings
 from django.utils.text import slugify
 from django.utils import timezone, six
+from django.utils.six.moves.urllib.parse import urlparse
 from django.utils.translation import ugettext_lazy as _
 from django.utils.timezone import now
 from django.core.files.base import ContentFile
@@ -34,6 +35,8 @@ import jsonfield
 
 import passerelle
 import requests
+from passerelle.utils.api import endpoint
+from passerelle.utils.jsonresponse import APIError
 
 KEYTYPE_CHOICES = (
     ('API', _('API Key')),
@@ -231,6 +234,10 @@ class BaseResource(models.Model):
         for name, method in inspect.getmembers(self, predicate=inspect.ismethod):
             if hasattr(method, 'endpoint_info'):
                 method.endpoint_info.object = self
+                endpoint_name = method.endpoint_info.name
+                if endpoint_name == 'up' and hasattr(self.check_status, 'not_implemented'):
+                    # hide automatic up endpoint if check_status method is not implemented
+                    continue
                 for http_method in method.endpoint_info.methods:
                     # duplicate information to give each method its own entry
                     endpoint_info = copy.copy(method.endpoint_info)
@@ -260,6 +267,12 @@ class BaseResource(models.Model):
         status = self.get_availability_status()
         return (status and status.down())
 
+    @endpoint(description=_('Check service availability'))
+    def up(self, request, **kwargs):
+        if self.down():
+            raise APIError('service not available')
+        return {'err': 0}
+
     def export_json(self):
         d = {
             '@type': 'passerelle-resource',
diff --git a/passerelle/views.py b/passerelle/views.py
index 17c8a2ad..7295afeb 100644
--- a/passerelle/views.py
+++ b/passerelle/views.py
@@ -364,6 +364,9 @@ class GenericEndpointView(GenericConnectorMixin, SingleObjectMixin, View):
                 self.endpoint = method
         if not self.endpoint:
             raise Http404()
+        if kwargs.get('endpoint') == 'up' and hasattr(connector.check_status, 'not_implemented'):
+            # hide automatic up endpoint if check_status method is not implemented
+            raise Http404()
         return super(GenericEndpointView, self).dispatch(request, *args, **kwargs)
 
     def _allowed_methods(self):
diff --git a/tests/test_generic_endpoint.py b/tests/test_generic_endpoint.py
index 978c0107..c841b3a1 100644
--- a/tests/test_generic_endpoint.py
+++ b/tests/test_generic_endpoint.py
@@ -27,8 +27,11 @@ import pytest
 
 import utils
 
+from django.core.urlresolvers import reverse
+
 from passerelle.apps.arcgis.models import ArcGIS
 from passerelle.base.models import ResourceLog, ProxyLogger, BaseResource, HTTPResource
+from passerelle.base.models import ResourceStatus
 from passerelle.apps.mdel.models import MDEL
 from passerelle.contrib.stub_invoices.models import StubInvoicesConnector
 from passerelle.utils.api import endpoint
@@ -403,3 +406,74 @@ def test_endpoint_typed_params(app, db, monkeypatch):
     json_res = app.get('/stub-invoices/fake/httpcall?floating=', status=400).json
     assert json_res['err'] == 1
     assert json_res['err_desc'] == 'invalid value for parameter "floating"'
+
+
+class DummyConnectorBase(BaseResource):
+    def get_availability_status(self):
+        # naive get_availability_status method for testing
+        try:
+            self.check_status()
+        except Exception:
+            return ResourceStatus(status='down')
+        return ResourceStatus(status='up')
+
+    class Meta:
+        app_label = 'dummy'
+        abstract = True
+
+
+class DummyConnectorWithCheckStatus(DummyConnectorBase):
+    def check_status(self):
+        return
+
+
+class DummyConnectorWithCheckStatusFailure(DummyConnectorBase):
+    def check_status(self):
+        raise Exception('dummy reason')
+
+
+class DummyConnectorWithoutCheckStatus(DummyConnectorBase):
+    pass
+
+
+@pytest.mark.parametrize('connector_class, expected_status, expected_response', [
+    (DummyConnectorWithCheckStatus, 200, {'err': 0}),
+    (DummyConnectorWithCheckStatusFailure, 200,
+     {'err_class': 'passerelle.utils.jsonresponse.APIError',
+      'err_desc': 'service not available', 'data': None, 'err': 1}),
+    (DummyConnectorWithoutCheckStatus, 404, None),
+])
+def test_generic_up_endpoint(db, app, connector_class, expected_status, expected_response):
+    connector = connector_class()
+    connector.id = 42
+
+    url = reverse('generic-endpoint', kwargs={
+        'connector': 'foo',
+        'slug': 'foo',
+        'endpoint': 'up',
+    })
+    patch_init = mock.patch('passerelle.views.GenericConnectorMixin.init_stuff')
+    patch_object = mock.patch('passerelle.views.GenericEndpointView.get_object',
+                              return_value=connector)
+    with patch_init, patch_object:
+        response = app.get(url, status=expected_status)
+    if expected_response is not None:
+        assert response.json == expected_response
+
+
+@pytest.mark.parametrize('connector_class, expected', [
+    (DummyConnectorWithCheckStatus, True),
+    (DummyConnectorWithCheckStatusFailure, True),
+    (DummyConnectorWithoutCheckStatus, False),
+])
+def test_generic_up_in_endpoints_infos(db, app, connector_class, expected):
+    connector = connector_class()
+    connector.id = 42
+
+    up_endpoints = [
+        ep for ep in connector.get_endpoints_infos()
+        if ep.name == 'up']
+    if expected:
+        assert len(up_endpoints) == 1
+    else:
+        assert up_endpoints == []
-- 
2.20.1

