using FluentValidation; using Moq; using Prefab.Shared.Catalog; using Prefab.Shared.Catalog.Products; using Prefab.Tests; using Prefab.Web.Client.Models.Catalog; using Prefab.Web.Client.Models.Home; using Prefab.Web.Client.Models.Shared; using Prefab.Web.Gateway; using Shouldly; using UrlPolicy = Prefab.Web.UrlPolicy.UrlPolicy; namespace Prefab.Tests.Unit.Web.Gateway; [Trait(TraitName.Category, TraitCategory.Unit)] public sealed class HomeServiceShould { [Fact] public async Task AggregateFeaturedProductsAndCategories() { var (service, productClient) = CreateService(); var featured = CreateNode("Featured Leaf", "featured-leaf", displayOrder: 2, isFeatured: true); var secondary = CreateNode("Secondary Leaf", "secondary-leaf", displayOrder: 1, isFeatured: false); productClient .Setup(client => client.GetCategoryTree(3, It.IsAny())) .ReturnsAsync(new GetCategoryTree.Response(new[] { featured, secondary })); productClient .Setup(client => client.GetCategoryModels("featured-leaf", It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(CreateListingResponse("featured-leaf", total: 3, cards: new[] { CreateProductCardDto("Featured 1", "featured-1", 19m), CreateProductCardDto("Duplicate", "duplicate", 21m) })); productClient .Setup(client => client.GetCategoryModels("secondary-leaf", It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(CreateListingResponse("secondary-leaf", total: 2, cards: new[] { CreateProductCardDto("Secondary 1", "secondary-1", 15m), CreateProductCardDto("Duplicate", "duplicate", 21m) })); var result = await service.Get(CancellationToken.None); result.IsSuccess.ShouldBeTrue(); var payload = result.Value.ShouldNotBeNull(); payload.LatestCategories.Count.ShouldBe(2); payload.LatestCategories.Select(category => category.Title).ShouldBe(["Featured Leaf", "Secondary Leaf"]); payload.FeaturedProducts.Count.ShouldBe(3); payload.FeaturedProducts.Select(product => product.Url).ShouldBe([ UrlPolicy.BrowseProduct("featured-1"), UrlPolicy.BrowseProduct("duplicate"), UrlPolicy.BrowseProduct("secondary-1") ]); } [Fact] public async Task SkipCategoriesWithoutProducts() { var (service, productClient) = CreateService(); var empty = CreateNode("Empty Leaf", "empty-leaf", displayOrder: 0, isFeatured: true); var populated = CreateNode("Populated Leaf", "populated-leaf", displayOrder: 1, isFeatured: false); productClient .Setup(client => client.GetCategoryTree(3, It.IsAny())) .ReturnsAsync(new GetCategoryTree.Response(new[] { empty, populated })); productClient .Setup(client => client.GetCategoryModels("empty-leaf", It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(CreateListingResponse("empty-leaf", total: 0, cards: Array.Empty())); productClient .Setup(client => client.GetCategoryModels("populated-leaf", It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .ReturnsAsync(CreateListingResponse("populated-leaf", total: 1, cards: new[] { CreateProductCardDto("Populated", "populated-1", 32m) })); var result = await service.Get(CancellationToken.None); result.IsSuccess.ShouldBeTrue(); var payload = result.Value.ShouldNotBeNull(); payload.LatestCategories.Count.ShouldBe(1); payload.LatestCategories.Single().Title.ShouldBe("Populated Leaf"); payload.FeaturedProducts.Single().Url.ShouldBe(UrlPolicy.BrowseProduct("populated-1")); } private static (Home.Service Service, Mock ProductClient) CreateService() { var moduleClient = new Mock(); var productClient = new Mock(); moduleClient.SetupGet(client => client.Product).Returns(productClient.Object); var listingService = new Products.ListingService(moduleClient.Object); var service = new Home.Service(moduleClient.Object, listingService); return (service, productClient); } private static GetCategoryModels.CategoryNodeDto CreateNode(string name, string slug, int displayOrder, bool isFeatured) => new( Guid.NewGuid(), name, slug, Description: null, HeroImageUrl: null, Icon: null, DisplayOrder: displayOrder, IsFeatured: isFeatured, IsLeaf: true, Children: Array.Empty()); private static GetCategoryModels.Response CreateListingResponse( string slug, int total, IReadOnlyList cards) { var category = new GetCategoryModels.CategoryDetailDto( Guid.NewGuid(), slug, slug, Description: null, HeroImageUrl: null, Icon: null, IsLeaf: true, Children: Array.Empty()); var response = new GetCategoryModels.CategoryProductsResponse( category, cards, total, Page: 1, PageSize: Math.Max(1, cards.Count)); return new GetCategoryModels.Response(response); } private static GetCategoryModels.ProductCardDto CreateProductCardDto(string name, string slug, decimal price) => new( Guid.NewGuid(), name, slug, FromPrice: price, PrimaryImageUrl: null, LastModifiedOn: DateTimeOffset.UtcNow); }