Init
This commit is contained in:
153
Prefab.Tests/Unit/Web/Gateway/HomeServiceShould.cs
Normal file
153
Prefab.Tests/Unit/Web/Gateway/HomeServiceShould.cs
Normal file
@@ -0,0 +1,153 @@
|
||||
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<CancellationToken>()))
|
||||
.ReturnsAsync(new GetCategoryTree.Response(new[] { featured, secondary }));
|
||||
|
||||
productClient
|
||||
.Setup(client => client.GetCategoryModels("featured-leaf", It.IsAny<int?>(), It.IsAny<int?>(), It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
|
||||
.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<int?>(), It.IsAny<int?>(), It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
|
||||
.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<CancellationToken>()))
|
||||
.ReturnsAsync(new GetCategoryTree.Response(new[] { empty, populated }));
|
||||
|
||||
productClient
|
||||
.Setup(client => client.GetCategoryModels("empty-leaf", It.IsAny<int?>(), It.IsAny<int?>(), It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(CreateListingResponse("empty-leaf", total: 0, cards: Array.Empty<GetCategoryModels.ProductCardDto>()));
|
||||
|
||||
productClient
|
||||
.Setup(client => client.GetCategoryModels("populated-leaf", It.IsAny<int?>(), It.IsAny<int?>(), It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<CancellationToken>()))
|
||||
.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<IProductClient> ProductClient) CreateService()
|
||||
{
|
||||
var moduleClient = new Mock<IModuleClient>();
|
||||
var productClient = new Mock<IProductClient>();
|
||||
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<GetCategoryModels.CategoryNodeDto>());
|
||||
|
||||
private static GetCategoryModels.Response CreateListingResponse(
|
||||
string slug,
|
||||
int total,
|
||||
IReadOnlyList<GetCategoryModels.ProductCardDto> cards)
|
||||
{
|
||||
var category = new GetCategoryModels.CategoryDetailDto(
|
||||
Guid.NewGuid(),
|
||||
slug,
|
||||
slug,
|
||||
Description: null,
|
||||
HeroImageUrl: null,
|
||||
Icon: null,
|
||||
IsLeaf: true,
|
||||
Children: Array.Empty<GetCategoryModels.CategoryNodeDto>());
|
||||
|
||||
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);
|
||||
}
|
||||
181
Prefab.Tests/Unit/Web/Gateway/NavMenuServiceShould.cs
Normal file
181
Prefab.Tests/Unit/Web/Gateway/NavMenuServiceShould.cs
Normal file
@@ -0,0 +1,181 @@
|
||||
using FluentValidation;
|
||||
using FluentValidation.Results;
|
||||
using Moq;
|
||||
using Prefab.Shared.Catalog;
|
||||
using Prefab.Shared.Catalog.Products;
|
||||
using Prefab.Web.Gateway.Shared;
|
||||
using Shouldly;
|
||||
|
||||
namespace Prefab.Tests.Unit.Web.Gateway;
|
||||
|
||||
[Trait(TraitName.Category, TraitCategory.Unit)]
|
||||
public sealed class NavMenuServiceShould
|
||||
{
|
||||
[Fact]
|
||||
public async Task ReturnOnlyFeaturedChildrenWhenAvailable()
|
||||
{
|
||||
var moduleClient = new Mock<IModuleClient>();
|
||||
var productClient = new Mock<IProductClient>();
|
||||
moduleClient.SetupGet(c => c.Product).Returns(productClient.Object);
|
||||
|
||||
var featuredChild = CreateNode(
|
||||
name: "Featured",
|
||||
slug: "featured",
|
||||
displayOrder: 2,
|
||||
isFeatured: true,
|
||||
isLeaf: true);
|
||||
|
||||
var secondaryChild = CreateNode(
|
||||
name: "Secondary",
|
||||
slug: "secondary",
|
||||
displayOrder: 1,
|
||||
isFeatured: false,
|
||||
isLeaf: true);
|
||||
|
||||
var root = CreateNode(
|
||||
name: "Root",
|
||||
slug: "root",
|
||||
displayOrder: 0,
|
||||
isFeatured: false,
|
||||
isLeaf: false,
|
||||
children: new[] { secondaryChild, featuredChild });
|
||||
|
||||
productClient
|
||||
.Setup(p => p.GetCategoryTree(It.IsAny<int>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new GetCategoryTree.Response(new[] { root }));
|
||||
|
||||
var sut = new NavMenu.Service(moduleClient.Object);
|
||||
|
||||
var result = await sut.Get(2, CancellationToken.None);
|
||||
|
||||
result.IsSuccess.ShouldBeTrue();
|
||||
var model = result.Value.ShouldNotBeNull();
|
||||
model.Depth.ShouldBe(2);
|
||||
model.Columns.Count.ShouldBe(1);
|
||||
|
||||
var column = model.Columns.Single();
|
||||
column.Root.Name.ShouldBe("Root");
|
||||
column.Root.Url.ShouldBe("/catalog/category?category-slug=root");
|
||||
column.Items.Count.ShouldBe(1);
|
||||
|
||||
var item = column.Items.Single();
|
||||
item.Name.ShouldBe("Featured");
|
||||
item.Url.ShouldBe("/catalog/products?category-slug=featured");
|
||||
column.HasMore.ShouldBeFalse();
|
||||
column.SeeAllUrl.ShouldBe("/catalog/category?category-slug=root");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FallBackToAllChildrenWhenNoFeatured()
|
||||
{
|
||||
var moduleClient = new Mock<IModuleClient>();
|
||||
var productClient = new Mock<IProductClient>();
|
||||
moduleClient.SetupGet(c => c.Product).Returns(productClient.Object);
|
||||
|
||||
var firstChild = CreateNode(
|
||||
name: "Alpha",
|
||||
slug: "alpha",
|
||||
displayOrder: 3,
|
||||
isFeatured: false,
|
||||
isLeaf: true);
|
||||
var secondChild = CreateNode(
|
||||
name: "Beta",
|
||||
slug: "beta",
|
||||
displayOrder: 1,
|
||||
isFeatured: false,
|
||||
isLeaf: false);
|
||||
var thirdChild = CreateNode(
|
||||
name: "Gamma",
|
||||
slug: "gamma",
|
||||
displayOrder: 2,
|
||||
isFeatured: false,
|
||||
isLeaf: true);
|
||||
|
||||
var root = CreateNode(
|
||||
name: "Root",
|
||||
slug: "root",
|
||||
displayOrder: 0,
|
||||
isFeatured: false,
|
||||
isLeaf: false,
|
||||
children: new[] { firstChild, secondChild, thirdChild });
|
||||
|
||||
productClient
|
||||
.Setup(p => p.GetCategoryTree(It.IsAny<int>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new GetCategoryTree.Response(new[] { root }));
|
||||
|
||||
var sut = new NavMenu.Service(moduleClient.Object);
|
||||
|
||||
var result = await sut.Get(1, CancellationToken.None);
|
||||
|
||||
result.IsSuccess.ShouldBeTrue();
|
||||
var model = result.Value.ShouldNotBeNull();
|
||||
model.Depth.ShouldBe(1);
|
||||
var names = model.Columns.Single().Items.Select(i => i.Name).ToList();
|
||||
names.ShouldBe(["Beta", "Gamma", "Alpha"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ClampDepthBeforeCallingCatalog()
|
||||
{
|
||||
var moduleClient = new Mock<IModuleClient>();
|
||||
var productClient = new Mock<IProductClient>();
|
||||
moduleClient.SetupGet(c => c.Product).Returns(productClient.Object);
|
||||
|
||||
var root = CreateNode("Root", "root", 0, false, false);
|
||||
|
||||
productClient
|
||||
.Setup(p => p.GetCategoryTree(2, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new GetCategoryTree.Response(new[] { root }));
|
||||
|
||||
var sut = new NavMenu.Service(moduleClient.Object);
|
||||
await sut.Get(10, CancellationToken.None);
|
||||
|
||||
productClient.Verify(p => p.GetCategoryTree(2, It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ReturnProblemWhenCatalogValidationFails()
|
||||
{
|
||||
var moduleClient = new Mock<IModuleClient>();
|
||||
var productClient = new Mock<IProductClient>();
|
||||
moduleClient.SetupGet(c => c.Product).Returns(productClient.Object);
|
||||
|
||||
productClient
|
||||
.Setup(p => p.GetCategoryTree(It.IsAny<int>(), It.IsAny<CancellationToken>()))
|
||||
.ThrowsAsync(new ValidationException([new ValidationFailure("Depth", "invalid")]));
|
||||
|
||||
var sut = new NavMenu.Service(moduleClient.Object);
|
||||
|
||||
var result = await sut.Get(-1, CancellationToken.None);
|
||||
|
||||
result.IsSuccess.ShouldBeFalse();
|
||||
result.Value.ShouldBeNull();
|
||||
var problem = result.Problem.ShouldNotBeNull();
|
||||
problem.StatusCode.ShouldBe(400);
|
||||
problem.Detail.ShouldBe("Request did not satisfy validation rules.");
|
||||
problem.Errors.ShouldNotBeNull();
|
||||
problem.Errors!.ShouldContainKey("Depth");
|
||||
problem.Errors["Depth"].ShouldContain("invalid");
|
||||
}
|
||||
|
||||
private static GetCategoryModels.CategoryNodeDto CreateNode(
|
||||
string name,
|
||||
string slug,
|
||||
int displayOrder,
|
||||
bool isFeatured,
|
||||
bool isLeaf,
|
||||
IReadOnlyList<GetCategoryModels.CategoryNodeDto>? children = null)
|
||||
{
|
||||
return new GetCategoryModels.CategoryNodeDto(
|
||||
Guid.NewGuid(),
|
||||
name,
|
||||
slug,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
displayOrder,
|
||||
isFeatured,
|
||||
isLeaf,
|
||||
children ?? []);
|
||||
}
|
||||
}
|
||||
121
Prefab.Tests/Unit/Web/Gateway/ProductListingServiceShould.cs
Normal file
121
Prefab.Tests/Unit/Web/Gateway/ProductListingServiceShould.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using Moq;
|
||||
using Prefab.Shared.Catalog;
|
||||
using Prefab.Shared.Catalog.Products;
|
||||
using Prefab.Tests;
|
||||
using Prefab.Web.Client.Models.Catalog;
|
||||
using Prefab.Web.Gateway;
|
||||
using Shouldly;
|
||||
|
||||
namespace Prefab.Tests.Unit.Web.Gateway;
|
||||
|
||||
[Trait(TraitName.Category, TraitCategory.Unit)]
|
||||
public sealed class ProductListingServiceShould
|
||||
{
|
||||
[Fact]
|
||||
public async Task MapPricesAndImagesFromModuleResponse()
|
||||
{
|
||||
var categorySlug = "ceiling-supports";
|
||||
var moduleClient = new Mock<IModuleClient>();
|
||||
var productClient = new Mock<IProductClient>();
|
||||
moduleClient.SetupGet(client => client.Product).Returns(productClient.Object);
|
||||
|
||||
var category = new GetCategoryModels.CategoryDetailDto(
|
||||
Guid.NewGuid(),
|
||||
"Ceiling Supports",
|
||||
categorySlug,
|
||||
Description: "Fixtures for ceiling support assemblies.",
|
||||
HeroImageUrl: "/media/categories/ceiling.jpg",
|
||||
Icon: null,
|
||||
IsLeaf: true,
|
||||
Children: Array.Empty<GetCategoryModels.CategoryNodeDto>());
|
||||
|
||||
var cardId = Guid.NewGuid();
|
||||
var response = new GetCategoryModels.CategoryProductsResponse(
|
||||
category,
|
||||
new[]
|
||||
{
|
||||
new GetCategoryModels.ProductCardDto(
|
||||
cardId,
|
||||
"Fan Hanger Assembly",
|
||||
"fan-hanger-assembly",
|
||||
FromPrice: 129.99m,
|
||||
PrimaryImageUrl: "/media/products/fan-hanger.jpg",
|
||||
LastModifiedOn: DateTimeOffset.UtcNow)
|
||||
},
|
||||
Total: 1,
|
||||
Page: 1,
|
||||
PageSize: 24);
|
||||
|
||||
productClient
|
||||
.Setup(client => client.GetCategoryModels(categorySlug, null, null, null, null, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new GetCategoryModels.Response(response));
|
||||
|
||||
var service = new Products.ListingService(moduleClient.Object);
|
||||
var result = await service.GetCategoryProducts(categorySlug, CancellationToken.None);
|
||||
|
||||
result.IsSuccess.ShouldBeTrue();
|
||||
result.Value.ShouldNotBeNull();
|
||||
|
||||
var model = result.Value;
|
||||
model.Category.Slug.ShouldBe(categorySlug);
|
||||
model.Products.Count.ShouldBe(1);
|
||||
|
||||
var card = model.Products.Single();
|
||||
card.Id.ShouldBe(cardId);
|
||||
card.Title.ShouldBe("Fan Hanger Assembly");
|
||||
card.Slug.ShouldBe("fan-hanger-assembly");
|
||||
card.PrimaryImageUrl.ShouldBe("/media/products/fan-hanger.jpg");
|
||||
card.IsPriced.ShouldBeTrue();
|
||||
card.FromPrice.ShouldNotBeNull();
|
||||
card.FromPrice!.Amount.ShouldBe(129.99m);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MarkProductsWithoutPriceAsUnpriced()
|
||||
{
|
||||
var categorySlug = "rod-strut-hardware";
|
||||
var moduleClient = new Mock<IModuleClient>();
|
||||
var productClient = new Mock<IProductClient>();
|
||||
moduleClient.SetupGet(client => client.Product).Returns(productClient.Object);
|
||||
|
||||
var category = new GetCategoryModels.CategoryDetailDto(
|
||||
Guid.NewGuid(),
|
||||
"Rod & Strut Hardware",
|
||||
categorySlug,
|
||||
Description: null,
|
||||
HeroImageUrl: null,
|
||||
Icon: null,
|
||||
IsLeaf: true,
|
||||
Children: Array.Empty<GetCategoryModels.CategoryNodeDto>());
|
||||
|
||||
var response = new GetCategoryModels.CategoryProductsResponse(
|
||||
category,
|
||||
new[]
|
||||
{
|
||||
new GetCategoryModels.ProductCardDto(
|
||||
Guid.NewGuid(),
|
||||
"Unpriced Assembly",
|
||||
"unpriced-assembly",
|
||||
FromPrice: null,
|
||||
PrimaryImageUrl: null,
|
||||
LastModifiedOn: DateTimeOffset.UtcNow)
|
||||
},
|
||||
Total: 1,
|
||||
Page: 1,
|
||||
PageSize: 12);
|
||||
|
||||
productClient
|
||||
.Setup(client => client.GetCategoryModels(categorySlug, null, null, null, null, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(new GetCategoryModels.Response(response));
|
||||
|
||||
var service = new Products.ListingService(moduleClient.Object);
|
||||
var result = await service.GetCategoryProducts(categorySlug, CancellationToken.None);
|
||||
|
||||
result.IsSuccess.ShouldBeTrue();
|
||||
result.Value.ShouldNotBeNull();
|
||||
|
||||
var card = result.Value.Products.Single();
|
||||
card.IsPriced.ShouldBeFalse();
|
||||
card.FromPrice.ShouldBeNull();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user