This commit is contained in:
2025-10-27 17:39:18 -04:00
commit 31f723bea4
1579 changed files with 642409 additions and 0 deletions

View File

@@ -0,0 +1,44 @@
using System.Net.Http.Json;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using Prefab.Tests.Infrastructure;
using Shouldly;
namespace Prefab.Tests.Integration.Modules.Catalog.App.Categories;
[Trait(TraitName.Category, TraitCategory.Integration)]
public sealed class CategoriesCreateShould(PrefabCompositeFixture_Ephemeral fixture)
: IClassFixture<PrefabCompositeFixture_Ephemeral>
{
[Fact]
public async Task ReturnDomainProblemDetailsWhenNameAlreadyExists()
{
var cancellationToken = TestContext.Current.CancellationToken;
var client = fixture.CreateHttpClientForWeb();
client.DefaultRequestHeaders.TryAddWithoutValidation("X-Forwarded-Proto", "https");
var suffix = Guid.NewGuid().ToString("N")[..6];
var name = $"Duplicate Category {suffix}";
var description = $"Duplicate description {suffix}";
var payload = new { name, description };
var firstResponse = await client.PostAsJsonAsync("/api/catalog/categories", payload, cancellationToken);
firstResponse.EnsureSuccessStatusCode();
var secondResponse = await client.PostAsJsonAsync("/api/catalog/categories", payload, cancellationToken);
var responseBody = await secondResponse.Content.ReadAsStringAsync(cancellationToken);
secondResponse.StatusCode.ShouldBe(HttpStatusCode.BadRequest, responseBody);
var problem = JsonSerializer.Deserialize<ProblemDetails?>(responseBody, new JsonSerializerOptions(JsonSerializerDefaults.Web));
problem.ShouldNotBeNull();
problem!.Type.ShouldBe("https://prefab.dev/problems/domain-error");
problem.Title.ShouldBe("Request could not be processed.");
problem.Detail.ShouldNotBeNull();
problem.Detail!.ShouldContain(name);
}
}

View File

@@ -0,0 +1,29 @@
using System.Net.Http.Json;
using Microsoft.AspNetCore.Mvc;
using Prefab.Tests.Infrastructure;
using Shouldly;
namespace Prefab.Tests.Integration.Modules.Catalog.App.Products;
[Trait(TraitName.Category, TraitCategory.Integration)]
public sealed class CatalogProductDetailShould(PrefabCompositeFixture_Ephemeral fixture)
: IClassFixture<PrefabCompositeFixture_Ephemeral>
{
[Fact]
public async Task ReturnNotFoundProblemDetailsForUnknownSlug()
{
var cancellationToken = TestContext.Current.CancellationToken;
var client = fixture.CreateHttpClientForWeb();
client.DefaultRequestHeaders.TryAddWithoutValidation("X-Forwarded-Proto", "https");
var response = await client.GetAsync("/api/catalog/products/does-not-exist", cancellationToken);
response.StatusCode.ShouldBe(HttpStatusCode.NotFound);
var problem = await response.Content.ReadFromJsonAsync<ProblemDetails?>(cancellationToken: cancellationToken);
problem.ShouldNotBeNull();
problem!.Title.ShouldBe("Resource not found.");
problem.Type.ShouldBe("https://prefab.dev/problems/not-found");
}
}

View File

