diff --git a/passerelle/apps/ovh/migrations/0010_auto_20201008_1126.py b/passerelle/apps/ovh/migrations/0010_auto_20201008_1126.py new file mode 100644 index 00000000..a354557d --- /dev/null +++ b/passerelle/apps/ovh/migrations/0010_auto_20201008_1126.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.18 on 2020-10-08 09:26 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ovh', '0009_auto_20200730_1047'), + ] + + operations = [ + migrations.AlterField( + model_name='ovhsmsgateway', + name='credit_left', + field=models.PositiveIntegerField(default=0, editable=False, verbose_name='Credit left'), + ), + ] diff --git a/passerelle/apps/ovh/models.py b/passerelle/apps/ovh/models.py index 5849f6af..8f31d2aa 100644 --- a/passerelle/apps/ovh/models.py +++ b/passerelle/apps/ovh/models.py @@ -2,6 +2,7 @@ import hashlib import json import requests import time +from urllib.parse import urljoin from django.db import models from django.utils.encoding import force_text @@ -13,8 +14,8 @@ from passerelle.utils.jsonresponse import APIError class OVHSMSGateway(SMSResource): documentation_url = 'https://doc-publik.entrouvert.com/admin-fonctionnel/les-tutos/configuration-envoi-sms/' - hide_description_fields = ['account'] - API_URL = 'https://eu.api.ovh.com/1.0/sms/%(serviceName)s/users/%(login)s/jobs/' + hide_description_fields = ['account', 'credit_left'] + API_URL = 'https://eu.api.ovh.com/1.0/sms/%(serviceName)s/' URL = 'https://www.ovh.com/cgi-bin/sms/http2sms.cgi' MESSAGES_CLASSES = ( (0, _('Message are directly shown to users on phone screen ' @@ -53,23 +54,24 @@ class OVHSMSGateway(SMSResource): ) username = models.CharField( - verbose_name=_('Username'), + verbose_name=_('Username (deprecated)'), max_length=64, - help_text=_('API user created on the SMS account.'), + help_text=_('API user created on the SMS account. This field is obsolete once keys and secret ' + 'fields above are filled.'), ) password = models.CharField( verbose_name=_('Password (deprecated)'), max_length=64, blank=True, help_text=_( - 'Password for legacy API. This field is obsolete once keys and secret fields below are filled.' + 'Password for legacy API. This field is obsolete once keys and secret fields above are filled.' ), ) msg_class = models.IntegerField(choices=MESSAGES_CLASSES, default=1, verbose_name=_('Message class')) credit_threshold_alert = models.PositiveIntegerField(verbose_name=_('Credit alert threshold'), default=100) - credit_left = models.PositiveIntegerField(verbose_name=_('Credit left'), default=0) + credit_left = models.PositiveIntegerField(verbose_name=_('Credit left'), default=0, editable=False) TEST_DEFAULTS = { 'create_kwargs': { @@ -114,24 +116,19 @@ class OVHSMSGateway(SMSResource): verbose_name = 'OVH' db_table = 'sms_ovh' - def send_msg(self, text, sender, destinations, **kwargs): - if not (self.application_key and self.consumer_key and self.application_secret): - return self.send_msg_legacy(text, sender, destinations, **kwargs) + @property + def uses_new_api(self): + return self.application_key and self.consumer_key and self.application_secret + def request(self, method, endpoint, **kwargs): url = self.API_URL % {'serviceName': self.account, 'login': self.username} - body = { - 'sender': sender, - 'receivers': destinations, - 'message': text, - 'class': self.NEW_MESSAGES_CLASSES[self.msg_class], - } - if not kwargs['stop']: - body.update({'noStopClause': True}) + url = urljoin(url, endpoint) # sign request + body = json.dumps(kwargs['json']) if 'json' in kwargs else '' now = str(int(time.time())) signature = hashlib.sha1() - to_sign = "+".join((self.application_secret, self.consumer_key, 'POST', url, json.dumps(body), now)) + to_sign = "+".join((self.application_secret, self.consumer_key, method.upper(), url, body, now)) signature.update(to_sign.encode()) headers = { @@ -142,7 +139,7 @@ class OVHSMSGateway(SMSResource): } try: - response = self.requests.post(url, headers=headers, json=body) + response = self.requests.request(method, url, headers=headers, **kwargs) except requests.RequestException as e: raise APIError('OVH error: POST failed, %s' % e) else: @@ -154,6 +151,22 @@ class OVHSMSGateway(SMSResource): response.raise_for_status() except requests.RequestException as e: raise APIError('OVH error: %s "%s"' % (e, result)) + return result + + def send_msg(self, text, sender, destinations, **kwargs): + if not self.uses_new_api: + return self.send_msg_legacy(text, sender, destinations, **kwargs) + + body = { + 'sender': sender, + 'receivers': destinations, + 'message': text, + 'class': self.NEW_MESSAGES_CLASSES[self.msg_class], + } + if not kwargs['stop']: + body.update({'noStopClause': True}) + + result = self.request('post', 'jobs/', json=body) ret = {} credits_removed = result['totalCreditsRemoved'] @@ -161,7 +174,7 @@ class OVHSMSGateway(SMSResource): self.credit_left -= credits_removed if self.credit_left < 0: self.credit_left = 0 - self.save() + self.save(update_credit=False) if self.credit_left < self.credit_threshold_alert: ret['warning'] = 'credit level too low for %s: %s (threshold %s)' % ( self.slug, @@ -174,6 +187,22 @@ class OVHSMSGateway(SMSResource): return ret + def update_credit_left(self): + if not self.uses_new_api: + return + result = self.request('get', endpoint='') + self.credit_left = result['creditsLeft'] + self.save(update_credit=False) + + def hourly(self): + super().hourly() + self.update_credit_left() + + def save(self, *args, update_credit=True, **kwargs): + super().save(*args, **kwargs) + if update_credit: + self.add_job('update_credit_left') + def send_msg_legacy(self, text, sender, destinations, **kwargs): """Send a SMS using the HTTP2 endpoint""" if not self.password: diff --git a/passerelle/apps/ovh/templates/ovh/ovhsmsgateway_detail.html b/passerelle/apps/ovh/templates/ovh/ovhsmsgateway_detail.html new file mode 100644 index 00000000..926dfdf8 --- /dev/null +++ b/passerelle/apps/ovh/templates/ovh/ovhsmsgateway_detail.html @@ -0,0 +1,15 @@ +{% extends "passerelle/manage/messages_service_view.html" %} +{% load i18n passerelle %} + +{% block description %} +{{ block.super }} +{% if object.uses_new_api %} +

