From 8eefe0fdc22793f6573f82c9856abcbe36a7bb80 Mon Sep 17 00:00:00 2001 From: Jass Bagga Date: Thu, 19 Oct 2017 10:46:08 -0700 Subject: [PATCH] Add Test Operation (#114) Addresses #1 --- .../Adapters/IObjectAdapter.cs | 95 ++++++- .../Adapters/IObjectAdapterWithTest.cs | 32 +++ .../Adapters/ObjectAdapter.cs | 172 +++---------- .../Internal/DictionaryAdapterOfTU.cs | 51 ++++ .../Internal/DynamicObjectAdapter.cs | 32 +++ .../Internal/IAdapter.cs | 7 + .../Internal/ListAdapter.cs | 39 +++ .../Internal/ObjectVisitor.cs | 9 +- .../Internal/PocoAdapter.cs | 39 +++ .../JsonPatchDocument.cs | 18 ++ .../JsonPatchDocumentOfT.cs | 71 ++++++ .../Operations/Operation.cs | 10 +- .../Operations/OperationOfT.cs | 11 +- .../Properties/Resources.Designer.cs | 46 +++- .../Resources.resx | 9 + .../{ => Adapters}/DictionaryAdapterTest.cs | 37 +++ .../DynamicObjectAdapterTest.cs | 37 +++ .../{ => Adapters}/ListAdapterTest.cs | 50 ++++ .../Adapters/PocoAdapterTest.cs | 241 ++++++++++++++++++ ...tegrationTests.cs => DynamicObjectTest.cs} | 42 ++- .../JsonPatchDocumentTest.cs | 36 --- 21 files changed, 895 insertions(+), 189 deletions(-) create mode 100644 src/Microsoft.AspNetCore.JsonPatch/Adapters/IObjectAdapterWithTest.cs rename test/Microsoft.AspNetCore.JsonPatch.Test/{ => Adapters}/DictionaryAdapterTest.cs (88%) rename test/Microsoft.AspNetCore.JsonPatch.Test/{ => Adapters}/DynamicObjectAdapterTest.cs (85%) rename test/Microsoft.AspNetCore.JsonPatch.Test/{ => Adapters}/ListAdapterTest.cs (89%) create mode 100644 test/Microsoft.AspNetCore.JsonPatch.Test/Adapters/PocoAdapterTest.cs rename test/Microsoft.AspNetCore.JsonPatch.Test/IntegrationTests/{DynamicObjectIntegrationTests.cs => DynamicObjectTest.cs} (86%) diff --git a/src/Microsoft.AspNetCore.JsonPatch/Adapters/IObjectAdapter.cs b/src/Microsoft.AspNetCore.JsonPatch/Adapters/IObjectAdapter.cs index f699755ed4..e5206bfa0d 100644 --- a/src/Microsoft.AspNetCore.JsonPatch/Adapters/IObjectAdapter.cs +++ b/src/Microsoft.AspNetCore.JsonPatch/Adapters/IObjectAdapter.cs @@ -8,12 +8,105 @@ namespace Microsoft.AspNetCore.JsonPatch.Adapters /// /// Defines the operations that can be performed on a JSON patch document. /// - public interface IObjectAdapter + public interface IObjectAdapter { + /// + /// Using the "add" operation a new value is inserted into the root of the target + /// document, into the target array at the specified valid index, or to a target object at + /// the specified location. + /// + /// When adding to arrays, the specified index MUST NOT be greater than the number of elements in the array. + /// To append the value to the array, the index of "-" character is used (see [RFC6901]). + /// + /// When adding to an object, if an object member does not already exist, a new member is added to the object at the + /// specified location or if an object member does exist, that member's value is replaced. + /// + /// The operation object MUST contain a "value" member whose content + /// specifies the value to be added. + /// + /// For example: + /// + /// { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] } + /// + /// See RFC 6902 https://tools.ietf.org/html/rfc6902#page-4 + /// + /// The add operation. + /// Object to apply the operation to. void Add(Operation operation, object objectToApplyTo); + + /// + /// Using the "copy" operation, a value is copied from a specified location to the + /// target location. + /// + /// The operation object MUST contain a "from" member, which references the location in the + /// target document to copy the value from. + /// + /// The "from" location MUST exist for the operation to be successful. + /// + /// For example: + /// + /// { "op": "copy", "from": "/a/b/c", "path": "/a/b/e" } + /// + /// See RFC 6902 https://tools.ietf.org/html/rfc6902#page-7 + /// + /// The copy operation. + /// Object to apply the operation to. void Copy(Operation operation, object objectToApplyTo); + + /// + /// Using the "move" operation the value at a specified location is removed and + /// added to the target location. + /// + /// The operation object MUST contain a "from" member, which references the location in the + /// target document to move the value from. + /// + /// The "from" location MUST exist for the operation to be successful. + /// + /// For example: + /// + /// { "op": "move", "from": "/a/b/c", "path": "/a/b/d" } + /// + /// A location cannot be moved into one of its children. + /// + /// See RFC 6902 https://tools.ietf.org/html/rfc6902#page-6 + /// + /// The move operation. + /// Object to apply the operation to. void Move(Operation operation, object objectToApplyTo); + + /// + /// Using the "remove" operation the value at the target location is removed. + /// + /// The target location MUST exist for the operation to be successful. + /// + /// For example: + /// + /// { "op": "remove", "path": "/a/b/c" } + /// + /// If removing an element from an array, any elements above the + /// specified index are shifted one position to the left. + /// + /// See RFC 6902 https://tools.ietf.org/html/rfc6902#page-6 + /// + /// The remove operation. + /// Object to apply the operation to. void Remove(Operation operation, object objectToApplyTo); + + /// + /// Using the "replace" operation he value at the target location is replaced + /// with a new value. The operation object MUST contain a "value" member + /// which specifies the replacement value. + /// + /// The target location MUST exist for the operation to be successful. + /// + /// For example: + /// + /// { "op": "replace", "path": "/a/b/c", "value": 42 } + /// + /// See RFC 6902 https://tools.ietf.org/html/rfc6902#page-6 + /// + /// The replace operation. + /// Object to apply the operation to. void Replace(Operation operation, object objectToApplyTo); } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.JsonPatch/Adapters/IObjectAdapterWithTest.cs b/src/Microsoft.AspNetCore.JsonPatch/Adapters/IObjectAdapterWithTest.cs new file mode 100644 index 0000000000..e1b4ce7950 --- /dev/null +++ b/src/Microsoft.AspNetCore.JsonPatch/Adapters/IObjectAdapterWithTest.cs @@ -0,0 +1,32 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.JsonPatch.Operations; + +namespace Microsoft.AspNetCore.JsonPatch.Adapters +{ + /// + /// Defines the operations that can be performed on a JSON patch document, including "test". + /// + public interface IObjectAdapterWithTest : IObjectAdapter + { + /// + /// Using the "test" operation a value at the target location is compared for + /// equality to a specified value. + /// + /// The operation object MUST contain a "value" member that specifies + /// value to be compared to the target location's value. + /// + /// The target location MUST be equal to the "value" value for the + /// operation to be considered successful. + /// + /// For example: + /// { "op": "test", "path": "/a/b/c", "value": "foo" } + /// + /// See RFC 6902 https://tools.ietf.org/html/rfc6902#page-7 + /// + /// The test operation. + /// Object to apply the operation to. + void Test(Operation operation, object objectToApplyTo); + } +} diff --git a/src/Microsoft.AspNetCore.JsonPatch/Adapters/ObjectAdapter.cs b/src/Microsoft.AspNetCore.JsonPatch/Adapters/ObjectAdapter.cs index a30343868a..73095b52c2 100644 --- a/src/Microsoft.AspNetCore.JsonPatch/Adapters/ObjectAdapter.cs +++ b/src/Microsoft.AspNetCore.JsonPatch/Adapters/ObjectAdapter.cs @@ -9,7 +9,7 @@ using Newtonsoft.Json.Serialization; namespace Microsoft.AspNetCore.JsonPatch.Adapters { /// - public class ObjectAdapter : IObjectAdapter + public class ObjectAdapter : IObjectAdapterWithTest { /// /// Initializes a new instance of . @@ -34,66 +34,6 @@ namespace Microsoft.AspNetCore.JsonPatch.Adapters /// public Action LogErrorAction { get; } - /// - /// The "add" operation performs one of the following functions, - /// depending upon what the target location references: - /// - /// o If the target location specifies an array index, a new value is - /// inserted into the array at the specified index. - /// - /// o If the target location specifies an object member that does not - /// already exist, a new member is added to the object. - /// - /// o If the target location specifies an object member that does exist, - /// that member's value is replaced. - /// - /// The operation object MUST contain a "value" member whose content - /// specifies the value to be added. - /// - /// For example: - /// - /// { "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] } - /// - /// When the operation is applied, the target location MUST reference one - /// of: - /// - /// o The root of the target document - whereupon the specified value - /// becomes the entire content of the target document. - /// - /// o A member to add to an existing object - whereupon the supplied - /// value is added to that object at the indicated location. If the - /// member already exists, it is replaced by the specified value. - /// - /// o An element to add to an existing array - whereupon the supplied - /// value is added to the array at the indicated location. Any - /// elements at or above the specified index are shifted one position - /// to the right. The specified index MUST NOT be greater than the - /// number of elements in the array. If the "-" character is used to - /// index the end of the array (see [RFC6901]), this has the effect of - /// appending the value to the array. - /// - /// Because this operation is designed to add to existing objects and - /// arrays, its target location will often not exist. Although the - /// pointer's error handling algorithm will thus be invoked, this - /// specification defines the error handling behavior for "add" pointers - /// to ignore that error and add the value as specified. - /// - /// However, the object itself or an array containing it does need to - /// exist, and it remains an error for that not to be the case. For - /// example, an "add" with a target location of "/a/b" starting with this - /// document: - /// - /// { "a": { "foo": 1 } } - /// - /// is not an error, because "a" exists, and "b" will be added to its - /// value. It is an error in this document: - /// - /// { "q": { "bar": 2 } } - /// - /// because "a" does not exist. - /// - /// The add operation. - /// Object to apply the operation to. public void Add(Operation operation, object objectToApplyTo) { if (operation == null) @@ -153,29 +93,6 @@ namespace Microsoft.AspNetCore.JsonPatch.Adapters } } - /// - /// The "move" operation removes the value at a specified location and - /// adds it to the target location. - /// - /// The operation object MUST contain a "from" member, which is a string - /// containing a JSON Pointer value that references the location in the - /// target document to move the value from. - /// - /// The "from" location MUST exist for the operation to be successful. - /// - /// For example: - /// - /// { "op": "move", "from": "/a/b/c", "path": "/a/b/d" } - /// - /// This operation is functionally identical to a "remove" operation on - /// the "from" location, followed immediately by an "add" operation at - /// the target location with the value that was just removed. - /// - /// The "from" location MUST NOT be a proper prefix of the "path" - /// location; i.e., a location cannot be moved into one of its children. - /// - /// The move operation. - /// Object to apply the operation to. public void Move(Operation operation, object objectToApplyTo) { if (operation == null) @@ -202,20 +119,6 @@ namespace Microsoft.AspNetCore.JsonPatch.Adapters } } - /// - /// The "remove" operation removes the value at the target location. - /// - /// The target location MUST exist for the operation to be successful. - /// - /// For example: - /// - /// { "op": "remove", "path": "/a/b/c" } - /// - /// If removing an element from an array, any elements above the - /// specified index are shifted one position to the left. - /// - /// The remove operation. - /// Object to apply the operation to. public void Remove(Operation operation, object objectToApplyTo) { if (operation == null) @@ -259,26 +162,6 @@ namespace Microsoft.AspNetCore.JsonPatch.Adapters } } - /// - /// The "replace" operation replaces the value at the target location - /// with a new value. The operation object MUST contain a "value" member - /// whose content specifies the replacement value. - /// - /// The target location MUST exist for the operation to be successful. - /// - /// For example: - /// - /// { "op": "replace", "path": "/a/b/c", "value": 42 } - /// - /// This operation is functionally identical to a "remove" operation for - /// a value, followed immediately by an "add" operation at the same - /// location with the replacement value. - /// - /// Note: even though it's the same functionally, we do not call remove + add - /// for performance reasons (multiple checks of same requirements). - /// - /// The replace operation. - /// Object to apply the operation to. public void Replace(Operation operation, object objectToApplyTo) { if (operation == null) @@ -310,28 +193,6 @@ namespace Microsoft.AspNetCore.JsonPatch.Adapters } } - /// - /// The "copy" operation copies the value at a specified location to the - /// target location. - /// - /// The operation object MUST contain a "from" member, which is a string - /// containing a JSON Pointer value that references the location in the - /// target document to copy the value from. - /// - /// The "from" location MUST exist for the operation to be successful. - /// - /// For example: - /// - /// { "op": "copy", "from": "/a/b/c", "path": "/a/b/e" } - /// - /// This operation is functionally identical to an "add" operation at the - /// target location using the value specified in the "from" member. - /// - /// Note: even though it's the same functionally, we do not call add with - /// the value specified in from for performance reasons (multiple checks of same requirements). - /// - /// The copy operation. - /// Object to apply the operation to. public void Copy(Operation operation, object objectToApplyTo) { if (operation == null) @@ -365,6 +226,37 @@ namespace Microsoft.AspNetCore.JsonPatch.Adapters } } + public void Test(Operation operation, object objectToApplyTo) + { + if (operation == null) + { + throw new ArgumentNullException(nameof(operation)); + } + + if (objectToApplyTo == null) + { + throw new ArgumentNullException(nameof(objectToApplyTo)); + } + + var parsedPath = new ParsedPath(operation.path); + var visitor = new ObjectVisitor(parsedPath, ContractResolver); + + var target = objectToApplyTo; + if (!visitor.TryVisit(ref target, out var adapter, out var errorMessage)) + { + var error = CreatePathNotFoundError(objectToApplyTo, operation.path, operation, errorMessage); + ErrorReporter(error); + return; + } + + if (!adapter.TryTest(target, parsedPath.LastSegment, ContractResolver, operation.value, out errorMessage)) + { + var error = CreateOperationFailedError(objectToApplyTo, operation.path, operation, errorMessage); + ErrorReporter(error); + return; + } + } + private bool TryGetValue( string fromLocation, object objectToGetValueFrom, diff --git a/src/Microsoft.AspNetCore.JsonPatch/Internal/DictionaryAdapterOfTU.cs b/src/Microsoft.AspNetCore.JsonPatch/Internal/DictionaryAdapterOfTU.cs index 67fd33e231..8a344e24ee 100644 --- a/src/Microsoft.AspNetCore.JsonPatch/Internal/DictionaryAdapterOfTU.cs +++ b/src/Microsoft.AspNetCore.JsonPatch/Internal/DictionaryAdapterOfTU.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; namespace Microsoft.AspNetCore.JsonPatch.Internal @@ -126,6 +128,55 @@ namespace Microsoft.AspNetCore.JsonPatch.Internal return true; } + public bool TryTest( + object target, + string segment, + IContractResolver contractResolver, + object value, + out string errorMessage) + { + var contract = (JsonDictionaryContract)contractResolver.ResolveContract(target.GetType()); + var key = contract.DictionaryKeyResolver(segment); + var dictionary = (IDictionary)target; + + if (!TryConvertKey(key, out var convertedKey, out errorMessage)) + { + return false; + } + + // As per JsonPatch spec, the target location must exist for test to be successful + if (!dictionary.ContainsKey(convertedKey)) + { + errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment); + return false; + } + + if (!TryConvertValue(value, out var convertedValue, out errorMessage)) + { + return false; + } + + var currentValue = dictionary[convertedKey]; + + // The target segment does not have an assigned value to compare the test value with + if (currentValue == null || string.IsNullOrEmpty(currentValue.ToString())) + { + errorMessage = Resources.FormatValueForTargetSegmentCannotBeNullOrEmpty(segment); + return false; + } + + if (!JToken.DeepEquals(JsonConvert.SerializeObject(currentValue), JsonConvert.SerializeObject(convertedValue))) + { + errorMessage = Resources.FormatValueNotEqualToTestValue(currentValue, value, segment); + return false; + } + else + { + errorMessage = null; + return true; + } + } + public bool TryTraverse( object target, string segment, diff --git a/src/Microsoft.AspNetCore.JsonPatch/Internal/DynamicObjectAdapter.cs b/src/Microsoft.AspNetCore.JsonPatch/Internal/DynamicObjectAdapter.cs index 433326ce53..fb4adeb1f2 100644 --- a/src/Microsoft.AspNetCore.JsonPatch/Internal/DynamicObjectAdapter.cs +++ b/src/Microsoft.AspNetCore.JsonPatch/Internal/DynamicObjectAdapter.cs @@ -6,6 +6,8 @@ using System.Collections.Generic; using System.Reflection; using System.Runtime.CompilerServices; using Microsoft.CSharp.RuntimeBinder; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; using CSharpBinder = Microsoft.CSharp.RuntimeBinder; @@ -103,6 +105,36 @@ namespace Microsoft.AspNetCore.JsonPatch.Internal return true; } + public bool TryTest( + object target, + string segment, + IContractResolver contractResolver, + object value, + out string errorMessage) + { + if (!TryGetDynamicObjectProperty(target, contractResolver, segment, out var property, out errorMessage)) + { + return false; + } + + if (!TryConvertValue(value, property.GetType(), out var convertedValue)) + { + errorMessage = Resources.FormatInvalidValueForProperty(value); + return false; + } + + if (!JToken.DeepEquals(JsonConvert.SerializeObject(property), JsonConvert.SerializeObject(convertedValue))) + { + errorMessage = Resources.FormatValueNotEqualToTestValue(property, value, segment); + return false; + } + else + { + errorMessage = null; + return true; + } + } + public bool TryTraverse( object target, string segment, diff --git a/src/Microsoft.AspNetCore.JsonPatch/Internal/IAdapter.cs b/src/Microsoft.AspNetCore.JsonPatch/Internal/IAdapter.cs index 1866b42ed4..ec28131f7d 100644 --- a/src/Microsoft.AspNetCore.JsonPatch/Internal/IAdapter.cs +++ b/src/Microsoft.AspNetCore.JsonPatch/Internal/IAdapter.cs @@ -40,5 +40,12 @@ namespace Microsoft.AspNetCore.JsonPatch.Internal IContractResolver contractResolver, object value, out string errorMessage); + + bool TryTest( + object target, + string segment, + IContractResolver contractResolver, + object value, + out string errorMessage); } } diff --git a/src/Microsoft.AspNetCore.JsonPatch/Internal/ListAdapter.cs b/src/Microsoft.AspNetCore.JsonPatch/Internal/ListAdapter.cs index a1c0498e47..d1348fd5c6 100644 --- a/src/Microsoft.AspNetCore.JsonPatch/Internal/ListAdapter.cs +++ b/src/Microsoft.AspNetCore.JsonPatch/Internal/ListAdapter.cs @@ -5,6 +5,8 @@ using System; using System.Collections; using System.Collections.Generic; using Microsoft.Extensions.Internal; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; namespace Microsoft.AspNetCore.JsonPatch.Internal @@ -150,6 +152,43 @@ namespace Microsoft.AspNetCore.JsonPatch.Internal return true; } + public bool TryTest( + object target, + string segment, + IContractResolver contractResolver, + object value, + out string errorMessage) + { + var list = (IList)target; + + if (!TryGetListTypeArgument(list, out var typeArgument, out errorMessage)) + { + return false; + } + + if (!TryGetPositionInfo(list, segment, OperationType.Replace, out var positionInfo, out errorMessage)) + { + return false; + } + + if (!TryConvertValue(value, typeArgument, segment, out var convertedValue, out errorMessage)) + { + return false; + } + + var currentValue = list[positionInfo.Index]; + if (!JToken.DeepEquals(JsonConvert.SerializeObject(currentValue), JsonConvert.SerializeObject(convertedValue))) + { + errorMessage = Resources.FormatValueAtListPositionNotEqualToTestValue(currentValue, value, positionInfo.Index); + return false; + } + else + { + errorMessage = null; + return true; + } + } + public bool TryTraverse( object target, string segment, diff --git a/src/Microsoft.AspNetCore.JsonPatch/Internal/ObjectVisitor.cs b/src/Microsoft.AspNetCore.JsonPatch/Internal/ObjectVisitor.cs index 97f108c0a0..8994f0aa52 100644 --- a/src/Microsoft.AspNetCore.JsonPatch/Internal/ObjectVisitor.cs +++ b/src/Microsoft.AspNetCore.JsonPatch/Internal/ObjectVisitor.cs @@ -3,9 +3,6 @@ using System; using System.Collections; -using System.Collections.Generic; -using System.Dynamic; -using Microsoft.AspNetCore.JsonPatch.Adapters; using Newtonsoft.Json.Serialization; namespace Microsoft.AspNetCore.JsonPatch.Internal @@ -30,7 +27,7 @@ namespace Microsoft.AspNetCore.JsonPatch.Internal return false; } - adapter = SelectAdapater(target); + adapter = SelectAdapter(target); // Traverse until the penultimate segment to get the target object and adapter for (var i = 0; i < _path.Segments.Count - 1; i++) @@ -42,14 +39,14 @@ namespace Microsoft.AspNetCore.JsonPatch.Internal } target = next; - adapter = SelectAdapater(target); + adapter = SelectAdapter(target); } errorMessage = null; return true; } - private IAdapter SelectAdapater(object targetObject) + private IAdapter SelectAdapter(object targetObject) { var jsonContract = _contractResolver.ResolveContract(targetObject.GetType()); diff --git a/src/Microsoft.AspNetCore.JsonPatch/Internal/PocoAdapter.cs b/src/Microsoft.AspNetCore.JsonPatch/Internal/PocoAdapter.cs index eab16a3035..0eee0fc889 100644 --- a/src/Microsoft.AspNetCore.JsonPatch/Internal/PocoAdapter.cs +++ b/src/Microsoft.AspNetCore.JsonPatch/Internal/PocoAdapter.cs @@ -4,6 +4,8 @@ using System; using System.Linq; using System.Reflection; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; namespace Microsoft.AspNetCore.JsonPatch.Internal @@ -132,6 +134,43 @@ namespace Microsoft.AspNetCore.JsonPatch.Internal return true; } + public bool TryTest( + object target, + string segment, + IContractResolver + contractResolver, + object value, + out string errorMessage) + { + if (!TryGetJsonProperty(target, contractResolver, segment, out var jsonProperty)) + { + errorMessage = Resources.FormatTargetLocationAtPathSegmentNotFound(segment); + return false; + } + + if (!jsonProperty.Readable) + { + errorMessage = Resources.FormatCannotReadProperty(segment); + return false; + } + + if (!TryConvertValue(value, jsonProperty.PropertyType, out var convertedValue)) + { + errorMessage = Resources.FormatInvalidValueForProperty(value); + return false; + } + + var currentValue = jsonProperty.ValueProvider.GetValue(target); + if (!JToken.DeepEquals(JsonConvert.SerializeObject(currentValue), JsonConvert.SerializeObject(convertedValue))) + { + errorMessage = Resources.FormatValueNotEqualToTestValue(currentValue, value, segment); + return false; + } + + errorMessage = null; + return true; + } + public bool TryTraverse( object target, string segment, diff --git a/src/Microsoft.AspNetCore.JsonPatch/JsonPatchDocument.cs b/src/Microsoft.AspNetCore.JsonPatch/JsonPatchDocument.cs index 4da2bdbe1e..4420e576bf 100644 --- a/src/Microsoft.AspNetCore.JsonPatch/JsonPatchDocument.cs +++ b/src/Microsoft.AspNetCore.JsonPatch/JsonPatchDocument.cs @@ -99,6 +99,24 @@ namespace Microsoft.AspNetCore.JsonPatch return this; } + /// + /// Test value. Will result in, for example, + /// { "op": "test", "path": "/a/b/c", "value": 42 } + /// + /// target location + /// value + /// + public JsonPatchDocument Test(string path, object value) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation("test", PathHelpers.NormalizePath(path), null, value)); + return this; + } + /// /// Removes value at specified location and add it to the target location. Will result in, for example: /// { "op": "move", "from": "/a/b/c", "path": "/a/b/d" } diff --git a/src/Microsoft.AspNetCore.JsonPatch/JsonPatchDocumentOfT.cs b/src/Microsoft.AspNetCore.JsonPatch/JsonPatchDocumentOfT.cs index 5b9df2b52b..8ae1430185 100644 --- a/src/Microsoft.AspNetCore.JsonPatch/JsonPatchDocumentOfT.cs +++ b/src/Microsoft.AspNetCore.JsonPatch/JsonPatchDocumentOfT.cs @@ -247,6 +247,77 @@ namespace Microsoft.AspNetCore.JsonPatch return this; } + /// + /// Test value. Will result in, for example, + /// { "op": "test", "path": "/a/b/c", "value": 42 } + /// + /// target location + /// value + /// + public JsonPatchDocument Test(Expression> path, TProp value) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "test", + GetPath(path, null), + from: null, + value: value)); + + return this; + } + + /// + /// Test value in a list at given position + /// + /// value type + /// target location + /// value + /// position + /// + public JsonPatchDocument Test(Expression>> path, + TProp value, int position) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "test", + GetPath(path, position.ToString()), + from: null, + value: value)); + + return this; + } + + /// + /// Test value at end of a list + /// + /// value type + /// target location + /// value + /// + public JsonPatchDocument Test(Expression>> path, TProp value) + { + if (path == null) + { + throw new ArgumentNullException(nameof(path)); + } + + Operations.Add(new Operation( + "test", + GetPath(path, "-"), + from: null, + value: value)); + + return this; + } + /// /// Removes value at specified location and add it to the target location. Will result in, for example: /// { "op": "move", "from": "/a/b/c", "path": "/a/b/d" } diff --git a/src/Microsoft.AspNetCore.JsonPatch/Operations/Operation.cs b/src/Microsoft.AspNetCore.JsonPatch/Operations/Operation.cs index ea81b0c668..690ade4776 100644 --- a/src/Microsoft.AspNetCore.JsonPatch/Operations/Operation.cs +++ b/src/Microsoft.AspNetCore.JsonPatch/Operations/Operation.cs @@ -58,7 +58,15 @@ namespace Microsoft.AspNetCore.JsonPatch.Operations adapter.Copy(this, objectToApplyTo); break; case OperationType.Test: - throw new NotSupportedException(Resources.TestOperationNotSupported); + if (adapter is IObjectAdapterWithTest adapterWithTest) + { + adapterWithTest.Test(this, objectToApplyTo); + break; + } + else + { + throw new NotSupportedException(Resources.TestOperationNotSupported); + } default: break; } diff --git a/src/Microsoft.AspNetCore.JsonPatch/Operations/OperationOfT.cs b/src/Microsoft.AspNetCore.JsonPatch/Operations/OperationOfT.cs index 7189cf8210..bd13528775 100644 --- a/src/Microsoft.AspNetCore.JsonPatch/Operations/OperationOfT.cs +++ b/src/Microsoft.AspNetCore.JsonPatch/Operations/OperationOfT.cs @@ -74,7 +74,15 @@ namespace Microsoft.AspNetCore.JsonPatch.Operations adapter.Copy(this, objectToApplyTo); break; case OperationType.Test: - throw new JsonPatchException(new JsonPatchError(objectToApplyTo, this, Resources.TestOperationNotSupported)); + if (adapter is IObjectAdapterWithTest adapterWithTest) + { + adapterWithTest.Test(this, objectToApplyTo); + break; + } + else + { + throw new JsonPatchException(new JsonPatchError(objectToApplyTo, this, Resources.TestOperationNotSupported)); + } case OperationType.Invalid: throw new JsonPatchException( Resources.FormatInvalidJsonPatchOperation(op), innerException: null); @@ -82,6 +90,5 @@ namespace Microsoft.AspNetCore.JsonPatch.Operations break; } } - } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.JsonPatch/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.JsonPatch/Properties/Resources.Designer.cs index 432fab2cbf..cfb8e087d0 100644 --- a/src/Microsoft.AspNetCore.JsonPatch/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.JsonPatch/Properties/Resources.Designer.cs @@ -81,7 +81,7 @@ namespace Microsoft.AspNetCore.JsonPatch => string.Format(CultureInfo.CurrentCulture, GetString("CannotUpdateProperty"), p0); /// - /// The expression '{0}' is not supported. + /// The expression '{0}' is not supported. Supported expressions include member access and indexer expressions. /// internal static string ExpressionTypeNotSupported { @@ -89,7 +89,7 @@ namespace Microsoft.AspNetCore.JsonPatch } /// - /// The expression '{0}' is not supported. + /// The expression '{0}' is not supported. Supported expressions include member access and indexer expressions. /// internal static string FormatExpressionTypeNotSupported(object p0) => string.Format(CultureInfo.CurrentCulture, GetString("ExpressionTypeNotSupported"), p0); @@ -276,6 +276,48 @@ namespace Microsoft.AspNetCore.JsonPatch internal static string FormatTestOperationNotSupported() => GetString("TestOperationNotSupported"); + /// + /// The current value '{0}' at position '{2}' is not equal to the test value '{1}'. + /// + internal static string ValueAtListPositionNotEqualToTestValue + { + get => GetString("ValueAtListPositionNotEqualToTestValue"); + } + + /// + /// The current value '{0}' at position '{2}' is not equal to the test value '{1}'. + /// + internal static string FormatValueAtListPositionNotEqualToTestValue(object p0, object p1, object p2) + => string.Format(CultureInfo.CurrentCulture, GetString("ValueAtListPositionNotEqualToTestValue"), p0, p1, p2); + + /// + /// The value at '{0}' cannot be null or empty to perform the test operation. + /// + internal static string ValueForTargetSegmentCannotBeNullOrEmpty + { + get => GetString("ValueForTargetSegmentCannotBeNullOrEmpty"); + } + + /// + /// The value at '{0}' cannot be null or empty to perform the test operation. + /// + internal static string FormatValueForTargetSegmentCannotBeNullOrEmpty(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("ValueForTargetSegmentCannotBeNullOrEmpty"), p0); + + /// + /// The current value '{0}' at path '{2}' is not equal to the test value '{1}'. + /// + internal static string ValueNotEqualToTestValue + { + get => GetString("ValueNotEqualToTestValue"); + } + + /// + /// The current value '{0}' at path '{2}' is not equal to the test value '{1}'. + /// + internal static string FormatValueNotEqualToTestValue(object p0, object p1, object p2) + => string.Format(CultureInfo.CurrentCulture, GetString("ValueNotEqualToTestValue"), p0, p1, p2); + private static string GetString(string name, params string[] formatterNames) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNetCore.JsonPatch/Resources.resx b/src/Microsoft.AspNetCore.JsonPatch/Resources.resx index bf07e4a31e..3763e4a841 100644 --- a/src/Microsoft.AspNetCore.JsonPatch/Resources.resx +++ b/src/Microsoft.AspNetCore.JsonPatch/Resources.resx @@ -174,4 +174,13 @@ The test operation is not supported. + + The current value '{0}' at position '{2}' is not equal to the test value '{1}'. + + + The value at '{0}' cannot be null or empty to perform the test operation. + + + The current value '{0}' at path '{2}' is not equal to the test value '{1}'. + \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.JsonPatch.Test/DictionaryAdapterTest.cs b/test/Microsoft.AspNetCore.JsonPatch.Test/Adapters/DictionaryAdapterTest.cs similarity index 88% rename from test/Microsoft.AspNetCore.JsonPatch.Test/DictionaryAdapterTest.cs rename to test/Microsoft.AspNetCore.JsonPatch.Test/Adapters/DictionaryAdapterTest.cs index cfb99d61ce..d1d2216c2c 100644 --- a/test/Microsoft.AspNetCore.JsonPatch.Test/DictionaryAdapterTest.cs +++ b/test/Microsoft.AspNetCore.JsonPatch.Test/Adapters/DictionaryAdapterTest.cs @@ -274,5 +274,42 @@ namespace Microsoft.AspNetCore.JsonPatch.Internal Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); Assert.Empty(dictionary); } + + [Fact] + public void Test_DoesNotThrowException_IfTestIsSuccessful() + { + // Arrange + var key = "Name"; + var dictionary = new Dictionary(); + dictionary[key] = "James"; + var dictionaryAdapter = new DictionaryAdapter(); + var resolver = new DefaultContractResolver(); + + // Act + var testStatus = dictionaryAdapter.TryTest(dictionary, key, resolver, "James", out var message); + + //Assert + Assert.True(testStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + } + + [Fact] + public void Test_ThrowsJsonPatchException_IfTestFails() + { + // Arrange + var key = "Name"; + var dictionary = new Dictionary(); + dictionary[key] = "James"; + var dictionaryAdapter = new DictionaryAdapter(); + var resolver = new DefaultContractResolver(); + var expectedErrorMessage = "The current value 'James' at path 'Name' is not equal to the test value 'John'."; + + // Act + var testStatus = dictionaryAdapter.TryTest(dictionary, key, resolver, "John", out var errorMessage); + + //Assert + Assert.False(testStatus); + Assert.Equal(expectedErrorMessage, errorMessage); + } } } diff --git a/test/Microsoft.AspNetCore.JsonPatch.Test/DynamicObjectAdapterTest.cs b/test/Microsoft.AspNetCore.JsonPatch.Test/Adapters/DynamicObjectAdapterTest.cs similarity index 85% rename from test/Microsoft.AspNetCore.JsonPatch.Test/DynamicObjectAdapterTest.cs rename to test/Microsoft.AspNetCore.JsonPatch.Test/Adapters/DynamicObjectAdapterTest.cs index 18bb7a775c..9ed9a51ed0 100644 --- a/test/Microsoft.AspNetCore.JsonPatch.Test/DynamicObjectAdapterTest.cs +++ b/test/Microsoft.AspNetCore.JsonPatch.Test/Adapters/DynamicObjectAdapterTest.cs @@ -227,5 +227,42 @@ namespace Microsoft.AspNetCore.JsonPatch.Internal Assert.False(removeStatus); Assert.Equal($"The target location specified by path segment '{segment}' was not found.", removeErrorMessage); } + + [Fact] + public void TryTest_DoesNotThrowException_IfTestSuccessful() + { + var adapter = new DynamicObjectAdapter(); + dynamic target = new DynamicTestObject(); + target.NewProperty = "Joana"; + var segment = "NewProperty"; + var resolver = new DefaultContractResolver(); + + // Act + var testStatus = adapter.TryTest(target, segment, resolver, "Joana", out string errorMessage); + + // Assert + Assert.Equal("Joana", target.NewProperty); + Assert.True(testStatus); + Assert.True(string.IsNullOrEmpty(errorMessage), "Expected no error message"); + } + + [Fact] + public void TryTest_ThrowsJsonPatchException_IfTestFails() + { + // Arrange + var adapter = new DynamicObjectAdapter(); + dynamic target = new DynamicTestObject(); + target.NewProperty = "Joana"; + var segment = "NewProperty"; + var resolver = new DefaultContractResolver(); + var expectedErrorMessage = $"The current value 'Joana' at path '{segment}' is not equal to the test value 'John'."; + + // Act + var testStatus = adapter.TryTest(target, segment, resolver, "John", out string errorMessage); + + // Assert + Assert.False(testStatus); + Assert.Equal(expectedErrorMessage, errorMessage); + } } } diff --git a/test/Microsoft.AspNetCore.JsonPatch.Test/ListAdapterTest.cs b/test/Microsoft.AspNetCore.JsonPatch.Test/Adapters/ListAdapterTest.cs similarity index 89% rename from test/Microsoft.AspNetCore.JsonPatch.Test/ListAdapterTest.cs rename to test/Microsoft.AspNetCore.JsonPatch.Test/Adapters/ListAdapterTest.cs index 3c692ea150..25d8ae387b 100644 --- a/test/Microsoft.AspNetCore.JsonPatch.Test/ListAdapterTest.cs +++ b/test/Microsoft.AspNetCore.JsonPatch.Test/Adapters/ListAdapterTest.cs @@ -459,5 +459,55 @@ namespace Microsoft.AspNetCore.JsonPatch.Internal Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); Assert.Equal(expected, targetObject); } + + [Fact] + public void Test_DoesNotThrowException_IfTestIsSuccessful() + { + // Arrange + var resolver = new Mock(MockBehavior.Strict); + var targetObject = new List() { 10, 20 }; + var listAdapter = new ListAdapter(); + + // Act + var testStatus = listAdapter.TryTest(targetObject, "0", resolver.Object, "10", out var message); + + //Assert + Assert.True(testStatus); + Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); + } + + [Fact] + public void Test_ThrowsJsonPatchException_IfTestFails() + { + // Arrange + var resolver = new Mock(MockBehavior.Strict); + var targetObject = new List() { 10, 20 }; + var listAdapter = new ListAdapter(); + var expectedErrorMessage = "The current value '20' at position '1' is not equal to the test value '10'."; + + // Act + var testStatus = listAdapter.TryTest(targetObject, "1", resolver.Object, "10", out var errorMessage); + + //Assert + Assert.False(testStatus); + Assert.Equal(expectedErrorMessage, errorMessage); + } + + [Fact] + public void Test_ThrowsJsonPatchException_IfListPositionOutOfBounds() + { + // Arrange + var resolver = new Mock(MockBehavior.Strict); + var targetObject = new List() { 10, 20 }; + var listAdapter = new ListAdapter(); + var expectedErrorMessage = "The index value provided by path segment '2' is out of bounds of the array size."; + + // Act + var testStatus = listAdapter.TryTest(targetObject, "2", resolver.Object, "10", out var errorMessage); + + //Assert + Assert.False(testStatus); + Assert.Equal(expectedErrorMessage, errorMessage); + } } } diff --git a/test/Microsoft.AspNetCore.JsonPatch.Test/Adapters/PocoAdapterTest.cs b/test/Microsoft.AspNetCore.JsonPatch.Test/Adapters/PocoAdapterTest.cs new file mode 100644 index 0000000000..9a31ea11a8 --- /dev/null +++ b/test/Microsoft.AspNetCore.JsonPatch.Test/Adapters/PocoAdapterTest.cs @@ -0,0 +1,241 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Newtonsoft.Json.Serialization; +using Xunit; + +namespace Microsoft.AspNetCore.JsonPatch.Internal +{ + public class PocoAdapterTest + { + [Fact] + public void TryAdd_ReplacesExistingProperty() + { + // Arrange + var adapter = new PocoAdapter(); + var contractResolver = new DefaultContractResolver(); + var model = new Customer + { + Name = "Joana" + }; + + // Act + var addStatus = adapter.TryAdd(model, "Name", contractResolver, "John", out var errorMessage); + + // Assert + Assert.Equal("John", model.Name); + Assert.True(addStatus); + Assert.True(string.IsNullOrEmpty(errorMessage), "Expected no error message"); + } + + [Fact] + public void TryAdd_ThrowsJsonPatchException_IfPropertyDoesNotExist() + { + // Arrange + var adapter = new PocoAdapter(); + var contractResolver = new DefaultContractResolver(); + var model = new Customer + { + Name = "Joana" + }; + var expectedErrorMessage = "The target location specified by path segment 'LastName' was not found."; + + // Act + var addStatus = adapter.TryAdd(model, "LastName", contractResolver, "Smith", out var errorMessage); + + // Assert + Assert.False(addStatus); + Assert.Equal(expectedErrorMessage, errorMessage); + } + + [Fact] + public void TryGet_ExistingProperty() + { + // Arrange + var adapter = new PocoAdapter(); + var contractResolver = new DefaultContractResolver(); + var model = new Customer + { + Name = "Joana" + }; + + // Act + var getStatus = adapter.TryGet(model, "Name", contractResolver, out var value, out var errorMessage); + + // Assert + Assert.Equal("Joana", value); + Assert.True(getStatus); + Assert.True(string.IsNullOrEmpty(errorMessage), "Expected no error message"); + } + + [Fact] + public void TryGet_ThrowsJsonPatchException_IfPropertyDoesNotExist() + { + // Arrange + var adapter = new PocoAdapter(); + var contractResolver = new DefaultContractResolver(); + var model = new Customer + { + Name = "Joana" + }; + var expectedErrorMessage = "The target location specified by path segment 'LastName' was not found."; + + // Act + var getStatus = adapter.TryGet(model, "LastName", contractResolver, out var value, out var errorMessage); + + // Assert + Assert.Null(value); + Assert.False(getStatus); + Assert.Equal(expectedErrorMessage, errorMessage); + } + + [Fact] + public void TryRemove_SetsPropertyToNull() + { + // Arrange + var adapter = new PocoAdapter(); + var contractResolver = new DefaultContractResolver(); + var model = new Customer + { + Name = "Joana" + }; + + // Act + var removeStatus = adapter.TryRemove(model, "Name", contractResolver, out var errorMessage); + + // Assert + Assert.Null(model.Name); + Assert.True(removeStatus); + Assert.True(string.IsNullOrEmpty(errorMessage), "Expected no error message"); + } + + [Fact] + public void TryRemove_ThrowsJsonPatchException_IfPropertyDoesNotExist() + { + // Arrange + var adapter = new PocoAdapter(); + var contractResolver = new DefaultContractResolver(); + var model = new Customer + { + Name = "Joana" + }; + var expectedErrorMessage = "The target location specified by path segment 'LastName' was not found."; + + // Act + var removeStatus = adapter.TryRemove(model, "LastName", contractResolver, out var errorMessage); + + // Assert + Assert.False(removeStatus); + Assert.Equal(expectedErrorMessage, errorMessage); + } + + [Fact] + public void TryReplace_OverwritesExistingValue() + { + // Arrange + var adapter = new PocoAdapter(); + var contractResolver = new DefaultContractResolver(); + var model = new Customer + { + Name = "Joana" + }; + + // Act + var replaceStatus = adapter.TryReplace(model, "Name", contractResolver, "John", out var errorMessage); + + // Assert + Assert.Equal("John", model.Name); + Assert.True(replaceStatus); + Assert.True(string.IsNullOrEmpty(errorMessage), "Expected no error message"); + } + + [Fact] + public void TryReplace_ThrowsJsonPatchException_IfNewValueIsInvalidType() + { + // Arrange + var adapter = new PocoAdapter(); + var contractResolver = new DefaultContractResolver(); + var model = new Customer + { + Age = 25 + }; + + var expectedErrorMessage = "The value 'TwentySix' is invalid for target location."; + + // Act + var replaceStatus = adapter.TryReplace(model, "Age", contractResolver, "TwentySix", out var errorMessage); + + // Assert + Assert.Equal(25, model.Age); + Assert.False(replaceStatus); + Assert.Equal(expectedErrorMessage, errorMessage); + } + + [Fact] + public void TryReplace_ThrowsJsonPatchException_IfPropertyDoesNotExist() + { + // Arrange + var adapter = new PocoAdapter(); + var contractResolver = new DefaultContractResolver(); + var model = new Customer + { + Name = "Joana" + }; + var expectedErrorMessage = "The target location specified by path segment 'LastName' was not found."; + + // Act + var replaceStatus = adapter.TryReplace(model, "LastName", contractResolver, "Smith", out var errorMessage); + + // Assert + Assert.Equal("Joana", model.Name); + Assert.False(replaceStatus); + Assert.Equal(expectedErrorMessage, errorMessage); + } + + [Fact] + public void TryTest_DoesNotThrowException_IfTestSuccessful() + { + var adapter = new PocoAdapter(); + var contractResolver = new DefaultContractResolver(); + var model = new Customer + { + Name = "Joana" + }; + + // Act + var testStatus = adapter.TryTest(model, "Name", contractResolver, "Joana", out var errorMessage); + + // Assert + Assert.Equal("Joana", model.Name); + Assert.True(testStatus); + Assert.True(string.IsNullOrEmpty(errorMessage), "Expected no error message"); + } + + [Fact] + public void TryTest_ThrowsJsonPatchException_IfTestFails() + { + // Arrange + var adapter = new PocoAdapter(); + var contractResolver = new DefaultContractResolver(); + var model = new Customer + { + Name = "Joana" + }; + var expectedErrorMessage = "The current value 'Joana' at path 'Name' is not equal to the test value 'John'."; + + // Act + var testStatus = adapter.TryTest(model, "Name", contractResolver, "John", out var errorMessage); + + // Assert + Assert.False(testStatus); + Assert.Equal(expectedErrorMessage, errorMessage); + } + + private class Customer + { + public string Name { get; set; } + + public int Age { get; set; } + } + } +} diff --git a/test/Microsoft.AspNetCore.JsonPatch.Test/IntegrationTests/DynamicObjectIntegrationTests.cs b/test/Microsoft.AspNetCore.JsonPatch.Test/IntegrationTests/DynamicObjectTest.cs similarity index 86% rename from test/Microsoft.AspNetCore.JsonPatch.Test/IntegrationTests/DynamicObjectIntegrationTests.cs rename to test/Microsoft.AspNetCore.JsonPatch.Test/IntegrationTests/DynamicObjectTest.cs index 4b5fc96e24..be7738419f 100644 --- a/test/Microsoft.AspNetCore.JsonPatch.Test/IntegrationTests/DynamicObjectIntegrationTests.cs +++ b/test/Microsoft.AspNetCore.JsonPatch.Test/IntegrationTests/DynamicObjectTest.cs @@ -7,7 +7,7 @@ using Xunit; namespace Microsoft.AspNetCore.JsonPatch.Internal { - public class DynamicObjectIntegrationTests + public class DynamicObjectTest { [Fact] public void AddResults_ShouldReplaceExistingPropertyValue_InNestedDynamicObject() @@ -259,5 +259,45 @@ namespace Microsoft.AspNetCore.JsonPatch.Internal // Assert Assert.Equal(new List() { 4, 5, 6 }, dynamicTestObject.IntegerList); } + + [Fact] + public void TestPropertyValue_FromListToNonList_InNestedTypedObject_InDynamicObject() + { + // Arrange + dynamic dynamicTestObject = new DynamicTestObject(); + dynamicTestObject.Nested = new SimpleObject() + { + IntegerList = new List() { 1, 2, 3 } + }; + + var patchDoc = new JsonPatchDocument(); + patchDoc.Test("Nested/IntegerList/1", 2); + + // Act & Assert + patchDoc.ApplyTo(dynamicTestObject); + } + + [Fact] + public void TestPropertyValue_FromListToNonList_InNestedTypedObject_InDynamicObject_ThrowsJsonPatchException_IfTestFails() + { + // Arrange + dynamic dynamicTestObject = new DynamicTestObject(); + dynamicTestObject.Nested = new SimpleObject() + { + IntegerList = new List() { 1, 2, 3 } + }; + + var patchDoc = new JsonPatchDocument(); + patchDoc.Test("Nested/IntegerList/0", 2); + + // Act + var exception = Assert.Throws(() => + { + patchDoc.ApplyTo(dynamicTestObject); + }); + + // Assert + Assert.Equal("The current value '1' at position '0' is not equal to the test value '2'.", exception.Message); + } } } diff --git a/test/Microsoft.AspNetCore.JsonPatch.Test/JsonPatchDocumentTest.cs b/test/Microsoft.AspNetCore.JsonPatch.Test/JsonPatchDocumentTest.cs index 723ef15b64..d754e8da5b 100644 --- a/test/Microsoft.AspNetCore.JsonPatch.Test/JsonPatchDocumentTest.cs +++ b/test/Microsoft.AspNetCore.JsonPatch.Test/JsonPatchDocumentTest.cs @@ -9,42 +9,6 @@ namespace Microsoft.AspNetCore.JsonPatch.Test { public class JsonPatchDocumentTest { - [Fact] - public void TestOperation_ThrowsException_CallsIntoLogErrorAction() - { - // Arrange - var serialized = "[{\"value\":\"John\",\"path\":\"/Name\",\"op\":\"test\"}]"; - var jsonPatchDocument = JsonConvert.DeserializeObject>(serialized); - var model = new Customer(); - var expectedErrorMessage = "The test operation is not supported."; - string actualErrorMessage = null; - - // Act - jsonPatchDocument.ApplyTo(model, (jsonPatchError) => - { - actualErrorMessage = jsonPatchError.ErrorMessage; - }); - - // Assert - Assert.Equal(expectedErrorMessage, actualErrorMessage); - } - - [Fact] - public void TestOperation_NoLogErrorAction_ThrowsJsonPatchException() - { - // Arrange - var serialized = "[{\"value\":\"John\",\"path\":\"/Name\",\"op\":\"test\"}]"; - var jsonPatchDocument = JsonConvert.DeserializeObject>(serialized); - var model = new Customer(); - var expectedErrorMessage = "The test operation is not supported."; - - // Act - var jsonPatchException = Assert.Throws(() => jsonPatchDocument.ApplyTo(model)); - - // Assert - Assert.Equal(expectedErrorMessage, jsonPatchException.Message); - } - [Fact] public void InvalidOperation_ThrowsException_CallsIntoLogErrorAction() {