diff test/rest_common.py @ 5705:457fc482e6b1

Method PUT: ignore specification of protected properties which can not be set. Filtering them out of the payload list. This lets the result of a get using: class/id?@protected=true&@verbose=0 be used as input to a PUT operation without having to strip the protected properties. Note this does not raise an error if the PUT protected property is different from the value in the db. If the property is different but the etag/if-match passes, the user attempted to set the protected property and this should result in an error, but will not with this patch. Method DELETE class/id/attribute: raise error when trying to delete protected or required attribute/property. Raise UsageError when attribute doesn't exist. Method PATCH class/id: raise error when trying to replace/remove protected attribute/property raise error when trying to remove required attribute/property Catch KeyError at top level and turn into 400 error. If payload has an attribute/property that does not exist, raise UsageError which becomes a 400 error.
author John Rouillard <rouilj@ieee.org>
date Thu, 11 Apr 2019 20:54:39 -0400
parents 4aae822e2cb4
children dfca6136dd7b
line wrap: on
line diff
--- a/test/rest_common.py	Wed Apr 10 22:43:16 2019 -0400
+++ b/test/rest_common.py	Thu Apr 11 20:54:39 2019 -0400
@@ -4,6 +4,7 @@
 import errno
 
 from roundup.cgi.exceptions import *
+from roundup.exceptions import *
 from roundup import password, hyperdb
 from roundup.rest import RestfulInstance, calculate_etag
 from roundup.backends import list_backends
@@ -56,6 +57,7 @@
         self.db.issue.addprop(anint=hyperdb.Integer())
         self.db.issue.addprop(afloat=hyperdb.Number())
         self.db.issue.addprop(abool=hyperdb.Boolean())
+        self.db.issue.addprop(requireme=hyperdb.String(required=True))
         self.db.msg.addprop(tx_Source=hyperdb.String())
 
         self.db.post_init()
@@ -144,8 +146,6 @@
     def testOutputFormat(self):
         """ test of @fields and @verbose implementation """
 
-        from roundup.exceptions import UsageError
-
         self.maxDiff = 4000
         # create sample data
         try:
@@ -620,7 +620,7 @@
         ''' Make sure etag generation is stable
         
             FIXME need to mock somehow date.Date() when creating
-            the target to be mocked. The differening dates makes
+            the target to be mocked. The differing dates makes
             this test impossible.
         '''
         newuser = self.db.user.create(
@@ -985,7 +985,7 @@
                          'foo bar')
         del(self.headers)
 
