From 37561a16d0211c5d173bb1485bf550c6b81946a5 Mon Sep 17 00:00:00 2001
From: Nicolas Roche <nroche@narval.fr.eu.org>
Date: Sun, 5 May 2019 23:49:10 +0200
Subject: [PATCH 1/2] matomo: simulate first tracking visit (#32796)

---
 hobo/matomo/utils.py       | 19 +++++++++-
 tests/test_matomo_utils.py | 74 +++++++++++++++++++++++++++++++++-----
 tests/test_matomo_views.py | 59 ++++++++++++++++--------------
 3 files changed, 115 insertions(+), 37 deletions(-)

diff --git a/hobo/matomo/utils.py b/hobo/matomo/utils.py
index 220a6c0..24c7cac 100644
--- a/hobo/matomo/utils.py
+++ b/hobo/matomo/utils.py
@@ -13,7 +13,6 @@
 #
 # 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 hashlib
 import re
 import requests
@@ -215,6 +214,23 @@ class MatomoWS(object):
             raise MatomoException('get_javascript_tag fails')
         return tag.text
 
+    def ping(self, id_site):
+        """this function use a different matomo's webservice API"""
+        url = "%s/matomo.php" % self.url_ws_base
+        data = '{"requests":["?idsite=%s&action_name=ping&rec=1"]}' % id_site
+        resp = requests.post(url, data=data)
+        tree = resp.json()
+        if not isinstance(tree, dict):
+            raise MatomoException(
+                'internal error on ping (JSON expected): %s' % resp.content)
+        if 'status' not in tree:
+            raise MatomoException(
+                'internal error on ping (status expected): %s' % resp.content)
+        if tree['status'] != 'success':
+            raise MatomoError('ping fails: %s' % resp.content)
+        return True
+
+
 def upgrade_site(matomo, tenant_name, site_urls):
     try:
         # tenant name match because it is the basename of one of registered urls
@@ -293,6 +309,7 @@ def auto_configure_matomo():
     id_site = upgrade_site(matomo, tenant_name, site_urls)
     logme_url = upgrade_user(matomo, tenant_name, id_site)
     tracking_js = upgrade_javascript_tag(matomo, id_site)
+    matomo.ping(id_site)
 
     # save matomo's variables
     logme_url_var = get_variable('matomo_logme_url')
diff --git a/tests/test_matomo_utils.py b/tests/test_matomo_utils.py
index 9a29c67..cbb60d9 100644
--- a/tests/test_matomo_utils.py
+++ b/tests/test_matomo_utils.py
@@ -1,5 +1,6 @@
 # -*- coding: utf-8 -*-
 
+import json
 import mock
 import pytest
 from requests import Response
@@ -161,12 +162,23 @@ JAVASCRIPT_TAG_BAD_RESPONSE = """<?xml version="1.0" encoding="utf-8" ?>
 <no_result_tag/>
 """
 
+PING_SUCCESS = '{"status":"success","tracked":1,"invalid":0}'
+PING_ERROR = '{"status":"not success"}'
+PING_NOSTATUS_ERROR = '{}'
+PING_NOJSON_ERROR = 'not json'
+
 def requests_post_mocked_replies(contents):
     """buid an iterator for mock's side_effect parameter"""
     responses = []
     for content in contents:
         response = Response()
-        response._content = content
+
+        # response may be XML or JSON
+        if content[0] == '{':
+            response.json = mock.MagicMock(return_value = json.loads(content))
+        else:
+            response._content = content
+
         response.status_code = 200
         responses.append(response)
     return responses
@@ -498,6 +510,43 @@ def test_get_javascript_tag(mocked_post):
         else:
             assert False
 
+@mock.patch('requests.post')
+def test_ping(mocked_post):
+    """webservice to simulate a first tracking call"""
+    with override_settings(MATOMO_SERVER=CONFIG):
+        matomo = MatomoWS()
+        response = Response()
+
+        # success
+        content = PING_SUCCESS
+        response.json = mock.MagicMock(return_value=json.loads(content))
+        mocked_post.return_value = response
+        matomo.ping('42')
+        assert True
+
+        # error
+        content = PING_ERROR
+        response.json = mock.MagicMock(return_value=json.loads(content))
+        mocked_post.return_value = response
+        with pytest.raises(MatomoError, match='ping fails'):
+            matomo.ping('42')
+
+        # failure (no status)
+        content = PING_NOSTATUS_ERROR
+        response.json = mock.MagicMock(return_value=json.loads(content))
+        mocked_post.return_value = response
+        with pytest.raises(MatomoException,
+                           match='internal error on ping \(status expected\)'):
+            matomo.ping('42')
+
+        # failure (no JSON)
+        content = PING_NOJSON_ERROR
+        response.json = mock.MagicMock(return_value=content)
+        mocked_post.return_value = response
+        with pytest.raises(MatomoException,
+                           match='internal error on ping \(JSON expected\)'):
+            matomo.ping('42')
+
 @mock.patch('requests.post')
 def test_upgrade_site(mocked_post):
     """function to test if the site is already regisered"""
@@ -627,7 +676,7 @@ def test_put_tracking_js():
     value1 = get_variable_value('cnil_compliant_visits_tracking_js', 'undefined')
     value2 = get_variable_value('visits_tracking_js', 'undefined')
     assert value1 == 'undefined'
-    assert value2 == 'undefined' 
+    assert value2 == 'undefined'
 
 @mock.patch('requests.post')
 def test_upgrade_javascript_tag(mocked_post):
@@ -658,7 +707,7 @@ def test_auto_configure_matomo(mocked_post):
     with override_settings(MATOMO_SERVER=CONFIG):
         contents = [GET_NO_SITE_FROM_URL, ADD_SITE_SUCCESS,
                     DEL_UNKNOWN_USER, MATOMO_SUCCESS,
-                    JAVASCRIPT_TAG]
+                    JAVASCRIPT_TAG, PING_SUCCESS]
         mocked_post.side_effect = requests_post_mocked_replies(contents)
         assert auto_configure_matomo() is True
         logme_url_var = get_variable('matomo_logme_url')
