Repository: laterpay/laterpay-client-python

Issue:
We must only pass lptoken or muid to /access
Only one of `lptoken` and `muid` is allowed to the API's `/access` calls. Not both.

Comments:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0ba7c00..5ad4dde 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,9 @@
 
 ## 5.3.1 (under development)
 
+* Only passed one of `lptoken` and `muid` to `/access` calls. Passing both is
+  not supported.
+
 ## 5.3.0
 
 * Added explicit support for the `muid` argument to `get_add_url()`,
diff --git a/laterpay/__init__.py b/laterpay/__init__.py
index 1a39bfc..af44ce6 100644
--- a/laterpay/__init__.py
+++ b/laterpay/__init__.py
@@ -360,16 +360,34 @@ class LaterPayClient(object):
         params = {
             'cp': self.cp_key,
             'ts': str(int(time.time())),
-            'lptoken': str(lptoken or self.lptoken),
             'article_id': article_ids,
         }
 
-        if muid:
-            # TODO: The behavior when lptoken and muid are given is not yet
-            # defined. Thus we'll allow both at the same time for now. It might
-            # be that in the end only one is allowed or one is prefered over
-            # the other.
+        """
+        l = lptoken
+        s = self.lptoken
+        m = muid
+        x = error
+
+               |   L   | not L |   L   | not L
+        -------+-------+-------+-------+-------
+           l   |   x   |   x   |   l   |   l
+        -------+-------+-------+-------+-------
+         not l |   m   |   m   |   s   |   x
+        -------+-------+-------+-------+-------
+               |   m   |   m   | not m | not m
+        """
+        if lptoken is None and muid is not None:
             params['muid'] = muid
+        elif lptoken is not None and muid is None:
+            params['lptoken'] = lptoken
+        elif lptoken is None and muid is None and self.lptoken is not None:
+            params['lptoken'] = self.lptoken
+        else:
+            raise AssertionError(
+                'Either lptoken, self.lptoken or muid has to be passed. '
+                'Passing neither or both lptoken and muid is not allowed.',
+            )
 
         params['hmac'] = signing.sign(
             secret=self.shared_secret,


Test patch:
diff --git a/tests/test_client.py b/tests/test_client.py
index 7ffe43a..bc9e37f 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -326,7 +326,6 @@ class TestLaterPayClient(unittest.TestCase):
         data = client.get_access_data(
             ['article-1', 'article-2'],
             lptoken='fake-lptoken',
-            muid='some-user',
         )
 
         self.assertEqual(data, {
@@ -349,7 +348,7 @@ class TestLaterPayClient(unittest.TestCase):
         self.assertEqual(qd['cp'], ['fake-cp-key'])
         self.assertEqual(qd['article_id'], ['article-1', 'article-2'])
         self.assertEqual(qd['hmac'], ['fake-signature'])
-        self.assertEqual(qd['muid'], ['some-user'])
+        self.assertNotIn('muid', 'qd')
 
         sign_mock.assert_called_once_with(
             secret='fake-shared-secret',
@@ -358,6 +357,70 @@ class TestLaterPayClient(unittest.TestCase):
                 'article_id': ['article-1', 'article-2'],
                 'ts': '123',
                 'lptoken': 'fake-lptoken',
+            },
+            url='http://example.net/access',
+            method='GET',
+        )
+
+    @mock.patch('laterpay.signing.sign')
+    @mock.patch('time.time')
+    @responses.activate
+    def test_get_access_data_success_muid(self, time_time_mock, sign_mock):
+        time_time_mock.return_value = 123
+        sign_mock.return_value = 'fake-signature'
+        responses.add(
+            responses.GET,
+            'http://example.net/access',
+            body=json.dumps({
+                "status": "ok",
+                "articles": {
+                    "article-1": {"access": True},
+                    "article-2": {"access": False},
+                },
+            }),
+            status=200,
+            content_type='application/json',
+        )
+
+        client = LaterPayClient(
+            'fake-cp-key',
+            'fake-shared-secret',
+            api_root='http://example.net',
+        )
+
+        data = client.get_access_data(
+            ['article-1', 'article-2'],
+            muid='some-user',
+        )
+
+        self.assertEqual(data, {
+            "status": "ok",
+            "articles": {
+                "article-1": {"access": True},
+                "article-2": {"access": False},
+            },
+        })
+        self.assertEqual(len(responses.calls), 1)
+
+        call = responses.calls[0]
+
+        self.assertEqual(call.request.headers['X-LP-APIVersion'], '2')
+
+        qd = parse_qs(urlparse(call.request.url).query)
+
+        self.assertEqual(qd['ts'], ['123'])
+        self.assertEqual(qd['cp'], ['fake-cp-key'])
+        self.assertEqual(qd['article_id'], ['article-1', 'article-2'])
+        self.assertEqual(qd['hmac'], ['fake-signature'])
+        self.assertEqual(qd['muid'], ['some-user'])
+        self.assertNotIn('lptoken', 'qd')
+
+        sign_mock.assert_called_once_with(
+            secret='fake-shared-secret',
+            params={
+                'cp': 'fake-cp-key',
+                'article_id': ['article-1', 'article-2'],
+                'ts': '123',
                 'muid': 'some-user',
             },
             url='http://example.net/access',
@@ -379,16 +442,31 @@ class TestLaterPayClient(unittest.TestCase):
             'hmac': 'fake-signature',
         })
 
-        params = self.lp.get_access_params('article-1', lptoken='fake-lptoken', muid='some-user')
+        params = self.lp.get_access_params('article-1', muid='some-user')
         self.assertEqual(params, {
             'cp': '1',
             'ts': '123',
-            'lptoken': 'fake-lptoken',
             'article_id': ['article-1'],
             'hmac': 'fake-signature',
             'muid': 'some-user',
         })
 
+        lpclient = LaterPayClient('1', 'some-secret', lptoken='instance-lptoken')
+        params = lpclient.get_access_params('article-1')
+        self.assertEqual(params, {
+            'cp': '1',
+            'ts': '123',
+            'lptoken': 'instance-lptoken',
+            'article_id': ['article-1'],
+            'hmac': 'fake-signature',
+        })
+
+        with self.assertRaises(AssertionError):
+            self.lp.get_access_params('article-1', lptoken='fake-lptoken', muid='some-user')
+
+        with self.assertRaises(AssertionError):
+            self.lp.get_access_params('article-1')
+
     @mock.patch('time.time')
     def test_get_gettoken_redirect(self, time_mock):
         time_mock.return_value = 12345678