@@ -0,0 +1,100 @@
using System.Net.Http.Json;
using System.Text.RegularExpressions;
using Prefab.Catalog.Domain.Entities;
using Prefab.Catalog.Domain.Services;
using Prefab.Shared.Catalog.Products;
using Prefab.Tests.Infrastructure;
using Shouldly;
namespace Prefab.Tests.Integration.Modules.Catalog.App.Products;
[Trait(TraitName.Category, TraitCategory.Integration)]
public sealed class CategoryModelsOverHttpShould(PrefabCompositeFixture_Ephemeral fixture)
: IClassFixture<PrefabCompositeFixture_Ephemeral>
{
[Fact]
public async Task ReturnFromPriceForVariantsAndStandaloneModel()
{
var cancellationToken = TestContext.Current.CancellationToken;
var suffix = Guid.NewGuid().ToString("N")[..8];
var categoryName = $"Browse Category {suffix}";
var categoryDescription = $"Category description {suffix}";
var categorySlug = Slugify(categoryName);
await fixture.SeedAsync(async (db, token) =>
{
var checker = new SeedUniqueChecker();
var category = await Category.Create(categoryName, categoryDescription, checker, token);
db.Categories.Add(category);
var modelWithVariants = await Product.CreateModel(
$"Model With Variants {suffix}",
$"model-with-variants-{suffix}",
"Variants model",
checker,
token);
modelWithVariants.SetBasePrice(15.75m);
var variantSkuPrefix = $"SKU-{suffix}";
var variantA = await Product.CreateVariant(modelWithVariants.Id, $"{variantSkuPrefix}-A", $"Variant A {suffix}", 12.99m, checker, token);
var variantB = await Product.CreateVariant(modelWithVariants.Id, $"{variantSkuPrefix}-B", $"Variant B {suffix}", 14.49m, checker, token);
modelWithVariants.AttachVariant(variantA);
modelWithVariants.AttachVariant(variantB);
modelWithVariants.AssignToCategory(category.Id, true);
var modelWithoutVariants = await Product.CreateModel(
$"Standalone Model {suffix}",
$"standalone-model-{suffix}",
"Standalone model",
checker,
token);
modelWithoutVariants.SetBasePrice(9.75m);
modelWithoutVariants.AssignToCategory(category.Id, true);
db.Products.AddRange(modelWithVariants, variantA, variantB, modelWithoutVariants);
await db.SaveChangesAsync(token);
}, cancellationToken);
var client = fixture.CreateHttpClientForWeb();
var httpResponse = await client.GetAsync($"/api/catalog/categories/{categorySlug}/models", cancellationToken);
httpResponse.EnsureSuccessStatusCode();
var payload = await httpResponse.Content.ReadFromJsonAsync<GetCategoryModels.Response>(cancellationToken: cancellationToken);
payload.ShouldNotBeNull();
payload.Result.ShouldNotBeNull();
payload.Result.Products.ShouldNotBeNull();
var variantCard = payload.Result.Products.Single(card => card.Slug == $"model-with-variants-{suffix}");
variantCard.FromPrice.ShouldBe(15.75m);
var standaloneCard = payload.Result.Products.Single(card => card.Slug == $"standalone-model-{suffix}");
standaloneCard.FromPrice.ShouldBe(9.75m);
}
private static string Slugify(string value)
{
var normalized = Regex.Replace(value.Trim().ToLowerInvariant(), "[^a-z0-9]+", "-");
return normalized.Trim('-');
}
private sealed class SeedUniqueChecker : IUniqueChecker
{
public Task<bool> CategoryNameIsUnique(string name, CancellationToken cancellationToken = default)
=> Task.FromResult(true);
public Task<bool> ProductModelNameIsUnique(string name, CancellationToken cancellationToken = default)
=> Task.FromResult(true);
public Task<bool> ProductSlugIsUnique(string? slug, CancellationToken cancellationToken = default)
=> Task.FromResult(true);
public Task<bool> ProductSkuIsUnique(string sku, CancellationToken cancellationToken = default)
=> Task.FromResult(true);
public Task<bool> OptionCodeIsUniqueForProduct(Guid productId, string code, CancellationToken cancellationToken = default)
=> Task.FromResult(true);
}
}

View File

@@ -0,0 +1,20 @@
using Prefab.Tests.Infrastructure;
using Shouldly;
namespace Prefab.Tests.Integration.Smoke;
[Collection("Prefab.Debug")]
[Trait(TraitName.Category, TraitCategory.Integration)]
public sealed class RootShould(PrefabCompositeFixture_Debug fixture)
{
[Fact]
public async Task RespondWithSuccess()
{
var cancellationToken = TestContext.Current.CancellationToken;
var client = fixture.CreateHttpClientForWeb();
using var response = await client.GetAsync("/", cancellationToken);
response.StatusCode.ShouldBe(HttpStatusCode.OK);
}
}

View File