@@ -670,7 +719,7 @@ def test_auto_configure_matomo(mocked_post):
 
 @mock.patch('requests.post')
 def test_auto_configure_matomo_no_url(mocked_post):
-    # no Wc url so as to raise
+    # no Combo url so as to raise
     Wcs.objects.create(base_url='https://wcs.dev.publik.love')
     Fargo.objects.create(base_url='https://fargo.dev.publik.love')
 
@@ -696,11 +745,18 @@ def test_auto_configure_matomo_error(mocked_post):
                     DEL_UNKNOWN_USER, MATOMO_SUCCESS,
                     JAVASCRIPT_TAG_BAD_RESPONSE]
         mocked_post.side_effect = requests_post_mocked_replies(contents)
-        try:
+        with pytest.raises(MatomoException) as exc:
+            auto_configure_matomo()
+        assert "get_javascript_tag fails" in str(exc)
+        tracking_js_var = get_variable('visits_tracking_js')
+        assert tracking_js_var.value == 'js_code'
+
+    with override_settings(MATOMO_SERVER=CONFIG):
+        contents = [GET_NO_SITE_FROM_URL, ADD_SITE_SUCCESS,
+                    DEL_UNKNOWN_USER, MATOMO_SUCCESS,
+                    JAVASCRIPT_TAG, PING_ERROR]
+        mocked_post.side_effect = requests_post_mocked_replies(contents)
+        with pytest.raises(MatomoError, match='ping fails'):
             auto_configure_matomo()
-        except MatomoException as exc:
-            assert str(exc) == "get_javascript_tag fails"
-        else:
-            assert False
         tracking_js_var = get_variable('visits_tracking_js')
         assert tracking_js_var.value == 'js_code'
diff --git a/tests/test_matomo_views.py b/tests/test_matomo_views.py
index 723e9ba..3728f44 100644
--- a/tests/test_matomo_views.py
+++ b/tests/test_matomo_views.py
@@ -1,5 +1,6 @@
 # -*- coding: utf-8 -*-
 
+import json
 import mock
 import pytest
 import re
@@ -62,6 +63,25 @@ JAVASCRIPT_TAG = """<?xml version="1.0" encoding="utf-8" ?>
 </result>
 """
 
