Init
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
20
Prefab.Tests/Integration/Smoke/RootShould.cs
Normal file
20
Prefab.Tests/Integration/Smoke/RootShould.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
135
Prefab.Tests/Integration/Web/Gateway/CategoriesShould.cs
Normal file
135
Prefab.Tests/Integration/Web/Gateway/CategoriesShould.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
25
Prefab.Tests/Integration/Web/Nav/NavMenuShould.cs
Normal file
25
Prefab.Tests/Integration/Web/Nav/NavMenuShould.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user