From 543131e3ef9c08307cfc7b4a18ebbbce5f7ac47e Mon Sep 17 00:00:00 2001 From: Bart Koelman Date: Wed, 7 Oct 2020 16:57:44 +0200 Subject: [PATCH] Rewrite flaky tests using new infrastructure This change affects solely the test implementations, without changing their names or what they assert on. Both need improvements, but are not in scope of this effort. --- .../Extensibility/CustomErrorHandlingTests.cs | 5 +- .../Acceptance/Spec/EndToEndTest.cs | 102 --- .../Acceptance/Spec/UpdatingDataTests.cs | 582 ++++++++++-------- .../FakeLoggerFactory.cs | 15 +- .../IntegrationTestContext.cs | 20 + 5 files changed, 357 insertions(+), 367 deletions(-) delete mode 100644 test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorHandlingTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorHandlingTests.cs index 51f3568667..91143812b9 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorHandlingTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Extensibility/CustomErrorHandlingTests.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Net; using JsonApiDotNetCore.Configuration; using JsonApiDotNetCore.Errors; @@ -29,8 +30,8 @@ public void When_using_custom_exception_handler_it_must_create_error_document_an Assert.NotEmpty((string[]) errorDocument.Errors[0].Meta.Data["StackTrace"]); Assert.Single(loggerFactory.Logger.Messages); - Assert.Equal(LogLevel.Warning, loggerFactory.Logger.Messages[0].LogLevel); - Assert.Contains("Access is denied.", loggerFactory.Logger.Messages[0].Text); + Assert.Equal(LogLevel.Warning, loggerFactory.Logger.Messages.Single().LogLevel); + Assert.Contains("Access is denied.", loggerFactory.Logger.Messages.Single().Text); } public class CustomExceptionHandler : ExceptionHandler diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs deleted file mode 100644 index 701235ba41..0000000000 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/EndToEndTest.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System; -using System.Linq.Expressions; -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading.Tasks; -using JsonApiDotNetCore.Middleware; -using JsonApiDotNetCore.Resources; -using JsonApiDotNetCore.Serialization.Client.Internal; -using JsonApiDotNetCoreExample; -using JsonApiDotNetCoreExample.Data; -using JsonApiDotNetCoreExample.Models; -using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.TestHost; -using Xunit; - -namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec -{ -} - - -namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec -{ - public class EndToEndTest - { - public static MediaTypeHeaderValue JsonApiContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType); - private HttpClient _client; - protected TestFixture _fixture; - protected readonly IResponseDeserializer _deserializer; - public EndToEndTest(TestFixture fixture) - { - _fixture = fixture; - _deserializer = GetDeserializer(); - } - - public AppDbContext PrepareTest() where TStartup : class - { - var builder = WebHost.CreateDefaultBuilder().UseStartup(); - var server = new TestServer(builder); - _client = server.CreateClient(); - - var dbContext = GetDbContext(); - dbContext.ClearTable(); - dbContext.ClearTable(); - dbContext.ClearTable(); - dbContext.ClearTable(); - dbContext.SaveChanges(); - return dbContext; - } - - public AppDbContext GetDbContext() - { - return _fixture.GetRequiredService(); - } - - public async Task<(string, HttpResponseMessage)> SendRequest(string method, string route, string content = null) - { - var request = new HttpRequestMessage(new HttpMethod(method), route); - if (content != null) - { - request.Content = new StringContent(content); - request.Content.Headers.ContentType = JsonApiContentType; - } - var response = await _client.SendAsync(request); - - var body = await response.Content?.ReadAsStringAsync(); - return (body, response); - } - - public Task<(string, HttpResponseMessage)> Get(string route) - { - return SendRequest("GET", route); - } - - public Task<(string, HttpResponseMessage)> Post(string route, string content) - { - return SendRequest("POST", route, content); - } - - public Task<(string, HttpResponseMessage)> Patch(string route, string content) - { - return SendRequest("PATCH", route, content); - } - - public IRequestSerializer GetSerializer(Expression> attributes = null, Expression> relationships = null) where TResource : class, IIdentifiable - { - return _fixture.GetSerializer(attributes, relationships); - } - - public IResponseDeserializer GetDeserializer() - { - return _fixture.GetDeserializer(); - } - - protected void AssertEqualStatusCode(HttpStatusCode expected, HttpResponseMessage response) - { - Assert.True(expected == response.StatusCode, $"Got {response.StatusCode} status code with payload instead of {expected}. Payload: {response.Content.ReadAsStringAsync().Result}"); - } - } - -} diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs index ad62bb6b6e..163e82d4ea 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/UpdatingDataTests.cs @@ -1,192 +1,232 @@ -using System; using System.Collections.Generic; -using System.Linq; using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; using System.Threading.Tasks; using Bogus; -using JsonApiDotNetCore.Middleware; +using FluentAssertions; using JsonApiDotNetCore.Serialization; using JsonApiDotNetCore.Serialization.Objects; using JsonApiDotNetCoreExample; using JsonApiDotNetCoreExample.Data; using JsonApiDotNetCoreExample.Models; -using Microsoft.AspNetCore; using Microsoft.AspNetCore.Authentication; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.TestHost; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Newtonsoft.Json; using Xunit; using Person = JsonApiDotNetCoreExample.Models.Person; namespace JsonApiDotNetCoreExampleTests.Acceptance.Spec { - [Collection("WebHostCollection")] - public sealed class UpdatingDataTests : EndToEndTest + public sealed class UpdatingDataTests : IClassFixture> { - private readonly AppDbContext _context; - private readonly Faker _todoItemFaker; - private readonly Faker _personFaker; - - public UpdatingDataTests(TestFixture fixture) : base(fixture) - { - _context = fixture.GetRequiredService(); - - _todoItemFaker = new Faker() - .RuleFor(t => t.Description, f => f.Lorem.Sentence()) - .RuleFor(t => t.Ordinal, f => f.Random.Number()) - .RuleFor(t => t.CreatedDate, f => f.Date.Past()); - _personFaker = new Faker() - .RuleFor(p => p.FirstName, f => f.Name.FirstName()) - .RuleFor(p => p.LastName, f => f.Name.LastName()); + private readonly IntegrationTestContext _testContext; + + private readonly Faker _todoItemFaker = new Faker() + .RuleFor(t => t.Description, f => f.Lorem.Sentence()) + .RuleFor(t => t.Ordinal, f => f.Random.Number()) + .RuleFor(t => t.CreatedDate, f => f.Date.Past()); + + private readonly Faker _personFaker = new Faker() + .RuleFor(p => p.FirstName, f => f.Name.FirstName()) + .RuleFor(p => p.LastName, f => f.Name.LastName()); + + public UpdatingDataTests(IntegrationTestContext testContext) + { + _testContext = testContext; + + FakeLoggerFactory loggerFactory = null; + + testContext.ConfigureLogging(options => + { + loggerFactory = new FakeLoggerFactory(); + + options.ClearProviders(); + options.AddProvider(loggerFactory); + options.SetMinimumLevel(LogLevel.Trace); + options.AddFilter((category, level) => level == LogLevel.Trace && + (category == typeof(JsonApiReader).FullName || category == typeof(JsonApiWriter).FullName)); + }); + + testContext.ConfigureServicesBeforeStartup(services => + { + if (loggerFactory != null) + { + services.AddSingleton(_ => loggerFactory); + } + }); } [Fact] public async Task PatchResource_ModelWithEntityFrameworkInheritance_IsPatched() { // Arrange - var dbContext = PrepareTest(); + var clock = _testContext.Factory.Services.GetRequiredService(); - var builder = WebHost.CreateDefaultBuilder().UseStartup(); - var server = new TestServer(builder); - - var clock = server.Host.Services.GetRequiredService(); + SuperUser superUser = null; + await _testContext.RunOnDatabaseAsync(async dbContext => + { + superUser = new SuperUser(dbContext) + { + SecurityLevel = 1337, + UserName = "joe@account.com", + Password = "12345", + LastPasswordChange = clock.UtcNow.LocalDateTime.AddMinutes(-15) + }; + + dbContext.Users.Add(superUser); + await dbContext.SaveChangesAsync(); + }); - var serializer = TestFixture.GetSerializer(server.Host.Services, e => new { e.SecurityLevel, e.UserName, e.Password }); - var superUser = new SuperUser(_context) { SecurityLevel = 1337, UserName = "Super", Password = "User", LastPasswordChange = clock.UtcNow.LocalDateTime.AddMinutes(-15) }; - dbContext.Set().Add(superUser); - await dbContext.SaveChangesAsync(); + var requestBody = new + { + data = new + { + type = "superUsers", + id = superUser.StringId, + attributes = new Dictionary + { + ["securityLevel"] = 2674, + ["userName"] = "joe@other-domain.com", + ["password"] = "secret" + } + } + }; - var su = new SuperUser(_context) { Id = superUser.Id, SecurityLevel = 2674, UserName = "Power", Password = "secret" }; - var content = serializer.Serialize(su); + var route = "/api/v1/superUsers/" + superUser.StringId; // Act - var (body, response) = await Patch($"/api/v1/superUsers/{su.Id}", content); + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - AssertEqualStatusCode(HttpStatusCode.OK, response); - var updated = _deserializer.DeserializeSingle(body).Data; - Assert.Equal(su.SecurityLevel, updated.SecurityLevel); - Assert.Equal(su.UserName, updated.UserName); - Assert.Null(updated.Password); + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.SingleData.Should().NotBeNull(); + responseDocument.SingleData.Attributes["securityLevel"].Should().Be(2674); + responseDocument.SingleData.Attributes["userName"].Should().Be("joe@other-domain.com"); + responseDocument.SingleData.Attributes.Should().NotContainKey("password"); } [Fact] public async Task Response422IfUpdatingNotSettableAttribute() { // Arrange - var builder = WebHost.CreateDefaultBuilder().UseStartup(); + var loggerFactory = _testContext.Factory.Services.GetRequiredService(); + loggerFactory.Logger.Clear(); + + var todoItem = _todoItemFaker.Generate(); - var loggerFactory = new FakeLoggerFactory(); - builder.ConfigureLogging(options => + await _testContext.RunOnDatabaseAsync(async dbContext => { - options.AddProvider(loggerFactory); - options.SetMinimumLevel(LogLevel.Trace); - options.AddFilter((category, level) => level == LogLevel.Trace && - (category == typeof(JsonApiReader).FullName || category == typeof(JsonApiWriter).FullName)); + dbContext.TodoItems.Add(todoItem); + await dbContext.SaveChangesAsync(); }); - var server = new TestServer(builder); - var client = server.CreateClient(); - - var todoItem = _todoItemFaker.Generate(); - _context.TodoItems.Add(todoItem); - await _context.SaveChangesAsync(); + var requestBody = new + { + data = new + { + type = "todoItems", + id = todoItem.StringId, + attributes = new Dictionary + { + ["calculatedValue"] = "calculated" + } + } + }; - var serializer = TestFixture.GetSerializer(server.Host.Services, ti => new { ti.CalculatedValue }); - var content = serializer.Serialize(todoItem); - var request = PrepareRequest("PATCH", $"/api/v1/todoItems/{todoItem.Id}", content); + var route = "/api/v1/todoItems/" + todoItem.StringId; // Act - var response = await client.SendAsync(request); + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - Assert.Equal(HttpStatusCode.UnprocessableEntity, response.StatusCode); - - var body = await response.Content.ReadAsStringAsync(); - var document = JsonConvert.DeserializeObject(body); - Assert.Single(document.Errors); - - var error = document.Errors.Single(); - Assert.Equal(HttpStatusCode.UnprocessableEntity, error.StatusCode); - Assert.Equal("Failed to deserialize request body.", error.Title); - Assert.StartsWith("Property 'TodoItem.CalculatedValue' is read-only. - Request body: <<", error.Detail); - - Assert.NotEmpty(loggerFactory.Logger.Messages); - Assert.Contains(loggerFactory.Logger.Messages, - x => x.Text.StartsWith("Received request at ") && x.Text.Contains("with body:")); - Assert.Contains(loggerFactory.Logger.Messages, - x => x.Text.StartsWith("Sending 422 response for request at ") && x.Text.Contains("Failed to deserialize request body.")); + httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body."); + responseDocument.Errors[0].Detail.Should().StartWith("Property 'TodoItem.CalculatedValue' is read-only. - Request body: <<"); + + loggerFactory.Logger.Messages.Should().HaveCount(2); + loggerFactory.Logger.Messages.Should().Contain(x => + x.Text.StartsWith("Received request at ") && x.Text.Contains("with body:")); + loggerFactory.Logger.Messages.Should().Contain(x => + x.Text.StartsWith("Sending 422 response for request at ") && + x.Text.Contains("Failed to deserialize request body.")); } [Fact] public async Task Respond_404_If_ResourceDoesNotExist() { // Arrange - await _context.ClearTableAsync(); - await _context.SaveChangesAsync(); - - var todoItem = _todoItemFaker.Generate(); - todoItem.Id = 100; - todoItem.CreatedDate = new DateTime(2002, 2,2); - var builder = WebHost.CreateDefaultBuilder() - .UseStartup(); + await _testContext.RunOnDatabaseAsync(async dbContext => + { + await dbContext.ClearTableAsync(); + }); - var server = new TestServer(builder); - var client = server.CreateClient(); + var requestBody = new + { + data = new + { + type = "todoItems", + id = 99999999, + attributes = new Dictionary + { + ["description"] = "something else" + } + } + }; - var serializer = TestFixture.GetSerializer(server.Host.Services, ti => new { ti.Description, ti.Ordinal, ti.CreatedDate }); - var content = serializer.Serialize(todoItem); - var request = PrepareRequest("PATCH", $"/api/v1/todoItems/{todoItem.Id}", content); + var route = "/api/v1/todoItems/" + 99999999; // Act - var response = await client.SendAsync(request); + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - var body = await response.Content.ReadAsStringAsync(); - Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); - - var errorDocument = JsonConvert.DeserializeObject(body); - Assert.Single(errorDocument.Errors); - Assert.Equal(HttpStatusCode.NotFound, errorDocument.Errors[0].StatusCode); - Assert.Equal("The requested resource does not exist.", errorDocument.Errors[0].Title); - Assert.Equal("Resource of type 'todoItems' with ID '100' does not exist.", errorDocument.Errors[0].Detail); + httpResponse.Should().HaveStatusCode(HttpStatusCode.NotFound); + + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.NotFound); + responseDocument.Errors[0].Title.Should().Be("The requested resource does not exist."); + responseDocument.Errors[0].Detail.Should().Be("Resource of type 'todoItems' with ID '99999999' does not exist."); } [Fact] public async Task Respond_422_If_IdNotInAttributeList() { // Arrange - var maxPersonId = _context.TodoItems.ToList().LastOrDefault()?.Id ?? 0; var todoItem = _todoItemFaker.Generate(); - todoItem.CreatedDate = new DateTime(2002, 2,2); - var builder = WebHost.CreateDefaultBuilder() - .UseStartup(); - var server = new TestServer(builder); - var client = server.CreateClient(); - var serializer = TestFixture.GetSerializer(server.Host.Services, ti => new {ti.Description, ti.Ordinal, ti.CreatedDate}); - var content = serializer.Serialize(todoItem); - var request = PrepareRequest("PATCH", $"/api/v1/todoItems/{maxPersonId}", content); + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.TodoItems.Add(todoItem); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + data = new + { + type = "todoItems", + attributes = new Dictionary + { + ["description"] = "something else" + } + } + }; + + var route = "/api/v1/todoItems/" + todoItem.StringId; // Act - var response = await client.SendAsync(request); + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - Assert.Equal(HttpStatusCode.UnprocessableEntity, response.StatusCode); + httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); - var body = await response.Content.ReadAsStringAsync(); - var document = JsonConvert.DeserializeObject(body); - Assert.Single(document.Errors); - - var error = document.Errors.Single(); - Assert.Equal(HttpStatusCode.UnprocessableEntity, error.StatusCode); - Assert.Equal("Failed to deserialize request body: Payload must include 'id' element.", error.Title); - Assert.StartsWith("Request body: <<", error.Detail); + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Payload must include 'id' element."); + responseDocument.Errors[0].Detail.Should().StartWith("Request body: <<"); } [Fact] @@ -194,63 +234,60 @@ public async Task Respond_409_If_IdInUrlIsDifferentFromIdInRequestBody() { // Arrange var todoItem = _todoItemFaker.Generate(); - todoItem.CreatedDate = new DateTime(2002, 2,2); - _context.TodoItems.Add(todoItem); - await _context.SaveChangesAsync(); + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.TodoItems.Add(todoItem); + await dbContext.SaveChangesAsync(); + }); - var wrongTodoItemId = todoItem.Id + 1; + int differentTodoItemId = todoItem.Id + 1; - var builder = WebHost.CreateDefaultBuilder().UseStartup(); - var server = new TestServer(builder); - var client = server.CreateClient(); - var serializer = TestFixture.GetSerializer(server.Host.Services, ti => new {ti.Description, ti.Ordinal, ti.CreatedDate}); - var content = serializer.Serialize(todoItem); - var request = PrepareRequest("PATCH", $"/api/v1/todoItems/{wrongTodoItemId}", content); + var requestBody = new + { + data = new + { + type = "todoItems", + id = differentTodoItemId, + attributes = new Dictionary + { + ["description"] = "something else" + } + } + }; + + var route = "/api/v1/todoItems/" + todoItem.StringId; // Act - var response = await client.SendAsync(request); + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - Assert.Equal(HttpStatusCode.Conflict, response.StatusCode); - - var body = await response.Content.ReadAsStringAsync(); - var document = JsonConvert.DeserializeObject(body); - Assert.Single(document.Errors); + httpResponse.Should().HaveStatusCode(HttpStatusCode.Conflict); - var error = document.Errors.Single(); - Assert.Equal(HttpStatusCode.Conflict, error.StatusCode); - Assert.Equal("Resource ID mismatch between request body and endpoint URL.", error.Title); - Assert.Equal($"Expected resource ID '{wrongTodoItemId}' in PATCH request body at endpoint 'http://localhost/api/v1/todoItems/{wrongTodoItemId}', instead of '{todoItem.Id}'.", error.Detail); + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.Conflict); + responseDocument.Errors[0].Title.Should().Be("Resource ID mismatch between request body and endpoint URL."); + responseDocument.Errors[0].Detail.Should().Be($"Expected resource ID '{todoItem.Id}' in PATCH request body at endpoint 'http://localhost/api/v1/todoItems/{todoItem.Id}', instead of '{differentTodoItemId}'."); } [Fact] public async Task Respond_422_If_Broken_JSON_Payload() { // Arrange - var builder = WebHost.CreateDefaultBuilder() - .UseStartup(); - - var server = new TestServer(builder); - var client = server.CreateClient(); + var requestBody = "{ \"data\" {"; - var content = "{ \"data\" {"; - var request = PrepareRequest("POST", "/api/v1/todoItems", content); + var route = "/api/v1/todoItems/"; // Act - var response = await client.SendAsync(request); + var (httpResponse, responseDocument) = await _testContext.ExecutePostAsync(route, requestBody); // Assert - Assert.Equal(HttpStatusCode.UnprocessableEntity, response.StatusCode); + httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); - var body = await response.Content.ReadAsStringAsync(); - var document = JsonConvert.DeserializeObject(body); - Assert.Single(document.Errors); - - var error = document.Errors.Single(); - Assert.Equal(HttpStatusCode.UnprocessableEntity, error.StatusCode); - Assert.Equal("Failed to deserialize request body.", error.Title); - Assert.StartsWith("Invalid character after parsing", error.Detail); + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body."); + responseDocument.Errors[0].Detail.Should().StartWith("Invalid character after parsing"); } [Fact] @@ -258,10 +295,14 @@ public async Task Respond_422_If_Blocked_For_Update() { // Arrange var todoItem = _todoItemFaker.Generate(); - _context.TodoItems.Add(todoItem); - await _context.SaveChangesAsync(); - var content = new + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.TodoItems.Add(todoItem); + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new { data = new { @@ -269,81 +310,76 @@ public async Task Respond_422_If_Blocked_For_Update() id = todoItem.StringId, attributes = new Dictionary { - { "offsetDate", "2000-01-01" } + ["offsetDate"] = "2000-01-01" } } }; - - var builder = WebHost.CreateDefaultBuilder() - .UseStartup(); - var server = new TestServer(builder); - var client = server.CreateClient(); - var requestBody = JsonConvert.SerializeObject(content); - var request = PrepareRequest(HttpMethod.Patch.Method, "/api/v1/todoItems/" + todoItem.StringId, requestBody); + var route = "/api/v1/todoItems/" + todoItem.StringId; // Act - var response = await client.SendAsync(request); + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - AssertEqualStatusCode(HttpStatusCode.UnprocessableEntity, response); + httpResponse.Should().HaveStatusCode(HttpStatusCode.UnprocessableEntity); - var responseBody = await response.Content.ReadAsStringAsync(); - var errorDocument = JsonConvert.DeserializeObject(responseBody); - Assert.Single(errorDocument.Errors); - - var error = errorDocument.Errors.Single(); - Assert.Equal(HttpStatusCode.UnprocessableEntity, errorDocument.Errors[0].StatusCode); - Assert.Equal("Failed to deserialize request body: Changing the value of the requested attribute is not allowed.", error.Title); - Assert.StartsWith("Changing the value of 'offsetDate' is not allowed. - Request body:", error.Detail); + responseDocument.Errors.Should().HaveCount(1); + responseDocument.Errors[0].StatusCode.Should().Be(HttpStatusCode.UnprocessableEntity); + responseDocument.Errors[0].Title.Should().Be("Failed to deserialize request body: Changing the value of the requested attribute is not allowed."); + responseDocument.Errors[0].Detail.Should().StartWith("Changing the value of 'offsetDate' is not allowed. - Request body:"); } [Fact] public async Task Can_Patch_Resource() { // Arrange - await _context.ClearTableAsync(); - await _context.ClearTableAsync(); - await _context.ClearTableAsync(); - await _context.SaveChangesAsync(); - var todoItem = _todoItemFaker.Generate(); - var person = _personFaker.Generate(); - todoItem.Owner = person; - _context.TodoItems.Add(todoItem); - await _context.SaveChangesAsync(); + todoItem.Owner = _personFaker.Generate(); - var newTodoItem = _todoItemFaker.Generate(); - newTodoItem.Id = todoItem.Id; - var builder = WebHost.CreateDefaultBuilder().UseStartup(); - var server = new TestServer(builder); - var client = server.CreateClient(); - var serializer = TestFixture.GetSerializer(server.Host.Services, p => new { p.Description, p.Ordinal }); + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.TodoItems.Add(todoItem); + await dbContext.SaveChangesAsync(); + }); - var request = PrepareRequest("PATCH", $"/api/v1/todoItems/{todoItem.Id}", serializer.Serialize(newTodoItem)); + var requestBody = new + { + data = new + { + type = "todoItems", + id = todoItem.StringId, + attributes = new Dictionary + { + ["description"] = "something else", + ["ordinal"] = 1 + } + } + }; + + var route = "/api/v1/todoItems/" + todoItem.StringId; // Act - var response = await client.SendAsync(request); - - // Assert -- response - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var body = await response.Content.ReadAsStringAsync(); - var document = JsonConvert.DeserializeObject(body); - Assert.NotNull(document); - Assert.NotNull(document.Data); - Assert.NotNull(document.SingleData.Attributes); - Assert.Equal(newTodoItem.Description, document.SingleData.Attributes["description"]); - Assert.Equal(newTodoItem.Ordinal, (long)document.SingleData.Attributes["ordinal"]); - Assert.True(document.SingleData.Relationships.ContainsKey("owner")); - Assert.Null(document.SingleData.Relationships["owner"].SingleData); - - // Assert -- database - var updatedTodoItem = _context.TodoItems.AsNoTracking() - .Include(t => t.Owner) - .SingleOrDefault(t => t.Id == todoItem.Id); - Assert.Equal(person.Id, todoItem.OwnerId); - Assert.Equal(newTodoItem.Description, updatedTodoItem.Description); - Assert.Equal(newTodoItem.Ordinal, updatedTodoItem.Ordinal); + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.SingleData.Should().NotBeNull(); + responseDocument.SingleData.Attributes["description"].Should().Be("something else"); + responseDocument.SingleData.Attributes["ordinal"].Should().Be(1); + responseDocument.SingleData.Relationships.Should().ContainKey("owner"); + responseDocument.SingleData.Relationships["owner"].SingleData.Should().BeNull(); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + var updated = await dbContext.TodoItems + .Include(t => t.Owner) + .SingleAsync(t => t.Id == todoItem.Id); + + updated.Description.Should().Be("something else"); + updated.Ordinal.Should().Be(1); + updated.Owner.Id.Should().Be(todoItem.Owner.Id); + }); } [Fact] @@ -352,32 +388,40 @@ public async Task Patch_Resource_With_HasMany_Does_Not_Include_Relationships() // Arrange var todoItem = _todoItemFaker.Generate(); todoItem.Owner = _personFaker.Generate(); - _context.TodoItems.Add(todoItem); - await _context.SaveChangesAsync(); - var newPerson = _personFaker.Generate(); - newPerson.Id = todoItem.Owner.Id; - var builder = WebHost.CreateDefaultBuilder().UseStartup(); - var server = new TestServer(builder); - var client = server.CreateClient(); - var serializer = TestFixture.GetSerializer(server.Host.Services, p => new { p.LastName, p.FirstName }); + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.TodoItems.Add(todoItem); + await dbContext.SaveChangesAsync(); + }); - var request = PrepareRequest("PATCH", $"/api/v1/people/{todoItem.Owner.Id}", serializer.Serialize(newPerson)); + var requestBody = new + { + data = new + { + type = "people", + id = todoItem.Owner.StringId, + attributes = new Dictionary + { + ["firstName"] = "John", + ["lastName"] = "Doe" + } + } + }; + + var route = "/api/v1/people/" + todoItem.Owner.StringId; // Act - var response = await client.SendAsync(request); - - // Assert -- response - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - var body = await response.Content.ReadAsStringAsync(); - var document = JsonConvert.DeserializeObject(body); - Assert.NotNull(document); - Assert.NotNull(document.Data); - Assert.NotNull(document.SingleData.Attributes); - Assert.Equal(newPerson.LastName, document.SingleData.Attributes["lastName"]); - Assert.Equal(newPerson.FirstName, document.SingleData.Attributes["firstName"]); - Assert.True(document.SingleData.Relationships.ContainsKey("todoItems")); - Assert.Null(document.SingleData.Relationships["todoItems"].Data); + var (httpResponse, responseDocument) = await _testContext.ExecutePatchAsync(route, requestBody); + + // Assert + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); + + responseDocument.SingleData.Should().NotBeNull(); + responseDocument.SingleData.Attributes["firstName"].Should().Be("John"); + responseDocument.SingleData.Attributes["lastName"].Should().Be("Doe"); + responseDocument.SingleData.Relationships.Should().ContainKey("todoItems"); + responseDocument.SingleData.Relationships["todoItems"].Data.Should().BeNull(); } [Fact] @@ -385,39 +429,57 @@ public async Task Can_Patch_Resource_And_HasOne_Relationships() { // Arrange var todoItem = _todoItemFaker.Generate(); - todoItem.CreatedDate = new DateTime(2002, 2,2); var person = _personFaker.Generate(); - _context.TodoItems.Add(todoItem); - _context.People.Add(person); - await _context.SaveChangesAsync(); - todoItem.Owner = person; - - var builder = WebHost.CreateDefaultBuilder() - .UseStartup(); - var server = new TestServer(builder); - var client = server.CreateClient(); - var serializer = _fixture.GetSerializer(ti => new { ti.Description, ti.Ordinal, ti.CreatedDate }, ti => new { ti.Owner }); - var content = serializer.Serialize(todoItem); - var request = PrepareRequest("PATCH", $"/api/v1/todoItems/{todoItem.Id}", content); + + await _testContext.RunOnDatabaseAsync(async dbContext => + { + dbContext.TodoItems.Add(todoItem); + dbContext.People.Add(person); + + await dbContext.SaveChangesAsync(); + }); + + var requestBody = new + { + data = new + { + type = "todoItems", + id = todoItem.StringId, + attributes = new Dictionary + { + ["description"] = "Something else", + }, + relationships = new Dictionary + { + ["owner"] = new + { + data = new + { + type = "people", + id = person.StringId + } + } + } + } + }; + + var route = "/api/v1/todoItems/" + todoItem.StringId; // Act - var response = await client.SendAsync(request); - var updatedTodoItem = _context.TodoItems.AsNoTracking() - .Include(t => t.Owner) - .SingleOrDefault(t => t.Id == todoItem.Id); + var (httpResponse, _) = await _testContext.ExecutePatchAsync(route, requestBody); // Assert - Assert.Equal(HttpStatusCode.OK, response.StatusCode); - Assert.Equal(person.Id, updatedTodoItem.OwnerId); - } + httpResponse.Should().HaveStatusCode(HttpStatusCode.OK); - private HttpRequestMessage PrepareRequest(string method, string route, string content) - { - var httpMethod = new HttpMethod(method); - var request = new HttpRequestMessage(httpMethod, route) {Content = new StringContent(content)}; + await _testContext.RunOnDatabaseAsync(async dbContext => + { + var updated = await dbContext.TodoItems + .Include(t => t.Owner) + .SingleAsync(t => t.Id == todoItem.Id); - request.Content.Headers.ContentType = new MediaTypeHeaderValue(HeaderConstants.MediaType); - return request; + updated.Description.Should().Be("Something else"); + updated.Owner.Id.Should().Be(person.Id); + }); } } } diff --git a/test/JsonApiDotNetCoreExampleTests/FakeLoggerFactory.cs b/test/JsonApiDotNetCoreExampleTests/FakeLoggerFactory.cs index 39d28aaeab..f2c69db509 100644 --- a/test/JsonApiDotNetCoreExampleTests/FakeLoggerFactory.cs +++ b/test/JsonApiDotNetCoreExampleTests/FakeLoggerFactory.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using Microsoft.Extensions.Logging; @@ -25,16 +26,24 @@ public void Dispose() internal sealed class FakeLogger : ILogger { - public List<(LogLevel LogLevel, string Text)> Messages = new List<(LogLevel, string)>(); + private readonly ConcurrentBag<(LogLevel LogLevel, string Text)> _messages = new ConcurrentBag<(LogLevel LogLevel, string Text)>(); + + public IReadOnlyCollection<(LogLevel LogLevel, string Text)> Messages => _messages; + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Clear() + { + _messages.Clear(); + } public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { var message = formatter(state, exception); - Messages.Add((logLevel, message)); + _messages.Add((logLevel, message)); } - public bool IsEnabled(LogLevel logLevel) => true; public IDisposable BeginScope(TState state) => null; } } diff --git a/test/JsonApiDotNetCoreExampleTests/IntegrationTestContext.cs b/test/JsonApiDotNetCoreExampleTests/IntegrationTestContext.cs index 70e7136cd7..44a347fc63 100644 --- a/test/JsonApiDotNetCoreExampleTests/IntegrationTestContext.cs +++ b/test/JsonApiDotNetCoreExampleTests/IntegrationTestContext.cs @@ -9,6 +9,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using Newtonsoft.Json; namespace JsonApiDotNetCoreExampleTests @@ -26,6 +27,7 @@ public class IntegrationTestContext : IDisposable where TDbContext : DbContext { private readonly Lazy> _lazyFactory; + private Action _loggingConfiguration; private Action _beforeServicesConfiguration; private Action _afterServicesConfiguration; @@ -44,6 +46,8 @@ private WebApplicationFactory CreateFactory() var factory = new IntegrationTestWebApplicationFactory(); + factory.ConfigureLogging(_loggingConfiguration); + factory.ConfigureServicesBeforeStartup(services => { _beforeServicesConfiguration?.Invoke(services); @@ -74,6 +78,11 @@ public void Dispose() Factory.Dispose(); } + public void ConfigureLogging(Action loggingConfiguration) + { + _loggingConfiguration = loggingConfiguration; + } + public void ConfigureServicesBeforeStartup(Action servicesConfiguration) { _beforeServicesConfiguration = servicesConfiguration; @@ -165,9 +174,15 @@ private TResponseDocument DeserializeResponse(string response private sealed class IntegrationTestWebApplicationFactory : WebApplicationFactory { + private Action _loggingConfiguration; private Action _beforeServicesConfiguration; private Action _afterServicesConfiguration; + public void ConfigureLogging(Action loggingConfiguration) + { + _loggingConfiguration = loggingConfiguration; + } + public void ConfigureServicesBeforeStartup(Action servicesConfiguration) { _beforeServicesConfiguration = servicesConfiguration; @@ -183,6 +198,11 @@ protected override IHostBuilder CreateHostBuilder() return Host.CreateDefaultBuilder(null) .ConfigureWebHostDefaults(webBuilder => { + webBuilder.ConfigureLogging(options => + { + _loggingConfiguration?.Invoke(options); + }); + webBuilder.ConfigureServices(services => { _beforeServicesConfiguration?.Invoke(services);