+PING_SUCCESS = '{"status":"success","tracked":1,"invalid":0}'
+PING_ERROR = 'somethings else'
+
+def requests_post_mocked_replies(contents):
+    """buid an iterator for mock's side_effect parameter"""
+    responses = []
+    for content in contents:
+        response = Response()
+
+        # response may be XML or JSON
+        if content[0] == '{':
+            response.json = mock.MagicMock(return_value = json.loads(content))
+        else:
+            response._content = content
+
+        response.status_code = 200
+        responses.append(response)
+    return responses
+
 @pytest.fixture
 def admin_user():
     try:
@@ -120,28 +140,6 @@ def test_enable_manual(admin_user):
     assert resp3.body.find('<button class="submit-button">Save</button>') != -1
     assert resp3.body.find('Good respect of user rights') != -1
 
-def auto_conf_mocked_post(url, **kwargs):
-    contents = [GET_NO_SITE_FROM_URL, ADD_SITE_SUCCESS,
-                DEL_UNKNOWN_USER, MATOMO_SUCCESS,
-                JAVASCRIPT_TAG]
-    response = Response()
-    response._content = contents[auto_conf_mocked_post.cpt]
-    response.status_code = 200
-
-    auto_conf_mocked_post.cpt += 1
-    return response
-
-def auto_conf_failure_mocked_post(url, **kwargs):
-    contents = [GET_NO_SITE_FROM_URL, ADD_SITE_SUCCESS,
-                DEL_UNKNOWN_USER, MATOMO_SUCCESS,
-                JAVASCRIPT_TAG_BAD_RESPONSE]
-    response = Response()
-    response._content = contents[auto_conf_mocked_post.cpt]
-    response.status_code = 200
-
-    auto_conf_mocked_post.cpt += 1
-    return response
-
 def test_available_options(admin_user):
     """check available buttons (manual/automatic configurations)"""
     with override_settings(MATOMO_SERVER=CONFIG):
@@ -156,7 +154,7 @@ def test_available_options(admin_user):
     assert str(resp).find('href="/matomo/enable-manual"') != -1
     assert str(resp).find('href="/matomo/enable-auto"') == -1
 
-@mock.patch('requests.post', side_effect=auto_conf_mocked_post)
+@mock.patch('requests.post')
 def test_enable_auto(mocked_post, admin_user):
     """succesfull automatic scenario"""
     Combo.objects.create(base_url='https://combo.dev.publik.love',
@@ -164,7 +162,11 @@ def test_enable_auto(mocked_post, admin_user):
     Wcs.objects.create(base_url='https://wcs.dev.publik.love')
     Fargo.objects.create(base_url='https://fargo.dev.publik.love')
 
-    auto_conf_mocked_post.cpt = 0
+    contents = [GET_NO_SITE_FROM_URL, ADD_SITE_SUCCESS,
+                DEL_UNKNOWN_USER, MATOMO_SUCCESS,
+                JAVASCRIPT_TAG, PING_SUCCESS]
+
+    mocked_post.side_effect = requests_post_mocked_replies(contents)
     with override_settings(MATOMO_SERVER=CONFIG):
         app = login(TestApp(application))
         resp1 = app.get('/matomo/enable-auto', status=200)
@@ -177,8 +179,7 @@ def test_enable_auto(mocked_post, admin_user):
         # expect the CNIL compliance message is displayed
         assert resp3.body.find('Excellent respect of user rights') != -1
 
-
-@mock.patch('requests.post', side_effect=auto_conf_failure_mocked_post)
+@mock.patch('requests.post')
 def test_enable_auto_failure(mocked_post, admin_user):
     """error on automatic scenario"""
     Combo.objects.create(base_url='https://combo.dev.publik.love',
@@ -186,7 +187,11 @@ def test_enable_auto_failure(mocked_post, admin_user):
     Wcs.objects.create(base_url='https://wcs.dev.publik.love')
     Fargo.objects.create(base_url='https://fargo.dev.publik.love')
 
-    auto_conf_mocked_post.cpt = 0
+    contents = [GET_NO_SITE_FROM_URL, ADD_SITE_SUCCESS,
+                DEL_UNKNOWN_USER, MATOMO_SUCCESS,
+                JAVASCRIPT_TAG_BAD_RESPONSE]
+
+    mocked_post.side_effect = requests_post_mocked_replies(contents)
     with override_settings(MATOMO_SERVER=CONFIG):
         app = login(TestApp(application))
         resp1 = app.get('/matomo/enable-auto', status=200)
-- 
2.20.1

