Skip to content

Commit

Permalink
Unit tests to illustrate issue #39 (#40)
Browse files Browse the repository at this point in the history
* First unit tests relating to issue 39 - two fail to illustrate the problem and two pass already and only exist as regression tests

* Added more tests for issue 39 - this time with a List<string> that is a property instead of a string[] that is being directly deserialised to; again two tests that fail to illustrate the problem and two that pass to act as regression tests

When the code is updated to fix the problem in the previous changeset, I'm sure that it will fix these as well but I thought that it made sense to include a few extra cases

* Improve null check

Co-authored-by: Rico Suter <mail@rsuter.com>
  • Loading branch information
ProductiveRage and RicoSuter authored Sep 28, 2020
1 parent 7b16d9b commit 1991689
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 9 deletions.
117 changes: 117 additions & 0 deletions src/Namotion.Reflection.Tests/ObjectExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Xunit;

Expand Down Expand Up @@ -74,6 +75,122 @@ public void When_object_has_no_null_properties_then_errors_is_empty()
Assert.Empty(errors);
}

///// <summary>
///// One of the reproduce cases for issue 39 - should throw InvalidOperationException as 'nullable enable' is set for this file
///// </summary>
//[Fact]
//public void When_array_of_non_nullable_strings_contains_null_then_EnsureValidNullability_should_throw_InvalidOperationException()
//{
// //// Arrange
// var arrayOfNonNulllableStrings = JsonConvert.DeserializeObject<string[]>("[ null ]");

// //// Act and assert
// Assert.Throws<InvalidOperationException>(() => arrayOfNonNulllableStrings.EnsureValidNullability());

// TODO: This here is not possible because string[] (without property or parameter) has no context and nullability information.
//}

/// <summary>
/// One of the reproduce cases for issue 39 - should not throw anything because a non-null string value is being deserialised into an array
/// </summary>
[Fact]
public void When_array_of_non_nullable_strings_contains_single_non_null_string_then_EnsureValidNullability_should_not_throw()
{
//// Arrange
var arrayOfNonNulllableStrings = JsonConvert.DeserializeObject<string[]>("[ \"ok\" ]");

//// Act and assert
arrayOfNonNulllableStrings.EnsureValidNullability();
}

/// <summary>
/// One of the reproduce cases for issue 39 - should not throw anything because a null string value is being deserialised into an array of strings that are allowed to be null
/// </summary>
[Fact]
public void When_array_of_nullable_strings_contains_null_then_EnsureValidNullability_should_not_throw()
{
//// Arrange
var arrayOfNonNulllableStrings = JsonConvert.DeserializeObject<string?[]>("[ null ]");

//// Act and assert
arrayOfNonNulllableStrings.EnsureValidNullability();
}

/// <summary>
/// One of the reproduce cases for issue 39 - should not throw anything because a non-null string value is being deserialised into an array of strings (that are allowed to be null)
/// </summary>
[Fact]
public void When_array_of_nullable_strings_contains_single_non_null_string_then_EnsureValidNullability_should_not_throw()
{
//// Arrange
var arrayOfNonNulllableStrings = JsonConvert.DeserializeObject<string?[]>("[ \"ok\" ]");

//// Act and assert
arrayOfNonNulllableStrings.EnsureValidNullability();
}

/// <summary>
/// One of the reproduce cases for issue 39 - should not throw anything because a non-null string value is being deserialised into a property that is a list of non-nullable strings
/// </summary>
[Fact]
public void When_property_list_of_non_nullable_strings_contains_single_non_null_string_then_EnsureValidNullability_should_not_throw()
{
//// Arrange
var objectWithItemsListContainingSingleNonNullEntry = JsonConvert.DeserializeObject<SomethingWithListOfNonNullableStringItems>("{ \"Items\": [ \"ok\" ] }");

//// Act and assert
objectWithItemsListContainingSingleNonNullEntry.EnsureValidNullability();
}

/// <summary>
/// One of the reproduce cases for issue 39 - should not throw anything because a non-null string value is being deserialised into a property that is a list of non-nullable strings
/// </summary>
[Fact]
public void When_property_list_of_non_nullable_strings_contains_null_string_then_EnsureValidNullability_should_throw_InvalidOperationException()
{
//// Arrange
var objectWithItemsListContainingSingleNullEntry = JsonConvert.DeserializeObject<SomethingWithListOfNonNullableStringItems>("{ \"Items\": [ null ] }");

//// Act and assert
Assert.Throws<InvalidOperationException>(() => objectWithItemsListContainingSingleNullEntry.EnsureValidNullability());
}

