Skip to content

Commit ffa5015

Browse files
committed
Fix missing HttpContext on ValidationContext during atomic operations
1 parent c7e396d commit ffa5015

File tree

10 files changed

+103
-3
lines changed

10 files changed

+103
-3
lines changed

src/JsonApiDotNetCore/Controllers/BaseJsonApiOperationsController.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,8 @@ private int ValidateOperation(OperationContainer operation, int operationIndex,
166166
ModelState =
167167
{
168168
MaxAllowedErrors = maxErrorsRemaining
169-
}
169+
},
170+
HttpContext = HttpContext
170171
};
171172

172173
ObjectValidator.Validate(validationContext, null, string.Empty, operation.Resource);

test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceTests.cs

+6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using FluentAssertions.Extensions;
44
using JsonApiDotNetCore.Configuration;
55
using JsonApiDotNetCore.Serialization.Objects;
6+
using Microsoft.AspNetCore.Authentication;
67
using Microsoft.EntityFrameworkCore;
78
using Microsoft.Extensions.DependencyInjection;
89
using TestBuildingBlocks;
@@ -26,6 +27,11 @@ public AtomicCreateResourceTests(IntegrationTestContext<TestableStartup<Operatio
2627
testContext.UseController<MusicTracksController>();
2728
testContext.UseController<PlaylistsController>();
2829

30+
testContext.ConfigureServicesBeforeStartup(services =>
31+
{
32+
services.AddSingleton<ISystemClock, FrozenSystemClock>();
33+
});
34+
2935
var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService<IJsonApiOptions>();
3036
options.AllowUnknownFieldsInRequestBody = false;
3137
}

test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Creating/AtomicCreateResourceWithClientGeneratedIdTests.cs

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using FluentAssertions;
33
using JsonApiDotNetCore.Configuration;
44
using JsonApiDotNetCore.Serialization.Objects;
5+
using Microsoft.AspNetCore.Authentication;
56
using Microsoft.Extensions.DependencyInjection;
67
using TestBuildingBlocks;
78
using Xunit;
@@ -28,6 +29,7 @@ public AtomicCreateResourceWithClientGeneratedIdTests(IntegrationTestContext<Tes
2829
services.AddResourceDefinition<ImplicitlyChangingTextLanguageDefinition>();
2930

3031
services.AddSingleton<ResourceDefinitionHitCounter>();
32+
services.AddSingleton<ISystemClock, FrozenSystemClock>();
3133
});
3234

3335
var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService<IJsonApiOptions>();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using System.ComponentModel.DataAnnotations;
2+
using System.Reflection;
3+
using JsonApiDotNetCore.Resources;
4+
using Microsoft.AspNetCore.Authentication;
5+
using Microsoft.Extensions.DependencyInjection;
6+
7+
namespace JsonApiDotNetCoreTests.IntegrationTests.AtomicOperations;
8+
9+
[AttributeUsage(AttributeTargets.Property)]
10+
internal sealed class DateMustBeInThePastAttribute : ValidationAttribute
11+
{
12+
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
13+
{
14+
var targetedFields = validationContext.GetRequiredService<ITargetedFields>();
15+
16+
if (targetedFields.Attributes.Any(attribute => attribute.Property.Name == validationContext.MemberName))
17+
{
18+
PropertyInfo propertyInfo = validationContext.ObjectType.GetProperty(validationContext.MemberName!)!;
19+
20+
if (propertyInfo.PropertyType == typeof(DateTimeOffset) || propertyInfo.PropertyType == typeof(DateTimeOffset?))
21+
{
22+
var typedValue = (DateTimeOffset?)propertyInfo.GetValue(validationContext.ObjectInstance);
23+
var systemClock = validationContext.GetRequiredService<ISystemClock>();
24+
25+
if (typedValue >= systemClock.UtcNow)
26+
{
27+
return new ValidationResult($"{validationContext.MemberName} must be in the past.");
28+
}
29+
}
30+
}
31+
32+
return ValidationResult.Success;
33+
}
34+
}

test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Meta/AtomicResourceMetaTests.cs

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using FluentAssertions.Extensions;
55
using JsonApiDotNetCore.Configuration;
66
using JsonApiDotNetCore.Serialization.Objects;
7+
using Microsoft.AspNetCore.Authentication;
78
using Microsoft.Extensions.DependencyInjection;
89
using TestBuildingBlocks;
910
using Xunit;
@@ -27,6 +28,7 @@ public AtomicResourceMetaTests(IntegrationTestContext<TestableStartup<Operations
2728
services.AddResourceDefinition<TextLanguageMetaDefinition>();
2829

2930
services.AddSingleton<ResourceDefinitionHitCounter>();
31+
services.AddSingleton<ISystemClock, FrozenSystemClock>();
3032
});
3133

