From 6a6d5a6e4a95117f64da0172d7b35c7bd124537c Mon Sep 17 00:00:00 2001 From: Nano Taboada <87288+nanotaboada@users.noreply.github.com> Date: Mon, 7 Apr 2025 10:03:27 -0300 Subject: [PATCH 1/2] feat(service)!: migrate to AutoMapper for model mapping --- .codacy.yml | 1 + .../Unit/PlayerControllerTests.cs | 192 ++++++++++-------- .../Unit/PlayerServiceTests.cs | 186 ++++++++++++----- .../Utilities/DatabaseFakes.cs | 2 +- .../Utilities/PlayerFakes.cs | 144 ++++++++++--- .../Utilities/PlayerMocks.cs | 64 +++--- .../packages.lock.json | 9 + .../Controllers/PlayerController.cs | 34 ++-- .../Data/Repository.cs | 2 +- .../Dotnet.Samples.AspNetCore.WebApi.csproj | 1 + .../Mappings/PlayerMappingProfile.cs | 55 +++++ .../Models/Player.cs | 11 +- .../Models/PlayerRequestModel.cs | 36 ++++ .../Models/PlayerResponseModel.cs | 28 +++ Dotnet.Samples.AspNetCore.WebApi/Program.cs | 2 + .../Services/IPlayerService.cs | 18 +- .../Services/PlayerService.cs | 86 ++++---- .../packages.lock.json | 9 + codecov.yml | 1 + 19 files changed, 610 insertions(+), 271 deletions(-) create mode 100644 Dotnet.Samples.AspNetCore.WebApi/Mappings/PlayerMappingProfile.cs create mode 100644 Dotnet.Samples.AspNetCore.WebApi/Models/PlayerRequestModel.cs create mode 100644 Dotnet.Samples.AspNetCore.WebApi/Models/PlayerResponseModel.cs diff --git a/.codacy.yml b/.codacy.yml index f2591c2..473ea83 100644 --- a/.codacy.yml +++ b/.codacy.yml @@ -13,6 +13,7 @@ exclude_paths: - "**/*Program.cs" - "**/Data/**" - "**/Enums/**" + - "**/Mappings/**" - "**/Migrations/**" - "**/Models/**" - "**/Properties/**" diff --git a/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerControllerTests.cs b/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerControllerTests.cs index 84bbfeb..98ff675 100644 --- a/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerControllerTests.cs +++ b/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerControllerTests.cs @@ -1,6 +1,5 @@ using Dotnet.Samples.AspNetCore.WebApi.Controllers; using Dotnet.Samples.AspNetCore.WebApi.Models; -using Dotnet.Samples.AspNetCore.WebApi.Services; using Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities; using FluentAssertions; using Microsoft.AspNetCore.Http; @@ -27,17 +26,17 @@ public PlayerControllerTests() public async Task GivenPostAsync_WhenModelStateIsInvalid_ThenResponseStatusCodeShouldBe400BadRequest() { // Arrange - var (service, logger) = PlayerMocks.SetupControllerMocks(); + var (service, logger) = PlayerMocks.InitControllerMocks(); var controller = new PlayerController(service.Object, logger.Object); - controller.ModelState.Merge(PlayerStubs.CreateModelError("FirstName", "Required")); + controller.ModelState.Merge(PlayerStubs.CreateModelError("SquadNumber", "Required")); // Act - var response = await controller.PostAsync(It.IsAny()) as BadRequest; + var result = await controller.PostAsync(It.IsAny()) as BadRequest; // Assert - response.Should().NotBeNull().And.BeOfType(); - response?.StatusCode.Should().Be(StatusCodes.Status400BadRequest); + result.Should().NotBeNull().And.BeOfType(); + result?.StatusCode.Should().Be(StatusCodes.Status400BadRequest); } [Fact] @@ -45,19 +44,22 @@ public async Task GivenPostAsync_WhenModelStateIsInvalid_ThenResponseStatusCodeS public async Task GivenPostAsync_WhenServiceRetrieveByIdAsyncReturnsPlayer_ThenResponseStatusCodeShouldBe409Conflict() { // Arrange - var player = PlayerFakes.CreateOneByIdFromStarting11(10); - var (service, logger) = PlayerMocks.SetupControllerMocks(); - service.Setup(service => service.RetrieveByIdAsync(It.IsAny())).ReturnsAsync(player); + var id = 10; + var (service, logger) = PlayerMocks.InitControllerMocks(); + service + .Setup(service => service.RetrieveByIdAsync(It.IsAny())) + .ReturnsAsync(PlayerFakes.CreateResponseModelForOneExistingById(id)); + var payload = PlayerFakes.CreateRequestModelForOneExistingById(id); var controller = new PlayerController(service.Object, logger.Object); // Act - var response = await controller.PostAsync(player) as Conflict; + var result = await controller.PostAsync(payload) as Conflict; // Assert service.Verify(service => service.RetrieveByIdAsync(It.IsAny()), Times.Exactly(1)); - response.Should().NotBeNull().And.BeOfType(); - response?.StatusCode.Should().Be(StatusCodes.Status409Conflict); + result.Should().NotBeNull().And.BeOfType(); + result?.StatusCode.Should().Be(StatusCodes.Status409Conflict); } [Fact] @@ -65,26 +67,31 @@ public async Task GivenPostAsync_WhenServiceRetrieveByIdAsyncReturnsPlayer_ThenR public async Task GivenPostAsync_WhenServiceRetrieveByIdAsyncReturnsNull_ThenResponseStatusCodeShouldBe201Created() { // Arrange - var player = PlayerFakes.CreateOneNew(); - var (service, logger) = PlayerMocks.SetupControllerMocks(); + var request = PlayerFakes.CreateRequestModelForOneNew(); + var (service, logger) = PlayerMocks.InitControllerMocks(); service .Setup(service => service.RetrieveByIdAsync(It.IsAny())) - .ReturnsAsync(null as Player); - service.Setup(service => service.CreateAsync(It.IsAny())); + .ReturnsAsync(null as PlayerResponseModel); + service.Setup(service => service.CreateAsync(It.IsAny())); var controller = new PlayerController(service.Object, logger.Object) { - Url = PlayerMocks.UrlHelperMock().Object, + Url = PlayerMocks.SetupUrlHelperMock().Object, }; // Act - var response = await controller.PostAsync(player) as Created; + var result = await controller.PostAsync(request) as Created; // Assert service.Verify(service => service.RetrieveByIdAsync(It.IsAny()), Times.Exactly(1)); - service.Verify(service => service.CreateAsync(It.IsAny()), Times.Exactly(1)); - response.Should().NotBeNull().And.BeOfType>(); - response?.StatusCode.Should().Be(StatusCodes.Status201Created); + service.Verify( + service => service.CreateAsync(It.IsAny()), + Times.Exactly(1) + ); + result.Should().NotBeNull().And.BeOfType>(); + result?.StatusCode.Should().Be(StatusCodes.Status201Created); + result?.Value.Should().BeEquivalentTo(request); // Request not mapped to Response + result?.Location.Should().Be($"/players/{request.Id}"); } /* ------------------------------------------------------------------------- @@ -96,21 +103,21 @@ public async Task GivenPostAsync_WhenServiceRetrieveByIdAsyncReturnsNull_ThenRes public async Task GivenGetAsync_WhenServiceRetrieveAsyncReturnsListOfPlayers_ThenResponseShouldBeEquivalentToListOfPlayers() { // Arrange - var players = PlayerFakes.CreateStarting11(); - var (service, logger) = PlayerMocks.SetupControllerMocks(); + var players = PlayerFakes.CreateStarting11ResponseModels(); + var (service, logger) = PlayerMocks.InitControllerMocks(); service.Setup(service => service.RetrieveAsync()).ReturnsAsync(players); var controller = new PlayerController(service.Object, logger.Object); // Act - var response = await controller.GetAsync() as Ok>; + var result = await controller.GetAsync() as Ok>; // Assert service.Verify(service => service.RetrieveAsync(), Times.Exactly(1)); - response.Should().NotBeNull().And.BeOfType>>(); - response?.StatusCode.Should().Be(StatusCodes.Status200OK); - response?.Value.Should().NotBeNull().And.BeOfType>(); - response?.Value.Should().BeEquivalentTo(players); + result.Should().NotBeNull().And.BeOfType>>(); + result?.StatusCode.Should().Be(StatusCodes.Status200OK); + result?.Value.Should().NotBeNull().And.BeOfType>(); + result?.Value.Should().BeEquivalentTo(players); } [Fact] @@ -118,19 +125,18 @@ public async Task GivenGetAsync_WhenServiceRetrieveAsyncReturnsListOfPlayers_The public async Task GivenGetAsync_WhenServiceRetrieveAsyncReturnsEmptyList_ThenResponseStatusCodeShouldBe404NotFound() { // Arrange - var players = new List(); // Count = 0 - var (service, logger) = PlayerMocks.SetupControllerMocks(); - service.Setup(service => service.RetrieveAsync()).ReturnsAsync(players); + var (service, logger) = PlayerMocks.InitControllerMocks(); + service.Setup(service => service.RetrieveAsync()).ReturnsAsync([]); var controller = new PlayerController(service.Object, logger.Object); // Act - var response = await controller.GetAsync() as NotFound; + var result = await controller.GetAsync() as NotFound; // Assert service.Verify(service => service.RetrieveAsync(), Times.Exactly(1)); - response.Should().NotBeNull().And.BeOfType(); - response?.StatusCode.Should().Be(StatusCodes.Status404NotFound); + result.Should().NotBeNull().And.BeOfType(); + result?.StatusCode.Should().Be(StatusCodes.Status404NotFound); } [Fact] @@ -138,20 +144,20 @@ public async Task GivenGetAsync_WhenServiceRetrieveAsyncReturnsEmptyList_ThenRes public async Task GivenGetByIdAsync_WhenServiceRetrieveByIdAsyncReturnsNull_ThenResponseStatusCodeShouldBe404NotFound() { // Arrange - var (service, logger) = PlayerMocks.SetupControllerMocks(); + var (service, logger) = PlayerMocks.InitControllerMocks(); service .Setup(service => service.RetrieveByIdAsync(It.IsAny())) - .ReturnsAsync(null as Player); + .ReturnsAsync(null as PlayerResponseModel); var controller = new PlayerController(service.Object, logger.Object); // Act - var response = await controller.GetByIdAsync(It.IsAny()) as NotFound; + var result = await controller.GetByIdAsync(It.IsAny()) as NotFound; // Assert service.Verify(service => service.RetrieveByIdAsync(It.IsAny()), Times.Exactly(1)); - response.Should().NotBeNull().And.BeOfType(); - response?.StatusCode.Should().Be(StatusCodes.Status404NotFound); + result.Should().NotBeNull().And.BeOfType(); + result?.StatusCode.Should().Be(StatusCodes.Status404NotFound); } [Fact] @@ -159,21 +165,21 @@ public async Task GivenGetByIdAsync_WhenServiceRetrieveByIdAsyncReturnsNull_Then public async Task GivenGetByIdAsync_WhenServiceRetrieveByIdAsyncReturnsPlayer_ThenResponseStatusCodeShouldBe200Ok() { // Arrange - var player = PlayerFakes.CreateOneByIdFromStarting11(10); - var (service, logger) = PlayerMocks.SetupControllerMocks(); + var player = PlayerFakes.CreateResponseModelForOneExistingById(10); + var (service, logger) = PlayerMocks.InitControllerMocks(); service.Setup(service => service.RetrieveByIdAsync(It.IsAny())).ReturnsAsync(player); var controller = new PlayerController(service.Object, logger.Object); // Act - var response = await controller.GetByIdAsync(It.IsAny()) as Ok; + var result = await controller.GetByIdAsync(It.IsAny()) as Ok; // Assert service.Verify(service => service.RetrieveByIdAsync(It.IsAny()), Times.Exactly(1)); - response.Should().NotBeNull().And.BeOfType>(); - response?.StatusCode.Should().Be(StatusCodes.Status200OK); - response?.Value.Should().NotBeNull().And.BeOfType(); - response?.Value.Should().BeEquivalentTo(player); + result.Should().NotBeNull().And.BeOfType>(); + result?.StatusCode.Should().Be(StatusCodes.Status200OK); + result?.Value.Should().NotBeNull().And.BeOfType(); + result?.Value.Should().BeEquivalentTo(player); } [Fact] @@ -181,23 +187,23 @@ public async Task GivenGetByIdAsync_WhenServiceRetrieveByIdAsyncReturnsPlayer_Th public async Task GivenGetBySquadNumberAsync_WhenServiceRetrieveBySquadNumberAsyncReturnsNull_ThenResponseStatusCodeShouldBe404NotFound() { // Arrange - var (service, logger) = PlayerMocks.SetupControllerMocks(); + var (service, logger) = PlayerMocks.InitControllerMocks(); service .Setup(service => service.RetrieveBySquadNumberAsync(It.IsAny())) - .ReturnsAsync(null as Player); + .ReturnsAsync(null as PlayerResponseModel); var controller = new PlayerController(service.Object, logger.Object); // Act - var response = await controller.GetBySquadNumberAsync(It.IsAny()) as NotFound; + var result = await controller.GetBySquadNumberAsync(It.IsAny()) as NotFound; // Assert service.Verify( service => service.RetrieveBySquadNumberAsync(It.IsAny()), Times.Exactly(1) ); - response.Should().NotBeNull().And.BeOfType(); - response?.StatusCode.Should().Be(StatusCodes.Status404NotFound); + result.Should().NotBeNull().And.BeOfType(); + result?.StatusCode.Should().Be(StatusCodes.Status404NotFound); } [Fact] @@ -205,8 +211,8 @@ public async Task GivenGetBySquadNumberAsync_WhenServiceRetrieveBySquadNumberAsy public async Task GivenGetBySquadNumberAsync_WhenServiceRetrieveBySquadNumberAsyncReturnsPlayer_ThenResponseStatusCodeShouldBe200Ok() { // Arrange - var player = PlayerFakes.CreateOneByIdFromStarting11(10); - var (service, logger) = PlayerMocks.SetupControllerMocks(); + var player = PlayerFakes.CreateResponseModelForOneExistingById(10); + var (service, logger) = PlayerMocks.InitControllerMocks(); service .Setup(service => service.RetrieveBySquadNumberAsync(It.IsAny())) .ReturnsAsync(player); @@ -214,17 +220,18 @@ public async Task GivenGetBySquadNumberAsync_WhenServiceRetrieveBySquadNumberAsy var controller = new PlayerController(service.Object, logger.Object); // Act - var response = await controller.GetBySquadNumberAsync(It.IsAny()) as Ok; + var result = + await controller.GetBySquadNumberAsync(It.IsAny()) as Ok; // Assert service.Verify( service => service.RetrieveBySquadNumberAsync(It.IsAny()), Times.Exactly(1) ); - response.Should().NotBeNull().And.BeOfType>(); - response?.StatusCode.Should().Be(StatusCodes.Status200OK); - response?.Value.Should().NotBeNull().And.BeOfType(); - response?.Value.Should().BeEquivalentTo(player); + result.Should().NotBeNull().And.BeOfType>(); + result?.StatusCode.Should().Be(StatusCodes.Status200OK); + result?.Value.Should().NotBeNull().And.BeOfType(); + result?.Value.Should().BeEquivalentTo(player); } /* ------------------------------------------------------------------------- @@ -236,18 +243,19 @@ public async Task GivenGetBySquadNumberAsync_WhenServiceRetrieveBySquadNumberAsy public async Task GivenPutAsync_WhenModelStateIsInvalid_ThenResponseStatusCodeShouldBe400BadRequest() { // Arrange - var (service, logger) = PlayerMocks.SetupControllerMocks(); + var (service, logger) = PlayerMocks.InitControllerMocks(); var controller = new PlayerController(service.Object, logger.Object); - controller.ModelState.Merge(PlayerStubs.CreateModelError("FirstName", "Required")); + controller.ModelState.Merge(PlayerStubs.CreateModelError("SquadNumber", "Required")); // Act - var response = - await controller.PutAsync(It.IsAny(), It.IsAny()) as BadRequest; + var result = + await controller.PutAsync(It.IsAny(), It.IsAny()) + as BadRequest; // Assert - response.Should().NotBeNull().And.BeOfType(); - response?.StatusCode.Should().Be(StatusCodes.Status400BadRequest); + result.Should().NotBeNull().And.BeOfType(); + result?.StatusCode.Should().Be(StatusCodes.Status400BadRequest); } [Fact] @@ -255,21 +263,21 @@ public async Task GivenPutAsync_WhenModelStateIsInvalid_ThenResponseStatusCodeSh public async Task GivenPutAsync_WhenServiceRetrieveByIdAsyncReturnsNull_ThenResponseStatusCodeShouldBe404NotFound() { // Arrange - var service = new Mock(); + var (service, logger) = PlayerMocks.InitControllerMocks(); service .Setup(service => service.RetrieveByIdAsync(It.IsAny())) - .ReturnsAsync(null as Player); - var logger = PlayerMocks.LoggerMock(); + .ReturnsAsync(null as PlayerResponseModel); var controller = new PlayerController(service.Object, logger.Object); // Act - var response = await controller.PutAsync(It.IsAny(), It.IsAny()) as NotFound; + var result = + await controller.PutAsync(It.IsAny(), It.IsAny()) as NotFound; // Assert service.Verify(service => service.RetrieveByIdAsync(It.IsAny()), Times.Exactly(1)); - response.Should().NotBeNull().And.BeOfType(); - response?.StatusCode.Should().Be(StatusCodes.Status404NotFound); + result.Should().NotBeNull().And.BeOfType(); + result?.StatusCode.Should().Be(StatusCodes.Status404NotFound); } [Fact] @@ -278,21 +286,26 @@ public async Task GivenPutAsync_WhenServiceRetrieveByIdAsyncReturnsPlayer_ThenRe { // Arrange var id = 10; - var player = PlayerFakes.CreateOneByIdFromStarting11(id); - var (service, logger) = PlayerMocks.SetupControllerMocks(); - service.Setup(service => service.RetrieveByIdAsync(It.IsAny())).ReturnsAsync(player); - service.Setup(service => service.UpdateAsync(It.IsAny())); + var player = PlayerFakes.CreateRequestModelForOneExistingById(id); + var (service, logger) = PlayerMocks.InitControllerMocks(); + service + .Setup(service => service.RetrieveByIdAsync(It.IsAny())) + .ReturnsAsync(PlayerFakes.CreateResponseModelForOneExistingById(id)); + service.Setup(service => service.UpdateAsync(It.IsAny())); var controller = new PlayerController(service.Object, logger.Object); // Act - var response = await controller.PutAsync(id, player) as NoContent; + var result = await controller.PutAsync(id, player) as NoContent; // Assert service.Verify(service => service.RetrieveByIdAsync(It.IsAny()), Times.Exactly(1)); - service.Verify(service => service.UpdateAsync(It.IsAny()), Times.Exactly(1)); - response.Should().NotBeNull().And.BeOfType(); - response?.StatusCode.Should().Be(StatusCodes.Status204NoContent); + service.Verify( + service => service.UpdateAsync(It.IsAny()), + Times.Exactly(1) + ); + result.Should().NotBeNull().And.BeOfType(); + result?.StatusCode.Should().Be(StatusCodes.Status204NoContent); } /* ------------------------------------------------------------------------- @@ -304,20 +317,20 @@ public async Task GivenPutAsync_WhenServiceRetrieveByIdAsyncReturnsPlayer_ThenRe public async Task GivenDeleteAsync_WhenServiceRetrieveByIdAsyncReturnsNull_ThenResponseStatusCodeShouldBe404NotFound() { // Arrange - var (service, logger) = PlayerMocks.SetupControllerMocks(); + var (service, logger) = PlayerMocks.InitControllerMocks(); service .Setup(service => service.RetrieveByIdAsync(It.IsAny())) - .ReturnsAsync(null as Player); + .ReturnsAsync(null as PlayerResponseModel); var controller = new PlayerController(service.Object, logger.Object); // Act - var response = await controller.DeleteAsync(It.IsAny()) as NotFound; + var result = await controller.DeleteAsync(It.IsAny()) as NotFound; // Assert service.Verify(service => service.RetrieveByIdAsync(It.IsAny()), Times.Exactly(1)); - response.Should().NotBeNull().And.BeOfType(); - response?.StatusCode.Should().Be(StatusCodes.Status404NotFound); + result.Should().NotBeNull().And.BeOfType(); + result?.StatusCode.Should().Be(StatusCodes.Status404NotFound); } [Fact] @@ -325,21 +338,22 @@ public async Task GivenDeleteAsync_WhenServiceRetrieveByIdAsyncReturnsNull_ThenR public async Task GivenDeleteAsync_WhenServiceRetrieveByIdAsyncReturnsPlayer_ThenResponseStatusCodeShouldBe204NoContent() { // Arrange - var player = PlayerFakes.CreateOneByIdFromStarting11(10); - var (service, logger) = PlayerMocks.SetupControllerMocks(); - service.Setup(service => service.RetrieveByIdAsync(It.IsAny())).ReturnsAsync(player); + var (service, logger) = PlayerMocks.InitControllerMocks(); + service + .Setup(service => service.RetrieveByIdAsync(It.IsAny())) + .ReturnsAsync(PlayerFakes.CreateResponseModelForOneExistingById(10)); service.Setup(service => service.DeleteAsync(It.IsAny())); var controller = new PlayerController(service.Object, logger.Object); // Act - var response = await controller.DeleteAsync(It.IsAny()) as NoContent; + var result = await controller.DeleteAsync(It.IsAny()) as NoContent; // Assert service.Verify(service => service.RetrieveByIdAsync(It.IsAny()), Times.Exactly(1)); service.Verify(service => service.DeleteAsync(It.IsAny()), Times.Exactly(1)); - response.Should().NotBeNull().And.BeOfType(); - response?.StatusCode.Should().Be(StatusCodes.Status204NoContent); + result.Should().NotBeNull().And.BeOfType(); + result?.StatusCode.Should().Be(StatusCodes.Status204NoContent); } protected virtual void Dispose(bool disposing) diff --git a/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerServiceTests.cs b/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerServiceTests.cs index 56f208c..512d942 100644 --- a/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerServiceTests.cs +++ b/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerServiceTests.cs @@ -22,15 +22,20 @@ public PlayerServiceTests() [Fact] [Trait("Category", "Unit")] - public async Task GivenCreateAsync_WhenRepositoryAddAsync_ThenAddsPlayerToRepositoryAndRemovesMemoryCache() + public async Task GivenCreateAsync_WhenRepositoryAddAsync_ThenAddsPlayerToRepositoryAndRemovesCache() { // Arrange - var (repository, logger, memoryCache) = PlayerMocks.SetupServiceMocks(); + var (repository, logger, memoryCache, mapper) = PlayerMocks.InitServiceMocks(); - var service = new PlayerService(repository.Object, logger.Object, memoryCache.Object); + var service = new PlayerService( + repository.Object, + logger.Object, + memoryCache.Object, + mapper.Object + ); // Act - await service.CreateAsync(It.IsAny()); + await service.CreateAsync(It.IsAny()); // Assert repository.Verify(repository => repository.AddAsync(It.IsAny()), Times.Once); @@ -46,12 +51,21 @@ public async Task GivenCreateAsync_WhenRepositoryAddAsync_ThenAddsPlayerToReposi public async Task GivenRetrieveAsync_WhenRepositoryGetAllAsyncReturnsPlayers_ThenCacheCreateEntryAndResultShouldBeListOfPlayers() { // Arrange - var players = PlayerFakes.CreateStarting11(); - var (repository, logger, memoryCache) = PlayerMocks.SetupServiceMocks(); + var players = PlayerFakes.GetStarting11(); + var response = PlayerFakes.CreateStarting11ResponseModels(); + var (repository, logger, memoryCache, mapper) = PlayerMocks.InitServiceMocks(); repository.Setup(repository => repository.GetAllAsync()).ReturnsAsync(players); + mapper + .Setup(mapper => mapper.Map>(It.IsAny>())) + .Returns(response); var value = It.IsAny(); - var service = new PlayerService(repository.Object, logger.Object, memoryCache.Object); + var service = new PlayerService( + repository.Object, + logger.Object, + memoryCache.Object, + mapper.Object + ); // Act var result = await service.RetrieveAsync(); @@ -60,7 +74,11 @@ public async Task GivenRetrieveAsync_WhenRepositoryGetAllAsyncReturnsPlayers_The repository.Verify(repository => repository.GetAllAsync(), Times.Once); memoryCache.Verify(cache => cache.TryGetValue(It.IsAny(), out value), Times.Once); memoryCache.Verify(cache => cache.CreateEntry(It.IsAny()), Times.Once); - result.Should().BeEquivalentTo(players); + mapper.Verify( + mapper => mapper.Map>(It.IsAny>()), + Times.Once + ); + result.Should().BeEquivalentTo(response); } [Fact] @@ -68,12 +86,21 @@ public async Task GivenRetrieveAsync_WhenRepositoryGetAllAsyncReturnsPlayers_The public async Task GivenRetrieveAsync_WhenExecutedForTheSecondTime_ThenSecondExecutionTimeShouldBeLessThanFirst() { // Arrange - var players = PlayerFakes.CreateStarting11(); - var (repository, logger, memoryCache) = PlayerMocks.SetupServiceMocks(cacheValue: players); - repository.Setup(repository => repository.GetAllAsync()).ReturnsAsync(players); + var (repository, logger, memoryCache, mapper) = PlayerMocks.InitServiceMocks(); + repository + .Setup(repository => repository.GetAllAsync()) + .ReturnsAsync(PlayerFakes.GetStarting11()); + mapper + .Setup(mapper => mapper.Map>(It.IsAny>())) + .Returns(PlayerFakes.CreateStarting11ResponseModels()); var value = It.IsAny(); - var service = new PlayerService(repository.Object, logger.Object, memoryCache.Object); + var service = new PlayerService( + repository.Object, + logger.Object, + memoryCache.Object, + mapper.Object + ); // Act var first = await ExecutionTimeAsync(() => service.RetrieveAsync()); @@ -84,6 +111,10 @@ public async Task GivenRetrieveAsync_WhenExecutedForTheSecondTime_ThenSecondExec cache => cache.TryGetValue(It.IsAny(), out value), Times.Exactly(2) // first + second ); + mapper.Verify( + mapper => mapper.Map>(It.IsAny>()), + Times.Once // first only + ); second.Should().BeLessThan(first); } @@ -92,14 +123,20 @@ public async Task GivenRetrieveAsync_WhenExecutedForTheSecondTime_ThenSecondExec public async Task GivenRetrieveByIdAsync_WhenRepositoryFindByIdAsyncReturnsNull_TheResultShouldBeNull() { // Arrange - var id = 999; - var (repository, logger, memoryCache) = PlayerMocks.SetupServiceMocks(); - repository.Setup(repository => repository.FindByIdAsync(id)).ReturnsAsync((Player?)null); + var (repository, logger, memoryCache, mapper) = PlayerMocks.InitServiceMocks(); + repository + .Setup(repository => repository.FindByIdAsync(It.IsAny())) + .ReturnsAsync((Player?)null); - var service = new PlayerService(repository.Object, logger.Object, memoryCache.Object); + var service = new PlayerService( + repository.Object, + logger.Object, + memoryCache.Object, + mapper.Object + ); // Act - var result = await service.RetrieveByIdAsync(id); + var result = await service.RetrieveByIdAsync(It.IsAny()); // Assert repository.Verify(repository => repository.FindByIdAsync(It.IsAny()), Times.Once); @@ -111,19 +148,32 @@ public async Task GivenRetrieveByIdAsync_WhenRepositoryFindByIdAsyncReturnsNull_ public async Task GivenRetrieveByIdAsync_WhenRepositoryFindByIdAsyncReturnsPlayer_TheResultShouldBePlayer() { // Arrange - var player = PlayerFakes.CreateOneByIdFromStarting11(9); - var (repository, logger, memoryCache) = PlayerMocks.SetupServiceMocks(); - repository.Setup(repository => repository.FindByIdAsync(player.Id)).ReturnsAsync(player); - - var service = new PlayerService(repository.Object, logger.Object, memoryCache.Object); + var id = 9; + var player = PlayerFakes.GetOneExistingById(id); + var response = PlayerFakes.CreateResponseModelForOneExistingById(id); + var (repository, logger, memoryCache, mapper) = PlayerMocks.InitServiceMocks(); + repository + .Setup(repository => repository.FindByIdAsync(It.IsAny())) + .ReturnsAsync(player); + mapper + .Setup(mapper => mapper.Map(It.IsAny())) + .Returns(response); + + var service = new PlayerService( + repository.Object, + logger.Object, + memoryCache.Object, + mapper.Object + ); // Act - var result = await service.RetrieveByIdAsync(player.Id); + var result = await service.RetrieveByIdAsync(It.IsAny()); // Assert - result.Should().BeOfType(); - result.Should().BeEquivalentTo(player); repository.Verify(repository => repository.FindByIdAsync(It.IsAny()), Times.Once); + mapper.Verify(mapper => mapper.Map(It.IsAny()), Times.Once); + result.Should().BeOfType(); + result.Should().BeEquivalentTo(response); } [Fact] @@ -131,16 +181,20 @@ public async Task GivenRetrieveByIdAsync_WhenRepositoryFindByIdAsyncReturnsPlaye public async Task GivenRetrieveBySquadNumberAsync_WhenRepositoryFindBySquadNumberAsyncReturnsNull_ThenResultShouldBeNull() { // Arrange - var squadNumber = 999; - var (repository, logger, memoryCache) = PlayerMocks.SetupServiceMocks(); + var (repository, logger, memoryCache, mapper) = PlayerMocks.InitServiceMocks(); repository - .Setup(repository => repository.FindBySquadNumberAsync(squadNumber)) + .Setup(repository => repository.FindBySquadNumberAsync(It.IsAny())) .ReturnsAsync((Player?)null); - var service = new PlayerService(repository.Object, logger.Object, memoryCache.Object); + var service = new PlayerService( + repository.Object, + logger.Object, + memoryCache.Object, + mapper.Object + ); // Act - var result = await service.RetrieveBySquadNumberAsync(squadNumber); + var result = await service.RetrieveBySquadNumberAsync(It.IsAny()); // Assert repository.Verify( @@ -155,24 +209,35 @@ public async Task GivenRetrieveBySquadNumberAsync_WhenRepositoryFindBySquadNumbe public async Task GivenRetrieveBySquadNumberAsync_WhenRepositoryFindBySquadNumberAsyncReturnsPlayer_ThenResultShouldBePlayer() { // Arrange - var player = PlayerFakes.CreateOneByIdFromStarting11(9); - var (repository, logger, memoryCache) = PlayerMocks.SetupServiceMocks(); + var id = 9; + var player = PlayerFakes.GetOneExistingById(id); + var response = PlayerFakes.CreateResponseModelForOneExistingById(id); + var (repository, logger, memoryCache, mapper) = PlayerMocks.InitServiceMocks(); repository - .Setup(repository => repository.FindBySquadNumberAsync(player.SquadNumber)) + .Setup(repository => repository.FindBySquadNumberAsync(It.IsAny())) .ReturnsAsync(player); - - var service = new PlayerService(repository.Object, logger.Object, memoryCache.Object); + mapper + .Setup(mapper => mapper.Map(It.IsAny())) + .Returns(response); + + var service = new PlayerService( + repository.Object, + logger.Object, + memoryCache.Object, + mapper.Object + ); // Act - var result = await service.RetrieveBySquadNumberAsync(player.SquadNumber); + var result = await service.RetrieveBySquadNumberAsync(It.IsAny()); // Assert repository.Verify( repository => repository.FindBySquadNumberAsync(It.IsAny()), Times.Once ); - result.Should().BeOfType(); - result.Should().BeEquivalentTo(player); + mapper.Verify(mapper => mapper.Map(It.IsAny()), Times.Once); + result.Should().BeOfType(); + result.Should().BeEquivalentTo(response); } /* ------------------------------------------------------------------------- @@ -184,19 +249,32 @@ public async Task GivenRetrieveBySquadNumberAsync_WhenRepositoryFindBySquadNumbe public async Task GivenUpdateAsync_WhenRepositoryFindByIdAsyncReturnsPlayer_ThenRepositoryUpdateAsyncAndCacheRemove() { // Arrange - var player = PlayerFakes.CreateOneByIdFromStarting11(9); - var (repository, logger, memoryCache) = PlayerMocks.SetupServiceMocks(); - repository.Setup(repository => repository.FindByIdAsync(player.Id)).ReturnsAsync(player); + var id = 9; + var player = PlayerFakes.GetOneExistingById(id); + var request = PlayerFakes.CreateRequestModelForOneExistingById(id); + var (repository, logger, memoryCache, mapper) = PlayerMocks.InitServiceMocks(); + repository + .Setup(repository => repository.FindByIdAsync(It.IsAny())) + .ReturnsAsync(player); - var service = new PlayerService(repository.Object, logger.Object, memoryCache.Object); + var service = new PlayerService( + repository.Object, + logger.Object, + memoryCache.Object, + mapper.Object + ); // Act - await service.UpdateAsync(player); + await service.UpdateAsync(request); // Assert repository.Verify(repository => repository.FindByIdAsync(It.IsAny()), Times.Once); - repository.Verify(repository => repository.UpdateAsync(player), Times.Once); + repository.Verify(repository => repository.UpdateAsync(It.IsAny()), Times.Once); memoryCache.Verify(cache => cache.Remove(It.IsAny()), Times.Once); + mapper.Verify( + mapper => mapper.Map(It.IsAny(), It.IsAny()), + Times.Once + ); } /* ------------------------------------------------------------------------- @@ -208,14 +286,22 @@ public async Task GivenUpdateAsync_WhenRepositoryFindByIdAsyncReturnsPlayer_Then public async Task GivenDeleteAsync_WhenRepositoryFindByIdAsyncReturnsPlayer_ThenRepositoryDeleteAsyncAndCacheRemove() { // Arrange - var player = PlayerFakes.CreateOneNew(); - var (repository, logger, memoryCache) = PlayerMocks.SetupServiceMocks(); - repository.Setup(repository => repository.FindByIdAsync(player.Id)).ReturnsAsync(player); + var id = 9; + var player = PlayerFakes.GetOneExistingById(id); + var (repository, logger, memoryCache, mapper) = PlayerMocks.InitServiceMocks(); + repository + .Setup(repository => repository.FindByIdAsync(It.IsAny())) + .ReturnsAsync(player); - var service = new PlayerService(repository.Object, logger.Object, memoryCache.Object); + var service = new PlayerService( + repository.Object, + logger.Object, + memoryCache.Object, + mapper.Object + ); // Act - await service.DeleteAsync(player.Id); + await service.DeleteAsync(It.IsAny()); // Assert repository.Verify(repository => repository.FindByIdAsync(It.IsAny()), Times.Once); @@ -223,7 +309,7 @@ public async Task GivenDeleteAsync_WhenRepositoryFindByIdAsyncReturnsPlayer_Then memoryCache.Verify(cache => cache.Remove(It.IsAny()), Times.Exactly(1)); } - private async Task ExecutionTimeAsync(Func awaitable) + private static async Task ExecutionTimeAsync(Func awaitable) { var stopwatch = new Stopwatch(); diff --git a/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/DatabaseFakes.cs b/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/DatabaseFakes.cs index a941681..4ac0ea8 100644 --- a/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/DatabaseFakes.cs +++ b/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/DatabaseFakes.cs @@ -48,7 +48,7 @@ CREATE TABLE players ( public static void Seed(this PlayerDbContext context) { - context.Players.AddRange(PlayerFakes.CreateStarting11()); + context.Players.AddRange(PlayerFakes.GetStarting11()); context.SaveChanges(); } } diff --git a/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerFakes.cs b/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerFakes.cs index c5c1c44..0adf0bb 100644 --- a/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerFakes.cs +++ b/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerFakes.cs @@ -1,4 +1,3 @@ -using System.Text.Json; using Dotnet.Samples.AspNetCore.WebApi.Enums; using Dotnet.Samples.AspNetCore.WebApi.Models; @@ -13,28 +12,7 @@ namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities; /// public static class PlayerFakes { - public static Player CreateOneByIdFromStarting11(int id) - { - return CreateStarting11().SingleOrDefault(player => player.Id == id) ?? new(); - } - - public static Player CreateOneNew() => - new() - { - Id = 12, - FirstName = "Leandro", - MiddleName = "Daniel", - LastName = "Paredes", - DateOfBirth = new DateTime(1994, 06, 29, 0, 0, 0, DateTimeKind.Utc), - SquadNumber = 5, - Position = Position.DefensiveMidfield.Text, - AbbrPosition = Position.DefensiveMidfield.Abbr, - Team = "AS Roma", - League = "Serie A", - Starting11 = false - }; - - public static List CreateStarting11() + public static List GetStarting11() { return [ @@ -191,4 +169,124 @@ public static List CreateStarting11() } ]; } + + public static List CreateStarting11ResponseModels() => + [ + .. GetStarting11() + .Select(player => new PlayerResponseModel + { + Id = player.Id, + FullName = + $"{player.FirstName} {(string.IsNullOrWhiteSpace(player.MiddleName) ? "" : player.MiddleName + " ")}{player.LastName}".Trim(), + Birth = $"{player.DateOfBirth:MMMM d, yyyy}", + Dorsal = player.SquadNumber, + Position = player.Position, + Club = player.Team, + League = player.League, + Starting11 = player.Starting11 ? "Yes" : "No" + }) + ]; + + public static Player CreateOneNew() + { + return new() + { + Id = 12, + FirstName = "Leandro", + MiddleName = "Daniel", + LastName = "Paredes", + DateOfBirth = new DateTime(1994, 06, 29, 0, 0, 0, DateTimeKind.Utc), + SquadNumber = 5, + Position = Position.DefensiveMidfield.Text, + AbbrPosition = Position.DefensiveMidfield.Abbr, + Team = "AS Roma", + League = "Serie A", + Starting11 = false + }; + } + + public static PlayerRequestModel CreateRequestModelForOneNew() + { + var player = CreateOneNew(); + + return new() + { + Id = player.Id, + FirstName = player.FirstName, + MiddleName = player.MiddleName, + LastName = player.LastName, + DateOfBirth = player.DateOfBirth, + SquadNumber = player.SquadNumber, + AbbrPosition = player.AbbrPosition, + Team = player.Team, + League = player.League + }; + } + + public static PlayerResponseModel CreateResponseModelForOneNew() + { + var player = CreateOneNew(); + + return new PlayerResponseModel + { + Id = player.Id, + FullName = + $"{player.FirstName} {(string.IsNullOrWhiteSpace(player.MiddleName) ? "" : player.MiddleName + " ")}{player.LastName}".Trim(), + Birth = $"{player.DateOfBirth:MMMM d, yyyy}", + Dorsal = player.SquadNumber, + Position = player.Position, + Club = player.Team, + League = player.League, + Starting11 = player.Starting11 ? "Yes" : "No" + }; + } + + public static Player GetOneExistingById(long id) + { + var player = + GetStarting11().SingleOrDefault(player => player.Id == id) + ?? throw new ArgumentNullException($"Player with ID {id} not found."); + + return player; + } + + public static PlayerRequestModel CreateRequestModelForOneExistingById(long id) + { + var player = + GetStarting11().SingleOrDefault(player => player.Id == id) + ?? throw new ArgumentNullException($"Player with ID {id} not found."); + + return new PlayerRequestModel + { + Id = player.Id, + FirstName = player.FirstName, + MiddleName = player.MiddleName, + LastName = player.LastName, + DateOfBirth = player.DateOfBirth, + SquadNumber = player.SquadNumber, + AbbrPosition = player.AbbrPosition, + Team = player.Team, + League = player.League + }; + } + + public static PlayerResponseModel CreateResponseModelForOneExistingById(long id) + { + var player = + GetStarting11().SingleOrDefault(player => player.Id == id) + ?? throw new ArgumentNullException($"Player with ID {id} not found."); + + return new PlayerResponseModel + { + Id = player.Id, + FullName = + $"{player.FirstName} {(string.IsNullOrWhiteSpace(player.MiddleName) ? "" : player.MiddleName + " ")}{player.LastName}".Trim(), + Birth = $"{player.DateOfBirth:MMMM d, yyyy}", + Dorsal = player.SquadNumber, + Position = player.Position, + Club = player.Team, + League = player.League, + Starting11 = player.Starting11 ? "Yes" : "No" + }; + } } diff --git a/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerMocks.cs b/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerMocks.cs index 735cb15..127db72 100644 --- a/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerMocks.cs +++ b/Dotnet.Samples.AspNetCore.WebApi.Tests/Utilities/PlayerMocks.cs @@ -1,3 +1,4 @@ +using AutoMapper; using Dotnet.Samples.AspNetCore.WebApi.Controllers; using Dotnet.Samples.AspNetCore.WebApi.Data; using Dotnet.Samples.AspNetCore.WebApi.Services; @@ -18,23 +19,40 @@ namespace Dotnet.Samples.AspNetCore.WebApi.Tests.Utilities /// public static class PlayerMocks { - public static Mock ServiceMock() + public static ( + Mock service, + Mock> logger + ) InitControllerMocks() { - return new Mock(); + var service = new Mock(); + var logger = new Mock>(); + + return (service, logger); } - public static Mock RepositoryMock() + public static Mock SetupUrlHelperMock() { - return new Mock(); + var mock = new Mock(); + mock.Setup(url => url.Action(It.IsAny())).Returns(It.IsAny()); + + return mock; } - public static Mock> LoggerMock() - where T : class + public static ( + Mock repository, + Mock> logger, + Mock memoryCache, + Mock mapper + ) InitServiceMocks(object? cacheValue = null) { - return new Mock>(); + var repository = new Mock(); + var logger = new Mock>(); + var memoryCache = SetupMemoryCacheMock(cacheValue ?? It.IsAny()); + var mapper = new Mock(); + return (repository, logger, memoryCache, mapper); } - public static Mock MemoryCacheMock(object? value) + public static Mock SetupMemoryCacheMock(object? value) { var cachedValue = false; var mock = new Mock(); @@ -63,35 +81,5 @@ public static Mock MemoryCacheMock(object? value) } private delegate void TryGetValueDelegate(object key, out object? value); - - public static Mock UrlHelperMock() - { - var mock = new Mock(); - mock.Setup(url => url.Action(It.IsAny())).Returns(It.IsAny()); - - return mock; - } - - public static ( - Mock repository, - Mock> logger, - Mock memoryCache - ) SetupServiceMocks(object? cacheValue = null) - { - var repository = RepositoryMock(); - var logger = LoggerMock(); - var memoryCache = MemoryCacheMock(cacheValue ?? It.IsAny()); - return (repository, logger, memoryCache); - } - - public static ( - Mock service, - Mock> logger - ) SetupControllerMocks(object? cacheValue = null) - { - var service = ServiceMock(); - var logger = LoggerMock(); - return (service, logger); - } } } diff --git a/Dotnet.Samples.AspNetCore.WebApi.Tests/packages.lock.json b/Dotnet.Samples.AspNetCore.WebApi.Tests/packages.lock.json index 8ac1704..f893f0c 100644 --- a/Dotnet.Samples.AspNetCore.WebApi.Tests/packages.lock.json +++ b/Dotnet.Samples.AspNetCore.WebApi.Tests/packages.lock.json @@ -44,6 +44,14 @@ "resolved": "3.0.2", "contentHash": "oXbusR6iPq0xlqoikjdLvzh+wQDkMv9If58myz9MEzldS4nIcp442Btgs2sWbYWV+caEluMe2pQCZ0hUZgPiow==" }, + "AutoMapper": { + "type": "Transitive", + "resolved": "14.0.0", + "contentHash": "OC+1neAPM4oCCqQj3g2GJ2shziNNhOkxmNB9cVS8jtx4JbgmRzLcUOxB9Tsz6cVPHugdkHgCaCrTjjSI0Z5sCQ==", + "dependencies": { + "Microsoft.Extensions.Options": "8.0.0" + } + }, "Azure.Core": { "type": "Transitive", "resolved": "1.38.0", @@ -1806,6 +1814,7 @@ "dotnet.samples.aspnetcore.webapi": { "type": "Project", "dependencies": { + "AutoMapper": "[14.0.0, )", "Microsoft.AspNetCore.OpenApi": "[8.0.14, )", "Microsoft.EntityFrameworkCore.InMemory": "[9.0.3, )", "Microsoft.EntityFrameworkCore.SqlServer": "[9.0.3, )", diff --git a/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs b/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs index 4aa01ee..d982a6b 100644 --- a/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs +++ b/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs @@ -19,18 +19,18 @@ public class PlayerController(IPlayerService playerService, ILogger - /// Creates a Player + /// Creates a new Player /// - /// Player + /// The PlayerRequestModel /// Created /// Bad Request /// Conflict [HttpPost] [Consumes(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status409Conflict)] - public async Task PostAsync([FromBody] Player player) + public async Task PostAsync([FromBody] PlayerRequestModel player) { if (!ModelState.IsValid) { @@ -52,12 +52,12 @@ public async Task PostAsync([FromBody] Player player) * ---------------------------------------------------------------------- */ /// - /// Retrieves all players + /// Retrieves all Players /// /// OK /// Not Found [HttpGet] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetAsync() { @@ -74,13 +74,13 @@ public async Task GetAsync() } /// - /// Retrieves a Player by its Id + /// Retrieves a Player by its ID /// - /// Player.Id + /// The ID of the Player /// OK /// Not Found [HttpGet("{id}")] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetByIdAsync([FromRoute] long id) { @@ -99,11 +99,11 @@ public async Task GetByIdAsync([FromRoute] long id) /// /// Retrieves a Player by its Squad Number /// - /// Player.SquadNumber + /// The Squad Number of the Player /// OK /// Not Found [HttpGet("squadNumber/{squadNumber}")] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetBySquadNumberAsync([FromRoute] int squadNumber) { @@ -124,10 +124,10 @@ public async Task GetBySquadNumberAsync([FromRoute] int squadNumber) * ---------------------------------------------------------------------- */ /// - /// Updates (entirely) a Player by its Id + /// Updates (entirely) a Player by its ID /// - /// Player.Id - /// Player + /// The ID of the Player + /// The PlayerRequestModel /// No Content /// Bad Request /// Not Found @@ -136,7 +136,7 @@ public async Task GetBySquadNumberAsync([FromRoute] int squadNumber) [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task PutAsync([FromRoute] long id, [FromBody] Player player) + public async Task PutAsync([FromRoute] long id, [FromBody] PlayerRequestModel player) { if (!ModelState.IsValid) { @@ -159,9 +159,9 @@ public async Task PutAsync([FromRoute] long id, [FromBody] Player playe * ---------------------------------------------------------------------- */ /// - /// Deletes a Player by its Id + /// Deletes a Player by its ID /// - /// Player.Id + /// The ID of the Player /// No Content /// Not Found [HttpDelete("{id}")] diff --git a/Dotnet.Samples.AspNetCore.WebApi/Data/Repository.cs b/Dotnet.Samples.AspNetCore.WebApi/Data/Repository.cs index c097741..ef698af 100644 --- a/Dotnet.Samples.AspNetCore.WebApi/Data/Repository.cs +++ b/Dotnet.Samples.AspNetCore.WebApi/Data/Repository.cs @@ -20,7 +20,7 @@ public async Task AddAsync(T entity) await _dbContext.SaveChangesAsync(); } - public async Task> GetAllAsync() => await _dbSet.ToListAsync(); + public async Task> GetAllAsync() => await _dbSet.AsNoTracking().ToListAsync(); public async ValueTask FindByIdAsync(long id) => await _dbSet.FindAsync(id); diff --git a/Dotnet.Samples.AspNetCore.WebApi/Dotnet.Samples.AspNetCore.WebApi.csproj b/Dotnet.Samples.AspNetCore.WebApi/Dotnet.Samples.AspNetCore.WebApi.csproj index b50d47c..530815b 100644 --- a/Dotnet.Samples.AspNetCore.WebApi/Dotnet.Samples.AspNetCore.WebApi.csproj +++ b/Dotnet.Samples.AspNetCore.WebApi/Dotnet.Samples.AspNetCore.WebApi.csproj @@ -8,6 +8,7 @@ + diff --git a/Dotnet.Samples.AspNetCore.WebApi/Mappings/PlayerMappingProfile.cs b/Dotnet.Samples.AspNetCore.WebApi/Mappings/PlayerMappingProfile.cs new file mode 100644 index 0000000..e444719 --- /dev/null +++ b/Dotnet.Samples.AspNetCore.WebApi/Mappings/PlayerMappingProfile.cs @@ -0,0 +1,55 @@ +using AutoMapper; +using Dotnet.Samples.AspNetCore.WebApi.Enums; +using Dotnet.Samples.AspNetCore.WebApi.Models; + +namespace Dotnet.Samples.AspNetCore.WebApi.Mappings; + +/// +/// Mapping profile for Player. +/// +/// +/// This class defines the mapping configuration between PlayerRequestModel and Player, +/// and between Player and PlayerResponseModel. +/// +public class PlayerMappingProfile : Profile +{ + public PlayerMappingProfile() + { + // PlayerRequestModel → Player + CreateMap() + .ForMember( + destination => destination.Position, + options => + options.MapFrom(source => + Position.FromAbbr(source.AbbrPosition ?? string.Empty) + ) + ) + .ForMember(destination => destination.Starting11, options => options.Ignore()); + + // Player → PlayerResponseModel + CreateMap() + .ForMember( + destination => destination.FullName, + options => + options.MapFrom(source => + $"{source.FirstName} {(string.IsNullOrWhiteSpace(source.MiddleName) ? "" : source.MiddleName + " ")}{source.LastName}".Trim() + ) + ) + .ForMember( + destination => destination.Birth, + options => options.MapFrom(source => $"{source.DateOfBirth:MMMM d, yyyy}") + ) + .ForMember( + destination => destination.Dorsal, + options => options.MapFrom(source => source.SquadNumber) + ) + .ForMember( + destination => destination.Club, + options => options.MapFrom(source => source.Team) + ) + .ForMember( + destination => destination.Starting11, + options => options.MapFrom(source => source.Starting11 ? "Yes" : "No") + ); + } +} diff --git a/Dotnet.Samples.AspNetCore.WebApi/Models/Player.cs b/Dotnet.Samples.AspNetCore.WebApi/Models/Player.cs index bf9d4d7..1dfef0c 100644 --- a/Dotnet.Samples.AspNetCore.WebApi/Models/Player.cs +++ b/Dotnet.Samples.AspNetCore.WebApi/Models/Player.cs @@ -2,27 +2,28 @@ namespace Dotnet.Samples.AspNetCore.WebApi.Models; +/// +/// Model for Player entity. +/// +/// +/// This class represents the Player entity in the database. +/// public class Player { public long Id { get; set; } - [Required] public string? FirstName { get; set; } public string? MiddleName { get; set; } - [Required] public string? LastName { get; set; } public DateTime? DateOfBirth { get; set; } - [Required] public int SquadNumber { get; set; } - [Required] public string? Position { get; set; } - [Required] public string? AbbrPosition { get; set; } public string? Team { get; set; } diff --git a/Dotnet.Samples.AspNetCore.WebApi/Models/PlayerRequestModel.cs b/Dotnet.Samples.AspNetCore.WebApi/Models/PlayerRequestModel.cs new file mode 100644 index 0000000..f343139 --- /dev/null +++ b/Dotnet.Samples.AspNetCore.WebApi/Models/PlayerRequestModel.cs @@ -0,0 +1,36 @@ +using System.ComponentModel.DataAnnotations; + +namespace Dotnet.Samples.AspNetCore.WebApi.Models; + +/// +/// Model for Player request. +/// +/// +/// This class is used to receive Player data from the client. +/// The properties are decorated with validation attributes to ensure that +/// the required fields are provided and that the data is in the correct format. +/// +public class PlayerRequestModel +{ + public long Id { get; set; } + + [Required] + public string? FirstName { get; set; } + + public string? MiddleName { get; set; } + + [Required] + public string? LastName { get; set; } + + public DateTime? DateOfBirth { get; set; } + + [Required] + public int SquadNumber { get; set; } + + [Required] + public string? AbbrPosition { get; set; } + + public string? Team { get; set; } + + public string? League { get; set; } +} diff --git a/Dotnet.Samples.AspNetCore.WebApi/Models/PlayerResponseModel.cs b/Dotnet.Samples.AspNetCore.WebApi/Models/PlayerResponseModel.cs new file mode 100644 index 0000000..6fd67bf --- /dev/null +++ b/Dotnet.Samples.AspNetCore.WebApi/Models/PlayerResponseModel.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; + +namespace Dotnet.Samples.AspNetCore.WebApi.Models; + +/// +/// Model for Player response. +/// +/// +/// This class is used to send Player data to the client. +/// +public class PlayerResponseModel +{ + public long Id { get; set; } + + public string? FullName { get; set; } + + public string? Birth { get; set; } + + public int Dorsal { get; set; } + + public string? Position { get; set; } + + public string? Club { get; set; } + + public string? League { get; set; } + + public string? Starting11 { get; set; } +} diff --git a/Dotnet.Samples.AspNetCore.WebApi/Program.cs b/Dotnet.Samples.AspNetCore.WebApi/Program.cs index 19cc234..d8c798d 100644 --- a/Dotnet.Samples.AspNetCore.WebApi/Program.cs +++ b/Dotnet.Samples.AspNetCore.WebApi/Program.cs @@ -1,5 +1,6 @@ using System.Reflection; using Dotnet.Samples.AspNetCore.WebApi.Data; +using Dotnet.Samples.AspNetCore.WebApi.Mappings; using Dotnet.Samples.AspNetCore.WebApi.Services; using Microsoft.EntityFrameworkCore; using Microsoft.OpenApi.Models; @@ -45,6 +46,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddMemoryCache(); +builder.Services.AddAutoMapper(typeof(PlayerMappingProfile)); if (builder.Environment.IsDevelopment()) { diff --git a/Dotnet.Samples.AspNetCore.WebApi/Services/IPlayerService.cs b/Dotnet.Samples.AspNetCore.WebApi/Services/IPlayerService.cs index 434efa8..8c2cd92 100644 --- a/Dotnet.Samples.AspNetCore.WebApi/Services/IPlayerService.cs +++ b/Dotnet.Samples.AspNetCore.WebApi/Services/IPlayerService.cs @@ -10,43 +10,43 @@ public interface IPlayerService /// /// Adds a new Player to the repository. /// - /// The Player to create. + /// The Player to create. /// A Task representing the asynchronous operation. - public Task CreateAsync(Player player); + public Task CreateAsync(PlayerRequestModel playerRequestModel); /// /// Retrieves all players from the repository. /// /// A Task representing the asynchronous operation, /// containing a list of all players. - public Task> RetrieveAsync(); + public Task> RetrieveAsync(); /// /// Retrieves a Player from the repository by its ID. /// /// The ID of the Player to retrieve. /// - /// A ValueTask representing the asynchronous operation, containing the Player if found, + /// A Task representing the asynchronous operation, containing the Player if found, /// or null if not. /// - public ValueTask RetrieveByIdAsync(long id); + public Task RetrieveByIdAsync(long id); /// /// Retrieves a Player from the repository by its Squad Number. /// /// The Squad Number of the Player to retrieve. /// - /// A ValueTask representing the asynchronous operation, containing the Player if found, + /// A Task representing the asynchronous operation, containing the Player if found, /// or null if not. /// - public ValueTask RetrieveBySquadNumberAsync(int squadNumber); + public Task RetrieveBySquadNumberAsync(int squadNumber); /// /// Updates (entirely) an existing Player in the repository. /// - /// The Player to update. + /// The Player to update. /// A Task representing the asynchronous operation. - public Task UpdateAsync(Player player); + public Task UpdateAsync(PlayerRequestModel playerRequestModel); /// /// Removes an existing Player from the repository. diff --git a/Dotnet.Samples.AspNetCore.WebApi/Services/PlayerService.cs b/Dotnet.Samples.AspNetCore.WebApi/Services/PlayerService.cs index 25dfd30..cc1e287 100644 --- a/Dotnet.Samples.AspNetCore.WebApi/Services/PlayerService.cs +++ b/Dotnet.Samples.AspNetCore.WebApi/Services/PlayerService.cs @@ -1,6 +1,6 @@ -using Dotnet.Samples.AspNetCore.WebApi.Data; +using AutoMapper; +using Dotnet.Samples.AspNetCore.WebApi.Data; using Dotnet.Samples.AspNetCore.WebApi.Models; -using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; namespace Dotnet.Samples.AspNetCore.WebApi.Services; @@ -8,28 +8,32 @@ namespace Dotnet.Samples.AspNetCore.WebApi.Services; public class PlayerService( IPlayerRepository playerRepository, ILogger logger, - IMemoryCache memoryCache + IMemoryCache memoryCache, + IMapper mapper ) : IPlayerService { - private const string MemoryCache_Key_RetrieveAsync = "MemoryCache_Key_RetrieveAsync"; - private const string AspNetCore_Environment = "ASPNETCORE_ENVIRONMENT"; - private const string Development = "Development"; + private static readonly string CacheKey_RetrieveAsync = nameof(RetrieveAsync); + private static readonly string AspNetCore_Environment = "ASPNETCORE_ENVIRONMENT"; + private static readonly string Development = "Development"; + private readonly IPlayerRepository _playerRepository = playerRepository; private readonly ILogger _logger = logger; private readonly IMemoryCache _memoryCache = memoryCache; + private readonly IMapper _mapper = mapper; /* ------------------------------------------------------------------------- * Create * ---------------------------------------------------------------------- */ - public async Task CreateAsync(Player player) + public async Task CreateAsync(PlayerRequestModel playerRequestModel) { + var player = _mapper.Map(playerRequestModel); await _playerRepository.AddAsync(player); _logger.LogInformation("Player added to Repository: {Player}", player); - _memoryCache.Remove(MemoryCache_Key_RetrieveAsync); + _memoryCache.Remove(CacheKey_RetrieveAsync); _logger.LogInformation( - "Key removed from MemoryCache: {Key}", - MemoryCache_Key_RetrieveAsync + "Removed objects from Cache with Key: {Key}", + CacheKey_RetrieveAsync ); } @@ -37,12 +41,12 @@ public async Task CreateAsync(Player player) * Retrieve * ---------------------------------------------------------------------- */ - public async Task> RetrieveAsync() + public async Task> RetrieveAsync() { - if (_memoryCache.TryGetValue(MemoryCache_Key_RetrieveAsync, out List? players)) + if (_memoryCache.TryGetValue(CacheKey_RetrieveAsync, out List? cached)) { - _logger.LogInformation("Players retrieved from MemoryCache"); - return players!; + _logger.LogInformation("Players retrieved from Cache"); + return cached!; } else { @@ -53,46 +57,52 @@ public async Task> RetrieveAsync() await SimulateRepositoryDelayAsync(); } - players = await _playerRepository.GetAllAsync(); + var players = await _playerRepository.GetAllAsync(); _logger.LogInformation("Players retrieved from Repository"); - - using (var cacheEntry = _memoryCache.CreateEntry(MemoryCache_Key_RetrieveAsync)) + var playerResponseModels = _mapper.Map>(players); + using (var cacheEntry = _memoryCache.CreateEntry(CacheKey_RetrieveAsync)) { _logger.LogInformation( - "{Count} players added to MemoryCache with key {Key}", - players.Count, - MemoryCache_Key_RetrieveAsync + "{Count} entries created in Cache with key: {Key}", + playerResponseModels.Count, + CacheKey_RetrieveAsync ); - cacheEntry.SetSize(players.Count); - cacheEntry.Value = players; + cacheEntry.SetSize(playerResponseModels.Count); + cacheEntry.Value = playerResponseModels; cacheEntry.SetOptions(GetMemoryCacheEntryOptions()); } - return players; + return playerResponseModels; } } - public async ValueTask RetrieveByIdAsync(long id) => - await _playerRepository.FindByIdAsync(id); + public async Task RetrieveByIdAsync(long id) + { + var player = await _playerRepository.FindByIdAsync(id); + return player is not null ? _mapper.Map(player) : null; + } - public async ValueTask RetrieveBySquadNumberAsync(int squadNumber) => - await _playerRepository.FindBySquadNumberAsync(squadNumber); + public async Task RetrieveBySquadNumberAsync(int squadNumber) + { + var player = await _playerRepository.FindBySquadNumberAsync(squadNumber); + return player is not null ? _mapper.Map(player) : null; + } /* ------------------------------------------------------------------------- * Update * ---------------------------------------------------------------------- */ - public async Task UpdateAsync(Player player) + public async Task UpdateAsync(PlayerRequestModel playerRequestModel) { - if (await _playerRepository.FindByIdAsync(player.Id) is Player entity) + if (await _playerRepository.FindByIdAsync(playerRequestModel.Id) is Player player) { - entity.MapFrom(player); - await _playerRepository.UpdateAsync(entity); + _mapper.Map(playerRequestModel, player); + await _playerRepository.UpdateAsync(player); _logger.LogInformation("Player updated in Repository: {Player}", player); - _memoryCache.Remove(MemoryCache_Key_RetrieveAsync); + _memoryCache.Remove(CacheKey_RetrieveAsync); _logger.LogInformation( - "Key removed from MemoryCache: {Key}", - MemoryCache_Key_RetrieveAsync + "Removed objects from Cache with Key: {Key}", + CacheKey_RetrieveAsync ); } } @@ -106,11 +116,11 @@ public async Task DeleteAsync(long id) if (await _playerRepository.FindByIdAsync(id) is not null) { await _playerRepository.RemoveAsync(id); - _logger.LogInformation("Player ID removed from Repository {Id}", id); - _memoryCache.Remove(MemoryCache_Key_RetrieveAsync); + _logger.LogInformation("Player with Id {Id} removed from Repository", id); + _memoryCache.Remove(CacheKey_RetrieveAsync); _logger.LogInformation( - "Key removed from MemoryCache: {Key}", - MemoryCache_Key_RetrieveAsync + "Removed objects from Cache with Key: {Key}", + CacheKey_RetrieveAsync ); } } diff --git a/Dotnet.Samples.AspNetCore.WebApi/packages.lock.json b/Dotnet.Samples.AspNetCore.WebApi/packages.lock.json index ac38665..415077d 100644 --- a/Dotnet.Samples.AspNetCore.WebApi/packages.lock.json +++ b/Dotnet.Samples.AspNetCore.WebApi/packages.lock.json @@ -2,6 +2,15 @@ "version": 1, "dependencies": { "net8.0": { + "AutoMapper": { + "type": "Direct", + "requested": "[14.0.0, )", + "resolved": "14.0.0", + "contentHash": "OC+1neAPM4oCCqQj3g2GJ2shziNNhOkxmNB9cVS8jtx4JbgmRzLcUOxB9Tsz6cVPHugdkHgCaCrTjjSI0Z5sCQ==", + "dependencies": { + "Microsoft.Extensions.Options": "8.0.0" + } + }, "Microsoft.AspNetCore.OpenApi": { "type": "Direct", "requested": "[8.0.14, )", diff --git a/codecov.yml b/codecov.yml index 24feabf..016bfe1 100644 --- a/codecov.yml +++ b/codecov.yml @@ -27,6 +27,7 @@ coverage: ignore: - "Dotnet.Samples.AspNetCore.WebApi/Data" - "Dotnet.Samples.AspNetCore.WebApi/Enums" + - "Dotnet.Samples.AspNetCore.WebApi/Mappings" - "Dotnet.Samples.AspNetCore.WebApi/Migrations" - "Dotnet.Samples.AspNetCore.WebApi/Models" - "Dotnet.Samples.AspNetCore.WebApi/Properties" From ecc10ac52e1eabb95da177dcd6cd6b46a7749a11 Mon Sep 17 00:00:00 2001 From: Nano Taboada <87288+nanotaboada@users.noreply.github.com> Date: Mon, 7 Apr 2025 12:02:18 -0300 Subject: [PATCH 2/2] fix(controllers): ensure consistent return type in PostAsync method --- .../Unit/PlayerControllerTests.cs | 203 ++++++++++-------- .../Unit/PlayerServiceTests.cs | 14 +- .../Controllers/PlayerController.cs | 16 +- .../Services/IPlayerService.cs | 5 +- .../Services/PlayerService.cs | 4 +- 5 files changed, 147 insertions(+), 95 deletions(-) diff --git a/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerControllerTests.cs b/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerControllerTests.cs index 98ff675..4465423 100644 --- a/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerControllerTests.cs +++ b/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerControllerTests.cs @@ -32,11 +32,14 @@ public async Task GivenPostAsync_WhenModelStateIsInvalid_ThenResponseStatusCodeS controller.ModelState.Merge(PlayerStubs.CreateModelError("SquadNumber", "Required")); // Act - var result = await controller.PostAsync(It.IsAny()) as BadRequest; + var result = await controller.PostAsync(It.IsAny()); // Assert - result.Should().NotBeNull().And.BeOfType(); - result?.StatusCode.Should().Be(StatusCodes.Status400BadRequest); + if (result is BadRequest response) + { + response.Should().NotBeNull().And.BeOfType(); + response.StatusCode.Should().Be(StatusCodes.Status400BadRequest); + } } [Fact] @@ -45,21 +48,25 @@ public async Task GivenPostAsync_WhenServiceRetrieveByIdAsyncReturnsPlayer_ThenR { // Arrange var id = 10; + var payload = PlayerFakes.CreateRequestModelForOneExistingById(id); var (service, logger) = PlayerMocks.InitControllerMocks(); service .Setup(service => service.RetrieveByIdAsync(It.IsAny())) .ReturnsAsync(PlayerFakes.CreateResponseModelForOneExistingById(id)); - var payload = PlayerFakes.CreateRequestModelForOneExistingById(id); var controller = new PlayerController(service.Object, logger.Object); // Act - var result = await controller.PostAsync(payload) as Conflict; + var result = await controller.PostAsync(payload); // Assert - service.Verify(service => service.RetrieveByIdAsync(It.IsAny()), Times.Exactly(1)); - result.Should().NotBeNull().And.BeOfType(); - result?.StatusCode.Should().Be(StatusCodes.Status409Conflict); + service.Verify(service => service.RetrieveByIdAsync(It.IsAny()), Times.Once); + service.Verify(service => service.CreateAsync(It.IsAny()), Times.Never); + if (result is Conflict response) + { + response.Should().NotBeNull().And.BeOfType(); + response.StatusCode.Should().Be(StatusCodes.Status409Conflict); + } } [Fact] @@ -67,12 +74,15 @@ public async Task GivenPostAsync_WhenServiceRetrieveByIdAsyncReturnsPlayer_ThenR public async Task GivenPostAsync_WhenServiceRetrieveByIdAsyncReturnsNull_ThenResponseStatusCodeShouldBe201Created() { // Arrange - var request = PlayerFakes.CreateRequestModelForOneNew(); + var payload = PlayerFakes.CreateRequestModelForOneNew(); + var content = PlayerFakes.CreateResponseModelForOneNew(); var (service, logger) = PlayerMocks.InitControllerMocks(); service .Setup(service => service.RetrieveByIdAsync(It.IsAny())) .ReturnsAsync(null as PlayerResponseModel); - service.Setup(service => service.CreateAsync(It.IsAny())); + service + .Setup(service => service.CreateAsync(It.IsAny())) + .ReturnsAsync(content); var controller = new PlayerController(service.Object, logger.Object) { @@ -80,18 +90,19 @@ public async Task GivenPostAsync_WhenServiceRetrieveByIdAsyncReturnsNull_ThenRes }; // Act - var result = await controller.PostAsync(request) as Created; + var result = await controller.PostAsync(payload); // Assert - service.Verify(service => service.RetrieveByIdAsync(It.IsAny()), Times.Exactly(1)); - service.Verify( - service => service.CreateAsync(It.IsAny()), - Times.Exactly(1) - ); - result.Should().NotBeNull().And.BeOfType>(); - result?.StatusCode.Should().Be(StatusCodes.Status201Created); - result?.Value.Should().BeEquivalentTo(request); // Request not mapped to Response - result?.Location.Should().Be($"/players/{request.Id}"); + service.Verify(service => service.RetrieveByIdAsync(It.IsAny()), Times.Once); + service.Verify(service => service.CreateAsync(It.IsAny()), Times.Once); + if (result is CreatedAtRoute response) + { + response.Should().NotBeNull().And.BeOfType>(); + response.StatusCode.Should().Be(StatusCodes.Status201Created); + response.Value.Should().BeEquivalentTo(content); + response.RouteName.Should().Be("GetById"); + response.RouteValues.Should().NotBeNull().And.ContainKey("id"); + } } /* ------------------------------------------------------------------------- @@ -110,14 +121,17 @@ public async Task GivenGetAsync_WhenServiceRetrieveAsyncReturnsListOfPlayers_The var controller = new PlayerController(service.Object, logger.Object); // Act - var result = await controller.GetAsync() as Ok>; + var result = await controller.GetAsync(); // Assert - service.Verify(service => service.RetrieveAsync(), Times.Exactly(1)); - result.Should().NotBeNull().And.BeOfType>>(); - result?.StatusCode.Should().Be(StatusCodes.Status200OK); - result?.Value.Should().NotBeNull().And.BeOfType>(); - result?.Value.Should().BeEquivalentTo(players); + service.Verify(service => service.RetrieveAsync(), Times.Once); + if (result is Ok> response) + { + response.Should().NotBeNull().And.BeOfType>>(); + response.StatusCode.Should().Be(StatusCodes.Status200OK); + response.Value.Should().NotBeNull().And.BeOfType>(); + response.Value.Should().BeEquivalentTo(players); + } } [Fact] @@ -131,12 +145,15 @@ public async Task GivenGetAsync_WhenServiceRetrieveAsyncReturnsEmptyList_ThenRes var controller = new PlayerController(service.Object, logger.Object); // Act - var result = await controller.GetAsync() as NotFound; + var result = await controller.GetAsync(); // Assert - service.Verify(service => service.RetrieveAsync(), Times.Exactly(1)); - result.Should().NotBeNull().And.BeOfType(); - result?.StatusCode.Should().Be(StatusCodes.Status404NotFound); + service.Verify(service => service.RetrieveAsync(), Times.Once); + if (result is NotFound response) + { + response.Should().NotBeNull().And.BeOfType(); + response.StatusCode.Should().Be(StatusCodes.Status404NotFound); + } } [Fact] @@ -152,12 +169,15 @@ public async Task GivenGetByIdAsync_WhenServiceRetrieveByIdAsyncReturnsNull_Then var controller = new PlayerController(service.Object, logger.Object); // Act - var result = await controller.GetByIdAsync(It.IsAny()) as NotFound; + var result = await controller.GetByIdAsync(It.IsAny()); // Assert - service.Verify(service => service.RetrieveByIdAsync(It.IsAny()), Times.Exactly(1)); - result.Should().NotBeNull().And.BeOfType(); - result?.StatusCode.Should().Be(StatusCodes.Status404NotFound); + service.Verify(service => service.RetrieveByIdAsync(It.IsAny()), Times.Once); + if (result is NotFound response) + { + response.Should().NotBeNull().And.BeOfType(); + response.StatusCode.Should().Be(StatusCodes.Status404NotFound); + } } [Fact] @@ -172,14 +192,17 @@ public async Task GivenGetByIdAsync_WhenServiceRetrieveByIdAsyncReturnsPlayer_Th var controller = new PlayerController(service.Object, logger.Object); // Act - var result = await controller.GetByIdAsync(It.IsAny()) as Ok; + var result = await controller.GetByIdAsync(It.IsAny()); // Assert - service.Verify(service => service.RetrieveByIdAsync(It.IsAny()), Times.Exactly(1)); - result.Should().NotBeNull().And.BeOfType>(); - result?.StatusCode.Should().Be(StatusCodes.Status200OK); - result?.Value.Should().NotBeNull().And.BeOfType(); - result?.Value.Should().BeEquivalentTo(player); + service.Verify(service => service.RetrieveByIdAsync(It.IsAny()), Times.Once); + if (result is Ok response) + { + response.Should().NotBeNull().And.BeOfType>(); + response.StatusCode.Should().Be(StatusCodes.Status200OK); + response.Value.Should().NotBeNull().And.BeOfType(); + response.Value.Should().BeEquivalentTo(player); + } } [Fact] @@ -195,15 +218,15 @@ public async Task GivenGetBySquadNumberAsync_WhenServiceRetrieveBySquadNumberAsy var controller = new PlayerController(service.Object, logger.Object); // Act - var result = await controller.GetBySquadNumberAsync(It.IsAny()) as NotFound; + var result = await controller.GetBySquadNumberAsync(It.IsAny()); // Assert - service.Verify( - service => service.RetrieveBySquadNumberAsync(It.IsAny()), - Times.Exactly(1) - ); - result.Should().NotBeNull().And.BeOfType(); - result?.StatusCode.Should().Be(StatusCodes.Status404NotFound); + service.Verify(service => service.RetrieveBySquadNumberAsync(It.IsAny()), Times.Once); + if (result is NotFound response) + { + response.Should().NotBeNull().And.BeOfType(); + response.StatusCode.Should().Be(StatusCodes.Status404NotFound); + } } [Fact] @@ -220,18 +243,17 @@ public async Task GivenGetBySquadNumberAsync_WhenServiceRetrieveBySquadNumberAsy var controller = new PlayerController(service.Object, logger.Object); // Act - var result = - await controller.GetBySquadNumberAsync(It.IsAny()) as Ok; + var result = await controller.GetBySquadNumberAsync(It.IsAny()); // Assert - service.Verify( - service => service.RetrieveBySquadNumberAsync(It.IsAny()), - Times.Exactly(1) - ); - result.Should().NotBeNull().And.BeOfType>(); - result?.StatusCode.Should().Be(StatusCodes.Status200OK); - result?.Value.Should().NotBeNull().And.BeOfType(); - result?.Value.Should().BeEquivalentTo(player); + service.Verify(service => service.RetrieveBySquadNumberAsync(It.IsAny()), Times.Once); + if (result is Ok response) + { + response.Should().NotBeNull().And.BeOfType>(); + response.StatusCode.Should().Be(StatusCodes.Status200OK); + response.Value.Should().NotBeNull().And.BeOfType(); + response.Value.Should().BeEquivalentTo(player); + } } /* ------------------------------------------------------------------------- @@ -249,13 +271,16 @@ public async Task GivenPutAsync_WhenModelStateIsInvalid_ThenResponseStatusCodeSh controller.ModelState.Merge(PlayerStubs.CreateModelError("SquadNumber", "Required")); // Act - var result = - await controller.PutAsync(It.IsAny(), It.IsAny()) - as BadRequest; + var result = await controller.PutAsync(It.IsAny(), It.IsAny()); // Assert - result.Should().NotBeNull().And.BeOfType(); - result?.StatusCode.Should().Be(StatusCodes.Status400BadRequest); + service.Verify(service => service.RetrieveByIdAsync(It.IsAny()), Times.Never); + service.Verify(service => service.UpdateAsync(It.IsAny()), Times.Never); + if (result is BadRequest response) + { + response.Should().NotBeNull().And.BeOfType(); + response.StatusCode.Should().Be(StatusCodes.Status400BadRequest); + } } [Fact] @@ -271,13 +296,16 @@ public async Task GivenPutAsync_WhenServiceRetrieveByIdAsyncReturnsNull_ThenResp var controller = new PlayerController(service.Object, logger.Object); // Act - var result = - await controller.PutAsync(It.IsAny(), It.IsAny()) as NotFound; + var result = await controller.PutAsync(It.IsAny(), It.IsAny()); // Assert - service.Verify(service => service.RetrieveByIdAsync(It.IsAny()), Times.Exactly(1)); - result.Should().NotBeNull().And.BeOfType(); - result?.StatusCode.Should().Be(StatusCodes.Status404NotFound); + service.Verify(service => service.RetrieveByIdAsync(It.IsAny()), Times.Once); + service.Verify(service => service.UpdateAsync(It.IsAny()), Times.Never); + if (result is NotFound response) + { + response.Should().NotBeNull().And.BeOfType(); + response.StatusCode.Should().Be(StatusCodes.Status404NotFound); + } } [Fact] @@ -296,16 +324,16 @@ public async Task GivenPutAsync_WhenServiceRetrieveByIdAsyncReturnsPlayer_ThenRe var controller = new PlayerController(service.Object, logger.Object); // Act - var result = await controller.PutAsync(id, player) as NoContent; + var result = await controller.PutAsync(id, player); // Assert - service.Verify(service => service.RetrieveByIdAsync(It.IsAny()), Times.Exactly(1)); - service.Verify( - service => service.UpdateAsync(It.IsAny()), - Times.Exactly(1) - ); - result.Should().NotBeNull().And.BeOfType(); - result?.StatusCode.Should().Be(StatusCodes.Status204NoContent); + service.Verify(service => service.RetrieveByIdAsync(It.IsAny()), Times.Once); + service.Verify(service => service.UpdateAsync(It.IsAny()), Times.Once); + if (result is NoContent response) + { + response.Should().NotBeNull().And.BeOfType(); + response.StatusCode.Should().Be(StatusCodes.Status204NoContent); + } } /* ------------------------------------------------------------------------- @@ -325,12 +353,16 @@ public async Task GivenDeleteAsync_WhenServiceRetrieveByIdAsyncReturnsNull_ThenR var controller = new PlayerController(service.Object, logger.Object); // Act - var result = await controller.DeleteAsync(It.IsAny()) as NotFound; + var result = await controller.DeleteAsync(It.IsAny()); // Assert - service.Verify(service => service.RetrieveByIdAsync(It.IsAny()), Times.Exactly(1)); - result.Should().NotBeNull().And.BeOfType(); - result?.StatusCode.Should().Be(StatusCodes.Status404NotFound); + service.Verify(service => service.RetrieveByIdAsync(It.IsAny()), Times.Once); + service.Verify(service => service.DeleteAsync(It.IsAny()), Times.Never); + if (result is NotFound response) + { + response.Should().NotBeNull().And.BeOfType(); + response.StatusCode.Should().Be(StatusCodes.Status404NotFound); + } } [Fact] @@ -347,13 +379,16 @@ public async Task GivenDeleteAsync_WhenServiceRetrieveByIdAsyncReturnsPlayer_The var controller = new PlayerController(service.Object, logger.Object); // Act - var result = await controller.DeleteAsync(It.IsAny()) as NoContent; + var result = await controller.DeleteAsync(It.IsAny()); // Assert - service.Verify(service => service.RetrieveByIdAsync(It.IsAny()), Times.Exactly(1)); - service.Verify(service => service.DeleteAsync(It.IsAny()), Times.Exactly(1)); - result.Should().NotBeNull().And.BeOfType(); - result?.StatusCode.Should().Be(StatusCodes.Status204NoContent); + service.Verify(service => service.RetrieveByIdAsync(It.IsAny()), Times.Once); + service.Verify(service => service.DeleteAsync(It.IsAny()), Times.Once); + if (result is NoContent response) + { + response.Should().NotBeNull().And.BeOfType(); + response.StatusCode.Should().Be(StatusCodes.Status204NoContent); + } } protected virtual void Dispose(bool disposing) diff --git a/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerServiceTests.cs b/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerServiceTests.cs index 512d942..a841e30 100644 --- a/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerServiceTests.cs +++ b/Dotnet.Samples.AspNetCore.WebApi.Tests/Unit/PlayerServiceTests.cs @@ -25,7 +25,11 @@ public PlayerServiceTests() public async Task GivenCreateAsync_WhenRepositoryAddAsync_ThenAddsPlayerToRepositoryAndRemovesCache() { // Arrange + var response = PlayerFakes.CreateResponseModelForOneExistingById(9); var (repository, logger, memoryCache, mapper) = PlayerMocks.InitServiceMocks(); + mapper + .Setup(mapper => mapper.Map(It.IsAny())) + .Returns(response); var service = new PlayerService( repository.Object, @@ -40,6 +44,10 @@ public async Task GivenCreateAsync_WhenRepositoryAddAsync_ThenAddsPlayerToReposi // Assert repository.Verify(repository => repository.AddAsync(It.IsAny()), Times.Once); memoryCache.Verify(cache => cache.Remove(It.IsAny()), Times.Once); + mapper.Verify( + mapper => mapper.Map(It.IsAny()), + Times.Once + ); } /* ------------------------------------------------------------------------- @@ -111,6 +119,8 @@ public async Task GivenRetrieveAsync_WhenExecutedForTheSecondTime_ThenSecondExec cache => cache.TryGetValue(It.IsAny(), out value), Times.Exactly(2) // first + second ); + memoryCache.Verify(cache => cache.CreateEntry(It.IsAny()), Times.Once); // first only + repository.Verify(repository => repository.GetAllAsync(), Times.Once); // first only mapper.Verify( mapper => mapper.Map>(It.IsAny>()), Times.Once // first only @@ -140,6 +150,7 @@ public async Task GivenRetrieveByIdAsync_WhenRepositoryFindByIdAsyncReturnsNull_ // Assert repository.Verify(repository => repository.FindByIdAsync(It.IsAny()), Times.Once); + mapper.Verify(mapper => mapper.Map(It.IsAny()), Times.Never); result.Should().BeNull(); } @@ -201,6 +212,7 @@ public async Task GivenRetrieveBySquadNumberAsync_WhenRepositoryFindBySquadNumbe repository => repository.FindBySquadNumberAsync(It.IsAny()), Times.Once ); + mapper.Verify(mapper => mapper.Map(It.IsAny()), Times.Never); result.Should().BeNull(); } @@ -306,7 +318,7 @@ public async Task GivenDeleteAsync_WhenRepositoryFindByIdAsyncReturnsPlayer_Then // Assert repository.Verify(repository => repository.FindByIdAsync(It.IsAny()), Times.Once); repository.Verify(repository => repository.RemoveAsync(It.IsAny()), Times.Once); - memoryCache.Verify(cache => cache.Remove(It.IsAny()), Times.Exactly(1)); + memoryCache.Verify(cache => cache.Remove(It.IsAny()), Times.Once); } private static async Task ExecutionTimeAsync(Func awaitable) diff --git a/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs b/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs index d982a6b..e32c5cb 100644 --- a/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs +++ b/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs @@ -27,7 +27,7 @@ public class PlayerController(IPlayerService playerService, ILoggerConflict [HttpPost] [Consumes(MediaTypeNames.Application.Json)] - [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status409Conflict)] public async Task PostAsync([FromBody] PlayerRequestModel player) @@ -42,8 +42,12 @@ public async Task PostAsync([FromBody] PlayerRequestModel player) } else { - await _playerService.CreateAsync(player); - return TypedResults.Created($"/players/{player.Id}", player); + var result = await _playerService.CreateAsync(player); + return TypedResults.CreatedAtRoute( + routeName: "GetById", + routeValues: new { id = result.Id }, + value: result + ); } } @@ -79,7 +83,7 @@ public async Task GetAsync() /// The ID of the Player /// OK /// Not Found - [HttpGet("{id}")] + [HttpGet("{id:long}", Name = "GetById")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetByIdAsync([FromRoute] long id) @@ -102,7 +106,7 @@ public async Task GetByIdAsync([FromRoute] long id) /// The Squad Number of the Player /// OK /// Not Found - [HttpGet("squadNumber/{squadNumber}")] + [HttpGet("squad/{squadNumber:int}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task GetBySquadNumberAsync([FromRoute] int squadNumber) @@ -164,7 +168,7 @@ public async Task PutAsync([FromRoute] long id, [FromBody] PlayerReques /// The ID of the Player /// No Content /// Not Found - [HttpDelete("{id}")] + [HttpDelete("{id:long}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task DeleteAsync([FromRoute] long id) diff --git a/Dotnet.Samples.AspNetCore.WebApi/Services/IPlayerService.cs b/Dotnet.Samples.AspNetCore.WebApi/Services/IPlayerService.cs index 8c2cd92..45f0e8e 100644 --- a/Dotnet.Samples.AspNetCore.WebApi/Services/IPlayerService.cs +++ b/Dotnet.Samples.AspNetCore.WebApi/Services/IPlayerService.cs @@ -11,8 +11,9 @@ public interface IPlayerService /// Adds a new Player to the repository. /// /// The Player to create. - /// A Task representing the asynchronous operation. - public Task CreateAsync(PlayerRequestModel playerRequestModel); + /// A Task representing the asynchronous operation, + /// containing the created Player. + public Task CreateAsync(PlayerRequestModel playerRequestModel); /// /// Retrieves all players from the repository. diff --git a/Dotnet.Samples.AspNetCore.WebApi/Services/PlayerService.cs b/Dotnet.Samples.AspNetCore.WebApi/Services/PlayerService.cs index cc1e287..d5a6b79 100644 --- a/Dotnet.Samples.AspNetCore.WebApi/Services/PlayerService.cs +++ b/Dotnet.Samples.AspNetCore.WebApi/Services/PlayerService.cs @@ -25,7 +25,7 @@ IMapper mapper * Create * ---------------------------------------------------------------------- */ - public async Task CreateAsync(PlayerRequestModel playerRequestModel) + public async Task CreateAsync(PlayerRequestModel playerRequestModel) { var player = _mapper.Map(playerRequestModel); await _playerRepository.AddAsync(player); @@ -35,6 +35,7 @@ public async Task CreateAsync(PlayerRequestModel playerRequestModel) "Removed objects from Cache with Key: {Key}", CacheKey_RetrieveAsync ); + return _mapper.Map(player); } /* ------------------------------------------------------------------------- @@ -71,7 +72,6 @@ public async Task> RetrieveAsync() cacheEntry.Value = playerResponseModels; cacheEntry.SetOptions(GetMemoryCacheEntryOptions()); } - return playerResponseModels; } }