// 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 Newtonsoft.Json.Serialization; using Xunit; namespace Microsoft.AspNetCore.JsonPatch.Internal { public class ObjectVisitorTest { private class Class1 { public string Name { get; set; } public IList States { get; set; } = new List(); public IDictionary Countries = new Dictionary(); public dynamic Items { get; set; } = new ExpandoObject(); } private class Class1Nested { public List Customers { get; set; } = new List(); } public static IEnumerable ReturnsListAdapterData { get { var model = new Class1(); yield return new object[] { model, "/States/-", model.States }; yield return new object[] { model.States, "/-", model.States }; var nestedModel = new Class1Nested(); nestedModel.Customers.Add(new Class1()); yield return new object[] { nestedModel, "/Customers/0/States/-", nestedModel.Customers[0].States }; yield return new object[] { nestedModel, "/Customers/0/States/0", nestedModel.Customers[0].States }; yield return new object[] { nestedModel.Customers, "/0/States/-", nestedModel.Customers[0].States }; yield return new object[] { nestedModel.Customers[0], "/States/-", nestedModel.Customers[0].States }; } } [Theory] [MemberData(nameof(ReturnsListAdapterData))] public void Visit_ValidPathToArray_ReturnsListAdapter(object targetObject, string path, object expectedTargetObject) { // Arrange var visitor = new ObjectVisitor(new ParsedPath(path), new DefaultContractResolver()); // Act var visitStatus = visitor.TryVisit(ref targetObject, out var adapter, out var message); // Assert Assert.True(visitStatus); Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); Assert.Same(expectedTargetObject, targetObject); Assert.IsType(adapter); } public static IEnumerable ReturnsDictionaryAdapterData { get { var model = new Class1(); yield return new object[] { model, "/Countries/USA", model.Countries }; yield return new object[] { model.Countries, "/USA", model.Countries }; var nestedModel = new Class1Nested(); nestedModel.Customers.Add(new Class1()); yield return new object[] { nestedModel, "/Customers/0/Countries/USA", nestedModel.Customers[0].Countries }; yield return new object[] { nestedModel.Customers, "/0/Countries/USA", nestedModel.Customers[0].Countries }; yield return new object[] { nestedModel.Customers[0], "/Countries/USA", nestedModel.Customers[0].Countries }; } } [Theory] [MemberData(nameof(ReturnsDictionaryAdapterData))] public void Visit_ValidPathToDictionary_ReturnsDictionaryAdapter(object targetObject, string path, object expectedTargetObject) { // Arrange var visitor = new ObjectVisitor(new ParsedPath(path), new DefaultContractResolver()); // Act var visitStatus = visitor.TryVisit(ref targetObject, out var adapter, out var message); // Assert Assert.True(visitStatus); Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); Assert.Same(expectedTargetObject, targetObject); Assert.IsType(adapter); } public static IEnumerable ReturnsExpandoAdapterData { get { var nestedModel = new Class1Nested(); nestedModel.Customers.Add(new Class1()); yield return new object[] { nestedModel, "/Customers/0/Items/Name", nestedModel.Customers[0].Items }; yield return new object[] { nestedModel.Customers, "/0/Items/Name", nestedModel.Customers[0].Items }; yield return new object[] { nestedModel.Customers[0], "/Items/Name", nestedModel.Customers[0].Items }; } } [Theory] [MemberData(nameof(ReturnsExpandoAdapterData))] public void Visit_ValidPathToExpandoObject_ReturnsExpandoAdapter(object targetObject, string path, object expectedTargetObject) { // Arrange var visitor = new ObjectVisitor(new ParsedPath(path), new DefaultContractResolver()); // Act var visitStatus = visitor.TryVisit(ref targetObject, out var adapter, out var message); // Assert Assert.True(visitStatus); Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); Assert.Same(expectedTargetObject, targetObject); Assert.IsType(adapter); } public static IEnumerable ReturnsPocoAdapterData { get { var model = new Class1(); yield return new object[] { model, "/Name", model }; var nestedModel = new Class1Nested(); nestedModel.Customers.Add(new Class1()); yield return new object[] { nestedModel, "/Customers/0/Name", nestedModel.Customers[0] }; yield return new object[] { nestedModel.Customers, "/0/Name", nestedModel.Customers[0] }; yield return new object[] { nestedModel.Customers[0], "/Name", nestedModel.Customers[0] }; } } [Theory] [MemberData(nameof(ReturnsPocoAdapterData))] public void Visit_ValidPath_ReturnsExpandoAdapter(object targetObject, string path, object expectedTargetObject) { // Arrange var visitor = new ObjectVisitor(new ParsedPath(path), new DefaultContractResolver()); // Act var visitStatus = visitor.TryVisit(ref targetObject, out var adapter, out var message); // Assert Assert.True(visitStatus); Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); Assert.Same(expectedTargetObject, targetObject); Assert.IsType(adapter); } [Theory] [InlineData("0")] [InlineData("-1")] public void Visit_InvalidIndexToArray_Fails(string position) { // Arrange var visitor = new ObjectVisitor(new ParsedPath($"/Customers/{position}/States/-"), new DefaultContractResolver()); var automobileDepartment = new Class1Nested(); object targetObject = automobileDepartment; // Act var visitStatus = visitor.TryVisit(ref targetObject, out var adapter, out var message); // Assert Assert.False(visitStatus); Assert.Equal( string.Format("The index value provided by path segment '{0}' is out of bounds of the array size.", position), message); } [Theory] [InlineData("-")] [InlineData("foo")] public void Visit_InvalidIndexFormatToArray_Fails(string position) { // Arrange var visitor = new ObjectVisitor(new ParsedPath($"/Customers/{position}/States/-"), new DefaultContractResolver()); var automobileDepartment = new Class1Nested(); object targetObject = automobileDepartment; // Act var visitStatus = visitor.TryVisit(ref targetObject, out var adapter, out var message); // Assert Assert.False(visitStatus); Assert.Equal(string.Format( "The path segment '{0}' is invalid for an array index.", position), message); } // The adapter takes care of the responsibility of validating the final segment [Fact] public void Visit_DoesNotValidate_FinalPathSegment() { // Arrange var visitor = new ObjectVisitor(new ParsedPath($"/NonExisting"), new DefaultContractResolver()); var model = new Class1(); object targetObject = model; // Act var visitStatus = visitor.TryVisit(ref targetObject, out var adapter, out var message); // Assert Assert.True(visitStatus); Assert.True(string.IsNullOrEmpty(message), "Expected no error message"); Assert.IsType(adapter); } [Fact] public void Visit_NullTarget_ReturnsNullAdapter() { // Arrange var visitor = new ObjectVisitor(new ParsedPath("test"), new DefaultContractResolver()); // Act object target = null; var visitStatus = visitor.TryVisit(ref target, out var adapter, out var message); // Assert Assert.False(visitStatus); Assert.Null(adapter); Assert.Null(message); } } }