Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix: filter by category #437

Open
wants to merge 5 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 86 additions & 6 deletions src/Hng.Application.Test/Features/Products/GetAllProductsShould.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,24 @@ namespace Hng.Application.Test.Features.Products
public class GetAllProductsShould
{
private readonly Mock<IRepository<Product>> _mockProductRepository;
private readonly Mock<IRepository<Category>> _mockCategoryRepository;
private readonly Mock<IMapper> _mockMapper;
private readonly GetAllProductsHandler _handler;

public GetAllProductsShould()
{
_mockProductRepository = new Mock<IRepository<Product>>();
_mockCategoryRepository = new Mock<IRepository<Category>>();
_mockMapper = new Mock<IMapper>();
_handler = new GetAllProductsHandler(_mockProductRepository.Object, _mockMapper.Object);
_handler = new GetAllProductsHandler(_mockProductRepository.Object, _mockCategoryRepository.Object, _mockMapper.Object);
}

[Fact]
public async Task Handle_ShouldReturnAllProductsForOrganization()
{
// Arrange
var orgId = Guid.NewGuid();
var query = new GetAllProductsQuery(orgId);
var query = new GetAllProductsQuery(orgId, null);

var products = new List<Product>
{
Expand All @@ -49,6 +51,9 @@ public async Task Handle_ShouldReturnAllProductsForOrganization()
_mockMapper.Setup(m => m.Map<IEnumerable<ProductResponseDto>>(It.IsAny<IEnumerable<Product>>()))
.Returns(productDtos);

_mockCategoryRepository.Setup(r => r.GetAllAsync()).ReturnsAsync(new List<Category>());


// Act
var result = await _handler.Handle(query, CancellationToken.None);

Expand All @@ -62,20 +67,67 @@ public async Task Handle_ShouldReturnAllProductsForOrganization()

_mockProductRepository.Verify(r => r.GetAllBySpec(It.IsAny<Expression<Func<Product, bool>>>()), Times.Once);
_mockMapper.Verify(m => m.Map<IEnumerable<ProductResponseDto>>(It.IsAny<IEnumerable<Product>>()), Times.Once);
_mockCategoryRepository.Verify(r => r.GetAllAsync(), Times.Once);
}

[Fact]
public async Task Handle_ShouldReturnFilteredProducts_WhenCategoryIsProvided()
{
// Arrange
var orgId = Guid.NewGuid();
var category = "Electronics";
var query = new GetAllProductsQuery(orgId, category);

var products = new List<Product>
{
new Product { Id = Guid.NewGuid(), Name = "Product 1", Quantity = 5, OrganizationId = orgId, Category = "Electronics" },
};

var productDtos = new List<ProductResponseDto>
{
new ProductResponseDto { Id = products[0].Id, Name = "Product 1", Quantity = 5 }
};

var validCategories = new List<Category> { new Category { Id = Guid.NewGuid(), Name = category } };

_mockProductRepository.Setup(r => r.GetAllBySpec(It.IsAny<Expression<Func<Product, bool>>>()))
.ReturnsAsync(products);

_mockMapper.Setup(m => m.Map<IEnumerable<ProductResponseDto>>(It.IsAny<IEnumerable<Product>>()))
.Returns(productDtos);

_mockCategoryRepository.Setup(r => r.GetAllAsync()).ReturnsAsync(validCategories);


// Act
var result = await _handler.Handle(query, CancellationToken.None);

// Assert
Assert.NotNull(result);
Assert.Single(result); // Only one product matches category

var resultList = result.ToList();
Assert.Equal("in stock", resultList[0].Status);

_mockProductRepository.Verify(r => r.GetAllBySpec(It.IsAny<Expression<Func<Product, bool>>>()), Times.Once);
_mockMapper.Verify(m => m.Map<IEnumerable<ProductResponseDto>>(It.IsAny<IEnumerable<Product>>()), Times.Once);
_mockCategoryRepository.Verify(r => r.GetAllAsync(), Times.Once);
}

[Fact]
public async Task Handle_ShouldReturnEmptyListWhenNoProducts()
{
// Arrange
var orgId = Guid.NewGuid();
var query = new GetAllProductsQuery(orgId);
var query = new GetAllProductsQuery(orgId, null);

_mockProductRepository.Setup(r => r.GetAllBySpec(It.IsAny<Expression<Func<Product, bool>>>()))
.ReturnsAsync(new List<Product>());

_mockMapper.Setup(m => m.Map<IEnumerable<ProductResponseDto>>(It.IsAny<IEnumerable<Product>>()))
.Returns(new List<ProductResponseDto>());
.Returns(new List<ProductResponseDto>());

_mockCategoryRepository.Setup(r => r.GetAllAsync()).ReturnsAsync(new List<Category>());

// Act
var result = await _handler.Handle(query, CancellationToken.None);
Expand All @@ -86,6 +138,34 @@ public async Task Handle_ShouldReturnEmptyListWhenNoProducts()

_mockProductRepository.Verify(r => r.GetAllBySpec(It.IsAny<Expression<Func<Product, bool>>>()), Times.Once);
_mockMapper.Verify(m => m.Map<IEnumerable<ProductResponseDto>>(It.IsAny<IEnumerable<Product>>()), Times.Once);
_mockCategoryRepository.Verify(r => r.GetAllAsync(), Times.Once);

}

[Fact]
public async Task Handle_ShouldThrowError_WhenInvalidCategoryIsProvided()
{
// Arrange
var orgId = Guid.NewGuid();
var invalidCategory = "InvalidCategory";
var query = new GetAllProductsQuery(orgId, invalidCategory);

var validCategories = new List<Category>
{
new Category { Id = Guid.NewGuid(), Name = "Electronics" },
new Category { Id = Guid.NewGuid(), Name = "Books" }
};

_mockCategoryRepository.Setup(r => r.GetAllAsync()).ReturnsAsync(validCategories);

// Act & Assert
var exception = await Assert.ThrowsAsync<ArgumentException>(async () =>
await _handler.Handle(query, CancellationToken.None));

Assert.Equal($"Invalid category '{invalidCategory}' provided.", exception.Message);

_mockCategoryRepository.Verify(r => r.GetAllAsync(), Times.Once);
_mockProductRepository.Verify(r => r.GetAllBySpec(It.IsAny<Expression<Func<Product, bool>>>()), Times.Never);
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,27 @@ namespace Hng.Application.Features.Products.Handlers
public class GetAllProductsHandler : IRequestHandler<GetAllProductsQuery, IEnumerable<ProductResponseDto>>
{
private readonly IRepository<Product> _productRepository;
private readonly IRepository<Category> _categoryRepository;
private readonly IMapper _mapper;

public GetAllProductsHandler(IRepository<Product> productRepository, IMapper mapper)
public GetAllProductsHandler(IRepository<Product> productRepository, IRepository<Category> categoryRepository, IMapper mapper)
{
_productRepository = productRepository;
_categoryRepository = categoryRepository;
_mapper = mapper;
}

public async Task<IEnumerable<ProductResponseDto>> Handle(GetAllProductsQuery request, CancellationToken cancellationToken)
{
var products = await _productRepository.GetAllBySpec(u => u.OrganizationId == request.OrgId);
var validCategories = await _categoryRepository.GetAllAsync();
var categoriesNames = validCategories.Select(category => category.Name).ToHashSet();

if (!string.IsNullOrEmpty(request.Category) && !categoriesNames.Contains(request.Category))
{
throw new ArgumentException($"Invalid category '{request.Category}' provided.");
}

var products = await _productRepository.GetAllBySpec(product => product.OrganizationId == request.OrgId && (string.IsNullOrEmpty(request.Category) || product.Category == request.Category));
var productDtos = _mapper.Map<IEnumerable<ProductResponseDto>>(products);

foreach (var product in productDtos)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
public class GetAllProductsQuery : IRequest<IEnumerable<ProductResponseDto>>
{
public Guid OrgId { get; }
public string Category { get; }

public GetAllProductsQuery(Guid orgId)
public GetAllProductsQuery(Guid orgId, string? category)

Check warning on line 11 in src/Hng.Application/Features/Products/Queries/GetAllProductsQuery.cs

View workflow job for this annotation

GitHub Actions / build_lint_test

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
{
OrgId = orgId;
Category = category;
}
}
}
4 changes: 2 additions & 2 deletions src/Hng.Web/Controllers/ProductController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,11 @@
[ProducesResponseType(typeof(SuccessResponseDto<IEnumerable<ProductResponseDto>>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(FailureResponseDto<object>), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(FailureResponseDto<object>), StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> GetAllProducts(Guid orgId)
public async Task<IActionResult> GetAllProducts(Guid orgId, [FromQuery] string? category)

Check warning on line 62 in src/Hng.Web/Controllers/ProductController.cs

View workflow job for this annotation

GitHub Actions / build_lint_test

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
{
try
{
var query = new GetAllProductsQuery(orgId);
var query = new GetAllProductsQuery(orgId, category);
var response = await _mediator.Send(query);
return Ok(new SuccessResponseDto<IEnumerable<ProductResponseDto>>
{
Expand Down
3 changes: 1 addition & 2 deletions src/Hng.Web/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
"ConnectionStrings": {
"DefaultConnectionString": "Host=localhost; Port=5432; Database=boiler_plate_db; Username=postgres; Password=\"Test123$\";",
"RedisConnectionString": "localhost:6379"

},
"Jwt": {
"SecretKey": "V@7y$#z9Gq!Np3X2rD6&K*B5wLm%+T8aJ4fH",
Expand Down Expand Up @@ -43,4 +42,4 @@
"FrontendUrl": {
"path": "https://kimiko-csharp.teams.hng.tech"
}
}
}
Loading