-    def testPut(self):
+    def testPutElement(self):
         """
         Change joe's 'realname'
         Check if we can't change admin's detail
@@ -1026,9 +1026,14 @@
         del(self.headers)
 
         # Reset joe's 'realname'. etag in body
+        # Also try to set protected items. The protected items should
+        # be ignored on put_element to make it easy to get the item
+        # with all fields, change one field and put the result without
+        # having to filter out protected items.
         form = cgi.FieldStorage()
         etag = calculate_etag(self.db.user.getnode(self.joeid))
         form.list = [
+            cgi.MiniFieldStorage('creator', '3'),
             cgi.MiniFieldStorage('realname', 'Joe Doe'),
             cgi.MiniFieldStorage('@etag', etag)
         ]
@@ -1038,11 +1043,79 @@
         self.assertEqual(self.dummy_client.response_code, 200)
         self.assertEqual(results['data']['attributes']['realname'], 'Joe Doe')
 
-        # check we can't change admin's details
+        # We are joe, so check we can't change admin's details
         results = self.server.put_element('user', '1', form)
         self.assertEqual(self.dummy_client.response_code, 403)
         self.assertEqual(results['error']['status'], 403)
 
+        # Try to reset joe's 'realname' and add a broken prop.
+        # This should result in no change to the name and
+        # a 400 UsageError stating prop does not exist.
+        form = cgi.FieldStorage()
+        etag = calculate_etag(self.db.user.getnode(self.joeid))
+        form.list = [
+            cgi.MiniFieldStorage('JustKidding', '3'),
+            cgi.MiniFieldStorage('realname', 'Joe Doe'),
+            cgi.MiniFieldStorage('@etag', etag)
+        ]
+        results = self.server.put_element('user', self.joeid, form)
+        expected= {'error': {'status': 400,
+                             'msg': UsageError('Property JustKidding not '
+                                               'found in class user')}}
+        self.assertEqual(results['error']['status'],
+                         expected['error']['status'])
+        self.assertEqual(type(results['error']['msg']),
+                         type(expected['error']['msg']))
+        self.assertEqual(self.dummy_client.response_code, 400)
+        results = self.server.get_element('user', self.joeid, self.empty_form)
+        self.assertEqual(self.dummy_client.response_code, 200)
+        self.assertEqual(results['data']['attributes']['realname'], 'Joe Doe')
+
+    def testPutAttribute(self):
+        # put protected property
+        # make sure we don't have permission issues
+        self.db.setCurrentUser('admin')
+        form = cgi.FieldStorage()
+        etag = calculate_etag(self.db.user.getnode(self.joeid))
+        form.list = [
+            cgi.MiniFieldStorage('data', '3'),
+            cgi.MiniFieldStorage('@etag', etag)
+        ]
+        results = self.server.put_attribute(
+            'user', self.joeid, 'creator', form
+        )
+        expected= {'error': {'status': 400, 'msg': 
+                             UsageError('\'"creator", "actor", "creation" and '
+                                        '"activity" are reserved\'')}}
+        print(results)
+        self.assertEqual(results['error']['status'],
+                         expected['error']['status'])
+        self.assertEqual(type(results['error']['msg']),
+                         type(expected['error']['msg']))
+        self.assertEqual(self.dummy_client.response_code, 400)
+
+        # put invalid property
+        # make sure we don't have permission issues
+        self.db.setCurrentUser('admin')
+        form = cgi.FieldStorage()
+        etag = calculate_etag(self.db.user.getnode(self.joeid))
+        form.list = [
+            cgi.MiniFieldStorage('data', '3'),
+            cgi.MiniFieldStorage('@etag', etag)
+        ]
+        results = self.server.put_attribute(
+            'user', self.joeid, 'youMustBeKiddingMe', form
+        )
+        expected= {'error': {'status': 400,
+                             'msg': UsageError("'youMustBeKiddingMe' "
+                                      "is not a property of user")}}
+        print(results)
+        self.assertEqual(results['error']['status'],
+                         expected['error']['status'])
+        self.assertEqual(type(results['error']['msg']),
+                         type(expected['error']['msg']))
+        self.assertEqual(self.dummy_client.response_code, 400)
+
     def testPost(self):
         """
         Post a new issue with title: foo
@@ -1149,6 +1222,7 @@
         """
         Test Delete an attribute
         """
+        self.maxDiff = 4000
         # create a new issue with userid 1 in the nosy list
         issue_id = self.db.issue.create(title='foo', nosy=['1'])
 
@@ -1190,6 +1264,39 @@
         self.assertListEqual(results['attributes']['nosy'], [])
         self.assertEqual(results['attributes']['title'], None)
 