@@ -0,0 +1,135 @@
using System.Security.Claims;
using Prefab.Catalog.Domain.Entities;
using Prefab.Catalog.Domain.Services;
using Prefab.Tests.Infrastructure;
using Prefab.Web.Client.Models.Shared;
using Prefab.Web.Client.Pages;
using Prefab.Web.Client.Services;
namespace Prefab.Tests.Integration.Web.Gateway;
//[Collection("Prefab.Debug")]
//[Collection("Prefab.Ephemeral")]
[Trait(TraitName.Category, TraitCategory.Integration)]
//public sealed class CategoriesOverHttpShould(PrefabCompositeFixture_Debug fixture)
//public sealed class CategoriesOverHttpShould(PrefabCompositeFixture_Ephemeral fixture)
public sealed class CategoriesOverHttpShould(PrefabCompositeFixture_Ephemeral fixture)
: IClassFixture<PrefabCompositeFixture_Ephemeral>
{
[Theory]
[InlineData("Accessories")]
[InlineData("Shoes")]
public async Task ReturnSeededCategoryRecords(string prefix)
{
var cancellationToken = TestContext.Current.CancellationToken;
var suffix = Guid.NewGuid().ToString("N")[..8];
var categoryName = $"{prefix}-{suffix}";
var categoryDescription = $"{prefix} description {suffix}";
await fixture.SeedAsync(async (db, token) =>
{
var checker = new CategoriesTestHelpers.TestUniqueChecker();
var category = await Category.Create(categoryName, categoryDescription, checker, token);
db.Categories.Add(category);
await db.SaveChangesAsync(token);
}, cancellationToken);
var client = fixture.CreateHttpClientForWeb();
var pageService = new CategoriesPageService(client);
var result = await pageService.GetPage(cancellationToken);
var model = CategoriesTestHelpers.EnsureSuccess(result);
Assert.Contains(model.Categories, c => c.Name == categoryName);
}
}
//[Collection("Prefab.InProcess")]
[Trait(TraitName.Category, TraitCategory.Integration)]
public sealed class CategoriesInProcessShould(PrefabCompositeFixture_Ephemeral fixture)
: IClassFixture<PrefabCompositeFixture_Ephemeral>
{
[Fact]
public async Task ResolveServerImplementationWithoutHttp()
{
var cancellationToken = TestContext.Current.CancellationToken;
fixture.AuthenticationStateProvider.SetAnonymous();
using var scope = fixture.CreateWebScope();
var service = scope.ServiceProvider.GetRequiredService<ICategoriesPageService>();
var result = await service.GetPage(cancellationToken);
var model = CategoriesTestHelpers.EnsureSuccess(result);
Assert.NotNull(model.Categories);
}
}
internal static class CategoriesTestHelpers
{
public static CategoriesPageModel EnsureSuccess(Result<CategoriesPageModel> result)
{
if (!result.IsSuccess || result.Value is null)
{
var detail = result.Problem?.Detail ?? "Categories page request failed.";
throw new InvalidOperationException(detail);
}
return result.Value;
}
public sealed class TestUniqueChecker : IUniqueChecker
{
public Task<bool> CategoryNameIsUnique(string name, CancellationToken cancellationToken = default)
=> Task.FromResult(true);
public Task<bool> ProductModelNameIsUnique(string name, CancellationToken cancellationToken = default)
=> Task.FromResult(true);
public Task<bool> ProductSlugIsUnique(string? slug, CancellationToken cancellationToken = default)
=> Task.FromResult(true);
public Task<bool> ProductSkuIsUnique(string sku, CancellationToken cancellationToken = default)
=> Task.FromResult(true);
public Task<bool> OptionCodeIsUniqueForProduct(Guid productId, string code, CancellationToken cancellationToken = default)
=> Task.FromResult(true);
}
}
[Collection("Prefab.InProcess")]
[Trait(TraitName.Category, TraitCategory.Integration)]
public sealed class CategoriesAuthExample(PrefabCompositeFixture_InProcess fixture)
{
[Fact(Skip = "Documentation example for simulated authenticated user. Enable when wiring real auth scenarios.")]
public async Task Example_UsingAuthenticatedPrincipal()
{
var cancellationToken = TestContext.Current.CancellationToken;
var principal = new ClaimsPrincipal(new ClaimsIdentity(
[
new Claim(ClaimTypes.NameIdentifier, "user-123"),
new Claim(ClaimTypes.Name, "Integration Tester"),
new Claim(ClaimTypes.Role, "Catalog.Editor")
], "TestHarness"));
fixture.AuthenticationStateProvider.SetPrincipal(principal);
try
{
using var scope = fixture.CreateWebScope();
var service = scope.ServiceProvider.GetRequiredService<ICategoriesPageService>();
var result = await service.GetPage(cancellationToken);
var model = CategoriesTestHelpers.EnsureSuccess(result);
Assert.NotNull(model.Categories);
}
finally
{
fixture.AuthenticationStateProvider.SetAnonymous();
}
}
}

View File

@@ -0,0 +1,25 @@
using System.Net.Http.Json;
using Prefab.Tests.Infrastructure;
using Prefab.Web.Client.Models.Shared;
using Shouldly;
namespace Prefab.Tests.Integration.Web.Nav;
[Trait(TraitName.Category, TraitCategory.Integration)]
public sealed class NavMenuShould(PrefabCompositeFixture_Ephemeral fixture)
: IClassFixture<PrefabCompositeFixture_Ephemeral>
{
[Fact]
public async Task ReturnNavigationColumnsFromBff()
{
var cancellationToken = TestContext.Current.CancellationToken;
var client = fixture.CreateHttpClientForWeb();
var result = await client.GetFromJsonAsync<Result<NavMenuModel>>("/bff/nav-menu/2", cancellationToken);
result.ShouldNotBeNull();
result!.IsSuccess.ShouldBeTrue(result.Problem?.Detail);
result.Value.ShouldNotBeNull();
result.Value!.Columns.ShouldNotBeNull();
}
}