This commit is contained in:
2025-10-27 21:39:50 -04:00
parent 31f723bea4
commit 2fecd5b315
22 changed files with 1198 additions and 14 deletions

View File

@@ -0,0 +1,165 @@
using System.Net;
using System.Net.Http.Json;
using Prefab.Catalog.Domain.Exceptions;
using Prefab.Shared.Catalog.Products;
using Prefab.Tests.Infrastructure;
using Prefab.Web.Client.Models.Catalog;
using Prefab.Web.Client.Models.Shared;
using Shouldly;
namespace Prefab.Tests.Integration.Web.Gateway;
[Collection("Prefab.BffProduct")]
[Trait(TraitName.Category, TraitCategory.Integration)]
public sealed class ProductDetailGatewayShould(PrefabCompositeFixture_BffProduct fixture)
: IClassFixture<PrefabCompositeFixture_BffProduct>
{
[Fact]
public async Task ReturnProductModelWhenFound()
{
fixture.UseProductClient(new SuccessProductClient(CreateSampleProductResponse()));
var cancellationToken = TestContext.Current.CancellationToken;
var client = fixture.CreateHttpClientForWeb();
var response = await client.GetAsync("/bff/catalog/products/sample-widget", cancellationToken);
response.StatusCode.ShouldBe(HttpStatusCode.OK);
var payload = await response.Content.ReadFromJsonAsync<Result<ProductDisplayModel>>(cancellationToken: cancellationToken);
payload.ShouldNotBeNull();
payload!.IsSuccess.ShouldBeTrue(payload.Problem?.Detail);
payload.Value.ShouldNotBeNull();
payload.Value!.Slug.ShouldBe("sample-widget");
payload.Value.Name.ShouldBe("Sample Widget");
payload.Value.Sku.ShouldBe("SW-001");
payload.Value.Categories.ShouldNotBeEmpty();
}
[Fact]
public async Task ReturnNotFoundWhenMissing()
{
fixture.UseProductClient(new NotFoundProductClient());
var cancellationToken = TestContext.Current.CancellationToken;
var client = fixture.CreateHttpClientForWeb();
var response = await client.GetAsync("/bff/catalog/products/missing-widget", cancellationToken);
response.StatusCode.ShouldBe(HttpStatusCode.NotFound);
var payload = await response.Content.ReadFromJsonAsync<Result<ProductDisplayModel>>(cancellationToken: cancellationToken);
payload.ShouldNotBeNull();
payload!.IsSuccess.ShouldBeFalse();
payload.Problem.ShouldNotBeNull();
payload.Problem!.StatusCode.ShouldBe((int)HttpStatusCode.NotFound);
}
private static GetProductDetail.Response CreateSampleProductResponse()
{
var optionValues = new[]
{
new GetProductDetail.OptionValueDto(Guid.NewGuid(), "red", "Red", 0m, (int)GetProductDetail.PriceDeltaKind.Absolute, null),
new GetProductDetail.OptionValueDto(Guid.NewGuid(), "blue", "Blue", 0m, (int)GetProductDetail.PriceDeltaKind.Absolute, null)
};
var optionDefinitions = new[]
{
new GetProductDetail.OptionDefinitionDto(
Guid.NewGuid(),
"color",
"Color",
(int)GetProductDetail.OptionDataType.Choice,
true,
Unit: null,
Min: null,
Max: null,
Step: null,
PricePerUnit: null,
Tiers: Array.Empty<GetProductDetail.OptionTierDto>(),
Values: optionValues,
Rules: null)
};
var specs = new[]
{
new GetProductDetail.SpecDto("Weight", "25 lb", null, null)
};
var attributes = new Dictionary<string, string>
{
["catalog.product.docs"] = """[{ "title": "Spec Sheet", "url": "https://example.com/spec.pdf" }]"""
};
var categories = new[]
{
new GetProductDetail.CategoryDto(Guid.NewGuid(), "Lighting", "lighting")
};
var product = new GetProductDetail.ProductDetailDto(
Guid.NewGuid(),
"Sample Widget",
"sample-widget",
"Sample description.",
42.50m,
"SW-001",
categories,
optionDefinitions,
specs,
attributes);
return new GetProductDetail.Response(product);
}
private sealed class SuccessProductClient(GetProductDetail.Response response) : IProductClient
{
public Task<GetCategoryModels.Response> GetCategoryModels(
string slug,
int? page,
int? pageSize,
string? sort,
string? direction,
CancellationToken cancellationToken) =>
throw new NotSupportedException("Category models are not required for this test.");
public Task<GetCategory.Response> GetCategory(string slug, CancellationToken cancellationToken) =>
throw new NotSupportedException("Category client is not required for this test.");
public Task<GetCategoryTree.Response> GetCategoryTree(int? depth, CancellationToken cancellationToken) =>
throw new NotSupportedException("Category tree is not required for this test.");
public Task<GetProductDetail.Response> GetProductDetail(string slug, CancellationToken cancellationToken) =>
Task.FromResult(response);
public Task<QuotePrice.Response> Quote(QuotePrice.Request request, CancellationToken cancellationToken) =>
throw new NotSupportedException("Price quotes are not required for this test.");
public Task<QuotePrice.Response> QuotePrice(QuotePrice.Request request, CancellationToken cancellationToken) =>
throw new NotSupportedException("Price quotes are not required for this test.");
}
private sealed class NotFoundProductClient : IProductClient
{
public Task<GetCategoryModels.Response> GetCategoryModels(
string slug,
int? page,
int? pageSize,
string? sort,
string? direction,
CancellationToken cancellationToken) =>
throw new NotSupportedException("Category models are not required for this test.");
public Task<GetCategory.Response> GetCategory(string slug, CancellationToken cancellationToken) =>
throw new NotSupportedException("Category client is not required for this test.");
public Task<GetCategoryTree.Response> GetCategoryTree(int? depth, CancellationToken cancellationToken) =>
throw new NotSupportedException("Category tree is not required for this test.");
public Task<GetProductDetail.Response> GetProductDetail(string slug, CancellationToken cancellationToken) =>
throw new CatalogNotFoundException("Product", slug);
public Task<QuotePrice.Response> Quote(QuotePrice.Request request, CancellationToken cancellationToken) =>
throw new NotSupportedException("Price quotes are not required for this test.");
public Task<QuotePrice.Response> QuotePrice(QuotePrice.Request request, CancellationToken cancellationToken) =>
throw new NotSupportedException("Price quotes are not required for this test.");
}
}