diff --git a/jsonpatch.py b/jsonpatch.py index 3123eca..a84555b 100644 --- a/jsonpatch.py +++ b/jsonpatch.py @@ -518,11 +518,18 @@ def __init__(self, patch, pointer_cls=JsonPointer): self.patch = patch self.pointer_cls = pointer_cls + if isinstance(patch, (str, bytes, Mapping)): + raise InvalidJsonPatch("Document is expected to be sequence of operations") + try: + patch_iter = iter(patch) + except TypeError: + raise InvalidJsonPatch("Document is expected to be sequence of operations") + # Verify that the structure of the patch document # is correct by retrieving each patch element. # Much of the validation is done in the initializer # though some is delayed until the patch is applied. - for op in self.patch: + for op in patch_iter: # We're only checking for strings in the following check # for two reasons: # diff --git a/property_tests.py b/property_tests.py index 3d41ab5..8dc1c97 100644 --- a/property_tests.py +++ b/property_tests.py @@ -324,6 +324,15 @@ def test_result_shares_nothing_with_dst(self, docs): class ApplyPatchProperties(unittest.TestCase): + @given(st.one_of(json_scalars, st.dictionaries(json_keys, json_docs))) + @example(patch={}) + @example(patch='') + def test_non_array_patch_documents_are_rejected(self, patch): + with self.assertRaises(jsonpatch.InvalidJsonPatch): + jsonpatch.JsonPatch(patch) + with self.assertRaises(jsonpatch.InvalidJsonPatch): + jsonpatch.JsonPatch.from_string(json.dumps(patch)) + @given(docs_with_patches()) def test_does_not_modify_document(self, case): doc, patch = case diff --git a/tests.py b/tests.py index 537b091..cc16701 100755 --- a/tests.py +++ b/tests.py @@ -1533,6 +1533,35 @@ def test_copy_from_pointer(self): class JsonPatchCreationTest(unittest.TestCase): + def test_creation_rejects_non_sequence_documents(self): + for patch in ({}, {'op': 'add', 'path': '/a', 'value': 1}, + MappingProxyType({}), '', b'', None, True, False, + 0, 1, 1.5): + with self.subTest(patch=patch): + with self.assertRaises(jsonpatch.InvalidJsonPatch): + jsonpatch.JsonPatch(patch) + + def test_serialized_patch_rejects_non_array_documents(self): + for patch in ({}, {'op': 'add', 'path': '/a', 'value': 1}, + '', 'text', None, True, False, 0, 1, 1.5): + for serialized in (json.dumps(patch), json.dumps(patch).encode()): + with self.subTest(patch=serialized): + with self.assertRaises(jsonpatch.InvalidJsonPatch): + jsonpatch.JsonPatch.from_string(serialized) + doc = {'a': 'unchanged'} + with self.assertRaises(jsonpatch.InvalidJsonPatch): + jsonpatch.apply_patch(doc, serialized, in_place=True) + self.assertEqual(doc, {'a': 'unchanged'}) + + def test_creation_preserves_operation_sequences(self): + for operations in ([], (), + [{'op': 'add', 'path': '/a', 'value': 1}], + ({'op': 'add', 'path': '/a', 'value': 1},)): + with self.subTest(operations=operations): + patch = jsonpatch.JsonPatch(operations) + self.assertIs(patch.patch, operations) + self.assertEqual(patch.apply({}), {'a': 1} if operations else {}) + def test_creation_fails_with_invalid_patch(self): invalid_patches = [ { 'path': '/foo', 'value': 'bar'},