diff --git a/Directory.Build.props b/Directory.Build.props index d55c6ddae7..2ef7f513f1 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -18,4 +18,4 @@ 29.0.1 4.13.1 - \ No newline at end of file + diff --git a/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs b/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs index b75bacd347..0ff4c7fbbd 100644 --- a/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs +++ b/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs @@ -8,6 +8,7 @@ using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.Server; +using Microsoft.AspNetCore.Http; using Newtonsoft.Json; namespace Benchmarks.Serialization @@ -38,8 +39,7 @@ public JsonApiDeserializerBenchmarks() var options = new JsonApiOptions(); IResourceGraph resourceGraph = DependencyFactory.CreateResourceGraph(options); var targetedFields = new TargetedFields(); - - _jsonApiDeserializer = new RequestDeserializer(resourceGraph, new DefaultResourceFactory(new ServiceContainer()), targetedFields); + _jsonApiDeserializer = new RequestDeserializer(resourceGraph, new DefaultResourceFactory(new ServiceContainer()), targetedFields, new HttpContextAccessor()); } [Benchmark] diff --git a/docs/usage/options.md b/docs/usage/options.md index 5283055728..4f52a93883 100644 --- a/docs/usage/options.md +++ b/docs/usage/options.md @@ -88,4 +88,12 @@ If you would like to use ASP.NET Core ModelState validation into your controller ```c# options.ValidateModelState = true; ``` +You will need to use the JsonApiDotNetCore 'IsRequiredAttribute' instead of the built-in 'RequiredAttribute' because it contains modifications to enable partial patching. +```c# +public class Person : Identifiable +{ + [IsRequired(AllowEmptyStrings = true)] + public string FirstName { get; set; } +} +``` diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs index 01b0d1e352..bde8b8f310 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs @@ -7,6 +7,7 @@ namespace JsonApiDotNetCoreExample.Models public sealed class Article : Identifiable { [Attr] + [IsRequired(AllowEmptyStrings = true)] public string Name { get; set; } [HasOne] diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Author.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Author.cs index 27b817ca9c..7af14d5235 100644 --- a/src/Examples/JsonApiDotNetCoreExample/Models/Author.cs +++ b/src/Examples/JsonApiDotNetCoreExample/Models/Author.cs @@ -6,6 +6,7 @@ namespace JsonApiDotNetCoreExample.Models public sealed class Author : Identifiable { [Attr] + [IsRequired(AllowEmptyStrings = true)] public string Name { get; set; } [HasMany] diff --git a/src/JsonApiDotNetCore/Extensions/HttpContextExtensions.cs b/src/JsonApiDotNetCore/Extensions/HttpContextExtensions.cs index c6a4a8988c..07bd3b66f4 100644 --- a/src/JsonApiDotNetCore/Extensions/HttpContextExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/HttpContextExtensions.cs @@ -14,5 +14,17 @@ internal static void SetJsonApiRequest(this HttpContext httpContext) { httpContext.Items["IsJsonApiRequest"] = bool.TrueString; } + + internal static void DisableValidator(this HttpContext httpContext, string propertyName, string model) + { + var itemKey = $"JsonApiDotNetCore_DisableValidation_{model}_{propertyName}"; + httpContext.Items[itemKey] = true; + } + + internal static bool IsValidatorDisabled(this HttpContext httpContext, string propertyName, string model) + { + return httpContext.Items.ContainsKey($"JsonApiDotNetCore_DisableValidation_{model}_{propertyName}") || + httpContext.Items.ContainsKey($"JsonApiDotNetCore_DisableValidation_{model}_Relation"); + } } } diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj index 9a1e0c0104..e2808125b5 100644 --- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj +++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj @@ -1,4 +1,4 @@ - + 4.0.0 $(NetCoreAppVersion) diff --git a/src/JsonApiDotNetCore/Models/IsRequiredAttribute.cs b/src/JsonApiDotNetCore/Models/IsRequiredAttribute.cs new file mode 100644 index 0000000000..d28c106e09 --- /dev/null +++ b/src/JsonApiDotNetCore/Models/IsRequiredAttribute.cs @@ -0,0 +1,24 @@ +using System.ComponentModel.DataAnnotations; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using JsonApiDotNetCore.Extensions; + +namespace JsonApiDotNetCore.Models +{ + public sealed class IsRequiredAttribute : RequiredAttribute + { + private bool _isDisabled; + + public override bool IsValid(object value) + { + return _isDisabled || base.IsValid(value); + } + + protected override ValidationResult IsValid(object value, ValidationContext validationContext) + { + var httpContextAccessor = (IHttpContextAccessor)validationContext.GetRequiredService(typeof(IHttpContextAccessor)); + _isDisabled = httpContextAccessor.HttpContext.IsValidatorDisabled(validationContext.MemberName, validationContext.ObjectType.Name); + return _isDisabled ? ValidationResult.Success : base.IsValid(value, validationContext); + } + } +} diff --git a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs index b4626bf884..7169686f03 100644 --- a/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs +++ b/src/JsonApiDotNetCore/Serialization/Common/BaseDocumentParser.cs @@ -69,7 +69,7 @@ protected object Deserialize(string body) /// Attributes and their values, as in the serialized content /// Exposed attributes for /// - protected IIdentifiable SetAttributes(IIdentifiable entity, Dictionary attributeValues, List attributes) + protected virtual IIdentifiable SetAttributes(IIdentifiable entity, Dictionary attributeValues, List attributes) { if (attributeValues == null || attributeValues.Count == 0) return entity; @@ -86,6 +86,7 @@ protected IIdentifiable SetAttributes(IIdentifiable entity, Dictionary /// Sets the relationships on a parsed entity /// @@ -93,7 +94,7 @@ protected IIdentifiable SetAttributes(IIdentifiable entity, DictionaryRelationships and their values, as in the serialized content /// Exposed relationships for /// - protected IIdentifiable SetRelationships(IIdentifiable entity, Dictionary relationshipsValues, List relationshipAttributes) + protected virtual IIdentifiable SetRelationships(IIdentifiable entity, Dictionary relationshipsValues, List relationshipAttributes) { if (relationshipsValues == null || relationshipsValues.Count == 0) return entity; @@ -108,7 +109,6 @@ protected IIdentifiable SetRelationships(IIdentifiable entity, Dictionary @@ -50,5 +56,40 @@ protected override void AfterProcessField(IIdentifiable entity, IResourceField f else if (field is RelationshipAttribute relationship) _targetedFields.Relationships.Add(relationship); } + + protected override IIdentifiable SetAttributes(IIdentifiable entity, Dictionary attributeValues, List attributes) + { + if (_httpContextAccessor.HttpContext.Request.Method == HttpMethod.Patch.Method) + { + foreach (AttrAttribute attr in attributes) + { + if (attr.PropertyInfo.GetCustomAttribute() != null) + { + bool disableValidator = attributeValues == null || attributeValues.Count == 0 || + !attributeValues.TryGetValue(attr.PublicAttributeName, out _); + + if (disableValidator) + { + _httpContextAccessor.HttpContext.DisableValidator(attr.PropertyInfo.Name, entity.GetType().Name); + } + } + } + } + + return base.SetAttributes(entity, attributeValues, attributes); + } + + protected override IIdentifiable SetRelationships(IIdentifiable entity, Dictionary relationshipsValues, List relationshipAttributes) + { + // If there is a relationship included in the data of the POST or PATCH, then the 'IsRequired' attribute will be disabled for any + // property within that object. For instance, a new article is posted and has a relationship included to an author. In this case, + // the author name (which has the 'IsRequired' attribute) will not be included in the POST. Unless disabled, the POST will fail. + foreach (RelationshipAttribute attr in relationshipAttributes) + { + _httpContextAccessor.HttpContext.DisableValidator("Relation", attr.PropertyInfo.Name); + } + + return base.SetRelationships(entity, relationshipsValues, relationshipAttributes); + } } } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs index 94cffa66bb..9ca4b55a70 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/ManyToManyTests.cs @@ -21,18 +21,23 @@ namespace JsonApiDotNetCoreExampleTests.Acceptance [Collection("WebHostCollection")] public sealed class ManyToManyTests { - private readonly Faker
_articleFaker = new Faker
() - .RuleFor(a => a.Name, f => f.Random.AlphaNumeric(10)) - .RuleFor(a => a.Author, f => new Author()); + private readonly TestFixture _fixture; + private readonly Faker _authorFaker; + private readonly Faker
_articleFaker; private readonly Faker _tagFaker; - private readonly TestFixture _fixture; - public ManyToManyTests(TestFixture fixture) { _fixture = fixture; + _authorFaker = new Faker() + .RuleFor(a => a.Name, f => f.Random.Words(2)); + + _articleFaker = new Faker
() + .RuleFor(a => a.Name, f => f.Random.AlphaNumeric(10)) + .RuleFor(a => a.Author, f => _authorFaker.Generate()); + _tagFaker = new Faker() .CustomInstantiator(f => new Tag(_fixture.GetService())) .RuleFor(a => a.Name, f => f.Random.AlphaNumeric(10)); @@ -282,7 +287,7 @@ public async Task Can_Create_Many_To_Many() // Arrange var context = _fixture.GetService(); var tag = _tagFaker.Generate(); - var author = new Author(); + var author = _authorFaker.Generate(); context.Tags.Add(tag); context.AuthorDifferentDbContextName.Add(author); await context.SaveChangesAsync(); @@ -294,6 +299,10 @@ public async Task Can_Create_Many_To_Many() data = new { type = "articles", + attributes = new Dictionary + { + {"name", "An article with relationships"} + }, relationships = new Dictionary { { "author", new { diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/ModelStateValidationTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/ModelStateValidationTests.cs index 7907b28305..20ecf96a5a 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/ModelStateValidationTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/ModelStateValidationTests.cs @@ -1,13 +1,16 @@ +using System.Collections.Generic; using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; +using Bogus; using JsonApiDotNetCore; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Models.JsonApiDocuments; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; using JsonApiDotNetCoreExampleTests.Acceptance.Spec; +using Microsoft.EntityFrameworkCore; using Newtonsoft.Json; using Xunit; @@ -15,9 +18,28 @@ namespace JsonApiDotNetCoreExampleTests.Acceptance { public sealed class ModelStateValidationTests : FunctionalTestCollection { + private readonly Faker
_articleFaker; + private readonly Faker _authorFaker; + private readonly Faker _tagFaker; + public ModelStateValidationTests(StandardApplicationFactory factory) : base(factory) { + var options = (JsonApiOptions) _factory.GetService(); + options.ValidateModelState = true; + + var context = _factory.GetService(); + + _authorFaker = new Faker() + .RuleFor(a => a.Name, f => f.Random.Words(2)); + + _articleFaker = new Faker
() + .RuleFor(a => a.Name, f => f.Random.AlphaNumeric(10)) + .RuleFor(a => a.Author, f => _authorFaker.Generate()); + + _tagFaker = new Faker() + .CustomInstantiator(f => new Tag(context)) + .RuleFor(a => a.Name, f => f.Random.AlphaNumeric(10)); } [Fact] @@ -38,9 +60,6 @@ public async Task When_posting_tag_with_invalid_name_it_must_fail() }; request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType); - var options = (JsonApiOptions)_factory.GetService(); - options.ValidateModelState = true; - // Act var response = await _factory.Client.SendAsync(request); @@ -112,9 +131,6 @@ public async Task When_patching_tag_with_invalid_name_it_must_fail() }; request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType); - var options = (JsonApiOptions)_factory.GetService(); - options.ValidateModelState = true; - // Act var response = await _factory.Client.SendAsync(request); @@ -167,5 +183,378 @@ public async Task When_patching_tag_with_invalid_name_without_model_state_valida // Assert Assert.Equal(HttpStatusCode.OK, response.StatusCode); } + + [Fact] + public async Task Create_Article_With_IsRequired_Name_Attribute_Succeeds() + { + // Arrange + string name = "Article Title"; + var context = _factory.GetService(); + var author = _authorFaker.Generate(); + context.AuthorDifferentDbContextName.Add(author); + await context.SaveChangesAsync(); + + var route = "/api/v1/articles"; + var request = new HttpRequestMessage(HttpMethod.Post, route); + var content = new + { + data = new + { + type = "articles", + attributes = new Dictionary + { + {"name", name} + }, + relationships = new Dictionary + { + { "author", new + { + data = new + { + type = "authors", + id = author.StringId + } + } + } + } + } + }; + request.Content = new StringContent(JsonConvert.SerializeObject(content)); + request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType); + + // Act + var response = await _factory.Client.SendAsync(request); + + // Assert + var body = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + + var articleResponse = GetDeserializer().DeserializeSingle
(body).Data; + Assert.NotNull(articleResponse); + + var persistedArticle = await _dbContext.Articles + .SingleAsync(a => a.Id == articleResponse.Id); + + Assert.Equal(name, persistedArticle.Name); + } + + [Fact] + public async Task Create_Article_With_IsRequired_Name_Attribute_Empty_Succeeds() + { + // Arrange + string name = string.Empty; + var context = _factory.GetService(); + var author = _authorFaker.Generate(); + context.AuthorDifferentDbContextName.Add(author); + await context.SaveChangesAsync(); + + var route = "/api/v1/articles"; + var request = new HttpRequestMessage(HttpMethod.Post, route); + var content = new + { + data = new + { + type = "articles", + attributes = new Dictionary + { + {"name", name} + }, + relationships = new Dictionary + { + { "author", new + { + data = new + { + type = "authors", + id = author.StringId + } + } + } + } + } + }; + request.Content = new StringContent(JsonConvert.SerializeObject(content)); + request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType); + + // Act + var response = await _factory.Client.SendAsync(request); + + // Assert + var body = await response.Content.ReadAsStringAsync(); + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + + var articleResponse = GetDeserializer().DeserializeSingle
(body).Data; + Assert.NotNull(articleResponse); + + var persistedArticle = await _dbContext.Articles + .SingleAsync(a => a.Id == articleResponse.Id); + + Assert.Equal(name, persistedArticle.Name); + } + + [Fact] + public async Task Create_Article_With_IsRequired_Name_Attribute_Explicitly_Null_Fails() + { + // Arrange + var context = _factory.GetService(); + var author = _authorFaker.Generate(); + context.AuthorDifferentDbContextName.Add(author); + await context.SaveChangesAsync(); + + var route = "/api/v1/articles"; + var request = new HttpRequestMessage(HttpMethod.Post, route); + var content = new + { + data = new + { + type = "articles", + attributes = new Dictionary + { + {"name", null} + }, + relationships = new Dictionary + { + { "author", new + { + data = new + { + type = "authors", + id = author.StringId + } + } + } + } + } + }; + request.Content = new StringContent(JsonConvert.SerializeObject(content)); + request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType); + + // Act + var response = await _factory.Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.UnprocessableEntity, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + var errorDocument = JsonConvert.DeserializeObject(body); + Assert.Single(errorDocument.Errors); + Assert.Equal("Input validation failed.", errorDocument.Errors[0].Title); + Assert.Equal("422", errorDocument.Errors[0].Status); + Assert.Equal("The Name field is required.", errorDocument.Errors[0].Detail); + } + + [Fact] + public async Task Create_Article_With_IsRequired_Name_Attribute_Missing_Fails() + { + // Arrange + var context = _factory.GetService(); + var author = _authorFaker.Generate(); + context.AuthorDifferentDbContextName.Add(author); + await context.SaveChangesAsync(); + + var route = "/api/v1/articles"; + var request = new HttpRequestMessage(HttpMethod.Post, route); + var content = new + { + data = new + { + type = "articles", + relationships = new Dictionary + { + { "author", new + { + data = new + { + type = "authors", + id = author.StringId + } + } + } + } + } + }; + request.Content = new StringContent(JsonConvert.SerializeObject(content)); + request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType); + + // Act + var response = await _factory.Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.UnprocessableEntity, response.StatusCode); + var body = await response.Content.ReadAsStringAsync(); + var errorDocument = JsonConvert.DeserializeObject(body); + Assert.Single(errorDocument.Errors); + Assert.Equal("Input validation failed.", errorDocument.Errors[0].Title); + Assert.Equal("422", errorDocument.Errors[0].Status); + Assert.Equal("The Name field is required.", errorDocument.Errors[0].Detail); + } + + [Fact] + public async Task Update_Article_With_IsRequired_Name_Attribute_Succeeds() + { + // Arrange + var name = "Article Name"; + var context = _factory.GetService(); + var article = _articleFaker.Generate(); + context.Articles.Add(article); + await context.SaveChangesAsync(); + + var route = $"/api/v1/articles/{article.Id}"; + var request = new HttpRequestMessage(HttpMethod.Patch, route); + var content = new + { + data = new + { + type = "articles", + id = article.StringId, + attributes = new Dictionary + { + {"name", name} + } + } + }; + + request.Content = new StringContent(JsonConvert.SerializeObject(content)); + request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType); + + // Act + var response = await _factory.Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var persistedArticle = await _dbContext.Articles + .SingleOrDefaultAsync(a => a.Id == article.Id); + + var updatedName = persistedArticle.Name; + Assert.Equal(name, updatedName); + } + + [Fact] + public async Task Update_Article_With_IsRequired_Name_Attribute_Missing_Succeeds() + { + // Arrange + var context = _factory.GetService(); + var tag = _tagFaker.Generate(); + var article = _articleFaker.Generate(); + context.Tags.Add(tag); + context.Articles.Add(article); + await context.SaveChangesAsync(); + + var route = $"/api/v1/articles/{article.Id}"; + var request = new HttpRequestMessage(HttpMethod.Patch, route); + var content = new + { + data = new + { + type = "articles", + id = article.StringId, + relationships = new Dictionary + { + { "tags", new + { + data = new [] + { + new + { + type = "tags", + id = tag.StringId + } + } + } + } + } + } + }; + + request.Content = new StringContent(JsonConvert.SerializeObject(content)); + request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType); + + // Act + var response = await _factory.Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task Update_Article_With_IsRequired_Name_Attribute_Explicitly_Null_Fails() + { + // Arrange + var context = _factory.GetService(); + var article = _articleFaker.Generate(); + context.Articles.Add(article); + await context.SaveChangesAsync(); + + var route = $"/api/v1/articles/{article.Id}"; + var request = new HttpRequestMessage(HttpMethod.Patch, route); + var content = new + { + data = new + { + type = "articles", + id = article.StringId, + attributes = new Dictionary + { + {"name", null} + } + } + }; + + request.Content = new StringContent(JsonConvert.SerializeObject(content)); + request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType); + + // Act + var response = await _factory.Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.UnprocessableEntity, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var errorDocument = JsonConvert.DeserializeObject(body); + Assert.Single(errorDocument.Errors); + Assert.Equal("Input validation failed.", errorDocument.Errors[0].Title); + Assert.Equal("422", errorDocument.Errors[0].Status); + Assert.Equal("The Name field is required.", errorDocument.Errors[0].Detail); + } + + [Fact] + public async Task Update_Article_With_IsRequired_Name_Attribute_Empty_Succeeds() + { + // Arrange + var context = _factory.GetService(); + var article = _articleFaker.Generate(); + context.Articles.Add(article); + await context.SaveChangesAsync(); + + var route = $"/api/v1/articles/{article.Id}"; + var request = new HttpRequestMessage(HttpMethod.Patch, route); + var content = new + { + data = new + { + type = "articles", + id = article.StringId, + attributes = new Dictionary + { + {"name", ""} + } + } + }; + + request.Content = new StringContent(JsonConvert.SerializeObject(content)); + request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType); + + // Act + var response = await _factory.Client.SendAsync(request); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var persistedArticle = await _dbContext.Articles + .SingleOrDefaultAsync(a => a.Id == article.Id); + + var updatedName = persistedArticle.Name; + Assert.Equal("", updatedName); + } } } diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/ResourceDefinitionTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/ResourceDefinitionTests.cs index 41ea2d9d45..8629e4f4b9 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/ResourceDefinitionTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/ResourceDefinitions/ResourceDefinitionTests.cs @@ -22,20 +22,24 @@ namespace JsonApiDotNetCoreExampleTests.Acceptance public sealed class ResourceDefinitionTests { private readonly TestFixture _fixture; + private readonly AppDbContext _context; private readonly Faker _userFaker; private readonly Faker _todoItemFaker; private readonly Faker _personFaker; - private readonly Faker
_articleFaker = new Faker
() - .RuleFor(a => a.Name, f => f.Random.AlphaNumeric(10)) - .RuleFor(a => a.Author, f => new Author()); - + private readonly Faker
_articleFaker; + private readonly Faker _authorFaker; private readonly Faker _tagFaker; public ResourceDefinitionTests(TestFixture fixture) { _fixture = fixture; _context = fixture.GetService(); + _authorFaker = new Faker() + .RuleFor(a => a.Name, f => f.Random.Words(2)); + _articleFaker = new Faker
() + .RuleFor(a => a.Name, f => f.Random.AlphaNumeric(10)) + .RuleFor(a => a.Author, f => _authorFaker.Generate()); _userFaker = new Faker() .CustomInstantiator(f => new User(_context)) .RuleFor(u => u.Username, f => f.Internet.UserName()) diff --git a/test/UnitTests/Models/ResourceConstructionTests.cs b/test/UnitTests/Models/ResourceConstructionTests.cs index b684491f85..044fe6300f 100644 --- a/test/UnitTests/Models/ResourceConstructionTests.cs +++ b/test/UnitTests/Models/ResourceConstructionTests.cs @@ -7,8 +7,10 @@ using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.Server; using JsonApiDotNetCoreExample.Data; +using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.Abstractions; +using Moq; using Newtonsoft.Json; using Xunit; @@ -16,6 +18,14 @@ namespace UnitTests.Models { public sealed class ResourceConstructionTests { + public Mock _mockHttpContextAccessor; + + public ResourceConstructionTests() + { + _mockHttpContextAccessor = new Mock(); + _mockHttpContextAccessor.Setup(mock => mock.HttpContext).Returns(new DefaultHttpContext()); + } + [Fact] public void When_resource_has_default_constructor_it_must_succeed() { @@ -24,7 +34,7 @@ public void When_resource_has_default_constructor_it_must_succeed() .AddResource() .Build(); - var serializer = new RequestDeserializer(graph, new DefaultResourceFactory(new ServiceContainer()), new TargetedFields()); + var serializer = new RequestDeserializer(graph, new DefaultResourceFactory(new ServiceContainer()), new TargetedFields(), _mockHttpContextAccessor.Object); var body = new { @@ -53,7 +63,7 @@ public void When_resource_has_default_constructor_that_throws_it_must_fail() .AddResource() .Build(); - var serializer = new RequestDeserializer(graph, new DefaultResourceFactory(new ServiceContainer()), new TargetedFields()); + var serializer = new RequestDeserializer(graph, new DefaultResourceFactory(new ServiceContainer()), new TargetedFields(), _mockHttpContextAccessor.Object); var body = new { @@ -89,7 +99,7 @@ public void When_resource_has_constructor_with_injectable_parameter_it_must_succ var serviceContainer = new ServiceContainer(); serviceContainer.AddService(typeof(AppDbContext), appDbContext); - var serializer = new RequestDeserializer(graph, new DefaultResourceFactory(serviceContainer), new TargetedFields()); + var serializer = new RequestDeserializer(graph, new DefaultResourceFactory(serviceContainer), new TargetedFields(), _mockHttpContextAccessor.Object); var body = new { @@ -119,7 +129,7 @@ public void When_resource_has_constructor_with_string_parameter_it_must_fail() .AddResource() .Build(); - var serializer = new RequestDeserializer(graph, new DefaultResourceFactory(new ServiceContainer()), new TargetedFields()); + var serializer = new RequestDeserializer(graph, new DefaultResourceFactory(new ServiceContainer()), new TargetedFields(), _mockHttpContextAccessor.Object); var body = new { diff --git a/test/UnitTests/Serialization/DeserializerTestsSetup.cs b/test/UnitTests/Serialization/DeserializerTestsSetup.cs index 16c0426b7e..3d35316cf4 100644 --- a/test/UnitTests/Serialization/DeserializerTestsSetup.cs +++ b/test/UnitTests/Serialization/DeserializerTestsSetup.cs @@ -1,14 +1,22 @@ -using System; using JsonApiDotNetCore.Internal.Contracts; using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Serialization; using System.Collections.Generic; using JsonApiDotNetCore.Internal; +using Microsoft.AspNetCore.Http; +using Moq; namespace UnitTests.Serialization { public class DeserializerTestsSetup : SerializationTestsSetupBase { + public Mock _mockHttpContextAccessor; + + public DeserializerTestsSetup() + { + _mockHttpContextAccessor = new Mock(); + _mockHttpContextAccessor.Setup(mock => mock.HttpContext).Returns(new DefaultHttpContext()); + } protected sealed class TestDocumentParser : BaseDocumentParser { public TestDocumentParser(IResourceGraph resourceGraph, IResourceFactory resourceFactory) : base(resourceGraph, resourceFactory) { } diff --git a/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs b/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs index 1094f60b37..9f31c2dc28 100644 --- a/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs +++ b/test/UnitTests/Serialization/Server/RequestDeserializerTests.cs @@ -6,6 +6,7 @@ using JsonApiDotNetCore.Models; using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.Server; +using Microsoft.AspNetCore.Http; using Moq; using Newtonsoft.Json; using Xunit; @@ -19,7 +20,7 @@ public sealed class RequestDeserializerTests : DeserializerTestsSetup private readonly Mock _fieldsManagerMock = new Mock(); public RequestDeserializerTests() { - _deserializer = new RequestDeserializer(_resourceGraph, new DefaultResourceFactory(new ServiceContainer()), _fieldsManagerMock.Object); + _deserializer = new RequestDeserializer(_resourceGraph, new DefaultResourceFactory(new ServiceContainer()), _fieldsManagerMock.Object, _mockHttpContextAccessor.Object); } [Fact]