Skip to content

Commit

Permalink
Add BeValidJson to StringAssertions
Browse files Browse the repository at this point in the history
  • Loading branch information
vbreuss committed Jan 14, 2024
1 parent f3ff0ff commit fc41d92
Show file tree
Hide file tree
Showing 3 changed files with 191 additions and 0 deletions.
59 changes: 59 additions & 0 deletions Src/FluentAssertions/Primitives/StringAssertions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
#if NET6_0_OR_GREATER
using System.Text.Json;
#endif
using System.Text.RegularExpressions;
using FluentAssertions.Common;
using FluentAssertions.Equivalency;
Expand Down Expand Up @@ -1973,6 +1976,62 @@ public AndConstraint<TAssertions> NotBeLowerCased(string because = "", params ob
return new AndConstraint<TAssertions>((TAssertions)this);
}

#if NET6_0_OR_GREATER
/// <summary>
/// Asserts that the string is a valid JSON document.
/// </summary>
/// <param name="because">
/// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion
/// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
/// </param>
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
/// </param>
public AndWhichConstraint<TAssertions, JsonDocument> BeValidJson(
string because = "", params object[] becauseArgs)
{
return BeValidJson(default, because, becauseArgs);
}

/// <summary>
/// Asserts that the string is a valid JSON document.
/// </summary>
/// <param name="options">
/// Options to control the reader behavior during parsing.
/// </param>
/// <param name="because">
/// A formatted phrase as is supported by <see cref="string.Format(string,object[])" /> explaining why the assertion
/// is needed. If the phrase does not start with the word <i>because</i>, it is prepended automatically.
/// </param>
/// <param name="becauseArgs">
/// Zero or more objects to format using the placeholders in <paramref name="because" />.
/// </param>
public AndWhichConstraint<TAssertions, JsonDocument> BeValidJson(
JsonDocumentOptions options,
string because = "", params object[] becauseArgs)
{
JsonDocument json = null;

Execute.Assertion
.ForCondition(Subject is not null)
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context:string} to be valid JSON{reason}, but found null.");

try
{
json = JsonDocument.Parse(Subject, options);
}
catch (JsonException jsonException)
{
Execute.Assertion
.BecauseOf(because, becauseArgs)
.FailWith("Expected {context:string} to be valid JSON{reason}, but parsing failed with {0}.", jsonException.Message);
}

return new AndWhichConstraint<TAssertions, JsonDocument>((TAssertions)this, json);
}
#endif

private static bool Contains(string actual, string expected, StringComparison comparison)
{
return (actual ?? string.Empty).Contains(expected ?? string.Empty, comparison);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2126,6 +2126,8 @@ namespace FluentAssertions.Primitives
public FluentAssertions.AndConstraint<TAssertions> BeOneOf(params string[] validValues) { }
public FluentAssertions.AndConstraint<TAssertions> BeOneOf(System.Collections.Generic.IEnumerable<string> validValues, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> BeUpperCased(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndWhichConstraint<TAssertions, System.Text.Json.JsonDocument> BeValidJson(string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndWhichConstraint<TAssertions, System.Text.Json.JsonDocument> BeValidJson(System.Text.Json.JsonDocumentOptions options, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> Contain(string expected, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> Contain(string expected, FluentAssertions.OccurrenceConstraint occurrenceConstraint, string because = "", params object[] becauseArgs) { }
public FluentAssertions.AndConstraint<TAssertions> ContainAll(params string[] values) { }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
#if NET6_0_OR_GREATER
using System;
using System.Text.Json;
using Xunit;
using Xunit.Sdk;

namespace FluentAssertions.Specs.Primitives;

public partial class StringAssertionSpecs
{
public class BeValidJson
{
[Fact]
public void Allow_consecutive_assertions()
{
// Arrange
string subject = """{ "id": 42, "admin": true }""";

// Act
object which = subject.Should().BeValidJson().Which;

// Assert
which.Should().BeAssignableTo<JsonDocument>();
}

[Fact]
public void Fail_for_null_string()
{
// Arrange
string subject = null;

// Act
Action act = () => subject.Should().BeValidJson("null is not allowed");

// Assert
act.Should().Throw<XunitException>()
.WithMessage("Expected subject to be valid JSON because null is not allowed, but found null.");
}

[Fact]
public void Fail_for_empty_string()
{
// Arrange
string subject = "";

// Act
Action act = () => subject.Should().BeValidJson("empty string is not allowed");

// Assert
act.Should().Throw<XunitException>()
.WithMessage("Expected subject to be valid JSON because empty string is not allowed, but parsing failed*");
}

[Fact]
public void Fail_for_invalid_string()
{
// Arrange
string subject = "invalid json";

// Act
Action act = () => subject.Should().BeValidJson("we like {0}", "JSON");

// Assert
act.Should().Throw<XunitException>()
.WithMessage("Expected subject to be valid JSON because we like JSON, but parsing failed*");
}

[Theory]
[InlineData("""{{"id":1}""")]
[InlineData("""{"id":1}}""")]
[InlineData("""[[{"id":1}]""")]
[InlineData("""[{"id":1}]]""")]
public void Fail_for_string_with_unmatched_paranthesis(string subject)
{
// Act
Action act = () => subject.Should().BeValidJson("it contains unmatched paranthesis");

// Assert
act.Should().Throw<XunitException>()
.WithMessage("Expected subject to be valid JSON because it contains unmatched paranthesis, but parsing failed*");
}

[Fact]
public void Succeed_for_empty_object_string()
{
// Arrange
string subject = "{}";

// Act / Assert
subject.Should().BeValidJson();
}

[Theory]
[InlineData("""{ "id": 42, "admin": true }""")]
[InlineData("""[{ "id": 1 }, { "id": 2 }]""")]
public void Succeed_for_valid_string(string subject)
{
// Act / Assert
subject.Should().BeValidJson();
}

[Fact]
public void Fail_with_trailing_commas_when_not_allowed_by_provided_options()
{
// Arrange
string subject = """{"values":[1,2,3,]}""";

// Act
Action act = () => subject.Should().BeValidJson(
new JsonDocumentOptions { AllowTrailingCommas = false },
"trailing commas are not allowed");

// Assert
act.Should().Throw<XunitException>()
.WithMessage("Expected subject to be valid JSON because trailing commas are not allowed, but parsing failed*");
}

[Fact]
public void Succeed_with_trailing_commas_when_allowed_by_provided_options()
{
// Arrange
string subject = """{"values":[1,2,3,]}""";

// Act / Assert
subject.Should().BeValidJson(
new JsonDocumentOptions { AllowTrailingCommas = true });
}
}
}
#endif

0 comments on commit fc41d92

Please sign in to comment.