Skip to content

Commit

Permalink
Feature/delete favorites (#1)
Browse files Browse the repository at this point in the history
* Implementation of delete endpoint

* add and fix unit tests

* fix codacy issues
  • Loading branch information
Gramli authored Mar 6, 2024
1 parent 0a8ea21 commit f417228
Show file tree
Hide file tree
Showing 15 changed files with 260 additions and 35 deletions.
6 changes: 5 additions & 1 deletion src/Tests/HttpDebug/debug-tests.http
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,8 @@ Content-Type: application/json
"latitude": 38.5,
"longitude": -78.5
}
}
}

### add favorites weather request
DELETE https://{{host}}/weather/v1/favorite/1
Content-Type: application/json
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ public class AddFavoriteHandlerTests
private readonly Mock<IMapper> _mapperMock;
private readonly Mock<DbSet<FavoriteLocationEntity>> _favoriteLocationEntityDbSetMock;

private readonly IRequestHandler<bool, AddFavoriteCommand> _uut;
private readonly IRequestHandler<int, AddFavoriteCommand> _uut;
public AddFavoriteHandlerTests()
{
_favoriteLocationEntityDbSetMock = new();
_weatherContextMock = new();
_weatherContextMock.Setup(x => x.FavoriteLocations).Returns(_favoriteLocationEntityDbSetMock.Object);

_addFavoriteCommandValidatorMock = new Mock<IValidator<AddFavoriteCommand>>();
_addFavoriteCommandValidatorMock = new();
_loggerMock = new();
_mapperMock = new();

Expand All @@ -51,7 +51,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<AddFavoriteCommand>(y => y.Equals(addFavoriteCommand))), Times.Once);
}

Expand All @@ -72,7 +71,6 @@ public async Task AddFavoriteLocation_Failed()
//Assert
Assert.Equal(HttpStatusCode.InternalServerError, result.StatusCode);
Assert.Single(result.Errors);
Assert.False(result.Data);
_addFavoriteCommandValidatorMock.Verify(x => x.IsValid(It.Is<AddFavoriteCommand>(y => y.Equals(addFavoriteCommand))), Times.Once);
_loggerMock.VerifyLog(LogLevel.Error, LogEvents.FavoriteWeathersStoreToDatabase, Times.Once());
}
Expand All @@ -96,7 +94,6 @@ public async Task Success()
//Assert
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
Assert.Empty(result.Errors);
Assert.True(result.Data);
_addFavoriteCommandValidatorMock.Verify(x => x.IsValid(It.Is<AddFavoriteCommand>(y => y.Equals(addFavoriteCommand))), Times.Once);
_favoriteLocationEntityDbSetMock.Verify(x => x.AddAsync(It.IsAny<FavoriteLocationEntity>(), It.IsAny<CancellationToken>()), Times.Once);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Moq;
using System.Net;
using Validot;
using Weather.API.Domain.Abstractions;
using Weather.API.Domain.Database.Entities;
using Weather.API.Features.DeleteFavorites;
using Weather.API.UnitTests.Domain.Database;