+{% if object.credit_left %} +{% trans "Credit left:" %} {{ object.credit_left }} +{% else %} +{% trans "There is no credit left." %} +{% endif %} +

+{% endif %} +{% endblock %} diff --git a/tests/test_sms.py b/tests/test_sms.py index dcb68b4e..593fb46b 100644 --- a/tests/test_sms.py +++ b/tests/test_sms.py @@ -69,14 +69,14 @@ def test_connectors(app, connector, freezer): test_vectors = getattr(connector, 'TEST_DEFAULTS', {}).get('test_vectors', []) total = len(test_vectors) nb_failed = 0 - assert Job.objects.count() == 0 + assert Job.objects.filter(method_name='send_job').count() == 0 for test_vector in test_vectors: # register job freezer.move_to('2019-01-01 00:00:00') result = app.post_json(path, params=payload) assert result.json['err'] == 0 - job_id = Job.objects.get(status='registered').id + job_id = Job.objects.get(method_name='send_job', status='registered').id # perform job freezer.move_to('2019-01-01 01:00:03') @@ -92,7 +92,7 @@ def test_connectors(app, connector, freezer): nb_failed += 1 else: assert job.status == 'completed' - assert Job.objects.count() == total + assert Job.objects.filter(method_name='send_job').count() == total assert SMSLog.objects.count() == total - nb_failed @@ -186,7 +186,7 @@ def test_sms_nostop_parameter(app, connector): def test_ovh_new_api(app, freezer): connector = OVHSMSGateway.objects.create( - slug='ovh', account='sms-test42', username='john', + slug='ovh', account='sms-test42', application_key='RHrTdU2oTsrVC0pu', application_secret='CLjtS69tTcPgCKxedeoZlgMSoQGSiXMa', consumer_key='iF0zi0MJrbjNcI3hvuvwkhNk8skrigxz' @@ -208,7 +208,7 @@ def test_ovh_new_api(app, freezer): path = '/%s/%s/send/' % (connector.get_connector_slug(), connector.slug) result = app.post_json(path, params=payload) assert result.json['err'] == 0 - job_id = Job.objects.get(status='registered').id + job_id = Job.objects.get(method_name='send_job', status='registered').id # perform job freezer.move_to('2019-01-01 01:00:03') @@ -218,7 +218,8 @@ def test_ovh_new_api(app, freezer): 'ids': [241615100], 'invalidReceivers': [] } - url = connector.API_URL % {'serviceName': 'sms-test42', 'login': 'john'} + base_url = connector.API_URL % {'serviceName': 'sms-test42'} + url = base_url + 'jobs/' with utils.mock_url(url, resp, 200) as mocked: connector.jobs() job = Job.objects.get(id=job_id) @@ -255,3 +256,36 @@ def test_sms_test_send(admin_user, app, connector): } assert resp.status_code == 302 assert resp.location == url + + +def test_ovh_new_api_credit(app, freezer): + connector = OVHSMSGateway.objects.create( + slug='ovh', account='sms-test42', + application_key='RHrTdU2oTsrVC0pu', + application_secret='CLjtS69tTcPgCKxedeoZlgMSoQGSiXMa', + consumer_key='iF0zi0MJrbjNcI3hvuvwkhNk8skrigxz' + ) + + manager_url = '/%s/%s/' % (connector.get_connector_slug(), connector.slug) + resp = app.get(manager_url) + assert 'no credit left' in resp.text + + # a job to update credit was added on connector creation + resp = { + 'creditsLeft': 123, + } + ovh_url = connector.API_URL % {'serviceName': 'sms-test42'} + with utils.mock_url(ovh_url, resp, 200) as mocked: + connector.jobs() + assert connector.credit_left == 123 + + resp = app.get(manager_url) + assert '123' in resp.text + + # hourly update + resp = { + 'creditsLeft': 456, + } + with utils.mock_url(ovh_url, resp, 200) as mocked: + connector.hourly() + assert connector.credit_left == 456