/// <summary>
/// One of the reproduce cases for issue 39 - should not throw anything because a non-null string value is being deserialised into a property that is a list of (non-nullable) strings
/// </summary>
[Fact]
public void When_property_list_of_nullable_strings_contains_single_non_null_string_then_EnsureValidNullability_should_not_throw()
{
//// Arrange
var objectWithItemsListContainingSingleNonNullEntry = JsonConvert.DeserializeObject<SomethingWithListOfNullableStringItems>("{ \"Items\": [ \"ok\" ] }");

//// Act and assert
objectWithItemsListContainingSingleNonNullEntry.EnsureValidNullability();
}

/// <summary>
/// One of the reproduce cases for issue 39 - should not throw anything because a non-null string value is being deserialised into a property that is a list of non-nullable strings
/// </summary>
[Fact]
public void When_property_list_of_nullable_strings_contains_null_string_then_EnsureValidNullability_should_not_throw()
{
//// Arrange
var objectWithItemsListContainingSingleNullEntry = JsonConvert.DeserializeObject<SomethingWithListOfNullableStringItems>("{ \"Items\": [ null ] }");

//// Act and assert
objectWithItemsListContainingSingleNullEntry.EnsureValidNullability();
}

private sealed class SomethingWithListOfNonNullableStringItems
{
public List<string> Items { get; set; } = new List<string>();
}

private sealed class SomethingWithListOfNullableStringItems
{
public List<string?> Items { get; set; } = new List<string?>();
}

/// <summary>
/// This is a reproduce case for Issue 24
/// </summary>
Expand Down
29 changes: 20 additions & 9 deletions src/Namotion.Reflection/ObjectExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public static bool HasValidNullability(this object obj, bool checkChildren = tru
/// <returns>The result.</returns>
public static void EnsureValidNullability(this object obj, bool checkChildren = true)
{
ValidateNullability(obj, checkChildren ? new HashSet<object>() : null, null, false);
ValidateNullability(obj, obj?.GetType().ToContextualType(), checkChildren ? new HashSet<object>() : null, null, false);
}

/// <summary>Checks which non nullable properties are null (invalid).</summary>
Expand All @@ -69,11 +69,11 @@ public static void EnsureValidNullability(this object obj, bool checkChildren =
public static IEnumerable<string> ValidateNullability(this object obj, bool checkChildren = true)
{
var errors = new List<string>();
ValidateNullability(obj, checkChildren ? new HashSet<object>() : null, errors, false);
ValidateNullability(obj, obj?.GetType().ToContextualType(), checkChildren ? new HashSet<object>() : null, errors, false);
return errors;
}

private static void ValidateNullability(object obj, HashSet<object> checkedObjects, List<string> errors, bool stopFirstFail)
private static void ValidateNullability(object obj, ContextualType type, HashSet<object> checkedObjects, List<string> errors, bool stopFirstFail)
{
if (DisableNullabilityValidation)
{
Expand All @@ -97,25 +97,36 @@ private static void ValidateNullability(object obj, HashSet<object> checkedObjec
}
}

var type = obj.GetType();
if (checkedObjects != null && obj is IDictionary dictionary)
{
foreach (var item in dictionary.Keys.Cast<object>()
.Concat(dictionary.Values.Cast<object>()))
{
ValidateNullability(item, checkedObjects, errors, stopFirstFail);
ValidateNullability(item, type.GenericArguments[1], checkedObjects, errors, stopFirstFail);
}
}
else if (checkedObjects != null && obj is IEnumerable enumerable && !(obj is string))
{
var itemType = type.ElementType ?? type.GenericArguments[0];
foreach (var item in enumerable.Cast<object>())
{
ValidateNullability(item, checkedObjects, errors, stopFirstFail);
if (item == null)
{
if (itemType.Nullability == Nullability.NotNullable)
{
throw new InvalidOperationException(
"The object's nullability is invalid, item in enumerable.");
}
}
else
{
ValidateNullability(item, itemType, checkedObjects, errors, stopFirstFail);
}
}
}
else if (!type.ToCachedType().TypeInfo.IsValueType)
else if (!type.TypeInfo.IsValueType)
{
var properties = type.GetContextualProperties();
var properties = type.Type.GetContextualProperties();
for (int i = 0; i < properties.Length; i++)
{
var property = properties[i];
Expand Down Expand Up @@ -143,7 +154,7 @@ private static void ValidateNullability(object obj, HashSet<object> checkedObjec
}
else if (checkedObjects != null)
{
ValidateNullability(value, checkedObjects, errors, stopFirstFail);
ValidateNullability(value, property.MemberInfo.ToContextualMember(), checkedObjects, errors, stopFirstFail);
}
}
}
Expand Down

0 comments on commit 1991689

Please sign in to comment.