namespace Weather.API.UnitTests.Features.DeleteFavorites
{
public class DeleteFavoriteHandlerTests
{
private readonly Mock<IValidator<DeleteFavoriteCommand>> _deleteFavoriteCommandValidatorMock;
private readonly Mock<TestWeatherContext> _weatherContextMock;
private readonly Mock<DbSet<FavoriteLocationEntity>> _favoriteLocationEntityDbSetMock;

private readonly IRequestHandler<bool, DeleteFavoriteCommand> _uut;
public DeleteFavoriteHandlerTests()
{
_deleteFavoriteCommandValidatorMock = new();
var loggerMock = new Mock<ILogger<DeleteFavoriteHandler>>();
_weatherContextMock = new();
_favoriteLocationEntityDbSetMock = new();

_uut = new DeleteFavoriteHandler(_deleteFavoriteCommandValidatorMock.Object, loggerMock.Object, _weatherContextMock.Object);
}

[Fact]
public async Task InvalidRequest()
{
//Arrange
var deleteFavoriteCommand = new DeleteFavoriteCommand { Id = 1 };

_deleteFavoriteCommandValidatorMock.Setup(x => x.IsValid(deleteFavoriteCommand)).Returns(false);

//Act
var result = await _uut.HandleAsync(deleteFavoriteCommand, CancellationToken.None);

//Assert
Assert.Equal(HttpStatusCode.BadRequest, result.StatusCode);
Assert.Single(result.Errors);
Assert.False(result.Data);
}

[Fact]
public async Task DeleteFavoriteLocationSafeAsync_Failed()
{
//Arrange
var deleteFavoriteCommand = new DeleteFavoriteCommand { Id = 1 };

_deleteFavoriteCommandValidatorMock.Setup(x => x.IsValid(deleteFavoriteCommand)).Returns(true);

_favoriteLocationEntityDbSetMock.Setup(x => x.FindAsync(It.IsAny<int>(), CancellationToken.None)).ThrowsAsync(new DbUpdateException());
_weatherContextMock.Setup(x => x.FavoriteLocations).Returns(_favoriteLocationEntityDbSetMock.Object);

//Act
var result = await _uut.HandleAsync(deleteFavoriteCommand, CancellationToken.None);

//Assert
Assert.Equal(HttpStatusCode.InternalServerError, result.StatusCode);
Assert.Single(result.Errors);
Assert.False(result.Data);
}

[Fact]
public async Task Success()
{
//Arrange
var deleteFavoriteCommand = new DeleteFavoriteCommand { Id = 1 };

_deleteFavoriteCommandValidatorMock.Setup(x => x.IsValid(deleteFavoriteCommand)).Returns(true);

var favoriteLocation = new FavoriteLocationEntity();
_favoriteLocationEntityDbSetMock.Setup(x => x.FindAsync(It.IsAny<int>(), CancellationToken.None)).ReturnsAsync(favoriteLocation);
_favoriteLocationEntityDbSetMock.Setup(x => x.Remove(favoriteLocation));
_weatherContextMock.Setup(x => x.FavoriteLocations).Returns(_favoriteLocationEntityDbSetMock.Object);

//Act
var result = await _uut.HandleAsync(deleteFavoriteCommand, CancellationToken.None);

//Assert
Assert.Equal(HttpStatusCode.OK, result.StatusCode);
Assert.Empty(result.Errors);
Assert.True(result.Data);
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ public GetFavoritesHandlerTests()
_currentWeatherValidatorMock = new();
_weatherContextMock = new();
_mapperMock = new();

_favoriteLocationEntityDbSetMock = new();

_uut = new GetFavoritesHandler(
Expand Down Expand Up @@ -94,11 +93,10 @@ public async Task EmptyResult_GetCurrentWeather_Fail()
var favoriteLocations = new List<FavoriteLocationEntity>() { new() };
_favoriteLocationEntityDbSetMock.SetupMock(favoriteLocations);
_weatherContextMock.Setup(x => x.FavoriteLocations).Returns(_favoriteLocationEntityDbSetMock.Object);
_mapperMock.Setup(x => x.Map<List<LocationDto>>(favoriteLocations)).Returns(new List<LocationDto> { locationDto });

_mapperMock.Setup(x => x.Map<LocationDto>(It.IsAny<FavoriteLocationEntity>())).Returns(locationDto);
_locationValidatorMock.Setup(x => x.IsValid(It.IsAny<LocationDto>())).Returns(true);

_weatherServiceMock.Setup(x => x.GetCurrentWeather(It.IsAny<LocationDto>(), It.IsAny<CancellationToken>())).ReturnsAsync(Result.Fail(failMessage));

//Act
var result = await _uut.HandleAsync(EmptyRequest.Instance, CancellationToken.None);

Expand All @@ -117,11 +115,11 @@ public async Task One_Of_GetCurrentWeather_Failed()
var failMessage = "Some fail message";
var locationDto = new LocationDto { Latitude = 1, Longitude = 1 };

var favoriteLocations = new List<FavoriteLocationEntity>() { new() };
var favoriteLocations = new List<FavoriteLocationEntity> { new(), new FavoriteLocationEntity { Latitude = locationDto.Latitude, Longitude = locationDto.Longitude } };
_favoriteLocationEntityDbSetMock.SetupMock(favoriteLocations);
_weatherContextMock.Setup(x => x.FavoriteLocations).Returns(_favoriteLocationEntityDbSetMock.Object);
_mapperMock.Setup(x => x.Map<List<LocationDto>>(favoriteLocations)).Returns(new List<LocationDto> { locationDto, new() });

_mapperMock.Setup(x => x.Map<LocationDto>(It.Is<FavoriteLocationEntity>(y=> y.Latitude!= locationDto.Latitude))).Returns(new LocationDto());
_mapperMock.Setup(x => x.Map<LocationDto>(It.Is<FavoriteLocationEntity>(y => y.Latitude == locationDto.Latitude))).Returns(locationDto);
_locationValidatorMock.Setup(x => x.IsValid(It.IsAny<LocationDto>())).Returns(true);

var currentWeather = new CurrentWeatherDto();
Expand All @@ -138,7 +136,6 @@ 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());
_weatherServiceMock.Verify(x => x.GetCurrentWeather(It.IsAny<LocationDto>(), It.IsAny<CancellationToken>()), Times.Exactly(2));
_loggerMock.VerifyLog(LogLevel.Warning, LogEvents.FavoriteWeathersGeneral, failMessage, Times.Once());
_locationValidatorMock.Verify(x => x.IsValid(It.Is<LocationDto>(y => y.Equals(locationDto))), Times.Once);
Expand All @@ -154,7 +151,7 @@ public async Task GetCurrentWeather_Validation_Fail()
var favoriteLocations = new List<FavoriteLocationEntity>() { new() };
_favoriteLocationEntityDbSetMock.SetupMock(favoriteLocations);
_weatherContextMock.Setup(x => x.FavoriteLocations).Returns(_favoriteLocationEntityDbSetMock.Object);
_mapperMock.Setup(x => x.Map<List<LocationDto>>(favoriteLocations)).Returns(new List<LocationDto> { locationDto });
_mapperMock.Setup(x => x.Map<LocationDto>(It.IsAny<FavoriteLocationEntity>())).Returns(locationDto);

_locationValidatorMock.Setup(x => x.IsValid(It.IsAny<LocationDto>())).Returns(true);
_currentWeatherValidatorMock.Setup(x => x.IsValid(It.IsAny<CurrentWeatherDto>())).Returns(false);
Expand Down Expand Up @@ -182,7 +179,7 @@ public async Task Success()
var favoriteLocations = new List<FavoriteLocationEntity>() { new() };
_favoriteLocationEntityDbSetMock.SetupMock(favoriteLocations);
_weatherContextMock.Setup(x => x.FavoriteLocations).Returns(_favoriteLocationEntityDbSetMock.Object);
_mapperMock.Setup(x => x.Map<List<LocationDto>>(favoriteLocations)).Returns(new List<LocationDto> { locationDto });
_mapperMock.Setup(x => x.Map<LocationDto>(It.IsAny<FavoriteLocationEntity>())).Returns(locationDto);

_locationValidatorMock.Setup(x => x.IsValid(It.IsAny<LocationDto>())).Returns(true);
_currentWeatherValidatorMock.Setup(x => x.IsValid(It.IsAny<CurrentWeatherDto>())).Returns(true);
Expand All @@ -197,7 +194,6 @@ public async Task Success()
Assert.Empty(result.Errors);
Assert.NotNull(result.Data);
Assert.Single(result.Data.FavoriteWeathers);
Assert.Equal(currentWeather, result.Data.FavoriteWeathers.Single());
_weatherServiceMock.Verify(x => x.GetCurrentWeather(It.Is<LocationDto>(y => y.Equals(locationDto)), It.IsAny<CancellationToken>()), Times.Once);
_locationValidatorMock.Verify(x => x.IsValid(It.Is<LocationDto>(y => y.Equals(locationDto))), Times.Once);
_currentWeatherValidatorMock.Verify(x => x.IsValid(It.Is<CurrentWeatherDto>(y => y.Equals(currentWeather))), Times.Once);
Expand Down
2 changes: 1 addition & 1 deletion src/Weather.API/Domain/Dtos/CurrentWeatherDto.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace Weather.API.Domain.Dtos
{
public sealed class CurrentWeatherDto
public class CurrentWeatherDto
{
public double Temperature { get; init; }

Expand Down
10 changes: 5 additions & 5 deletions src/Weather.API/Features/AddFavorites/AddFavoriteHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

namespace Weather.API.Features.Favorites.AddFavorites
{
internal sealed class AddFavoriteHandler : IRequestHandler<bool, AddFavoriteCommand>
internal sealed class AddFavoriteHandler : IRequestHandler<int, AddFavoriteCommand>
{
private readonly IMapper _mapper;
private readonly WeatherContext _weatherContext;
Expand All @@ -31,20 +31,20 @@ public AddFavoriteHandler(
_mapper = Guard.Against.Null(mapper);
}

public async Task<HttpDataResponse<bool>> HandleAsync(AddFavoriteCommand request, CancellationToken cancellationToken)
public async Task<HttpDataResponse<int>> HandleAsync(AddFavoriteCommand request, CancellationToken cancellationToken)
{
if (!_addFavoriteCommandValidator.IsValid(request))
{
return HttpDataResponses.AsBadRequest<bool>(string.Format(ErrorMessages.RequestValidationError, request));
return HttpDataResponses.AsBadRequest<int>(string.Format(ErrorMessages.RequestValidationError, request));
}

var addResult = await AddFavoriteLocationSafeAsync(request, cancellationToken);
if (addResult.IsFailed)
{
return HttpDataResponses.AsInternalServerError<bool>("Location was not stored in database.");
return HttpDataResponses.AsInternalServerError<int>("Location was not stored in database.");
}

return HttpDataResponses.AsOK(true);
return HttpDataResponses.AsOK(addResult.Value);
}

public async Task<Result<int>> AddFavoriteLocationSafeAsync(AddFavoriteCommand addFavoriteCommand, CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ public static class ContainerConfigurationExtension
public static IEndpointRouteBuilder BuildAddFavoriteWeatherEndpoints(this IEndpointRouteBuilder endpointRouteBuilder)
{
endpointRouteBuilder.MapPost("v1/favorite",
async ([FromBody] AddFavoriteCommand addFavoriteCommand, [FromServices] IRequestHandler<bool, AddFavoriteCommand> handler, CancellationToken cancellationToken) =>
async ([FromBody] AddFavoriteCommand addFavoriteCommand, [FromServices] IRequestHandler<int, AddFavoriteCommand> handler, CancellationToken cancellationToken) =>
await handler.SendAsync(addFavoriteCommand, cancellationToken))
.Produces<DataResponse<bool>>()
.Produces<DataResponse<int>>()
.WithName("AddFavorite")
.WithTags("Setters");

Expand All @@ -23,7 +23,7 @@ await handler.SendAsync(addFavoriteCommand, cancellationToken))

public static IServiceCollection AddAddFavorites(this IServiceCollection serviceCollection)
=> serviceCollection
.AddScoped<IRequestHandler<bool, AddFavoriteCommand>, AddFavoriteHandler>()
.AddScoped<IRequestHandler<int, AddFavoriteCommand>, AddFavoriteHandler>()
.AddValidotSingleton<IValidator<AddFavoriteCommand>, AddFavoriteCommandSpecificationHolder, AddFavoriteCommand>();

}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Microsoft.AspNetCore.Mvc;
using Validot;
using Weather.API.Domain.Abstractions;
using Weather.API.Domain.Extensions;
using WeatherApi.Domain.Http;

namespace Weather.API.Features.DeleteFavorites
{
public static class ContainerConfigurationExtension
{
public static IEndpointRouteBuilder BuildDeleteFavoriteWeatherEndpoints(this IEndpointRouteBuilder endpointRouteBuilder)
{
endpointRouteBuilder.MapDelete("v1/favorite/{id}",
async (int id, [FromServices] IRequestHandler<bool, DeleteFavoriteCommand> handler, CancellationToken cancellationToken) =>
await handler.SendAsync(new DeleteFavoriteCommand { Id = id }, cancellationToken))
.Produces<DataResponse<bool>>()
.WithName("DeleteFavorite")
.WithTags("Delete");

return endpointRouteBuilder;
}

public static IServiceCollection AddDeleteFavorites(this IServiceCollection serviceCollection)
=> serviceCollection
.AddScoped<IRequestHandler<bool, DeleteFavoriteCommand>, DeleteFavoriteHandler>()
.AddValidotSingleton<IValidator<DeleteFavoriteCommand>, DeleteFavoriteCommandSpecificationHolder, DeleteFavoriteCommand>();

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Weather.API.Features.DeleteFavorites
{
internal sealed class DeleteFavoriteCommand
{
public int Id { get; init; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using Validot;

namespace Weather.API.Features.DeleteFavorites
{
internal sealed class DeleteFavoriteCommandSpecificationHolder : ISpecificationHolder<DeleteFavoriteCommand>
{
public Specification<DeleteFavoriteCommand> Specification { get; }

public DeleteFavoriteCommandSpecificationHolder()
{
Specification<DeleteFavoriteCommand> addFavoriteCommandSpecification = s => s
.Member(m => m.Id, r => r.NonNegative());

Specification = addFavoriteCommandSpecification;
}
}
}
61 changes: 61 additions & 0 deletions src/Weather.API/Features/DeleteFavorites/DeleteFavoriteHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using Ardalis.GuardClauses;
using FluentResults;
using Microsoft.EntityFrameworkCore;
using Validot;
using Weather.API.Domain.Abstractions;
using Weather.API.Domain.Database.EFContext;
using Weather.API.Domain.Extensions;
using Weather.API.Domain.Logging;
using Weather.API.Domain.Resources;
using WeatherApi.Domain.Http;

namespace Weather.API.Features.DeleteFavorites
{
internal sealed class DeleteFavoriteHandler : IRequestHandler<bool, DeleteFavoriteCommand>
{
private readonly WeatherContext _weatherContext;
private readonly IValidator<DeleteFavoriteCommand> _validator;
private readonly ILogger<DeleteFavoriteHandler> _logger;
public DeleteFavoriteHandler(
IValidator<DeleteFavoriteCommand> validator,
ILogger<DeleteFavoriteHandler> logger,
WeatherContext weatherContext)
{
_validator = Guard.Against.Null(validator);
_logger = Guard.Against.Null(logger);
_weatherContext = Guard.Against.Null(weatherContext);
}

public async Task<HttpDataResponse<bool>> HandleAsync(DeleteFavoriteCommand request, CancellationToken cancellationToken)
{
if (!_validator.IsValid(request))
{
return HttpDataResponses.AsBadRequest<bool>(string.Format(ErrorMessages.RequestValidationError, request));
}

var addResult = await DeleteFavoriteLocationSafeAsync(request, cancellationToken);
if (addResult.IsFailed)
{
return HttpDataResponses.AsInternalServerError<bool>("Location was not deleted from database.");
}

return HttpDataResponses.AsOK(true);
}

public async Task<Result> 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);
}
}
}
}
Loading

0 comments on commit f417228

Please sign in to comment.