From 77abd3c4bcdc4d45d1f182e3c3ae89874081a010 Mon Sep 17 00:00:00 2001
From: Nicolas ROCHE <nroche@entrouvert.com>
Date: Fri, 14 Aug 2020 19:09:59 +0200
Subject: [PATCH] api: add api to request jobs status (#43278)

---
 passerelle/api_urls.py  | 23 +++++++++++
 passerelle/api_views.py | 36 +++++++++++++++++
 passerelle/urls.py      |  3 ++
 tests/test_api.py       | 86 +++++++++++++++++++++++++++++++++++++++++
 4 files changed, 148 insertions(+)
 create mode 100644 passerelle/api_urls.py
 create mode 100644 passerelle/api_views.py
 create mode 100644 tests/test_api.py

diff --git a/passerelle/api_urls.py b/passerelle/api_urls.py
new file mode 100644
index 00000000..abbe6867
--- /dev/null
+++ b/passerelle/api_urls.py
@@ -0,0 +1,23 @@
+# passerelle - uniform access to multiple data sources and services
+# Copyright (C) 2020  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 django.conf.urls import url
+
+from .api_views import JobView
+
+api_urlpatterns = [
+    url(r'jobs/(?P<pk>[\w,-]+)/$', JobView.as_view(), name='api-job'),
+]
diff --git a/passerelle/api_views.py b/passerelle/api_views.py
new file mode 100644
index 00000000..0082f97f
--- /dev/null
+++ b/passerelle/api_views.py
@@ -0,0 +1,36 @@
+# passerelle - uniform access to multiple data sources and services
+# Copyright (C) 2020  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 django.http import JsonResponse
+from django.views.generic import DetailView
+
+from passerelle.base.models import Job
+
+class JobView(DetailView):
+    model = Job
+
+    def get(self, *args, **kwargs):
+        job = self.get_object()
+        data = {
+            'id': job.id,
+            'resource': job.resource.__class__.__name__,
+            'parameters': job.parameters,
+            'status': job.status,
+            'status_details': job.status_details,
+            'update_timestamp': job.update_timestamp,
+            'done_timestamp': job.done_timestamp,
+        }
+        return JsonResponse({'err': 0, 'data': data})
diff --git a/passerelle/urls.py b/passerelle/urls.py
index 8d45e06a..85c2bdb5 100644
--- a/passerelle/urls.py
+++ b/passerelle/urls.py
@@ -2,16 +2,17 @@ from django.conf import settings
 from django.conf.urls import include, url
 
 from django.contrib import admin
 
 from django.contrib.auth.decorators import login_required
 from django.contrib.staticfiles.urls import staticfiles_urlpatterns
 from django.views.static import serve as static_serve
 
+from .api_urls import api_urlpatterns
 from .views import (
     HomePageView, ManageView, ManageAddView,
     GenericCreateConnectorView, GenericDeleteConnectorView,
     GenericEditConnectorView, GenericEndpointView, GenericConnectorView,
     GenericViewLogsConnectorView, GenericLogView, GenericExportConnectorView,
     login, logout, menu_json)
 from .base.views import GenericViewJobsConnectorView, GenericJobView, GenericRestartJobView
 from .urls_utils import decorated_includes, manager_required
@@ -32,16 +33,18 @@ urlpatterns = [
         'document_root': settings.MEDIA_ROOT,
     }),
     url(r'^admin/', admin.site.urls),
 
     url(r'^manage/access/',
         decorated_includes(manager_required, include(access_urlpatterns))),
     url(r'^manage/',
         decorated_includes(manager_required, include(import_export_urlpatterns))),
+    url('^api/',
+        decorated_includes(manager_required, include(api_urlpatterns))),
 ]
 
 # add patterns from apps
 urlpatterns = register_apps_urls(urlpatterns)
 
 # add authentication patterns
 urlpatterns += [
     url(r'^logout/$', logout, name='logout'),
diff --git a/tests/test_api.py b/tests/test_api.py
new file mode 100644
index 00000000..aac15053
--- /dev/null
+++ b/tests/test_api.py
@@ -0,0 +1,86 @@
+# passerelle - uniform access to multiple data sources and services
+# Copyright (C) 2020  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/>.
+
+import mock
+import pytest
+
+from django.core.urlresolvers import reverse
+
+from passerelle.apps.orange.models import OrangeSMSGateway
+from passerelle.base.models import Job
+
+from test_manager import login, admin_user
+
+
+pytestmark = pytest.mark.django_db
+
+
+@pytest.fixture
+def connector(db):
+    return OrangeSMSGateway.objects.create(
+        slug='my_connector',
+        username='jdoe',
+        password='secret',
+        groupname='group2'
+    )
+
+
+@mock.patch('passerelle.sms.models.SMSResource.send_job')
+def test_api_jobs(mocked_send_job, admin_user, app, connector):
+    assert Job.objects.count() == 0
+
+    url = reverse('api-job', kwargs={'pk': 1})
+    resp = app.get(url, status=302)
+    assert resp.location == '/login/?next=%s' % url
+
+    app = login(app)
+    assert app.get(url, status=404)
+
+    # registered job
+    job = connector.add_job('send_job', my_parameter='my_value')
+    url = reverse('api-job', kwargs={'pk': job.id})
+    assert Job.objects.count() == 1
+    resp = app.get(url, status=200)
+    assert not resp.json['err']
+    assert resp.json['data']['id'] == job.id
+    assert resp.json['data']['resource'] == 'OrangeSMSGateway'
+    assert resp.json['data']['parameters'] == {'my_parameter': 'my_value'}
+    assert resp.json['data']['status'] == 'registered'
+    assert resp.json['data']['done_timestamp'] is None
+    update_timestamp1 = resp.json['data']['update_timestamp']
+
+    # completed job
+    connector.jobs()
+    assert mocked_send_job.call_args == mock.call(my_parameter='my_value')
+    resp = app.get(url, status=200)
+    assert not resp.json['err']
+    assert resp.json['data']['id'] == job.id
+    assert resp.json['data']['status'] == 'completed'
+    assert resp.json['data']['done_timestamp'] is not None
+    resp.json['data']['update_timestamp'] < update_timestamp1
+
+    # failed job
+    job = connector.add_job('send_job')
+    assert Job.objects.count() == 2
+    mocked_send_job.side_effect = Exception('my error message')
+    connector.jobs()
+    url = reverse('api-job', kwargs={'pk': job.id})
+    resp = app.get(url, status=200)
+    assert not resp.json['err']
+    assert resp.json['data']['id'] == job.id
+    assert resp.json['data']['status'] == 'failed'
+    assert resp.json['data']['status_details'] == {'error_summary': 'Exception: my error message'}
+    assert resp.json['data']['done_timestamp'] is not None
-- 
2.28.0

