From e1de9929185d94c5920abcc0af80416bed0a180a Mon Sep 17 00:00:00 2001 From: Gramli Date: Mon, 11 Mar 2024 21:41:08 +0100 Subject: [PATCH 1/3] implementation of new endpoint for delete favorites --- src/Tests/HttpDebug/debug-tests.http | 10 +++-- .../WeatherSystemTests.cs | 29 ++++++++++++-- .../Commands/AddFavoriteHandlerTests.cs | 4 +- .../Queries/GetFavoritesHandlerTests.cs | 33 +++++++-------- .../EndpointBuilders/WeatherBuilder.cs | 17 +++++--- .../Abstractions/IAddFavoriteHandler.cs | 2 +- .../Abstractions/IDeleteFavoriteHandler.cs | 8 ++++ .../IWeatherCommandsRepository.cs | 1 + .../Abstractions/IWeatherQueriesRepository.cs | 5 +-- .../Commands/AddFavoriteHandler.cs | 8 ++-- .../Commands/DeleteFavoriteHandler.cs | 40 +++++++++++++++++++ .../ContainerConfigurationExtension.cs | 6 ++- .../Queries/GetFavoritesHandler.cs | 15 +++++-- ...eleteFavoriteCommandSpecificationHolder.cs | 18 +++++++++ .../Validation/GeneralPredicates.cs | 4 +- src/Weather.Core/Weather.Core.csproj | 4 -- .../BusinessEntities/FavoriteLocation.cs | 9 +++++ .../Commands/DeleteFavoriteCommand.cs | 7 ++++ src/Weather.Domain/Dtos/CurrentWeatherDto.cs | 2 +- .../Dtos/FavoriteCurrentWeatherDto.cs | 7 ++++ .../Dtos/FavoritesWeatherDto.cs | 2 +- src/Weather.Domain/Dtos/LocationDto.cs | 2 +- .../Repositories/WeatherCommandsRepository.cs | 16 ++++++++ .../Repositories/WeatherQueriesRepository.cs | 12 +++--- .../Profiles/WeatherEntitiesProfile.cs | 3 +- 25 files changed, 203 insertions(+), 61 deletions(-) create mode 100644 src/Weather.Core/Abstractions/IDeleteFavoriteHandler.cs create mode 100644 src/Weather.Core/Commands/DeleteFavoriteHandler.cs create mode 100644 src/Weather.Core/Validation/DeleteFavoriteCommandSpecificationHolder.cs create mode 100644 src/Weather.Domain/BusinessEntities/FavoriteLocation.cs create mode 100644 src/Weather.Domain/Commands/DeleteFavoriteCommand.cs create mode 100644 src/Weather.Domain/Dtos/FavoriteCurrentWeatherDto.cs diff --git a/src/Tests/HttpDebug/debug-tests.http b/src/Tests/HttpDebug/debug-tests.http index ecc6bf3..9f9828a 100644 --- a/src/Tests/HttpDebug/debug-tests.http +++ b/src/Tests/HttpDebug/debug-tests.http @@ -3,11 +3,11 @@ @host={{hostname}}:{{port}} ### get current weather request -GET https://{{host}}/weather/v1/current?latitude=38.5&longtitude=-78.5 +GET https://{{host}}/weather/v1/current?latitude=38.5&longitude=-78.5 Content-Type: application/json ### get forecast weather request -GET https://{{host}}/weather/v1/forecast?latitude=38.5&longtitude=-78.5 +GET https://{{host}}/weather/v1/forecast?latitude=38.5&longitude=-78.5 Content-Type: application/json ### get favorites weather request @@ -23,4 +23,8 @@ Content-Type: application/json "latitude": 38.5, "longitude": -78.5 } -} \ No newline at end of file +} + +### add favorites weather request +DELETE https://{{host}}/weather/v1/favorite/1 +Content-Type: application/json \ No newline at end of file diff --git a/src/Tests/SystemTests/Weather.API.SystemTests/WeatherSystemTests.cs b/src/Tests/SystemTests/Weather.API.SystemTests/WeatherSystemTests.cs index 4d13c15..c122239 100644 --- a/src/Tests/SystemTests/Weather.API.SystemTests/WeatherSystemTests.cs +++ b/src/Tests/SystemTests/Weather.API.SystemTests/WeatherSystemTests.cs @@ -10,7 +10,7 @@ namespace Weather.API.SystemTests public class WeatherSystemTests { private readonly double latitude = 38.5; - private readonly double longtitude = -78.5; + private readonly double longitude = -78.5; private readonly string cityName = "Stanley"; private readonly HttpClient _httpClient; @@ -26,7 +26,7 @@ public async Task GetCurrentWeather() { //Arrange //Act - var response = await _httpClient.GetAsync($"weather/v1/current?latitude={latitude}&longtitude={longtitude}"); + var response = await _httpClient.GetAsync($"weather/v1/current?latitude={latitude}&longitude={longitude}"); //Assert response.EnsureSuccessStatusCode(); @@ -41,7 +41,7 @@ public async Task GetForecastWeather() { //Arrange //Act - var response = await _httpClient.GetAsync($"weather/v1/forecast?latitude={latitude}&longtitude={longtitude}"); + var response = await _httpClient.GetAsync($"weather/v1/forecast?latitude={latitude}&longitude={longitude}"); //Assert response.EnsureSuccessStatusCode(); @@ -82,6 +82,27 @@ public async Task GetWeatherFavorites() Assert.Equal(cityName, resultDto.Data.FavoriteWeathers.First().CityName); } + [Fact] + public async Task DeleteWeatherFavorites() + { + //Arrange + var addResponse = await AddFavorite(); + + addResponse.EnsureSuccessStatusCode(); + + var content = await addResponse.Content.ReadAsStringAsync(); + var addResult = JsonConvert.DeserializeObject>(content); + //Act + var response = await _httpClient.DeleteAsync($"weather/v1/favorite/{addResult!.Data}"); + + //Assert + response.EnsureSuccessStatusCode(); + var stringResult = await response.Content.ReadAsStringAsync(); + var resultDto = JsonConvert.DeserializeObject>(stringResult); + Assert.NotNull(resultDto?.Data); + Assert.True(resultDto.Data); + } + private async Task AddFavorite() { //Arrange @@ -90,7 +111,7 @@ private async Task AddFavorite() Location = new LocationDto { Latitude = latitude, - Longitude = longtitude, + Longitude = longitude, } }); var content = new StringContent(body, Encoding.UTF8, "application/json"); diff --git a/src/Tests/UnitTests/Weather.Core.UnitTests/Commands/AddFavoriteHandlerTests.cs b/src/Tests/UnitTests/Weather.Core.UnitTests/Commands/AddFavoriteHandlerTests.cs index 188256a..f609c4a 100644 --- a/src/Tests/UnitTests/Weather.Core.UnitTests/Commands/AddFavoriteHandlerTests.cs +++ b/src/Tests/UnitTests/Weather.Core.UnitTests/Commands/AddFavoriteHandlerTests.cs @@ -38,7 +38,6 @@ public async Task InvalidLocation() //Assert Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode); Assert.Single(result.Errors); - Assert.False(result.Data); _addFavoriteCommandValidatorMock.Verify(x => x.IsValid(It.Is(y => y.Equals(addFavoriteCommand))), Times.Once); } @@ -58,7 +57,6 @@ public async Task AddFavoriteLocation_Failed() //Assert Assert.Equal(HttpStatusCode.InternalServerError, result.StatusCode); Assert.Single(result.Errors); - Assert.False(result.Data); Assert.Equal(ErrorMessages.CantStoreLocation, result.Errors.Single()); _addFavoriteCommandValidatorMock.Verify(x => x.IsValid(It.Is(y => y.Equals(addFavoriteCommand))), Times.Once); _weatherCommandsRepositoryMock.Verify(x => x.AddFavoriteLocation(It.Is(y=>y.Equals(addFavoriteCommand)), It.IsAny()), Times.Once); @@ -81,7 +79,7 @@ public async Task Success() //Assert Assert.Equal(HttpStatusCode.OK, result.StatusCode); Assert.Empty(result.Errors); - Assert.True(result.Data); + Assert.Equal(locationId, result.Data); _addFavoriteCommandValidatorMock.Verify(x => x.IsValid(It.Is(y => y.Equals(addFavoriteCommand))), Times.Once); _weatherCommandsRepositoryMock.Verify(x => x.AddFavoriteLocation(It.Is(y => y.Equals(addFavoriteCommand)), It.IsAny()), Times.Once); } diff --git a/src/Tests/UnitTests/Weather.Core.UnitTests/Queries/GetFavoritesHandlerTests.cs b/src/Tests/UnitTests/Weather.Core.UnitTests/Queries/GetFavoritesHandlerTests.cs index 076244a..e4200eb 100644 --- a/src/Tests/UnitTests/Weather.Core.UnitTests/Queries/GetFavoritesHandlerTests.cs +++ b/src/Tests/UnitTests/Weather.Core.UnitTests/Queries/GetFavoritesHandlerTests.cs @@ -1,6 +1,7 @@ using Weather.Core.Abstractions; using Weather.Core.Queries; using Weather.Core.Resources; +using Weather.Domain.BusinessEntities; using Weather.Domain.Dtos; using Weather.Domain.Http; using Weather.Domain.Logging; @@ -37,7 +38,7 @@ public async Task GetFavorites_Empty() { //Arrange _weatherRepositoryMock.Setup(x => x.GetFavorites(It.IsAny())) - .ReturnsAsync(new List()); + .ReturnsAsync(new List()); //Act var result = await _uut.HandleAsync(EmptyRequest.Instance, CancellationToken.None); @@ -52,10 +53,10 @@ public async Task GetFavorites_Empty() public async Task InvalidLocation() { //Arrange - var locationDto = new LocationDto { Latitude = 1, Longitude = 1 }; + var locationDto = new FavoriteLocation { Id =0, Latitude = 1, Longitude = 1 }; _weatherRepositoryMock.Setup(x => x.GetFavorites(It.IsAny())) - .ReturnsAsync(new List + .ReturnsAsync(new List { locationDto, }); @@ -79,10 +80,10 @@ public async Task EmptyResult_GetCurrentWeather_Fail() { //Arrange var failMessage = "Some fail message"; - var locationDto = new LocationDto { Latitude = 1, Longitude = 1 }; + var locationDto = new FavoriteLocation { Id = 0, Latitude = 1, Longitude = 1 }; _weatherRepositoryMock.Setup(x => x.GetFavorites(It.IsAny())) - .ReturnsAsync(new List + .ReturnsAsync(new List { locationDto, }); @@ -107,13 +108,13 @@ public async Task One_Of_GetCurrentWeather_Failed() { //Arrange var failMessage = "Some fail message"; - var locationDto = new LocationDto { Latitude = 1, Longitude = 1 }; + var locationDto = new FavoriteLocation { Id = 0, Latitude = 1, Longitude = 1 }; _weatherRepositoryMock.Setup(x => x.GetFavorites(It.IsAny())) - .ReturnsAsync(new List + .ReturnsAsync(new List { locationDto, - new LocationDto(), + new FavoriteLocation(), }); _locationValidatorMock.Setup(x => x.IsValid(It.IsAny())).Returns(true); @@ -122,8 +123,8 @@ public async Task One_Of_GetCurrentWeather_Failed() _currentWeatherValidatorMock.Setup(x => x.IsValid(It.IsAny())).Returns(true); - _weatherServiceMock.Setup(x => x.GetCurrentWeather(It.Is(y=> y.Equals(locationDto)), It.IsAny())).ReturnsAsync(Result.Fail(failMessage)); - _weatherServiceMock.Setup(x => x.GetCurrentWeather(It.Is(y => !y.Equals(locationDto)), It.IsAny())).ReturnsAsync(Result.Ok(currentWeather)); + _weatherServiceMock.Setup(x => x.GetCurrentWeather(It.Is(y=> y.Equals(locationDto)), It.IsAny())).ReturnsAsync(Result.Fail(failMessage)); + _weatherServiceMock.Setup(x => x.GetCurrentWeather(It.Is(y => !y.Equals(locationDto)), It.IsAny())).ReturnsAsync(Result.Ok(currentWeather)); //Act var result = await _uut.HandleAsync(EmptyRequest.Instance, CancellationToken.None); @@ -132,7 +133,7 @@ public async Task One_Of_GetCurrentWeather_Failed() Assert.Single(result.Errors); Assert.NotNull(result.Data); Assert.Single(result.Data.FavoriteWeathers); - Assert.Equal(currentWeather, result.Data.FavoriteWeathers.Single()); + Assert.Equal(currentWeather.CityName, result.Data.FavoriteWeathers.Single().CityName); _weatherRepositoryMock.Verify(x => x.GetFavorites(It.IsAny()), Times.Once); _weatherServiceMock.Verify(x => x.GetCurrentWeather(It.IsAny(), It.IsAny()), Times.Exactly(2)); _loggerMock.VerifyLog(LogLevel.Warning, LogEvents.FavoriteWeathersGeneral, failMessage, Times.Once()); @@ -144,10 +145,10 @@ public async Task One_Of_GetCurrentWeather_Failed() public async Task GetCurrentWeather_Validation_Fail() { //Arrange - var locationDto = new LocationDto { Latitude = 1, Longitude = 1 }; + var locationDto = new FavoriteLocation { Id = 0, Latitude = 1, Longitude = 1 }; _weatherRepositoryMock.Setup(x => x.GetFavorites(It.IsAny())) - .ReturnsAsync(new List + .ReturnsAsync(new List { locationDto, }); @@ -174,10 +175,10 @@ public async Task GetCurrentWeather_Validation_Fail() public async Task Success() { //Arrange - var locationDto = new LocationDto { Latitude = 1, Longitude = 1 }; + var locationDto = new FavoriteLocation { Latitude = 1, Longitude = 1 }; _weatherRepositoryMock.Setup(x => x.GetFavorites(It.IsAny())) - .ReturnsAsync(new List + .ReturnsAsync(new List { locationDto, }); @@ -195,7 +196,7 @@ public async Task Success() Assert.Empty(result.Errors); Assert.NotNull(result.Data); Assert.Single(result.Data.FavoriteWeathers); - Assert.Equal(currentWeather, result.Data.FavoriteWeathers.Single()); + Assert.Equal(currentWeather.CityName, result.Data.FavoriteWeathers.Single().CityName); _weatherRepositoryMock.Verify(x => x.GetFavorites(It.IsAny()), Times.Once); _weatherServiceMock.Verify(x => x.GetCurrentWeather(It.Is(y=>y.Equals(locationDto)), It.IsAny()), Times.Once); _locationValidatorMock.Verify(x => x.IsValid(It.Is(y => y.Equals(locationDto))), Times.Once); diff --git a/src/Weather.API/EndpointBuilders/WeatherBuilder.cs b/src/Weather.API/EndpointBuilders/WeatherBuilder.cs index 5e7c66d..0c4222a 100644 --- a/src/Weather.API/EndpointBuilders/WeatherBuilder.cs +++ b/src/Weather.API/EndpointBuilders/WeatherBuilder.cs @@ -25,8 +25,8 @@ public static IEndpointRouteBuilder BuildWeatherEndpoints(this IEndpointRouteBui private static IEndpointRouteBuilder BuildActualWeatherEndpoints(this IEndpointRouteBuilder endpointRouteBuilder) { endpointRouteBuilder.MapGet("v1/current", - async (double latitude, double longtitude, [FromServices] IGetCurrentWeatherHandler handler, CancellationToken cancellationToken) => - await handler.SendAsync(new GetCurrentWeatherQuery(latitude,longtitude), cancellationToken)) + async (double latitude, double longitude, [FromServices] IGetCurrentWeatherHandler handler, CancellationToken cancellationToken) => + await handler.SendAsync(new GetCurrentWeatherQuery(latitude, longitude), cancellationToken)) .Produces>() .WithName("GetCurrentWeather") .WithTags("Getters"); @@ -36,8 +36,8 @@ await handler.SendAsync(new GetCurrentWeatherQuery(latitude,longtitude), cancell private static IEndpointRouteBuilder BuildForecastWeatherEndpoints(this IEndpointRouteBuilder endpointRouteBuilder) { endpointRouteBuilder.MapGet("v1/forecast", - async (double latitude, double longtitude, [FromServices] IGetForecastWeatherHandler handler, CancellationToken cancellationToken) => - await handler.SendAsync(new GetForecastWeatherQuery(latitude, longtitude), cancellationToken)) + async (double latitude, double longitude, [FromServices] IGetForecastWeatherHandler handler, CancellationToken cancellationToken) => + await handler.SendAsync(new GetForecastWeatherQuery(latitude, longitude), cancellationToken)) .Produces>() .WithName("GetForecastWeather") .WithTags("Getters"); @@ -57,10 +57,17 @@ await handler.SendAsync(EmptyRequest.Instance, cancellationToken)) endpointRouteBuilder.MapPost("v1/favorite", async ([FromBody] AddFavoriteCommand addFavoriteCommand, [FromServices] IAddFavoriteHandler handler, CancellationToken cancellationToken) => await handler.SendAsync(addFavoriteCommand, cancellationToken)) - .Produces>() + .Produces>() .WithName("AddFavorite") .WithTags("Setters"); + endpointRouteBuilder.MapDelete("v1/favorite/{id}", + async (int id, [FromServices] IDeleteFavoriteHandler handler, CancellationToken cancellationToken) => + await handler.SendAsync(new DeleteFavoriteCommand { Id = id }, cancellationToken)) + .Produces>() + .WithName("DeleteFavorite") + .WithTags("Delete"); + return endpointRouteBuilder; } } diff --git a/src/Weather.Core/Abstractions/IAddFavoriteHandler.cs b/src/Weather.Core/Abstractions/IAddFavoriteHandler.cs index 762c7f0..34a5af3 100644 --- a/src/Weather.Core/Abstractions/IAddFavoriteHandler.cs +++ b/src/Weather.Core/Abstractions/IAddFavoriteHandler.cs @@ -2,7 +2,7 @@ namespace Weather.Core.Abstractions { - public interface IAddFavoriteHandler : IRequestHandler + public interface IAddFavoriteHandler : IRequestHandler { } diff --git a/src/Weather.Core/Abstractions/IDeleteFavoriteHandler.cs b/src/Weather.Core/Abstractions/IDeleteFavoriteHandler.cs new file mode 100644 index 0000000..d302dae --- /dev/null +++ b/src/Weather.Core/Abstractions/IDeleteFavoriteHandler.cs @@ -0,0 +1,8 @@ +using Weather.Domain.Commands; + +namespace Weather.Core.Abstractions +{ + public interface IDeleteFavoriteHandler : IRequestHandler + { + } +} diff --git a/src/Weather.Core/Abstractions/IWeatherCommandsRepository.cs b/src/Weather.Core/Abstractions/IWeatherCommandsRepository.cs index c105a04..0b0302a 100644 --- a/src/Weather.Core/Abstractions/IWeatherCommandsRepository.cs +++ b/src/Weather.Core/Abstractions/IWeatherCommandsRepository.cs @@ -6,5 +6,6 @@ namespace Weather.Core.Abstractions public interface IWeatherCommandsRepository { Task> AddFavoriteLocation(AddFavoriteCommand addFavoriteCommand, CancellationToken cancellationToken); + Task DeleteFavoriteLocationSafeAsync(DeleteFavoriteCommand command, CancellationToken cancellationToken); } } diff --git a/src/Weather.Core/Abstractions/IWeatherQueriesRepository.cs b/src/Weather.Core/Abstractions/IWeatherQueriesRepository.cs index 9941738..0bd7172 100644 --- a/src/Weather.Core/Abstractions/IWeatherQueriesRepository.cs +++ b/src/Weather.Core/Abstractions/IWeatherQueriesRepository.cs @@ -1,10 +1,9 @@ -using FluentResults; -using Weather.Domain.Dtos; +using Weather.Domain.BusinessEntities; namespace Weather.Core.Abstractions { public interface IWeatherQueriesRepository { - Task> GetFavorites(CancellationToken cancellationToken); + Task> GetFavorites(CancellationToken cancellationToken); } } diff --git a/src/Weather.Core/Commands/AddFavoriteHandler.cs b/src/Weather.Core/Commands/AddFavoriteHandler.cs index 30ba2f8..be1f806 100644 --- a/src/Weather.Core/Commands/AddFavoriteHandler.cs +++ b/src/Weather.Core/Commands/AddFavoriteHandler.cs @@ -22,21 +22,21 @@ public AddFavoriteHandler(IWeatherCommandsRepository weatherCommandsRepository, _logger = Guard.Against.Null(logger); } - public async Task> HandleAsync(AddFavoriteCommand request, CancellationToken cancellationToken) + public async Task> HandleAsync(AddFavoriteCommand request, CancellationToken cancellationToken) { if (!_addFavoriteCommandValidator.IsValid(request)) { - return HttpDataResponses.AsBadRequest(string.Format(ErrorMessages.RequestValidationError, request)); + return HttpDataResponses.AsBadRequest(string.Format(ErrorMessages.RequestValidationError, request)); } var addResult = await _weatherCommandsRepository.AddFavoriteLocation(request, cancellationToken); if(addResult.IsFailed) { _logger.LogError(LogEvents.FavoriteWeathersStoreToDatabase, addResult.Errors.JoinToMessage()); - return HttpDataResponses.AsInternalServerError(ErrorMessages.CantStoreLocation); + return HttpDataResponses.AsInternalServerError(ErrorMessages.CantStoreLocation); } - return HttpDataResponses.AsOK(true); + return HttpDataResponses.AsOK(addResult.Value); } } } diff --git a/src/Weather.Core/Commands/DeleteFavoriteHandler.cs b/src/Weather.Core/Commands/DeleteFavoriteHandler.cs new file mode 100644 index 0000000..3b1450e --- /dev/null +++ b/src/Weather.Core/Commands/DeleteFavoriteHandler.cs @@ -0,0 +1,40 @@ +using Ardalis.GuardClauses; +using Validot; +using Weather.Core.Abstractions; +using Weather.Core.Resources; +using Weather.Domain.Commands; +using Weather.Domain.Extensions; +using Weather.Domain.Http; + +namespace Weather.Core.Commands +{ + internal sealed class DeleteFavoriteHandler : IDeleteFavoriteHandler + { + private readonly IWeatherCommandsRepository _weatherCommandsRepository; + private readonly IValidator _validator; + + public DeleteFavoriteHandler( + IWeatherCommandsRepository weatherCommandsRepository, + IValidator validator) + { + _weatherCommandsRepository = Guard.Against.Null(weatherCommandsRepository); + _validator = Guard.Against.Null(validator); + } + + public async Task> HandleAsync(DeleteFavoriteCommand request, CancellationToken cancellationToken) + { + if (!_validator.IsValid(request)) + { + return HttpDataResponses.AsBadRequest(string.Format(ErrorMessages.RequestValidationError, request)); + } + + var addResult = await _weatherCommandsRepository.DeleteFavoriteLocationSafeAsync(request, cancellationToken); + if (addResult.IsFailed) + { + return HttpDataResponses.AsInternalServerError("Location was not deleted from database."); + } + + return HttpDataResponses.AsOK(true); + } + } +} diff --git a/src/Weather.Core/Configuration/ContainerConfigurationExtension.cs b/src/Weather.Core/Configuration/ContainerConfigurationExtension.cs index d461959..c9d28b3 100644 --- a/src/Weather.Core/Configuration/ContainerConfigurationExtension.cs +++ b/src/Weather.Core/Configuration/ContainerConfigurationExtension.cs @@ -24,7 +24,8 @@ private static IServiceCollection AddHandlers(this IServiceCollection serviceCol .AddScoped() .AddScoped() .AddScoped() - .AddScoped(); + .AddScoped() + .AddScoped(); private static IServiceCollection AddValidation(this IServiceCollection serviceCollection) => serviceCollection @@ -33,6 +34,7 @@ private static IServiceCollection AddValidation(this IServiceCollection serviceC .AddValidotSingleton, LocationDtoSpecificationHolder, LocationDto>() .AddValidotSingleton, AddFavoriteCommandSpecificationHolder, AddFavoriteCommand>() .AddValidotSingleton, GetCurrentWeatherQuerySpecificationHolder, GetCurrentWeatherQuery>() - .AddValidotSingleton, GetForecastWeatherSpecificationHolder, GetForecastWeatherQuery>(); + .AddValidotSingleton, GetForecastWeatherSpecificationHolder, GetForecastWeatherQuery>() + .AddValidotSingleton, DeleteFavoriteCommandSpecificationHolder, DeleteFavoriteCommand>(); } } diff --git a/src/Weather.Core/Queries/GetFavoritesHandler.cs b/src/Weather.Core/Queries/GetFavoritesHandler.cs index d7c4489..ff8fbb8 100644 --- a/src/Weather.Core/Queries/GetFavoritesHandler.cs +++ b/src/Weather.Core/Queries/GetFavoritesHandler.cs @@ -4,6 +4,7 @@ using Validot; using Weather.Core.Abstractions; using Weather.Core.Resources; +using Weather.Domain.BusinessEntities; using Weather.Domain.Dtos; using Weather.Domain.Extensions; using Weather.Domain.Http; @@ -46,9 +47,9 @@ public async Task> HandleAsync(EmptyReques } - private async Task> GetFavoritesAsync(IEnumerable favoriteLocationsResult, CancellationToken cancellationToken) + private async Task> GetFavoritesAsync(IEnumerable favoriteLocationsResult, CancellationToken cancellationToken) { - var result = new List(); + var result = new List(); var errorMessages = new List(); await favoriteLocationsResult.ForEachAsync(async (location) => @@ -61,7 +62,15 @@ await favoriteLocationsResult.ForEachAsync(async (location) => return; } - result.Add(favoriteWeather.Value); + result.Add(new FavoriteCurrentWeatherDto + { + CityName = favoriteWeather.Value.CityName, + DateTime = favoriteWeather.Value.DateTime, + Sunrise = favoriteWeather.Value.Sunrise, + Sunset = favoriteWeather.Value.Sunset, + Id = location.Id, + Temperature = favoriteWeather.Value.Temperature + }); }); return result.Any() ? diff --git a/src/Weather.Core/Validation/DeleteFavoriteCommandSpecificationHolder.cs b/src/Weather.Core/Validation/DeleteFavoriteCommandSpecificationHolder.cs new file mode 100644 index 0000000..b34c8c9 --- /dev/null +++ b/src/Weather.Core/Validation/DeleteFavoriteCommandSpecificationHolder.cs @@ -0,0 +1,18 @@ +using Validot; +using Weather.Domain.Commands; + +namespace Weather.Core.Validation +{ + internal sealed class DeleteFavoriteCommandSpecificationHolder : ISpecificationHolder + { + public Specification Specification { get; } + + public DeleteFavoriteCommandSpecificationHolder() + { + Specification deleteFavoriteCommandSpecification = s => s + .Member(m => m.Id, r => r.NonNegative()); + + Specification = deleteFavoriteCommandSpecification; + } + } +} diff --git a/src/Weather.Core/Validation/GeneralPredicates.cs b/src/Weather.Core/Validation/GeneralPredicates.cs index f08a1f6..b0cc80f 100644 --- a/src/Weather.Core/Validation/GeneralPredicates.cs +++ b/src/Weather.Core/Validation/GeneralPredicates.cs @@ -7,9 +7,9 @@ internal static class GeneralPredicates { internal static readonly Predicate isValidTemperature = m => m < 60 && m > -90; internal static readonly Predicate isValidLatitude = m => m >= -90 && m <= 90; - internal static readonly Predicate isValidLongtitude = m => m >= -180 && m <= 180; + internal static readonly Predicate isValidLongitude = m => m >= -180 && m <= 180; internal static readonly Specification isValidLocation = s => s .Member(m => m.Latitude, m => m.Rule(isValidLatitude)) - .Member(m => m.Longitude, m => m.Rule(isValidLongtitude)); + .Member(m => m.Longitude, m => m.Rule(isValidLongitude)); } } diff --git a/src/Weather.Core/Weather.Core.csproj b/src/Weather.Core/Weather.Core.csproj index 9028718..be170ad 100644 --- a/src/Weather.Core/Weather.Core.csproj +++ b/src/Weather.Core/Weather.Core.csproj @@ -10,10 +10,6 @@ - - - - diff --git a/src/Weather.Domain/BusinessEntities/FavoriteLocation.cs b/src/Weather.Domain/BusinessEntities/FavoriteLocation.cs new file mode 100644 index 0000000..e82dcf9 --- /dev/null +++ b/src/Weather.Domain/BusinessEntities/FavoriteLocation.cs @@ -0,0 +1,9 @@ +using Weather.Domain.Dtos; + +namespace Weather.Domain.BusinessEntities +{ + public sealed class FavoriteLocation : LocationDto + { + public int Id { get; init; } + } +} diff --git a/src/Weather.Domain/Commands/DeleteFavoriteCommand.cs b/src/Weather.Domain/Commands/DeleteFavoriteCommand.cs new file mode 100644 index 0000000..b8e294e --- /dev/null +++ b/src/Weather.Domain/Commands/DeleteFavoriteCommand.cs @@ -0,0 +1,7 @@ +namespace Weather.Domain.Commands +{ + public sealed class DeleteFavoriteCommand + { + public int Id { get; init; } + } +} diff --git a/src/Weather.Domain/Dtos/CurrentWeatherDto.cs b/src/Weather.Domain/Dtos/CurrentWeatherDto.cs index db95845..087f41e 100644 --- a/src/Weather.Domain/Dtos/CurrentWeatherDto.cs +++ b/src/Weather.Domain/Dtos/CurrentWeatherDto.cs @@ -1,6 +1,6 @@ namespace Weather.Domain.Dtos { - public sealed class CurrentWeatherDto + public class CurrentWeatherDto { public double Temperature { get; init; } diff --git a/src/Weather.Domain/Dtos/FavoriteCurrentWeatherDto.cs b/src/Weather.Domain/Dtos/FavoriteCurrentWeatherDto.cs new file mode 100644 index 0000000..dc67d50 --- /dev/null +++ b/src/Weather.Domain/Dtos/FavoriteCurrentWeatherDto.cs @@ -0,0 +1,7 @@ +namespace Weather.Domain.Dtos +{ + public sealed class FavoriteCurrentWeatherDto : CurrentWeatherDto + { + public int Id { get; init; } + } +} diff --git a/src/Weather.Domain/Dtos/FavoritesWeatherDto.cs b/src/Weather.Domain/Dtos/FavoritesWeatherDto.cs index 6046fa8..22e7186 100644 --- a/src/Weather.Domain/Dtos/FavoritesWeatherDto.cs +++ b/src/Weather.Domain/Dtos/FavoritesWeatherDto.cs @@ -2,6 +2,6 @@ { public sealed class FavoritesWeatherDto { - public IReadOnlyCollection FavoriteWeathers { get; init; } = new List(); + public IReadOnlyCollection FavoriteWeathers { get; init; } = new List(); } } diff --git a/src/Weather.Domain/Dtos/LocationDto.cs b/src/Weather.Domain/Dtos/LocationDto.cs index fcafd01..055fb0a 100644 --- a/src/Weather.Domain/Dtos/LocationDto.cs +++ b/src/Weather.Domain/Dtos/LocationDto.cs @@ -1,6 +1,6 @@ namespace Weather.Domain.Dtos { - public sealed class LocationDto + public class LocationDto { public double Latitude { get; init; } public double Longitude { get; init; } diff --git a/src/Weather.Infrastructure/Database/Repositories/WeatherCommandsRepository.cs b/src/Weather.Infrastructure/Database/Repositories/WeatherCommandsRepository.cs index e678491..db6beb9 100644 --- a/src/Weather.Infrastructure/Database/Repositories/WeatherCommandsRepository.cs +++ b/src/Weather.Infrastructure/Database/Repositories/WeatherCommandsRepository.cs @@ -35,5 +35,21 @@ public async Task> AddFavoriteLocation(AddFavoriteCommand addFavorit return Result.Fail(ex.Message); } } + + public async Task DeleteFavoriteLocationSafeAsync(DeleteFavoriteCommand command, CancellationToken cancellationToken) + { + try + { + var location = await _weatherContext.FavoriteLocations.FindAsync(command.Id, cancellationToken); + _weatherContext.Remove(location!); + await _weatherContext.SaveChangesAsync(cancellationToken); + return Result.Ok(); + } + catch (DbUpdateException ex) + { + _logger.LogError(LogEvents.FavoriteWeathersStoreToDatabase, ex, "Can't delete location."); + return Result.Fail(ex.Message); + } + } } } diff --git a/src/Weather.Infrastructure/Database/Repositories/WeatherQueriesRepository.cs b/src/Weather.Infrastructure/Database/Repositories/WeatherQueriesRepository.cs index 953e58e..2e18a2d 100644 --- a/src/Weather.Infrastructure/Database/Repositories/WeatherQueriesRepository.cs +++ b/src/Weather.Infrastructure/Database/Repositories/WeatherQueriesRepository.cs @@ -1,9 +1,7 @@ -using Ardalis.GuardClauses; -using AutoMapper; -using FluentResults; +using AutoMapper; using Microsoft.EntityFrameworkCore; using Weather.Core.Abstractions; -using Weather.Domain.Dtos; +using Weather.Domain.BusinessEntities; using Weather.Infrastructure.Database.EFContext; namespace Weather.Infrastructure.Database.Repositories @@ -13,10 +11,10 @@ internal sealed class WeatherQueriesRepository : RepositoryBase, IWeatherQueries public WeatherQueriesRepository(WeatherContext weatherContext, IMapper mapper) : base(weatherContext, mapper) { } - public async Task> GetFavorites(CancellationToken cancellationToken) + public async Task> GetFavorites(CancellationToken cancellationToken) { - var facoriteLocationEntities = await _weatherContext.FavoriteLocations.ToListAsync(cancellationToken); - return _mapper.Map>(facoriteLocationEntities); + var favoriteLocationEntities = await _weatherContext.FavoriteLocations.ToListAsync(cancellationToken); + return _mapper.Map>(favoriteLocationEntities); } } } diff --git a/src/Weather.Infrastructure/Mapping/Profiles/WeatherEntitiesProfile.cs b/src/Weather.Infrastructure/Mapping/Profiles/WeatherEntitiesProfile.cs index ab1bfdf..e0ffb28 100644 --- a/src/Weather.Infrastructure/Mapping/Profiles/WeatherEntitiesProfile.cs +++ b/src/Weather.Infrastructure/Mapping/Profiles/WeatherEntitiesProfile.cs @@ -1,4 +1,5 @@ using AutoMapper; +using Weather.Domain.BusinessEntities; using Weather.Domain.Dtos; using Weather.Infrastructure.Database.EFContext.Entities; @@ -9,7 +10,7 @@ internal sealed class WeatherEntitiesProfile : Profile public WeatherEntitiesProfile() { CreateMap(); - CreateMap(); + CreateMap(); } } } From 259bc837b9281a278476e4eb1d77f88242ab5702 Mon Sep 17 00:00:00 2001 From: Gramli Date: Tue, 12 Mar 2024 21:54:30 +0100 Subject: [PATCH 2/3] add unit tests --- .../Commands/DeleteFavoriteHandlerTests.cs | 78 +++++++++++++++++++ .../WeatherCommandsRepositoryTests.cs | 73 +++++++++++++++-- .../Repositories/WeatherCommandsRepository.cs | 2 +- 3 files changed, 146 insertions(+), 7 deletions(-) create mode 100644 src/Tests/UnitTests/Weather.Core.UnitTests/Commands/DeleteFavoriteHandlerTests.cs diff --git a/src/Tests/UnitTests/Weather.Core.UnitTests/Commands/DeleteFavoriteHandlerTests.cs b/src/Tests/UnitTests/Weather.Core.UnitTests/Commands/DeleteFavoriteHandlerTests.cs new file mode 100644 index 0000000..cb2f878 --- /dev/null +++ b/src/Tests/UnitTests/Weather.Core.UnitTests/Commands/DeleteFavoriteHandlerTests.cs @@ -0,0 +1,78 @@ +using Weather.Core.Abstractions; +using Weather.Core.Commands; +using Weather.Domain.Commands; + +namespace Weather.Core.UnitTests.Commands +{ + public class DeleteFavoriteHandlerTests + { + private readonly Mock _weatherCommandsRepositoryMock; + private readonly Mock> _validatorMock; + + private readonly IDeleteFavoriteHandler _uut; + public DeleteFavoriteHandlerTests() + { + _weatherCommandsRepositoryMock = new(); + _validatorMock = new(); + + _uut = new DeleteFavoriteHandler(_weatherCommandsRepositoryMock.Object, _validatorMock.Object); + } + + [Fact] + public async Task InvalidRequest() + { + //Arrange + var deleteFavoriteCommand = new DeleteFavoriteCommand { Id = 5 }; + + _validatorMock.Setup(x => x.IsValid(It.IsAny())).Returns(false); + + //Act + var result = await _uut.HandleAsync(deleteFavoriteCommand, CancellationToken.None); + + //Assert + Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode); + Assert.Single(result.Errors); + _validatorMock.Verify(x => x.IsValid(It.Is(y => y.Equals(deleteFavoriteCommand))), Times.Once); + } + + [Fact] + public async Task DeleteFavoriteLocationSafeAsync_Failed() + { + //Arrange + var deleteFavoriteCommand = new DeleteFavoriteCommand { Id = 5 }; + + _validatorMock.Setup(x => x.IsValid(deleteFavoriteCommand)).Returns(true); + _weatherCommandsRepositoryMock.Setup(x => x.DeleteFavoriteLocationSafeAsync(deleteFavoriteCommand, CancellationToken.None)) + .ReturnsAsync(Result.Fail(string.Empty)); + + //Act + var result = await _uut.HandleAsync(deleteFavoriteCommand, CancellationToken.None); + + //Assert + Assert.Equal(HttpStatusCode.InternalServerError, result.StatusCode); + Assert.Single(result.Errors); + _validatorMock.Verify(x => x.IsValid(deleteFavoriteCommand), Times.Once); + _weatherCommandsRepositoryMock.Verify(x => x.DeleteFavoriteLocationSafeAsync(deleteFavoriteCommand, CancellationToken.None), Times.Once); + } + + [Fact] + public async Task DeleteFavoriteLocationSafeAsync_Success() + { + //Arrange + var deleteFavoriteCommand = new DeleteFavoriteCommand { Id = 5 }; + + _validatorMock.Setup(x => x.IsValid(deleteFavoriteCommand)).Returns(true); + _weatherCommandsRepositoryMock.Setup(x => x.DeleteFavoriteLocationSafeAsync(deleteFavoriteCommand, CancellationToken.None)) + .ReturnsAsync(Result.Ok()); + + //Act + var result = await _uut.HandleAsync(deleteFavoriteCommand, CancellationToken.None); + + //Assert + Assert.Equal(HttpStatusCode.OK, result.StatusCode); + Assert.Empty(result.Errors); + _validatorMock.Verify(x => x.IsValid(deleteFavoriteCommand), Times.Once); + _weatherCommandsRepositoryMock.Verify(x => x.DeleteFavoriteLocationSafeAsync(deleteFavoriteCommand, CancellationToken.None), Times.Once); + } + } +} diff --git a/src/Tests/UnitTests/Weather.Infrastructure.UnitTests/Database/Repositories/WeatherCommandsRepositoryTests.cs b/src/Tests/UnitTests/Weather.Infrastructure.UnitTests/Database/Repositories/WeatherCommandsRepositoryTests.cs index abf9682..adb9754 100644 --- a/src/Tests/UnitTests/Weather.Infrastructure.UnitTests/Database/Repositories/WeatherCommandsRepositoryTests.cs +++ b/src/Tests/UnitTests/Weather.Infrastructure.UnitTests/Database/Repositories/WeatherCommandsRepositoryTests.cs @@ -35,13 +35,13 @@ public WeatherCommandsRepositoryTests() public async Task AddFavoriteLocation_Success() { //Arrange - var addFacoriteCommand = new AddFavoriteCommand { Location = new LocationDto { Latitude = 1, Longitude = 1 } }; + var addFavoriteCommand = new AddFavoriteCommand { Location = new LocationDto { Latitude = 1, Longitude = 1 } }; var favoriteLocationEntity = new FavoriteLocationEntity(); _mapperMock.Setup(x => x.Map(It.IsAny())).Returns(favoriteLocationEntity); //Act - var result = await _uut.AddFavoriteLocation(addFacoriteCommand, CancellationToken.None); + var result = await _uut.AddFavoriteLocation(addFavoriteCommand, CancellationToken.None); //Assert Assert.True(result.IsSuccess); @@ -54,14 +54,14 @@ public async Task AddFavoriteLocation_Success() public async Task AddFavoriteLocation_Failed() { //Arrange - var addFacoriteCommand = new AddFavoriteCommand { Location = new LocationDto { Latitude = 1, Longitude = 1 } }; + var addFavoriteCommand = new AddFavoriteCommand { Location = new LocationDto { Latitude = 1, Longitude = 1 } }; var favoriteLocationEntity = new FavoriteLocationEntity(); _mapperMock.Setup(x => x.Map(It.IsAny())).Returns(favoriteLocationEntity); _favoriteLocationEntityDbSetMock.Setup(x => x.AddAsync(It.IsAny(), It.IsAny())).Throws(new DbUpdateException()); //Act - var result = await _uut.AddFavoriteLocation(addFacoriteCommand, CancellationToken.None); + var result = await _uut.AddFavoriteLocation(addFavoriteCommand, CancellationToken.None); //Assert Assert.True(result.IsFailed); @@ -75,7 +75,7 @@ public async Task AddFavoriteLocation_Failed() public async Task AddFavoriteLocation_Throw() { //Arrange - var addFacoriteCommand = new AddFavoriteCommand { Location = new LocationDto { Latitude = 1, Longitude = 1 } }; + var addFavoriteCommand = new AddFavoriteCommand { Location = new LocationDto { Latitude = 1, Longitude = 1 } }; var favoriteLocationEntity = new FavoriteLocationEntity(); var exception = new ArgumentException("some message"); @@ -83,7 +83,7 @@ public async Task AddFavoriteLocation_Throw() _favoriteLocationEntityDbSetMock.Setup(x => x.AddAsync(It.IsAny(), It.IsAny())).Throws(exception); //Act - var exceptionResult = await Assert.ThrowsAsync(() => _uut.AddFavoriteLocation(addFacoriteCommand, CancellationToken.None)); + var exceptionResult = await Assert.ThrowsAsync(() => _uut.AddFavoriteLocation(addFavoriteCommand, CancellationToken.None)); //Assert Assert.Equivalent(exception, exceptionResult); @@ -91,5 +91,66 @@ public async Task AddFavoriteLocation_Throw() _weatherDbContextMock.Verify(x => x.SaveChangesAsync(It.IsAny()), Times.Never); _favoriteLocationEntityDbSetMock.Verify(x => x.AddAsync(It.IsAny(), It.IsAny()), Times.Once); } + + [Fact] + public async Task DeleteFavoriteLocationSafeAsync_Success() + { + //Arrange + var deleteFavoriteCommand = new DeleteFavoriteCommand { Id = 1 }; + var favoriteLocationEntity = new FavoriteLocationEntity(); + _favoriteLocationEntityDbSetMock.Setup(x => x.FindAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(favoriteLocationEntity); + + //Act + var result = await _uut.DeleteFavoriteLocationSafeAsync(deleteFavoriteCommand, CancellationToken.None); + + //Assert + Assert.True(result.IsSuccess); + _weatherDbContextMock.Verify(x => x.SaveChangesAsync(It.IsAny()), Times.Once); + _favoriteLocationEntityDbSetMock.Verify(x => x.FindAsync(deleteFavoriteCommand.Id, CancellationToken.None), Times.Once); + _favoriteLocationEntityDbSetMock.Verify(x => x.Remove(favoriteLocationEntity), Times.Once); + } + + [Fact] + public async Task DeleteFavoriteLocationSafeAsync_Failed() + { + //Arrange + var deleteFavoriteCommand = new DeleteFavoriteCommand { Id = 1 }; + var favoriteLocationEntity = new FavoriteLocationEntity(); + _favoriteLocationEntityDbSetMock.Setup(x => x.FindAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(favoriteLocationEntity); + _weatherDbContextMock.Setup(x => x.SaveChangesAsync(It.IsAny())) + .ThrowsAsync(new DbUpdateException()); + + //Act + var result = await _uut.DeleteFavoriteLocationSafeAsync(deleteFavoriteCommand, CancellationToken.None); + + //Assert + Assert.True(result.IsFailed); + _weatherDbContextMock.Verify(x => x.SaveChangesAsync(It.IsAny()), Times.Once); + _favoriteLocationEntityDbSetMock.Verify(x => x.FindAsync(deleteFavoriteCommand.Id, CancellationToken.None), Times.Once); + _favoriteLocationEntityDbSetMock.Verify(x => x.Remove(favoriteLocationEntity), Times.Once); + } + + [Fact] + public async Task DeleteFavoriteLocationSafeAsync_Throw() + { + //Arrange + var deleteFavoriteCommand = new DeleteFavoriteCommand { Id = 1 }; + var favoriteLocationEntity = new FavoriteLocationEntity(); + _favoriteLocationEntityDbSetMock.Setup(x => x.FindAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(favoriteLocationEntity); + + _weatherDbContextMock.Setup(x => x.SaveChangesAsync(It.IsAny())) + .ThrowsAsync(new ArgumentException()); + + //Act + var _ = await Assert.ThrowsAsync(() => _uut.DeleteFavoriteLocationSafeAsync(deleteFavoriteCommand, CancellationToken.None)); + + //Assert + _weatherDbContextMock.Verify(x => x.SaveChangesAsync(It.IsAny()), Times.Once); + _favoriteLocationEntityDbSetMock.Verify(x => x.FindAsync(deleteFavoriteCommand.Id, CancellationToken.None), Times.Once); + _favoriteLocationEntityDbSetMock.Verify(x => x.Remove(favoriteLocationEntity), Times.Once); + } } } diff --git a/src/Weather.Infrastructure/Database/Repositories/WeatherCommandsRepository.cs b/src/Weather.Infrastructure/Database/Repositories/WeatherCommandsRepository.cs index db6beb9..029b1ff 100644 --- a/src/Weather.Infrastructure/Database/Repositories/WeatherCommandsRepository.cs +++ b/src/Weather.Infrastructure/Database/Repositories/WeatherCommandsRepository.cs @@ -41,7 +41,7 @@ public async Task DeleteFavoriteLocationSafeAsync(DeleteFavoriteCommand try { var location = await _weatherContext.FavoriteLocations.FindAsync(command.Id, cancellationToken); - _weatherContext.Remove(location!); + _weatherContext.FavoriteLocations.Remove(location!); await _weatherContext.SaveChangesAsync(cancellationToken); return Result.Ok(); } From cfe0d726630d21599de26f623966f14a99bc655e Mon Sep 17 00:00:00 2001 From: Gramli Date: Tue, 12 Mar 2024 22:08:23 +0100 Subject: [PATCH 3/3] fix codacy --- .../Database/Repositories/WeatherCommandsRepositoryTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tests/UnitTests/Weather.Infrastructure.UnitTests/Database/Repositories/WeatherCommandsRepositoryTests.cs b/src/Tests/UnitTests/Weather.Infrastructure.UnitTests/Database/Repositories/WeatherCommandsRepositoryTests.cs index adb9754..758ea5f 100644 --- a/src/Tests/UnitTests/Weather.Infrastructure.UnitTests/Database/Repositories/WeatherCommandsRepositoryTests.cs +++ b/src/Tests/UnitTests/Weather.Infrastructure.UnitTests/Database/Repositories/WeatherCommandsRepositoryTests.cs @@ -145,7 +145,7 @@ public async Task DeleteFavoriteLocationSafeAsync_Throw() .ThrowsAsync(new ArgumentException()); //Act - var _ = await Assert.ThrowsAsync(() => _uut.DeleteFavoriteLocationSafeAsync(deleteFavoriteCommand, CancellationToken.None)); + await Assert.ThrowsAsync(() => _uut.DeleteFavoriteLocationSafeAsync(deleteFavoriteCommand, CancellationToken.None)); //Assert _weatherDbContextMock.Verify(x => x.SaveChangesAsync(It.IsAny()), Times.Once);