+        # delete protected property
+        etag = calculate_etag(self.db.issue.getnode(issue_id))
+        form.list.append(cgi.MiniFieldStorage('@etag', etag))
+        results = self.server.delete_attribute(
+            'issue', issue_id, 'creator', form
+        )
+        expected= {'error': {
+            'status': 400, 
+            'msg': UsageError("Attribute 'creator' can not be updated for class issue.")
+        }}
+
+        self.assertEqual(results['error']['status'],
+                         expected['error']['status'])
+        self.assertEqual(type(results['error']['msg']),
+                         type(expected['error']['msg']))
+        self.assertEqual(self.dummy_client.response_code, 400)
+
+        # delete required property
+        etag = calculate_etag(self.db.issue.getnode(issue_id))
+        form.list.append(cgi.MiniFieldStorage('@etag', etag))
+        results = self.server.delete_attribute(
+            'issue', issue_id, 'requireme', form
+        )
+        expected= {'error': {'status': 400,
+                    'msg': UsageError("Attribute 'requireme' is "
+                        "required by class issue and can not be deleted.")}}
+        print(results)
+        self.assertEqual(results['error']['status'],
+                         expected['error']['status'])
+        self.assertEqual(type(results['error']['msg']),
+                         type(expected['error']['msg']))
+        self.assertEqual(self.dummy_client.response_code, 400)
+
     def testPatchAdd(self):
         """
         Test Patch op 'Add'
@@ -1267,6 +1374,26 @@
         self.assertEqual(len(results['attributes']['nosy']), 1)
         self.assertListEqual(results['attributes']['nosy'], ['2'])
 
+        # try to set a protected prop. It should fail.
+        etag = calculate_etag(self.db.issue.getnode(issue_id))
+        form = cgi.FieldStorage()
+        form.list = [
+            cgi.MiniFieldStorage('@op', 'replace'),
+            cgi.MiniFieldStorage('creator', '2'),
+            cgi.MiniFieldStorage('@etag', etag)
+        ]
+        results = self.server.patch_element('issue', issue_id, form)
+        expected= {'error': {'status': 400,
+                             'msg': KeyError('"creator", "actor", "creation" and "activity" are reserved',)}}
+        print(results)
+        self.assertEqual(results['error']['status'],
+                         expected['error']['status'])
+        self.assertEqual(type(results['error']['msg']),
+                         type(expected['error']['msg']))
+        self.assertEqual(str(results['error']['msg']),
+                         str(expected['error']['msg']))
+        self.assertEqual(self.dummy_client.response_code, 400)
+
     def testPatchRemoveAll(self):
         """
         Test Patch Action 'Remove'
@@ -1311,6 +1438,46 @@
         self.assertEqual(len(results['attributes']['nosy']), 0)
         self.assertEqual(results['attributes']['nosy'], [])
 
+        # try to remove a protected prop. It should fail.
+        etag = calculate_etag(self.db.issue.getnode(issue_id))
+        form = cgi.FieldStorage()
+        form.list = [
+            cgi.MiniFieldStorage('@op', 'remove'),
+            cgi.MiniFieldStorage('creator', '2'),
+            cgi.MiniFieldStorage('@etag', etag)
+        ]
+        results = self.server.patch_element('issue', issue_id, form)
+        expected= {'error': {'status': 400,
+                             'msg': KeyError('"creator", "actor", "creation" and "activity" are reserved',)}}
+        print(results)
+        self.assertEqual(results['error']['status'],
+                         expected['error']['status'])
+        self.assertEqual(type(results['error']['msg']),
+                         type(expected['error']['msg']))
+        self.assertEqual(str(results['error']['msg']),
+                         str(expected['error']['msg']))
+        self.assertEqual(self.dummy_client.response_code, 400)
+
+        # try to remove a required prop. it should fail
+        etag = calculate_etag(self.db.issue.getnode(issue_id))
+        form.list = [
+            cgi.MiniFieldStorage('@op', 'remove'),
+            cgi.MiniFieldStorage('requireme', ''),
+            cgi.MiniFieldStorage('@etag', etag)
+        ]
+        results = self.server.patch_element('issue', issue_id, form)
+        expected= {'error': {'status': 400,
+                             'msg': UsageError("Attribute 'requireme' is required by class issue and can not be removed.")
+                             }}
+        print(results)
+        self.assertEqual(results['error']['status'],
+                         expected['error']['status'])
+        self.assertEqual(type(results['error']['msg']),
+                         type(expected['error']['msg']))
+        self.assertEqual(str(results['error']['msg']),
+                         str(expected['error']['msg']))
+        self.assertEqual(self.dummy_client.response_code, 400)
+
     def testPatchAction(self):
         """
         Test Patch Action 'Action'

Roundup Issue Tracker: http://roundup-tracker.org/