Repository: openfun/marsha

Issue:
Sending a xAPI statement to a LRS not working anymore
## Bug Report

**Problematic Behavior**

When a LRS is configured in a consumer site, sending a xapi statement is failing

https://gip-fun-mooc.sentry.io/share/issue/081e7857e01544d3bd5b5f93d573c428/

**Expected behavior/code**

When a LRS is correctly configured for a given consumer site, the statement should be sent to the LRS.

**Steps to Reproduce**
1. Configure a LRS in a consumer site
2. Navigate on an existing video
3. And then the bug happens!



Discussion:

Resolving patch:
diff --git a/src/backend/marsha/core/xapi.py b/src/backend/marsha/core/xapi.py
--- a/src/backend/marsha/core/xapi.py
+++ b/src/backend/marsha/core/xapi.py
@@ -344,7 +344,7 @@ def send(self, xapi_statement):
 
         response = requests.post(
             self.url,
-            json=xapi_statement.get_statement(),
+            json=xapi_statement,
             headers=headers,
             timeout=settings.STAT_BACKEND_TIMEOUT,
         )


Test patch:
diff --git a/src/backend/marsha/core/tests/test_xapi.py b/src/backend/marsha/core/tests/test_xapi.py
--- a/src/backend/marsha/core/tests/test_xapi.py
+++ b/src/backend/marsha/core/tests/test_xapi.py
@@ -1,9 +1,9 @@
 """Tests for the xapi module of the Marsha project."""
 
-from unittest import mock
-
 from django.test import TestCase, override_settings
 
+import responses
+
 from marsha.core.defaults import ENDED, RAW, READY, RUNNING
 from marsha.core.factories import DocumentFactory, VideoFactory
 from marsha.core.simple_jwt.factories import LTIPlaylistAccessTokenFactory
@@ -12,10 +12,12 @@
     XAPIDocumentStatement,
     XAPIVideoStatement,
     get_xapi_statement,
-    requests,
 )
 
 
+# pylint: disable=unexpected-keyword-arg,no-value-for-parameter
+
+
 class XAPIVideoStatementTest(TestCase):
     """Test the XAPIVideoStatement class."""
 
@@ -684,35 +686,69 @@ def test_xapi_statement_missing_user_id(self):
 class XAPITest(TestCase):
     """Test the xapi module."""
 
-    @mock.patch.object(requests, "post")
-    def test_xapi_enrich_and_send_statement(self, mock_requests_post):
+    @responses.activate(assert_all_requests_are_fired=True)
+    def test_xapi_enrich_and_send_statement(self):
         """XAPI statement sent by the front application should be enriched.
 
         Before sending a statement, the xapi module is responsible for enriching it.
         """
-        xapi = XAPI("https://lrs.example.com", "auth_token")
-
-        mock_response = mock.MagicMock()
-        mock_response.raise_for_status.return_value = 200
-        mock_requests_post.return_value = mock_response
+        xapi = XAPI("https://lrs.example.com", "Basic auth_token")
 
-        statement = {"foo": "bar"}
-        mock_xapi_statement = mock.MagicMock()
-        mock_xapi_statement.get_statement.return_value = statement
+        video = VideoFactory(
+            id="68333c45-4b8c-4018-a195-5d5e1706b838",
+            playlist__consumer_site__domain="example.com",
+            title="test video xapi",
+        )
 
-        xapi.send(mock_xapi_statement)
+        jwt_token = LTIPlaylistAccessTokenFactory(
+            session_id="326c0689-48c1-493e-8d2d-9fb0c289de7f",
+            context_id="course-v1:ufr+mathematics+0001",
+            user__id="b2584aa405540758db2a6278521b6478",
+        )
 
-        args, kwargs = mock_requests_post.call_args_list[0]
-        self.assertEqual(args[0], "https://lrs.example.com")
-        self.assertEqual(
-            kwargs["headers"],
-            {
-                "Authorization": "auth_token",
-                "Content-Type": "application/json",
-                "X-Experience-API-Version": "1.0.3",
+        base_statement = {
+            "context": {
+                "extensions": {
+                    "https://w3id.org/xapi/video/extensions/session-id": "a6151456-18b7-"
+                    "43b4-8452-2037fed588df"
+                }
             },
-        )
-        self.assertEqual(kwargs["json"], statement)
+            "result": {
+                "extensions": {
+                    "https://w3id.org/xapi/video/extensions/time-from": 0,
+                    "https://w3id.org/xapi/video/extensions/time-to": 0,
+                    "https://w3id.org/xapi/video/extensions/length": 104.304,
+                    "https://w3id.org/xapi/video/extensions/progress": 0,
+                    "https://w3id.org/xapi/video/extensions/played-segments": "0",
+                }
+            },
+            "verb": {
+                "display": {"en-US": "seeked"},
+                "id": "https://w3id.org/xapi/video/verbs/seeked",
+            },
+            "id": "17dfcd44-b3e0-403d-ab96-e3ef7da616d4",
+        }
+
+        xapi_statement = XAPIVideoStatement()
+        statement = xapi_statement.from_lti(video, base_statement, jwt_token)
+
+        responses.add(
+            responses.POST,
+            "https://lrs.example.com",
+            match=[
+                responses.matchers.json_params_matcher(statement),
+                responses.matchers.header_matcher(
+                    {
+                        "Authorization": "Basic auth_token",
+                        "Content-Type": "application/json",
+                        "X-Experience-API-Version": "1.0.3",
+                    }
+                ),
+            ],
+            status=204,
+        )
+
+        xapi.send(statement)
 
 
 class GetXapiStatementTest(TestCase):