3234
var hitCounter = _testContext.Factory.Services.GetRequiredService<ResourceDefinitionHitCounter>();

test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/ModelStateValidation/AtomicModelStateValidationTests.cs

+52
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
using System.Net;
22
using FluentAssertions;
33
using JsonApiDotNetCore.Serialization.Objects;
4+
using Microsoft.AspNetCore.Authentication;
45
using Microsoft.EntityFrameworkCore;
6+
using Microsoft.Extensions.DependencyInjection;
57
using TestBuildingBlocks;
68
using Xunit;
79

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

21+
_testContext.ConfigureServicesBeforeStartup(services =>
22+
{
23+
services.AddSingleton<ISystemClock, FrozenSystemClock>();
24+
});
25+
1926
testContext.UseController<OperationsController>();
2027
}
2128

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

77+
[Fact]
78+
public async Task Cannot_create_resource_when_violation_from_custom_ValidationAttribute()
79+
{
80+
// Arrange
81+
var clock = _testContext.Factory.Services.GetRequiredService<ISystemClock>();
82+
83+
var requestBody = new
84+
{
85+
atomic__operations = new[]
86+
{
87+
new
88+
{
89+
op = "add",
90+
data = new
91+
{
92+
type = "musicTracks",
93+
attributes = new
94+
{
95+
title = "some",
96+
lengthInSeconds = 120,
97+
releasedAt = clock.UtcNow.AddDays(1)
98+
}
99+
}
100+
}
101+
}
102+
};
103+
104+
const string route = "/operations";
105+
106+
// Act
107+
(HttpResponseMessage httpResponse, Document responseDocument) = await _testContext.ExecutePostAtomicAsync<Document>(route, requestBody);
108+
109+
// Assert
110+
httpResponse.ShouldHaveStatusCode(HttpStatusCode.UnprocessableEntity);
111+
112+
responseDocument.Errors.ShouldHaveCount(1);
113+
114+
ErrorObject error = responseDocument.Errors[0];
115+
error.StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity);
116+
error.Title.Should().Be("Input validation failed.");
117+
error.Detail.Should().Be("ReleasedAt must be in the past.");
118+
error.Source.ShouldNotBeNull();
119+
error.Source.Pointer.Should().Be("/atomic:operations[0]/data/attributes/releasedAt");
120+
}
121+
70122
[Fact]
71123
public async Task Can_create_resource_with_annotated_relationship()
72124
{

test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/MusicTrack.cs

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public sealed class MusicTrack : Identifiable<Guid>
2323
public string? Genre { get; set; }
2424

2525
[Attr]
26+
[DateMustBeInThePast]
2627
public DateTimeOffset ReleasedAt { get; set; }
2728

2829
[HasOne]

test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations/Updating/Resources/AtomicUpdateResourceTests.cs

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using FluentAssertions.Extensions;
44
using JsonApiDotNetCore.Configuration;
55
using JsonApiDotNetCore.Serialization.Objects;
6+
using Microsoft.AspNetCore.Authentication;
67
using Microsoft.EntityFrameworkCore;
78
using Microsoft.Extensions.DependencyInjection;
89
using TestBuildingBlocks;
@@ -29,6 +30,7 @@ public AtomicUpdateResourceTests(IntegrationTestContext<TestableStartup<Operatio
2930
services.AddResourceDefinition<ImplicitlyChangingTextLanguageDefinition>();
3031

3132
services.AddSingleton<ResourceDefinitionHitCounter>();
33+
services.AddSingleton<ISystemClock, FrozenSystemClock>();
3234
});
3335

3436
var options = (JsonApiOptions)testContext.Factory.Services.GetRequiredService<IJsonApiOptions>();

test/TestBuildingBlocks/FakerContainer.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ static FakerContainer()
1212
{
1313
// Setting the system DateTime to kind Utc, so that faker calls like PastOffset() don't depend on the system time zone.
1414
// See https://docs.microsoft.com/en-us/dotnet/api/system.datetimeoffset.op_implicit?view=net-6.0#remarks
15-
Date.SystemClock = () => 1.January(2020).AsUtc();
15+
Date.SystemClock = () => 1.January(2020).At(1, 1, 1).AsUtc();
1616
}
1717

1818
protected static int GetFakerSeed()

test/TestBuildingBlocks/FrozenSystemClock.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace TestBuildingBlocks;
55

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

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

0 commit comments

Comments
 (0)