Skip to content

Fix missing HttpContext on ValidationContext during atomic operations #1251

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,8 @@ private int ValidateOperation(OperationContainer operation, int operationIndex,
ModelState =
{
MaxAllowedErrors = maxErrorsRemaining
}
},
HttpContext = HttpContext
};

ObjectValidator.Validate(validationContext, null, string.Empty, operation.Resource);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using FluentAssertions.Extensions;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Serialization.Objects;
using Microsoft.AspNetCore.Authentication;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using TestBuildingBlocks;
Expand All @@ -26,6 +27,11 @@ public AtomicCreateResourceTests(IntegrationTestContext<TestableStartup<Operatio
testContext.UseController<MusicTracksController>();
testContext.UseController<PlaylistsController>();

testContext.ConfigureServicesBeforeStartup(services =>
{
services.AddSingleton<ISystemClock, FrozenSystemClock>();
});

var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService<IJsonApiOptions>();
options.AllowUnknownFieldsInRequestBody = false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using FluentAssertions;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Serialization.Objects;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.DependencyInjection;
using TestBuildingBlocks;
using Xunit;
Expand All @@ -28,6 +29,7 @@ public AtomicCreateResourceWithClientGeneratedIdTests(IntegrationTestContext<Tes
services.AddResourceDefinition<ImplicitlyChangingTextLanguageDefinition>();

services.AddSingleton<ResourceDefinitionHitCounter>();
services.AddSingleton<ISystemClock, FrozenSystemClock>();
});

var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService<IJsonApiOptions>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using JsonApiDotNetCore.Resources;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.DependencyInjection;

namespace JsonApiDotNetCoreTests.IntegrationTests.AtomicOperations;

[AttributeUsage(AttributeTargets.Property)]
internal sealed class DateMustBeInThePastAttribute : ValidationAttribute
{
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
{
var targetedFields = validationContext.GetRequiredService<ITargetedFields>();

if (targetedFields.Attributes.Any(attribute => attribute.Property.Name == validationContext.MemberName))
{
PropertyInfo propertyInfo = validationContext.ObjectType.GetProperty(validationContext.MemberName!)!;

if (propertyInfo.PropertyType == typeof(DateTimeOffset) || propertyInfo.PropertyType == typeof(DateTimeOffset?))
{
var typedValue = (DateTimeOffset?)propertyInfo.GetValue(validationContext.ObjectInstance);
var systemClock = validationContext.GetRequiredService<ISystemClock>();

if (typedValue >= systemClock.UtcNow)
{
return new ValidationResult($"{validationContext.MemberName} must be in the past.");
}
}
}

return ValidationResult.Success;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using FluentAssertions.Extensions;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Serialization.Objects;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.DependencyInjection;
using TestBuildingBlocks;
using Xunit;
Expand All @@ -27,6 +28,7 @@ public AtomicResourceMetaTests(IntegrationTestContext<TestableStartup<Operations
services.AddResourceDefinition<TextLanguageMetaDefinition>();

services.AddSingleton<ResourceDefinitionHitCounter>();
services.AddSingleton<ISystemClock, FrozenSystemClock>();
});

var hitCounter = _testContext.Factory.Services.GetRequiredService<ResourceDefinitionHitCounter>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System.Net;
using FluentAssertions;
using JsonApiDotNetCore.Serialization.Objects;
using Microsoft.AspNetCore.Authentication;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using TestBuildingBlocks;
using Xunit;

Expand All @@ -16,6 +18,11 @@ public AtomicModelStateValidationTests(IntegrationTestContext<TestableStartup<Op
{
_testContext = testContext;

_testContext.ConfigureServicesBeforeStartup(services =>
{
services.AddSingleton<ISystemClock, FrozenSystemClock>();
});

testContext.UseController<OperationsController>();
}

Expand Down Expand Up @@ -67,6 +74,51 @@ public async Task Cannot_create_resource_with_multiple_violations()
error2.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/lengthInSeconds");
}

[Fact]
public async Task Cannot_create_resource_when_violation_from_custom_ValidationAttribute()
{
// Arrange
var clock = _testContext.Factory.Services.GetRequiredService<ISystemClock>();

var requestBody = new
{
atomic__operations = new[]
{
new
{
op = "add",
data = new
{
type = "musicTracks",
attributes = new
{
title = "some",
lengthInSeconds = 120,
releasedAt = clock.UtcNow.AddDays(1)
}
}
}
}
};

const string route = "/operations";

// Act
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync<Document>(route, requestBody);

// Assert
httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity);

responseDocument.Errors.ShouldHaveCount(1);

ErrorObject error = responseDocument.Errors[0];
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
error.Title.Should().Be("Input validation failed.");
error.Detail.Should().Be("ReleasedAt must be in the past.");
error.Source.ShouldNotBeNull();
error.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/releasedAt");
}

[Fact]
public async Task Can_create_resource_with_annotated_relationship()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public sealed class MusicTrack : Identifiable<Guid>
public string? Genre { get; set; }

[Attr]
[DateMustBeInThePast]
public DateTimeOffset ReleasedAt { get; set; }

[HasOne]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using FluentAssertions.Extensions;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Serialization.Objects;
using Microsoft.AspNetCore.Authentication;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using TestBuildingBlocks;
Expand All @@ -29,6 +30,7 @@ public AtomicUpdateResourceTests(IntegrationTestContext<TestableStartup<Operatio
services.AddResourceDefinition<ImplicitlyChangingTextLanguageDefinition>();

services.AddSingleton<ResourceDefinitionHitCounter>();
services.AddSingleton<ISystemClock, FrozenSystemClock>();
});

var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService<IJsonApiOptions>();
Expand Down
2 changes: 1 addition & 1 deletion test/TestBuildingBlocks/FakerContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ static FakerContainer()
{
// Setting the system DateTime to kind Utc, so that faker calls like PastOffset() don't depend on the system time zone.
// See https://docs.microsoft.com/en-us/dotnet/api/system.datetimeoffset.op_implicit?view=net-6.0#remarks
Date.SystemClock = () => 1.January(2020).AsUtc();
Date.SystemClock = () => 1.January(2020).At(1, 1, 1).AsUtc();
}

protected static int GetFakerSeed()
Expand Down
2 changes: 1 addition & 1 deletion test/TestBuildingBlocks/FrozenSystemClock.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace TestBuildingBlocks;

public sealed class FrozenSystemClock : ISystemClock
{
private static readonly DateTimeOffset DefaultTime = 1.January(2000).At(1, 1, 1).AsUtc();
private static readonly DateTimeOffset DefaultTime = 1.January(2020).At(1, 1, 1).AsUtc();

public DateTimeOffset UtcNow { get; set; } = DefaultTime;
}