"""JSON Patch test cases.""" import json import re from collections.abc import Mapping from io import StringIO from typing import Any from typing import Iterator import pytest from jsonpath import JSONPatch from jsonpath import patch from jsonpath.exceptions import JSONPatchError class MockMapping(Mapping): # type: ignore def __getitem__(self, __key: Any) -> Any: return "foo" def __iter__(self) -> Iterator[str]: return iter(["foo"]) def __len__(self) -> int: return 1 def test_add_to_immutable_mapping() -> None: patch = JSONPatch().add("/foo/bar", "baz") with pytest.raises( JSONPatchError, match=re.escape("unexpected operation on 'MockMapping' (add:0)") ): patch.apply({"foo": MockMapping()}) def test_remove_root() -> None: patch = JSONPatch().remove("") with pytest.raises(JSONPatchError, match=re.escape("can't remove root (remove:0)")): patch.apply({"foo": "bar"}) def test_remove_nonexistent_value() -> None: patch = JSONPatch().remove("/baz") with pytest.raises( JSONPatchError, match=re.escape("can't remove nonexistent property (remove:0)") ): patch.apply({"foo": "bar"}) def test_remove_array_end() -> None: patch = JSONPatch().remove("/foo/-") with pytest.raises( JSONPatchError, match=re.escape("can't remove nonexistent item (remove:0)") ): patch.apply({"foo": [1, 2, 3]}) def test_remove_from_immutable_mapping() -> None: patch = JSONPatch().remove("/bar/foo") with pytest.raises( JSONPatchError, match=re.escape("unexpected operation on 'MockMapping' (remove:0)"), ): patch.apply({"bar": MockMapping()}) def test_replace_root() -> None: assert patch.apply( [{"op": "replace", "path": "", "value": [1, 2, 3]}], {"foo": "bar"} ) == [1, 2, 3] def test_replace_a_nonexistent_item() -> None: with pytest.raises( JSONPatchError, match=re.escape("can't replace nonexistent item (replace:0)") ): patch.apply( [{"op": "replace", "path": "/foo/99", "value": 5}], {"foo": [1, 2, 3]} ) def test_replace_a_nonexistent_value() -> None: with pytest.raises( JSONPatchError, match=re.escape("can't replace nonexistent property (replace:0)"), ): patch.apply( [{"op": "replace", "path": "/foo/bar", "value": 5}], {"foo": {"baz": 10}} ) def test_replace_immutable_mapping() -> None: with pytest.raises( JSONPatchError, match=re.escape("unexpected operation on 'MockMapping' (replace:0)"), ): patch.apply( [{"op": "replace", "path": "/bar/foo", "value": "baz"}], {"bar": MockMapping()}, ) def test_move_to_child() -> None: with pytest.raises( JSONPatchError, match=re.escape("can't move object to one of its own children (move:0)"), ): patch.apply( [{"op": "move", "from": "/foo/bar", "path": "/foo/bar/baz"}], {"foo": {"bar": {"baz": [1, 2, 3]}}}, ) def test_move_nonexistent_value() -> None: with pytest.raises( JSONPatchError, match=re.escape("source object does not exist (move:0)") ): JSONPatch().move(from_="/foo/bar", path="/bar").apply({"foo": {"baz": 1}}) def test_move_to_root() -> None: patch = JSONPatch().move(from_="/foo", path="") assert patch.apply({"foo": {"bar": "baz"}}) == {"bar": "baz"} def test_move_to_immutable_mapping() -> None: patch = JSONPatch().move(from_="/foo/bar", path="/baz/bar") with pytest.raises( JSONPatchError, match=re.escape("unexpected operation on 'MockMapping' (move:0)"), ): patch.apply({"foo": {"bar": "hello"}, "baz": MockMapping()}) def test_copy_nonexistent_value() -> None: with pytest.raises( JSONPatchError, match=re.escape("source object does not exist (copy:0)") ): JSONPatch().copy(from_="/foo/bar", path="/bar").apply({"foo": {"baz": "hello"}}) def test_copy_to_root() -> None: patch = JSONPatch().copy(from_="/foo/bar", path="") assert patch.apply({"foo": {"bar": [1, 2, 3]}}) == [1, 2, 3] def test_copy_to_immutable_mapping() -> None: with pytest.raises( JSONPatchError, match=re.escape("unexpected operation on 'MockMapping' (copy:0)"), ): JSONPatch().copy(from_="/foo/bar", path="/baz/bar").apply( {"foo": {"bar": [1, 2, 3]}, "baz": MockMapping()} ) def test_patch_from_file_like() -> None: patch_doc = StringIO( json.dumps( [ {"op": "add", "path": "", "value": {"foo": {}}}, {"op": "add", "path": "/foo", "value": {"bar": []}}, {"op": "add", "path": "/foo/bar/-", "value": 1}, ] ) ) patch = JSONPatch(patch_doc) assert patch.apply({}) == {"foo": {"bar": [1]}} def test_patch_from_string() -> None: patch_doc = json.dumps( [ {"op": "add", "path": "", "value": {"foo": {}}}, {"op": "add", "path": "/foo", "value": {"bar": []}}, {"op": "add", "path": "/foo/bar/-", "value": 1}, ] ) patch = JSONPatch(patch_doc) assert patch.apply({}) == {"foo": {"bar": [1]}} def test_unexpected_patch_ops() -> None: with pytest.raises( JSONPatchError, match=re.escape("expected a sequence of patch operations, found 'MockMapping'"), ): JSONPatch(MockMapping()) # type: ignore def test_construct_missing_op() -> None: with pytest.raises(JSONPatchError, match=re.escape("missing 'op' member at op 0")): JSONPatch([{}]) def test_construct_unknown_op() -> None: msg = ( "expected 'op' to be one of 'add', 'remove', 'replace', " "'move', 'copy' or 'test' (foo:0)" ) with pytest.raises(JSONPatchError, match=re.escape(msg)): JSONPatch([{"op": "foo"}]) def test_construct_missing_pointer() -> None: msg = "missing property 'path' (add:0)" with pytest.raises(JSONPatchError, match=re.escape(msg)): JSONPatch([{"op": "add", "value": "foo"}]) def test_construct_missing_value() -> None: msg = "missing property 'value' (add:0)" with pytest.raises(JSONPatchError, match=re.escape(msg)): JSONPatch([{"op": "add", "path": "/foo"}]) def test_construct_pointer_not_a_string() -> None: msg = "expected a JSON Pointer string for 'path', found 'int' (add:0)" with pytest.raises(JSONPatchError, match=re.escape(msg)): JSONPatch([{"op": "add", "path": 5, "value": "foo"}]) def test_apply_to_str() -> None: patch_doc = json.dumps( [ {"op": "add", "path": "", "value": {"foo": {}}}, {"op": "add", "path": "/foo", "value": {"bar": []}}, {"op": "add", "path": "/foo/bar/-", "value": 1}, ] ) data_doc = json.dumps({}) assert patch.apply(patch_doc, data_doc) == {"foo": {"bar": [1]}} def test_apply_to_file_like() -> None: patch_doc = StringIO( json.dumps( [ {"op": "add", "path": "", "value": {"foo": {}}}, {"op": "add", "path": "/foo", "value": {"bar": []}}, {"op": "add", "path": "/foo/bar/-", "value": 1}, ] ) ) data_doc = StringIO(json.dumps({})) assert patch.apply(patch_doc, data_doc) == {"foo": {"bar": [1]}} def test_asdict() -> None: patch_doc = [ {"op": "add", "path": "/foo/bar", "value": "foo"}, {"op": "remove", "path": "/foo/bar"}, {"op": "replace", "path": "/foo/bar", "value": "foo"}, {"op": "move", "from": "/baz/foo", "path": "/foo/bar"}, {"op": "copy", "from": "/baz/foo", "path": "/foo/bar"}, {"op": "test", "path": "/foo/bar", "value": "foo"}, ] patch = JSONPatch(patch_doc) assert patch.asdicts() == patch_doc def test_non_standard_addap_op() -> None: # Index 7 is out of range and would raises a JSONPatchError with the `add` op. patch = JSONPatch().addap(path="/foo/7", value=99) assert patch.apply({"foo": [1, 2, 3]}) == {"foo": [1, 2, 3, 99]} def test_add_to_mapping_with_int_key() -> None: patch = JSONPatch().add(path="/1", value=99) assert patch.apply({"foo": 1}) == {"foo": 1, "1": 99}