From 187f03c31d9483ea15c4147f6311f956f1c0404d Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sun, 16 Nov 2025 11:27:55 +0100 Subject: [PATCH 01/26] Use StringComparer.Ordinal MA0002 - IEqualityComparer or IComparer is missing --- Src/FluentAssertions.Json/Common/JTokenExtensions.cs | 6 +++--- Src/FluentAssertions.Json/JTokenDifferentiator.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Src/FluentAssertions.Json/Common/JTokenExtensions.cs b/Src/FluentAssertions.Json/Common/JTokenExtensions.cs index 6dd845c5..81cbdb04 100644 --- a/Src/FluentAssertions.Json/Common/JTokenExtensions.cs +++ b/Src/FluentAssertions.Json/Common/JTokenExtensions.cs @@ -18,7 +18,7 @@ public static JToken Normalize(this JToken token) { return token switch { - JObject obj => new JObject(obj.Properties().OrderBy(p => p.Name).Select(p => new JProperty(p.Name, Normalize(p.Value)))), + JObject obj => new JObject(obj.Properties().OrderBy(p => p.Name, StringComparer.Ordinal).Select(p => new JProperty(p.Name, Normalize(p.Value)))), JArray array => new JArray(array.Select(Normalize).OrderBy(x => x, Comparer)), _ => token }; @@ -78,8 +78,8 @@ private static int Compare(JObject x, JObject y) return countComparison; return x.Properties() - .OrderBy(p => p.Name) - .Zip(y.Properties().OrderBy(p => p.Name), (px, py) => Compare(px, py)) + .OrderBy(p => p.Name, StringComparer.Ordinal) + .Zip(y.Properties().OrderBy(p => p.Name, StringComparer.Ordinal), (px, py) => Compare(px, py)) .FirstOrDefault(itemComparison => itemComparison != 0); } diff --git a/Src/FluentAssertions.Json/JTokenDifferentiator.cs b/Src/FluentAssertions.Json/JTokenDifferentiator.cs index 38293bb6..6b89393c 100644 --- a/Src/FluentAssertions.Json/JTokenDifferentiator.cs +++ b/Src/FluentAssertions.Json/JTokenDifferentiator.cs @@ -156,8 +156,8 @@ private Difference FindJObjectDifference(JObject actual, JToken expected, JPath private Difference CompareProperties(IEnumerable actual, IEnumerable expected, JPath path) { - var actualDictionary = actual?.ToDictionary(p => p.Name, p => p.Value) ?? new Dictionary(); - var expectedDictionary = expected?.ToDictionary(p => p.Name, p => p.Value) ?? new Dictionary(); + var actualDictionary = actual?.ToDictionary(p => p.Name, p => p.Value, StringComparer.Ordinal) ?? new Dictionary(StringComparer.Ordinal); + var expectedDictionary = expected?.ToDictionary(p => p.Name, p => p.Value, StringComparer.Ordinal) ?? new Dictionary(StringComparer.Ordinal); foreach (KeyValuePair expectedPair in expectedDictionary) { From d928ae97d37680ee4ee91a78d0023b1bbb907d1f Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sun, 16 Nov 2025 11:29:26 +0100 Subject: [PATCH 02/26] Use braces SA1503 - Braces should not be omitted --- Src/FluentAssertions.Json/Common/JTokenExtensions.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Src/FluentAssertions.Json/Common/JTokenExtensions.cs b/Src/FluentAssertions.Json/Common/JTokenExtensions.cs index 81cbdb04..e43cd73a 100644 --- a/Src/FluentAssertions.Json/Common/JTokenExtensions.cs +++ b/Src/FluentAssertions.Json/Common/JTokenExtensions.cs @@ -29,17 +29,25 @@ private sealed class JTokenComparer : IComparer public int Compare(JToken x, JToken y) { if (ReferenceEquals(x, y)) + { return 0; + } if (x is null) + { return -1; + } if (y is null) + { return 1; + } var typeComparison = x.Type.CompareTo(y.Type); if (typeComparison != 0) + { return typeComparison; + } return x switch { @@ -64,7 +72,9 @@ private static int Compare(JContainer x, JContainer y) { var countComparison = x.Count.CompareTo(y.Count); if (countComparison != 0) + { return countComparison; + } return x .Select((t, i) => Comparer.Compare(t, y[i])) @@ -75,7 +85,9 @@ private static int Compare(JObject x, JObject y) { var countComparison = x.Count.CompareTo(y.Count); if (countComparison != 0) + { return countComparison; + } return x.Properties() .OrderBy(p => p.Name, StringComparer.Ordinal) From 227ddce344ea328fdf1e117e3a91b7fc63dd2081 Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sun, 16 Nov 2025 11:30:22 +0100 Subject: [PATCH 03/26] Suppress MA0015 We already do the same for S3928 --- Src/FluentAssertions.Json/JTokenDifferentiator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Src/FluentAssertions.Json/JTokenDifferentiator.cs b/Src/FluentAssertions.Json/JTokenDifferentiator.cs index 6b89393c..b004b19b 100644 --- a/Src/FluentAssertions.Json/JTokenDifferentiator.cs +++ b/Src/FluentAssertions.Json/JTokenDifferentiator.cs @@ -304,9 +304,9 @@ public override string ToString() DifferenceKind.ExpectedMissesProperty => $"has extra property {Path}", DifferenceKind.ActualMissesElement => $"misses expected element {Path}", DifferenceKind.WrongOrder => $"has expected element {Path} in the wrong order", -#pragma warning disable S3928 // Parameter names used into ArgumentException constructors should match an existing one +#pragma warning disable S3928, MA0015 // Parameter names used into ArgumentException constructors should match an existing one _ => throw new ArgumentOutOfRangeException(), -#pragma warning restore S3928 // Parameter names used into ArgumentException constructors should match an existing one +#pragma warning restore S3928, MA0015 // Parameter names used into ArgumentException constructors should match an existing one }; } } From c55ac54425a5a8d4b5428bad1b44e6ba5cc3a55b Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sun, 16 Nov 2025 11:31:40 +0100 Subject: [PATCH 04/26] Remove unnecessary accessibility modifier IDE0040 - Accessibility modifier unnecessary --- Src/FluentAssertions.Json/IJsonAssertionRestriction.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Src/FluentAssertions.Json/IJsonAssertionRestriction.cs b/Src/FluentAssertions.Json/IJsonAssertionRestriction.cs index f4997818..d8d37de5 100644 --- a/Src/FluentAssertions.Json/IJsonAssertionRestriction.cs +++ b/Src/FluentAssertions.Json/IJsonAssertionRestriction.cs @@ -9,6 +9,7 @@ public interface IJsonAssertionRestriction /// Allows overriding the way structural equality is applied to (nested) objects of type /// /// - public IJsonAssertionOptions WhenTypeIs() where TMemberType : TMember; + IJsonAssertionOptions WhenTypeIs() + where TMemberType : TMember; } } From 896423a7183c9cdde5b0124566183eda83e4598b Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sun, 16 Nov 2025 11:33:30 +0100 Subject: [PATCH 05/26] Move types to separate files MA0048 - File name must match type name --- Src/FluentAssertions.Json/Difference.cs | 48 ++++++++++ Src/FluentAssertions.Json/DifferenceKind.cs | 16 ++++ Src/FluentAssertions.Json/JPath.cs | 35 ++++++++ .../JTokenDifferentiator.cs | 89 ------------------- 4 files changed, 99 insertions(+), 89 deletions(-) create mode 100644 Src/FluentAssertions.Json/Difference.cs create mode 100644 Src/FluentAssertions.Json/DifferenceKind.cs create mode 100644 Src/FluentAssertions.Json/JPath.cs diff --git a/Src/FluentAssertions.Json/Difference.cs b/Src/FluentAssertions.Json/Difference.cs new file mode 100644 index 00000000..62e0e572 --- /dev/null +++ b/Src/FluentAssertions.Json/Difference.cs @@ -0,0 +1,48 @@ +using System; + +namespace FluentAssertions.Json +{ + internal class Difference + { + public Difference(DifferenceKind kind, JPath path, object actual, object expected) + : this(kind, path) + { + Actual = actual; + Expected = expected; + } + + public Difference(DifferenceKind kind, JPath path) + { + Kind = kind; + Path = path; + } + + private DifferenceKind Kind { get; } + + private JPath Path { get; } + + private object Actual { get; } + + private object Expected { get; } + + public override string ToString() + { + return Kind switch + { + DifferenceKind.ActualIsNull => "is null", + DifferenceKind.ExpectedIsNull => "is not null", + DifferenceKind.OtherType => $"has {Actual} instead of {Expected} at {Path}", + DifferenceKind.OtherName => $"has a different name at {Path}", + DifferenceKind.OtherValue => $"has a different value at {Path}", + DifferenceKind.DifferentLength => $"has {Actual} elements instead of {Expected} at {Path}", + DifferenceKind.ActualMissesProperty => $"misses property {Path}", + DifferenceKind.ExpectedMissesProperty => $"has extra property {Path}", + DifferenceKind.ActualMissesElement => $"misses expected element {Path}", + DifferenceKind.WrongOrder => $"has expected element {Path} in the wrong order", +#pragma warning disable S3928, MA0015 // Parameter names used into ArgumentException constructors should match an existing one + _ => throw new ArgumentOutOfRangeException(), +#pragma warning restore S3928, MA0015 // Parameter names used into ArgumentException constructors should match an existing one + }; + } + } +} diff --git a/Src/FluentAssertions.Json/DifferenceKind.cs b/Src/FluentAssertions.Json/DifferenceKind.cs new file mode 100644 index 00000000..fa37b053 --- /dev/null +++ b/Src/FluentAssertions.Json/DifferenceKind.cs @@ -0,0 +1,16 @@ +namespace FluentAssertions.Json +{ + internal enum DifferenceKind + { + ActualIsNull, + ExpectedIsNull, + OtherType, + OtherName, + OtherValue, + DifferentLength, + ActualMissesProperty, + ExpectedMissesProperty, + ActualMissesElement, + WrongOrder + } +} diff --git a/Src/FluentAssertions.Json/JPath.cs b/Src/FluentAssertions.Json/JPath.cs new file mode 100644 index 00000000..b5b8d2d0 --- /dev/null +++ b/Src/FluentAssertions.Json/JPath.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; + +namespace FluentAssertions.Json +{ + internal class JPath + { + private readonly List nodes = new(); + + public JPath() + { + nodes.Add("$"); + } + + private JPath(JPath existingPath, string extraNode) + { + nodes.AddRange(existingPath.nodes); + nodes.Add(extraNode); + } + + public JPath AddProperty(string name) + { + return new JPath(this, $".{name}"); + } + + public JPath AddIndex(int index) + { + return new JPath(this, $"[{index}]"); + } + + public override string ToString() + { + return string.Concat(nodes); + } + } +} diff --git a/Src/FluentAssertions.Json/JTokenDifferentiator.cs b/Src/FluentAssertions.Json/JTokenDifferentiator.cs index b004b19b..f5b1aacb 100644 --- a/Src/FluentAssertions.Json/JTokenDifferentiator.cs +++ b/Src/FluentAssertions.Json/JTokenDifferentiator.cs @@ -266,93 +266,4 @@ private static string Describe(JTokenType jTokenType) }; } } - - internal class Difference - { - public Difference(DifferenceKind kind, JPath path, object actual, object expected) - : this(kind, path) - { - Actual = actual; - Expected = expected; - } - - public Difference(DifferenceKind kind, JPath path) - { - Kind = kind; - Path = path; - } - - private DifferenceKind Kind { get; } - - private JPath Path { get; } - - private object Actual { get; } - - private object Expected { get; } - - public override string ToString() - { - return Kind switch - { - DifferenceKind.ActualIsNull => "is null", - DifferenceKind.ExpectedIsNull => "is not null", - DifferenceKind.OtherType => $"has {Actual} instead of {Expected} at {Path}", - DifferenceKind.OtherName => $"has a different name at {Path}", - DifferenceKind.OtherValue => $"has a different value at {Path}", - DifferenceKind.DifferentLength => $"has {Actual} elements instead of {Expected} at {Path}", - DifferenceKind.ActualMissesProperty => $"misses property {Path}", - DifferenceKind.ExpectedMissesProperty => $"has extra property {Path}", - DifferenceKind.ActualMissesElement => $"misses expected element {Path}", - DifferenceKind.WrongOrder => $"has expected element {Path} in the wrong order", -#pragma warning disable S3928, MA0015 // Parameter names used into ArgumentException constructors should match an existing one - _ => throw new ArgumentOutOfRangeException(), -#pragma warning restore S3928, MA0015 // Parameter names used into ArgumentException constructors should match an existing one - }; - } - } - - internal class JPath - { - private readonly List nodes = new(); - - public JPath() - { - nodes.Add("$"); - } - - private JPath(JPath existingPath, string extraNode) - { - nodes.AddRange(existingPath.nodes); - nodes.Add(extraNode); - } - - public JPath AddProperty(string name) - { - return new JPath(this, $".{name}"); - } - - public JPath AddIndex(int index) - { - return new JPath(this, $"[{index}]"); - } - - public override string ToString() - { - return string.Concat(nodes); - } - } - - internal enum DifferenceKind - { - ActualIsNull, - ExpectedIsNull, - OtherType, - OtherName, - OtherValue, - DifferentLength, - ActualMissesProperty, - ExpectedMissesProperty, - ActualMissesElement, - WrongOrder - } } From f741fe6683944fe49e5db280272ffa47fff543d0 Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sun, 16 Nov 2025 11:36:01 +0100 Subject: [PATCH 06/26] CA1852: Seal internal classes --- Src/FluentAssertions.Json/Difference.cs | 2 +- Src/FluentAssertions.Json/JPath.cs | 2 +- Src/FluentAssertions.Json/JTokenDifferentiator.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Src/FluentAssertions.Json/Difference.cs b/Src/FluentAssertions.Json/Difference.cs index 62e0e572..511360a0 100644 --- a/Src/FluentAssertions.Json/Difference.cs +++ b/Src/FluentAssertions.Json/Difference.cs @@ -2,7 +2,7 @@ namespace FluentAssertions.Json { - internal class Difference + internal sealed class Difference { public Difference(DifferenceKind kind, JPath path, object actual, object expected) : this(kind, path) diff --git a/Src/FluentAssertions.Json/JPath.cs b/Src/FluentAssertions.Json/JPath.cs index b5b8d2d0..3cfcb742 100644 --- a/Src/FluentAssertions.Json/JPath.cs +++ b/Src/FluentAssertions.Json/JPath.cs @@ -2,7 +2,7 @@ namespace FluentAssertions.Json { - internal class JPath + internal sealed class JPath { private readonly List nodes = new(); diff --git a/Src/FluentAssertions.Json/JTokenDifferentiator.cs b/Src/FluentAssertions.Json/JTokenDifferentiator.cs index f5b1aacb..bd5acc63 100644 --- a/Src/FluentAssertions.Json/JTokenDifferentiator.cs +++ b/Src/FluentAssertions.Json/JTokenDifferentiator.cs @@ -7,7 +7,7 @@ namespace FluentAssertions.Json { - internal class JTokenDifferentiator + internal sealed class JTokenDifferentiator { private readonly bool ignoreExtraProperties; From cf014c9dfb7a8a71f4c9db5414fe2b4ae970a944 Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sun, 16 Nov 2025 11:37:14 +0100 Subject: [PATCH 07/26] Format constructors SA1128, SA1502 --- Src/FluentAssertions.Json/JsonAssertionOptions.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Src/FluentAssertions.Json/JsonAssertionOptions.cs b/Src/FluentAssertions.Json/JsonAssertionOptions.cs index f98a9ba6..f6f42d3f 100644 --- a/Src/FluentAssertions.Json/JsonAssertionOptions.cs +++ b/Src/FluentAssertions.Json/JsonAssertionOptions.cs @@ -8,9 +8,12 @@ namespace FluentAssertions.Json /// public sealed class JsonAssertionOptions : EquivalencyOptions, IJsonAssertionOptions { - internal JsonAssertionOptions() { } + internal JsonAssertionOptions() + { + } - public JsonAssertionOptions(EquivalencyOptions equivalencyAssertionOptions) : base(equivalencyAssertionOptions) + public JsonAssertionOptions(EquivalencyOptions equivalencyAssertionOptions) + : base(equivalencyAssertionOptions) { } From 9699dd6fb7483e459eab13d488a9a6281d41930c Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sun, 16 Nov 2025 11:38:49 +0100 Subject: [PATCH 08/26] RCS1263: Adjust xml parameter --- Src/FluentAssertions.Json/JTokenAssertions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Src/FluentAssertions.Json/JTokenAssertions.cs b/Src/FluentAssertions.Json/JTokenAssertions.cs index 19f82151..be92f8db 100644 --- a/Src/FluentAssertions.Json/JTokenAssertions.cs +++ b/Src/FluentAssertions.Json/JTokenAssertions.cs @@ -27,7 +27,7 @@ static JTokenAssertions() /// Initializes a new instance of the class. /// /// The subject - /// + /// The assertion chain public JTokenAssertions(JToken subject, AssertionChain assertionChain) : base(subject, assertionChain) { From 01081b8b484c3c36f650b3c1a4c90429bb105933 Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sun, 16 Nov 2025 11:40:57 +0100 Subject: [PATCH 09/26] Suppress CA1716 CA1716: Identifiers should not match keywords --- Src/FluentAssertions.Json/IJsonAssertionOptions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Src/FluentAssertions.Json/IJsonAssertionOptions.cs b/Src/FluentAssertions.Json/IJsonAssertionOptions.cs index 6b64d181..ecd0df38 100644 --- a/Src/FluentAssertions.Json/IJsonAssertionOptions.cs +++ b/Src/FluentAssertions.Json/IJsonAssertionOptions.cs @@ -15,7 +15,9 @@ public interface IJsonAssertionOptions /// /// The assertion to execute when the predicate is met. /// +#pragma warning disable CA1716 // CA1716: Identifiers should not match keywords IJsonAssertionRestriction Using(Action> action); +#pragma warning restore CA1716 /// /// Configures the JSON assertion to ignore the order of elements in arrays or collections during comparison, allowing for equivalency checks regardless of element sequence. From a66a64517301d5a9a1b903e433341bee193907a5 Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sun, 16 Nov 2025 11:43:07 +0100 Subject: [PATCH 10/26] IDE0005: remove unnecessary using directives --- Src/FluentAssertions.Json/ObjectAssertionsExtensions.cs | 1 - Src/FluentAssertions.Json/StringAssertionsExtensions.cs | 1 - Tests/FluentAssertions.Json.Specs/WithoutStrictOrderingSpecs.cs | 1 - 3 files changed, 3 deletions(-) diff --git a/Src/FluentAssertions.Json/ObjectAssertionsExtensions.cs b/Src/FluentAssertions.Json/ObjectAssertionsExtensions.cs index 01016caa..ef629eaa 100644 --- a/Src/FluentAssertions.Json/ObjectAssertionsExtensions.cs +++ b/Src/FluentAssertions.Json/ObjectAssertionsExtensions.cs @@ -1,6 +1,5 @@ using System; using FluentAssertions.Equivalency; -using FluentAssertions.Execution; using FluentAssertions.Primitives; using Newtonsoft.Json; diff --git a/Src/FluentAssertions.Json/StringAssertionsExtensions.cs b/Src/FluentAssertions.Json/StringAssertionsExtensions.cs index d788fc26..d4837218 100644 --- a/Src/FluentAssertions.Json/StringAssertionsExtensions.cs +++ b/Src/FluentAssertions.Json/StringAssertionsExtensions.cs @@ -1,5 +1,4 @@ using System; -using FluentAssertions.Execution; using FluentAssertions.Primitives; using Newtonsoft.Json.Linq; diff --git a/Tests/FluentAssertions.Json.Specs/WithoutStrictOrderingSpecs.cs b/Tests/FluentAssertions.Json.Specs/WithoutStrictOrderingSpecs.cs index 2cd47d8e..564ddd19 100644 --- a/Tests/FluentAssertions.Json.Specs/WithoutStrictOrderingSpecs.cs +++ b/Tests/FluentAssertions.Json.Specs/WithoutStrictOrderingSpecs.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Newtonsoft.Json.Linq; using Xunit; using Xunit.Sdk; From 4a39b265f7f0a986509dd20664c4cf37de72f43c Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sun, 16 Nov 2025 11:43:34 +0100 Subject: [PATCH 11/26] SA1028: remove trailing whitespace --- Src/FluentAssertions.Json/Difference.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Src/FluentAssertions.Json/Difference.cs b/Src/FluentAssertions.Json/Difference.cs index 511360a0..7edee1a1 100644 --- a/Src/FluentAssertions.Json/Difference.cs +++ b/Src/FluentAssertions.Json/Difference.cs @@ -39,9 +39,9 @@ public override string ToString() DifferenceKind.ExpectedMissesProperty => $"has extra property {Path}", DifferenceKind.ActualMissesElement => $"misses expected element {Path}", DifferenceKind.WrongOrder => $"has expected element {Path} in the wrong order", -#pragma warning disable S3928, MA0015 // Parameter names used into ArgumentException constructors should match an existing one +#pragma warning disable S3928, MA0015 // Parameter names used into ArgumentException constructors should match an existing one _ => throw new ArgumentOutOfRangeException(), -#pragma warning restore S3928, MA0015 // Parameter names used into ArgumentException constructors should match an existing one +#pragma warning restore S3928, MA0015 // Parameter names used into ArgumentException constructors should match an existing one }; } } From 7b97c851774fc4dc888800e7bb586a664d9a6e9a Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sun, 16 Nov 2025 11:47:14 +0100 Subject: [PATCH 12/26] RCS1235: Optimize 'string.Compare' call --- Src/FluentAssertions.Json/Common/JTokenExtensions.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Src/FluentAssertions.Json/Common/JTokenExtensions.cs b/Src/FluentAssertions.Json/Common/JTokenExtensions.cs index e43cd73a..b6677ced 100644 --- a/Src/FluentAssertions.Json/Common/JTokenExtensions.cs +++ b/Src/FluentAssertions.Json/Common/JTokenExtensions.cs @@ -56,7 +56,7 @@ public int Compare(JToken x, JToken y) JProperty p => Compare(p, (JProperty)y), JValue v => Compare(v, (JValue)y), JConstructor c => Compare(c, (JConstructor)y), - _ => string.Compare(x.ToString(), y.ToString(), StringComparison.Ordinal) + _ => string.CompareOrdinal(x.ToString(), y.ToString()) }; } @@ -64,7 +64,7 @@ public int Compare(JToken x, JToken y) private static int Compare(JConstructor x, JConstructor y) { - var nameComparison = string.Compare(x.Name, y.Name, StringComparison.Ordinal); + var nameComparison = string.CompareOrdinal(x.Name, y.Name); return nameComparison != 0 ? nameComparison : Compare(x, (JContainer)y); } @@ -97,7 +97,7 @@ private static int Compare(JObject x, JObject y) private static int Compare(JProperty x, JProperty y) { - var nameComparison = string.Compare(x.Name, y.Name, StringComparison.Ordinal); + var nameComparison = string.CompareOrdinal(x.Name, y.Name); return nameComparison != 0 ? nameComparison : Comparer.Compare(x.Value, y.Value); } } From d6fb239019d1ab4f126a030b8a90785637dd3613 Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sun, 16 Nov 2025 11:53:00 +0100 Subject: [PATCH 13/26] Use raw string over verbatim string --- .../JTokenAssertionsSpecs.cs | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/Tests/FluentAssertions.Json.Specs/JTokenAssertionsSpecs.cs b/Tests/FluentAssertions.Json.Specs/JTokenAssertionsSpecs.cs index 6193dcd9..5cfd433a 100644 --- a/Tests/FluentAssertions.Json.Specs/JTokenAssertionsSpecs.cs +++ b/Tests/FluentAssertions.Json.Specs/JTokenAssertionsSpecs.cs @@ -29,24 +29,25 @@ public void When_both_tokens_are_null_they_should_be_treated_as_equivalent() public void When_both_tokens_represent_the_same_json_content_they_should_be_treated_as_equivalent() { // Arrange - string json = @" + string json = + """ { friends: [{ id: 123, - name: ""John Doe"" + name: "John Doe" }, { id: 456, - name: ""Jane Doe"", + name: "Jane Doe", kids: [ - ""Jimmy"", - ""James"" + "Jimmy", + "James" ] } ] } - "; + """; var a = JToken.Parse(json); var b = JToken.Parse(json); @@ -258,24 +259,25 @@ public void When_only_the_order_of_properties_differ_they_should_be_treated_as_e public void When_a_token_is_compared_to_its_string_representation_they_should_be_treated_as_equivalent() { // Arrange - string jsonString = @" + string jsonString = + """ { friends: [{ id: 123, - name: ""John Doe"" + name: "John Doe" }, { id: 456, - name: ""Jane Doe"", + name: "Jane Doe", kids: [ - ""Jimmy"", - ""James"" + "Jimmy", + "James" ] } ] - } - "; + } + """; var actualJSON = JToken.Parse(jsonString); From 48381ef9c0f6e5d5d4c8525e04b6dd13748eea96 Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sun, 16 Nov 2025 11:53:28 +0100 Subject: [PATCH 14/26] RCS1251: remove unnecessary braces --- Tests/FluentAssertions.Json.Specs/JsonAssertionOptionsSpecs.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/FluentAssertions.Json.Specs/JsonAssertionOptionsSpecs.cs b/Tests/FluentAssertions.Json.Specs/JsonAssertionOptionsSpecs.cs index b246f509..b54427af 100644 --- a/Tests/FluentAssertions.Json.Specs/JsonAssertionOptionsSpecs.cs +++ b/Tests/FluentAssertions.Json.Specs/JsonAssertionOptionsSpecs.cs @@ -39,5 +39,5 @@ public void Dispose() // Due to tests that call AssertionOptions [CollectionDefinition("AssertionOptionsSpecs", DisableParallelization = true)] - public class AssertionOptionsSpecsDefinition { } + public class AssertionOptionsSpecsDefinition; } From 6ccebde2c6520cc6254ffa65bba40f5468b61e50 Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sun, 16 Nov 2025 12:08:03 +0100 Subject: [PATCH 15/26] IDE0300: use collection expressions --- .../ShouldBeJsonSerializableTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/FluentAssertions.Json.Specs/ShouldBeJsonSerializableTests.cs b/Tests/FluentAssertions.Json.Specs/ShouldBeJsonSerializableTests.cs index 0c6a5fa6..b433542e 100644 --- a/Tests/FluentAssertions.Json.Specs/ShouldBeJsonSerializableTests.cs +++ b/Tests/FluentAssertions.Json.Specs/ShouldBeJsonSerializableTests.cs @@ -25,7 +25,7 @@ public void Simple_poco_should_be_serializable() Weight = 1, ShoeSize = 1, IsActive = true, - Image = new[] { (byte)1 }, + Image = [1], Category = '1' }; From fb00ce3ddd8bcd9d984ea5c43c1db059576becd3 Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sun, 16 Nov 2025 12:09:37 +0100 Subject: [PATCH 16/26] Use xunit's MemberData --- .../JTokenAssertionsSpecs.cs | 133 ++++++++---------- 1 file changed, 56 insertions(+), 77 deletions(-) diff --git a/Tests/FluentAssertions.Json.Specs/JTokenAssertionsSpecs.cs b/Tests/FluentAssertions.Json.Specs/JTokenAssertionsSpecs.cs index 5cfd433a..14e970e1 100644 --- a/Tests/FluentAssertions.Json.Specs/JTokenAssertionsSpecs.cs +++ b/Tests/FluentAssertions.Json.Specs/JTokenAssertionsSpecs.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using FluentAssertions.Formatting; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -149,44 +148,37 @@ public void When_both_tokens_are_not_equivalent_it_should_throw_and_mention_the_ .WithMessage(expectedMessage); } - [Fact] - public void When_properties_differ_between_two_tokens_it_should_not_treat_them_as_equivalent() + [Theory] + [MemberData(nameof(PropertiesDifferingBetweenTwoTokens))] + public void When_properties_differ_between_two_tokens_it_should_not_treat_them_as_equivalent(JToken actual, JToken expected, string expectedDifference) { - // Arrange - var testCases = new[] - { - Tuple.Create( - new JProperty("eyes", "blue"), - new JArray(), - "has a property instead of an array at $") - , - Tuple.Create( - new JProperty("eyes", "blue"), - new JProperty("hair", "black"), - "has a different name at $") - , - }; - - foreach (var testCase in testCases) - { - var actual = testCase.Item1; - var expected = testCase.Item2; - var expectedDifference = testCase.Item3; - - var expectedMessage = + // Act & Assert + var expectedMessage = $"JSON document {expectedDifference}." + "Actual document" + $"{Format(actual, true)}" + "was expected to be equivalent to" + $"{Format(expected, true)}."; - // Act & Assert - actual.Should().Invoking(x => x.BeEquivalentTo(expected)) - .Should().Throw() - .WithMessage(expectedMessage); - } + actual.Should().Invoking(x => x.BeEquivalentTo(expected)) + .Should().Throw() + .WithMessage(expectedMessage); } + public static TheoryData PropertiesDifferingBetweenTwoTokens => new() + { + { + new JProperty("eyes", "blue"), + new JArray(), + "has a property instead of an array at $" + }, + { + new JProperty("eyes", "blue"), + new JProperty("hair", "black"), + "has a different name at $" + }, + }; + [Fact] public void When_both_property_values_are_null_it_should_treat_them_as_equivalent() { @@ -198,62 +190,49 @@ public void When_both_property_values_are_null_it_should_treat_them_as_equivalen actual.Should().BeEquivalentTo(expected); } - [Fact] - public void When_two_json_arrays_have_the_same_properties_in_the_same_order_they_should_be_treated_as_equivalent() + [Theory] + [MemberData(nameof(JsonArraysHavingTheSamePropertiesInTheSameOrder))] + public void When_two_json_arrays_have_the_same_properties_in_the_same_order_they_should_be_treated_as_equivalent(JArray actual, JArray expected) { - // Arrange - var testCases = new[] - { - Tuple.Create( - new JArray(1, 2, 3), - new JArray(1, 2, 3)) - , - Tuple.Create( - new JArray("blue", "green"), - new JArray("blue", "green")) - , - Tuple.Create( - new JArray(JToken.Parse("{ car: { color: \"blue\" }}"), JToken.Parse("{ flower: { color: \"red\" }}")), - new JArray(JToken.Parse("{ car: { color: \"blue\" }}"), JToken.Parse("{ flower: { color: \"red\" }}"))) - }; + // Act & Assert + actual.Should().BeEquivalentTo(expected); + } - foreach (var testCase in testCases) + public static TheoryData JsonArraysHavingTheSamePropertiesInTheSameOrder => new() + { + { + new JArray(1, 2, 3), + new JArray(1, 2, 3) + }, { - var actual = testCase.Item1; - var expected = testCase.Item2; + new JArray("blue", "green"), + new JArray("blue", "green") + }, + { + new JArray(JToken.Parse("{ car: { color: \"blue\" }}"), JToken.Parse("{ flower: { color: \"red\" }}")), + new JArray(JToken.Parse("{ car: { color: \"blue\" }}"), JToken.Parse("{ flower: { color: \"red\" }}")) + }, + }; - // Act & Assert - actual.Should().BeEquivalentTo(expected); - } + [Theory] + [MemberData(nameof(JsonArraysHavingTheSamePropertiesInDifferentOrder))] + public void When_only_the_order_of_properties_differ_they_should_be_treated_as_equivalent(JToken actual, JToken expected) + { + // Act & Assert + actual.Should().BeEquivalentTo(expected); } - [Fact] - public void When_only_the_order_of_properties_differ_they_should_be_treated_as_equivalent() + public static TheoryData JsonArraysHavingTheSamePropertiesInDifferentOrder => new() { - // Arrange - var testCases = new Dictionary { - { - "{ friends: [{ id: 123, name: \"Corby Page\" }, { id: 456, name: \"Carter Page\" }] }", - "{ friends: [{ name: \"Corby Page\", id: 123 }, { id: 456, name: \"Carter Page\" }] }" - }, - { - "{ id: 2, admin: true }", - "{ admin: true, id: 2}" - } - }; - - foreach (var testCase in testCases) + JToken.Parse("{ friends: [{ id: 123, name: \"Corby Page\" }, { id: 456, name: \"Carter Page\" }] }"), + JToken.Parse("{ friends: [{ name: \"Corby Page\", id: 123 }, { id: 456, name: \"Carter Page\" }] }") + }, { - var actualJson = testCase.Key; - var expectedJson = testCase.Value; - var a = JToken.Parse(actualJson); - var b = JToken.Parse(expectedJson); - - // Act & Assert - a.Should().BeEquivalentTo(b); - } - } + JToken.Parse("{ id: 2, admin: true }"), + JToken.Parse("{ admin: true, id: 2}") + }, + }; [Fact] public void When_a_token_is_compared_to_its_string_representation_they_should_be_treated_as_equivalent() From 7b878e8e1c148092f19d9ec660018a7c27081a20 Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sun, 16 Nov 2025 12:11:53 +0100 Subject: [PATCH 17/26] Use file-scoped namespace --- .../Common/JTokenExtensions.cs | 151 +- .../Common/StringExtensions.cs | 23 +- Src/FluentAssertions.Json/Difference.cs | 65 +- Src/FluentAssertions.Json/DifferenceKind.cs | 27 +- .../IJsonAssertionOptions.cs | 33 +- .../IJsonAssertionRestriction.cs | 21 +- Src/FluentAssertions.Json/JPath.cs | 49 +- Src/FluentAssertions.Json/JTokenAssertions.cs | 883 +++++---- .../JTokenDifferentiator.cs | 377 ++-- Src/FluentAssertions.Json/JTokenFormatter.cs | 55 +- .../JsonAssertionExtensions.cs | 55 +- .../JsonAssertionOptions.cs | 43 +- .../JsonAssertionRestriction.cs | 25 +- .../ObjectAssertionsExtensions.cs | 165 +- .../StringAssertionsExtensions.cs | 39 +- .../JTokenAssertionsSpecs.cs | 1615 ++++++++--------- .../JTokenComparerSpecs.cs | 297 ++- .../JTokenFormatterSpecs.cs | 83 +- .../JsonAssertionExtensionsSpecs.cs | 41 +- .../JsonAssertionOptionsSpecs.cs | 55 +- .../Models/AddressDto.cs | 29 +- .../Models/EmploymentDto.cs | 17 +- .../Models/PocoWithIgnoredProperty.cs | 13 +- .../Models/PocoWithNoDefaultConstructor.cs | 23 +- .../Models/PocoWithStructure.cs | 19 +- .../Models/SimplePocoWithPrimitiveTypes.cs | 33 +- .../ShouldBeJsonSerializableTests.cs | 291 ++- .../StringAssertionsExtensionsSpecs.cs | 125 +- .../StringExtensions.cs | 11 +- .../WithoutStrictOrderingSpecs.cs | 101 +- 30 files changed, 2367 insertions(+), 2397 deletions(-) diff --git a/Src/FluentAssertions.Json/Common/JTokenExtensions.cs b/Src/FluentAssertions.Json/Common/JTokenExtensions.cs index b6677ced..7322bfce 100644 --- a/Src/FluentAssertions.Json/Common/JTokenExtensions.cs +++ b/Src/FluentAssertions.Json/Common/JTokenExtensions.cs @@ -3,103 +3,102 @@ using System.Linq; using Newtonsoft.Json.Linq; -namespace FluentAssertions.Json.Common +namespace FluentAssertions.Json.Common; + +internal static class JTokenExtensions { - internal static class JTokenExtensions - { - private static readonly JTokenComparer Comparer = new(); + private static readonly JTokenComparer Comparer = new(); - /// - /// Recursively sorts the properties of JObject instances by name and - /// the elements of JArray instances by their string representation, - /// producing a normalized JToken for consistent comparison - /// - public static JToken Normalize(this JToken token) + /// + /// Recursively sorts the properties of JObject instances by name and + /// the elements of JArray instances by their string representation, + /// producing a normalized JToken for consistent comparison + /// + public static JToken Normalize(this JToken token) + { + return token switch { - return token switch - { - JObject obj => new JObject(obj.Properties().OrderBy(p => p.Name, StringComparer.Ordinal).Select(p => new JProperty(p.Name, Normalize(p.Value)))), - JArray array => new JArray(array.Select(Normalize).OrderBy(x => x, Comparer)), - _ => token - }; - } + JObject obj => new JObject(obj.Properties().OrderBy(p => p.Name, StringComparer.Ordinal).Select(p => new JProperty(p.Name, Normalize(p.Value)))), + JArray array => new JArray(array.Select(Normalize).OrderBy(x => x, Comparer)), + _ => token + }; + } - private sealed class JTokenComparer : IComparer + private sealed class JTokenComparer : IComparer + { + public int Compare(JToken x, JToken y) { - public int Compare(JToken x, JToken y) + if (ReferenceEquals(x, y)) { - if (ReferenceEquals(x, y)) - { - return 0; - } - - if (x is null) - { - return -1; - } - - if (y is null) - { - return 1; - } - - var typeComparison = x.Type.CompareTo(y.Type); - if (typeComparison != 0) - { - return typeComparison; - } - - return x switch - { - JArray a => Compare(a, (JArray)y), - JObject o => Compare(o, (JObject)y), - JProperty p => Compare(p, (JProperty)y), - JValue v => Compare(v, (JValue)y), - JConstructor c => Compare(c, (JConstructor)y), - _ => string.CompareOrdinal(x.ToString(), y.ToString()) - }; + return 0; } - private static int Compare(JValue x, JValue y) => Comparer.Default.Compare(x.Value, y.Value); - - private static int Compare(JConstructor x, JConstructor y) + if (x is null) { - var nameComparison = string.CompareOrdinal(x.Name, y.Name); - return nameComparison != 0 ? nameComparison : Compare(x, (JContainer)y); + return -1; } - private static int Compare(JContainer x, JContainer y) + if (y is null) { - var countComparison = x.Count.CompareTo(y.Count); - if (countComparison != 0) - { - return countComparison; - } + return 1; + } - return x - .Select((t, i) => Comparer.Compare(t, y[i])) - .FirstOrDefault(itemComparison => itemComparison != 0); + var typeComparison = x.Type.CompareTo(y.Type); + if (typeComparison != 0) + { + return typeComparison; } - private static int Compare(JObject x, JObject y) + return x switch { - var countComparison = x.Count.CompareTo(y.Count); - if (countComparison != 0) - { - return countComparison; - } + JArray a => Compare(a, (JArray)y), + JObject o => Compare(o, (JObject)y), + JProperty p => Compare(p, (JProperty)y), + JValue v => Compare(v, (JValue)y), + JConstructor c => Compare(c, (JConstructor)y), + _ => string.CompareOrdinal(x.ToString(), y.ToString()) + }; + } + + private static int Compare(JValue x, JValue y) => Comparer.Default.Compare(x.Value, y.Value); + + private static int Compare(JConstructor x, JConstructor y) + { + var nameComparison = string.CompareOrdinal(x.Name, y.Name); + return nameComparison != 0 ? nameComparison : Compare(x, (JContainer)y); + } - return x.Properties() - .OrderBy(p => p.Name, StringComparer.Ordinal) - .Zip(y.Properties().OrderBy(p => p.Name, StringComparer.Ordinal), (px, py) => Compare(px, py)) - .FirstOrDefault(itemComparison => itemComparison != 0); + private static int Compare(JContainer x, JContainer y) + { + var countComparison = x.Count.CompareTo(y.Count); + if (countComparison != 0) + { + return countComparison; } - private static int Compare(JProperty x, JProperty y) + return x + .Select((t, i) => Comparer.Compare(t, y[i])) + .FirstOrDefault(itemComparison => itemComparison != 0); + } + + private static int Compare(JObject x, JObject y) + { + var countComparison = x.Count.CompareTo(y.Count); + if (countComparison != 0) { - var nameComparison = string.CompareOrdinal(x.Name, y.Name); - return nameComparison != 0 ? nameComparison : Comparer.Compare(x.Value, y.Value); + return countComparison; } + + return x.Properties() + .OrderBy(p => p.Name, StringComparer.Ordinal) + .Zip(y.Properties().OrderBy(p => p.Name, StringComparer.Ordinal), (px, py) => Compare(px, py)) + .FirstOrDefault(itemComparison => itemComparison != 0); + } + + private static int Compare(JProperty x, JProperty y) + { + var nameComparison = string.CompareOrdinal(x.Name, y.Name); + return nameComparison != 0 ? nameComparison : Comparer.Compare(x.Value, y.Value); } } } diff --git a/Src/FluentAssertions.Json/Common/StringExtensions.cs b/Src/FluentAssertions.Json/Common/StringExtensions.cs index 43922f47..928e6c35 100644 --- a/Src/FluentAssertions.Json/Common/StringExtensions.cs +++ b/Src/FluentAssertions.Json/Common/StringExtensions.cs @@ -1,16 +1,15 @@ -namespace FluentAssertions.Json.Common +namespace FluentAssertions.Json.Common; + +internal static class StringExtensions { - internal static class StringExtensions - { - /// - /// Replaces all characters that might conflict with formatting placeholders with their escaped counterparts. - /// - public static string EscapePlaceholders(this string value) => - value.Replace("{", "{{").Replace("}", "}}"); + /// + /// Replaces all characters that might conflict with formatting placeholders with their escaped counterparts. + /// + public static string EscapePlaceholders(this string value) => + value.Replace("{", "{{").Replace("}", "}}"); - public static string RemoveNewLines(this string @this) - { - return @this.Replace("\n", string.Empty).Replace("\r", string.Empty).Replace("\\r\\n", string.Empty); - } + public static string RemoveNewLines(this string @this) + { + return @this.Replace("\n", string.Empty).Replace("\r", string.Empty).Replace("\\r\\n", string.Empty); } } diff --git a/Src/FluentAssertions.Json/Difference.cs b/Src/FluentAssertions.Json/Difference.cs index 7edee1a1..29c35b13 100644 --- a/Src/FluentAssertions.Json/Difference.cs +++ b/Src/FluentAssertions.Json/Difference.cs @@ -1,48 +1,47 @@ using System; -namespace FluentAssertions.Json +namespace FluentAssertions.Json; + +internal sealed class Difference { - internal sealed class Difference + public Difference(DifferenceKind kind, JPath path, object actual, object expected) + : this(kind, path) { - public Difference(DifferenceKind kind, JPath path, object actual, object expected) - : this(kind, path) - { - Actual = actual; - Expected = expected; - } + Actual = actual; + Expected = expected; + } - public Difference(DifferenceKind kind, JPath path) - { - Kind = kind; - Path = path; - } + public Difference(DifferenceKind kind, JPath path) + { + Kind = kind; + Path = path; + } - private DifferenceKind Kind { get; } + private DifferenceKind Kind { get; } - private JPath Path { get; } + private JPath Path { get; } - private object Actual { get; } + private object Actual { get; } - private object Expected { get; } + private object Expected { get; } - public override string ToString() + public override string ToString() + { + return Kind switch { - return Kind switch - { - DifferenceKind.ActualIsNull => "is null", - DifferenceKind.ExpectedIsNull => "is not null", - DifferenceKind.OtherType => $"has {Actual} instead of {Expected} at {Path}", - DifferenceKind.OtherName => $"has a different name at {Path}", - DifferenceKind.OtherValue => $"has a different value at {Path}", - DifferenceKind.DifferentLength => $"has {Actual} elements instead of {Expected} at {Path}", - DifferenceKind.ActualMissesProperty => $"misses property {Path}", - DifferenceKind.ExpectedMissesProperty => $"has extra property {Path}", - DifferenceKind.ActualMissesElement => $"misses expected element {Path}", - DifferenceKind.WrongOrder => $"has expected element {Path} in the wrong order", + DifferenceKind.ActualIsNull => "is null", + DifferenceKind.ExpectedIsNull => "is not null", + DifferenceKind.OtherType => $"has {Actual} instead of {Expected} at {Path}", + DifferenceKind.OtherName => $"has a different name at {Path}", + DifferenceKind.OtherValue => $"has a different value at {Path}", + DifferenceKind.DifferentLength => $"has {Actual} elements instead of {Expected} at {Path}", + DifferenceKind.ActualMissesProperty => $"misses property {Path}", + DifferenceKind.ExpectedMissesProperty => $"has extra property {Path}", + DifferenceKind.ActualMissesElement => $"misses expected element {Path}", + DifferenceKind.WrongOrder => $"has expected element {Path} in the wrong order", #pragma warning disable S3928, MA0015 // Parameter names used into ArgumentException constructors should match an existing one - _ => throw new ArgumentOutOfRangeException(), + _ => throw new ArgumentOutOfRangeException(), #pragma warning restore S3928, MA0015 // Parameter names used into ArgumentException constructors should match an existing one - }; - } + }; } } diff --git a/Src/FluentAssertions.Json/DifferenceKind.cs b/Src/FluentAssertions.Json/DifferenceKind.cs index fa37b053..34387bc5 100644 --- a/Src/FluentAssertions.Json/DifferenceKind.cs +++ b/Src/FluentAssertions.Json/DifferenceKind.cs @@ -1,16 +1,15 @@ -namespace FluentAssertions.Json +namespace FluentAssertions.Json; + +internal enum DifferenceKind { - internal enum DifferenceKind - { - ActualIsNull, - ExpectedIsNull, - OtherType, - OtherName, - OtherValue, - DifferentLength, - ActualMissesProperty, - ExpectedMissesProperty, - ActualMissesElement, - WrongOrder - } + ActualIsNull, + ExpectedIsNull, + OtherType, + OtherName, + OtherValue, + DifferentLength, + ActualMissesProperty, + ExpectedMissesProperty, + ActualMissesElement, + WrongOrder } diff --git a/Src/FluentAssertions.Json/IJsonAssertionOptions.cs b/Src/FluentAssertions.Json/IJsonAssertionOptions.cs index ecd0df38..9a581f19 100644 --- a/Src/FluentAssertions.Json/IJsonAssertionOptions.cs +++ b/Src/FluentAssertions.Json/IJsonAssertionOptions.cs @@ -1,27 +1,26 @@ using System; using FluentAssertions.Equivalency; -namespace FluentAssertions.Json +namespace FluentAssertions.Json; + +/// +/// Provides the run-time details of the class. +/// +public interface IJsonAssertionOptions { /// - /// Provides the run-time details of the class. + /// Overrides the comparison of subject and expectation to use provided + /// when the predicate is met. /// - public interface IJsonAssertionOptions - { - /// - /// Overrides the comparison of subject and expectation to use provided - /// when the predicate is met. - /// - /// - /// The assertion to execute when the predicate is met. - /// + /// + /// The assertion to execute when the predicate is met. + /// #pragma warning disable CA1716 // CA1716: Identifiers should not match keywords - IJsonAssertionRestriction Using(Action> action); + IJsonAssertionRestriction Using(Action> action); #pragma warning restore CA1716 - /// - /// Configures the JSON assertion to ignore the order of elements in arrays or collections during comparison, allowing for equivalency checks regardless of element sequence. - /// - IJsonAssertionOptions WithoutStrictOrdering(); - } + /// + /// Configures the JSON assertion to ignore the order of elements in arrays or collections during comparison, allowing for equivalency checks regardless of element sequence. + /// + IJsonAssertionOptions WithoutStrictOrdering(); } diff --git a/Src/FluentAssertions.Json/IJsonAssertionRestriction.cs b/Src/FluentAssertions.Json/IJsonAssertionRestriction.cs index d8d37de5..a79f4166 100644 --- a/Src/FluentAssertions.Json/IJsonAssertionRestriction.cs +++ b/Src/FluentAssertions.Json/IJsonAssertionRestriction.cs @@ -1,15 +1,14 @@ -namespace FluentAssertions.Json +namespace FluentAssertions.Json; + +/// +/// Defines additional overrides when used with +/// +public interface IJsonAssertionRestriction { /// - /// Defines additional overrides when used with + /// Allows overriding the way structural equality is applied to (nested) objects of type + /// /// - public interface IJsonAssertionRestriction - { - /// - /// Allows overriding the way structural equality is applied to (nested) objects of type - /// - /// - IJsonAssertionOptions WhenTypeIs() - where TMemberType : TMember; - } + IJsonAssertionOptions WhenTypeIs() + where TMemberType : TMember; } diff --git a/Src/FluentAssertions.Json/JPath.cs b/Src/FluentAssertions.Json/JPath.cs index 3cfcb742..9dfb8bcb 100644 --- a/Src/FluentAssertions.Json/JPath.cs +++ b/Src/FluentAssertions.Json/JPath.cs @@ -1,35 +1,34 @@ using System.Collections.Generic; -namespace FluentAssertions.Json +namespace FluentAssertions.Json; + +internal sealed class JPath { - internal sealed class JPath - { - private readonly List nodes = new(); + private readonly List nodes = new(); - public JPath() - { - nodes.Add("$"); - } + public JPath() + { + nodes.Add("$"); + } - private JPath(JPath existingPath, string extraNode) - { - nodes.AddRange(existingPath.nodes); - nodes.Add(extraNode); - } + private JPath(JPath existingPath, string extraNode) + { + nodes.AddRange(existingPath.nodes); + nodes.Add(extraNode); + } - public JPath AddProperty(string name) - { - return new JPath(this, $".{name}"); - } + public JPath AddProperty(string name) + { + return new JPath(this, $".{name}"); + } - public JPath AddIndex(int index) - { - return new JPath(this, $"[{index}]"); - } + public JPath AddIndex(int index) + { + return new JPath(this, $"[{index}]"); + } - public override string ToString() - { - return string.Concat(nodes); - } + public override string ToString() + { + return string.Concat(nodes); } } diff --git a/Src/FluentAssertions.Json/JTokenAssertions.cs b/Src/FluentAssertions.Json/JTokenAssertions.cs index be92f8db..f737e25d 100644 --- a/Src/FluentAssertions.Json/JTokenAssertions.cs +++ b/Src/FluentAssertions.Json/JTokenAssertions.cs @@ -8,508 +8,507 @@ using FluentAssertions.Primitives; using Newtonsoft.Json.Linq; -namespace FluentAssertions.Json +namespace FluentAssertions.Json; + +/// +/// Contains a number of methods to assert that an is in the expected state. +/// +[DebuggerNonUserCode] +public class JTokenAssertions : ReferenceTypeAssertions { + private GenericCollectionAssertions EnumerableSubject { get; } + + static JTokenAssertions() + { + Formatter.AddFormatter(new JTokenFormatter()); + } + /// - /// Contains a number of methods to assert that an is in the expected state. + /// Initializes a new instance of the class. /// - [DebuggerNonUserCode] - public class JTokenAssertions : ReferenceTypeAssertions + /// The subject + /// The assertion chain + public JTokenAssertions(JToken subject, AssertionChain assertionChain) + : base(subject, assertionChain) { - private GenericCollectionAssertions EnumerableSubject { get; } + EnumerableSubject = new GenericCollectionAssertions(subject, assertionChain); + } - static JTokenAssertions() - { - Formatter.AddFormatter(new JTokenFormatter()); - } + /// + /// Returns the type of the subject the assertion applies on. + /// + protected override string Identifier => nameof(JToken); - /// - /// Initializes a new instance of the class. - /// - /// The subject - /// The assertion chain - public JTokenAssertions(JToken subject, AssertionChain assertionChain) - : base(subject, assertionChain) + /// + /// Asserts that the current is equivalent to the parsed JSON, + /// using an equivalent of . + /// + /// The string representation of the expected element + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndConstraint BeEquivalentTo(string expected, string because = "", + params object[] becauseArgs) + { + JToken parsedExpected; + try { - EnumerableSubject = new GenericCollectionAssertions(subject, assertionChain); + parsedExpected = JToken.Parse(expected); } - - /// - /// Returns the type of the subject the assertion applies on. - /// - protected override string Identifier => nameof(JToken); - - /// - /// Asserts that the current is equivalent to the parsed JSON, - /// using an equivalent of . - /// - /// The string representation of the expected element - /// - /// A formatted phrase as is supported by explaining why the assertion - /// is needed. If the phrase does not start with the word because, it is prepended automatically. - /// - /// - /// Zero or more objects to format using the placeholders in . - /// - public AndConstraint BeEquivalentTo(string expected, string because = "", - params object[] becauseArgs) + catch (Exception ex) { - JToken parsedExpected; - try - { - parsedExpected = JToken.Parse(expected); - } - catch (Exception ex) - { - throw new ArgumentException( - $"Unable to parse expected JSON string:{Environment.NewLine}" + - $"{expected}{Environment.NewLine}" + - "Check inner exception for more details.", - nameof(expected), ex); - } - - return BeEquivalentTo(parsedExpected, because, becauseArgs); + throw new ArgumentException( + $"Unable to parse expected JSON string:{Environment.NewLine}" + + $"{expected}{Environment.NewLine}" + + "Check inner exception for more details.", + nameof(expected), ex); } - /// - /// Asserts that the current is equivalent to the element, - /// using an equivalent of . - /// - /// The expected element - /// - /// A formatted phrase as is supported by explaining why the assertion - /// is needed. If the phrase does not start with the word because, it is prepended automatically. - /// - /// - /// Zero or more objects to format using the placeholders in . - /// - public AndConstraint BeEquivalentTo(JToken expected, string because = "", - params object[] becauseArgs) - { - return BeEquivalentTo(expected, false, options => options, because, becauseArgs); - } + return BeEquivalentTo(parsedExpected, because, becauseArgs); + } - /// - /// Asserts that the current is equivalent to the element, - /// using an equivalent of . - /// - /// The expected element - /// The options to consider while asserting values - /// - /// A formatted phrase as is supported by explaining why the assertion - /// is needed. If the phrase does not start with the word because, it is prepended automatically. - /// - /// - /// Zero or more objects to format using the placeholders in . - /// - public AndConstraint BeEquivalentTo(JToken expected, - Func, IJsonAssertionOptions> config, - string because = "", - params object[] becauseArgs) - { - return BeEquivalentTo(expected, false, config, because, becauseArgs); - } + /// + /// Asserts that the current is equivalent to the element, + /// using an equivalent of . + /// + /// The expected element + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndConstraint BeEquivalentTo(JToken expected, string because = "", + params object[] becauseArgs) + { + return BeEquivalentTo(expected, false, options => options, because, becauseArgs); + } - private AndConstraint BeEquivalentTo(JToken expected, bool ignoreExtraProperties, - Func, IJsonAssertionOptions> config, - string because = "", - params object[] becauseArgs) - { - var differentiator = new JTokenDifferentiator(ignoreExtraProperties, config); - Difference difference = differentiator.FindFirstDifference(Subject, expected); + /// + /// Asserts that the current is equivalent to the element, + /// using an equivalent of . + /// + /// The expected element + /// The options to consider while asserting values + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndConstraint BeEquivalentTo(JToken expected, + Func, IJsonAssertionOptions> config, + string because = "", + params object[] becauseArgs) + { + return BeEquivalentTo(expected, false, config, because, becauseArgs); + } - var expectation = ignoreExtraProperties ? "was expected to contain" : "was expected to be equivalent to"; + private AndConstraint BeEquivalentTo(JToken expected, bool ignoreExtraProperties, + Func, IJsonAssertionOptions> config, + string because = "", + params object[] becauseArgs) + { + var differentiator = new JTokenDifferentiator(ignoreExtraProperties, config); + Difference difference = differentiator.FindFirstDifference(Subject, expected); - var message = $"JSON document {difference?.ToString().EscapePlaceholders()}.{Environment.NewLine}" + - $"Actual document{Environment.NewLine}" + - $"{Format(Subject, true).EscapePlaceholders()}{Environment.NewLine}" + - $"{expectation}{Environment.NewLine}" + - $"{Format(expected, true).EscapePlaceholders()}{Environment.NewLine}" + - "{reason}."; + var expectation = ignoreExtraProperties ? "was expected to contain" : "was expected to be equivalent to"; - CurrentAssertionChain - .ForCondition(difference == null) - .BecauseOf(because, becauseArgs) - .FailWith(message); + var message = $"JSON document {difference?.ToString().EscapePlaceholders()}.{Environment.NewLine}" + + $"Actual document{Environment.NewLine}" + + $"{Format(Subject, true).EscapePlaceholders()}{Environment.NewLine}" + + $"{expectation}{Environment.NewLine}" + + $"{Format(expected, true).EscapePlaceholders()}{Environment.NewLine}" + + "{reason}."; - return new AndConstraint(this); - } + CurrentAssertionChain + .ForCondition(difference == null) + .BecauseOf(because, becauseArgs) + .FailWith(message); - /// - /// Asserts that the current is not equivalent to the parsed JSON, - /// using an equivalent of . - /// - /// The string representation of the unexpected element - /// - /// A formatted phrase as is supported by explaining why the assertion - /// is needed. If the phrase does not start with the word because, it is prepended automatically. - /// - /// - /// Zero or more objects to format using the placeholders in . - /// - public AndConstraint NotBeEquivalentTo(string unexpected, string because = "", - params object[] becauseArgs) - { - JToken parsedUnexpected; - try - { - parsedUnexpected = JToken.Parse(unexpected); - } - catch (Exception ex) - { - throw new ArgumentException( - $"Unable to parse unexpected JSON string:{Environment.NewLine}" + - $"{unexpected}{Environment.NewLine}" + - "Check inner exception for more details.", - nameof(unexpected), ex); - } - - return NotBeEquivalentTo(parsedUnexpected, because, becauseArgs); - } + return new AndConstraint(this); + } - /// - /// Asserts that the current is not equivalent to the element, - /// using an equivalent of . - /// - /// The unexpected element - /// - /// A formatted phrase as is supported by explaining why the assertion - /// is needed. If the phrase does not start with the word because, it is prepended automatically. - /// - /// - /// Zero or more objects to format using the placeholders in . - /// - public AndConstraint NotBeEquivalentTo(JToken unexpected, string because = "", params object[] becauseArgs) + /// + /// Asserts that the current is not equivalent to the parsed JSON, + /// using an equivalent of . + /// + /// The string representation of the unexpected element + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndConstraint NotBeEquivalentTo(string unexpected, string because = "", + params object[] becauseArgs) + { + JToken parsedUnexpected; + try { - CurrentAssertionChain - .ForCondition((Subject is null && unexpected is not null) || - !JToken.DeepEquals(Subject, unexpected)) - .BecauseOf(because, becauseArgs) - .FailWith("Expected JSON document not to be equivalent to {0}{reason}.", unexpected); - - return new AndConstraint(this); + parsedUnexpected = JToken.Parse(unexpected); } - - /// - /// Asserts that the current has the specified value. - /// - /// The expected value - public AndConstraint HaveValue(string expected) + catch (Exception ex) { - return HaveValue(expected, string.Empty); + throw new ArgumentException( + $"Unable to parse unexpected JSON string:{Environment.NewLine}" + + $"{unexpected}{Environment.NewLine}" + + "Check inner exception for more details.", + nameof(unexpected), ex); } - /// - /// Asserts that the current has the specified value. - /// - /// The expected value - /// - /// A formatted phrase as is supported by explaining why the assertion - /// is needed. If the phrase does not start with the word because, it is prepended automatically. - /// - /// - /// Zero or more objects to format using the placeholders in . - /// - public AndConstraint HaveValue(string expected, string because, params object[] becauseArgs) - { - CurrentAssertionChain - .ForCondition(Subject is not null) - .BecauseOf(because, becauseArgs) - .FailWith("Expected JSON token to have value {0}, but the element was .", expected); + return NotBeEquivalentTo(parsedUnexpected, because, becauseArgs); + } - CurrentAssertionChain - .ForCondition(Subject.Value() == expected) - .BecauseOf(because, becauseArgs) - .FailWith("Expected JSON property {0} to have value {1}{reason}, but found {2}.", - Subject.Path, expected, Subject.Value()); + /// + /// Asserts that the current is not equivalent to the element, + /// using an equivalent of . + /// + /// The unexpected element + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndConstraint NotBeEquivalentTo(JToken unexpected, string because = "", params object[] becauseArgs) + { + CurrentAssertionChain + .ForCondition((Subject is null && unexpected is not null) || + !JToken.DeepEquals(Subject, unexpected)) + .BecauseOf(because, becauseArgs) + .FailWith("Expected JSON document not to be equivalent to {0}{reason}.", unexpected); - return new AndConstraint(this); - } + return new AndConstraint(this); + } - /// - /// Asserts that the current does not have the specified value. - /// - /// The unexpected element - /// - /// A formatted phrase as is supported by explaining why the assertion - /// is needed. If the phrase does not start with the word because, it is prepended automatically. - /// - /// - /// Zero or more objects to format using the placeholders in . - /// - public AndConstraint NotHaveValue(string unexpected, string because = "", params object[] becauseArgs) - { - CurrentAssertionChain - .ForCondition(Subject is not null) - .BecauseOf(because, becauseArgs) - .FailWith("Did not expect the JSON property to have value {0}, but the token was .", unexpected); + /// + /// Asserts that the current has the specified value. + /// + /// The expected value + public AndConstraint HaveValue(string expected) + { + return HaveValue(expected, string.Empty); + } - CurrentAssertionChain - .ForCondition(Subject.Value() != unexpected) - .BecauseOf(because, becauseArgs) - .FailWith("Did not expect JSON property {0} to have value {1}{reason}.", - Subject.Path, unexpected, Subject.Value()); + /// + /// Asserts that the current has the specified value. + /// + /// The expected value + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndConstraint HaveValue(string expected, string because, params object[] becauseArgs) + { + CurrentAssertionChain + .ForCondition(Subject is not null) + .BecauseOf(because, becauseArgs) + .FailWith("Expected JSON token to have value {0}, but the element was .", expected); + + CurrentAssertionChain + .ForCondition(Subject.Value() == expected) + .BecauseOf(because, becauseArgs) + .FailWith("Expected JSON property {0} to have value {1}{reason}, but found {2}.", + Subject.Path, expected, Subject.Value()); + + return new AndConstraint(this); + } - return new AndConstraint(this); - } + /// + /// Asserts that the current does not have the specified value. + /// + /// The unexpected element + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndConstraint NotHaveValue(string unexpected, string because = "", params object[] becauseArgs) + { + CurrentAssertionChain + .ForCondition(Subject is not null) + .BecauseOf(because, becauseArgs) + .FailWith("Did not expect the JSON property to have value {0}, but the token was .", unexpected); + + CurrentAssertionChain + .ForCondition(Subject.Value() != unexpected) + .BecauseOf(because, becauseArgs) + .FailWith("Did not expect JSON property {0} to have value {1}{reason}.", + Subject.Path, unexpected, Subject.Value()); + + return new AndConstraint(this); + } - /// - /// Asserts that the current matches the specified regular expression pattern. - /// - /// The expected regular expression pattern - public AndConstraint MatchRegex(string regularExpression) + /// + /// Asserts that the current matches the specified regular expression pattern. + /// + /// The expected regular expression pattern + public AndConstraint MatchRegex(string regularExpression) + { + return MatchRegex(regularExpression, string.Empty); + } + + /// + /// Asserts that the current matches the specified regular expression pattern. + /// + /// The expected regular expression pattern + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndConstraint MatchRegex(string regularExpression, string because, params object[] becauseArgs) + { + if (regularExpression == null) { - return MatchRegex(regularExpression, string.Empty); + throw new ArgumentNullException(nameof(regularExpression), "MatchRegex does not support pattern"); } - /// - /// Asserts that the current matches the specified regular expression pattern. - /// - /// The expected regular expression pattern - /// - /// A formatted phrase as is supported by explaining why the assertion - /// is needed. If the phrase does not start with the word because, it is prepended automatically. - /// - /// - /// Zero or more objects to format using the placeholders in . - /// - public AndConstraint MatchRegex(string regularExpression, string because, params object[] becauseArgs) - { - if (regularExpression == null) - { - throw new ArgumentNullException(nameof(regularExpression), "MatchRegex does not support pattern"); - } + CurrentAssertionChain + .ForCondition(Regex.IsMatch(Subject.Value(), regularExpression)) + .BecauseOf(because, becauseArgs) + .FailWith("Expected {context:JSON property} {0} to match regex pattern {1}{reason}, but found {2}.", + Subject.Path, regularExpression, Subject.Value()); - CurrentAssertionChain - .ForCondition(Regex.IsMatch(Subject.Value(), regularExpression)) - .BecauseOf(because, becauseArgs) - .FailWith("Expected {context:JSON property} {0} to match regex pattern {1}{reason}, but found {2}.", - Subject.Path, regularExpression, Subject.Value()); + return new AndConstraint(this); + } - return new AndConstraint(this); + /// + /// Asserts that the current does not match the specified regular expression pattern. + /// + /// The expected regular expression pattern + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndConstraint NotMatchRegex(string regularExpression, string because = "", params object[] becauseArgs) + { + if (regularExpression == null) + { + throw new ArgumentNullException(nameof(regularExpression), "MatchRegex does not support pattern"); } - /// - /// Asserts that the current does not match the specified regular expression pattern. - /// - /// The expected regular expression pattern - /// - /// A formatted phrase as is supported by explaining why the assertion - /// is needed. If the phrase does not start with the word because, it is prepended automatically. - /// - /// - /// Zero or more objects to format using the placeholders in . - /// - public AndConstraint NotMatchRegex(string regularExpression, string because = "", params object[] becauseArgs) - { - if (regularExpression == null) - { - throw new ArgumentNullException(nameof(regularExpression), "MatchRegex does not support pattern"); - } + CurrentAssertionChain + .ForCondition(!Regex.IsMatch(Subject.Value(), regularExpression)) + .BecauseOf(because, becauseArgs) + .FailWith("Did not expect {context:JSON property} {0} to match regex pattern {1}{reason}.", + Subject.Path, regularExpression, Subject.Value()); - CurrentAssertionChain - .ForCondition(!Regex.IsMatch(Subject.Value(), regularExpression)) - .BecauseOf(because, becauseArgs) - .FailWith("Did not expect {context:JSON property} {0} to match regex pattern {1}{reason}.", - Subject.Path, regularExpression, Subject.Value()); + return new AndConstraint(this); + } - return new AndConstraint(this); - } + /// + /// Asserts that the current has a direct child element with the specified + /// name. + /// + /// The name of the expected child element + public AndWhichConstraint HaveElement(string expected) + { + return HaveElement(expected, string.Empty); + } - /// - /// Asserts that the current has a direct child element with the specified - /// name. - /// - /// The name of the expected child element - public AndWhichConstraint HaveElement(string expected) - { - return HaveElement(expected, string.Empty); - } + /// + /// Asserts that the current has a direct child element with the specified + /// name. + /// + /// The name of the expected child element + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndWhichConstraint HaveElement(string expected, string because, + params object[] becauseArgs) + { + JToken jToken = Subject[expected]; - /// - /// Asserts that the current has a direct child element with the specified - /// name. - /// - /// The name of the expected child element - /// - /// A formatted phrase as is supported by explaining why the assertion - /// is needed. If the phrase does not start with the word because, it is prepended automatically. - /// - /// - /// Zero or more objects to format using the placeholders in . - /// - public AndWhichConstraint HaveElement(string expected, string because, - params object[] becauseArgs) - { - JToken jToken = Subject[expected]; + CurrentAssertionChain + .ForCondition(jToken != null) + .BecauseOf(because, becauseArgs) + .FailWith("Expected JSON document {0} to have element \"" + expected.EscapePlaceholders() + "\"{reason}" + + ", but no such element was found.", Subject); - CurrentAssertionChain - .ForCondition(jToken != null) - .BecauseOf(because, becauseArgs) - .FailWith("Expected JSON document {0} to have element \"" + expected.EscapePlaceholders() + "\"{reason}" + - ", but no such element was found.", Subject); + return new AndWhichConstraint(this, jToken); + } - return new AndWhichConstraint(this, jToken); - } + /// + /// Asserts that the current does not have a direct child element with the specified + /// name. + /// + /// The name of the not expected child element + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndWhichConstraint NotHaveElement(string unexpected, string because = "", + params object[] becauseArgs) + { + JToken jToken = Subject[unexpected]; - /// - /// Asserts that the current does not have a direct child element with the specified - /// name. - /// - /// The name of the not expected child element - /// - /// A formatted phrase as is supported by explaining why the assertion - /// is needed. If the phrase does not start with the word because, it is prepended automatically. - /// - /// - /// Zero or more objects to format using the placeholders in . - /// - public AndWhichConstraint NotHaveElement(string unexpected, string because = "", - params object[] becauseArgs) - { - JToken jToken = Subject[unexpected]; + CurrentAssertionChain + .ForCondition(jToken == null) + .BecauseOf(because, becauseArgs) + .FailWith("Did not expect JSON document {0} to have element \"" + unexpected.EscapePlaceholders() + "\"{reason}.", Subject); - CurrentAssertionChain - .ForCondition(jToken == null) - .BecauseOf(because, becauseArgs) - .FailWith("Did not expect JSON document {0} to have element \"" + unexpected.EscapePlaceholders() + "\"{reason}.", Subject); + return new AndWhichConstraint(this, jToken); + } - return new AndWhichConstraint(this, jToken); - } + /// + /// Expects the current to contain only a single item. + /// + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndWhichConstraint ContainSingleItem(string because = "", params object[] becauseArgs) + { + string formattedDocument = Format(Subject).Replace("{", "{{").Replace("}", "}}"); - /// - /// Expects the current to contain only a single item. - /// - /// - /// A formatted phrase as is supported by explaining why the assertion - /// is needed. If the phrase does not start with the word because, it is prepended automatically. - /// - /// - /// Zero or more objects to format using the placeholders in . - /// - public AndWhichConstraint ContainSingleItem(string because = "", params object[] becauseArgs) + using (new AssertionScope("JSON document " + formattedDocument)) { - string formattedDocument = Format(Subject).Replace("{", "{{").Replace("}", "}}"); - - using (new AssertionScope("JSON document " + formattedDocument)) - { - var constraint = EnumerableSubject.ContainSingle(because, becauseArgs); - return new AndWhichConstraint(this, constraint.Which); - } + var constraint = EnumerableSubject.ContainSingle(because, becauseArgs); + return new AndWhichConstraint(this, constraint.Which); } + } - /// - /// Asserts that the number of items in the current matches the supplied amount. - /// - /// The expected number of items in the current . - /// - /// A formatted phrase as is supported by explaining why the assertion - /// is needed. If the phrase does not start with the word because, it is prepended automatically. - /// - /// - /// Zero or more objects to format using the placeholders in . - /// - public AndConstraint HaveCount(int expected, string because = "", params object[] becauseArgs) - { - string formattedDocument = Format(Subject).Replace("{", "{{").Replace("}", "}}"); + /// + /// Asserts that the number of items in the current matches the supplied amount. + /// + /// The expected number of items in the current . + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + public AndConstraint HaveCount(int expected, string because = "", params object[] becauseArgs) + { + string formattedDocument = Format(Subject).Replace("{", "{{").Replace("}", "}}"); - using (new AssertionScope("JSON document " + formattedDocument)) - { - EnumerableSubject.HaveCount(expected, because, becauseArgs); - return new AndConstraint(this); - } + using (new AssertionScope("JSON document " + formattedDocument)) + { + EnumerableSubject.HaveCount(expected, because, becauseArgs); + return new AndConstraint(this); } + } - /// - /// Recursively asserts that the current contains at least the properties or elements of the specified . - /// - /// The subtree to search for - /// - /// A formatted phrase as is supported by explaining why the assertion - /// is needed. If the phrase does not start with the word because, it is prepended automatically. - /// - /// - /// Zero or more objects to format using the placeholders in . - /// - /// Use this method to match the current against an arbitrary subtree, - /// permitting it to contain any additional properties or elements. This way we can test multiple properties on a at once, - /// or test if a contains any items that match a set of properties, assert that a JSON document has a given shape, etc. - /// - /// This example asserts the values of multiple properties of a child object within a JSON document. - /// - /// var json = JToken.Parse("{ success: true, data: { id: 123, type: 'my-type', name: 'Noone' } }"); - /// json.Should().ContainSubtree(JToken.Parse("{ success: true, data: { type: 'my-type', name: 'Noone' } }")); - /// - /// - /// This example asserts that a within a has at least one element with at least the given properties - /// - /// var json = JToken.Parse("{ id: 1, items: [ { id: 2, type: 'my-type', name: 'Alpha' }, { id: 3, type: 'other-type', name: 'Bravo' } ] }"); - /// json.Should().ContainSubtree(JToken.Parse("{ items: [ { type: 'my-type', name: 'Alpha' } ] }")); - /// - public AndConstraint ContainSubtree(string subtree, string because = "", params object[] becauseArgs) + /// + /// Recursively asserts that the current contains at least the properties or elements of the specified . + /// + /// The subtree to search for + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + /// Use this method to match the current against an arbitrary subtree, + /// permitting it to contain any additional properties or elements. This way we can test multiple properties on a at once, + /// or test if a contains any items that match a set of properties, assert that a JSON document has a given shape, etc. + /// + /// This example asserts the values of multiple properties of a child object within a JSON document. + /// + /// var json = JToken.Parse("{ success: true, data: { id: 123, type: 'my-type', name: 'Noone' } }"); + /// json.Should().ContainSubtree(JToken.Parse("{ success: true, data: { type: 'my-type', name: 'Noone' } }")); + /// + /// + /// This example asserts that a within a has at least one element with at least the given properties + /// + /// var json = JToken.Parse("{ id: 1, items: [ { id: 2, type: 'my-type', name: 'Alpha' }, { id: 3, type: 'other-type', name: 'Bravo' } ] }"); + /// json.Should().ContainSubtree(JToken.Parse("{ items: [ { type: 'my-type', name: 'Alpha' } ] }")); + /// + public AndConstraint ContainSubtree(string subtree, string because = "", params object[] becauseArgs) + { + JToken subtreeToken; + try { - JToken subtreeToken; - try - { - subtreeToken = JToken.Parse(subtree); - } - catch (Exception ex) - { - throw new ArgumentException( - $"Unable to parse expected JSON string:{Environment.NewLine}" + - $"{subtree}{Environment.NewLine}" + - "Check inner exception for more details.", - nameof(subtree), ex); - } - - return ContainSubtree(subtreeToken, because, becauseArgs); + subtreeToken = JToken.Parse(subtree); } - - /// - /// Recursively asserts that the current contains at least the properties or elements of the specified . - /// - /// The subtree to search for - /// - /// A formatted phrase as is supported by explaining why the assertion - /// is needed. If the phrase does not start with the word because, it is prepended automatically. - /// - /// - /// Zero or more objects to format using the placeholders in . - /// - /// Use this method to match the current against an arbitrary subtree, - /// permitting it to contain any additional properties or elements. This way we can test multiple properties on a at once, - /// or test if a contains any items that match a set of properties, assert that a JSON document has a given shape, etc. - /// - /// This example asserts the values of multiple properties of a child object within a JSON document. - /// - /// var json = JToken.Parse("{ success: true, data: { id: 123, type: 'my-type', name: 'Noone' } }"); - /// json.Should().ContainSubtree(JToken.Parse("{ success: true, data: { type: 'my-type', name: 'Noone' } }")); - /// - /// - /// This example asserts that a within a has at least one element with at least the given properties - /// - /// var json = JToken.Parse("{ id: 1, items: [ { id: 2, type: 'my-type', name: 'Alpha' }, { id: 3, type: 'other-type', name: 'Bravo' } ] }"); - /// json.Should().ContainSubtree(JToken.Parse("{ items: [ { type: 'my-type', name: 'Alpha' } ] }")); - /// - public AndConstraint ContainSubtree(JToken subtree, string because = "", params object[] becauseArgs) + catch (Exception ex) { - return BeEquivalentTo(subtree, true, options => options, because, becauseArgs); + throw new ArgumentException( + $"Unable to parse expected JSON string:{Environment.NewLine}" + + $"{subtree}{Environment.NewLine}" + + "Check inner exception for more details.", + nameof(subtree), ex); } + return ContainSubtree(subtreeToken, because, becauseArgs); + } + + /// + /// Recursively asserts that the current contains at least the properties or elements of the specified . + /// + /// The subtree to search for + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + /// Use this method to match the current against an arbitrary subtree, + /// permitting it to contain any additional properties or elements. This way we can test multiple properties on a at once, + /// or test if a contains any items that match a set of properties, assert that a JSON document has a given shape, etc. + /// + /// This example asserts the values of multiple properties of a child object within a JSON document. + /// + /// var json = JToken.Parse("{ success: true, data: { id: 123, type: 'my-type', name: 'Noone' } }"); + /// json.Should().ContainSubtree(JToken.Parse("{ success: true, data: { type: 'my-type', name: 'Noone' } }")); + /// + /// + /// This example asserts that a within a has at least one element with at least the given properties + /// + /// var json = JToken.Parse("{ id: 1, items: [ { id: 2, type: 'my-type', name: 'Alpha' }, { id: 3, type: 'other-type', name: 'Bravo' } ] }"); + /// json.Should().ContainSubtree(JToken.Parse("{ items: [ { type: 'my-type', name: 'Alpha' } ] }")); + /// + public AndConstraint ContainSubtree(JToken subtree, string because = "", params object[] becauseArgs) + { + return BeEquivalentTo(subtree, true, options => options, because, becauseArgs); + } + #pragma warning disable CA1822 // Making this method static is a breaking chan - public string Format(JToken value, bool useLineBreaks = false) - { - // SMELL: Why is this method necessary at all? - // SMELL: Why aren't we using the Formatter class directly? - var output = new FormattedObjectGraph(maxLines: 100); + public string Format(JToken value, bool useLineBreaks = false) + { + // SMELL: Why is this method necessary at all? + // SMELL: Why aren't we using the Formatter class directly? + var output = new FormattedObjectGraph(maxLines: 100); - new JTokenFormatter().Format(value, output, new FormattingContext - { - UseLineBreaks = useLineBreaks - }, null); + new JTokenFormatter().Format(value, output, new FormattingContext + { + UseLineBreaks = useLineBreaks + }, null); - return output.ToString(); - } -#pragma warning restore CA1822 // Making this method static is a breaking chan + return output.ToString(); } +#pragma warning restore CA1822 // Making this method static is a breaking chan } diff --git a/Src/FluentAssertions.Json/JTokenDifferentiator.cs b/Src/FluentAssertions.Json/JTokenDifferentiator.cs index bd5acc63..b3344b71 100644 --- a/Src/FluentAssertions.Json/JTokenDifferentiator.cs +++ b/Src/FluentAssertions.Json/JTokenDifferentiator.cs @@ -5,265 +5,264 @@ using FluentAssertions.Json.Common; using Newtonsoft.Json.Linq; -namespace FluentAssertions.Json +namespace FluentAssertions.Json; + +internal sealed class JTokenDifferentiator { - internal sealed class JTokenDifferentiator + private readonly bool ignoreExtraProperties; + + private readonly Func, IJsonAssertionOptions> config; + private readonly JsonAssertionOptions options; + + public JTokenDifferentiator(bool ignoreExtraProperties, + Func, IJsonAssertionOptions> config) { - private readonly bool ignoreExtraProperties; + this.ignoreExtraProperties = ignoreExtraProperties; + this.config = config; + this.options = (JsonAssertionOptions)config(new JsonAssertionOptions()); + } - private readonly Func, IJsonAssertionOptions> config; - private readonly JsonAssertionOptions options; + public Difference FindFirstDifference(JToken actual, JToken expected) + { + var path = new JPath(); - public JTokenDifferentiator(bool ignoreExtraProperties, - Func, IJsonAssertionOptions> config) + if (actual == expected) { - this.ignoreExtraProperties = ignoreExtraProperties; - this.config = config; - this.options = (JsonAssertionOptions)config(new JsonAssertionOptions()); + return null; } - public Difference FindFirstDifference(JToken actual, JToken expected) + if (actual == null) { - var path = new JPath(); + return new Difference(DifferenceKind.ActualIsNull, path); + } - if (actual == expected) - { - return null; - } + if (expected == null) + { + return new Difference(DifferenceKind.ExpectedIsNull, path); + } - if (actual == null) - { - return new Difference(DifferenceKind.ActualIsNull, path); - } + if (!options.IsStrictlyOrdered) + { + actual = actual.Normalize(); + expected = expected.Normalize(); + } - if (expected == null) - { - return new Difference(DifferenceKind.ExpectedIsNull, path); - } + return FindFirstDifference(actual, expected, path); + } - if (!options.IsStrictlyOrdered) - { - actual = actual.Normalize(); - expected = expected.Normalize(); - } + private Difference FindFirstDifference(JToken actual, JToken expected, JPath path) + { + return actual switch + { + JArray actualArray => FindJArrayDifference(actualArray, expected, path), + JObject actualObject => FindJObjectDifference(actualObject, expected, path), + JProperty actualProperty => FindJPropertyDifference(actualProperty, expected, path), + JValue actualValue => FindValueDifference(actualValue, expected, path), + _ => throw new NotSupportedException(), + }; + } - return FindFirstDifference(actual, expected, path); + private Difference FindJArrayDifference(JArray actualArray, JToken expected, JPath path) + { + if (expected is not JArray expectedArray) + { + return new Difference(DifferenceKind.OtherType, path, Describe(actualArray.Type), Describe(expected.Type)); } - private Difference FindFirstDifference(JToken actual, JToken expected, JPath path) + if (ignoreExtraProperties) { - return actual switch - { - JArray actualArray => FindJArrayDifference(actualArray, expected, path), - JObject actualObject => FindJObjectDifference(actualObject, expected, path), - JProperty actualProperty => FindJPropertyDifference(actualProperty, expected, path), - JValue actualValue => FindValueDifference(actualValue, expected, path), - _ => throw new NotSupportedException(), - }; + return CompareExpectedItems(actualArray, expectedArray, path); } - - private Difference FindJArrayDifference(JArray actualArray, JToken expected, JPath path) + else { - if (expected is not JArray expectedArray) - { - return new Difference(DifferenceKind.OtherType, path, Describe(actualArray.Type), Describe(expected.Type)); - } - - if (ignoreExtraProperties) - { - return CompareExpectedItems(actualArray, expectedArray, path); - } - else - { - return CompareItems(actualArray, expectedArray, path); - } + return CompareItems(actualArray, expectedArray, path); } + } - private Difference CompareExpectedItems(JArray actual, JArray expected, JPath path) - { - JToken[] actualChildren = actual.Children().ToArray(); - JToken[] expectedChildren = expected.Children().ToArray(); + private Difference CompareExpectedItems(JArray actual, JArray expected, JPath path) + { + JToken[] actualChildren = actual.Children().ToArray(); + JToken[] expectedChildren = expected.Children().ToArray(); - int matchingIndex = 0; - for (int expectedIndex = 0; expectedIndex < expectedChildren.Length; expectedIndex++) + int matchingIndex = 0; + for (int expectedIndex = 0; expectedIndex < expectedChildren.Length; expectedIndex++) + { + var expectedChild = expectedChildren[expectedIndex]; + bool match = false; + for (int actualIndex = matchingIndex; actualIndex < actualChildren.Length; actualIndex++) { - var expectedChild = expectedChildren[expectedIndex]; - bool match = false; - for (int actualIndex = matchingIndex; actualIndex < actualChildren.Length; actualIndex++) - { - var difference = FindFirstDifference(actualChildren[actualIndex], expectedChild); + var difference = FindFirstDifference(actualChildren[actualIndex], expectedChild); - if (difference == null) - { - match = true; - matchingIndex = actualIndex + 1; - break; - } + if (difference == null) + { + match = true; + matchingIndex = actualIndex + 1; + break; } + } - if (!match) + if (!match) + { + if (matchingIndex >= actualChildren.Length) { - if (matchingIndex >= actualChildren.Length) + if (actualChildren.Any(actualChild => FindFirstDifference(actualChild, expectedChild) == null)) { - if (actualChildren.Any(actualChild => FindFirstDifference(actualChild, expectedChild) == null)) - { - return new Difference(DifferenceKind.WrongOrder, path.AddIndex(expectedIndex)); - } - - return new Difference(DifferenceKind.ActualMissesElement, path.AddIndex(expectedIndex)); + return new Difference(DifferenceKind.WrongOrder, path.AddIndex(expectedIndex)); } - return FindFirstDifference(actualChildren[matchingIndex], expectedChild, - path.AddIndex(expectedIndex)); + return new Difference(DifferenceKind.ActualMissesElement, path.AddIndex(expectedIndex)); } - } - - return null; - } - private Difference CompareItems(JArray actual, JArray expected, JPath path) - { - JToken[] actualChildren = actual.Children().ToArray(); - JToken[] expectedChildren = expected.Children().ToArray(); - - if (actualChildren.Length != expectedChildren.Length) - { - return new Difference(DifferenceKind.DifferentLength, path, actualChildren.Length, expectedChildren.Length); + return FindFirstDifference(actualChildren[matchingIndex], expectedChild, + path.AddIndex(expectedIndex)); } + } - for (int i = 0; i < actualChildren.Length; i++) - { - Difference firstDifference = FindFirstDifference(actualChildren[i], expectedChildren[i], path.AddIndex(i)); + return null; + } - if (firstDifference != null) - { - return firstDifference; - } - } + private Difference CompareItems(JArray actual, JArray expected, JPath path) + { + JToken[] actualChildren = actual.Children().ToArray(); + JToken[] expectedChildren = expected.Children().ToArray(); - return null; + if (actualChildren.Length != expectedChildren.Length) + { + return new Difference(DifferenceKind.DifferentLength, path, actualChildren.Length, expectedChildren.Length); } - private Difference FindJObjectDifference(JObject actual, JToken expected, JPath path) + for (int i = 0; i < actualChildren.Length; i++) { - if (expected is not JObject expectedObject) + Difference firstDifference = FindFirstDifference(actualChildren[i], expectedChildren[i], path.AddIndex(i)); + + if (firstDifference != null) { - return new Difference(DifferenceKind.OtherType, path, Describe(actual.Type), Describe(expected.Type)); + return firstDifference; } - - return CompareProperties(actual?.Properties(), expectedObject.Properties(), path); } - private Difference CompareProperties(IEnumerable actual, IEnumerable expected, JPath path) + return null; + } + + private Difference FindJObjectDifference(JObject actual, JToken expected, JPath path) + { + if (expected is not JObject expectedObject) { - var actualDictionary = actual?.ToDictionary(p => p.Name, p => p.Value, StringComparer.Ordinal) ?? new Dictionary(StringComparer.Ordinal); - var expectedDictionary = expected?.ToDictionary(p => p.Name, p => p.Value, StringComparer.Ordinal) ?? new Dictionary(StringComparer.Ordinal); + return new Difference(DifferenceKind.OtherType, path, Describe(actual.Type), Describe(expected.Type)); + } - foreach (KeyValuePair expectedPair in expectedDictionary) - { - if (!actualDictionary.ContainsKey(expectedPair.Key)) - { - return new Difference(DifferenceKind.ActualMissesProperty, path.AddProperty(expectedPair.Key)); - } - } + return CompareProperties(actual?.Properties(), expectedObject.Properties(), path); + } - foreach (KeyValuePair actualPair in actualDictionary) - { - if (!ignoreExtraProperties && !expectedDictionary.ContainsKey(actualPair.Key)) - { - return new Difference(DifferenceKind.ExpectedMissesProperty, path.AddProperty(actualPair.Key)); - } - } + private Difference CompareProperties(IEnumerable actual, IEnumerable expected, JPath path) + { + var actualDictionary = actual?.ToDictionary(p => p.Name, p => p.Value, StringComparer.Ordinal) ?? new Dictionary(StringComparer.Ordinal); + var expectedDictionary = expected?.ToDictionary(p => p.Name, p => p.Value, StringComparer.Ordinal) ?? new Dictionary(StringComparer.Ordinal); - foreach (KeyValuePair expectedPair in expectedDictionary) + foreach (KeyValuePair expectedPair in expectedDictionary) + { + if (!actualDictionary.ContainsKey(expectedPair.Key)) { - JToken actualValue = actualDictionary[expectedPair.Key]; - - Difference firstDifference = FindFirstDifference(actualValue, expectedPair.Value, - path.AddProperty(expectedPair.Key)); - - if (firstDifference != null) - { - return firstDifference; - } + return new Difference(DifferenceKind.ActualMissesProperty, path.AddProperty(expectedPair.Key)); } - - return null; } - private Difference FindJPropertyDifference(JProperty actualProperty, JToken expected, JPath path) + foreach (KeyValuePair actualPair in actualDictionary) { - if (expected is not JProperty expectedProperty) + if (!ignoreExtraProperties && !expectedDictionary.ContainsKey(actualPair.Key)) { - return new Difference(DifferenceKind.OtherType, path, Describe(actualProperty.Type), Describe(expected.Type)); + return new Difference(DifferenceKind.ExpectedMissesProperty, path.AddProperty(actualPair.Key)); } + } - if (actualProperty.Name != expectedProperty.Name) + foreach (KeyValuePair expectedPair in expectedDictionary) + { + JToken actualValue = actualDictionary[expectedPair.Key]; + + Difference firstDifference = FindFirstDifference(actualValue, expectedPair.Value, + path.AddProperty(expectedPair.Key)); + + if (firstDifference != null) { - return new Difference(DifferenceKind.OtherName, path); + return firstDifference; } - - return FindFirstDifference(actualProperty.Value, expectedProperty.Value, path); } - private Difference FindValueDifference(JValue actualValue, JToken expected, JPath path) + return null; + } + + private Difference FindJPropertyDifference(JProperty actualProperty, JToken expected, JPath path) + { + if (expected is not JProperty expectedProperty) { - if (expected is not JValue expectedValue) - { - return new Difference(DifferenceKind.OtherType, path, Describe(actualValue.Type), Describe(expected.Type)); - } + return new Difference(DifferenceKind.OtherType, path, Describe(actualProperty.Type), Describe(expected.Type)); + } - return CompareValues(actualValue, expectedValue, path); + if (actualProperty.Name != expectedProperty.Name) + { + return new Difference(DifferenceKind.OtherName, path); } - private Difference CompareValues(JValue actual, JValue expected, JPath path) + return FindFirstDifference(actualProperty.Value, expectedProperty.Value, path); + } + + private Difference FindValueDifference(JValue actualValue, JToken expected, JPath path) + { + if (expected is not JValue expectedValue) { - if (actual.Type != expected.Type) - { - return new Difference(DifferenceKind.OtherType, path, Describe(actual.Type), Describe(expected.Type)); - } + return new Difference(DifferenceKind.OtherType, path, Describe(actualValue.Type), Describe(expected.Type)); + } - bool hasMismatches; - using (var scope = new AssertionScope()) - { - actual.Value.Should().BeEquivalentTo(expected.Value, options => - (JsonAssertionOptions)config.Invoke(new JsonAssertionOptions(options))); + return CompareValues(actualValue, expectedValue, path); + } - hasMismatches = scope.Discard().Length > 0; - } + private Difference CompareValues(JValue actual, JValue expected, JPath path) + { + if (actual.Type != expected.Type) + { + return new Difference(DifferenceKind.OtherType, path, Describe(actual.Type), Describe(expected.Type)); + } - if (hasMismatches) - { - return new Difference(DifferenceKind.OtherValue, path); - } + bool hasMismatches; + using (var scope = new AssertionScope()) + { + actual.Value.Should().BeEquivalentTo(expected.Value, options => + (JsonAssertionOptions)config.Invoke(new JsonAssertionOptions(options))); - return null; + hasMismatches = scope.Discard().Length > 0; } - private static string Describe(JTokenType jTokenType) + if (hasMismatches) { - return jTokenType switch - { - JTokenType.None => "type none", - JTokenType.Object => "an object", - JTokenType.Array => "an array", - JTokenType.Constructor => "a constructor", - JTokenType.Property => "a property", - JTokenType.Comment => "a comment", - JTokenType.Integer => "an integer", - JTokenType.Float => "a float", - JTokenType.String => "a string", - JTokenType.Boolean => "a boolean", - JTokenType.Null => "type null", - JTokenType.Undefined => "type undefined", - JTokenType.Date => "a date", - JTokenType.Raw => "type raw", - JTokenType.Bytes => "type bytes", - JTokenType.Guid => "a GUID", - JTokenType.Uri => "a URI", - JTokenType.TimeSpan => "a timespan", - _ => throw new ArgumentOutOfRangeException(nameof(jTokenType), jTokenType, null), - }; + return new Difference(DifferenceKind.OtherValue, path); } + + return null; + } + + private static string Describe(JTokenType jTokenType) + { + return jTokenType switch + { + JTokenType.None => "type none", + JTokenType.Object => "an object", + JTokenType.Array => "an array", + JTokenType.Constructor => "a constructor", + JTokenType.Property => "a property", + JTokenType.Comment => "a comment", + JTokenType.Integer => "an integer", + JTokenType.Float => "a float", + JTokenType.String => "a string", + JTokenType.Boolean => "a boolean", + JTokenType.Null => "type null", + JTokenType.Undefined => "type undefined", + JTokenType.Date => "a date", + JTokenType.Raw => "type raw", + JTokenType.Bytes => "type bytes", + JTokenType.Guid => "a GUID", + JTokenType.Uri => "a URI", + JTokenType.TimeSpan => "a timespan", + _ => throw new ArgumentOutOfRangeException(nameof(jTokenType), jTokenType, null), + }; } } diff --git a/Src/FluentAssertions.Json/JTokenFormatter.cs b/Src/FluentAssertions.Json/JTokenFormatter.cs index 2a1848e1..10309245 100644 --- a/Src/FluentAssertions.Json/JTokenFormatter.cs +++ b/Src/FluentAssertions.Json/JTokenFormatter.cs @@ -2,45 +2,44 @@ using FluentAssertions.Json.Common; using Newtonsoft.Json.Linq; -namespace FluentAssertions.Json +namespace FluentAssertions.Json; + +/// +/// A for . +/// +public class JTokenFormatter : IValueFormatter { /// - /// A for . + /// Indicates whether the current can handle the specified . /// - public class JTokenFormatter : IValueFormatter + /// The value for which to create a . + /// + /// true if the current can handle the specified value; otherwise, false. + /// + public bool CanHandle(object value) { - /// - /// Indicates whether the current can handle the specified . - /// - /// The value for which to create a . - /// - /// true if the current can handle the specified value; otherwise, false. - /// - public bool CanHandle(object value) - { - return value is JToken; - } + return value is JToken; + } - public void Format(object value, FormattedObjectGraph formattedGraph, FormattingContext context, FormatChild formatChild) - { - var jToken = value as JToken; + public void Format(object value, FormattedObjectGraph formattedGraph, FormattingContext context, FormatChild formatChild) + { + var jToken = value as JToken; - if (context.UseLineBreaks) + if (context.UseLineBreaks) + { + var result = jToken?.ToString(Newtonsoft.Json.Formatting.Indented); + if (result is not null) { - var result = jToken?.ToString(Newtonsoft.Json.Formatting.Indented); - if (result is not null) - { - formattedGraph.AddFragmentOnNewLine(result); - } - else - { - formattedGraph.AddFragment(""); - } + formattedGraph.AddFragmentOnNewLine(result); } else { - formattedGraph.AddFragment(jToken?.ToString().RemoveNewLines() ?? ""); + formattedGraph.AddFragment(""); } } + else + { + formattedGraph.AddFragment(jToken?.ToString().RemoveNewLines() ?? ""); + } } } diff --git a/Src/FluentAssertions.Json/JsonAssertionExtensions.cs b/Src/FluentAssertions.Json/JsonAssertionExtensions.cs index 0e5f0616..b002c728 100644 --- a/Src/FluentAssertions.Json/JsonAssertionExtensions.cs +++ b/Src/FluentAssertions.Json/JsonAssertionExtensions.cs @@ -3,39 +3,38 @@ using JetBrains.Annotations; using Newtonsoft.Json.Linq; -namespace FluentAssertions.Json +namespace FluentAssertions.Json; + +/// +/// Contains extension methods for JToken assertions. +/// +[DebuggerNonUserCode] +public static class JsonAssertionExtensions { /// - /// Contains extension methods for JToken assertions. + /// Returns an object that can be used to assert the current . /// - [DebuggerNonUserCode] - public static class JsonAssertionExtensions + [Pure] + public static JTokenAssertions Should(this JToken jToken) { - /// - /// Returns an object that can be used to assert the current . - /// - [Pure] - public static JTokenAssertions Should(this JToken jToken) - { - return new JTokenAssertions(jToken, AssertionChain.GetOrCreate()); - } + return new JTokenAssertions(jToken, AssertionChain.GetOrCreate()); + } - /// - /// Returns an object that can be used to assert the current . - /// - [Pure] - public static JTokenAssertions Should(this JObject jObject) - { - return new JTokenAssertions(jObject, AssertionChain.GetOrCreate()); - } + /// + /// Returns an object that can be used to assert the current . + /// + [Pure] + public static JTokenAssertions Should(this JObject jObject) + { + return new JTokenAssertions(jObject, AssertionChain.GetOrCreate()); + } - /// - /// Returns an object that can be used to assert the current . - /// - [Pure] - public static JTokenAssertions Should(this JValue jValue) - { - return new JTokenAssertions(jValue, AssertionChain.GetOrCreate()); - } + /// + /// Returns an object that can be used to assert the current . + /// + [Pure] + public static JTokenAssertions Should(this JValue jValue) + { + return new JTokenAssertions(jValue, AssertionChain.GetOrCreate()); } } diff --git a/Src/FluentAssertions.Json/JsonAssertionOptions.cs b/Src/FluentAssertions.Json/JsonAssertionOptions.cs index f6f42d3f..87191674 100644 --- a/Src/FluentAssertions.Json/JsonAssertionOptions.cs +++ b/Src/FluentAssertions.Json/JsonAssertionOptions.cs @@ -1,33 +1,32 @@ using System; using FluentAssertions.Equivalency; -namespace FluentAssertions.Json +namespace FluentAssertions.Json; + +/// +/// Represents the run-time type-specific behavior of a JSON structural equivalency assertion. It is the equivalent of +/// +public sealed class JsonAssertionOptions : EquivalencyOptions, IJsonAssertionOptions { - /// - /// Represents the run-time type-specific behavior of a JSON structural equivalency assertion. It is the equivalent of - /// - public sealed class JsonAssertionOptions : EquivalencyOptions, IJsonAssertionOptions + internal JsonAssertionOptions() { - internal JsonAssertionOptions() - { - } + } - public JsonAssertionOptions(EquivalencyOptions equivalencyAssertionOptions) - : base(equivalencyAssertionOptions) - { - } + public JsonAssertionOptions(EquivalencyOptions equivalencyAssertionOptions) + : base(equivalencyAssertionOptions) + { + } - internal bool IsStrictlyOrdered { get; private set; } = true; + internal bool IsStrictlyOrdered { get; private set; } = true; - public new IJsonAssertionRestriction Using(Action> action) - { - return new JsonAssertionRestriction(base.Using(action)); - } + public new IJsonAssertionRestriction Using(Action> action) + { + return new JsonAssertionRestriction(base.Using(action)); + } - public new IJsonAssertionOptions WithoutStrictOrdering() - { - IsStrictlyOrdered = false; - return this; - } + public new IJsonAssertionOptions WithoutStrictOrdering() + { + IsStrictlyOrdered = false; + return this; } } diff --git a/Src/FluentAssertions.Json/JsonAssertionRestriction.cs b/Src/FluentAssertions.Json/JsonAssertionRestriction.cs index 885f3f34..bdac52e7 100644 --- a/Src/FluentAssertions.Json/JsonAssertionRestriction.cs +++ b/Src/FluentAssertions.Json/JsonAssertionRestriction.cs @@ -1,18 +1,17 @@ -namespace FluentAssertions.Json +namespace FluentAssertions.Json; + +public sealed class JsonAssertionRestriction : IJsonAssertionRestriction { - public sealed class JsonAssertionRestriction : IJsonAssertionRestriction - { - private readonly JsonAssertionOptions.Restriction restriction; + private readonly JsonAssertionOptions.Restriction restriction; - internal JsonAssertionRestriction(JsonAssertionOptions.Restriction restriction) - { - this.restriction = restriction; - } + internal JsonAssertionRestriction(JsonAssertionOptions.Restriction restriction) + { + this.restriction = restriction; + } - public IJsonAssertionOptions WhenTypeIs() - where TMemberType : TProperty - { - return (JsonAssertionOptions)restriction.WhenTypeIs(); - } + public IJsonAssertionOptions WhenTypeIs() + where TMemberType : TProperty + { + return (JsonAssertionOptions)restriction.WhenTypeIs(); } } diff --git a/Src/FluentAssertions.Json/ObjectAssertionsExtensions.cs b/Src/FluentAssertions.Json/ObjectAssertionsExtensions.cs index ef629eaa..dc1d764a 100644 --- a/Src/FluentAssertions.Json/ObjectAssertionsExtensions.cs +++ b/Src/FluentAssertions.Json/ObjectAssertionsExtensions.cs @@ -3,102 +3,101 @@ using FluentAssertions.Primitives; using Newtonsoft.Json; -namespace FluentAssertions.Json +namespace FluentAssertions.Json; + +/// +/// Contains extension methods for JSON serialization assertion methods +/// +public static class ObjectAssertionsExtensions { /// - /// Contains extension methods for JSON serialization assertion methods + /// Asserts that an object can be serialized and deserialized using the JSON serializer and that it still retains + /// the values of all members. /// - public static class ObjectAssertionsExtensions + /// + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + [CustomAssertion] + public static AndConstraint BeJsonSerializable(this ObjectAssertions assertions, string because = "", params object[] becauseArgs) { - /// - /// Asserts that an object can be serialized and deserialized using the JSON serializer and that it still retains - /// the values of all members. - /// - /// - /// - /// A formatted phrase as is supported by explaining why the assertion - /// is needed. If the phrase does not start with the word because, it is prepended automatically. - /// - /// - /// Zero or more objects to format using the placeholders in . - /// - [CustomAssertion] - public static AndConstraint BeJsonSerializable(this ObjectAssertions assertions, string because = "", params object[] becauseArgs) - { - return BeJsonSerializable(assertions, options => options, because, becauseArgs); - } + return BeJsonSerializable(assertions, options => options, because, becauseArgs); + } - /// - /// Asserts that an object can be serialized and deserialized using the JSON serializer and that it stills retains - /// the values of all members. - /// - /// - /// - /// A formatted phrase as is supported by explaining why the assertion - /// is needed. If the phrase does not start with the word because, it is prepended automatically. - /// - /// - /// Zero or more objects to format using the placeholders in . - /// - [CustomAssertion] - public static AndConstraint BeJsonSerializable(this ObjectAssertions assertions, string because = "", params object[] becauseArgs) - { - return BeJsonSerializable(assertions, options => options, because, becauseArgs); - } + /// + /// Asserts that an object can be serialized and deserialized using the JSON serializer and that it stills retains + /// the values of all members. + /// + /// + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + [CustomAssertion] + public static AndConstraint BeJsonSerializable(this ObjectAssertions assertions, string because = "", params object[] becauseArgs) + { + return BeJsonSerializable(assertions, options => options, because, becauseArgs); + } - /// - /// Asserts that an object can be serialized and deserialized using the JSON serializer and that it stills retains - /// the values of all members. - /// - /// - /// - /// - /// A formatted phrase as is supported by explaining why the assertion - /// is needed. If the phrase does not start with the word because, it is prepended automatically. - /// - /// - /// Zero or more objects to format using the placeholders in . - /// - [CustomAssertion] - public static AndConstraint BeJsonSerializable(this ObjectAssertions assertions, Func, EquivalencyOptions> options, string because = "", params object[] becauseArgs) - { - assertions.CurrentAssertionChain.ForCondition(assertions.Subject != null) - .BecauseOf(because, becauseArgs) - .FailWith("Expected {context:object} to be JSON serializable{reason}, but the value is null. Please provide a value for the assertion."); + /// + /// Asserts that an object can be serialized and deserialized using the JSON serializer and that it stills retains + /// the values of all members. + /// + /// + /// + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + [CustomAssertion] + public static AndConstraint BeJsonSerializable(this ObjectAssertions assertions, Func, EquivalencyOptions> options, string because = "", params object[] becauseArgs) + { + assertions.CurrentAssertionChain.ForCondition(assertions.Subject != null) + .BecauseOf(because, becauseArgs) + .FailWith("Expected {context:object} to be JSON serializable{reason}, but the value is null. Please provide a value for the assertion."); - assertions.CurrentAssertionChain.ForCondition(assertions.Subject is T) - .BecauseOf(because, becauseArgs) - .FailWith("Expected {context:object} to be JSON serializable{reason}, but {context:object} is not assignable to {0}", typeof(T)); + assertions.CurrentAssertionChain.ForCondition(assertions.Subject is T) + .BecauseOf(because, becauseArgs) + .FailWith("Expected {context:object} to be JSON serializable{reason}, but {context:object} is not assignable to {0}", typeof(T)); - try - { - var deserializedObject = CreateCloneUsingJsonSerializer(assertions.Subject); + try + { + var deserializedObject = CreateCloneUsingJsonSerializer(assertions.Subject); - var defaultOptions = AssertionConfiguration.Current.Equivalency.CloneDefaults() - .PreferringRuntimeMemberTypes() - .IncludingFields() - .IncludingProperties(); + var defaultOptions = AssertionConfiguration.Current.Equivalency.CloneDefaults() + .PreferringRuntimeMemberTypes() + .IncludingFields() + .IncludingProperties(); - var typedSubject = (T)assertions.Subject; - ((T)deserializedObject).Should().BeEquivalentTo(typedSubject, _ => options(defaultOptions)); - } + var typedSubject = (T)assertions.Subject; + ((T)deserializedObject).Should().BeEquivalentTo(typedSubject, _ => options(defaultOptions)); + } #pragma warning disable CA1031 // Ignore catching general exception - catch (Exception exc) + catch (Exception exc) #pragma warning restore CA1031 // Ignore catching general exception - { - assertions.CurrentAssertionChain - .BecauseOf(because, becauseArgs) - .FailWith("Expected {context:object} to be JSON serializable{reason}, but serializing {0} failed with {1}", assertions.Subject, exc); - } - - return new AndConstraint(assertions); - } - - private static object CreateCloneUsingJsonSerializer(object subject) { - var serializedObject = JsonConvert.SerializeObject(subject); - var cloneUsingJsonSerializer = JsonConvert.DeserializeObject(serializedObject, subject.GetType()); - return cloneUsingJsonSerializer; + assertions.CurrentAssertionChain + .BecauseOf(because, becauseArgs) + .FailWith("Expected {context:object} to be JSON serializable{reason}, but serializing {0} failed with {1}", assertions.Subject, exc); } + + return new AndConstraint(assertions); + } + + private static object CreateCloneUsingJsonSerializer(object subject) + { + var serializedObject = JsonConvert.SerializeObject(subject); + var cloneUsingJsonSerializer = JsonConvert.DeserializeObject(serializedObject, subject.GetType()); + return cloneUsingJsonSerializer; } } diff --git a/Src/FluentAssertions.Json/StringAssertionsExtensions.cs b/Src/FluentAssertions.Json/StringAssertionsExtensions.cs index d4837218..689dccc5 100644 --- a/Src/FluentAssertions.Json/StringAssertionsExtensions.cs +++ b/Src/FluentAssertions.Json/StringAssertionsExtensions.cs @@ -2,31 +2,30 @@ using FluentAssertions.Primitives; using Newtonsoft.Json.Linq; -namespace FluentAssertions.Json +namespace FluentAssertions.Json; + +public static class StringAssertionsExtensions { - public static class StringAssertionsExtensions + [CustomAssertionAttribute] + public static AndWhichConstraint BeValidJson( + this StringAssertions stringAssertions, + string because = "", + params object[] becauseArgs) { - [CustomAssertionAttribute] - public static AndWhichConstraint BeValidJson( - this StringAssertions stringAssertions, - string because = "", - params object[] becauseArgs) - { - JToken json = null; + JToken json = null; - try - { - json = JToken.Parse(stringAssertions.Subject); - } + try + { + json = JToken.Parse(stringAssertions.Subject); + } #pragma warning disable CA1031 // Ignore catching general exception - catch (Exception ex) + catch (Exception ex) #pragma warning restore CA1031 // Ignore catching general exception - { - stringAssertions.CurrentAssertionChain.BecauseOf(because, becauseArgs) - .FailWith("Expected {context:string} to be valid JSON{reason}, but parsing failed with {0}.", ex.Message); - } - - return new AndWhichConstraint(stringAssertions, json); + { + stringAssertions.CurrentAssertionChain.BecauseOf(because, becauseArgs) + .FailWith("Expected {context:string} to be valid JSON{reason}, but parsing failed with {0}.", ex.Message); } + + return new AndWhichConstraint(stringAssertions, json); } } diff --git a/Tests/FluentAssertions.Json.Specs/JTokenAssertionsSpecs.cs b/Tests/FluentAssertions.Json.Specs/JTokenAssertionsSpecs.cs index 14e970e1..21e0682d 100644 --- a/Tests/FluentAssertions.Json.Specs/JTokenAssertionsSpecs.cs +++ b/Tests/FluentAssertions.Json.Specs/JTokenAssertionsSpecs.cs @@ -5,960 +5,959 @@ using Xunit; using Xunit.Sdk; -namespace FluentAssertions.Json.Specs +namespace FluentAssertions.Json.Specs; + +// ReSharper disable InconsistentNaming +// ReSharper disable ExpressionIsAlwaysNull +public class JTokenAssertionsSpecs { - // ReSharper disable InconsistentNaming - // ReSharper disable ExpressionIsAlwaysNull - public class JTokenAssertionsSpecs + #region (Not)BeEquivalentTo + + [Fact] + public void When_both_tokens_are_null_they_should_be_treated_as_equivalent() { - #region (Not)BeEquivalentTo + // Arrange + JToken actual = null; + JToken expected = null; - [Fact] - public void When_both_tokens_are_null_they_should_be_treated_as_equivalent() - { - // Arrange - JToken actual = null; - JToken expected = null; + // Act & Assert + actual.Should().BeEquivalentTo(expected); + } - // Act & Assert - actual.Should().BeEquivalentTo(expected); - } + [Fact] + public void When_both_tokens_represent_the_same_json_content_they_should_be_treated_as_equivalent() + { + // Arrange + string json = + """ + { + friends: + [{ + id: 123, + name: "John Doe" + }, { + id: 456, + name: "Jane Doe", + kids: + [ + "Jimmy", + "James" + ] + } + ] + } + """; - [Fact] - public void When_both_tokens_represent_the_same_json_content_they_should_be_treated_as_equivalent() - { - // Arrange - string json = - """ - { - friends: - [{ - id: 123, - name: "John Doe" - }, { - id: 456, - name: "Jane Doe", - kids: - [ - "Jimmy", - "James" - ] - } - ] - } - """; - - var a = JToken.Parse(json); - var b = JToken.Parse(json); - - // Act & Assert - a.Should().BeEquivalentTo(a); - b.Should().BeEquivalentTo(b); - a.Should().BeEquivalentTo(b); - } + var a = JToken.Parse(json); + var b = JToken.Parse(json); - public static TheoryData FailingBeEquivalentCases => new() - { - { - null, - "{ id: 2 }", - "is null" - }, - { - "{ id: 1 }", - null, - "is not null" - }, - { - "{ items: [] }", - "{ items: 2 }", - "has an array instead of an integer at $.items" - }, - { - "{ items: [ \"fork\", \"knife\" , \"spoon\" ] }", - "{ items: [ \"fork\", \"knife\" ] }", - "has 3 elements instead of 2 at $.items" - }, - { - "{ items: [ \"fork\", \"knife\" ] }", - "{ items: [ \"fork\", \"knife\" , \"spoon\" ] }", - "has 2 elements instead of 3 at $.items" - }, - { - "{ items: [ \"fork\", \"knife\" , \"spoon\" ] }", - "{ items: [ \"fork\", \"spoon\", \"knife\" ] }", - "has a different value at $.items[1]" - }, - { - "{ tree: { } }", - "{ tree: \"oak\" }", - "has an object instead of a string at $.tree" - }, - { - "{ tree: { leaves: 10} }", - "{ tree: { branches: 5, leaves: 10 } }", - "misses property $.tree.branches" - }, - { - "{ tree: { branches: 5, leaves: 10 } }", - "{ tree: { leaves: 10} }", - "has extra property $.tree.branches" - }, - { - "{ tree: { leaves: 5 } }", - "{ tree: { leaves: 10} }", - "has a different value at $.tree.leaves" - }, - { - "{ eyes: \"blue\" }", - "{ eyes: [] }", - "has a string instead of an array at $.eyes" - }, - { - "{ eyes: \"blue\" }", - "{ eyes: 2 }", - "has a string instead of an integer at $.eyes" - }, - { - "{ id: 1 }", - "{ id: 2 }", - "has a different value at $.id" - } - }; + // Act & Assert + a.Should().BeEquivalentTo(a); + b.Should().BeEquivalentTo(b); + a.Should().BeEquivalentTo(b); + } - [Theory] - [MemberData(nameof(FailingBeEquivalentCases))] - public void When_both_tokens_are_not_equivalent_it_should_throw_and_mention_the_difference( - string actualJson, string expectedJson, string expectedDifference) + public static TheoryData FailingBeEquivalentCases => new() + { { - // Arrange - var actual = (actualJson != null) ? JToken.Parse(actualJson) : null; - var expected = (expectedJson != null) ? JToken.Parse(expectedJson) : null; + null, + "{ id: 2 }", + "is null" + }, + { + "{ id: 1 }", + null, + "is not null" + }, + { + "{ items: [] }", + "{ items: 2 }", + "has an array instead of an integer at $.items" + }, + { + "{ items: [ \"fork\", \"knife\" , \"spoon\" ] }", + "{ items: [ \"fork\", \"knife\" ] }", + "has 3 elements instead of 2 at $.items" + }, + { + "{ items: [ \"fork\", \"knife\" ] }", + "{ items: [ \"fork\", \"knife\" , \"spoon\" ] }", + "has 2 elements instead of 3 at $.items" + }, + { + "{ items: [ \"fork\", \"knife\" , \"spoon\" ] }", + "{ items: [ \"fork\", \"spoon\", \"knife\" ] }", + "has a different value at $.items[1]" + }, + { + "{ tree: { } }", + "{ tree: \"oak\" }", + "has an object instead of a string at $.tree" + }, + { + "{ tree: { leaves: 10} }", + "{ tree: { branches: 5, leaves: 10 } }", + "misses property $.tree.branches" + }, + { + "{ tree: { branches: 5, leaves: 10 } }", + "{ tree: { leaves: 10} }", + "has extra property $.tree.branches" + }, + { + "{ tree: { leaves: 5 } }", + "{ tree: { leaves: 10} }", + "has a different value at $.tree.leaves" + }, + { + "{ eyes: \"blue\" }", + "{ eyes: [] }", + "has a string instead of an array at $.eyes" + }, + { + "{ eyes: \"blue\" }", + "{ eyes: 2 }", + "has a string instead of an integer at $.eyes" + }, + { + "{ id: 1 }", + "{ id: 2 }", + "has a different value at $.id" + } + }; + + [Theory] + [MemberData(nameof(FailingBeEquivalentCases))] + public void When_both_tokens_are_not_equivalent_it_should_throw_and_mention_the_difference( + string actualJson, string expectedJson, string expectedDifference) + { + // Arrange + var actual = (actualJson != null) ? JToken.Parse(actualJson) : null; + var expected = (expectedJson != null) ? JToken.Parse(expectedJson) : null; + + var expectedMessage = + $"JSON document {expectedDifference}." + + "Actual document" + + $"{Format(actual, true)}" + + "was expected to be equivalent to" + + $"{Format(expected, true)}."; + + // Act & Assert + actual.Should().Invoking(x => x.BeEquivalentTo(expected)) + .Should().Throw() + .WithMessage(expectedMessage); + } - var expectedMessage = + [Theory] + [MemberData(nameof(PropertiesDifferingBetweenTwoTokens))] + public void When_properties_differ_between_two_tokens_it_should_not_treat_them_as_equivalent(JToken actual, JToken expected, string expectedDifference) + { + // Act & Assert + var expectedMessage = $"JSON document {expectedDifference}." + "Actual document" + $"{Format(actual, true)}" + "was expected to be equivalent to" + $"{Format(expected, true)}."; - // Act & Assert - actual.Should().Invoking(x => x.BeEquivalentTo(expected)) - .Should().Throw() - .WithMessage(expectedMessage); - } - - [Theory] - [MemberData(nameof(PropertiesDifferingBetweenTwoTokens))] - public void When_properties_differ_between_two_tokens_it_should_not_treat_them_as_equivalent(JToken actual, JToken expected, string expectedDifference) - { - // Act & Assert - var expectedMessage = - $"JSON document {expectedDifference}." + - "Actual document" + - $"{Format(actual, true)}" + - "was expected to be equivalent to" + - $"{Format(expected, true)}."; - - actual.Should().Invoking(x => x.BeEquivalentTo(expected)) - .Should().Throw() - .WithMessage(expectedMessage); - } + actual.Should().Invoking(x => x.BeEquivalentTo(expected)) + .Should().Throw() + .WithMessage(expectedMessage); + } - public static TheoryData PropertiesDifferingBetweenTwoTokens => new() + public static TheoryData PropertiesDifferingBetweenTwoTokens => new() + { { - { - new JProperty("eyes", "blue"), - new JArray(), - "has a property instead of an array at $" - }, - { - new JProperty("eyes", "blue"), - new JProperty("hair", "black"), - "has a different name at $" - }, - }; - - [Fact] - public void When_both_property_values_are_null_it_should_treat_them_as_equivalent() + new JProperty("eyes", "blue"), + new JArray(), + "has a property instead of an array at $" + }, { - // Arrange - var actual = JToken.Parse("{ \"id\": null }"); - var expected = JToken.Parse("{ \"id\": null }"); - - // Act & Assert - actual.Should().BeEquivalentTo(expected); - } + new JProperty("eyes", "blue"), + new JProperty("hair", "black"), + "has a different name at $" + }, + }; - [Theory] - [MemberData(nameof(JsonArraysHavingTheSamePropertiesInTheSameOrder))] - public void When_two_json_arrays_have_the_same_properties_in_the_same_order_they_should_be_treated_as_equivalent(JArray actual, JArray expected) - { - // Act & Assert - actual.Should().BeEquivalentTo(expected); - } + [Fact] + public void When_both_property_values_are_null_it_should_treat_them_as_equivalent() + { + // Arrange + var actual = JToken.Parse("{ \"id\": null }"); + var expected = JToken.Parse("{ \"id\": null }"); - public static TheoryData JsonArraysHavingTheSamePropertiesInTheSameOrder => new() - { - { - new JArray(1, 2, 3), - new JArray(1, 2, 3) - }, - { - new JArray("blue", "green"), - new JArray("blue", "green") - }, - { - new JArray(JToken.Parse("{ car: { color: \"blue\" }}"), JToken.Parse("{ flower: { color: \"red\" }}")), - new JArray(JToken.Parse("{ car: { color: \"blue\" }}"), JToken.Parse("{ flower: { color: \"red\" }}")) - }, - }; + // Act & Assert + actual.Should().BeEquivalentTo(expected); + } - [Theory] - [MemberData(nameof(JsonArraysHavingTheSamePropertiesInDifferentOrder))] - public void When_only_the_order_of_properties_differ_they_should_be_treated_as_equivalent(JToken actual, JToken expected) - { - // Act & Assert - actual.Should().BeEquivalentTo(expected); - } + [Theory] + [MemberData(nameof(JsonArraysHavingTheSamePropertiesInTheSameOrder))] + public void When_two_json_arrays_have_the_same_properties_in_the_same_order_they_should_be_treated_as_equivalent(JArray actual, JArray expected) + { + // Act & Assert + actual.Should().BeEquivalentTo(expected); + } - public static TheoryData JsonArraysHavingTheSamePropertiesInDifferentOrder => new() + public static TheoryData JsonArraysHavingTheSamePropertiesInTheSameOrder => new() + { { - { - JToken.Parse("{ friends: [{ id: 123, name: \"Corby Page\" }, { id: 456, name: \"Carter Page\" }] }"), - JToken.Parse("{ friends: [{ name: \"Corby Page\", id: 123 }, { id: 456, name: \"Carter Page\" }] }") - }, - { - JToken.Parse("{ id: 2, admin: true }"), - JToken.Parse("{ admin: true, id: 2}") - }, - }; - - [Fact] - public void When_a_token_is_compared_to_its_string_representation_they_should_be_treated_as_equivalent() - { - // Arrange - string jsonString = - """ - { - friends: - [{ - id: 123, - name: "John Doe" - }, { - id: 456, - name: "Jane Doe", - kids: - [ - "Jimmy", - "James" - ] - } - ] - } - """; - - var actualJSON = JToken.Parse(jsonString); - - // Act & Assert - actualJSON.Should().BeEquivalentTo(jsonString); - } - - [Fact] - public void When_checking_non_equivalency_with_an_invalid_expected_string_it_should_provide_a_clear_error_message() + new JArray(1, 2, 3), + new JArray(1, 2, 3) + }, { - // Arrange - var actualJson = JToken.Parse("{ \"id\": null }"); - var expectedString = "{ invalid JSON }"; - - // Act & Assert - actualJson.Should().Invoking(x => x.BeEquivalentTo(expectedString)) - .Should().Throw() - .WithMessage($"Unable to parse expected JSON string:{expectedString}*") - .WithInnerException(); - } - - [Fact] - public void When_checking_for_non_equivalency_with_an_unparseable_string_it_should_provide_a_clear_error_message() + new JArray("blue", "green"), + new JArray("blue", "green") + }, { - // Arrange - var actualJson = JToken.Parse("{ \"id\": null }"); - var unexpectedString = "{ invalid JSON }"; + new JArray(JToken.Parse("{ car: { color: \"blue\" }}"), JToken.Parse("{ flower: { color: \"red\" }}")), + new JArray(JToken.Parse("{ car: { color: \"blue\" }}"), JToken.Parse("{ flower: { color: \"red\" }}")) + }, + }; - // Act & Assert - actualJson.Should().Invoking(x => x.NotBeEquivalentTo(unexpectedString)) - .Should().Throw() - .WithMessage($"Unable to parse unexpected JSON string:{unexpectedString}*") - .WithInnerException(); - } + [Theory] + [MemberData(nameof(JsonArraysHavingTheSamePropertiesInDifferentOrder))] + public void When_only_the_order_of_properties_differ_they_should_be_treated_as_equivalent(JToken actual, JToken expected) + { + // Act & Assert + actual.Should().BeEquivalentTo(expected); + } - [Fact] - public void When_specifying_a_reason_why_a_token_should_be_equivalent_it_should_use_that_in_the_error_message() + public static TheoryData JsonArraysHavingTheSamePropertiesInDifferentOrder => new() + { { - // Arrange - var subject = JToken.Parse("{ child: { subject: 'foo' } }"); - var expected = JToken.Parse("{ child: { expected: 'bar' } }"); - - var expectedMessage = - "JSON document misses property $.child.expected." + - "Actual document" + - $"{Format(subject, true)}" + - "was expected to be equivalent to" + - $"{Format(expected, true)} " + - "because we want to test the failure message."; - - // Act & Assert - subject.Should().Invoking(x => x.BeEquivalentTo(expected, "we want to test the failure {0}", "message")) - .Should().Throw() - .WithMessage(expectedMessage); - } - - [Fact] - public void When_property_values_differ_a_non_equivalency_check_should_succeed() + JToken.Parse("{ friends: [{ id: 123, name: \"Corby Page\" }, { id: 456, name: \"Carter Page\" }] }"), + JToken.Parse("{ friends: [{ name: \"Corby Page\", id: 123 }, { id: 456, name: \"Carter Page\" }] }") + }, { - // Arrange - var actual = JToken.Parse("{ \"id\": 1 }"); - var expected = JToken.Parse("{ \"id\": 2 }"); + JToken.Parse("{ id: 2, admin: true }"), + JToken.Parse("{ admin: true, id: 2}") + }, + }; - // Act & Assert - actual.Should().NotBeEquivalentTo(expected); - } + [Fact] + public void When_a_token_is_compared_to_its_string_representation_they_should_be_treated_as_equivalent() + { + // Arrange + string jsonString = + """ + { + friends: + [{ + id: 123, + name: "John Doe" + }, { + id: 456, + name: "Jane Doe", + kids: + [ + "Jimmy", + "James" + ] + } + ] + } + """; + + var actualJSON = JToken.Parse(jsonString); + + // Act & Assert + actualJSON.Should().BeEquivalentTo(jsonString); + } - [Fact] - public void When_two_tokens_are_the_same_the_non_equivalency_check_should_throw() - { - // Arrange - var a = JToken.Parse("{ \"id\": 1 }"); - var b = JToken.Parse("{ \"id\": 1 }"); + [Fact] + public void When_checking_non_equivalency_with_an_invalid_expected_string_it_should_provide_a_clear_error_message() + { + // Arrange + var actualJson = JToken.Parse("{ \"id\": null }"); + var expectedString = "{ invalid JSON }"; + + // Act & Assert + actualJson.Should().Invoking(x => x.BeEquivalentTo(expectedString)) + .Should().Throw() + .WithMessage($"Unable to parse expected JSON string:{expectedString}*") + .WithInnerException(); + } - // Act & Assert - a.Invoking(x => x.Should().NotBeEquivalentTo(b)) - .Should().Throw() - .WithMessage($"Expected JSON document not to be equivalent to {Format(b)}."); - } + [Fact] + public void When_checking_for_non_equivalency_with_an_unparseable_string_it_should_provide_a_clear_error_message() + { + // Arrange + var actualJson = JToken.Parse("{ \"id\": null }"); + var unexpectedString = "{ invalid JSON }"; + + // Act & Assert + actualJson.Should().Invoking(x => x.NotBeEquivalentTo(unexpectedString)) + .Should().Throw() + .WithMessage($"Unable to parse unexpected JSON string:{unexpectedString}*") + .WithInnerException(); + } - [Fact] - public void When_a_token_is_equal_to_its_string_representation_the_non_equivalency_check_should_throw() - { - // Arrange - string jsonString = "{ \"id\": 1 }"; - var actualJson = JToken.Parse(jsonString); + [Fact] + public void When_specifying_a_reason_why_a_token_should_be_equivalent_it_should_use_that_in_the_error_message() + { + // Arrange + var subject = JToken.Parse("{ child: { subject: 'foo' } }"); + var expected = JToken.Parse("{ child: { expected: 'bar' } }"); + + var expectedMessage = + "JSON document misses property $.child.expected." + + "Actual document" + + $"{Format(subject, true)}" + + "was expected to be equivalent to" + + $"{Format(expected, true)} " + + "because we want to test the failure message."; + + // Act & Assert + subject.Should().Invoking(x => x.BeEquivalentTo(expected, "we want to test the failure {0}", "message")) + .Should().Throw() + .WithMessage(expectedMessage); + } - // Act - Action action = () => actualJson.Should().NotBeEquivalentTo(jsonString); + [Fact] + public void When_property_values_differ_a_non_equivalency_check_should_succeed() + { + // Arrange + var actual = JToken.Parse("{ \"id\": 1 }"); + var expected = JToken.Parse("{ \"id\": 2 }"); - // Assert - action.Should().Throw() - .WithMessage("Expected JSON document not to be equivalent*"); - } + // Act & Assert + actual.Should().NotBeEquivalentTo(expected); + } - [Fact] - public void When_a_float_is_within_approximation_check_should_succeed() - { - // Arrange - var actual = JToken.Parse("{ \"id\": 1.1232 }"); - var expected = JToken.Parse("{ \"id\": 1.1235 }"); + [Fact] + public void When_two_tokens_are_the_same_the_non_equivalency_check_should_throw() + { + // Arrange + var a = JToken.Parse("{ \"id\": 1 }"); + var b = JToken.Parse("{ \"id\": 1 }"); + + // Act & Assert + a.Invoking(x => x.Should().NotBeEquivalentTo(b)) + .Should().Throw() + .WithMessage($"Expected JSON document not to be equivalent to {Format(b)}."); + } - // Act & Assert - actual.Should().BeEquivalentTo(expected, options => options - .Using(d => d.Subject.Should().BeApproximately(d.Expectation, 1e-3)) - .WhenTypeIs()); - } + [Fact] + public void When_a_token_is_equal_to_its_string_representation_the_non_equivalency_check_should_throw() + { + // Arrange + string jsonString = "{ \"id\": 1 }"; + var actualJson = JToken.Parse(jsonString); - [Fact] - public void When_a_float_is_not_within_approximation_check_should_throw() - { - // Arrange - var actual = JToken.Parse("{ \"id\": 1.1232 }"); - var expected = JToken.Parse("{ \"id\": 1.1235 }"); + // Act + Action action = () => actualJson.Should().NotBeEquivalentTo(jsonString); - // Act & Assert - actual.Should(). - Invoking(x => x.BeEquivalentTo(expected, options => options - .Using(d => d.Subject.Should().BeApproximately(d.Expectation, 1e-5)) - .WhenTypeIs())) - .Should().Throw() - .WithMessage("JSON document has a different value at $.id.*"); - } + // Assert + action.Should().Throw() + .WithMessage("Expected JSON document not to be equivalent*"); + } - [Fact] - public void When_the_value_of_a_property_contains_curly_braces_the_equivalency_check_should_not_choke_on_them() - { - // Arrange - var actual = JToken.Parse(@"{ ""{a1}"": {b: 1 }}"); - var expected = JToken.Parse(@"{ ""{a1}"": {b: 2 }}"); + [Fact] + public void When_a_float_is_within_approximation_check_should_succeed() + { + // Arrange + var actual = JToken.Parse("{ \"id\": 1.1232 }"); + var expected = JToken.Parse("{ \"id\": 1.1235 }"); + + // Act & Assert + actual.Should().BeEquivalentTo(expected, options => options + .Using(d => d.Subject.Should().BeApproximately(d.Expectation, 1e-3)) + .WhenTypeIs()); + } - // Act & Assert - var expectedMessage = - "JSON document has a different value at $.{a1}.b." + - "Actual document" + - $"{Format(actual, true)}" + - "was expected to be equivalent to" + - $"{Format(expected, true)}."; + [Fact] + public void When_a_float_is_not_within_approximation_check_should_throw() + { + // Arrange + var actual = JToken.Parse("{ \"id\": 1.1232 }"); + var expected = JToken.Parse("{ \"id\": 1.1235 }"); + + // Act & Assert + actual.Should(). + Invoking(x => x.BeEquivalentTo(expected, options => options + .Using(d => d.Subject.Should().BeApproximately(d.Expectation, 1e-5)) + .WhenTypeIs())) + .Should().Throw() + .WithMessage("JSON document has a different value at $.id.*"); + } - actual.Should().Invoking(x => x.BeEquivalentTo(expected)) - .Should().Throw() - .WithMessage(expectedMessage); - } + [Fact] + public void When_the_value_of_a_property_contains_curly_braces_the_equivalency_check_should_not_choke_on_them() + { + // Arrange + var actual = JToken.Parse(@"{ ""{a1}"": {b: 1 }}"); + var expected = JToken.Parse(@"{ ""{a1}"": {b: 2 }}"); + + // Act & Assert + var expectedMessage = + "JSON document has a different value at $.{a1}.b." + + "Actual document" + + $"{Format(actual, true)}" + + "was expected to be equivalent to" + + $"{Format(expected, true)}."; + + actual.Should().Invoking(x => x.BeEquivalentTo(expected)) + .Should().Throw() + .WithMessage(expectedMessage); + } - #endregion (Not)BeEquivalentTo + #endregion (Not)BeEquivalentTo - #region (Not)HaveValue + #region (Not)HaveValue - [Fact] - public void When_the_token_has_the_expected_value_it_should_succeed() - { - // Arrange - var subject = JToken.Parse("{ 'id': 42 }"); + [Fact] + public void When_the_token_has_the_expected_value_it_should_succeed() + { + // Arrange + var subject = JToken.Parse("{ 'id': 42 }"); - // Act & Assert - subject["id"].Should().HaveValue("42"); - } + // Act & Assert + subject["id"].Should().HaveValue("42"); + } - [Fact] - public void When_the_token_is_null_then_asserting_on_a_value_expectation_should_throw() - { - // Arrange - JToken subject = null; + [Fact] + public void When_the_token_is_null_then_asserting_on_a_value_expectation_should_throw() + { + // Arrange + JToken subject = null; - // Act - Action act = () => subject.Should().HaveValue("foo"); + // Act + Action act = () => subject.Should().HaveValue("foo"); - // Assert - act.Should().Throw().WithMessage("Expected*foo*was*null*"); - } + // Assert + act.Should().Throw().WithMessage("Expected*foo*was*null*"); + } - [Fact] - public void When_the_token_has_another_value_than_expected_it_should_throw() - { - // Arrange - var subject = JToken.Parse("{ 'id': 42 }"); + [Fact] + public void When_the_token_has_another_value_than_expected_it_should_throw() + { + // Arrange + var subject = JToken.Parse("{ 'id': 42 }"); - // Act & Assert - subject["id"].Should().Invoking(x => x.HaveValue("43", "because foo")) - .Should().Throw() - .WithMessage("Expected JSON property \"id\" to have value \"43\" because foo, but found \"42\"."); - } + // Act & Assert + subject["id"].Should().Invoking(x => x.HaveValue("43", "because foo")) + .Should().Throw() + .WithMessage("Expected JSON property \"id\" to have value \"43\" because foo, but found \"42\"."); + } - [Fact] - public void When_the_token_does_not_have_the_unexpected_value_it_should_succeed() - { - // Arrange - var subject = JToken.Parse("{ 'id': 43 }"); + [Fact] + public void When_the_token_does_not_have_the_unexpected_value_it_should_succeed() + { + // Arrange + var subject = JToken.Parse("{ 'id': 43 }"); - // Act & Assert - subject["id"].Should().NotHaveValue("42"); - } + // Act & Assert + subject["id"].Should().NotHaveValue("42"); + } - [Fact] - public void When_the_token_is_null_assertions_on_not_having_a_value_should_throw() - { - // Arrange - JToken subject = null; + [Fact] + public void When_the_token_is_null_assertions_on_not_having_a_value_should_throw() + { + // Arrange + JToken subject = null; - // Act - Action act = () => subject.Should().NotHaveValue("foo"); + // Act + Action act = () => subject.Should().NotHaveValue("foo"); - // Assert - act.Should().Throw().WithMessage("Did not expect*foo*was*null*"); - } + // Assert + act.Should().Throw().WithMessage("Did not expect*foo*was*null*"); + } - [Fact] - public void When_the_token_has_a_value_that_it_was_not_supposed_to_have_it_should_throw() - { - // Arrange - var subject = JToken.Parse("{ 'id': 42 }"); + [Fact] + public void When_the_token_has_a_value_that_it_was_not_supposed_to_have_it_should_throw() + { + // Arrange + var subject = JToken.Parse("{ 'id': 42 }"); - // Act & Assert - subject["id"].Should().Invoking(x => x.NotHaveValue("42", "because foo")) - .Should().Throw() - .WithMessage("Did not expect JSON property \"id\" to have value \"42\" because foo."); - } + // Act & Assert + subject["id"].Should().Invoking(x => x.NotHaveValue("42", "because foo")) + .Should().Throw() + .WithMessage("Did not expect JSON property \"id\" to have value \"42\" because foo."); + } - #endregion (Not)HaveValue + #endregion (Not)HaveValue - #region (Not)MatchRegex + #region (Not)MatchRegex - [Fact] - public void When_a_tokens_value_matches_the_regex_pattern_it_should_succeed() - { - // Arrange - var subject = JToken.Parse("{ 'id': 42 }"); + [Fact] + public void When_a_tokens_value_matches_the_regex_pattern_it_should_succeed() + { + // Arrange + var subject = JToken.Parse("{ 'id': 42 }"); - // Act & Assert - subject["id"].Should().MatchRegex("\\d{2}"); - } + // Act & Assert + subject["id"].Should().MatchRegex("\\d{2}"); + } - [Fact] - public void When_a_tokens_value_does_not_match_the_regex_pattern_it_should_throw() - { - // Arrange - var subject = JToken.Parse("{ 'id': 'not two digits' }"); + [Fact] + public void When_a_tokens_value_does_not_match_the_regex_pattern_it_should_throw() + { + // Arrange + var subject = JToken.Parse("{ 'id': 'not two digits' }"); - // Act & Assert - subject["id"].Should().Invoking(x => x.MatchRegex("\\d{2}", "because foo")) - .Should().Throw() - .WithMessage("Expected JSON property \"id\" to match regex pattern \"\\d{2}\" because foo, but found \"not two digits\"."); - } + // Act & Assert + subject["id"].Should().Invoking(x => x.MatchRegex("\\d{2}", "because foo")) + .Should().Throw() + .WithMessage("Expected JSON property \"id\" to match regex pattern \"\\d{2}\" because foo, but found \"not two digits\"."); + } - [Fact] - public void When_a_tokens_value_does_not_match_the_regex_pattern_and_that_is_expected_it_should_succeed() - { - // Arrange - var subject = JToken.Parse("{ 'id': 'not two digits' }"); + [Fact] + public void When_a_tokens_value_does_not_match_the_regex_pattern_and_that_is_expected_it_should_succeed() + { + // Arrange + var subject = JToken.Parse("{ 'id': 'not two digits' }"); - // Act & Assert - subject["id"].Should().NotMatchRegex("\\d{2}"); - } + // Act & Assert + subject["id"].Should().NotMatchRegex("\\d{2}"); + } - [Fact] - public void When_a_tokens_value_matches_the_regex_pattern_unexpectedly_it_should_throw() - { - // Arrange - var subject = JToken.Parse("{ 'id': 42 }"); + [Fact] + public void When_a_tokens_value_matches_the_regex_pattern_unexpectedly_it_should_throw() + { + // Arrange + var subject = JToken.Parse("{ 'id': 42 }"); - // Act & Assert - subject["id"].Should().Invoking(x => x.NotMatchRegex("\\d{2}", "because foo")) - .Should().Throw() - .WithMessage("Did not expect JSON property \"id\" to match regex pattern \"\\d{2}\" because foo."); - } + // Act & Assert + subject["id"].Should().Invoking(x => x.NotMatchRegex("\\d{2}", "because foo")) + .Should().Throw() + .WithMessage("Did not expect JSON property \"id\" to match regex pattern \"\\d{2}\" because foo."); + } - #endregion (Not)MatchRegex + #endregion (Not)MatchRegex - #region (Not)HaveElement + #region (Not)HaveElement - [Fact] - public void When_the_token_has_a_property_with_the_specified_key_it_should_succeed() - { - // Arrange - var subject = JToken.Parse("{ 'id': 42 }"); + [Fact] + public void When_the_token_has_a_property_with_the_specified_key_it_should_succeed() + { + // Arrange + var subject = JToken.Parse("{ 'id': 42 }"); - // Act & Assert - subject.Should().HaveElement("id"); - } + // Act & Assert + subject.Should().HaveElement("id"); + } - [Fact] - public void When_the_token_does_not_have_the_specified_property_it_should_throw() - { - // Arrange - var subject = JToken.Parse("{ 'id': 42 }"); + [Fact] + public void When_the_token_does_not_have_the_specified_property_it_should_throw() + { + // Arrange + var subject = JToken.Parse("{ 'id': 42 }"); - // Act & Assert - subject.Should().Invoking(x => x.HaveElement("name", "because foo")) - .Should().Throw() - .WithMessage($"Expected JSON document {Format(subject)} to have element \"name\" because foo, but no such element was found."); - } + // Act & Assert + subject.Should().Invoking(x => x.HaveElement("name", "because foo")) + .Should().Throw() + .WithMessage($"Expected JSON document {Format(subject)} to have element \"name\" because foo, but no such element was found."); + } - [Fact] - public void When_the_token_does_not_have_the_specified_element_and_that_was_expected_it_should_succeed() - { - // Arrange - var subject = JToken.Parse("{ 'id': 42 }"); + [Fact] + public void When_the_token_does_not_have_the_specified_element_and_that_was_expected_it_should_succeed() + { + // Arrange + var subject = JToken.Parse("{ 'id': 42 }"); - // Act & Assert - subject.Should().NotHaveElement("name"); - } + // Act & Assert + subject.Should().NotHaveElement("name"); + } - [Fact] - public void When_the_token_has_an_unexpected_element_it_should_throw() - { - // Arrange - var subject = JToken.Parse("{ 'id': 42 }"); + [Fact] + public void When_the_token_has_an_unexpected_element_it_should_throw() + { + // Arrange + var subject = JToken.Parse("{ 'id': 42 }"); - // Act & Assert - subject.Should().Invoking(x => x.NotHaveElement("id", "because foo")) - .Should().Throw() - .WithMessage($"Did not expect JSON document {Format(subject)} to have element \"id\" because foo."); - } + // Act & Assert + subject.Should().Invoking(x => x.NotHaveElement("id", "because foo")) + .Should().Throw() + .WithMessage($"Did not expect JSON document {Format(subject)} to have element \"id\" because foo."); + } - #endregion (Not)HaveElement + #endregion (Not)HaveElement - #region ContainSingleItem + #region ContainSingleItem - [Fact] - public void When_the_token_has_a_single_child_and_that_was_expected_it_should_succeed() - { - // Arrange - var subject = JToken.Parse("{ id: 42 }"); + [Fact] + public void When_the_token_has_a_single_child_and_that_was_expected_it_should_succeed() + { + // Arrange + var subject = JToken.Parse("{ id: 42 }"); - // Act - Action act = () => subject.Should().ContainSingleItem(); + // Act + Action act = () => subject.Should().ContainSingleItem(); - // Assert - act.Should().NotThrow(); - } + // Assert + act.Should().NotThrow(); + } - [Fact] - public void When_the_token_has_a_single_child_it_should_return_that_child_for_chaining() - { - // Arrange - var subject = JToken.Parse("{ id: 42 }"); + [Fact] + public void When_the_token_has_a_single_child_it_should_return_that_child_for_chaining() + { + // Arrange + var subject = JToken.Parse("{ id: 42 }"); - // Act - var element = subject.Should().ContainSingleItem().Which; + // Act + var element = subject.Should().ContainSingleItem().Which; - // Assert - element.Should().BeEquivalentTo(new JProperty("id", 42)); - } + // Assert + element.Should().BeEquivalentTo(new JProperty("id", 42)); + } - [Fact] - public void When_the_token_is_null_then_asserting_a_single_child_should_throw_with_a_clear_failure() - { - // Arrange - JToken subject = null; + [Fact] + public void When_the_token_is_null_then_asserting_a_single_child_should_throw_with_a_clear_failure() + { + // Arrange + JToken subject = null; - // Act - Action act = () => subject.Should().ContainSingleItem("null is not allowed"); + // Act + Action act = () => subject.Should().ContainSingleItem("null is not allowed"); - // Assert - act.Should().Throw() - .WithMessage("Expected JSON document to contain a single item because null is not allowed, but found ."); - } + // Assert + act.Should().Throw() + .WithMessage("Expected JSON document to contain a single item because null is not allowed, but found ."); + } - [Fact] - public void When_the_token_is_an_empty_object_then_the_assertion_on_a_single_item_should_throw() - { - // Arrange - var subject = JToken.Parse("{ }"); + [Fact] + public void When_the_token_is_an_empty_object_then_the_assertion_on_a_single_item_should_throw() + { + // Arrange + var subject = JToken.Parse("{ }"); - // Act - Action act = () => subject.Should().ContainSingleItem("less is not allowed"); + // Act + Action act = () => subject.Should().ContainSingleItem("less is not allowed"); - // Assert - act.Should().Throw() - .WithMessage("Expected JSON document * to contain a single item because less is not allowed, but the collection is empty."); - } + // Assert + act.Should().Throw() + .WithMessage("Expected JSON document * to contain a single item because less is not allowed, but the collection is empty."); + } - [Fact] - public void When_the_token_contains_multiple_properties_then_the_single_item_assertion_should_throw() - { - // Arrange - var subject = JToken.Parse("{ id: 42, admin: true }"); + [Fact] + public void When_the_token_contains_multiple_properties_then_the_single_item_assertion_should_throw() + { + // Arrange + var subject = JToken.Parse("{ id: 42, admin: true }"); - // Act - Action act = () => subject.Should().ContainSingleItem("more is not allowed"); + // Act + Action act = () => subject.Should().ContainSingleItem("more is not allowed"); - // Assert - act.Should().Throw() - .WithMessage("Expected JSON document*id*42*admin*true*to contain a single item because more is not allowed, but found*"); - } + // Assert + act.Should().Throw() + .WithMessage("Expected JSON document*id*42*admin*true*to contain a single item because more is not allowed, but found*"); + } - [Fact] - public void When_the_token_is_an_array_with_a_single_property_then_that_should_satisfy_the_single_item_assertion() - { - // Arrange - var subject = JToken.Parse("[{ id: 42 }]"); + [Fact] + public void When_the_token_is_an_array_with_a_single_property_then_that_should_satisfy_the_single_item_assertion() + { + // Arrange + var subject = JToken.Parse("[{ id: 42 }]"); - // Act - Action act = () => subject.Should().ContainSingleItem(); + // Act + Action act = () => subject.Should().ContainSingleItem(); - // Assert - act.Should().NotThrow(); - } + // Assert + act.Should().NotThrow(); + } - [Fact] - public void When_the_token_is_an_array_with_a_single_property_the_single_item_assertion_should_return_that_item_for_chaining() - { - // Arrange - var subject = JToken.Parse("[{ id: 42 }]"); + [Fact] + public void When_the_token_is_an_array_with_a_single_property_the_single_item_assertion_should_return_that_item_for_chaining() + { + // Arrange + var subject = JToken.Parse("[{ id: 42 }]"); - // Act - var element = subject.Should().ContainSingleItem().Which; + // Act + var element = subject.Should().ContainSingleItem().Which; - // Assert - element.Should().BeEquivalentTo(JToken.Parse("{ id: 42 }")); - } + // Assert + element.Should().BeEquivalentTo(JToken.Parse("{ id: 42 }")); + } - [Fact] - public void When_the_token_is_an_empty_array_then_an_assertion_for_a_single_item_should_throw() - { - // Arrange - var subject = JToken.Parse("[]"); + [Fact] + public void When_the_token_is_an_empty_array_then_an_assertion_for_a_single_item_should_throw() + { + // Arrange + var subject = JToken.Parse("[]"); - // Act - Action act = () => subject.Should().ContainSingleItem("less is not allowed"); + // Act + Action act = () => subject.Should().ContainSingleItem("less is not allowed"); - // Assert - act.Should().Throw() - .WithMessage("Expected JSON document [] to contain a single item because less is not allowed, but the collection is empty."); - } + // Assert + act.Should().Throw() + .WithMessage("Expected JSON document [] to contain a single item because less is not allowed, but the collection is empty."); + } - [Fact] - public void When_the_token_is_an_array_with_multiple_items_asserting_for_a_single_item_should_throw() - { - // Arrange - var subject = JToken.Parse("[1, 2]"); + [Fact] + public void When_the_token_is_an_array_with_multiple_items_asserting_for_a_single_item_should_throw() + { + // Arrange + var subject = JToken.Parse("[1, 2]"); - // Act - Action act = () => subject.Should().ContainSingleItem("more is not allowed"); + // Act + Action act = () => subject.Should().ContainSingleItem("more is not allowed"); - // Assert - string formattedSubject = Format(subject); + // Assert + string formattedSubject = Format(subject); - act.Should().Throw() - .WithMessage($"Expected JSON document {formattedSubject} to contain a single item because more is not allowed, but found {formattedSubject}."); - } + act.Should().Throw() + .WithMessage($"Expected JSON document {formattedSubject} to contain a single item because more is not allowed, but found {formattedSubject}."); + } - #endregion ContainSingleItem + #endregion ContainSingleItem - #region HaveCount + #region HaveCount - [Fact] - public void When_the_number_of_items_match_the_expectation_it_should_succeed() - { - // Arrange - var subject = JToken.Parse("{ id: 42, admin: true }"); + [Fact] + public void When_the_number_of_items_match_the_expectation_it_should_succeed() + { + // Arrange + var subject = JToken.Parse("{ id: 42, admin: true }"); - // Act - Action act = () => subject.Should().HaveCount(2); + // Act + Action act = () => subject.Should().HaveCount(2); - // Assert - act.Should().NotThrow(); - } + // Assert + act.Should().NotThrow(); + } - [Fact] - public void When_the_number_of_items_match_the_expectation_it_should_allow_chaining_more_assertions() - { - // Arrange - var subject = JToken.Parse("{ id: 42 }"); + [Fact] + public void When_the_number_of_items_match_the_expectation_it_should_allow_chaining_more_assertions() + { + // Arrange + var subject = JToken.Parse("{ id: 42 }"); - // Act - JTokenAssertions and = subject.Should().HaveCount(1).And; + // Act + JTokenAssertions and = subject.Should().HaveCount(1).And; - // Assert - and.BeEquivalentTo(subject); - } + // Assert + and.BeEquivalentTo(subject); + } - [Fact] - public void When_the_token_is_null_then_an_assertion_on_the_count_should_throw() - { - // Arrange - JToken subject = null; + [Fact] + public void When_the_token_is_null_then_an_assertion_on_the_count_should_throw() + { + // Arrange + JToken subject = null; - // Act - Action act = () => subject.Should().HaveCount(1, "null is not allowed"); + // Act + Action act = () => subject.Should().HaveCount(1, "null is not allowed"); - // Assert - act.Should().Throw() - .WithMessage("Expected JSON document to contain 1 item(s) because null is not allowed, but found ."); - } + // Assert + act.Should().Throw() + .WithMessage("Expected JSON document to contain 1 item(s) because null is not allowed, but found ."); + } - [Fact] - public void When_expecting_a_different_number_of_elements_than_the_actual_number_it_should_throw() - { - // Arrange - var subject = JToken.Parse("{ }"); + [Fact] + public void When_expecting_a_different_number_of_elements_than_the_actual_number_it_should_throw() + { + // Arrange + var subject = JToken.Parse("{ }"); - // Act - Action act = () => subject.Should().HaveCount(1, "numbers matter"); + // Act + Action act = () => subject.Should().HaveCount(1, "numbers matter"); - // Assert - act.Should().Throw() - .WithMessage("Expected JSON document * to contain 1 item(s) because numbers matter, but found 0*"); - } + // Assert + act.Should().Throw() + .WithMessage("Expected JSON document * to contain 1 item(s) because numbers matter, but found 0*"); + } - [Fact] - public void When_expecting_the_actual_number_of_array_items_it_should_succeed() - { - // Arrange - var subject = JToken.Parse("[ 'Hello', 'World!' ]"); + [Fact] + public void When_expecting_the_actual_number_of_array_items_it_should_succeed() + { + // Arrange + var subject = JToken.Parse("[ 'Hello', 'World!' ]"); - // Act - Action act = () => subject.Should().HaveCount(2); + // Act + Action act = () => subject.Should().HaveCount(2); - // Assert - act.Should().NotThrow(); - } + // Assert + act.Should().NotThrow(); + } - [Fact] - public void When_expecting_a_different_number_of_array_items_than_the_actual_number_it_should_fail() - { - // Arrange - var subject = JToken.Parse("[ 'Hello', 'World!' ]"); + [Fact] + public void When_expecting_a_different_number_of_array_items_than_the_actual_number_it_should_fail() + { + // Arrange + var subject = JToken.Parse("[ 'Hello', 'World!' ]"); - // Act - Action act = () => subject.Should().HaveCount(3, "the more the better"); + // Act + Action act = () => subject.Should().HaveCount(3, "the more the better"); - // Assert - act.Should().Throw() - .WithMessage("Expected JSON document * to contain 3 item(s) because the more the better, but found 2*"); - } + // Assert + act.Should().Throw() + .WithMessage("Expected JSON document * to contain 3 item(s) because the more the better, but found 2*"); + } - #endregion HaveCount + #endregion HaveCount - #region ContainSubtree + #region ContainSubtree - [Fact] - public void When_all_expected_subtree_properties_match_it_should_succeed() - { - // Arrange - var subject = JToken.Parse("{ foo: 'foo', bar: 'bar', baz: 'baz'} "); + [Fact] + public void When_all_expected_subtree_properties_match_it_should_succeed() + { + // Arrange + var subject = JToken.Parse("{ foo: 'foo', bar: 'bar', baz: 'baz'} "); - // Act - Action act = () => subject.Should().ContainSubtree(" { foo: 'foo', baz: 'baz' } "); + // Act + Action act = () => subject.Should().ContainSubtree(" { foo: 'foo', baz: 'baz' } "); - // Assert - act.Should().NotThrow(); - } + // Assert + act.Should().NotThrow(); + } - [Fact] - public void When_deep_subtree_matches_it_should_succeed() - { - // Arrange - var subject = JToken.Parse("{ foo: 'foo', bar: 'bar', child: { x: 1, y: 2, grandchild: { tag: 'abrakadabra' } }} "); + [Fact] + public void When_deep_subtree_matches_it_should_succeed() + { + // Arrange + var subject = JToken.Parse("{ foo: 'foo', bar: 'bar', child: { x: 1, y: 2, grandchild: { tag: 'abrakadabra' } }} "); - // Act - Action act = () => subject.Should().ContainSubtree(" { child: { grandchild: { tag: 'abrakadabra' } } } "); + // Act + Action act = () => subject.Should().ContainSubtree(" { child: { grandchild: { tag: 'abrakadabra' } } } "); - // Assert - act.Should().NotThrow(); - } + // Assert + act.Should().NotThrow(); + } - [Fact] - public void When_array_elements_are_matching_within_a_nested_structure_it_should_succeed() - { - // Arrange - var subject = JToken.Parse("{ foo: 'foo', bar: 'bar', items: [ { id: 1 }, { id: 2 }, { id: 3 } ] } "); + [Fact] + public void When_array_elements_are_matching_within_a_nested_structure_it_should_succeed() + { + // Arrange + var subject = JToken.Parse("{ foo: 'foo', bar: 'bar', items: [ { id: 1 }, { id: 2 }, { id: 3 } ] } "); - // Act - Action act = () => subject.Should().ContainSubtree(" { items: [ { id: 1 }, { id: 3 } ] } "); + // Act + Action act = () => subject.Should().ContainSubtree(" { items: [ { id: 1 }, { id: 3 } ] } "); - // Assert - act.Should().NotThrow(); - } + // Assert + act.Should().NotThrow(); + } - public static TheoryData FailingContainSubtreeCases => new() + public static TheoryData FailingContainSubtreeCases => new() + { { - { - null, - "{ id: 2 }", - "is null" - }, - { - "{ id: 1 }", - null, - "is not null" - }, - { - "{ foo: 'foo', bar: 'bar' }", - "{ baz: 'baz' }", - "misses property $.baz" - }, - { - "{ items: [] }", - "{ items: 2 }", - "has an array instead of an integer at $.items" - }, - { - "{ items: [ \"fork\", \"knife\" ] }", - "{ items: [ \"fork\", \"knife\" , \"spoon\" ] }", - "misses expected element $.items[2]" - }, - { - "{ items: [ \"fork\", \"knife\" , \"spoon\" ] }", - "{ items: [ \"fork\", \"spoon\", \"knife\" ] }", - "has expected element $.items[2] in the wrong order" - }, - { - "{ items: [ \"fork\", \"knife\" , \"spoon\" ] }", - "{ items: [ \"fork\", \"fork\" ] }", - "has a different value at $.items[1]" - }, - { - "{ tree: { } }", - "{ tree: \"oak\" }", - "has an object instead of a string at $.tree" - }, - { - "{ tree: { leaves: 10} }", - "{ tree: { branches: 5, leaves: 10 } }", - "misses property $.tree.branches" - }, - { - "{ tree: { leaves: 5 } }", - "{ tree: { leaves: 10} }", - "has a different value at $.tree.leaves" - }, - { - "{ eyes: \"blue\" }", - "{ eyes: [] }", - "has a string instead of an array at $.eyes" - }, - { - "{ eyes: \"blue\" }", - "{ eyes: 2 }", - "has a string instead of an integer at $.eyes" - }, - { - "{ id: 1 }", - "{ id: 2 }", - "has a different value at $.id" - }, - { - "{ items: [ { id: 1 }, { id: 3 }, { id: 5 } ] }", - "{ items: [ { id: 1 }, { id: 2 } ] }", - "has a different value at $.items[1].id" - }, - { - "{ foo: '1' }", - "{ foo: 1 }", - "has a string instead of an integer at $.foo" - }, - { - "{ foo: 'foo', bar: 'bar', child: { x: 1, y: 2, grandchild: { tag: 'abrakadabra' } } }", - "{ child: { grandchild: { tag: 'ooops' } } }", - "has a different value at $.child.grandchild.tag" - } - }; - - [Theory] - [MemberData(nameof(FailingContainSubtreeCases))] - public void When_some_JSON_does_not_contain_all_elements_from_a_subtree_it_should_throw( - string actualJson, string expectedJson, string expectedDifference) - { - // Arrange - var actual = (actualJson != null) ? JToken.Parse(actualJson) : null; - var expected = (expectedJson != null) ? JToken.Parse(expectedJson) : null; - - // Act - Action action = () => actual.Should().ContainSubtree(expected); - - // Assert - action.Should().Throw() - .WithMessage( - $"JSON document {expectedDifference}.{Environment.NewLine}" + - $"Actual document{Environment.NewLine}" + - $"{Format(actual, true)}{Environment.NewLine}" + - $"was expected to contain{Environment.NewLine}" + - $"{Format(expected, true)}.{Environment.NewLine}"); - } + null, + "{ id: 2 }", + "is null" + }, + { + "{ id: 1 }", + null, + "is not null" + }, + { + "{ foo: 'foo', bar: 'bar' }", + "{ baz: 'baz' }", + "misses property $.baz" + }, + { + "{ items: [] }", + "{ items: 2 }", + "has an array instead of an integer at $.items" + }, + { + "{ items: [ \"fork\", \"knife\" ] }", + "{ items: [ \"fork\", \"knife\" , \"spoon\" ] }", + "misses expected element $.items[2]" + }, + { + "{ items: [ \"fork\", \"knife\" , \"spoon\" ] }", + "{ items: [ \"fork\", \"spoon\", \"knife\" ] }", + "has expected element $.items[2] in the wrong order" + }, + { + "{ items: [ \"fork\", \"knife\" , \"spoon\" ] }", + "{ items: [ \"fork\", \"fork\" ] }", + "has a different value at $.items[1]" + }, + { + "{ tree: { } }", + "{ tree: \"oak\" }", + "has an object instead of a string at $.tree" + }, + { + "{ tree: { leaves: 10} }", + "{ tree: { branches: 5, leaves: 10 } }", + "misses property $.tree.branches" + }, + { + "{ tree: { leaves: 5 } }", + "{ tree: { leaves: 10} }", + "has a different value at $.tree.leaves" + }, + { + "{ eyes: \"blue\" }", + "{ eyes: [] }", + "has a string instead of an array at $.eyes" + }, + { + "{ eyes: \"blue\" }", + "{ eyes: 2 }", + "has a string instead of an integer at $.eyes" + }, + { + "{ id: 1 }", + "{ id: 2 }", + "has a different value at $.id" + }, + { + "{ items: [ { id: 1 }, { id: 3 }, { id: 5 } ] }", + "{ items: [ { id: 1 }, { id: 2 } ] }", + "has a different value at $.items[1].id" + }, + { + "{ foo: '1' }", + "{ foo: 1 }", + "has a string instead of an integer at $.foo" + }, + { + "{ foo: 'foo', bar: 'bar', child: { x: 1, y: 2, grandchild: { tag: 'abrakadabra' } } }", + "{ child: { grandchild: { tag: 'ooops' } } }", + "has a different value at $.child.grandchild.tag" + } + }; + + [Theory] + [MemberData(nameof(FailingContainSubtreeCases))] + public void When_some_JSON_does_not_contain_all_elements_from_a_subtree_it_should_throw( + string actualJson, string expectedJson, string expectedDifference) + { + // Arrange + var actual = (actualJson != null) ? JToken.Parse(actualJson) : null; + var expected = (expectedJson != null) ? JToken.Parse(expectedJson) : null; + + // Act + Action action = () => actual.Should().ContainSubtree(expected); + + // Assert + action.Should().Throw() + .WithMessage( + $"JSON document {expectedDifference}.{Environment.NewLine}" + + $"Actual document{Environment.NewLine}" + + $"{Format(actual, true)}{Environment.NewLine}" + + $"was expected to contain{Environment.NewLine}" + + $"{Format(expected, true)}.{Environment.NewLine}"); + } - [Fact] - public void When_checking_subtree_with_an_invalid_expected_string_it_should_provide_a_clear_error_message() - { - // Arrange - var actualJson = JToken.Parse("{ \"id\": null }"); - var invalidSubtree = "{ invalid JSON }"; + [Fact] + public void When_checking_subtree_with_an_invalid_expected_string_it_should_provide_a_clear_error_message() + { + // Arrange + var actualJson = JToken.Parse("{ \"id\": null }"); + var invalidSubtree = "{ invalid JSON }"; + + // Act & Assert + actualJson.Should().Invoking(x => x.ContainSubtree(invalidSubtree)) + .Should().Throw() + .WithMessage($"Unable to parse expected JSON string:{invalidSubtree}*") + .WithInnerException(); + } - // Act & Assert - actualJson.Should().Invoking(x => x.ContainSubtree(invalidSubtree)) - .Should().Throw() - .WithMessage($"Unable to parse expected JSON string:{invalidSubtree}*") - .WithInnerException(); - } + #endregion - #endregion + private static string Format(JToken value, bool useLineBreaks = false) + { + var output = new FormattedObjectGraph(100); - private static string Format(JToken value, bool useLineBreaks = false) + new JTokenFormatter().Format(value, output, new FormattingContext { - var output = new FormattedObjectGraph(100); - - new JTokenFormatter().Format(value, output, new FormattingContext - { - UseLineBreaks = useLineBreaks - }, null); + UseLineBreaks = useLineBreaks + }, null); - return output.ToString(); - } + return output.ToString(); } } diff --git a/Tests/FluentAssertions.Json.Specs/JTokenComparerSpecs.cs b/Tests/FluentAssertions.Json.Specs/JTokenComparerSpecs.cs index ab3c086d..45e9b34a 100644 --- a/Tests/FluentAssertions.Json.Specs/JTokenComparerSpecs.cs +++ b/Tests/FluentAssertions.Json.Specs/JTokenComparerSpecs.cs @@ -3,156 +3,155 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace FluentAssertions.Json.Specs +namespace FluentAssertions.Json.Specs; + +public class JTokenComparerSpecs { - public class JTokenComparerSpecs + private static readonly IComparer Comparer = + Type.GetType("FluentAssertions.Json.Common.JTokenExtensions, FluentAssertions.Json")! + .GetField("Comparer", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic)! + .GetValue(null) as IComparer; + + [Fact] + public void Should_return_zero_for_same_reference() + { + // Arrange + var token = JToken.Parse(@"{""a"":1}"); + + // Act & Assert + Comparer.Compare(token, token).Should().Be(0); + } + + [Fact] + public void Should_handle_nulls() + { + // Arrange + var token = JToken.Parse("1"); + + // Act & Assert + Comparer.Compare(null, token).Should().Be(-1); + Comparer.Compare(token, null).Should().Be(1); + Comparer.Compare(null, null).Should().Be(0); + } + + [Fact] + public void Should_compare_different_types() + { + // Arrange + var obj = JToken.Parse(@"{""a"":1}"); + var arr = JToken.Parse("[1]"); + + // Act & Assert + Comparer.Compare(obj, arr).Should().NotBe(0); + } + + [Fact] + public void Should_compare_jvalues() + { + // Arrange + var v1 = new JValue(1); + var v2 = new JValue(2); + + // Act & Assert + Comparer.Compare(v1, v2).Should().Be(-1); + Comparer.Compare(v2, v1).Should().Be(1); + Comparer.Compare(v1, new JValue(1)).Should().Be(0); + } + + [Fact] + public void Should_compare_jarrays_by_count_and_elements() + { + // Arrange + var arr1 = JArray.Parse("[1,2]"); + var arr2 = JArray.Parse("[1,2,3]"); + var arr3 = JArray.Parse("[1,3]"); + var arr4 = JArray.Parse("[1,2,3]"); + + // Act & Assert + Comparer.Compare(arr1, arr2).Should().Be(-1); + Comparer.Compare(arr1, arr3).Should().Be(-1); + Comparer.Compare(arr3, arr1).Should().Be(1); + Comparer.Compare(arr2, arr4).Should().Be(0); + } + + [Fact] + public void Should_compare_jobjects_by_count_and_properties() { - private static readonly IComparer Comparer = - Type.GetType("FluentAssertions.Json.Common.JTokenExtensions, FluentAssertions.Json")! - .GetField("Comparer", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic)! - .GetValue(null) as IComparer; - - [Fact] - public void Should_return_zero_for_same_reference() - { - // Arrange - var token = JToken.Parse(@"{""a"":1}"); - - // Act & Assert - Comparer.Compare(token, token).Should().Be(0); - } - - [Fact] - public void Should_handle_nulls() - { - // Arrange - var token = JToken.Parse("1"); - - // Act & Assert - Comparer.Compare(null, token).Should().Be(-1); - Comparer.Compare(token, null).Should().Be(1); - Comparer.Compare(null, null).Should().Be(0); - } - - [Fact] - public void Should_compare_different_types() - { - // Arrange - var obj = JToken.Parse(@"{""a"":1}"); - var arr = JToken.Parse("[1]"); - - // Act & Assert - Comparer.Compare(obj, arr).Should().NotBe(0); - } - - [Fact] - public void Should_compare_jvalues() - { - // Arrange - var v1 = new JValue(1); - var v2 = new JValue(2); - - // Act & Assert - Comparer.Compare(v1, v2).Should().Be(-1); - Comparer.Compare(v2, v1).Should().Be(1); - Comparer.Compare(v1, new JValue(1)).Should().Be(0); - } - - [Fact] - public void Should_compare_jarrays_by_count_and_elements() - { - // Arrange - var arr1 = JArray.Parse("[1,2]"); - var arr2 = JArray.Parse("[1,2,3]"); - var arr3 = JArray.Parse("[1,3]"); - var arr4 = JArray.Parse("[1,2,3]"); - - // Act & Assert - Comparer.Compare(arr1, arr2).Should().Be(-1); - Comparer.Compare(arr1, arr3).Should().Be(-1); - Comparer.Compare(arr3, arr1).Should().Be(1); - Comparer.Compare(arr2, arr4).Should().Be(0); - } - - [Fact] - public void Should_compare_jobjects_by_count_and_properties() - { - // Arrange - var obj1 = JObject.Parse(@"{""a"":1}"); - var obj2 = JObject.Parse(@"{""a"":1,""b"":2}"); - var obj3 = JObject.Parse(@"{""a"":2}"); - var obj4 = JObject.Parse(@"{""a"":1,""b"":2}"); - var obj5 = JObject.Parse(@"{""b"":2}"); - - // Act & Assert - Comparer.Compare(obj1, obj2).Should().Be(-1); - Comparer.Compare(obj1, obj3).Should().Be(-1); - Comparer.Compare(obj3, obj1).Should().Be(1); - Comparer.Compare(obj2, obj4).Should().Be(0); - Comparer.Compare(obj1, obj5).Should().Be(-1); - Comparer.Compare(obj5, obj1).Should().Be(1); - } - - [Fact] - public void Should_compare_jproperties_by_name_and_value() - { - // Arrange - var prop1 = new JProperty("a", 1); - var prop2 = new JProperty("b", 1); - var prop3 = new JProperty("a", 2); - var prop4 = new JProperty("a", 1); - - // Act & Assert - Comparer.Compare(prop1, prop2).Should().Be(-1); - Comparer.Compare(prop1, prop3).Should().Be(-1); - Comparer.Compare(prop3, prop1).Should().Be(1); - Comparer.Compare(prop4, prop1).Should().Be(0); - Comparer.Compare(prop2, prop3).Should().Be(1); - Comparer.Compare(prop3, prop2).Should().Be(-1); - } - - [Fact] - public void Should_compare_jconstructors_by_name() - { - // Arrange - var ctor1 = new JConstructor("foo", new JValue(1)); - var ctor2 = new JConstructor("bar", new JValue(1)); - - // Act & Assert - Comparer.Compare(ctor1, ctor2).Should().BeGreaterThan(0); // "foo" > "bar" - } - - [Fact] - public void Should_compare_jconstructors_by_argument_count() - { - // Arrange - var ctor1 = new JConstructor("foo", new JValue(1)); - var ctor2 = new JConstructor("foo", new JValue(1), new JValue(2)); - - // Act & Assert - Comparer.Compare(ctor1, ctor2).Should().Be(-1); - } - - [Fact] - public void Should_compare_jconstructors_by_argument_values() - { - // Arrange - var ctor1 = new JConstructor("foo", new JValue(1), new JValue(2)); - var ctor2 = new JConstructor("foo", new JValue(1), new JValue(3)); - - // Act & Assert - Comparer.Compare(ctor1, ctor2).Should().Be(-1); - } - - [Fact] - public void Should_return_zero_for_equal_jconstructors() - { - // Arrange - var ctor1 = new JConstructor("foo", new JValue(1), new JValue(2)); - var ctor2 = new JConstructor("foo", new JValue(1), new JValue(2)); - - // Act & Assert - Comparer.Compare(ctor1, ctor2).Should().Be(0); - } + // Arrange + var obj1 = JObject.Parse(@"{""a"":1}"); + var obj2 = JObject.Parse(@"{""a"":1,""b"":2}"); + var obj3 = JObject.Parse(@"{""a"":2}"); + var obj4 = JObject.Parse(@"{""a"":1,""b"":2}"); + var obj5 = JObject.Parse(@"{""b"":2}"); + + // Act & Assert + Comparer.Compare(obj1, obj2).Should().Be(-1); + Comparer.Compare(obj1, obj3).Should().Be(-1); + Comparer.Compare(obj3, obj1).Should().Be(1); + Comparer.Compare(obj2, obj4).Should().Be(0); + Comparer.Compare(obj1, obj5).Should().Be(-1); + Comparer.Compare(obj5, obj1).Should().Be(1); + } + + [Fact] + public void Should_compare_jproperties_by_name_and_value() + { + // Arrange + var prop1 = new JProperty("a", 1); + var prop2 = new JProperty("b", 1); + var prop3 = new JProperty("a", 2); + var prop4 = new JProperty("a", 1); + + // Act & Assert + Comparer.Compare(prop1, prop2).Should().Be(-1); + Comparer.Compare(prop1, prop3).Should().Be(-1); + Comparer.Compare(prop3, prop1).Should().Be(1); + Comparer.Compare(prop4, prop1).Should().Be(0); + Comparer.Compare(prop2, prop3).Should().Be(1); + Comparer.Compare(prop3, prop2).Should().Be(-1); + } + + [Fact] + public void Should_compare_jconstructors_by_name() + { + // Arrange + var ctor1 = new JConstructor("foo", new JValue(1)); + var ctor2 = new JConstructor("bar", new JValue(1)); + + // Act & Assert + Comparer.Compare(ctor1, ctor2).Should().BeGreaterThan(0); // "foo" > "bar" + } + + [Fact] + public void Should_compare_jconstructors_by_argument_count() + { + // Arrange + var ctor1 = new JConstructor("foo", new JValue(1)); + var ctor2 = new JConstructor("foo", new JValue(1), new JValue(2)); + + // Act & Assert + Comparer.Compare(ctor1, ctor2).Should().Be(-1); + } + + [Fact] + public void Should_compare_jconstructors_by_argument_values() + { + // Arrange + var ctor1 = new JConstructor("foo", new JValue(1), new JValue(2)); + var ctor2 = new JConstructor("foo", new JValue(1), new JValue(3)); + + // Act & Assert + Comparer.Compare(ctor1, ctor2).Should().Be(-1); + } + + [Fact] + public void Should_return_zero_for_equal_jconstructors() + { + // Arrange + var ctor1 = new JConstructor("foo", new JValue(1), new JValue(2)); + var ctor2 = new JConstructor("foo", new JValue(1), new JValue(2)); + + // Act & Assert + Comparer.Compare(ctor1, ctor2).Should().Be(0); } } diff --git a/Tests/FluentAssertions.Json.Specs/JTokenFormatterSpecs.cs b/Tests/FluentAssertions.Json.Specs/JTokenFormatterSpecs.cs index d2343a6b..69ce6bbf 100644 --- a/Tests/FluentAssertions.Json.Specs/JTokenFormatterSpecs.cs +++ b/Tests/FluentAssertions.Json.Specs/JTokenFormatterSpecs.cs @@ -2,57 +2,56 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace FluentAssertions.Json.Specs +namespace FluentAssertions.Json.Specs; + +// ReSharper disable InconsistentNaming +public class JTokenFormatterSpecs { - // ReSharper disable InconsistentNaming - public class JTokenFormatterSpecs + public JTokenFormatterSpecs() { - public JTokenFormatterSpecs() - { - Formatter.AddFormatter(new JTokenFormatter()); - } + Formatter.AddFormatter(new JTokenFormatter()); + } - [Fact] - public void Should_Handle_JToken() - { - // Act / Arrange - var actual = Formatter.ToString(JToken.Parse("{}")); + [Fact] + public void Should_Handle_JToken() + { + // Act / Arrange + var actual = Formatter.ToString(JToken.Parse("{}")); - // Assert - actual.Should().Be("{}"); - } + // Assert + actual.Should().Be("{}"); + } - [Fact] - public void Should_preserve_indenting() + [Fact] + public void Should_preserve_indenting() + { + // Arrange + var json = JToken.Parse("{ \"id\":1 }"); + + // Act + var actual = Formatter.ToString(json, new FormattingOptions { - // Arrange - var json = JToken.Parse("{ \"id\":1 }"); + UseLineBreaks = true + }); - // Act - var actual = Formatter.ToString(json, new FormattingOptions - { - UseLineBreaks = true - }); + // Assert + actual.Should().Be(json.ToString(Newtonsoft.Json.Formatting.Indented)); + } - // Assert - actual.Should().Be(json.ToString(Newtonsoft.Json.Formatting.Indented)); - } + [Fact] + public void Should_Remove_line_breaks_and_indenting() + { + // Arrange + var json = JToken.Parse("{ \"id\":1 }"); - [Fact] - public void Should_Remove_line_breaks_and_indenting() + // Act + // ReSharper disable once RedundantArgumentDefaultValue + var actual = Formatter.ToString(json, new FormattingOptions { - // Arrange - var json = JToken.Parse("{ \"id\":1 }"); - - // Act - // ReSharper disable once RedundantArgumentDefaultValue - var actual = Formatter.ToString(json, new FormattingOptions - { - UseLineBreaks = false - }); - - // Assert - actual.Should().Be(json.ToString().RemoveNewLines()); - } + UseLineBreaks = false + }); + + // Assert + actual.Should().Be(json.ToString().RemoveNewLines()); } } diff --git a/Tests/FluentAssertions.Json.Specs/JsonAssertionExtensionsSpecs.cs b/Tests/FluentAssertions.Json.Specs/JsonAssertionExtensionsSpecs.cs index fbe44f2a..f5039199 100644 --- a/Tests/FluentAssertions.Json.Specs/JsonAssertionExtensionsSpecs.cs +++ b/Tests/FluentAssertions.Json.Specs/JsonAssertionExtensionsSpecs.cs @@ -4,31 +4,30 @@ using Xunit; // NOTE that we are using both namespaces 'FluentAssertions' & 'FluentAssertions.Json' from an external namespace to force compiler disambiguation warnings -namespace SomeOtherNamespace +namespace SomeOtherNamespace; + +// ReSharper disable InconsistentNaming +public class JsonAssertionExtensionsSpecs { - // ReSharper disable InconsistentNaming - public class JsonAssertionExtensionsSpecs + [Fact] + public void Should_Provide_Unambiguos_JTokenAssertions() { - [Fact] - public void Should_Provide_Unambiguos_JTokenAssertions() + // Arrange + var assertions = new[] { - // Arrange - var assertions = new[] - { - JToken.Parse("{\"token\":\"value\"}").Should() - , new JProperty("property","value").Should() - , new JObject(new JProperty("object", "value")).Should() - , new JArray(new[] { 42, 43 }).Should() - , new JConstructor("property","value").Should() - , new JValue("value").Should() - , new JRaw("value").Should() - }; + JToken.Parse("{\"token\":\"value\"}").Should() + , new JProperty("property","value").Should() + , new JObject(new JProperty("object", "value")).Should() + , new JArray(new[] { 42, 43 }).Should() + , new JConstructor("property","value").Should() + , new JValue("value").Should() + , new JRaw("value").Should() + }; - // Act & Assert - foreach (var sut in assertions) - { - ((object)sut).Should().BeOfType("extensions should provide assertions for all JSon primitives, i.e. JObject, JToken and JProperty"); - } + // Act & Assert + foreach (var sut in assertions) + { + ((object)sut).Should().BeOfType("extensions should provide assertions for all JSon primitives, i.e. JObject, JToken and JProperty"); } } } diff --git a/Tests/FluentAssertions.Json.Specs/JsonAssertionOptionsSpecs.cs b/Tests/FluentAssertions.Json.Specs/JsonAssertionOptionsSpecs.cs index b54427af..2a5987de 100644 --- a/Tests/FluentAssertions.Json.Specs/JsonAssertionOptionsSpecs.cs +++ b/Tests/FluentAssertions.Json.Specs/JsonAssertionOptionsSpecs.cs @@ -3,41 +3,40 @@ using Newtonsoft.Json.Linq; using Xunit; -namespace FluentAssertions.Json.Specs +namespace FluentAssertions.Json.Specs; + +[Collection("AssertionOptionsSpecs")] +public class JsonAssertionOptionsSpecs { - [Collection("AssertionOptionsSpecs")] - public class JsonAssertionOptionsSpecs + [Fact] + public void Local_equivalency_options_are_applied_on_top_of_global_equivalency_options() { - [Fact] - public void Local_equivalency_options_are_applied_on_top_of_global_equivalency_options() - { - using var assertionOptions = new TempDefaultAssertionOptions(e => e - .Using(ctx => ctx.Subject.Should().BeApproximately(ctx.Expectation, 0.1)) - .WhenTypeIs()); + using var assertionOptions = new TempDefaultAssertionOptions(e => e + .Using(ctx => ctx.Subject.Should().BeApproximately(ctx.Expectation, 0.1)) + .WhenTypeIs()); - // Arrange - var actual = JToken.Parse("{ \"id\": 1.1232 }"); - var expected = JToken.Parse("{ \"id\": 1.1235 }"); + // Arrange + var actual = JToken.Parse("{ \"id\": 1.1232 }"); + var expected = JToken.Parse("{ \"id\": 1.1235 }"); - // Act & Assert - actual.Should().BeEquivalentTo(expected, options => options); - } + // Act & Assert + actual.Should().BeEquivalentTo(expected, options => options); + } - private sealed class TempDefaultAssertionOptions : IDisposable + private sealed class TempDefaultAssertionOptions : IDisposable + { + public TempDefaultAssertionOptions(Func config) { - public TempDefaultAssertionOptions(Func config) - { - AssertionConfiguration.Current.Equivalency.Modify(config); - } + AssertionConfiguration.Current.Equivalency.Modify(config); + } - public void Dispose() - { - AssertionConfiguration.Current.Equivalency.Modify(_ => new()); - } + public void Dispose() + { + AssertionConfiguration.Current.Equivalency.Modify(_ => new()); } } - - // Due to tests that call AssertionOptions - [CollectionDefinition("AssertionOptionsSpecs", DisableParallelization = true)] - public class AssertionOptionsSpecsDefinition; } + +// Due to tests that call AssertionOptions +[CollectionDefinition("AssertionOptionsSpecs", DisableParallelization = true)] +public class AssertionOptionsSpecsDefinition; diff --git a/Tests/FluentAssertions.Json.Specs/Models/AddressDto.cs b/Tests/FluentAssertions.Json.Specs/Models/AddressDto.cs index b99b0d91..799186dc 100644 --- a/Tests/FluentAssertions.Json.Specs/Models/AddressDto.cs +++ b/Tests/FluentAssertions.Json.Specs/Models/AddressDto.cs @@ -1,23 +1,22 @@ using System; using Newtonsoft.Json; -namespace FluentAssertions.Json.Specs.Models -{ - // ReSharper disable UnusedMember.Global - public class AddressDto - { - public string AddressLine1 { get; set; } +namespace FluentAssertions.Json.Specs.Models; - public string AddressLine2 { get; set; } +// ReSharper disable UnusedMember.Global +public class AddressDto +{ + public string AddressLine1 { get; set; } - public string AddressLine3 { get; set; } - } + public string AddressLine2 { get; set; } - public class DerivedFromAddressDto : AddressDto - { - [JsonIgnore] - public DateTime LastUpdated { get; set; } - } + public string AddressLine3 { get; set; } +} - // ReSharper restore UnusedMember.Global +public class DerivedFromAddressDto : AddressDto +{ + [JsonIgnore] + public DateTime LastUpdated { get; set; } } + +// ReSharper restore UnusedMember.Global diff --git a/Tests/FluentAssertions.Json.Specs/Models/EmploymentDto.cs b/Tests/FluentAssertions.Json.Specs/Models/EmploymentDto.cs index b103e6fd..cd4f013a 100644 --- a/Tests/FluentAssertions.Json.Specs/Models/EmploymentDto.cs +++ b/Tests/FluentAssertions.Json.Specs/Models/EmploymentDto.cs @@ -1,12 +1,11 @@ -namespace FluentAssertions.Json.Specs.Models -{ - // ReSharper disable UnusedMember.Global - public class EmploymentDto - { - public string JobTitle { get; set; } +namespace FluentAssertions.Json.Specs.Models; - public string PhoneNumber { get; set; } - } +// ReSharper disable UnusedMember.Global +public class EmploymentDto +{ + public string JobTitle { get; set; } - // ReSharper restore UnusedMember.Global + public string PhoneNumber { get; set; } } + +// ReSharper restore UnusedMember.Global diff --git a/Tests/FluentAssertions.Json.Specs/Models/PocoWithIgnoredProperty.cs b/Tests/FluentAssertions.Json.Specs/Models/PocoWithIgnoredProperty.cs index 6aa5eafd..b368f50f 100644 --- a/Tests/FluentAssertions.Json.Specs/Models/PocoWithIgnoredProperty.cs +++ b/Tests/FluentAssertions.Json.Specs/Models/PocoWithIgnoredProperty.cs @@ -1,12 +1,11 @@ using Newtonsoft.Json; -namespace FluentAssertions.Json.Specs.Models +namespace FluentAssertions.Json.Specs.Models; + +public class PocoWithIgnoredProperty { - public class PocoWithIgnoredProperty - { - public int Id { get; set; } + public int Id { get; set; } - [JsonIgnore] - public string Name { get; set; } - } + [JsonIgnore] + public string Name { get; set; } } diff --git a/Tests/FluentAssertions.Json.Specs/Models/PocoWithNoDefaultConstructor.cs b/Tests/FluentAssertions.Json.Specs/Models/PocoWithNoDefaultConstructor.cs index 07b65ac6..0910a461 100644 --- a/Tests/FluentAssertions.Json.Specs/Models/PocoWithNoDefaultConstructor.cs +++ b/Tests/FluentAssertions.Json.Specs/Models/PocoWithNoDefaultConstructor.cs @@ -1,16 +1,15 @@ -namespace FluentAssertions.Json.Specs.Models +namespace FluentAssertions.Json.Specs.Models; + +public class PocoWithNoDefaultConstructor { - public class PocoWithNoDefaultConstructor - { - public int Id { get; } + public int Id { get; } - /// - /// Newtonsoft.Json will deserialise this successfully if the parameter name id the same as the property - /// - /// DO NOT CHANGE THE NAME OF THIS PARAMETER - public PocoWithNoDefaultConstructor(int value) - { - Id = value; - } + /// + /// Newtonsoft.Json will deserialise this successfully if the parameter name id the same as the property + /// + /// DO NOT CHANGE THE NAME OF THIS PARAMETER + public PocoWithNoDefaultConstructor(int value) + { + Id = value; } } diff --git a/Tests/FluentAssertions.Json.Specs/Models/PocoWithStructure.cs b/Tests/FluentAssertions.Json.Specs/Models/PocoWithStructure.cs index 5724095c..d9bd24c8 100644 --- a/Tests/FluentAssertions.Json.Specs/Models/PocoWithStructure.cs +++ b/Tests/FluentAssertions.Json.Specs/Models/PocoWithStructure.cs @@ -1,14 +1,13 @@ -namespace FluentAssertions.Json.Specs.Models -{ - // ReSharper disable UnusedMember.Global - public class PocoWithStructure - { - public int Id { get; set; } +namespace FluentAssertions.Json.Specs.Models; - public AddressDto Address { get; set; } +// ReSharper disable UnusedMember.Global +public class PocoWithStructure +{ + public int Id { get; set; } - public EmploymentDto Employment { get; set; } - } + public AddressDto Address { get; set; } - // ReSharper restore UnusedMember.Global + public EmploymentDto Employment { get; set; } } + +// ReSharper restore UnusedMember.Global diff --git a/Tests/FluentAssertions.Json.Specs/Models/SimplePocoWithPrimitiveTypes.cs b/Tests/FluentAssertions.Json.Specs/Models/SimplePocoWithPrimitiveTypes.cs index 230f2d0d..c43fd39d 100644 --- a/Tests/FluentAssertions.Json.Specs/Models/SimplePocoWithPrimitiveTypes.cs +++ b/Tests/FluentAssertions.Json.Specs/Models/SimplePocoWithPrimitiveTypes.cs @@ -1,32 +1,31 @@ using System; -namespace FluentAssertions.Json.Specs.Models +namespace FluentAssertions.Json.Specs.Models; + +// ReSharper disable UnusedMember.Global +public class SimplePocoWithPrimitiveTypes { - // ReSharper disable UnusedMember.Global - public class SimplePocoWithPrimitiveTypes - { - public int Id { get; set; } + public int Id { get; set; } - public Guid GlobalId { get; set; } + public Guid GlobalId { get; set; } - public string Name { get; set; } + public string Name { get; set; } - public DateTime DateOfBirth { get; set; } + public DateTime DateOfBirth { get; set; } - public decimal Height { get; set; } + public decimal Height { get; set; } - public double Weight { get; set; } + public double Weight { get; set; } - public float ShoeSize { get; set; } + public float ShoeSize { get; set; } - public bool IsActive { get; set; } + public bool IsActive { get; set; } #pragma warning disable CA1819 // Properties should not return arrays - public byte[] Image { get; set; } + public byte[] Image { get; set; } #pragma warning restore CA1819 // Properties should not return arrays - public char Category { get; set; } - } - - // ReSharper restore UnusedMember.Global + public char Category { get; set; } } + +// ReSharper restore UnusedMember.Global diff --git a/Tests/FluentAssertions.Json.Specs/ShouldBeJsonSerializableTests.cs b/Tests/FluentAssertions.Json.Specs/ShouldBeJsonSerializableTests.cs index b433542e..35279b58 100644 --- a/Tests/FluentAssertions.Json.Specs/ShouldBeJsonSerializableTests.cs +++ b/Tests/FluentAssertions.Json.Specs/ShouldBeJsonSerializableTests.cs @@ -6,170 +6,169 @@ // NOTE that we are using both namespaces 'FluentAssertions' & 'FluentAssertions.Json' from an external namespace to force compiler disambiguation warnings // ReSharper disable CheckNamespace -namespace SomeOtherNamespace +namespace SomeOtherNamespace; + +// ReSharper restore CheckNamespace +public class ShouldBeJsonSerializableTests { - // ReSharper restore CheckNamespace - public class ShouldBeJsonSerializableTests + [Fact] + public void Simple_poco_should_be_serializable() { - [Fact] - public void Simple_poco_should_be_serializable() - { - // arrange - var target = new SimplePocoWithPrimitiveTypes - { - Id = 1, - GlobalId = Guid.NewGuid(), - Name = "Name", - DateOfBirth = DateTime.UtcNow, - Height = 1, - Weight = 1, - ShoeSize = 1, - IsActive = true, - Image = [1], - Category = '1' - }; - - // act - Action act = () => target.Should().BeJsonSerializable(); - - // assert - act.Should().NotThrow(); - } - - [Fact] - public void Complex_poco_should_be_serializable() - { - // arrange - var target = new PocoWithStructure - { - Address = new AddressDto - { - AddressLine1 = "AddressLine1", - AddressLine2 = "AddressLine2", - AddressLine3 = "AddressLine3", - }, - Employment = new EmploymentDto - { - JobTitle = "JobTitle", - PhoneNumber = "PhoneNumber", - }, - Id = 1, - }; - - // act - Action act = () => target.Should().BeJsonSerializable(); - - // assert - act.Should().NotThrow(); - } - - [Fact] - public void Class_that_does_not_have_default_constructor_should_not_be_serializable() + // arrange + var target = new SimplePocoWithPrimitiveTypes { - // arrange - const string reasonText = "this is the reason"; - var target = new PocoWithNoDefaultConstructor(1); - - // act - Action act = () => target.Should().BeJsonSerializable(reasonText); - - // assert - act.Should().Throw() - .Which.Message.Should() - .Contain("to be JSON serializable") - .And.Contain(reasonText) - .And.Contain("but serializing") - .And.Contain("failed with"); - } - - [Fact] - public void Class_that_has_ignored_property_should_not_be_serializable_if_equivalency_options_are_not_configured() + Id = 1, + GlobalId = Guid.NewGuid(), + Name = "Name", + DateOfBirth = DateTime.UtcNow, + Height = 1, + Weight = 1, + ShoeSize = 1, + IsActive = true, + Image = [1], + Category = '1' + }; + + // act + Action act = () => target.Should().BeJsonSerializable(); + + // assert + act.Should().NotThrow(); + } + + [Fact] + public void Complex_poco_should_be_serializable() + { + // arrange + var target = new PocoWithStructure { - // arrange - const string reasonText = "this is the reason"; - var target = new PocoWithIgnoredProperty + Address = new AddressDto { - Id = 1, - Name = "Name", - }; - - // act - Action act = () => target.Should().BeJsonSerializable(reasonText); - - // assert - act.Should().Throw() - .Which.Message.Should() - .Contain("to be JSON serializable") - .And.Contain(reasonText) - .And.Contain("but serializing") - .And.Contain("failed with"); - } - - [Fact] - public void Class_that_has_ignored_property_should_be_serializable_when_equivalency_options_are_configured() - { - // arrange - var target = new PocoWithIgnoredProperty + AddressLine1 = "AddressLine1", + AddressLine2 = "AddressLine2", + AddressLine3 = "AddressLine3", + }, + Employment = new EmploymentDto { - Id = 1, - Name = "Name", - }; + JobTitle = "JobTitle", + PhoneNumber = "PhoneNumber", + }, + Id = 1, + }; - // act - Action act = () => target.Should().BeJsonSerializable(opts => opts.Excluding(p => p.Name)); + // act + Action act = () => target.Should().BeJsonSerializable(); - // assert - act.Should().NotThrow(); - } + // assert + act.Should().NotThrow(); + } - [Fact] - public void Should_fail_when_instance_is_null() + [Fact] + public void Class_that_does_not_have_default_constructor_should_not_be_serializable() + { + // arrange + const string reasonText = "this is the reason"; + var target = new PocoWithNoDefaultConstructor(1); + + // act + Action act = () => target.Should().BeJsonSerializable(reasonText); + + // assert + act.Should().Throw() + .Which.Message.Should() + .Contain("to be JSON serializable") + .And.Contain(reasonText) + .And.Contain("but serializing") + .And.Contain("failed with"); + } + + [Fact] + public void Class_that_has_ignored_property_should_not_be_serializable_if_equivalency_options_are_not_configured() + { + // arrange + const string reasonText = "this is the reason"; + var target = new PocoWithIgnoredProperty { - // arrange - const SimplePocoWithPrimitiveTypes target = null; + Id = 1, + Name = "Name", + }; + + // act + Action act = () => target.Should().BeJsonSerializable(reasonText); + + // assert + act.Should().Throw() + .Which.Message.Should() + .Contain("to be JSON serializable") + .And.Contain(reasonText) + .And.Contain("but serializing") + .And.Contain("failed with"); + } - // act - Action act = () => target.Should().BeJsonSerializable(); + [Fact] + public void Class_that_has_ignored_property_should_be_serializable_when_equivalency_options_are_configured() + { + // arrange + var target = new PocoWithIgnoredProperty + { + Id = 1, + Name = "Name", + }; - // assert - act.Should() - .Throw(because: "This is consistent with BeBinarySerializable() and BeDataContractSerializable()") - .WithMessage("*value is null*Please provide a value for the assertion*"); - } + // act + Action act = () => target.Should().BeJsonSerializable(opts => opts.Excluding(p => p.Name)); - [Fact] - public void Should_fail_when_subject_is_not_same_type_as_the_specified_generic_type() - { - // arrange - var target = new AddressDto(); + // assert + act.Should().NotThrow(); + } - // act - Action act = () => target.Should().BeJsonSerializable(); + [Fact] + public void Should_fail_when_instance_is_null() + { + // arrange + const SimplePocoWithPrimitiveTypes target = null; - // assert - act.Should().Throw(because: "This is consistent with BeBinarySerializable() and BeDataContractSerializable()") - .Which.Message - .Should().Contain("is not assignable to") - .And.Contain(nameof(SimplePocoWithPrimitiveTypes)); - } + // act + Action act = () => target.Should().BeJsonSerializable(); + + // assert + act.Should() + .Throw(because: "This is consistent with BeBinarySerializable() and BeDataContractSerializable()") + .WithMessage("*value is null*Please provide a value for the assertion*"); + } + + [Fact] + public void Should_fail_when_subject_is_not_same_type_as_the_specified_generic_type() + { + // arrange + var target = new AddressDto(); - [Fact] - public void Should_fail_when_derived_type_is_not_serializable_when_presented_as_base_class() + // act + Action act = () => target.Should().BeJsonSerializable(); + + // assert + act.Should().Throw(because: "This is consistent with BeBinarySerializable() and BeDataContractSerializable()") + .Which.Message + .Should().Contain("is not assignable to") + .And.Contain(nameof(SimplePocoWithPrimitiveTypes)); + } + + [Fact] + public void Should_fail_when_derived_type_is_not_serializable_when_presented_as_base_class() + { + // arrange + AddressDto target = new DerivedFromAddressDto { - // arrange - AddressDto target = new DerivedFromAddressDto - { - AddressLine1 = "AddressLine1", - AddressLine2 = "AddressLine2", - AddressLine3 = "AddressLine3", - LastUpdated = DateTime.UtcNow, - }; + AddressLine1 = "AddressLine1", + AddressLine2 = "AddressLine2", + AddressLine3 = "AddressLine3", + LastUpdated = DateTime.UtcNow, + }; - // act - Action act = () => target.Should().BeJsonSerializable(); + // act + Action act = () => target.Should().BeJsonSerializable(); - // assert - act.Should().Throw("The derived class is not serializable due to a JsonIgnore attribute"); - } + // assert + act.Should().Throw("The derived class is not serializable due to a JsonIgnore attribute"); } } diff --git a/Tests/FluentAssertions.Json.Specs/StringAssertionsExtensionsSpecs.cs b/Tests/FluentAssertions.Json.Specs/StringAssertionsExtensionsSpecs.cs index 63e0d555..042d2c71 100644 --- a/Tests/FluentAssertions.Json.Specs/StringAssertionsExtensionsSpecs.cs +++ b/Tests/FluentAssertions.Json.Specs/StringAssertionsExtensionsSpecs.cs @@ -3,71 +3,70 @@ using Xunit; using Xunit.Sdk; -namespace FluentAssertions.Json.Specs +namespace FluentAssertions.Json.Specs; + +// ReSharper disable ExpressionIsAlwaysNull +public class StringAssertionsExtensionsSpecs { - // ReSharper disable ExpressionIsAlwaysNull - public class StringAssertionsExtensionsSpecs + #region BeValidJson + + [Fact] + public void When_checking_valid_json_BeValidJson_should_succeed() + { + // Arrange + string subject = "{ id: 42, admin: true }"; + + // Act + Action act = () => subject.Should().BeValidJson(); + + // Assert + act.Should().NotThrow(); + } + + [Fact] + public void When_checking_valid_json_BeValidJson_should_enable_consecutive_jtoken_assertions() + { + // Arrange + string subject = "{ id: 42 }"; + + // Act + object which = subject.Should().BeValidJson().Which; + + // Assert + which.Should().BeAssignableTo(); + } + + [Fact] + public void When_checking_null_BeValidJson_should_fail() + { + // Arrange + string subject = null; + + // Act + Action act = () => subject.Should().BeValidJson("null is not allowed"); + + // Assert + act.Should().Throw() + .Which.Message.Should() + .Match("Expected subject to be valid JSON because null is not allowed, but parsing failed with \"*\"."); + } + + [Fact] + public void When_checking_invalid_json_BeValidJson_should_fail() { - #region BeValidJson - - [Fact] - public void When_checking_valid_json_BeValidJson_should_succeed() - { - // Arrange - string subject = "{ id: 42, admin: true }"; - - // Act - Action act = () => subject.Should().BeValidJson(); - - // Assert - act.Should().NotThrow(); - } - - [Fact] - public void When_checking_valid_json_BeValidJson_should_enable_consecutive_jtoken_assertions() - { - // Arrange - string subject = "{ id: 42 }"; - - // Act - object which = subject.Should().BeValidJson().Which; - - // Assert - which.Should().BeAssignableTo(); - } - - [Fact] - public void When_checking_null_BeValidJson_should_fail() - { - // Arrange - string subject = null; - - // Act - Action act = () => subject.Should().BeValidJson("null is not allowed"); - - // Assert - act.Should().Throw() - .Which.Message.Should() - .Match("Expected subject to be valid JSON because null is not allowed, but parsing failed with \"*\"."); - } - - [Fact] - public void When_checking_invalid_json_BeValidJson_should_fail() - { - // Arrange - string subject = "invalid json"; - - // Act - Action act = () => subject.Should().BeValidJson("we like {0}", "JSON"); - - // Assert - act.Should() - .Throw() - .Which.Message.Should() - .Match("Expected subject to be valid JSON because we like JSON, but parsing failed with \"*\"."); - } - - #endregion + // Arrange + string subject = "invalid json"; + + // Act + Action act = () => subject.Should().BeValidJson("we like {0}", "JSON"); + // Assert + act.Should() + .Throw() + .Which.Message.Should() + .Match("Expected subject to be valid JSON because we like JSON, but parsing failed with \"*\"."); } + + #endregion + } diff --git a/Tests/FluentAssertions.Json.Specs/StringExtensions.cs b/Tests/FluentAssertions.Json.Specs/StringExtensions.cs index c681a3ae..20729a8b 100644 --- a/Tests/FluentAssertions.Json.Specs/StringExtensions.cs +++ b/Tests/FluentAssertions.Json.Specs/StringExtensions.cs @@ -1,10 +1,9 @@ -namespace FluentAssertions.Json.Specs +namespace FluentAssertions.Json.Specs; + +internal static class StringExtensions { - internal static class StringExtensions + public static string RemoveNewLines(this string @this) { - public static string RemoveNewLines(this string @this) - { - return @this.Replace("\n", "").Replace("\r", "").Replace("\\r\\n", ""); - } + return @this.Replace("\n", "").Replace("\r", "").Replace("\\r\\n", ""); } } diff --git a/Tests/FluentAssertions.Json.Specs/WithoutStrictOrderingSpecs.cs b/Tests/FluentAssertions.Json.Specs/WithoutStrictOrderingSpecs.cs index 564ddd19..10b4cab8 100644 --- a/Tests/FluentAssertions.Json.Specs/WithoutStrictOrderingSpecs.cs +++ b/Tests/FluentAssertions.Json.Specs/WithoutStrictOrderingSpecs.cs @@ -3,69 +3,68 @@ using Xunit; using Xunit.Sdk; -namespace FluentAssertions.Json.Specs +namespace FluentAssertions.Json.Specs; + +public class WithoutStrictOrderingSpecs { - public class WithoutStrictOrderingSpecs + [Theory] + [MemberData(nameof(When_ignoring_ordering_BeEquivalentTo_should_succeed_sample_data))] + public void When_ignoring_ordering_BeEquivalentTo_should_succeed(string subject, string expectation) { - [Theory] - [MemberData(nameof(When_ignoring_ordering_BeEquivalentTo_should_succeed_sample_data))] - public void When_ignoring_ordering_BeEquivalentTo_should_succeed(string subject, string expectation) - { - // Arrange - var subjectJToken = JToken.Parse(subject); - var expectationJToken = JToken.Parse(expectation); + // Arrange + var subjectJToken = JToken.Parse(subject); + var expectationJToken = JToken.Parse(expectation); - // Act - subjectJToken.Should().BeEquivalentTo(expectationJToken, opt => opt.WithoutStrictOrdering()); + // Act + subjectJToken.Should().BeEquivalentTo(expectationJToken, opt => opt.WithoutStrictOrdering()); - // Assert - } + // Assert + } - public static TheoryData When_ignoring_ordering_BeEquivalentTo_should_succeed_sample_data() + public static TheoryData When_ignoring_ordering_BeEquivalentTo_should_succeed_sample_data() + { + return new TheoryData { - return new TheoryData + { @"{""ids"":[1,2,3]}", @"{""ids"":[3,2,1]}" }, + { @"{""ids"":[1,2,3]}", @"{""ids"":[1,2,3]}" }, + { @"{""type"":2,""name"":""b""}", @"{""name"":""b"",""type"":2}" }, + { @"{""names"":[""a"",""b""]}", @"{""names"":[""b"",""a""]}" }, + { + @"{""vals"":[{""type"":1,""name"":""a""},{""name"":""b"",""type"":2}]}", + @"{""vals"":[{""type"":2,""name"":""b""},{""name"":""a"",""type"":1}]}" + }, { - { @"{""ids"":[1,2,3]}", @"{""ids"":[3,2,1]}" }, - { @"{""ids"":[1,2,3]}", @"{""ids"":[1,2,3]}" }, - { @"{""type"":2,""name"":""b""}", @"{""name"":""b"",""type"":2}" }, - { @"{""names"":[""a"",""b""]}", @"{""names"":[""b"",""a""]}" }, - { - @"{""vals"":[{""type"":1,""name"":""a""},{""name"":""b"",""type"":2}]}", - @"{""vals"":[{""type"":2,""name"":""b""},{""name"":""a"",""type"":1}]}" - }, - { - @"{""vals"":[{""type"":1,""name"":""a""},{""name"":""b"",""type"":2}]}", - @"{""vals"":[{""name"":""a"",""type"":1},{""type"":2,""name"":""b""}]}" - } - }; - } + @"{""vals"":[{""type"":1,""name"":""a""},{""name"":""b"",""type"":2}]}", + @"{""vals"":[{""name"":""a"",""type"":1},{""type"":2,""name"":""b""}]}" + } + }; + } - [Theory] - [MemberData(nameof(When_not_ignoring_ordering_BeEquivalentTo_should_throw_sample_data))] - public void When_not_ignoring_ordering_BeEquivalentTo_should_throw(string subject, string expectation) - { - // Arrange - var subjectJToken = JToken.Parse(subject); - var expectationJToken = JToken.Parse(expectation); + [Theory] + [MemberData(nameof(When_not_ignoring_ordering_BeEquivalentTo_should_throw_sample_data))] + public void When_not_ignoring_ordering_BeEquivalentTo_should_throw(string subject, string expectation) + { + // Arrange + var subjectJToken = JToken.Parse(subject); + var expectationJToken = JToken.Parse(expectation); - // Act - var action = new Func>(() => subjectJToken.Should().BeEquivalentTo(expectationJToken)); + // Act + var action = new Func>(() => subjectJToken.Should().BeEquivalentTo(expectationJToken)); - // Assert - action.Should().Throw(); - } + // Assert + action.Should().Throw(); + } - public static TheoryData When_not_ignoring_ordering_BeEquivalentTo_should_throw_sample_data() + public static TheoryData When_not_ignoring_ordering_BeEquivalentTo_should_throw_sample_data() + { + return new TheoryData { - return new TheoryData + { @"{""ids"":[1,2,3]}", @"{""ids"":[3,2,1]}" }, + { @"{""names"":[""a"",""b""]}", @"{""names"":[""b"",""a""]}" }, { - { @"{""ids"":[1,2,3]}", @"{""ids"":[3,2,1]}" }, - { @"{""names"":[""a"",""b""]}", @"{""names"":[""b"",""a""]}" }, - { - @"{""vals"":[{""type"":1,""name"":""a""},{""name"":""b"",""type"":2}]}", - @"{""vals"":[{""type"":2,""name"":""b""},{""name"":""a"",""type"":1}]}" - } - }; - } + @"{""vals"":[{""type"":1,""name"":""a""},{""name"":""b"",""type"":2}]}", + @"{""vals"":[{""type"":2,""name"":""b""},{""name"":""a"",""type"":1}]}" + } + }; } } From 3f0069c498663b02119cebfc415a2904207ef568 Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sun, 16 Nov 2025 12:21:38 +0100 Subject: [PATCH 18/26] Simplify JPath initialization --- Src/FluentAssertions.Json/JPath.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Src/FluentAssertions.Json/JPath.cs b/Src/FluentAssertions.Json/JPath.cs index 9dfb8bcb..3baeba27 100644 --- a/Src/FluentAssertions.Json/JPath.cs +++ b/Src/FluentAssertions.Json/JPath.cs @@ -1,20 +1,17 @@ -using System.Collections.Generic; - -namespace FluentAssertions.Json; +namespace FluentAssertions.Json; internal sealed class JPath { - private readonly List nodes = new(); + private readonly string[] nodes; public JPath() { - nodes.Add("$"); + nodes = ["$"]; } private JPath(JPath existingPath, string extraNode) { - nodes.AddRange(existingPath.nodes); - nodes.Add(extraNode); + nodes = [.. existingPath.nodes, extraNode]; } public JPath AddProperty(string name) From e98f10b99ec689b2324907e780376212932cb500 Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sun, 16 Nov 2025 12:25:28 +0100 Subject: [PATCH 19/26] Disallow unsafe blocks --- Src/FluentAssertions.Json/FluentAssertions.Json.csproj | 3 --- 1 file changed, 3 deletions(-) diff --git a/Src/FluentAssertions.Json/FluentAssertions.Json.csproj b/Src/FluentAssertions.Json/FluentAssertions.Json.csproj index 3620b34e..b0829079 100644 --- a/Src/FluentAssertions.Json/FluentAssertions.Json.csproj +++ b/Src/FluentAssertions.Json/FluentAssertions.Json.csproj @@ -29,9 +29,6 @@ Copyright 2024-$([System.DateTime]::Now.ToString(yyyy)) Xceed Software Inc., all rights reserved 9.0 - - false - From 58632762f7e11c73ef050c127e1a9972702e7a85 Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sun, 16 Nov 2025 12:28:39 +0100 Subject: [PATCH 20/26] Update analyzers --- .editorconfig | 15 +++++++ Directory.Build.props | 40 +++++++++++++++++++ FluentAssertions.Json.sln | 4 +- .../FluentAssertions.Json.csproj | 26 ------------ Tests/.editorconfig | 14 ++++++- .../FluentAssertions.Json.Specs.csproj | 25 ------------ 6 files changed, 69 insertions(+), 55 deletions(-) create mode 100644 Directory.Build.props diff --git a/.editorconfig b/.editorconfig index 63068ab4..3bf927f9 100644 --- a/.editorconfig +++ b/.editorconfig @@ -218,6 +218,11 @@ dotnet_diagnostic.SA1633.severity = none dotnet_diagnostic.SA1642.severity = suggestion # CSharpGuidelines + +# Purpose: Argument for parameter calls nested method +# Rationale: Modern debuggers allow stepping into nested calls, so no need to avoid them. +dotnet_diagnostic.AV1580.severity = none + dotnet_diagnostic.AV1561.max_parameter_count = 5 # AV1008: Class should be non-static or its name should be suffixed with Extensions dotnet_diagnostic.AV1008.severity = none @@ -262,6 +267,16 @@ dotnet_diagnostic.AV2305.severity = none # AV2407: Region should be removed dotnet_diagnostic.AV2407.severity = none +# Meziantou.Analyzer + +# Purpose: Use string.Equals instead of Equals operator +# Rationale: Does not improve readability +dotnet_diagnostic.MA0006.severity = none + +# Purpose: Regular expressions should not be vulnerable to Denial of Service attacks +# Rationale: See https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0009.md +dotnet_diagnostic.MA0009.severity = suggestion + # ReSharper/Rider # Convert lambda expression to method group resharper_convert_closure_to_method_group_highlighting=none diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 00000000..6c747a1d --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,40 @@ + + + 13.0 + true + + + + false + false + false + + + true + 9.0 + All + true + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + diff --git a/FluentAssertions.Json.sln b/FluentAssertions.Json.sln index 35f712b7..daaa4852 100644 --- a/FluentAssertions.Json.sln +++ b/FluentAssertions.Json.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.2.32210.308 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.11205.157 d18.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E5A0B454-22D4-4694-99FF-D6A8B7DE7DA3}" ProjectSection(SolutionItems) = preProject diff --git a/Src/FluentAssertions.Json/FluentAssertions.Json.csproj b/Src/FluentAssertions.Json/FluentAssertions.Json.csproj index b0829079..8ac9b519 100644 --- a/Src/FluentAssertions.Json/FluentAssertions.Json.csproj +++ b/Src/FluentAssertions.Json/FluentAssertions.Json.csproj @@ -1,5 +1,4 @@  - net47;netstandard2.0 true @@ -10,12 +9,6 @@ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb true true - true - - - false - false - false Dennis Doomen;Marcel Körtgen @@ -27,7 +20,6 @@ FluentAssertions.png See https://github.com/fluentassertions/fluentassertions.json/releases/ Copyright 2024-$([System.DateTime]::Now.ToString(yyyy)) Xceed Software Inc., all rights reserved - 9.0 @@ -42,22 +34,4 @@ - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - \ No newline at end of file diff --git a/Tests/.editorconfig b/Tests/.editorconfig index 82352038..5cc57a60 100644 --- a/Tests/.editorconfig +++ b/Tests/.editorconfig @@ -1,4 +1,14 @@ [*.cs] -# CA1825: Avoid unnecessary zero-length array allocations. Use Array.Empty() instead -dotnet_diagnostic.CA1825.severity = none \ No newline at end of file +# CA1861: Avoid constant arrays as arguments +dotnet_diagnostic.CA1861.severity = none + +# Purpose: Use an overload that has a IEqualityComparer or IComparer parameter +# Reason: Not important in tests +dotnet_diagnostic.MA0002.severity = none + +# File name must match type name. Too many purposeful violations. +dotnet_diagnostic.MA0048.severity = none + +# Mark local variable as const. +dotnet_diagnostic.RCS1118.severity = none \ No newline at end of file diff --git a/Tests/FluentAssertions.Json.Specs/FluentAssertions.Json.Specs.csproj b/Tests/FluentAssertions.Json.Specs/FluentAssertions.Json.Specs.csproj index b4482f52..348fc259 100644 --- a/Tests/FluentAssertions.Json.Specs/FluentAssertions.Json.Specs.csproj +++ b/Tests/FluentAssertions.Json.Specs/FluentAssertions.Json.Specs.csproj @@ -1,17 +1,7 @@  - false net47;net8.0 - 9.0 - true - false - - - - false - false - false @@ -32,19 +22,4 @@ - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - From 5dfb309309badcdcfcbfd4a462f5c47a1c94a7cf Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sun, 16 Nov 2025 12:28:58 +0100 Subject: [PATCH 21/26] coverlet.collector 6.0.3 -> 6.0.4 --- .../FluentAssertions.Json.Specs.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/FluentAssertions.Json.Specs/FluentAssertions.Json.Specs.csproj b/Tests/FluentAssertions.Json.Specs/FluentAssertions.Json.Specs.csproj index 348fc259..c4204c2e 100644 --- a/Tests/FluentAssertions.Json.Specs/FluentAssertions.Json.Specs.csproj +++ b/Tests/FluentAssertions.Json.Specs/FluentAssertions.Json.Specs.csproj @@ -12,7 +12,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 604dd63b51713b73be1a4c064401c232d5dd2cea Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sun, 16 Nov 2025 12:52:22 +0100 Subject: [PATCH 22/26] PublicApiGenerator 11.3.0 -> 11.5.0 --- Tests/Approval.Tests/Approval.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Approval.Tests/Approval.Tests.csproj b/Tests/Approval.Tests/Approval.Tests.csproj index f2fee17d..64967eb0 100644 --- a/Tests/Approval.Tests/Approval.Tests.csproj +++ b/Tests/Approval.Tests/Approval.Tests.csproj @@ -12,7 +12,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + From 26455a623cfdabc170596a0161201acd33638745 Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sun, 16 Nov 2025 12:52:45 +0100 Subject: [PATCH 23/26] Verify.Xunit 28.9.0 -> 31.7.1 --- Tests/Approval.Tests/Approval.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Approval.Tests/Approval.Tests.csproj b/Tests/Approval.Tests/Approval.Tests.csproj index 64967eb0..8c2ef7e8 100644 --- a/Tests/Approval.Tests/Approval.Tests.csproj +++ b/Tests/Approval.Tests/Approval.Tests.csproj @@ -14,7 +14,7 @@ - + From bceae73ebd11cda39b16eb49565d696e4fb22df1 Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sun, 16 Nov 2025 12:53:03 +0100 Subject: [PATCH 24/26] xunit.runner.visualstudio 3.0.1 -> 3.1.5 --- Tests/Approval.Tests/Approval.Tests.csproj | 2 +- .../FluentAssertions.Json.Specs.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/Approval.Tests/Approval.Tests.csproj b/Tests/Approval.Tests/Approval.Tests.csproj index 8c2ef7e8..62dbb22b 100644 --- a/Tests/Approval.Tests/Approval.Tests.csproj +++ b/Tests/Approval.Tests/Approval.Tests.csproj @@ -8,7 +8,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Tests/FluentAssertions.Json.Specs/FluentAssertions.Json.Specs.csproj b/Tests/FluentAssertions.Json.Specs/FluentAssertions.Json.Specs.csproj index c4204c2e..49605675 100644 --- a/Tests/FluentAssertions.Json.Specs/FluentAssertions.Json.Specs.csproj +++ b/Tests/FluentAssertions.Json.Specs/FluentAssertions.Json.Specs.csproj @@ -8,7 +8,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 96a096d73fdf7b14a4e9359c5c1147ff78836b80 Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Sun, 16 Nov 2025 13:04:40 +0100 Subject: [PATCH 25/26] ReportGenerator 5.1.23 -> 5.5.0 --- Build/_build.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Build/_build.csproj b/Build/_build.csproj index 3d7812d3..39c84fb5 100644 --- a/Build/_build.csproj +++ b/Build/_build.csproj @@ -13,7 +13,7 @@ - + From 4647ab19eeff5913ee4cd083ff21b0a73236a917 Mon Sep 17 00:00:00 2001 From: Jonas Nyrup Date: Mon, 17 Nov 2025 12:49:58 +0100 Subject: [PATCH 26/26] Remove S3928 pragma S3928 was from once when I locally had SonarQube installed --- Src/FluentAssertions.Json/Difference.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Src/FluentAssertions.Json/Difference.cs b/Src/FluentAssertions.Json/Difference.cs index 29c35b13..9ad92388 100644 --- a/Src/FluentAssertions.Json/Difference.cs +++ b/Src/FluentAssertions.Json/Difference.cs @@ -39,9 +39,9 @@ public override string ToString() DifferenceKind.ExpectedMissesProperty => $"has extra property {Path}", DifferenceKind.ActualMissesElement => $"misses expected element {Path}", DifferenceKind.WrongOrder => $"has expected element {Path} in the wrong order", -#pragma warning disable S3928, MA0015 // Parameter names used into ArgumentException constructors should match an existing one +#pragma warning disable MA0015 // Specify the parameter name in ArgumentException _ => throw new ArgumentOutOfRangeException(), -#pragma warning restore S3928, MA0015 // Parameter names used into ArgumentException constructors should match an existing one +#pragma warning restore MA0015 // Specify the parameter name in ArgumentException }; } }