From 054b46c1cce2bc90eea177a0fecfcc19448ad2b8 Mon Sep 17 00:00:00 2001 From: KevinDockx Date: Wed, 12 Aug 2015 21:39:37 +0200 Subject: [PATCH] =?UTF-8?q?Implement=20new=20Remove=20op=20&=20fix=20value?= =?UTF-8?q?.GetTyp=C3=AA=20issue=20in=20Add=20op?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Adapters/ObjectAdapter.cs | 257 ++++++-- .../Helpers/RemovedPropertyTypeResult.cs | 38 ++ .../Properties/Resources.Designer.cs | 82 +-- src/Microsoft.AspNet.JsonPatch/Resources.resx | 3 + .../Dynamic/AddOperationTests.cs | 569 ++++++++++++++++++ .../Dynamic/AddTypedOperationTests.cs | 121 ++++ .../Dynamic/NestedDTO.cs | 11 + .../Dynamic/PatchDocumentTests.cs | 95 +++ .../Dynamic/RemoveOperationTests.cs | 302 ++++++++++ .../Dynamic/RemoveTypedOperationTests.cs | 244 ++++++++ .../Dynamic/SimpleDTO.cs | 21 + .../Dynamic/SimpleDTOWithNestedDTO.cs | 22 + .../NestedObjectTests.cs | 13 +- .../ObjectAdapterTests.cs | 10 +- 14 files changed, 1683 insertions(+), 105 deletions(-) create mode 100644 src/Microsoft.AspNet.JsonPatch/Helpers/RemovedPropertyTypeResult.cs create mode 100644 test/Microsoft.AspNet.JsonPatch.Test/Dynamic/AddOperationTests.cs create mode 100644 test/Microsoft.AspNet.JsonPatch.Test/Dynamic/AddTypedOperationTests.cs create mode 100644 test/Microsoft.AspNet.JsonPatch.Test/Dynamic/NestedDTO.cs create mode 100644 test/Microsoft.AspNet.JsonPatch.Test/Dynamic/PatchDocumentTests.cs create mode 100644 test/Microsoft.AspNet.JsonPatch.Test/Dynamic/RemoveOperationTests.cs create mode 100644 test/Microsoft.AspNet.JsonPatch.Test/Dynamic/RemoveTypedOperationTests.cs create mode 100644 test/Microsoft.AspNet.JsonPatch.Test/Dynamic/SimpleDTO.cs create mode 100644 test/Microsoft.AspNet.JsonPatch.Test/Dynamic/SimpleDTOWithNestedDTO.cs diff --git a/src/Microsoft.AspNet.JsonPatch/Adapters/ObjectAdapter.cs b/src/Microsoft.AspNet.JsonPatch/Adapters/ObjectAdapter.cs index ca1e4ac34b..cfaaebb96c 100644 --- a/src/Microsoft.AspNet.JsonPatch/Adapters/ObjectAdapter.cs +++ b/src/Microsoft.AspNet.JsonPatch/Adapters/ObjectAdapter.cs @@ -161,7 +161,7 @@ namespace Microsoft.AspNet.JsonPatch.Adapters { // get the actual type var propertyValue = container.GetValueForCaseInsensitiveKey(treeAnalysisResult.PropertyPathInParent); - var typeOfPathProperty = value.GetType(); + var typeOfPathProperty = propertyValue.GetType(); if (!IsNonStringArray(typeOfPathProperty)) { @@ -447,101 +447,240 @@ namespace Microsoft.AspNet.JsonPatch.Adapters Remove(operation.path, objectToApplyTo, operation); } + /// /// Remove is used by various operations (eg: remove, move, ...), yet through different operations; - /// This method allows code reuse yet reporting the correct operation on error + /// This method allows code reuse yet reporting the correct operation on error. The return value + /// contains the type of the item that has been removed (and a bool possibly signifying an error) + /// This can be used by other methods, like replace, to ensure that we can pass in the correctly + /// typed value to whatever method follows. /// - private void Remove( - [NotNull] string path, - [NotNull] object objectToApplyTo, - [NotNull] Operation operationToReport) + private RemovedPropertyTypeResult Remove(string path, object objectToApplyTo, Operation operationToReport) { - var removeFromList = false; - var positionAsInteger = -1; - var actualPathToProperty = path; + // get path result + var pathResult = GetActualPropertyPath( + path, + objectToApplyTo, + operationToReport); - if (path.EndsWith("/-")) + if (pathResult == null) { - removeFromList = true; - actualPathToProperty = path.Substring(0, path.Length - 2); + return new RemovedPropertyTypeResult(null, true); } - else - { - positionAsInteger = GetNumericEnd(path); - if (positionAsInteger > -1) + var removeFromList = pathResult.ExecuteAtEnd; + var positionAsInteger = pathResult.NumericEnd; + var actualPathToProperty = pathResult.PathToProperty; + + var treeAnalysisResult = new ObjectTreeAnalysisResult( + objectToApplyTo, + actualPathToProperty, + ContractResolver); + + if (!treeAnalysisResult.IsValidPathForRemove) + { + LogError(new JsonPatchError( + objectToApplyTo, + operationToReport, + Resources.FormatPropertyCannotBeRemoved(path))); + return new RemovedPropertyTypeResult(null, true); + } + + if (treeAnalysisResult.UseDynamicLogic) + { + // if it's not an array, we can remove the property from + // the dictionary. If it's an array, we need to check the position first. + if (removeFromList || positionAsInteger > -1) { - actualPathToProperty = path.Substring(0, - path.IndexOf('/' + positionAsInteger.ToString())); - } - } + var propertyValue = treeAnalysisResult.Container + .GetValueForCaseInsensitiveKey(treeAnalysisResult.PropertyPathInParent); - var patchProperty = FindPropertyAndParent(objectToApplyTo, actualPathToProperty); + // we cannot continue when the value is null, because to be able to + // continue we need to be able to check if the array is a non-string array + if (propertyValue == null) + { + LogError(new JsonPatchError( + objectToApplyTo, + operationToReport, + Resources.FormatCannotDeterminePropertyType(path))); + return new RemovedPropertyTypeResult(null, true); + } - // does the target location exist? - if (!CheckIfPropertyExists(patchProperty, objectToApplyTo, operationToReport, path)) - { - return; - } + var typeOfPathProperty = propertyValue.GetType(); - // get the property, and remove it - in this case, for DTO's, that means setting - // it to null or its default value; in case of an array, remove at provided index - // or at the end. - if (removeFromList || positionAsInteger > -1) - { - // what if it's an array but there's no position?? - if (IsNonStringArray(patchProperty.Property.PropertyType)) - { - // now, get the generic type of the IList<> from Property type. - var genericTypeOfArray = GetIListType(patchProperty.Property.PropertyType); + if (!IsNonStringArray(typeOfPathProperty)) + { + LogError(new JsonPatchError( + objectToApplyTo, + operationToReport, + Resources.FormatInvalidIndexForArrayProperty(operationToReport.op, path))); + return new RemovedPropertyTypeResult(null, true); + } - // get value (it can be cast, we just checked that) - var array = (IList)patchProperty.Property.ValueProvider.GetValue(patchProperty.Parent); + // now, get the generic type of the enumerable (we'll return this type) + var genericTypeOfArray = GetIListType(typeOfPathProperty); + + // get the array + var array = (IList)treeAnalysisResult.Container.GetValueForCaseInsensitiveKey( + treeAnalysisResult.PropertyPathInParent); + + if (array.Count == 0) + { + // if the array is empty, we should throw an error + LogError(new JsonPatchError( + objectToApplyTo, + operationToReport, + Resources.FormatInvalidIndexForArrayProperty( + operationToReport.op, + path))); + return new RemovedPropertyTypeResult(null, true); + } if (removeFromList) { array.RemoveAt(array.Count - 1); + treeAnalysisResult.Container.SetValueForCaseInsensitiveKey( + treeAnalysisResult.PropertyPathInParent, array); + + // return the type of the value that has been removed. + return new RemovedPropertyTypeResult(genericTypeOfArray, false); } else { - if (positionAsInteger < array.Count) - { - array.RemoveAt(positionAsInteger); - } - else + if (positionAsInteger >= array.Count) { LogError(new JsonPatchError( objectToApplyTo, operationToReport, - Resources.FormatInvalidIndexForArrayProperty(operationToReport.op, path))); - - return; + Resources.FormatInvalidIndexForArrayProperty( + operationToReport.op, + path))); + return new RemovedPropertyTypeResult(null, true); } + + array.RemoveAt(positionAsInteger); + treeAnalysisResult.Container.SetValueForCaseInsensitiveKey( + treeAnalysisResult.PropertyPathInParent, array); + + // return the type of the value that has been removed. + return new RemovedPropertyTypeResult(genericTypeOfArray, false); } } else { - LogError(new JsonPatchError( - objectToApplyTo, - operationToReport, - Resources.FormatInvalidPathForArrayProperty(operationToReport.op, path))); + // get the property + var getResult = treeAnalysisResult.Container.GetValueForCaseInsensitiveKey( + treeAnalysisResult.PropertyPathInParent); - return; + // remove the property + treeAnalysisResult.Container.RemoveValueForCaseInsensitiveKey( + treeAnalysisResult.PropertyPathInParent); + + // value is not null, we can determine the type + if (getResult != null) + { + var actualType = getResult.GetType(); + return new RemovedPropertyTypeResult(actualType, false); + } + else + { + return new RemovedPropertyTypeResult(null, false); + } } } else { - // setting the value to "null" will use the default value in case of value types, and - // null in case of reference types - object value = null; + // not dynamic + var patchProperty = treeAnalysisResult.JsonPatchProperty; - if (patchProperty.Property.PropertyType.GetTypeInfo().IsValueType - && Nullable.GetUnderlyingType(patchProperty.Property.PropertyType) == null) + if (removeFromList || positionAsInteger > -1) { - value = Activator.CreateInstance(patchProperty.Property.PropertyType); - } + if (!IsNonStringArray(patchProperty.Property.PropertyType)) + { + LogError(new JsonPatchError( + objectToApplyTo, + operationToReport, + Resources.FormatInvalidIndexForArrayProperty(operationToReport.op, path))); + return new RemovedPropertyTypeResult(null, true); + } - patchProperty.Property.ValueProvider.SetValue(patchProperty.Parent, value); + // now, get the generic type of the IList<> from Property type. + var genericTypeOfArray = GetIListType(patchProperty.Property.PropertyType); + + if (!patchProperty.Property.Readable) + { + LogError(new JsonPatchError( + objectToApplyTo, + operationToReport, + Resources.FormatCannotReadProperty(path))); + return new RemovedPropertyTypeResult(null, true); + } + + var array = (IList)patchProperty.Property.ValueProvider + .GetValue(patchProperty.Parent); + + if (array.Count == 0) + { + // if the array is empty, we should throw an error + LogError(new JsonPatchError( + objectToApplyTo, + operationToReport, + Resources.FormatInvalidIndexForArrayProperty( + operationToReport.op, + path))); + return new RemovedPropertyTypeResult(null, true); + } + + if (removeFromList) + { + array.RemoveAt(array.Count - 1); + + // return the type of the value that has been removed + return new RemovedPropertyTypeResult(genericTypeOfArray, false); + } + else + { + if (positionAsInteger >= array.Count) + { + LogError(new JsonPatchError( + objectToApplyTo, + operationToReport, + Resources.FormatInvalidIndexForArrayProperty( + operationToReport.op, + path))); + return null; + } + + array.RemoveAt(positionAsInteger); + + // return the type of the value that has been removed + return new RemovedPropertyTypeResult(genericTypeOfArray, false); + } + } + else + { + if (!patchProperty.Property.Writable) + { + LogError(new JsonPatchError( + objectToApplyTo, + operationToReport, + Resources.FormatCannotUpdateProperty(path))); + return new RemovedPropertyTypeResult(null, true); + } + + // setting the value to "null" will use the default value in case of value types, and + // null in case of reference types + object value = null; + + if (patchProperty.Property.PropertyType.GetTypeInfo().IsValueType + && Nullable.GetUnderlyingType(patchProperty.Property.PropertyType) == null) + { + value = Activator.CreateInstance(patchProperty.Property.PropertyType); + } + + patchProperty.Property.ValueProvider.SetValue(patchProperty.Parent, value); + return new RemovedPropertyTypeResult(patchProperty.Property.PropertyType, false); + } } } diff --git a/src/Microsoft.AspNet.JsonPatch/Helpers/RemovedPropertyTypeResult.cs b/src/Microsoft.AspNet.JsonPatch/Helpers/RemovedPropertyTypeResult.cs new file mode 100644 index 0000000000..9f5b97a868 --- /dev/null +++ b/src/Microsoft.AspNet.JsonPatch/Helpers/RemovedPropertyTypeResult.cs @@ -0,0 +1,38 @@ +// 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 System; + +namespace Microsoft.AspNet.JsonPatch.Helpers +{ + /// + /// Return value for Remove operation. The combination tells us what to do next (if this operation + /// is called from inside another operation, eg: Replace, Copy. + /// + /// Possible combo: + /// - ActualType contains type: operation succesfully completed, can continue when called from inside + /// another operation + /// - ActualType null and HasError true: operation not completed succesfully, should not be allowed to continue + /// - ActualType null and HasError false: operation completed succesfully, but we should not be allowed to + /// continue when called from inside another method as we could not verify the type of the removed property. + /// This happens when the value of an item in an ExpandoObject dictionary is null. + /// + internal class RemovedPropertyTypeResult + { + /// + /// The type of the removed property (value) + /// + public Type ActualType { get; private set; } + + /// + /// HasError: true when an error occurred, the operation didn't complete succesfully + /// + public bool HasError { get; set; } + + public RemovedPropertyTypeResult(Type actualType, bool hasError) + { + ActualType = actualType; + HasError = hasError; + } + } +} diff --git a/src/Microsoft.AspNet.JsonPatch/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.JsonPatch/Properties/Resources.Designer.cs index 68a888f7eb..f0ee7f2439 100644 --- a/src/Microsoft.AspNet.JsonPatch/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.JsonPatch/Properties/Resources.Designer.cs @@ -10,6 +10,22 @@ namespace Microsoft.AspNet.JsonPatch private static readonly ResourceManager _resourceManager = new ResourceManager("Microsoft.AspNet.JsonPatch.Resources", typeof(Resources).GetTypeInfo().Assembly); + /// + /// The type of the property at path '{0}' could not be determined. + /// + internal static string CannotDeterminePropertyType + { + get { return GetString("CannotDeterminePropertyType"); } + } + + /// + /// The type of the property at path '{0}' could not be determined. + /// + internal static string FormatCannotDeterminePropertyType(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("CannotDeterminePropertyType"), p0); + } + /// /// The property at '{0}' could not be read. /// @@ -42,6 +58,22 @@ namespace Microsoft.AspNet.JsonPatch return string.Format(CultureInfo.CurrentCulture, GetString("CannotUpdateProperty"), p0); } + /// + /// The key '{0}' was not found. + /// + internal static string DictionaryKeyNotFound + { + get { return GetString("DictionaryKeyNotFound"); } + } + + /// + /// The key '{0}' was not found. + /// + internal static string FormatDictionaryKeyNotFound(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("DictionaryKeyNotFound"), p0); + } + /// /// For operation '{0}' on array property at path '{1}', the index is larger than the array size. /// @@ -58,23 +90,6 @@ namespace Microsoft.AspNet.JsonPatch return string.Format(CultureInfo.CurrentCulture, GetString("InvalidIndexForArrayProperty"), p0, p1); } - /// - /// For operation '{0}' on array property at path '{1}', the index is negative. - /// - internal static string NegativeIndexForArrayProperty - { - get { return GetString("NegativeIndexForArrayProperty"); } - } - - /// - /// For operation '{0}' on array property at path '{1}', the index is negative. - /// - internal static string FormatNegativeIndexForArrayProperty(object p0, object p1) - { - return string.Format(CultureInfo.CurrentCulture, GetString("NegativeIndexForArrayProperty"), p0, p1); - } - - /// /// The type '{0}' was malformed and could not be parsed. /// @@ -139,6 +154,22 @@ namespace Microsoft.AspNet.JsonPatch return string.Format(CultureInfo.CurrentCulture, GetString("InvalidValueForProperty"), p0, p1); } + /// + /// For operation '{0}' on array property at path '{1}', the index is negative. + /// + internal static string NegativeIndexForArrayProperty + { + get { return GetString("NegativeIndexForArrayProperty"); } + } + + /// + /// For operation '{0}' on array property at path '{1}', the index is negative. + /// + internal static string FormatNegativeIndexForArrayProperty(object p0, object p1) + { + return string.Format(CultureInfo.CurrentCulture, GetString("NegativeIndexForArrayProperty"), p0, p1); + } + /// /// '{0}' must be of type '{1}'. /// @@ -203,23 +234,6 @@ namespace Microsoft.AspNet.JsonPatch return string.Format(CultureInfo.CurrentCulture, GetString("PropertyDoesNotExist"), p0); } - /// - /// The key '{0}' was not found. - /// - internal static string DictionaryKeyNotFound - { - get { return GetString("DictionaryKeyNotFound"); } - } - - /// - /// The key '{0}' was not found. - /// - internal static string FormatDictionaryKeyNotFound(object p0) - { - return string.Format(CultureInfo.CurrentCulture, GetString("DictionaryKeyNotFound"), p0); - } - - /// /// The test operation is not supported. /// diff --git a/src/Microsoft.AspNet.JsonPatch/Resources.resx b/src/Microsoft.AspNet.JsonPatch/Resources.resx index 47c2291a9d..59ae2b59c3 100644 --- a/src/Microsoft.AspNet.JsonPatch/Resources.resx +++ b/src/Microsoft.AspNet.JsonPatch/Resources.resx @@ -117,6 +117,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + The type of the property at path '{0}' could not be determined. + The property at '{0}' could not be read. diff --git a/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/AddOperationTests.cs b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/AddOperationTests.cs new file mode 100644 index 0000000000..3c2523c5fb --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/AddOperationTests.cs @@ -0,0 +1,569 @@ +// 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 System; +using System.Collections.Generic; +using System.Dynamic; +using Microsoft.AspNet.JsonPatch.Exceptions; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNet.JsonPatch.Test.Dynamic +{ + public class AddOperationTests + { + [Fact] + public void AddNewPropertyShouldFailIfRootIsNotAnExpandoObject() + { + dynamic doc = new + { + Test = 1 + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("NewInt", 1); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "The property at path '/NewInt' could not be added.", + exception.Message); + } + + [Fact] + public void AddNewProperty() + { + dynamic obj = new ExpandoObject(); + obj.Test = 1; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("NewInt", 1); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(obj); + + Assert.Equal(1, obj.NewInt); + Assert.Equal(1, obj.Test); + } + + [Fact] + public void AddNewPropertyToNestedAnonymousObjectShouldFail() + { + dynamic doc = new + { + Test = 1, + nested = new { } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("Nested/NewInt", 1); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "The property at path '/Nested/NewInt' could not be added.", + exception.Message); + } + + [Fact] + public void AddNewPropertyToTypedObjectShouldFail() + { + dynamic doc = new + { + Test = 1, + nested = new NestedDTO() + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("Nested/NewInt", 1); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "The property at path '/Nested/NewInt' could not be added.", + exception.Message); + } + + [Fact] + public void AddToExistingPropertyOnNestedObject() + { + dynamic doc = new + { + Test = 1, + nested = new NestedDTO() + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("Nested/StringProperty", "A"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal("A", doc.nested.StringProperty); + Assert.Equal(1, doc.Test); + } + + [Fact] + public void AddNewPropertyToExpandoOject() + { + dynamic doc = new + { + Test = 1, + nested = new ExpandoObject() + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("Nested/NewInt", 1); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(1, doc.nested.NewInt); + Assert.Equal(1, doc.Test); + } + + [Fact] + public void AddNewPropertyToExpandoOjectInTypedObject() + { + var doc = new NestedDTO() + { + DynamicProperty = new ExpandoObject() + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("DynamicProperty/NewInt", 1); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(1, doc.DynamicProperty.NewInt); + } + + [Fact] + public void AddNewPropertyToTypedObjectInExpandoObject() + { + dynamic dynamicProperty = new ExpandoObject(); + dynamicProperty.StringProperty = "A"; + + var doc = new NestedDTO() + { + DynamicProperty = dynamicProperty + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("DynamicProperty/StringProperty", "B"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal("B", doc.DynamicProperty.StringProperty); + } + + [Fact] + public void AddNewPropertyToAnonymousObjectShouldFail() + { + dynamic doc = new + { + Test = 1 + }; + + dynamic valueToAdd = new { IntValue = 1, StringValue = "test", GuidValue = Guid.NewGuid() }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("ComplexProperty", valueToAdd); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "The property at path '/ComplexProperty' could not be added.", + exception.Message); + } + + [Fact] + public void AddResultsReplaceShouldFailOnAnonymousDueToNoSetter() + { + var doc = new + { + StringProperty = "A" + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("StringProperty", "B"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "The property at path '/StringProperty' could not be updated.", + exception.Message); + } + + [Fact] + public void AddResultsShouldReplace() + { + dynamic doc = new ExpandoObject(); + doc.StringProperty = "A"; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("StringProperty", "B"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal("B", doc.StringProperty); + } + + [Fact] + public void AddResultsShouldReplaceInNested() + { + dynamic doc = new ExpandoObject(); + doc.InBetweenFirst = new ExpandoObject(); + doc.InBetweenFirst.InBetweenSecond = new ExpandoObject(); + doc.InBetweenFirst.InBetweenSecond.StringProperty = "A"; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("/InBetweenFirst/InBetweenSecond/StringProperty", "B"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal("B", doc.InBetweenFirst.InBetweenSecond.StringProperty); + } + + [Fact] + public void AddResultsShouldReplaceInNestedInDynamic() + { + dynamic doc = new ExpandoObject(); + doc.Nested = new NestedDTO(); + doc.Nested.DynamicProperty = new ExpandoObject(); + doc.Nested.DynamicProperty.InBetweenFirst = new ExpandoObject(); + doc.Nested.DynamicProperty.InBetweenFirst.InBetweenSecond = new ExpandoObject(); + doc.Nested.DynamicProperty.InBetweenFirst.InBetweenSecond.StringProperty = "A"; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("/Nested/DynamicProperty/InBetweenFirst/InBetweenSecond/StringProperty", "B"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal("B", doc.Nested.DynamicProperty.InBetweenFirst.InBetweenSecond.StringProperty); + } + + [Fact] + public void ShouldNotBeAbleToAddToNonExistingPropertyThatIsNotTheRoot() + { + //Adding to a Nonexistent Target + // + // An example target JSON document: + // { "foo": "bar" } + // A JSON Patch document: + // [ + // { "op": "add", "path": "/baz/bat", "value": "qux" } + // ] + // This JSON Patch document, applied to the target JSON document above, + // would result in an error (therefore, it would not be applied), + // because the "add" operation's target location that references neither + // the root of the document, nor a member of an existing object, nor a + // member of an existing array. + + var doc = new NestedDTO() + { + DynamicProperty = new ExpandoObject() + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("DynamicProperty/OtherProperty/IntProperty", 1); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "The property at path '/DynamicProperty/OtherProperty/IntProperty' could not be added.", + exception.Message); + } + + [Fact] + public void ShouldNotBeAbleToAddToNonExistingPropertyInNestedPropertyThatIsNotTheRoot() + { + //Adding to a Nonexistent Target + // + // An example target JSON document: + // { "foo": "bar" } + // A JSON Patch document: + // [ + // { "op": "add", "path": "/baz/bat", "value": "qux" } + // ] + // This JSON Patch document, applied to the target JSON document above, + // would result in an error (therefore, it would not be applied), + // because the "add" operation's target location that references neither + // the root of the document, nor a member of an existing object, nor a + // member of an existing array. + + var doc = new + { + Foo = "bar" + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("baz/bat", "qux"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "The property at path '/baz/bat' could not be added.", + exception.Message); + } + + [Fact] + public void ShouldReplacePropertyWithDifferentCase() + { + dynamic doc = new ExpandoObject(); + doc.StringProperty = "A"; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("stringproperty", "B"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal("B", doc.StringProperty); + } + + [Fact] + public void AddToList() + { + var doc = new + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("IntegerList/0", 4); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 4, 1, 2, 3 }, doc.IntegerList); + } + + [Fact] + public void AddToListNegativePosition() + { + dynamic doc = new ExpandoObject(); + doc.IntegerList = new List() { 1, 2, 3 }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("IntegerList/-1", 4); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "For operation 'add' on array property at path '/IntegerList/-1', the index is negative.", + exception.Message); + } + + [Fact] + public void ShouldAddToListWithDifferentCase() + { + var doc = new + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("integerlist/0", 4); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 4, 1, 2, 3 }, doc.IntegerList); + } + + [Fact] + public void AddToListInvalidPositionTooLarge() + { + var doc = new + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("IntegerList/4", 4); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "For operation 'add' on array property at path '/IntegerList/4', the index is larger than the array size.", + exception.Message); + } + + [Fact] + public void AddToListAtEndWithSerialization() + { + var doc = new + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("IntegerList/3", 4); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 1, 2, 3, 4 }, doc.IntegerList); + } + + [Fact] + public void AddToListAtBeginning() + { + var doc = new + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("IntegerList/0", 4); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 4, 1, 2, 3 }, doc.IntegerList); + } + + [Fact] + public void AddToListInvalidPositionTooSmall() + { + var doc = new + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("IntegerList/-1", 4); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "For operation 'add' on array property at path '/IntegerList/-1', the index is negative.", + exception.Message); + } + + [Fact] + public void AddToListAppend() + { + var doc = new + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("IntegerList/-", 4); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 1, 2, 3, 4 }, doc.IntegerList); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/AddTypedOperationTests.cs b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/AddTypedOperationTests.cs new file mode 100644 index 0000000000..4d2e8c646d --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/AddTypedOperationTests.cs @@ -0,0 +1,121 @@ +// 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 System.Collections.Generic; +using Microsoft.AspNet.JsonPatch.Exceptions; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNet.JsonPatch.Test.Dynamic +{ + public class AddTypedOperationTests + { + [Fact] + public void AddToListNegativePosition() + { + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("IntegerList/-1", 4); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "For operation 'add' on array property at path '/IntegerList/-1', the index is negative.", + exception.Message); + } + + [Fact] + public void AddToListInList() + { + var doc = new SimpleDTOWithNestedDTO() + { + ListOfSimpleDTO = new List() + { + new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("ListOfSimpleDTO/0/IntegerList/0", 4); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + Assert.Equal(new List() { 4, 1, 2, 3 }, doc.ListOfSimpleDTO[0].IntegerList); + } + + [Fact] + public void AddToListInListInvalidPositionTooSmall() + { + var doc = new SimpleDTOWithNestedDTO() + { + ListOfSimpleDTO = new List() + { + new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("ListOfSimpleDTO/-1/IntegerList/0", 4); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "The property at path '/ListOfSimpleDTO/-1/IntegerList/0' could not be added.", + exception.Message); + } + + [Fact] + public void AddToListInListInvalidPositionTooLarge() + { + var doc = new SimpleDTOWithNestedDTO() + { + ListOfSimpleDTO = new List() + { + new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + } + }; + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Add("ListOfSimpleDTO/20/IntegerList/0", 4); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "The property at path '/ListOfSimpleDTO/20/IntegerList/0' could not be added.", + exception.Message); + } + } +} diff --git a/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/NestedDTO.cs b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/NestedDTO.cs new file mode 100644 index 0000000000..4d30cf60f2 --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/NestedDTO.cs @@ -0,0 +1,11 @@ +// 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. + +namespace Microsoft.AspNet.JsonPatch.Test.Dynamic +{ + public class NestedDTO + { + public string StringProperty { get; set; } + public dynamic DynamicProperty { get; set; } + } +} diff --git a/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/PatchDocumentTests.cs b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/PatchDocumentTests.cs new file mode 100644 index 0000000000..e15f85da8e --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/PatchDocumentTests.cs @@ -0,0 +1,95 @@ +// 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.AspNet.JsonPatch.Exceptions; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNet.JsonPatch.Test.Dynamic +{ + public class PatchDocumentTests + { + [Fact] + public void InvalidPathAtBeginningShouldThrowException() + { + JsonPatchDocument patchDoc = new JsonPatchDocument(); + var exception = Assert.Throws(() => + { + patchDoc.Add("//NewInt", 1); + }); + Assert.Equal( + "The provided string '//NewInt' is an invalid path.", + exception.Message); + } + + [Fact] + public void InvalidPathAtEndShouldThrowException() + { + JsonPatchDocument patchDoc = new JsonPatchDocument(); + var exception = Assert.Throws(() => + { + patchDoc.Add("NewInt//", 1); + }); + Assert.Equal( + "The provided string 'NewInt//' is an invalid path.", + exception.Message); + } + + [Fact] + public void InvalidPathWithDotShouldThrowException() + { + JsonPatchDocument patchDoc = new JsonPatchDocument(); + var exception = Assert.Throws(() => + { + patchDoc.Add("NewInt.Test", 1); + }); + Assert.Equal( + "The provided string 'NewInt.Test' is an invalid path.", + exception.Message); + } + + [Fact] + public void NonGenericPatchDocToGenericMustSerialize() + { + var doc = new SimpleDTO() + { + StringProperty = "A", + AnotherStringProperty = "B" + }; + + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Copy("StringProperty", "AnotherStringProperty"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject>(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal("A", doc.AnotherStringProperty); + } + + [Fact] + public void GenericPatchDocToNonGenericMustSerialize() + { + var doc = new SimpleDTO() + { + StringProperty = "A", + AnotherStringProperty = "B" + }; + + JsonPatchDocument patchDocTyped = new JsonPatchDocument(); + patchDocTyped.Copy(o => o.StringProperty, o => o.AnotherStringProperty); + + JsonPatchDocument patchDocUntyped = new JsonPatchDocument(); + patchDocUntyped.Copy("StringProperty", "AnotherStringProperty"); + + var serializedTyped = JsonConvert.SerializeObject(patchDocTyped); + var serializedUntyped = JsonConvert.SerializeObject(patchDocUntyped); + var deserialized = JsonConvert.DeserializeObject(serializedTyped); + + deserialized.ApplyTo(doc); + + Assert.Equal("A", doc.AnotherStringProperty); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/RemoveOperationTests.cs b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/RemoveOperationTests.cs new file mode 100644 index 0000000000..9ef28ad7cc --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/RemoveOperationTests.cs @@ -0,0 +1,302 @@ +// 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 System.Collections.Generic; +using System.Dynamic; +using Microsoft.AspNet.JsonPatch.Exceptions; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNet.JsonPatch.Test.Dynamic +{ + public class RemoveOperationTests + { + [Fact] + public void RemovePropertyShouldFailIfRootIsAnonymous() + { + dynamic doc = new + { + Test = 1 + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("Test"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "The property at path '/Test' could not be updated.", + exception.Message); + } + + [Fact] + public void RemovePropertyShouldFailIfItDoesntExist() + { + dynamic doc = new ExpandoObject(); + doc.Test = 1; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("NonExisting"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "The property at path '/NonExisting' could not be removed.", + exception.Message); + } + + [Fact] + public void RemovePropertyFromExpandoObject() + { + dynamic obj = new ExpandoObject(); + obj.Test = 1; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("Test"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(obj); + + var cont = obj as IDictionary; + object valueFromDictionary; + + cont.TryGetValue("Test", out valueFromDictionary); + Assert.Null(valueFromDictionary); + } + + [Fact] + public void RemovePropertyFromExpandoObjectMixedCase() + { + dynamic obj = new ExpandoObject(); + obj.Test = 1; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("test"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(obj); + + var cont = obj as IDictionary; + object valueFromDictionary; + + cont.TryGetValue("Test", out valueFromDictionary); + Assert.Null(valueFromDictionary); + } + + [Fact] + public void RemoveNestedPropertyFromExpandoObject() + { + dynamic obj = new ExpandoObject(); + obj.Test = new ExpandoObject(); + obj.Test.AnotherTest = "A"; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("Test"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(obj); + + var cont = obj as IDictionary; + object valueFromDictionary; + + cont.TryGetValue("Test", out valueFromDictionary); + Assert.Null(valueFromDictionary); + } + + [Fact] + public void RemoveNestedPropertyFromExpandoObjectMixedCase() + { + dynamic obj = new ExpandoObject(); + obj.Test = new ExpandoObject(); + obj.Test.AnotherTest = "A"; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("test"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(obj); + var cont = obj as IDictionary; + + object valueFromDictionary; + cont.TryGetValue("Test", out valueFromDictionary); + Assert.Null(valueFromDictionary); + } + + [Fact] + public void NestedRemove() + { + dynamic doc = new ExpandoObject(); + doc.SimpleDTO = new SimpleDTO() + { + StringProperty = "A" + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("SimpleDTO/StringProperty"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + Assert.Equal(null, doc.SimpleDTO.StringProperty); + } + + [Fact] + public void NestedRemoveMixedCase() + { + dynamic doc = new ExpandoObject(); + doc.SimpleDTO = new SimpleDTO() + { + StringProperty = "A" + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("Simpledto/stringProperty"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(null, doc.SimpleDTO.StringProperty); + } + + [Fact] + public void NestedRemoveFromList() + { + dynamic doc = new ExpandoObject(); + doc.SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("SimpleDTO/IntegerList/2"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 1, 2 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void NestedRemoveFromListMixedCase() + { + dynamic doc = new ExpandoObject(); + doc.SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("SimpleDTO/Integerlist/2"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 1, 2 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void NestedRemoveFromListInvalidPositionTooLarge() + { + dynamic doc = new ExpandoObject(); + doc.SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("SimpleDTO/IntegerList/3"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "For operation 'remove' on array property at path '/SimpleDTO/IntegerList/3', the index is larger than the array size.", + exception.Message); + } + + [Fact] + public void NestedRemoveFromListInvalidPositionTooSmall() + { + dynamic doc = new ExpandoObject(); + doc.SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("SimpleDTO/IntegerList/-1"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "For operation 'remove' on array property at path '/SimpleDTO/IntegerList/-1', the index is negative.", + exception.Message); + } + + [Fact] + public void NestedRemoveFromEndOfList() + { + dynamic doc = new ExpandoObject(); + doc.SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("SimpleDTO/IntegerList/-"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + Assert.Equal(new List() { 1, 2 }, doc.SimpleDTO.IntegerList); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/RemoveTypedOperationTests.cs b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/RemoveTypedOperationTests.cs new file mode 100644 index 0000000000..db5d6502d1 --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/RemoveTypedOperationTests.cs @@ -0,0 +1,244 @@ +// 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 System.Collections.Generic; +using Microsoft.AspNet.JsonPatch.Exceptions; +using Newtonsoft.Json; +using Xunit; + +namespace Microsoft.AspNet.JsonPatch.Test.Dynamic +{ + public class RemoveTypedOperationTests + { + [Fact] + public void Remove() + { + var doc = new SimpleDTO() + { + StringProperty = "A" + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("StringProperty"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(null, doc.StringProperty); + } + + [Fact] + public void RemoveFromList() + { + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("IntegerList/2"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 1, 2 }, doc.IntegerList); + } + + [Fact] + public void RemoveFromListInvalidPositionTooLarge() + { + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("IntegerList/3"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "For operation 'remove' on array property at path '/IntegerList/3', the index is larger than the array size.", + exception.Message); + } + + [Fact] + public void RemoveFromListInvalidPositionTooSmall() + { + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("IntegerList/-1"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "For operation 'remove' on array property at path '/IntegerList/-1', the index is negative.", + exception.Message); + } + + [Fact] + public void RemoveFromEndOfList() + { + var doc = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("IntegerList/-"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 1, 2 }, doc.IntegerList); + } + + [Fact] + public void NestedRemove() + { + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + StringProperty = "A" + } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("SimpleDTO/StringProperty"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(null, doc.SimpleDTO.StringProperty); + } + + [Fact] + public void NestedRemoveFromList() + { + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("SimpleDTO/IntegerList/2"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 1, 2 }, doc.SimpleDTO.IntegerList); + } + + [Fact] + public void NestedRemoveFromListInvalidPositionTooLarge() + { + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("SimpleDTO/IntegerList/3"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "For operation 'remove' on array property at path '/SimpleDTO/IntegerList/3', the index is larger than the array size.", + exception.Message); + } + + [Fact] + public void NestedRemoveFromListInvalidPositionTooSmall() + { + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("SimpleDTO/IntegerList/-1"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + var exception = Assert.Throws(() => + { + deserialized.ApplyTo(doc); + }); + Assert.Equal( + "For operation 'remove' on array property at path '/SimpleDTO/IntegerList/-1', the index is negative.", + exception.Message); + } + + [Fact] + public void NestedRemoveFromEndOfList() + { + var doc = new SimpleDTOWithNestedDTO() + { + SimpleDTO = new SimpleDTO() + { + IntegerList = new List() { 1, 2, 3 } + } + }; + + // create patch + JsonPatchDocument patchDoc = new JsonPatchDocument(); + patchDoc.Remove("SimpleDTO/IntegerList/-"); + + var serialized = JsonConvert.SerializeObject(patchDoc); + var deserialized = JsonConvert.DeserializeObject(serialized); + + deserialized.ApplyTo(doc); + + Assert.Equal(new List() { 1, 2 }, doc.SimpleDTO.IntegerList); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/SimpleDTO.cs b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/SimpleDTO.cs new file mode 100644 index 0000000000..a6938dd992 --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/SimpleDTO.cs @@ -0,0 +1,21 @@ +// 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 System; +using System.Collections.Generic; + +namespace Microsoft.AspNet.JsonPatch.Test.Dynamic +{ + public class SimpleDTO + { + public List SimpleDTOList { get; set; } + public List IntegerList { get; set; } + public int IntegerValue { get; set; } + public string StringProperty { get; set; } + public string AnotherStringProperty { get; set; } + public decimal DecimalValue { get; set; } + public double DoubleValue { get; set; } + public float FloatValue { get; set; } + public Guid GuidValue { get; set; } + } +} diff --git a/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/SimpleDTOWithNestedDTO.cs b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/SimpleDTOWithNestedDTO.cs new file mode 100644 index 0000000000..2147fcceb1 --- /dev/null +++ b/test/Microsoft.AspNet.JsonPatch.Test/Dynamic/SimpleDTOWithNestedDTO.cs @@ -0,0 +1,22 @@ +// 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 System.Collections.Generic; + +namespace Microsoft.AspNet.JsonPatch.Test.Dynamic +{ + public class SimpleDTOWithNestedDTO + { + public int IntegerValue { get; set; } + public NestedDTO NestedDTO { get; set; } + public SimpleDTO SimpleDTO { get; set; } + public List ListOfSimpleDTO { get; set; } + + public SimpleDTOWithNestedDTO() + { + NestedDTO = new NestedDTO(); + SimpleDTO = new SimpleDTO(); + ListOfSimpleDTO = new List(); + } + } +} diff --git a/test/Microsoft.AspNet.JsonPatch.Test/NestedObjectTests.cs b/test/Microsoft.AspNet.JsonPatch.Test/NestedObjectTests.cs index a1f73ec1bf..33649b7788 100644 --- a/test/Microsoft.AspNet.JsonPatch.Test/NestedObjectTests.cs +++ b/test/Microsoft.AspNet.JsonPatch.Test/NestedObjectTests.cs @@ -778,7 +778,7 @@ namespace Microsoft.AspNet.JsonPatch.Test // Act & Assert var exception = Assert.Throws(() => { patchDoc.ApplyTo(doc); }); - Assert.Equal("Property does not exist at path '/simpledto/integerlist/-1'.", exception.Message); + Assert.Equal("For operation 'remove' on array property at path '/simpledto/integerlist/-1', the index is negative.", exception.Message); } [Fact] @@ -805,7 +805,7 @@ namespace Microsoft.AspNet.JsonPatch.Test { deserialized.ApplyTo(doc); }); - Assert.Equal("Property does not exist at path '/simpledto/integerlist/-1'.", exception.Message); + Assert.Equal("For operation 'remove' on array property at path '/simpledto/integerlist/-1', the index is negative.", exception.Message); } [Fact] @@ -830,7 +830,7 @@ namespace Microsoft.AspNet.JsonPatch.Test patchDoc.ApplyTo(doc, logger.LogErrorMessage); // Assert - Assert.Equal("Property does not exist at path '/simpledto/integerlist/-1'.", logger.ErrorMessage); + Assert.Equal("For operation 'remove' on array property at path '/simpledto/integerlist/-1', the index is negative.", logger.ErrorMessage); } [Fact] @@ -1319,11 +1319,10 @@ namespace Microsoft.AspNet.JsonPatch.Test // create patch var patchDoc = new JsonPatchDocument(); patchDoc.Replace(o => o.SimpleDTO.IntegerList, 5, -1); - + // Act & Assert var exception = Assert.Throws(() => { patchDoc.ApplyTo(doc); }); - Assert.Equal( - "Property does not exist at path '/simpledto/integerlist/-1'.", + Assert.Equal("For operation 'replace' on array property at path '/simpledto/integerlist/-1', the index is negative.", exception.Message); } @@ -1349,7 +1348,7 @@ namespace Microsoft.AspNet.JsonPatch.Test // Act & Assert var exception = Assert.Throws(() => { deserialized.ApplyTo(doc); }); Assert.Equal( - "Property does not exist at path '/simpledto/integerlist/-1'.", + "For operation 'replace' on array property at path '/simpledto/integerlist/-1', the index is negative.", exception.Message); } diff --git a/test/Microsoft.AspNet.JsonPatch.Test/ObjectAdapterTests.cs b/test/Microsoft.AspNet.JsonPatch.Test/ObjectAdapterTests.cs index efdf09ea2e..0cc97e32c4 100644 --- a/test/Microsoft.AspNet.JsonPatch.Test/ObjectAdapterTests.cs +++ b/test/Microsoft.AspNet.JsonPatch.Test/ObjectAdapterTests.cs @@ -577,7 +577,7 @@ namespace Microsoft.AspNet.JsonPatch.Test // Act & Assert var exception = Assert.Throws(() => { patchDoc.ApplyTo(doc); }); - Assert.Equal("Property does not exist at path '/integerlist/-1'.", exception.Message); + Assert.Equal("For operation 'remove' on array property at path '/integerlist/-1', the index is negative.", exception.Message); } [Fact] @@ -598,7 +598,7 @@ namespace Microsoft.AspNet.JsonPatch.Test // Act & Assert var exception = Assert.Throws(() => { deserialized.ApplyTo(doc); }); - Assert.Equal("Property does not exist at path '/integerlist/-1'.", exception.Message); + Assert.Equal("For operation 'remove' on array property at path '/integerlist/-1', the index is negative.", exception.Message); } [Fact] @@ -621,7 +621,7 @@ namespace Microsoft.AspNet.JsonPatch.Test // Assert - Assert.Equal("Property does not exist at path '/integerlist/-1'.", logger.ErrorMessage); + Assert.Equal("For operation 'remove' on array property at path '/integerlist/-1', the index is negative.", logger.ErrorMessage); } [Fact] @@ -1172,7 +1172,7 @@ namespace Microsoft.AspNet.JsonPatch.Test { patchDoc.ApplyTo(doc); }); - Assert.Equal("Property does not exist at path '/integerlist/-1'.", exception.Message); + Assert.Equal("For operation 'replace' on array property at path '/integerlist/-1', the index is negative.", exception.Message); } [Fact] @@ -1196,7 +1196,7 @@ namespace Microsoft.AspNet.JsonPatch.Test { deserialized.ApplyTo(doc); }); - Assert.Equal("Property does not exist at path '/integerlist/-1'.", exception.Message); + Assert.Equal("For operation 'replace' on array property at path '/integerlist/-1', the index is negative.", exception.Message); } [Fact]